diff --git a/extensions/shared/capability-intent.ts b/extensions/shared/capability-intent.ts index f7da1d54..f473385f 100644 --- a/extensions/shared/capability-intent.ts +++ b/extensions/shared/capability-intent.ts @@ -7,8 +7,9 @@ const CAPABILITY_INTENT = { search: /\b(?:use|run)\s+(?:fd|rg)\b|\buse\s+(?:structured\s+)?(?:(?:file|code|content)\s+)?search\b|\b(?:structured|fast)\s+(?:file|code|content)\s+search\b|(?:使用|用|运行).{0,8}(?:fd|rg|git\s+(?:show|diff|log))|结构化(?:文件|代码|内容)搜索/iu, delegate: - /\bsubagents?\b|(?:^|[.!?]\s+)(?:please\s+)?(?:delegate|parallelize)\s+(?:this|the)\s+(?:task|work)\b|\b(?:can|could|would)\s+you\s+(?:please\s+)?(?:delegate|parallelize)\s+(?:this|the)\s+(?:task|work)\b|\bparallel\s+agents?\b|^\s*子代理\s*[,,::]?\s*(?:(?:来|去|请|帮(?:我|忙)?|负责|给我)\s*)?(?:(?:先|全面|系统(?:性地)?|深入|快速|仔细)\s*)?(?:了解|查看|检查|审查|分析|研究|调研|梳理|探索|实现|修复|处理|执行|完成|阅读|总结)|(?:使用|用|启动|调用|来|开).{0,8}子代理|(?:多个?|多路)子代理|并行.{0,8}(?:代理|agent)|委派.{0,6}(?:任务|给|出去)/iu, - workflow: /\bworkflows?\b|(?:使用|用|运行|创建|构建).{0,8}工作流/iu, + /(?:^|[.!?]\s+)(?:please\s+)?(?:delegate|parallelize)\s+(?:this|the)\s+(?:task|work)\b|\b(?:can|could|would)\s+you\s+(?:please\s+)?(?:delegate|parallelize)\s+(?:this|the)\s+(?:task|work)\b|\b(?:use|spawn|start)\s+(?:a\s+)?subagents?\b|\bparallel\s+agents?\b|(?:使用|用|启动|调用).{0,8}subagents?\b|^\s*子代理\s*[,,::]?\s*(?:(?:来|去|请|帮(?:我|忙)?|负责|给我)\s*)?(?:(?:先|全面|系统(?:性地)?|深入|快速|仔细)\s*)?(?:了解|查看|检查|审查|分析|研究|调研|梳理|探索|实现|修复|处理|执行|完成|阅读|总结)|(?:使用|用|启动|调用|来|开).{0,8}子代理|(?:多个?|多路)子代理|并行.{0,8}(?:代理|agent)|委派.{0,6}(?:任务|给|出去)/iu, + workflow: + /\b(?:use|run|start|create|build)\s+(?:a\s+)?workflows?\b|(?:使用|用|运行|创建|构建).{0,8}(?:工作流|workflows?)/iu, background: /\b(?:run|start|keep)\b.{0,40}\b(?:in the background|background\s+(?:process|terminal|job))\b|后台.{0,8}(?:运行|启动|进程|终端|任务)/iu, session: @@ -31,8 +32,9 @@ function isExplicitClause(clause: string) { /** * One fail-closed interpretation of explicit user capability intent. The - * English names `subagent` and `workflow` are reserved authorization words; - * negated or conditional clauses remain inert. Runtime activation and + * English capability names require an imperative request context; references + * in identifiers, discussion, or comparisons stay inert. Negated or + * conditional clauses remain inert. Runtime activation and * pre-submit UI feedback both cross this seam, so they cannot drift. */ export function capabilitiesRequestedByPrompt(prompt: string) { diff --git a/tests/extensions/shared/capability-intent.test.ts b/tests/extensions/shared/capability-intent.test.ts index 9b066702..967327e9 100644 --- a/tests/extensions/shared/capability-intent.test.ts +++ b/tests/extensions/shared/capability-intent.test.ts @@ -6,10 +6,7 @@ import { } from "../../../extensions/shared/capability-intent.ts"; test("classifies explicit capability intent across English, Chinese, and mixed phrases", () => { - assert.deepEqual(capabilitiesRequestedByPrompt("subagent, workflow"), [ - "delegate", - "workflow", - ]); + assert.deepEqual(capabilitiesRequestedByPrompt("subagent, workflow"), []); assert.deepEqual( capabilitiesRequestedByPrompt("用 Subagent 并行检查这三个模块"), ["delegate"], @@ -31,10 +28,19 @@ test("classifies explicit capability intent across English, Chinese, and mixed p ); }); -test("reserved English capability words authorize the matching groups", () => { +test("English references without an imperative stay inert", () => { assert.deepEqual( capabilitiesRequestedByPrompt("Compare Subagent and Workflow"), - ["delegate", "workflow"], + [], + ); + for (const prompt of [ + "git checkout -b subagent-matching", + "Please review the subagent.ts implementation", + "讨论 workflow 和 subagent 的区别", + ]) assert.deepEqual(capabilitiesRequestedByPrompt(prompt), [], prompt); + assert.deepEqual( + capabilitiesRequestedByPrompt("Use a subagent to review this change"), + ["delegate"], ); }); diff --git a/web/host/web-host.ts b/web/host/web-host.ts index 6203e647..9cde64dd 100644 --- a/web/host/web-host.ts +++ b/web/host/web-host.ts @@ -541,6 +541,28 @@ export class WebHost { cursor: this.sequence, }); } + if (url.pathname === "/api/prompt/cancel" && request.method === "POST") { + const body = await this.readJson(request); + if (typeof body.sessionId !== "string" || body.sessionId !== this.runtime.sessionManager.getSessionId()) { + return this.json(response, 409, { + code: "SESSION_CONFLICT", + error: "Only the active Web session accepts cancellation", + }); + } + try { + if (!this.runtime.interruptTurn) { + return this.json(response, 501, { + code: "PROMPT_CANCEL_FAILED", + error: "Turn cancellation is unavailable", + }); + } + const result = await this.runtime.interruptTurn({ expectedSessionId: body.sessionId }); + return this.json(response, 200, result); + } catch (error) { + const failure = this.runtimeRequestFailure(error, "PROMPT_CANCEL_FAILED", "prompt cancellation failed"); + return this.json(response, failure.status, { code: failure.code, error: failure.error }); + } + } if (request.method !== "GET") { return this.json(response, 405, { error: "method not allowed" }); } diff --git a/web/runtime/pi-runtime.ts b/web/runtime/pi-runtime.ts index cf792d39..cac27ef4 100644 --- a/web/runtime/pi-runtime.ts +++ b/web/runtime/pi-runtime.ts @@ -454,6 +454,24 @@ export class PiWebRuntime implements WebRuntimeController { await requestAdmission; } + async interruptTurn(options?: { expectedSessionId?: string }) { + this.assertActive(); + this.assertWorkspaceSelected(); + const session = this.runtime.session; + const sessionId = session.sessionManager.getSessionId(); + if (options?.expectedSessionId !== undefined && options.expectedSessionId !== sessionId) { + throw new WebRuntimeRequestError( + "Only the active Web session accepts cancellation", + "SESSION_CONFLICT", + 409, + ); + } + if (!session.isStreaming) return { state: "already_settled" as const }; + await session.abort(); + this.emit("prompt_cancelled", { sessionId }); + return { state: "cancelled" as const }; + } + newSession(workspacePath: string, options?: WebSessionCreationOptions) { return this.serializeControllerMutation(() => this.createNewSession(workspacePath, options), diff --git a/web/runtime/types.ts b/web/runtime/types.ts index 2b66c3f0..c8d4649f 100644 --- a/web/runtime/types.ts +++ b/web/runtime/types.ts @@ -10,6 +10,7 @@ export type WebRuntimeRequestErrorCode = | "MODEL_NOT_AVAILABLE" | "SESSION_CONFLICT" | "PROMPT_REJECTED" + | "PROMPT_CANCEL_FAILED" | "WORKSPACE_REQUIRED"; export class WebRuntimeRequestError extends Error { @@ -55,6 +56,9 @@ export interface WebRuntimeController { readonly sessionManager: SessionManager; isIdle(): boolean; sendPrompt(content: string, options?: WebPromptOptions): Promise; + interruptTurn?(options?: { expectedSessionId?: string }): Promise<{ + state: "cancelled" | "already_settled"; + }>; newSession( workspacePath: string, options?: WebSessionCreationOptions,