Skip to content

Commit cf0f150

Browse files
committed
Fix local agent discovery imports
1 parent b22d244 commit cf0f150

2 files changed

Lines changed: 196 additions & 25 deletions

File tree

sdk/src/__tests__/load-agents.test.ts

Lines changed: 156 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1-
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'fs'
1+
import { existsSync, mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'fs'
22
import os from 'os'
33
import path from 'path'
44

5-
import { describe, expect, test, beforeEach, afterEach, mock, spyOn } from 'bun:test'
5+
import {
6+
describe,
7+
expect,
8+
test,
9+
beforeEach,
10+
afterEach,
11+
mock,
12+
spyOn,
13+
} from 'bun:test'
614

715
import { loadLocalAgents } from '../agents/load-agents'
816

@@ -45,15 +53,19 @@ describe('loadLocalAgents', () => {
4553

4654
describe('without validation (backward compatible)', () => {
4755
test('returns empty object when agents directory does not exist', async () => {
48-
const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir })
56+
const result: LoadedAgents = await loadLocalAgents({
57+
agentsPath: agentsDir,
58+
})
4959

5060
expect(result).toEqual({})
5161
})
5262

5363
test('returns empty object when agents directory is empty', async () => {
5464
mkdirSync(agentsDir, { recursive: true })
5565

56-
const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir })
66+
const result: LoadedAgents = await loadLocalAgents({
67+
agentsPath: agentsDir,
68+
})
5769

5870
expect(result).toEqual({})
5971
})
@@ -73,16 +85,16 @@ describe('loadLocalAgents', () => {
7385
`,
7486
)
7587

76-
const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir })
88+
const result: LoadedAgents = await loadLocalAgents({
89+
agentsPath: agentsDir,
90+
})
7791

7892
const agent: LoadedAgentDefinition | undefined = result['my-agent']
7993
expect(agent).toBeDefined()
8094
expect(agent!.id).toBe('my-agent')
8195
expect(agent!.displayName).toBe('My Agent')
8296
expect(agent!.model).toBe(MODEL_NAME)
83-
expect(agent!._sourceFilePath).toBe(
84-
path.join(agentsDir, 'my-agent.ts'),
85-
)
97+
expect(agent!._sourceFilePath).toBe(path.join(agentsDir, 'my-agent.ts'))
8698
})
8799

88100
test('loads multiple agents from directory', async () => {
@@ -110,7 +122,9 @@ describe('loadLocalAgents', () => {
110122
`,
111123
)
112124

113-
const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir })
125+
const result: LoadedAgents = await loadLocalAgents({
126+
agentsPath: agentsDir,
127+
})
114128
const agentIds: string[] = Object.keys(result)
115129

116130
expect(agentIds).toHaveLength(2)
@@ -131,7 +145,9 @@ describe('loadLocalAgents', () => {
131145
`,
132146
)
133147

134-
const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir })
148+
const result: LoadedAgents = await loadLocalAgents({
149+
agentsPath: agentsDir,
150+
})
135151

136152
expect(Object.keys(result)).toHaveLength(0)
137153
})
@@ -149,7 +165,9 @@ describe('loadLocalAgents', () => {
149165
`,
150166
)
151167

152-
const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir })
168+
const result: LoadedAgents = await loadLocalAgents({
169+
agentsPath: agentsDir,
170+
})
153171

154172
expect(Object.keys(result)).toHaveLength(0)
155173
})
@@ -168,7 +186,9 @@ describe('loadLocalAgents', () => {
168186
`,
169187
)
170188

171-
const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir })
189+
const result: LoadedAgents = await loadLocalAgents({
190+
agentsPath: agentsDir,
191+
})
172192

173193
expect(result['dts-agent']).toBeUndefined()
174194
})
@@ -187,7 +207,9 @@ describe('loadLocalAgents', () => {
187207
`,
188208
)
189209

190-
const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir })
210+
const result: LoadedAgents = await loadLocalAgents({
211+
agentsPath: agentsDir,
212+
})
191213

192214
expect(result['test-file-agent']).toBeUndefined()
193215
})
@@ -207,7 +229,9 @@ describe('loadLocalAgents', () => {
207229
`,
208230
)
209231

210-
const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir })
232+
const result: LoadedAgents = await loadLocalAgents({
233+
agentsPath: agentsDir,
234+
})
211235

212236
expect(result['nested-agent']).toBeDefined()
213237
})
@@ -239,12 +263,122 @@ describe('loadLocalAgents', () => {
239263
`,
240264
)
241265

242-
const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir })
266+
const result: LoadedAgents = await loadLocalAgents({
267+
agentsPath: agentsDir,
268+
})
243269

244270
expect(result['skill-agent']).toBeUndefined()
245271
expect(result['real-agent']).toBeDefined()
246272
})
247273

274+
test('skips non-agent-shaped JavaScript files without importing them', async () => {
275+
mkdirSync(agentsDir, { recursive: true })
276+
const markerFile = path.join(tempDir, 'import-side-effect')
277+
writeAgentFile(
278+
agentsDir,
279+
'tapi-auth.cjs',
280+
`
281+
const { writeFileSync } = require('fs')
282+
writeFileSync(${JSON.stringify(markerFile)}, 'imported')
283+
console.log('Unrelated CLI help text')
284+
`,
285+
)
286+
writeAgentFile(
287+
agentsDir,
288+
'real-agent.ts',
289+
`
290+
export default {
291+
id: 'real-agent',
292+
displayName: 'Real Agent',
293+
model: '${MODEL_NAME}'
294+
}
295+
`,
296+
)
297+
298+
const result: LoadedAgents = await loadLocalAgents({
299+
agentsPath: agentsDir,
300+
})
301+
302+
expect(result['real-agent']).toBeDefined()
303+
expect(existsSync(markerFile)).toBe(false)
304+
})
305+
306+
test('skips quarantined skill directories without importing executable scripts', async () => {
307+
const quarantineScriptsDir = path.join(
308+
agentsDir,
309+
'skills-quarantine',
310+
'2026-02-23',
311+
'youtube-data',
312+
'scripts',
313+
)
314+
mkdirSync(quarantineScriptsDir, { recursive: true })
315+
const markerFile = path.join(tempDir, 'quarantine-side-effect')
316+
writeAgentFile(
317+
quarantineScriptsDir,
318+
'tapi-auth.cjs',
319+
`
320+
const { writeFileSync } = require('fs')
321+
writeFileSync(${JSON.stringify(markerFile)}, 'imported')
322+
module.exports = {
323+
id: 'quarantined-agent',
324+
displayName: 'Quarantined Agent',
325+
model: '${MODEL_NAME}'
326+
}
327+
`,
328+
)
329+
writeAgentFile(
330+
agentsDir,
331+
'real-agent.ts',
332+
`
333+
export default {
334+
id: 'real-agent',
335+
displayName: 'Real Agent',
336+
model: '${MODEL_NAME}'
337+
}
338+
`,
339+
)
340+
341+
const result: LoadedAgents = await loadLocalAgents({
342+
agentsPath: agentsDir,
343+
})
344+
345+
expect(result['real-agent']).toBeDefined()
346+
expect(result['quarantined-agent']).toBeUndefined()
347+
expect(existsSync(markerFile)).toBe(false)
348+
})
349+
350+
test('skips support directories without importing executable scripts', async () => {
351+
const scriptsDir = path.join(agentsDir, 'scripts')
352+
mkdirSync(scriptsDir, { recursive: true })
353+
const markerFile = path.join(tempDir, 'scripts-side-effect')
354+
writeAgentFile(
355+
scriptsDir,
356+
'exa-api.cjs',
357+
`
358+
const { writeFileSync } = require('fs')
359+
writeFileSync(${JSON.stringify(markerFile)}, 'imported')
360+
`,
361+
)
362+
writeAgentFile(
363+
agentsDir,
364+
'real-agent.ts',
365+
`
366+
export default {
367+
id: 'real-agent',
368+
displayName: 'Real Agent',
369+
model: '${MODEL_NAME}'
370+
}
371+
`,
372+
)
373+
374+
const result: LoadedAgents = await loadLocalAgents({
375+
agentsPath: agentsDir,
376+
})
377+
378+
expect(result['real-agent']).toBeDefined()
379+
expect(existsSync(markerFile)).toBe(false)
380+
})
381+
248382
test('converts handleSteps function to string', async () => {
249383
mkdirSync(agentsDir, { recursive: true })
250384
writeAgentFile(
@@ -263,7 +397,9 @@ describe('loadLocalAgents', () => {
263397
`,
264398
)
265399

266-
const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir })
400+
const result: LoadedAgents = await loadLocalAgents({
401+
agentsPath: agentsDir,
402+
})
267403
const agent: LoadedAgentDefinition | undefined = result['generator-agent']
268404

269405
expect(agent).toBeDefined()
@@ -299,7 +435,9 @@ describe('loadLocalAgents', () => {
299435
`,
300436
)
301437

302-
const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir })
438+
const result: LoadedAgents = await loadLocalAgents({
439+
agentsPath: agentsDir,
440+
})
303441

304442
// Should still load the valid agent
305443
expect(result['valid-agent']).toBeDefined()
@@ -326,9 +464,7 @@ describe('loadLocalAgents', () => {
326464
await loadLocalAgents({ agentsPath: agentsDir, verbose: true })
327465

328466
expect(consoleErrorSpy).toHaveBeenCalled()
329-
const errorMessage: string = consoleErrorSpy.mock.calls
330-
.flat()
331-
.join(' ')
467+
const errorMessage: string = consoleErrorSpy.mock.calls.flat().join(' ')
332468
expect(errorMessage).toContain('missing required attributes')
333469
})
334470
})

sdk/src/agents/load-agents.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,23 +105,58 @@ export type LoadLocalAgentsResult = {
105105

106106
const agentFileExtensions = new Set(['.ts', '.tsx', '.js', '.mjs', '.cjs'])
107107

108+
const shouldSkipAgentDirectory = (name: string): boolean =>
109+
name.startsWith('.') ||
110+
name === 'node_modules' ||
111+
name === 'skills' ||
112+
name.startsWith('skills-')
113+
114+
const isLoadableAgentFileName = (fileName: string): boolean => {
115+
const extension = path.extname(fileName).toLowerCase()
116+
return (
117+
agentFileExtensions.has(extension) &&
118+
!fileName.endsWith('.d.ts') &&
119+
!/[./](test|spec)\.[cm]?[tj]sx?$/.test(fileName)
120+
)
121+
}
122+
123+
const looksLikeAgentDefinitionSource = (fullPath: string): boolean => {
124+
let source: string
125+
try {
126+
source = fs.readFileSync(fullPath, 'utf8')
127+
} catch {
128+
return false
129+
}
130+
131+
const exportsAgentDefinition =
132+
/\bexport\s+default\b/.test(source) ||
133+
/\bmodule\.exports\s*=/.test(source) ||
134+
/\bexports\.default\s*=/.test(source)
135+
if (!exportsAgentDefinition) {
136+
return false
137+
}
138+
139+
return (
140+
/(^|[,{]\s*)['"]?id['"]?\s*:/m.test(source) ||
141+
/(^|[,{]\s*)['"]?model['"]?\s*:/m.test(source)
142+
)
143+
}
144+
108145
const getAllAgentFiles = (dir: string): string[] => {
109146
const files: string[] = []
110147
try {
111148
const entries = fs.readdirSync(dir, { withFileTypes: true })
112149
for (const entry of entries) {
113150
const fullPath = path.join(dir, entry.name)
114151
if (entry.isDirectory()) {
115-
if (entry.name === 'skills') continue
152+
if (shouldSkipAgentDirectory(entry.name)) continue
116153
files.push(...getAllAgentFiles(fullPath))
117154
continue
118155
}
119-
const extension = path.extname(entry.name).toLowerCase()
120156
const isAgentFile =
121157
entry.isFile() &&
122-
agentFileExtensions.has(extension) &&
123-
!entry.name.endsWith('.d.ts') &&
124-
!entry.name.endsWith('.test.ts')
158+
isLoadableAgentFileName(entry.name) &&
159+
looksLikeAgentDefinitionSource(fullPath)
125160
if (isAgentFile) {
126161
files.push(fullPath)
127162
}

0 commit comments

Comments
 (0)