cascade-ai 0.77.0 → 0.79.0
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/cli.cjs +347 -4
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +347 -4
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +1047 -10
- package/dist/index.cjs +1041 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +738 -1
- package/dist/index.d.ts +738 -1
- package/dist/index.js +1036 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -1849,7 +1849,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
|
|
|
1849
1849
|
var init_constants = __esm({
|
|
1850
1850
|
"src/constants.ts"() {
|
|
1851
1851
|
init_pricing();
|
|
1852
|
-
CASCADE_VERSION = "0.
|
|
1852
|
+
CASCADE_VERSION = "0.79.0";
|
|
1853
1853
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
1854
1854
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
1855
1855
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -15540,6 +15540,45 @@ var ToolsConfigSchema = zod.z.object({
|
|
|
15540
15540
|
shellBlocklist: zod.z.array(zod.z.string()).default(["rm -rf", "sudo rm", "format", "mkfs"]),
|
|
15541
15541
|
requireApprovalFor: zod.z.array(zod.z.string()).default([]),
|
|
15542
15542
|
browserEnabled: zod.z.boolean().default(false),
|
|
15543
|
+
/**
|
|
15544
|
+
* Let a run ACT on the page open in the desktop's built-in browser — click,
|
|
15545
|
+
* type, submit — not just read it.
|
|
15546
|
+
*
|
|
15547
|
+
* Distinct from `browserEnabled`, which turns on the headless Playwright
|
|
15548
|
+
* tool. That one drives a throwaway browser with an empty cookie jar; this
|
|
15549
|
+
* one drives the session the user is signed into, which is a different order
|
|
15550
|
+
* of consequence and so gets its own switch rather than riding on that one.
|
|
15551
|
+
*
|
|
15552
|
+
* Off by default. Turning it on does not hand anything over silently: the
|
|
15553
|
+
* tool still goes through the approval chain, and the browser panel carries a
|
|
15554
|
+
* stop control for the run that is acting.
|
|
15555
|
+
*/
|
|
15556
|
+
agentBrowserControl: zod.z.boolean().default(false),
|
|
15557
|
+
/**
|
|
15558
|
+
* Where a HOSTED run gets a browser, since it has none of its own.
|
|
15559
|
+
*
|
|
15560
|
+
* The desktop drives the Chromium the user is signed into and can watch. A
|
|
15561
|
+
* server has neither, and running one inside the container is wrong twice
|
|
15562
|
+
* over — page JavaScript issues requests no URL check ever sees, from inside
|
|
15563
|
+
* the trust boundary, and a session costs 200-400 MB. So this points at a
|
|
15564
|
+
* browser somebody else runs.
|
|
15565
|
+
*
|
|
15566
|
+
* Absent by default: no provider, no capability. There is nothing to turn
|
|
15567
|
+
* off because nothing is registered until an endpoint is supplied.
|
|
15568
|
+
*/
|
|
15569
|
+
remoteBrowser: zod.z.object({
|
|
15570
|
+
/** `cdp` for an endpoint you run; `steel` for a Steel deployment. */
|
|
15571
|
+
provider: zod.z.enum(["cdp", "steel"]).optional(),
|
|
15572
|
+
/** The websocket endpoint for `cdp`, or the API base for `steel`. */
|
|
15573
|
+
url: zod.z.string().optional(),
|
|
15574
|
+
apiKey: zod.z.string().optional(),
|
|
15575
|
+
/**
|
|
15576
|
+
* Concurrent sessions. One by default and deliberately so: every session
|
|
15577
|
+
* is billed, and a wave of workers each opening their own is a cost the
|
|
15578
|
+
* user did not choose. Raising it is a decision, not a default.
|
|
15579
|
+
*/
|
|
15580
|
+
maxSessions: zod.z.number().int().min(1).max(16).default(1)
|
|
15581
|
+
}).optional(),
|
|
15543
15582
|
mcpServers: zod.z.array(McpServerConfigSchema).optional(),
|
|
15544
15583
|
mcpTrusted: zod.z.array(zod.z.string()).optional(),
|
|
15545
15584
|
/** Web search backends — at least one should be configured for best results */
|
|
@@ -21464,6 +21503,23 @@ Now execute your subtask using this context where relevant.`
|
|
|
21464
21503
|
this.setStatus("FAILED", finalOutput);
|
|
21465
21504
|
this.peerBus?.publish(this.id, assignment.subtaskId, finalOutput, "FAILED");
|
|
21466
21505
|
return this.buildResult("ESCALATED", finalOutput, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
21506
|
+
} finally {
|
|
21507
|
+
this.releaseBrowserControl();
|
|
21508
|
+
}
|
|
21509
|
+
}
|
|
21510
|
+
/**
|
|
21511
|
+
* End this worker's hold on the shared browser.
|
|
21512
|
+
*
|
|
21513
|
+
* Duck-typed rather than imported: `browser_control` is registered only by a
|
|
21514
|
+
* host that HAS a browser (the desktop), so in the CLI and hosted runs there
|
|
21515
|
+
* is no such tool and this is a no-op. Importing the class to instanceof-check
|
|
21516
|
+
* it would couple the worker to a tool that usually is not there.
|
|
21517
|
+
*/
|
|
21518
|
+
releaseBrowserControl() {
|
|
21519
|
+
try {
|
|
21520
|
+
const tool = this.toolRegistry.getTool?.("browser_control");
|
|
21521
|
+
tool?.releaseActor?.(this.id);
|
|
21522
|
+
} catch {
|
|
21467
21523
|
}
|
|
21468
21524
|
}
|
|
21469
21525
|
sendToPeer(toId, content) {
|
|
@@ -31267,6 +31323,127 @@ var CurrentPageTool = class extends BaseTool {
|
|
|
31267
31323
|
}
|
|
31268
31324
|
};
|
|
31269
31325
|
|
|
31326
|
+
// src/tools/browser-control.ts
|
|
31327
|
+
var BrowserControlTool = class extends BaseTool {
|
|
31328
|
+
name = "browser_control";
|
|
31329
|
+
description = "Act on the web page the user has open in the built-in browser: navigate, click an element, type into a field, press a key, or wait for something to appear. This drives the REAL browser the user is signed into and can see, so actions have real consequences \u2014 prefer read_current_page when you only need to look. CSS selectors are matched against the live page; call extract_text first if you need to find one. There is one browser: if another part of this run is using it, this call waits its turn, so a slow reply means queued, not stuck.";
|
|
31330
|
+
inputSchema = {
|
|
31331
|
+
type: "object",
|
|
31332
|
+
properties: {
|
|
31333
|
+
action: {
|
|
31334
|
+
type: "string",
|
|
31335
|
+
enum: ["navigate", "click", "fill", "press", "wait_for", "extract_text"],
|
|
31336
|
+
description: "What to do."
|
|
31337
|
+
},
|
|
31338
|
+
url: { type: "string", description: "For navigate: the http(s) address to open." },
|
|
31339
|
+
selector: {
|
|
31340
|
+
type: "string",
|
|
31341
|
+
description: "CSS selector for click, fill and wait_for. Optional for extract_text (defaults to the whole page)."
|
|
31342
|
+
},
|
|
31343
|
+
value: { type: "string", description: "For fill: the text to type." },
|
|
31344
|
+
key: { type: "string", description: 'For press: a key name such as "Enter", "Tab" or "Escape".' },
|
|
31345
|
+
timeoutMs: { type: "number", description: "For wait_for: how long to wait before giving up (default 10000, max 30000)." }
|
|
31346
|
+
},
|
|
31347
|
+
required: ["action"]
|
|
31348
|
+
};
|
|
31349
|
+
controller;
|
|
31350
|
+
release;
|
|
31351
|
+
/** One-way. See `revoke`. */
|
|
31352
|
+
revoked = false;
|
|
31353
|
+
constructor(controller, release) {
|
|
31354
|
+
super();
|
|
31355
|
+
this.controller = controller;
|
|
31356
|
+
this.release = release;
|
|
31357
|
+
}
|
|
31358
|
+
/**
|
|
31359
|
+
* A worker has finished. Ends its hold on the browser.
|
|
31360
|
+
*
|
|
31361
|
+
* Called from the tier's terminal path rather than by the model, because a
|
|
31362
|
+
* model cannot be relied on to announce that it is done — which is why the
|
|
31363
|
+
* host cannot use idle time as the boundary instead.
|
|
31364
|
+
*/
|
|
31365
|
+
releaseActor(actorId) {
|
|
31366
|
+
this.release?.(actorId);
|
|
31367
|
+
}
|
|
31368
|
+
/**
|
|
31369
|
+
* Permanently disable this tool, whatever the registry still holds.
|
|
31370
|
+
*
|
|
31371
|
+
* Registration-time gates cannot express "this run turned out to be
|
|
31372
|
+
* unattended": a host that wires the controller and only then declares the
|
|
31373
|
+
* run unattended has already registered the tool, and a check that ran at
|
|
31374
|
+
* registration has no second chance. Enforcing at EXECUTION time makes the
|
|
31375
|
+
* guarantee independent of call order, which is the whole point of stating
|
|
31376
|
+
* the invariant rather than relying on the scheduler's current sequence.
|
|
31377
|
+
*
|
|
31378
|
+
* One-way on purpose. A safety gate that can be re-opened by a later call is
|
|
31379
|
+
* a gate that a refactor re-opens by accident.
|
|
31380
|
+
*/
|
|
31381
|
+
revoke() {
|
|
31382
|
+
this.revoked = true;
|
|
31383
|
+
}
|
|
31384
|
+
// Acting on a session the user is signed into is the highest-consequence
|
|
31385
|
+
// thing in the tool set — higher than shell, arguably, because the blast
|
|
31386
|
+
// radius is someone else's server rather than this machine.
|
|
31387
|
+
isDangerous() {
|
|
31388
|
+
return true;
|
|
31389
|
+
}
|
|
31390
|
+
async execute(input, options) {
|
|
31391
|
+
if (this.revoked) {
|
|
31392
|
+
return "Error: browser control is not available in this run. It requires a person watching who can stop it, and nobody is.";
|
|
31393
|
+
}
|
|
31394
|
+
const kind = input["action"];
|
|
31395
|
+
if (!kind) return "Error: action is required.";
|
|
31396
|
+
if (options?.signal?.aborted) return "Error: the run was cancelled before this action ran.";
|
|
31397
|
+
const missing = requiredFieldFor(kind, input);
|
|
31398
|
+
if (missing) return `Error: "${kind}" needs ${missing}.`;
|
|
31399
|
+
const action = {
|
|
31400
|
+
kind,
|
|
31401
|
+
...typeof input["url"] === "string" ? { url: input["url"] } : {},
|
|
31402
|
+
...typeof input["selector"] === "string" ? { selector: input["selector"] } : {},
|
|
31403
|
+
...typeof input["value"] === "string" ? { value: input["value"] } : {},
|
|
31404
|
+
...typeof input["key"] === "string" ? { key: input["key"] } : {},
|
|
31405
|
+
...typeof input["timeoutMs"] === "number" ? { timeoutMs: Math.min(Math.max(input["timeoutMs"], 0), 3e4) } : {}
|
|
31406
|
+
};
|
|
31407
|
+
let outcome;
|
|
31408
|
+
try {
|
|
31409
|
+
outcome = await this.controller(action, {
|
|
31410
|
+
// `sessionId` is the run. The host scopes both revocation and its
|
|
31411
|
+
// single-owner lease on it, so passing it is not telemetry — drop it
|
|
31412
|
+
// and a Stop in one run silently stops every later one.
|
|
31413
|
+
sessionId: options?.sessionId ?? "",
|
|
31414
|
+
actorId: options?.tierId ?? options?.sessionId ?? "",
|
|
31415
|
+
...options?.signal ? { signal: options.signal } : {}
|
|
31416
|
+
});
|
|
31417
|
+
} catch (err) {
|
|
31418
|
+
return `Error: the browser could not perform "${kind}" \u2014 ${err instanceof Error ? err.message : String(err)}`;
|
|
31419
|
+
}
|
|
31420
|
+
const where = outcome.url ? `
|
|
31421
|
+
Page: ${outcome.title ? `${outcome.title} \u2014 ` : ""}${outcome.url}` : "";
|
|
31422
|
+
return `${outcome.ok ? "" : "Failed: "}${outcome.detail}${where}`;
|
|
31423
|
+
}
|
|
31424
|
+
};
|
|
31425
|
+
function requiredFieldFor(kind, input) {
|
|
31426
|
+
const has = (k) => typeof input[k] === "string" && input[k].length > 0;
|
|
31427
|
+
const given = (k) => typeof input[k] === "string";
|
|
31428
|
+
switch (kind) {
|
|
31429
|
+
case "navigate":
|
|
31430
|
+
return has("url") ? null : "a url";
|
|
31431
|
+
case "click":
|
|
31432
|
+
return has("selector") ? null : "a selector";
|
|
31433
|
+
case "fill":
|
|
31434
|
+
return has("selector") ? given("value") ? null : "a value" : "a selector";
|
|
31435
|
+
case "press":
|
|
31436
|
+
return has("key") ? null : "a key";
|
|
31437
|
+
case "wait_for":
|
|
31438
|
+
return has("selector") ? null : "a selector";
|
|
31439
|
+
// extract_text works with or without a selector.
|
|
31440
|
+
case "extract_text":
|
|
31441
|
+
return null;
|
|
31442
|
+
default:
|
|
31443
|
+
return null;
|
|
31444
|
+
}
|
|
31445
|
+
}
|
|
31446
|
+
|
|
31270
31447
|
// src/core/cascade.ts
|
|
31271
31448
|
var ESCALATION_DECISION_TIMEOUT_MS = 5 * 6e4;
|
|
31272
31449
|
function buildContextualPrompt(prompt, history = []) {
|
|
@@ -31759,6 +31936,79 @@ ${input}` }],
|
|
|
31759
31936
|
if ((this.config.tools?.disabledTools ?? []).includes("read_current_page")) return;
|
|
31760
31937
|
this.toolRegistry.register(new CurrentPageTool(provider));
|
|
31761
31938
|
}
|
|
31939
|
+
/**
|
|
31940
|
+
* True when nobody is watching this run — a scheduled/cron task, which
|
|
31941
|
+
* auto-approves every tool because there is no one to ask.
|
|
31942
|
+
*
|
|
31943
|
+
* That auto-approval is fine for ordinary tools and disqualifying for this
|
|
31944
|
+
* one: `browser_control` drives the signed-in browser, and "every run asks
|
|
31945
|
+
* first" is the promise the Settings copy makes. The property used to hold
|
|
31946
|
+
* only because `runScheduledTask` happens not to call `setBrowserController`
|
|
31947
|
+
* — a guarantee resting on an ABSENT call, which is exactly what a later
|
|
31948
|
+
* refactor adds without noticing. Stated and enforced instead.
|
|
31949
|
+
*/
|
|
31950
|
+
unattended = false;
|
|
31951
|
+
/**
|
|
31952
|
+
* Declare that no human is watching this run, so capabilities that depend on
|
|
31953
|
+
* a person being able to see and stop them are refused outright.
|
|
31954
|
+
*/
|
|
31955
|
+
setUnattended(on) {
|
|
31956
|
+
this.unattended = on;
|
|
31957
|
+
if (!on) return;
|
|
31958
|
+
const tool = this.toolRegistry.getTool("browser_control");
|
|
31959
|
+
tool?.revoke?.();
|
|
31960
|
+
}
|
|
31961
|
+
/**
|
|
31962
|
+
* Wire the host's browser so a run can ACT on the open page, not just read it.
|
|
31963
|
+
*
|
|
31964
|
+
* Two gates before this registers anything, and both matter:
|
|
31965
|
+
*
|
|
31966
|
+
* - `tools.agentBrowserControl` must be on. Unlike read_current_page, which
|
|
31967
|
+
* reaches nothing the user has not already opened themselves, this drives
|
|
31968
|
+
* the session they are signed into. That is opt-in, not a default.
|
|
31969
|
+
* - `disabledTools` still removes it, the same as every other tool.
|
|
31970
|
+
* - The run must not be UNATTENDED. See `setUnattended`.
|
|
31971
|
+
*
|
|
31972
|
+
* Host-supplied like the page provider: the tool exists only where there is a
|
|
31973
|
+
* real browser to drive, so it never has to be refused in the CLI or a hosted
|
|
31974
|
+
* run — it is simply not there.
|
|
31975
|
+
*/
|
|
31976
|
+
setBrowserController(controller, release) {
|
|
31977
|
+
this.registerBrowserControl(controller, release, this.config.tools?.agentBrowserControl === true);
|
|
31978
|
+
}
|
|
31979
|
+
/**
|
|
31980
|
+
* Wire a REMOTE browser — one the host connects to rather than one the user
|
|
31981
|
+
* is signed into.
|
|
31982
|
+
*
|
|
31983
|
+
* A separate entry point because the enablement is a different question, and
|
|
31984
|
+
* conflating them made the hosted capability dead on arrival: this went out
|
|
31985
|
+
* calling `setBrowserController`, which gates on `agentBrowserControl`, and
|
|
31986
|
+
* that flag means "drive the browser I am signed into on this machine". It
|
|
31987
|
+
* defaults to false and has no meaning on a server, so the hosted tool was
|
|
31988
|
+
* never registered at all — the feature looked shipped and did nothing.
|
|
31989
|
+
*
|
|
31990
|
+
* Configuring a provider IS the opt-in here. It takes a deliberate act with
|
|
31991
|
+
* an endpoint and usually a paid account, and the browser it reaches is a
|
|
31992
|
+
* throwaway signed into nothing — a smaller consent than handing over the
|
|
31993
|
+
* session the user is already authenticated in, and one the desktop flag
|
|
31994
|
+
* cannot stand in for.
|
|
31995
|
+
*/
|
|
31996
|
+
setRemoteBrowserController(controller, release) {
|
|
31997
|
+
this.registerBrowserControl(controller, release, Boolean(this.config.tools?.remoteBrowser?.provider));
|
|
31998
|
+
}
|
|
31999
|
+
/**
|
|
32000
|
+
* The gates every surface shares, with only enablement differing.
|
|
32001
|
+
*
|
|
32002
|
+
* One place, because the two callers above are exactly the sibling-writer
|
|
32003
|
+
* shape that has already drifted twice in this feature: a rule added to one
|
|
32004
|
+
* and not the other is silent on the surface that missed it.
|
|
32005
|
+
*/
|
|
32006
|
+
registerBrowserControl(controller, release, enabled) {
|
|
32007
|
+
if (this.unattended) return;
|
|
32008
|
+
if (!enabled) return;
|
|
32009
|
+
if ((this.config.tools?.disabledTools ?? []).includes("browser_control")) return;
|
|
32010
|
+
this.toolRegistry.register(new BrowserControlTool(controller, release));
|
|
32011
|
+
}
|
|
31762
32012
|
registerMediaTools(workspacePath) {
|
|
31763
32013
|
const denied = new Set(this.config.tools?.disabledTools ?? []);
|
|
31764
32014
|
const permitted = (name) => !denied.has(name);
|
|
@@ -32364,6 +32614,7 @@ ${prompt}` : prompt;
|
|
|
32364
32614
|
this.router.setRunSignal(options.signal);
|
|
32365
32615
|
const startMs = Date.now();
|
|
32366
32616
|
const taskId = crypto.randomUUID();
|
|
32617
|
+
this.emit("run:started", { taskId });
|
|
32367
32618
|
this.decisionLog = [];
|
|
32368
32619
|
this.currentRunCompleted = [];
|
|
32369
32620
|
this.wroteCheckpointThisRun = false;
|
|
@@ -38231,6 +38482,20 @@ function applySettingsPayload(config, data) {
|
|
|
38231
38482
|
if (data.webSearch.tavilyApiKey) next.tavilyApiKey = data.webSearch.tavilyApiKey;
|
|
38232
38483
|
config.tools.webSearch = next;
|
|
38233
38484
|
}
|
|
38485
|
+
if (data.remoteBrowser && typeof data.remoteBrowser === "object") {
|
|
38486
|
+
const prior = config.tools.remoteBrowser ?? {};
|
|
38487
|
+
const next = { ...prior };
|
|
38488
|
+
if (data.remoteBrowser.provider === "cdp" || data.remoteBrowser.provider === "steel") {
|
|
38489
|
+
next.provider = data.remoteBrowser.provider;
|
|
38490
|
+
}
|
|
38491
|
+
if (typeof data.remoteBrowser.url === "string") next.url = data.remoteBrowser.url.trim() || void 0;
|
|
38492
|
+
if (data.remoteBrowser.apiKey) next.apiKey = data.remoteBrowser.apiKey;
|
|
38493
|
+
const n = data.remoteBrowser.maxSessions;
|
|
38494
|
+
if (typeof n === "number" && Number.isFinite(n)) {
|
|
38495
|
+
next.maxSessions = Math.min(Math.max(Math.floor(n), 1), 16);
|
|
38496
|
+
}
|
|
38497
|
+
config.tools.remoteBrowser = next;
|
|
38498
|
+
}
|
|
38234
38499
|
if (data.advanced && typeof data.advanced === "object") {
|
|
38235
38500
|
const a = data.advanced;
|
|
38236
38501
|
const num = (v, min, max) => {
|
|
@@ -38261,6 +38526,7 @@ function applySettingsPayload(config, data) {
|
|
|
38261
38526
|
if (["auto", "T1", "T2", "T3"].includes(a["forceTier"])) config.routing = { ...config.routing ?? {}, forceTier: a["forceTier"] };
|
|
38262
38527
|
if (typeof a["benchmarksLive"] === "boolean") config.benchmarks = { ...config.benchmarks ?? {}, live: a["benchmarksLive"] };
|
|
38263
38528
|
if (["isolate", "worker", "auto"].includes(a["dynamicToolSandbox"])) config.tools = { ...config.tools ?? {}, dynamicToolSandbox: a["dynamicToolSandbox"] };
|
|
38529
|
+
if (typeof a["agentBrowserControl"] === "boolean") config.tools = { ...config.tools ?? {}, agentBrowserControl: a["agentBrowserControl"] };
|
|
38264
38530
|
if (typeof a["factsExtraction"] === "boolean") config.knowledge = { ...config.knowledge ?? {}, factsExtraction: a["factsExtraction"] };
|
|
38265
38531
|
if (typeof a["rememberSessions"] === "boolean") config.memory = { ...config.memory ?? {}, rememberSessions: a["rememberSessions"] };
|
|
38266
38532
|
if (typeof a["enableToolCreation"] === "boolean") config.enableToolCreation = a["enableToolCreation"];
|
|
@@ -38276,6 +38542,7 @@ function settingsSnapshot(config) {
|
|
|
38276
38542
|
if (p?.type && p?.baseUrl && p.type !== "azure") endpoints[p.type] = p.baseUrl;
|
|
38277
38543
|
}
|
|
38278
38544
|
const ws = config.tools?.webSearch ?? {};
|
|
38545
|
+
const rb = config.tools?.remoteBrowser ?? {};
|
|
38279
38546
|
return {
|
|
38280
38547
|
models: config.models ?? {},
|
|
38281
38548
|
budget: {
|
|
@@ -38305,6 +38572,15 @@ function settingsSnapshot(config) {
|
|
|
38305
38572
|
hasBraveKey: typeof ws.braveApiKey === "string" && ws.braveApiKey.length > 0,
|
|
38306
38573
|
hasTavilyKey: typeof ws.tavilyApiKey === "string" && ws.tavilyApiKey.length > 0
|
|
38307
38574
|
},
|
|
38575
|
+
// `hasApiKey`, never the key. The panel needs to show whether one is set;
|
|
38576
|
+
// sending the value back would put a live credential in every settings
|
|
38577
|
+
// response, where it reaches any client that can read them.
|
|
38578
|
+
remoteBrowser: {
|
|
38579
|
+
provider: rb.provider,
|
|
38580
|
+
url: rb.url,
|
|
38581
|
+
hasApiKey: typeof rb.apiKey === "string" && rb.apiKey.length > 0,
|
|
38582
|
+
maxSessions: rb.maxSessions
|
|
38583
|
+
},
|
|
38308
38584
|
advanced: {
|
|
38309
38585
|
autonomy: config.autonomy,
|
|
38310
38586
|
planApproval: config.planApproval,
|
|
@@ -38318,6 +38594,7 @@ function settingsSnapshot(config) {
|
|
|
38318
38594
|
forceTier: config.routing?.forceTier,
|
|
38319
38595
|
benchmarksLive: config.benchmarks?.live,
|
|
38320
38596
|
dynamicToolSandbox: config.tools?.dynamicToolSandbox,
|
|
38597
|
+
agentBrowserControl: config.tools?.agentBrowserControl,
|
|
38321
38598
|
factsExtraction: config.knowledge?.factsExtraction,
|
|
38322
38599
|
rememberSessions: config.memory?.rememberSessions,
|
|
38323
38600
|
enableToolCreation: config.enableToolCreation,
|
|
@@ -38828,6 +39105,14 @@ var DashboardServer = class {
|
|
|
38828
39105
|
* always failing.
|
|
38829
39106
|
*/
|
|
38830
39107
|
currentPageProvider;
|
|
39108
|
+
/**
|
|
39109
|
+
* Supplied by the desktop shell so a run can ACT on that page — click, type,
|
|
39110
|
+
* submit — not just read it. Same reasoning as the provider above: absent in
|
|
39111
|
+
* the CLI, so the tool is not registered rather than present and failing.
|
|
39112
|
+
* Cascade applies the `tools.agentBrowserControl` gate on top of this.
|
|
39113
|
+
*/
|
|
39114
|
+
browserController;
|
|
39115
|
+
browserActorRelease;
|
|
38831
39116
|
activeControllers = /* @__PURE__ */ new Map();
|
|
38832
39117
|
/**
|
|
38833
39118
|
* Run taskIds per chat session — file snapshots are keyed by the run's
|
|
@@ -38889,6 +39174,7 @@ var DashboardServer = class {
|
|
|
38889
39174
|
});
|
|
38890
39175
|
this.socket.onConfigUpdate(async (data) => {
|
|
38891
39176
|
const result = await commitSettings(this.config, data, (committed) => this.persistConfig(committed));
|
|
39177
|
+
if (result.ok) this.settingsChangeHook?.(this.config);
|
|
38892
39178
|
const refused = result.refused.map((r) => ({
|
|
38893
39179
|
type: r.type,
|
|
38894
39180
|
reason: r.reason,
|
|
@@ -38912,6 +39198,7 @@ var DashboardServer = class {
|
|
|
38912
39198
|
const cascade = new Cascade(cfg, this.workspacePath, this.store);
|
|
38913
39199
|
this.activeSessions.set(sessionId, cascade);
|
|
38914
39200
|
if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
|
|
39201
|
+
if (this.browserController) cascade.setBrowserController(this.browserController, this.browserActorRelease);
|
|
38915
39202
|
cascade.on("stream:token", (e) => {
|
|
38916
39203
|
this.emitToSessionClients(socketId, sessionId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
38917
39204
|
});
|
|
@@ -38935,13 +39222,16 @@ var DashboardServer = class {
|
|
|
38935
39222
|
if (ev.requestId) this.clearPendingEscalation(ev.requestId);
|
|
38936
39223
|
this.emitToSessionClients(socketId, sessionId, "escalation:timeout", { sessionId, ...e });
|
|
38937
39224
|
});
|
|
39225
|
+
cascade.on("run:started", (e) => {
|
|
39226
|
+
const id = e.taskId;
|
|
39227
|
+
if (id) this.recordSessionTask(sessionId, id);
|
|
39228
|
+
});
|
|
38938
39229
|
try {
|
|
38939
39230
|
const result = await cascade.run({
|
|
38940
39231
|
prompt: runPrompt,
|
|
38941
39232
|
signal: abortController.signal,
|
|
38942
39233
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
38943
39234
|
});
|
|
38944
|
-
this.recordSessionTask(sessionId, result.taskId);
|
|
38945
39235
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
38946
39236
|
this.captureWhy(sessionId, cascade, result);
|
|
38947
39237
|
this.emitToSessionClients(socketId, sessionId, "session:complete", { sessionId, result });
|
|
@@ -39007,6 +39297,49 @@ var DashboardServer = class {
|
|
|
39007
39297
|
setCurrentPageProvider(provider) {
|
|
39008
39298
|
this.currentPageProvider = provider;
|
|
39009
39299
|
}
|
|
39300
|
+
/** Let runs act on that view, not just read it (desktop only). */
|
|
39301
|
+
setBrowserController(controller, release) {
|
|
39302
|
+
this.browserController = controller;
|
|
39303
|
+
this.browserActorRelease = release;
|
|
39304
|
+
}
|
|
39305
|
+
/**
|
|
39306
|
+
* Told whenever settings are committed through THIS server.
|
|
39307
|
+
*
|
|
39308
|
+
* Settings reach the live config by two routes — the desktop's Electron IPC
|
|
39309
|
+
* handler and this server's `config:update` socket — and both run the same
|
|
39310
|
+
* `commitSettings`. The desktop mirrors `tools.agentBrowserControl` into its
|
|
39311
|
+
* browser module after its own route, but had no way to learn about the
|
|
39312
|
+
* other, so a socket write left the module's copy stale: an enable the module
|
|
39313
|
+
* still refused, or worse a disable the module still honoured for a run
|
|
39314
|
+
* already holding the tool. This is the same sibling-writer drift
|
|
39315
|
+
* `settings-payload.ts` exists to prevent, one layer up.
|
|
39316
|
+
*/
|
|
39317
|
+
settingsChangeHook;
|
|
39318
|
+
onSettingsChanged(hook) {
|
|
39319
|
+
this.settingsChangeHook = hook;
|
|
39320
|
+
}
|
|
39321
|
+
/**
|
|
39322
|
+
* Told when a run ends, so a host holding run-scoped state can release it.
|
|
39323
|
+
*
|
|
39324
|
+
* The desktop uses this to retire a run's browser Stop control: that has to
|
|
39325
|
+
* outlive each individual action (the gap between two actions is when the
|
|
39326
|
+
* user most wants it) but must not outlive the run.
|
|
39327
|
+
*/
|
|
39328
|
+
/**
|
|
39329
|
+
* Fired when a run ends, carrying BOTH identifiers explicitly.
|
|
39330
|
+
*
|
|
39331
|
+
* They are different things and conflating them was a live bug: a chat
|
|
39332
|
+
* `sessionId` can contain many runs (hence `sessionTaskIds`), while
|
|
39333
|
+
* `Cascade.run()` mints a fresh random `taskId` per run — and it is the
|
|
39334
|
+
* taskId that tools see as `ToolExecuteOptions.sessionId`. Passing only the
|
|
39335
|
+
* chat id meant browser cleanup keyed on the task id matched nothing, so a
|
|
39336
|
+
* finished run kept its Stop banner armed and held its browser lease until
|
|
39337
|
+
* the deadlock ceiling.
|
|
39338
|
+
*/
|
|
39339
|
+
runEndedHook;
|
|
39340
|
+
onRunEnded(hook) {
|
|
39341
|
+
this.runEndedHook = hook;
|
|
39342
|
+
}
|
|
39010
39343
|
async start() {
|
|
39011
39344
|
const isLoopback = this.host === "127.0.0.1" || this.host === "::1" || this.host === "localhost";
|
|
39012
39345
|
if (!isLoopback) {
|
|
@@ -39197,6 +39530,8 @@ var DashboardServer = class {
|
|
|
39197
39530
|
}
|
|
39198
39531
|
/** Record run end: the assistant reply (when there is one) and the runtime row's final status. */
|
|
39199
39532
|
persistRunEnd(sessionId, title, latestPrompt, reply, status, result) {
|
|
39533
|
+
const endedTaskId = result?.taskId ?? this.sessionTaskIds.get(sessionId)?.at(-1);
|
|
39534
|
+
this.runEndedHook?.({ sessionId, ...endedTaskId ? { taskId: endedTaskId } : {} });
|
|
39200
39535
|
try {
|
|
39201
39536
|
if (reply && reply.trim()) {
|
|
39202
39537
|
this.store.addMessage({ id: crypto.randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -39290,6 +39625,7 @@ var DashboardServer = class {
|
|
|
39290
39625
|
const prompt = task.prompt;
|
|
39291
39626
|
const title = this.persistRunStart(sessionId, `[${task.name}] ${prompt}`);
|
|
39292
39627
|
const cascade = new Cascade(this.config, task.workspacePath ?? this.workspacePath, this.store);
|
|
39628
|
+
cascade.setUnattended(true);
|
|
39293
39629
|
this.activeSessions.set(sessionId, cascade);
|
|
39294
39630
|
cascade.on("tier:status", (e) => {
|
|
39295
39631
|
this.socket.broadcast("tier:status", { sessionId, ...e });
|
|
@@ -39297,13 +39633,16 @@ var DashboardServer = class {
|
|
|
39297
39633
|
cascade.on("peer:message", (e) => {
|
|
39298
39634
|
this.socket.emitPeerMessage(e);
|
|
39299
39635
|
});
|
|
39636
|
+
cascade.on("run:started", (e) => {
|
|
39637
|
+
const id = e.taskId;
|
|
39638
|
+
if (id) this.recordSessionTask(sessionId, id);
|
|
39639
|
+
});
|
|
39300
39640
|
try {
|
|
39301
39641
|
const result = await cascade.run({
|
|
39302
39642
|
prompt,
|
|
39303
39643
|
identityId: task.identityId,
|
|
39304
39644
|
approvalCallback: async () => ({ approved: true, always: false })
|
|
39305
39645
|
});
|
|
39306
|
-
this.recordSessionTask(sessionId, result.taskId);
|
|
39307
39646
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
39308
39647
|
this.captureWhy(sessionId, cascade, result);
|
|
39309
39648
|
this.socket.broadcast("session:complete", { sessionId, result });
|
|
@@ -39936,6 +40275,7 @@ ${prompt}`;
|
|
|
39936
40275
|
const cascade = new Cascade(this.config, this.workspacePath, this.store);
|
|
39937
40276
|
this.activeSessions.set(sessionId, cascade);
|
|
39938
40277
|
if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
|
|
40278
|
+
if (this.browserController) cascade.setBrowserController(this.browserController, this.browserActorRelease);
|
|
39939
40279
|
cascade.on("stream:token", (e) => {
|
|
39940
40280
|
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
39941
40281
|
});
|
|
@@ -39959,13 +40299,16 @@ ${prompt}`;
|
|
|
39959
40299
|
if (ev.requestId) this.clearPendingEscalation(ev.requestId);
|
|
39960
40300
|
this.socket.broadcastToRoom(`session:${sessionId}`, "escalation:timeout", { sessionId, ...e });
|
|
39961
40301
|
});
|
|
40302
|
+
cascade.on("run:started", (e) => {
|
|
40303
|
+
const id = e.taskId;
|
|
40304
|
+
if (id) this.recordSessionTask(sessionId, id);
|
|
40305
|
+
});
|
|
39962
40306
|
try {
|
|
39963
40307
|
const result = await cascade.run({
|
|
39964
40308
|
prompt: runPrompt,
|
|
39965
40309
|
identityId: body.identityId,
|
|
39966
40310
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
39967
40311
|
});
|
|
39968
|
-
this.recordSessionTask(sessionId, result.taskId);
|
|
39969
40312
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
39970
40313
|
this.captureWhy(sessionId, cascade, result);
|
|
39971
40314
|
this.socket.broadcast("cost:update", {
|