openmausbot 0.1.86 → 0.1.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-BTiuL79F.js} +1 -1
- package/dist/assets/{arc-DW7mrI4p.js → arc-C88Blc2v.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-BZsJIZrb.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CVeMe9Ah.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-Cedk3Bhx.js} +1 -1
- package/dist/assets/channel-taCNRlUU.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-D-VRqzTB.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-BChov01Y.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-C-TfwvHy.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-Db7CGj5Y.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-CNvIfqKT.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-C_bEp5tq.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Coi4BOls.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-CzylEuL3.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-m7p7ztaD.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BMy6o9x2.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-BDQ__0_J.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Djv4ghUb.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-BULrlGYH.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-D-jrT_hU.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-p2UhQmfL.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-DHvGWZ7q.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-ChhKDot-.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-B-T7A43R.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-CJm8ENNY.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-510tlza8.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-PkvRPHel.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-BVBVPoX3.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BgYMVm7L.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-BGqsnc_3.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-D_-UeReH.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-D0uooONx.js} +1 -1
- package/dist/assets/index-BwdMAQ6p.css +1 -0
- package/dist/assets/index-ByJennr7.js +581 -0
- package/dist/assets/{index-h9FFNwTu.js → index-zRsJERGJ.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-B9e1z77f.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-BB4vNwvD.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DpkosOTB.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-DbDP4kUu.js} +1 -1
- package/dist/assets/{linear-3su2TG93.js → linear-DBJGPmjT.js} +1 -1
- package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-DWPRi-ue.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-nszBbJmx.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-CWykJWu_.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-g3Da-Vnv.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-Wzrlpzgh.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-moHTFMC0.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-CwfXdFH8.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-3ZROYvnq.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-E1TBebP8.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-DHKEU5pu.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BguEj09D.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-C7IDYnhd.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BpEdoB2e.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-DCW-F5PW.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-xBMFtWYV.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-B5nIkuH8.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-DYFAhdbr.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-DeXwX6Cz.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/browser-proxy.js +2 -1
- package/dist-server/container-mcp.js +1029 -164
- package/dist-server/drivers/agents-proxy.js +64 -10
- package/dist-server/drivers/phone-proxy.js +33 -2
- package/dist-server/index.js +14577 -9001
- package/dist-server/local-computer-proxy.js +3 -3
- package/dist-server/local-computer.js +53 -17
- package/dist-server/openmausbot.js +3981 -1165
- package/dist-server/pair-cli.js +3981 -1165
- package/dist-server/permission-proxy.js +29 -9
- package/dist-server/server/attachments.js +55 -0
- package/dist-server/server/auto-approve.js +4 -3
- package/dist-server/server/auto-vm-claims.js +1 -0
- package/dist-server/server/bot-package.js +18 -292
- package/dist-server/server/bot-profile.js +5 -17
- package/dist-server/server/box-create-idempotency.js +3 -3
- package/dist-server/server/box.js +57 -55
- package/dist-server/server/browser-proxy.js +3 -2
- package/dist-server/server/browser-runtime.js +37 -1
- package/dist-server/server/cli.js +2 -2
- package/dist-server/server/command-allowlist.js +154 -0
- package/dist-server/server/computer-wait.js +22 -9
- package/dist-server/server/config.js +59 -9
- package/dist-server/server/connector-verdict.js +141 -0
- package/dist-server/server/container-computer.js +1 -1
- package/dist-server/server/default-model-selection.js +9 -0
- package/dist-server/server/delegations.js +85 -34
- package/dist-server/server/drivers/acp/core.js +57 -15
- package/dist-server/server/drivers/agents-call.js +42 -3
- package/dist-server/server/drivers/agents-catalog.js +34 -4
- package/dist-server/server/drivers/boxagent.js +174 -36
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/chat-box-tools.js +90 -0
- package/dist-server/server/drivers/chat-images.js +54 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +38 -8
- package/dist-server/server/drivers/chat-tool-approval.js +70 -18
- package/dist-server/server/drivers/claude.js +7 -2
- package/dist-server/server/drivers/codex-instructions.js +48 -6
- package/dist-server/server/drivers/codex-tool-surface.js +20 -0
- package/dist-server/server/drivers/codex.js +95 -30
- package/dist-server/server/drivers/mistral.js +115 -0
- package/dist-server/server/drivers/openai-chat.js +155 -32
- package/dist-server/server/drivers/openai-compat.js +1 -0
- package/dist-server/server/drivers/permission-command.js +35 -0
- package/dist-server/server/drivers/phone-proxy.js +47 -2
- package/dist-server/server/drivers/pi.js +75 -4
- package/dist-server/server/drivers/prompt-split.js +87 -0
- package/dist-server/server/fleet.js +1 -1
- package/dist-server/server/group-thread-usage.js +103 -0
- package/dist-server/server/harness/registry.js +1 -0
- package/dist-server/server/hosted-models.js +1 -1
- package/dist-server/server/index.js +1385 -322
- package/dist-server/server/local-computer.js +72 -23
- package/dist-server/server/managed-desktop.js +1 -1
- package/dist-server/server/mcp-bridge.js +6 -6
- package/dist-server/server/new-bot-defaults.js +115 -0
- package/dist-server/server/org-library.js +1051 -0
- package/dist-server/server/package-export.js +627 -39
- package/dist-server/server/package-import.js +458 -0
- package/dist-server/server/package-parts.js +87 -0
- package/dist-server/server/peer-approval.js +16 -7
- package/dist-server/server/peer-roster.js +10 -0
- package/dist-server/server/presets.js +492 -0
- package/dist-server/server/provider-key-check.js +3 -2
- package/dist-server/server/published-teams.js +80 -0
- package/dist-server/server/room-handoffs.js +19 -3
- package/dist-server/server/routes/bot-presets.js +25 -0
- package/dist-server/server/routine-requests.js +6 -3
- package/dist-server/server/routines.js +41 -1
- package/dist-server/server/section-context.js +10 -0
- package/dist-server/server/skills.js +34 -61
- package/dist-server/server/steer-queue.js +3 -2
- package/dist-server/server/store.js +259 -15
- package/dist-server/server/surface.js +10 -4
- package/dist-server/server/system-prompt.js +12 -4
- package/dist-server/server/team-backup.js +6 -2
- package/dist-server/server/team-computers.js +10 -0
- package/dist-server/server/team-library.js +6 -2
- package/dist-server/server/team-manifest.js +4 -3
- package/dist-server/server/team-setup-requests.js +16 -5
- package/dist-server/server/thread-events.js +17 -1
- package/dist-server/server/turn-context.js +2 -0
- package/dist-server/server/turn-watchdog.js +12 -3
- package/dist-server/server/usage-ledger.js +12 -1
- package/dist-server/server/vps-computer.js +139 -51
- package/dist-server/server/vps-container-mcp.js +7 -0
- package/dist-server/server/vps-ssh.js +36 -7
- package/dist-server/server/workspace-backup-policy.js +12 -0
- package/dist-server/server/workspace-backup.js +2 -2
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/ask-question.js +188 -12
- package/dist-server/shared/bot-profile.js +15 -0
- package/dist-server/shared/command-allowlist.js +1 -0
- package/dist-server/shared/credential-request.js +1 -1
- package/dist-server/shared/package-format.js +967 -0
- package/dist-server/shared/skill-md.js +64 -0
- package/dist-server/shared/team-backup.js +7 -0
- package/dist-server/shared/wire.js +4 -0
- package/dist-server/vps-container-mcp.js +1129 -170
- package/enterprise/README.md +1 -1
- package/package.json +1 -1
- package/dist/assets/channel-BXFkgus9.js +0 -1
- package/dist/assets/index-BGgqlWPJ.css +0 -1
- package/dist/assets/index-BtHo8ZKl.js +0 -577
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +0 -8
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { newEventId, newId } from "../contracts.js";
|
|
2
|
+
import { ASK_USER_TOOL, ASK_USER_TOOL_DEFINITION, askQuestionSummary, parseAskQuestions, questionChoices } from "../../shared/ask-question.js";
|
|
2
3
|
import { redactSecretsInText } from "../redact.js";
|
|
3
4
|
import { toolDetailPreview } from "../tool-summary.js";
|
|
4
5
|
import { ChatToolSessionError, mountChatTools } from "./chat-mcp-tools.js";
|
|
6
|
+
import { assertImageTransport, chatImageBudget, chatToolImages, chatUserContent } from "./chat-images.js";
|
|
7
|
+
import { promptHalves, volatileContextNote, withContextNote } from "./prompt-split.js";
|
|
5
8
|
import { createChatToolApproval } from "./chat-tool-approval.js";
|
|
6
9
|
import { ChatProtocolError, ChatReasoningDetails, ChatToolCalls, MAX_CHAT_TOOL_CALLS, object } from "./openai-chat-protocol.js";
|
|
7
10
|
import { appendNative } from "./native.js";
|
|
@@ -25,6 +28,26 @@ const usageFrom = (usage) => usage
|
|
|
25
28
|
? { input: usage.prompt_tokens ?? 0, output: usage.completion_tokens ?? 0 }
|
|
26
29
|
: null;
|
|
27
30
|
const asError = (value) => value instanceof Error ? value : new Error(String(value));
|
|
31
|
+
class UnsupportedChatToolsError extends Error {
|
|
32
|
+
}
|
|
33
|
+
function rejectsToolsParameter(status, body) {
|
|
34
|
+
if (status !== 400 && status !== 422)
|
|
35
|
+
return false;
|
|
36
|
+
let envelope;
|
|
37
|
+
try {
|
|
38
|
+
envelope = object(JSON.parse(body));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const error = object(envelope?.error);
|
|
44
|
+
if (error?.param === "tools" && error.code === "unsupported_parameter")
|
|
45
|
+
return true;
|
|
46
|
+
const message = error?.message ?? envelope?.error;
|
|
47
|
+
return typeof message === "string" && (/\bdoes not support (?:tools|tool calling|function calling)(?:[.!]?$|[.!]?\s)/i.test(message) ||
|
|
48
|
+
/\b(?:tools|tool calling|function calling) (?:is|are) not supported\b/i.test(message) ||
|
|
49
|
+
/^(?:unsupported|unknown|unrecognized) (?:parameter|field):?\s*['"]?tools['"]?[.!]?$/i.test(message.trim()));
|
|
50
|
+
}
|
|
28
51
|
/** Shared runtime for the three providers that speak OpenAI chat completions. */
|
|
29
52
|
export function createOpenAIChatRuntime(options) {
|
|
30
53
|
const { input } = options;
|
|
@@ -59,6 +82,7 @@ export function createOpenAIChatRuntime(options) {
|
|
|
59
82
|
: timeoutController.signal;
|
|
60
83
|
const response = await fetch(`${options.apiUrl}/chat/completions`, {
|
|
61
84
|
method: "POST",
|
|
85
|
+
...(messages.some(message => Array.isArray(message.content) && message.content.some(part => part.type === "image_url")) ? { redirect: "error" } : {}),
|
|
62
86
|
headers: { authorization: `Bearer ${options.apiKey}`, "content-type": "application/json" },
|
|
63
87
|
body: JSON.stringify({
|
|
64
88
|
...options.requestBody(model, messages, stream),
|
|
@@ -68,7 +92,10 @@ export function createOpenAIChatRuntime(options) {
|
|
|
68
92
|
});
|
|
69
93
|
if (!response.ok) {
|
|
70
94
|
const body = await response.text().catch(() => "");
|
|
71
|
-
|
|
95
|
+
const message = `${options.httpErrorLabel} HTTP ${response.status}${body ? `: ${body.slice(0, 200)}` : ""}`;
|
|
96
|
+
if (rejectsToolsParameter(response.status, body))
|
|
97
|
+
throw new UnsupportedChatToolsError(message);
|
|
98
|
+
throw new Error(message);
|
|
72
99
|
}
|
|
73
100
|
if (!stream || response.headers.get("content-type")?.includes("application/json")) {
|
|
74
101
|
const json = await response.json();
|
|
@@ -89,7 +116,7 @@ export function createOpenAIChatRuntime(options) {
|
|
|
89
116
|
const finishReason = json.choices?.[0]?.finish_reason ?? null;
|
|
90
117
|
activeSignal.throwIfAborted();
|
|
91
118
|
return {
|
|
92
|
-
text: typeof message?.content === "string" ? message.content : "",
|
|
119
|
+
text: options.contentText ? options.contentText(message?.content) : typeof message?.content === "string" ? message.content : "",
|
|
93
120
|
reasoning: options.reasoning && typeof reasoning === "string"
|
|
94
121
|
? reasoning
|
|
95
122
|
: "",
|
|
@@ -155,7 +182,7 @@ export function createOpenAIChatRuntime(options) {
|
|
|
155
182
|
const reasoningDelta = options.reasoning && typeof reasoningPart === "string"
|
|
156
183
|
? reasoningPart
|
|
157
184
|
: "";
|
|
158
|
-
const contentDelta = typeof delta?.content === "string" ? delta.content : "";
|
|
185
|
+
const contentDelta = options.contentText ? options.contentText(delta?.content) : typeof delta?.content === "string" ? delta.content : "";
|
|
159
186
|
if (reasoningDelta) {
|
|
160
187
|
reasoning += reasoningDelta;
|
|
161
188
|
onDelta?.(reasoningDelta, "reasoning_text");
|
|
@@ -220,24 +247,44 @@ export function createOpenAIChatRuntime(options) {
|
|
|
220
247
|
clearTimeout(idleTimer);
|
|
221
248
|
}
|
|
222
249
|
};
|
|
223
|
-
const messagesFor = (turn) =>
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
250
|
+
const messagesFor = (turn) => {
|
|
251
|
+
// The system message is the head of the resent prefix, so only the
|
|
252
|
+
// stable half belongs there: a volatile edit must not re-price the
|
|
253
|
+
// tools, instructions and transcript the provider already cached.
|
|
254
|
+
// The volatile half rides the newest user message instead, every
|
|
255
|
+
// turn. Unlike a CLI session, this request is rebuilt from the stored
|
|
256
|
+
// transcript, which never contains the delivered notes, so tracking a
|
|
257
|
+
// digest and delivering only on change would leave the model without
|
|
258
|
+
// its memory on unchanged turns. The newest message is fresh input
|
|
259
|
+
// on every request anyway.
|
|
260
|
+
const halves = promptHalves(turn);
|
|
261
|
+
const note = halves.stable !== null ? volatileContextNote(halves.volatile, false) : "";
|
|
262
|
+
const userTurn = note ? { ...turn, text: withContextNote(note, turn.text) } : turn;
|
|
263
|
+
const system = halves.stable ?? turn.system;
|
|
264
|
+
return [
|
|
265
|
+
...(system ? [{ role: "system", content: system }] : []),
|
|
266
|
+
...(turn.transcript ?? []).map((message) => ({
|
|
267
|
+
role: message.role,
|
|
268
|
+
content: message.text,
|
|
269
|
+
})),
|
|
270
|
+
{ role: "user", content: options.computerUse ? chatUserContent(userTurn) : userTurn.text },
|
|
271
|
+
];
|
|
272
|
+
};
|
|
231
273
|
const sendTurn = async (turn) => {
|
|
232
274
|
if (!options.apiKey)
|
|
233
275
|
throw new Error(options.missingKeyError);
|
|
234
276
|
if (active.has(turn.threadId))
|
|
235
277
|
throw new Error("a turn is already running on this thread");
|
|
278
|
+
if (options.computerUse && (turn.images?.length || turn.integrations?.computer || turn.integrations?.localComputer || turn.integrations?.browser))
|
|
279
|
+
assertImageTransport(options.apiUrl);
|
|
236
280
|
const turnId = newId();
|
|
237
281
|
const abort = new AbortController();
|
|
238
282
|
const messages = messagesFor(turn);
|
|
283
|
+
const retainImages = chatImageBudget();
|
|
284
|
+
for (const message of messages)
|
|
285
|
+
retainImages(message.content);
|
|
239
286
|
const model = turn.model || options.models().default;
|
|
240
|
-
const secrets = [options.apiKey];
|
|
287
|
+
const secrets = [options.apiKey, turn.integrations?.computer?.token, turn.integrations?.computer?.control?.token].filter((value) => Boolean(value));
|
|
241
288
|
for (const integration of Object.values(turn.integrations ?? {})) {
|
|
242
289
|
const entries = object(integration);
|
|
243
290
|
const specs = entries && "command" in entries ? [entries] : Object.values(entries ?? {}).map(object);
|
|
@@ -270,6 +317,18 @@ export function createOpenAIChatRuntime(options) {
|
|
|
270
317
|
...base(turn.threadId, turnId), type: "request.resolved", requestId: ask.id,
|
|
271
318
|
behavior: allowed ? "allow" : "deny", source,
|
|
272
319
|
}),
|
|
320
|
+
openQuestion: (ask, questions) => {
|
|
321
|
+
const choices = questionChoices(questions);
|
|
322
|
+
emit({
|
|
323
|
+
...base(turn.threadId, turnId), type: "request.opened", requestType: "question",
|
|
324
|
+
requestId: ask.id, tool: ask.tool, summary: ask.summary,
|
|
325
|
+
questions, ...(choices ? { choices } : {}),
|
|
326
|
+
});
|
|
327
|
+
},
|
|
328
|
+
resolvedQuestion: (ask, answered, source) => emit({
|
|
329
|
+
...base(turn.threadId, turnId), type: "request.resolved", requestId: ask.id,
|
|
330
|
+
behavior: answered ? "answer" : "deny", source,
|
|
331
|
+
}),
|
|
273
332
|
});
|
|
274
333
|
let resolveDone;
|
|
275
334
|
const done = new Promise((resolve) => { resolveDone = resolve; });
|
|
@@ -287,7 +346,16 @@ export function createOpenAIChatRuntime(options) {
|
|
|
287
346
|
const denials = [];
|
|
288
347
|
const seenCalls = new Set();
|
|
289
348
|
try {
|
|
290
|
-
tools = await mountChatTools(options.tools === false ? undefined : turn.integrations, abort.signal);
|
|
349
|
+
tools = await mountChatTools(options.tools === false ? undefined : turn.integrations, abort.signal, options.computerUse);
|
|
350
|
+
let optionalQuestionOnly = options.tools !== false && tools.definitions.length === 0;
|
|
351
|
+
// The runtime's one built-in tool rides the same list: ask_user is
|
|
352
|
+
// how a chat-completions engine reaches a person. An MCP server that
|
|
353
|
+
// squats the name cannot shadow it — dispatch intercepts the name
|
|
354
|
+
// before validate — but the definition is then skipped so the list
|
|
355
|
+
// never advertises two.
|
|
356
|
+
if (options.tools !== false && !tools.definitions.some((definition) => definition.function.name === ASK_USER_TOOL)) {
|
|
357
|
+
tools.definitions.push(ASK_USER_TOOL_DEFINITION);
|
|
358
|
+
}
|
|
291
359
|
for (let round = 0; round < 16; round++) {
|
|
292
360
|
abort.signal.throwIfAborted();
|
|
293
361
|
native("out", options.nativeLog.outgoing(turn, messages, model));
|
|
@@ -332,6 +400,15 @@ export function createOpenAIChatRuntime(options) {
|
|
|
332
400
|
catch (value) {
|
|
333
401
|
const error = asError(value);
|
|
334
402
|
const verdict = classifyError(error);
|
|
403
|
+
// Only our optional question changed a previously plain request.
|
|
404
|
+
// A structured parameter rejection has executed nothing; never
|
|
405
|
+
// downgrade mounted tools, streamed output or a handled call.
|
|
406
|
+
if (optionalQuestionOnly && round === 0 && !streamed && !seenCalls.size && !abort.signal.aborted &&
|
|
407
|
+
error instanceof UnsupportedChatToolsError && verdict.reason === "invalid_request") {
|
|
408
|
+
optionalQuestionOnly = false;
|
|
409
|
+
tools.definitions.length = 0;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
335
412
|
// Once a call has been handled, never replay it through a turn retry.
|
|
336
413
|
if (options.retryScale === undefined || abort.signal.aborted || streamed || seenCalls.size ||
|
|
337
414
|
error instanceof ChatProtocolError || !verdict.transient || attempt >= RETRY_MAX_ATTEMPTS - 1)
|
|
@@ -381,6 +458,7 @@ export function createOpenAIChatRuntime(options) {
|
|
|
381
458
|
...(completion.protocolReasoning ? { reasoning_content: completion.protocolReasoning } : {}),
|
|
382
459
|
...(completion.protocolReasoningDetails.length ? { reasoning_details: completion.protocolReasoningDetails } : {}),
|
|
383
460
|
});
|
|
461
|
+
const screenshotParts = [];
|
|
384
462
|
for (const call of completion.toolCalls) {
|
|
385
463
|
abort.signal.throwIfAborted();
|
|
386
464
|
let result;
|
|
@@ -396,26 +474,66 @@ export function createOpenAIChatRuntime(options) {
|
|
|
396
474
|
}
|
|
397
475
|
if (!object(args))
|
|
398
476
|
throw new ChatProtocolError("tool arguments must be a JSON object");
|
|
399
|
-
tools.validate(call.function.name, args);
|
|
400
477
|
const inputPreview = preview(args);
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
478
|
+
if (call.function.name === ASK_USER_TOOL) {
|
|
479
|
+
// A question is the person's card, not a permission, so it is
|
|
480
|
+
// handled before the gate below: under Full access that gate
|
|
481
|
+
// would auto-run an unanswered ask, and under Ask it would
|
|
482
|
+
// render Allow/Deny over a question nobody can answer that way.
|
|
483
|
+
const questions = parseAskQuestions(args);
|
|
484
|
+
abort.signal.throwIfAborted();
|
|
485
|
+
emit({ ...base(turn.threadId, turnId), type: "item.started", itemType: "tool", itemId: call.id,
|
|
486
|
+
title: call.function.name, ...(inputPreview ? { input: inputPreview } : {}),
|
|
487
|
+
});
|
|
488
|
+
started = true;
|
|
489
|
+
if (!questions) {
|
|
490
|
+
denials.push(ASK_USER_TOOL);
|
|
491
|
+
result = { ok: false, text: "The ask_user arguments are malformed: pass a JSON object with a questions array of one to six questions, each with a question string and at most twelve options carrying a label. Ask again with valid arguments." };
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
// The tool result is the card's Q:/A: reply verbatim — never
|
|
495
|
+
// a summary, and never words the person did not send.
|
|
496
|
+
const answer = await approval.question(ASK_USER_TOOL, askQuestionSummary(questions), questions);
|
|
497
|
+
abort.signal.throwIfAborted();
|
|
498
|
+
if (answer === null) {
|
|
499
|
+
denials.push(ASK_USER_TOOL);
|
|
500
|
+
result = { ok: false, text: "The person did not answer this question. Do not guess an answer; ask again later or proceed without it." };
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
result = { ok: true, text: answer };
|
|
504
|
+
}
|
|
505
|
+
}
|
|
415
506
|
}
|
|
416
507
|
else {
|
|
417
|
-
|
|
418
|
-
|
|
508
|
+
tools.validate(call.function.name, args);
|
|
509
|
+
// Full access is the person's explicit grant to answer every
|
|
510
|
+
// prompt. This runtime has no provider reviewer to hand it to,
|
|
511
|
+
// so it is honoured here: without it every single tool call on
|
|
512
|
+
// an OpenAI-compatible engine stops for a card, and a Chief's
|
|
513
|
+
// delegated Full access cannot help either.
|
|
514
|
+
const allowed = turn.approvalMode === "full"
|
|
515
|
+
|| await approval.ask(call.function.name, inputPreview ?? "This tool has no arguments.");
|
|
516
|
+
abort.signal.throwIfAborted();
|
|
517
|
+
emit({ ...base(turn.threadId, turnId), type: "item.started", itemType: "tool", itemId: call.id,
|
|
518
|
+
title: call.function.name, ...(inputPreview ? { input: inputPreview } : {}),
|
|
519
|
+
});
|
|
520
|
+
started = true;
|
|
521
|
+
if (allowed) {
|
|
522
|
+
result = await tools.execute(call.function.name, args, abort.signal);
|
|
523
|
+
if (result.images?.length) {
|
|
524
|
+
try {
|
|
525
|
+
assertImageTransport(options.apiUrl);
|
|
526
|
+
retainImages(result.images);
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
throw new ChatToolSessionError(asError(error).message);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
denials.push(call.function.name);
|
|
535
|
+
result = { ok: false, text: "Permission denied or expired; the tool was not executed." };
|
|
536
|
+
}
|
|
419
537
|
}
|
|
420
538
|
}
|
|
421
539
|
catch (error) {
|
|
@@ -433,10 +551,13 @@ export function createOpenAIChatRuntime(options) {
|
|
|
433
551
|
if (!result.ok)
|
|
434
552
|
toolFailed = true;
|
|
435
553
|
messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify({ ok: result.ok, result: text }) });
|
|
554
|
+
screenshotParts.push(...chatToolImages(call.id, result.images));
|
|
436
555
|
abort.signal.throwIfAborted();
|
|
437
556
|
if (fatal)
|
|
438
557
|
throw fatal;
|
|
439
558
|
}
|
|
559
|
+
if (screenshotParts.length)
|
|
560
|
+
messages.push({ role: "user", content: screenshotParts });
|
|
440
561
|
}
|
|
441
562
|
if (!ok)
|
|
442
563
|
throw new ChatProtocolError("model-call limit reached before a final response");
|
|
@@ -488,7 +609,9 @@ export function createOpenAIChatRuntime(options) {
|
|
|
488
609
|
: { state: "unavailable", reason: options.unavailableReason },
|
|
489
610
|
adapter: {
|
|
490
611
|
provider: options.driverKind,
|
|
491
|
-
capabilities: {
|
|
612
|
+
capabilities: { ...(options.computerUse ? { computerMcp: options.tools !== false, cloudComputerMcp: options.tools !== false, localComputerMcp: options.tools !== false,
|
|
613
|
+
browserMcp: options.tools !== false, nativeImageInput: true, images: true } : {}),
|
|
614
|
+
sessionModelSwitch: "in-session", customMcp: options.tools !== false, agentsMcp: options.tools !== false, composioMcp: options.tools !== false },
|
|
492
615
|
sendTurn,
|
|
493
616
|
interruptTurn: async (threadId, turnId) => {
|
|
494
617
|
const turn = active.get(threadId);
|
|
@@ -497,7 +620,7 @@ export function createOpenAIChatRuntime(options) {
|
|
|
497
620
|
turn.abort.abort();
|
|
498
621
|
await turn.done;
|
|
499
622
|
},
|
|
500
|
-
respondToRequest: async (threadId, requestId, decision) => active.get(threadId)?.approval.answer(requestId, decision.behavior) ?? "unavailable",
|
|
623
|
+
respondToRequest: async (threadId, requestId, decision) => active.get(threadId)?.approval.answer(requestId, decision.behavior, decision.message) ?? "unavailable",
|
|
501
624
|
hasSession: (threadId) => active.has(threadId),
|
|
502
625
|
stopAll: async () => {
|
|
503
626
|
const turns = [...active.values()];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
/** Approval identity preserves every command byte, including whitespace and
|
|
4
|
+
* newlines. Callers must supply native shell input, never a display preview. */
|
|
5
|
+
export function permissionCommand(command, cwd) {
|
|
6
|
+
if (typeof command !== "string" || !command.trim() || command.includes("\0"))
|
|
7
|
+
return undefined;
|
|
8
|
+
if (typeof cwd !== "string" || !isAbsolute(cwd) || cwd.includes("\0"))
|
|
9
|
+
return undefined;
|
|
10
|
+
return { command, cwd };
|
|
11
|
+
}
|
|
12
|
+
export function permissionLaunchCwd(cwd) {
|
|
13
|
+
try {
|
|
14
|
+
return realpathSync(cwd);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** ACP's rawInput is provider-specific. Accept a complete command string and
|
|
21
|
+
* unambiguous absolute directory fields. A relative/invalid/conflicting
|
|
22
|
+
* directory is not evidence of where the agent will execute it. */
|
|
23
|
+
export function acpPermissionCommand(rawInput, launchCwd) {
|
|
24
|
+
if (!rawInput || typeof rawInput !== "object" || Array.isArray(rawInput))
|
|
25
|
+
return undefined;
|
|
26
|
+
const input = rawInput;
|
|
27
|
+
const directories = ["cwd", "workdir", "working_directory", "directory", "dir"]
|
|
28
|
+
.filter((key) => input[key] !== undefined)
|
|
29
|
+
.map((key) => input[key]);
|
|
30
|
+
if (directories.some((value) => typeof value !== "string" || !isAbsolute(value) || value.includes("\0")))
|
|
31
|
+
return undefined;
|
|
32
|
+
if (new Set(directories).size > 1)
|
|
33
|
+
return undefined;
|
|
34
|
+
return permissionCommand(input.command, directories[0] ?? launchCwd);
|
|
35
|
+
}
|
|
@@ -6,6 +6,10 @@ import { createInterface } from "node:readline";
|
|
|
6
6
|
import { delimiter, join } from "node:path";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
const MAX_ADB_OUTPUT = 16 * 1024 * 1024;
|
|
9
|
+
// Mirrors the shared-computer blockedReason shape: name the holder's
|
|
10
|
+
// situation, say the call did not happen, and tell the model what beats a
|
|
11
|
+
// blind retry once the phone frees up.
|
|
12
|
+
const PHONE_BUSY_TEXT = "Another thread is using the phone. This call was not performed. Pause phone work until that thread finishes, then read the screen again before acting.";
|
|
9
13
|
const PACKAGE = /^[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+$/;
|
|
10
14
|
const COMPONENT = /^([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+)\/\S+$/;
|
|
11
15
|
const SAFE_TEXT = /^[A-Za-z0-9 _.,@-]+$/;
|
|
@@ -240,6 +244,43 @@ const TOOLS = [
|
|
|
240
244
|
{ name: "press", description: "Press an Android navigation or keyboard key.", inputSchema: { type: "object", properties: { key: { type: "string", enum: Object.keys(KEYCODES) }, serial: { type: "string" } }, required: ["key"] } },
|
|
241
245
|
];
|
|
242
246
|
const textResult = (text, isError = false) => ({ content: [{ type: "text", text }], ...(isError ? { isError: true } : {}) });
|
|
247
|
+
// The harness reserves the physical phone for one turn at a time, but only
|
|
248
|
+
// from the first real tool call (issue #1663): trigger-term matching decides
|
|
249
|
+
// whether these tools are mounted, never who holds the device. Revalidate
|
|
250
|
+
// every call: retained MCP processes can outlive their turn, whose claim and
|
|
251
|
+
// capability are released at settlement. Reclaiming for the same live owner
|
|
252
|
+
// is idempotent; a blocked caller can retry once the holding turn settles.
|
|
253
|
+
export function createPhoneClaim(env = process.env, fetchImpl = fetch) {
|
|
254
|
+
const url = env.OMB_HARNESS_URL?.trim();
|
|
255
|
+
const token = env.OMB_PHONE_TOKEN?.trim();
|
|
256
|
+
if (!url || !token) {
|
|
257
|
+
// Spawned outside the harness (development, direct debugging): no
|
|
258
|
+
// exclusivity to enforce, exactly the behavior this proxy always had.
|
|
259
|
+
return async () => ({ ok: true });
|
|
260
|
+
}
|
|
261
|
+
return async () => {
|
|
262
|
+
try {
|
|
263
|
+
const response = await fetchImpl(new URL("/api/internal/phone/claim", url), {
|
|
264
|
+
method: "POST",
|
|
265
|
+
redirect: "error",
|
|
266
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
267
|
+
body: "{}",
|
|
268
|
+
signal: AbortSignal.timeout(10_000),
|
|
269
|
+
});
|
|
270
|
+
if (response.ok)
|
|
271
|
+
return { ok: true };
|
|
272
|
+
if (response.status === 409)
|
|
273
|
+
return { ok: false, message: PHONE_BUSY_TEXT };
|
|
274
|
+
if (response.status === 401 || response.status === 403) {
|
|
275
|
+
return { ok: false, message: "This turn no longer has phone access. Start a new turn to use the phone." };
|
|
276
|
+
}
|
|
277
|
+
return { ok: false, message: "OpenMausBot could not reserve the phone for this call. This call was not performed; try again, or start a new turn if it keeps failing." };
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return { ok: false, message: "OpenMausBot could not be reached to reserve the phone. This call was not performed; try again." };
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
243
284
|
async function callTool(name, args) {
|
|
244
285
|
if (name === "status") {
|
|
245
286
|
const adb = resolveAdbPath();
|
|
@@ -334,7 +375,7 @@ async function callTool(name, args) {
|
|
|
334
375
|
const send = (message) => process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
335
376
|
const ok = (id, result) => send({ jsonrpc: "2.0", id, result });
|
|
336
377
|
const rpcError = (id, code, message) => send({ jsonrpc: "2.0", id, error: { code, message } });
|
|
337
|
-
async function handle(message) {
|
|
378
|
+
async function handle(message, ensureClaim) {
|
|
338
379
|
const id = message.id;
|
|
339
380
|
const method = message.method;
|
|
340
381
|
const params = (message.params ?? {});
|
|
@@ -350,6 +391,9 @@ async function handle(message) {
|
|
|
350
391
|
const name = String(params.name ?? "");
|
|
351
392
|
if (!TOOLS.some((tool) => tool.name === name))
|
|
352
393
|
return rpcError(id, -32602, `Unknown tool: ${name}`);
|
|
394
|
+
const claim = await ensureClaim();
|
|
395
|
+
if (!claim.ok)
|
|
396
|
+
return ok(id, textResult(claim.message, true));
|
|
353
397
|
try {
|
|
354
398
|
return ok(id, await callTool(name, (params.arguments ?? {})));
|
|
355
399
|
}
|
|
@@ -361,11 +405,12 @@ async function handle(message) {
|
|
|
361
405
|
rpcError(id, -32601, `Method not found: ${String(method)}`);
|
|
362
406
|
}
|
|
363
407
|
if (process.argv[1] && existsSync(process.argv[1]) && /phone-proxy\.(?:ts|js)$/.test(process.argv[1])) {
|
|
408
|
+
const ensureClaim = createPhoneClaim();
|
|
364
409
|
const lines = createInterface({ input: process.stdin, terminal: false });
|
|
365
410
|
lines.on("line", (line) => {
|
|
366
411
|
try {
|
|
367
412
|
const message = JSON.parse(line);
|
|
368
|
-
void handle(message).catch((error) => rpcError(message.id, -32603, error instanceof Error ? error.message : String(error)));
|
|
413
|
+
void handle(message, ensureClaim).catch((error) => rpcError(message.id, -32603, error instanceof Error ? error.message : String(error)));
|
|
369
414
|
}
|
|
370
415
|
catch { }
|
|
371
416
|
});
|
|
@@ -30,6 +30,7 @@ import { SPAWNED_PROXIES } from "../proxy-paths.js";
|
|
|
30
30
|
import { commandSummary, toolDetailPreview } from "../tool-summary.js";
|
|
31
31
|
import { EFFORT_LEVELS } from "../../shared/wire.js";
|
|
32
32
|
import { newEventId, newId } from "../contracts.js";
|
|
33
|
+
import { parseAskQuestions, parseChoices, questionAnswersByQuestion } from "../../shared/ask-question.js";
|
|
33
34
|
import { decodeInjectId, encodeInjectId, hostApiKey, localHost, mergeLocalInject, } from "./local-inject.js";
|
|
34
35
|
import { appendNative } from "./native.js";
|
|
35
36
|
const DRIVER_KIND = "piAgent";
|
|
@@ -425,6 +426,10 @@ export const PiDriver = {
|
|
|
425
426
|
}
|
|
426
427
|
const turnId = newId();
|
|
427
428
|
const pending = new Map();
|
|
429
|
+
// Ask fail-safe timers, tracked so settle() can cancel them outright:
|
|
430
|
+
// a cleared pending map alone leaves each timer holding the ask
|
|
431
|
+
// closure (send, child) alive until it fires.
|
|
432
|
+
const askTimers = new Set();
|
|
428
433
|
let settled = false;
|
|
429
434
|
// pi's RPC surface accepts image content directly. Read before spawning
|
|
430
435
|
// so an attachment that disappeared produces one clear dispatch error
|
|
@@ -522,6 +527,13 @@ export const PiDriver = {
|
|
|
522
527
|
if (settled)
|
|
523
528
|
return;
|
|
524
529
|
settled = true;
|
|
530
|
+
// The turn is over: drop unanswered asks so their 15-minute
|
|
531
|
+
// fail-safe timers are cancelled outright instead of no-oping on a
|
|
532
|
+
// dead child while holding the ask closure alive.
|
|
533
|
+
for (const timer of askTimers)
|
|
534
|
+
clearTimeout(timer);
|
|
535
|
+
askTimers.clear();
|
|
536
|
+
pending.clear();
|
|
525
537
|
flushAssistantText();
|
|
526
538
|
emit({
|
|
527
539
|
...base(threadId, turnId),
|
|
@@ -626,26 +638,79 @@ export const PiDriver = {
|
|
|
626
638
|
if (evt.method === "select" || evt.method === "confirm" || evt.method === "input") {
|
|
627
639
|
flushAssistantText();
|
|
628
640
|
const reqId = evt.id ?? newId();
|
|
629
|
-
const
|
|
641
|
+
const isSelect = evt.method === "select";
|
|
642
|
+
const isQuestion = isSelect || evt.method === "input";
|
|
643
|
+
const selectOptions = isSelect && Array.isArray(evt.options)
|
|
644
|
+
? evt.options.filter((option) => typeof option === "string") : [];
|
|
645
|
+
const summary = String(evt.title ?? (isQuestion ? "pi has a question" : "pi wants confirmation")).slice(0, 200);
|
|
646
|
+
// A select is a question with named options; the structured
|
|
647
|
+
// card renders from it while the flat choices keep older
|
|
648
|
+
// clients answering. An input has nothing to pick from and
|
|
649
|
+
// stays the free-text question it always was.
|
|
650
|
+
const question = isSelect
|
|
651
|
+
? (parseAskQuestions({
|
|
652
|
+
questions: [{ question: summary, options: selectOptions }],
|
|
653
|
+
}) ?? [])[0]
|
|
654
|
+
: undefined;
|
|
655
|
+
const choices = question?.options.length ? question.options.map((option) => option.label) : undefined;
|
|
656
|
+
let timer;
|
|
630
657
|
// Register BEFORE emitting: the harness may auto-approve from
|
|
631
658
|
// inside its synchronous request.opened listener. Emitting first
|
|
632
659
|
// made respondToRequest see no pending ask, return unavailable,
|
|
633
660
|
// then fall back to a human card on every "Always allow" call.
|
|
634
661
|
pending.set(reqId, (decision) => {
|
|
662
|
+
if (timer) {
|
|
663
|
+
clearTimeout(timer);
|
|
664
|
+
askTimers.delete(timer);
|
|
665
|
+
}
|
|
635
666
|
if (decision.behavior === "deny")
|
|
636
667
|
send({ type: "extension_ui_response", id: reqId, cancelled: true });
|
|
637
|
-
else if (isQuestion)
|
|
638
|
-
|
|
668
|
+
else if (isQuestion) {
|
|
669
|
+
// Recover the picked label from a structured card's Q:/A:
|
|
670
|
+
// reply; a flat answer or typed text passes through
|
|
671
|
+
// verbatim (questionAnswersByQuestion's single-question
|
|
672
|
+
// fallback does exactly that).
|
|
673
|
+
const value = question
|
|
674
|
+
? questionAnswersByQuestion(decision.message ?? "", [question])[question.question] ?? decision.message ?? ""
|
|
675
|
+
: decision.message ?? "";
|
|
676
|
+
// Display labels are capped/trimmed; pi expects the
|
|
677
|
+
// original option. Never guess if two normalize alike.
|
|
678
|
+
const matched = selectOptions.filter(option => parseChoices([option], 1)?.[0] === value);
|
|
679
|
+
send(matched.length > 1
|
|
680
|
+
? { type: "extension_ui_response", id: reqId, cancelled: true }
|
|
681
|
+
: { type: "extension_ui_response", id: reqId, value: matched[0] ?? value });
|
|
682
|
+
}
|
|
639
683
|
else
|
|
640
684
|
send({ type: "extension_ui_response", id: reqId, confirmed: true });
|
|
641
685
|
});
|
|
686
|
+
// The ask must never hold the turn forever: cancel it after 15
|
|
687
|
+
// minutes. The pending.delete guard makes this a no-op once the
|
|
688
|
+
// turn settled (settle clears pending) or a person answered.
|
|
689
|
+
timer = setTimeout(() => {
|
|
690
|
+
if (timer)
|
|
691
|
+
askTimers.delete(timer);
|
|
692
|
+
if (!pending.delete(reqId))
|
|
693
|
+
return;
|
|
694
|
+
send({ type: "extension_ui_response", id: reqId, cancelled: true });
|
|
695
|
+
emit({
|
|
696
|
+
...base(threadId, turnId),
|
|
697
|
+
requestId: reqId,
|
|
698
|
+
type: "request.resolved",
|
|
699
|
+
behavior: "deny",
|
|
700
|
+
source: "timeout",
|
|
701
|
+
});
|
|
702
|
+
}, 15 * 60_000);
|
|
703
|
+
askTimers.add(timer);
|
|
704
|
+
timer.unref?.();
|
|
642
705
|
emit({
|
|
643
706
|
...base(threadId, turnId),
|
|
644
707
|
requestId: reqId,
|
|
645
708
|
type: "request.opened",
|
|
646
709
|
requestType: isQuestion ? "question" : "permission",
|
|
647
710
|
tool: String(evt.title ?? "pi"),
|
|
648
|
-
summary
|
|
711
|
+
summary,
|
|
712
|
+
...(choices ? { choices } : {}),
|
|
713
|
+
...(question ? { questions: [question] } : {}),
|
|
649
714
|
});
|
|
650
715
|
}
|
|
651
716
|
return;
|
|
@@ -751,6 +816,12 @@ export const PiDriver = {
|
|
|
751
816
|
/* keep going on the engine default */
|
|
752
817
|
}
|
|
753
818
|
}
|
|
819
|
+
// pi compacts long sessions by summarizing older user messages, and
|
|
820
|
+
// this driver delivers the prompt as the leading user message: a
|
|
821
|
+
// receipt-based split would let a compacted session keep running
|
|
822
|
+
// bare, without its standing instructions. Re-deliver the full prompt
|
|
823
|
+
// every turn until pi exposes a compaction signal the harness can
|
|
824
|
+
// watch (its extension API has session_before_compact).
|
|
754
825
|
const message = turn.system ? `${turn.system}\n\n${turn.text}` : turn.text;
|
|
755
826
|
try {
|
|
756
827
|
send({ type: "prompt", message, ...(images.length ? { images } : {}) });
|