Skip to content

Commit 54f477b

Browse files
committed
Auto-repair tool calls with agent ids to spawn_agents tool calls!
1 parent 5dd279a commit 54f477b

File tree

3 files changed

+125
-1
lines changed

3 files changed

+125
-1
lines changed

common/src/types/contracts/llm.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { ParamsExcluding } from '../function-params'
66
import type { Logger } from './logger'
77
import type { Model } from '../../old-constants'
88
import type { Message } from '../messages/codebuff-message'
9+
import type { AgentTemplate } from '../agent-template'
910
import type { generateText, streamText, ToolCallPart } from 'ai'
1011
import type z from 'zod/v4'
1112

@@ -42,6 +43,10 @@ export type PromptAiSdkStreamFn = (
4243
onCostCalculated?: (credits: number) => Promise<void>
4344
includeCacheControl?: boolean
4445
agentProviderOptions?: OpenRouterProviderRoutingOptions
46+
/** List of agents that can be spawned - used to transform agent tool calls */
47+
spawnableAgents?: string[]
48+
/** Map of locally available agent templates - used to transform agent tool calls */
49+
localAgentTemplates?: Record<string, AgentTemplate>
4550
sendAction: SendActionFn
4651
logger: Logger
4752
trackEvent: TrackEventFn

packages/agent-runtime/src/prompt-agent-stream.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const getAgentStreamFromTemplate = (params: {
2121
fingerprintId: string
2222
includeCacheControl?: boolean
2323
liveUserInputRecord: UserInputRecord
24+
localAgentTemplates: Record<string, AgentTemplate>
2425
logger: Logger
2526
messages: Message[]
2627
runId: string
@@ -42,6 +43,7 @@ export const getAgentStreamFromTemplate = (params: {
4243
fingerprintId,
4344
includeCacheControl,
4445
liveUserInputRecord,
46+
localAgentTemplates,
4547
logger,
4648
messages,
4749
runId,
@@ -71,12 +73,14 @@ export const getAgentStreamFromTemplate = (params: {
7173
includeCacheControl,
7274
logger,
7375
liveUserInputRecord,
76+
localAgentTemplates,
7477
maxOutputTokens: 32_000,
7578
maxRetries: 3,
7679
messages,
7780
model,
7881
runId,
7982
sessionConnections,
83+
spawnableAgents: template.spawnableAgents,
8084
stopSequences: [globalStopSequence],
8185
tools,
8286
userId,

sdk/src/impl/llm.ts

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ import {
1818
OpenAICompatibleChatLanguageModel,
1919
VERSION,
2020
} from '@codebuff/internal/openai-compatible/index'
21-
import { streamText, APICallError, generateText, generateObject } from 'ai'
21+
import {
22+
streamText,
23+
APICallError,
24+
generateText,
25+
generateObject,
26+
NoSuchToolError,
27+
} from 'ai'
2228

2329
import { WEBSITE_URL } from '../constants'
2430
import { NetworkError, PaymentRequiredError, ErrorCodes } from '../errors'
@@ -217,6 +223,115 @@ export async function* promptAiSdkStream(
217223
...params,
218224
agentProviderOptions: params.agentProviderOptions,
219225
}),
226+
// Transform agent tool calls (e.g. 'file-picker') into spawn_agents calls
227+
experimental_repairToolCall: async ({ toolCall, tools, error }) => {
228+
// Only handle NoSuchToolError - when model tries to call a non-existent tool
229+
if (!NoSuchToolError.isInstance(error)) {
230+
return null
231+
}
232+
233+
const { spawnableAgents = [], localAgentTemplates = {} } = params
234+
235+
// Check if spawn_agents tool is available
236+
if (!('spawn_agents' in tools)) {
237+
return null
238+
}
239+
240+
// Check if the tool name matches a spawnable agent or local agent template
241+
const toolName = toolCall.toolName
242+
const isSpawnableAgent = spawnableAgents.some((agentId) => {
243+
// Match by exact name or by the agent part of publisher/agent@version format
244+
const withoutVersion = agentId.split('@')[0]
245+
const parts = withoutVersion.split('/')
246+
const agentName = parts[parts.length - 1]
247+
return agentName === toolName || agentId === toolName
248+
})
249+
const isLocalAgent = toolName in localAgentTemplates
250+
251+
if (!isSpawnableAgent && !isLocalAgent) {
252+
return null
253+
}
254+
255+
// Parse the input - it comes as a JSON string from the AI SDK
256+
// We also need to recursively parse any nested JSON strings
257+
const deepParseJson = (value: unknown): unknown => {
258+
if (typeof value === 'string') {
259+
try {
260+
const parsed = JSON.parse(value)
261+
// Recursively parse the result in case it contains more JSON strings
262+
return deepParseJson(parsed)
263+
} catch {
264+
// Not valid JSON, return as-is
265+
return value
266+
}
267+
}
268+
if (Array.isArray(value)) {
269+
return value.map(deepParseJson)
270+
}
271+
if (value !== null && typeof value === 'object') {
272+
const result: Record<string, unknown> = {}
273+
for (const [k, v] of Object.entries(value)) {
274+
result[k] = deepParseJson(v)
275+
}
276+
return result
277+
}
278+
return value
279+
}
280+
281+
let input: Record<string, unknown> = {}
282+
try {
283+
const rawInput =
284+
typeof toolCall.input === 'string'
285+
? JSON.parse(toolCall.input)
286+
: (toolCall.input as Record<string, unknown>)
287+
// Deep parse to handle any nested JSON strings
288+
input = deepParseJson(rawInput) as Record<string, unknown>
289+
} catch {
290+
// If parsing fails, use empty object
291+
}
292+
293+
// Extract prompt from input if it exists and is a string
294+
const prompt =
295+
typeof input.prompt === 'string' ? input.prompt : undefined
296+
297+
// All other input parameters become the params object
298+
// These should NOT be stringified - they should remain as objects
299+
const agentParams: Record<string, unknown> = {}
300+
for (const [key, value] of Object.entries(input)) {
301+
if (key === 'prompt' && typeof value === 'string') {
302+
continue
303+
}
304+
agentParams[key] = value
305+
}
306+
307+
// Transform into spawn_agents call
308+
// The input must be a JSON string for the AI SDK, but the nested objects
309+
// should remain as proper objects (not stringified)
310+
const spawnAgentsInput = {
311+
agents: [
312+
{
313+
agent_type: toolName,
314+
...(prompt !== undefined && { prompt }),
315+
...(Object.keys(agentParams).length > 0 && { params: agentParams }),
316+
},
317+
],
318+
}
319+
320+
logger.info(
321+
{
322+
originalToolName: toolName,
323+
transformedInput: spawnAgentsInput,
324+
},
325+
'Transformed agent tool call to spawn_agents',
326+
)
327+
328+
// Return the repaired tool call - input must be a JSON string
329+
return {
330+
...toolCall,
331+
toolName: 'spawn_agents',
332+
input: JSON.stringify(spawnAgentsInput),
333+
}
334+
},
220335
})
221336

222337
let content = ''

0 commit comments

Comments
 (0)