From 9cb689668fa372e674ac1059b879cdd5d17481ec Mon Sep 17 00:00:00 2001 From: Rob Colbert Date: Wed, 29 Apr 2026 11:47:28 -0400 Subject: [PATCH] more refactoring to standardize interfaces We want to speak only one language when dealing with AI content to minimize the number of maps, transforms, and copies. This initiative isn't done, this is a checkpoint along the way while conducting experiments. --- gadget-code/src/models/chat-turn.ts | 8 +- gadget-drone/src/services/agent.ts | 164 +++++++++++++---------- packages/ai/src/api.ts | 7 +- packages/api/src/interfaces/chat-turn.ts | 10 +- 4 files changed, 112 insertions(+), 77 deletions(-) diff --git a/gadget-code/src/models/chat-turn.ts b/gadget-code/src/models/chat-turn.ts index 2072cd5..136e3f4 100644 --- a/gadget-code/src/models/chat-turn.ts +++ b/gadget-code/src/models/chat-turn.ts @@ -7,12 +7,18 @@ import { Types, Schema, model } from "mongoose"; import { ChatSessionMode, ChatTurnStatus, + IChatTurnPrompts, IChatSubagentProcess, IChatToolCall, IChatTurn, IChatTurnStats, } from "@gadget/api"; +export const ChatTurnPromptsSchema = new Schema({ + user: { type: String, required: true }, + system: { type: String, required: false }, +}); + export const ChatTurnStatsSchema = new Schema({ toolCallCount: { type: Number, default: 0, required: true }, inputTokens: { type: Number, default: 0, required: true }, @@ -55,7 +61,7 @@ export const ChatTurnSchema = new Schema({ default: ChatTurnStatus.Processing, required: true, }, - prompt: { type: String, required: true }, + prompts: { type: ChatTurnPromptsSchema, required: true }, thinking: { type: String, required: false }, response: { type: String, required: false }, toolCalls: { type: [ChatToolCallSchema], default: [], required: true }, diff --git a/gadget-drone/src/services/agent.ts b/gadget-drone/src/services/agent.ts index b212aff..9f51c6f 100644 --- a/gadget-drone/src/services/agent.ts +++ b/gadget-drone/src/services/agent.ts @@ -2,25 +2,17 @@ // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 -import assert from "node:assert"; -import path from "node:path"; -import fs from "node:fs"; - -import dayjs from "dayjs"; - -import { type IAiProvider, type IContextChatMessage } from "@gadget/ai"; +import { + IAiChatOptions, + type IAiProvider, + type IContextChatMessage, +} from "@gadget/ai"; +import { IChatSession, IChatTurn, IProject, IUser } from "@gadget/api"; import AiService from "./ai.ts"; import { GadgetService } from "../lib/service.ts"; -export interface IProject { - _id: string; - name: string; - slug: string; - gitUrl: string; -} - export interface IToolCall { name: string; params: string; @@ -29,31 +21,12 @@ export interface IToolCall { error?: Error; } -export interface IChatMessage { - role: string; - content: string; -} - -export interface IChatTurn { - _id: string; - mode: string; - modelId?: string; - prompts: { - system: string; - user: string; - }; -} - -export interface IChatSession { - _id: string; - name: string; - context: IContextChatMessage[]; -} - export interface IAgentWorkOrder { + createdAt: Date; project: IProject; provider: IAiProvider; session: IChatSession; + context: IChatTurn[]; turn: IChatTurn; } @@ -80,20 +53,9 @@ class AgentService extends GadgetService { return "[all tool calls are stubbed out]"; } - // this turn's context (system, history, prompt, work) - const messages: IChatMessage[] = []; - - messages.push({ role: "system", content: turn.prompts.system }); - - // recall full session history into messages array - this.buildSessionContext(session, messages); - - // push the User's latest prompt to the context - messages.push({ role: "user", content: workOrder.turn.prompts.user }); - const modelConfig = { provider: workOrder.provider, - modelId: workOrder.turn.modelId ?? workOrder.provider.defaultModelId ?? "llama3.2", + modelId: workOrder.turn.llm, params: { reasoning: false, temperature: 0.8, @@ -102,42 +64,102 @@ class AgentService extends GadgetService { }, }; - const chatOptions = { - context: session.context, + const context = this.buildSessionContext(workOrder); + const chatOptions: IAiChatOptions = { + systemPrompt: workOrder.turn.prompts.system, + context, + userPrompt: workOrder.turn.prompts.user, }; let keepProcessing = true; do { - const response = await AiService.chat(workOrder.provider, modelConfig, chatOptions); - keepProcessing = (response.tool_calls?.length ?? 0) > 0; - for (const tool_call of response.tool_calls ?? []) { - const result = await aiCallTool(tool_call.function.name, tool_call.function.arguments); - messages.push({ role: "tool", content: result }); + const response = await AiService.chat( + workOrder.provider, + modelConfig, + chatOptions, + ); + keepProcessing = (response.toolCalls?.length ?? 0) > 0; + for (const toolCall of response.toolCalls ?? []) { + const result = await aiCallTool( + toolCall.function.name, + toolCall.function.arguments, + ); + context.push({ + createdAt: new Date(), + role: "tool", + callId: toolCall.callId, + content: result, + }); /* emit turn-tool-call socket message */ } } while (keepProcessing); - /* emit turn-finished socket message */ + /* + * TODO: + * 1. Call web service to POST results to the work order + * 2. Emit turn-finished socket message + */ } - buildSessionContext(session: IChatSession, messages: IChatMessage[]): void { - let content; - for (const message of session.context) { - switch (message.role) { - case "system": - continue; - case "user": - content = message.content; - break; - case "assistant": - content = message.content; - break; - case "tool": - content = message.content; - break; + buildSessionContext(workOrder: IAgentWorkOrder): IContextChatMessage[] { + const user: IUser = workOrder.session.user as IUser; + const messages: IContextChatMessage[] = []; + + for (const turn of workOrder.context) { + /* + * add the User message + */ + messages.push({ + createdAt: turn.createdAt, + role: "user", + content: turn.prompts.user, + user: { + _id: user._id.toHexString(), + username: user.email, + displayName: user.displayName, + }, + }); + + /* + * Add the agent's responses (thinking, respone text, tool calls) + */ + if (turn.toolCalls?.length > 0) { + for (const toolCall of turn.toolCalls) { + messages.push({ + createdAt: turn.createdAt, + role: "tool", + callId: toolCall.callId, + content: toolCall.response, + }); + } } - messages.push({ role: message.role, content: message.content }); + + /* + * Add the assistant's output (if any), to include the thinking + * (reasoning) output (if any). + */ + let content = ""; + if (turn.thinking) { + content += `${turn.thinking}`; + if (turn.response && turn.response.length) { + content += "\n"; + } + } + if (turn.response) { + content += turn.response; + } + + messages.push({ + createdAt: turn.createdAt, + role: "assistant", + content: + content && content.length + ? content + : "(you didn't say anything this turn)", + }); } + + return messages; } /** diff --git a/packages/ai/src/api.ts b/packages/ai/src/api.ts index 264d3f2..a32b92c 100644 --- a/packages/ai/src/api.ts +++ b/packages/ai/src/api.ts @@ -51,6 +51,7 @@ export interface IAiGenerateResponse { export interface IContextChatMessage { createdAt: Date; role: string; + callId?: string; content: string; user?: { _id: string; @@ -66,7 +67,7 @@ export interface IAiChatOptions { } export interface IToolCall { - call_id: string; + callId: string; function: { name: string; arguments: string; @@ -79,7 +80,7 @@ export interface IAiChatResponse { stats: IAiInferenceStats; done: boolean; doneReason?: string; - tool_calls?: IToolCall[]; + toolCalls?: IToolCall[]; } export interface IAiStreamChunk { @@ -129,4 +130,4 @@ export abstract class AiApi { options: IAiChatOptions, streamCallback?: IAiResponseStreamFn, ): Promise; -} \ No newline at end of file +} diff --git a/packages/api/src/interfaces/chat-turn.ts b/packages/api/src/interfaces/chat-turn.ts index e849131..4989d94 100644 --- a/packages/api/src/interfaces/chat-turn.ts +++ b/packages/api/src/interfaces/chat-turn.ts @@ -15,6 +15,11 @@ export enum ChatTurnStatus { Error = "error", } +export interface IChatTurnPrompts { + user: string; + system?: string; +} + export interface IChatTurnStats { toolCallCount: number; // total number of tool functions called this turn inputTokens: number; // total number of input tokens processed this turn @@ -25,6 +30,7 @@ export interface IChatTurnStats { } export interface IChatToolCall { + callId: string; // ID of the call so the agent can match response to call name: string; // tool function name being called parameters: string; // JSON.stringify of input parameters response: string; // the tool's response @@ -53,10 +59,10 @@ export interface IChatTurn extends Document { llm: string; // id/name of the model used to process the prompt mode: ChatSessionMode; // session mode for this turn/prompt status: ChatTurnStatus; - prompt: string; + prompts: IChatTurnPrompts; thinking?: string; response?: string; toolCalls: IChatToolCall[]; subagents: IChatSubagentProcess[]; // subagents used while processing this turn stats: IChatTurnStats; -} \ No newline at end of file +}