Skip to content

Commit 547e061

Browse files
[codex] Fix local agent discovery imports (#738)
Co-authored-by: James Grugett <jahooma@gmail.com>
1 parent 98d674e commit 547e061

2 files changed

Lines changed: 167 additions & 27 deletions

File tree

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

Lines changed: 149 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,115 @@ 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('loads valid agent definitions that use shorthand required fields', async () => {
275+
mkdirSync(agentsDir, { recursive: true })
276+
writeAgentFile(
277+
agentsDir,
278+
'shorthand-agent.ts',
279+
`
280+
const id = 'shorthand-agent'
281+
const model = '${MODEL_NAME}'
282+
283+
export default {
284+
id,
285+
displayName: 'Shorthand Agent',
286+
model
287+
}
288+
`,
289+
)
290+
291+
const result: LoadedAgents = await loadLocalAgents({
292+
agentsPath: agentsDir,
293+
})
294+
295+
expect(result['shorthand-agent']).toBeDefined()
296+
expect(result['shorthand-agent']!.model).toBe(MODEL_NAME)
297+
})
298+
299+
test('skips quarantined skill directories without importing executable scripts', async () => {
300+
const quarantineScriptsDir = path.join(
301+
agentsDir,
302+
'skills-quarantine',
303+
'2026-02-23',
304+
'youtube-data',
305+
'scripts',
306+
)
307+
mkdirSync(quarantineScriptsDir, { recursive: true })
308+
const markerFile = path.join(tempDir, 'quarantine-side-effect')
309+
writeAgentFile(
310+
quarantineScriptsDir,
311+
'tapi-auth.cjs',
312+
`
313+
const { writeFileSync } = require('fs')
314+
writeFileSync(${JSON.stringify(markerFile)}, 'imported')
315+
module.exports = {
316+
id: 'quarantined-agent',
317+
displayName: 'Quarantined Agent',
318+
model: '${MODEL_NAME}'
319+
}
320+
`,
321+
)
322+
writeAgentFile(
323+
agentsDir,
324+
'real-agent.ts',
325+
`
326+
export default {
327+
id: 'real-agent',
328+
displayName: 'Real Agent',
329+
model: '${MODEL_NAME}'
330+
}
331+
`,
332+
)
333+
334+
const result: LoadedAgents = await loadLocalAgents({
335+
agentsPath: agentsDir,
336+
})
337+
338+
expect(result['real-agent']).toBeDefined()
339+
expect(result['quarantined-agent']).toBeUndefined()
340+
expect(existsSync(markerFile)).toBe(false)
341+
})
342+
343+
test('skips support directories without importing executable scripts', async () => {
344+
const scriptsDir = path.join(agentsDir, 'scripts')
345+
mkdirSync(scriptsDir, { recursive: true })
346+
const markerFile = path.join(tempDir, 'scripts-side-effect')
347+
writeAgentFile(
348+
scriptsDir,
349+
'exa-api.cjs',
350+
`
351+
const { writeFileSync } = require('fs')
352+
writeFileSync(${JSON.stringify(markerFile)}, 'imported')
353+
`,
354+
)
355+
writeAgentFile(
356+
agentsDir,
357+
'real-agent.ts',
358+
`
359+
export default {
360+
id: 'real-agent',
361+
displayName: 'Real Agent',
362+
model: '${MODEL_NAME}'
363+
}
364+
`,
365+
)
366+
367+
const result: LoadedAgents = await loadLocalAgents({
368+
agentsPath: agentsDir,
369+
})
370+
371+
expect(result['real-agent']).toBeDefined()
372+
expect(existsSync(markerFile)).toBe(false)
373+
})
374+
248375
test('converts handleSteps function to string', async () => {
249376
mkdirSync(agentsDir, { recursive: true })
250377
writeAgentFile(
@@ -263,7 +390,9 @@ describe('loadLocalAgents', () => {
263390
`,
264391
)
265392

266-
const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir })
393+
const result: LoadedAgents = await loadLocalAgents({
394+
agentsPath: agentsDir,
395+
})
267396
const agent: LoadedAgentDefinition | undefined = result['generator-agent']
268397

269398
expect(agent).toBeDefined()
@@ -299,7 +428,9 @@ describe('loadLocalAgents', () => {
299428
`,
300429
)
301430

302-
const result: LoadedAgents = await loadLocalAgents({ agentsPath: agentsDir })
431+
const result: LoadedAgents = await loadLocalAgents({
432+
agentsPath: agentsDir,
433+
})
303434

304435
// Should still load the valid agent
305436
expect(result['valid-agent']).toBeDefined()
@@ -326,9 +457,7 @@ describe('loadLocalAgents', () => {
326457
await loadLocalAgents({ agentsPath: agentsDir, verbose: true })
327458

328459
expect(consoleErrorSpy).toHaveBeenCalled()
329-
const errorMessage: string = consoleErrorSpy.mock.calls
330-
.flat()
331-
.join(' ')
460+
const errorMessage: string = consoleErrorSpy.mock.calls.flat().join(' ')
332461
expect(errorMessage).toContain('missing required attributes')
333462
})
334463
})

sdk/src/agents/load-agents.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,23 +105,34 @@ 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 === 'scripts' ||
112+
name === 'skills' ||
113+
name.startsWith('skills-')
114+
115+
const isLoadableAgentFileName = (fileName: string): boolean => {
116+
const extension = path.extname(fileName).toLowerCase()
117+
return (
118+
agentFileExtensions.has(extension) &&
119+
!fileName.endsWith('.d.ts') &&
120+
!/[./](test|spec)\.[cm]?[tj]sx?$/.test(fileName)
121+
)
122+
}
123+
108124
const getAllAgentFiles = (dir: string): string[] => {
109125
const files: string[] = []
110126
try {
111127
const entries = fs.readdirSync(dir, { withFileTypes: true })
112128
for (const entry of entries) {
113129
const fullPath = path.join(dir, entry.name)
114130
if (entry.isDirectory()) {
115-
if (entry.name === 'skills') continue
131+
if (shouldSkipAgentDirectory(entry.name)) continue
116132
files.push(...getAllAgentFiles(fullPath))
117133
continue
118134
}
119-
const extension = path.extname(entry.name).toLowerCase()
120-
const isAgentFile =
121-
entry.isFile() &&
122-
agentFileExtensions.has(extension) &&
123-
!entry.name.endsWith('.d.ts') &&
124-
!entry.name.endsWith('.test.ts')
135+
const isAgentFile = entry.isFile() && isLoadableAgentFileName(entry.name)
125136
if (isAgentFile) {
126137
files.push(fullPath)
127138
}

0 commit comments

Comments
 (0)