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/desktop-core.cjs
CHANGED
|
@@ -312562,7 +312562,7 @@ function reconcilePrice(model, live) {
|
|
|
312562
312562
|
}
|
|
312563
312563
|
|
|
312564
312564
|
// src/constants.ts
|
|
312565
|
-
var CASCADE_VERSION = "0.
|
|
312565
|
+
var CASCADE_VERSION = "0.78.0";
|
|
312566
312566
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
312567
312567
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
312568
312568
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -346971,6 +346971,20 @@ var ToolsConfigSchema = external_exports.object({
|
|
|
346971
346971
|
shellBlocklist: external_exports.array(external_exports.string()).default(["rm -rf", "sudo rm", "format", "mkfs"]),
|
|
346972
346972
|
requireApprovalFor: external_exports.array(external_exports.string()).default([]),
|
|
346973
346973
|
browserEnabled: external_exports.boolean().default(false),
|
|
346974
|
+
/**
|
|
346975
|
+
* Let a run ACT on the page open in the desktop's built-in browser — click,
|
|
346976
|
+
* type, submit — not just read it.
|
|
346977
|
+
*
|
|
346978
|
+
* Distinct from `browserEnabled`, which turns on the headless Playwright
|
|
346979
|
+
* tool. That one drives a throwaway browser with an empty cookie jar; this
|
|
346980
|
+
* one drives the session the user is signed into, which is a different order
|
|
346981
|
+
* of consequence and so gets its own switch rather than riding on that one.
|
|
346982
|
+
*
|
|
346983
|
+
* Off by default. Turning it on does not hand anything over silently: the
|
|
346984
|
+
* tool still goes through the approval chain, and the browser panel carries a
|
|
346985
|
+
* stop control for the run that is acting.
|
|
346986
|
+
*/
|
|
346987
|
+
agentBrowserControl: external_exports.boolean().default(false),
|
|
346974
346988
|
mcpServers: external_exports.array(McpServerConfigSchema).optional(),
|
|
346975
346989
|
mcpTrusted: external_exports.array(external_exports.string()).optional(),
|
|
346976
346990
|
/** Web search backends — at least one should be configured for best results */
|
|
@@ -351755,6 +351769,23 @@ Now execute your subtask using this context where relevant.`
|
|
|
351755
351769
|
this.setStatus("FAILED", finalOutput);
|
|
351756
351770
|
this.peerBus?.publish(this.id, assignment.subtaskId, finalOutput, "FAILED");
|
|
351757
351771
|
return this.buildResult("ESCALATED", finalOutput, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
351772
|
+
} finally {
|
|
351773
|
+
this.releaseBrowserControl();
|
|
351774
|
+
}
|
|
351775
|
+
}
|
|
351776
|
+
/**
|
|
351777
|
+
* End this worker's hold on the shared browser.
|
|
351778
|
+
*
|
|
351779
|
+
* Duck-typed rather than imported: `browser_control` is registered only by a
|
|
351780
|
+
* host that HAS a browser (the desktop), so in the CLI and hosted runs there
|
|
351781
|
+
* is no such tool and this is a no-op. Importing the class to instanceof-check
|
|
351782
|
+
* it would couple the worker to a tool that usually is not there.
|
|
351783
|
+
*/
|
|
351784
|
+
releaseBrowserControl() {
|
|
351785
|
+
try {
|
|
351786
|
+
const tool = this.toolRegistry.getTool?.("browser_control");
|
|
351787
|
+
tool?.releaseActor?.(this.id);
|
|
351788
|
+
} catch {
|
|
351758
351789
|
}
|
|
351759
351790
|
}
|
|
351760
351791
|
sendToPeer(toId, content) {
|
|
@@ -397857,6 +397888,127 @@ var CurrentPageTool = class extends BaseTool {
|
|
|
397857
397888
|
}
|
|
397858
397889
|
};
|
|
397859
397890
|
|
|
397891
|
+
// src/tools/browser-control.ts
|
|
397892
|
+
var BrowserControlTool = class extends BaseTool {
|
|
397893
|
+
name = "browser_control";
|
|
397894
|
+
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.";
|
|
397895
|
+
inputSchema = {
|
|
397896
|
+
type: "object",
|
|
397897
|
+
properties: {
|
|
397898
|
+
action: {
|
|
397899
|
+
type: "string",
|
|
397900
|
+
enum: ["navigate", "click", "fill", "press", "wait_for", "extract_text"],
|
|
397901
|
+
description: "What to do."
|
|
397902
|
+
},
|
|
397903
|
+
url: { type: "string", description: "For navigate: the http(s) address to open." },
|
|
397904
|
+
selector: {
|
|
397905
|
+
type: "string",
|
|
397906
|
+
description: "CSS selector for click, fill and wait_for. Optional for extract_text (defaults to the whole page)."
|
|
397907
|
+
},
|
|
397908
|
+
value: { type: "string", description: "For fill: the text to type." },
|
|
397909
|
+
key: { type: "string", description: 'For press: a key name such as "Enter", "Tab" or "Escape".' },
|
|
397910
|
+
timeoutMs: { type: "number", description: "For wait_for: how long to wait before giving up (default 10000, max 30000)." }
|
|
397911
|
+
},
|
|
397912
|
+
required: ["action"]
|
|
397913
|
+
};
|
|
397914
|
+
controller;
|
|
397915
|
+
release;
|
|
397916
|
+
/** One-way. See `revoke`. */
|
|
397917
|
+
revoked = false;
|
|
397918
|
+
constructor(controller, release) {
|
|
397919
|
+
super();
|
|
397920
|
+
this.controller = controller;
|
|
397921
|
+
this.release = release;
|
|
397922
|
+
}
|
|
397923
|
+
/**
|
|
397924
|
+
* A worker has finished. Ends its hold on the browser.
|
|
397925
|
+
*
|
|
397926
|
+
* Called from the tier's terminal path rather than by the model, because a
|
|
397927
|
+
* model cannot be relied on to announce that it is done — which is why the
|
|
397928
|
+
* host cannot use idle time as the boundary instead.
|
|
397929
|
+
*/
|
|
397930
|
+
releaseActor(actorId) {
|
|
397931
|
+
this.release?.(actorId);
|
|
397932
|
+
}
|
|
397933
|
+
/**
|
|
397934
|
+
* Permanently disable this tool, whatever the registry still holds.
|
|
397935
|
+
*
|
|
397936
|
+
* Registration-time gates cannot express "this run turned out to be
|
|
397937
|
+
* unattended": a host that wires the controller and only then declares the
|
|
397938
|
+
* run unattended has already registered the tool, and a check that ran at
|
|
397939
|
+
* registration has no second chance. Enforcing at EXECUTION time makes the
|
|
397940
|
+
* guarantee independent of call order, which is the whole point of stating
|
|
397941
|
+
* the invariant rather than relying on the scheduler's current sequence.
|
|
397942
|
+
*
|
|
397943
|
+
* One-way on purpose. A safety gate that can be re-opened by a later call is
|
|
397944
|
+
* a gate that a refactor re-opens by accident.
|
|
397945
|
+
*/
|
|
397946
|
+
revoke() {
|
|
397947
|
+
this.revoked = true;
|
|
397948
|
+
}
|
|
397949
|
+
// Acting on a session the user is signed into is the highest-consequence
|
|
397950
|
+
// thing in the tool set — higher than shell, arguably, because the blast
|
|
397951
|
+
// radius is someone else's server rather than this machine.
|
|
397952
|
+
isDangerous() {
|
|
397953
|
+
return true;
|
|
397954
|
+
}
|
|
397955
|
+
async execute(input, options) {
|
|
397956
|
+
if (this.revoked) {
|
|
397957
|
+
return "Error: browser control is not available in this run. It requires a person watching who can stop it, and nobody is.";
|
|
397958
|
+
}
|
|
397959
|
+
const kind2 = input["action"];
|
|
397960
|
+
if (!kind2) return "Error: action is required.";
|
|
397961
|
+
if (options?.signal?.aborted) return "Error: the run was cancelled before this action ran.";
|
|
397962
|
+
const missing = requiredFieldFor(kind2, input);
|
|
397963
|
+
if (missing) return `Error: "${kind2}" needs ${missing}.`;
|
|
397964
|
+
const action = {
|
|
397965
|
+
kind: kind2,
|
|
397966
|
+
...typeof input["url"] === "string" ? { url: input["url"] } : {},
|
|
397967
|
+
...typeof input["selector"] === "string" ? { selector: input["selector"] } : {},
|
|
397968
|
+
...typeof input["value"] === "string" ? { value: input["value"] } : {},
|
|
397969
|
+
...typeof input["key"] === "string" ? { key: input["key"] } : {},
|
|
397970
|
+
...typeof input["timeoutMs"] === "number" ? { timeoutMs: Math.min(Math.max(input["timeoutMs"], 0), 3e4) } : {}
|
|
397971
|
+
};
|
|
397972
|
+
let outcome;
|
|
397973
|
+
try {
|
|
397974
|
+
outcome = await this.controller(action, {
|
|
397975
|
+
// `sessionId` is the run. The host scopes both revocation and its
|
|
397976
|
+
// single-owner lease on it, so passing it is not telemetry — drop it
|
|
397977
|
+
// and a Stop in one run silently stops every later one.
|
|
397978
|
+
sessionId: options?.sessionId ?? "",
|
|
397979
|
+
actorId: options?.tierId ?? options?.sessionId ?? "",
|
|
397980
|
+
...options?.signal ? { signal: options.signal } : {}
|
|
397981
|
+
});
|
|
397982
|
+
} catch (err) {
|
|
397983
|
+
return `Error: the browser could not perform "${kind2}" \u2014 ${err instanceof Error ? err.message : String(err)}`;
|
|
397984
|
+
}
|
|
397985
|
+
const where = outcome.url ? `
|
|
397986
|
+
Page: ${outcome.title ? `${outcome.title} \u2014 ` : ""}${outcome.url}` : "";
|
|
397987
|
+
return `${outcome.ok ? "" : "Failed: "}${outcome.detail}${where}`;
|
|
397988
|
+
}
|
|
397989
|
+
};
|
|
397990
|
+
function requiredFieldFor(kind2, input) {
|
|
397991
|
+
const has2 = (k4) => typeof input[k4] === "string" && input[k4].length > 0;
|
|
397992
|
+
const given = (k4) => typeof input[k4] === "string";
|
|
397993
|
+
switch (kind2) {
|
|
397994
|
+
case "navigate":
|
|
397995
|
+
return has2("url") ? null : "a url";
|
|
397996
|
+
case "click":
|
|
397997
|
+
return has2("selector") ? null : "a selector";
|
|
397998
|
+
case "fill":
|
|
397999
|
+
return has2("selector") ? given("value") ? null : "a value" : "a selector";
|
|
398000
|
+
case "press":
|
|
398001
|
+
return has2("key") ? null : "a key";
|
|
398002
|
+
case "wait_for":
|
|
398003
|
+
return has2("selector") ? null : "a selector";
|
|
398004
|
+
// extract_text works with or without a selector.
|
|
398005
|
+
case "extract_text":
|
|
398006
|
+
return null;
|
|
398007
|
+
default:
|
|
398008
|
+
return null;
|
|
398009
|
+
}
|
|
398010
|
+
}
|
|
398011
|
+
|
|
397860
398012
|
// src/core/cascade.ts
|
|
397861
398013
|
var ESCALATION_DECISION_TIMEOUT_MS = 5 * 6e4;
|
|
397862
398014
|
function buildContextualPrompt(prompt, history = []) {
|
|
@@ -398349,6 +398501,49 @@ ${input}` }],
|
|
|
398349
398501
|
if ((this.config.tools?.disabledTools ?? []).includes("read_current_page")) return;
|
|
398350
398502
|
this.toolRegistry.register(new CurrentPageTool(provider));
|
|
398351
398503
|
}
|
|
398504
|
+
/**
|
|
398505
|
+
* True when nobody is watching this run — a scheduled/cron task, which
|
|
398506
|
+
* auto-approves every tool because there is no one to ask.
|
|
398507
|
+
*
|
|
398508
|
+
* That auto-approval is fine for ordinary tools and disqualifying for this
|
|
398509
|
+
* one: `browser_control` drives the signed-in browser, and "every run asks
|
|
398510
|
+
* first" is the promise the Settings copy makes. The property used to hold
|
|
398511
|
+
* only because `runScheduledTask` happens not to call `setBrowserController`
|
|
398512
|
+
* — a guarantee resting on an ABSENT call, which is exactly what a later
|
|
398513
|
+
* refactor adds without noticing. Stated and enforced instead.
|
|
398514
|
+
*/
|
|
398515
|
+
unattended = false;
|
|
398516
|
+
/**
|
|
398517
|
+
* Declare that no human is watching this run, so capabilities that depend on
|
|
398518
|
+
* a person being able to see and stop them are refused outright.
|
|
398519
|
+
*/
|
|
398520
|
+
setUnattended(on) {
|
|
398521
|
+
this.unattended = on;
|
|
398522
|
+
if (!on) return;
|
|
398523
|
+
const tool = this.toolRegistry.getTool("browser_control");
|
|
398524
|
+
tool?.revoke?.();
|
|
398525
|
+
}
|
|
398526
|
+
/**
|
|
398527
|
+
* Wire the host's browser so a run can ACT on the open page, not just read it.
|
|
398528
|
+
*
|
|
398529
|
+
* Two gates before this registers anything, and both matter:
|
|
398530
|
+
*
|
|
398531
|
+
* - `tools.agentBrowserControl` must be on. Unlike read_current_page, which
|
|
398532
|
+
* reaches nothing the user has not already opened themselves, this drives
|
|
398533
|
+
* the session they are signed into. That is opt-in, not a default.
|
|
398534
|
+
* - `disabledTools` still removes it, the same as every other tool.
|
|
398535
|
+
* - The run must not be UNATTENDED. See `setUnattended`.
|
|
398536
|
+
*
|
|
398537
|
+
* Host-supplied like the page provider: the tool exists only where there is a
|
|
398538
|
+
* real browser to drive, so it never has to be refused in the CLI or a hosted
|
|
398539
|
+
* run — it is simply not there.
|
|
398540
|
+
*/
|
|
398541
|
+
setBrowserController(controller, release) {
|
|
398542
|
+
if (this.unattended) return;
|
|
398543
|
+
if (!this.config.tools?.agentBrowserControl) return;
|
|
398544
|
+
if ((this.config.tools?.disabledTools ?? []).includes("browser_control")) return;
|
|
398545
|
+
this.toolRegistry.register(new BrowserControlTool(controller, release));
|
|
398546
|
+
}
|
|
398352
398547
|
registerMediaTools(workspacePath) {
|
|
398353
398548
|
const denied = new Set(this.config.tools?.disabledTools ?? []);
|
|
398354
398549
|
const permitted = (name) => !denied.has(name);
|
|
@@ -398954,6 +399149,7 @@ ${prompt}` : prompt;
|
|
|
398954
399149
|
this.router.setRunSignal(options.signal);
|
|
398955
399150
|
const startMs = Date.now();
|
|
398956
399151
|
const taskId = crypto2.randomUUID();
|
|
399152
|
+
this.emit("run:started", { taskId });
|
|
398957
399153
|
this.decisionLog = [];
|
|
398958
399154
|
this.currentRunCompleted = [];
|
|
398959
399155
|
this.wroteCheckpointThisRun = false;
|
|
@@ -399719,6 +399915,7 @@ function applySettingsPayload(config2, data) {
|
|
|
399719
399915
|
if (["auto", "T1", "T2", "T3"].includes(a2["forceTier"])) config2.routing = { ...config2.routing ?? {}, forceTier: a2["forceTier"] };
|
|
399720
399916
|
if (typeof a2["benchmarksLive"] === "boolean") config2.benchmarks = { ...config2.benchmarks ?? {}, live: a2["benchmarksLive"] };
|
|
399721
399917
|
if (["isolate", "worker", "auto"].includes(a2["dynamicToolSandbox"])) config2.tools = { ...config2.tools ?? {}, dynamicToolSandbox: a2["dynamicToolSandbox"] };
|
|
399918
|
+
if (typeof a2["agentBrowserControl"] === "boolean") config2.tools = { ...config2.tools ?? {}, agentBrowserControl: a2["agentBrowserControl"] };
|
|
399722
399919
|
if (typeof a2["factsExtraction"] === "boolean") config2.knowledge = { ...config2.knowledge ?? {}, factsExtraction: a2["factsExtraction"] };
|
|
399723
399920
|
if (typeof a2["rememberSessions"] === "boolean") config2.memory = { ...config2.memory ?? {}, rememberSessions: a2["rememberSessions"] };
|
|
399724
399921
|
if (typeof a2["enableToolCreation"] === "boolean") config2.enableToolCreation = a2["enableToolCreation"];
|
|
@@ -399776,6 +399973,7 @@ function settingsSnapshot(config2) {
|
|
|
399776
399973
|
forceTier: config2.routing?.forceTier,
|
|
399777
399974
|
benchmarksLive: config2.benchmarks?.live,
|
|
399778
399975
|
dynamicToolSandbox: config2.tools?.dynamicToolSandbox,
|
|
399976
|
+
agentBrowserControl: config2.tools?.agentBrowserControl,
|
|
399779
399977
|
factsExtraction: config2.knowledge?.factsExtraction,
|
|
399780
399978
|
rememberSessions: config2.memory?.rememberSessions,
|
|
399781
399979
|
enableToolCreation: config2.enableToolCreation,
|
|
@@ -401064,6 +401262,14 @@ var DashboardServer = class {
|
|
|
401064
401262
|
* always failing.
|
|
401065
401263
|
*/
|
|
401066
401264
|
currentPageProvider;
|
|
401265
|
+
/**
|
|
401266
|
+
* Supplied by the desktop shell so a run can ACT on that page — click, type,
|
|
401267
|
+
* submit — not just read it. Same reasoning as the provider above: absent in
|
|
401268
|
+
* the CLI, so the tool is not registered rather than present and failing.
|
|
401269
|
+
* Cascade applies the `tools.agentBrowserControl` gate on top of this.
|
|
401270
|
+
*/
|
|
401271
|
+
browserController;
|
|
401272
|
+
browserActorRelease;
|
|
401067
401273
|
activeControllers = /* @__PURE__ */ new Map();
|
|
401068
401274
|
/**
|
|
401069
401275
|
* Run taskIds per chat session — file snapshots are keyed by the run's
|
|
@@ -401125,6 +401331,7 @@ var DashboardServer = class {
|
|
|
401125
401331
|
});
|
|
401126
401332
|
this.socket.onConfigUpdate(async (data) => {
|
|
401127
401333
|
const result = await commitSettings(this.config, data, (committed) => this.persistConfig(committed));
|
|
401334
|
+
if (result.ok) this.settingsChangeHook?.(this.config);
|
|
401128
401335
|
const refused = result.refused.map((r4) => ({
|
|
401129
401336
|
type: r4.type,
|
|
401130
401337
|
reason: r4.reason,
|
|
@@ -401148,6 +401355,7 @@ var DashboardServer = class {
|
|
|
401148
401355
|
const cascade = new Cascade(cfg, this.workspacePath, this.store);
|
|
401149
401356
|
this.activeSessions.set(sessionId, cascade);
|
|
401150
401357
|
if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
|
|
401358
|
+
if (this.browserController) cascade.setBrowserController(this.browserController, this.browserActorRelease);
|
|
401151
401359
|
cascade.on("stream:token", (e3) => {
|
|
401152
401360
|
this.emitToSessionClients(socketId, sessionId, "stream:token", { sessionId, tierId: e3.tierId, text: e3.text, primary: e3.primary });
|
|
401153
401361
|
});
|
|
@@ -401171,13 +401379,16 @@ var DashboardServer = class {
|
|
|
401171
401379
|
if (ev.requestId) this.clearPendingEscalation(ev.requestId);
|
|
401172
401380
|
this.emitToSessionClients(socketId, sessionId, "escalation:timeout", { sessionId, ...e3 });
|
|
401173
401381
|
});
|
|
401382
|
+
cascade.on("run:started", (e3) => {
|
|
401383
|
+
const id = e3.taskId;
|
|
401384
|
+
if (id) this.recordSessionTask(sessionId, id);
|
|
401385
|
+
});
|
|
401174
401386
|
try {
|
|
401175
401387
|
const result = await cascade.run({
|
|
401176
401388
|
prompt: runPrompt,
|
|
401177
401389
|
signal: abortController.signal,
|
|
401178
401390
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
401179
401391
|
});
|
|
401180
|
-
this.recordSessionTask(sessionId, result.taskId);
|
|
401181
401392
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
401182
401393
|
this.captureWhy(sessionId, cascade, result);
|
|
401183
401394
|
this.emitToSessionClients(socketId, sessionId, "session:complete", { sessionId, result });
|
|
@@ -401243,6 +401454,49 @@ var DashboardServer = class {
|
|
|
401243
401454
|
setCurrentPageProvider(provider) {
|
|
401244
401455
|
this.currentPageProvider = provider;
|
|
401245
401456
|
}
|
|
401457
|
+
/** Let runs act on that view, not just read it (desktop only). */
|
|
401458
|
+
setBrowserController(controller, release) {
|
|
401459
|
+
this.browserController = controller;
|
|
401460
|
+
this.browserActorRelease = release;
|
|
401461
|
+
}
|
|
401462
|
+
/**
|
|
401463
|
+
* Told whenever settings are committed through THIS server.
|
|
401464
|
+
*
|
|
401465
|
+
* Settings reach the live config by two routes — the desktop's Electron IPC
|
|
401466
|
+
* handler and this server's `config:update` socket — and both run the same
|
|
401467
|
+
* `commitSettings`. The desktop mirrors `tools.agentBrowserControl` into its
|
|
401468
|
+
* browser module after its own route, but had no way to learn about the
|
|
401469
|
+
* other, so a socket write left the module's copy stale: an enable the module
|
|
401470
|
+
* still refused, or worse a disable the module still honoured for a run
|
|
401471
|
+
* already holding the tool. This is the same sibling-writer drift
|
|
401472
|
+
* `settings-payload.ts` exists to prevent, one layer up.
|
|
401473
|
+
*/
|
|
401474
|
+
settingsChangeHook;
|
|
401475
|
+
onSettingsChanged(hook) {
|
|
401476
|
+
this.settingsChangeHook = hook;
|
|
401477
|
+
}
|
|
401478
|
+
/**
|
|
401479
|
+
* Told when a run ends, so a host holding run-scoped state can release it.
|
|
401480
|
+
*
|
|
401481
|
+
* The desktop uses this to retire a run's browser Stop control: that has to
|
|
401482
|
+
* outlive each individual action (the gap between two actions is when the
|
|
401483
|
+
* user most wants it) but must not outlive the run.
|
|
401484
|
+
*/
|
|
401485
|
+
/**
|
|
401486
|
+
* Fired when a run ends, carrying BOTH identifiers explicitly.
|
|
401487
|
+
*
|
|
401488
|
+
* They are different things and conflating them was a live bug: a chat
|
|
401489
|
+
* `sessionId` can contain many runs (hence `sessionTaskIds`), while
|
|
401490
|
+
* `Cascade.run()` mints a fresh random `taskId` per run — and it is the
|
|
401491
|
+
* taskId that tools see as `ToolExecuteOptions.sessionId`. Passing only the
|
|
401492
|
+
* chat id meant browser cleanup keyed on the task id matched nothing, so a
|
|
401493
|
+
* finished run kept its Stop banner armed and held its browser lease until
|
|
401494
|
+
* the deadlock ceiling.
|
|
401495
|
+
*/
|
|
401496
|
+
runEndedHook;
|
|
401497
|
+
onRunEnded(hook) {
|
|
401498
|
+
this.runEndedHook = hook;
|
|
401499
|
+
}
|
|
401246
401500
|
async start() {
|
|
401247
401501
|
const isLoopback = this.host === "127.0.0.1" || this.host === "::1" || this.host === "localhost";
|
|
401248
401502
|
if (!isLoopback) {
|
|
@@ -401433,6 +401687,8 @@ var DashboardServer = class {
|
|
|
401433
401687
|
}
|
|
401434
401688
|
/** Record run end: the assistant reply (when there is one) and the runtime row's final status. */
|
|
401435
401689
|
persistRunEnd(sessionId, title, latestPrompt, reply, status, result) {
|
|
401690
|
+
const endedTaskId = result?.taskId ?? this.sessionTaskIds.get(sessionId)?.at(-1);
|
|
401691
|
+
this.runEndedHook?.({ sessionId, ...endedTaskId ? { taskId: endedTaskId } : {} });
|
|
401436
401692
|
try {
|
|
401437
401693
|
if (reply && reply.trim()) {
|
|
401438
401694
|
this.store.addMessage({ id: crypto2.randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -401526,6 +401782,7 @@ var DashboardServer = class {
|
|
|
401526
401782
|
const prompt = task.prompt;
|
|
401527
401783
|
const title = this.persistRunStart(sessionId, `[${task.name}] ${prompt}`);
|
|
401528
401784
|
const cascade = new Cascade(this.config, task.workspacePath ?? this.workspacePath, this.store);
|
|
401785
|
+
cascade.setUnattended(true);
|
|
401529
401786
|
this.activeSessions.set(sessionId, cascade);
|
|
401530
401787
|
cascade.on("tier:status", (e3) => {
|
|
401531
401788
|
this.socket.broadcast("tier:status", { sessionId, ...e3 });
|
|
@@ -401533,13 +401790,16 @@ var DashboardServer = class {
|
|
|
401533
401790
|
cascade.on("peer:message", (e3) => {
|
|
401534
401791
|
this.socket.emitPeerMessage(e3);
|
|
401535
401792
|
});
|
|
401793
|
+
cascade.on("run:started", (e3) => {
|
|
401794
|
+
const id = e3.taskId;
|
|
401795
|
+
if (id) this.recordSessionTask(sessionId, id);
|
|
401796
|
+
});
|
|
401536
401797
|
try {
|
|
401537
401798
|
const result = await cascade.run({
|
|
401538
401799
|
prompt,
|
|
401539
401800
|
identityId: task.identityId,
|
|
401540
401801
|
approvalCallback: async () => ({ approved: true, always: false })
|
|
401541
401802
|
});
|
|
401542
|
-
this.recordSessionTask(sessionId, result.taskId);
|
|
401543
401803
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
401544
401804
|
this.captureWhy(sessionId, cascade, result);
|
|
401545
401805
|
this.socket.broadcast("session:complete", { sessionId, result });
|
|
@@ -402172,6 +402432,7 @@ ${prompt}`;
|
|
|
402172
402432
|
const cascade = new Cascade(this.config, this.workspacePath, this.store);
|
|
402173
402433
|
this.activeSessions.set(sessionId, cascade);
|
|
402174
402434
|
if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
|
|
402435
|
+
if (this.browserController) cascade.setBrowserController(this.browserController, this.browserActorRelease);
|
|
402175
402436
|
cascade.on("stream:token", (e3) => {
|
|
402176
402437
|
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e3.tierId, text: e3.text, primary: e3.primary });
|
|
402177
402438
|
});
|
|
@@ -402195,13 +402456,16 @@ ${prompt}`;
|
|
|
402195
402456
|
if (ev.requestId) this.clearPendingEscalation(ev.requestId);
|
|
402196
402457
|
this.socket.broadcastToRoom(`session:${sessionId}`, "escalation:timeout", { sessionId, ...e3 });
|
|
402197
402458
|
});
|
|
402459
|
+
cascade.on("run:started", (e3) => {
|
|
402460
|
+
const id = e3.taskId;
|
|
402461
|
+
if (id) this.recordSessionTask(sessionId, id);
|
|
402462
|
+
});
|
|
402198
402463
|
try {
|
|
402199
402464
|
const result = await cascade.run({
|
|
402200
402465
|
prompt: runPrompt,
|
|
402201
402466
|
identityId: body.identityId,
|
|
402202
402467
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
402203
402468
|
});
|
|
402204
|
-
this.recordSessionTask(sessionId, result.taskId);
|
|
402205
402469
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
402206
402470
|
this.captureWhy(sessionId, cascade, result);
|
|
402207
402471
|
this.socket.broadcast("cost:update", {
|
|
@@ -403673,6 +403937,7 @@ object-assign/index.js:
|
|
|
403673
403937
|
exports.AZURE_BASE_MODELS = AZURE_BASE_MODELS;
|
|
403674
403938
|
exports.AZURE_BASE_URL_TEMPLATE = AZURE_BASE_URL_TEMPLATE;
|
|
403675
403939
|
exports.AuditLogger = AuditLogger;
|
|
403940
|
+
exports.BrowserControlTool = BrowserControlTool;
|
|
403676
403941
|
exports.CASCADE_AUDIT_FILE = CASCADE_AUDIT_FILE;
|
|
403677
403942
|
exports.CASCADE_CONFIG_DIR = CASCADE_CONFIG_DIR;
|
|
403678
403943
|
exports.CASCADE_CONFIG_FILE = CASCADE_CONFIG_FILE;
|