diff --git a/.agents/README.md b/.agents/README.md index 2f323f4d76..11f4290df6 100644 --- a/.agents/README.md +++ b/.agents/README.md @@ -10,9 +10,26 @@ Create specialized agent workflows that coordinate multiple AI agents to tackle ## Need Help? -- For detailed documentation, see [agent-guide.md](./agent-guide.md). - For examples, check the `examples/` directory. - Join our [Discord community](https://codebuff.com/discord) and ask your questions! +- Check our [documentation](https://codebuff.com/docs) for more details + +# What is Codebuff? + +Codebuff is an **open-source AI coding assistant** that edits your codebase through natural language instructions. Instead of using one model for everything, it coordinates specialized agents that work together to understand your project and make precise changes. + +Codebuff beats Claude Code at 61% vs 53% on [our evals](https://github.com/CodebuffAI/codebuff/tree/main/evals) across 175+ coding tasks over multiple open-source repos that simulate real-world tasks. + +## How Codebuff Works + +When you ask Codebuff to "add authentication to my API," it might invoke: + +1. A **File Explorer Agent** to scan your codebase to understand the architecture and find relevant files +2. A **Planner Agent** to plan which files need changes and in what order +3. An **Editor Agent** to make precise edits +4. A **Reviewer Agent** to validate changes + +This multi-agent approach gives you better context understanding, more accurate edits, and fewer errors compared to single-model tools. ## Context Window Management @@ -54,3 +71,223 @@ export default { ``` This agent systematically analyzes changes, reads relevant files for context, then creates commits with clear, meaningful messages that explain the "why" behind changes. + +# Agent Development Guide + +This guide covers everything you need to know about building custom Codebuff agents. + +## Agent Structure + +Each agent is a TypeScript file that exports an `AgentDefinition` object: + +```typescript +export default { + id: 'my-agent', // Unique identifier (lowercase, hyphens only) + displayName: 'My Agent', // Human-readable name + model: 'claude-3-5-sonnet', // AI model to use + toolNames: ['read_files', 'write_file'], // Available tools + instructionsPrompt: 'You are...', // Agent behavior instructions + spawnerPrompt: 'Use this agent when...', // When others should spawn this + spawnableAgents: ['helper-agent'], // Agents this can spawn + + // Optional: Programmatic control + async *handleSteps() { + yield { tool: 'read_files', paths: ['src/config.ts'] } + yield 'STEP' // Let AI process and respond + }, +} +``` + +## Core Properties + +### Required Fields + +- **`id`**: Unique identifier using lowercase letters and hyphens only +- **`displayName`**: Human-readable name shown in UI +- **`model`**: AI model from OpenRouter (see [available models](https://openrouter.ai/models)) +- **`instructionsPrompt`**: Detailed instructions defining the agent's role and behavior + +### Optional Fields + +- **`toolNames`**: Array of tools the agent can use (defaults to common tools) +- **`spawnerPrompt`**: Instructions for when other agents should spawn this one +- **`spawnableAgents`**: Array of agent names this agent can spawn +- **`handleSteps`**: Generator function for programmatic control + +## Available Tools + +### File Operations + +- **`read_files`**: Read file contents +- **`write_file`**: Create or modify entire files +- **`str_replace`**: Make targeted string replacements +- **`code_search`**: Search for patterns across the codebase + +### Execution + +- **`run_terminal_command`**: Execute shell commands +- **`spawn_agents`**: Delegate tasks to other agents +- **`end_turn`**: Finish the agent's response + +### Web & Research + +- **`web_search`**: Search the internet for information +- **`read_docs`**: Read technical documentation +- **`browser_logs`**: Navigate and inspect web pages + +See `types/tools.ts` for detailed parameter information. + +## Programmatic Control + +Use the `handleSteps` generator function to mix AI reasoning with programmatic logic: + +```typescript +async *handleSteps() { + // Execute a tool + yield { tool: 'read_files', paths: ['package.json'] } + + // Let AI process results and respond + yield 'STEP' + + // Conditional logic + if (needsMoreAnalysis) { + yield { tool: 'spawn_agents', agents: ['deep-analyzer'] } + yield 'STEP_ALL' // Wait for spawned agents to complete + } + + // Final AI response + yield 'STEP' +} +``` + +### Control Commands + +- **`'STEP'`**: Let AI process and respond once +- **`'STEP_ALL'`**: Let AI continue until completion +- **Tool calls**: `{ tool: 'tool_name', ...params }` + +## Model Selection + +Choose models based on your agent's needs: + +- **`anthropic/claude-sonnet-4`**: Best for complex reasoning and code generation +- **`openai/gpt-5`**: Strong general-purpose capabilities +- **`x-ai/grok-4-fast`**: Fast and cost-effective for simple or medium-complexity tasks + +**Any model on OpenRouter**: Unlike Claude Code which locks you into Anthropic's models, Codebuff supports any model available on [OpenRouter](https://openrouter.ai/models) - from Claude and GPT to specialized models like Qwen, DeepSeek, and others. Switch models for different tasks or use the latest releases without waiting for platform updates. + +See [OpenRouter](https://openrouter.ai/models) for all available models and pricing. + +## Agent Coordination + +Agents can spawn other agents to create sophisticated workflows: + +```typescript +// Parent agent spawns specialists +async *handleSteps() { + yield { tool: 'spawn_agents', agents: [ + 'security-scanner', + 'performance-analyzer', + 'code-reviewer' + ]} + yield 'STEP_ALL' // Wait for all to complete + + // Synthesize results + yield 'STEP' +} +``` + +**Reuse any published agent**: Compose existing [published agents](https://www.codebuff.com/store) to get a leg up. Codebuff agents are the new MCP! + +## Best Practices + +### Instructions + +- Be specific about the agent's role and expertise +- Include examples of good outputs +- Specify when the agent should ask for clarification +- Define the agent's limitations + +### Tool Usage + +- Start with file exploration tools (`read_files`, `code_search`) +- Use `str_replace` for targeted edits, `write_file` for major changes +- Always use `end_turn` to finish responses cleanly + +### Error Handling + +- Include error checking in programmatic flows +- Provide fallback strategies for failed operations +- Log important decisions for debugging + +### Performance + +- Choose appropriate models for the task complexity +- Minimize unnecessary tool calls +- Use spawnable agents for parallel processing + +## Testing Your Agent + +1. **Local Testing**: `codebuff --agent your-agent-name` +2. **Debug Mode**: Add logging to your `handleSteps` function +3. **Unit Testing**: Test individual functions in isolation +4. **Integration Testing**: Test agent coordination workflows + +## Publishing & Sharing + +1. **Validate**: Ensure your agent works across different codebases +2. **Document**: Include clear usage instructions +3. **Publish**: `codebuff publish your-agent-name` +4. **Maintain**: Update as models and tools evolve + +## Advanced Patterns + +### Conditional Workflows + +```typescript +async *handleSteps() { + const config = yield { tool: 'read_files', paths: ['config.json'] } + yield 'STEP' + + if (config.includes('typescript')) { + yield { tool: 'spawn_agents', agents: ['typescript-expert'] } + } else { + yield { tool: 'spawn_agents', agents: ['javascript-expert'] } + } + yield 'STEP_ALL' +} +``` + +### Iterative Refinement + +```typescript +async *handleSteps() { + for (let attempt = 0; attempt < 3; attempt++) { + yield { tool: 'run_terminal_command', command: 'npm test' } + yield 'STEP' + + if (allTestsPass) break + + yield { tool: 'spawn_agents', agents: ['test-fixer'] } + yield 'STEP_ALL' + } +} +``` + +## Why Choose Codebuff for Custom Agents + +**Deep customizability**: Create sophisticated agent workflows with TypeScript generators that mix AI generation with programmatic control. Define custom agents that spawn subagents, implement conditional logic, and orchestrate complex multi-step processes that adapt to your specific use cases. + +**Fully customizable SDK**: Build Codebuff's capabilities directly into your applications with a complete TypeScript SDK. Create custom tools, integrate with your CI/CD pipeline, build AI-powered development environments, or embed intelligent coding assistance into your products. + +Learn more about the SDK [here](https://www.npmjs.com/package/@codebuff/sdk). + +## Community & Support + +- **Discord**: [Join our community](https://codebuff.com/discord) for help and inspiration +- **Examples**: Study the `examples/` directory for patterns +- **Documentation**: [codebuff.com/docs](https://codebuff.com/docs) and check `types/` for detailed type information +- **Issues**: [Report bugs and request features on GitHub](https://github.com/CodebuffAI/codebuff/issues) +- **Support**: [support@codebuff.com](mailto:support@codebuff.com) + +Happy agent building! 🤖 diff --git a/.agents/__tests__/context-pruner.test.ts b/.agents/__tests__/context-pruner.test.ts index 8d23c2879e..97a5ce8508 100644 --- a/.agents/__tests__/context-pruner.test.ts +++ b/.agents/__tests__/context-pruner.test.ts @@ -66,7 +66,16 @@ describe('context-pruner handleSteps', () => { const runHandleSteps = (messages: Message[]) => { mockAgentState.messageHistory = messages - const generator = contextPruner.handleSteps!({ agentState: mockAgentState }) + const mockLogger = { + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + } + const generator = contextPruner.handleSteps!({ + agentState: mockAgentState, + logger: mockLogger, + }) const results: any[] = [] let result = generator.next() while (!result.done) { @@ -324,7 +333,16 @@ describe('context-pruner edge cases', () => { const runHandleSteps = (messages: Message[]) => { mockAgentState.messageHistory = messages - const generator = contextPruner.handleSteps!({ agentState: mockAgentState }) + const mockLogger = { + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + } + const generator = contextPruner.handleSteps!({ + agentState: mockAgentState, + logger: mockLogger, + }) const results: ReturnType['value'][] = [] let result = generator.next() while (!result.done) { diff --git a/.agents/types/agent-definition.ts b/.agents/types/agent-definition.ts index 5d0596d087..34f969552f 100644 --- a/.agents/types/agent-definition.ts +++ b/.agents/types/agent-definition.ts @@ -18,6 +18,17 @@ import type * as Tools from './tools' import type { Message, ToolResultOutput, JsonObjectSchema } from './util-types' type ToolName = Tools.ToolName +// ============================================================================ +// Logger Interface +// ============================================================================ + +export interface Logger { + debug: (data: any, msg?: string) => void + info: (data: any, msg?: string) => void + warn: (data: any, msg?: string) => void + error: (data: any, msg?: string) => void +} + // ============================================================================ // Agent Definition and Utility Types // ============================================================================ @@ -144,7 +155,8 @@ export interface AgentDefinition { * Or use 'return' to end the turn. * * Example 1: - * function* handleSteps({ agentStep, prompt, params}) { + * function* handleSteps({ agentState, prompt, params, logger }) { + * logger.info('Starting file read process') * const { toolResult } = yield { * toolName: 'read_files', * input: { paths: ['file1.txt', 'file2.txt'] } @@ -152,6 +164,7 @@ export interface AgentDefinition { * yield 'STEP_ALL' * * // Optionally do a post-processing step here... + * logger.info('Files read successfully, setting output') * yield { * toolName: 'set_output', * input: { @@ -161,8 +174,9 @@ export interface AgentDefinition { * } * * Example 2: - * handleSteps: function* ({ agentState, prompt, params }) { + * handleSteps: function* ({ agentState, prompt, params, logger }) { * while (true) { + * logger.debug('Spawning thinker agent') * yield { * toolName: 'spawn_agents', * input: { @@ -213,6 +227,7 @@ export interface AgentStepContext { agentState: AgentState prompt?: string params?: Record + logger: Logger } /** diff --git a/backend/src/__tests__/run-programmatic-step.test.ts b/backend/src/__tests__/run-programmatic-step.test.ts index fc9acfd2af..d25d5479a6 100644 --- a/backend/src/__tests__/run-programmatic-step.test.ts +++ b/backend/src/__tests__/run-programmatic-step.test.ts @@ -84,10 +84,9 @@ describe('runProgrammaticStep', () => { ) // Mock sendAction - sendActionSpy = spyOn( - websocketAction, - 'sendAction', - ).mockImplementation(() => {}) + sendActionSpy = spyOn(websocketAction, 'sendAction').mockImplementation( + () => {}, + ) // Mock crypto.randomUUID spyOn(crypto, 'randomUUID').mockImplementation( @@ -118,7 +117,8 @@ describe('runProgrammaticStep', () => { mockAgentState = { ...sessionState.mainAgentState, agentId: 'test-agent-id', - runId: 'test-run-id' as `${string}-${string}-${string}-${string}-${string}`, + runId: + 'test-run-id' as `${string}-${string}-${string}-${string}-${string}`, messageHistory: [ { role: 'user', content: 'Initial message' }, { role: 'assistant', content: 'Initial response' }, diff --git a/backend/src/run-programmatic-step.ts b/backend/src/run-programmatic-step.ts index ab0ceff243..2d5f9d14fa 100644 --- a/backend/src/run-programmatic-step.ts +++ b/backend/src/run-programmatic-step.ts @@ -86,8 +86,29 @@ export async function runProgrammaticStep( let generator = runIdToGenerator[agentState.runId] let sandbox = sandboxManager.getSandbox(agentState.runId) - // Check if we need to initialize a generator (either native or QuickJS-based) + // Check if we need to initialize a generator if (!generator && !sandbox) { + const createLogMethod = + (level: 'debug' | 'info' | 'warn' | 'error') => + (data: any, msg?: string) => { + logger[level](data, msg) // Log to backend + sendAction(ws, { + type: 'handlesteps-log-chunk', + userInputId, + agentId: agentState.agentId, + level, + data, + message: msg, + }) + } + + const streamingLogger = { + debug: createLogMethod('debug'), + info: createLogMethod('info'), + warn: createLogMethod('warn'), + error: createLogMethod('error'), + } + if (typeof template.handleSteps === 'string') { // Initialize QuickJS sandbox for string-based generator sandbox = await sandboxManager.getOrCreateSandbox( @@ -97,7 +118,10 @@ export async function runProgrammaticStep( agentState, prompt, params, + logger: streamingLogger, }, + undefined, // config + streamingLogger, // pass the streaming logger instance for internal use ) } else { // Initialize native generator @@ -105,6 +129,7 @@ export async function runProgrammaticStep( agentState, prompt, params, + logger: streamingLogger, }) runIdToGenerator[agentState.runId] = generator } @@ -147,9 +172,10 @@ export async function runProgrammaticStep( ...data, }) }, - agentState: cloneDeep( - agentState as AgentState & Required>, - ), + agentState: cloneDeep({ + ...agentState, + runId: agentState.runId!, // We've already verified runId exists above + }), agentContext: cloneDeep(agentState.agentContext), messages: cloneDeep(agentState.messageHistory), } diff --git a/backend/src/util/quickjs-sandbox.ts b/backend/src/util/quickjs-sandbox.ts index 8d0be3eee7..1f5cd7a87d 100644 --- a/backend/src/util/quickjs-sandbox.ts +++ b/backend/src/util/quickjs-sandbox.ts @@ -52,6 +52,12 @@ export class QuickJSSandbox { generatorCode: string, initialInput: any, config: SandboxConfig = {}, + logger?: { + debug: (data: any, msg?: string) => void + info: (data: any, msg?: string) => void + warn: (data: any, msg?: string) => void + error: (data: any, msg?: string) => void + }, ): Promise { const { memoryLimit = 1024 * 1024 * 20, // 20MB @@ -80,6 +86,38 @@ export class QuickJSSandbox { const context = runtime.newContext() try { + // Set up logger handler + const loggerHandler = context.newFunction( + '_loggerHandler', + (level, data, msg) => { + try { + const levelStr = context.getString(level) + let dataObj: any + let msgStr: string | undefined + + try { + dataObj = data ? JSON.parse(context.getString(data)) : undefined + } catch { + dataObj = context.getString(data) + } + + msgStr = msg ? context.getString(msg) : undefined + + // Call the appropriate logger method if available + if (logger?.[levelStr as keyof typeof logger]) { + logger[levelStr as keyof typeof logger](dataObj, msgStr) + } + } catch (err) { + // Fallback for logging errors + if (logger?.error) { + logger.error({ error: err }, 'Logger handler error') + } + } + }, + ) + + context.setProp(context.global, '_loggerHandler', loggerHandler) + loggerHandler.dispose() // Inject safe globals and the generator function const setupCode = ` // Safe console implementation @@ -89,11 +127,26 @@ export class QuickJSSandbox { warn: (...args) => undefined }; + // Logger implementation + const createLogMethod = (level) => (data, msg) => + globalThis._loggerHandler(level, + typeof data === 'object' ? JSON.stringify(data) : String(data), + msg ? String(msg) : undefined); + + const logger = { + debug: createLogMethod('debug'), + info: createLogMethod('info'), + warn: createLogMethod('warn'), + error: createLogMethod('error') + }; + // Agent function const handleSteps = ${generatorCode}; - // Create generator instance - let generator = handleSteps(${JSON.stringify(initialInput)}); + // Create generator instance with logger injected into context + const context = ${JSON.stringify(initialInput)}; + context.logger = logger; + let generator = handleSteps(context); // Generator management globalThis._generator = generator; @@ -213,6 +266,12 @@ export class SandboxManager { generatorCode: string, initialInput: any, config?: SandboxConfig, + logger?: { + debug: (data: any, msg?: string) => void + info: (data: any, msg?: string) => void + warn: (data: any, msg?: string) => void + error: (data: any, msg?: string) => void + }, ): Promise { const existing = this.sandboxes.get(runId) if (existing && existing.isInitialized()) { @@ -229,6 +288,7 @@ export class SandboxManager { generatorCode, initialInput, config, + logger, ) this.sandboxes.set(runId, sandbox) return sandbox diff --git a/common/src/actions.ts b/common/src/actions.ts index 1ccb7b373c..9bd13f1d83 100644 --- a/common/src/actions.ts +++ b/common/src/actions.ts @@ -130,6 +130,14 @@ export const SERVER_ACTION_SCHEMA = z.discriminatedUnion('type', [ chunk: z.string(), prompt: z.string().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'), diff --git a/common/src/templates/agent-validation.ts b/common/src/templates/agent-validation.ts index 82ab3dc2e2..2c97204760 100644 --- a/common/src/templates/agent-validation.ts +++ b/common/src/templates/agent-validation.ts @@ -284,6 +284,7 @@ export function validateSingleAgent( stepPrompt: validatedConfig.stepPrompt ?? '', outputSchema, inputSchema, + } return { diff --git a/common/src/templates/initial-agents-dir/types/agent-definition.ts b/common/src/templates/initial-agents-dir/types/agent-definition.ts index 5d0596d087..34f969552f 100644 --- a/common/src/templates/initial-agents-dir/types/agent-definition.ts +++ b/common/src/templates/initial-agents-dir/types/agent-definition.ts @@ -18,6 +18,17 @@ import type * as Tools from './tools' import type { Message, ToolResultOutput, JsonObjectSchema } from './util-types' type ToolName = Tools.ToolName +// ============================================================================ +// Logger Interface +// ============================================================================ + +export interface Logger { + debug: (data: any, msg?: string) => void + info: (data: any, msg?: string) => void + warn: (data: any, msg?: string) => void + error: (data: any, msg?: string) => void +} + // ============================================================================ // Agent Definition and Utility Types // ============================================================================ @@ -144,7 +155,8 @@ export interface AgentDefinition { * Or use 'return' to end the turn. * * Example 1: - * function* handleSteps({ agentStep, prompt, params}) { + * function* handleSteps({ agentState, prompt, params, logger }) { + * logger.info('Starting file read process') * const { toolResult } = yield { * toolName: 'read_files', * input: { paths: ['file1.txt', 'file2.txt'] } @@ -152,6 +164,7 @@ export interface AgentDefinition { * yield 'STEP_ALL' * * // Optionally do a post-processing step here... + * logger.info('Files read successfully, setting output') * yield { * toolName: 'set_output', * input: { @@ -161,8 +174,9 @@ export interface AgentDefinition { * } * * Example 2: - * handleSteps: function* ({ agentState, prompt, params }) { + * handleSteps: function* ({ agentState, prompt, params, logger }) { * while (true) { + * logger.debug('Spawning thinker agent') * yield { * toolName: 'spawn_agents', * input: { @@ -213,6 +227,7 @@ export interface AgentStepContext { agentState: AgentState prompt?: string params?: Record + logger: Logger } /** diff --git a/common/src/types/agent-template.ts b/common/src/types/agent-template.ts index 4ea162fb7b..6b4cbf31de 100644 --- a/common/src/types/agent-template.ts +++ b/common/src/types/agent-template.ts @@ -1,9 +1,19 @@ +/** + * Backend Agent Template Types + * + * This file provides backend-compatible agent template types with strict validation. + * It imports base types from the user-facing template to eliminate duplication. + */ + import type { Model } from '../old-constants' import type { ToolResultOutput } from './messages/content-part' import type { AgentState, AgentTemplateType } from './session-state' import type { + AgentDefinition, ToolCall, AgentState as PublicAgentState, + Logger, + AgentStepContext, } from '../templates/initial-agents-dir/types/agent-definition' import type { ToolName } from '../tools/constants' import type { OpenRouterProviderOptions } from '@codebuff/internal/openrouter-ai-sdk' @@ -11,6 +21,10 @@ import type { z } from 'zod/v4' export type AgentId = `${string}/${string}@${number}.${number}.${number}` +/** + * Backend agent template with strict validation and Zod schemas + * Extends the user-facing AgentDefinition but with backend-specific requirements + */ export type AgentTemplate< P = string | undefined, T = Record | undefined, @@ -54,6 +68,11 @@ export type StepGenerator = Generator< export type StepHandler< P = string | undefined, T = Record | undefined, -> = (params: { agentState: AgentState; prompt: P; params: T }) => StepGenerator +> = (context: { + agentState: AgentState + prompt: P + params: T + logger: Logger +}) => StepGenerator -export { PublicAgentState } +export { Logger, PublicAgentState } diff --git a/common/src/types/dynamic-agent-template.ts b/common/src/types/dynamic-agent-template.ts index 928849bdfe..dc00ad403a 100644 --- a/common/src/types/dynamic-agent-template.ts +++ b/common/src/types/dynamic-agent-template.ts @@ -68,6 +68,26 @@ export type PromptField = z.infer const functionSchema = (schema: T) => z.custom[0]>((fn: any) => schema.implement(fn)) +// Schema for the Logger interface +const LoggerSchema = z.object({ + debug: z.function({ + input: [z.any(), z.string().optional()], + output: z.void(), + }), + info: z.function({ + input: [z.any(), z.string().optional()], + output: z.void(), + }), + warn: z.function({ + input: [z.any(), z.string().optional()], + output: z.void(), + }), + error: z.function({ + input: [z.any(), z.string().optional()], + output: z.void(), + }), +}) + // Schema for validating handleSteps function signature const HandleStepsSchema = functionSchema( z.function({ @@ -81,6 +101,7 @@ const HandleStepsSchema = functionSchema( prompt: z.string().optional(), params: z.any().optional(), }), + LoggerSchema.optional(), ], output: z.any(), }), diff --git a/npm-app/src/client.ts b/npm-app/src/client.ts index 574471e446..bf9e7f6db1 100644 --- a/npm-app/src/client.ts +++ b/npm-app/src/client.ts @@ -205,6 +205,7 @@ export class Client { private responseComplete: boolean = false private userInputId: string | undefined private nonCancelledUserInputIds: string[] = [] + private currentOnChunk: ((chunk: string | PrintModeEvent) => void) | undefined public usageData: UsageData = { usage: 0, @@ -951,6 +952,58 @@ export class Client { // Refresh display if we're currently viewing this agent refreshSubagentDisplay(agentId) }) + + // Handle handleSteps log streaming + this.webSocket.subscribe('handlesteps-log-chunk', (action) => { + const { agentId, level, data, message } = action + const formattedMessage = this.formatLogMessage( + level, + data, + message, + agentId, + ) + + if (this.currentOnChunk && this.userInputId) { + this.currentOnChunk(formattedMessage + '\n') + } else { + process.stdout.write(formattedMessage + '\n') + } + }) + } + + private formatLogMessage( + level: string, + data: any, + message?: string, + agentId?: string, + ): string { + const timestamp = new Date().toISOString().substring(11, 23) // HH:MM:SS.mmm + const levelColors = { debug: blue, info: green, warn: yellow, error: red } + const levelColor = + levelColors[level as keyof typeof levelColors] || ((s: string) => s) + + const timeTag = `[${timestamp}]` + const levelTag = levelColor(`[${level.toUpperCase()}]`) + const agentTag = agentId ? `[Agent ${agentId}]` : '' + const dataStr = this.serializeLogData(data) + + return [timeTag, levelTag, agentTag, message, dataStr] + .filter(Boolean) + .join(' ') + } + + private serializeLogData(data: any): string { + if (data === undefined || data === null) return '' + + if (typeof data === 'object') { + try { + return JSON.stringify(data, null, 2) + } catch { + return String(data) + } + } + + return String(data) } private showUsageWarning() { @@ -1264,12 +1317,14 @@ export class Client { }) this.userInputId = userInputId + this.currentOnChunk = onChunk const stopResponse = () => { responseStopped = true unsubscribeChunks() unsubscribeComplete() this.cancelCurrentInput() + this.currentOnChunk = undefined const additionalMessages = prompt ? [ @@ -1491,6 +1546,10 @@ Go to https://www.codebuff.com/config for more information.`) + unsubscribeChunks() unsubscribeComplete() } + + // Clear the onChunk callback when response is complete + this.currentOnChunk = undefined + resolveResponse({ ...a, wasStoppedByUser: false }) }, )