From 8fe75b8c1c191fc4d819c3a5008881925c97a54c Mon Sep 17 00:00:00 2001 From: Rob Colbert Date: Wed, 29 Apr 2026 16:21:23 -0400 Subject: [PATCH] Phase 1-2: Fix type conflicts and implement prompt submission MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Resolve duplicate DroneStatus enum (import from @gadget/api) - Fix IAiProvider interface conflict with DB→runtime mapper - Add callId to ToolCallMessage and ChatToolCallSchema - Fix ChatTurnStats schema field name (thinkingTokenCount) - Add provider/selectedModel to ChatSession interface and model - Implement CodeSession.onSubmitPrompt() to create ChatTurn and send work orders - Add drone/chat session tracking to CodeSession - Add unit tests for CodeSession (9 tests, all passing) --- .opencode/plans/foundation-todo.md | 29 ++- gadget-code/src/lib/code-session.ts | 110 ++++++++++- gadget-code/src/models/chat-session.ts | 2 + gadget-code/src/models/chat-turn.ts | 3 +- gadget-code/tests/code-session.test.ts | 209 ++++++++++++++++++++ gadget-drone/src/gadget-drone.ts | 6 +- gadget-drone/src/services/agent.ts | 31 +-- gadget-drone/src/services/ai.ts | 79 ++++++-- gadget-drone/src/services/platform.ts | 8 +- packages/api/src/interfaces/chat-session.ts | 2 + packages/api/src/messages/drone.ts | 1 + 11 files changed, 425 insertions(+), 55 deletions(-) create mode 100644 gadget-code/tests/code-session.test.ts diff --git a/.opencode/plans/foundation-todo.md b/.opencode/plans/foundation-todo.md index c1a3c3f..f18dc14 100644 --- a/.opencode/plans/foundation-todo.md +++ b/.opencode/plans/foundation-todo.md @@ -5,41 +5,41 @@ --- -## Phase 1: Fix Type Errors & Interface Conflicts +## Phase 1: Fix Type Errors & Interface Conflicts ✅ COMPLETE ### 1.1 Resolve Duplicate `DroneStatus` Enum - **File:** `gadget-drone/src/services/platform.ts` - **Action:** Remove local `DroneStatus` enum, import from `@gadget/api` -- **Status:** ⬜ Pending +- **Status:** ✅ Complete ### 1.2 Resolve `IAiProvider` Interface Conflict - **Files:** - `packages/api/src/interfaces/ai-provider.ts` (Mongoose document) - `packages/ai/src/api.ts` (runtime config) - **Action:** Create mapper in `gadget-drone/src/services/ai.ts` to convert DB model → runtime config -- **Status:** ⬜ Pending +- **Status:** ✅ Complete ### 1.3 Fix `ToolCallMessage` Signature - **File:** `packages/api/src/messages/drone.ts:26-30` - **Issue:** Missing `callId` parameter required by `IChatToolCall` - **Action:** Add `callId: string` as first parameter -- **Status:** ⬜ Pending +- **Status:** ✅ Complete ### 1.4 Fix `ChatTurnStats` Schema Mismatch - **File:** `gadget-code/src/models/chat-turn.ts:70-76` - **Issue:** Schema uses `thinkingTokens`, interface uses `thinkingTokenCount` - **Action:** Standardize on `thinkingTokenCount` in schema -- **Status:** ⬜ Pending +- **Status:** ✅ Complete ### 1.5 Fix `ChatToolCallSchema` Missing `callId` - **File:** `gadget-code/src/models/chat-turn.ts:31-36` - **Issue:** Schema doesn't include required `callId` field - **Action:** Add `callId: { type: String, required: true }` to schema -- **Status:** ⬜ Pending +- **Status:** ✅ Complete --- -## Phase 2: Implement Prompt Submission Flow +## Phase 2: Implement Prompt Submission Flow ✅ COMPLETE ### 2.1 Implement `CodeSession.onSubmitPrompt()` - **File:** `gadget-code/src/lib/code-session.ts:58-60` @@ -50,12 +50,23 @@ - Emit `processWorkOrder` to drone - Update `ChatTurn` with drone acknowledgment - **Missing:** Track `selectedDrone`, `chatSession`, `project` in `CodeSession` -- **Status:** ⬜ Pending +- **Status:** ✅ Complete ### 2.2 Add Drone Selection to `CodeSession` - **File:** `gadget-code/src/lib/code-session.ts` - **Action:** Add properties and methods to track selected drone, chat session, project -- **Status:** ⬜ Pending +- **Status:** ✅ Complete + +### 2.3 Add `provider` and `selectedModel` to ChatSession +- **Files:** + - `packages/api/src/interfaces/chat-session.ts` + - `gadget-code/src/models/chat-session.ts` +- **Status:** ✅ Complete + +### 2.4 Unit Tests for CodeSession +- **File:** `gadget-code/tests/code-session.test.ts` +- **Tests:** 9 tests covering prompt submission flow +- **Status:** ✅ Complete (all passing) --- diff --git a/gadget-code/src/lib/code-session.ts b/gadget-code/src/lib/code-session.ts index 22eb6b4..a5ed15a 100644 --- a/gadget-code/src/lib/code-session.ts +++ b/gadget-code/src/lib/code-session.ts @@ -2,20 +2,30 @@ // Copyright (C) 2026 Robert Colbert // All Rights Reserved +import { Types } from "mongoose"; import { GadgetSocket, SocketSession, SocketSessionType, } from "./socket-session"; -import { IChatSession, IDroneRegistration, IProject, IUser } from "@gadget/api"; +import { + IChatSession, + IDroneRegistration, + IProject, + IUser, + ChatTurnStatus, +} from "@gadget/api"; import SocketService from "../services/socket.ts"; +import { ChatTurn } from "../models/chat-turn.ts"; export class CodeSession extends SocketSession { protected type: SocketSessionType = SocketSessionType.Code; protected project: IProject | undefined; protected chatSession: IChatSession | undefined; + protected selectedDrone: IDroneRegistration | undefined; + protected currentTurnId: Types.ObjectId | undefined; constructor(socket: GadgetSocket, user: IUser) { super(socket, user); @@ -28,6 +38,24 @@ export class CodeSession extends SocketSession { this.socket.on("submitPrompt", this.onSubmitPrompt.bind(this)); } + /** + * Sets the selected drone for this code session. + */ + setSelectedDrone(registration: IDroneRegistration): void { + this.selectedDrone = registration; + } + + /** + * Sets the active chat session and project for this code session. + */ + setChatSession( + chatSession: IChatSession, + project: IProject, + ): void { + this.chatSession = chatSession; + this.project = project; + } + /** * Called when the IDE sends a requestSessionLock event to lock a gadget-drone * instance to this code session. @@ -50,12 +78,90 @@ export class CodeSession extends SocketSession { project, chatSession, (success: boolean, chatSessionId: string): void => { + if (success) { + this.selectedDrone = registration; + this.chatSession = chatSession; + this.project = project; + } cb(success, chatSessionId); }, ); } + /** + * Called when the IDE submits a prompt to be processed by the agent. + * Creates a ChatTurn document and sends a work order to the selected drone. + */ async onSubmitPrompt(content: string): Promise { - this.log.debug("prompt received", { content }); + if (!this.selectedDrone) { + this.log.warn("prompt rejected: no drone selected"); + throw new Error("No drone selected"); + } + if (!this.chatSession) { + this.log.warn("prompt rejected: no chat session active"); + throw new Error("No chat session active"); + } + if (!this.project) { + this.log.warn("prompt rejected: no project selected"); + throw new Error("No project selected"); + } + + const droneSession = SocketService.getDroneSession(this.selectedDrone); + + const turn = new ChatTurn({ + createdAt: new Date(), + user: this.user._id, + project: this.project._id, + session: this.chatSession._id, + provider: this.chatSession.provider, + llm: this.chatSession.selectedModel, + mode: this.chatSession.mode, + status: ChatTurnStatus.Processing, + prompts: { + user: content, + system: undefined, + }, + toolCalls: [], + subagents: [], + stats: { + toolCallCount: 0, + inputTokens: 0, + thinkingTokenCount: 0, + responseTokens: 0, + durationMs: 0, + durationLabel: "pending", + }, + }); + await turn.save(); + this.currentTurnId = turn._id; + + this.log.info("ChatTurn created", { + turnId: turn._id.toHexString(), + chatSessionId: this.chatSession._id.toHexString(), + }); + + droneSession.socket.emit( + "processWorkOrder", + this.selectedDrone, + this.project, + this.chatSession, + turn, + (success: boolean, message?: string) => { + if (success) { + this.log.info("work order accepted by drone", { + turnId: turn._id.toHexString(), + message, + }); + } else { + this.log.error("work order rejected by drone", { + turnId: turn._id.toHexString(), + message, + }); + turn.status = ChatTurnStatus.Error; + turn.response = message || "Drone rejected work order"; + turn.save(); + } + }, + ); } } diff --git a/gadget-code/src/models/chat-session.ts b/gadget-code/src/models/chat-session.ts index d2b1c0d..69a8ad1 100644 --- a/gadget-code/src/models/chat-session.ts +++ b/gadget-code/src/models/chat-session.ts @@ -21,6 +21,8 @@ export const ChatSessionSchema = new Schema({ default: ChatSessionMode.Build, required: true, }, + provider: { type: Schema.Types.ObjectId, required: true, ref: "AiProvider" }, + selectedModel: { type: String, required: true }, stats: { turnCount: { type: Number, default: 0, required: true }, toolCallCount: { type: Number, default: 0, required: true }, diff --git a/gadget-code/src/models/chat-turn.ts b/gadget-code/src/models/chat-turn.ts index 136e3f4..40ef374 100644 --- a/gadget-code/src/models/chat-turn.ts +++ b/gadget-code/src/models/chat-turn.ts @@ -29,6 +29,7 @@ export const ChatTurnStatsSchema = new Schema({ }); export const ChatToolCallSchema = new Schema({ + callId: { type: String, required: true }, name: { type: String, required: true }, parameters: { type: String, required: false }, response: { type: String, required: false }, @@ -69,7 +70,7 @@ export const ChatTurnSchema = new Schema({ stats: { toolCallCount: { type: Number, default: 0, required: true }, inputTokens: { type: Number, default: 0, required: true }, - thinkingTokens: { type: Number, default: 0, required: true }, + thinkingTokenCount: { type: Number, default: 0, required: true }, responseTokens: { type: Number, default: 0, required: true }, durationMs: { type: Number, default: 0, required: true }, durationLabel: { type: String, default: "pending", required: true }, diff --git a/gadget-code/tests/code-session.test.ts b/gadget-code/tests/code-session.test.ts new file mode 100644 index 0000000..1d6cbf2 --- /dev/null +++ b/gadget-code/tests/code-session.test.ts @@ -0,0 +1,209 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { Types } from 'mongoose'; +import { CodeSession } from '../src/lib/code-session'; +import { IChatSession, IProject, IUser, IDroneRegistration, ChatTurnStatus } from '@gadget/api'; +import SocketService from '../src/services/socket'; +import { ChatTurn } from '../src/models/chat-turn'; + +// Mock dependencies +vi.mock('../src/services/socket'); +vi.mock('../src/models/chat-turn'); + +describe('CodeSession', () => { + let mockSocket: any; + let mockUser: IUser; + let mockDrone: IDroneRegistration; + let mockChatSession: IChatSession; + let mockProject: IProject; + let codeSession: CodeSession; + + beforeEach(() => { + vi.clearAllMocks(); + + mockSocket = { + id: 'test-socket-id', + on: vi.fn(), + emit: vi.fn(), + }; + + mockUser = { + _id: new Types.ObjectId(), + email: 'test@example.com', + displayName: 'Test User', + } as IUser; + + mockDrone = { + _id: new Types.ObjectId(), + hostname: 'test-host', + workspaceDir: '/test/workspace', + status: 'available', + } as IDroneRegistration; + + mockChatSession = { + _id: new Types.ObjectId(), + name: 'Test Session', + mode: 'build', + provider: new Types.ObjectId(), + selectedModel: 'llama3.1', + } as IChatSession; + + mockProject = { + _id: new Types.ObjectId(), + slug: 'test-project', + name: 'Test Project', + } as IProject; + + codeSession = new CodeSession(mockSocket, mockUser); + }); + + describe('setSelectedDrone', () => { + it('should set the selected drone', () => { + codeSession.setSelectedDrone(mockDrone); + // Can't directly access private property, but we can verify through behavior + expect(() => codeSession.setSelectedDrone(mockDrone)).not.toThrow(); + }); + }); + + describe('setChatSession', () => { + it('should set the chat session and project', () => { + codeSession.setChatSession(mockChatSession, mockProject); + expect(() => codeSession.setChatSession(mockChatSession, mockProject)).not.toThrow(); + }); + }); + + describe('onSubmitPrompt', () => { + it('should throw error if no drone is selected', async () => { + codeSession.setChatSession(mockChatSession, mockProject); + + await expect(codeSession.onSubmitPrompt('test prompt')) + .rejects.toThrow('No drone selected'); + }); + + it('should throw error if no chat session is active', async () => { + codeSession.setSelectedDrone(mockDrone); + + await expect(codeSession.onSubmitPrompt('test prompt')) + .rejects.toThrow('No chat session active'); + }); + + it('should throw error if no project is selected', async () => { + codeSession.setSelectedDrone(mockDrone); + codeSession.setChatSession(mockChatSession, undefined as any); + + await expect(codeSession.onSubmitPrompt('test prompt')) + .rejects.toThrow('No project selected'); + }); + + it('should create a ChatTurn and emit processWorkOrder to drone', async () => { + codeSession.setSelectedDrone(mockDrone); + codeSession.setChatSession(mockChatSession, mockProject); + + const mockTurn = { + _id: new Types.ObjectId(), + save: vi.fn().mockResolvedValue(undefined), + }; + vi.mocked(ChatTurn).mockImplementation(function() { + return mockTurn as any; + }); + + const mockDroneSession = { + socket: { + emit: vi.fn(), + }, + }; + vi.mocked(SocketService.getDroneSession).mockReturnValue(mockDroneSession as any); + + await codeSession.onSubmitPrompt('test prompt'); + + expect(ChatTurn).toHaveBeenCalledWith(expect.objectContaining({ + user: mockUser._id, + project: mockProject._id, + session: mockChatSession._id, + provider: mockChatSession.provider, + llm: mockChatSession.selectedModel, + status: ChatTurnStatus.Processing, + prompts: { + user: 'test prompt', + system: undefined, + }, + })); + + expect(mockTurn.save).toHaveBeenCalled(); + expect(mockDroneSession.socket.emit).toHaveBeenCalledWith( + 'processWorkOrder', + mockDrone, + mockProject, + mockChatSession, + mockTurn, + expect.any(Function) + ); + }); + + it('should update ChatTurn to Error status if drone rejects work order', async () => { + codeSession.setSelectedDrone(mockDrone); + codeSession.setChatSession(mockChatSession, mockProject); + + const mockTurn = { + _id: new Types.ObjectId(), + status: ChatTurnStatus.Processing, + response: '', + save: vi.fn().mockResolvedValue(undefined), + }; + vi.mocked(ChatTurn).mockImplementation(function() { + return mockTurn as any; + }); + + const mockDroneSession = { + socket: { + emit: vi.fn((event, ...args) => { + const callback = args[args.length - 1]; + callback(false, 'Drone is busy'); + }), + }, + }; + vi.mocked(SocketService.getDroneSession).mockReturnValue(mockDroneSession as any); + + await codeSession.onSubmitPrompt('test prompt'); + + expect(mockTurn.status).toBe(ChatTurnStatus.Error); + expect(mockTurn.response).toBe('Drone is busy'); + expect(mockTurn.save).toHaveBeenCalled(); + }); + }); + + describe('onRequestSessionLock', () => { + it('should set selected drone, chat session, and project on success', () => { + const mockDroneSession = { + socket: { + emit: vi.fn((event, ...args) => { + const callback = args[args.length - 1]; + callback(true, mockChatSession._id.toHexString()); + }), + }, + }; + vi.mocked(SocketService.getDroneSession).mockReturnValue(mockDroneSession as any); + + const callback = vi.fn(); + codeSession.onRequestSessionLock(mockDrone, mockProject, mockChatSession, callback); + + expect(callback).toHaveBeenCalledWith(true, mockChatSession._id.toHexString()); + }); + + it('should not set session data on failure', () => { + const mockDroneSession = { + socket: { + emit: vi.fn((event, ...args) => { + const callback = args[args.length - 1]; + callback(false, ''); + }), + }, + }; + vi.mocked(SocketService.getDroneSession).mockReturnValue(mockDroneSession as any); + + const callback = vi.fn(); + codeSession.onRequestSessionLock(mockDrone, mockProject, mockChatSession, callback); + + expect(callback).toHaveBeenCalledWith(false, ''); + }); + }); +}); diff --git a/gadget-drone/src/gadget-drone.ts b/gadget-drone/src/gadget-drone.ts index b0cc2b1..65135d4 100644 --- a/gadget-drone/src/gadget-drone.ts +++ b/gadget-drone/src/gadget-drone.ts @@ -10,10 +10,8 @@ import { input as inqInput, password as inqPassword } from "@inquirer/prompts"; import AgentService, { IAgentWorkOrder } from "./services/agent.ts"; import AiService from "./services/ai.ts"; -import PlatformService, { - DroneStatus, - PlatformRegistration, -} from "./services/platform.ts"; +import PlatformService, { PlatformRegistration } from "./services/platform.ts"; +import { DroneStatus } from "@gadget/api"; import { GadgetProcess } from "./lib/process.ts"; import { diff --git a/gadget-drone/src/services/agent.ts b/gadget-drone/src/services/agent.ts index 212ce30..bfe5fff 100644 --- a/gadget-drone/src/services/agent.ts +++ b/gadget-drone/src/services/agent.ts @@ -2,9 +2,9 @@ // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 +import { Types } from "mongoose"; import { IAiChatOptions, - type IAiProvider, type IContextChatMessage, } from "@gadget/ai"; import { IChatSession, IChatTurn, IProject, IUser } from "@gadget/api"; @@ -50,17 +50,6 @@ class AgentService extends GadgetService { return "[all tool calls are stubbed out]"; } - const modelConfig = { - provider: turn.provider, - modelId: turn.llm, - params: { - reasoning: false, - temperature: 0.8, - topP: 0.9, - topK: 40, - }, - }; - const context = this.buildSessionContext(workOrder); const chatOptions: IAiChatOptions = { systemPrompt: turn.prompts.system, @@ -72,7 +61,15 @@ class AgentService extends GadgetService { do { const response = await AiService.chat( turn.provider, - modelConfig, + { + modelId: turn.llm, + params: { + reasoning: false, + temperature: 0.8, + topP: 0.9, + topK: 40, + }, + }, chatOptions, ); keepProcessing = (response.toolCalls?.length ?? 0) > 0; @@ -99,7 +96,13 @@ class AgentService extends GadgetService { } buildSessionContext(workOrder: IAgentWorkOrder): IContextChatMessage[] { - const user: IUser = workOrder.turn.session.user as IUser; + const session = workOrder.turn.session; + if (session instanceof Types.ObjectId || !session.user) { + throw new Error( + "ChatSession must be populated with user data", + ); + } + const user: IUser = session.user as IUser; const messages: IContextChatMessage[] = []; for (const turn of workOrder.context) { diff --git a/gadget-drone/src/services/ai.ts b/gadget-drone/src/services/ai.ts index 3063932..9035d29 100644 --- a/gadget-drone/src/services/ai.ts +++ b/gadget-drone/src/services/ai.ts @@ -2,6 +2,7 @@ // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 +import { Types } from "mongoose"; import { GadgetService } from "../lib/service.ts"; import { type IAiChatOptions, @@ -9,10 +10,25 @@ import { type IAiGenerateOptions, type IAiGenerateResponse, type IAiModelConfig, - type IAiProvider, + type IAiProvider as AiProviderConfig, type IAiResponseStreamFn, createAiApi, } from "@gadget/ai"; +import { IAiProvider as DbAiProvider } from "@gadget/api"; + +/** + * Drone-specific model config that accepts the database provider type. + */ +export interface IDroneModelConfig { + provider: DbAiProvider | Types.ObjectId; + modelId: string; + params: { + reasoning: boolean; + temperature: number; + topP: number; + topK: number; + }; +} /** * An abstraction of the backend AI APIs (Ollama, OpenAI) that provides one @@ -40,54 +56,81 @@ class AiService extends GadgetService { this.log.info("stopped"); } + /** + * Converts a database IAiProvider document to a runtime IAiProvider config. + * The DB model uses `apiType` and extends Mongoose Document, while the runtime + * config uses `sdk` and is a plain object. + */ + mapDbProviderToConfig( + provider: DbAiProvider | Types.ObjectId, + ): AiProviderConfig { + if (provider instanceof Types.ObjectId) { + throw new Error( + "Provider must be populated, not an ObjectId reference", + ); + } + return { + _id: provider._id.toHexString(), + name: provider.name, + sdk: provider.apiType, // map apiType → sdk + baseUrl: provider.baseUrl, + apiKey: provider.apiKey, + }; + } + /** * Query the list of models available from the provider, then queries the * models for their individual capabilities. The results are cached in the Gadget */ - async discovery(provider: IAiProvider): Promise { + async discovery(provider: DbAiProvider | Types.ObjectId): Promise { + const config = this.mapDbProviderToConfig(provider); this.log.info("discovering provider model list", { - name: provider.name, - sdk: provider.sdk, + name: config.name, + sdk: config.sdk, }); - const api = this.getApi(provider); + const api = this.getApi(config); const response = await api.listModels(); this.log.debug("listModels response", { response }); } async generate( - provider: IAiProvider, - model: IAiModelConfig, + provider: DbAiProvider | Types.ObjectId, + model: Omit, options: IAiGenerateOptions, streamCallback?: IAiResponseStreamFn, ): Promise { + const config = this.mapDbProviderToConfig(provider); this.log.info("calling provider to generate a response", { - name: provider.name, - sdk: provider.sdk, + name: config.name, + sdk: config.sdk, haveStreamCallback: !!streamCallback, options, }); - const api = this.getApi(provider); - return api.generate(model, options, streamCallback); + const api = this.getApi(config); + const modelConfig: IAiModelConfig = { ...model, provider: config }; + return api.generate(modelConfig, options, streamCallback); } async chat( - provider: IAiProvider, - model: IAiModelConfig, + provider: DbAiProvider | Types.ObjectId, + model: Omit, options: IAiChatOptions, streamCallback?: IAiResponseStreamFn, ): Promise { + const config = this.mapDbProviderToConfig(provider); this.log.info("calling provider to process chat", { - provider: provider.name, - sdk: provider.sdk, + provider: config.name, + sdk: config.sdk, haveStreamCallback: !!streamCallback, model, options, }); - const api = this.getApi(provider); - return await api.chat(model, options, streamCallback); + const api = this.getApi(config); + const modelConfig: IAiModelConfig = { ...model, provider: config }; + return await api.chat(modelConfig, options, streamCallback); } - getApi(provider: IAiProvider) { + getApi(provider: AiProviderConfig) { return createAiApi(provider, this.log); } } diff --git a/gadget-drone/src/services/platform.ts b/gadget-drone/src/services/platform.ts index 8e5527c..c5e55a6 100644 --- a/gadget-drone/src/services/platform.ts +++ b/gadget-drone/src/services/platform.ts @@ -9,13 +9,7 @@ import path from "node:path"; import os from "node:os"; import { GadgetService } from "../lib/service.ts"; - -export enum DroneStatus { - Starting = "starting", - Available = "available", - Busy = "busy", - Offline = "offline", -} +import { DroneStatus } from "@gadget/api"; export interface PlatformRegistration { _id: string; // your drone's registration ID, channel, and queue diff --git a/packages/api/src/interfaces/chat-session.ts b/packages/api/src/interfaces/chat-session.ts index c1f6c23..ffd1726 100644 --- a/packages/api/src/interfaces/chat-session.ts +++ b/packages/api/src/interfaces/chat-session.ts @@ -26,6 +26,8 @@ export interface IChatSession extends Document { project: IProject | Types.ObjectId; name: string; mode: ChatSessionMode; + provider: Types.ObjectId; + selectedModel: string; stats: { turnCount: number; toolCallCount: number; diff --git a/packages/api/src/messages/drone.ts b/packages/api/src/messages/drone.ts index cc2abdc..e9253df 100644 --- a/packages/api/src/messages/drone.ts +++ b/packages/api/src/messages/drone.ts @@ -24,6 +24,7 @@ export type ThinkingMessage = (content: string) => void; export type ResponseMessage = (content: string) => void; export type ToolCallMessage = ( + callId: string, name: string, params: string, response: string,