diff --git a/cli/release/package.json b/cli/release/package.json index 1f46a0c0f..b6d6c62fa 100644 --- a/cli/release/package.json +++ b/cli/release/package.json @@ -1,6 +1,6 @@ { "name": "codebuff", - "version": "1.0.669", + "version": "1.0.671", "description": "AI coding agent", "license": "MIT", "bin": { diff --git a/cli/src/components/tools/__tests__/apply-patch.test.tsx b/cli/src/components/tools/__tests__/apply-patch.test.tsx index 75154bd96..6e177757f 100644 --- a/cli/src/components/tools/__tests__/apply-patch.test.tsx +++ b/cli/src/components/tools/__tests__/apply-patch.test.tsx @@ -47,7 +47,7 @@ describe('ApplyPatchComponent', () => { expect(markup).toContain('src/new-file.ts') }) - test('renders update_file operation with diff content', () => { + test('renders update_file operation without diff content while diff rendering is disabled', () => { const toolBlock = createToolBlock({ type: 'update_file', path: 'src/existing.ts', @@ -62,8 +62,8 @@ describe('ApplyPatchComponent', () => { const markup = renderToStaticMarkup(result?.content as React.ReactElement) expect(markup).toContain('Edit') expect(markup).toContain('src/existing.ts') - expect(markup).toContain('-oldLine') - expect(markup).toContain('+newLine') + expect(markup).not.toContain('-oldLine') + expect(markup).not.toContain('+newLine') }) test('renders delete_file operation', () => { diff --git a/cli/src/components/tools/diff-viewer.tsx b/cli/src/components/tools/diff-viewer.tsx index 72ee7361f..37d613a9a 100644 --- a/cli/src/components/tools/diff-viewer.tsx +++ b/cli/src/components/tools/diff-viewer.tsx @@ -6,6 +6,8 @@ interface DiffViewerProps { diffText: string } +const RENDER_DIFFS = false + const DIFF_LINE_COLORS = { dark: { added: '#7ACC35', @@ -50,6 +52,11 @@ const lineColor = ( export const DiffViewer = ({ diffText }: DiffViewerProps) => { const theme = useTheme() + + if (!RENDER_DIFFS) { + return null + } + const lines = diffText.trim().split('\n') return ( diff --git a/cli/src/utils/__tests__/message-block-helpers.test.ts b/cli/src/utils/__tests__/message-block-helpers.test.ts index d813de400..304514aab 100644 --- a/cli/src/utils/__tests__/message-block-helpers.test.ts +++ b/cli/src/utils/__tests__/message-block-helpers.test.ts @@ -39,10 +39,6 @@ describe('getAgentBaseName', () => { expect(getAgentBaseName('file-picker')).toBe('file-picker') }) - test('normalizes direct tool aliases to canonical agent names', () => { - expect(getAgentBaseName('code_reviewer_lite')).toBe('code-reviewer-lite') - }) - test('handles scoped name without version', () => { expect(getAgentBaseName('codebuff/file-picker')).toBe('file-picker') }) diff --git a/cli/src/utils/__tests__/sdk-event-handlers.test.ts b/cli/src/utils/__tests__/sdk-event-handlers.test.ts index b86566b43..051a59689 100644 --- a/cli/src/utils/__tests__/sdk-event-handlers.test.ts +++ b/cli/src/utils/__tests__/sdk-event-handlers.test.ts @@ -212,89 +212,6 @@ describe('sdk-event-handlers', () => { expect(getStreamingAgents().has('tool-1-0')).toBe(false) }) - test('matches underscore direct-tool aliases to hyphenated agent ids', () => { - const { ctx, getMessages, getStreamingAgents } = createTestContext() - const handleEvent = createEventHandler(ctx) - const handleChunk = createStreamChunkHandler(ctx) - - handleEvent({ - type: 'tool_call', - toolCallId: 'tool-1', - toolName: 'spawn_agents', - input: { - agents: [ - { - agent_type: 'code_reviewer_lite', - prompt: 'Review this change', - }, - ], - }, - agentId: 'main-agent', - parentAgentId: undefined, - } as any) - - handleEvent({ - type: 'subagent_start', - agentId: 'agent-real', - agentType: 'code-reviewer-lite', - displayName: 'Code Reviewer Lite', - onlyChild: true, - parentAgentId: undefined, - params: undefined, - prompt: 'Review this change', - }) - - handleChunk({ - type: 'subagent_chunk', - agentId: 'agent-real', - agentType: 'code-reviewer-lite', - chunk: 'streamed review', - }) - - handleEvent({ - type: 'subagent_finish', - agentId: 'agent-real', - agentType: 'code-reviewer-lite', - displayName: 'Code Reviewer Lite', - onlyChild: true, - parentAgentId: undefined, - params: undefined, - prompt: 'Review this change', - }) - - handleEvent({ - type: 'tool_result', - toolCallId: 'tool-1', - toolName: 'spawn_agents', - output: [ - { - type: 'json', - value: [ - { - agentName: 'code-reviewer-lite', - agentType: 'code-reviewer-lite', - value: 'streamed review', - }, - ], - }, - ], - } as any) - - const blocks = getMessages()[0].blocks ?? [] - expect(blocks).toHaveLength(1) - const agentBlock = blocks[0] as AgentContentBlock - expect(agentBlock.agentId).toBe('agent-real') - expect(agentBlock.agentName).toBe('code-reviewer-lite') - expect(agentBlock.agentType).toBe('code-reviewer-lite') - expect(agentBlock.status).toBe('complete') - expect(agentBlock.blocks).toHaveLength(1) - expect(agentBlock.blocks?.[0]).toMatchObject({ - type: 'text', - content: 'streamed review', - }) - expect(getStreamingAgents().size).toBe(0) - }) - test('handles spawn_agents tool results and clears streaming agents', () => { const { ctx, getMessages, getStreamingAgents } = createTestContext() ctx.message.updater.addBlock( diff --git a/cli/src/utils/__tests__/send-message-helpers.test.ts b/cli/src/utils/__tests__/send-message-helpers.test.ts index 00f95b899..4967498cf 100644 --- a/cli/src/utils/__tests__/send-message-helpers.test.ts +++ b/cli/src/utils/__tests__/send-message-helpers.test.ts @@ -1325,10 +1325,6 @@ describe('getAgentBaseName', () => { test('returns simple name unchanged', () => { expect(getAgentBaseName('file-picker')).toBe('file-picker') }) - - test('normalizes direct tool aliases to canonical agent names', () => { - expect(getAgentBaseName('code_reviewer_lite')).toBe('code-reviewer-lite') - }) }) describe('agentTypesMatch', () => { diff --git a/cli/src/utils/message-block-helpers.ts b/cli/src/utils/message-block-helpers.ts index 2d0eb29fe..b9668da41 100644 --- a/cli/src/utils/message-block-helpers.ts +++ b/cli/src/utils/message-block-helpers.ts @@ -16,11 +16,10 @@ import type { * getAgentBaseName('codebuff/file-picker@0.0.2') // 'file-picker' * getAgentBaseName('file-picker@1.0.0') // 'file-picker' * getAgentBaseName('file-picker') // 'file-picker' - * getAgentBaseName('file_picker') // 'file-picker' */ export const getAgentBaseName = (type: string): string => { const segment = type.split('/').pop() ?? type - return segment.split('@')[0].replace(/_/g, '-') + return segment.split('@')[0] } /** @@ -467,7 +466,6 @@ export const moveSpawnAgentBlock = ( parentId?: string, params?: Record, prompt?: string, - realAgentType?: string, ): ContentBlock[] => { const updateAgentBlock = (block: ContentBlock): ContentBlock => { if (block.type !== 'agent') { @@ -486,11 +484,6 @@ export const moveSpawnAgentBlock = ( updatedBlock.initialPrompt = prompt } - if (realAgentType) { - updatedBlock.agentType = realAgentType - updatedBlock.agentName = realAgentType - } - return updatedBlock } diff --git a/cli/src/utils/sdk-event-handlers.ts b/cli/src/utils/sdk-event-handlers.ts index 42c273a82..6f304f147 100644 --- a/cli/src/utils/sdk-event-handlers.ts +++ b/cli/src/utils/sdk-event-handlers.ts @@ -183,7 +183,6 @@ const handleSubagentStart = ( blocks, match: spawnAgentMatch, realAgentId: event.agentId, - realAgentType: event.agentType, parentAgentId: event.parentAgentId, params: event.params, prompt: event.prompt, diff --git a/cli/src/utils/spawn-agent-matcher.ts b/cli/src/utils/spawn-agent-matcher.ts index a87e493b1..c3eb5c054 100644 --- a/cli/src/utils/spawn-agent-matcher.ts +++ b/cli/src/utils/spawn-agent-matcher.ts @@ -28,7 +28,6 @@ export const resolveSpawnAgentToReal = (options: { blocks: ContentBlock[] match: SpawnAgentMatch realAgentId: string - realAgentType?: string parentAgentId?: string params?: Record prompt?: string @@ -37,7 +36,6 @@ export const resolveSpawnAgentToReal = (options: { blocks, match, realAgentId, - realAgentType, parentAgentId, params: agentParams, prompt, @@ -50,6 +48,5 @@ export const resolveSpawnAgentToReal = (options: { parentAgentId, agentParams, prompt, - realAgentType, ) } diff --git a/freebuff/cli/release/package.json b/freebuff/cli/release/package.json index 55a51a2cc..0b810c657 100644 --- a/freebuff/cli/release/package.json +++ b/freebuff/cli/release/package.json @@ -1,6 +1,6 @@ { "name": "freebuff", - "version": "0.0.80", + "version": "0.0.82", "description": "The world's strongest free coding agent", "license": "MIT", "bin": { diff --git a/packages/agent-runtime/src/__tests__/run-programmatic-step.test.ts b/packages/agent-runtime/src/__tests__/run-programmatic-step.test.ts index 5a06372e0..954bdc73f 100644 --- a/packages/agent-runtime/src/__tests__/run-programmatic-step.test.ts +++ b/packages/agent-runtime/src/__tests__/run-programmatic-step.test.ts @@ -212,28 +212,6 @@ describe('runProgrammaticStep', () => { }) describe('tool execution', () => { - it('assigns deterministic per-tool ids to handleSteps tool calls', async () => { - const mockGenerator = (function* () { - yield { toolName: 'read_files', input: { paths: ['first.txt'] } } - yield { toolName: 'read_files', input: { paths: ['second.txt'] } } - yield { toolName: 'end_turn', input: {} } - })() as StepGenerator - - mockTemplate.handleSteps = () => mockGenerator - - await runProgrammaticStep(mockParams) - - expect(executeToolCallSpy.mock.calls[0][0].toolCallId).toBe( - 'functions.read_files:0', - ) - expect(executeToolCallSpy.mock.calls[1][0].toolCallId).toBe( - 'functions.read_files:1', - ) - expect(executeToolCallSpy.mock.calls[2][0].toolCallId).toBe( - 'functions.end_turn:0', - ) - }) - it('should not add tool call message for add_message tool', async () => { const mockGenerator = (function* () { yield { diff --git a/packages/agent-runtime/src/__tests__/tool-validation-error.test.ts b/packages/agent-runtime/src/__tests__/tool-validation-error.test.ts index 520b4d087..c07ce42cb 100644 --- a/packages/agent-runtime/src/__tests__/tool-validation-error.test.ts +++ b/packages/agent-runtime/src/__tests__/tool-validation-error.test.ts @@ -464,7 +464,6 @@ describe('tool validation error handling', () => { ) expect(toolCallEvents.length).toBe(1) expect(toolCallEvents[0].toolName).toBe('read_files') - expect(toolCallEvents[0].toolCallId).toBe('functions.read_files:0') // Verify tool_result event was emitted const toolResultEvents = responseChunks.filter( @@ -472,8 +471,6 @@ describe('tool validation error handling', () => { typeof chunk !== 'string' && chunk.type === 'tool_result', ) expect(toolResultEvents.length).toBe(1) - expect(toolResultEvents[0].toolName).toBe('read_files') - expect(toolResultEvents[0].toolCallId).toBe('functions.read_files:0') // Verify NO error events const errorEvents = responseChunks.filter( diff --git a/packages/agent-runtime/src/run-programmatic-step.ts b/packages/agent-runtime/src/run-programmatic-step.ts index 83bd94368..64addd410 100644 --- a/packages/agent-runtime/src/run-programmatic-step.ts +++ b/packages/agent-runtime/src/run-programmatic-step.ts @@ -6,7 +6,7 @@ import { cloneDeep } from 'lodash' import { clearProposedContentForRun } from './tools/handlers/tool/proposed-content-store' import { executeToolCall } from './tools/tool-executor' import { parseTextWithToolCalls } from './util/parse-tool-calls-from-text' -import { createToolCallIdGenerator } from './util/tool-call-id' + import type { FileProcessingState } from './tools/handlers/tool/write-file' import type { ExecuteToolCallParams } from './tools/tool-executor' @@ -213,7 +213,6 @@ export async function runProgrammaticStep( let toolResult: ToolResultOutput[] | undefined = undefined let endTurn = false let generateN: number | undefined = undefined - const getToolCallId = createToolCallIdGenerator(agentState.messageHistory) let startTime = new Date() let creditsBefore = agentState.directCreditsUsed @@ -274,7 +273,6 @@ export async function runProgrammaticStep( previousToolCallFinished: Promise.resolve(), toolCalls, toolResults, - getToolCallId, onResponseChunk, }) } @@ -303,7 +301,6 @@ export async function runProgrammaticStep( previousToolCallFinished: Promise.resolve(), toolCalls, toolResults, - getToolCallId, onResponseChunk, }) @@ -435,7 +432,6 @@ type ExecuteToolCallsArrayParams = Omit< | 'toolResultsToAddToMessageHistory' > & { agentState: AgentState - getToolCallId: (toolName: string) => string onResponseChunk: (chunk: string | PrintModeEvent) => void } @@ -449,7 +445,7 @@ async function executeSingleToolCall( toolCallToExecute: ToolCallToExecute, params: ExecuteToolCallsArrayParams, ): Promise { - const { agentState, getToolCallId, onResponseChunk, toolResults } = params + const { agentState, onResponseChunk, toolResults } = params // Note: We don't check if the tool is available for the agent template anymore. // You can run any tool from handleSteps now! @@ -459,7 +455,7 @@ async function executeSingleToolCall( // ) // } - const toolCallId = getToolCallId(toolCallToExecute.toolName) + const toolCallId = crypto.randomUUID() const excludeToolFromMessageHistory = toolCallToExecute.includeToolCall === false diff --git a/packages/agent-runtime/src/tool-stream-parser.ts b/packages/agent-runtime/src/tool-stream-parser.ts index 1f4deed9d..cd4ca58df 100644 --- a/packages/agent-runtime/src/tool-stream-parser.ts +++ b/packages/agent-runtime/src/tool-stream-parser.ts @@ -50,6 +50,7 @@ export async function* processStreamWithTools(params: { } trackEvent: TrackEventFn executeXmlToolCall: (params: { + toolCallId: string toolName: string input: Record }) => Promise @@ -149,9 +150,12 @@ export async function* processStreamWithTools(params: { // Then process and yield any XML tool calls found for (const toolCall of toolCalls) { + const toolCallId = `xml-${crypto.randomUUID().slice(0, 8)}` + // Execute the tool immediately if callback provided, pausing the stream // The callback handles emitting tool_call and tool_result events await executeXmlToolCall({ + toolCallId, toolName: toolCall.toolName, input: toolCall.input, }) diff --git a/packages/agent-runtime/src/tools/stream-parser.ts b/packages/agent-runtime/src/tools/stream-parser.ts index fd8f9ea0c..4cdb32117 100644 --- a/packages/agent-runtime/src/tools/stream-parser.ts +++ b/packages/agent-runtime/src/tools/stream-parser.ts @@ -5,6 +5,7 @@ import { assistantMessage, userMessage, } from '@codebuff/common/util/messages' +import { generateCompactId } from '@codebuff/common/util/string' import { processStreamWithTools } from '../tool-stream-parser' import { INCLUDE_REASONING_IN_MESSAGE_HISTORY } from '../constants' @@ -13,7 +14,6 @@ import { executeToolCall, tryTransformAgentToolCall, } from './tool-executor' -import { createToolCallIdGenerator } from '../util/tool-call-id' import { withSystemTags } from '../util/messages' import type { CustomToolCall, ExecuteToolCallParams } from './tool-executor' @@ -91,7 +91,6 @@ export async function processStream( const toolCalls: (CodebuffToolCall | CustomToolCall)[] = [] const toolCallsToAddToMessageHistory: (CodebuffToolCall | CustomToolCall)[] = [] const assistantMessages: Message[] = [] - const getToolCallId = createToolCallIdGenerator(params.messages) let hadToolCallError = false const errorMessages: Message[] = [] const { promise: streamDonePromise, resolve: resolveStreamDonePromise } = @@ -138,6 +137,7 @@ export async function processStream( if (signal.aborted) { return } + const toolCallId = generateCompactId() const isNativeTool = toolNames.includes(toolName as ToolName) // Check if this is an agent tool call that should be transformed to spawn_agents @@ -160,20 +160,19 @@ export async function processStream( // Determine which executor to use and with what parameters let toolPromise: Promise if (isNativeTool || transformed) { - const effectiveToolName = transformed - ? transformed.toolName - : (toolName as ToolName) // Use executeToolCall for native tools or transformed agent calls toolPromise = executeToolCall({ ...params, - toolName: effectiveToolName, + toolName: transformed + ? transformed.toolName + : (toolName as ToolName), input: transformed ? transformed.input : input, fromHandleSteps: false, fileProcessingState, fullResponse: fullResponseChunks.join(''), previousToolCallFinished: previousPromise, - toolCallId: getToolCallId(effectiveToolName), + toolCallId, toolCalls, toolCallsToAddToMessageHistory, toolResults, @@ -192,7 +191,7 @@ export async function processStream( fileProcessingState, fullResponse: fullResponseChunks.join(''), previousToolCallFinished: previousPromise, - toolCallId: getToolCallId(toolName), + toolCallId, toolCalls, toolCallsToAddToMessageHistory, toolResults, diff --git a/packages/agent-runtime/src/tools/tool-executor.ts b/packages/agent-runtime/src/tools/tool-executor.ts index f50e8823c..8fd7130bf 100644 --- a/packages/agent-runtime/src/tools/tool-executor.ts +++ b/packages/agent-runtime/src/tools/tool-executor.ts @@ -1,13 +1,12 @@ import { endsAgentStepParam, toolNames } from '@codebuff/common/tools/constants' import { toolParams } from '@codebuff/common/tools/list' -import { normalizeAgentIdForLookup } from '@codebuff/common/util/agent-id-parsing' +import { generateCompactId } from '@codebuff/common/util/string' import { cloneDeep } from 'lodash' import { getMCPToolData } from '../mcp' import { MCP_TOOL_SEPARATOR } from '../mcp-constants' import { getAgentShortName, getAgentToolName } from '../templates/prompts' import { formatValueForError } from '../util/format-value' -import { createToolCallIdGenerator } from '../util/tool-call-id' import { codebuffToolHandlers } from './handlers/list' import { getMatchingSpawn } from './handlers/tool/spawn-agent-utils' import { getAgentTemplate } from '../templates/agent-registry' @@ -309,9 +308,7 @@ export async function executeToolCall( onResponseChunk, requestToolCall, } = params - const toolCallId = - params.toolCallId ?? - createToolCallIdGenerator(agentState.messageHistory, toolCalls)(toolName) + const toolCallId = params.toolCallId ?? generateCompactId() const toolCall: CodebuffToolCall | ToolCallError = parseRawToolCall({ rawToolCall: { @@ -372,9 +369,7 @@ export async function executeToolCall( } } - let agentIdToLoad = isBaseAgent - ? normalizeAgentIdForLookup(agentTypeStr) - : agentTypeStr + let agentIdToLoad = agentTypeStr if (!isBaseAgent) { const matchingSpawn = getMatchingSpawn( agentTemplate.spawnableAgents, @@ -423,13 +418,7 @@ export async function executeToolCall( } } - return { - valid: true as const, - agent: { - ...(agent as Record), - agent_type: agentIdToLoad, - }, - } + return { valid: true as const, agent } }), ) @@ -458,8 +447,8 @@ export async function executeToolCall( } const errorMsg = `Some agents could not be spawned: ${errors.join('; ')}. Proceeding with valid agents only.` onResponseChunk({ type: 'error', message: errorMsg }) + effectiveInput = { ...effectiveInput, agents: validAgents } } - effectiveInput = { ...effectiveInput, agents: validAgents } } } @@ -651,11 +640,7 @@ export async function executeCustomToolCall( }), rawToolCall: { toolName, - toolCallId: - toolCallId ?? - createToolCallIdGenerator(agentState.messageHistory, toolCalls)( - toolName, - ), + toolCallId: toolCallId ?? generateCompactId(), input, }, autoInsertEndStepParam, diff --git a/packages/agent-runtime/src/util/__tests__/tool-call-id.test.ts b/packages/agent-runtime/src/util/__tests__/tool-call-id.test.ts deleted file mode 100644 index 21a150f63..000000000 --- a/packages/agent-runtime/src/util/__tests__/tool-call-id.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { assistantMessage } from '@codebuff/common/util/messages' -import { describe, expect, it } from 'bun:test' - -import { - countToolCallsByName, - createToolCallIdGenerator, - formatToolCallId, -} from '../tool-call-id' - -describe('tool call ids', () => { - it('formats ids with the tool name and per-tool invocation index', () => { - expect(formatToolCallId('glob', 0)).toBe('functions.glob:0') - }) - - it('seeds per-tool counters from existing message history', () => { - const messages = [ - assistantMessage({ - type: 'tool-call', - toolName: 'glob', - toolCallId: 'functions.glob:0', - input: { pattern: '**/*.ts' }, - }), - assistantMessage({ - type: 'tool-call', - toolName: 'read_files', - toolCallId: 'functions.read_files:0', - input: { paths: ['src/index.ts'] }, - }), - assistantMessage({ - type: 'tool-call', - toolName: 'glob', - toolCallId: 'functions.glob:1', - input: { pattern: '**/*.tsx' }, - }), - ] - - expect(countToolCallsByName(messages)).toEqual( - new Map([ - ['glob', 2], - ['read_files', 1], - ]), - ) - - const getToolCallId = createToolCallIdGenerator(messages) - - expect(getToolCallId('glob')).toBe('functions.glob:2') - expect(getToolCallId('glob')).toBe('functions.glob:3') - expect(getToolCallId('read_files')).toBe('functions.read_files:1') - }) - - it('can seed counters from pending tool calls', () => { - const getToolCallId = createToolCallIdGenerator([], [ - { - toolName: 'glob', - }, - { - toolName: 'glob', - }, - ]) - - expect(getToolCallId('glob')).toBe('functions.glob:2') - }) -}) diff --git a/packages/agent-runtime/src/util/tool-call-id.ts b/packages/agent-runtime/src/util/tool-call-id.ts deleted file mode 100644 index bfa64f150..000000000 --- a/packages/agent-runtime/src/util/tool-call-id.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { Message } from '@codebuff/common/types/messages/codebuff-message' - -const TOOL_CALL_ID_PREFIX = 'functions' -type ToolCallLike = { toolName: string } - -export function formatToolCallId(toolName: string, index: number): string { - return `${TOOL_CALL_ID_PREFIX}.${toolName}:${index}` -} - -export function countToolCallsByName( - messages: Message[], - pendingToolCalls: ToolCallLike[] = [], -): Map { - const counts = new Map() - - for (const message of messages) { - if (message.role !== 'assistant') { - continue - } - - for (const part of message.content) { - if (part.type !== 'tool-call') { - continue - } - - counts.set(part.toolName, (counts.get(part.toolName) ?? 0) + 1) - } - } - - for (const toolCall of pendingToolCalls) { - counts.set(toolCall.toolName, (counts.get(toolCall.toolName) ?? 0) + 1) - } - - return counts -} - -export function createToolCallIdGenerator( - messages: Message[], - pendingToolCalls: ToolCallLike[] = [], -) { - const counts = countToolCallsByName(messages, pendingToolCalls) - - return (toolName: string): string => { - const index = counts.get(toolName) ?? 0 - counts.set(toolName, index + 1) - return formatToolCallId(toolName, index) - } -}