Chat with a Duck
chat_with_duckDebug your problems by explaining them to a rubber duck that maintains conversation context. Choose from various AI providers, models, and optionally include images.
Instructions
Have a conversation with a duck, maintaining context across messages
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| conversation_id | Yes | Conversation ID (creates new if not exists) | |
| message | Yes | Your message to the duck | |
| provider | No | Provider to use (can switch mid-conversation) | |
| model | No | Specific model to use (optional) | |
| images | No | Optional images to include with the message (for vision-capable models) |
Implementation Reference
- src/tools/chat-duck.ts:7-81 (handler)Main handler for chat_with_duck tool. Manages conversation context (create/retrieve/switch), adds user messages, calls the LLM provider, records assistant responses, and returns formatted output with conversation metadata.
export async function chatDuckTool( providerManager: ProviderManager, conversationManager: ConversationManager, args: Record<string, unknown> ) { const { conversation_id, message, provider, model, images } = args as { conversation_id?: string; message?: string; provider?: string; model?: string; images?: ImageInput[]; }; if (!conversation_id || !message) { throw new Error('conversation_id and message are required'); } // Get or create conversation let conversation = conversationManager.getConversation(conversation_id); if (!conversation) { // Create new conversation with specified or default provider const providerName = provider || providerManager.getProviderNames()[0]; conversation = conversationManager.createConversation(conversation_id, providerName); logger.info(`Created new conversation: ${conversation_id} with ${providerName}`); } else if (provider && provider !== conversation.provider) { // Switch provider if requested conversation = conversationManager.switchProvider(conversation_id, provider); logger.info(`Switched conversation ${conversation_id} to ${provider}`); } // Add user message to conversation const userContent = buildContent(message, images); conversationManager.addMessage(conversation_id, { role: 'user', content: userContent, timestamp: new Date(), }); // Get conversation context const messages = conversationManager.getConversationContext(conversation_id); // Get response from provider const providerToUse = provider || conversation.provider; const response = await providerManager.askDuck(providerToUse, '', { messages, model, }); // Add assistant response to conversation conversationManager.addMessage(conversation_id, { role: 'assistant', content: response.content, timestamp: new Date(), provider: providerToUse, }); // Format response const formattedResponse = formatDuckResponse(response.nickname, response.content, response.model); // Add conversation info const conversationInfo = `\n\nš¬ Conversation: ${conversation_id} | Messages: ${messages.length + 1}`; const latencyInfo = `\nā±ļø Latency: ${response.latency}ms`; logger.info(`Duck ${response.nickname} responded in conversation ${conversation_id}`); return { content: [ { type: 'text', text: formattedResponse + conversationInfo + latencyInfo, }, ], }; } - src/server.ts:284-317 (registration)Registration of the chat_with_duck tool via this.server.registerTool, including input schema with conversation_id, message, optional provider/model/images, and an async handler that delegates to chatDuckTool.
// chat_with_duck this.server.registerTool( 'chat_with_duck', { title: 'Chat with a Duck', description: 'Have a conversation with a duck, maintaining context across messages', inputSchema: { conversation_id: z.string().describe('Conversation ID (creates new if not exists)'), message: z.string().describe('Your message to the duck'), provider: this.providerEnum().optional().describe('Provider to use (can switch mid-conversation)'), model: z.string().optional().describe('Specific model to use (optional)'), images: z .array(ImageInputSchema) .optional() .describe('Optional images to include with the message (for vision-capable models)'), }, annotations: { openWorldHint: true, }, }, async (args) => { try { return this.toolResult( await chatDuckTool( this.providerManager, this.conversationManager, args as Record<string, unknown> ) ); } catch (error) { return this.toolErrorResult(error); } } ); - src/server.ts:27-35 (schema)Shared Zod schema for image inputs (base64 data, URL, MIME type) used by chat_with_duck and other tools.
const ImageInputSchema = z .object({ data: z.string().optional().describe('Base64-encoded image data'), url: z.string().optional().describe('Image URL ā passed directly to the LLM provider'), mimeType: z .string() .optional() .describe('MIME type (e.g., "image/png") ā required for base64 data, optional for URLs'), }) - src/utils/ascii-art.ts:80-85 (helper)Formats the duck's response with emoji, provider name, and optional model for display.
export function formatDuckResponse(provider: string, message: string, model?: string): string { if (model) { return `š¦ [${provider} | ${model}]: ${message}`; } return `š¦ [${provider}]: ${message}`; } - src/config/types.ts:201-214 (helper)Builds message content array with text and optional image parts (URL or base64) for chat_with_duck.
export function buildContent(text: string, images?: ImageInput[]): MessageContent { if (!images || images.length === 0) return text; const parts: ContentPart[] = [{ type: 'text', text }]; for (const img of images) { if (img.url) { const part: ImageContentPartUrl = { type: 'image', url: img.url }; if (img.mimeType) part.mimeType = img.mimeType; parts.push(part); } else if (img.data !== undefined && img.mimeType !== undefined) { parts.push({ type: 'image', data: img.data, mimeType: img.mimeType }); } } return parts; }