diff --git a/.changeset/gentle-windows-strive.md b/.changeset/gentle-windows-strive.md new file mode 100644 index 0000000..7876889 --- /dev/null +++ b/.changeset/gentle-windows-strive.md @@ -0,0 +1,6 @@ +--- +"@shellular/protocol": patch +"shellular": patch +--- + +feat(agents): prompt queueing diff --git a/biome.json b/biome.json index e993e77..6f0a417 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.5.6/schema.json", + "$schema": "https://biomejs.dev/schemas/2.5.7/schema.json", "vcs": { "enabled": true, "clientKind": "git", diff --git a/cli/package.json b/cli/package.json index c1617ff..11617a0 100644 --- a/cli/package.json +++ b/cli/package.json @@ -5,7 +5,9 @@ "main": "dist/main.js", "scripts": { "dev": "cross-env SHELLULAR_DEV=true tsx --import ./scripts/register-sql-loader.mjs --watch src/main.ts", + "predev": "pnpm --dir ../protocol run build", "start": "tsx --import ./scripts/register-sql-loader.mjs src/main.ts", + "prestart": "pnpm --dir ../protocol run build", "build": "pnpm run schema && tsc && tsup", "schema": "tsx scripts/generate-schema.ts", "prepublishOnly": "pnpm run build", diff --git a/cli/src/agents/index.ts b/cli/src/agents/index.ts index 18775e0..37900e8 100644 --- a/cli/src/agents/index.ts +++ b/cli/src/agents/index.ts @@ -9,6 +9,11 @@ import type { AiAttachmentWriteMsg, AiAttachmentWriteResultMsg, AiEvent, + AiPromptQueueItem, + AiPromptQueuePauseAckMsg, + AiPromptQueuePauseMsg, + AiPromptQueueRemoveAckMsg, + AiPromptQueueUpdateAckMsg, AiSession, AiSessionConfigOption, AiSessionCreateMsg, @@ -71,6 +76,23 @@ export interface MessageWindow { to?: number; } +interface QueuedPrompt { + id: string; + clientId: string; + agentId: AgentId; + sessionId: string; + text: string; + content: AcpPromptRequest["prompt"]; + createdAt: number; + updatedAt: number; +} + +interface PromptQueueState { + running: boolean; + paused: boolean; + items: QueuedPrompt[]; +} + /** True when a window asks for an explicit range rather than a tail. */ function isRangeWindow(window?: MessageWindow) { return window?.from !== undefined || window?.to !== undefined; @@ -288,6 +310,7 @@ export class AgentsManager { // after its load: if it changed, the replay predates a turn and its result // must not overwrite the newer live transcript. private sessionTurnCounts = new Map(); + private promptQueues = new Map(); constructor() { this.reloadDescriptors(); @@ -1404,6 +1427,180 @@ export class AgentsManager { return { ...result, messages }; } + private enqueuePrompt( + clientId: string, + agentId: AgentId, + sessionId: string, + content: string | unknown[], + ) { + const queue = this.getPromptQueue(agentId, sessionId); + const prompt = normalizePromptContent(content); + const now = Date.now(); + const item: QueuedPrompt = { + id: `prompt_queue_${now.toString(36)}_${Math.random().toString(36).slice(2, 8)}`, + clientId, + agentId, + sessionId, + text: promptContentText(prompt), + content: prompt, + createdAt: now, + updatedAt: now, + }; + queue.items.push(item); + this.emitPromptQueueUpdate(clientId, agentId, sessionId); + void this.drainPromptQueue(clientId, agentId, sessionId); + return item; + } + + private updateQueuedPrompt( + clientId: string, + agentId: AgentId, + sessionId: string, + queueId: string, + text: string, + content: unknown[], + ) { + const queue = this.getPromptQueue(agentId, sessionId); + const item = queue.items.find((entry) => entry.id === queueId); + if (!item) throw new Error("Queued prompt is no longer pending"); + const prompt = normalizePromptContent(content.length ? content : text); + item.text = promptContentText(prompt); + item.content = prompt; + item.updatedAt = Date.now(); + this.emitPromptQueueUpdate(clientId, agentId, sessionId); + return queue; + } + + private removeQueuedPrompt( + clientId: string, + agentId: AgentId, + sessionId: string, + queueId: string, + ) { + const queue = this.getPromptQueue(agentId, sessionId); + const nextItems = queue.items.filter((entry) => entry.id !== queueId); + if (nextItems.length === queue.items.length) { + throw new Error("Queued prompt is no longer pending"); + } + queue.items = nextItems; + this.emitPromptQueueUpdate(clientId, agentId, sessionId); + return queue; + } + + private async drainPromptQueue( + fallbackClientId: string, + agentId: AgentId, + sessionId: string, + ) { + const queue = this.getPromptQueue(agentId, sessionId); + if (queue.running) return; + queue.running = true; + this.emitPromptQueueUpdate(fallbackClientId, agentId, sessionId); + try { + if (queue.paused) return; + while (queue.items.length > 0) { + if (queue.paused) break; + const item = queue.items.shift(); + if (!item) break; + this.emitPromptQueueUpdate(item.clientId, agentId, sessionId); + this.emitQueuedUserMessage(item); + try { + await this.prompt(item.clientId, agentId, sessionId, item.content); + } catch (err) { + logger.error( + `Queued agent prompt failed for ${agentId} session ${sessionId} (client ${item.clientId}): ${getErrorMessage(err)}`, + err, + ); + this.emit(item.clientId, agentId, { + type: "error", + properties: { + sessionId, + error: getErrorMessage(err), + }, + }); + } + } + } finally { + queue.running = false; + this.emitPromptQueueUpdate(fallbackClientId, agentId, sessionId); + if (queue.items.length === 0) { + this.promptQueues.delete(this.sessionKey(agentId, sessionId)); + } + } + } + + private getPromptQueue(agentId: AgentId, sessionId: string) { + const key = this.sessionKey(agentId, sessionId); + let queue = this.promptQueues.get(key); + if (!queue) { + queue = { running: false, paused: false, items: [] }; + this.promptQueues.set(key, queue); + } + return queue; + } + + private promptQueueAckData(agentId: AgentId, sessionId: string) { + const queue = this.getPromptQueue(agentId, sessionId); + return { + backend: agentId, + sessionId, + queue: queue.items.map((item) => this.toPromptQueueItem(item)), + running: queue.running, + }; + } + + private toPromptQueueItem(item: QueuedPrompt): AiPromptQueueItem { + return { + id: item.id, + backend: item.agentId, + sessionId: item.sessionId, + text: item.text, + content: item.content, + createdAt: item.createdAt, + updatedAt: item.updatedAt, + }; + } + + private emitPromptQueueUpdate( + clientId: string, + agentId: AgentId, + sessionId: string, + ) { + this.emit(clientId, agentId, { + type: "prompt_queue.updated", + properties: this.promptQueueAckData(agentId, sessionId), + }); + } + + private setPromptQueuePaused( + clientId: string, + agentId: AgentId, + sessionId: string, + paused: boolean, + ) { + const queue = this.getPromptQueue(agentId, sessionId); + queue.paused = paused; + this.emitPromptQueueUpdate(clientId, agentId, sessionId); + if (!paused) void this.drainPromptQueue(clientId, agentId, sessionId); + return queue; + } + + private emitQueuedUserMessage(item: QueuedPrompt) { + this.emit(item.clientId, item.agentId, { + type: "message", + properties: { + sessionId: item.sessionId, + message: { + id: item.id, + requestId: item.id, + role: "user", + parts: promptContentToMessageParts(item.content), + timestamp: Date.now(), + }, + }, + }); + } + private async applyPersistedSessionConfig( clientId: string, agentId: AgentId, @@ -1949,20 +2146,12 @@ export class AgentsManager { }, }); if (session.id && msg.data.prompt.trim()) { - this.prompt( + this.enqueuePrompt( msg.clientId, msg.data.backend, session.id, msg.data.content ?? msg.data.prompt, - ).catch((err) => { - this.emit(msg.clientId, msg.data.backend, { - type: "error", - properties: { - sessionId: session.id, - error: getErrorMessage(err), - }, - }); - }); + ); } } catch (err) { conn.send({ @@ -2231,24 +2420,12 @@ export class AgentsManager { msg.data.sessionId, msg.clientId, ); - this.prompt( + const item = this.enqueuePrompt( msg.clientId, msg.data.backend, msg.data.sessionId, msg.data.content ?? msg.data.text, - ).catch((err) => { - logger.error( - `Agent prompt failed for ${msg.data.backend} session ${msg.data.sessionId} (client ${msg.clientId}): ${getErrorMessage(err)}`, - err, - ); - this.emit(msg.clientId, msg.data.backend, { - type: "error", - properties: { - sessionId: msg.data.sessionId, - error: getErrorMessage(err), - }, - }); - }); + ); conn.send({ type: MsgType.AI_PROMPT_ACK, clientId: msg.clientId, @@ -2257,6 +2434,8 @@ export class AgentsManager { ack: true, backend: msg.data.backend, sessionId: msg.data.sessionId, + promptId: item.id, + queued: true, }, }); } catch (err) { @@ -2269,6 +2448,86 @@ export class AgentsManager { } }); + conn.on(MsgType.AI_PROMPT_QUEUE_UPDATE, async (msg) => { + try { + this.updateQueuedPrompt( + msg.clientId, + msg.data.backend, + msg.data.sessionId, + msg.data.queueId, + msg.data.text, + msg.data.content, + ); + const response: AiPromptQueueUpdateAckMsg = { + type: MsgType.AI_PROMPT_QUEUE_UPDATE_ACK, + clientId: msg.clientId, + respTo: msg.id, + data: this.promptQueueAckData(msg.data.backend, msg.data.sessionId), + }; + conn.send(response); + } catch (err) { + const response: AiPromptQueueUpdateAckMsg = { + type: MsgType.AI_PROMPT_QUEUE_UPDATE_ACK, + clientId: msg.clientId, + respTo: msg.id, + error: getErrorMessage(err), + }; + conn.send(response); + } + }); + + conn.on(MsgType.AI_PROMPT_QUEUE_REMOVE, async (msg) => { + try { + this.removeQueuedPrompt( + msg.clientId, + msg.data.backend, + msg.data.sessionId, + msg.data.queueId, + ); + const response: AiPromptQueueRemoveAckMsg = { + type: MsgType.AI_PROMPT_QUEUE_REMOVE_ACK, + clientId: msg.clientId, + respTo: msg.id, + data: this.promptQueueAckData(msg.data.backend, msg.data.sessionId), + }; + conn.send(response); + } catch (err) { + const response: AiPromptQueueRemoveAckMsg = { + type: MsgType.AI_PROMPT_QUEUE_REMOVE_ACK, + clientId: msg.clientId, + respTo: msg.id, + error: getErrorMessage(err), + }; + conn.send(response); + } + }); + + conn.on(MsgType.AI_PROMPT_QUEUE_PAUSE, (msg: AiPromptQueuePauseMsg) => { + try { + this.setPromptQueuePaused( + msg.clientId, + msg.data.backend, + msg.data.sessionId, + msg.data.paused, + ); + const response: AiPromptQueuePauseAckMsg = { + type: MsgType.AI_PROMPT_QUEUE_PAUSE_ACK, + clientId: msg.clientId, + respTo: msg.id, + data: this.promptQueueAckData(msg.data.backend, msg.data.sessionId), + }; + conn.send(response); + } catch (err) { + const response: AiPromptQueuePauseAckMsg = { + type: MsgType.AI_PROMPT_QUEUE_PAUSE_ACK, + clientId: msg.clientId, + respTo: msg.id, + error: getErrorMessage(err), + }; + conn.send(response); + } + }); + conn.on(MsgType.AI_ATTACHMENT_WRITE, async (msg: AiAttachmentWriteMsg) => { try { const attachment = writeAgentAttachment(msg); @@ -3131,6 +3390,79 @@ function normalizePromptContent( return parsed.length ? parsed : [{ type: "text", text: "" }]; } +function promptContentText(prompt: AcpPromptRequest["prompt"]) { + return prompt + .map((block) => { + if (block.type === "text") return block.text; + if (block.type === "resource_link") { + return `@${path.basename(filePathFromUriSafe(block.uri) ?? block.uri)}`; + } + if (block.type === "resource") { + const resource = block.resource; + const uri = + typeof resource === "object" && resource + ? (resource as { uri?: unknown }).uri + : undefined; + return typeof uri === "string" + ? `@${path.basename(filePathFromUriSafe(uri) ?? uri)}` + : "@resource"; + } + return ""; + }) + .join("") + .trim(); +} + +function promptContentToMessageParts( + prompt: AcpPromptRequest["prompt"], +): AcpMessage["parts"] { + return prompt.flatMap((block) => { + if (block.type === "text") { + return block.text ? [{ type: "text", text: block.text }] : []; + } + if (block.type === "resource_link") { + const filePath = filePathFromUriSafe(block.uri); + return [ + { + type: "file_reference", + path: filePath ?? block.uri, + name: path.basename(filePath ?? block.uri), + title: path.basename(filePath ?? block.uri), + rawContent: block, + }, + ]; + } + if (block.type === "resource") { + const resource = block.resource as { + uri?: string; + mimeType?: string; + text?: string; + }; + const uri = resource.uri ?? ""; + return [ + { + type: "file_reference", + path: filePathFromUriSafe(uri) ?? uri, + name: path.basename(uri) || "Resource", + title: path.basename(uri) || "Resource", + mimeType: resource.mimeType, + rawContent: block, + }, + ]; + } + return []; + }); +} + +function filePathFromUriSafe(uri: string) { + if (!uri.startsWith("file://")) return null; + try { + return decodeURIComponent(new URL(uri).pathname); + } catch { + return uri.slice("file://".length); + } +} + function createAgentRuntime( agentId: AgentId, descriptor: AgentDescriptor, diff --git a/cli/src/connection.ts b/cli/src/connection.ts index 68b7ebe..7aa2ccc 100644 --- a/cli/src/connection.ts +++ b/cli/src/connection.ts @@ -18,6 +18,9 @@ import { type AiMessagesListMsg, type AiPermissionReplyMsg, type AiPromptMsg, + type AiPromptQueuePauseMsg, + type AiPromptQueueRemoveMsg, + type AiPromptQueueUpdateMsg, type AiProvidersListMsg, type AiQuestionRejectMsg, type AiQuestionReplyMsg, @@ -337,6 +340,18 @@ export class Connection extends EventEmitter { eventName: typeof MsgType.AI_PROMPT, listener: (msg: AiPromptMsg) => void, ): this; + on( + eventName: typeof MsgType.AI_PROMPT_QUEUE_UPDATE, + listener: (msg: AiPromptQueueUpdateMsg) => void, + ): this; + on( + eventName: typeof MsgType.AI_PROMPT_QUEUE_REMOVE, + listener: (msg: AiPromptQueueRemoveMsg) => void, + ): this; + on( + eventName: typeof MsgType.AI_PROMPT_QUEUE_PAUSE, + listener: (msg: AiPromptQueuePauseMsg) => void, + ): this; on( eventName: typeof MsgType.AI_ATTACHMENT_WRITE, listener: (msg: AiAttachmentWriteMsg) => void, @@ -664,6 +679,18 @@ export class Connection extends EventEmitter { msg: AiMessagesListMsg, ): boolean; emit(eventName: typeof MsgType.AI_PROMPT, msg: AiPromptMsg): boolean; + emit( + eventName: typeof MsgType.AI_PROMPT_QUEUE_UPDATE, + msg: AiPromptQueueUpdateMsg, + ): boolean; + emit( + eventName: typeof MsgType.AI_PROMPT_QUEUE_REMOVE, + msg: AiPromptQueueRemoveMsg, + ): boolean; + emit( + eventName: typeof MsgType.AI_PROMPT_QUEUE_PAUSE, + msg: AiPromptQueuePauseMsg, + ): boolean; emit( eventName: typeof MsgType.AI_ATTACHMENT_WRITE, msg: AiAttachmentWriteMsg, diff --git a/cli/src/main.ts b/cli/src/main.ts index 0543fa9..6f9b562 100644 --- a/cli/src/main.ts +++ b/cli/src/main.ts @@ -635,13 +635,6 @@ async function runCli({ `Messages are ${chalk.underline("end-to-end encrypted")}.`, ), ); - - logger.log( - "🚀", - chalk.cyan( - `New relay servers in US and EU for lower latency. Update the app to ${chalk.bold("v0.0.36")} to use them.`, - ), - ); logger.log(); if (showQr) { diff --git a/package.json b/package.json index bbc3b5d..d574997 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "author": "", "license": "AGPL-3.0-only", "devDependencies": { - "@biomejs/biome": "2.5.6", + "@biomejs/biome": "2.5.7", "@changesets/cli": "^2.31.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 05733d0..07da9c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: devDependencies: '@biomejs/biome': - specifier: 2.5.6 - version: 2.5.6 + specifier: 2.5.7 + version: 2.5.7 '@changesets/cli': specifier: ^2.31.0 version: 2.31.0(@types/node@22.19.17) @@ -134,59 +134,59 @@ packages: resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} - '@biomejs/biome@2.5.6': - resolution: {integrity: sha512-lxVNjv7UF6KfhMJfL9gaUHbWdJdHbsAj6OSmwSYNdhRuG67NxNQ4Xdvh3TUxsSK9sBzJBQhEJj3AopmmNJ5pSA==} + '@biomejs/biome@2.5.7': + resolution: {integrity: sha512-zr8K/DcY5tYsQOQwqMJ0AWElo6QgmgNI7idXgXLhevVszlt8RGVpesEJPqx3ThazLaOwjJ5Y8fz3BtH5fGZNsw==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.5.6': - resolution: {integrity: sha512-zMOLZP4oMrjh6m1zcSj1ud2awUPgTuMVbmQhYYWL7J8HwCnbHHBvTm7VBTRuY7epT5bez76IpKYQ11ZAqHFlnw==} + '@biomejs/cli-darwin-arm64@2.5.7': + resolution: {integrity: sha512-vxo/Ls3/PYdQWyLhYYcgMOCzQypAjcY+iihS8M0wW03l16TCLW4zqZzGo75gm1VdCMj38hTVZ31KBWrZ4G9dJw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.5.6': - resolution: {integrity: sha512-JAC1VqzvO7Th5ZplU0G2uGfkZbxEe9uDDektPAhF0JLusoz1w+T4okp2bkykI0bbaO2vslKiRfj4gU43JaGreA==} + '@biomejs/cli-darwin-x64@2.5.7': + resolution: {integrity: sha512-Cd3Ga61amT/Yl/0x8elP5hhGYaFy4bw6WuysTgf7oo8TA5tJ5A1k+DkVoJ2BHbTVil51gTX9VPzArnrlLJ3Kyg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.5.6': - resolution: {integrity: sha512-eUa3jeeYvfMt19LBeh6E5PUZpxnTC4JqNWo+EDjTtQjAr2xLGnWaxACtVU1DQqmHYbvThlJzLX+ZsYgrqh2qVw==} + '@biomejs/cli-linux-arm64-musl@2.5.7': + resolution: {integrity: sha512-xPI5yB6XlpDbNkS+bm1t42olw5c4l3UrlOmLg7KtLJvjvkNF/1V4tnUgfkylGIeb3u/T+BzMGYqgQhzjAoJzuQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [musl] - '@biomejs/cli-linux-arm64@2.5.6': - resolution: {integrity: sha512-6XsYwCFkp5sMxl85ffhgeGpGgs6A7dRYFnkceZ7WVxvycuTnGdD5xa534Z3xfrBQ0JCMK/mujT6ZNPJoghedwg==} + '@biomejs/cli-linux-arm64@2.5.7': + resolution: {integrity: sha512-rR2QE0yF2GYSuYuKIa7pKvODGJqnOH+2eDREAM8wV+mWKSkMQKdAp4zXEZfTaxY8PMoNONnpgSWcBCyLDPDOKg==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [glibc] - '@biomejs/cli-linux-x64-musl@2.5.6': - resolution: {integrity: sha512-2Vp13QdKysH3HIWLaYLhUUwbK+jbZonJD1K+Lr0d0RO4wH7mkYd43vJixEDm8cUWrowoRz4UUHF1nm9Ae7ym8A==} + '@biomejs/cli-linux-x64-musl@2.5.7': + resolution: {integrity: sha512-rE5VZi+qtmPgQH+l7jVxYoZ18b/TiHEhulhMpjmCZH1PltSbjRcxNWywC3HZ9tYottG7ORkeTtoscBilKSBm0g==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [musl] - '@biomejs/cli-linux-x64@2.5.6': - resolution: {integrity: sha512-Pop9VXCFUhFTMfFefZ39S+u2rOPyNp5iHlxbZRwXGACHLy2r0jjiRgJHmaEKJzL3SyxlVeGShXhvvElvWowonA==} + '@biomejs/cli-linux-x64@2.5.7': + resolution: {integrity: sha512-FQgqJhscrqJUFptGaRSUJWlXAExwWcDwLuK49dvKfkQ1bB5SEEyFssnsxQY83Xm6jR0EbbX3+8+D5bfvYqUG2Q==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [glibc] - '@biomejs/cli-win32-arm64@2.5.6': - resolution: {integrity: sha512-tDGshcm6BdkZOCGnTDX0Y8/U4IfBSlnUU7T56nNDuPEfed+aHg+u8G36NB43fJVl0Os6+QURXIE1yuD7AaEofA==} + '@biomejs/cli-win32-arm64@2.5.7': + resolution: {integrity: sha512-Oq4x0CCwP4jirrcTywXs5kOGZ4v5vuEP+gWrbtjApOA2CL9F3F9GlIdQIci8AKSCa/zURanMRpX/4wQ7Am6hHg==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.5.6': - resolution: {integrity: sha512-WN05KwXnTO/2J45RQPvzZMXf7tZUIofHoR35xIPfCo7pQ2RFidxI8sfb5mGsaTxdMmEOzHzOPRCdA5/fCpc7xQ==} + '@biomejs/cli-win32-x64@2.5.7': + resolution: {integrity: sha512-V+0wu/nrj2S+MhP4EQ0uHNolP0IALEsz45pg0WoKkHfDeh0+ItHwP/p7bX5RPoMOl9NkpHYWdYPhIcy2mACHvQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -1869,39 +1869,39 @@ snapshots: '@babel/runtime@7.29.2': {} - '@biomejs/biome@2.5.6': + '@biomejs/biome@2.5.7': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.5.6 - '@biomejs/cli-darwin-x64': 2.5.6 - '@biomejs/cli-linux-arm64': 2.5.6 - '@biomejs/cli-linux-arm64-musl': 2.5.6 - '@biomejs/cli-linux-x64': 2.5.6 - '@biomejs/cli-linux-x64-musl': 2.5.6 - '@biomejs/cli-win32-arm64': 2.5.6 - '@biomejs/cli-win32-x64': 2.5.6 + '@biomejs/cli-darwin-arm64': 2.5.7 + '@biomejs/cli-darwin-x64': 2.5.7 + '@biomejs/cli-linux-arm64': 2.5.7 + '@biomejs/cli-linux-arm64-musl': 2.5.7 + '@biomejs/cli-linux-x64': 2.5.7 + '@biomejs/cli-linux-x64-musl': 2.5.7 + '@biomejs/cli-win32-arm64': 2.5.7 + '@biomejs/cli-win32-x64': 2.5.7 - '@biomejs/cli-darwin-arm64@2.5.6': + '@biomejs/cli-darwin-arm64@2.5.7': optional: true - '@biomejs/cli-darwin-x64@2.5.6': + '@biomejs/cli-darwin-x64@2.5.7': optional: true - '@biomejs/cli-linux-arm64-musl@2.5.6': + '@biomejs/cli-linux-arm64-musl@2.5.7': optional: true - '@biomejs/cli-linux-arm64@2.5.6': + '@biomejs/cli-linux-arm64@2.5.7': optional: true - '@biomejs/cli-linux-x64-musl@2.5.6': + '@biomejs/cli-linux-x64-musl@2.5.7': optional: true - '@biomejs/cli-linux-x64@2.5.6': + '@biomejs/cli-linux-x64@2.5.7': optional: true - '@biomejs/cli-win32-arm64@2.5.6': + '@biomejs/cli-win32-arm64@2.5.7': optional: true - '@biomejs/cli-win32-x64@2.5.6': + '@biomejs/cli-win32-x64@2.5.7': optional: true '@changesets/apply-release-plan@7.1.1': diff --git a/protocol/src/agents/index.ts b/protocol/src/agents/index.ts index 935dd2f..a1d2e0a 100644 --- a/protocol/src/agents/index.ts +++ b/protocol/src/agents/index.ts @@ -332,6 +332,59 @@ export const AiAttachmentWriteMsgSchema = z.object({ }); export type AiAttachmentWriteMsg = z.infer; +const AiPromptQueueItemSchema = z.object({ + id: z.string(), + backend: AgentIdSchema, + sessionId: z.string(), + text: z.string(), + content: z.array(AcpContentBlockSchema), + createdAt: z.number(), + updatedAt: z.number(), +}); +export type AiPromptQueueItem = z.infer; + +export const AiPromptQueueUpdateMsgSchema = z.object({ + id: z.string(), + type: z.literal(MsgType.AI_PROMPT_QUEUE_UPDATE), + clientId: z.string(), + data: z.object({ + backend: AgentIdSchema, + sessionId: z.string(), + queueId: z.string(), + text: z.string(), + content: z.array(AcpContentBlockSchema), + }), +}); +export type AiPromptQueueUpdateMsg = z.infer< + typeof AiPromptQueueUpdateMsgSchema +>; + +export const AiPromptQueueRemoveMsgSchema = z.object({ + id: z.string(), + type: z.literal(MsgType.AI_PROMPT_QUEUE_REMOVE), + clientId: z.string(), + data: z.object({ + backend: AgentIdSchema, + sessionId: z.string(), + queueId: z.string(), + }), +}); +export type AiPromptQueueRemoveMsg = z.infer< + typeof AiPromptQueueRemoveMsgSchema +>; + +export const AiPromptQueuePauseMsgSchema = z.object({ + id: z.string(), + type: z.literal(MsgType.AI_PROMPT_QUEUE_PAUSE), + clientId: z.string(), + data: z.object({ + backend: AgentIdSchema, + sessionId: z.string(), + paused: z.boolean(), + }), +}); +export type AiPromptQueuePauseMsg = z.infer; + export const AiAgentsManageListMsgSchema = z.object({ id: z.string(), type: z.literal(MsgType.AI_AGENTS_MANAGE_LIST), @@ -592,3 +645,46 @@ export const AiAttachmentWriteResultMsgSchema = z.object({ export type AiAttachmentWriteResultMsg = z.infer< typeof AiAttachmentWriteResultMsgSchema >; + +const AiPromptQueueAckDataSchema = z.object({ + backend: AgentIdSchema, + sessionId: z.string(), + queue: z.array(AiPromptQueueItemSchema), + running: z.boolean(), +}); + +export const AiPromptQueueUpdateAckMsgSchema = z.object({ + id: z.string().optional(), + type: z.literal(MsgType.AI_PROMPT_QUEUE_UPDATE_ACK), + clientId: z.string(), + respTo: z.string().optional(), + error: z.string().optional(), + data: AiPromptQueueAckDataSchema.optional(), +}); +export type AiPromptQueueUpdateAckMsg = z.infer< + typeof AiPromptQueueUpdateAckMsgSchema +>; + +export const AiPromptQueueRemoveAckMsgSchema = z.object({ + id: z.string().optional(), + type: z.literal(MsgType.AI_PROMPT_QUEUE_REMOVE_ACK), + clientId: z.string(), + respTo: z.string().optional(), + error: z.string().optional(), + data: AiPromptQueueAckDataSchema.optional(), +}); +export type AiPromptQueueRemoveAckMsg = z.infer< + typeof AiPromptQueueRemoveAckMsgSchema +>; + +export const AiPromptQueuePauseAckMsgSchema = z.object({ + id: z.string().optional(), + type: z.literal(MsgType.AI_PROMPT_QUEUE_PAUSE_ACK), + clientId: z.string(), + respTo: z.string().optional(), + error: z.string().optional(), + data: AiPromptQueueAckDataSchema.optional(), +}); +export type AiPromptQueuePauseAckMsg = z.infer< + typeof AiPromptQueuePauseAckMsgSchema +>; diff --git a/protocol/src/ai-legacy.ts b/protocol/src/ai-legacy.ts index d97debf..534aa78 100644 --- a/protocol/src/ai-legacy.ts +++ b/protocol/src/ai-legacy.ts @@ -612,6 +612,8 @@ export const AiPromptAckMsgSchema = z.object({ ack: z.boolean(), backend: AiBackendSchema.optional(), sessionId: z.string().optional(), + promptId: z.string().optional(), + queued: z.boolean().optional(), }) .optional(), }); diff --git a/protocol/src/base.ts b/protocol/src/base.ts index 4af3c43..6ce0b4e 100644 --- a/protocol/src/base.ts +++ b/protocol/src/base.ts @@ -140,6 +140,12 @@ export const MsgType = { AI_MESSAGES_LIST_RESULT: "ai:messages:list:result", AI_PROMPT: "ai:prompt", AI_PROMPT_ACK: "ai:prompt:ack", + AI_PROMPT_QUEUE_UPDATE: "ai:prompt-queue:update", + AI_PROMPT_QUEUE_UPDATE_ACK: "ai:prompt-queue:update:ack", + AI_PROMPT_QUEUE_REMOVE: "ai:prompt-queue:remove", + AI_PROMPT_QUEUE_REMOVE_ACK: "ai:prompt-queue:remove:ack", + AI_PROMPT_QUEUE_PAUSE: "ai:prompt-queue:pause", + AI_PROMPT_QUEUE_PAUSE_ACK: "ai:prompt-queue:pause:ack", /** Request to write an agent chat attachment into CLI-owned storage */ AI_ATTACHMENT_WRITE: "ai:attachment:write", /** Response after agent chat attachment write completes */ diff --git a/protocol/src/client/to-host.ts b/protocol/src/client/to-host.ts index b492b2a..214432c 100644 --- a/protocol/src/client/to-host.ts +++ b/protocol/src/client/to-host.ts @@ -6,6 +6,9 @@ import { AiAgentsEnableSetMsgSchema, AiAgentsManageListMsgSchema, AiAttachmentWriteMsgSchema, + AiPromptQueuePauseMsgSchema, + AiPromptQueueRemoveMsgSchema, + AiPromptQueueUpdateMsgSchema, AiSessionAttachMsgSchema, AiSessionCloseMsgSchema, AiSessionConfigSetMsgSchema, @@ -111,6 +114,9 @@ export const ClientToHostMsgSchema = z.discriminatedUnion("type", [ AiSessionDeleteMsgSchema, AiMessagesListMsgSchema, AiPromptMsgSchema, + AiPromptQueueUpdateMsgSchema, + AiPromptQueueRemoveMsgSchema, + AiPromptQueuePauseMsgSchema, AiAttachmentWriteMsgSchema, AiSessionConfigSetMsgSchema, AiSessionModeSetMsgSchema, diff --git a/protocol/src/host/to-client.ts b/protocol/src/host/to-client.ts index 66a8a1e..b6b3d4c 100644 --- a/protocol/src/host/to-client.ts +++ b/protocol/src/host/to-client.ts @@ -6,6 +6,9 @@ import { AiAgentsEnableSetResultMsgSchema, AiAgentsManageListResultMsgSchema, AiAttachmentWriteResultMsgSchema, + AiPromptQueuePauseAckMsgSchema, + AiPromptQueueRemoveAckMsgSchema, + AiPromptQueueUpdateAckMsgSchema, AiSessionAttachResultMsgSchema, AiSessionCloseResultMsgSchema, AiSessionConfigSetResultMsgSchema, @@ -111,6 +114,9 @@ export const HostToClientSchema = z.discriminatedUnion("type", [ AiSessionDeletedMsgSchema, AiMessagesListResultMsgSchema, AiPromptAckMsgSchema, + AiPromptQueueUpdateAckMsgSchema, + AiPromptQueueRemoveAckMsgSchema, + AiPromptQueuePauseAckMsgSchema, AiAttachmentWriteResultMsgSchema, AiSessionConfigSetResultMsgSchema, AiSessionModeSetResultMsgSchema,