Skip to content

Commit 9361c1b

Browse files
committed
Restore state-backed tool call IDs
1 parent 5c8b5ce commit 9361c1b

10 files changed

Lines changed: 262 additions & 20 deletions

File tree

common/src/types/session-state.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ export type AgentState = {
4949
* This is updated on every agent step via the /api/v1/token-count endpoint.
5050
*/
5151
contextTokenCount: number
52+
toolCallState?: {
53+
nextIndex: number
54+
}
5255
}
5356

5457
export const AgentOutputSchema = z.discriminatedUnion('type', [
@@ -137,6 +140,7 @@ export function getInitialAgentState(): AgentState {
137140
systemPrompt: '',
138141
toolDefinitions: {},
139142
contextTokenCount: 0,
143+
toolCallState: { nextIndex: 0 },
140144
}
141145
}
142146
export function getInitialSessionState(

packages/agent-runtime/src/__tests__/run-programmatic-step.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,28 @@ describe('runProgrammaticStep', () => {
212212
})
213213

214214
describe('tool execution', () => {
215+
it('assigns deterministic global ids to handleSteps tool calls', async () => {
216+
const mockGenerator = (function* () {
217+
yield { toolName: 'read_files', input: { paths: ['first.txt'] } }
218+
yield { toolName: 'read_files', input: { paths: ['second.txt'] } }
219+
yield { toolName: 'end_turn', input: {} }
220+
})() as StepGenerator
221+
222+
mockTemplate.handleSteps = () => mockGenerator
223+
224+
await runProgrammaticStep(mockParams)
225+
226+
expect(executeToolCallSpy.mock.calls[0][0].toolCallId).toBe(
227+
'functions.read_files.0',
228+
)
229+
expect(executeToolCallSpy.mock.calls[1][0].toolCallId).toBe(
230+
'functions.read_files.1',
231+
)
232+
expect(executeToolCallSpy.mock.calls[2][0].toolCallId).toBe(
233+
'functions.end_turn.2',
234+
)
235+
})
236+
215237
it('should not add tool call message for add_message tool', async () => {
216238
const mockGenerator = (function* () {
217239
yield {

packages/agent-runtime/src/__tests__/tool-validation-error.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,13 +464,16 @@ describe('tool validation error handling', () => {
464464
)
465465
expect(toolCallEvents.length).toBe(1)
466466
expect(toolCallEvents[0].toolName).toBe('read_files')
467+
expect(toolCallEvents[0].toolCallId).toBe('functions.read_files.0')
467468

468469
// Verify tool_result event was emitted
469470
const toolResultEvents = responseChunks.filter(
470471
(chunk): chunk is Extract<PrintModeEvent, { type: 'tool_result' }> =>
471472
typeof chunk !== 'string' && chunk.type === 'tool_result',
472473
)
473474
expect(toolResultEvents.length).toBe(1)
475+
expect(toolResultEvents[0].toolName).toBe('read_files')
476+
expect(toolResultEvents[0].toolCallId).toBe('functions.read_files.0')
474477

475478
// Verify NO error events
476479
const errorEvents = responseChunks.filter(

packages/agent-runtime/src/run-programmatic-step.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { cloneDeep } from 'lodash'
66
import { clearProposedContentForRun } from './tools/handlers/tool/proposed-content-store'
77
import { executeToolCall } from './tools/tool-executor'
88
import { parseTextWithToolCalls } from './util/parse-tool-calls-from-text'
9-
9+
import { createToolCallIdGenerator } from './util/tool-call-id'
1010

1111
import type { FileProcessingState } from './tools/handlers/tool/write-file'
1212
import type { ExecuteToolCallParams } from './tools/tool-executor'
@@ -213,6 +213,7 @@ export async function runProgrammaticStep(
213213
let toolResult: ToolResultOutput[] | undefined = undefined
214214
let endTurn = false
215215
let generateN: number | undefined = undefined
216+
const getToolCallId = createToolCallIdGenerator(agentState)
216217

217218
let startTime = new Date()
218219
let creditsBefore = agentState.directCreditsUsed
@@ -273,6 +274,7 @@ export async function runProgrammaticStep(
273274
previousToolCallFinished: Promise.resolve(),
274275
toolCalls,
275276
toolResults,
277+
getToolCallId,
276278
onResponseChunk,
277279
})
278280
}
@@ -301,6 +303,7 @@ export async function runProgrammaticStep(
301303
previousToolCallFinished: Promise.resolve(),
302304
toolCalls,
303305
toolResults,
306+
getToolCallId,
304307
onResponseChunk,
305308
})
306309

@@ -432,6 +435,7 @@ type ExecuteToolCallsArrayParams = Omit<
432435
| 'toolResultsToAddToMessageHistory'
433436
> & {
434437
agentState: AgentState
438+
getToolCallId: (toolName: string) => string
435439
onResponseChunk: (chunk: string | PrintModeEvent) => void
436440
}
437441

@@ -445,7 +449,7 @@ async function executeSingleToolCall(
445449
toolCallToExecute: ToolCallToExecute,
446450
params: ExecuteToolCallsArrayParams,
447451
): Promise<ToolResultOutput[] | undefined> {
448-
const { agentState, onResponseChunk, toolResults } = params
452+
const { agentState, getToolCallId, onResponseChunk, toolResults } = params
449453

450454
// Note: We don't check if the tool is available for the agent template anymore.
451455
// You can run any tool from handleSteps now!
@@ -455,7 +459,7 @@ async function executeSingleToolCall(
455459
// )
456460
// }
457461

458-
const toolCallId = crypto.randomUUID()
462+
const toolCallId = getToolCallId(toolCallToExecute.toolName)
459463
const excludeToolFromMessageHistory =
460464
toolCallToExecute.includeToolCall === false
461465

packages/agent-runtime/src/tool-stream-parser.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ export async function* processStreamWithTools(params: {
5050
}
5151
trackEvent: TrackEventFn
5252
executeXmlToolCall: (params: {
53-
toolCallId: string
5453
toolName: string
5554
input: Record<string, unknown>
5655
}) => Promise<void>
@@ -150,12 +149,9 @@ export async function* processStreamWithTools(params: {
150149

151150
// Then process and yield any XML tool calls found
152151
for (const toolCall of toolCalls) {
153-
const toolCallId = `xml-${crypto.randomUUID().slice(0, 8)}`
154-
155152
// Execute the tool immediately if callback provided, pausing the stream
156153
// The callback handles emitting tool_call and tool_result events
157154
await executeXmlToolCall({
158-
toolCallId,
159155
toolName: toolCall.toolName,
160156
input: toolCall.input,
161157
})

packages/agent-runtime/src/tools/handlers/tool/spawn-agent-utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { generateCompactId } from '@codebuff/common/util/string'
99
import { loopAgentSteps } from '../../../run-agent-step'
1010
import { getAgentTemplate } from '../../../templates/agent-registry'
1111
import { formatValueForError } from '../../../util/format-value'
12+
import { ensureToolCallState } from '../../../util/tool-call-id'
1213
import {
1314
filterUnfinishedToolCalls,
1415
withSystemTags,
@@ -256,6 +257,7 @@ export function createAgentState(
256257
agentContext: Record<string, Subgoal>,
257258
): AgentState {
258259
const agentId = generateCompactId()
260+
const toolCallState = ensureToolCallState(parentAgentState)
259261

260262
// When including message history, filter out any tool calls that don't have
261263
// corresponding tool responses. This prevents the spawned agent from seeing
@@ -295,6 +297,7 @@ export function createAgentState(
295297
systemPrompt: '',
296298
toolDefinitions: {},
297299
contextTokenCount: parentAgentState.contextTokenCount,
300+
toolCallState,
298301
}
299302
}
300303

packages/agent-runtime/src/tools/stream-parser.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
assistantMessage,
66
userMessage,
77
} from '@codebuff/common/util/messages'
8-
import { generateCompactId } from '@codebuff/common/util/string'
98

109
import { processStreamWithTools } from '../tool-stream-parser'
1110
import { INCLUDE_REASONING_IN_MESSAGE_HISTORY } from '../constants'
@@ -14,6 +13,7 @@ import {
1413
executeToolCall,
1514
tryTransformAgentToolCall,
1615
} from './tool-executor'
16+
import { createToolCallIdGenerator } from '../util/tool-call-id'
1717
import { withSystemTags } from '../util/messages'
1818

1919
import type { CustomToolCall, ExecuteToolCallParams } from './tool-executor'
@@ -91,6 +91,7 @@ export async function processStream(
9191
const toolCalls: (CodebuffToolCall | CustomToolCall)[] = []
9292
const toolCallsToAddToMessageHistory: (CodebuffToolCall | CustomToolCall)[] = []
9393
const assistantMessages: Message[] = []
94+
const getToolCallId = createToolCallIdGenerator(agentState)
9495
let hadToolCallError = false
9596
const errorMessages: Message[] = []
9697
const { promise: streamDonePromise, resolve: resolveStreamDonePromise } =
@@ -137,7 +138,6 @@ export async function processStream(
137138
if (signal.aborted) {
138139
return
139140
}
140-
const toolCallId = generateCompactId()
141141
const isNativeTool = toolNames.includes(toolName as ToolName)
142142

143143
// Check if this is an agent tool call that should be transformed to spawn_agents
@@ -160,19 +160,20 @@ export async function processStream(
160160
// Determine which executor to use and with what parameters
161161
let toolPromise: Promise<void>
162162
if (isNativeTool || transformed) {
163+
const effectiveToolName = transformed
164+
? transformed.toolName
165+
: (toolName as ToolName)
163166
// Use executeToolCall for native tools or transformed agent calls
164167
toolPromise = executeToolCall({
165168
...params,
166-
toolName: transformed
167-
? transformed.toolName
168-
: (toolName as ToolName),
169+
toolName: effectiveToolName,
169170
input: transformed ? transformed.input : input,
170171
fromHandleSteps: false,
171172

172173
fileProcessingState,
173174
fullResponse: fullResponseChunks.join(''),
174175
previousToolCallFinished: previousPromise,
175-
toolCallId,
176+
toolCallId: getToolCallId(effectiveToolName),
176177
toolCalls,
177178
toolCallsToAddToMessageHistory,
178179
toolResults,
@@ -191,7 +192,7 @@ export async function processStream(
191192
fileProcessingState,
192193
fullResponse: fullResponseChunks.join(''),
193194
previousToolCallFinished: previousPromise,
194-
toolCallId,
195+
toolCallId: getToolCallId(toolName),
195196
toolCalls,
196197
toolCallsToAddToMessageHistory,
197198
toolResults,

packages/agent-runtime/src/tools/tool-executor.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { endsAgentStepParam, toolNames } from '@codebuff/common/tools/constants'
22
import { toolParams } from '@codebuff/common/tools/list'
3-
import { generateCompactId } from '@codebuff/common/util/string'
3+
import { normalizeAgentIdForLookup } from '@codebuff/common/util/agent-id-parsing'
44
import { cloneDeep } from 'lodash'
55

66
import { getMCPToolData } from '../mcp'
77
import { MCP_TOOL_SEPARATOR } from '../mcp-constants'
88
import { getAgentShortName, getAgentToolName } from '../templates/prompts'
99
import { formatValueForError } from '../util/format-value'
10+
import { createToolCallIdGenerator } from '../util/tool-call-id'
1011
import { codebuffToolHandlers } from './handlers/list'
1112
import { getMatchingSpawn } from './handlers/tool/spawn-agent-utils'
1213
import { getAgentTemplate } from '../templates/agent-registry'
@@ -308,7 +309,9 @@ export async function executeToolCall<T extends ToolName>(
308309
onResponseChunk,
309310
requestToolCall,
310311
} = params
311-
const toolCallId = params.toolCallId ?? generateCompactId()
312+
const toolCallId =
313+
params.toolCallId ??
314+
createToolCallIdGenerator(agentState, toolCalls)(toolName)
312315

313316
const toolCall: CodebuffToolCall<T> | ToolCallError = parseRawToolCall<T>({
314317
rawToolCall: {
@@ -369,7 +372,9 @@ export async function executeToolCall<T extends ToolName>(
369372
}
370373
}
371374

372-
let agentIdToLoad = agentTypeStr
375+
let agentIdToLoad = isBaseAgent
376+
? normalizeAgentIdForLookup(agentTypeStr)
377+
: agentTypeStr
373378
if (!isBaseAgent) {
374379
const matchingSpawn = getMatchingSpawn(
375380
agentTemplate.spawnableAgents,
@@ -418,7 +423,13 @@ export async function executeToolCall<T extends ToolName>(
418423
}
419424
}
420425

421-
return { valid: true as const, agent }
426+
return {
427+
valid: true as const,
428+
agent: {
429+
...(agent as Record<string, unknown>),
430+
agent_type: agentIdToLoad,
431+
},
432+
}
422433
}),
423434
)
424435

@@ -447,8 +458,8 @@ export async function executeToolCall<T extends ToolName>(
447458
}
448459
const errorMsg = `Some agents could not be spawned: ${errors.join('; ')}. Proceeding with valid agents only.`
449460
onResponseChunk({ type: 'error', message: errorMsg })
450-
effectiveInput = { ...effectiveInput, agents: validAgents }
451461
}
462+
effectiveInput = { ...effectiveInput, agents: validAgents }
452463
}
453464
}
454465

@@ -640,7 +651,9 @@ export async function executeCustomToolCall(
640651
}),
641652
rawToolCall: {
642653
toolName,
643-
toolCallId: toolCallId ?? generateCompactId(),
654+
toolCallId:
655+
toolCallId ??
656+
createToolCallIdGenerator(agentState, toolCalls)(toolName),
644657
input,
645658
},
646659
autoInsertEndStepParam,

0 commit comments

Comments
 (0)