openmausbot 0.1.86 → 0.1.88
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 +4 -3
- package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-qd9VD3GV.js} +1 -1
- package/dist/assets/{arc-DW7mrI4p.js → arc-3RwkUjRe.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-CFKSJS7a.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CG1rEfWD.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-CBW9N664.js} +1 -1
- package/dist/assets/channel-CnRnO3tF.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-2mRfTksB.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-Cv1dpErj.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-B7IACJp-.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-An0hZtYa.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-BcJB2nEm.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-CV_W7TN7.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Bcs_kwnA.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-lrx_DxFP.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-DabHlLGM.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BnHj6P7d.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-CfLp6UDi.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Dm4dbVA1.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-B9mH-PID.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-DDc_NVQJ.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-lwr8Ip8-.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-BLLe6dmI.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-6eUIq4lz.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-O-pRN7VQ.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-BietvGAI.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-Bd2Cl62X.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-B6iDy69W.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-DQVHfKe0.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BWdVp2tQ.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-WLE6rKB4.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-BbjS-4gn.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-Dpy2QSGq.js} +1 -1
- package/dist/assets/index-BIMdIbaw.css +1 -0
- package/dist/assets/index-CHnODCr2.js +583 -0
- package/dist/assets/{index-h9FFNwTu.js → index-CIaHn5NS.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-D_jKWlKe.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-DLJohSuK.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DPOFj0qN.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-BI7v-xpK.js} +1 -1
- package/dist/assets/{linear-3su2TG93.js → linear-C2pWPSxO.js} +1 -1
- package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-CR-j5WQU.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-lz9GvXRh.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-Dl4_ia61.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-DWuT3n88.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-DXWe9Jx7.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-COY1IBVY.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-DnyVuhsQ.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-BSc4NdGK.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-kqEU2EGq.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-BvB4bkWD.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BIrqNv7y.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-CFnalQUE.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C4a8qNGj.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-CsSWJnSQ.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-DkAmJ1EE.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-C3vd9drL.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-j26CM-pi.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-BYu1Q_at.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/browser-proxy.js +2 -1
- package/dist-server/companion/src/routes.js +8 -1
- package/dist-server/connector-proxy.js +105 -1
- package/dist-server/container-mcp.js +1032 -165
- package/dist-server/drivers/agents-proxy.js +338 -29
- package/dist-server/drivers/phone-proxy.js +33 -2
- package/dist-server/index.js +40590 -31811
- package/dist-server/local-computer-proxy.js +5 -3
- package/dist-server/local-computer.js +53 -17
- package/dist-server/mcp-gate.js +2 -0
- package/dist-server/mcp-server.js +5 -3
- package/dist-server/message-search.worker.js +52 -0
- package/dist-server/openmausbot.js +4457 -1399
- package/dist-server/pair-cli.js +4457 -1399
- package/dist-server/permission-proxy.js +29 -9
- package/dist-server/server/admission.js +81 -0
- package/dist-server/server/aside-queue.js +294 -0
- package/dist-server/server/attachments.js +98 -1
- package/dist-server/server/auto-approve.js +4 -3
- package/dist-server/server/auto-vm-claims.js +1 -0
- package/dist-server/server/bot-attachment.js +107 -0
- package/dist-server/server/bot-overview.js +19 -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-api-setup.js +1 -0
- package/dist-server/server/cli.js +2 -2
- package/dist-server/server/command-allowlist.js +154 -0
- package/dist-server/server/composio.js +297 -1
- package/dist-server/server/computer-wait.js +22 -9
- package/dist-server/server/config.js +84 -12
- package/dist-server/server/connector-advertisement.js +165 -0
- package/dist-server/server/connector-proxy.js +14 -2
- package/dist-server/server/connector-verdict.js +168 -0
- package/dist-server/server/container-computer.js +61 -1
- package/dist-server/server/contracts.js +22 -0
- 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 +80 -20
- package/dist-server/server/drivers/acp/cursor.js +3 -7
- package/dist-server/server/drivers/acp/droid.js +4 -7
- package/dist-server/server/drivers/acp/gemini.js +4 -4
- package/dist-server/server/drivers/acp/grok.js +4 -9
- package/dist-server/server/drivers/acp/hermes.js +3 -5
- package/dist-server/server/drivers/acp/opencode-go.js +2 -5
- package/dist-server/server/drivers/acp/qwen.js +9 -15
- package/dist-server/server/drivers/agents-call.js +179 -13
- package/dist-server/server/drivers/agents-catalog.js +134 -10
- package/dist-server/server/drivers/boxagent.js +160 -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 +38 -19
- package/dist-server/server/drivers/codex-catalog.js +14 -6
- package/dist-server/server/drivers/codex-identity.js +2 -1
- package/dist-server/server/drivers/codex-instructions.js +48 -6
- package/dist-server/server/drivers/codex-release.js +48 -0
- package/dist-server/server/drivers/codex-tool-surface.js +20 -0
- package/dist-server/server/drivers/codex.js +102 -55
- package/dist-server/server/drivers/mistral.js +115 -0
- package/dist-server/server/drivers/openai-chat.js +180 -33
- 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 +306 -28
- package/dist-server/server/drivers/prompt-split.js +110 -0
- package/dist-server/server/drivers/session-idle.js +29 -0
- package/dist-server/server/env-path.js +15 -0
- package/dist-server/server/fleet.js +1 -1
- package/dist-server/server/group-thread-usage.js +103 -0
- package/dist-server/server/guarded-requests.js +2 -2
- package/dist-server/server/harness/registry.js +1 -0
- package/dist-server/server/harness-capabilities.js +46 -0
- package/dist-server/server/hosted-models.js +1 -1
- package/dist-server/server/index.js +2396 -505
- 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/message-db.js +22 -42
- package/dist-server/server/message-file.js +9 -1
- package/dist-server/server/message-search-query.js +43 -0
- package/dist-server/server/message-search-worker.js +80 -0
- package/dist-server/server/message-search.worker.js +21 -0
- package/dist-server/server/model-requests.js +248 -0
- package/dist-server/server/new-bot-defaults.js +115 -0
- package/dist-server/server/options-card.js +21 -0
- package/dist-server/server/org-library.js +1065 -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-delivery.js +60 -0
- package/dist-server/server/peer-roster.js +67 -0
- package/dist-server/server/presets.js +492 -0
- package/dist-server/server/profile-requests.js +66 -39
- package/dist-server/server/profile-revision.js +9 -1
- package/dist-server/server/profile-versions.js +6 -3
- 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 +35 -10
- package/dist-server/server/routines.js +41 -1
- package/dist-server/server/section-context.js +10 -0
- package/dist-server/server/skill-fetch.js +72 -14
- package/dist-server/server/skills.js +35 -61
- package/dist-server/server/steer-queue.js +3 -2
- package/dist-server/server/store.js +291 -17
- package/dist-server/server/surface.js +10 -4
- package/dist-server/server/system-prompt.js +38 -6
- 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 +139 -27
- package/dist-server/server/thread-events.js +20 -2
- package/dist-server/server/tightening-requests.js +405 -0
- package/dist-server/server/tts/chatterbox.js +27 -5
- 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 +19 -2
- 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 +42 -5
- package/dist-server/server/workspace-backup.worker.js +10 -0
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/ask-question.js +207 -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 +6 -2
- package/dist-server/shared/line-diff.js +23 -0
- package/dist-server/shared/model-request.js +1 -0
- package/dist-server/shared/options-card.js +51 -0
- package/dist-server/shared/package-format.js +967 -0
- package/dist-server/shared/profile-request.js +5 -4
- package/dist-server/shared/skill-md.js +64 -0
- package/dist-server/shared/team-backup.js +7 -0
- package/dist-server/shared/tightening-request.js +87 -0
- package/dist-server/shared/wire.js +4 -0
- package/dist-server/src/lib/composer-attachments.js +25 -3
- package/dist-server/vps-container-mcp.js +1132 -171
- package/dist-server/workspace-backup.worker.js +19474 -0
- 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,10 +317,23 @@ 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; });
|
|
276
|
-
|
|
335
|
+
const turnEntry = { abort, turnId, done, approval, asides: [] };
|
|
336
|
+
active.set(turn.threadId, turnEntry);
|
|
277
337
|
emit({ ...base(turn.threadId, turnId), type: "turn.started" });
|
|
278
338
|
emit({ ...base(turn.threadId, turnId), type: "session.started", sessionId: null, model });
|
|
279
339
|
void (async () => {
|
|
@@ -287,9 +347,25 @@ export function createOpenAIChatRuntime(options) {
|
|
|
287
347
|
const denials = [];
|
|
288
348
|
const seenCalls = new Set();
|
|
289
349
|
try {
|
|
290
|
-
tools = await mountChatTools(options.tools === false ? undefined : turn.integrations, abort.signal);
|
|
350
|
+
tools = await mountChatTools(options.tools === false ? undefined : turn.integrations, abort.signal, options.computerUse);
|
|
351
|
+
let optionalQuestionOnly = options.tools !== false && tools.definitions.length === 0;
|
|
352
|
+
// The runtime's one built-in tool rides the same list: ask_user is
|
|
353
|
+
// how a chat-completions engine reaches a person. An MCP server that
|
|
354
|
+
// squats the name cannot shadow it — dispatch intercepts the name
|
|
355
|
+
// before validate — but the definition is then skipped so the list
|
|
356
|
+
// never advertises two.
|
|
357
|
+
if (options.tools !== false && !tools.definitions.some((definition) => definition.function.name === ASK_USER_TOOL)) {
|
|
358
|
+
tools.definitions.push(ASK_USER_TOOL_DEFINITION);
|
|
359
|
+
}
|
|
291
360
|
for (let round = 0; round < 16; round++) {
|
|
292
361
|
abort.signal.throwIfAborted();
|
|
362
|
+
// Drain parked mid-turn input here, before the next completion
|
|
363
|
+
// request: the previous round's tool results are complete, so a
|
|
364
|
+
// user message lands on a consistent array (never inside a tool
|
|
365
|
+
// batch) and the provider sees it as the newest input.
|
|
366
|
+
const parked = turnEntry.asides.splice(0);
|
|
367
|
+
if (parked.length)
|
|
368
|
+
messages.push({ role: "user", content: parked.join("\n\n") });
|
|
293
369
|
native("out", options.nativeLog.outgoing(turn, messages, model));
|
|
294
370
|
let attempt = 0;
|
|
295
371
|
let completion;
|
|
@@ -332,6 +408,15 @@ export function createOpenAIChatRuntime(options) {
|
|
|
332
408
|
catch (value) {
|
|
333
409
|
const error = asError(value);
|
|
334
410
|
const verdict = classifyError(error);
|
|
411
|
+
// Only our optional question changed a previously plain request.
|
|
412
|
+
// A structured parameter rejection has executed nothing; never
|
|
413
|
+
// downgrade mounted tools, streamed output or a handled call.
|
|
414
|
+
if (optionalQuestionOnly && round === 0 && !streamed && !seenCalls.size && !abort.signal.aborted &&
|
|
415
|
+
error instanceof UnsupportedChatToolsError && verdict.reason === "invalid_request") {
|
|
416
|
+
optionalQuestionOnly = false;
|
|
417
|
+
tools.definitions.length = 0;
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
335
420
|
// Once a call has been handled, never replay it through a turn retry.
|
|
336
421
|
if (options.retryScale === undefined || abort.signal.aborted || streamed || seenCalls.size ||
|
|
337
422
|
error instanceof ChatProtocolError || !verdict.transient || attempt >= RETRY_MAX_ATTEMPTS - 1)
|
|
@@ -381,6 +466,7 @@ export function createOpenAIChatRuntime(options) {
|
|
|
381
466
|
...(completion.protocolReasoning ? { reasoning_content: completion.protocolReasoning } : {}),
|
|
382
467
|
...(completion.protocolReasoningDetails.length ? { reasoning_details: completion.protocolReasoningDetails } : {}),
|
|
383
468
|
});
|
|
469
|
+
const screenshotParts = [];
|
|
384
470
|
for (const call of completion.toolCalls) {
|
|
385
471
|
abort.signal.throwIfAborted();
|
|
386
472
|
let result;
|
|
@@ -396,26 +482,66 @@ export function createOpenAIChatRuntime(options) {
|
|
|
396
482
|
}
|
|
397
483
|
if (!object(args))
|
|
398
484
|
throw new ChatProtocolError("tool arguments must be a JSON object");
|
|
399
|
-
tools.validate(call.function.name, args);
|
|
400
485
|
const inputPreview = preview(args);
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
486
|
+
if (call.function.name === ASK_USER_TOOL) {
|
|
487
|
+
// A question is the person's card, not a permission, so it is
|
|
488
|
+
// handled before the gate below: under Full access that gate
|
|
489
|
+
// would auto-run an unanswered ask, and under Ask it would
|
|
490
|
+
// render Allow/Deny over a question nobody can answer that way.
|
|
491
|
+
const questions = parseAskQuestions(args);
|
|
492
|
+
abort.signal.throwIfAborted();
|
|
493
|
+
emit({ ...base(turn.threadId, turnId), type: "item.started", itemType: "tool", itemId: call.id,
|
|
494
|
+
title: call.function.name, ...(inputPreview ? { input: inputPreview } : {}),
|
|
495
|
+
});
|
|
496
|
+
started = true;
|
|
497
|
+
if (!questions) {
|
|
498
|
+
denials.push(ASK_USER_TOOL);
|
|
499
|
+
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." };
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
// The tool result is the card's Q:/A: reply verbatim — never
|
|
503
|
+
// a summary, and never words the person did not send.
|
|
504
|
+
const answer = await approval.question(ASK_USER_TOOL, askQuestionSummary(questions), questions);
|
|
505
|
+
abort.signal.throwIfAborted();
|
|
506
|
+
if (answer === null) {
|
|
507
|
+
denials.push(ASK_USER_TOOL);
|
|
508
|
+
result = { ok: false, text: "The person did not answer this question. Do not guess an answer; ask again later or proceed without it." };
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
result = { ok: true, text: answer };
|
|
512
|
+
}
|
|
513
|
+
}
|
|
415
514
|
}
|
|
416
515
|
else {
|
|
417
|
-
|
|
418
|
-
|
|
516
|
+
tools.validate(call.function.name, args);
|
|
517
|
+
// Full access is the person's explicit grant to answer every
|
|
518
|
+
// prompt. This runtime has no provider reviewer to hand it to,
|
|
519
|
+
// so it is honoured here: without it every single tool call on
|
|
520
|
+
// an OpenAI-compatible engine stops for a card, and a Chief's
|
|
521
|
+
// delegated Full access cannot help either.
|
|
522
|
+
const allowed = turn.approvalMode === "full"
|
|
523
|
+
|| await approval.ask(call.function.name, inputPreview ?? "This tool has no arguments.");
|
|
524
|
+
abort.signal.throwIfAborted();
|
|
525
|
+
emit({ ...base(turn.threadId, turnId), type: "item.started", itemType: "tool", itemId: call.id,
|
|
526
|
+
title: call.function.name, ...(inputPreview ? { input: inputPreview } : {}),
|
|
527
|
+
});
|
|
528
|
+
started = true;
|
|
529
|
+
if (allowed) {
|
|
530
|
+
result = await tools.execute(call.function.name, args, abort.signal);
|
|
531
|
+
if (result.images?.length) {
|
|
532
|
+
try {
|
|
533
|
+
assertImageTransport(options.apiUrl);
|
|
534
|
+
retainImages(result.images);
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
throw new ChatToolSessionError(asError(error).message);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
denials.push(call.function.name);
|
|
543
|
+
result = { ok: false, text: "Permission denied or expired; the tool was not executed." };
|
|
544
|
+
}
|
|
419
545
|
}
|
|
420
546
|
}
|
|
421
547
|
catch (error) {
|
|
@@ -433,10 +559,13 @@ export function createOpenAIChatRuntime(options) {
|
|
|
433
559
|
if (!result.ok)
|
|
434
560
|
toolFailed = true;
|
|
435
561
|
messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify({ ok: result.ok, result: text }) });
|
|
562
|
+
screenshotParts.push(...chatToolImages(call.id, result.images));
|
|
436
563
|
abort.signal.throwIfAborted();
|
|
437
564
|
if (fatal)
|
|
438
565
|
throw fatal;
|
|
439
566
|
}
|
|
567
|
+
if (screenshotParts.length)
|
|
568
|
+
messages.push({ role: "user", content: screenshotParts });
|
|
440
569
|
}
|
|
441
570
|
if (!ok)
|
|
442
571
|
throw new ChatProtocolError("model-call limit reached before a final response");
|
|
@@ -488,7 +617,12 @@ export function createOpenAIChatRuntime(options) {
|
|
|
488
617
|
: { state: "unavailable", reason: options.unavailableReason },
|
|
489
618
|
adapter: {
|
|
490
619
|
provider: options.driverKind,
|
|
491
|
-
capabilities: {
|
|
620
|
+
capabilities: { ...(options.computerUse ? { computerMcp: options.tools !== false, cloudComputerMcp: options.tools !== false, localComputerMcp: options.tools !== false,
|
|
621
|
+
browserMcp: options.tools !== false, nativeImageInput: true, images: true } : {}),
|
|
622
|
+
sessionModelSwitch: "in-session", customMcp: options.tools !== false, agentsMcp: options.tools !== false, composioMcp: options.tools !== false,
|
|
623
|
+
// The runtime owns the whole tool loop, so it can always take a
|
|
624
|
+
// user message mid-turn: park it, deliver before the next completion.
|
|
625
|
+
queueing: true },
|
|
492
626
|
sendTurn,
|
|
493
627
|
interruptTurn: async (threadId, turnId) => {
|
|
494
628
|
const turn = active.get(threadId);
|
|
@@ -497,7 +631,20 @@ export function createOpenAIChatRuntime(options) {
|
|
|
497
631
|
turn.abort.abort();
|
|
498
632
|
await turn.done;
|
|
499
633
|
},
|
|
500
|
-
respondToRequest: async (threadId, requestId, decision) => active.get(threadId)?.approval.answer(requestId, decision.behavior) ?? "unavailable",
|
|
634
|
+
respondToRequest: async (threadId, requestId, decision) => active.get(threadId)?.approval.answer(requestId, decision.behavior, decision.message) ?? "unavailable",
|
|
635
|
+
steer: async (threadId, text) => {
|
|
636
|
+
// This engine has no external session to respect — parking the
|
|
637
|
+
// words in the live turn's entry IS delivery into the loop, so a
|
|
638
|
+
// successful park is "steered" and a missing turn is the only
|
|
639
|
+
// refusal. The words ride the array at the next round boundary;
|
|
640
|
+
// if the turn settles first, the harness's transcript record
|
|
641
|
+
// keeps them for the next turn (they are never re-sent here).
|
|
642
|
+
const running = active.get(threadId);
|
|
643
|
+
if (!running)
|
|
644
|
+
return "refused";
|
|
645
|
+
running.asides.push(text);
|
|
646
|
+
return "steered";
|
|
647
|
+
},
|
|
501
648
|
hasSession: (threadId) => active.has(threadId),
|
|
502
649
|
stopAll: async () => {
|
|
503
650
|
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
|
});
|