diff --git a/docs/socket-protocol.md b/docs/socket-protocol.md index 8b30ae0..6deadc2 100644 --- a/docs/socket-protocol.md +++ b/docs/socket-protocol.md @@ -21,12 +21,28 @@ Socket.IO is used to connect these components and exchange messages in real-time b. The drone synchronizes the project into the workspace. c. The drone follows the project's instructions for configuring it for development/testing/production. d. The drone grants the IDE the requested session lock. -6. gadget-code:ide navigates in the Chat Session view. +6. gadget-code:ide navigates to the Chat Session view. + +The User can now begin entering prompts and editing files. ### Editing A File -When in the Chat Session view (see: []()), the User can select a file from the Files pane to open it in the File Editor. The mode of the File Editor depends on what's happening in the IDE at that time. +When in the Chat Session view (see: [UI Design & Style Guide](../gadget-code/docs/ui-design-guide.md)), the User can select a file from the Files pane to open it in the File Editor. The mode of the File Editor depends on what's happening in the IDE at that time. If there is a prompt work order being processed, the File Editor is read-only. The User can browse and scroll through the contents of the file and see all metadata about the file. The information and file content displayed will update as the gadget-drone process is doing it's work. But, the User is not permitted to make any changes to the displayed file content, or save the file. If the workspace is idle (no prompt is executing), then the File Editor will open in read-write mode. The User will be able to edit the file (using the ACE editor). The User will be able to save the file. The User cannot enter a prompt or create a prompt work order while the session is in file edit mode. The user must close the File Editor and return to idle + +## Protocol Sequences + +### requestSessionLock + +IDE:requestSessionLock => gadget-code:web => gadget-drone => gadget-code:web => IDE + +The IDE initiates `requestSessionLock` when it wants to start working with a Chat Session. The event is received by the web server, which selects the drone for which it is intended based on the supplied DroneRegistration.\_id. The web server sends the message to the drone, which process it and returns the result to the web server. The web server sends the message back to the IDE. + +### requestWorkspaceMode + +IDE:requestWorkspaceMode => gadget-code:web => gadget-drone => gadget-code:web => IDE + +The IDE initiates `requestWorkspaceMode` when it wants to request the User mode so the User can begin editing files and making changes within the project. The event is received by the web server, which sends the message to the drone. The drone processes the request and returns the result to the web server. The web server sends the message back to the IDE. diff --git a/gadget-drone/src/gadget-drone.ts b/gadget-drone/src/gadget-drone.ts index 33ec787..b0cc2b1 100644 --- a/gadget-drone/src/gadget-drone.ts +++ b/gadget-drone/src/gadget-drone.ts @@ -19,10 +19,14 @@ import { GadgetProcess } from "./lib/process.ts"; import { ClientToServerEvents, IChatSession, + IChatTurn, IDroneRegistration, IProject, + ProcessWorkOrderCallback, RequestSessionLockCallback, + RequestWorkspaceModeCallback, ServerToClientEvents, + WorkspaceMode, } from "@gadget/api"; interface UserCredentials { @@ -30,13 +34,6 @@ interface UserCredentials { password: string; } -enum WorkspaceMode { - Idle = "idle", - Syncing = "syncing", - User = "user", - Agent = "agent", -} - type ClientSocket = Socket; class GadgetDrone extends GadgetProcess { @@ -160,6 +157,11 @@ class GadgetDrone extends GadgetProcess { "requestSessionLock", this.onRequestSessionLock.bind(this), ); + this.socket.on( + "requestWorkspaceMode", + this.onRequestWorkspaceMode.bind(this), + ); + this.socket.on("processWorkOrder", this.onProcessWorkOrder.bind(this)); }); } @@ -169,7 +171,11 @@ class GadgetDrone extends GadgetProcess { chatSession: IChatSession, cb: RequestSessionLockCallback, ) { - this.log.info(`requestSessionLock`, { registration, project, chatSession }); + this.log.info("requestSessionLock received", { + registration, + project, + chatSession, + }); if (!this.registration) { return cb(false, "not registered"); } @@ -181,6 +187,48 @@ class GadgetDrone extends GadgetProcess { cb(true, chatSession._id.toHexString()); } + async onRequestWorkspaceMode( + registration: IDroneRegistration, + project: IProject, + chatSession: IChatSession, + mode: WorkspaceMode, + cb: RequestWorkspaceModeCallback, + ) { + this.log.info("requestWorkspaceMode received", { + registration, + project, + chatSession, + }); + if (this.workspaceMode === WorkspaceMode.Idle) { + this.workspaceMode = mode; + return cb(true, this.workspaceMode); + } + return cb(false, this.workspaceMode); + } + + async onProcessWorkOrder( + registration: IDroneRegistration, + project: IProject, + chatSession: IChatSession, + turn: IChatTurn, + cb: ProcessWorkOrderCallback, + ) { + const order: IAgentWorkOrder = { + createdAt: turn.createdAt, + context: [], + turn, + }; + this.log.info("processWorkOrder received", { + registration, + project, + chatSession, + turn, + }); + cb(true); // the drone accepts the work order + + AgentService.process(order); + } + hookProcessSignals(): void { process.title = this.name; diff --git a/gadget-drone/src/services/agent.ts b/gadget-drone/src/services/agent.ts index 9f51c6f..212ce30 100644 --- a/gadget-drone/src/services/agent.ts +++ b/gadget-drone/src/services/agent.ts @@ -23,11 +23,8 @@ export interface IToolCall { export interface IAgentWorkOrder { createdAt: Date; - project: IProject; - provider: IAiProvider; - session: IChatSession; - context: IChatTurn[]; turn: IChatTurn; + context: IChatTurn[]; } class AgentService extends GadgetService { @@ -47,15 +44,15 @@ class AgentService extends GadgetService { } async process(workOrder: IAgentWorkOrder): Promise { - const { project, provider, session, turn } = workOrder; + const { turn } = workOrder; async function aiCallTool(name: string, args: string) { return "[all tool calls are stubbed out]"; } const modelConfig = { - provider: workOrder.provider, - modelId: workOrder.turn.llm, + provider: turn.provider, + modelId: turn.llm, params: { reasoning: false, temperature: 0.8, @@ -66,15 +63,15 @@ class AgentService extends GadgetService { const context = this.buildSessionContext(workOrder); const chatOptions: IAiChatOptions = { - systemPrompt: workOrder.turn.prompts.system, + systemPrompt: turn.prompts.system, context, - userPrompt: workOrder.turn.prompts.user, + userPrompt: turn.prompts.user, }; let keepProcessing = true; do { const response = await AiService.chat( - workOrder.provider, + turn.provider, modelConfig, chatOptions, ); @@ -102,7 +99,7 @@ class AgentService extends GadgetService { } buildSessionContext(workOrder: IAgentWorkOrder): IContextChatMessage[] { - const user: IUser = workOrder.session.user as IUser; + const user: IUser = workOrder.turn.session.user as IUser; const messages: IContextChatMessage[] = []; for (const turn of workOrder.context) { diff --git a/packages/ai/README.md b/packages/ai/README.md index 7c0e11d..113862a 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -19,7 +19,6 @@ const provider = { sdk: "ollama", baseUrl: "http://localhost:11434", apiKey: "", - defaultModelId: "llama3.2", }; const modelConfig = { diff --git a/packages/ai/src/api.ts b/packages/ai/src/api.ts index a32b92c..745bde1 100644 --- a/packages/ai/src/api.ts +++ b/packages/ai/src/api.ts @@ -9,7 +9,6 @@ export interface IAiProvider { sdk: AiSdkType; baseUrl: string; apiKey: string; - defaultModelId?: string; } export interface IAiModelConfig { diff --git a/packages/api/src/messages/drone.ts b/packages/api/src/messages/drone.ts index 1b64bb0..cc2abdc 100644 --- a/packages/api/src/messages/drone.ts +++ b/packages/api/src/messages/drone.ts @@ -2,6 +2,23 @@ // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 +import { IChatSession } from "../interfaces/chat-session.ts"; +import { IChatTurn } from "../interfaces/chat-turn.ts"; +import { IDroneRegistration } from "../interfaces/drone-registration.ts"; +import { IProject } from "../interfaces/project.ts"; + +export type ProcessWorkOrderCallback = ( + success: boolean, + message?: string, +) => void; +export type ProcessWorkOrderMessage = ( + registration: IDroneRegistration, + project: IProject, + chatSession: IChatSession, + turn: IChatTurn, + cb: ProcessWorkOrderCallback, +) => void; + export type ThinkingMessage = (content: string) => void; export type ResponseMessage = (content: string) => void; @@ -11,3 +28,9 @@ export type ToolCallMessage = ( params: string, response: string, ) => void; + +export type WorkOrderCompleteMessage = ( + workOrderId: string, + success: boolean, + message?: string, +) => void; diff --git a/packages/api/src/messages/ide.ts b/packages/api/src/messages/ide.ts index 85c9e2f..ded9ed9 100644 --- a/packages/api/src/messages/ide.ts +++ b/packages/api/src/messages/ide.ts @@ -6,6 +6,10 @@ import { IChatSession } from "../interfaces/chat-session.ts"; import { IDroneRegistration } from "../interfaces/drone-registration.ts"; import { IProject } from "../interfaces/project.ts"; +/* + * requestSessionLock + */ + export type RequestSessionLockCallback = ( success: boolean, chatSessionId: string, @@ -18,4 +22,32 @@ export type RequestSessionLockMessage = ( cb: RequestSessionLockCallback, ) => void; +/* + * requestWorkspaceMode + */ + +export enum WorkspaceMode { + Idle = "idle", + Syncing = "syncing", + User = "user", + Agent = "agent", +} + +export type RequestWorkspaceModeCallback = ( + success: boolean, + mode: WorkspaceMode, +) => void; + +export type RequestWorkspaceModeMessage = ( + registration: IDroneRegistration, + project: IProject, + chatSession: IChatSession, + mode: WorkspaceMode, + cb: RequestWorkspaceModeCallback, +) => void; + +/* + * submitPrompt + */ + export type SubmitPromptMessage = (prompt: string) => void; diff --git a/packages/api/src/messages/socket.ts b/packages/api/src/messages/socket.ts index 71a00f9..3bb3074 100644 --- a/packages/api/src/messages/socket.ts +++ b/packages/api/src/messages/socket.ts @@ -2,11 +2,21 @@ // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 -import { ResponseMessage, ThinkingMessage, ToolCallMessage } from "./drone.ts"; -import { RequestSessionLockMessage, SubmitPromptMessage } from "./ide.ts"; +import { + ProcessWorkOrderMessage, + ResponseMessage, + ThinkingMessage, + ToolCallMessage, + WorkOrderCompleteMessage, +} from "./drone.ts"; +import { + RequestSessionLockMessage, + RequestWorkspaceModeMessage, + SubmitPromptMessage, +} from "./ide.ts"; /* -There are two different kinds of clients that connect to the gadget-code +There are two different kinds of clients that connect to the gadget-code:web Socket.IO server: 1. The gadget-code:ide (ReactJS front-end) @@ -23,31 +33,36 @@ and serve as a remote control surface for one or more gadget-drone processes running work orders on projects in chat sessions. */ -export interface ServerToClientEvents { +export interface ClientToServerEvents { /* - * gadget-code:ide => gadget-code:web => gadget-drone + * gadget-code:ide => gadget-code:web */ + requestSessionLock: RequestSessionLockMessage; + requestWorkspaceMode: RequestWorkspaceModeMessage; submitPrompt: SubmitPromptMessage; /* - * gadget-drone => gadget-code => gadget-code:ide + * gadget-drone => gadget-code:web */ + thinking: ThinkingMessage; response: ResponseMessage; toolCall: ToolCallMessage; + workOrderComplete: WorkOrderCompleteMessage; } -export interface ClientToServerEvents { +export interface ServerToClientEvents { /* - * gadget-code:ide => gadget-code => gadget-drone + * gadget-code:web => gadget-drone */ requestSessionLock: RequestSessionLockMessage; - submitPrompt: SubmitPromptMessage; + requestWorkspaceMode: RequestWorkspaceModeMessage; + processWorkOrder: ProcessWorkOrderMessage; /* - * gadget-drone => gadget-code => gadget-code:ide + * gadget-code:web => gadget-code:ide */ thinking: ThinkingMessage;