diff options
| author | Adam Malczewski <[email protected]> | 2026-05-22 20:54:19 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-22 20:54:19 +0900 |
| commit | c47346cc6237044ecb60ff22c4011d89744af581 (patch) | |
| tree | 2359a25e687e1290ba5180fd60eae83b03b53a23 /packages/core | |
| parent | 288b21cec98421fda57028a0c8c9d835cfbb14b0 (diff) | |
| download | dispatch-c47346cc6237044ecb60ff22c4011d89744af581.tar.gz dispatch-c47346cc6237044ecb60ff22c4011d89744af581.zip | |
feat: message queue/interrupt system, CORS fix, mobile fixes, chat splitting
- Add message queue allowing users to send messages while agent is running
- Queue messages are injected into tool results as [USER INTERRUPT]
- Retrieve tool interrupted via Promise.race when user message arrives
- Queued messages show with 'queued' badge and cancel button
- Consumed messages repositioned and chat splits at interrupt point
- New assistant message block created after interrupt for clean flow
- Add POST /chat/cancel endpoint for cancelling queued messages
- Fix CORS to allow any origin (Tailscale/LAN access)
- Fix crypto.randomUUID fallback for non-secure contexts (HTTP)
- Fix frontend API URL derivation from page hostname
- Auto-create DB tab if missing on processMessage (foreign key fix)
- Add error logging to processMessage catch block
- Fix working directory input sync on agent switch
- Fix agent mode button to re-apply agent settings
Diffstat (limited to 'packages/core')
| -rw-r--r-- | packages/core/src/agent/agent.ts | 76 | ||||
| -rw-r--r-- | packages/core/src/tools/retrieve.ts | 28 | ||||
| -rw-r--r-- | packages/core/src/types/index.ts | 19 |
3 files changed, 98 insertions, 25 deletions
diff --git a/packages/core/src/agent/agent.ts b/packages/core/src/agent/agent.ts index c2c5880..b4c0eec 100644 --- a/packages/core/src/agent/agent.ts +++ b/packages/core/src/agent/agent.ts @@ -11,6 +11,7 @@ import type { AgentEvent, AgentStatus, ChatMessage, + QueueCallbacks, ToolCall, ToolResult, } from "../types/index.js"; @@ -72,9 +73,11 @@ export class Agent { messages: ChatMessage[] = []; private config: AgentConfig; + private queueCallbacks?: QueueCallbacks; - constructor(config: AgentConfig) { + constructor(config: AgentConfig, queueCallbacks?: QueueCallbacks) { this.config = config; + this.queueCallbacks = queueCallbacks; } private async executeToolWithStreaming( @@ -185,11 +188,12 @@ export class Agent { } try { - const execPromise = tool.execute(tc.arguments, { - onOutput: (data: string, stream: "stdout" | "stderr") => { - shellOutputQueue.push({ data, stream }); - }, - }); + const execPromise = tool.execute(tc.arguments, { + onOutput: (data: string, stream: "stdout" | "stderr") => { + shellOutputQueue.push({ data, stream }); + }, + queueCallbacks: this.queueCallbacks, + }); const rawResult = await execPromise; return { @@ -401,15 +405,30 @@ export class Agent { } } - // Drain any remaining shell output emitted before we read the result - while (shellOutputQueue.length > 0) { - const item = shellOutputQueue.shift()!; - yield { type: "shell-output", data: item.data, stream: item.stream }; + // Drain any remaining shell output emitted before we read the result + while (shellOutputQueue.length > 0) { + const item = shellOutputQueue.shift()!; + yield { type: "shell-output", data: item.data, stream: item.stream }; + } + + // Check for queued user messages and append them to the tool result + let finalToolResult = toolResult; + if (this.queueCallbacks) { + const queuedMsgs = this.queueCallbacks.dequeueMessages(); + if (queuedMsgs.length > 0) { + const userMessages = queuedMsgs + .map((m) => m.message) + .join("\n---\n"); + finalToolResult = { + ...toolResult, + result: `${toolResult.result}\n\n[USER INTERRUPT]\nThe user has sent you message(s) while you were working. You MUST address these before continuing with your current task:\n\n${userMessages}`, + }; } + } - stepToolResults.push(toolResult); - allToolResults.push(toolResult); - yield { type: "tool-result", toolResult }; + stepToolResults.push(finalToolResult); + allToolResults.push(finalToolResult); + yield { type: "tool-result", toolResult: finalToolResult }; } // Add tool results back to step messages so LLM can see them @@ -431,14 +450,29 @@ export class Agent { } } - const assistantMessage: ChatMessage = { - role: "assistant", - content: finalText, - toolCalls: allToolCalls.length > 0 ? allToolCalls : undefined, - toolResults: allToolResults.length > 0 ? allToolResults : undefined, - }; - this.messages.push(assistantMessage); - yield { type: "done", message: assistantMessage }; + const assistantMessage: ChatMessage = { + role: "assistant", + content: finalText, + toolCalls: allToolCalls.length > 0 ? allToolCalls : undefined, + toolResults: allToolResults.length > 0 ? allToolResults : undefined, + }; + this.messages.push(assistantMessage); + + // Drain any remaining queued messages that arrived after the last tool call + if (this.queueCallbacks) { + const remaining = this.queueCallbacks.dequeueMessages(); + if (remaining.length > 0) { + // These messages arrived too late to be injected into a tool result. + // Append them as a user message to the conversation so they're not lost. + const userMessages = remaining.map(m => m.message).join("\n---\n"); + this.messages.push({ + role: "user", + content: userMessages, + }); + } + } + + yield { type: "done", message: assistantMessage }; } catch (err) { const errorMsg = formatError(err, this.config); yield { type: "error", error: errorMsg }; diff --git a/packages/core/src/tools/retrieve.ts b/packages/core/src/tools/retrieve.ts index 93f4c89..8e14a96 100644 --- a/packages/core/src/tools/retrieve.ts +++ b/packages/core/src/tools/retrieve.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import type { ToolDefinition } from "../types/index.js"; +import type { ToolDefinition, ToolExecuteContext } from "../types/index.js"; export interface RetrieveCallbacks { getResult( @@ -24,11 +24,33 @@ export function createRetrieveTool(callbacks: RetrieveCallbacks): ToolDefinition parameters: z.object({ agent_id: z.string().describe("The agent_id returned by a previous summon call."), }), - execute: async (args: Record<string, unknown>): Promise<string> => { + execute: async (args: Record<string, unknown>, context?: ToolExecuteContext): Promise<string> => { const agentId = args.agent_id as string; + const queueCallbacks = context?.queueCallbacks; try { - const outcome = await callbacks.getResult(agentId); + let outcome: { status: "done"; result: string } | { status: "error"; error: string }; + + if (queueCallbacks) { + const childPromise = callbacks.getResult(agentId); + const { promise: queuePromise, cancel: cancelQueueWait } = queueCallbacks.waitForQueuedMessage(); + const queueSignal = queuePromise.then(() => "QUEUE_INTERRUPT" as const); + + const raceResult = await Promise.race([childPromise, queueSignal]); + + if (raceResult === "QUEUE_INTERRUPT") { + const queuedMsgs = queueCallbacks.dequeueMessages(); + const userMessages = queuedMsgs.map((m) => m.message).join("\n---\n"); + return `The subagent (agent_id: ${agentId}) has not completed its task yet. You will need to call retrieve with this agent_id again later to get the result.\n\n[USER INTERRUPT]\nThe user has sent you message(s) while you were working. You MUST address these before continuing with your current task:\n\n${userMessages}`; + } + + // Child finished first — clean up the queue listener + cancelQueueWait(); + outcome = raceResult; + } else { + outcome = await callbacks.getResult(agentId); + } + if (outcome.status === "done") { return ["<agent_result>", outcome.result, "</agent_result>"].join("\n"); } diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 8f80b08..be37674 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -47,12 +47,16 @@ export type AgentEvent = keyId: string | null; modelId: string | null; parentTabId: string | null; - }; + } + | { type: "message-queued"; tabId: string; messageId: string; message: string } + | { type: "message-consumed"; tabId: string; messageIds: string[] } + | { type: "message-cancelled"; tabId: string; messageId: string }; // ─── Tool Types ────────────────────────────────────────────────── export interface ToolExecuteContext { onOutput?: (data: string, stream: "stdout" | "stderr") => void; + queueCallbacks?: QueueCallbacks; } export interface ToolDefinition { @@ -147,6 +151,19 @@ export interface ConfigError { message: string; } +// ─── Message Queue Types ───────────────────────────────────────── + +export interface QueuedMessage { + id: string; + message: string; + timestamp: number; +} + +export interface QueueCallbacks { + dequeueMessages: () => QueuedMessage[]; + waitForQueuedMessage: () => { promise: Promise<void>; cancel: () => void }; +} + // ─── Agent Definition Types ────────────────────────────────────── export interface AgentModelEntry { |
