openmausbot 0.1.96 → 0.1.97
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/dist/assets/{DesktopViewer-DsnA23KM.js → DesktopViewer-CPidMCtt.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-Dkbd2hcj.js → abnfDiagram-O67JEVCF-k1UCNXb7.js} +1 -1
- package/dist/assets/{arc-Duj5Coy2.js → arc-BUWGa9UU.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-uYUsDawP.js → architectureDiagram-NJMV4G6O-DiPor3UT.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CWDlTpXe.js → blockDiagram-BEXU5L5S-Ag9nLW4H.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-B0t15PDc.js → c4Diagram-YGBWAQC7-Bh0sjgUa.js} +1 -1
- package/dist/assets/channel-DR8eD6QW.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BJSCL-HV.js → chunk-2Q5K7J3B-BFBcIIqu.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-D3_ILccd.js → chunk-3FUC2YCW-Dvfpex-f.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-D3dmwT_S.js → chunk-5VM5RSS4-CMjP2YZW.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Br73HFPD.js → chunk-GTNCS2PH-BryoeTc7.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-uSsjxubC.js → chunk-GWA4HPMP-BhfvcchO.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-C40VWFyr.js → chunk-JWPE2WC7-bcCVp8rp.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-D6WNNYwU.js → chunk-NETBCI7D-yKIjIlkQ.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-DOEhq64P.js → chunk-WEXAMYUT-D3Zqbmup.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-BqJEi6Pi.js → chunk-XXDRQBXY-CjVoIMG6.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-ByU_GnxT.js → classDiagram-v2-NBCMYWYE-mUCWdVyH.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-D-Uglxqq.js → cose-bilkent-JH36ORCC-BBmR1bbX.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CZWO7aYj.js → cynefin-OW5HDTMX-BpRrxoO4.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-C70-w0la.js → cynefinDiagram-VND7K2PF-D3VBWCPK.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-CnhjhExB.js → dagre-6A5THRUB-DxEIACOO.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-bnjWCNb4.js → diagram-22UHCM2B-BANgZEOS.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-A4uTWoFV.js → diagram-3UASUU5V-Cnq7nJir.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Zi6m3NUl.js → diagram-ATOU4E4O-4TBDIWnz.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DtCcmd5k.js → diagram-CDSNMT55-CD_B39D4.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-DofuETS3.js → diagram-MLGK6HIB-CzvnFhEC.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-Dw2T9PuU.js → diagram-MPIPVDR6-C4w2SNml.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-DbrwPiem.js → ebnfDiagram-ZINNZB2B-gkUEBm2s.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-BvCt1dCb.js → elk-276RUBZZ-B_56X6mw.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-Y-QKqPnU.js → erDiagram-OPXOYQCR-C_oByoDS.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-CDkL1B5w.js → flowDiagram-KWPJA3E3-CR9xXJkC.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-CKV5D2F6.js → ganttDiagram-FUAMR5RP-gtYBTu0B.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-nL9iFTCv.js → gitGraphDiagram-X574FWY7-VJ6-Tp_p.js} +1 -1
- package/dist/assets/{index-DreDnjOu.js → index-Cp4S4NOL.js} +1 -1
- package/dist/assets/index-CpaIXRLp.js +614 -0
- package/dist/assets/index-Z81SMsgR.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-C7aHjzsf.js → infoDiagram-VRGFBTTK-2Lu9te4D.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-KUZsNBTh.js → ishikawaDiagram-OU5B5YK6-DR49XY-e.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-CGUuodFl.js → journeyDiagram-ZHPQQLJL-ZoR0M305.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-Cy6r868Z.js → kanban-definition-PNTS6WVX-DLKHxrO-.js} +1 -1
- package/dist/assets/{linear-Bl669148.js → linear-BASsfWly.js} +1 -1
- package/dist/assets/{mermaid.core-86Xqz5Mr.js → mermaid.core-BVidiPkk.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-G4DzHLrd.js → mindmap-definition-NLK3R4M7-g_Qx0U5C.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-K4eruzY0.js → pegDiagram-GJSIUBJH-DI9Cjs8w.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DYdR9PPq.js → pieDiagram-5QR66LMP-sRI0Uhr5.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-BCAh0nEy.js → quadrantDiagram-O4NWA36T-CamKTby3.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-p0dGRc4g.js → railroadDiagram-XR7U4H2S-DOLlkbf3.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-tLeBG0aL.js → requirementDiagram-PLB6GJNP-DrRQ5r97.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-QRuN16rV.js → sankeyDiagram-IPEJSGJF-DGS_lXcX.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-ueT1nVoa.js → sequenceDiagram-PO4LG4MO-CkjWqwtf.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-wRS2zWg2.js → sizeCapture-INFHLROL-CepRgful.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-CYzA_hE1.js → stateDiagram-v2-GCMORJYK-hykr-9RS.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-kDnv6LMc.js → swimlanes-2SLR337P-CKOHEZbI.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D-8vqZ4-.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-D1y2UbbH.js → timeline-definition-EJHVYXUP-CB8pJ6AC.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-IqB4v9HX.js → usecaseDiagram-POWQR4AR-DrclJHdl.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-BEi7KYVF.js → vennDiagram-UO4OBE2U-zS23qjv8.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-Dkybo-Yf.js → wardleyDiagram-VNRHLVJA-52jgeKxz.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-BAPFJLpg.js → xychartDiagram-PMCCYNJV-DSdl9rQ8.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +1 -1
- package/dist-server/drivers/agents-proxy.js +5 -5
- package/dist-server/index.js +2379 -1596
- package/dist-server/openmausbot.js +857 -1012
- package/dist-server/pair-cli.js +857 -1012
- package/dist-server/permission-proxy.js +0 -4
- package/dist-server/server/auto-approve.js +1 -1
- package/dist-server/server/auto-vm-claims.js +13 -1
- package/dist-server/server/automatic-recovery.js +1 -2
- package/dist-server/server/boat.js +68 -84
- package/dist-server/server/bot-overview.js +1 -1
- package/dist-server/server/browser-engine.js +38 -1
- package/dist-server/server/browser-live.js +112 -25
- package/dist-server/server/browser-runtime.js +31 -1
- package/dist-server/server/checkpoints.js +275 -44
- package/dist-server/server/cli-setup.js +3 -3
- package/dist-server/server/cloud-computer-tools.js +11 -4
- package/dist-server/server/cloud-home-start.js +1 -1
- package/dist-server/server/cloud-home.js +3 -9
- package/dist-server/server/cloud-owner.js +1 -1
- package/dist-server/server/computer-engine-removal.js +70 -0
- package/dist-server/server/computer-selection.js +18 -0
- package/dist-server/server/config.js +7 -11
- package/dist-server/server/connector-scope-hint.js +83 -0
- package/dist-server/server/default-model-selection.js +20 -14
- package/dist-server/server/delegations.js +11 -5
- package/dist-server/server/delta-context.js +6 -1
- package/dist-server/server/drivers/acp/core.js +1 -1
- package/dist-server/server/drivers/acp/opencode-go.js +16 -21
- package/dist-server/server/drivers/agents-call.js +1 -1
- package/dist-server/server/drivers/agents-catalog.js +3 -3
- package/dist-server/server/drivers/builtIn.js +0 -2
- package/dist-server/server/drivers/claude.js +49 -30
- package/dist-server/server/drivers/codex.js +2 -2
- package/dist-server/server/drivers/openai-chat.js +98 -6
- package/dist-server/server/drivers/pi.js +184 -49
- package/dist-server/server/index.js +555 -279
- package/dist-server/server/live-call.js +11 -6
- package/dist-server/server/managed-policy.js +1 -1
- package/dist-server/server/model-catalog/presets.js +8 -0
- package/dist-server/server/peer-roster.js +10 -2
- package/dist-server/server/room-turn-end.js +15 -0
- package/dist-server/server/routes/live.js +3 -3
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +1 -1
- package/dist-server/server/screen-frame-gate.js +1 -3
- package/dist-server/server/store.js +125 -1
- package/dist-server/server/surface.js +29 -49
- package/dist-server/server/system-prompt.js +10 -10
- package/dist-server/server/turn-continuation.js +162 -0
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/ask-question.js +0 -70
- package/dist-server/shared/cloud-computer.js +4 -6
- package/dist-server/shared/credential-request.js +1 -1
- package/dist-server/shared/failed-turn.js +1 -4
- package/dist-server/shared/place-view.js +409 -0
- package/dist-server/shared/tool-scope-support.js +1 -2
- package/package.json +1 -1
- package/dist/assets/channel-BZxwC-Gw.js +0 -1
- package/dist/assets/index-Du7o6Dsf.js +0 -610
- package/dist/assets/index-KxG4bWSr.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C7voMDle.js +0 -8
- package/dist-server/server/drivers/boatagent.js +0 -443
|
@@ -1,443 +0,0 @@
|
|
|
1
|
-
import { newEventId, newId } from "../contracts.js";
|
|
2
|
-
import { boatCredential, boatProviderApi } from "../included-services.js";
|
|
3
|
-
import { appendNative } from "./native.js";
|
|
4
|
-
import { assertToolScopeSupported } from "../../shared/tool-scope-support.js";
|
|
5
|
-
import { OMB_ASK_TOOL, answerWithoutPreamble, askQuestionSummary, capAnswerEcho, ombAskProtocolPrompt, parseOmbAskQuestions, questionChoices, stripOmbAskBlock, } from "../../shared/ask-question.js";
|
|
6
|
-
// "boxAgent" is Boat's historical driver kind (catalog and wire); keep it.
|
|
7
|
-
const DRIVER_KIND = "boxAgent";
|
|
8
|
-
const MODELS = {
|
|
9
|
-
default: "claude-fable-5",
|
|
10
|
-
options: [
|
|
11
|
-
{ id: "claude-fable-5", label: "Claude Fable 5 · on the box" },
|
|
12
|
-
{ id: "sonnet", label: "Claude Sonnet · on the box" },
|
|
13
|
-
{ id: "gpt-5.4", label: "GPT-5.4 (Codex) · on the box" },
|
|
14
|
-
],
|
|
15
|
-
};
|
|
16
|
-
/** Any fence whose info string names the ask protocol, even when its body
|
|
17
|
-
* does not parse: the marker for "the model tried to ask and failed". */
|
|
18
|
-
const ASK_FENCE_ANY = /(^|\n)[ \t]{0,3}(`{3,}|~{3,})[ \t]*omb-ask\b/;
|
|
19
|
-
/** The boat runs every harness boat.dev ships (claude-code, codex, pi, opencode,
|
|
20
|
-
* prime-agent, kimi). Which one a model id belongs to comes from the public
|
|
21
|
-
* catalog, `GET /api/provider-models` at the API root: an object keyed by
|
|
22
|
-
* harness, each with its `models`. A bot that arrives here from another engine
|
|
23
|
-
* carries that engine's model id, so this is what lets it keep its model. The
|
|
24
|
-
* root is the API in use (Boat, or Cloud Pro's relay), so it is cached per root. */
|
|
25
|
-
let catalog = null;
|
|
26
|
-
let catalogRoot = "";
|
|
27
|
-
async function loadCatalog(api) {
|
|
28
|
-
const root = api.replace(/\/api\/box\/v1\/?$/, "");
|
|
29
|
-
if (catalog && catalogRoot === root)
|
|
30
|
-
return;
|
|
31
|
-
catalogRoot = root;
|
|
32
|
-
catalog = await fetch(`${root}/api/provider-models`, { signal: AbortSignal.timeout(15_000) })
|
|
33
|
-
.then((res) => (res.ok ? res.json() : null))
|
|
34
|
-
.catch(() => null);
|
|
35
|
-
}
|
|
36
|
-
const providerFor = (model) => {
|
|
37
|
-
const slash = model.indexOf("/");
|
|
38
|
-
if (slash > 0 && catalog?.[model.slice(0, slash)])
|
|
39
|
-
return { provider: model.slice(0, slash), model: model.slice(slash + 1) };
|
|
40
|
-
for (const [provider, harness] of Object.entries(catalog ?? {})) {
|
|
41
|
-
if (harness.models?.some((m) => m?.id === model))
|
|
42
|
-
return { provider, model };
|
|
43
|
-
}
|
|
44
|
-
return { provider: model.startsWith("gpt") ? "codex" : "claude-code", model };
|
|
45
|
-
};
|
|
46
|
-
/** Boat answers a prompt it cannot run with 409 provider_not_configured: the
|
|
47
|
-
* Boat has no AI sign-in for the harness the model needs. Say that, and the
|
|
48
|
-
* one thing the person can do about it here. */
|
|
49
|
-
export const PROVIDER_NOT_CONFIGURED_MESSAGE = "The Computer engine has no AI sign-in on its cloud computer. Choose another engine in this bot's settings.";
|
|
50
|
-
/** An OMB Cloud's included Boat account has no agent sign-ins and never
|
|
51
|
-
* will: the operator's account must not run (or pay for) customers' models. */
|
|
52
|
-
export const COMPUTER_ENGINE_CLOUD_UNAVAILABLE = "The Computer engine isn't available on OpenMaus Cloud — choose another engine.";
|
|
53
|
-
/** The words of a Boat error envelope ({code, message, error:{code,
|
|
54
|
-
* message}}), never a bare code when Boat sent a sentence. */
|
|
55
|
-
export function boatErrorMessage(body, status) {
|
|
56
|
-
const code = body?.code ?? body?.error?.code;
|
|
57
|
-
if (code === "provider_not_configured")
|
|
58
|
-
return PROVIDER_NOT_CONFIGURED_MESSAGE;
|
|
59
|
-
const message = body?.message ?? body?.error?.message ?? (typeof body?.error === "string" ? body.error : undefined) ?? code;
|
|
60
|
-
return typeof message === "string" && message.trim() ? message : `box HTTP ${status}`;
|
|
61
|
-
}
|
|
62
|
-
function decodeConfig(raw) {
|
|
63
|
-
const o = (raw ?? {});
|
|
64
|
-
return {
|
|
65
|
-
pollMs: typeof o.pollMs === "number" ? o.pollMs : 2500,
|
|
66
|
-
...(typeof o.askTimeoutMs === "number" ? { askTimeoutMs: o.askTimeoutMs } : {}),
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
export const BoatAgentDriver = {
|
|
70
|
-
driverKind: DRIVER_KIND,
|
|
71
|
-
metadata: { displayName: "Computer", supportsMultipleInstances: false },
|
|
72
|
-
models: MODELS,
|
|
73
|
-
decodeConfig,
|
|
74
|
-
defaultConfig: () => decodeConfig({}),
|
|
75
|
-
async create(input) {
|
|
76
|
-
const { instanceId, config } = input;
|
|
77
|
-
// The person's own token (instanceConfigs injects it), else Cloud Pro's
|
|
78
|
-
// included one, with the one base URL each may be sent to. Resolved per
|
|
79
|
-
// request, like every other Boat call.
|
|
80
|
-
const ownToken = input.environment.BOX_TOKEN ?? process.env.BOX_TOKEN;
|
|
81
|
-
const account = () => boatCredential(ownToken);
|
|
82
|
-
const listeners = new Set();
|
|
83
|
-
const active = new Map();
|
|
84
|
-
/** One open ask per thread: the turn-held transport's pending card. The
|
|
85
|
-
* thread is busy for exactly as long as the ask is open, so a second
|
|
86
|
-
* block can never race the first. */
|
|
87
|
-
const heldAsks = new Map();
|
|
88
|
-
/** Threads whose last run ended with an unparseable omb-ask fence: the
|
|
89
|
-
* next prompt carries the correction so the ask is never lost silently. */
|
|
90
|
-
const malformedAsks = new Set();
|
|
91
|
-
const emit = (event) => {
|
|
92
|
-
for (const l of Array.from(listeners))
|
|
93
|
-
l(event);
|
|
94
|
-
};
|
|
95
|
-
const base = (threadId, turnId) => ({
|
|
96
|
-
eventId: newEventId(),
|
|
97
|
-
provider: DRIVER_KIND,
|
|
98
|
-
threadId,
|
|
99
|
-
turnId,
|
|
100
|
-
createdAt: new Date().toISOString(),
|
|
101
|
-
});
|
|
102
|
-
const api = async (path, opts = {}) => {
|
|
103
|
-
const credential = account();
|
|
104
|
-
const res = await fetch(`${credential?.api ?? boatProviderApi()}${path}`, {
|
|
105
|
-
...opts,
|
|
106
|
-
headers: { authorization: `Bearer ${credential?.token ?? ""}`, "content-type": "application/json", ...opts.headers },
|
|
107
|
-
signal: opts.signal ?? AbortSignal.timeout(30_000),
|
|
108
|
-
});
|
|
109
|
-
const body = await res.json().catch(() => null);
|
|
110
|
-
if (!res.ok || body?.ok === false)
|
|
111
|
-
throw new Error(boatErrorMessage(body, res.status));
|
|
112
|
-
return body;
|
|
113
|
-
};
|
|
114
|
-
const sendTurn = async (turn) => {
|
|
115
|
-
turn = { ...turn, toolScope: assertToolScopeSupported(DRIVER_KIND, turn.toolScope) };
|
|
116
|
-
const { threadId } = turn;
|
|
117
|
-
const computer = turn.integrations?.computer;
|
|
118
|
-
const boxId = computer && (!computer.kind || computer.kind === "box") ? computer.boxId : undefined;
|
|
119
|
-
if (!account())
|
|
120
|
-
throw new Error('box not configured — add {"box":{"token":"…"}} to ~/.openmausbot/config.json');
|
|
121
|
-
if (account()?.included)
|
|
122
|
-
throw new Error(COMPUTER_ENGINE_CLOUD_UNAVAILABLE);
|
|
123
|
-
if (!boxId) {
|
|
124
|
-
throw new Error("this bot has no computer yet — open the Computer panel and provision one");
|
|
125
|
-
}
|
|
126
|
-
if (active.has(threadId))
|
|
127
|
-
throw new Error("a turn is already running on this thread");
|
|
128
|
-
const turnId = newId();
|
|
129
|
-
const model = turn.model || MODELS.default;
|
|
130
|
-
const correction = malformedAsks.delete(threadId)
|
|
131
|
-
? "Your previous omb-ask block was malformed or empty, so the person never saw it. Ask again with a valid fenced omb-ask JSON block, or ask in plain words."
|
|
132
|
-
: "";
|
|
133
|
-
const prompt = [
|
|
134
|
-
turn.system,
|
|
135
|
-
"You are working on the assigned cloud computer — use its desktop, Chrome, and shell within the access described above.",
|
|
136
|
-
ombAskProtocolPrompt(),
|
|
137
|
-
...(correction ? [correction] : []),
|
|
138
|
-
"",
|
|
139
|
-
turn.text,
|
|
140
|
-
]
|
|
141
|
-
.filter((s) => s !== undefined)
|
|
142
|
-
.join("\n");
|
|
143
|
-
const postPrompt = async (promptText) => {
|
|
144
|
-
await loadCatalog(account()?.api ?? boatProviderApi());
|
|
145
|
-
const started = await api(`/boxes/${boxId}/prompt`, {
|
|
146
|
-
method: "POST",
|
|
147
|
-
body: JSON.stringify({ ...providerFor(model), prompt: promptText }),
|
|
148
|
-
});
|
|
149
|
-
appendNative(threadId, { dir: "out", source: "box.prompt", msg: { model, prompt: promptText, response: started } });
|
|
150
|
-
// real shape (2026-08): {type:"prompt.queued", promptId, promptRun:{id,…},
|
|
151
|
-
// id:<boat id>} — never fall back to the bare id, it's the boat's
|
|
152
|
-
return started?.promptRun?.id ?? started?.prompt?.id ?? started?.promptId ?? null;
|
|
153
|
-
};
|
|
154
|
-
const promptId = await postPrompt(prompt);
|
|
155
|
-
let cancelled = false;
|
|
156
|
-
active.set(threadId, {
|
|
157
|
-
turnId,
|
|
158
|
-
boxId,
|
|
159
|
-
cancel: () => {
|
|
160
|
-
cancelled = true;
|
|
161
|
-
heldAsks.get(threadId)?.settle(null, "system");
|
|
162
|
-
void api(`/boxes/${boxId}/interrupt`, { method: "POST" }).catch(() => { });
|
|
163
|
-
},
|
|
164
|
-
});
|
|
165
|
-
emit({ ...base(threadId, turnId), type: "turn.started" });
|
|
166
|
-
emit({ ...base(threadId, turnId), type: "session.started", sessionId: promptId, model });
|
|
167
|
-
// poll events + run status until the prompt settles — and, when the
|
|
168
|
-
// run ends on an omb-ask block, until the person answers it: the OMB
|
|
169
|
-
// turn is the unit the whole server already understands (busy thread,
|
|
170
|
-
// waiting-on-you, routines waiting), so it stays open across the ask
|
|
171
|
-
// and the continuation prompt continues it rather than starting a
|
|
172
|
-
// driver-initiated turn nobody accounts for.
|
|
173
|
-
(async () => {
|
|
174
|
-
// Event ids seen this TURN. The events stream is the whole
|
|
175
|
-
// conversation, so a continuation run must not re-ingest history;
|
|
176
|
-
// `taskId` normally filters it, but ids survive even shape drift.
|
|
177
|
-
const seen = new Set();
|
|
178
|
-
let lastText = "";
|
|
179
|
-
let pendingText = "";
|
|
180
|
-
/** Why the boat could not answer (login expired, model refused, …). */
|
|
181
|
-
let problem = null;
|
|
182
|
-
/** Emit unflushed deltas as assistant_text and reset pendingText.
|
|
183
|
-
* The omb-ask block is protocol, not prose: it streamed raw (the
|
|
184
|
-
* card is its readable form), and the settled message shows the
|
|
185
|
-
* words around it, never the JSON. */
|
|
186
|
-
const flushAssistantText = () => {
|
|
187
|
-
const text = stripOmbAskBlock(pendingText);
|
|
188
|
-
pendingText = "";
|
|
189
|
-
if (!text.trim())
|
|
190
|
-
return;
|
|
191
|
-
emit({ ...base(threadId, turnId), type: "item.completed", itemType: "assistant_text", text });
|
|
192
|
-
};
|
|
193
|
-
/** Stream a full-text snapshot as a delta and accumulate it for flush. */
|
|
194
|
-
const ingest = (text) => {
|
|
195
|
-
const delta = text.startsWith(lastText) ? text.slice(lastText.length) : text;
|
|
196
|
-
lastText = text;
|
|
197
|
-
if (!delta)
|
|
198
|
-
return;
|
|
199
|
-
pendingText += delta;
|
|
200
|
-
emit({ ...base(threadId, turnId), type: "content.delta", streamKind: "assistant_text", delta });
|
|
201
|
-
};
|
|
202
|
-
/** The run has settled: flush its prose, then hold the OMB turn open
|
|
203
|
-
* on an omb-ask block (if any) until the person answers or the ask
|
|
204
|
-
* times out. An answer chains the continuation run under this same
|
|
205
|
-
* turn; a deny or timeout ends it. */
|
|
206
|
-
const finishRun = async (ok, stopReason) => {
|
|
207
|
-
flushAssistantText();
|
|
208
|
-
// An interrupt that lands between ask text streaming and settle
|
|
209
|
-
// finds no heldAsks entry to settle; without this check the run
|
|
210
|
-
// would register a fresh ask for a dead turn and hold stop pending
|
|
211
|
-
// for the ask timeout.
|
|
212
|
-
if (cancelled) {
|
|
213
|
-
return { ok: false, stopReason: "interrupted" };
|
|
214
|
-
}
|
|
215
|
-
// A remotely interrupted or failed run must stay stopped, even
|
|
216
|
-
// if its partial output already contains a complete question.
|
|
217
|
-
if (!ok)
|
|
218
|
-
return { ok, stopReason };
|
|
219
|
-
const questions = parseOmbAskQuestions(lastText);
|
|
220
|
-
if (!questions) {
|
|
221
|
-
// A fence that did not parse is a question the person never saw.
|
|
222
|
-
// Say so on the next prompt instead of losing it silently. (An
|
|
223
|
-
// over-cap block does not land here: the parser caps it at six
|
|
224
|
-
// questions and still shows the card.)
|
|
225
|
-
if (ASK_FENCE_ANY.test(lastText))
|
|
226
|
-
malformedAsks.add(threadId);
|
|
227
|
-
return { ok: ok && !cancelled, stopReason: cancelled ? "interrupted" : stopReason };
|
|
228
|
-
}
|
|
229
|
-
const requestId = newId();
|
|
230
|
-
let settleHeld;
|
|
231
|
-
const held = new Promise((resolve) => {
|
|
232
|
-
const timer = setTimeout(() => settleHeld(null, "timeout"), config.askTimeoutMs ?? 15 * 60_000);
|
|
233
|
-
timer.unref?.();
|
|
234
|
-
settleHeld = (reply, source) => {
|
|
235
|
-
heldAsks.delete(threadId);
|
|
236
|
-
clearTimeout(timer);
|
|
237
|
-
resolve([reply, source]);
|
|
238
|
-
};
|
|
239
|
-
});
|
|
240
|
-
// register before emitting — an answer can race the emit
|
|
241
|
-
heldAsks.set(threadId, { requestId, settle: settleHeld });
|
|
242
|
-
const choices = questionChoices(questions);
|
|
243
|
-
emit({
|
|
244
|
-
...base(threadId, turnId),
|
|
245
|
-
requestId,
|
|
246
|
-
type: "request.opened",
|
|
247
|
-
requestType: "question",
|
|
248
|
-
tool: OMB_ASK_TOOL,
|
|
249
|
-
summary: askQuestionSummary(questions),
|
|
250
|
-
questions,
|
|
251
|
-
...(choices?.length ? { choices } : {}),
|
|
252
|
-
origin: "output",
|
|
253
|
-
});
|
|
254
|
-
const [reply, source] = await held;
|
|
255
|
-
emit({ ...base(threadId, turnId), requestId, type: "request.resolved", behavior: reply !== null ? "answer" : "deny", source });
|
|
256
|
-
if (reply === null || cancelled) {
|
|
257
|
-
return { ok: ok && !cancelled, stopReason: cancelled ? "interrupted" : stopReason };
|
|
258
|
-
}
|
|
259
|
-
// The turn is still open, so this prompt continues it: same turnId,
|
|
260
|
-
// same accounting, and the answer reaches the boat the way the ask
|
|
261
|
-
// contract promised — Q:/A: blocks, capped for echo.
|
|
262
|
-
const continuation = [
|
|
263
|
-
capAnswerEcho(answerWithoutPreamble(reply)),
|
|
264
|
-
"",
|
|
265
|
-
"The person answered the omb-ask questions above (Q:/A:). Continue the task with their answers; end with another omb-ask block only if you truly need more.",
|
|
266
|
-
].join("\n");
|
|
267
|
-
const nextPromptId = await postPrompt(continuation);
|
|
268
|
-
// Stop can land while the continuation POST is in flight: the
|
|
269
|
-
// interrupt inside cancel() then hits a boat with no active run,
|
|
270
|
-
// and the continuation would start after it. Interrupt the run
|
|
271
|
-
// that just started before ending the turn.
|
|
272
|
-
if (cancelled) {
|
|
273
|
-
void api(`/boxes/${boxId}/interrupt`, { method: "POST" }).catch(() => { });
|
|
274
|
-
return { ok: false, stopReason: "interrupted" };
|
|
275
|
-
}
|
|
276
|
-
return await settleRun(nextPromptId);
|
|
277
|
-
};
|
|
278
|
-
/** Poll one boat run to its settle. */
|
|
279
|
-
const settleRun = async (runPromptId) => {
|
|
280
|
-
const startedAt = Date.now(); // one 30-min ceiling per boat run, ask chains included
|
|
281
|
-
lastText = "";
|
|
282
|
-
pendingText = "";
|
|
283
|
-
problem = null;
|
|
284
|
-
for (;;) {
|
|
285
|
-
if (cancelled)
|
|
286
|
-
break;
|
|
287
|
-
await new Promise((r) => setTimeout(r, config.pollMs));
|
|
288
|
-
const events = await api(`/boxes/${boxId}/events`).catch(() => null);
|
|
289
|
-
const list = events?.events ?? events?.items ?? [];
|
|
290
|
-
for (const ev of list) {
|
|
291
|
-
// The stream is the whole conversation from its start; `taskId`
|
|
292
|
-
// names the prompt run an event belongs to. Earlier turns are
|
|
293
|
-
// history, not this answer.
|
|
294
|
-
if (runPromptId && ev.taskId && String(ev.taskId) !== runPromptId)
|
|
295
|
-
continue;
|
|
296
|
-
const id = String(ev.id ?? ev.eventId ?? JSON.stringify(ev).slice(0, 120));
|
|
297
|
-
if (seen.has(id))
|
|
298
|
-
continue;
|
|
299
|
-
seen.add(id);
|
|
300
|
-
appendNative(threadId, { dir: "in", source: "box.events", msg: ev });
|
|
301
|
-
const kind = String(ev.type ?? ev.kind ?? "");
|
|
302
|
-
// "response" events carry the agent's text at data.content —
|
|
303
|
-
// the FULL text so far, not a chunk. Clients accumulate
|
|
304
|
-
// deltas, so forward only the growth; a drifted (non-prefix)
|
|
305
|
-
// event re-sends whole and the settled message replaces the
|
|
306
|
-
// stream anyway.
|
|
307
|
-
const text = ev.text ?? ev.message ?? ev.data?.text ?? ev.data?.content ?? null;
|
|
308
|
-
if (/assistant|message|output|response/i.test(kind) && typeof text === "string" && text.trim()) {
|
|
309
|
-
ingest(text);
|
|
310
|
-
}
|
|
311
|
-
else if (/usage_limit|error|fail/i.test(kind)) {
|
|
312
|
-
const why = ev.data?.summary ?? ev.data?.message ?? ev.data?.error ?? ev.message;
|
|
313
|
-
if (typeof why === "string" && why.trim())
|
|
314
|
-
problem = why.trim();
|
|
315
|
-
}
|
|
316
|
-
else if (/tool|command|exec|browse/i.test(kind)) {
|
|
317
|
-
flushAssistantText();
|
|
318
|
-
emit({
|
|
319
|
-
...base(threadId, turnId),
|
|
320
|
-
type: "item.started",
|
|
321
|
-
itemType: "tool",
|
|
322
|
-
itemId: id,
|
|
323
|
-
title: String(ev.title ?? ev.command ?? kind).slice(0, 80),
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
// shape-drift backstop: without a promptId the status poll
|
|
327
|
-
// below can never see a terminal state, so settle off the
|
|
328
|
-
// events themselves instead of hanging to the 30-min ceiling
|
|
329
|
-
if (!runPromptId && /complete|finish|done|success|fail|error/i.test(kind)) {
|
|
330
|
-
const failed = /fail|error/i.test(kind);
|
|
331
|
-
return await finishRun(!failed, failed ? kind : null);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
if (runPromptId) {
|
|
335
|
-
const status = await api(`/boxes/${boxId}/prompts/${runPromptId}`).catch(() => null);
|
|
336
|
-
appendNative(threadId, { dir: "in", source: "box.prompt.status", msg: status });
|
|
337
|
-
// real shape (2026-08): {promptRun:{status:"finished",…}} —
|
|
338
|
-
// flat fallbacks kept for drift
|
|
339
|
-
const run = status?.promptRun ?? status?.prompt ?? status ?? {};
|
|
340
|
-
const state = String(run?.status ?? "");
|
|
341
|
-
if (/completed|succeeded|done|finished/i.test(state)) {
|
|
342
|
-
const result = run?.result ?? run?.output ?? lastText;
|
|
343
|
-
if (typeof result === "string" && result.trim() && result !== lastText) {
|
|
344
|
-
ingest(result);
|
|
345
|
-
}
|
|
346
|
-
if (!pendingText.trim() && !lastText.trim()) {
|
|
347
|
-
// a run that ends with nothing said and a recorded problem
|
|
348
|
-
// (login expired, …) is a failure the person must see
|
|
349
|
-
if (problem)
|
|
350
|
-
throw new Error(problem);
|
|
351
|
-
pendingText = "(finished)";
|
|
352
|
-
}
|
|
353
|
-
return await finishRun(true, null);
|
|
354
|
-
}
|
|
355
|
-
if (/failed|error|cancelled|interrupted/i.test(state)) {
|
|
356
|
-
const runError = [run?.error, run?.failureReason, run?.message].find((v) => typeof v === "string" && v.trim());
|
|
357
|
-
if (problem || runError || /failed|error/i.test(state))
|
|
358
|
-
throw new Error(problem ?? runError ?? `the box run ${state}`);
|
|
359
|
-
return await finishRun(false, state);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
if (Date.now() - startedAt > 30 * 60_000) {
|
|
363
|
-
throw new Error("box run exceeded 30 minutes — interrupted");
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
// cancelled
|
|
367
|
-
return await finishRun(false, "interrupted");
|
|
368
|
-
};
|
|
369
|
-
try {
|
|
370
|
-
const outcome = await settleRun(promptId);
|
|
371
|
-
active.delete(threadId);
|
|
372
|
-
emit({ ...base(threadId, turnId), type: "turn.completed", ok: outcome.ok, stopReason: outcome.stopReason, cost: null });
|
|
373
|
-
}
|
|
374
|
-
catch (e) {
|
|
375
|
-
flushAssistantText();
|
|
376
|
-
heldAsks.delete(threadId);
|
|
377
|
-
active.delete(threadId);
|
|
378
|
-
emit({ ...base(threadId, turnId), type: "runtime.error", message: e.message });
|
|
379
|
-
emit({ ...base(threadId, turnId), type: "turn.completed", ok: false, stopReason: "error", cost: null });
|
|
380
|
-
}
|
|
381
|
-
})();
|
|
382
|
-
return { turnId };
|
|
383
|
-
};
|
|
384
|
-
const snapshot = async () => {
|
|
385
|
-
if (!account()) {
|
|
386
|
-
return { state: "unavailable", reason: 'no Boat token — add {"box":{"token":"…"}} to ~/.openmausbot/config.json' };
|
|
387
|
-
}
|
|
388
|
-
if (account()?.included)
|
|
389
|
-
return { state: "unavailable", reason: COMPUTER_ENGINE_CLOUD_UNAVAILABLE };
|
|
390
|
-
try {
|
|
391
|
-
await api("/me");
|
|
392
|
-
return { state: "available", authenticated: true, version: null };
|
|
393
|
-
}
|
|
394
|
-
catch (e) {
|
|
395
|
-
return { state: "unavailable", reason: `box API unreachable: ${e.message}` };
|
|
396
|
-
}
|
|
397
|
-
};
|
|
398
|
-
return {
|
|
399
|
-
instanceId,
|
|
400
|
-
driverKind: DRIVER_KIND,
|
|
401
|
-
displayName: input.displayName,
|
|
402
|
-
enabled: input.enabled,
|
|
403
|
-
models: MODELS,
|
|
404
|
-
snapshot,
|
|
405
|
-
adapter: {
|
|
406
|
-
provider: DRIVER_KIND,
|
|
407
|
-
capabilities: { sessionModelSwitch: "in-session", remoteAgent: true },
|
|
408
|
-
sendTurn,
|
|
409
|
-
interruptTurn: async (threadId) => active.get(threadId)?.cancel(),
|
|
410
|
-
respondToRequest: async (threadId, requestId, decision) => {
|
|
411
|
-
const held = heldAsks.get(threadId);
|
|
412
|
-
if (!held || held.requestId !== requestId)
|
|
413
|
-
return "unavailable"; // settled, timed out, or restarted away
|
|
414
|
-
const reply = decision.message?.trim();
|
|
415
|
-
if (decision.behavior === "answer" && reply) {
|
|
416
|
-
held.settle(decision.message, "user");
|
|
417
|
-
return "answered";
|
|
418
|
-
}
|
|
419
|
-
if (decision.behavior === "deny") {
|
|
420
|
-
held.settle(null, "user");
|
|
421
|
-
return "rejected";
|
|
422
|
-
}
|
|
423
|
-
// "allow" is not an answer to a question, and blank text is not either
|
|
424
|
-
return "unavailable";
|
|
425
|
-
},
|
|
426
|
-
hasSession: (threadId) => active.has(threadId),
|
|
427
|
-
stopAll: async () => {
|
|
428
|
-
for (const { cancel } of active.values())
|
|
429
|
-
cancel();
|
|
430
|
-
},
|
|
431
|
-
onEvent: (listener) => {
|
|
432
|
-
listeners.add(listener);
|
|
433
|
-
return () => listeners.delete(listener);
|
|
434
|
-
},
|
|
435
|
-
},
|
|
436
|
-
dispose: async () => {
|
|
437
|
-
for (const { cancel } of active.values())
|
|
438
|
-
cancel();
|
|
439
|
-
listeners.clear();
|
|
440
|
-
},
|
|
441
|
-
};
|
|
442
|
-
},
|
|
443
|
-
};
|