openmausbot 0.1.92 → 0.1.94
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-Do6lBa4C.js → DesktopViewer-bxM7NV3N.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-Cd3X7Hxc.js → abnfDiagram-O67JEVCF-BhMdJhRJ.js} +1 -1
- package/dist/assets/{arc-DIYc7Ymx.js → arc-B5itKDcx.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-DBmRGVyP.js → architectureDiagram-NJMV4G6O-C5d7UY6U.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CPFePSEE.js → blockDiagram-BEXU5L5S-BrZIhMmu.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-DD_DO1qy.js → c4Diagram-YGBWAQC7-7I0ArfbD.js} +1 -1
- package/dist/assets/channel-CdJX6i8k.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BNEeNSjX.js → chunk-2Q5K7J3B-BnEsyN36.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BbinLaqH.js → chunk-3FUC2YCW-BXATQ52s.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-BBgYrgDR.js → chunk-5VM5RSS4-B0BpTkEx.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-ezXj-kA-.js → chunk-GTNCS2PH-BtLTz6FR.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-9FxZ3diV.js → chunk-GWA4HPMP-D6gcZOYt.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CO-40JpI.js → chunk-JWPE2WC7-DCLrosvf.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-BEhjslYE.js → chunk-NETBCI7D-B9JsknV9.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CNeq4n7_.js → chunk-WEXAMYUT-CsoTxw7z.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-OsCY7KSi.js → chunk-XXDRQBXY-0F_frB0c.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-CloRUEtk.js → classDiagram-v2-NBCMYWYE-DkrpnflX.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-D1BGZhp4.js → cose-bilkent-JH36ORCC-BNDwYBt9.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CUf4T2CN.js → cynefin-OW5HDTMX-JUkNr1Mk.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-DPEZrFGA.js → cynefinDiagram-VND7K2PF-DKeh9N4M.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-bDe6GIv5.js → dagre-6A5THRUB-ve-6T_wX.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-CdcUQz55.js → diagram-22UHCM2B-BuJ2d8rT.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-BZpqGg7c.js → diagram-3UASUU5V-BgOs_Ni-.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-DvGM6ugp.js → diagram-ATOU4E4O-DMK6VmcP.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DSTmMm0s.js → diagram-CDSNMT55-DksASlcY.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-15cucnMn.js → diagram-MLGK6HIB-Bgn8I5k6.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-tMbuaE6n.js → diagram-MPIPVDR6-zK0jqO8N.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BYxYzjHI.js → ebnfDiagram-ZINNZB2B-vKrbzTwr.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-v6bj852W.js → elk-276RUBZZ-Bzv2mPNf.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-5lBBlnd2.js → erDiagram-OPXOYQCR-CvOyYuxu.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-CZyl1zIj.js → flowDiagram-KWPJA3E3-CtbnDbc0.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-BiAI-ltZ.js → ganttDiagram-FUAMR5RP-c4dMRYyG.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-BAZISkj3.js → gitGraphDiagram-X574FWY7-B34uaQho.js} +1 -1
- package/dist/assets/index-CPYrGsv1.js +610 -0
- package/dist/assets/{index-DGBaEXrM.js → index-CPiqc_DN.js} +1 -1
- package/dist/assets/index-CzT42GR8.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-B6ooOy-Z.js → infoDiagram-VRGFBTTK-o64ma4CI.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js → ishikawaDiagram-OU5B5YK6-DV4C9BAm.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DWqlzIEe.js → journeyDiagram-ZHPQQLJL-Bf0AHk1H.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-JX6piDwL.js → kanban-definition-PNTS6WVX-B1UMSKmE.js} +1 -1
- package/dist/assets/{linear-CmOWzX0P.js → linear-BpKhuyLm.js} +1 -1
- package/dist/assets/{mermaid.core-CF1zwoQ-.js → mermaid.core-B_BKNCw5.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-CGh2K4fW.js → mindmap-definition-NLK3R4M7-DeARruT1.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-CDwYN0s7.js → pegDiagram-GJSIUBJH-BOQEk_1D.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-1rfjJocB.js → pieDiagram-5QR66LMP-EIfoiTZo.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-tKPvPiok.js → quadrantDiagram-O4NWA36T-GgZwT-fi.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BHLm5vXR.js → railroadDiagram-XR7U4H2S-D9rDQomj.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CB45STp7.js → requirementDiagram-PLB6GJNP-DAh1MIeo.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-55QA_huC.js → sankeyDiagram-IPEJSGJF-CPr6Uz64.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-BckFmQHn.js → sequenceDiagram-PO4LG4MO-BU6VIdcm.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BFCOJF11.js → sizeCapture-INFHLROL-qF2HO41r.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-CZvmiVVN.js → stateDiagram-v2-GCMORJYK-DlJ8G4MD.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-BpDUCEKy.js → swimlanes-2SLR337P-DzYE8I54.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D-U4GtLQ.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BpxHq6IK.js → timeline-definition-EJHVYXUP-B7wzEQ0A.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-Cj66vm02.js → usecaseDiagram-POWQR4AR-BvGgrxIP.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-C9nRkWE_.js → vennDiagram-UO4OBE2U-CEECdgQg.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-D-me_nWj.js → wardleyDiagram-VNRHLVJA-XQHA0P_P.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-Dc_fL4mk.js → xychartDiagram-PMCCYNJV-B5mLcZI2.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +1 -0
- package/dist-server/companion/src/routes.js +45 -1
- package/dist-server/container-mcp.js +99 -77
- package/dist-server/drivers/agents-proxy.js +40 -1
- package/dist-server/drivers/pi-mcp-extension.ts +245 -299
- package/dist-server/enterprise/server/index.js +1 -0
- package/dist-server/index.js +18361 -13794
- package/dist-server/local-computer-proxy.js +5 -1
- package/dist-server/local-computer.js +1 -0
- package/dist-server/mcp-gate.js +152 -20
- package/dist-server/mcp-remote-proxy.js +437 -0
- package/dist-server/openmausbot.js +8872 -7620
- package/dist-server/pair-cli.js +8872 -7620
- package/dist-server/permission-proxy.js +65 -1
- package/dist-server/proxy-paths.js +1 -0
- package/dist-server/server/auto-approve.js +33 -11
- package/dist-server/server/boat.js +3 -3
- package/dist-server/server/browser-live.js +14 -5
- package/dist-server/server/browser-runtime.js +6 -0
- package/dist-server/server/channel-queue.js +4 -1
- package/dist-server/server/cli.js +32 -12
- package/dist-server/server/cloud-move-http.js +9 -3
- package/dist-server/server/cloud-move.js +18 -0
- package/dist-server/server/cloud-owner.js +1 -1
- package/dist-server/server/computer-control.js +6 -0
- package/dist-server/server/computer-wait.js +33 -13
- package/dist-server/server/config.js +57 -10
- package/dist-server/server/container-computer.js +43 -32
- package/dist-server/server/decision-log.js +9 -6
- package/dist-server/server/delegations.js +47 -22
- package/dist-server/server/digest.js +12 -2
- package/dist-server/server/drivers/acp/core.js +229 -20
- package/dist-server/server/drivers/acp/cursor.js +13 -1
- package/dist-server/server/drivers/acp/grok.js +208 -3
- package/dist-server/server/drivers/acp/quiet-status.js +257 -0
- package/dist-server/server/drivers/acp/qwen.js +18 -0
- package/dist-server/server/drivers/agents-call.js +28 -0
- package/dist-server/server/drivers/agents-catalog.js +10 -1
- package/dist-server/server/drivers/boatagent.js +2 -0
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/cerebras.js +108 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +45 -15
- package/dist-server/server/drivers/claude.js +35 -9
- package/dist-server/server/drivers/codex.js +84 -10
- package/dist-server/server/drivers/openai-chat.js +37 -5
- package/dist-server/server/drivers/pi.js +48 -4
- package/dist-server/server/drivers/prompt-split.js +2 -0
- package/dist-server/server/env-path.js +6 -2
- package/dist-server/server/environment.js +3 -0
- package/dist-server/server/guarded-requests.js +2 -1
- package/dist-server/server/index.js +1353 -311
- package/dist-server/server/live-call-controller.js +868 -0
- package/dist-server/server/live-call.js +216 -0
- package/dist-server/server/local-vm-stop-reason.js +26 -0
- package/dist-server/server/mcp-gate-config.js +43 -12
- package/dist-server/server/mcp-gate.js +127 -25
- package/dist-server/server/mcp-http.js +46 -8
- package/dist-server/server/mcp-remote-proxy.js +108 -0
- package/dist-server/server/new-bot-defaults.js +3 -0
- package/dist-server/server/peer-provenance.js +4 -2
- package/dist-server/server/permission-proxy.js +15 -1
- package/dist-server/server/plan-usage.js +797 -0
- package/dist-server/server/provider-key-check.js +2 -1
- package/dist-server/server/proxy-paths.js +1 -0
- package/dist-server/server/redact.js +2 -0
- package/dist-server/server/request-auth.js +3 -2
- package/dist-server/server/routes/desktop-viewer.js +50 -6
- package/dist-server/server/routes/live.js +98 -0
- package/dist-server/server/routines.js +6 -3
- package/dist-server/server/skill-library.js +207 -0
- package/dist-server/server/skills-library-migration.js +235 -0
- package/dist-server/server/skills.js +66 -3
- package/dist-server/server/steer-queue.js +13 -1
- package/dist-server/server/store.js +86 -4
- package/dist-server/server/surface.js +9 -3
- package/dist-server/server/team-backup.js +2 -1
- package/dist-server/server/thread-events.js +1 -1
- package/dist-server/server/tunnel.js +20 -1
- package/dist-server/server/turn-log.js +10 -0
- package/dist-server/server/turn-resources.js +124 -0
- package/dist-server/server/vps-computer.js +1 -1
- package/dist-server/server/workspace-backup.js +22 -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 +13 -0
- package/dist-server/shared/call-consent.js +28 -0
- package/dist-server/shared/live-approval.js +124 -0
- package/dist-server/shared/live-call.js +176 -0
- package/dist-server/shared/redact.js +1 -0
- package/dist-server/shared/skill-md.js +10 -0
- package/dist-server/shared/team-backup.js +3 -0
- package/dist-server/shared/tool-name.js +22 -0
- package/dist-server/shared/tool-scope-support.js +25 -0
- package/dist-server/shared/tool-scope.js +136 -0
- package/dist-server/src/lib/thread-refs.js +406 -0
- package/dist-server/vps-container-mcp.js +99 -77
- package/dist-server/workspace-backup.worker.js +103 -100
- package/enterprise/server/index.js +1 -0
- package/package.json +1 -1
- package/dist/assets/channel-Dtk5Mtic.js +0 -1
- package/dist/assets/index-Cdmjrl28.css +0 -1
- package/dist/assets/index-DR7YPMkW.js +0 -597
- package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +0 -8
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
// The generic protocol runtime lives in acp/core.ts; this file is only the
|
|
5
5
|
// per-harness quirks. Verified against grok 1.0.0.
|
|
6
6
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
import {
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
9
|
+
import { parse as parseYaml } from "yaml";
|
|
10
|
+
import { harnessHome, splitCliString } from "../../env-path.js";
|
|
9
11
|
import { decodeInjectId, hostApiKey, localHost, mergeLocalInject } from "../local-inject.js";
|
|
10
12
|
import { createAcpDriver } from "./core.js";
|
|
13
|
+
import { allowsTool, canUseMcpServer, narrowsNativeTools, parseToolScope } from "../../../shared/tool-scope.js";
|
|
11
14
|
export const STATIC_GROK_MODELS = {
|
|
12
15
|
default: "grok-4.7",
|
|
13
16
|
options: [
|
|
@@ -181,6 +184,140 @@ export function grokAcceptsUnadvertisedImages(init) {
|
|
|
181
184
|
const version = /^1\.0\.(\d+)$/.exec(meta.agentVersion);
|
|
182
185
|
return Boolean(version && Number(version[1]) >= 25);
|
|
183
186
|
}
|
|
187
|
+
// Original names in the official 1.0.41 default registry, not arbitrary
|
|
188
|
+
// owner strings forwarded into Grok's inherit-all profile allowlist.
|
|
189
|
+
const GROK_NATIVE_TOOLS = [
|
|
190
|
+
["read_file", "GrokBuild:read_file"], ["search_replace", "GrokBuild:search_replace"], ["write", "OpenCode:write"],
|
|
191
|
+
["run_terminal_command", "GrokBuild:run_terminal_cmd"], ["list_dir", "GrokBuild:list_dir"], ["grep", "GrokBuild:grep"],
|
|
192
|
+
["kill_command_or_subagent", "GrokBuild:kill_task"], ["todo_write", "GrokBuild:todo_write"],
|
|
193
|
+
["get_command_or_subagent_output", "GrokBuild:get_task_output"], ["spawn_subagent", "GrokBuild:task"],
|
|
194
|
+
["scheduler_create", "GrokBuild:scheduler_create"], ["scheduler_delete", "GrokBuild:scheduler_delete"], ["scheduler_list", "GrokBuild:scheduler_list"],
|
|
195
|
+
["monitor", "GrokBuild:monitor"], ["search_tool", "GrokBuild:search_tool"], ["use_tool", "GrokBuild:use_tool"],
|
|
196
|
+
["workflow", "GrokBuild:workflow"], ["enter_plan_mode", "GrokBuild:enter_plan_mode"], ["exit_plan_mode", "GrokBuild:exit_plan_mode"],
|
|
197
|
+
["ask_user_question", "GrokBuild:ask_user_question"], ["send_feedback", "GrokBuild:send_feedback"],
|
|
198
|
+
["image_gen", "GrokBuild:image_gen"], ["image_edit", "GrokBuild:image_edit"],
|
|
199
|
+
["image_to_video", "GrokBuild:image_to_video"], ["reference_to_video", "GrokBuild:reference_to_video"],
|
|
200
|
+
];
|
|
201
|
+
/** Read a supported on-disk profile rather than overriding its restriction. */
|
|
202
|
+
export function grokInheritedProfile(cli, env, cwd) {
|
|
203
|
+
const unsupported = () => { throw new Error("Grok's existing agent profile cannot be safely intersected with tool selection. Use a profile file with explicit tools or a separate default Grok account."); };
|
|
204
|
+
if ((env.GROK_AGENT && env.GROK_AGENT !== "grok-build") || env.GROK_CONFIG || env.GROK_CONFIG_PATH)
|
|
205
|
+
return unsupported();
|
|
206
|
+
const args = splitCliString(cli);
|
|
207
|
+
// Operator filters are applied after ACP's profile by the CLI. Refuse
|
|
208
|
+
// unverified overrides rather than replacing an inherited restriction.
|
|
209
|
+
if (args.some(arg => /^(--tools|--disallowed-tools|--disallowedTools|--agent)(=|$)/.test(arg)))
|
|
210
|
+
return unsupported();
|
|
211
|
+
const flags = args.flatMap((arg, index) => arg === "--agent-profile" || arg.startsWith("--agent-profile=") ? [index] : []);
|
|
212
|
+
if (flags.length > 1)
|
|
213
|
+
return unsupported();
|
|
214
|
+
const flag = flags[0] ?? -1;
|
|
215
|
+
let path = flag >= 0 ? (args[flag].includes("=") ? args[flag].slice(args[flag].indexOf("=") + 1) : args[flag + 1]) : undefined;
|
|
216
|
+
if (flag >= 0 && (!path || path.startsWith("-")))
|
|
217
|
+
return unsupported();
|
|
218
|
+
const files = [join(env.GROK_HOME || harnessHome("grok", env), "config.toml")];
|
|
219
|
+
for (let directory = resolve(cwd);;) {
|
|
220
|
+
const file = join(directory, ".grok", "config.toml");
|
|
221
|
+
if (!files.includes(file))
|
|
222
|
+
files.push(file);
|
|
223
|
+
const parent = dirname(directory);
|
|
224
|
+
if (parent === directory)
|
|
225
|
+
break;
|
|
226
|
+
directory = parent;
|
|
227
|
+
}
|
|
228
|
+
for (const file of files) {
|
|
229
|
+
if (!existsSync(file))
|
|
230
|
+
continue;
|
|
231
|
+
const text = readFileSync(file, "utf8");
|
|
232
|
+
if (text.length > 262_144)
|
|
233
|
+
return unsupported();
|
|
234
|
+
// Support a deliberately small, unambiguous TOML subset. Scan the whole
|
|
235
|
+
// file: quoted/dotted keys, inline agent tables and multiline constructs
|
|
236
|
+
// must never be silently missed by a section regex. Unsupported syntax
|
|
237
|
+
// refuses this scoped turn; ordinary unrestricted turns are unaffected.
|
|
238
|
+
let section = "";
|
|
239
|
+
for (const line of text.split(/\r?\n/).map(line => line.trim()).filter(line => line && !line.startsWith("#"))) {
|
|
240
|
+
const header = /^\[([A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*)\]\s*(?:#.*)?$/.exec(line);
|
|
241
|
+
if (header) {
|
|
242
|
+
section = header[1];
|
|
243
|
+
if (section.startsWith("agent."))
|
|
244
|
+
return unsupported();
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const assignment = /^([A-Za-z0-9_-]+)\s*=\s*(.+)$/.exec(line);
|
|
248
|
+
if (!assignment || /'''|"""/.test(assignment[2]) || (!section && assignment[1] === "agent"))
|
|
249
|
+
return unsupported();
|
|
250
|
+
if (section !== "agent")
|
|
251
|
+
continue;
|
|
252
|
+
const match = /^(name|definition)\s*=\s*("(?:[^"\\]|\\.)*"|'[^']*')\s*(?:#.*)?$/.exec(line);
|
|
253
|
+
if (!match)
|
|
254
|
+
return unsupported();
|
|
255
|
+
let value;
|
|
256
|
+
try {
|
|
257
|
+
value = match[2].startsWith('"') ? JSON.parse(match[2]) : match[2].slice(1, -1);
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return unsupported();
|
|
261
|
+
}
|
|
262
|
+
// Grok expands environment variables in paths. Do not resolve a
|
|
263
|
+
// different literal file or fall back when that contract is unknown.
|
|
264
|
+
if (value.includes("$") || !value)
|
|
265
|
+
return unsupported();
|
|
266
|
+
if (match[1] === "name" && value !== "grok-build")
|
|
267
|
+
return unsupported();
|
|
268
|
+
if (match[1] === "definition" && flag < 0) {
|
|
269
|
+
if (path !== undefined && path !== value)
|
|
270
|
+
return unsupported();
|
|
271
|
+
path = value;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (!path)
|
|
276
|
+
return;
|
|
277
|
+
const text = readFileSync(resolve(cwd, path), "utf8");
|
|
278
|
+
if (text.length > 262_144)
|
|
279
|
+
return unsupported();
|
|
280
|
+
const match = /^\s*---\r?\n([\s\S]*?)\r?\n---(?:\r?\n([\s\S]*))?$/.exec(text);
|
|
281
|
+
if (!match)
|
|
282
|
+
return unsupported();
|
|
283
|
+
const profile = parseYaml(match[1]);
|
|
284
|
+
if (!profile || typeof profile.name !== "string" || typeof profile.description !== "string"
|
|
285
|
+
|| (profile.tools !== undefined && (!Array.isArray(profile.tools) || !profile.tools.every((tool) => typeof tool === "string")))
|
|
286
|
+
|| (profile.disallowedTools !== undefined && (!Array.isArray(profile.disallowedTools) || !profile.disallowedTools.every((tool) => typeof tool === "string"))))
|
|
287
|
+
return unsupported();
|
|
288
|
+
const toolConfig = profile.toolConfig ?? { tools: GROK_NATIVE_TOOLS.map(([name, id]) => ({ id, name_override: name })) };
|
|
289
|
+
if (!Array.isArray(toolConfig.tools) || !toolConfig.tools.every((tool) => typeof tool?.id === "string" && (tool.name_override === undefined || typeof tool.name_override === "string")))
|
|
290
|
+
return unsupported();
|
|
291
|
+
return { ...profile, name: profile.name, description: profile.description, toolConfig, ...(match[2]?.trim() ? { promptBody: match[2].trim() } : {}) };
|
|
292
|
+
}
|
|
293
|
+
/** A curated registry: empty/unknown selections never inherit Grok's defaults. */
|
|
294
|
+
export function grokToolScopeProfile(scope, init, inherited, hasMcp = false) {
|
|
295
|
+
const parsed = parseToolScope(scope);
|
|
296
|
+
if (!parsed.ok)
|
|
297
|
+
throw new Error(parsed.error);
|
|
298
|
+
const meta = init?._meta;
|
|
299
|
+
if (meta?.grokShell !== true || meta.agentVersion !== "1.0.41") {
|
|
300
|
+
throw new Error("This Grok runtime does not have verified native tool selection. Use Grok 1.0.41 or Pi; update Grok support before using another version.");
|
|
301
|
+
}
|
|
302
|
+
if (hasMcp && (!allowsTool(scope, { kind: "native", name: "search_tool" }) || !allowsTool(scope, { kind: "native", name: "use_tool" }))) {
|
|
303
|
+
throw new Error("Grok requires native:search_tool and native:use_tool to find and call selected MCP tools. Allow both explicitly or use Pi.");
|
|
304
|
+
}
|
|
305
|
+
const candidates = inherited?.toolConfig.tools ?? GROK_NATIVE_TOOLS.map(([name, id]) => ({ id, name_override: name }));
|
|
306
|
+
const selected = candidates.filter((tool) => {
|
|
307
|
+
const name = tool.name_override ?? GROK_NATIVE_TOOLS.find(([, id]) => id === tool.id)?.[0];
|
|
308
|
+
return name !== undefined && allowsTool(scope, { kind: "native", name })
|
|
309
|
+
&& (!inherited?.tools?.length || inherited.tools.includes(name) || inherited.tools.includes(tool.id))
|
|
310
|
+
&& !inherited?.disallowedTools?.some((denied) => denied === name || denied === tool.id);
|
|
311
|
+
});
|
|
312
|
+
// Grok rejects an empty curated construction. A real read entry explicitly
|
|
313
|
+
// disabled by its exact native name constructs an empty final registry.
|
|
314
|
+
return {
|
|
315
|
+
...inherited, name: inherited?.name ?? "openmausbot-tools", description: inherited?.description ?? "Owner-selected tools",
|
|
316
|
+
injectDefaultTools: false, discoverSkills: false,
|
|
317
|
+
toolConfig: { ...inherited?.toolConfig, tools: selected.length ? selected : [{ id: "GrokBuild:read_file", name_override: "read_file" }] },
|
|
318
|
+
tools: [], disallowedTools: [...new Set([...(inherited?.disallowedTools ?? []), ...(selected.length ? [] : ["read_file"])])],
|
|
319
|
+
};
|
|
320
|
+
}
|
|
184
321
|
const support = {
|
|
185
322
|
driverKind: "grokAgent",
|
|
186
323
|
displayName: "Grok",
|
|
@@ -225,17 +362,69 @@ const support = {
|
|
|
225
362
|
? "bypassPermissions"
|
|
226
363
|
: turn.approvalMode === "auto" ? "auto" : turn.approvalMode === "edits" ? "acceptEdits" : "default",
|
|
227
364
|
"agent",
|
|
365
|
+
...(turn.toolScope !== undefined ? ["--no-leader"] : []),
|
|
228
366
|
...(turn.model ? ["-m", turn.model] : []),
|
|
229
367
|
// long form on purpose: `--effort` is documented as an alias, and an
|
|
230
368
|
// alias is the part a CLI is free to rename
|
|
231
369
|
...(turn.effort ? ["--reasoning-effort", turn.effort] : []),
|
|
232
370
|
"stdio",
|
|
233
371
|
],
|
|
372
|
+
toolScopeSessionParams: (turn, init, hasMcp, { config, env, cwd }) => {
|
|
373
|
+
if (!narrowsNativeTools(turn.toolScope))
|
|
374
|
+
return {};
|
|
375
|
+
const profile = grokToolScopeProfile(turn.toolScope, init, grokInheritedProfile(config.cli, env, cwd), hasMcp);
|
|
376
|
+
// Establish on the selected model. Changing harnesses after session/new
|
|
377
|
+
// can discard an ACP profile and restore the model's default tools.
|
|
378
|
+
if (turn.model)
|
|
379
|
+
profile.model = ensureGrokInjectSlug(turn.model, env);
|
|
380
|
+
return { _meta: { agentProfile: profile } };
|
|
381
|
+
},
|
|
382
|
+
toolScopeCacheKey: ({ config, env, cwd }) => JSON.stringify(grokInheritedProfile(config.cli, env, cwd) ?? null),
|
|
234
383
|
// -m on argv is necessary but not sufficient: session/new still starts on
|
|
235
384
|
// [models].default. Pin the slug over the wire, same as Hermes/Droid.
|
|
236
|
-
async configureSession({ request, sessionId, turn }) {
|
|
385
|
+
async configureSession({ request, sessionId, turn, currentModelId }) {
|
|
386
|
+
if (turn.toolScope !== undefined) {
|
|
387
|
+
const available = new Set([
|
|
388
|
+
...(turn.integrations?.agents ? ["agents"] : []), ...(turn.integrations?.composio ? ["composio"] : []),
|
|
389
|
+
...(turn.integrations?.browser ? ["browser"] : []), ...(turn.integrations?.localComputer ? ["computer"] : []),
|
|
390
|
+
...Object.keys(turn.integrations?.custom ?? {}),
|
|
391
|
+
].filter((name) => canUseMcpServer(turn.toolScope, name)));
|
|
392
|
+
const deadline = Date.now() + 10_000;
|
|
393
|
+
for (;;) {
|
|
394
|
+
const reply = await request("_x.ai/mcp/list", { sessionId, cache: true }, 10_000);
|
|
395
|
+
const catalog = reply?.result;
|
|
396
|
+
if (!Array.isArray(catalog?.servers))
|
|
397
|
+
throw new Error("Grok could not confirm its MCP catalog. No prompt was sent.");
|
|
398
|
+
for (const server of catalog.servers) {
|
|
399
|
+
if (server?.session?.enabled === false)
|
|
400
|
+
continue;
|
|
401
|
+
if (!available.has(server?.name))
|
|
402
|
+
throw new Error("Grok has an MCP connection outside the bot's selection. Disable native MCP connections before using tool selection. No prompt was sent.");
|
|
403
|
+
if (Array.isArray(server?.session?.tools) && server.session.tools.some((tool) => typeof tool.name !== "string" || !allowsTool(turn.toolScope, { kind: "mcp", server: server.name, name: tool.name }))) {
|
|
404
|
+
throw new Error("Grok could not enforce the selected MCP catalog. No prompt was sent.");
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (catalog.sessionMcpResolved === true) {
|
|
408
|
+
if ([...available].some((name) => !catalog.servers.some((server) => server.name === name && server.session?.enabled === true && server.session.status === "ready"))) {
|
|
409
|
+
throw new Error("Grok could not connect a selected MCP server. No prompt was sent.");
|
|
410
|
+
}
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
if (Date.now() >= deadline)
|
|
414
|
+
throw new Error("Grok did not confirm its selected MCP catalog in time. No prompt was sent.");
|
|
415
|
+
await delay(50);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
237
418
|
if (!turn.model)
|
|
238
419
|
return;
|
|
420
|
+
if (narrowsNativeTools(turn.toolScope)) {
|
|
421
|
+
if (currentModelId !== turn.model) {
|
|
422
|
+
throw new Error("Grok could not confirm the selected model without replacing its tool profile. Start a new conversation or choose a model with a supported Grok profile. No prompt was sent.");
|
|
423
|
+
}
|
|
424
|
+
// The inline profile already pins this model. Repeating set_model can
|
|
425
|
+
// rebuild the harness, or reject an otherwise valid resumed profile.
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
239
428
|
try {
|
|
240
429
|
await request("session/set_model", { sessionId, modelId: turn.model });
|
|
241
430
|
}
|
|
@@ -249,6 +438,22 @@ const support = {
|
|
|
249
438
|
transformEnv: (env) => {
|
|
250
439
|
delete env.XAI_API_KEY;
|
|
251
440
|
},
|
|
441
|
+
applyTurnEnv: (env, { toolScope }) => {
|
|
442
|
+
if (toolScope !== undefined) {
|
|
443
|
+
// The verified runtime gives these local env switches priority over
|
|
444
|
+
// account defaults, preventing an unfiltered managed gateway fallback.
|
|
445
|
+
env.GROK_MANAGED_MCPS_ENABLED = "false";
|
|
446
|
+
env.GROK_MANAGED_MCP_GATEWAY_TOOLS_ENABLED = "false";
|
|
447
|
+
if (narrowsNativeTools(toolScope)) {
|
|
448
|
+
if (env.GROK_AGENT && env.GROK_AGENT !== "grok-build") {
|
|
449
|
+
throw new Error("Grok's existing agent profile cannot be safely intersected with tool selection. Use a separate default Grok account.");
|
|
450
|
+
}
|
|
451
|
+
// A model's agent_type otherwise takes priority over ACP profiles.
|
|
452
|
+
// Preserve profile-file restrictions when intersecting them below.
|
|
453
|
+
env.GROK_AGENT = "grok-build";
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
},
|
|
252
457
|
// Bind the grok.com subscription login. No API-key fallback by design —
|
|
253
458
|
// an unauthenticated CLI is a user action, not something to paper over.
|
|
254
459
|
pickAuthMethod: (methods) => (methods.some((m) => m.id === "cached_token") ? "cached_token" : null),
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// What an ACP agent is doing while it sends nothing.
|
|
2
|
+
//
|
|
3
|
+
// ACP has no "still working" message, and agents go quiet for minutes in
|
|
4
|
+
// normal work: Qwen Code 0.24 says nothing over the wire while it waits out
|
|
5
|
+
// a rate limit, while its SDK retries a request, or while one slow model
|
|
6
|
+
// call runs. Silence alone cannot tell that from a wedged process, so a
|
|
7
|
+
// quiet turn is probed from outside: the agent's own debug log where it
|
|
8
|
+
// keeps one, and the process tree's open connections and CPU time. The
|
|
9
|
+
// result becomes a plain-words notice in the chat, so the person is never
|
|
10
|
+
// left watching a spinner without knowing why.
|
|
11
|
+
import { execFile } from "node:child_process";
|
|
12
|
+
import { closeSync, fstatSync, openSync, readSync } from "node:fs";
|
|
13
|
+
/** Qwen Code's debug log (QWEN_DEBUG_LOG_FILE=1), lines like
|
|
14
|
+
* `2026-10-02T02:50:41.757Z [WARN] [RETRY] Attempt 1 failed with status 429. Retrying after explicit delay of 20000ms...`.
|
|
15
|
+
* Only the facts worth telling a person are parsed; every other line still
|
|
16
|
+
* counts as proof the process is alive. */
|
|
17
|
+
export function parseQwenLogLine(line) {
|
|
18
|
+
if (line.includes("[RETRY]") && /\bAttempt \d+ failed\b/.test(line)) {
|
|
19
|
+
const attempt = /\bAttempt (\d+) failed/.exec(line);
|
|
20
|
+
const status = /\bwith status (\d{3})\b/.exec(line);
|
|
21
|
+
const explicit = /explicit delay of (\d+)\s*ms/.exec(line);
|
|
22
|
+
const seconds = /Retrying in (\d+)\s*s\b/.exec(line);
|
|
23
|
+
return {
|
|
24
|
+
kind: "retry",
|
|
25
|
+
attempt: attempt ? Number(attempt[1]) : null,
|
|
26
|
+
status: status ? Number(status[1]) : null,
|
|
27
|
+
delayMs: explicit ? Number(explicit[1]) : seconds ? Number(seconds[1]) * 1000 : null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (/\[compaction\] hard-tier rescue triggered|Reactive compression|tryCompress/.test(line)
|
|
31
|
+
&& !/succeeded|failed|skipped|stopped/.test(line)) {
|
|
32
|
+
return { kind: "compressing" };
|
|
33
|
+
}
|
|
34
|
+
const gate = /\[compaction\] cheap-gate NOOP: effectiveTokens=(\d+), auto=(\d+)/.exec(line);
|
|
35
|
+
if (gate)
|
|
36
|
+
return { kind: "context", tokens: Number(gate[1]), threshold: Number(gate[2]) };
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/** Reads the lines appended to a log file since the last read. A file that
|
|
40
|
+
* does not exist yet is simply empty; the first read skips what was already
|
|
41
|
+
* there, so an old conversation's log is not replayed as news. */
|
|
42
|
+
export class LogTail {
|
|
43
|
+
offset = null;
|
|
44
|
+
partial = "";
|
|
45
|
+
path;
|
|
46
|
+
maxBytes;
|
|
47
|
+
constructor(path, maxBytes = 256 * 1024) {
|
|
48
|
+
this.path = path;
|
|
49
|
+
this.maxBytes = maxBytes;
|
|
50
|
+
}
|
|
51
|
+
read() {
|
|
52
|
+
let fd;
|
|
53
|
+
try {
|
|
54
|
+
fd = openSync(this.path, "r");
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
if (this.offset === null)
|
|
58
|
+
this.offset = 0;
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const size = fstatSync(fd).size;
|
|
63
|
+
if (this.offset === null) {
|
|
64
|
+
this.offset = size;
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
// truncated or replaced: start over from its beginning
|
|
68
|
+
if (size < this.offset) {
|
|
69
|
+
this.offset = 0;
|
|
70
|
+
this.partial = "";
|
|
71
|
+
}
|
|
72
|
+
if (size === this.offset)
|
|
73
|
+
return [];
|
|
74
|
+
// a burst bigger than the cap keeps only its tail
|
|
75
|
+
const start = Math.max(this.offset, size - this.maxBytes);
|
|
76
|
+
if (start > this.offset)
|
|
77
|
+
this.partial = "";
|
|
78
|
+
const buf = Buffer.alloc(size - start);
|
|
79
|
+
readSync(fd, buf, 0, buf.length, start);
|
|
80
|
+
this.offset = size;
|
|
81
|
+
const text = this.partial + buf.toString("utf8");
|
|
82
|
+
const lines = text.split(/\r?\n/);
|
|
83
|
+
this.partial = lines.pop() ?? "";
|
|
84
|
+
return lines.filter((l) => l.trim());
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
try {
|
|
91
|
+
closeSync(fd);
|
|
92
|
+
}
|
|
93
|
+
catch { }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const run = (file, args, timeoutMs = 4_000) => new Promise((resolve) => {
|
|
98
|
+
execFile(file, args, { timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024, windowsHide: true }, (err, stdout) => {
|
|
99
|
+
// lsof exits 1 when it finds nothing to list; its stdout is still the answer
|
|
100
|
+
if (err && !(typeof stdout === "string" && err.code === 1))
|
|
101
|
+
resolve(null);
|
|
102
|
+
else
|
|
103
|
+
resolve(String(stdout));
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
/** `ps` TIME: [[dd-]hh:]mm:ss[.cc] */
|
|
107
|
+
function psTimeMs(raw) {
|
|
108
|
+
const m = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+(?:\.\d+)?)$/.exec(raw.trim());
|
|
109
|
+
if (!m)
|
|
110
|
+
return null;
|
|
111
|
+
const [, d, h, min, s] = m;
|
|
112
|
+
return ((Number(d ?? 0) * 24 + Number(h ?? 0)) * 60 + Number(min)) * 60_000 + Number(s) * 1000;
|
|
113
|
+
}
|
|
114
|
+
/** The root and every descendant, from a `pid ppid ...` table. */
|
|
115
|
+
export function treePids(rows, root) {
|
|
116
|
+
const kids = new Map();
|
|
117
|
+
for (const r of rows) {
|
|
118
|
+
const list = kids.get(r.ppid);
|
|
119
|
+
if (list)
|
|
120
|
+
list.push(r.pid);
|
|
121
|
+
else
|
|
122
|
+
kids.set(r.ppid, [r.pid]);
|
|
123
|
+
}
|
|
124
|
+
const out = [];
|
|
125
|
+
const queue = [root];
|
|
126
|
+
while (queue.length && out.length < 512) {
|
|
127
|
+
const pid = queue.shift();
|
|
128
|
+
if (out.includes(pid))
|
|
129
|
+
continue;
|
|
130
|
+
out.push(pid);
|
|
131
|
+
queue.push(...(kids.get(pid) ?? []));
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
async function samplePosix(root) {
|
|
136
|
+
const table = await run("ps", ["-A", "-o", "pid=,ppid=,time="]);
|
|
137
|
+
if (table === null)
|
|
138
|
+
return { cpuMs: null, connections: null };
|
|
139
|
+
const rows = [];
|
|
140
|
+
for (const line of table.split("\n")) {
|
|
141
|
+
const [pid, ppid, time] = line.trim().split(/\s+/);
|
|
142
|
+
if (!pid || !ppid || !time)
|
|
143
|
+
continue;
|
|
144
|
+
rows.push({ pid: Number(pid), ppid: Number(ppid), cpuMs: psTimeMs(time) });
|
|
145
|
+
}
|
|
146
|
+
const pids = treePids(rows, root);
|
|
147
|
+
if (!rows.some((r) => r.pid === root))
|
|
148
|
+
return { cpuMs: null, connections: null };
|
|
149
|
+
let cpuMs = 0;
|
|
150
|
+
for (const r of rows)
|
|
151
|
+
if (pids.includes(r.pid))
|
|
152
|
+
cpuMs += r.cpuMs ?? 0;
|
|
153
|
+
const lsof = await run("lsof", ["-nP", "-a", "-p", pids.join(","), "-iTCP", "-sTCP:ESTABLISHED", "-Fn"]);
|
|
154
|
+
// -Fn prints one `n<addr>` line per connection
|
|
155
|
+
const connections = lsof === null ? null : lsof.split("\n").filter((l) => l.startsWith("n")).length;
|
|
156
|
+
return { cpuMs, connections };
|
|
157
|
+
}
|
|
158
|
+
async function sampleWindows(root) {
|
|
159
|
+
const script = "$ErrorActionPreference='SilentlyContinue';" +
|
|
160
|
+
"$all=Get-CimInstance Win32_Process|Select-Object ProcessId,ParentProcessId,KernelModeTime,UserModeTime;" +
|
|
161
|
+
`$ids=@(${root});$q=@(${root});` +
|
|
162
|
+
"while($q.Count){$p=$q[0];$q=@($q|Select-Object -Skip 1);foreach($c in $all|Where-Object{$_.ParentProcessId -eq $p}){if($ids -notcontains $c.ProcessId){$ids+=$c.ProcessId;$q+=$c.ProcessId}}};" +
|
|
163
|
+
"$cpu=($all|Where-Object{$ids -contains $_.ProcessId}|ForEach-Object{[double]$_.KernelModeTime+[double]$_.UserModeTime}|Measure-Object -Sum).Sum/10000;" +
|
|
164
|
+
"$n=@(Get-NetTCPConnection -State Established|Where-Object{$ids -contains $_.OwningProcess}).Count;" +
|
|
165
|
+
"Write-Output \"$cpu $n\"";
|
|
166
|
+
const out = await run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], 8_000);
|
|
167
|
+
const [cpu, n] = (out ?? "").trim().split(/\s+/);
|
|
168
|
+
const cpuMs = Number(cpu);
|
|
169
|
+
const connections = Number(n);
|
|
170
|
+
return {
|
|
171
|
+
cpuMs: cpu !== undefined && Number.isFinite(cpuMs) ? cpuMs : null,
|
|
172
|
+
connections: n !== undefined && Number.isFinite(connections) ? connections : null,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/** CPU time and open connections of a process and its children. Best
|
|
176
|
+
* effort: a missing tool or a refused query reads as null (unknown), never
|
|
177
|
+
* as "idle". */
|
|
178
|
+
export function sampleProcessTree(pid) {
|
|
179
|
+
return (process.platform === "win32" ? sampleWindows(pid) : samplePosix(pid))
|
|
180
|
+
.catch(() => ({ cpuMs: null, connections: null }));
|
|
181
|
+
}
|
|
182
|
+
/** CPU a process tree can burn on bookkeeping while doing no real work. */
|
|
183
|
+
const BUSY_CPU_MS_PER_MIN = 3_000;
|
|
184
|
+
export function classifyQuiet(input) {
|
|
185
|
+
const { logged, sample, previous } = input;
|
|
186
|
+
if (logged?.kind === "retry")
|
|
187
|
+
return { kind: "retrying", status: logged.status, delayMs: logged.delayMs, attempt: logged.attempt };
|
|
188
|
+
if (logged?.kind === "compressing")
|
|
189
|
+
return { kind: "compressing" };
|
|
190
|
+
const nearFullContext = input.context !== null && input.context.threshold > 0
|
|
191
|
+
&& input.context.tokens >= input.context.threshold * 0.9;
|
|
192
|
+
if (sample.connections !== null && sample.connections > 0)
|
|
193
|
+
return { kind: "waiting-model", nearFullContext };
|
|
194
|
+
const cpuDelta = sample.cpuMs !== null && previous?.cpuMs != null ? sample.cpuMs - previous.cpuMs : null;
|
|
195
|
+
if (cpuDelta !== null && input.sinceMs > 0 && cpuDelta > BUSY_CPU_MS_PER_MIN * (input.sinceMs / 60_000))
|
|
196
|
+
return { kind: "busy" };
|
|
197
|
+
if (sample.connections === 0 && cpuDelta !== null)
|
|
198
|
+
return { kind: "no-signs" };
|
|
199
|
+
return { kind: "unknown" };
|
|
200
|
+
}
|
|
201
|
+
/** Two states read the same to a person when this matches; a notice is
|
|
202
|
+
* sent only when it changes. */
|
|
203
|
+
export function quietKey(state) {
|
|
204
|
+
if (state.kind === "retrying")
|
|
205
|
+
return `retrying:${state.attempt ?? "?"}:${state.status ?? "?"}`;
|
|
206
|
+
if (state.kind === "waiting-model")
|
|
207
|
+
return `waiting-model:${state.nearFullContext}`;
|
|
208
|
+
return state.kind;
|
|
209
|
+
}
|
|
210
|
+
const minutes = (ms) => {
|
|
211
|
+
if (ms < 90_000)
|
|
212
|
+
return `${Math.max(1, Math.round(ms / 1000))} s`;
|
|
213
|
+
return `${Math.round(ms / 60_000)} min`;
|
|
214
|
+
};
|
|
215
|
+
/** The chat notice for a quiet state. */
|
|
216
|
+
export function describeQuiet(name, state, quietMs, stopAfterMs) {
|
|
217
|
+
const quiet = minutes(quietMs);
|
|
218
|
+
switch (state.kind) {
|
|
219
|
+
case "retrying": {
|
|
220
|
+
const why = state.status === 429 ? "hit a rate limit (HTTP 429)"
|
|
221
|
+
: state.status !== null && state.status >= 500 ? `got a server error from its model provider (HTTP ${state.status})`
|
|
222
|
+
: state.status !== null ? `got an error from its model provider (HTTP ${state.status})`
|
|
223
|
+
: "had a failed model request";
|
|
224
|
+
const next = state.delayMs !== null ? ` Next try in ${Math.max(1, Math.round(state.delayMs / 1000))} s.` : "";
|
|
225
|
+
const attempt = state.attempt !== null ? ` (attempt ${state.attempt + 1})` : "";
|
|
226
|
+
return `${name} ${why} and is retrying${attempt}.${next} It is still working.`;
|
|
227
|
+
}
|
|
228
|
+
case "compressing":
|
|
229
|
+
return `${name} is compressing its conversation history to make room. This can take a few minutes; it is still working.`;
|
|
230
|
+
case "waiting-model":
|
|
231
|
+
return state.nearFullContext
|
|
232
|
+
? `${name} is waiting on its model: no reply for ${quiet}, but its request is still open. Its history is nearly full, so it may be compressing it.`
|
|
233
|
+
: `${name} is waiting on its model: no reply for ${quiet}, but its request is still open. Slow models and rate limits look like this.`;
|
|
234
|
+
case "busy":
|
|
235
|
+
return `${name} is busy on this computer but has sent nothing for ${quiet}.`;
|
|
236
|
+
case "between-requests":
|
|
237
|
+
return `${name} has sent nothing for ${quiet} and has no request open to its model right now. ` +
|
|
238
|
+
"It may be waiting before a retry. Still waiting; press Stop to end the turn.";
|
|
239
|
+
case "no-signs":
|
|
240
|
+
return `${name} has sent nothing for ${quiet}, has no open request to its model, and is not using the CPU. It may be stuck. ` +
|
|
241
|
+
`Press Stop to end the turn; otherwise it is stopped after ${minutes(stopAfterMs)} of silence.`;
|
|
242
|
+
case "unknown":
|
|
243
|
+
return `${name} has sent nothing for ${quiet}. Still waiting; press Stop to end the turn.`;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/** A short phrase for the final stuck error: what the agent looked like last. */
|
|
247
|
+
export function lastSeenPhrase(state) {
|
|
248
|
+
switch (state.kind) {
|
|
249
|
+
case "retrying": return "retrying a failed model request";
|
|
250
|
+
case "compressing": return "compressing its history";
|
|
251
|
+
case "waiting-model": return "waiting on its model with a request open";
|
|
252
|
+
case "busy": return "busy on this computer";
|
|
253
|
+
case "between-requests": return "no request open to its model";
|
|
254
|
+
case "no-signs": return "no open request and no CPU use";
|
|
255
|
+
case "unknown": return "no readable process state";
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -6,6 +6,7 @@ import { createHash } from "node:crypto";
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { qualifiedModelLabel } from "../../contracts.js";
|
|
8
8
|
import { harnessHome } from "../../env-path.js";
|
|
9
|
+
import { parseQwenLogLine } from "./quiet-status.js";
|
|
9
10
|
import { decodeInjectId, hostApiKey, localHost, mergeLocalInject } from "../local-inject.js";
|
|
10
11
|
import { createAcpDriver } from "./core.js";
|
|
11
12
|
const EMPTY = { default: "", options: [] };
|
|
@@ -199,6 +200,15 @@ export function resolveQwenTurnModel(model, env) {
|
|
|
199
200
|
throw new Error("This Qwen model is no longer configured. Refresh models and select it again.");
|
|
200
201
|
return matches[0].id;
|
|
201
202
|
}
|
|
203
|
+
/** Where Qwen Code 0.24 writes a session's debug log: `<runtime dir>/debug/<session id>.txt`,
|
|
204
|
+
* the runtime dir being QWEN_RUNTIME_DIR, else QWEN_HOME, else ~/.qwen. */
|
|
205
|
+
export function qwenDebugLogPath(env, sessionId) {
|
|
206
|
+
const off = ["", "0", "false", "off", "no"].includes((env.QWEN_DEBUG_LOG_FILE ?? "").trim().toLowerCase());
|
|
207
|
+
if (off || !/^[\w-]{1,128}$/.test(sessionId))
|
|
208
|
+
return null;
|
|
209
|
+
const base = env.QWEN_RUNTIME_DIR || env.QWEN_HOME || harnessHome("qwen", env);
|
|
210
|
+
return join(base, "debug", `${sessionId}.txt`);
|
|
211
|
+
}
|
|
202
212
|
/** Qwen Code's own approval ladder, passed through (qwen --help, 0.24):
|
|
203
213
|
* `--approval-mode default` asks, `auto-edit` approves file edits, `auto`
|
|
204
214
|
* runs Qwen's LLM classifier that approves safe actions and blocks risky
|
|
@@ -243,5 +253,13 @@ const support = {
|
|
|
243
253
|
authFailure: "continue",
|
|
244
254
|
isAuthenticated: () => true,
|
|
245
255
|
buildPromptText: (turn) => (turn.system ? `${turn.system}\n\n${turn.text}` : turn.text),
|
|
256
|
+
// Qwen says nothing over ACP while it retries a rate limit or compresses
|
|
257
|
+
// its history, but its debug log does. Turn the log on (unless the person
|
|
258
|
+
// turned it off) and read it while a prompt is quiet.
|
|
259
|
+
transformEnv: (env) => {
|
|
260
|
+
if (env.QWEN_DEBUG_LOG_FILE === undefined)
|
|
261
|
+
env.QWEN_DEBUG_LOG_FILE = "1";
|
|
262
|
+
},
|
|
263
|
+
statusLog: { path: qwenDebugLogPath, parse: parseQwenLogLine },
|
|
246
264
|
};
|
|
247
265
|
export const QwenAgentDriver = createAcpDriver(support);
|
|
@@ -507,6 +507,34 @@ export async function callTool(name, args, context) {
|
|
|
507
507
|
}) });
|
|
508
508
|
return { text: JSON.stringify(r), ...(r.error ? { isError: true } : {}) };
|
|
509
509
|
}
|
|
510
|
+
if (name === "send_to_bot") {
|
|
511
|
+
const botId = String(args.bot_id ?? "").trim();
|
|
512
|
+
const title = String(args.title ?? "").trim();
|
|
513
|
+
const message = String(args.message ?? "").trim();
|
|
514
|
+
if (!botId || !title || !message) {
|
|
515
|
+
return { text: "send_to_bot needs bot_id, title and message.", isError: true };
|
|
516
|
+
}
|
|
517
|
+
if (botId === context.botId) {
|
|
518
|
+
return { text: "send_to_bot is cross-bot only. Use start_thread to send independent work to yourself.", isError: true };
|
|
519
|
+
}
|
|
520
|
+
if (turn.threadsOpenedThisTurn >= MAX_THREADS_PER_TURN) {
|
|
521
|
+
return { text: `You have already opened ${MAX_THREADS_PER_TURN} threads this turn, which is the limit.`, isError: true };
|
|
522
|
+
}
|
|
523
|
+
const r = await api("/api/internal/threads", { method: "POST", body: JSON.stringify({
|
|
524
|
+
fromBotId: context.botId, fromThreadId: context.threadId, toBotId: botId,
|
|
525
|
+
title, message, depth: context.depth, oneWay: true,
|
|
526
|
+
}) });
|
|
527
|
+
if (r.error)
|
|
528
|
+
return { text: `Couldn't send to that bot: ${String(r.error)}`, isError: true };
|
|
529
|
+
turn.threadsOpenedThisTurn += 1;
|
|
530
|
+
const destination = `@${String(r.botName ?? "that bot")} in #${String(r.title ?? title)} [thread id: ${String(r.threadId ?? "")}]`;
|
|
531
|
+
const state = r.approvalRequired === true
|
|
532
|
+
? `Send to ${destination} is pending approval. The person's approval card appears after this turn ends; dispatch starts once approved.`
|
|
533
|
+
: r.state === "queued"
|
|
534
|
+
? `Send to ${destination} is queued ${ordinal(Number(r.position) || 1)} for a free slot and can dispatch after this turn ends.`
|
|
535
|
+
: `Send to ${destination} is pending until this turn ends, then dispatch starts.`;
|
|
536
|
+
return { text: `${state} Its result stays in that thread; nothing there will resume you.` };
|
|
537
|
+
}
|
|
510
538
|
if (name === "list_bots") {
|
|
511
539
|
const r = await api(`/api/internal/agents?self=${encodeURIComponent(BOT_ID)}`);
|
|
512
540
|
const bots = r.bots ?? [];
|
|
@@ -221,6 +221,15 @@ const toolDefinitions = (externalRuntime) => [
|
|
|
221
221
|
label: { type: "string", description: "Optional short name (one line, at most 60 characters) for this job. Names the new direct assignment thread." },
|
|
222
222
|
}, required: ["bot_ids", "message", "request_key"] },
|
|
223
223
|
},
|
|
224
|
+
{
|
|
225
|
+
name: "send_to_bot",
|
|
226
|
+
description: "Send work one way to another bot in a fresh thread. This is cross-bot only: use start_thread to send independent work to yourself. The recipient owns the new thread; its results, failures and questions stay there and never resume you. Use coordinate_bots when you need the result returned here.",
|
|
227
|
+
inputSchema: { type: "object", additionalProperties: false, properties: {
|
|
228
|
+
bot_id: { type: "string", description: "One reachable teammate id from list_bots or list_room_targets." },
|
|
229
|
+
title: { type: "string", description: "A short one-line title, at most 80 characters." },
|
|
230
|
+
message: { type: "string", description: "Complete instructions; the recipient does not inherit this conversation." },
|
|
231
|
+
}, required: ["bot_id", "title", "message"] },
|
|
232
|
+
},
|
|
224
233
|
{
|
|
225
234
|
name: "list_bots",
|
|
226
235
|
description: "List the other bots (agents) you may contact in your own team and any additional teams the owner has explicitly allowed you to coordinate, with their team, model and current status. Call this to discover exact teammate IDs before assigning work or requesting advice through your available coordination tools.",
|
|
@@ -757,7 +766,7 @@ const VOICE_TOOL_NAMES = new Set(["send_voice_note"]);
|
|
|
757
766
|
// One teamwork path in room turns; keep all unrelated integrations available.
|
|
758
767
|
// Ordinary direct chats use this same bounded coordinator. Goal-owned turns
|
|
759
768
|
// retain their independent loop and cannot start a second coordinator.
|
|
760
|
-
const ROOM_ONLY_TOOLS = new Set(["list_room_targets", "coordinate_bots"]);
|
|
769
|
+
const ROOM_ONLY_TOOLS = new Set(["list_room_targets", "coordinate_bots", "send_to_bot"]);
|
|
761
770
|
const ROOM_REPLACED_TOOLS = new Set(["ask_bot", "delegate_bot", "check_delegation", "wait_delegation", "start_thread", "send_to_thread", "wait_thread"]);
|
|
762
771
|
const EXTERNAL_TOOL_NAMES = new Set(["list_bots", "ask_bot", "delegate_bot", "check_delegation", "wait_delegation"]);
|
|
763
772
|
const WATCHER_TOOL_NAMES = new Set(["create_options_card"]);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { newEventId, newId } from "../contracts.js";
|
|
2
2
|
import { boatCredential, boatProviderApi } from "../included-services.js";
|
|
3
3
|
import { appendNative } from "./native.js";
|
|
4
|
+
import { assertToolScopeSupported } from "../../shared/tool-scope-support.js";
|
|
4
5
|
import { OMB_ASK_TOOL, answerWithoutPreamble, askQuestionSummary, capAnswerEcho, ombAskProtocolPrompt, parseOmbAskQuestions, questionChoices, stripOmbAskBlock, } from "../../shared/ask-question.js";
|
|
5
6
|
// "boxAgent" is Boat's historical driver kind (catalog and wire); keep it.
|
|
6
7
|
const DRIVER_KIND = "boxAgent";
|
|
@@ -96,6 +97,7 @@ export const BoatAgentDriver = {
|
|
|
96
97
|
return body;
|
|
97
98
|
};
|
|
98
99
|
const sendTurn = async (turn) => {
|
|
100
|
+
turn = { ...turn, toolScope: assertToolScopeSupported(DRIVER_KIND, turn.toolScope) };
|
|
99
101
|
const { threadId } = turn;
|
|
100
102
|
const computer = turn.integrations?.computer;
|
|
101
103
|
const boxId = computer && (!computer.kind || computer.kind === "box") ? computer.boxId : undefined;
|
|
@@ -15,6 +15,7 @@ import { HermesAgentDriver } from "./acp/hermes.js";
|
|
|
15
15
|
import { OpenAICompatDriver } from "./openai-compat.js";
|
|
16
16
|
import { PiDriver } from "./pi.js";
|
|
17
17
|
import { MistralDriver } from "./mistral.js";
|
|
18
|
+
import { CerebrasDriver } from "./cerebras.js";
|
|
18
19
|
import { MinimaxDriver } from "./minimax.js";
|
|
19
20
|
export const BUILT_IN_DRIVERS = [
|
|
20
21
|
GrokDriver,
|
|
@@ -35,4 +36,5 @@ export const BUILT_IN_DRIVERS = [
|
|
|
35
36
|
BoatAgentDriver,
|
|
36
37
|
MinimaxDriver,
|
|
37
38
|
MistralDriver,
|
|
39
|
+
CerebrasDriver,
|
|
38
40
|
];
|