openmausbot 0.1.96 → 0.1.98
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/ActivityPanel-H6D3SD39.js +1 -0
- package/dist/assets/BotSettingsDialog-Dqz_AiBZ.js +7 -0
- package/dist/assets/CloudBackendPicker-iezpJFpF.js +1 -0
- package/dist/assets/ComputerPanel-DZPj-IWu.js +1 -0
- package/dist/assets/DesktopViewer-utONms1p.js +5 -0
- package/dist/assets/InspectorPanel-OVaBVrzC.js +5 -0
- package/dist/assets/KeyboardShortcutsModal-gk9mvHj_.js +1 -0
- package/dist/assets/LocalVmWorkspace-DLIvd2kC.js +1 -0
- package/dist/assets/MacLocalControl-bJv8yiPB.js +1 -0
- package/dist/assets/NewBotDialog-1qGOAdUM.js +2 -0
- package/dist/assets/RemoteAgentSettingsPanel-DqKr0HkO.js +1 -0
- package/dist/assets/RoutinesPage-BM2D4qGi.js +1 -0
- package/dist/assets/RoutinesSection-Dny9ra-c.js +1 -0
- package/dist/assets/SettingsModal-gC6XJJmt.js +11 -0
- package/dist/assets/TeamMapPage-B6fuu4r_.js +8 -0
- package/dist/assets/TriggersPanel-j0qTP2CO.js +1 -0
- package/dist/assets/VisibilitySection-DT0FqPvu.js +12 -0
- package/dist/assets/WebhooksPanel--YQyCjKO.js +3 -0
- package/dist/assets/{abnfDiagram-O67JEVCF-Dkbd2hcj.js → abnfDiagram-O67JEVCF-BsjcFX0-.js} +1 -1
- package/dist/assets/activity-BxOkayvD.js +1 -0
- package/dist/assets/{arc-Duj5Coy2.js → arc-CJNA94c6.js} +1 -1
- package/dist/assets/architectureDiagram-NJMV4G6O-dz2ud7lP.js +36 -0
- package/dist/assets/{blockDiagram-BEXU5L5S-CWDlTpXe.js → blockDiagram-BEXU5L5S-ChBEALoR.js} +1 -1
- package/dist/assets/brain-CFZ7PEXc.js +1 -0
- package/dist/assets/{c4Diagram-YGBWAQC7-B0t15PDc.js → c4Diagram-YGBWAQC7-KZNRKDQe.js} +1 -1
- package/dist/assets/channel-WhFyujhZ.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BJSCL-HV.js → chunk-2Q5K7J3B-BLLwmCET.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-D3_ILccd.js → chunk-3FUC2YCW-CQxyreBZ.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-D3dmwT_S.js → chunk-5VM5RSS4-D6sFMW0u.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Br73HFPD.js → chunk-GTNCS2PH-BaZ1YgLF.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-uSsjxubC.js → chunk-GWA4HPMP-CcalphmC.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-C40VWFyr.js → chunk-JWPE2WC7-CSk8R3ve.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-D6WNNYwU.js → chunk-NETBCI7D-CTmLjA57.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-DOEhq64P.js → chunk-WEXAMYUT-CM9Z77Ne.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-BqJEi6Pi.js → chunk-XXDRQBXY-B1fxpMS2.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-ByU_GnxT.js → classDiagram-v2-NBCMYWYE-DCCyoLpj.js} +1 -1
- package/dist/assets/computer-control-NLov352p.js +1 -0
- package/dist/assets/cose-bilkent-JH36ORCC-KyImBKvE.js +1 -0
- package/dist/assets/{cynefin-OW5HDTMX-CZWO7aYj.js → cynefin-OW5HDTMX-fsZc6MGa.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-C70-w0la.js → cynefinDiagram-VND7K2PF-CGmHsShB.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-CnhjhExB.js → dagre-6A5THRUB-Dah1h-CH.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-bnjWCNb4.js → diagram-22UHCM2B-BIu71OyV.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-A4uTWoFV.js → diagram-3UASUU5V-gxhUB_Mu.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Zi6m3NUl.js → diagram-ATOU4E4O-CBhusUBn.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DtCcmd5k.js → diagram-CDSNMT55-BdPrc86x.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-DofuETS3.js → diagram-MLGK6HIB-Clp110R_.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-Dw2T9PuU.js → diagram-MPIPVDR6-DJKfbugw.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-DbrwPiem.js → ebnfDiagram-ZINNZB2B-DnsltoBu.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-BvCt1dCb.js → elk-276RUBZZ-jWdq6gqH.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-Y-QKqPnU.js → erDiagram-OPXOYQCR-DArPVf8u.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-CDkL1B5w.js → flowDiagram-KWPJA3E3-D2HZ-2f7.js} +1 -1
- package/dist/assets/ganttDiagram-FUAMR5RP--o0KIxEo.js +292 -0
- package/dist/assets/{gitGraphDiagram-X574FWY7-nL9iFTCv.js → gitGraphDiagram-X574FWY7-CsoXr2D7.js} +1 -1
- package/dist/assets/image-plus-BbVjGHin.js +1 -0
- package/dist/assets/index-BrPW5HDO.css +1 -0
- package/dist/assets/index-C3NqfdVU.js +585 -0
- package/dist/assets/{index-DreDnjOu.js → index-CJTp9JEp.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-C7aHjzsf.js → infoDiagram-VRGFBTTK-g-6lNp7r.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-KUZsNBTh.js → ishikawaDiagram-OU5B5YK6-B1d6qVTV.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-CGUuodFl.js → journeyDiagram-ZHPQQLJL-BTPuiU6T.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-Cy6r868Z.js → kanban-definition-PNTS6WVX-bDDuRJvP.js} +1 -1
- package/dist/assets/{linear-Bl669148.js → linear-BVVKbEMy.js} +1 -1
- package/dist/assets/{mermaid.core-86Xqz5Mr.js → mermaid.core-JJdoKyKB.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-G4DzHLrd.js → mindmap-definition-NLK3R4M7-B4aE3T0q.js} +1 -1
- package/dist/assets/module-DCUsy2BQ.js +3 -0
- package/dist/assets/page-visible-BBo_Zm8g.js +1 -0
- package/dist/assets/{pegDiagram-GJSIUBJH-K4eruzY0.js → pegDiagram-GJSIUBJH-DFKQWpR7.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DYdR9PPq.js → pieDiagram-5QR66LMP-Iv7OKVJi.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-BCAh0nEy.js → quadrantDiagram-O4NWA36T-U2YnfwBk.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-p0dGRc4g.js → railroadDiagram-XR7U4H2S-CuPy6RqM.js} +1 -1
- package/dist/assets/remote-desktop-panel-BNEj7tUx.js +1 -0
- package/dist/assets/{requirementDiagram-PLB6GJNP-tLeBG0aL.js → requirementDiagram-PLB6GJNP-bMI5AK7T.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-QRuN16rV.js → sankeyDiagram-IPEJSGJF-BKcFE2WQ.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-ueT1nVoa.js → sequenceDiagram-PO4LG4MO-Bn9q88kl.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-wRS2zWg2.js → sizeCapture-INFHLROL-Cvmdq9HD.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-CYzA_hE1.js → stateDiagram-v2-GCMORJYK-JJQYw3eQ.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-kDnv6LMc.js → swimlanes-2SLR337P-BkSCBEwp.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CmTirbVT.js +8 -0
- package/dist/assets/table-file.worker-KkMx-SNx.js +10 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-D1y2UbbH.js → timeline-definition-EJHVYXUP-DsaM4GHr.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-IqB4v9HX.js → usecaseDiagram-POWQR4AR-3FOwLq7B.js} +1 -1
- package/dist/assets/user-DcZBnAuv.js +1 -0
- package/dist/assets/{vennDiagram-UO4OBE2U-BEi7KYVF.js → vennDiagram-UO4OBE2U-DyNjJsKt.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-Dkybo-Yf.js → wardleyDiagram-VNRHLVJA-Y4ZMcnBC.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-BAPFJLpg.js → xychartDiagram-PMCCYNJV-BXZpg955.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +20 -9
- package/dist-server/container-mcp.js +8 -0
- package/dist-server/desktop-entry.mjs +54 -0
- package/dist-server/drivers/agents-proxy.js +26 -9
- package/dist-server/index.js +3734 -2360
- package/dist-server/openmausbot.js +1664 -1553
- package/dist-server/pair-cli.js +1664 -1553
- package/dist-server/permission-proxy.js +2 -4
- package/dist-server/server/atomic.js +70 -2
- 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 +4 -4
- 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 +29 -12
- package/dist-server/server/connector-card-text.js +15 -0
- package/dist-server/server/connector-scope-hint.js +83 -0
- package/dist-server/server/default-model-selection.js +23 -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 +11 -1
- package/dist-server/server/drivers/acp/grok.js +47 -5
- 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 +27 -7
- package/dist-server/server/drivers/agents-proxy.js +1 -0
- package/dist-server/server/drivers/builtIn.js +0 -2
- package/dist-server/server/drivers/chat-mcp-tools.js +5 -3
- package/dist-server/server/drivers/claude-login-auth.js +1 -1
- package/dist-server/server/drivers/claude.js +84 -41
- package/dist-server/server/drivers/codex-device-auth.js +27 -319
- package/dist-server/server/drivers/codex.js +2 -2
- package/dist-server/server/drivers/device-auth.js +342 -0
- package/dist-server/server/drivers/openai-chat.js +110 -10
- package/dist-server/server/drivers/pi.js +184 -49
- package/dist-server/server/fleet-agent.js +18 -5
- package/dist-server/server/harness/http.js +5 -2
- package/dist-server/server/harness/registry.js +19 -0
- package/dist-server/server/harness-capabilities.js +7 -4
- package/dist-server/server/hosted-models.js +3 -1
- package/dist-server/server/index.js +823 -333
- package/dist-server/server/key-rejections.js +57 -0
- package/dist-server/server/live-call.js +11 -6
- package/dist-server/server/managed-policy.js +1 -1
- package/dist-server/server/message-file.js +7 -4
- package/dist-server/server/model-catalog/presets.js +8 -0
- package/dist-server/server/peer-roster.js +10 -2
- package/dist-server/server/permission-proxy.js +4 -0
- package/dist-server/server/provider-key-check.js +5 -1
- package/dist-server/server/request-auth.js +1 -1
- package/dist-server/server/room-turn-end.js +15 -0
- package/dist-server/server/room-turn-timeout.js +28 -0
- package/dist-server/server/routes/live.js +3 -3
- package/dist-server/server/routes/thread-models.js +36 -0
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +3 -3
- package/dist-server/server/screen-frame-gate.js +1 -3
- package/dist-server/server/skills.js +3 -2
- package/dist-server/server/store.js +273 -61
- package/dist-server/server/surface.js +29 -49
- package/dist-server/server/system-prompt.js +10 -10
- package/dist-server/server/team-backup.js +19 -4
- package/dist-server/server/thread-model.js +25 -0
- package/dist-server/server/turn-continuation.js +168 -0
- package/dist-server/server/webhook-ingress.js +8 -3
- package/dist-server/server/webhooks.js +40 -5
- 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/device-sign-in.js +57 -0
- package/dist-server/shared/failed-turn.js +1 -4
- package/dist-server/shared/place-view.js +409 -0
- package/dist-server/shared/team-backup.js +2 -0
- package/dist-server/shared/thread-model.js +13 -0
- package/dist-server/shared/tool-scope-support.js +1 -2
- package/dist-server/vps-container-mcp.js +8 -0
- package/dist-server/workspace-backup.worker.js +41 -22
- package/package.json +1 -1
- package/dist/assets/DesktopViewer-DsnA23KM.js +0 -5
- package/dist/assets/architectureDiagram-NJMV4G6O-uYUsDawP.js +0 -36
- package/dist/assets/channel-BZxwC-Gw.js +0 -1
- package/dist/assets/cose-bilkent-JH36ORCC-D-Uglxqq.js +0 -1
- package/dist/assets/ganttDiagram-FUAMR5RP-CKV5D2F6.js +0 -292
- 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
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { stripVTControlCharacters } from "node:util";
|
|
6
|
+
import { deviceSignInPrompt } from "../../shared/device-sign-in.js";
|
|
7
|
+
import { killCliTree, spawnCli } from "../procs.js";
|
|
8
|
+
const MAX_OUTPUT = 16_384;
|
|
9
|
+
// Two configured instances can use the same on-disk login. Only the auth
|
|
10
|
+
// operation is exclusive: normal turns are not blocked by this lock.
|
|
11
|
+
const authenticatingHomes = new Set();
|
|
12
|
+
/** The real path of a credential home, so two spellings of one home share
|
|
13
|
+
* one sign-in lock. A missing tail keeps its name under its real parent. */
|
|
14
|
+
export function canonicalPath(path) {
|
|
15
|
+
try {
|
|
16
|
+
return realpathSync(path);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
const parent = dirname(path);
|
|
20
|
+
return parent === path ? path : join(canonicalPath(parent), basename(path));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** A fixed login command, not a remotely accessible terminal. CLI output
|
|
24
|
+
* stays in bounded private memory; only a device page and code are exposed. */
|
|
25
|
+
export class DeviceAuthController {
|
|
26
|
+
flow = null;
|
|
27
|
+
command = null;
|
|
28
|
+
disposed = false;
|
|
29
|
+
spec;
|
|
30
|
+
options;
|
|
31
|
+
constructor(spec, options) {
|
|
32
|
+
this.spec = spec;
|
|
33
|
+
this.options = options;
|
|
34
|
+
}
|
|
35
|
+
/** The credential home the lock is held on, for this environment. */
|
|
36
|
+
home(env, action) {
|
|
37
|
+
const { homeEnv, homeDir, product } = this.spec;
|
|
38
|
+
const home = env.HOME || env.USERPROFILE || homedir();
|
|
39
|
+
const named = env[homeEnv];
|
|
40
|
+
if (!isAbsolute(home) || (named && !isAbsolute(named))) {
|
|
41
|
+
throw new Error(`Use an absolute HOME and ${homeEnv} path for this server's ${product} provider before ${action}.`);
|
|
42
|
+
}
|
|
43
|
+
return { home, homeKey: canonicalPath(resolve(named || join(home, homeDir))) };
|
|
44
|
+
}
|
|
45
|
+
async start() {
|
|
46
|
+
const { product, account, status } = this.spec;
|
|
47
|
+
if (this.disposed)
|
|
48
|
+
throw new Error("This provider was removed. Refresh Settings before signing in.");
|
|
49
|
+
if (this.flow?.status.phase === "waiting") {
|
|
50
|
+
if (this.flow.ready)
|
|
51
|
+
return { ...this.flow.status, phase: "waiting" };
|
|
52
|
+
throw new Error(`A ${account} sign-in is already starting. Please wait for the code.`);
|
|
53
|
+
}
|
|
54
|
+
await this.flow?.stopping;
|
|
55
|
+
if (this.disposed)
|
|
56
|
+
throw new Error("This provider was removed. Refresh Settings before signing in.");
|
|
57
|
+
const env = this.options.environment();
|
|
58
|
+
const { home, homeKey } = this.home(env, "signing in");
|
|
59
|
+
if (authenticatingHomes.has(homeKey)) {
|
|
60
|
+
throw new Error(`A ${account} sign-in is already running for this server account. Finish or cancel it before starting another.`);
|
|
61
|
+
}
|
|
62
|
+
authenticatingHomes.add(homeKey);
|
|
63
|
+
return new Promise((resolveStart, reject) => {
|
|
64
|
+
const flow = {
|
|
65
|
+
status: {
|
|
66
|
+
phase: "waiting", flowId: randomUUID(), authorizationUrl: null,
|
|
67
|
+
expiresAt: new Date(Date.now() + (this.options.lifetimeMs ?? 15 * 60_000)).toISOString(),
|
|
68
|
+
},
|
|
69
|
+
child: null, ready: false, resolve: resolveStart, reject, homeKey,
|
|
70
|
+
startupTimer: setTimeout(() => this.finish(flow, "failed", `${product} did not provide a sign-in code in time. Check the server connection and update ${product}, then try again.`), this.options.startupTimeoutMs ?? 30_000),
|
|
71
|
+
expiryTimer: setTimeout(() => this.finish(flow, "expired", `The ${account} sign-in code expired. Start sign-in again.`), this.options.lifetimeMs ?? 15 * 60_000),
|
|
72
|
+
};
|
|
73
|
+
flow.startupTimer.unref();
|
|
74
|
+
flow.expiryTimer.unref();
|
|
75
|
+
this.flow = flow;
|
|
76
|
+
const login = () => this.run(flow, [...this.spec.loginArgs], env, home, (loginCode, loginOutput) => {
|
|
77
|
+
if (loginCode !== 0 || !flow.ready) {
|
|
78
|
+
this.finish(flow, "failed", this.spec.failure(loginOutput));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// A successful command is not enough: confirm the sign-in the way
|
|
82
|
+
// this client's bots will find it, with the same executable and
|
|
83
|
+
// environment.
|
|
84
|
+
flow.startupTimer = setTimeout(() => this.finish(flow, "failed", `${product} finished sign-in but could not confirm the account. Refresh Settings and try again.`), this.options.startupTimeoutMs ?? 30_000);
|
|
85
|
+
flow.startupTimer.unref();
|
|
86
|
+
this.confirm(flow, env, home, (signedIn) => this.finish(flow, signedIn ? "succeeded" : "failed", `${product} finished sign-in but did not confirm a ${account} account. Refresh Settings and try again.`));
|
|
87
|
+
}, true);
|
|
88
|
+
// Never overwrite a working login just because Connect was clicked twice.
|
|
89
|
+
if (!status) {
|
|
90
|
+
login();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
this.run(flow, [...status.args], env, home, (code, output) => {
|
|
94
|
+
const state = status.read(code, output);
|
|
95
|
+
if (state === "signed-in")
|
|
96
|
+
this.finish(flow, "succeeded");
|
|
97
|
+
else if (state === "other")
|
|
98
|
+
this.finish(flow, "failed", `${product} already has a different sign-in method on this server. Ask the server administrator to review it before changing accounts.`);
|
|
99
|
+
else if (state === "signed-out")
|
|
100
|
+
login();
|
|
101
|
+
else
|
|
102
|
+
this.finish(flow, "failed", `${product} could not confirm the existing sign-in on this server. Update ${product} and check its login status before trying again.`);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
async get(flowId) {
|
|
107
|
+
if (!flowId || this.flow?.status.flowId !== flowId)
|
|
108
|
+
throw new Error("This sign-in is no longer available. Start sign-in again.");
|
|
109
|
+
return { ...this.flow.status };
|
|
110
|
+
}
|
|
111
|
+
async cancel() {
|
|
112
|
+
if (this.flow?.status.phase === "waiting")
|
|
113
|
+
this.finish(this.flow, "cancelled", `${this.spec.account} sign-in cancelled.`);
|
|
114
|
+
await this.flow?.stopping;
|
|
115
|
+
if (this.flow?.terminationFailed)
|
|
116
|
+
throw new Error(this.flow.status.message);
|
|
117
|
+
if (this.command && !await this.stopChild(this.command)) {
|
|
118
|
+
throw new Error(`${this.spec.product} could not be stopped on this server. Ask the server administrator to stop the account command before trying again.`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async dispose() {
|
|
122
|
+
this.disposed = true;
|
|
123
|
+
await this.cancel();
|
|
124
|
+
this.flow = null;
|
|
125
|
+
}
|
|
126
|
+
/** Remove the sign-in the CLI stores for this server account so a
|
|
127
|
+
* different account can connect. A sign-in in progress is never pulled
|
|
128
|
+
* away underneath the browser completing it. */
|
|
129
|
+
async signOut() {
|
|
130
|
+
const { product, account, status, logoutArgs } = this.spec;
|
|
131
|
+
if (!status || !logoutArgs)
|
|
132
|
+
throw new Error(`Signing out of ${account} is not available from here.`);
|
|
133
|
+
if (this.disposed)
|
|
134
|
+
throw new Error("This provider was removed. Refresh Settings before signing out.");
|
|
135
|
+
if (this.flow?.status.phase === "waiting")
|
|
136
|
+
throw new Error(`Finish or cancel the ${account} sign-in in progress before signing out.`);
|
|
137
|
+
await this.flow?.stopping;
|
|
138
|
+
if (this.disposed)
|
|
139
|
+
throw new Error("This provider was removed. Refresh Settings before signing out.");
|
|
140
|
+
const env = this.options.environment();
|
|
141
|
+
const { home, homeKey } = this.home(env, "signing out");
|
|
142
|
+
if (authenticatingHomes.has(homeKey)) {
|
|
143
|
+
throw new Error(`A ${account} sign-in is running for this server account. Finish or cancel it before signing out.`);
|
|
144
|
+
}
|
|
145
|
+
authenticatingHomes.add(homeKey);
|
|
146
|
+
try {
|
|
147
|
+
const before = await this.exec([...status.args], env, home);
|
|
148
|
+
const state = status.read(before.code, before.output);
|
|
149
|
+
if (state === "signed-out")
|
|
150
|
+
return;
|
|
151
|
+
if (state !== "signed-in") {
|
|
152
|
+
throw new Error(`${product} did not confirm a ${account} sign-in. Other authentication methods will not be removed; ask the server administrator to check the account.`);
|
|
153
|
+
}
|
|
154
|
+
const logout = await this.exec([...logoutArgs], env, home);
|
|
155
|
+
if (logout.code !== 0)
|
|
156
|
+
throw new Error(`${product} could not remove the sign-in on this server. Check the server's ${product} installation and try again.`);
|
|
157
|
+
// The command's own report is not enough: confirm with the same status
|
|
158
|
+
// check that decides whether bots may run on this account.
|
|
159
|
+
const after = await this.exec([...status.args], env, home);
|
|
160
|
+
if (status.read(after.code, after.output) !== "signed-out") {
|
|
161
|
+
throw new Error(`${product} still reports a sign-in on this server. Update ${product} and check its login status before trying again.`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
// If the OS cannot stop a command, it may still change credentials.
|
|
166
|
+
// Keep the home reserved until that exact child actually exits.
|
|
167
|
+
const child = this.command;
|
|
168
|
+
if (child && child.exitCode === null && child.signalCode === null) {
|
|
169
|
+
child.once("close", () => authenticatingHomes.delete(homeKey));
|
|
170
|
+
}
|
|
171
|
+
else
|
|
172
|
+
authenticatingHomes.delete(homeKey);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** After a login command succeeded: the status command when the CLI has
|
|
176
|
+
* one, else the stored sign-in a turn checks for. */
|
|
177
|
+
confirm(flow, env, home, done) {
|
|
178
|
+
const status = this.spec.status;
|
|
179
|
+
if (status) {
|
|
180
|
+
this.run(flow, [...status.args], env, home, (code, output) => done(status.read(code, output) === "signed-in"));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
let signedIn = false;
|
|
184
|
+
try {
|
|
185
|
+
signedIn = this.spec.signedIn?.(env) === true;
|
|
186
|
+
}
|
|
187
|
+
catch { /* unreadable: not signed in */ }
|
|
188
|
+
done(signedIn);
|
|
189
|
+
}
|
|
190
|
+
startFailure(error) {
|
|
191
|
+
const { product, install } = this.spec;
|
|
192
|
+
return error.code === "ENOENT"
|
|
193
|
+
? `${product} is not installed on this server. ${install}`
|
|
194
|
+
: `${product} could not start on this server. Check the configured CLI path and its executable permissions.`;
|
|
195
|
+
}
|
|
196
|
+
/** One bounded, non-interactive command. Its output stays here; callers
|
|
197
|
+
* see an exit code and a status-line match, never the text. */
|
|
198
|
+
exec(args, env, cwd) {
|
|
199
|
+
const { product } = this.spec;
|
|
200
|
+
return new Promise((resolveExec, rejectExec) => {
|
|
201
|
+
if (this.disposed) {
|
|
202
|
+
rejectExec(new Error("This provider was removed. Refresh Settings before signing out."));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
let child;
|
|
206
|
+
try {
|
|
207
|
+
child = spawnCli(this.options.cli, args, { env: { ...env, NO_COLOR: "1" }, cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
rejectExec(new Error(`${product} could not start on this server. Install or update the configured ${product} CLI, then try again.`));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
this.command = child;
|
|
214
|
+
child.stdin.end();
|
|
215
|
+
let output = "";
|
|
216
|
+
const receive = (chunk) => {
|
|
217
|
+
if (output.length < MAX_OUTPUT)
|
|
218
|
+
output += chunk.toString("utf8").slice(0, MAX_OUTPUT - output.length);
|
|
219
|
+
};
|
|
220
|
+
child.stdout.on("data", receive);
|
|
221
|
+
child.stderr.on("data", receive);
|
|
222
|
+
// A hung CLI must not hold the credential-home lock forever.
|
|
223
|
+
const timer = setTimeout(() => {
|
|
224
|
+
void this.stopChild(child).catch(() => false).then((stopped) => {
|
|
225
|
+
if (!stopped)
|
|
226
|
+
rejectExec(new Error(`${product} could not be stopped on this server. Ask the server administrator to stop the account command before trying again.`));
|
|
227
|
+
});
|
|
228
|
+
}, this.options.startupTimeoutMs ?? 30_000);
|
|
229
|
+
timer.unref();
|
|
230
|
+
child.once("error", (error) => {
|
|
231
|
+
clearTimeout(timer);
|
|
232
|
+
rejectExec(new Error(this.startFailure(error)));
|
|
233
|
+
});
|
|
234
|
+
child.once("close", (code) => {
|
|
235
|
+
clearTimeout(timer);
|
|
236
|
+
if (this.command === child)
|
|
237
|
+
this.command = null;
|
|
238
|
+
resolveExec({ code, output: stripVTControlCharacters(output) });
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
run(flow, args, env, cwd, done, parsePrompt = false) {
|
|
243
|
+
const { product, provider } = this.spec;
|
|
244
|
+
if (flow.status.phase !== "waiting")
|
|
245
|
+
return;
|
|
246
|
+
let child;
|
|
247
|
+
try {
|
|
248
|
+
child = spawnCli(this.options.cli, args, { env: { ...env, NO_COLOR: "1" }, cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
this.finish(flow, "failed", `${product} could not start on this server. Install or update the configured ${product} CLI, then try again.`);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
flow.child = child;
|
|
255
|
+
child.stdin.end();
|
|
256
|
+
let output = "";
|
|
257
|
+
const receive = (chunk) => {
|
|
258
|
+
if (flow.status.phase !== "waiting")
|
|
259
|
+
return;
|
|
260
|
+
if (output.length + chunk.length > MAX_OUTPUT) {
|
|
261
|
+
this.finish(flow, "failed", `${product} returned an unexpected sign-in response. Update the server's ${product} CLI and try again.`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
output += chunk.toString("utf8");
|
|
265
|
+
const prompt = parsePrompt && !flow.ready ? deviceSignInPrompt(provider, stripVTControlCharacters(output)) : null;
|
|
266
|
+
if (prompt) {
|
|
267
|
+
flow.status = { ...flow.status, ...prompt };
|
|
268
|
+
flow.ready = true;
|
|
269
|
+
clearTimeout(flow.startupTimer);
|
|
270
|
+
flow.resolve({ ...flow.status, phase: "waiting" });
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
child.stdout.on("data", receive);
|
|
274
|
+
child.stderr.on("data", receive);
|
|
275
|
+
child.once("error", (error) => this.finish(flow, "failed", this.startFailure(error)));
|
|
276
|
+
child.once("close", (code) => {
|
|
277
|
+
if (flow.child === child)
|
|
278
|
+
flow.child = null;
|
|
279
|
+
if (flow.status.phase === "waiting")
|
|
280
|
+
done(code, stripVTControlCharacters(output));
|
|
281
|
+
output = "";
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
finish(flow, phase, message) {
|
|
285
|
+
const { product, account } = this.spec;
|
|
286
|
+
if (flow.status.phase !== "waiting")
|
|
287
|
+
return;
|
|
288
|
+
clearTimeout(flow.startupTimer);
|
|
289
|
+
clearTimeout(flow.expiryTimer);
|
|
290
|
+
flow.status = {
|
|
291
|
+
phase, flowId: flow.status.flowId, authorizationUrl: null, expiresAt: null,
|
|
292
|
+
...(phase !== "succeeded" && message ? { message } : {}),
|
|
293
|
+
};
|
|
294
|
+
if (!flow.ready) {
|
|
295
|
+
if (phase === "succeeded")
|
|
296
|
+
flow.resolve({ ...flow.status, phase });
|
|
297
|
+
else
|
|
298
|
+
flow.reject(new Error(message ?? `${account} sign-in did not finish.`));
|
|
299
|
+
}
|
|
300
|
+
flow.stopping = this.stopChild(flow.child).catch(() => false).then((stopped) => {
|
|
301
|
+
const release = () => authenticatingHomes.delete(flow.homeKey);
|
|
302
|
+
if (stopped) {
|
|
303
|
+
release();
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
flow.terminationFailed = true;
|
|
307
|
+
flow.status = { ...flow.status, phase: "failed", message: `${product} could not be stopped on this server. Ask the server administrator to stop the login process before trying again.` };
|
|
308
|
+
// Retain the credential-home lock until the OS actually reports exit.
|
|
309
|
+
const child = flow.child;
|
|
310
|
+
if (!child || child.exitCode !== null || child.signalCode !== null)
|
|
311
|
+
release();
|
|
312
|
+
else
|
|
313
|
+
child.once("close", release);
|
|
314
|
+
});
|
|
315
|
+
if (phase === "succeeded")
|
|
316
|
+
void this.options.onAuthenticated?.().catch(() => { });
|
|
317
|
+
}
|
|
318
|
+
async stopChild(child) {
|
|
319
|
+
if (!child || await killCliTree(child, this.options.terminateTimeoutMs ?? 1500))
|
|
320
|
+
return true;
|
|
321
|
+
// A broken CLI may ignore SIGTERM. Do not leave a stale device flow able
|
|
322
|
+
// to write credentials after the user cancelled it.
|
|
323
|
+
if (process.platform !== "win32" && child.pid) {
|
|
324
|
+
try {
|
|
325
|
+
process.kill(-child.pid, "SIGKILL");
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
child.kill("SIGKILL");
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
else
|
|
332
|
+
child.kill("SIGKILL");
|
|
333
|
+
await new Promise((resolveStop) => {
|
|
334
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
335
|
+
return resolveStop();
|
|
336
|
+
const timer = setTimeout(resolveStop, 1500);
|
|
337
|
+
timer.unref();
|
|
338
|
+
child.once("close", () => { clearTimeout(timer); resolveStop(); });
|
|
339
|
+
});
|
|
340
|
+
return child.exitCode !== null || child.signalCode !== null;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
@@ -10,6 +10,8 @@ import { createChatToolApproval } from "./chat-tool-approval.js";
|
|
|
10
10
|
import { ChatProtocolError, ChatReasoningDetails, ChatToolCalls, object } from "./openai-chat-protocol.js";
|
|
11
11
|
import { appendNative } from "./native.js";
|
|
12
12
|
import { classifyError, computeBackoff, interruptibleDelay, RETRY_MAX_ATTEMPTS } from "./retry.js";
|
|
13
|
+
import { classifyContinuable, writeTurnHandoff } from "../turn-continuation.js";
|
|
14
|
+
import { KEY_REJECTED_REASON, keyRejected, noteKeyAccepted, noteKeyRejected, rejectsKey } from "../key-rejections.js";
|
|
13
15
|
/** The message of a JSON error body a provider returned with HTTP 200.
|
|
14
16
|
* MiniMax reports auth, balance, and parameter failures as `base_resp`. */
|
|
15
17
|
function providerError(json) {
|
|
@@ -87,6 +89,53 @@ function rejectsReasoningReplay(status, body) {
|
|
|
87
89
|
return typeof message === "string" && /\breasoning_content\b/.test(message) &&
|
|
88
90
|
/\b(?:unsupported|not supported|unknown|unrecognized|unexpected|not permitted|not allowed)\b/i.test(message);
|
|
89
91
|
}
|
|
92
|
+
/** Groq checks each generated tool call against the request and fails the
|
|
93
|
+
* completion with `tool_use_failed` when the call is invalid: most often a
|
|
94
|
+
* tool it was not offered (gpt-oss inventing a "json" tool to answer in
|
|
95
|
+
* JSON), sometimes arguments that miss the schema. `refusal` is the
|
|
96
|
+
* provider's whole error object, `failed_generation` included. */
|
|
97
|
+
class RejectedToolCallError extends ChatProtocolError {
|
|
98
|
+
refusal;
|
|
99
|
+
constructor(message, refusal) {
|
|
100
|
+
super(message);
|
|
101
|
+
this.refusal = refusal;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const toolCallRefusal = (error) => {
|
|
105
|
+
const body = object(error);
|
|
106
|
+
return body?.code === "tool_use_failed" ? body : undefined;
|
|
107
|
+
};
|
|
108
|
+
function refusedToolCall(status, body) {
|
|
109
|
+
if (status !== 400)
|
|
110
|
+
return undefined;
|
|
111
|
+
try {
|
|
112
|
+
return toolCallRefusal(object(JSON.parse(body))?.error);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function completionError(json, label) {
|
|
119
|
+
const message = providerError(json);
|
|
120
|
+
if (!message)
|
|
121
|
+
return null;
|
|
122
|
+
const text = `provider returned a ${label}: ${message.slice(0, 200)}`;
|
|
123
|
+
const refusal = toolCallRefusal(json.error);
|
|
124
|
+
return refusal ? new RejectedToolCallError(text, refusal) : new ChatProtocolError(text);
|
|
125
|
+
}
|
|
126
|
+
/** What a failed turn says once the provider refused every attempt: what
|
|
127
|
+
* happened, the next step, then the provider's words. The refused call ran
|
|
128
|
+
* nothing, but tool calls from earlier steps of the turn did, and a Retry
|
|
129
|
+
* of the whole request would repeat them. */
|
|
130
|
+
function refusedToolCallFailure(error, earlierSteps) {
|
|
131
|
+
const words = typeof error.refusal.message === "string" ? error.refusal.message : error.message;
|
|
132
|
+
const tool = /attempted to call tool '([^']+)'/.exec(words)?.[1];
|
|
133
|
+
const what = tool ? `tried to use a tool it was not given ("${tool}")` : "made a tool call the provider rejected";
|
|
134
|
+
const next = earlierSteps
|
|
135
|
+
? "The steps before it already ran, so ask only for what's left."
|
|
136
|
+
: "Nothing ran. Retry; if it keeps happening, rephrase the request or choose another model.";
|
|
137
|
+
return `The model ${what}. ${next} Provider: ${words.slice(0, 200)}`;
|
|
138
|
+
}
|
|
90
139
|
/** Shared runtime for the three providers that speak OpenAI chat completions. */
|
|
91
140
|
export function createOpenAIChatRuntime(options) {
|
|
92
141
|
const { input } = options;
|
|
@@ -134,14 +183,20 @@ export function createOpenAIChatRuntime(options) {
|
|
|
134
183
|
});
|
|
135
184
|
if (!response.ok) {
|
|
136
185
|
const body = await response.text().catch(() => "");
|
|
186
|
+
if (rejectsKey(response.status, body))
|
|
187
|
+
noteKeyRejected(options.apiUrl, options.apiKey);
|
|
137
188
|
const message = `${options.httpErrorLabel} HTTP ${response.status}${body ? `: ${body.slice(0, 200)}` : ""}`;
|
|
138
189
|
if (rejectsToolsParameter(response.status, body))
|
|
139
190
|
throw new UnsupportedChatToolsError(message);
|
|
191
|
+
const refusal = refusedToolCall(response.status, body);
|
|
192
|
+
if (refusal)
|
|
193
|
+
throw new RejectedToolCallError(message, refusal);
|
|
140
194
|
if (messages.some((entry) => entry.reasoning_content !== undefined) && rejectsReasoningReplay(response.status, body)) {
|
|
141
195
|
throw new UnsupportedReasoningReplayError(message);
|
|
142
196
|
}
|
|
143
197
|
throw new Error(message);
|
|
144
198
|
}
|
|
199
|
+
noteKeyAccepted(options.apiUrl, options.apiKey);
|
|
145
200
|
if (!stream || response.headers.get("content-type")?.includes("application/json")) {
|
|
146
201
|
const json = await response.json();
|
|
147
202
|
if (onUsage) {
|
|
@@ -155,9 +210,9 @@ export function createOpenAIChatRuntime(options) {
|
|
|
155
210
|
costUsd: count(json.usage?.cost),
|
|
156
211
|
});
|
|
157
212
|
}
|
|
158
|
-
const bodyError =
|
|
213
|
+
const bodyError = completionError(json, "completion error");
|
|
159
214
|
if (bodyError)
|
|
160
|
-
throw
|
|
215
|
+
throw bodyError;
|
|
161
216
|
const message = json.choices?.[0]?.message;
|
|
162
217
|
if (!message || !object(message) || !["content", "reasoning_content", "reasoning", "reasoning_details", "tool_calls", "function_call"].some((key) => key in message)) {
|
|
163
218
|
throw new ChatProtocolError("provider returned no completion message");
|
|
@@ -219,9 +274,9 @@ export function createOpenAIChatRuntime(options) {
|
|
|
219
274
|
malformedFrame = true;
|
|
220
275
|
return false;
|
|
221
276
|
}
|
|
222
|
-
const chunkError =
|
|
277
|
+
const chunkError = completionError(chunk, "streaming completion error");
|
|
223
278
|
if (chunkError)
|
|
224
|
-
throw
|
|
279
|
+
throw chunkError;
|
|
225
280
|
const choice = chunk.choices?.find((row) => row.index === undefined || row.index === 0);
|
|
226
281
|
const delta = choice?.delta;
|
|
227
282
|
if (object(delta))
|
|
@@ -271,9 +326,9 @@ export function createOpenAIChatRuntime(options) {
|
|
|
271
326
|
catch {
|
|
272
327
|
body = undefined;
|
|
273
328
|
}
|
|
274
|
-
const bodyError = body ?
|
|
329
|
+
const bodyError = body ? completionError(body, "completion error") : null;
|
|
275
330
|
if (bodyError)
|
|
276
|
-
throw
|
|
331
|
+
throw bodyError;
|
|
277
332
|
}
|
|
278
333
|
throw new ChatProtocolError("Stream ended before completion");
|
|
279
334
|
}
|
|
@@ -411,7 +466,7 @@ export function createOpenAIChatRuntime(options) {
|
|
|
411
466
|
if (!parsed.ok)
|
|
412
467
|
throw new Error(parsed.error);
|
|
413
468
|
const scope = parsed.scope;
|
|
414
|
-
tools = await mountChatTools(options.tools === false ? undefined : turn.integrations, abort.signal, options.computerUse, scope);
|
|
469
|
+
tools = await mountChatTools(options.tools === false ? undefined : turn.integrations, abort.signal, options.computerUse, scope, turn.mcpCallTimeoutMs);
|
|
415
470
|
const questionAllowed = options.tools !== false && allowsTool(scope, { kind: "native", name: ASK_USER_TOOL });
|
|
416
471
|
let optionalQuestionOnly = questionAllowed && tools.definitions.length === 0;
|
|
417
472
|
// The runtime's one built-in tool rides the same list: ask_user is
|
|
@@ -436,6 +491,7 @@ export function createOpenAIChatRuntime(options) {
|
|
|
436
491
|
let completion;
|
|
437
492
|
for (;;) {
|
|
438
493
|
let streamed = false;
|
|
494
|
+
let answerStreamed = false;
|
|
439
495
|
const pending = { assistant_text: "", reasoning_text: "" };
|
|
440
496
|
const delta = (text, streamKind, flush = false) => {
|
|
441
497
|
let combined = pending[streamKind] + text;
|
|
@@ -464,6 +520,8 @@ export function createOpenAIChatRuntime(options) {
|
|
|
464
520
|
try {
|
|
465
521
|
completion = await complete(messages, model, true, abort.signal, (text, streamKind) => {
|
|
466
522
|
streamed = true;
|
|
523
|
+
if (streamKind === "assistant_text")
|
|
524
|
+
answerStreamed = true;
|
|
467
525
|
delta(text, streamKind);
|
|
468
526
|
}, tools.definitions);
|
|
469
527
|
delta("", "assistant_text", true);
|
|
@@ -483,6 +541,20 @@ export function createOpenAIChatRuntime(options) {
|
|
|
483
541
|
delete message.reasoning_content;
|
|
484
542
|
continue;
|
|
485
543
|
}
|
|
544
|
+
// A refused tool call ran nothing, so the same request goes
|
|
545
|
+
// again; the model usually answers properly next time. Not once
|
|
546
|
+
// answer text streamed: the phones' live bubble would join it to
|
|
547
|
+
// the next attempt's. Streamed thinking stays thinking. Each
|
|
548
|
+
// refusal, failed_generation included, goes to the native log.
|
|
549
|
+
if (error instanceof RejectedToolCallError) {
|
|
550
|
+
native("in", { refused: "tool_use_failed", attempt: attempt + 1, error: error.refusal });
|
|
551
|
+
if (!answerStreamed && !abort.signal.aborted && attempt < RETRY_MAX_ATTEMPTS - 1) {
|
|
552
|
+
emit({ ...base(turn.threadId, turnId), type: "turn.retrying", attempt: ++attempt, delayMs: 0, reason: "tool_use_failed" });
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
if (!abort.signal.aborted)
|
|
556
|
+
throw new ChatProtocolError(refusedToolCallFailure(error, seenCalls.size > 0));
|
|
557
|
+
}
|
|
486
558
|
// Only our optional question changed a previously plain request.
|
|
487
559
|
// A structured parameter rejection has executed nothing; never
|
|
488
560
|
// downgrade mounted tools, streamed output or a handled call.
|
|
@@ -526,6 +598,9 @@ export function createOpenAIChatRuntime(options) {
|
|
|
526
598
|
continue;
|
|
527
599
|
}
|
|
528
600
|
if (toolFailed) {
|
|
601
|
+
// A failed or denied tool op ends the turn: the final answer is
|
|
602
|
+
// not an execution receipt, and a person's "no" must never be
|
|
603
|
+
// answered with a prompt to run the same thing again.
|
|
529
604
|
stopReason = "tool_error";
|
|
530
605
|
throw new ChatProtocolError("One or more tool operations failed or were denied. See the tool results; the final response is not an execution receipt.");
|
|
531
606
|
}
|
|
@@ -680,6 +755,27 @@ export function createOpenAIChatRuntime(options) {
|
|
|
680
755
|
if (failure && (!abort.signal.aborted || cleanupFailed)) {
|
|
681
756
|
emit({ ...base(turn.threadId, turnId), type: "runtime.error", message: failure, terminal: !abort.signal.aborted });
|
|
682
757
|
}
|
|
758
|
+
// Automatic continuity: a resumable terminal (budget cap or tool
|
|
759
|
+
// errors) persists a handoff and raises cap.exhausted so the harness
|
|
760
|
+
// can start a `Continue:` thread. Interruptions and provider config
|
|
761
|
+
// errors are excluded by classifyContinuable.
|
|
762
|
+
if (!ok && !abort.signal.aborted) {
|
|
763
|
+
const continuable = classifyContinuable(stopReason, failure);
|
|
764
|
+
if (continuable) {
|
|
765
|
+
const handoffPath = writeTurnHandoff({
|
|
766
|
+
threadId: turn.threadId,
|
|
767
|
+
turnId,
|
|
768
|
+
model,
|
|
769
|
+
usage,
|
|
770
|
+
hasUsage,
|
|
771
|
+
failure,
|
|
772
|
+
toolCalls: seenCalls.size,
|
|
773
|
+
messages,
|
|
774
|
+
});
|
|
775
|
+
if (handoffPath)
|
|
776
|
+
emit({ ...base(turn.threadId, turnId), type: "cap.exhausted", handoffPath, reason: continuable });
|
|
777
|
+
}
|
|
778
|
+
}
|
|
683
779
|
active.delete(turn.threadId);
|
|
684
780
|
emit({ ...base(turn.threadId, turnId), type: "turn.completed", ok, stopReason, cost: null,
|
|
685
781
|
...(hasUsage && (options.includeUsageInCompleted || seenCalls.size) ? { usage } : {}),
|
|
@@ -699,9 +795,13 @@ export function createOpenAIChatRuntime(options) {
|
|
|
699
795
|
return options.models();
|
|
700
796
|
},
|
|
701
797
|
...(options.refreshModels ? { refreshModels: options.refreshModels } : {}),
|
|
702
|
-
snapshot: async () =>
|
|
703
|
-
|
|
704
|
-
|
|
798
|
+
snapshot: async () => {
|
|
799
|
+
if (!options.apiKey)
|
|
800
|
+
return { state: "unavailable", reason: options.unavailableReason };
|
|
801
|
+
if (keyRejected(options.apiUrl, options.apiKey))
|
|
802
|
+
return { state: "available", authenticated: false, reason: KEY_REJECTED_REASON, version: null };
|
|
803
|
+
return { state: "available", authenticated: true, version: null, ...(options.billing ? { billing: options.billing } : {}) };
|
|
804
|
+
},
|
|
705
805
|
adapter: {
|
|
706
806
|
provider: options.driverKind,
|
|
707
807
|
capabilities: { ...(options.computerUse ? { computerMcp: options.tools !== false,
|