cascade-ai 0.77.0 → 0.78.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 +268 -4
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +268 -4
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +269 -4
- package/dist/index.cjs +269 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +252 -1
- package/dist/index.d.ts +252 -1
- package/dist/index.js +269 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -11669,7 +11669,7 @@ function reconcilePrice(model, live) {
|
|
|
11669
11669
|
}
|
|
11670
11670
|
|
|
11671
11671
|
// src/constants.ts
|
|
11672
|
-
var CASCADE_VERSION = "0.
|
|
11672
|
+
var CASCADE_VERSION = "0.78.0";
|
|
11673
11673
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
11674
11674
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
11675
11675
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -16130,6 +16130,20 @@ var ToolsConfigSchema = zod.z.object({
|
|
|
16130
16130
|
shellBlocklist: zod.z.array(zod.z.string()).default(["rm -rf", "sudo rm", "format", "mkfs"]),
|
|
16131
16131
|
requireApprovalFor: zod.z.array(zod.z.string()).default([]),
|
|
16132
16132
|
browserEnabled: zod.z.boolean().default(false),
|
|
16133
|
+
/**
|
|
16134
|
+
* Let a run ACT on the page open in the desktop's built-in browser — click,
|
|
16135
|
+
* type, submit — not just read it.
|
|
16136
|
+
*
|
|
16137
|
+
* Distinct from `browserEnabled`, which turns on the headless Playwright
|
|
16138
|
+
* tool. That one drives a throwaway browser with an empty cookie jar; this
|
|
16139
|
+
* one drives the session the user is signed into, which is a different order
|
|
16140
|
+
* of consequence and so gets its own switch rather than riding on that one.
|
|
16141
|
+
*
|
|
16142
|
+
* Off by default. Turning it on does not hand anything over silently: the
|
|
16143
|
+
* tool still goes through the approval chain, and the browser panel carries a
|
|
16144
|
+
* stop control for the run that is acting.
|
|
16145
|
+
*/
|
|
16146
|
+
agentBrowserControl: zod.z.boolean().default(false),
|
|
16133
16147
|
mcpServers: zod.z.array(McpServerConfigSchema).optional(),
|
|
16134
16148
|
mcpTrusted: zod.z.array(zod.z.string()).optional(),
|
|
16135
16149
|
/** Web search backends — at least one should be configured for best results */
|
|
@@ -20914,6 +20928,23 @@ Now execute your subtask using this context where relevant.`
|
|
|
20914
20928
|
this.setStatus("FAILED", finalOutput);
|
|
20915
20929
|
this.peerBus?.publish(this.id, assignment.subtaskId, finalOutput, "FAILED");
|
|
20916
20930
|
return this.buildResult("ESCALATED", finalOutput, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
20931
|
+
} finally {
|
|
20932
|
+
this.releaseBrowserControl();
|
|
20933
|
+
}
|
|
20934
|
+
}
|
|
20935
|
+
/**
|
|
20936
|
+
* End this worker's hold on the shared browser.
|
|
20937
|
+
*
|
|
20938
|
+
* Duck-typed rather than imported: `browser_control` is registered only by a
|
|
20939
|
+
* host that HAS a browser (the desktop), so in the CLI and hosted runs there
|
|
20940
|
+
* is no such tool and this is a no-op. Importing the class to instanceof-check
|
|
20941
|
+
* it would couple the worker to a tool that usually is not there.
|
|
20942
|
+
*/
|
|
20943
|
+
releaseBrowserControl() {
|
|
20944
|
+
try {
|
|
20945
|
+
const tool = this.toolRegistry.getTool?.("browser_control");
|
|
20946
|
+
tool?.releaseActor?.(this.id);
|
|
20947
|
+
} catch {
|
|
20917
20948
|
}
|
|
20918
20949
|
}
|
|
20919
20950
|
sendToPeer(toId, content) {
|
|
@@ -30730,6 +30761,127 @@ var CurrentPageTool = class extends BaseTool {
|
|
|
30730
30761
|
}
|
|
30731
30762
|
};
|
|
30732
30763
|
|
|
30764
|
+
// src/tools/browser-control.ts
|
|
30765
|
+
var BrowserControlTool = class extends BaseTool {
|
|
30766
|
+
name = "browser_control";
|
|
30767
|
+
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.";
|
|
30768
|
+
inputSchema = {
|
|
30769
|
+
type: "object",
|
|
30770
|
+
properties: {
|
|
30771
|
+
action: {
|
|
30772
|
+
type: "string",
|
|
30773
|
+
enum: ["navigate", "click", "fill", "press", "wait_for", "extract_text"],
|
|
30774
|
+
description: "What to do."
|
|
30775
|
+
},
|
|
30776
|
+
url: { type: "string", description: "For navigate: the http(s) address to open." },
|
|
30777
|
+
selector: {
|
|
30778
|
+
type: "string",
|
|
30779
|
+
description: "CSS selector for click, fill and wait_for. Optional for extract_text (defaults to the whole page)."
|
|
30780
|
+
},
|
|
30781
|
+
value: { type: "string", description: "For fill: the text to type." },
|
|
30782
|
+
key: { type: "string", description: 'For press: a key name such as "Enter", "Tab" or "Escape".' },
|
|
30783
|
+
timeoutMs: { type: "number", description: "For wait_for: how long to wait before giving up (default 10000, max 30000)." }
|
|
30784
|
+
},
|
|
30785
|
+
required: ["action"]
|
|
30786
|
+
};
|
|
30787
|
+
controller;
|
|
30788
|
+
release;
|
|
30789
|
+
/** One-way. See `revoke`. */
|
|
30790
|
+
revoked = false;
|
|
30791
|
+
constructor(controller, release) {
|
|
30792
|
+
super();
|
|
30793
|
+
this.controller = controller;
|
|
30794
|
+
this.release = release;
|
|
30795
|
+
}
|
|
30796
|
+
/**
|
|
30797
|
+
* A worker has finished. Ends its hold on the browser.
|
|
30798
|
+
*
|
|
30799
|
+
* Called from the tier's terminal path rather than by the model, because a
|
|
30800
|
+
* model cannot be relied on to announce that it is done — which is why the
|
|
30801
|
+
* host cannot use idle time as the boundary instead.
|
|
30802
|
+
*/
|
|
30803
|
+
releaseActor(actorId) {
|
|
30804
|
+
this.release?.(actorId);
|
|
30805
|
+
}
|
|
30806
|
+
/**
|
|
30807
|
+
* Permanently disable this tool, whatever the registry still holds.
|
|
30808
|
+
*
|
|
30809
|
+
* Registration-time gates cannot express "this run turned out to be
|
|
30810
|
+
* unattended": a host that wires the controller and only then declares the
|
|
30811
|
+
* run unattended has already registered the tool, and a check that ran at
|
|
30812
|
+
* registration has no second chance. Enforcing at EXECUTION time makes the
|
|
30813
|
+
* guarantee independent of call order, which is the whole point of stating
|
|
30814
|
+
* the invariant rather than relying on the scheduler's current sequence.
|
|
30815
|
+
*
|
|
30816
|
+
* One-way on purpose. A safety gate that can be re-opened by a later call is
|
|
30817
|
+
* a gate that a refactor re-opens by accident.
|
|
30818
|
+
*/
|
|
30819
|
+
revoke() {
|
|
30820
|
+
this.revoked = true;
|
|
30821
|
+
}
|
|
30822
|
+
// Acting on a session the user is signed into is the highest-consequence
|
|
30823
|
+
// thing in the tool set — higher than shell, arguably, because the blast
|
|
30824
|
+
// radius is someone else's server rather than this machine.
|
|
30825
|
+
isDangerous() {
|
|
30826
|
+
return true;
|
|
30827
|
+
}
|
|
30828
|
+
async execute(input, options) {
|
|
30829
|
+
if (this.revoked) {
|
|
30830
|
+
return "Error: browser control is not available in this run. It requires a person watching who can stop it, and nobody is.";
|
|
30831
|
+
}
|
|
30832
|
+
const kind = input["action"];
|
|
30833
|
+
if (!kind) return "Error: action is required.";
|
|
30834
|
+
if (options?.signal?.aborted) return "Error: the run was cancelled before this action ran.";
|
|
30835
|
+
const missing = requiredFieldFor(kind, input);
|
|
30836
|
+
if (missing) return `Error: "${kind}" needs ${missing}.`;
|
|
30837
|
+
const action = {
|
|
30838
|
+
kind,
|
|
30839
|
+
...typeof input["url"] === "string" ? { url: input["url"] } : {},
|
|
30840
|
+
...typeof input["selector"] === "string" ? { selector: input["selector"] } : {},
|
|
30841
|
+
...typeof input["value"] === "string" ? { value: input["value"] } : {},
|
|
30842
|
+
...typeof input["key"] === "string" ? { key: input["key"] } : {},
|
|
30843
|
+
...typeof input["timeoutMs"] === "number" ? { timeoutMs: Math.min(Math.max(input["timeoutMs"], 0), 3e4) } : {}
|
|
30844
|
+
};
|
|
30845
|
+
let outcome;
|
|
30846
|
+
try {
|
|
30847
|
+
outcome = await this.controller(action, {
|
|
30848
|
+
// `sessionId` is the run. The host scopes both revocation and its
|
|
30849
|
+
// single-owner lease on it, so passing it is not telemetry — drop it
|
|
30850
|
+
// and a Stop in one run silently stops every later one.
|
|
30851
|
+
sessionId: options?.sessionId ?? "",
|
|
30852
|
+
actorId: options?.tierId ?? options?.sessionId ?? "",
|
|
30853
|
+
...options?.signal ? { signal: options.signal } : {}
|
|
30854
|
+
});
|
|
30855
|
+
} catch (err) {
|
|
30856
|
+
return `Error: the browser could not perform "${kind}" \u2014 ${err instanceof Error ? err.message : String(err)}`;
|
|
30857
|
+
}
|
|
30858
|
+
const where = outcome.url ? `
|
|
30859
|
+
Page: ${outcome.title ? `${outcome.title} \u2014 ` : ""}${outcome.url}` : "";
|
|
30860
|
+
return `${outcome.ok ? "" : "Failed: "}${outcome.detail}${where}`;
|
|
30861
|
+
}
|
|
30862
|
+
};
|
|
30863
|
+
function requiredFieldFor(kind, input) {
|
|
30864
|
+
const has = (k) => typeof input[k] === "string" && input[k].length > 0;
|
|
30865
|
+
const given = (k) => typeof input[k] === "string";
|
|
30866
|
+
switch (kind) {
|
|
30867
|
+
case "navigate":
|
|
30868
|
+
return has("url") ? null : "a url";
|
|
30869
|
+
case "click":
|
|
30870
|
+
return has("selector") ? null : "a selector";
|
|
30871
|
+
case "fill":
|
|
30872
|
+
return has("selector") ? given("value") ? null : "a value" : "a selector";
|
|
30873
|
+
case "press":
|
|
30874
|
+
return has("key") ? null : "a key";
|
|
30875
|
+
case "wait_for":
|
|
30876
|
+
return has("selector") ? null : "a selector";
|
|
30877
|
+
// extract_text works with or without a selector.
|
|
30878
|
+
case "extract_text":
|
|
30879
|
+
return null;
|
|
30880
|
+
default:
|
|
30881
|
+
return null;
|
|
30882
|
+
}
|
|
30883
|
+
}
|
|
30884
|
+
|
|
30733
30885
|
// src/core/cascade.ts
|
|
30734
30886
|
var ESCALATION_DECISION_TIMEOUT_MS = 5 * 6e4;
|
|
30735
30887
|
function buildContextualPrompt(prompt, history = []) {
|
|
@@ -31222,6 +31374,49 @@ ${input}` }],
|
|
|
31222
31374
|
if ((this.config.tools?.disabledTools ?? []).includes("read_current_page")) return;
|
|
31223
31375
|
this.toolRegistry.register(new CurrentPageTool(provider));
|
|
31224
31376
|
}
|
|
31377
|
+
/**
|
|
31378
|
+
* True when nobody is watching this run — a scheduled/cron task, which
|
|
31379
|
+
* auto-approves every tool because there is no one to ask.
|
|
31380
|
+
*
|
|
31381
|
+
* That auto-approval is fine for ordinary tools and disqualifying for this
|
|
31382
|
+
* one: `browser_control` drives the signed-in browser, and "every run asks
|
|
31383
|
+
* first" is the promise the Settings copy makes. The property used to hold
|
|
31384
|
+
* only because `runScheduledTask` happens not to call `setBrowserController`
|
|
31385
|
+
* — a guarantee resting on an ABSENT call, which is exactly what a later
|
|
31386
|
+
* refactor adds without noticing. Stated and enforced instead.
|
|
31387
|
+
*/
|
|
31388
|
+
unattended = false;
|
|
31389
|
+
/**
|
|
31390
|
+
* Declare that no human is watching this run, so capabilities that depend on
|
|
31391
|
+
* a person being able to see and stop them are refused outright.
|
|
31392
|
+
*/
|
|
31393
|
+
setUnattended(on) {
|
|
31394
|
+
this.unattended = on;
|
|
31395
|
+
if (!on) return;
|
|
31396
|
+
const tool = this.toolRegistry.getTool("browser_control");
|
|
31397
|
+
tool?.revoke?.();
|
|
31398
|
+
}
|
|
31399
|
+
/**
|
|
31400
|
+
* Wire the host's browser so a run can ACT on the open page, not just read it.
|
|
31401
|
+
*
|
|
31402
|
+
* Two gates before this registers anything, and both matter:
|
|
31403
|
+
*
|
|
31404
|
+
* - `tools.agentBrowserControl` must be on. Unlike read_current_page, which
|
|
31405
|
+
* reaches nothing the user has not already opened themselves, this drives
|
|
31406
|
+
* the session they are signed into. That is opt-in, not a default.
|
|
31407
|
+
* - `disabledTools` still removes it, the same as every other tool.
|
|
31408
|
+
* - The run must not be UNATTENDED. See `setUnattended`.
|
|
31409
|
+
*
|
|
31410
|
+
* Host-supplied like the page provider: the tool exists only where there is a
|
|
31411
|
+
* real browser to drive, so it never has to be refused in the CLI or a hosted
|
|
31412
|
+
* run — it is simply not there.
|
|
31413
|
+
*/
|
|
31414
|
+
setBrowserController(controller, release) {
|
|
31415
|
+
if (this.unattended) return;
|
|
31416
|
+
if (!this.config.tools?.agentBrowserControl) return;
|
|
31417
|
+
if ((this.config.tools?.disabledTools ?? []).includes("browser_control")) return;
|
|
31418
|
+
this.toolRegistry.register(new BrowserControlTool(controller, release));
|
|
31419
|
+
}
|
|
31225
31420
|
registerMediaTools(workspacePath) {
|
|
31226
31421
|
const denied = new Set(this.config.tools?.disabledTools ?? []);
|
|
31227
31422
|
const permitted = (name) => !denied.has(name);
|
|
@@ -31827,6 +32022,7 @@ ${prompt}` : prompt;
|
|
|
31827
32022
|
this.router.setRunSignal(options.signal);
|
|
31828
32023
|
const startMs = Date.now();
|
|
31829
32024
|
const taskId = crypto.randomUUID();
|
|
32025
|
+
this.emit("run:started", { taskId });
|
|
31830
32026
|
this.decisionLog = [];
|
|
31831
32027
|
this.currentRunCompleted = [];
|
|
31832
32028
|
this.wroteCheckpointThisRun = false;
|
|
@@ -32592,6 +32788,7 @@ function applySettingsPayload(config, data) {
|
|
|
32592
32788
|
if (["auto", "T1", "T2", "T3"].includes(a["forceTier"])) config.routing = { ...config.routing ?? {}, forceTier: a["forceTier"] };
|
|
32593
32789
|
if (typeof a["benchmarksLive"] === "boolean") config.benchmarks = { ...config.benchmarks ?? {}, live: a["benchmarksLive"] };
|
|
32594
32790
|
if (["isolate", "worker", "auto"].includes(a["dynamicToolSandbox"])) config.tools = { ...config.tools ?? {}, dynamicToolSandbox: a["dynamicToolSandbox"] };
|
|
32791
|
+
if (typeof a["agentBrowserControl"] === "boolean") config.tools = { ...config.tools ?? {}, agentBrowserControl: a["agentBrowserControl"] };
|
|
32595
32792
|
if (typeof a["factsExtraction"] === "boolean") config.knowledge = { ...config.knowledge ?? {}, factsExtraction: a["factsExtraction"] };
|
|
32596
32793
|
if (typeof a["rememberSessions"] === "boolean") config.memory = { ...config.memory ?? {}, rememberSessions: a["rememberSessions"] };
|
|
32597
32794
|
if (typeof a["enableToolCreation"] === "boolean") config.enableToolCreation = a["enableToolCreation"];
|
|
@@ -32649,6 +32846,7 @@ function settingsSnapshot(config) {
|
|
|
32649
32846
|
forceTier: config.routing?.forceTier,
|
|
32650
32847
|
benchmarksLive: config.benchmarks?.live,
|
|
32651
32848
|
dynamicToolSandbox: config.tools?.dynamicToolSandbox,
|
|
32849
|
+
agentBrowserControl: config.tools?.agentBrowserControl,
|
|
32652
32850
|
factsExtraction: config.knowledge?.factsExtraction,
|
|
32653
32851
|
rememberSessions: config.memory?.rememberSessions,
|
|
32654
32852
|
enableToolCreation: config.enableToolCreation,
|
|
@@ -33153,6 +33351,14 @@ var DashboardServer = class {
|
|
|
33153
33351
|
* always failing.
|
|
33154
33352
|
*/
|
|
33155
33353
|
currentPageProvider;
|
|
33354
|
+
/**
|
|
33355
|
+
* Supplied by the desktop shell so a run can ACT on that page — click, type,
|
|
33356
|
+
* submit — not just read it. Same reasoning as the provider above: absent in
|
|
33357
|
+
* the CLI, so the tool is not registered rather than present and failing.
|
|
33358
|
+
* Cascade applies the `tools.agentBrowserControl` gate on top of this.
|
|
33359
|
+
*/
|
|
33360
|
+
browserController;
|
|
33361
|
+
browserActorRelease;
|
|
33156
33362
|
activeControllers = /* @__PURE__ */ new Map();
|
|
33157
33363
|
/**
|
|
33158
33364
|
* Run taskIds per chat session — file snapshots are keyed by the run's
|
|
@@ -33214,6 +33420,7 @@ var DashboardServer = class {
|
|
|
33214
33420
|
});
|
|
33215
33421
|
this.socket.onConfigUpdate(async (data) => {
|
|
33216
33422
|
const result = await commitSettings(this.config, data, (committed) => this.persistConfig(committed));
|
|
33423
|
+
if (result.ok) this.settingsChangeHook?.(this.config);
|
|
33217
33424
|
const refused = result.refused.map((r) => ({
|
|
33218
33425
|
type: r.type,
|
|
33219
33426
|
reason: r.reason,
|
|
@@ -33237,6 +33444,7 @@ var DashboardServer = class {
|
|
|
33237
33444
|
const cascade = new Cascade(cfg, this.workspacePath, this.store);
|
|
33238
33445
|
this.activeSessions.set(sessionId, cascade);
|
|
33239
33446
|
if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
|
|
33447
|
+
if (this.browserController) cascade.setBrowserController(this.browserController, this.browserActorRelease);
|
|
33240
33448
|
cascade.on("stream:token", (e) => {
|
|
33241
33449
|
this.emitToSessionClients(socketId, sessionId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
33242
33450
|
});
|
|
@@ -33260,13 +33468,16 @@ var DashboardServer = class {
|
|
|
33260
33468
|
if (ev.requestId) this.clearPendingEscalation(ev.requestId);
|
|
33261
33469
|
this.emitToSessionClients(socketId, sessionId, "escalation:timeout", { sessionId, ...e });
|
|
33262
33470
|
});
|
|
33471
|
+
cascade.on("run:started", (e) => {
|
|
33472
|
+
const id = e.taskId;
|
|
33473
|
+
if (id) this.recordSessionTask(sessionId, id);
|
|
33474
|
+
});
|
|
33263
33475
|
try {
|
|
33264
33476
|
const result = await cascade.run({
|
|
33265
33477
|
prompt: runPrompt,
|
|
33266
33478
|
signal: abortController.signal,
|
|
33267
33479
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
33268
33480
|
});
|
|
33269
|
-
this.recordSessionTask(sessionId, result.taskId);
|
|
33270
33481
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
33271
33482
|
this.captureWhy(sessionId, cascade, result);
|
|
33272
33483
|
this.emitToSessionClients(socketId, sessionId, "session:complete", { sessionId, result });
|
|
@@ -33332,6 +33543,49 @@ var DashboardServer = class {
|
|
|
33332
33543
|
setCurrentPageProvider(provider) {
|
|
33333
33544
|
this.currentPageProvider = provider;
|
|
33334
33545
|
}
|
|
33546
|
+
/** Let runs act on that view, not just read it (desktop only). */
|
|
33547
|
+
setBrowserController(controller, release) {
|
|
33548
|
+
this.browserController = controller;
|
|
33549
|
+
this.browserActorRelease = release;
|
|
33550
|
+
}
|
|
33551
|
+
/**
|
|
33552
|
+
* Told whenever settings are committed through THIS server.
|
|
33553
|
+
*
|
|
33554
|
+
* Settings reach the live config by two routes — the desktop's Electron IPC
|
|
33555
|
+
* handler and this server's `config:update` socket — and both run the same
|
|
33556
|
+
* `commitSettings`. The desktop mirrors `tools.agentBrowserControl` into its
|
|
33557
|
+
* browser module after its own route, but had no way to learn about the
|
|
33558
|
+
* other, so a socket write left the module's copy stale: an enable the module
|
|
33559
|
+
* still refused, or worse a disable the module still honoured for a run
|
|
33560
|
+
* already holding the tool. This is the same sibling-writer drift
|
|
33561
|
+
* `settings-payload.ts` exists to prevent, one layer up.
|
|
33562
|
+
*/
|
|
33563
|
+
settingsChangeHook;
|
|
33564
|
+
onSettingsChanged(hook) {
|
|
33565
|
+
this.settingsChangeHook = hook;
|
|
33566
|
+
}
|
|
33567
|
+
/**
|
|
33568
|
+
* Told when a run ends, so a host holding run-scoped state can release it.
|
|
33569
|
+
*
|
|
33570
|
+
* The desktop uses this to retire a run's browser Stop control: that has to
|
|
33571
|
+
* outlive each individual action (the gap between two actions is when the
|
|
33572
|
+
* user most wants it) but must not outlive the run.
|
|
33573
|
+
*/
|
|
33574
|
+
/**
|
|
33575
|
+
* Fired when a run ends, carrying BOTH identifiers explicitly.
|
|
33576
|
+
*
|
|
33577
|
+
* They are different things and conflating them was a live bug: a chat
|
|
33578
|
+
* `sessionId` can contain many runs (hence `sessionTaskIds`), while
|
|
33579
|
+
* `Cascade.run()` mints a fresh random `taskId` per run — and it is the
|
|
33580
|
+
* taskId that tools see as `ToolExecuteOptions.sessionId`. Passing only the
|
|
33581
|
+
* chat id meant browser cleanup keyed on the task id matched nothing, so a
|
|
33582
|
+
* finished run kept its Stop banner armed and held its browser lease until
|
|
33583
|
+
* the deadlock ceiling.
|
|
33584
|
+
*/
|
|
33585
|
+
runEndedHook;
|
|
33586
|
+
onRunEnded(hook) {
|
|
33587
|
+
this.runEndedHook = hook;
|
|
33588
|
+
}
|
|
33335
33589
|
async start() {
|
|
33336
33590
|
const isLoopback = this.host === "127.0.0.1" || this.host === "::1" || this.host === "localhost";
|
|
33337
33591
|
if (!isLoopback) {
|
|
@@ -33522,6 +33776,8 @@ var DashboardServer = class {
|
|
|
33522
33776
|
}
|
|
33523
33777
|
/** Record run end: the assistant reply (when there is one) and the runtime row's final status. */
|
|
33524
33778
|
persistRunEnd(sessionId, title, latestPrompt, reply, status, result) {
|
|
33779
|
+
const endedTaskId = result?.taskId ?? this.sessionTaskIds.get(sessionId)?.at(-1);
|
|
33780
|
+
this.runEndedHook?.({ sessionId, ...endedTaskId ? { taskId: endedTaskId } : {} });
|
|
33525
33781
|
try {
|
|
33526
33782
|
if (reply && reply.trim()) {
|
|
33527
33783
|
this.store.addMessage({ id: crypto.randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -33615,6 +33871,7 @@ var DashboardServer = class {
|
|
|
33615
33871
|
const prompt = task.prompt;
|
|
33616
33872
|
const title = this.persistRunStart(sessionId, `[${task.name}] ${prompt}`);
|
|
33617
33873
|
const cascade = new Cascade(this.config, task.workspacePath ?? this.workspacePath, this.store);
|
|
33874
|
+
cascade.setUnattended(true);
|
|
33618
33875
|
this.activeSessions.set(sessionId, cascade);
|
|
33619
33876
|
cascade.on("tier:status", (e) => {
|
|
33620
33877
|
this.socket.broadcast("tier:status", { sessionId, ...e });
|
|
@@ -33622,13 +33879,16 @@ var DashboardServer = class {
|
|
|
33622
33879
|
cascade.on("peer:message", (e) => {
|
|
33623
33880
|
this.socket.emitPeerMessage(e);
|
|
33624
33881
|
});
|
|
33882
|
+
cascade.on("run:started", (e) => {
|
|
33883
|
+
const id = e.taskId;
|
|
33884
|
+
if (id) this.recordSessionTask(sessionId, id);
|
|
33885
|
+
});
|
|
33625
33886
|
try {
|
|
33626
33887
|
const result = await cascade.run({
|
|
33627
33888
|
prompt,
|
|
33628
33889
|
identityId: task.identityId,
|
|
33629
33890
|
approvalCallback: async () => ({ approved: true, always: false })
|
|
33630
33891
|
});
|
|
33631
|
-
this.recordSessionTask(sessionId, result.taskId);
|
|
33632
33892
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
33633
33893
|
this.captureWhy(sessionId, cascade, result);
|
|
33634
33894
|
this.socket.broadcast("session:complete", { sessionId, result });
|
|
@@ -34261,6 +34521,7 @@ ${prompt}`;
|
|
|
34261
34521
|
const cascade = new Cascade(this.config, this.workspacePath, this.store);
|
|
34262
34522
|
this.activeSessions.set(sessionId, cascade);
|
|
34263
34523
|
if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
|
|
34524
|
+
if (this.browserController) cascade.setBrowserController(this.browserController, this.browserActorRelease);
|
|
34264
34525
|
cascade.on("stream:token", (e) => {
|
|
34265
34526
|
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
34266
34527
|
});
|
|
@@ -34284,13 +34545,16 @@ ${prompt}`;
|
|
|
34284
34545
|
if (ev.requestId) this.clearPendingEscalation(ev.requestId);
|
|
34285
34546
|
this.socket.broadcastToRoom(`session:${sessionId}`, "escalation:timeout", { sessionId, ...e });
|
|
34286
34547
|
});
|
|
34548
|
+
cascade.on("run:started", (e) => {
|
|
34549
|
+
const id = e.taskId;
|
|
34550
|
+
if (id) this.recordSessionTask(sessionId, id);
|
|
34551
|
+
});
|
|
34287
34552
|
try {
|
|
34288
34553
|
const result = await cascade.run({
|
|
34289
34554
|
prompt: runPrompt,
|
|
34290
34555
|
identityId: body.identityId,
|
|
34291
34556
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
34292
34557
|
});
|
|
34293
|
-
this.recordSessionTask(sessionId, result.taskId);
|
|
34294
34558
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
34295
34559
|
this.captureWhy(sessionId, cascade, result);
|
|
34296
34560
|
this.socket.broadcast("cost:update", {
|
|
@@ -35333,6 +35597,7 @@ function applySyncBundle(bundle, config, cleanup) {
|
|
|
35333
35597
|
exports.AZURE_BASE_MODELS = AZURE_BASE_MODELS;
|
|
35334
35598
|
exports.AZURE_BASE_URL_TEMPLATE = AZURE_BASE_URL_TEMPLATE;
|
|
35335
35599
|
exports.AuditLogger = AuditLogger;
|
|
35600
|
+
exports.BrowserControlTool = BrowserControlTool;
|
|
35336
35601
|
exports.CASCADE_AUDIT_FILE = CASCADE_AUDIT_FILE;
|
|
35337
35602
|
exports.CASCADE_CONFIG_DIR = CASCADE_CONFIG_DIR;
|
|
35338
35603
|
exports.CASCADE_CONFIG_FILE = CASCADE_CONFIG_FILE;
|