Skip to content

Commit 3fa8a3c

Browse files
committed
refactor(sdk): simplify and enhance loadLocalAgents API
- Remove confusing global loadedAgents export (was mutable state) - Add proper typing with LoadedAgents and LoadedAgentDefinition types - Add _sourceFilePath to each loaded agent for debugging - Add validate option to automatically filter invalid agents - Update README with comprehensive documentation - All changes are backwards-compatible
1 parent e511ef9 commit 3fa8a3c

File tree

3 files changed

+150
-8
lines changed

3 files changed

+150
-8
lines changed

sdk/README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,55 @@ main()
136136

137137
## API Reference
138138

139+
### `loadLocalAgents(options)`
140+
141+
Loads agent definitions from `.agents` directories on disk.
142+
143+
```typescript
144+
import { loadLocalAgents } from '@codebuff/sdk'
145+
146+
// Load from default locations (.agents in cwd, parent, or home)
147+
const agents = await loadLocalAgents({ verbose: true })
148+
149+
// Load from a specific directory
150+
const agents = await loadLocalAgents({ agentsPath: './my-agents' })
151+
152+
// Load and validate agents (invalid agents are filtered out)
153+
const agents = await loadLocalAgents({ validate: true, verbose: true })
154+
155+
// Access source file path for debugging
156+
for (const agent of Object.values(agents)) {
157+
console.log(`${agent.id} loaded from ${agent._sourceFilePath}`)
158+
}
159+
160+
// Use the loaded agents with client.run()
161+
const result = await client.run({
162+
agent: 'my-custom-agent',
163+
agentDefinitions: Object.values(agents),
164+
prompt: 'Hello',
165+
})
166+
```
167+
168+
#### Parameters
169+
170+
- **`agentsPath`** (string, optional): Path to a specific agents directory. If omitted, searches in `{cwd}/.agents`, `{cwd}/../.agents`, and `{homedir}/.agents`.
171+
- **`verbose`** (boolean, optional): Whether to log errors during loading. Defaults to `false`.
172+
- **`validate`** (boolean, optional): Whether to validate agents after loading. Invalid agents are filtered out. Defaults to `false`.
173+
174+
#### Returns
175+
176+
Returns a `Promise<LoadedAgents>` - a `Record<string, LoadedAgentDefinition>` of agent definitions keyed by their ID.
177+
178+
Each `LoadedAgentDefinition` extends `AgentDefinition` with:
179+
- **`_sourceFilePath`** (string): The file path the agent was loaded from
180+
181+
#### Supported File Types
182+
183+
- `.ts`, `.tsx` - TypeScript files (automatically transpiled)
184+
- `.js`, `.mjs`, `.cjs` - JavaScript files
185+
186+
Files ending in `.d.ts` or `.test.ts` are excluded.
187+
139188
### `client.run(options)`
140189

141190
Runs a Codebuff agent with the specified options.

sdk/src/agents/load-agents.ts

Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,20 @@ import { pathToFileURL } from 'url'
77

88
import { build } from 'esbuild'
99

10-
export let loadedAgents: Record<string, any> = {}
10+
import type { AgentDefinition } from '@codebuff/common/templates/initial-agents-dir/types/agent-definition'
11+
12+
/**
13+
* Agent definition with source file path metadata.
14+
*/
15+
export type LoadedAgentDefinition = AgentDefinition & {
16+
/** The file path this agent was loaded from */
17+
_sourceFilePath: string
18+
}
19+
20+
/**
21+
* Loaded agent definitions keyed by agent ID.
22+
*/
23+
export type LoadedAgents = Record<string, LoadedAgentDefinition>
1124

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

@@ -44,20 +57,62 @@ const getDefaultAgentDirs = () => {
4457
return [cwdAgents, parentAgents, homeAgents]
4558
}
4659

60+
/**
61+
* Load agent definitions from `.agents` directories.
62+
*
63+
* By default, searches for agents in:
64+
* - `{cwd}/.agents`
65+
* - `{cwd}/../.agents`
66+
* - `{homedir}/.agents`
67+
*
68+
* Agent files can be `.ts`, `.tsx`, `.js`, `.mjs`, or `.cjs`.
69+
* TypeScript files are automatically transpiled.
70+
*
71+
* @param options.agentsPath - Optional path to a specific agents directory
72+
* @param options.verbose - Whether to log errors during loading
73+
* @param options.validate - Whether to validate agents after loading (filters out invalid agents)
74+
* @returns Record of agent definitions keyed by agent ID
75+
*
76+
* @example
77+
* ```typescript
78+
* // Load from default locations
79+
* const agents = await loadLocalAgents({ verbose: true })
80+
*
81+
* // Load from a specific directory
82+
* const agents = await loadLocalAgents({ agentsPath: './my-agents' })
83+
*
84+
* // Load and validate agents (invalid agents are filtered out)
85+
* const agents = await loadLocalAgents({ validate: true, verbose: true })
86+
*
87+
* // Access source file path for debugging
88+
* for (const agent of Object.values(agents)) {
89+
* console.log(`${agent.id} loaded from ${agent._sourceFilePath}`)
90+
* }
91+
*
92+
* // Use with client.run()
93+
* const result = await client.run({
94+
* agent: 'my-agent',
95+
* agentDefinitions: Object.values(agents),
96+
* prompt: 'Hello',
97+
* })
98+
* ```
99+
*/
47100
export async function loadLocalAgents({
48101
agentsPath,
49102
verbose = false,
103+
validate = false,
50104
}: {
51105
agentsPath?: string
52106
verbose?: boolean
53-
}): Promise<typeof loadedAgents> {
54-
loadedAgents = {}
107+
validate?: boolean
108+
}): Promise<LoadedAgents> {
109+
const agents: LoadedAgents = {}
55110

56111
const agentDirs = agentsPath ? [agentsPath] : getDefaultAgentDirs()
57112
const allAgentFiles = agentDirs.flatMap((dir) => getAllAgentFiles(dir))
58113

59114
if (allAgentFiles.length === 0) {
60-
return loadedAgents
115+
return agents
61116
}
62117

63118
for (const fullPath of allAgentFiles) {
@@ -77,13 +132,16 @@ export async function loadLocalAgents({
77132
continue
78133
}
79134

80-
const processedAgentDefinition = { ...agentDefinition }
135+
const processedAgentDefinition: LoadedAgentDefinition = {
136+
...agentDefinition,
137+
_sourceFilePath: fullPath,
138+
}
81139
if (agentDefinition.handleSteps) {
82140
processedAgentDefinition.handleSteps =
83141
agentDefinition.handleSteps.toString()
84142
}
85143

86-
loadedAgents[processedAgentDefinition.id] = processedAgentDefinition
144+
agents[processedAgentDefinition.id] = processedAgentDefinition
87145
} catch (error) {
88146
if (verbose) {
89147
console.error(
@@ -94,7 +152,41 @@ export async function loadLocalAgents({
94152
}
95153
}
96154

97-
return loadedAgents
155+
// Validate agents if requested
156+
if (validate && Object.keys(agents).length > 0) {
157+
const { validateAgents } = await import('../validate-agents')
158+
const result = await validateAgents(Object.values(agents))
159+
160+
if (!result.success) {
161+
// Build a map of agent IDs to their validation errors
162+
// The validation error id format is "{agentId}_{index}" from validateAgents
163+
const errorsByAgentId = new Map<string, string>()
164+
for (const err of result.validationErrors) {
165+
// Extract agent ID by removing the "_index" suffix added by validateAgents
166+
const lastUnderscoreIdx = err.id.lastIndexOf('_')
167+
const agentId =
168+
lastUnderscoreIdx > 0 ? err.id.slice(0, lastUnderscoreIdx) : err.id
169+
if (!errorsByAgentId.has(agentId)) {
170+
errorsByAgentId.set(agentId, err.message)
171+
}
172+
}
173+
174+
// Filter out invalid agents
175+
for (const agentId of Object.keys(agents)) {
176+
const errorMessage = errorsByAgentId.get(agentId)
177+
if (errorMessage) {
178+
if (verbose) {
179+
console.error(
180+
`Validation failed for agent '${agentId}': ${errorMessage}`,
181+
)
182+
}
183+
delete agents[agentId]
184+
}
185+
}
186+
}
187+
}
188+
189+
return agents
98190
}
99191

100192
async function importAgentModule(

sdk/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ export * from './constants'
3636

3737
export { getUserInfoFromApiKey } from './impl/database'
3838
export * from './credentials'
39-
export { loadLocalAgents, loadedAgents } from './agents/load-agents'
39+
export { loadLocalAgents } from './agents/load-agents'
40+
export type { LoadedAgents, LoadedAgentDefinition } from './agents/load-agents'
4041

4142
export { validateAgents } from './validate-agents'
4243
export type { ValidationResult, ValidateAgentsOptions } from './validate-agents'

0 commit comments

Comments
 (0)