From cf0f150a361c26053a1ec9b9c5ee53c97454756b Mon Sep 17 00:00:00 2001 From: James Grugett Date: Sat, 23 May 2026 16:23:39 -0700 Subject: [PATCH 1/3] Fix local agent discovery imports --- sdk/src/__tests__/load-agents.test.ts | 176 +++++++++++++++++++++++--- sdk/src/agents/load-agents.ts | 45 ++++++- 2 files changed, 196 insertions(+), 25 deletions(-) diff --git a/sdk/src/__tests__/load-agents.test.ts b/sdk/src/__tests__/load-agents.test.ts index e844bb3cb7..e3daf40d42 100644 --- a/sdk/src/__tests__/load-agents.test.ts +++ b/sdk/src/__tests__/load-agents.test.ts @@ -1,8 +1,16 @@ -import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'fs' +import { existsSync, mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'fs' import os from 'os' import path from 'path' -import { describe, expect, test, beforeEach, afterEach, mock, spyOn } from 'bun:test' +import { + describe, + expect, + test, + beforeEach, + afterEach, + mock, + spyOn, +} from 'bun:test' import { loadLocalAgents } from '../agents/load-agents' @@ -45,7 +53,9 @@ describe('loadLocalAgents', () => { describe('without validation (backward compatible)', () => { test('returns empty object when agents directory does not exist', async () => { - const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir }) + const result: LoadedAgents = await loadLocalAgents({ + agentsPath: agentsDir, + }) expect(result).toEqual({}) }) @@ -53,7 +63,9 @@ describe('loadLocalAgents', () => { test('returns empty object when agents directory is empty', async () => { mkdirSync(agentsDir, { recursive: true }) - const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir }) + const result: LoadedAgents = await loadLocalAgents({ + agentsPath: agentsDir, + }) expect(result).toEqual({}) }) @@ -73,16 +85,16 @@ describe('loadLocalAgents', () => { `, ) - const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir }) + const result: LoadedAgents = await loadLocalAgents({ + agentsPath: agentsDir, + }) const agent: LoadedAgentDefinition | undefined = result['my-agent'] expect(agent).toBeDefined() expect(agent!.id).toBe('my-agent') expect(agent!.displayName).toBe('My Agent') expect(agent!.model).toBe(MODEL_NAME) - expect(agent!._sourceFilePath).toBe( - path.join(agentsDir, 'my-agent.ts'), - ) + expect(agent!._sourceFilePath).toBe(path.join(agentsDir, 'my-agent.ts')) }) test('loads multiple agents from directory', async () => { @@ -110,7 +122,9 @@ describe('loadLocalAgents', () => { `, ) - const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir }) + const result: LoadedAgents = await loadLocalAgents({ + agentsPath: agentsDir, + }) const agentIds: string[] = Object.keys(result) expect(agentIds).toHaveLength(2) @@ -131,7 +145,9 @@ describe('loadLocalAgents', () => { `, ) - const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir }) + const result: LoadedAgents = await loadLocalAgents({ + agentsPath: agentsDir, + }) expect(Object.keys(result)).toHaveLength(0) }) @@ -149,7 +165,9 @@ describe('loadLocalAgents', () => { `, ) - const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir }) + const result: LoadedAgents = await loadLocalAgents({ + agentsPath: agentsDir, + }) expect(Object.keys(result)).toHaveLength(0) }) @@ -168,7 +186,9 @@ describe('loadLocalAgents', () => { `, ) - const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir }) + const result: LoadedAgents = await loadLocalAgents({ + agentsPath: agentsDir, + }) expect(result['dts-agent']).toBeUndefined() }) @@ -187,7 +207,9 @@ describe('loadLocalAgents', () => { `, ) - const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir }) + const result: LoadedAgents = await loadLocalAgents({ + agentsPath: agentsDir, + }) expect(result['test-file-agent']).toBeUndefined() }) @@ -207,7 +229,9 @@ describe('loadLocalAgents', () => { `, ) - const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir }) + const result: LoadedAgents = await loadLocalAgents({ + agentsPath: agentsDir, + }) expect(result['nested-agent']).toBeDefined() }) @@ -239,12 +263,122 @@ describe('loadLocalAgents', () => { `, ) - const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir }) + const result: LoadedAgents = await loadLocalAgents({ + agentsPath: agentsDir, + }) expect(result['skill-agent']).toBeUndefined() expect(result['real-agent']).toBeDefined() }) + test('skips non-agent-shaped JavaScript files without importing them', async () => { + mkdirSync(agentsDir, { recursive: true }) + const markerFile = path.join(tempDir, 'import-side-effect') + writeAgentFile( + agentsDir, + 'tapi-auth.cjs', + ` + const { writeFileSync } = require('fs') + writeFileSync(${JSON.stringify(markerFile)}, 'imported') + console.log('Unrelated CLI help text') + `, + ) + writeAgentFile( + agentsDir, + 'real-agent.ts', + ` + export default { + id: 'real-agent', + displayName: 'Real Agent', + model: '${MODEL_NAME}' + } + `, + ) + + const result: LoadedAgents = await loadLocalAgents({ + agentsPath: agentsDir, + }) + + expect(result['real-agent']).toBeDefined() + expect(existsSync(markerFile)).toBe(false) + }) + + test('skips quarantined skill directories without importing executable scripts', async () => { + const quarantineScriptsDir = path.join( + agentsDir, + 'skills-quarantine', + '2026-02-23', + 'youtube-data', + 'scripts', + ) + mkdirSync(quarantineScriptsDir, { recursive: true }) + const markerFile = path.join(tempDir, 'quarantine-side-effect') + writeAgentFile( + quarantineScriptsDir, + 'tapi-auth.cjs', + ` + const { writeFileSync } = require('fs') + writeFileSync(${JSON.stringify(markerFile)}, 'imported') + module.exports = { + id: 'quarantined-agent', + displayName: 'Quarantined Agent', + model: '${MODEL_NAME}' + } + `, + ) + writeAgentFile( + agentsDir, + 'real-agent.ts', + ` + export default { + id: 'real-agent', + displayName: 'Real Agent', + model: '${MODEL_NAME}' + } + `, + ) + + const result: LoadedAgents = await loadLocalAgents({ + agentsPath: agentsDir, + }) + + expect(result['real-agent']).toBeDefined() + expect(result['quarantined-agent']).toBeUndefined() + expect(existsSync(markerFile)).toBe(false) + }) + + test('skips support directories without importing executable scripts', async () => { + const scriptsDir = path.join(agentsDir, 'scripts') + mkdirSync(scriptsDir, { recursive: true }) + const markerFile = path.join(tempDir, 'scripts-side-effect') + writeAgentFile( + scriptsDir, + 'exa-api.cjs', + ` + const { writeFileSync } = require('fs') + writeFileSync(${JSON.stringify(markerFile)}, 'imported') + `, + ) + writeAgentFile( + agentsDir, + 'real-agent.ts', + ` + export default { + id: 'real-agent', + displayName: 'Real Agent', + model: '${MODEL_NAME}' + } + `, + ) + + const result: LoadedAgents = await loadLocalAgents({ + agentsPath: agentsDir, + }) + + expect(result['real-agent']).toBeDefined() + expect(existsSync(markerFile)).toBe(false) + }) + test('converts handleSteps function to string', async () => { mkdirSync(agentsDir, { recursive: true }) writeAgentFile( @@ -263,7 +397,9 @@ describe('loadLocalAgents', () => { `, ) - const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir }) + const result: LoadedAgents = await loadLocalAgents({ + agentsPath: agentsDir, + }) const agent: LoadedAgentDefinition | undefined = result['generator-agent'] expect(agent).toBeDefined() @@ -299,7 +435,9 @@ describe('loadLocalAgents', () => { `, ) - const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir }) + const result: LoadedAgents = await loadLocalAgents({ + agentsPath: agentsDir, + }) // Should still load the valid agent expect(result['valid-agent']).toBeDefined() @@ -326,9 +464,7 @@ describe('loadLocalAgents', () => { await loadLocalAgents({ agentsPath: agentsDir, verbose: true }) expect(consoleErrorSpy).toHaveBeenCalled() - const errorMessage: string = consoleErrorSpy.mock.calls - .flat() - .join(' ') + const errorMessage: string = consoleErrorSpy.mock.calls.flat().join(' ') expect(errorMessage).toContain('missing required attributes') }) }) diff --git a/sdk/src/agents/load-agents.ts b/sdk/src/agents/load-agents.ts index ed23c78d28..f4d88b90e9 100644 --- a/sdk/src/agents/load-agents.ts +++ b/sdk/src/agents/load-agents.ts @@ -105,6 +105,43 @@ export type LoadLocalAgentsResult = { const agentFileExtensions = new Set(['.ts', '.tsx', '.js', '.mjs', '.cjs']) +const shouldSkipAgentDirectory = (name: string): boolean => + name.startsWith('.') || + name === 'node_modules' || + name === 'skills' || + name.startsWith('skills-') + +const isLoadableAgentFileName = (fileName: string): boolean => { + const extension = path.extname(fileName).toLowerCase() + return ( + agentFileExtensions.has(extension) && + !fileName.endsWith('.d.ts') && + !/[./](test|spec)\.[cm]?[tj]sx?$/.test(fileName) + ) +} + +const looksLikeAgentDefinitionSource = (fullPath: string): boolean => { + let source: string + try { + source = fs.readFileSync(fullPath, 'utf8') + } catch { + return false + } + + const exportsAgentDefinition = + /\bexport\s+default\b/.test(source) || + /\bmodule\.exports\s*=/.test(source) || + /\bexports\.default\s*=/.test(source) + if (!exportsAgentDefinition) { + return false + } + + return ( + /(^|[,{]\s*)['"]?id['"]?\s*:/m.test(source) || + /(^|[,{]\s*)['"]?model['"]?\s*:/m.test(source) + ) +} + const getAllAgentFiles = (dir: string): string[] => { const files: string[] = [] try { @@ -112,16 +149,14 @@ const getAllAgentFiles = (dir: string): string[] => { for (const entry of entries) { const fullPath = path.join(dir, entry.name) if (entry.isDirectory()) { - if (entry.name === 'skills') continue + if (shouldSkipAgentDirectory(entry.name)) continue files.push(...getAllAgentFiles(fullPath)) continue } - const extension = path.extname(entry.name).toLowerCase() const isAgentFile = entry.isFile() && - agentFileExtensions.has(extension) && - !entry.name.endsWith('.d.ts') && - !entry.name.endsWith('.test.ts') + isLoadableAgentFileName(entry.name) && + looksLikeAgentDefinitionSource(fullPath) if (isAgentFile) { files.push(fullPath) } From 27163ec364c59393ffefcc80e3da4d5078b5179a Mon Sep 17 00:00:00 2001 From: James Grugett Date: Sat, 23 May 2026 16:38:31 -0700 Subject: [PATCH 2/3] Support shorthand agent exports --- sdk/src/__tests__/load-agents.test.ts | 25 +++++++++++++++++++++++++ sdk/src/agents/load-agents.ts | 4 ++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/sdk/src/__tests__/load-agents.test.ts b/sdk/src/__tests__/load-agents.test.ts index e3daf40d42..a6bf79c2f5 100644 --- a/sdk/src/__tests__/load-agents.test.ts +++ b/sdk/src/__tests__/load-agents.test.ts @@ -303,6 +303,31 @@ describe('loadLocalAgents', () => { expect(existsSync(markerFile)).toBe(false) }) + test('loads valid agent definitions that use shorthand required fields', async () => { + mkdirSync(agentsDir, { recursive: true }) + writeAgentFile( + agentsDir, + 'shorthand-agent.ts', + ` + const id = 'shorthand-agent' + const model = '${MODEL_NAME}' + + export default { + id, + displayName: 'Shorthand Agent', + model + } + `, + ) + + const result: LoadedAgents = await loadLocalAgents({ + agentsPath: agentsDir, + }) + + expect(result['shorthand-agent']).toBeDefined() + expect(result['shorthand-agent']!.model).toBe(MODEL_NAME) + }) + test('skips quarantined skill directories without importing executable scripts', async () => { const quarantineScriptsDir = path.join( agentsDir, diff --git a/sdk/src/agents/load-agents.ts b/sdk/src/agents/load-agents.ts index f4d88b90e9..8b9f492485 100644 --- a/sdk/src/agents/load-agents.ts +++ b/sdk/src/agents/load-agents.ts @@ -137,8 +137,8 @@ const looksLikeAgentDefinitionSource = (fullPath: string): boolean => { } return ( - /(^|[,{]\s*)['"]?id['"]?\s*:/m.test(source) || - /(^|[,{]\s*)['"]?model['"]?\s*:/m.test(source) + /(?:^|[^\w$])id(?:[^\w$]|$)/m.test(source) || + /(?:^|[^\w$])model(?:[^\w$]|$)/m.test(source) ) } From 837a6541fa3d2378edd7d13f72e9849632d3cdce Mon Sep 17 00:00:00 2001 From: James Grugett Date: Sun, 24 May 2026 14:19:00 -0700 Subject: [PATCH 3/3] Simplify local agent discovery fix --- sdk/src/__tests__/load-agents.test.ts | 32 --------------------------- sdk/src/agents/load-agents.ts | 28 ++--------------------- 2 files changed, 2 insertions(+), 58 deletions(-) diff --git a/sdk/src/__tests__/load-agents.test.ts b/sdk/src/__tests__/load-agents.test.ts index a6bf79c2f5..3eea6cc22e 100644 --- a/sdk/src/__tests__/load-agents.test.ts +++ b/sdk/src/__tests__/load-agents.test.ts @@ -271,38 +271,6 @@ describe('loadLocalAgents', () => { expect(result['real-agent']).toBeDefined() }) - test('skips non-agent-shaped JavaScript files without importing them', async () => { - mkdirSync(agentsDir, { recursive: true }) - const markerFile = path.join(tempDir, 'import-side-effect') - writeAgentFile( - agentsDir, - 'tapi-auth.cjs', - ` - const { writeFileSync } = require('fs') - writeFileSync(${JSON.stringify(markerFile)}, 'imported') - console.log('Unrelated CLI help text') - `, - ) - writeAgentFile( - agentsDir, - 'real-agent.ts', - ` - export default { - id: 'real-agent', - displayName: 'Real Agent', - model: '${MODEL_NAME}' - } - `, - ) - - const result: LoadedAgents = await loadLocalAgents({ - agentsPath: agentsDir, - }) - - expect(result['real-agent']).toBeDefined() - expect(existsSync(markerFile)).toBe(false) - }) - test('loads valid agent definitions that use shorthand required fields', async () => { mkdirSync(agentsDir, { recursive: true }) writeAgentFile( diff --git a/sdk/src/agents/load-agents.ts b/sdk/src/agents/load-agents.ts index 8b9f492485..bef77a91a6 100644 --- a/sdk/src/agents/load-agents.ts +++ b/sdk/src/agents/load-agents.ts @@ -108,6 +108,7 @@ const agentFileExtensions = new Set(['.ts', '.tsx', '.js', '.mjs', '.cjs']) const shouldSkipAgentDirectory = (name: string): boolean => name.startsWith('.') || name === 'node_modules' || + name === 'scripts' || name === 'skills' || name.startsWith('skills-') @@ -120,28 +121,6 @@ const isLoadableAgentFileName = (fileName: string): boolean => { ) } -const looksLikeAgentDefinitionSource = (fullPath: string): boolean => { - let source: string - try { - source = fs.readFileSync(fullPath, 'utf8') - } catch { - return false - } - - const exportsAgentDefinition = - /\bexport\s+default\b/.test(source) || - /\bmodule\.exports\s*=/.test(source) || - /\bexports\.default\s*=/.test(source) - if (!exportsAgentDefinition) { - return false - } - - return ( - /(?:^|[^\w$])id(?:[^\w$]|$)/m.test(source) || - /(?:^|[^\w$])model(?:[^\w$]|$)/m.test(source) - ) -} - const getAllAgentFiles = (dir: string): string[] => { const files: string[] = [] try { @@ -153,10 +132,7 @@ const getAllAgentFiles = (dir: string): string[] => { files.push(...getAllAgentFiles(fullPath)) continue } - const isAgentFile = - entry.isFile() && - isLoadableAgentFileName(entry.name) && - looksLikeAgentDefinitionSource(fullPath) + const isAgentFile = entry.isFile() && isLoadableAgentFileName(entry.name) if (isAgentFile) { files.push(fullPath) }