diff --git a/backend/src/__tests__/main-prompt.integration.test.ts b/backend/src/__tests__/main-prompt.integration.test.ts index a3356fa91a..e873bb210a 100644 --- a/backend/src/__tests__/main-prompt.integration.test.ts +++ b/backend/src/__tests__/main-prompt.integration.test.ts @@ -7,7 +7,7 @@ import { getToolCallString } from '@codebuff/common/tools/utils' import { getInitialSessionState } from '@codebuff/common/types/session-state' import { assistantMessage, - toolJsonContent, + jsonToolResult, } from '@codebuff/common/util/messages' import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test' @@ -342,12 +342,10 @@ export function getMessagesSubset(messages: Message[], otherTokens: number) { role: 'tool', toolName: 'read_files', toolCallId: 'test-id', - content: [ - toolJsonContent({ - path: 'src/util/messages.ts', - content: initialContent, - }), - ], + content: jsonToolResult({ + path: 'src/util/messages.ts', + content: initialContent, + }), }, ) @@ -415,12 +413,10 @@ export function getMessagesSubset(messages: Message[], otherTokens: number) { role: 'tool', toolName: 'read_files', toolCallId: 'test-id', - content: [ - toolJsonContent({ - path: 'packages/backend/src/index.ts', - content: initialContent, - }), - ], + content: jsonToolResult({ + path: 'packages/backend/src/index.ts', + content: initialContent, + }), }, ) diff --git a/backend/src/websockets/server.ts b/backend/src/websockets/server.ts index 267e874162..094206c374 100644 --- a/backend/src/websockets/server.ts +++ b/backend/src/websockets/server.ts @@ -1,5 +1,4 @@ import { setSessionConnected } from '@codebuff/agent-runtime/live-user-inputs' -import { CLIENT_MESSAGE_SCHEMA } from '@codebuff/common/websockets/websocket-schema' import { isError } from 'lodash' import { WebSocketServer } from 'ws' @@ -50,7 +49,7 @@ async function processMessage(params: { } try { - const msg = CLIENT_MESSAGE_SCHEMA.parse(messageObj) + const msg = messageObj const { type, txid } = msg switch (type) { case 'subscribe': { diff --git a/backend/src/websockets/websocket-action.ts b/backend/src/websockets/websocket-action.ts index 8b21c959f6..68076e30d1 100644 --- a/backend/src/websockets/websocket-action.ts +++ b/backend/src/websockets/websocket-action.ts @@ -2,12 +2,13 @@ import { cancelUserInput, startUserInput, } from '@codebuff/agent-runtime/live-user-inputs' +import { callMainPrompt } from '@codebuff/agent-runtime/main-prompt' import { calculateUsageAndBalance } from '@codebuff/billing' import { trackEvent } from '@codebuff/common/analytics' import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events' +import { getErrorObject } from '@codebuff/common/util/error' import db from '@codebuff/internal/db/index' import * as schema from '@codebuff/internal/db/schema' -import { getErrorObject } from '@codebuff/common/util/error' import { eq } from 'drizzle-orm' import { protec } from './middleware' @@ -22,7 +23,6 @@ import type { Logger } from '@codebuff/common/types/contracts/logger' import type { ParamsExcluding } from '@codebuff/common/types/function-params' import type { ClientMessage } from '@codebuff/common/websockets/websocket-schema' import type { WebSocket } from 'ws' -import { callMainPrompt } from '@codebuff/agent-runtime/main-prompt' /** * Generates a usage response object for the client @@ -330,9 +330,12 @@ export const onWebsocketAction = async (params: { } // Register action handlers -subscribeToAction('prompt', protec.run({ baseAction: onPrompt })) -subscribeToAction('init', protec.run({ baseAction: onInit, silent: true })) +subscribeToAction('prompt', protec.run<'prompt'>({ baseAction: onPrompt })) +subscribeToAction( + 'init', + protec.run<'init'>({ baseAction: onInit, silent: true }), +) subscribeToAction( 'cancel-user-input', - protec.run({ baseAction: onCancelUserInput }), + protec.run<'cancel-user-input'>({ baseAction: onCancelUserInput }), ) diff --git a/common/src/actions.ts b/common/src/actions.ts index 804cb39ef7..232c9ec96e 100644 --- a/common/src/actions.ts +++ b/common/src/actions.ts @@ -1,21 +1,17 @@ import { z } from 'zod/v4' -import { costModes } from './old-constants' -import { GrantTypeValues } from './types/grant' -import { mcpConfigSchema } from './types/mcp' -import { toolMessageSchema } from './types/messages/codebuff-message' -import { - toolResultOutputSchema, - textPartSchema, - imagePartSchema, +import type { CostMode } from './old-constants' +import type { GrantType } from './types/grant' +import type { MCPConfig } from './types/mcp' +import type { ToolMessage } from './types/messages/codebuff-message' +import type { + TextPart, + ImagePart, + ToolResultOutput, } from './types/messages/content-part' -import { printModeEventSchema } from './types/print-mode' -import { - AgentOutputSchema, - SessionStateSchema, - toolCallSchema, -} from './types/session-state' -import { ProjectFileContextSchema } from './util/file' +import type { PrintModeEvent } from './types/print-mode' +import type { AgentOutput, SessionState, ToolCall } from './types/session-state' +import type { ProjectFileContext } from './util/file' export const FileChangeSchema = z.object({ type: z.enum(['patch', 'file']), @@ -26,178 +22,194 @@ export type FileChange = z.infer export const CHANGES = z.array(FileChangeSchema) export type FileChanges = z.infer -export const CLIENT_ACTION_SCHEMA = z.discriminatedUnion('type', [ - z.object({ - type: z.literal('prompt'), - promptId: z.string(), - prompt: z.string().or(z.undefined()), - content: z.array(z.union([textPartSchema, imagePartSchema])).optional(), - promptParams: z.record(z.string(), z.any()).optional(), // Additional json params. - fingerprintId: z.string(), - authToken: z.string().optional(), - costMode: z.enum(costModes).optional().default('normal'), - sessionState: SessionStateSchema, - toolResults: z.array(toolMessageSchema), - model: z.string().optional(), - repoUrl: z.string().optional(), - agentId: z.string().optional(), - }), - z.object({ - type: z.literal('read-files-response'), - files: z.record(z.string(), z.union([z.string(), z.null()])), - requestId: z.string().optional(), - }), - z.object({ - type: z.literal('init'), - fingerprintId: z.string(), - authToken: z.string().optional(), - fileContext: ProjectFileContextSchema, - repoUrl: z.string().optional(), - }), - z.object({ - type: z.literal('tool-call-response'), - requestId: z.string(), - output: toolResultOutputSchema.array(), - }), - z.object({ - type: z.literal('cancel-user-input'), - authToken: z.string(), - promptId: z.string(), - }), - z.object({ - type: z.literal('mcp-tool-data'), - requestId: z.string(), - tools: z - .object({ - name: z.string(), - description: z.string().optional(), - inputSchema: z.looseObject({ - type: z.literal('object'), - }), - }) - .array(), - }), -]) - -type ClientActionAny = z.infer -export type ClientAction< - T extends ClientActionAny['type'] = ClientActionAny['type'], -> = Extract - -export const UsageReponseSchema = z.object({ - type: z.literal('usage-response'), - usage: z.number(), - remainingBalance: z.number(), - balanceBreakdown: z - .record( - z.enum([GrantTypeValues[0], ...GrantTypeValues.slice(1)]), - z.number(), - ) - .optional(), - next_quota_reset: z.coerce.date().nullable(), - autoTopupAdded: z.number().optional(), -}) -export type UsageResponse = z.infer - -export const InitResponseSchema = z - .object({ - type: z.literal('init-response'), - message: z.string().optional(), - agentNames: z.record(z.string(), z.string()).optional(), - }) - .merge( - UsageReponseSchema.omit({ - type: true, - }), - ) -export type InitResponse = z.infer - -export const MessageCostResponseSchema = z.object({ - type: z.literal('message-cost-response'), - promptId: z.string(), - credits: z.number(), - agentId: z.string().optional(), -}) -export type MessageCostResponse = z.infer - -export const PromptResponseSchema = z.object({ - type: z.literal('prompt-response'), - promptId: z.string(), - sessionState: SessionStateSchema, - toolCalls: z.array(toolCallSchema).optional(), - toolResults: z.array(toolMessageSchema).optional(), - output: AgentOutputSchema.optional(), -}) -export type PromptResponse = z.infer - -export const SERVER_ACTION_SCHEMA = z.discriminatedUnion('type', [ - z.object({ - type: z.literal('response-chunk'), - userInputId: z.string(), - chunk: z.union([z.string(), printModeEventSchema]), - }), - z.object({ - type: z.literal('subagent-response-chunk'), - userInputId: z.string(), - agentId: z.string(), - agentType: z.string(), - chunk: z.string(), - prompt: z.string().optional(), - forwardToPrompt: z.boolean().optional(), - }), - z.object({ - type: z.literal('handlesteps-log-chunk'), - userInputId: z.string(), - agentId: z.string(), - level: z.enum(['debug', 'info', 'warn', 'error']), - data: z.any(), - message: z.string().optional(), - }), - PromptResponseSchema, - z.object({ - type: z.literal('read-files'), - filePaths: z.array(z.string()), - requestId: z.string(), - }), - z.object({ - type: z.literal('tool-call-request'), - userInputId: z.string(), - requestId: z.string(), - toolName: z.string(), - input: z.record(z.string(), z.any()), - timeout: z.number().optional(), - mcpConfig: mcpConfigSchema.optional(), - }), - InitResponseSchema, - UsageReponseSchema, - MessageCostResponseSchema, - - z.object({ - type: z.literal('action-error'), - message: z.string(), - error: z.string().optional(), - remainingBalance: z.number().optional(), - }), - z.object({ - type: z.literal('prompt-error'), - userInputId: z.string(), - message: z.string(), - error: z.string().optional(), - remainingBalance: z.number().optional(), - }), - z.object({ - // The server is imminently going to shutdown, and the client should reconnect - type: z.literal('request-reconnect'), - }), - z.object({ - type: z.literal('request-mcp-tool-data'), - requestId: z.string(), - mcpConfig: mcpConfigSchema, - toolNames: z.string().array().optional(), - }), -]) - -type ServerActionAny = z.infer -export type ServerAction< - T extends ServerActionAny['type'] = ServerActionAny['type'], -> = Extract +type ClientActionPrompt = { + type: 'prompt' + promptId: string + prompt: string | undefined + content?: (TextPart | ImagePart)[] + promptParams?: Record // Additional json params. + fingerprintId: string + authToken?: string + costMode?: CostMode + sessionState: SessionState + toolResults: ToolMessage[] + model?: string + repoUrl?: string + agentId?: string +} + +type ClientActionReadFilesResponse = { + type: 'read-files-response' + files: Record + requestId?: string +} + +type ClientActionInit = { + type: 'init' + fingerprintId: string + authToken?: string + fileContext: ProjectFileContext + repoUrl?: string +} + +type ClientActionToolCallResponse = { + type: 'tool-call-response' + requestId: string + output: ToolResultOutput[] +} + +type ClientActionCancelUserInput = { + type: 'cancel-user-input' + authToken: string + promptId: string +} + +type ClientActionMcpToolData = { + type: 'mcp-tool-data' + requestId: string + tools: { + name: string + description?: string + inputSchema: { type: 'object'; [k: string]: unknown } + }[] +} + +type ClientActionAny = + | ClientActionPrompt + | ClientActionReadFilesResponse + | ClientActionInit + | ClientActionToolCallResponse + | ClientActionCancelUserInput + | ClientActionMcpToolData +type ClientActionType = ClientActionAny['type'] +export type ClientAction = { + [K in ClientActionType]: Extract< + ClientActionAny, + { + type: K + } + > +}[T] + +type ServerActionResponseChunk = { + type: 'response-chunk' + userInputId: string + chunk: string | PrintModeEvent +} + +type ServerActionSubagentResponseChunk = { + type: 'subagent-response-chunk' + userInputId: string + agentId: string + agentType: string + chunk: string + prompt?: string + forwardToPrompt?: boolean +} + +type ServerActionHandleStepsLogChunk = { + type: 'handlesteps-log-chunk' + userInputId: string + agentId: string + level: 'debug' | 'info' | 'warn' | 'error' + data: any + message?: string +} + +export type PromptResponse = { + type: 'prompt-response' + promptId: string + sessionState: SessionState + toolCalls?: ToolCall[] + toolResults?: ToolMessage[] + output?: AgentOutput +} + +type ServerActionReadFiles = { + type: 'read-files' + filePaths: string[] + requestId: string +} + +type ServerActionToolCallRequest = { + type: 'tool-call-request' + userInputId: string + requestId: string + toolName: string + input: Record + timeout?: number + mcpConfig?: MCPConfig +} + +export type InitResponse = { + type: 'init-response' + message?: string + agentNames?: Record +} & Omit + +export type UsageResponse = { + type: 'usage-response' + usage: number + remainingBalance: number + balanceBreakdown?: Record + next_quota_reset: Date | null + autoTopupAdded?: number +} + +export type MessageCostResponse = { + type: 'message-cost-response' + promptId: string + credits: number + agentId?: string +} + +type ServerActionActionError = { + type: 'action-error' + message: string + error?: string + remainingBalance?: number +} + +type ServerActionPromptError = { + type: 'prompt-error' + userInputId: string + message: string + error?: string + remainingBalance?: number +} + +type ServerActionRequestReconnect = { + // The server is imminently going to shutdown, and the client should reconnect + type: 'request-reconnect' +} + +type ServerActionRequestMcpToolData = { + type: 'request-mcp-tool-data' + requestId: string + mcpConfig: MCPConfig + toolNames?: string[] +} + +type ServerActionAny = + | ServerActionResponseChunk + | ServerActionSubagentResponseChunk + | ServerActionHandleStepsLogChunk + | PromptResponse + | ServerActionReadFiles + | ServerActionToolCallRequest + | InitResponse + | UsageResponse + | MessageCostResponse + | ServerActionActionError + | ServerActionPromptError + | ServerActionRequestReconnect + | ServerActionRequestMcpToolData +type ServerActionType = ServerActionAny['type'] +export type ServerAction = { + [K in ServerActionType]: Extract< + ServerActionAny, + { + type: K + } + > +}[T] diff --git a/common/src/tools/compile-tool-definitions.ts b/common/src/tools/compile-tool-definitions.ts index 33b304533c..a2dc2c372e 100644 --- a/common/src/tools/compile-tool-definitions.ts +++ b/common/src/tools/compile-tool-definitions.ts @@ -1,7 +1,7 @@ import z from 'zod/v4' import { publishedTools } from './constants' -import { $toolParams } from './list' +import { toolParams } from './list' /** * Compiles all tool definitions into a single TypeScript definition file content. @@ -9,12 +9,12 @@ import { $toolParams } from './list' */ export function compileToolDefinitions(): string { const toolEntries = publishedTools.map( - (toolName) => [toolName, $toolParams[toolName]] as const, + (toolName) => [toolName, toolParams[toolName]] as const, ) const toolInterfaces = toolEntries .map(([toolName, toolDef]) => { - const parameterSchema = toolDef.parameters + const parameterSchema = toolDef.inputSchema // Convert Zod schema to TypeScript interface using JSON schema let typeDefinition: string diff --git a/common/src/tools/constants.ts b/common/src/tools/constants.ts index 89f341c145..4a3f941979 100644 --- a/common/src/tools/constants.ts +++ b/common/src/tools/constants.ts @@ -1,5 +1,5 @@ import type { ToolResultOutput } from '../types/messages/content-part' -import type z from 'zod/v4' +import type { Tool } from 'ai' export const toolNameParam = 'cb_tool_name' export const endsAgentStepParam = 'cb_easp' @@ -75,14 +75,13 @@ export const publishedTools = [ export type ToolName = (typeof toolNames)[number] export type PublishedToolName = (typeof publishedTools)[number] -export type $ToolParams = { +/** Only used for validating tool definitions */ +export type $ToolParams = Required< + Pick< + Tool, + 'description' | 'inputSchema' | 'outputSchema' + > +> & { toolName: T endsAgentStep: boolean - parameters: z.ZodType - outputs: z.ZodType -} - -export type $ToolResults = { - toolName: string - outputs: $ToolParams['outputs'] } diff --git a/common/src/tools/list.ts b/common/src/tools/list.ts index e56e993f46..65def150bb 100644 --- a/common/src/tools/list.ts +++ b/common/src/tools/list.ts @@ -28,16 +28,11 @@ import { webSearchParams } from './params/tool/web-search' import { writeFileParams } from './params/tool/write-file' import { writeTodosParams } from './params/tool/write-todos' -import type { - $ToolParams, - $ToolResults, - PublishedToolName, - ToolName, -} from './constants' +import type { $ToolParams, PublishedToolName, ToolName } from './constants' import type { ToolMessage } from '../types/messages/codebuff-message' import type { ToolCallPart } from '../types/messages/content-part' -export const $toolParams = { +export const toolParams = { add_message: addMessageParams, add_subgoal: addSubgoalParams, browser_logs: browserLogsParams, @@ -68,43 +63,33 @@ export const $toolParams = { [K in ToolName]: $ToolParams } -export const additionalToolResultSchemas = { - // None for now! -} satisfies Record -type ResultOnlyToolName = keyof typeof additionalToolResultSchemas - -// Tool call from LLM +// Tool call from LLM after parsing export type CodebuffToolCall = { [K in ToolName]: { toolName: K - input: z.infer<(typeof $toolParams)[K]['parameters']> + input: z.infer<(typeof toolParams)[K]['inputSchema']> } & Omit }[T] -export type CodebuffToolOutput< - T extends ToolName | ResultOnlyToolName = ToolName, -> = { - [K in ToolName | ResultOnlyToolName]: K extends ToolName - ? z.infer<(typeof $toolParams)[K]['outputs']> - : K extends ResultOnlyToolName - ? z.infer<(typeof additionalToolResultSchemas)[K]['outputs']> - : never +export type CodebuffToolOutput = { + [K in ToolName]: K extends ToolName + ? z.infer<(typeof toolParams)[K]['outputSchema']> + : never }[T] -export type CodebuffToolMessage< - T extends ToolName | ResultOnlyToolName = ToolName, -> = ToolMessage & { content: CodebuffToolOutput } +export type CodebuffToolMessage = ToolMessage & { + content: CodebuffToolOutput +} // Tool call to send to client -export type ClientToolName = (typeof clientToolNames)[number] export const clientToolCallSchema = z.discriminatedUnion('toolName', [ z.object({ toolName: z.literal('browser_logs'), - input: $toolParams.browser_logs.parameters, + input: toolParams.browser_logs.inputSchema, }), z.object({ toolName: z.literal('code_search'), - input: $toolParams.code_search.parameters, + input: toolParams.code_search.inputSchema, }), z.object({ toolName: z.literal('create_plan'), @@ -112,19 +97,19 @@ export const clientToolCallSchema = z.discriminatedUnion('toolName', [ }), z.object({ toolName: z.literal('glob'), - input: $toolParams.glob.parameters, + input: toolParams.glob.inputSchema, }), z.object({ toolName: z.literal('list_directory'), - input: $toolParams.list_directory.parameters, + input: toolParams.list_directory.inputSchema, }), z.object({ toolName: z.literal('run_file_change_hooks'), - input: $toolParams.run_file_change_hooks.parameters, + input: toolParams.run_file_change_hooks.inputSchema, }), z.object({ toolName: z.literal('run_terminal_command'), - input: $toolParams.run_terminal_command.parameters.and( + input: toolParams.run_terminal_command.inputSchema.and( z.object({ mode: z.enum(['assistant', 'user']) }), ), }), @@ -140,9 +125,12 @@ export const clientToolCallSchema = z.discriminatedUnion('toolName', [ export const clientToolNames = clientToolCallSchema.def.options.map( (opt) => opt.shape.toolName.value, ) satisfies ToolName[] +export type ClientToolName = (typeof clientToolNames)[number] -export type ClientToolCall = z.infer< - typeof clientToolCallSchema -> & { toolName: T } & Omit +export type ClientToolCall = Extract< + z.infer, + { toolName: T } +> & + Pick export type PublishedClientToolName = ClientToolName & PublishedToolName diff --git a/common/src/tools/params/tool/add-message.ts b/common/src/tools/params/tool/add-message.ts index 93eae3e6df..2866cc2d3d 100644 --- a/common/src/tools/params/tool/add-message.ts +++ b/common/src/tools/params/tool/add-message.ts @@ -1,19 +1,36 @@ import z from 'zod/v4' +import { $getToolCallString, emptyToolResultSchema } from '../utils' + import type { $ToolParams } from '../../constants' const toolName = 'add_message' const endsAgentStep = true +const inputSchema = z + .object({ + role: z.enum(['user', 'assistant']), + content: z.string(), + }) + .describe( + `Add a new message to the conversation history. To be used for complex requests that can't be solved in a single step, as you may forget what happened!`, + ) +const description = ` +Example: +${$getToolCallString({ + toolName, + inputSchema, + input: { + role: 'user', + content: 'Hello, how are you?', + }, + endsAgentStep, +})} +`.trim() + export const addMessageParams = { toolName, endsAgentStep, - parameters: z - .object({ - role: z.enum(['user', 'assistant']), - content: z.string(), - }) - .describe( - `Add a new message to the conversation history. To be used for complex requests that can't be solved in a single step, as you may forget what happened!`, - ), - outputs: z.tuple([]), + description, + inputSchema, + outputSchema: emptyToolResultSchema(), } satisfies $ToolParams diff --git a/common/src/tools/params/tool/add-subgoal.ts b/common/src/tools/params/tool/add-subgoal.ts index 8379660770..ed592797b9 100644 --- a/common/src/tools/params/tool/add-subgoal.ts +++ b/common/src/tools/params/tool/add-subgoal.ts @@ -1,44 +1,57 @@ import z from 'zod/v4' +import { $getToolCallString, jsonToolResultSchema } from '../utils' + import type { $ToolParams } from '../../constants' const toolName = 'add_subgoal' const endsAgentStep = false +const inputSchema = z + .object({ + id: z + .string() + .min(1, 'Id cannot be empty') + .describe( + `A unique identifier for the subgoal. Try to choose the next sequential integer that is not already in use.`, + ), + objective: z + .string() + .min(1, 'Objective cannot be empty') + .describe(`The objective of the subgoal, concisely and clearly stated.`), + status: z + .enum(['NOT_STARTED', 'IN_PROGRESS', 'COMPLETE', 'ABORTED']) + .describe(`The status of the subgoal.`), + plan: z.string().optional().describe('A plan for the subgoal.'), + log: z + .string() + .optional() + .describe('A log message for the subgoal progress.'), + }) + .describe( + `Add a new subgoal for tracking progress. To be used for complex requests that can't be solved in a single step, as you may forget what happened!`, + ) +const description = ` +Example: +${$getToolCallString({ + toolName, + inputSchema, + input: { + id: '1', + objective: 'Add a new "deploy api" subgoal', + status: 'IN_PROGRESS', + }, + endsAgentStep, +})} +`.trim() + export const addSubgoalParams = { toolName, endsAgentStep, - parameters: z - .object({ - id: z - .string() - .min(1, 'Id cannot be empty') - .describe( - `A unique identifier for the subgoal. Try to choose the next sequential integer that is not already in use.`, - ), - objective: z - .string() - .min(1, 'Objective cannot be empty') - .describe( - `The objective of the subgoal, concisely and clearly stated.`, - ), - status: z - .enum(['NOT_STARTED', 'IN_PROGRESS', 'COMPLETE', 'ABORTED']) - .describe(`The status of the subgoal.`), - plan: z.string().optional().describe('A plan for the subgoal.'), - log: z - .string() - .optional() - .describe('A log message for the subgoal progress.'), - }) - .describe( - `Add a new subgoal for tracking progress. To be used for complex requests that can't be solved in a single step, as you may forget what happened!`, - ), - outputs: z.tuple([ + description, + inputSchema, + outputSchema: jsonToolResultSchema( z.object({ - type: z.literal('json'), - value: z.object({ - message: z.string(), - }), + message: z.string(), }), - ]), + ), } satisfies $ToolParams diff --git a/common/src/tools/params/tool/browser-logs.ts b/common/src/tools/params/tool/browser-logs.ts index c0e3f35f12..acb4d51d94 100644 --- a/common/src/tools/params/tool/browser-logs.ts +++ b/common/src/tools/params/tool/browser-logs.ts @@ -1,32 +1,85 @@ import z from 'zod/v4' import { BrowserResponseSchema } from '../../../browser-actions' +import { $getToolCallString, jsonToolResultSchema } from '../utils' import type { $ToolParams } from '../../constants' const toolName = 'browser_logs' const endsAgentStep = true +const inputSchema = z.object({ + type: z + .string() + .min(1, 'Type cannot be empty') + .describe('The type of browser action to perform (e.g., "navigate").'), + url: z + .string() + .min(1, 'URL cannot be empty') + .describe('The URL to navigate to.'), + waitUntil: z + .enum(['load', 'domcontentloaded', 'networkidle0']) + .optional() + .describe("When to consider navigation successful. Defaults to 'load'."), +}) +const description = ` +Purpose: Use this tool to check the output of console.log or errors in order to debug issues, test functionality, or verify expected behavior. + +IMPORTANT: Assume the user's development server is ALREADY running and active, unless you see logs indicating otherwise. Never start the user's development server for them, unless they ask you to do so. +Never offer to interact with the website aside from reading them (see available actions below). The user will manipulate the website themselves and bring you to the UI they want you to interact with. + +### Response Analysis + +After each action, you'll receive: +1. Success/failure status +2. New console logs since last action +3. Network requests and responses +4. JavaScript errors with stack traces + +Use this data to: +- Verify expected behavior +- Debug issues +- Guide next actions +- Make informed decisions about fixes + +### Best Practices + +**Workflow** +- Navigate to the user's website, probably on localhost, but you can compare with the production site if you want. +- Scroll to the relevant section +- Take screenshots and analyze confirm changes +- Check network requests for anomalies + +**Debugging Flow** +- Start with minimal reproduction steps +- Collect data at each step +- Analyze results before next action +- Take screenshots to track your changes after each UI change you make + +There is currently only one type of browser action available: +Navigate: + - Load a new URL in the current browser window and get the logs after page load. + Params: + - \`type\`: (required) Must be equal to 'navigate' + - \`url\`: (required) The URL to navigate to. + - \`waitUntil\`: (required) One of 'load', 'domcontentloaded', 'networkidle0' + +Example: +${$getToolCallString({ + toolName, + inputSchema, + input: { + type: 'navigate', + url: 'localhost:3000', + waitUntil: 'domcontentloaded', + }, + endsAgentStep, +})} + `.trim() + export const browserLogsParams = { toolName, endsAgentStep, - parameters: z.object({ - type: z - .string() - .min(1, 'Type cannot be empty') - .describe('The type of browser action to perform (e.g., "navigate").'), - url: z - .string() - .min(1, 'URL cannot be empty') - .describe('The URL to navigate to.'), - waitUntil: z - .enum(['load', 'domcontentloaded', 'networkidle0']) - .optional() - .describe("When to consider navigation successful. Defaults to 'load'."), - }), - outputs: z.tuple([ - z.object({ - type: z.literal('json'), - value: BrowserResponseSchema, - }), - ]), + description, + inputSchema, + outputSchema: jsonToolResultSchema(BrowserResponseSchema), } satisfies $ToolParams diff --git a/common/src/tools/params/tool/code-search.ts b/common/src/tools/params/tool/code-search.ts index a41dd3a686..876ea29349 100644 --- a/common/src/tools/params/tool/code-search.ts +++ b/common/src/tools/params/tool/code-search.ts @@ -1,57 +1,144 @@ import z from 'zod/v4' +import { $getToolCallString, jsonToolResultSchema } from '../utils' + import type { $ToolParams } from '../../constants' const toolName = 'code_search' const endsAgentStep = true +const inputSchema = z + .object({ + pattern: z + .string() + .min(1, 'Pattern cannot be empty') + .describe(`The pattern to search for.`), + flags: z + .string() + .optional() + .describe( + `Optional ripgrep flags to customize the search (e.g., "-i" for case-insensitive, "-g *.ts -g *.js" for TypeScript and JavaScript files only, "-g !*.test.ts" to exclude Typescript test files, "-A 3" for 3 lines after match, "-B 2" for 2 lines before match).`, + ), + cwd: z + .string() + .optional() + .describe( + `Optional working directory to search within, relative to the project root. Defaults to searching the entire project.`, + ), + maxResults: z + .number() + .int() + .positive() + .optional() + .default(15) + .describe( + `Maximum number of results to return per file. Defaults to 15. There is also a global limit of 250 results across all files.`, + ), + }) + .describe( + `Search for string patterns in the project's files. This tool uses ripgrep (rg), a fast line-oriented search tool. Use this tool only when read_files is not sufficient to find the files you need.`, + ) +const description = ` +Purpose: Search through code files to find files with specific text patterns, function names, variable names, and more. + +Prefer to use read_files instead of code_search unless you need to search for a specific pattern in multiple files. + +Use cases: +1. Finding all references to a function, class, or variable name across the codebase +2. Searching for specific code patterns or implementations +3. Looking up where certain strings or text appear +4. Finding files that contain specific imports or dependencies +5. Locating configuration settings or environment variables + +The pattern supports regular expressions and will search recursively through all files in the project by default. Some tips: +- Be as constraining in the pattern as possible to limit the number of files returned, e.g. if searching for the definition of a function, use "(function foo|const foo)" or "def foo" instead of merely "foo". +- Use Rust-style regex, not grep-style, PCRE, RE2 or JavaScript regex - you must always escape special characters like { and } +- Be as constraining as possible to limit results, e.g. use "(function foo|const foo)" or "def foo" instead of merely "foo" +- Add context to your search with surrounding terms (e.g., "function handleAuth" rather than just "handleAuth") +- Use word boundaries (\\b) to match whole words only +- Use the cwd parameter to narrow your search to specific directories +- For case-sensitive searches like constants (e.g., ERROR vs error), omit the "-i" flag +- Searches file content and filenames +- Automatically ignores binary files, hidden files, and files in .gitignore + + +Advanced ripgrep flags (use the flags parameter): + +- Case sensitivity: "-i" for case-insensitive search +- File type filtering: "-t ts -t js" (TypeScript and JavaScript), "-t py" (Python), etc. +- Exclude file types: "--type-not py" to exclude Python files +- Context lines: "-A 3" (3 lines after), "-B 2" (2 lines before), "-C 2" (2 lines before and after) +- Line numbers: "-n" to show line numbers +- Count matches: "-c" to count matches per file +- Only filenames: "-l" to show only filenames with matches +- Invert match: "-v" to show lines that don't match +- Word boundaries: "-w" to match whole words only +- Fixed strings: "-F" to treat pattern as literal string (not regex) + +Note: Do not use the end_turn tool after this tool! You will want to see the output of this tool before ending your turn. + +RESULT LIMITING: + +- The maxResults parameter limits the number of results shown per file (default: 15) +- There is also a global limit of 250 total results across all files +- These limits allow you to see results across multiple files without being overwhelmed by matches in a single file +- If a file has more matches than maxResults, you'll see a truncation notice indicating how many results were found +- If the global limit is reached, remaining files will be skipped + +Examples: +${$getToolCallString({ + toolName, + inputSchema, + input: { pattern: 'foo' }, + endsAgentStep, +})} +${$getToolCallString({ + toolName, + inputSchema, + input: { pattern: 'foo\\.bar = 1\\.0' }, + endsAgentStep, +})} +${$getToolCallString({ + toolName, + inputSchema, + input: { pattern: 'import.*foo', cwd: 'src' }, + endsAgentStep, +})} +${$getToolCallString({ + toolName, + inputSchema, + input: { pattern: 'function.*authenticate', flags: '-i -t ts -t js' }, + endsAgentStep, +})} +${$getToolCallString({ + toolName, + inputSchema, + input: { pattern: 'TODO', flags: '-n --type-not py' }, + endsAgentStep, +})} +${$getToolCallString({ + toolName, + inputSchema, + input: { pattern: 'getUserData', maxResults: 10 }, + endsAgentStep, +})} +`.trim() + export const codeSearchParams = { toolName, endsAgentStep, - parameters: z - .object({ - pattern: z - .string() - .min(1, 'Pattern cannot be empty') - .describe(`The pattern to search for.`), - flags: z - .string() - .optional() - .describe( - `Optional ripgrep flags to customize the search (e.g., "-i" for case-insensitive, "-g *.ts -g *.js" for TypeScript and JavaScript files only, "-g !*.test.ts" to exclude Typescript test files, "-A 3" for 3 lines after match, "-B 2" for 2 lines before match).`, - ), - cwd: z - .string() - .optional() - .describe( - `Optional working directory to search within, relative to the project root. Defaults to searching the entire project.`, - ), - maxResults: z - .number() - .int() - .positive() - .optional() - .default(15) - .describe( - `Maximum number of results to return per file. Defaults to 15. There is also a global limit of 250 results across all files.`, - ), - }) - .describe( - `Search for string patterns in the project's files. This tool uses ripgrep (rg), a fast line-oriented search tool. Use this tool only when read_files is not sufficient to find the files you need.`, - ), - outputs: z.tuple([ - z.object({ - type: z.literal('json'), - value: z.union([ - z.object({ - stdout: z.string(), - stderr: z.string().optional(), - exitCode: z.number().optional(), - message: z.string(), - }), - z.object({ - errorMessage: z.string(), - }), - ]), - }), - ]), + description, + inputSchema, + outputSchema: jsonToolResultSchema( + z.union([ + z.object({ + stdout: z.string(), + stderr: z.string().optional(), + exitCode: z.number().optional(), + message: z.string(), + }), + z.object({ + errorMessage: z.string(), + }), + ]), + ), } satisfies $ToolParams diff --git a/common/src/tools/params/tool/create-plan.ts b/common/src/tools/params/tool/create-plan.ts index eb05159beb..1aca1d6cee 100644 --- a/common/src/tools/params/tool/create-plan.ts +++ b/common/src/tools/params/tool/create-plan.ts @@ -1,32 +1,80 @@ import z from 'zod/v4' import { updateFileResultSchema } from './str-replace' +import { $getToolCallString, jsonToolResultSchema } from '../utils' import type { $ToolParams } from '../../constants' const toolName = 'create_plan' const endsAgentStep = false +const inputSchema = z + .object({ + path: z + .string() + .min(1, 'Path cannot be empty') + .describe( + `The path including the filename of a markdown file that will be overwritten with the plan.`, + ), + plan: z + .string() + .min(1, 'Plan cannot be empty') + .describe(`A detailed plan to solve the user's request.`), + }) + .describe(`Generate a detailed markdown plan for complex tasks.`) +const description = ` +Use when: +- User explicitly requests a detailed plan. +- Use this tool to overwrite a previous plan by using the exact same file name. + +Don't include: +- Goals, timelines, benefits, next steps. +- Background context or extensive explanations. + +For a technical plan, act as an expert architect engineer and provide direction to your editor engineer. +- Study the change request and the current code. +- Describe how to modify the code to complete the request. The editor engineer will rely solely on your instructions, so make them unambiguous and complete. +- Explain all needed code changes clearly and completely, but concisely. +- Just show the changes needed. + +What to include in the plan: +- Include key snippets of code -- not full files of it. Use pseudo code. For example, include interfaces between modules, function signatures, and other code that is not immediately obvious should be written out explicitly. Function and method bodies could be written out in psuedo code. +- Do not waste time on much background information, focus on the exact steps of the implementation. +- Do not wrap the path content in markdown code blocks, e.g. \`\`\`. + +Do not include any of the following sections in the plan: +- goals +- a timeline or schedule +- benefits/key improvements +- next steps + +After creating the plan, you should end turn to let the user review the plan. + +Important: Use this tool sparingly. Do not use this tool more than once in a conversation, unless in ask mode. + +Examples: +${$getToolCallString({ + toolName, + inputSchema, + input: { + path: 'feature-x-plan.md', + plan: [ + '1. Create module `auth.ts` in `/src/auth/`.', + '```ts', + 'export function authenticate(user: User): boolean { /* pseudo-code logic */ }', + '```', + '2. Refactor existing auth logic into this module.', + '3. Update imports across codebase.', + '4. Write integration tests covering new module logic.', + ].join('\n'), + }, + endsAgentStep, +})} +`.trim() + export const createPlanParams = { toolName, endsAgentStep, - parameters: z - .object({ - path: z - .string() - .min(1, 'Path cannot be empty') - .describe( - `The path including the filename of a markdown file that will be overwritten with the plan.`, - ), - plan: z - .string() - .min(1, 'Plan cannot be empty') - .describe(`A detailed plan to solve the user's request.`), - }) - .describe(`Generate a detailed markdown plan for complex tasks.`), - outputs: z.tuple([ - z.object({ - type: z.literal('json'), - value: updateFileResultSchema, - }), - ]), + description, + inputSchema, + outputSchema: jsonToolResultSchema(updateFileResultSchema), } satisfies $ToolParams diff --git a/common/src/tools/params/tool/end-turn.ts b/common/src/tools/params/tool/end-turn.ts index fc13c3295a..16d21a6720 100644 --- a/common/src/tools/params/tool/end-turn.ts +++ b/common/src/tools/params/tool/end-turn.ts @@ -1,16 +1,54 @@ import z from 'zod/v4' +import { $getToolCallString, emptyToolResultSchema } from '../utils' + import type { $ToolParams } from '../../constants' const toolName = 'end_turn' const endsAgentStep = true +const inputSchema = z + .object({}) + .describe( + `End your turn, regardless of any new tool results that might be coming. This will allow the user to type another prompt.`, + ) +const description = ` +Only use this tool to hand control back to the user. + +- When to use: after you have completed a meaningful chunk of work and you are either (a) fully done, or (b) explicitly waiting for the user's next message. +- Do NOT use: as a stop token mid-work, to pause between tool calls, to wait for tool results, or to "check in" unnecessarily. +- Before calling: finish all pending steps, resolve tool results, and include any outputs the user needs to review. +- Effect: Signals the UI to wait for the user's reply; any pending tool results will be ignored. + +*INCORRECT USAGE*: +${$getToolCallString({ + toolName: 'some_tool_that_produces_results', + inputSchema: null, + input: { query: 'some example search term' }, + endsAgentStep: false, +})} + +${$getToolCallString({ + toolName, + inputSchema, + input: {}, + endsAgentStep: true, +})} + +*CORRECT USAGE*: +All done! Would you like some more help with xyz? + +${$getToolCallString({ + toolName, + inputSchema, + input: {}, + endsAgentStep: false, +})} +`.trim() + export const endTurnParams = { toolName, endsAgentStep, - parameters: z - .object({}) - .describe( - `End your turn, regardless of any new tool results that might be coming. This will allow the user to type another prompt.`, - ), - outputs: z.tuple([]), + description, + inputSchema, + outputSchema: emptyToolResultSchema(), } satisfies $ToolParams diff --git a/common/src/tools/params/tool/find-files.ts b/common/src/tools/params/tool/find-files.ts index 0b091f98bc..4b46e15ec2 100644 --- a/common/src/tools/params/tool/find-files.ts +++ b/common/src/tools/params/tool/find-files.ts @@ -1,35 +1,60 @@ import z from 'zod/v4' import { fileContentsSchema } from './read-files' +import { $getToolCallString, jsonToolResultSchema } from '../utils' import type { $ToolParams } from '../../constants' const toolName = 'find_files' const endsAgentStep = true +const inputSchema = z + .object({ + prompt: z + .string() + .min(1, 'Prompt cannot be empty') + .describe( + `A brief natural language description of the files or the name of a function or class you are looking for. It's also helpful to mention a directory or two to look within.`, + ), + }) + .describe( + `Find several files related to a brief natural language description of the files or the name of a function or class you are looking for.`, + ) +const description = ` +Example: +${$getToolCallString({ + toolName, + inputSchema, + input: { + prompt: 'The implementation of function foo', + }, + endsAgentStep, +})} + +Purpose: Better fulfill the user request by reading files which could contain information relevant to the user's request. +Use cases: +- If you are calling a function or creating a class and want to know how it works, use this tool to get the implementation. +- If you need to understand a section of the codebase, read more files in that directory or subdirectories. +- Some requests require a broad understanding of multiple parts of the codebase. Consider using find_files to gain more context before making changes. + +Don't use this tool if: +- You already know the exact path of the file(s) you are looking for — in this case, use read_files. +- You already read the files you need in context. +- You know the name of the file you need. Instead use run_terminal_command with \`find -name\` (or \`dir /s /b\` or \`Get-ChildItem -Recurse -Filter\`) + +This tool is not guaranteed to find the correct file. In general, prefer using read_files instead of find_files. +`.trim() + export const findFilesParams = { toolName, endsAgentStep, - parameters: z - .object({ - prompt: z - .string() - .min(1, 'Prompt cannot be empty') - .describe( - `A brief natural language description of the files or the name of a function or class you are looking for. It's also helpful to mention a directory or two to look within.`, - ), - }) - .describe( - `Find several files related to a brief natural language description of the files or the name of a function or class you are looking for.`, - ), - outputs: z.tuple([ - z.object({ - type: z.literal('json'), - value: z.union([ - fileContentsSchema.array(), - z.object({ - message: z.string(), - }), - ]), - }), - ]), + description, + inputSchema, + outputSchema: jsonToolResultSchema( + z.union([ + fileContentsSchema.array(), + z.object({ + message: z.string(), + }), + ]), + ), } satisfies $ToolParams diff --git a/common/src/tools/params/tool/glob.ts b/common/src/tools/params/tool/glob.ts index cd342a9109..e98dc67986 100644 --- a/common/src/tools/params/tool/glob.ts +++ b/common/src/tools/params/tool/glob.ts @@ -1,45 +1,74 @@ import z from 'zod/v4' +import { $getToolCallString, jsonToolResultSchema } from '../utils' + import type { $ToolParams } from '../../constants' const toolName = 'glob' const endsAgentStep = false +const inputSchema = z + .object({ + pattern: z + .string() + .min(1, 'Pattern cannot be empty') + .describe( + 'Glob pattern to match files against (e.g., *.js, src/glob/*.ts, glob/test/glob/*.go).', + ), + cwd: z + .string() + .optional() + .describe( + 'Optional working directory to search within, relative to project root. If not provided, searches from project root.', + ), + }) + .describe( + `Search for files matching a glob pattern. Returns matching file paths sorted by modification time.`, + ) +const description = ` +Example: +${$getToolCallString({ + toolName, + inputSchema, + input: { + pattern: '**/*.test.ts', + }, + endsAgentStep, +})} + +Purpose: Search for files matching a glob pattern to discover files by name patterns rather than content. +Use cases: +- Find all files with a specific extension (e.g., "*.js", "*.test.ts") +- Locate files in specific directories (e.g., "src/**/*.ts") +- Find files with specific naming patterns (e.g., "**/test_*.go", "**/*-config.json") +- Discover test files, configuration files, or other files with predictable naming + +Glob patterns support: +- * matches any characters except / +- ** matches any characters including / +- ? matches a single character +- [abc] matches one of the characters in brackets +- {a,b} matches one of the comma-separated patterns + +This tool is fast and works well for discovering files by name patterns. +`.trim() + export const globParams = { toolName, + description, endsAgentStep, - parameters: z - .object({ - pattern: z - .string() - .min(1, 'Pattern cannot be empty') - .describe( - 'Glob pattern to match files against (e.g., *.js, src/glob/*.ts, glob/test/glob/*.go).', - ), - cwd: z - .string() - .optional() - .describe( - 'Optional working directory to search within, relative to project root. If not provided, searches from project root.', - ), - }) - .describe( - `Search for files matching a glob pattern. Returns matching file paths sorted by modification time.`, - ), - outputs: z.tuple([ - z.object({ - type: z.literal('json'), - value: z.union([ - z.object({ - files: z.array(z.string()).describe('Array of matching file paths'), - count: z - .number() - .describe('Total number of files matching the pattern'), - message: z.string().describe('Success message'), - }), - z.object({ - errorMessage: z.string().describe('Error message if search failed'), - }), - ]), - }), - ]), + inputSchema, + outputSchema: jsonToolResultSchema( + z.union([ + z.object({ + files: z.array(z.string()).describe('Array of matching file paths'), + count: z + .number() + .describe('Total number of files matching the pattern'), + message: z.string().describe('Success message'), + }), + z.object({ + errorMessage: z.string().describe('Error message if search failed'), + }), + ]), + ), } satisfies $ToolParams diff --git a/common/src/tools/params/tool/list-directory.ts b/common/src/tools/params/tool/list-directory.ts index 854d13b410..4031799818 100644 --- a/common/src/tools/params/tool/list-directory.ts +++ b/common/src/tools/params/tool/list-directory.ts @@ -1,34 +1,58 @@ import z from 'zod/v4' +import { $getToolCallString, jsonToolResultSchema } from '../utils' + import type { $ToolParams } from '../../constants' const toolName = 'list_directory' const endsAgentStep = true +const inputSchema = z + .object({ + path: z + .string() + .describe('Directory path to list, relative to the project root.'), + }) + .describe( + 'List files and directories in the specified path. Returns separate arrays of file names and directory names.', + ) +const description = ` +Lists all files and directories in the specified path. Useful for exploring directory structure and finding files. + +Example: +${$getToolCallString({ + toolName, + inputSchema, + input: { + path: 'src/components', + }, + endsAgentStep, +})} + +${$getToolCallString({ + toolName, + inputSchema, + input: { + path: '.', + }, + endsAgentStep, +})} + `.trim() + export const listDirectoryParams = { toolName, endsAgentStep, - parameters: z - .object({ - path: z - .string() - .describe('Directory path to list, relative to the project root.'), - }) - .describe( - 'List files and directories in the specified path. Returns separate arrays of file names and directory names.', - ), - outputs: z.tuple([ - z.object({ - type: z.literal('json'), - value: z.union([ - z.object({ - files: z.array(z.string()).describe('Array of file names'), - directories: z.array(z.string()).describe('Array of directory names'), - path: z.string().describe('The directory path that was listed'), - }), - z.object({ - errorMessage: z.string(), - }), - ]), - }), - ]), + description, + inputSchema, + outputSchema: jsonToolResultSchema( + z.union([ + z.object({ + files: z.array(z.string()).describe('Array of file names'), + directories: z.array(z.string()).describe('Array of directory names'), + path: z.string().describe('The directory path that was listed'), + }), + z.object({ + errorMessage: z.string(), + }), + ]), + ), } satisfies $ToolParams diff --git a/common/src/tools/params/tool/lookup-agent-info.ts b/common/src/tools/params/tool/lookup-agent-info.ts index ae0d965e7e..4f1ee5cc5a 100644 --- a/common/src/tools/params/tool/lookup-agent-info.ts +++ b/common/src/tools/params/tool/lookup-agent-info.ts @@ -1,24 +1,37 @@ import z from 'zod/v4' import { jsonValueSchema } from '../../../types/json' +import { $getToolCallString, jsonToolResultSchema } from '../utils' + import type { $ToolParams } from '../../constants' const toolName = 'lookup_agent_info' const endsAgentStep = false +const inputSchema = z + .object({ + agentId: z + .string() + .describe('Agent ID (short local or full published format)'), + }) + .describe('Retrieve information about an agent by ID') +const description = ` +Retrieve information about an agent by ID for proper spawning. Use this when you see a request with a full agent ID like "@publisher/agent-id@version" to validate the agent exists and get its metadata. Only agents that are published under a publisher and version are supported for this tool. + +Example: +${$getToolCallString({ + toolName, + inputSchema, + input: { + agentId: 'codebuff/researcher@0.0.1', + }, + endsAgentStep, +})} +`.trim() + export const lookupAgentInfoParams = { toolName, endsAgentStep, - parameters: z - .object({ - agentId: z - .string() - .describe('Agent ID (short local or full published format)'), - }) - .describe('Retrieve information about an agent by ID'), - outputs: z.tuple([ - z.object({ - type: z.literal('json'), - value: jsonValueSchema, - }), - ]), + description, + inputSchema, + outputSchema: jsonToolResultSchema(jsonValueSchema), } satisfies $ToolParams diff --git a/common/src/tools/params/tool/read-docs.ts b/common/src/tools/params/tool/read-docs.ts index 4979105d07..235c3faee7 100644 --- a/common/src/tools/params/tool/read-docs.ts +++ b/common/src/tools/params/tool/read-docs.ts @@ -1,42 +1,85 @@ import z from 'zod/v4' +import { $getToolCallString, jsonToolResultSchema } from '../utils' + import type { $ToolParams } from '../../constants' const toolName = 'read_docs' const endsAgentStep = true +const inputSchema = z + .object({ + libraryTitle: z + .string() + .min(1, 'Library title cannot be empty') + .describe( + `The library or framework name (e.g., "Next.js", "MongoDB", "React"). Use the official name as it appears in documentation if possible. Only public libraries available in Context7's database are supported, so small or private libraries may not be available.`, + ), + topic: z + .string() + .describe( + `Specific topic to focus on (e.g., "routing", "hooks", "authentication")`, + ), + max_tokens: z + .number() + .default(10_000) + .optional() + .describe( + `Optional maximum number of tokens to return. Defaults to 20000. Values less than 10000 are automatically increased to 10000.`, + ), + }) + .describe( + `Fetch up-to-date documentation for libraries and frameworks using Context7 API.`, + ) +const description = ` +Purpose: Get current, accurate documentation for popular libraries, frameworks, and technologies. This tool searches Context7's database of up-to-date documentation and returns relevant content. + +**IMPORTANT**: The \`libraryTitle\` parameter should be the exact, official name of the library or framework, not a search query. Think of it as looking up a specific book title in a library catalog. + +Correct examples: +- "Next.js" (not "nextjs tutorial" or "how to use nextjs") +- "React" (not "react hooks guide") +- "MongoDB" (not "mongodb database setup") +- "Express.js" (not "express server") + +Use cases: +- Getting current API documentation for a library +- Finding usage examples and best practices +- Understanding how to implement specific features +- Checking the latest syntax and methods + +The tool will search for the library and return the most relevant documentation content. If a topic is specified, it will focus the results on that specific area. + +Example: +${$getToolCallString({ + toolName, + inputSchema, + input: { + libraryTitle: 'Next.js', + topic: 'app router', + max_tokens: 15000, + }, + endsAgentStep, +})} + +${$getToolCallString({ + toolName, + inputSchema, + input: { + libraryTitle: 'MongoDB', + topic: 'database setup', + }, + endsAgentStep, +})} +`.trim() + export const readDocsParams = { toolName, endsAgentStep, - parameters: z - .object({ - libraryTitle: z - .string() - .min(1, 'Library title cannot be empty') - .describe( - `The library or framework name (e.g., "Next.js", "MongoDB", "React"). Use the official name as it appears in documentation if possible. Only public libraries available in Context7's database are supported, so small or private libraries may not be available.`, - ), - topic: z - .string() - .describe( - `Specific topic to focus on (e.g., "routing", "hooks", "authentication")`, - ), - max_tokens: z - .number() - .default(10_000) - .optional() - .describe( - `Optional maximum number of tokens to return. Defaults to 20000. Values less than 10000 are automatically increased to 10000.`, - ), - }) - .describe( - `Fetch up-to-date documentation for libraries and frameworks using Context7 API.`, - ), - outputs: z.tuple([ + description, + inputSchema, + outputSchema: jsonToolResultSchema( z.object({ - type: z.literal('json'), - value: z.object({ - documentation: z.string(), - }), + documentation: z.string(), }), - ]), + ), } satisfies $ToolParams diff --git a/common/src/tools/params/tool/read-files.ts b/common/src/tools/params/tool/read-files.ts index edf17f61b2..948f44af2f 100644 --- a/common/src/tools/params/tool/read-files.ts +++ b/common/src/tools/params/tool/read-files.ts @@ -1,5 +1,7 @@ import z from 'zod/v4' +import { $getToolCallString, jsonToolResultSchema } from '../utils' + import type { $ToolParams } from '../../constants' export const fileContentsSchema = z.union([ @@ -16,29 +18,39 @@ export const fileContentsSchema = z.union([ const toolName = 'read_files' const endsAgentStep = true +const inputSchema = z + .object({ + paths: z + .array( + z + .string() + .min(1, 'Paths cannot be empty') + .describe( + `File path to read relative to the **project root**. Absolute file paths will not work.`, + ), + ) + .describe('List of file paths to read.'), + }) + .describe( + `Read the multiple files from disk and return their contents. Use this tool to read as many files as would be helpful to answer the user's request.`, + ) +const description = ` +Note: DO NOT call this tool for files you've already read! There's no need to read them again — any changes to the files will be surfaced to you as a file update tool result. + +Example: +${$getToolCallString({ + toolName, + inputSchema, + input: { + paths: ['path/to/file1.ts', 'path/to/file2.ts'], + }, + endsAgentStep, +})} +`.trim() export const readFilesParams = { toolName, endsAgentStep, - parameters: z - .object({ - paths: z - .array( - z - .string() - .min(1, 'Paths cannot be empty') - .describe( - `File path to read relative to the **project root**. Absolute file paths will not work.`, - ), - ) - .describe('List of file paths to read.'), - }) - .describe( - `Read the multiple files from disk and return their contents. Use this tool to read as many files as would be helpful to answer the user's request.`, - ), - outputs: z.tuple([ - z.object({ - type: z.literal('json'), - value: fileContentsSchema.array(), - }), - ]), + description, + inputSchema, + outputSchema: jsonToolResultSchema(fileContentsSchema.array()), } satisfies $ToolParams diff --git a/common/src/tools/params/tool/read-subtree.ts b/common/src/tools/params/tool/read-subtree.ts index e3373a5ab0..3156d8ca74 100644 --- a/common/src/tools/params/tool/read-subtree.ts +++ b/common/src/tools/params/tool/read-subtree.ts @@ -1,60 +1,79 @@ import z from 'zod/v4' +import { $getToolCallString, jsonToolResultSchema } from '../utils' + import type { $ToolParams } from '../../constants' const toolName = 'read_subtree' const endsAgentStep = true +const inputSchema = z + .object({ + paths: z + .array(z.string().min(1)) + .optional() + .describe( + `List of paths to directories or files. Relative to the project root. If omitted, the entire project tree is used.`, + ), + maxTokens: z + .number() + .int() + .positive() + .default(4000) + .describe( + `Maximum token budget for the subtree blob; the tree will be truncated to fit within this budget by first dropping file variables and then removing the most-nested files and directories.`, + ), + }) + .describe( + `Read one or more directory subtrees (as a blob including subdirectories, file names, and parsed variables within each source file) or return parsed variable names for files. If no paths are provided, returns the entire project tree.`, + ) +const description = ` +Example: +${$getToolCallString({ + toolName, + inputSchema, + input: { + paths: ['src', 'package.json'], + maxTokens: 4000, + }, + endsAgentStep, +})} + +Purpose: Read a directory subtree and return a blob containing subdirectories, file names, and parsed variable/functions names from source files. For files, return only the parsed variable names. If no paths are provided, returns the entire project tree. The output is truncated to fit within the provided token budget. + +- Use this tool on particular subdirectories when you need to know all the nested files and directories. E.g. for a refactoring task, or to understand a particular part of the codebase. +- In normal use, don't set maxTokens beyond 10,000. +`.trim() + export const readSubtreeParams = { toolName, endsAgentStep, - parameters: z - .object({ - paths: z - .array(z.string().min(1)) - .optional() - .describe( - `List of paths to directories or files. Relative to the project root. If omitted, the entire project tree is used.`, - ), - maxTokens: z - .number() - .int() - .positive() - .default(4000) - .describe( - `Maximum token budget for the subtree blob; the tree will be truncated to fit within this budget by first dropping file variables and then removing the most-nested files and directories.`, - ), - }) - .describe( - `Read one or more directory subtrees (as a blob including subdirectories, file names, and parsed variables within each source file) or return parsed variable names for files. If no paths are provided, returns the entire project tree.`, + description, + inputSchema, + outputSchema: jsonToolResultSchema( + z.array( + z.union([ + z.object({ + path: z.string(), + type: z.literal('directory'), + printedTree: z.string(), + tokenCount: z.number(), + truncationLevel: z.enum([ + 'none', + 'unimportant-files', + 'tokens', + 'depth-based', + ]), + }), + z.object({ + path: z.string(), + type: z.literal('file'), + variables: z.array(z.string()), + }), + z.object({ + path: z.string(), + errorMessage: z.string(), + }), + ]), ), - outputs: z.tuple([ - z.object({ - type: z.literal('json'), - value: z.array( - z.union([ - z.object({ - path: z.string(), - type: z.literal('directory'), - printedTree: z.string(), - tokenCount: z.number(), - truncationLevel: z.enum([ - 'none', - 'unimportant-files', - 'tokens', - 'depth-based', - ]), - }), - z.object({ - path: z.string(), - type: z.literal('file'), - variables: z.array(z.string()), - }), - z.object({ - path: z.string(), - errorMessage: z.string(), - }), - ]), - ), - }), - ]), + ), } satisfies $ToolParams diff --git a/common/src/tools/params/tool/run-file-change-hooks.ts b/common/src/tools/params/tool/run-file-change-hooks.ts index f550ffe5a3..1b13799821 100644 --- a/common/src/tools/params/tool/run-file-change-hooks.ts +++ b/common/src/tools/params/tool/run-file-change-hooks.ts @@ -1,36 +1,57 @@ import z from 'zod/v4' import { terminalCommandOutputSchema } from './run-terminal-command' +import { $getToolCallString, jsonToolResultSchema } from '../utils' import type { $ToolParams } from '../../constants' const toolName = 'run_file_change_hooks' const endsAgentStep = true +const inputSchema = z.object({ + files: z + .array(z.string()) + .describe( + `List of file paths that were changed and should trigger file change hooks`, + ), +}) +const description = ` +Purpose: Trigger file change hooks defined in codebuff.json for the specified files. This tool allows the backend to request the client to run its configured file change hooks (like tests, linting, type checking) after file changes have been applied. + +Use cases: +- After making code changes, trigger the relevant tests and checks +- Ensure code quality by running configured linters and type checkers +- Validate that changes don't break the build + +The client will run only the hooks whose filePattern matches the provided files. + +Example: +${$getToolCallString({ + toolName, + inputSchema, + input: { + files: ['src/components/Button.tsx', 'src/utils/helpers.ts'], + }, + endsAgentStep, +})} +`.trim() + export const runFileChangeHooksParams = { toolName, endsAgentStep, - parameters: z.object({ - files: z - .array(z.string()) - .describe( - `List of file paths that were changed and should trigger file change hooks`, - ), - }), - outputs: z.tuple([ - z.object({ - type: z.literal('json'), - value: z - .union([ - terminalCommandOutputSchema.and( - z.object({ - hookName: z.string(), - }), - ), + description, + inputSchema, + outputSchema: jsonToolResultSchema( + z + .union([ + terminalCommandOutputSchema.and( z.object({ - errorMessage: z.string(), + hookName: z.string(), }), - ]) - .array(), - }), - ]), + ), + z.object({ + errorMessage: z.string(), + }), + ]) + .array(), + ), } satisfies $ToolParams diff --git a/common/src/tools/params/tool/run-terminal-command.ts b/common/src/tools/params/tool/run-terminal-command.ts index 633123721a..c89e16e570 100644 --- a/common/src/tools/params/tool/run-terminal-command.ts +++ b/common/src/tools/params/tool/run-terminal-command.ts @@ -1,5 +1,7 @@ import z from 'zod/v4' +import { $getToolCallString, jsonToolResultSchema } from '../utils' + import type { $ToolParams } from '../../constants' export const terminalCommandOutputSchema = z.union([ @@ -30,45 +32,156 @@ export const terminalCommandOutputSchema = z.union([ }), ]) +export const gitCommitGuidePrompt = ` +### Using git to commit changes + +When the user requests a new git commit, please follow these steps closely: + +1. **Run two run_terminal_command tool calls:** + - Run \`git diff\` to review both staged and unstaged modifications. + - Run \`git log\` to check recent commit messages, ensuring consistency with this repository's style. + +2. **Select relevant files to include in the commit:** + Use the git context established at the start of this conversation to decide which files are pertinent to the changes. Stage any new untracked files that are relevant, but avoid committing previously modified files (from the beginning of the conversation) unless they directly relate to this commit. + +3. **Analyze the staged changes and compose a commit message:** + Enclose your analysis in tags. Within these tags, you should: + - Note which files have been altered or added. + - Categorize the nature of the changes (e.g., new feature, fix, refactor, documentation, etc.). + - Consider the purpose or motivation behind the alterations. + - Refrain from using tools to inspect code beyond what is presented in the git context. + - Evaluate the overall impact on the project. + - Check for sensitive details that should not be committed. + - Draft a concise, one- to two-sentence commit message focusing on the “why” rather than the “what.” + - Use precise, straightforward language that accurately represents the changes. + - Ensure the message provides clarity—avoid generic or vague terms like “Update” or “Fix” without context. + - Revisit your draft to confirm it truly reflects the changes and their intention. + +4. **Create the commit, ending with this specific footer:** + \`\`\` + Generated with Codebuff 🤖 + Co-Authored-By: Codebuff + \`\`\` + To maintain proper formatting, use cross-platform compatible commit messages: + + **For Unix/bash shells:** + \`\`\` + git commit -m "$(cat <<'EOF' + Your commit message here. + + 🤖 Generated with Codebuff + Co-Authored-By: Codebuff + EOF + )" + \`\`\` + + **For Windows Command Prompt:** + \`\`\` + git commit -m "Your commit message here. + + 🤖 Generated with Codebuff + Co-Authored-By: Codebuff " + \`\`\` + + Always detect the platform and use the appropriate syntax. HEREDOC syntax (\`<<'EOF'\`) only works in bash/Unix shells and will fail on Windows Command Prompt. + +**Important details** + +- When feasible, use a single \`git commit -am\` command to add and commit together, but do not accidentally stage unrelated files. +- Never alter the git config. +- Do not push to the remote repository. +- Avoid using interactive flags (e.g., \`-i\`) that require unsupported interactive input. +- Do not create an empty commit if there are no changes. +- Make sure your commit message is concise yet descriptive, focusing on the intention behind the changes rather than merely describing them. +` + const toolName = 'run_terminal_command' const endsAgentStep = true +const inputSchema = z + .object({ + // Can be empty to use it for a timeout. + command: z + .string() + .min(1, 'Command cannot be empty') + .describe(`CLI command valid for user's OS.`), + process_type: z + .enum(['SYNC', 'BACKGROUND']) + .default('SYNC') + .describe( + `Either SYNC (waits, returns output) or BACKGROUND (runs in background). Default SYNC`, + ), + cwd: z + .string() + .optional() + .describe( + `The working directory to run the command in. Default is the project root.`, + ), + timeout_seconds: z + .number() + .default(30) + .optional() + .describe( + `Set to -1 for no timeout. Does not apply for BACKGROUND commands. Default 30`, + ), + }) + .describe( + `Execute a CLI command from the **project root** (different from the user's cwd).`, + ) +const description = ` +Stick to these use cases: +1. Typechecking the project or running build (e.g., "npm run build"). Reading the output can help you edit code to fix build errors. If possible, use an option that performs checks but doesn't emit files, e.g. \`tsc --noEmit\`. +2. Running tests (e.g., "npm test"). Reading the output can help you edit code to fix failing tests. Or, you could write new unit tests and then run them. +3. Moving, renaming, or deleting files and directories. These actions can be vital for refactoring requests. Use commands like \`mv\`/\`move\` or \`rm\`/\`del\`. + +Most likely, you should ask for permission for any other type of command you want to run. If asking for permission, show the user the command you want to run using \`\`\` tags and *do not* use the tool call format, e.g.: +\`\`\`bash +git branch -D foo +\`\`\` + +DO NOT do any of the following: +1. Run commands that can modify files outside of the project directory, install packages globally, install virtual environments, or have significant side effects outside of the project directory, unless you have explicit permission from the user. Treat anything outside of the project directory as read-only. +2. Run \`git push\` because it can break production (!) if the user was not expecting it. Don't run \`git commit\`, \`git rebase\`, or related commands unless you get explicit permission. If a user asks to commit changes, you can do so, but you should not invoke any further git commands beyond the git commit command. +3. Run scripts without asking. Especially don't run scripts that could run against the production environment or have permanent effects without explicit permission from the user. +4. Be careful with any command that has big or irreversible effects. Anything that touches a production environment, servers, the database, or other systems that could be affected by a command should be run with explicit permission from the user. +5. Use the run_terminal_command tool to create or edit files. Do not use \`cat\` or \`echo\` to create or edit files. You should instead use other tools for creating or editing files. +6. Use the wrong package manager for the project. For example, if the project uses \`pnpm\` or \`bun\` or \`yarn\`, you should not use \`npm\`. Similarly not everyone uses \`pip\` for python, etc. + +Do: +- If there's an opportunity to use "-y" or "--yes" flags, use them. Any command that prompts for confirmation will hang if you don't use the flags. + +Notes: +- If the user references a specific file, it could be either from their cwd or from the project root. You **must** determine which they are referring to (either infer or ask). Then, you must specify the path relative to the project root (or use the cwd parameter) +- Commands can succeed without giving any output, e.g. if no type errors were found. + +${gitCommitGuidePrompt} + +Example: +${$getToolCallString({ + toolName, + inputSchema, + input: { + command: 'echo "hello world"', + }, + endsAgentStep, +})} + +${$getToolCallString({ + toolName, + inputSchema, + input: { + command: `git commit -m "Your commit message here. + +🤖 Generated with Codebuff +Co-Authored-By: Codebuff "`, + }, + endsAgentStep, +})} + `.trim() + export const runTerminalCommandParams = { toolName, endsAgentStep, - parameters: z - .object({ - // Can be empty to use it for a timeout. - command: z - .string() - .min(1, 'Command cannot be empty') - .describe(`CLI command valid for user's OS.`), - process_type: z - .enum(['SYNC', 'BACKGROUND']) - .default('SYNC') - .describe( - `Either SYNC (waits, returns output) or BACKGROUND (runs in background). Default SYNC`, - ), - cwd: z - .string() - .optional() - .describe( - `The working directory to run the command in. Default is the project root.`, - ), - timeout_seconds: z - .number() - .default(30) - .optional() - .describe( - `Set to -1 for no timeout. Does not apply for BACKGROUND commands. Default 30`, - ), - }) - .describe( - `Execute a CLI command from the **project root** (different from the user's cwd).`, - ), - outputs: z.tuple([ - z.object({ - type: z.literal('json'), - value: terminalCommandOutputSchema, - }), - ]), + description, + inputSchema, + outputSchema: jsonToolResultSchema(terminalCommandOutputSchema), } satisfies $ToolParams diff --git a/common/src/tools/params/tool/set-messages.ts b/common/src/tools/params/tool/set-messages.ts index 2608055455..bb062cadff 100644 --- a/common/src/tools/params/tool/set-messages.ts +++ b/common/src/tools/params/tool/set-messages.ts @@ -1,16 +1,41 @@ import z from 'zod/v4' +import { $getToolCallString, emptyToolResultSchema } from '../utils' + import type { $ToolParams } from '../../constants' const toolName = 'set_messages' const endsAgentStep = true +const inputSchema = z + .object({ + messages: z.any(), + }) + .describe(`Set the conversation history to the provided messages.`) +const description = ` +Example: +${$getToolCallString({ + toolName, + inputSchema, + input: { + messages: [ + { + role: 'user', + content: 'Hello, how are you?', + }, + { + role: 'assistant', + content: 'I am fine, thank you.', + }, + ], + }, + endsAgentStep, +})} +`.trim() + export const setMessagesParams = { toolName, endsAgentStep, - parameters: z - .object({ - messages: z.any(), - }) - .describe(`Set the conversation history to the provided messages.`), - outputs: z.tuple([]), + description, + inputSchema, + outputSchema: emptyToolResultSchema(), } satisfies $ToolParams diff --git a/common/src/tools/params/tool/set-output.ts b/common/src/tools/params/tool/set-output.ts index ff76844d7f..f86c94f800 100644 --- a/common/src/tools/params/tool/set-output.ts +++ b/common/src/tools/params/tool/set-output.ts @@ -1,18 +1,37 @@ import z from 'zod/v4' +import { $getToolCallString } from '../utils' + import type { $ToolParams } from '../../constants' const toolName = 'set_output' const endsAgentStep = false +const inputSchema = z + .looseObject({}) + .describe( + 'JSON object to set as the agent output. This completely replaces any previous output. If the agent was spawned, this value will be passed back to its parent. If the agent has an outputSchema defined, the output will be validated against it.', + ) +const description = ` +You must use this tool as it is the only way to report any findings to the user. Nothing else you write will be shown to the user. + +Please set the output with all the information and analysis you want to pass on to the user. If you just want to send a simple message, use an object with the key "message" and value of the message you want to send. +Example: +${$getToolCallString({ + toolName, + inputSchema, + input: { + message: 'I found a bug in the code!', + }, + endsAgentStep, +})} +`.trim() + export const setOutputParams = { toolName, endsAgentStep, - parameters: z - .looseObject({}) - .describe( - 'JSON object to set as the agent output. This completely replaces any previous output. If the agent was spawned, this value will be passed back to its parent. If the agent has an outputSchema defined, the output will be validated against it.', - ), - outputs: z.tuple([ + description, + inputSchema, + outputSchema: z.tuple([ z.object({ type: z.literal('json'), value: z.object({ diff --git a/common/src/tools/params/tool/spawn-agent-inline.ts b/common/src/tools/params/tool/spawn-agent-inline.ts index 4a22dc0b51..6ee9a9d442 100644 --- a/common/src/tools/params/tool/spawn-agent-inline.ts +++ b/common/src/tools/params/tool/spawn-agent-inline.ts @@ -1,23 +1,52 @@ import z from 'zod/v4' +import { $getToolCallString, emptyToolResultSchema } from '../utils' + import type { $ToolParams } from '../../constants' const toolName = 'spawn_agent_inline' const endsAgentStep = true +const inputSchema = z + .object({ + agent_type: z.string().describe('Agent to spawn'), + prompt: z.string().optional().describe('Prompt to send to the agent'), + params: z + .record(z.string(), z.any()) + .optional() + .describe('Parameters object for the agent (if any)'), + }) + .describe( + `Spawn a single agent that runs within the current message history.`, + ) +const description = ` +Spawn a single agent that runs within the current message history. +The spawned agent sees all previous messages and any messages it adds +are preserved when control returns to you. + +You should prefer to use the spawn_agents tool unless instructed otherwise. This tool is only for special cases. + +This is useful for: +- Delegating specific tasks while maintaining context +- Having specialized agents process information inline +- Managing message history (e.g., summarization) +The agent will run until it calls end_turn, then control returns to you. There is no tool result for this tool. +Example: +${$getToolCallString({ + toolName, + inputSchema, + input: { + agent_type: 'file-picker', + prompt: 'Find files related to authentication', + params: { paths: ['src/auth.ts', 'src/user.ts'] }, + }, + endsAgentStep, +})} +`.trim() + export const spawnAgentInlineParams = { toolName, endsAgentStep, - parameters: z - .object({ - agent_type: z.string().describe('Agent to spawn'), - prompt: z.string().optional().describe('Prompt to send to the agent'), - params: z - .record(z.string(), z.any()) - .optional() - .describe('Parameters object for the agent (if any)'), - }) - .describe( - `Spawn a single agent that runs within the current message history.`, - ), - outputs: z.tuple([]), + description, + inputSchema, + outputSchema: emptyToolResultSchema(), } satisfies $ToolParams diff --git a/common/src/tools/params/tool/spawn-agents.ts b/common/src/tools/params/tool/spawn-agents.ts index 9a4a5d1d37..f7da5e5d7d 100644 --- a/common/src/tools/params/tool/spawn-agents.ts +++ b/common/src/tools/params/tool/spawn-agents.ts @@ -1,6 +1,7 @@ import z from 'zod/v4' import { jsonObjectSchema } from '../../../types/json' +import { $getToolCallString, jsonToolResultSchema } from '../utils' import type { $ToolParams } from '../../constants' @@ -13,29 +14,48 @@ export const spawnAgentsOutputSchema = z const toolName = 'spawn_agents' const endsAgentStep = true +const inputSchema = z + .object({ + agents: z + .object({ + agent_type: z.string().describe('Agent to spawn'), + prompt: z.string().optional().describe('Prompt to send to the agent'), + params: z + .record(z.string(), z.any()) + .optional() + .describe('Parameters object for the agent (if any)'), + }) + .array(), + }) + .describe( + `Spawn multiple agents and send a prompt and/or parameters to each of them. These agents will run in parallel. Note that that means they will run independently. If you need to run agents sequentially, use spawn_agents with one agent at a time instead.`, + ) +const description = ` +Use this tool to spawn agents to help you complete the user request. Each agent has specific requirements for prompt and params based on their inputSchema. + +The prompt field is a simple string, while params is a JSON object that gets validated against the agent's schema. + +Example: +${$getToolCallString({ + toolName, + inputSchema, + input: { + agents: [ + { + agent_type: 'planner', + prompt: 'Create a plan for implementing user authentication', + params: { filePaths: ['src/auth.ts', 'src/user.ts'] }, + }, + ], + }, + endsAgentStep, +})} +`.trim() + export const spawnAgentsParams = { toolName, endsAgentStep, - parameters: z - .object({ - agents: z - .object({ - agent_type: z.string().describe('Agent to spawn'), - prompt: z.string().optional().describe('Prompt to send to the agent'), - params: z - .record(z.string(), z.any()) - .optional() - .describe('Parameters object for the agent (if any)'), - }) - .array(), - }) - .describe( - `Spawn multiple agents and send a prompt and/or parameters to each of them. These agents will run in parallel. Note that that means they will run independently. If you need to run agents sequentially, use spawn_agents with one agent at a time instead.`, - ), - outputs: z.tuple([ - z.object({ - type: z.literal('json'), - value: spawnAgentsOutputSchema, - }), - ]), + description, + inputSchema, + outputSchema: jsonToolResultSchema(spawnAgentsOutputSchema), } satisfies $ToolParams diff --git a/common/src/tools/params/tool/str-replace.ts b/common/src/tools/params/tool/str-replace.ts index 9fb2d2626f..5aee745fec 100644 --- a/common/src/tools/params/tool/str-replace.ts +++ b/common/src/tools/params/tool/str-replace.ts @@ -1,5 +1,7 @@ import z from 'zod/v4' +import { $getToolCallString, jsonToolResultSchema } from '../utils' + import type { $ToolParams } from '../../constants' export const updateFileResultSchema = z.union([ @@ -17,48 +19,74 @@ export const updateFileResultSchema = z.union([ const toolName = 'str_replace' const endsAgentStep = false +const inputSchema = z + .object({ + path: z + .string() + .min(1, 'Path cannot be empty') + .describe(`The path to the file to edit.`), + replacements: z + .array( + z + .object({ + old: z + .string() + .min(1, 'Old cannot be empty') + .describe( + `The string to replace. This must be an *exact match* of the string you want to replace, including whitespace and punctuation.`, + ), + new: z + .string() + .describe( + `The string to replace the corresponding old string with. Can be empty to delete.`, + ), + allowMultiple: z + .boolean() + .optional() + .default(false) + .describe( + 'Whether to allow multiple replacements of old string.', + ), + }) + .describe('Pair of old and new strings.'), + ) + .min(1, 'Replacements cannot be empty') + .describe('Array of replacements to make.'), + }) + .describe(`Replace strings in a file with new strings.`) +const description = ` +Use this tool to make edits within existing files. Prefer this tool over the write_file tool for existing files, unless you need to make major changes throughout the file, in which case use write_file. + +Important: +If you are making multiple edits in a row to a file, use only one str_replace call with multiple replacements instead of multiple str_replace tool calls. + +Example: +${$getToolCallString({ + toolName, + inputSchema, + input: { + path: 'path/to/file', + replacements: [ + { old: 'This is the old string', new: 'This is the new string' }, + { + old: '\n\t\t// @codebuff delete this log line please\n\t\tconsole.log("Hello, world!");\n', + new: '\n', + }, + { + old: '\nfoo:', + new: '\nbar:', + allowMultiple: true, + }, + ], + }, + endsAgentStep, +})} + `.trim() + export const strReplaceParams = { toolName, endsAgentStep, - parameters: z - .object({ - path: z - .string() - .min(1, 'Path cannot be empty') - .describe(`The path to the file to edit.`), - replacements: z - .array( - z - .object({ - old: z - .string() - .min(1, 'Old cannot be empty') - .describe( - `The string to replace. This must be an *exact match* of the string you want to replace, including whitespace and punctuation.`, - ), - new: z - .string() - .describe( - `The string to replace the corresponding old string with. Can be empty to delete.`, - ), - allowMultiple: z - .boolean() - .optional() - .default(false) - .describe( - 'Whether to allow multiple replacements of old string.', - ), - }) - .describe('Pair of old and new strings.'), - ) - .min(1, 'Replacements cannot be empty') - .describe('Array of replacements to make.'), - }) - .describe(`Replace strings in a file with new strings.`), - outputs: z.tuple([ - z.object({ - type: z.literal('json'), - value: updateFileResultSchema, - }), - ]), + description, + inputSchema, + outputSchema: jsonToolResultSchema(updateFileResultSchema), } satisfies $ToolParams diff --git a/common/src/tools/params/tool/task-completed.ts b/common/src/tools/params/tool/task-completed.ts index 5532d7231a..a8c35d1c68 100644 --- a/common/src/tools/params/tool/task-completed.ts +++ b/common/src/tools/params/tool/task-completed.ts @@ -1,19 +1,58 @@ import z from 'zod/v4' +import { $getToolCallString, emptyToolResultSchema } from '../utils' + import type { $ToolParams } from '../../constants' const toolName = 'task_completed' const endsAgentStep = true -export const taskCompletedParams = { - toolName, - endsAgentStep, - parameters: z.object({}).describe( - `Signal that the task is complete. Use this tool when: +const inputSchema = z.object({}).describe( + `Signal that the task is complete. Use this tool when: - The user's request is completely fulfilled - You need clarification from the user before continuing - You are stuck or need help from the user to continue This tool explicitly marks the end of your work on the current task.`, - ), - outputs: z.tuple([]), +) +const description = ` +Use this tool to signal that the task is complete. + +- When to use: + * The user's request is completely fulfilled and you have nothing more to do + * You need clarification from the user before continuing + * You need help from the user to continue (e.g., missing information, unclear requirements) + * You've encountered a blocker that requires user intervention + +- Before calling: + * Ensure all pending work is finished + * Resolve all tool results + * Provide any outputs or summaries the user needs + +- Effect: Signals completion of the current task and returns control to the user + +*EXAMPLE USAGE*: + +All changes have been implemented and tested successfully! + +${$getToolCallString({ toolName, inputSchema, input: {}, endsAgentStep })} + +OR + +I need more information to proceed. Which database schema should I use for this migration? + +${$getToolCallString({ toolName, inputSchema, input: {}, endsAgentStep })} + +OR + +I can't get the tests to pass after several different attempts. I need help from the user to proceed. + +${$getToolCallString({ toolName, inputSchema, input: {}, endsAgentStep })} +`.trim() + +export const taskCompletedParams = { + toolName, + endsAgentStep, + description, + inputSchema, + outputSchema: emptyToolResultSchema(), } satisfies $ToolParams diff --git a/common/src/tools/params/tool/think-deeply.ts b/common/src/tools/params/tool/think-deeply.ts index 275dfe05de..4292332fa5 100644 --- a/common/src/tools/params/tool/think-deeply.ts +++ b/common/src/tools/params/tool/think-deeply.ts @@ -1,23 +1,53 @@ import z from 'zod/v4' +import { $getToolCallString, emptyToolResultSchema } from '../utils' + import type { $ToolParams } from '../../constants' const toolName = 'think_deeply' const endsAgentStep = false +const inputSchema = z + .object({ + thought: z + .string() + .min(1, 'Thought cannot be empty') + .describe( + `Detailed step-by-step analysis. Initially keep each step concise (max ~5-7 words per step).`, + ), + }) + .describe( + `Deeply consider complex tasks by brainstorming approaches and tradeoffs step-by-step.`, + ) +const description = ` +Use when user request: +- Explicitly asks for deep planning. +- Requires multi-file changes or complex logic. +- Involves significant architecture or potential edge cases. + +Avoid for simple changes (e.g., single functions, minor edits). + +This tool does not generate a tool result. + +Example: +${$getToolCallString({ + toolName, + inputSchema, + input: { + thought: [ + '1. Check current user authentication', + '2. Refactor auth logic into module', + '3. Update imports across project', + '4. Add tests for new module', + ].join('\n'), + }, + endsAgentStep, +})} +`.trim() + export const thinkDeeplyParams = { toolName, endsAgentStep, - parameters: z - .object({ - thought: z - .string() - .min(1, 'Thought cannot be empty') - .describe( - `Detailed step-by-step analysis. Initially keep each step concise (max ~5-7 words per step).`, - ), - }) - .describe( - `Deeply consider complex tasks by brainstorming approaches and tradeoffs step-by-step.`, - ), - outputs: z.tuple([]), + description, + inputSchema, + outputSchema: emptyToolResultSchema(), } satisfies $ToolParams diff --git a/common/src/tools/params/tool/update-subgoal.ts b/common/src/tools/params/tool/update-subgoal.ts index dc19235ad5..299ca9eeaf 100644 --- a/common/src/tools/params/tool/update-subgoal.ts +++ b/common/src/tools/params/tool/update-subgoal.ts @@ -1,39 +1,89 @@ import z from 'zod/v4' +import { $getToolCallString, jsonToolResultSchema } from '../utils' + import type { $ToolParams } from '../../constants' const toolName = 'update_subgoal' const endsAgentStep = false +const inputSchema = z + .object({ + id: z + .string() + .min(1, 'Id cannot be empty') + .describe(`The id of the subgoal to update.`), + status: z + .enum(['NOT_STARTED', 'IN_PROGRESS', 'COMPLETE', 'ABORTED']) + .optional() + .describe(`Change the status of the subgoal.`), + plan: z.string().optional().describe(`Change the plan for the subgoal.`), + log: z + .string() + .optional() + .describe( + `Add a log message to the subgoal. This will create a new log entry and append it to the existing logs. Use this to record your progress and any new information you learned as you go.`, + ), + }) + .describe( + `Update a subgoal in the context given the id, and optionally the status or plan, or a new log to append. Feel free to update any combination of the status, plan, or log in one invocation.`, + ) +const description = ` +Examples: + +Usage 1 (update status): +${$getToolCallString({ + toolName, + inputSchema, + input: { + id: '1', + status: 'COMPLETE', + }, + endsAgentStep, +})} + +Usage 2 (update plan): +${$getToolCallString({ + toolName, + inputSchema, + input: { + id: '3', + plan: 'Create file for endpoint in the api. Register it in the router.', + }, + endsAgentStep, +})} + +Usage 3 (add log): +${$getToolCallString({ + toolName, + inputSchema, + input: { + id: '1', + log: 'Found the error in the tests. Culprit: foo function.', + }, + endsAgentStep, +})} + +Usage 4 (update status and add log): +${$getToolCallString({ + toolName, + inputSchema, + input: { + id: '1', + status: 'COMPLETE', + log: 'Reran the tests (passed)', + }, + endsAgentStep, +})} +`.trim() + export const updateSubgoalParams = { toolName, endsAgentStep, - parameters: z - .object({ - id: z - .string() - .min(1, 'Id cannot be empty') - .describe(`The id of the subgoal to update.`), - status: z - .enum(['NOT_STARTED', 'IN_PROGRESS', 'COMPLETE', 'ABORTED']) - .optional() - .describe(`Change the status of the subgoal.`), - plan: z.string().optional().describe(`Change the plan for the subgoal.`), - log: z - .string() - .optional() - .describe( - `Add a log message to the subgoal. This will create a new log entry and append it to the existing logs. Use this to record your progress and any new information you learned as you go.`, - ), - }) - .describe( - `Update a subgoal in the context given the id, and optionally the status or plan, or a new log to append. Feel free to update any combination of the status, plan, or log in one invocation.`, - ), - outputs: z.tuple([ + description, + inputSchema, + outputSchema: jsonToolResultSchema( z.object({ - type: z.literal('json'), - value: z.object({ - message: z.string(), - }), + message: z.string(), }), - ]), + ), } satisfies $ToolParams diff --git a/common/src/tools/params/tool/web-search.ts b/common/src/tools/params/tool/web-search.ts index 65f8e787c7..7a458cc01a 100644 --- a/common/src/tools/params/tool/web-search.ts +++ b/common/src/tools/params/tool/web-search.ts @@ -1,38 +1,73 @@ import z from 'zod/v4' +import { $getToolCallString, jsonToolResultSchema } from '../utils' + import type { $ToolParams } from '../../constants' const toolName = 'web_search' const endsAgentStep = true +const inputSchema = z + .object({ + query: z + .string() + .min(1, 'Query cannot be empty') + .describe(`The search query to find relevant web content`), + depth: z + .enum(['standard', 'deep']) + .optional() + .default('standard') + .describe( + `Search depth - 'standard' for quick results, 'deep' for more comprehensive search. Default is 'standard'.`, + ), + }) + .describe(`Search the web for current information using Linkup API.`) +const description = ` +Purpose: Search the web for current, up-to-date information on any topic. This tool uses Linkup's web search API to find relevant content from across the internet. + +Use cases: +- Finding current information about technologies, libraries, or frameworks +- Researching best practices and solutions +- Getting up-to-date news or documentation +- Finding examples and tutorials +- Checking current status of services or APIs + +The tool will return search results with titles, URLs, and content snippets. + +Example: +${$getToolCallString({ + toolName, + inputSchema, + input: { + query: 'Next.js 15 new features', + depth: 'standard', + }, + endsAgentStep, +})} + +${$getToolCallString({ + toolName, + inputSchema, + input: { + query: 'React Server Components tutorial', + depth: 'deep', + }, + endsAgentStep, +})} +`.trim() + export const webSearchParams = { toolName, endsAgentStep, - parameters: z - .object({ - query: z - .string() - .min(1, 'Query cannot be empty') - .describe(`The search query to find relevant web content`), - depth: z - .enum(['standard', 'deep']) - .optional() - .default('standard') - .describe( - `Search depth - 'standard' for quick results, 'deep' for more comprehensive search. Default is 'standard'.`, - ), - }) - .describe(`Search the web for current information using Linkup API.`), - outputs: z.tuple([ - z.object({ - type: z.literal('json'), - value: z.union([ - z.object({ - result: z.string(), - }), - z.object({ - errorMessage: z.string(), - }), - ]), - }), - ]), + description, + inputSchema, + outputSchema: jsonToolResultSchema( + z.union([ + z.object({ + result: z.string(), + }), + z.object({ + errorMessage: z.string(), + }), + ]), + ), } satisfies $ToolParams diff --git a/common/src/tools/params/tool/write-file.ts b/common/src/tools/params/tool/write-file.ts index 4afd64fff9..00ec71c6d2 100644 --- a/common/src/tools/params/tool/write-file.ts +++ b/common/src/tools/params/tool/write-file.ts @@ -1,30 +1,86 @@ import z from 'zod/v4' import { updateFileResultSchema } from './str-replace' +import { $getToolCallString, jsonToolResultSchema } from '../utils' import type { $ToolParams } from '../../constants' const toolName = 'write_file' const endsAgentStep = false +const inputSchema = z + .object({ + path: z + .string() + .min(1, 'Path cannot be empty') + .describe(`Path to the file relative to the **project root**`), + instructions: z + .string() + .describe('What the change is intended to do in only one sentence.'), + content: z.string().describe(`Edit snippet to apply to the file.`), + }) + .describe(`Create or edit a file with the given content.`) +const description = ` +Create or replace a file with the given content. + +#### Edit Snippet + +Format the \`content\` parameter with the entire content of the file or as an edit snippet that describes how you would like to modify the provided existing code. + +You may abbreviate any sections of the code in your response that will remain the same with placeholder comments: "// ... existing code ...". Abbreviate as much as possible to save the user credits! + +If you don't use any placeholder comments, the entire file will be replaced. E.g. don't write out a single function without using placeholder comments unless you want to replace the entire file with that function. + +#### Additional Info + +Prefer str_replace to write_file for most edits, including small-to-medium edits to a file, for deletions, or for editing large files (>1000 lines). Otherwise, prefer write_file for major edits throughout a file, or for creating new files. + +Do not use this tool to delete or rename a file. Instead run a terminal command for that. + +Examples: + +Example 1 - Simple file creation: +${$getToolCallString({ + toolName, + inputSchema, + input: { + path: 'new-file.ts', + instructions: 'Prints Hello, world', + content: 'console.log("Hello, world!");', + }, + endsAgentStep, +})} + +Example 2 - Editing with placeholder comments: +${$getToolCallString({ + toolName, + inputSchema, + input: { + path: 'foo.ts', + instructions: 'Update foo and remove console.log', + content: `// ... existing code ... + +function foo() { + console.log('foo'); + for (let i = 0; i < 10; i++) { + console.log(i); + } + doSomething(); + + // Delete the console.log line from here + + doSomethingElse(); +} + +// ... existing code ...`, + }, + endsAgentStep, +})} +`.trim() + export const writeFileParams = { toolName, endsAgentStep, - parameters: z - .object({ - path: z - .string() - .min(1, 'Path cannot be empty') - .describe(`Path to the file relative to the **project root**`), - instructions: z - .string() - .describe('What the change is intended to do in only one sentence.'), - content: z.string().describe(`Edit snippet to apply to the file.`), - }) - .describe(`Create or edit a file with the given content.`), - outputs: z.tuple([ - z.object({ - type: z.literal('json'), - value: updateFileResultSchema, - }), - ]), + description, + inputSchema, + outputSchema: jsonToolResultSchema(updateFileResultSchema), } satisfies $ToolParams diff --git a/common/src/tools/params/tool/write-todos.ts b/common/src/tools/params/tool/write-todos.ts index dd298fb698..7b7489a6f2 100644 --- a/common/src/tools/params/tool/write-todos.ts +++ b/common/src/tools/params/tool/write-todos.ts @@ -1,29 +1,59 @@ import z from 'zod/v4' +import { $getToolCallString } from '../utils' + import type { $ToolParams } from '../../constants' const toolName = 'write_todos' const endsAgentStep = false +const inputSchema = z + .object({ + todos: z + .array( + z.object({ + task: z.string().describe('Description of the task'), + completed: z.boolean().describe('Whether the task is completed'), + }), + ) + .describe( + "List of todos with their completion status. Add ALL of the applicable tasks to the list, so you don't forget to do anything. Try to order the todos the same way you will complete them. Do not mark todos as completed if you have not completed them yet!", + ), + }) + .describe( + 'Write a todo list to track tasks for multi-step implementations. Use this frequently to maintain an updated step-by-step plan.', + ) +const description = ` +Use this tool to track your objectives through an ordered step-by-step plan. Call this tool after you have gathered context on the user's request to plan out the implementation steps for the user's request. + +After completing each todo step, call this tool again to update the list and mark that task as completed. Note that each time you call this tool, rewrite ALL todos with their current status. + +Use this tool frequently as you work through tasks to update the list of todos with their current status. Doing this is extremely useful because it helps you stay on track and complete all the requirements of the user's request. It also helps inform the user of your plans and the current progress, which they want to know at all times. + +Example: +${$getToolCallString({ + toolName, + inputSchema, + input: { + todos: [ + { task: 'Create new implementation in foo.ts', completed: true }, + { task: 'Update bar.ts to use the new implementation', completed: false }, + { task: 'Write tests for the new implementation', completed: false }, + { + task: 'Run the tests to verify the new implementation', + completed: false, + }, + ], + }, + endsAgentStep, +})} +`.trim() + export const writeTodosParams = { toolName, endsAgentStep, - parameters: z - .object({ - todos: z - .array( - z.object({ - task: z.string().describe('Description of the task'), - completed: z.boolean().describe('Whether the task is completed'), - }), - ) - .describe( - 'List of todos with their completion status. Add ALL of the applicable tasks to the list, so you don\'t forget to do anything. Try to order the todos the same way you will complete them. Do not mark todos as completed if you have not completed them yet!', - ), - }) - .describe( - 'Write a todo list to track tasks for multi-step implementations. Use this frequently to maintain an updated step-by-step plan.', - ), - outputs: z.tuple([ + description, + inputSchema, + outputSchema: z.tuple([ z.object({ type: z.literal('json'), value: z.object({ diff --git a/common/src/tools/params/utils.ts b/common/src/tools/params/utils.ts new file mode 100644 index 0000000000..cbf79d327f --- /dev/null +++ b/common/src/tools/params/utils.ts @@ -0,0 +1,52 @@ +import z from 'zod/v4' + +import { + endsAgentStepParam, + endToolTag, + startToolTag, + toolNameParam, +} from '../constants' + +import type { JSONValue } from '../../types/json' +import type { ToolResultOutput } from '../../types/messages/content-part' + +/** Only used for generating tool call strings before all tools are defined. + * + * @param toolName - The name of the tool to call + * @param inputSchema - The zod schema for the tool. This is only used as type validation and is unused otherwise. + * @param input - The input to the tool + * @param endsAgentStep - Whether the agent should end its turn after this tool call + */ +export function $getToolCallString(params: { + toolName: string + inputSchema: z.ZodType | null + input: Input + endsAgentStep: boolean +}): string { + const { toolName, input, endsAgentStep } = params + const obj: Record = { + [toolNameParam]: toolName, + ...input, + } + if (endsAgentStep) { + obj[endsAgentStepParam] = endsAgentStep satisfies true + } + return [startToolTag, JSON.stringify(obj, null, 2), endToolTag].join('') +} + +/** Generates the zod schema for a single JSON tool result. */ +export function jsonToolResultSchema( + valueSchema: z.ZodType, +) { + return z.tuple([ + z.object({ + type: z.literal('json'), + value: valueSchema, + }) satisfies z.ZodType, + ]) +} + +/** Generates the zod schema for an empty tool result. */ +export function emptyToolResultSchema() { + return z.tuple([]) +} diff --git a/common/src/tools/utils.ts b/common/src/tools/utils.ts index e787592c23..93d95fe960 100644 --- a/common/src/tools/utils.ts +++ b/common/src/tools/utils.ts @@ -1,31 +1,24 @@ -import { - endsAgentStepParam, - endToolTag, - startToolTag, - toolNameParam, -} from './constants' -import { $toolParams } from './list' +import { toolParams } from './list' +import { $getToolCallString } from './params/utils' import type { ToolName } from './constants' import type z from 'zod/v4' export function getToolCallString( toolName: T, - params: T extends ToolName - ? z.input<(typeof $toolParams)[T]['parameters']> + input: T extends ToolName + ? z.input<(typeof toolParams)[T]['inputSchema']> : Record, ...endsAgentStep: T extends ToolName ? [] : [boolean] ): string { const endsAgentStepValue = - toolName in $toolParams - ? $toolParams[toolName as keyof typeof $toolParams].endsAgentStep + toolName in toolParams + ? toolParams[toolName as keyof typeof toolParams].endsAgentStep : endsAgentStep[0] ?? false - const obj: Record = { - [toolNameParam]: toolName, - ...params, - } - if (endsAgentStepValue) { - obj[endsAgentStepParam] = endsAgentStepValue satisfies true - } - return [startToolTag, JSON.stringify(obj, null, 2), endToolTag].join('') + return $getToolCallString({ + toolName, + inputSchema: null, + input, + endsAgentStep: endsAgentStepValue, + }) } diff --git a/common/src/types/messages/codebuff-message.ts b/common/src/types/messages/codebuff-message.ts index f142b7bcfe..0ce1708b31 100644 --- a/common/src/types/messages/codebuff-message.ts +++ b/common/src/types/messages/codebuff-message.ts @@ -1,83 +1,51 @@ -import z from 'zod/v4' - -import { - filePartSchema, - imagePartSchema, - reasoningPartSchema, - textPartSchema, - toolCallPartSchema, - toolResultOutputSchema, +import type { + FilePart, + ImagePart, + ReasoningPart, + TextPart, + ToolCallPart, + ToolResultOutput, } from './content-part' -import { providerMetadataSchema } from './provider-metadata' - -const auxiliaryDataSchema = z.object({ - providerOptions: providerMetadataSchema.optional(), +import type { ProviderMetadata } from './provider-metadata' - tags: z.string().array().optional(), +export type AuxiliaryMessageData = { + providerOptions?: ProviderMetadata + tags?: string[] // James: All the below is overly prescriptive for the framework. // Instead, let's tag what the message is, and let the user decide time to live, keep during truncation, etc. /** @deprecated Use tags instead. */ - timeToLive: z - .union([z.literal('agentStep'), z.literal('userPrompt')]) - .optional(), + timeToLive?: 'agentStep' | 'userPrompt' /** @deprecated Use tags instead. */ - keepDuringTruncation: z.boolean().optional(), + keepDuringTruncation?: boolean /** @deprecated Use tags instead. */ - keepLastTags: z.string().array().optional(), -}) -export type AuxiliaryMessageData = z.infer - -export const systemMessageSchema = z - .object({ - role: z.literal('system'), - content: textPartSchema.array(), - }) - .and(auxiliaryDataSchema) -export type SystemMessage = z.infer - -export const userMessageSchema = z - .object({ - role: z.literal('user'), - content: z - .discriminatedUnion('type', [ - textPartSchema, - imagePartSchema, - filePartSchema, - ]) - .array(), - }) - .and(auxiliaryDataSchema) -export type UserMessage = z.infer - -export const assistantMessageSchema = z - .object({ - role: z.literal('assistant'), - content: z - .discriminatedUnion('type', [ - textPartSchema, - reasoningPartSchema, - toolCallPartSchema, - ]) - .array(), - }) - .and(auxiliaryDataSchema) -export type AssistantMessage = z.infer - -export const toolMessageSchema = z - .object({ - role: z.literal('tool'), - toolCallId: z.string(), - toolName: z.string(), - content: toolResultOutputSchema.array(), - }) - .and(auxiliaryDataSchema) -export type ToolMessage = z.infer - -export const messageSchema = z.union([ - systemMessageSchema, - userMessageSchema, - assistantMessageSchema, - toolMessageSchema, -]) -export type Message = z.infer + keepLastTags?: string[] +} + +export type SystemMessage = { + role: 'system' + content: TextPart[] +} & AuxiliaryMessageData + +export type UserMessage = { + role: 'user' + content: (TextPart | ImagePart | FilePart)[] +} & AuxiliaryMessageData + +export type AssistantMessage = { + role: 'assistant' + content: (TextPart | ReasoningPart | ToolCallPart)[] +} & AuxiliaryMessageData + +export type ToolMessage = { + role: 'tool' + toolCallId: string + toolName: string + content: ToolResultOutput[] +} & AuxiliaryMessageData + +export type Message = + | SystemMessage + | UserMessage + | AssistantMessage + | ToolMessage diff --git a/common/src/types/session-state.ts b/common/src/types/session-state.ts index 52abf50f71..ca04c73121 100644 --- a/common/src/types/session-state.ts +++ b/common/src/types/session-state.ts @@ -1,8 +1,6 @@ import { z } from 'zod/v4' import { MAX_AGENT_STEPS_DEFAULT } from '../constants/agents' -import { ProjectFileContextSchema } from '../util/file' -import { messageSchema } from './messages/codebuff-message' import type { Message } from './messages/codebuff-message' import type { ProjectFileContext } from '../util/file' @@ -24,7 +22,7 @@ export const subgoalSchema = z.object({ }) export type Subgoal = z.infer -export const AgentStateSchema: z.ZodType<{ +export type AgentState = { /** * @deprecated agentId is replaced by runId */ @@ -41,30 +39,7 @@ export const AgentStateSchema: z.ZodType<{ directCreditsUsed: number output?: Record parentId?: string -}> = z.lazy(() => - z.object({ - agentId: z.string(), - agentType: z.string().nullable(), - agentContext: z.record(z.string(), subgoalSchema), - ancestorRunIds: z - .string() - .array() - .default(() => []), - runId: z.string().optional(), - subagents: AgentStateSchema.array(), - childRunIds: z - .string() - .array() - .default(() => []), - messageHistory: messageSchema.array(), - stepsRemaining: z.number(), - creditsUsed: z.number().default(0), - directCreditsUsed: z.number().default(0), - output: z.record(z.string(), z.any()).optional(), - parentId: z.string().optional(), - }), -) -export type AgentState = z.infer +} export const AgentOutputSchema = z.discriminatedUnion('type', [ z.object({ @@ -124,11 +99,10 @@ export type AgentTemplateType = | z.infer | (string & {}) -export const SessionStateSchema = z.object({ - fileContext: ProjectFileContextSchema, - mainAgentState: AgentStateSchema, -}) -export type SessionState = z.infer +export type SessionState = { + fileContext: ProjectFileContext + mainAgentState: AgentState +} export function getInitialAgentState(): AgentState { return { diff --git a/common/src/util/__tests__/messages.test.ts b/common/src/util/__tests__/messages.test.ts index 12aff0e63c..53e1cb7225 100644 --- a/common/src/util/__tests__/messages.test.ts +++ b/common/src/util/__tests__/messages.test.ts @@ -8,8 +8,8 @@ import { systemMessage, userMessage, assistantMessage, - toolJsonContent, - toolMediaContent, + jsonToolResult, + mediaToolResult, } from '../messages' import type { Message } from '../../types/messages/codebuff-message' @@ -200,7 +200,7 @@ describe('convertCbToModelMessages', () => { role: 'tool', toolName: 'test_tool', toolCallId: 'call_123', - content: [toolJsonContent({ result: 'success' })], + content: jsonToolResult({ result: 'success' }), }, ] @@ -228,12 +228,10 @@ describe('convertCbToModelMessages', () => { role: 'tool', toolName: 'test_tool', toolCallId: 'call_123', - content: [ - toolMediaContent({ - data: 'base64data', - mediaType: 'image/png', - }), - ], + content: mediaToolResult({ + data: 'base64data', + mediaType: 'image/png', + }), }, ] @@ -261,8 +259,8 @@ describe('convertCbToModelMessages', () => { toolName: 'test_tool', toolCallId: 'call_123', content: [ - toolJsonContent({ result1: 'success' }), - toolJsonContent({ result2: 'also success' }), + { type: 'json', value: { result1: 'success' } }, + { type: 'json', value: { result2: 'also success' } }, ], }, ] diff --git a/common/src/util/messages.ts b/common/src/util/messages.ts index a211d31170..b8387a96f2 100644 --- a/common/src/util/messages.ts +++ b/common/src/util/messages.ts @@ -424,30 +424,31 @@ export function assistantMessage( } } -export function toolJsonContent( +export function jsonToolResult( value: T, -): ToolResultOutput & { - type: 'json' - value: T -} { - return { - type: 'json', - value, - } +): [ + Extract & { + value: T + }, +] { + return [ + { + type: 'json', + value, + }, + ] } -export function toolMediaContent(params: { +export function mediaToolResult(params: { data: string mediaType: string -}): ToolResultOutput & { - type: 'media' - data: string - mediaType: string -} { +}): [Extract] { const { data, mediaType } = params - return { - type: 'media', - data, - mediaType, - } + return [ + { + type: 'media', + data, + mediaType, + }, + ] } diff --git a/common/src/websockets/websocket-schema.ts b/common/src/websockets/websocket-schema.ts index 55acb040e8..4e87d0014d 100644 --- a/common/src/websockets/websocket-schema.ts +++ b/common/src/websockets/websocket-schema.ts @@ -1,64 +1,65 @@ -import { z } from 'zod/v4' +import type { ClientAction, ServerAction } from '../actions' -import { CLIENT_ACTION_SCHEMA, SERVER_ACTION_SCHEMA } from '../actions' - -export const CLIENT_MESSAGE_SCHEMAS = { - identify: z.object({ - type: z.literal('identify'), - txid: z.number(), - clientSessionId: z.string(), - }), - subscribe: z.object({ - type: z.literal('subscribe'), - txid: z.number(), - topics: z.array(z.string()), - }), - unsubscribe: z.object({ - type: z.literal('unsubscribe'), - txid: z.number(), - topics: z.array(z.string()), - }), - ping: z.object({ - type: z.literal('ping'), - txid: z.number(), - }), - action: z.object({ - type: z.literal('action'), - txid: z.number(), - data: CLIENT_ACTION_SCHEMA, - }), -} as const - -export const CLIENT_MESSAGE_SCHEMA = z.union([ - CLIENT_MESSAGE_SCHEMAS.identify, - CLIENT_MESSAGE_SCHEMAS.subscribe, - CLIENT_MESSAGE_SCHEMAS.unsubscribe, - CLIENT_MESSAGE_SCHEMAS.ping, - CLIENT_MESSAGE_SCHEMAS.action, -]) +type ClientMessageIdentify = { + type: 'identify' + txid: number + clientSessionId: string +} +type ClientMessageSubscribe = { + type: 'subscribe' + txid: number + topics: string[] +} +type ClientMessageUnsubscribe = { + type: 'unsubscribe' + txid: number + topics: string[] +} +type ClientMessagePing = { + type: 'ping' + txid: number +} +type ClientMessageAction = { + type: 'action' + txid: number + data: ClientAction +} -export type ClientMessageType = keyof typeof CLIENT_MESSAGE_SCHEMAS -export type ClientMessage = - z.infer<(typeof CLIENT_MESSAGE_SCHEMAS)[T]> +type ClientMessageAny = + | ClientMessageIdentify + | ClientMessageSubscribe + | ClientMessageUnsubscribe + | ClientMessagePing + | ClientMessageAction +export type ClientMessageType = ClientMessageAny['type'] +export type ClientMessage = { + [K in ClientMessageType]: Extract< + ClientMessageAny, + { + type: K + } + > +}[T] -export const SERVER_MESSAGE_SCHEMAS = { - ack: z.object({ - type: z.literal('ack'), - txid: z.number().optional(), - success: z.boolean(), - error: z.string().optional(), - }), - action: z.object({ - type: z.literal('action'), - data: SERVER_ACTION_SCHEMA, - }), +type ServerMessageAck = { + type: 'ack' + txid?: number + success: boolean + error?: string } -export const SERVER_MESSAGE_SCHEMA = z.union([ - SERVER_MESSAGE_SCHEMAS.ack, - SERVER_MESSAGE_SCHEMAS.action, -]) +type ServerMessageAction = { + type: 'action' + data: ServerAction +} -export type ServerMessageType = keyof typeof SERVER_MESSAGE_SCHEMAS -export type ServerMessage = - z.infer<(typeof SERVER_MESSAGE_SCHEMAS)[T]> +type ServerMessageAny = ServerMessageAck | ServerMessageAction +export type ServerMessageType = ServerMessageAny['type'] +export type ServerMessage = { + [K in ServerMessageType]: Extract< + ServerMessageAny, + { + type: K + } + > +}[T] diff --git a/npm-app/src/background-process-manager.ts b/npm-app/src/background-process-manager.ts index e29ea9c316..7d48f918ee 100644 --- a/npm-app/src/background-process-manager.ts +++ b/npm-app/src/background-process-manager.ts @@ -11,7 +11,7 @@ import path from 'path' import process from 'process' import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events' -import { toolJsonContent } from '@codebuff/common/util/messages' +import { jsonToolResult } from '@codebuff/common/util/messages' import { truncateStringWithMessage } from '@codebuff/common/util/string' import { gray, red } from 'picocolors' import { z } from 'zod/v4' @@ -181,7 +181,7 @@ export function getBackgroundProcessUpdates(): ToolMessage[] { role: 'tool', toolCallId, toolName: 'background_process_update', - content: [toolJsonContent(update)], + content: jsonToolResult(update), } satisfies ToolMessage }) } diff --git a/npm-app/src/client.ts b/npm-app/src/client.ts index 5bcb9c3879..f4c4ead41e 100644 --- a/npm-app/src/client.ts +++ b/npm-app/src/client.ts @@ -8,11 +8,6 @@ import { import os from 'os' import path from 'path' -import { - InitResponseSchema, - MessageCostResponseSchema, - UsageReponseSchema, -} from '@codebuff/common/actions' import { READABLE_NAME } from '@codebuff/common/api-keys/constants' import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events' import { codebuffConfigFile as CONFIG_FILE_NAME } from '@codebuff/common/json-config/constants' @@ -907,9 +902,7 @@ export class Client { }) this.webSocket.subscribe('message-cost-response', (action) => { - const parsedAction = MessageCostResponseSchema.safeParse(action) - if (!parsedAction.success) return - const response = parsedAction.data + const response = action // Store credits used for this prompt if (!this.creditsByPromptId[response.promptId]) { @@ -924,27 +917,11 @@ export class Client { }) this.webSocket.subscribe('usage-response', (action) => { - const parsedAction = UsageReponseSchema.safeParse(action) - if (!parsedAction.success) { - console.error( - red('Received invalid usage data from server:'), - parsedAction.error.issues, - ) - logger.error( - { - errorMessage: 'Received invalid usage data from server', - errors: parsedAction.error.issues, - }, - 'Invalid usage data from server', - ) - return - } - - this.setUsage(parsedAction.data) + this.setUsage(action) // Store auto-topup amount if present, to be displayed when returning control to user - if (parsedAction.data.autoTopupAdded) { - this.pendingTopUpMessageAmount += parsedAction.data.autoTopupAdded + if (action.autoTopupAdded) { + this.pendingTopUpMessageAmount += action.autoTopupAdded } // Only show warning if the response is complete @@ -1614,7 +1591,7 @@ Go to https://www.codebuff.com/config for more information.`) + const data = await response.json() // Use zod schema to validate response - const parsedResponse = UsageReponseSchema.parse(data) + const parsedResponse: UsageResponse = data if ((data as any).type === 'action-error') { console.error(red((data as any).message)) @@ -1730,29 +1707,24 @@ Go to https://www.codebuff.com/config for more information.`) + throw new Error('Failed to initialize project file context') } - this.webSocket.subscribe('init-response', (a) => { - const parsedAction = InitResponseSchema.safeParse(a) - if (!parsedAction.success) { - return - } - + this.webSocket.subscribe('init-response', (action) => { // Store agent names for tool renderer (merge backend and local agents) - if (parsedAction.data.agentNames) { + if (action.agentNames) { const localAgentNames = getLoadedAgentNames() this.agentNames = { - ...parsedAction.data.agentNames, + ...action.agentNames, ...localAgentNames, } } // Log the message if it's defined - if (parsedAction.data.message) { - console.log(`\n${parsedAction.data.message}`) + if (action.message) { + console.log(`\n${action.message}`) this.freshPrompt() } // Set initial usage data from the init response - this.setUsage(parsedAction.data) + this.setUsage(action) }) const initAction: ClientAction<'init'> = { diff --git a/packages/agent-runtime/src/__tests__/cost-aggregation.test.ts b/packages/agent-runtime/src/__tests__/cost-aggregation.test.ts index 7c670e6266..4c1d405d0e 100644 --- a/packages/agent-runtime/src/__tests__/cost-aggregation.test.ts +++ b/packages/agent-runtime/src/__tests__/cost-aggregation.test.ts @@ -54,20 +54,6 @@ describe('Cost Aggregation System', () => { let params: ParamsExcluding beforeEach(() => { - params = { - ...TEST_AGENT_RUNTIME_IMPL, - repoId: undefined, - repoUrl: undefined, - previousToolCallFinished: Promise.resolve(), - fileContext: mockFileContext, - clientSessionId: 'test-session', - userInputId: 'test-input', - ancestorRunIds: [], - signal: new AbortController().signal, - writeToClient: () => {}, - getLatestState: () => ({ messages: [] }), - } - // Setup mock agent template mockAgentTemplate = { id: 'test-agent', @@ -88,6 +74,25 @@ describe('Cost Aggregation System', () => { 'test-agent': mockAgentTemplate, } + params = { + ...TEST_AGENT_RUNTIME_IMPL, + agentTemplate: mockAgentTemplate, + ancestorRunIds: [], + clientSessionId: 'test-session', + fileContext: mockFileContext, + fingerprintId: 'test-fingerprint', + localAgentTemplates: mockLocalAgentTemplates, + previousToolCallFinished: Promise.resolve(), + repoId: undefined, + repoUrl: undefined, + signal: new AbortController().signal, + userId: 'test-user', + userInputId: 'test-input', + writeToClient: () => {}, + + getLatestState: () => ({ messages: [] }), + } + // Mock getAgentTemplate to return our mock template spyOn(agentRegistry, 'getAgentTemplate').mockResolvedValue( mockAgentTemplate, @@ -145,7 +150,6 @@ describe('Cost Aggregation System', () => { const mockValidatedState = { fingerprintId: 'test-fingerprint', userId: 'test-user', - agentTemplate: mockAgentTemplate, localAgentTemplates: mockLocalAgentTemplates, messages: [], agentState: parentAgentState, diff --git a/packages/agent-runtime/src/__tests__/malformed-tool-call.test.ts b/packages/agent-runtime/src/__tests__/malformed-tool-call.test.ts index a3bf9b338c..c34d5bfb0f 100644 --- a/packages/agent-runtime/src/__tests__/malformed-tool-call.test.ts +++ b/packages/agent-runtime/src/__tests__/malformed-tool-call.test.ts @@ -81,6 +81,7 @@ describe('malformed tool call error handling', () => { onResponseChunk: mock(() => {}), onCostCalculated: mock(async () => {}), fullResponse: '', + prompt: '', signal: new AbortController().signal, } 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 960a8b4be8..2a45ed18bc 100644 --- a/packages/agent-runtime/src/__tests__/run-programmatic-step.test.ts +++ b/packages/agent-runtime/src/__tests__/run-programmatic-step.test.ts @@ -4,7 +4,7 @@ import { TEST_AGENT_RUNTIME_IMPL } from '@codebuff/common/testing/impl/agent-run import { getInitialSessionState } from '@codebuff/common/types/session-state' import { assistantMessage, - toolJsonContent, + jsonToolResult, userMessage, } from '@codebuff/common/util/messages' import { @@ -319,14 +319,12 @@ describe('runProgrammaticStep', () => { role: 'tool', toolName: 'find_files', toolCallId: 'find-files-call-id', - content: [ - toolJsonContent({ - files: [ - { path: 'src/auth.ts', relevance: 0.9 }, - { path: 'src/login.ts', relevance: 0.8 }, - ], - }), - ], + content: jsonToolResult({ + files: [ + { path: 'src/auth.ts', relevance: 0.9 }, + { path: 'src/login.ts', relevance: 0.8 }, + ], + }), } options.toolResults.push(toolResult) diff --git a/packages/agent-runtime/src/__tests__/spawn-agents-message-history.test.ts b/packages/agent-runtime/src/__tests__/spawn-agents-message-history.test.ts index 80cab98955..a8bb1f6c8f 100644 --- a/packages/agent-runtime/src/__tests__/spawn-agents-message-history.test.ts +++ b/packages/agent-runtime/src/__tests__/spawn-agents-message-history.test.ts @@ -35,7 +35,11 @@ describe('Spawn Agents Message History', () => { let handleSpawnAgentsBaseParams: ParamsExcluding< typeof handleSpawnAgents, - 'toolCall' | 'state' | 'getLatestState' + | 'agentTemplate' + | 'getLatestState' + | 'localAgentTemplates' + | 'state' + | 'toolCall' > let baseState: Omit< ParamsOf['state'], @@ -66,20 +70,20 @@ describe('Spawn Agents Message History', () => { handleSpawnAgentsBaseParams = { ...TEST_AGENT_RUNTIME_IMPL, + ancestorRunIds: [], + clientSessionId: 'test-session', + fingerprintId: 'test-fingerprint', + fileContext: mockFileContext, repoId: undefined, repoUrl: undefined, previousToolCallFinished: Promise.resolve(), - fileContext: mockFileContext, - clientSessionId: 'test-session', + signal: new AbortController().signal, + userId: TEST_USER_ID, userInputId: 'test-input', - ancestorRunIds: [], writeToClient: () => {}, - signal: new AbortController().signal, } baseState = { - fingerprintId: 'test-fingerprint', - userId: TEST_USER_ID, sendSubagentChunk: mockSendSubagentChunk, system: 'Test system prompt', } @@ -141,12 +145,12 @@ describe('Spawn Agents Message History', () => { const { result } = handleSpawnAgents({ ...handleSpawnAgentsBaseParams, + agentTemplate: parentAgent, + localAgentTemplates: { 'child-agent': childAgent }, toolCall, getLatestState: () => ({ messages: mockMessages }), state: { ...baseState, - agentTemplate: parentAgent, - localAgentTemplates: { 'child-agent': childAgent }, messages: mockMessages, agentState: sessionState.mainAgentState, }, @@ -206,12 +210,12 @@ describe('Spawn Agents Message History', () => { const { result } = handleSpawnAgents({ ...handleSpawnAgentsBaseParams, + agentTemplate: parentAgent, + localAgentTemplates: { 'child-agent': childAgent }, toolCall, getLatestState: () => ({ messages: mockMessages }), state: { ...baseState, - agentTemplate: parentAgent, - localAgentTemplates: { 'child-agent': childAgent }, messages: mockMessages, agentState: sessionState.mainAgentState, }, @@ -233,12 +237,12 @@ describe('Spawn Agents Message History', () => { const { result } = handleSpawnAgents({ ...handleSpawnAgentsBaseParams, + agentTemplate: parentAgent, + localAgentTemplates: { 'child-agent': childAgent }, toolCall, getLatestState: () => ({ messages: mockMessages }), state: { ...baseState, - agentTemplate: parentAgent, - localAgentTemplates: { 'child-agent': childAgent }, messages: mockMessages, agentState: sessionState.mainAgentState, }, @@ -263,12 +267,12 @@ describe('Spawn Agents Message History', () => { const { result } = handleSpawnAgents({ ...handleSpawnAgentsBaseParams, + agentTemplate: parentAgent, + localAgentTemplates: { 'child-agent': childAgent }, toolCall, getLatestState: () => ({ messages: mockMessages }), state: { ...baseState, - agentTemplate: parentAgent, - localAgentTemplates: { 'child-agent': childAgent }, messages: mockMessages, agentState: sessionState.mainAgentState, }, diff --git a/packages/agent-runtime/src/__tests__/spawn-agents-permissions.test.ts b/packages/agent-runtime/src/__tests__/spawn-agents-permissions.test.ts index 32170d63a0..71a9055a37 100644 --- a/packages/agent-runtime/src/__tests__/spawn-agents-permissions.test.ts +++ b/packages/agent-runtime/src/__tests__/spawn-agents-permissions.test.ts @@ -1,6 +1,7 @@ import { TEST_USER_ID } from '@codebuff/common/old-constants' import { TEST_AGENT_RUNTIME_IMPL } from '@codebuff/common/testing/impl/agent-runtime' import { getInitialSessionState } from '@codebuff/common/types/session-state' +import { assistantMessage } from '@codebuff/common/util/messages' import { describe, expect, @@ -23,14 +24,13 @@ import type { ParamsExcluding, ParamsOf, } from '@codebuff/common/types/function-params' -import { assistantMessage } from '@codebuff/common/util/messages' describe('Spawn Agents Permissions', () => { let mockSendSubagentChunk: any let mockLoopAgentSteps: any let handleSpawnAgentsBaseParams: ParamsExcluding< typeof handleSpawnAgents, - 'toolCall' | 'state' + 'toolCall' | 'state' | 'agentTemplate' | 'localAgentTemplates' > let baseState: Omit< ParamsOf['state'], @@ -64,21 +64,22 @@ describe('Spawn Agents Permissions', () => { beforeEach(() => { handleSpawnAgentsBaseParams = { ...TEST_AGENT_RUNTIME_IMPL, + ancestorRunIds: [], + clientSessionId: 'test-session', + fileContext: mockFileContext, + fingerprintId: 'test-fingerprint', + previousToolCallFinished: Promise.resolve(), repoId: undefined, repoUrl: undefined, - previousToolCallFinished: Promise.resolve(), - fileContext: mockFileContext, - clientSessionId: 'test-session', + signal: new AbortController().signal, + userId: TEST_USER_ID, userInputId: 'test-input', - ancestorRunIds: [], writeToClient: () => {}, + getLatestState: () => ({ messages: [] }), - signal: new AbortController().signal, } baseState = { - fingerprintId: 'test-fingerprint', - userId: TEST_USER_ID, sendSubagentChunk: mockSendSubagentChunk, messages: [], system: 'Test system prompt', @@ -95,9 +96,7 @@ describe('Spawn Agents Permissions', () => { return { agentState: { ...options.agentState, - messageHistory: [ - assistantMessage('Mock agent response'), - ], + messageHistory: [assistantMessage('Mock agent response')], }, output: { type: 'lastMessage', value: 'Mock agent response' }, } @@ -269,11 +268,11 @@ describe('Spawn Agents Permissions', () => { const { result } = handleSpawnAgents({ ...handleSpawnAgentsBaseParams, + agentTemplate: parentAgent, + localAgentTemplates: { thinker: childAgent }, toolCall, state: { ...baseState, - agentTemplate: parentAgent, - localAgentTemplates: { thinker: childAgent }, agentState: sessionState.mainAgentState, }, }) @@ -291,11 +290,11 @@ describe('Spawn Agents Permissions', () => { const { result } = handleSpawnAgents({ ...handleSpawnAgentsBaseParams, + agentTemplate: parentAgent, + localAgentTemplates: { reviewer: childAgent }, toolCall, state: { ...baseState, - agentTemplate: parentAgent, - localAgentTemplates: { reviewer: childAgent }, agentState: sessionState.mainAgentState, }, }) @@ -315,11 +314,11 @@ describe('Spawn Agents Permissions', () => { const { result } = handleSpawnAgents({ ...handleSpawnAgentsBaseParams, + agentTemplate: parentAgent, + localAgentTemplates: {}, // Empty - agent not found toolCall, state: { ...baseState, - agentTemplate: parentAgent, - localAgentTemplates: {}, // Empty - agent not found agentState: sessionState.mainAgentState, }, }) @@ -341,11 +340,11 @@ describe('Spawn Agents Permissions', () => { const { result } = handleSpawnAgents({ ...handleSpawnAgentsBaseParams, + agentTemplate: parentAgent, + localAgentTemplates: { 'codebuff/thinker@1.0.0': childAgent }, toolCall, state: { ...baseState, - agentTemplate: parentAgent, - localAgentTemplates: { 'codebuff/thinker@1.0.0': childAgent }, agentState: sessionState.mainAgentState, }, }) @@ -363,14 +362,14 @@ describe('Spawn Agents Permissions', () => { const { result } = handleSpawnAgents({ ...handleSpawnAgentsBaseParams, + agentTemplate: parentAgent, + localAgentTemplates: { + thinker: childAgent, + 'codebuff/thinker@1.0.0': childAgent, // Register with both keys + }, toolCall, state: { ...baseState, - agentTemplate: parentAgent, - localAgentTemplates: { - thinker: childAgent, - 'codebuff/thinker@1.0.0': childAgent, // Register with both keys - }, agentState: sessionState.mainAgentState, }, }) @@ -388,11 +387,11 @@ describe('Spawn Agents Permissions', () => { const { result } = handleSpawnAgents({ ...handleSpawnAgentsBaseParams, + agentTemplate: parentAgent, + localAgentTemplates: { 'codebuff/thinker@2.0.0': childAgent }, toolCall, state: { ...baseState, - agentTemplate: parentAgent, - localAgentTemplates: { 'codebuff/thinker@2.0.0': childAgent }, agentState: sessionState.mainAgentState, }, }) @@ -424,14 +423,14 @@ describe('Spawn Agents Permissions', () => { const { result } = handleSpawnAgents({ ...handleSpawnAgentsBaseParams, + agentTemplate: parentAgent, + localAgentTemplates: { + thinker: thinkerAgent, + reviewer: reviewerAgent, + }, toolCall, state: { ...baseState, - agentTemplate: parentAgent, - localAgentTemplates: { - thinker: thinkerAgent, - reviewer: reviewerAgent, - }, agentState: sessionState.mainAgentState, }, }) @@ -467,11 +466,11 @@ describe('Spawn Agents Permissions', () => { const { result } = handleSpawnAgentInline({ ...handleSpawnAgentsBaseParams, + agentTemplate: parentAgent, + localAgentTemplates: { thinker: childAgent }, toolCall, state: { ...baseState, - agentTemplate: parentAgent, - localAgentTemplates: { thinker: childAgent }, agentState: sessionState.mainAgentState, }, }) @@ -488,11 +487,11 @@ describe('Spawn Agents Permissions', () => { const { result } = handleSpawnAgentInline({ ...handleSpawnAgentsBaseParams, + agentTemplate: parentAgent, + localAgentTemplates: { reviewer: childAgent }, toolCall, state: { ...baseState, - agentTemplate: parentAgent, - localAgentTemplates: { reviewer: childAgent }, agentState: sessionState.mainAgentState, }, }) @@ -510,11 +509,11 @@ describe('Spawn Agents Permissions', () => { const { result } = handleSpawnAgentInline({ ...handleSpawnAgentsBaseParams, + agentTemplate: parentAgent, + localAgentTemplates: {}, // Empty - agent not found toolCall, state: { ...baseState, - agentTemplate: parentAgent, - localAgentTemplates: {}, // Empty - agent not found agentState: sessionState.mainAgentState, }, }) @@ -531,11 +530,11 @@ describe('Spawn Agents Permissions', () => { const { result } = handleSpawnAgentInline({ ...handleSpawnAgentsBaseParams, + agentTemplate: parentAgent, + localAgentTemplates: { 'codebuff/thinker@1.0.0': childAgent }, toolCall, state: { ...baseState, - agentTemplate: parentAgent, - localAgentTemplates: { 'codebuff/thinker@1.0.0': childAgent }, agentState: sessionState.mainAgentState, }, }) @@ -552,14 +551,14 @@ describe('Spawn Agents Permissions', () => { const { result } = handleSpawnAgentInline({ ...handleSpawnAgentsBaseParams, + agentTemplate: parentAgent, + localAgentTemplates: { + thinker: childAgent, + 'codebuff/thinker@1.0.0': childAgent, // Register with both keys + }, toolCall, state: { ...baseState, - agentTemplate: parentAgent, - localAgentTemplates: { - thinker: childAgent, - 'codebuff/thinker@1.0.0': childAgent, // Register with both keys - }, agentState: sessionState.mainAgentState, }, }) @@ -576,11 +575,11 @@ describe('Spawn Agents Permissions', () => { const { result } = handleSpawnAgentInline({ ...handleSpawnAgentsBaseParams, + agentTemplate: parentAgent, + localAgentTemplates: { 'codebuff/thinker@2.0.0': childAgent }, toolCall, state: { ...baseState, - agentTemplate: parentAgent, - localAgentTemplates: { 'codebuff/thinker@2.0.0': childAgent }, agentState: sessionState.mainAgentState, }, }) @@ -590,25 +589,5 @@ describe('Spawn Agents Permissions', () => { ) expect(mockLoopAgentSteps).not.toHaveBeenCalled() }) - - it('should validate required state parameters for inline spawn', async () => { - const parentAgent = createMockAgent('parent', ['thinker']) - const toolCall = createInlineSpawnToolCall('thinker') - - expect(() => { - handleSpawnAgentInline({ - ...handleSpawnAgentsBaseParams, - toolCall, - state: { - // Missing required fields like fingerprintId, etc. - agentTemplate: parentAgent, - localAgentTemplates: {}, - }, - }) - }).toThrow( - 'Internal error for spawn_agent_inline: Missing fingerprintId in state', - ) - expect(mockLoopAgentSteps).not.toHaveBeenCalled() - }) }) }) diff --git a/packages/agent-runtime/src/__tests__/subagent-streaming.test.ts b/packages/agent-runtime/src/__tests__/subagent-streaming.test.ts index a09ef908cd..0751d259df 100644 --- a/packages/agent-runtime/src/__tests__/subagent-streaming.test.ts +++ b/packages/agent-runtime/src/__tests__/subagent-streaming.test.ts @@ -36,7 +36,7 @@ describe('Subagent Streaming', () => { > let handleSpawnAgentsBaseParams: ParamsExcluding< typeof handleSpawnAgents, - 'toolCall' | 'state' + 'toolCall' | 'state' | 'agentTemplate' | 'localAgentTemplates' > let baseState: Omit< ParamsOf['state'], @@ -67,21 +67,21 @@ describe('Subagent Streaming', () => { handleSpawnAgentsBaseParams = { ...TEST_AGENT_RUNTIME_IMPL, + ancestorRunIds: [], + clientSessionId: 'test-session', + fileContext: mockFileContext, + fingerprintId: 'test-fingerprint', + previousToolCallFinished: Promise.resolve(), repoId: undefined, repoUrl: undefined, - previousToolCallFinished: Promise.resolve(), - fileContext: mockFileContext, - clientSessionId: 'test-session', + signal: new AbortController().signal, + userId: TEST_USER_ID, userInputId: 'test-input', writeToClient: mockWriteToClient, - ancestorRunIds: [], getLatestState: () => ({ messages: [] }), - signal: new AbortController().signal, } baseState = { - fingerprintId: 'test-fingerprint', - userId: TEST_USER_ID, sendSubagentChunk: mockSendSubagentChunk, messages: [], system: 'Test system prompt', @@ -160,13 +160,13 @@ describe('Subagent Streaming', () => { const { result } = handleSpawnAgents({ ...handleSpawnAgentsBaseParams, + agentTemplate: parentTemplate, + localAgentTemplates: { + [mockAgentTemplate.id]: mockAgentTemplate, + }, toolCall, state: { ...baseState, - agentTemplate: parentTemplate, - localAgentTemplates: { - [mockAgentTemplate.id]: mockAgentTemplate, - }, agentState, }, }) @@ -214,13 +214,13 @@ describe('Subagent Streaming', () => { const { result } = handleSpawnAgents({ ...handleSpawnAgentsBaseParams, + agentTemplate: parentTemplate, + localAgentTemplates: { + [mockAgentTemplate.id]: mockAgentTemplate, + }, toolCall, state: { ...baseState, - agentTemplate: parentTemplate, - localAgentTemplates: { - [mockAgentTemplate.id]: mockAgentTemplate, - }, agentState, }, }) diff --git a/packages/agent-runtime/src/main-prompt.ts b/packages/agent-runtime/src/main-prompt.ts index cd4e1459f2..bbc2df56ce 100644 --- a/packages/agent-runtime/src/main-prompt.ts +++ b/packages/agent-runtime/src/main-prompt.ts @@ -119,7 +119,7 @@ export async function mainPrompt( max: AgentTemplateTypes.base_max, experimental: 'base2', } satisfies Record - )[costMode] + )[costMode ?? 'normal'] } } diff --git a/packages/agent-runtime/src/run-agent-step.ts b/packages/agent-runtime/src/run-agent-step.ts index 3b327aad1b..c66f12737f 100644 --- a/packages/agent-runtime/src/run-agent-step.ts +++ b/packages/agent-runtime/src/run-agent-step.ts @@ -38,7 +38,10 @@ import type { ParamsExcluding, ParamsOf, } from '@codebuff/common/types/function-params' -import type { Message, ToolMessage } from '@codebuff/common/types/messages/codebuff-message' +import type { + Message, + ToolMessage, +} from '@codebuff/common/types/messages/codebuff-message' import type { TextPart, ImagePart, @@ -336,22 +339,22 @@ export const runAgentStep = async ( const stream = getStream([systemMessage(system), ...agentMessages]) const { - toolCalls, - toolResults: newToolResults, - state, fullResponse: fullResponseAfterStream, fullResponseChunks, messageId, + state, + toolCalls, + toolResults: newToolResults, } = await processStreamWithTools({ ...params, - stream, - agentStepId, + agentContext, agentState, - repoId, - messages: agentMessages, + agentStepId, agentTemplate, - agentContext, fullResponse, + messages: agentMessages, + repoId, + stream, onCostCalculated, }) toolResults.push(...newToolResults) diff --git a/packages/agent-runtime/src/run-programmatic-step.ts b/packages/agent-runtime/src/run-programmatic-step.ts index d22e767066..8198910277 100644 --- a/packages/agent-runtime/src/run-programmatic-step.ts +++ b/packages/agent-runtime/src/run-programmatic-step.ts @@ -48,6 +48,7 @@ export async function runProgrammaticStep( repoUrl: string | undefined userInputId: string fingerprintId: string + clientSessionId: string onResponseChunk: (chunk: string | PrintModeEvent) => void localAgentTemplates: Record stepsComplete: boolean @@ -92,6 +93,7 @@ export async function runProgrammaticStep( const { agentState, template, + clientSessionId, prompt, toolCallParams, nResponses, @@ -178,7 +180,7 @@ export async function runProgrammaticStep( userId, agentTemplate: template, localAgentTemplates, - system, + system: system ?? '', sendSubagentChunk: (data: { userInputId: string agentId: string @@ -200,6 +202,17 @@ export async function runProgrammaticStep( }), agentContext: cloneDeep(agentState.agentContext), messages: cloneDeep(agentState.messageHistory), + promisesByPath: {}, + allPromises: [], + fileChangeErrors: [], + fileChanges: [], + firstFileProcessed: false, + repoId: undefined, + logger, + prompt, + fullResponse: '', + clientSessionId, + userInputId, } let toolResult: ToolResultOutput[] | undefined = undefined diff --git a/packages/agent-runtime/src/system-prompt/prompts.ts b/packages/agent-runtime/src/system-prompt/prompts.ts index f8584af932..f91c011e42 100644 --- a/packages/agent-runtime/src/system-prompt/prompts.ts +++ b/packages/agent-runtime/src/system-prompt/prompts.ts @@ -251,66 +251,3 @@ ${truncateString(gitChanges.lastCommitMessages, maxLength / 10)} ${closeXml('git_commit_messages_most_recent_first')} `.trim() } - -export const gitCommitGuidePrompt = ` -### Using git to commit changes - -When the user requests a new git commit, please follow these steps closely: - -1. **Run two run_terminal_command tool calls:** - - Run \`git diff\` to review both staged and unstaged modifications. - - Run \`git log\` to check recent commit messages, ensuring consistency with this repository's style. - -2. **Select relevant files to include in the commit:** - Use the git context established at the start of this conversation to decide which files are pertinent to the changes. Stage any new untracked files that are relevant, but avoid committing previously modified files (from the beginning of the conversation) unless they directly relate to this commit. - -3. **Analyze the staged changes and compose a commit message:** - Enclose your analysis in tags. Within these tags, you should: - - Note which files have been altered or added. - - Categorize the nature of the changes (e.g., new feature, fix, refactor, documentation, etc.). - - Consider the purpose or motivation behind the alterations. - - Refrain from using tools to inspect code beyond what is presented in the git context. - - Evaluate the overall impact on the project. - - Check for sensitive details that should not be committed. - - Draft a concise, one- to two-sentence commit message focusing on the “why” rather than the “what.” - - Use precise, straightforward language that accurately represents the changes. - - Ensure the message provides clarity—avoid generic or vague terms like “Update” or “Fix” without context. - - Revisit your draft to confirm it truly reflects the changes and their intention. - -4. **Create the commit, ending with this specific footer:** - \`\`\` - Generated with Codebuff 🤖 - Co-Authored-By: Codebuff - \`\`\` - To maintain proper formatting, use cross-platform compatible commit messages: - - **For Unix/bash shells:** - \`\`\` - git commit -m "$(cat <<'EOF' - Your commit message here. - - 🤖 Generated with Codebuff - Co-Authored-By: Codebuff - EOF - )" - \`\`\` - - **For Windows Command Prompt:** - \`\`\` - git commit -m "Your commit message here. - - 🤖 Generated with Codebuff - Co-Authored-By: Codebuff " - \`\`\` - - Always detect the platform and use the appropriate syntax. HEREDOC syntax (\`<<'EOF'\`) only works in bash/Unix shells and will fail on Windows Command Prompt. - -**Important details** - -- When feasible, use a single \`git commit -am\` command to add and commit together, but do not accidentally stage unrelated files. -- Never alter the git config. -- Do not push to the remote repository. -- Avoid using interactive flags (e.g., \`-i\`) that require unsupported interactive input. -- Do not create an empty commit if there are no changes. -- Make sure your commit message is concise yet descriptive, focusing on the intention behind the changes rather than merely describing them. -` diff --git a/packages/agent-runtime/src/tools/definitions/list.ts b/packages/agent-runtime/src/tools/definitions/list.ts deleted file mode 100644 index a85f4dac07..0000000000 --- a/packages/agent-runtime/src/tools/definitions/list.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { $toolParams } from '@codebuff/common/tools/list' - -import { addMessageTool } from './tool/add-message' -import { addSubgoalTool } from './tool/add-subgoal' -import { browserLogsTool } from './tool/browser-logs' -import { codeSearchTool } from './tool/code-search' -import { createPlanTool } from './tool/create-plan' -import { endTurnTool } from './tool/end-turn' -import { findFilesTool } from './tool/find-files' -import { globTool } from './tool/glob' -import { listDirectoryTool } from './tool/list-directory' -import { lookupAgentInfoTool } from './tool/lookup-agent-info' -import { readDocsTool } from './tool/read-docs' -import { readFilesTool } from './tool/read-files' -import { readSubtreeTool } from './tool/read-subtree' -import { runFileChangeHooksTool } from './tool/run-file-change-hooks' -import { runTerminalCommandTool } from './tool/run-terminal-command' -import { setMessagesTool } from './tool/set-messages' -import { setOutputTool } from './tool/set-output' -import { spawnAgentInlineTool } from './tool/spawn-agent-inline' -import { spawnAgentsTool } from './tool/spawn-agents' -import { strReplaceTool } from './tool/str-replace' -import { taskCompletedTool } from './tool/task-completed' -import { thinkDeeplyTool } from './tool/think-deeply' -import { updateSubgoalTool } from './tool/update-subgoal' -import { webSearchTool } from './tool/web-search' -import { writeFileTool } from './tool/write-file' -import { writeTodosTool } from './tool/write-todos' - -import type { ToolDescription } from './tool-def-type' -import type { ToolName } from '@codebuff/common/tools/constants' -import type { ToolSet } from 'ai' - -const toolDescriptions = { - add_message: addMessageTool, - add_subgoal: addSubgoalTool, - browser_logs: browserLogsTool, - code_search: codeSearchTool, - create_plan: createPlanTool, - end_turn: endTurnTool, - find_files: findFilesTool, - glob: globTool, - list_directory: listDirectoryTool, - lookup_agent_info: lookupAgentInfoTool, - read_docs: readDocsTool, - read_files: readFilesTool, - read_subtree: readSubtreeTool, - run_file_change_hooks: runFileChangeHooksTool, - run_terminal_command: runTerminalCommandTool, - set_messages: setMessagesTool, - set_output: setOutputTool, - spawn_agents: spawnAgentsTool, - spawn_agent_inline: spawnAgentInlineTool, - str_replace: strReplaceTool, - task_completed: taskCompletedTool, - think_deeply: thinkDeeplyTool, - update_subgoal: updateSubgoalTool, - web_search: webSearchTool, - write_file: writeFileTool, - write_todos: writeTodosTool, -} satisfies { - [K in ToolName]: ToolDescription -} - -export type ToolDefinition = { - [K in ToolName]: (typeof toolDescriptions)[K] & (typeof $toolParams)[K] -}[T] - -export const codebuffToolDefs = Object.fromEntries( - Object.entries(toolDescriptions).map(([toolName, toolDescription]) => [ - toolName, - { - ...toolDescriptions[toolName as ToolName], - ...$toolParams[toolName as ToolName], - } satisfies ToolDefinition, - ]), -) as { [K in ToolName]: ToolDefinition } satisfies ToolSet diff --git a/packages/agent-runtime/src/tools/definitions/tool-def-type.ts b/packages/agent-runtime/src/tools/definitions/tool-def-type.ts deleted file mode 100644 index 1c80750ac4..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool-def-type.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { ToolName } from '@codebuff/common/tools/constants' - -export type ToolDescription = { - toolName: T - description: string -} diff --git a/packages/agent-runtime/src/tools/definitions/tool/add-message.ts b/packages/agent-runtime/src/tools/definitions/tool/add-message.ts deleted file mode 100644 index e4452416ac..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/add-message.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'add_message' -export const addMessageTool = { - toolName, - description: ` -Example: -${getToolCallString(toolName, { - role: 'user', - content: 'Hello, how are you?', -})} - `.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/definitions/tool/add-subgoal.ts b/packages/agent-runtime/src/tools/definitions/tool/add-subgoal.ts deleted file mode 100644 index 0bd53c800c..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/add-subgoal.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'add_subgoal' -export const addSubgoalTool = { - toolName, - description: ` -Example: -${getToolCallString(toolName, { - id: '1', - objective: 'Add a new "deploy api" subgoal', - status: 'IN_PROGRESS', -})} -`.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/definitions/tool/browser-logs.ts b/packages/agent-runtime/src/tools/definitions/tool/browser-logs.ts deleted file mode 100644 index 171d55affa..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/browser-logs.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'browser_logs' -export const browserLogsTool = { - toolName, - description: ` -Purpose: Use this tool to check the output of console.log or errors in order to debug issues, test functionality, or verify expected behavior. - -IMPORTANT: Assume the user's development server is ALREADY running and active, unless you see logs indicating otherwise. Never start the user's development server for them, unless they ask you to do so. -Never offer to interact with the website aside from reading them (see available actions below). The user will manipulate the website themselves and bring you to the UI they want you to interact with. - -### Response Analysis - -After each action, you'll receive: -1. Success/failure status -2. New console logs since last action -3. Network requests and responses -4. JavaScript errors with stack traces - -Use this data to: -- Verify expected behavior -- Debug issues -- Guide next actions -- Make informed decisions about fixes - -### Best Practices - -**Workflow** -- Navigate to the user's website, probably on localhost, but you can compare with the production site if you want. -- Scroll to the relevant section -- Take screenshots and analyze confirm changes -- Check network requests for anomalies - -**Debugging Flow** -- Start with minimal reproduction steps -- Collect data at each step -- Analyze results before next action -- Take screenshots to track your changes after each UI change you make - -There is currently only one type of browser action available: -Navigate: - - Load a new URL in the current browser window and get the logs after page load. - Params: - - \`type\`: (required) Must be equal to 'navigate' - - \`url\`: (required) The URL to navigate to. - - \`waitUntil\`: (required) One of 'load', 'domcontentloaded', 'networkidle0' - -Example: -${getToolCallString(toolName, { - type: 'navigate', - url: 'localhost:3000', - waitUntil: 'domcontentloaded', -})} - `.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/definitions/tool/code-search.ts b/packages/agent-runtime/src/tools/definitions/tool/code-search.ts deleted file mode 100644 index 70fdeed787..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/code-search.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'code_search' -export const codeSearchTool = { - toolName, - description: ` -Purpose: Search through code files to find files with specific text patterns, function names, variable names, and more. - -Prefer to use read_files instead of code_search unless you need to search for a specific pattern in multiple files. - -Use cases: -1. Finding all references to a function, class, or variable name across the codebase -2. Searching for specific code patterns or implementations -3. Looking up where certain strings or text appear -4. Finding files that contain specific imports or dependencies -5. Locating configuration settings or environment variables - -The pattern supports regular expressions and will search recursively through all files in the project by default. Some tips: -- Be as constraining in the pattern as possible to limit the number of files returned, e.g. if searching for the definition of a function, use "(function foo|const foo)" or "def foo" instead of merely "foo". -- Use Rust-style regex, not grep-style, PCRE, RE2 or JavaScript regex - you must always escape special characters like { and } -- Be as constraining as possible to limit results, e.g. use "(function foo|const foo)" or "def foo" instead of merely "foo" -- Add context to your search with surrounding terms (e.g., "function handleAuth" rather than just "handleAuth") -- Use word boundaries (\\b) to match whole words only -- Use the cwd parameter to narrow your search to specific directories -- For case-sensitive searches like constants (e.g., ERROR vs error), omit the "-i" flag -- Searches file content and filenames -- Automatically ignores binary files, hidden files, and files in .gitignore - - -Advanced ripgrep flags (use the flags parameter): - -- Case sensitivity: "-i" for case-insensitive search -- File type filtering: "-t ts -t js" (TypeScript and JavaScript), "-t py" (Python), etc. -- Exclude file types: "--type-not py" to exclude Python files -- Context lines: "-A 3" (3 lines after), "-B 2" (2 lines before), "-C 2" (2 lines before and after) -- Line numbers: "-n" to show line numbers -- Count matches: "-c" to count matches per file -- Only filenames: "-l" to show only filenames with matches -- Invert match: "-v" to show lines that don't match -- Word boundaries: "-w" to match whole words only -- Fixed strings: "-F" to treat pattern as literal string (not regex) - -Note: Do not use the end_turn tool after this tool! You will want to see the output of this tool before ending your turn. - -RESULT LIMITING: - -- The maxResults parameter limits the number of results shown per file (default: 15) -- There is also a global limit of 250 total results across all files -- These limits allow you to see results across multiple files without being overwhelmed by matches in a single file -- If a file has more matches than maxResults, you'll see a truncation notice indicating how many results were found -- If the global limit is reached, remaining files will be skipped - -Examples: -${getToolCallString(toolName, { pattern: 'foo' })} -${getToolCallString(toolName, { pattern: 'foo\\.bar = 1\\.0' })} -${getToolCallString(toolName, { pattern: 'import.*foo', cwd: 'src' })} -${getToolCallString(toolName, { pattern: 'function.*authenticate', flags: '-i -t ts -t js' })} -${getToolCallString(toolName, { pattern: 'TODO', flags: '-n --type-not py' })} -${getToolCallString(toolName, { pattern: 'getUserData', maxResults: 10 })} - `.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/definitions/tool/create-plan.ts b/packages/agent-runtime/src/tools/definitions/tool/create-plan.ts deleted file mode 100644 index 732d84f406..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/create-plan.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'create_plan' -const endsAgentStep = false -export const createPlanTool = { - toolName, - description: ` -Use when: -- User explicitly requests a detailed plan. -- Use this tool to overwrite a previous plan by using the exact same file name. - -Don't include: -- Goals, timelines, benefits, next steps. -- Background context or extensive explanations. - -For a technical plan, act as an expert architect engineer and provide direction to your editor engineer. -- Study the change request and the current code. -- Describe how to modify the code to complete the request. The editor engineer will rely solely on your instructions, so make them unambiguous and complete. -- Explain all needed code changes clearly and completely, but concisely. -- Just show the changes needed. - -What to include in the plan: -- Include key snippets of code -- not full files of it. Use pseudo code. For example, include interfaces between modules, function signatures, and other code that is not immediately obvious should be written out explicitly. Function and method bodies could be written out in psuedo code. -- Do not waste time on much background information, focus on the exact steps of the implementation. -- Do not wrap the path content in markdown code blocks, e.g. \`\`\`. - -Do not include any of the following sections in the plan: -- goals -- a timeline or schedule -- benefits/key improvements -- next steps - -After creating the plan, you should end turn to let the user review the plan. - -Important: Use this tool sparingly. Do not use this tool more than once in a conversation, unless in ask mode. - -Examples: -${getToolCallString(toolName, { - path: 'feature-x-plan.md', - plan: [ - '1. Create module `auth.ts` in `/src/auth/`.', - '```ts', - 'export function authenticate(user: User): boolean { /* pseudo-code logic */ }', - '```', - '2. Refactor existing auth logic into this module.', - '3. Update imports across codebase.', - '4. Write integration tests covering new module logic.', - ].join('\n'), -})} - `.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/definitions/tool/end-turn.ts b/packages/agent-runtime/src/tools/definitions/tool/end-turn.ts deleted file mode 100644 index 86b18938b2..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/end-turn.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'end_turn' -export const endTurnTool = { - toolName, - description: ` -Only use this tool to hand control back to the user. - -- When to use: after you have completed a meaningful chunk of work and you are either (a) fully done, or (b) explicitly waiting for the user's next message. -- Do NOT use: as a stop token mid-work, to pause between tool calls, to wait for tool results, or to "check in" unnecessarily. -- Before calling: finish all pending steps, resolve tool results, and include any outputs the user needs to review. -- Effect: Signals the UI to wait for the user's reply; any pending tool results will be ignored. - -*INCORRECT USAGE*: -${getToolCallString('some_tool_that_produces_results', { query: 'some example search term' }, false)} - -${getToolCallString(toolName, {})} - -*CORRECT USAGE*: -All done! Would you like some more help with xyz? - -${getToolCallString(toolName, {})} - - `.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/definitions/tool/find-files.ts b/packages/agent-runtime/src/tools/definitions/tool/find-files.ts deleted file mode 100644 index e184fe7b83..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/find-files.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'find_files' -export const findFilesTool = { - toolName, - description: ` -Example: -${getToolCallString(toolName, { - prompt: 'The implementation of function foo', -})} - -Purpose: Better fulfill the user request by reading files which could contain information relevant to the user's request. -Use cases: -- If you are calling a function or creating a class and want to know how it works, use this tool to get the implementation. -- If you need to understand a section of the codebase, read more files in that directory or subdirectories. -- Some requests require a broad understanding of multiple parts of the codebase. Consider using find_files to gain more context before making changes. - -Don't use this tool if: -- You already know the exact path of the file(s) you are looking for — in this case, use read_files. -- You already read the files you need in context. -- You know the name of the file you need. Instead use run_terminal_command with \`find -name\` (or \`dir /s /b\` or \`Get-ChildItem -Recurse -Filter\`) - -This tool is not guaranteed to find the correct file. In general, prefer using read_files instead of find_files. - `.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/definitions/tool/glob.ts b/packages/agent-runtime/src/tools/definitions/tool/glob.ts deleted file mode 100644 index bbaa46a107..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/glob.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'glob' -export const globTool = { - toolName, - description: ` -Example: -${getToolCallString(toolName, { - pattern: '**/*.test.ts', -})} - -Purpose: Search for files matching a glob pattern to discover files by name patterns rather than content. -Use cases: -- Find all files with a specific extension (e.g., "*.js", "*.test.ts") -- Locate files in specific directories (e.g., "src/**/*.ts") -- Find files with specific naming patterns (e.g., "**/test_*.go", "**/*-config.json") -- Discover test files, configuration files, or other files with predictable naming - -Glob patterns support: -- * matches any characters except / -- ** matches any characters including / -- ? matches a single character -- [abc] matches one of the characters in brackets -- {a,b} matches one of the comma-separated patterns - -This tool is fast and works well for discovering files by name patterns. - `.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/definitions/tool/list-directory.ts b/packages/agent-runtime/src/tools/definitions/tool/list-directory.ts deleted file mode 100644 index e4dadeb967..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/list-directory.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'list_directory' -export const listDirectoryTool = { - toolName, - description: ` -Lists all files and directories in the specified path. Useful for exploring directory structure and finding files. - -Example: -${getToolCallString(toolName, { - path: 'src/components', -})} - -${getToolCallString(toolName, { - path: '.', -})} - `.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/definitions/tool/lookup-agent-info.ts b/packages/agent-runtime/src/tools/definitions/tool/lookup-agent-info.ts deleted file mode 100644 index 07c1b228a3..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/lookup-agent-info.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'lookup_agent_info' -export const lookupAgentInfoTool = { - toolName, - description: ` -Retrieve information about an agent by ID for proper spawning. Use this when you see a request with a full agent ID like "@publisher/agent-id@version" to validate the agent exists and get its metadata. Only agents that are published under a publisher and version are supported for this tool. - -Example: -${getToolCallString(toolName, { - agentId: 'codebuff/researcher@0.0.1', -})} - `.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/definitions/tool/read-docs.ts b/packages/agent-runtime/src/tools/definitions/tool/read-docs.ts deleted file mode 100644 index 319ae9abc8..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/read-docs.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'read_docs' -export const readDocsTool = { - toolName, - description: ` -Purpose: Get current, accurate documentation for popular libraries, frameworks, and technologies. This tool searches Context7's database of up-to-date documentation and returns relevant content. - -**IMPORTANT**: The \`libraryTitle\` parameter should be the exact, official name of the library or framework, not a search query. Think of it as looking up a specific book title in a library catalog. - -Correct examples: -- "Next.js" (not "nextjs tutorial" or "how to use nextjs") -- "React" (not "react hooks guide") -- "MongoDB" (not "mongodb database setup") -- "Express.js" (not "express server") - -Use cases: -- Getting current API documentation for a library -- Finding usage examples and best practices -- Understanding how to implement specific features -- Checking the latest syntax and methods - -The tool will search for the library and return the most relevant documentation content. If a topic is specified, it will focus the results on that specific area. - -Example: -${getToolCallString(toolName, { - libraryTitle: 'Next.js', - topic: 'app router', - max_tokens: 15000, -})} - -${getToolCallString(toolName, { - libraryTitle: 'MongoDB', - topic: 'database setup', -})} - `.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/definitions/tool/read-files.ts b/packages/agent-runtime/src/tools/definitions/tool/read-files.ts deleted file mode 100644 index 8d6ad4520a..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/read-files.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'read_files' -export const readFilesTool = { - toolName, - description: ` -Note: DO NOT call this tool for files you've already read! There's no need to read them again — any changes to the files will be surfaced to you as a file update tool result. - -Example: -${getToolCallString(toolName, { - paths: ['path/to/file1.ts', 'path/to/file2.ts'], -})} - `.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/definitions/tool/read-subtree.ts b/packages/agent-runtime/src/tools/definitions/tool/read-subtree.ts deleted file mode 100644 index 65e27258a5..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/read-subtree.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'read_subtree' -export const readSubtreeTool = { - toolName, - description: ` -Example: -${getToolCallString(toolName, { - paths: ['src', 'package.json'], - maxTokens: 4000, -})} - -Purpose: Read a directory subtree and return a blob containing subdirectories, file names, and parsed variable/functions names from source files. For files, return only the parsed variable names. If no paths are provided, returns the entire project tree. The output is truncated to fit within the provided token budget. - -- Use this tool on particular subdirectories when you need to know all the nested files and directories. E.g. for a refactoring task, or to understand a particular part of the codebase. -- In normal use, don't set maxTokens beyond 10,000. - `.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/definitions/tool/run-file-change-hooks.ts b/packages/agent-runtime/src/tools/definitions/tool/run-file-change-hooks.ts deleted file mode 100644 index 22f2e70f8b..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/run-file-change-hooks.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'run_file_change_hooks' -const endsAgentStep = true -export const runFileChangeHooksTool = { - toolName, - description: ` -Purpose: Trigger file change hooks defined in codebuff.json for the specified files. This tool allows the backend to request the client to run its configured file change hooks (like tests, linting, type checking) after file changes have been applied. - -Use cases: -- After making code changes, trigger the relevant tests and checks -- Ensure code quality by running configured linters and type checkers -- Validate that changes don't break the build - -The client will run only the hooks whose filePattern matches the provided files. - -Example: -${getToolCallString(toolName, { - files: ['src/components/Button.tsx', 'src/utils/helpers.ts'], -})} - `.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/definitions/tool/run-terminal-command.ts b/packages/agent-runtime/src/tools/definitions/tool/run-terminal-command.ts deleted file mode 100644 index eaf3720b52..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/run-terminal-command.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { gitCommitGuidePrompt } from '../../../system-prompt/prompts' -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'run_terminal_command' -export const runTerminalCommandTool = { - toolName, - description: ` -Stick to these use cases: -1. Typechecking the project or running build (e.g., "npm run build"). Reading the output can help you edit code to fix build errors. If possible, use an option that performs checks but doesn't emit files, e.g. \`tsc --noEmit\`. -2. Running tests (e.g., "npm test"). Reading the output can help you edit code to fix failing tests. Or, you could write new unit tests and then run them. -3. Moving, renaming, or deleting files and directories. These actions can be vital for refactoring requests. Use commands like \`mv\`/\`move\` or \`rm\`/\`del\`. - -Most likely, you should ask for permission for any other type of command you want to run. If asking for permission, show the user the command you want to run using \`\`\` tags and *do not* use the tool call format, e.g.: -\`\`\`bash -git branch -D foo -\`\`\` - -DO NOT do any of the following: -1. Run commands that can modify files outside of the project directory, install packages globally, install virtual environments, or have significant side effects outside of the project directory, unless you have explicit permission from the user. Treat anything outside of the project directory as read-only. -2. Run \`git push\` because it can break production (!) if the user was not expecting it. Don't run \`git commit\`, \`git rebase\`, or related commands unless you get explicit permission. If a user asks to commit changes, you can do so, but you should not invoke any further git commands beyond the git commit command. -3. Run scripts without asking. Especially don't run scripts that could run against the production environment or have permanent effects without explicit permission from the user. -4. Be careful with any command that has big or irreversible effects. Anything that touches a production environment, servers, the database, or other systems that could be affected by a command should be run with explicit permission from the user. -5. Use the run_terminal_command tool to create or edit files. Do not use \`cat\` or \`echo\` to create or edit files. You should instead use other tools for creating or editing files. -6. Use the wrong package manager for the project. For example, if the project uses \`pnpm\` or \`bun\` or \`yarn\`, you should not use \`npm\`. Similarly not everyone uses \`pip\` for python, etc. - -Do: -- If there's an opportunity to use "-y" or "--yes" flags, use them. Any command that prompts for confirmation will hang if you don't use the flags. - -Notes: -- If the user references a specific file, it could be either from their cwd or from the project root. You **must** determine which they are referring to (either infer or ask). Then, you must specify the path relative to the project root (or use the cwd parameter) -- Commands can succeed without giving any output, e.g. if no type errors were found. - -${gitCommitGuidePrompt} - -Example: -${getToolCallString(toolName, { - command: 'echo "hello world"', -})} - -${getToolCallString(toolName, { - command: `git commit -m "Your commit message here. - -🤖 Generated with Codebuff -Co-Authored-By: Codebuff "`, -})} - `.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/definitions/tool/set-messages.ts b/packages/agent-runtime/src/tools/definitions/tool/set-messages.ts deleted file mode 100644 index 498f870d5e..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/set-messages.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'set_messages' -const endsAgentStep = true -export const setMessagesTool = { - toolName, - description: ` -Example: -${getToolCallString(toolName, { - messages: [ - { - role: 'user', - content: 'Hello, how are you?', - }, - { - role: 'assistant', - content: 'I am fine, thank you.', - }, - ], -})} - `.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/definitions/tool/set-output.ts b/packages/agent-runtime/src/tools/definitions/tool/set-output.ts deleted file mode 100644 index 57d9e41422..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/set-output.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'set_output' -export const setOutputTool = { - toolName, - description: ` -You must use this tool as it is the only way to report any findings to the user. Nothing else you write will be shown to the user. - -Please set the output with all the information and analysis you want to pass on to the user. If you just want to send a simple message, use an object with the key "message" and value of the message you want to send. -Example: -${getToolCallString(toolName, { - message: 'I found a bug in the code!', -})} - `.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/definitions/tool/spawn-agent-inline.ts b/packages/agent-runtime/src/tools/definitions/tool/spawn-agent-inline.ts deleted file mode 100644 index d06c462c3d..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/spawn-agent-inline.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'spawn_agent_inline' -export const spawnAgentInlineTool = { - toolName, - description: ` -Spawn a single agent that runs within the current message history. -The spawned agent sees all previous messages and any messages it adds -are preserved when control returns to you. - -You should prefer to use the spawn_agents tool unless instructed otherwise. This tool is only for special cases. - -This is useful for: -- Delegating specific tasks while maintaining context -- Having specialized agents process information inline -- Managing message history (e.g., summarization) -The agent will run until it calls end_turn, then control returns to you. There is no tool result for this tool. -Example: -${getToolCallString(toolName, { - agent_type: 'file-picker', - prompt: 'Find files related to authentication', - params: { paths: ['src/auth.ts', 'src/user.ts'] }, -})} - `.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/definitions/tool/spawn-agents.ts b/packages/agent-runtime/src/tools/definitions/tool/spawn-agents.ts deleted file mode 100644 index 60d4f4670a..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/spawn-agents.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'spawn_agents' -export const spawnAgentsTool = { - toolName, - description: ` -Use this tool to spawn agents to help you complete the user request. Each agent has specific requirements for prompt and params based on their inputSchema. - -The prompt field is a simple string, while params is a JSON object that gets validated against the agent's schema. - -Example: -${getToolCallString(toolName, { - agents: [ - { - agent_type: 'planner', - prompt: 'Create a plan for implementing user authentication', - params: { filePaths: ['src/auth.ts', 'src/user.ts'] }, - }, - ], -})} - `.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/definitions/tool/str-replace.ts b/packages/agent-runtime/src/tools/definitions/tool/str-replace.ts deleted file mode 100644 index af76f3dd37..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/str-replace.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'str_replace' -export const strReplaceTool = { - toolName, - description: ` -Use this tool to make edits within existing files. Prefer this tool over the write_file tool for existing files, unless you need to make major changes throughout the file, in which case use write_file. - -Important: -If you are making multiple edits in a row to a file, use only one str_replace call with multiple replacements instead of multiple str_replace tool calls. - -Example: -${getToolCallString(toolName, { - path: 'path/to/file', - replacements: [ - { old: 'This is the old string', new: 'This is the new string' }, - { - old: '\n\t\t// @codebuff delete this log line please\n\t\tconsole.log("Hello, world!");\n', - new: '\n', - }, - { - old: '\nfoo:', - new: '\nbar:', - allowMultiple: true, - }, - ], -})} - `.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/definitions/tool/task-completed.ts b/packages/agent-runtime/src/tools/definitions/tool/task-completed.ts deleted file mode 100644 index 5fa33439a2..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/task-completed.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'task_completed' -export const taskCompletedTool = { - toolName, - description: ` -Use this tool to signal that the task is complete. - -- When to use: - * The user's request is completely fulfilled and you have nothing more to do - * You need clarification from the user before continuing - * You need help from the user to continue (e.g., missing information, unclear requirements) - * You've encountered a blocker that requires user intervention - -- Before calling: - * Ensure all pending work is finished - * Resolve all tool results - * Provide any outputs or summaries the user needs - -- Effect: Signals completion of the current task and returns control to the user - -*EXAMPLE USAGE*: - -All changes have been implemented and tested successfully! - -${getToolCallString(toolName, {})} - -OR - -I need more information to proceed. Which database schema should I use for this migration? - -${getToolCallString(toolName, {})} - -OR - -I can't get the tests to pass after several different attempts. I need help from the user to proceed. - -${getToolCallString(toolName, {})} - - `.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/definitions/tool/think-deeply.ts b/packages/agent-runtime/src/tools/definitions/tool/think-deeply.ts deleted file mode 100644 index bb4cfd5f7b..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/think-deeply.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'think_deeply' -export const thinkDeeplyTool = { - toolName, - description: ` -Use when user request: -- Explicitly asks for deep planning. -- Requires multi-file changes or complex logic. -- Involves significant architecture or potential edge cases. - -Avoid for simple changes (e.g., single functions, minor edits). - -This tool does not generate a tool result. - -Example: -${getToolCallString(toolName, { - thought: [ - '1. Check current user authentication', - '2. Refactor auth logic into module', - '3. Update imports across project', - '4. Add tests for new module', - ].join('\n'), -})} - `.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/definitions/tool/update-subgoal.ts b/packages/agent-runtime/src/tools/definitions/tool/update-subgoal.ts deleted file mode 100644 index 05c1a505c0..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/update-subgoal.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'update_subgoal' -export const updateSubgoalTool = { - toolName, - description: ` -Examples: - -Usage 1 (update status): -${getToolCallString(toolName, { - id: '1', - status: 'COMPLETE', -})} - -Usage 2 (update plan): -${getToolCallString(toolName, { - id: '3', - plan: 'Create file for endpoint in the api. Register it in the router.', -})} - -Usage 3 (add log): -${getToolCallString(toolName, { - id: '1', - log: 'Found the error in the tests. Culprit: foo function.', -})} - -Usage 4 (update status and add log): -${getToolCallString(toolName, { - id: '1', - status: 'COMPLETE', - log: 'Reran the tests (passed)', -})} - `.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/definitions/tool/web-search.ts b/packages/agent-runtime/src/tools/definitions/tool/web-search.ts deleted file mode 100644 index d5f30ca756..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/web-search.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'web_search' -export const webSearchTool = { - toolName, - description: ` -Purpose: Search the web for current, up-to-date information on any topic. This tool uses Linkup's web search API to find relevant content from across the internet. - -Use cases: -- Finding current information about technologies, libraries, or frameworks -- Researching best practices and solutions -- Getting up-to-date news or documentation -- Finding examples and tutorials -- Checking current status of services or APIs - -The tool will return search results with titles, URLs, and content snippets. - -Example: -${getToolCallString(toolName, { - query: 'Next.js 15 new features', - depth: 'standard', -})} - -${getToolCallString(toolName, { - query: 'React Server Components tutorial', - depth: 'deep', -})} - `.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/definitions/tool/write-file.ts b/packages/agent-runtime/src/tools/definitions/tool/write-file.ts deleted file mode 100644 index 545d292cc7..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/write-file.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'write_file' -export const writeFileTool = { - toolName, - description: ` -Create or replace a file with the given content. - -#### Edit Snippet - -Format the \`content\` parameter with the entire content of the file or as an edit snippet that describes how you would like to modify the provided existing code. - -You may abbreviate any sections of the code in your response that will remain the same with placeholder comments: "// ... existing code ...". Abbreviate as much as possible to save the user credits! - -If you don't use any placeholder comments, the entire file will be replaced. E.g. don't write out a single function without using placeholder comments unless you want to replace the entire file with that function. - -#### Additional Info - -Prefer str_replace to write_file for most edits, including small-to-medium edits to a file, for deletions, or for editing large files (>1000 lines). Otherwise, prefer write_file for major edits throughout a file, or for creating new files. - -Do not use this tool to delete or rename a file. Instead run a terminal command for that. - -Examples: - -Example 1 - Simple file creation: -${getToolCallString(toolName, { - path: 'new-file.ts', - instructions: 'Prints Hello, world', - content: 'console.log("Hello, world!");', -})} - -Example 2 - Editing with placeholder comments: -${getToolCallString(toolName, { - path: 'foo.ts', - instructions: 'Update foo and remove console.log', - content: `// ... existing code ... - -function foo() { - console.log('foo'); - for (let i = 0; i < 10; i++) { - console.log(i); - } - doSomething(); - - // Delete the console.log line from here - - doSomethingElse(); -} - -// ... existing code ...`, -})} - - `.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/definitions/tool/write-todos.ts b/packages/agent-runtime/src/tools/definitions/tool/write-todos.ts deleted file mode 100644 index bc712c9377..0000000000 --- a/packages/agent-runtime/src/tools/definitions/tool/write-todos.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { getToolCallString } from '@codebuff/common/tools/utils' - -import type { ToolDescription } from '../tool-def-type' - -const toolName = 'write_todos' -export const writeTodosTool = { - toolName, - description: ` -Use this tool to track your objectives through an ordered step-by-step plan. Call this tool after you have gathered context on the user's request to plan out the implementation steps for the user's request. - -After completing each todo step, call this tool again to update the list and mark that task as completed. Note that each time you call this tool, rewrite ALL todos with their current status. - -Use this tool frequently as you work through tasks to update the list of todos with their current status. Doing this is extremely useful because it helps you stay on track and complete all the requirements of the user's request. It also helps inform the user of your plans and the current progress, which they want to know at all times. - -Example: -${getToolCallString(toolName, { - todos: [ - { task: 'Create new implementation in foo.ts', completed: true }, - { task: 'Update bar.ts to use the new implementation', completed: false }, - { task: 'Write tests for the new implementation', completed: false }, - { - task: 'Run the tests to verify the new implementation', - completed: false, - }, - ], -})} -`.trim(), -} satisfies ToolDescription diff --git a/packages/agent-runtime/src/tools/handlers/handler-function-type.ts b/packages/agent-runtime/src/tools/handlers/handler-function-type.ts index 180409dc50..dcd0674642 100644 --- a/packages/agent-runtime/src/tools/handlers/handler-function-type.ts +++ b/packages/agent-runtime/src/tools/handlers/handler-function-type.ts @@ -1,3 +1,4 @@ +import type { FileProcessingState } from './tool/write-file' import type { ToolName } from '@codebuff/common/tools/constants' import type { ClientToolCall, @@ -6,44 +7,73 @@ import type { CodebuffToolMessage, CodebuffToolOutput, } from '@codebuff/common/tools/list' +import type { AgentTemplate } from '@codebuff/common/types/agent-template' import type { AgentRuntimeDeps, AgentRuntimeScopedDeps, } from '@codebuff/common/types/contracts/agent-runtime' import type { TrackEventFn } from '@codebuff/common/types/contracts/analytics' +import type { SendSubagentChunkFn } from '@codebuff/common/types/contracts/client' +import type { Logger } from '@codebuff/common/types/contracts/logger' +import type { Message } from '@codebuff/common/types/messages/codebuff-message' import type { PrintModeEvent } from '@codebuff/common/types/print-mode' +import type { AgentState } from '@codebuff/common/types/session-state' import type { ProjectFileContext } from '@codebuff/common/util/file' type PresentOrAbsent = | { [P in K]: V } | { [P in K]: never } +export type State = { + creditsUsed?: number | Promise + sendSubagentChunk: SendSubagentChunkFn + agentState: AgentState + prompt: string | undefined + fullResponse: string | undefined + agentContext: Record< + string, + { + logs: string[] + objective?: string | undefined + status?: + | 'NOT_STARTED' + | 'IN_PROGRESS' + | 'COMPLETE' + | 'ABORTED' + | undefined + plan?: string | undefined + } + > + messages: Message[] + system: string + logger: Logger +} & FileProcessingState export type CodebuffToolHandlerFunction = ( params: { previousToolCallFinished: Promise toolCall: CodebuffToolCall - runId: string agentStepId: string + agentTemplate: AgentTemplate + ancestorRunIds: string[] + apiKey: string clientSessionId: string - userInputId: string - repoUrl: string | undefined - repoId: string | undefined fileContext: ProjectFileContext - apiKey: string - + fingerprintId: string + fullResponse: string + localAgentTemplates: Record + repoId: string | undefined + repoUrl: string | undefined + runId: string signal: AbortSignal + state: State + userId: string | undefined + userInputId: string - ancestorRunIds: string[] - - fullResponse: string fetch: typeof globalThis.fetch - - writeToClient: (chunk: string | PrintModeEvent) => void + getLatestState: () => State trackEvent: TrackEventFn - - getLatestState: () => any - state: { [K in string]?: any } + writeToClient: (chunk: string | PrintModeEvent) => void } & PresentOrAbsent< 'requestClientToolCall', ( @@ -54,5 +84,5 @@ export type CodebuffToolHandlerFunction = ( AgentRuntimeScopedDeps, ) => { result: Promise['content']> - state?: Record + state?: Partial } diff --git a/packages/agent-runtime/src/tools/handlers/tool/add-subgoal.ts b/packages/agent-runtime/src/tools/handlers/tool/add-subgoal.ts index fec5d87cc3..934d5e2775 100644 --- a/packages/agent-runtime/src/tools/handlers/tool/add-subgoal.ts +++ b/packages/agent-runtime/src/tools/handlers/tool/add-subgoal.ts @@ -10,7 +10,7 @@ import type { Subgoal } from '@codebuff/common/types/session-state' export const handleAddSubgoal = ((params: { previousToolCallFinished: Promise toolCall: CodebuffToolCall<'add_subgoal'> - state: { agentContext?: Record } + state: { agentContext: Record } }): { result: Promise> state: { agentContext: Record } diff --git a/packages/agent-runtime/src/tools/handlers/tool/create-plan.ts b/packages/agent-runtime/src/tools/handlers/tool/create-plan.ts index b66beac591..ea4fc602dc 100644 --- a/packages/agent-runtime/src/tools/handlers/tool/create-plan.ts +++ b/packages/agent-runtime/src/tools/handlers/tool/create-plan.ts @@ -3,10 +3,7 @@ import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events' import { getFileProcessingValues, postStreamProcessing } from './write-file' import type { CodebuffToolHandlerFunction } from '../handler-function-type' -import type { - FileProcessingState, - OptionalFileProcessingState, -} from './write-file' +import type { FileProcessingState } from './write-file' import type { ClientToolCall, CodebuffToolCall, @@ -18,45 +15,43 @@ import type { Logger } from '@codebuff/common/types/contracts/logger' export const handleCreatePlan = ((params: { previousToolCallFinished: Promise toolCall: CodebuffToolCall<'create_plan'> + + agentStepId: string + clientSessionId: string + fingerprintId: string + logger: Logger + repoId: string | undefined + userId: string | undefined + userInputId: string requestClientToolCall: ( toolCall: ClientToolCall<'create_plan'>, ) => Promise> - writeToClient: (chunk: string) => void - logger: Logger trackEvent: TrackEventFn + writeToClient: (chunk: string) => void getLatestState: () => FileProcessingState - state: { - agentStepId?: string - clientSessionId?: string - fingerprintId?: string - userId?: string - userInputId?: string - repoId?: string - } & OptionalFileProcessingState + state: FileProcessingState }): { result: Promise> state: FileProcessingState } => { const { + agentStepId, + clientSessionId, + fingerprintId, + logger, previousToolCallFinished, + repoId, + state, toolCall, - requestClientToolCall, - writeToClient, - logger, + userId, + userInputId, getLatestState, + requestClientToolCall, trackEvent, - state, + writeToClient, } = params const { path, plan } = toolCall.input - const { - agentStepId, - clientSessionId, - fingerprintId, - userId, - userInputId, - repoId, - } = state const fileProcessingState = getFileProcessingValues(state) logger.debug( diff --git a/packages/agent-runtime/src/tools/handlers/tool/find-files.ts b/packages/agent-runtime/src/tools/handlers/tool/find-files.ts index fe01acd975..1ad1066d26 100644 --- a/packages/agent-runtime/src/tools/handlers/tool/find-files.ts +++ b/packages/agent-runtime/src/tools/handlers/tool/find-files.ts @@ -31,58 +31,46 @@ export const handleFindFiles = (( toolCall: CodebuffToolCall<'find_files'> logger: Logger - fileContext: ProjectFileContext agentStepId: string clientSessionId: string + fileContext: ProjectFileContext + fingerprintId: string + repoId: string | undefined + userId: string | undefined userInputId: string state: { - fingerprintId?: string - userId?: string - repoId?: string - messages?: Message[] + messages: Message[] } } & ParamsExcluding< typeof requestRelevantFiles, - | 'messages' - | 'system' - | 'assistantPrompt' - | 'fingerprintId' - | 'userId' - | 'repoId' + 'messages' | 'system' | 'assistantPrompt' > & ParamsExcluding< typeof uploadExpandedFileContextForTraining, - | 'messages' - | 'system' - | 'assistantPrompt' - | 'fingerprintId' - | 'userId' - | 'repoId' + 'messages' | 'system' | 'assistantPrompt' > & ParamsExcluding, ): { result: Promise>; state: {} } => { const { previousToolCallFinished, toolCall, - logger, - fileContext, + agentStepId, clientSessionId, - userInputId, + fileContext, + fingerprintId, + logger, state, + userId, + userInputId, } = params const { prompt } = toolCall.input - const { fingerprintId, userId, repoId, messages } = state + const { messages } = state if (!messages) { throw new Error('Internal error for find_files: Missing messages in state') } - if (!fingerprintId) { - throw new Error( - 'Internal error for find_files: Missing fingerprintId in state', - ) - } const fileRequestMessagesTokens = countTokensJson(messages) const system = getSearchSystemPrompt({ @@ -106,9 +94,6 @@ export const handleFindFiles = (( messages, system, assistantPrompt: prompt, - fingerprintId, - userId, - repoId, }) if (requestedFiles && requestedFiles.length > 0) { @@ -123,9 +108,6 @@ export const handleFindFiles = (( messages, system, assistantPrompt: prompt, - fingerprintId, - userId, - repoId, }).catch((error) => { logger.error( { error }, @@ -176,24 +158,10 @@ export const handleFindFiles = (( async function uploadExpandedFileContextForTraining( params: { - agentStepId: string - clientSessionId: string - fingerprintId: string - userInputId: string - userId: string | undefined requestFiles: RequestFilesFn - logger: Logger } & ParamsOf, ) { - const { - agentStepId, - clientSessionId, - fingerprintId, - userInputId, - userId, - requestFiles, - logger, - } = params + const { requestFiles } = params const files = await requestRelevantFilesForTraining(params) const loadedFiles = await requestFiles({ filePaths: files }) diff --git a/packages/agent-runtime/src/tools/handlers/tool/lookup-agent-info.ts b/packages/agent-runtime/src/tools/handlers/tool/lookup-agent-info.ts index 40c8ade8cf..5d24bcf8f3 100644 --- a/packages/agent-runtime/src/tools/handlers/tool/lookup-agent-info.ts +++ b/packages/agent-runtime/src/tools/handlers/tool/lookup-agent-info.ts @@ -1,20 +1,36 @@ -import { getAgentTemplate } from '../../../templates/agent-registry' import { removeUndefinedProps } from '@codebuff/common/util/object' import z from 'zod/v4' +import { getAgentTemplate } from '../../../templates/agent-registry' + import type { CodebuffToolHandlerFunction } from '../handler-function-type' +import type { CodebuffToolCall } from '@codebuff/common/tools/list' +import type { + AgentTemplate, + Logger, +} from '@codebuff/common/types/agent-template' +import type { FetchAgentFromDatabaseFn } from '@codebuff/common/types/contracts/database' -export const handleLookupAgentInfo: CodebuffToolHandlerFunction< - 'lookup_agent_info' -> = (params) => { - const { agentId } = params.toolCall.input +export const handleLookupAgentInfo = ((params: { + toolCall: CodebuffToolCall<'lookup_agent_info'> + previousToolCallFinished: Promise + + apiKey: string + databaseAgentCache: Map + localAgentTemplates: Record + logger: Logger + fetchAgentFromDatabase: FetchAgentFromDatabaseFn +}) => { + const { toolCall, previousToolCallFinished } = params + const { agentId } = toolCall.input return { result: (async () => { + await previousToolCallFinished + const agentTemplate = await getAgentTemplate({ ...params, agentId, - localAgentTemplates: params.state.localAgentTemplates || {}, }) if (!agentTemplate) { @@ -70,7 +86,7 @@ export const handleLookupAgentInfo: CodebuffToolHandlerFunction< ] })(), } -} +}) satisfies CodebuffToolHandlerFunction<'lookup_agent_info'> const toJSONSchema = (schema: z.ZodSchema) => { const jsonSchema = z.toJSONSchema(schema, { io: 'input' }) as { diff --git a/packages/agent-runtime/src/tools/handlers/tool/read-docs.ts b/packages/agent-runtime/src/tools/handlers/tool/read-docs.ts index c515935ab1..914634f7b4 100644 --- a/packages/agent-runtime/src/tools/handlers/tool/read-docs.ts +++ b/packages/agent-runtime/src/tools/handlers/tool/read-docs.ts @@ -1,6 +1,6 @@ -import { fetchContext7LibraryDocumentation } from '../../../llm-api/context7-api' import { callDocsSearchAPI } from '../../../llm-api/codebuff-web-api' +import type { fetchContext7LibraryDocumentation } from '../../../llm-api/context7-api' import type { CodebuffToolHandlerFunction } from '../handler-function-type' import type { CodebuffToolCall, @@ -13,17 +13,14 @@ export const handleReadDocs = (( params: { previousToolCallFinished: Promise toolCall: CodebuffToolCall<'read_docs'> - logger: Logger agentStepId: string clientSessionId: string + fingerprintId: string + logger: Logger + repoId: string | undefined + userId: string | undefined userInputId: string - - state: { - userId?: string - fingerprintId?: string - repoId?: string - } } & ParamsExcluding< typeof fetchContext7LibraryDocumentation, 'query' | 'topic' | 'tokens' @@ -35,15 +32,18 @@ export const handleReadDocs = (( const { previousToolCallFinished, toolCall, - logger, + agentStepId, clientSessionId, + fingerprintId, + logger, + repoId, + userId, userInputId, - state, + fetch, } = params const { libraryTitle, topic, max_tokens } = toolCall.input - const { userId, fingerprintId, repoId } = state const docsStartTime = Date.now() const docsContext = { diff --git a/packages/agent-runtime/src/tools/handlers/tool/read-files.ts b/packages/agent-runtime/src/tools/handlers/tool/read-files.ts index d33dc9dfb0..0fe58a0aed 100644 --- a/packages/agent-runtime/src/tools/handlers/tool/read-files.ts +++ b/packages/agent-runtime/src/tools/handlers/tool/read-files.ts @@ -16,14 +16,12 @@ export const handleReadFiles = (( previousToolCallFinished: Promise toolCall: CodebuffToolCall - userInputId: string fileContext: ProjectFileContext + fingerprintId: string + userInputId: string state: { - userId?: string - fingerprintId?: string - repoId?: string - messages?: Message[] + messages: Message[] } } & ParamsExcluding, ): { @@ -33,11 +31,14 @@ export const handleReadFiles = (( const { previousToolCallFinished, toolCall, - userInputId, + fileContext, + fingerprintId, + userInputId, + state, } = params - const { fingerprintId, userId, repoId, messages } = state + const { messages } = state const { paths } = toolCall.input if (!messages) { throw new Error('Internal error for read_files: Missing messages in state') diff --git a/packages/agent-runtime/src/tools/handlers/tool/set-output.ts b/packages/agent-runtime/src/tools/handlers/tool/set-output.ts index 3a53fc4a76..cbb5a2357a 100644 --- a/packages/agent-runtime/src/tools/handlers/tool/set-output.ts +++ b/packages/agent-runtime/src/tools/handlers/tool/set-output.ts @@ -1,14 +1,32 @@ import { getAgentTemplate } from '../../../templates/agent-registry' import type { CodebuffToolHandlerFunction } from '../handler-function-type' +import type { CodebuffToolCall } from '@codebuff/common/tools/list' +import type { + AgentTemplate, + Logger, +} from '@codebuff/common/types/agent-template' +import type { FetchAgentFromDatabaseFn } from '@codebuff/common/types/contracts/database' +import type { AgentState } from '@codebuff/common/types/session-state' type ToolName = 'set_output' -export const handleSetOutput: CodebuffToolHandlerFunction = ( - params, -) => { +export const handleSetOutput = ((params: { + previousToolCallFinished: Promise + toolCall: CodebuffToolCall + + apiKey: string + databaseAgentCache: Map + localAgentTemplates: Record + logger: Logger + fetchAgentFromDatabase: FetchAgentFromDatabaseFn + + state: { + agentState: AgentState + } +}) => { const { previousToolCallFinished, toolCall, state, logger } = params const output = toolCall.input - const { agentState, localAgentTemplates } = state + const { agentState } = state if (!agentState) { throw new Error( @@ -16,12 +34,6 @@ export const handleSetOutput: CodebuffToolHandlerFunction = ( ) } - if (!localAgentTemplates) { - throw new Error( - 'Internal error for set_output: Missing localAgentTemplates in state', - ) - } - const triggerSetOutput = async () => { // Validate output against outputSchema if defined let agentTemplate = null @@ -29,7 +41,6 @@ export const handleSetOutput: CodebuffToolHandlerFunction = ( agentTemplate = await getAgentTemplate({ ...params, agentId: agentState.agentType, - localAgentTemplates, }) } if (agentTemplate?.outputSchema) { @@ -70,4 +81,4 @@ export const handleSetOutput: CodebuffToolHandlerFunction = ( })(), state: { agentState: agentState }, } -} +}) satisfies CodebuffToolHandlerFunction diff --git a/packages/agent-runtime/src/tools/handlers/tool/spawn-agent-inline.ts b/packages/agent-runtime/src/tools/handlers/tool/spawn-agent-inline.ts index dcc3d39094..dda6c19980 100644 --- a/packages/agent-runtime/src/tools/handlers/tool/spawn-agent-inline.ts +++ b/packages/agent-runtime/src/tools/handlers/tool/spawn-agent-inline.ts @@ -25,22 +25,23 @@ export const handleSpawnAgentInline = (( params: { previousToolCallFinished: Promise toolCall: CodebuffToolCall - fileContext: ProjectFileContext + + agentTemplate: AgentTemplate clientSessionId: string + fileContext: ProjectFileContext + fingerprintId: string + localAgentTemplates: Record + logger: Logger + userId: string | undefined userInputId: string writeToClient: (chunk: string | PrintModeEvent) => void getLatestState: () => { messages: Message[] } state: { - fingerprintId?: string - userId?: string - agentTemplate?: AgentTemplate - localAgentTemplates?: Record - messages?: Message[] - agentState?: AgentState - system?: string + messages: Message[] + agentState: AgentState + system: string } - logger: Logger } & ParamsExcluding< typeof executeSubagent, | 'userInputId' @@ -49,8 +50,6 @@ export const handleSpawnAgentInline = (( | 'agentTemplate' | 'parentAgentState' | 'agentState' - | 'localAgentTemplates' - | 'userId' | 'parentSystemPrompt' | 'onResponseChunk' | 'clearUserPromptMessagesAfterResponse' @@ -60,31 +59,30 @@ export const handleSpawnAgentInline = (( const { previousToolCallFinished, toolCall, + + agentTemplate: parentAgentTemplate, + fingerprintId, userInputId, + writeToClient, + getLatestState, state, - writeToClient, } = params const { agent_type: agentTypeStr, prompt, params: spawnParams, } = toolCall.input - const { - fingerprintId, - userId, - agentTemplate: parentAgentTemplate, - localAgentTemplates, - agentState: parentAgentState, - system, - } = validateSpawnState(state, 'spawn_agent_inline') + const { agentState: parentAgentState, system } = validateSpawnState( + state, + 'spawn_agent_inline', + ) const triggerSpawnAgentInline = async () => { const { agentTemplate, agentType } = await validateAndGetAgentTemplate({ ...params, agentTypeStr, parentAgentTemplate, - localAgentTemplates, }) validateAgentInput(agentTemplate, agentType, prompt, spawnParams) @@ -117,8 +115,6 @@ export const handleSpawnAgentInline = (( agentTemplate, parentAgentState, agentState: childAgentState, - localAgentTemplates, - userId, fingerprintId, parentSystemPrompt: system, onResponseChunk: (chunk) => { diff --git a/packages/agent-runtime/src/tools/handlers/tool/spawn-agent-utils.ts b/packages/agent-runtime/src/tools/handlers/tool/spawn-agent-utils.ts index 010b7ad9af..5da2381606 100644 --- a/packages/agent-runtime/src/tools/handlers/tool/spawn-agent-utils.ts +++ b/packages/agent-runtime/src/tools/handlers/tool/spawn-agent-utils.ts @@ -27,13 +27,9 @@ export interface SpawnAgentParams { } export interface BaseSpawnState { - fingerprintId?: string - userId?: string - agentTemplate?: AgentTemplate - localAgentTemplates?: Record - messages?: Message[] - agentState?: AgentState - system?: string + messages: Message[] + agentState: AgentState + system: string } export interface SpawnContext { @@ -49,27 +45,9 @@ export interface SpawnContext { export function validateSpawnState( state: BaseSpawnState, toolName: string, -): Omit, 'userId'> & { userId: string | undefined } { - const { - fingerprintId, - agentTemplate: parentAgentTemplate, - localAgentTemplates, - messages, - agentState, - userId, - system, - } = state +): Required { + const { messages, agentState, system } = state - if (!fingerprintId) { - throw new Error( - `Internal error for ${toolName}: Missing fingerprintId in state`, - ) - } - if (!parentAgentTemplate) { - throw new Error( - `Internal error for ${toolName}: Missing agentTemplate in state`, - ) - } if (!messages) { throw new Error(`Internal error for ${toolName}: Missing messages in state`) } @@ -78,20 +56,11 @@ export function validateSpawnState( `Internal error for ${toolName}: Missing agentState in state`, ) } - if (!localAgentTemplates) { - throw new Error( - `Internal error for ${toolName}: Missing localAgentTemplates in state`, - ) - } if (!system) { throw new Error(`Internal error for ${toolName}: Missing system in state`) } return { - fingerprintId, - userId, - agentTemplate: parentAgentTemplate, - localAgentTemplates, messages, agentState, system, @@ -170,8 +139,7 @@ export async function validateAndGetAgentTemplate( logger: Logger } & ParamsExcluding, ): Promise<{ agentTemplate: AgentTemplate; agentType: string }> { - const { agentTypeStr, parentAgentTemplate, localAgentTemplates, logger } = - params + const { agentTypeStr, parentAgentTemplate } = params const agentTemplate = await getAgentTemplate({ ...params, agentId: agentTypeStr, diff --git a/packages/agent-runtime/src/tools/handlers/tool/spawn-agents.ts b/packages/agent-runtime/src/tools/handlers/tool/spawn-agents.ts index e089256289..043a6bafd4 100644 --- a/packages/agent-runtime/src/tools/handlers/tool/spawn-agents.ts +++ b/packages/agent-runtime/src/tools/handlers/tool/spawn-agents.ts @@ -34,24 +34,24 @@ export const handleSpawnAgents = (( previousToolCallFinished: Promise toolCall: CodebuffToolCall + agentTemplate: AgentTemplate + fingerprintId: string + localAgentTemplates: Record + userId: string | undefined userInputId: string writeToClient: (chunk: string | PrintModeEvent) => void getLatestState: () => { messages: Message[] } state: { - fingerprintId?: string - userId?: string - agentTemplate?: AgentTemplate - localAgentTemplates?: Record - sendSubagentChunk?: SendSubagentChunk - messages?: Message[] - agentState?: AgentState - system?: string + sendSubagentChunk: SendSubagentChunk + messages: Message[] + agentState: AgentState + system: string } logger: Logger } & ParamsExcluding< typeof validateAndGetAgentTemplate, - 'agentTypeStr' | 'parentAgentTemplate' | 'localAgentTemplates' + 'agentTypeStr' | 'parentAgentTemplate' > & ParamsExcluding< typeof executeSubagent, @@ -62,8 +62,6 @@ export const handleSpawnAgents = (( | 'parentAgentState' | 'agentState' | 'fingerprintId' - | 'localAgentTemplates' - | 'userId' | 'isOnlyChild' | 'parentSystemPrompt' | 'onResponseChunk' @@ -73,10 +71,14 @@ export const handleSpawnAgents = (( previousToolCallFinished, toolCall, + agentTemplate: parentAgentTemplate, + fingerprintId, + localAgentTemplates, userInputId, + writeToClient, + getLatestState, state, - writeToClient, } = params const { agents } = toolCall.input const validatedState = validateSpawnState(state, 'spawn_agents') @@ -90,10 +92,6 @@ export const handleSpawnAgents = (( } const { - fingerprintId, - userId, - agentTemplate: parentAgentTemplate, - localAgentTemplates, agentState: parentAgentState, } = validatedState @@ -106,7 +104,6 @@ export const handleSpawnAgents = (( ...params, agentTypeStr, parentAgentTemplate, - localAgentTemplates, }) validateAgentInput(agentTemplate, agentType, prompt, spawnParams) @@ -138,8 +135,6 @@ export const handleSpawnAgents = (( parentAgentState, agentState: subAgentState, fingerprintId, - localAgentTemplates, - userId, isOnlyChild: agents.length === 1, parentSystemPrompt, onResponseChunk: (chunk: string | PrintModeEvent) => { diff --git a/packages/agent-runtime/src/tools/handlers/tool/str-replace.ts b/packages/agent-runtime/src/tools/handlers/tool/str-replace.ts index e164d86f4d..5daec82001 100644 --- a/packages/agent-runtime/src/tools/handlers/tool/str-replace.ts +++ b/packages/agent-runtime/src/tools/handlers/tool/str-replace.ts @@ -2,10 +2,7 @@ import { getFileProcessingValues, postStreamProcessing } from './write-file' import { processStrReplace } from '../../../process-str-replace' import type { CodebuffToolHandlerFunction } from '../handler-function-type' -import type { - FileProcessingState, - OptionalFileProcessingState, -} from './write-file' +import type { FileProcessingState } from './write-file' import type { ClientToolCall, CodebuffToolCall, @@ -26,7 +23,7 @@ export function handleStrReplace( logger: Logger getLatestState: () => FileProcessingState - state: OptionalFileProcessingState + state: FileProcessingState requestOptionalFile: RequestOptionalFileFn } & ParamsExcluding, ): { diff --git a/packages/agent-runtime/src/tools/handlers/tool/update-subgoal.ts b/packages/agent-runtime/src/tools/handlers/tool/update-subgoal.ts index bf39ff881c..e7c5c6143b 100644 --- a/packages/agent-runtime/src/tools/handlers/tool/update-subgoal.ts +++ b/packages/agent-runtime/src/tools/handlers/tool/update-subgoal.ts @@ -9,7 +9,7 @@ type ToolName = 'update_subgoal' export const handleUpdateSubgoal = ((params: { previousToolCallFinished: Promise toolCall: CodebuffToolCall - state: { agentContext?: Record } + state: { agentContext: Record } }): { result: Promise> state: { agentContext: Record } diff --git a/packages/agent-runtime/src/tools/handlers/tool/web-search.ts b/packages/agent-runtime/src/tools/handlers/tool/web-search.ts index 0a9361952e..5e861ad7ff 100644 --- a/packages/agent-runtime/src/tools/handlers/tool/web-search.ts +++ b/packages/agent-runtime/src/tools/handlers/tool/web-search.ts @@ -5,7 +5,6 @@ import type { CodebuffToolCall, CodebuffToolOutput, } from '@codebuff/common/tools/list' -import type { ConsumeCreditsWithFallbackFn } from '@codebuff/common/types/contracts/billing' import type { Logger } from '@codebuff/common/types/contracts/logger' export const handleWebSearch = ((params: { @@ -16,31 +15,31 @@ export const handleWebSearch = ((params: { agentStepId: string clientSessionId: string - userInputId: string + fingerprintId: string + repoId: string | undefined repoUrl: string | undefined + userInputId: string + userId: string | undefined - state: { - userId?: string - fingerprintId?: string - repoId?: string - } fetch: typeof globalThis.fetch - consumeCreditsWithFallback: ConsumeCreditsWithFallbackFn }): { result: Promise>; state: {} } => { const { previousToolCallFinished, toolCall, - logger, + agentStepId, + apiKey, clientSessionId, - userInputId, + fingerprintId, + logger, + repoId, repoUrl, - state, + userId, + userInputId, + fetch, - apiKey } = params const { query, depth } = toolCall.input - const { userId, fingerprintId, repoId } = state const searchStartTime = Date.now() const searchContext = { @@ -65,7 +64,7 @@ export const handleWebSearch = ((params: { repoUrl: repoUrl ?? null, fetch, logger, - apiKey + apiKey, }) if (webApi.error) { diff --git a/packages/agent-runtime/src/tools/handlers/tool/write-file.ts b/packages/agent-runtime/src/tools/handlers/tool/write-file.ts index b50e5a5332..fa1e68bf4a 100644 --- a/packages/agent-runtime/src/tools/handlers/tool/write-file.ts +++ b/packages/agent-runtime/src/tools/handlers/tool/write-file.ts @@ -39,12 +39,8 @@ export type FileProcessingState = { firstFileProcessed: boolean } -export type OptionalFileProcessingState = { - [K in keyof FileProcessingState]?: FileProcessingState[K] -} - export function getFileProcessingValues( - state: OptionalFileProcessingState, + state: FileProcessingState, ): FileProcessingState { const fileProcessingValues: FileProcessingState = { promisesByPath: {}, @@ -68,29 +64,28 @@ export function handleWriteFile( toolCall: CodebuffToolCall<'write_file'> clientSessionId: string + fingerprintId: string + logger: Logger + userId: string | undefined userInputId: string requestClientToolCall: ( toolCall: ClientToolCall<'write_file'>, ) => Promise> + requestOptionalFile: RequestOptionalFileFn writeToClient: (chunk: string) => void getLatestState: () => FileProcessingState state: { - fingerprintId?: string - userId?: string - fullResponse?: string - prompt?: string - messages?: Message[] - } & OptionalFileProcessingState - requestOptionalFile: RequestOptionalFileFn - logger: Logger + fullResponse: string | undefined + prompt: string | undefined + messages: Message[] + } & FileProcessingState } & ParamsExcluding< typeof processFileBlock, | 'path' | 'instructions' | 'fingerprintId' - | 'userId' | 'initialContentPromise' | 'newContent' | 'messages' @@ -107,23 +102,19 @@ export function handleWriteFile( toolCall, clientSessionId, + fingerprintId, + logger, userInputId, requestClientToolCall, + requestOptionalFile, writeToClient, getLatestState, state, - requestOptionalFile, - logger, } = params const { path, instructions, content } = toolCall.input - const { fingerprintId, userId, fullResponse, prompt } = state - if (!fingerprintId) { - throw new Error( - 'Internal error for write_file: Missing fingerprintId in state', - ) - } + const { fullResponse, prompt } = state const fileProcessingState = getFileProcessingValues(state) const fileProcessingPromisesByPath = fileProcessingState.promisesByPath @@ -166,7 +157,6 @@ export function handleWriteFile( clientSessionId, fingerprintId, userInputId, - userId, logger, }) .catch((error) => { diff --git a/packages/agent-runtime/src/tools/prompts.ts b/packages/agent-runtime/src/tools/prompts.ts index 83aebb2be1..2baa5b5f3b 100644 --- a/packages/agent-runtime/src/tools/prompts.ts +++ b/packages/agent-runtime/src/tools/prompts.ts @@ -1,11 +1,10 @@ -import { endsAgentStepParam, toolNames } from '@codebuff/common/tools/constants' +import { endsAgentStepParam } from '@codebuff/common/tools/constants' +import { toolParams } from '@codebuff/common/tools/list' import { getToolCallString } from '@codebuff/common/tools/utils' import { buildArray } from '@codebuff/common/util/array' import { pluralize } from '@codebuff/common/util/string' import z from 'zod/v4' -import { codebuffToolDefs } from './definitions/list' - import type { ToolName } from '@codebuff/common/tools/constants' import type { customToolDefinitionsSchema } from '@codebuff/common/util/file' import type { JSONSchema } from 'zod/v4/core' @@ -96,16 +95,16 @@ export function buildToolDescription(params: { } export const toolDescriptions = Object.fromEntries( - Object.entries(codebuffToolDefs).map(([name, config]) => [ + Object.entries(toolParams).map(([name, config]) => [ name, buildToolDescription({ toolName: name, - schema: { type: 'zod', value: config.parameters }, + schema: { type: 'zod', value: config.inputSchema }, description: config.description, endsAgentStep: config.endsAgentStep, }), ]), -) as Record +) as Record function buildShortToolDescription(params: { toolName: string @@ -248,13 +247,13 @@ export const getShortToolInstructions = ( const toolDescriptions = [ ...( toolNames.filter( - (name) => (name as keyof typeof codebuffToolDefs) in codebuffToolDefs, - ) as (keyof typeof codebuffToolDefs)[] + (name) => (name as keyof typeof toolParams) in toolParams, + ) as (keyof typeof toolParams)[] ).map((name) => { - const tool = codebuffToolDefs[name] + const tool = toolParams[name] return buildShortToolDescription({ toolName: name, - schema: { type: 'zod', value: tool.parameters }, + schema: { type: 'zod', value: tool.inputSchema }, endsAgentStep: tool.endsAgentStep, }) }), diff --git a/packages/agent-runtime/src/tools/stream-parser.ts b/packages/agent-runtime/src/tools/stream-parser.ts index 0b597a3008..b932227224 100644 --- a/packages/agent-runtime/src/tools/stream-parser.ts +++ b/packages/agent-runtime/src/tools/stream-parser.ts @@ -1,7 +1,7 @@ import { toolNames } from '@codebuff/common/tools/constants' import { buildArray } from '@codebuff/common/util/array' import { - toolJsonContent, + jsonToolResult, assistantMessage, } from '@codebuff/common/util/messages' import { generateCompactId } from '@codebuff/common/util/string' @@ -11,6 +11,7 @@ import { processStreamWithTags } from '../tool-stream-parser' import { executeCustomToolCall, executeToolCall } from './tool-executor' import { expireMessages } from '../util/messages' +import type { State } from './handlers/handler-function-type' import type { CustomToolCall, ExecuteToolCallParams } from './tool-executor' import type { AgentTemplate } from '../templates/types' import type { ToolName } from '@codebuff/common/tools/constants' @@ -35,35 +36,35 @@ export type ToolCallError = { export async function processStreamWithTools( params: { - clientSessionId: string - fingerprintId: string - userId: string | undefined - repoId: string | undefined - ancestorRunIds: string[] - runId: string + agentContext: Record + agentState: AgentState agentTemplate: AgentTemplate - localAgentTemplates: Record + ancestorRunIds: string[] fileContext: ProjectFileContext + fingerprintId: string + fullResponse: string + logger: Logger messages: Message[] - system: string - agentState: AgentState - agentContext: Record + repoId: string | undefined + runId: string signal: AbortSignal + system: string + prompt: string | undefined + userId: string | undefined + + onCostCalculated: (credits: number) => Promise onResponseChunk: (chunk: string | PrintModeEvent) => void - fullResponse: string sendSubagentChunk: SendSubagentChunkFn - logger: Logger - onCostCalculated: (credits: number) => Promise } & Omit< ExecuteToolCallParams, - | 'toolName' + | 'fullResponse' | 'input' + | 'previousToolCallFinished' + | 'state' | 'toolCalls' + | 'toolName' | 'toolResults' | 'toolResultsToAddAfterStream' - | 'previousToolCallFinished' - | 'fullResponse' - | 'state' > & ParamsExcluding< typeof processStreamWithTags, @@ -71,18 +72,17 @@ export async function processStreamWithTools( >, ) { const { - fingerprintId, userId, ancestorRunIds, runId, - repoId, agentTemplate, - localAgentTemplates, fileContext, agentContext, system, agentState, signal, + fullResponse, + prompt, onResponseChunk, sendSubagentChunk, logger, @@ -99,18 +99,20 @@ export async function processStreamWithTools( Promise.withResolvers() let previousToolCallFinished = streamDonePromise - const state: Record = { - fingerprintId, - userId, - repoId, - agentTemplate, - localAgentTemplates, + const state: State = { + fullResponse, + prompt, sendSubagentChunk, agentState, agentContext, messages, system, logger, + promisesByPath: {}, + allPromises: [], + fileChangeErrors: [], + fileChanges: [], + firstFileProcessed: false, } function toolCallback(toolName: T) { @@ -125,13 +127,15 @@ export async function processStreamWithTools( ...params, toolName, input, + + fromHandleSteps: false, + fullResponse: fullResponseChunks.join(''), + previousToolCallFinished, + state, toolCalls, toolResults, toolResultsToAddAfterStream, - previousToolCallFinished, - fullResponse: fullResponseChunks.join(''), - state, - fromHandleSteps: false, + onCostCalculated, }) }, @@ -149,12 +153,13 @@ export async function processStreamWithTools( ...params, toolName, input, + + fullResponse: fullResponseChunks.join(''), + previousToolCallFinished, + state, toolCalls, toolResults, toolResultsToAddAfterStream, - previousToolCallFinished, - fullResponse: fullResponseChunks.join(''), - state, }) }, } @@ -175,11 +180,9 @@ export async function processStreamWithTools( role: 'tool', toolName, toolCallId: generateCompactId(), - content: [ - toolJsonContent({ - errorMessage: error, - }), - ], + content: jsonToolResult({ + errorMessage: error, + }), } toolResults.push(cloneDeep(toolResult)) toolResultsToAddAfterStream.push(cloneDeep(toolResult)) @@ -231,11 +234,11 @@ export async function processStreamWithTools( await previousToolCallFinished } return { - toolCalls, - toolResults, - state, fullResponse: fullResponseChunks.join(''), fullResponseChunks, messageId, + state, + toolCalls, + toolResults, } } diff --git a/packages/agent-runtime/src/tools/tool-executor.ts b/packages/agent-runtime/src/tools/tool-executor.ts index cff55f3606..63bfc65015 100644 --- a/packages/agent-runtime/src/tools/tool-executor.ts +++ b/packages/agent-runtime/src/tools/tool-executor.ts @@ -1,18 +1,21 @@ import { endsAgentStepParam } from '@codebuff/common/tools/constants' -import { toolJsonContent } from '@codebuff/common/util/messages' +import { toolParams } from '@codebuff/common/tools/list' +import { jsonToolResult } from '@codebuff/common/util/messages' +import { removeUndefinedProps } from '@codebuff/common/util/object' import { generateCompactId } from '@codebuff/common/util/string' -import { type ToolCallPart } from 'ai' import { cloneDeep } from 'lodash' import z from 'zod/v4' import { convertJsonSchemaToZod } from 'zod-from-json-schema' import { checkLiveUserInput } from '../live-user-inputs' import { getMCPToolData } from '../mcp' -import { codebuffToolDefs } from './definitions/list' import { codebuffToolHandlers } from './handlers/list' import type { AgentTemplate } from '../templates/types' -import type { CodebuffToolHandlerFunction } from './handlers/handler-function-type' +import type { + State, + CodebuffToolHandlerFunction, +} from './handlers/handler-function-type' import type { ToolName } from '@codebuff/common/tools/constants' import type { ClientToolCall, @@ -31,6 +34,7 @@ import type { customToolDefinitionsSchema, ProjectFileContext, } from '@codebuff/common/util/file' +import type { ToolCallPart } from 'ai' export type CustomToolCall = { toolName: string @@ -54,7 +58,7 @@ export function parseRawToolCall(params: { const { rawToolCall, autoInsertEndStepParam = false } = params const toolName = rawToolCall.toolName - if (!(toolName in codebuffToolDefs)) { + if (!(toolName in toolParams)) { return { toolName, toolCallId: rawToolCall.toolCallId, @@ -72,19 +76,16 @@ export function parseRawToolCall(params: { // Add the required codebuff_end_step parameter with the correct value for this tool if requested if (autoInsertEndStepParam) { processedParameters[endsAgentStepParam] = - codebuffToolDefs[validName].endsAgentStep + toolParams[validName].endsAgentStep } - const paramsSchema = codebuffToolDefs[validName].endsAgentStep + const paramsSchema = toolParams[validName].endsAgentStep ? ( - codebuffToolDefs[validName] - .parameters satisfies z.ZodObject as z.ZodObject + toolParams[validName].inputSchema satisfies z.ZodObject as z.ZodObject ).extend({ - [endsAgentStepParam]: z.literal( - codebuffToolDefs[validName].endsAgentStep, - ), + [endsAgentStepParam]: z.literal(toolParams[validName].endsAgentStep), }) - : codebuffToolDefs[validName].parameters + : toolParams[validName].inputSchema const result = paramsSchema.safeParse(processedParameters) if (!result.success) { @@ -114,29 +115,33 @@ export function parseRawToolCall(params: { export type ExecuteToolCallParams = { toolName: T input: Record - toolCalls: (CodebuffToolCall | CustomToolCall)[] - toolResults: ToolMessage[] - toolResultsToAddAfterStream: ToolMessage[] - previousToolCallFinished: Promise - agentTemplate: AgentTemplate - fileContext: ProjectFileContext - runId: string + autoInsertEndStepParam?: boolean + excludeToolFromMessageHistory?: boolean + agentStepId: string + ancestorRunIds: string[] + agentTemplate: AgentTemplate clientSessionId: string - userInputId: string + fileContext: ProjectFileContext + fingerprintId: string + fromHandleSteps?: boolean fullResponse: string + localAgentTemplates: Record + previousToolCallFinished: Promise repoId: string | undefined repoUrl: string | undefined + runId: string signal: AbortSignal - onResponseChunk: (chunk: string | PrintModeEvent) => void - state: Record + state: State + toolCalls: (CodebuffToolCall | CustomToolCall)[] + toolResults: ToolMessage[] + toolResultsToAddAfterStream: ToolMessage[] userId: string | undefined - autoInsertEndStepParam?: boolean - excludeToolFromMessageHistory?: boolean + userInputId: string + fetch: typeof globalThis.fetch - fromHandleSteps?: boolean onCostCalculated: (credits: number) => Promise - ancestorRunIds: string[] + onResponseChunk: (chunk: string | PrintModeEvent) => void } & AgentRuntimeDeps & AgentRuntimeScopedDeps @@ -146,28 +151,22 @@ export function executeToolCall( const { toolName, input, + autoInsertEndStepParam = false, + excludeToolFromMessageHistory = false, + fromHandleSteps = false, + + agentTemplate, + logger, + previousToolCallFinished, + state, toolCalls, toolResults, toolResultsToAddAfterStream, - previousToolCallFinished, - agentTemplate, - fileContext, - agentStepId, - clientSessionId, userInputId, - fullResponse, + + onCostCalculated, onResponseChunk, - state, - repoId, - repoUrl, - userId, - autoInsertEndStepParam = false, - excludeToolFromMessageHistory = false, requestToolCall, - requestMcpToolData, - logger, - fromHandleSteps = false, - onCostCalculated, } = params const toolCall: CodebuffToolCall | ToolCallError = parseRawToolCall({ rawToolCall: { @@ -182,11 +181,9 @@ export function executeToolCall( role: 'tool', toolName, toolCallId: toolCall.toolCallId, - content: [ - toolJsonContent({ - errorMessage: toolCall.error, - }), - ], + content: jsonToolResult({ + errorMessage: toolCall.error, + }), } toolResults.push(cloneDeep(toolResult)) toolResultsToAddAfterStream.push(cloneDeep(toolResult)) @@ -219,11 +216,9 @@ export function executeToolCall( role: 'tool', toolName, toolCallId: toolCall.toolCallId, - content: [ - toolJsonContent({ - errorMessage: `Tool \`${toolName}\` is not currently available. Make sure to only use tools listed in the system instructions.`, - }), - ], + content: jsonToolResult({ + errorMessage: `Tool \`${toolName}\` is not currently available. Make sure to only use tools listed in the system instructions.`, + }), } toolResults.push(cloneDeep(toolResult)) toolResultsToAddAfterStream.push(cloneDeep(toolResult)) @@ -257,20 +252,37 @@ export function executeToolCall( state, }) - for (const [key, value] of Object.entries(stateUpdate ?? {})) { - if (key === 'agentState' && typeof value === 'object' && value !== null) { - // Replace the agentState reference to ensure all updates are captured - state.agentState = value - } else if (key === 'creditsUsed') { + for (const [pairk, pairv] of Object.entries( + removeUndefinedProps(stateUpdate ?? {}), + )) { + const pair = { key: pairk, value: pairv } as { + [K in keyof Required]: { + key: K + value: Required[K] + } + }[keyof Required] + if (pair.key === 'creditsUsed') { // Handle both synchronous and asynchronous creditsUsed values - if (value instanceof Promise) { + if (pair.value instanceof Promise) { // Store the promise to be awaited later - state.creditsUsed = value - } else if (typeof value === 'number') { - onCostCalculated(value) + state.creditsUsed = pair.value + } else if (typeof pair.value === 'number') { + onCostCalculated(pair.value) + } + } else if (pair.value !== undefined) { + if (pair.key === 'agentContext') { + state.agentContext = pair.value + } else if (pair.key === 'agentState') { + state.agentState = pair.value + } else if (pair.key === 'logger') { + state.logger = pair.value + } else if (pair.key === 'messages') { + state.messages = pair.value + } else if (pair.key === 'sendSubagentChunk') { + state.sendSubagentChunk = pair.value + } else if (pair.key === 'system') { + state.system = pair.value } - } else { - state[key] = value } } @@ -433,11 +445,9 @@ export async function executeCustomToolCall( role: 'tool', toolName, toolCallId: toolCall.toolCallId, - content: [ - toolJsonContent({ - errorMessage: toolCall.error, - }), - ], + content: jsonToolResult({ + errorMessage: toolCall.error, + }), } toolResults.push(cloneDeep(toolResult)) toolResultsToAddAfterStream.push(cloneDeep(toolResult)) @@ -474,11 +484,9 @@ export async function executeCustomToolCall( role: 'tool', toolName, toolCallId: toolCall.toolCallId, - content: [ - toolJsonContent({ - errorMessage: `Tool \`${toolName}\` is not currently available. Make sure to only use tools listed in the system instructions.`, - }), - ], + content: jsonToolResult({ + errorMessage: `Tool \`${toolName}\` is not currently available. Make sure to only use tools listed in the system instructions.`, + }), } toolResults.push(cloneDeep(toolResult)) toolResultsToAddAfterStream.push(cloneDeep(toolResult)) diff --git a/packages/agent-runtime/src/util/__tests__/messages.test.ts b/packages/agent-runtime/src/util/__tests__/messages.test.ts index 2923f4b188..3ee55e7093 100644 --- a/packages/agent-runtime/src/util/__tests__/messages.test.ts +++ b/packages/agent-runtime/src/util/__tests__/messages.test.ts @@ -1,7 +1,7 @@ import { assistantMessage, + jsonToolResult, systemMessage, - toolJsonContent, userMessage, } from '@codebuff/common/util/messages' import { @@ -75,34 +75,34 @@ describe('trimMessagesToFitTokenLimit', () => { role: 'tool', toolName: 'run_terminal_command', toolCallId: 'test-id-0', - content: [toolJsonContent(`Terminal output 0${'.'.repeat(2000)}`)], + content: jsonToolResult(`Terminal output 0${'.'.repeat(2000)}`), }, { // Terminal output 1 - should be preserved (shorter than '[Output omitted]') role: 'tool', toolName: 'run_terminal_command', toolCallId: 'test-id-1', - content: [toolJsonContent(`Short output 1`)], + content: jsonToolResult(`Short output 1`), }, { // Terminal output 2 - should be simplified role: 'tool', toolName: 'run_terminal_command', toolCallId: 'test-id-2', - content: [toolJsonContent(`Terminal output 2${'.'.repeat(2000)}`)], + content: jsonToolResult(`Terminal output 2${'.'.repeat(2000)}`), }, { // Terminal output 3 - should be preserved (5th most recent) role: 'tool', toolName: 'run_terminal_command', toolCallId: 'test-id-3', - content: [toolJsonContent(`Terminal output 3`)], + content: jsonToolResult(`Terminal output 3`), }, { role: 'tool', toolName: 'run_terminal_command', toolCallId: 'test-id-4', - content: [toolJsonContent(`Terminal output 4`)], + content: jsonToolResult(`Terminal output 4`), }, // Regular message - should never be shortened userMessage({ @@ -115,21 +115,21 @@ describe('trimMessagesToFitTokenLimit', () => { role: 'tool', toolName: 'run_terminal_command', toolCallId: 'test-id-5', - content: [toolJsonContent(`Terminal output 5`)], + content: jsonToolResult(`Terminal output 5`), }, { // Terminal output 6 - should be preserved (2nd most recent) role: 'tool', toolName: 'run_terminal_command', toolCallId: 'test-id-6', - content: [toolJsonContent(`Terminal output 6`)], + content: jsonToolResult(`Terminal output 6`), }, { // Terminal output 7 - should be preserved (most recent) role: 'tool', toolName: 'run_terminal_command', toolCallId: 'test-id-7', - content: [toolJsonContent(`Terminal output 7`)], + content: jsonToolResult(`Terminal output 7`), }, // Regular message - should never be shortened assistantMessage( @@ -420,12 +420,10 @@ describe('getPreviouslyReadFiles', () => { role: 'tool', toolName: 'write_file', toolCallId: 'test-id', - content: [ - toolJsonContent({ - file: 'test.ts', - errorMessage: 'error', - }), - ], + content: jsonToolResult({ + file: 'test.ts', + errorMessage: 'error', + }), } satisfies CodebuffToolMessage<'write_file'>, ] @@ -439,19 +437,17 @@ describe('getPreviouslyReadFiles', () => { role: 'tool', toolName: 'read_files', toolCallId: 'test-id', - content: [ - toolJsonContent([ - { - path: 'src/test.ts', - content: 'export function test() {}', - referencedBy: { 'main.ts': ['line 10'] }, - }, - { - path: 'src/utils.ts', - content: 'export const utils = {}', - }, - ] as const), - ], + content: jsonToolResult([ + { + path: 'src/test.ts', + content: 'export function test() {}', + referencedBy: { 'main.ts': ['line 10'] }, + }, + { + path: 'src/utils.ts', + content: 'export const utils = {}', + }, + ] as const), } satisfies CodebuffToolMessage<'read_files'>, ] @@ -475,14 +471,12 @@ describe('getPreviouslyReadFiles', () => { role: 'tool', toolName: 'find_files', toolCallId: 'test-id', - content: [ - toolJsonContent([ - { - path: 'components/Button.tsx', - content: 'export const Button = () => {}', - }, - ] as const), - ], + content: jsonToolResult([ + { + path: 'components/Button.tsx', + content: 'export const Button = () => {}', + }, + ] as const), } satisfies CodebuffToolMessage<'find_files'>, ] @@ -501,27 +495,23 @@ describe('getPreviouslyReadFiles', () => { role: 'tool', toolName: 'read_files', toolCallId: 'test-id-1', - content: [ - toolJsonContent([ - { - path: 'file1.ts', - content: 'content 1', - }, - ]), - ], + content: jsonToolResult([ + { + path: 'file1.ts', + content: 'content 1', + }, + ]), } satisfies CodebuffToolMessage<'read_files'>, { role: 'tool', toolName: 'find_files', toolCallId: 'test-id-2', - content: [ - toolJsonContent([ - { - path: 'file2.ts', - content: 'content 2', - }, - ]), - ], + content: jsonToolResult([ + { + path: 'file2.ts', + content: 'content 2', + }, + ]), } satisfies CodebuffToolMessage<'find_files'>, userMessage('Some user message'), ] @@ -539,22 +529,20 @@ describe('getPreviouslyReadFiles', () => { role: 'tool', toolName: 'read_files', toolCallId: 'test-id', - content: [ - toolJsonContent([ - { - path: 'small-file.ts', - content: 'small content', - }, - { - path: 'large-file.ts', - contentOmittedForLength: true, - }, - { - path: 'another-small-file.ts', - content: 'another small content', - }, - ] as const), - ], + content: jsonToolResult([ + { + path: 'small-file.ts', + content: 'small content', + }, + { + path: 'large-file.ts', + contentOmittedForLength: true, + }, + { + path: 'another-small-file.ts', + content: 'another small content', + }, + ] as const), } satisfies CodebuffToolMessage<'read_files'>, ] @@ -590,11 +578,9 @@ describe('getPreviouslyReadFiles', () => { role: 'tool', toolName: 'find_files', toolCallId: 'test-id', - content: [ - toolJsonContent({ - message: 'No files found matching the criteria', - }), - ], + content: jsonToolResult({ + message: 'No files found matching the criteria', + }), } satisfies CodebuffToolMessage<'find_files'>, ] @@ -611,14 +597,12 @@ describe('getPreviouslyReadFiles', () => { role: 'tool', toolName: 'read_files', toolCallId: 'test-id', - content: [ - toolJsonContent([ - { - path: 'test.ts', - content: 'test content', - }, - ]), - ], + content: jsonToolResult([ + { + path: 'test.ts', + content: 'test content', + }, + ]), } satisfies CodebuffToolMessage<'read_files'>, ] @@ -632,7 +616,7 @@ describe('getPreviouslyReadFiles', () => { role: 'tool', toolName: 'read_files', toolCallId: 'test-id', - content: [toolJsonContent([])], + content: jsonToolResult([]), } satisfies CodebuffToolMessage<'read_files'>, ]