Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions extensions/shared/capability-intent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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) {
Expand Down
18 changes: 12 additions & 6 deletions tests/extensions/shared/capability-intent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand All @@ -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"],
);
});

Expand Down
22 changes: 22 additions & 0 deletions web/host/web-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" });
}
Expand Down
18 changes: 18 additions & 0 deletions web/runtime/pi-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
4 changes: 4 additions & 0 deletions web/runtime/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type WebRuntimeRequestErrorCode =
| "MODEL_NOT_AVAILABLE"
| "SESSION_CONFLICT"
| "PROMPT_REJECTED"
| "PROMPT_CANCEL_FAILED"
| "WORKSPACE_REQUIRED";

export class WebRuntimeRequestError extends Error {
Expand Down Expand Up @@ -55,6 +56,9 @@ export interface WebRuntimeController {
readonly sessionManager: SessionManager;
isIdle(): boolean;
sendPrompt(content: string, options?: WebPromptOptions): Promise<void>;
interruptTurn?(options?: { expectedSessionId?: string }): Promise<{
state: "cancelled" | "already_settled";
}>;
newSession(
workspacePath: string,
options?: WebSessionCreationOptions,
Expand Down
Loading