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/index.js
CHANGED
|
@@ -11623,7 +11623,7 @@ function reconcilePrice(model, live) {
|
|
|
11623
11623
|
}
|
|
11624
11624
|
|
|
11625
11625
|
// src/constants.ts
|
|
11626
|
-
var CASCADE_VERSION = "0.
|
|
11626
|
+
var CASCADE_VERSION = "0.79.0";
|
|
11627
11627
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
11628
11628
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
11629
11629
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -16084,6 +16084,45 @@ var ToolsConfigSchema = z.object({
|
|
|
16084
16084
|
shellBlocklist: z.array(z.string()).default(["rm -rf", "sudo rm", "format", "mkfs"]),
|
|
16085
16085
|
requireApprovalFor: z.array(z.string()).default([]),
|
|
16086
16086
|
browserEnabled: z.boolean().default(false),
|
|
16087
|
+
/**
|
|
16088
|
+
* Let a run ACT on the page open in the desktop's built-in browser — click,
|
|
16089
|
+
* type, submit — not just read it.
|
|
16090
|
+
*
|
|
16091
|
+
* Distinct from `browserEnabled`, which turns on the headless Playwright
|
|
16092
|
+
* tool. That one drives a throwaway browser with an empty cookie jar; this
|
|
16093
|
+
* one drives the session the user is signed into, which is a different order
|
|
16094
|
+
* of consequence and so gets its own switch rather than riding on that one.
|
|
16095
|
+
*
|
|
16096
|
+
* Off by default. Turning it on does not hand anything over silently: the
|
|
16097
|
+
* tool still goes through the approval chain, and the browser panel carries a
|
|
16098
|
+
* stop control for the run that is acting.
|
|
16099
|
+
*/
|
|
16100
|
+
agentBrowserControl: z.boolean().default(false),
|
|
16101
|
+
/**
|
|
16102
|
+
* Where a HOSTED run gets a browser, since it has none of its own.
|
|
16103
|
+
*
|
|
16104
|
+
* The desktop drives the Chromium the user is signed into and can watch. A
|
|
16105
|
+
* server has neither, and running one inside the container is wrong twice
|
|
16106
|
+
* over — page JavaScript issues requests no URL check ever sees, from inside
|
|
16107
|
+
* the trust boundary, and a session costs 200-400 MB. So this points at a
|
|
16108
|
+
* browser somebody else runs.
|
|
16109
|
+
*
|
|
16110
|
+
* Absent by default: no provider, no capability. There is nothing to turn
|
|
16111
|
+
* off because nothing is registered until an endpoint is supplied.
|
|
16112
|
+
*/
|
|
16113
|
+
remoteBrowser: z.object({
|
|
16114
|
+
/** `cdp` for an endpoint you run; `steel` for a Steel deployment. */
|
|
16115
|
+
provider: z.enum(["cdp", "steel"]).optional(),
|
|
16116
|
+
/** The websocket endpoint for `cdp`, or the API base for `steel`. */
|
|
16117
|
+
url: z.string().optional(),
|
|
16118
|
+
apiKey: z.string().optional(),
|
|
16119
|
+
/**
|
|
16120
|
+
* Concurrent sessions. One by default and deliberately so: every session
|
|
16121
|
+
* is billed, and a wave of workers each opening their own is a cost the
|
|
16122
|
+
* user did not choose. Raising it is a decision, not a default.
|
|
16123
|
+
*/
|
|
16124
|
+
maxSessions: z.number().int().min(1).max(16).default(1)
|
|
16125
|
+
}).optional(),
|
|
16087
16126
|
mcpServers: z.array(McpServerConfigSchema).optional(),
|
|
16088
16127
|
mcpTrusted: z.array(z.string()).optional(),
|
|
16089
16128
|
/** Web search backends — at least one should be configured for best results */
|
|
@@ -20868,6 +20907,23 @@ Now execute your subtask using this context where relevant.`
|
|
|
20868
20907
|
this.setStatus("FAILED", finalOutput);
|
|
20869
20908
|
this.peerBus?.publish(this.id, assignment.subtaskId, finalOutput, "FAILED");
|
|
20870
20909
|
return this.buildResult("ESCALATED", finalOutput, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
20910
|
+
} finally {
|
|
20911
|
+
this.releaseBrowserControl();
|
|
20912
|
+
}
|
|
20913
|
+
}
|
|
20914
|
+
/**
|
|
20915
|
+
* End this worker's hold on the shared browser.
|
|
20916
|
+
*
|
|
20917
|
+
* Duck-typed rather than imported: `browser_control` is registered only by a
|
|
20918
|
+
* host that HAS a browser (the desktop), so in the CLI and hosted runs there
|
|
20919
|
+
* is no such tool and this is a no-op. Importing the class to instanceof-check
|
|
20920
|
+
* it would couple the worker to a tool that usually is not there.
|
|
20921
|
+
*/
|
|
20922
|
+
releaseBrowserControl() {
|
|
20923
|
+
try {
|
|
20924
|
+
const tool = this.toolRegistry.getTool?.("browser_control");
|
|
20925
|
+
tool?.releaseActor?.(this.id);
|
|
20926
|
+
} catch {
|
|
20871
20927
|
}
|
|
20872
20928
|
}
|
|
20873
20929
|
sendToPeer(toId, content) {
|
|
@@ -30684,6 +30740,127 @@ var CurrentPageTool = class extends BaseTool {
|
|
|
30684
30740
|
}
|
|
30685
30741
|
};
|
|
30686
30742
|
|
|
30743
|
+
// src/tools/browser-control.ts
|
|
30744
|
+
var BrowserControlTool = class extends BaseTool {
|
|
30745
|
+
name = "browser_control";
|
|
30746
|
+
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.";
|
|
30747
|
+
inputSchema = {
|
|
30748
|
+
type: "object",
|
|
30749
|
+
properties: {
|
|
30750
|
+
action: {
|
|
30751
|
+
type: "string",
|
|
30752
|
+
enum: ["navigate", "click", "fill", "press", "wait_for", "extract_text"],
|
|
30753
|
+
description: "What to do."
|
|
30754
|
+
},
|
|
30755
|
+
url: { type: "string", description: "For navigate: the http(s) address to open." },
|
|
30756
|
+
selector: {
|
|
30757
|
+
type: "string",
|
|
30758
|
+
description: "CSS selector for click, fill and wait_for. Optional for extract_text (defaults to the whole page)."
|
|
30759
|
+
},
|
|
30760
|
+
value: { type: "string", description: "For fill: the text to type." },
|
|
30761
|
+
key: { type: "string", description: 'For press: a key name such as "Enter", "Tab" or "Escape".' },
|
|
30762
|
+
timeoutMs: { type: "number", description: "For wait_for: how long to wait before giving up (default 10000, max 30000)." }
|
|
30763
|
+
},
|
|
30764
|
+
required: ["action"]
|
|
30765
|
+
};
|
|
30766
|
+
controller;
|
|
30767
|
+
release;
|
|
30768
|
+
/** One-way. See `revoke`. */
|
|
30769
|
+
revoked = false;
|
|
30770
|
+
constructor(controller, release) {
|
|
30771
|
+
super();
|
|
30772
|
+
this.controller = controller;
|
|
30773
|
+
this.release = release;
|
|
30774
|
+
}
|
|
30775
|
+
/**
|
|
30776
|
+
* A worker has finished. Ends its hold on the browser.
|
|
30777
|
+
*
|
|
30778
|
+
* Called from the tier's terminal path rather than by the model, because a
|
|
30779
|
+
* model cannot be relied on to announce that it is done — which is why the
|
|
30780
|
+
* host cannot use idle time as the boundary instead.
|
|
30781
|
+
*/
|
|
30782
|
+
releaseActor(actorId) {
|
|
30783
|
+
this.release?.(actorId);
|
|
30784
|
+
}
|
|
30785
|
+
/**
|
|
30786
|
+
* Permanently disable this tool, whatever the registry still holds.
|
|
30787
|
+
*
|
|
30788
|
+
* Registration-time gates cannot express "this run turned out to be
|
|
30789
|
+
* unattended": a host that wires the controller and only then declares the
|
|
30790
|
+
* run unattended has already registered the tool, and a check that ran at
|
|
30791
|
+
* registration has no second chance. Enforcing at EXECUTION time makes the
|
|
30792
|
+
* guarantee independent of call order, which is the whole point of stating
|
|
30793
|
+
* the invariant rather than relying on the scheduler's current sequence.
|
|
30794
|
+
*
|
|
30795
|
+
* One-way on purpose. A safety gate that can be re-opened by a later call is
|
|
30796
|
+
* a gate that a refactor re-opens by accident.
|
|
30797
|
+
*/
|
|
30798
|
+
revoke() {
|
|
30799
|
+
this.revoked = true;
|
|
30800
|
+
}
|
|
30801
|
+
// Acting on a session the user is signed into is the highest-consequence
|
|
30802
|
+
// thing in the tool set — higher than shell, arguably, because the blast
|
|
30803
|
+
// radius is someone else's server rather than this machine.
|
|
30804
|
+
isDangerous() {
|
|
30805
|
+
return true;
|
|
30806
|
+
}
|
|
30807
|
+
async execute(input, options) {
|
|
30808
|
+
if (this.revoked) {
|
|
30809
|
+
return "Error: browser control is not available in this run. It requires a person watching who can stop it, and nobody is.";
|
|
30810
|
+
}
|
|
30811
|
+
const kind = input["action"];
|
|
30812
|
+
if (!kind) return "Error: action is required.";
|
|
30813
|
+
if (options?.signal?.aborted) return "Error: the run was cancelled before this action ran.";
|
|
30814
|
+
const missing = requiredFieldFor(kind, input);
|
|
30815
|
+
if (missing) return `Error: "${kind}" needs ${missing}.`;
|
|
30816
|
+
const action = {
|
|
30817
|
+
kind,
|
|
30818
|
+
...typeof input["url"] === "string" ? { url: input["url"] } : {},
|
|
30819
|
+
...typeof input["selector"] === "string" ? { selector: input["selector"] } : {},
|
|
30820
|
+
...typeof input["value"] === "string" ? { value: input["value"] } : {},
|
|
30821
|
+
...typeof input["key"] === "string" ? { key: input["key"] } : {},
|
|
30822
|
+
...typeof input["timeoutMs"] === "number" ? { timeoutMs: Math.min(Math.max(input["timeoutMs"], 0), 3e4) } : {}
|
|
30823
|
+
};
|
|
30824
|
+
let outcome;
|
|
30825
|
+
try {
|
|
30826
|
+
outcome = await this.controller(action, {
|
|
30827
|
+
// `sessionId` is the run. The host scopes both revocation and its
|
|
30828
|
+
// single-owner lease on it, so passing it is not telemetry — drop it
|
|
30829
|
+
// and a Stop in one run silently stops every later one.
|
|
30830
|
+
sessionId: options?.sessionId ?? "",
|
|
30831
|
+
actorId: options?.tierId ?? options?.sessionId ?? "",
|
|
30832
|
+
...options?.signal ? { signal: options.signal } : {}
|
|
30833
|
+
});
|
|
30834
|
+
} catch (err) {
|
|
30835
|
+
return `Error: the browser could not perform "${kind}" \u2014 ${err instanceof Error ? err.message : String(err)}`;
|
|
30836
|
+
}
|
|
30837
|
+
const where = outcome.url ? `
|
|
30838
|
+
Page: ${outcome.title ? `${outcome.title} \u2014 ` : ""}${outcome.url}` : "";
|
|
30839
|
+
return `${outcome.ok ? "" : "Failed: "}${outcome.detail}${where}`;
|
|
30840
|
+
}
|
|
30841
|
+
};
|
|
30842
|
+
function requiredFieldFor(kind, input) {
|
|
30843
|
+
const has = (k) => typeof input[k] === "string" && input[k].length > 0;
|
|
30844
|
+
const given = (k) => typeof input[k] === "string";
|
|
30845
|
+
switch (kind) {
|
|
30846
|
+
case "navigate":
|
|
30847
|
+
return has("url") ? null : "a url";
|
|
30848
|
+
case "click":
|
|
30849
|
+
return has("selector") ? null : "a selector";
|
|
30850
|
+
case "fill":
|
|
30851
|
+
return has("selector") ? given("value") ? null : "a value" : "a selector";
|
|
30852
|
+
case "press":
|
|
30853
|
+
return has("key") ? null : "a key";
|
|
30854
|
+
case "wait_for":
|
|
30855
|
+
return has("selector") ? null : "a selector";
|
|
30856
|
+
// extract_text works with or without a selector.
|
|
30857
|
+
case "extract_text":
|
|
30858
|
+
return null;
|
|
30859
|
+
default:
|
|
30860
|
+
return null;
|
|
30861
|
+
}
|
|
30862
|
+
}
|
|
30863
|
+
|
|
30687
30864
|
// src/core/cascade.ts
|
|
30688
30865
|
var ESCALATION_DECISION_TIMEOUT_MS = 5 * 6e4;
|
|
30689
30866
|
function buildContextualPrompt(prompt, history = []) {
|
|
@@ -31176,6 +31353,79 @@ ${input}` }],
|
|
|
31176
31353
|
if ((this.config.tools?.disabledTools ?? []).includes("read_current_page")) return;
|
|
31177
31354
|
this.toolRegistry.register(new CurrentPageTool(provider));
|
|
31178
31355
|
}
|
|
31356
|
+
/**
|
|
31357
|
+
* True when nobody is watching this run — a scheduled/cron task, which
|
|
31358
|
+
* auto-approves every tool because there is no one to ask.
|
|
31359
|
+
*
|
|
31360
|
+
* That auto-approval is fine for ordinary tools and disqualifying for this
|
|
31361
|
+
* one: `browser_control` drives the signed-in browser, and "every run asks
|
|
31362
|
+
* first" is the promise the Settings copy makes. The property used to hold
|
|
31363
|
+
* only because `runScheduledTask` happens not to call `setBrowserController`
|
|
31364
|
+
* — a guarantee resting on an ABSENT call, which is exactly what a later
|
|
31365
|
+
* refactor adds without noticing. Stated and enforced instead.
|
|
31366
|
+
*/
|
|
31367
|
+
unattended = false;
|
|
31368
|
+
/**
|
|
31369
|
+
* Declare that no human is watching this run, so capabilities that depend on
|
|
31370
|
+
* a person being able to see and stop them are refused outright.
|
|
31371
|
+
*/
|
|
31372
|
+
setUnattended(on) {
|
|
31373
|
+
this.unattended = on;
|
|
31374
|
+
if (!on) return;
|
|
31375
|
+
const tool = this.toolRegistry.getTool("browser_control");
|
|
31376
|
+
tool?.revoke?.();
|
|
31377
|
+
}
|
|
31378
|
+
/**
|
|
31379
|
+
* Wire the host's browser so a run can ACT on the open page, not just read it.
|
|
31380
|
+
*
|
|
31381
|
+
* Two gates before this registers anything, and both matter:
|
|
31382
|
+
*
|
|
31383
|
+
* - `tools.agentBrowserControl` must be on. Unlike read_current_page, which
|
|
31384
|
+
* reaches nothing the user has not already opened themselves, this drives
|
|
31385
|
+
* the session they are signed into. That is opt-in, not a default.
|
|
31386
|
+
* - `disabledTools` still removes it, the same as every other tool.
|
|
31387
|
+
* - The run must not be UNATTENDED. See `setUnattended`.
|
|
31388
|
+
*
|
|
31389
|
+
* Host-supplied like the page provider: the tool exists only where there is a
|
|
31390
|
+
* real browser to drive, so it never has to be refused in the CLI or a hosted
|
|
31391
|
+
* run — it is simply not there.
|
|
31392
|
+
*/
|
|
31393
|
+
setBrowserController(controller, release) {
|
|
31394
|
+
this.registerBrowserControl(controller, release, this.config.tools?.agentBrowserControl === true);
|
|
31395
|
+
}
|
|
31396
|
+
/**
|
|
31397
|
+
* Wire a REMOTE browser — one the host connects to rather than one the user
|
|
31398
|
+
* is signed into.
|
|
31399
|
+
*
|
|
31400
|
+
* A separate entry point because the enablement is a different question, and
|
|
31401
|
+
* conflating them made the hosted capability dead on arrival: this went out
|
|
31402
|
+
* calling `setBrowserController`, which gates on `agentBrowserControl`, and
|
|
31403
|
+
* that flag means "drive the browser I am signed into on this machine". It
|
|
31404
|
+
* defaults to false and has no meaning on a server, so the hosted tool was
|
|
31405
|
+
* never registered at all — the feature looked shipped and did nothing.
|
|
31406
|
+
*
|
|
31407
|
+
* Configuring a provider IS the opt-in here. It takes a deliberate act with
|
|
31408
|
+
* an endpoint and usually a paid account, and the browser it reaches is a
|
|
31409
|
+
* throwaway signed into nothing — a smaller consent than handing over the
|
|
31410
|
+
* session the user is already authenticated in, and one the desktop flag
|
|
31411
|
+
* cannot stand in for.
|
|
31412
|
+
*/
|
|
31413
|
+
setRemoteBrowserController(controller, release) {
|
|
31414
|
+
this.registerBrowserControl(controller, release, Boolean(this.config.tools?.remoteBrowser?.provider));
|
|
31415
|
+
}
|
|
31416
|
+
/**
|
|
31417
|
+
* The gates every surface shares, with only enablement differing.
|
|
31418
|
+
*
|
|
31419
|
+
* One place, because the two callers above are exactly the sibling-writer
|
|
31420
|
+
* shape that has already drifted twice in this feature: a rule added to one
|
|
31421
|
+
* and not the other is silent on the surface that missed it.
|
|
31422
|
+
*/
|
|
31423
|
+
registerBrowserControl(controller, release, enabled) {
|
|
31424
|
+
if (this.unattended) return;
|
|
31425
|
+
if (!enabled) return;
|
|
31426
|
+
if ((this.config.tools?.disabledTools ?? []).includes("browser_control")) return;
|
|
31427
|
+
this.toolRegistry.register(new BrowserControlTool(controller, release));
|
|
31428
|
+
}
|
|
31179
31429
|
registerMediaTools(workspacePath) {
|
|
31180
31430
|
const denied = new Set(this.config.tools?.disabledTools ?? []);
|
|
31181
31431
|
const permitted = (name) => !denied.has(name);
|
|
@@ -31781,6 +32031,7 @@ ${prompt}` : prompt;
|
|
|
31781
32031
|
this.router.setRunSignal(options.signal);
|
|
31782
32032
|
const startMs = Date.now();
|
|
31783
32033
|
const taskId = randomUUID();
|
|
32034
|
+
this.emit("run:started", { taskId });
|
|
31784
32035
|
this.decisionLog = [];
|
|
31785
32036
|
this.currentRunCompleted = [];
|
|
31786
32037
|
this.wroteCheckpointThisRun = false;
|
|
@@ -32516,6 +32767,20 @@ function applySettingsPayload(config, data) {
|
|
|
32516
32767
|
if (data.webSearch.tavilyApiKey) next.tavilyApiKey = data.webSearch.tavilyApiKey;
|
|
32517
32768
|
config.tools.webSearch = next;
|
|
32518
32769
|
}
|
|
32770
|
+
if (data.remoteBrowser && typeof data.remoteBrowser === "object") {
|
|
32771
|
+
const prior = config.tools.remoteBrowser ?? {};
|
|
32772
|
+
const next = { ...prior };
|
|
32773
|
+
if (data.remoteBrowser.provider === "cdp" || data.remoteBrowser.provider === "steel") {
|
|
32774
|
+
next.provider = data.remoteBrowser.provider;
|
|
32775
|
+
}
|
|
32776
|
+
if (typeof data.remoteBrowser.url === "string") next.url = data.remoteBrowser.url.trim() || void 0;
|
|
32777
|
+
if (data.remoteBrowser.apiKey) next.apiKey = data.remoteBrowser.apiKey;
|
|
32778
|
+
const n = data.remoteBrowser.maxSessions;
|
|
32779
|
+
if (typeof n === "number" && Number.isFinite(n)) {
|
|
32780
|
+
next.maxSessions = Math.min(Math.max(Math.floor(n), 1), 16);
|
|
32781
|
+
}
|
|
32782
|
+
config.tools.remoteBrowser = next;
|
|
32783
|
+
}
|
|
32519
32784
|
if (data.advanced && typeof data.advanced === "object") {
|
|
32520
32785
|
const a = data.advanced;
|
|
32521
32786
|
const num = (v, min, max) => {
|
|
@@ -32546,6 +32811,7 @@ function applySettingsPayload(config, data) {
|
|
|
32546
32811
|
if (["auto", "T1", "T2", "T3"].includes(a["forceTier"])) config.routing = { ...config.routing ?? {}, forceTier: a["forceTier"] };
|
|
32547
32812
|
if (typeof a["benchmarksLive"] === "boolean") config.benchmarks = { ...config.benchmarks ?? {}, live: a["benchmarksLive"] };
|
|
32548
32813
|
if (["isolate", "worker", "auto"].includes(a["dynamicToolSandbox"])) config.tools = { ...config.tools ?? {}, dynamicToolSandbox: a["dynamicToolSandbox"] };
|
|
32814
|
+
if (typeof a["agentBrowserControl"] === "boolean") config.tools = { ...config.tools ?? {}, agentBrowserControl: a["agentBrowserControl"] };
|
|
32549
32815
|
if (typeof a["factsExtraction"] === "boolean") config.knowledge = { ...config.knowledge ?? {}, factsExtraction: a["factsExtraction"] };
|
|
32550
32816
|
if (typeof a["rememberSessions"] === "boolean") config.memory = { ...config.memory ?? {}, rememberSessions: a["rememberSessions"] };
|
|
32551
32817
|
if (typeof a["enableToolCreation"] === "boolean") config.enableToolCreation = a["enableToolCreation"];
|
|
@@ -32561,6 +32827,7 @@ function settingsSnapshot(config) {
|
|
|
32561
32827
|
if (p?.type && p?.baseUrl && p.type !== "azure") endpoints[p.type] = p.baseUrl;
|
|
32562
32828
|
}
|
|
32563
32829
|
const ws = config.tools?.webSearch ?? {};
|
|
32830
|
+
const rb = config.tools?.remoteBrowser ?? {};
|
|
32564
32831
|
return {
|
|
32565
32832
|
models: config.models ?? {},
|
|
32566
32833
|
budget: {
|
|
@@ -32590,6 +32857,15 @@ function settingsSnapshot(config) {
|
|
|
32590
32857
|
hasBraveKey: typeof ws.braveApiKey === "string" && ws.braveApiKey.length > 0,
|
|
32591
32858
|
hasTavilyKey: typeof ws.tavilyApiKey === "string" && ws.tavilyApiKey.length > 0
|
|
32592
32859
|
},
|
|
32860
|
+
// `hasApiKey`, never the key. The panel needs to show whether one is set;
|
|
32861
|
+
// sending the value back would put a live credential in every settings
|
|
32862
|
+
// response, where it reaches any client that can read them.
|
|
32863
|
+
remoteBrowser: {
|
|
32864
|
+
provider: rb.provider,
|
|
32865
|
+
url: rb.url,
|
|
32866
|
+
hasApiKey: typeof rb.apiKey === "string" && rb.apiKey.length > 0,
|
|
32867
|
+
maxSessions: rb.maxSessions
|
|
32868
|
+
},
|
|
32593
32869
|
advanced: {
|
|
32594
32870
|
autonomy: config.autonomy,
|
|
32595
32871
|
planApproval: config.planApproval,
|
|
@@ -32603,6 +32879,7 @@ function settingsSnapshot(config) {
|
|
|
32603
32879
|
forceTier: config.routing?.forceTier,
|
|
32604
32880
|
benchmarksLive: config.benchmarks?.live,
|
|
32605
32881
|
dynamicToolSandbox: config.tools?.dynamicToolSandbox,
|
|
32882
|
+
agentBrowserControl: config.tools?.agentBrowserControl,
|
|
32606
32883
|
factsExtraction: config.knowledge?.factsExtraction,
|
|
32607
32884
|
rememberSessions: config.memory?.rememberSessions,
|
|
32608
32885
|
enableToolCreation: config.enableToolCreation,
|
|
@@ -33107,6 +33384,14 @@ var DashboardServer = class {
|
|
|
33107
33384
|
* always failing.
|
|
33108
33385
|
*/
|
|
33109
33386
|
currentPageProvider;
|
|
33387
|
+
/**
|
|
33388
|
+
* Supplied by the desktop shell so a run can ACT on that page — click, type,
|
|
33389
|
+
* submit — not just read it. Same reasoning as the provider above: absent in
|
|
33390
|
+
* the CLI, so the tool is not registered rather than present and failing.
|
|
33391
|
+
* Cascade applies the `tools.agentBrowserControl` gate on top of this.
|
|
33392
|
+
*/
|
|
33393
|
+
browserController;
|
|
33394
|
+
browserActorRelease;
|
|
33110
33395
|
activeControllers = /* @__PURE__ */ new Map();
|
|
33111
33396
|
/**
|
|
33112
33397
|
* Run taskIds per chat session — file snapshots are keyed by the run's
|
|
@@ -33168,6 +33453,7 @@ var DashboardServer = class {
|
|
|
33168
33453
|
});
|
|
33169
33454
|
this.socket.onConfigUpdate(async (data) => {
|
|
33170
33455
|
const result = await commitSettings(this.config, data, (committed) => this.persistConfig(committed));
|
|
33456
|
+
if (result.ok) this.settingsChangeHook?.(this.config);
|
|
33171
33457
|
const refused = result.refused.map((r) => ({
|
|
33172
33458
|
type: r.type,
|
|
33173
33459
|
reason: r.reason,
|
|
@@ -33191,6 +33477,7 @@ var DashboardServer = class {
|
|
|
33191
33477
|
const cascade = new Cascade(cfg, this.workspacePath, this.store);
|
|
33192
33478
|
this.activeSessions.set(sessionId, cascade);
|
|
33193
33479
|
if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
|
|
33480
|
+
if (this.browserController) cascade.setBrowserController(this.browserController, this.browserActorRelease);
|
|
33194
33481
|
cascade.on("stream:token", (e) => {
|
|
33195
33482
|
this.emitToSessionClients(socketId, sessionId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
33196
33483
|
});
|
|
@@ -33214,13 +33501,16 @@ var DashboardServer = class {
|
|
|
33214
33501
|
if (ev.requestId) this.clearPendingEscalation(ev.requestId);
|
|
33215
33502
|
this.emitToSessionClients(socketId, sessionId, "escalation:timeout", { sessionId, ...e });
|
|
33216
33503
|
});
|
|
33504
|
+
cascade.on("run:started", (e) => {
|
|
33505
|
+
const id = e.taskId;
|
|
33506
|
+
if (id) this.recordSessionTask(sessionId, id);
|
|
33507
|
+
});
|
|
33217
33508
|
try {
|
|
33218
33509
|
const result = await cascade.run({
|
|
33219
33510
|
prompt: runPrompt,
|
|
33220
33511
|
signal: abortController.signal,
|
|
33221
33512
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
33222
33513
|
});
|
|
33223
|
-
this.recordSessionTask(sessionId, result.taskId);
|
|
33224
33514
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
33225
33515
|
this.captureWhy(sessionId, cascade, result);
|
|
33226
33516
|
this.emitToSessionClients(socketId, sessionId, "session:complete", { sessionId, result });
|
|
@@ -33286,6 +33576,49 @@ var DashboardServer = class {
|
|
|
33286
33576
|
setCurrentPageProvider(provider) {
|
|
33287
33577
|
this.currentPageProvider = provider;
|
|
33288
33578
|
}
|
|
33579
|
+
/** Let runs act on that view, not just read it (desktop only). */
|
|
33580
|
+
setBrowserController(controller, release) {
|
|
33581
|
+
this.browserController = controller;
|
|
33582
|
+
this.browserActorRelease = release;
|
|
33583
|
+
}
|
|
33584
|
+
/**
|
|
33585
|
+
* Told whenever settings are committed through THIS server.
|
|
33586
|
+
*
|
|
33587
|
+
* Settings reach the live config by two routes — the desktop's Electron IPC
|
|
33588
|
+
* handler and this server's `config:update` socket — and both run the same
|
|
33589
|
+
* `commitSettings`. The desktop mirrors `tools.agentBrowserControl` into its
|
|
33590
|
+
* browser module after its own route, but had no way to learn about the
|
|
33591
|
+
* other, so a socket write left the module's copy stale: an enable the module
|
|
33592
|
+
* still refused, or worse a disable the module still honoured for a run
|
|
33593
|
+
* already holding the tool. This is the same sibling-writer drift
|
|
33594
|
+
* `settings-payload.ts` exists to prevent, one layer up.
|
|
33595
|
+
*/
|
|
33596
|
+
settingsChangeHook;
|
|
33597
|
+
onSettingsChanged(hook) {
|
|
33598
|
+
this.settingsChangeHook = hook;
|
|
33599
|
+
}
|
|
33600
|
+
/**
|
|
33601
|
+
* Told when a run ends, so a host holding run-scoped state can release it.
|
|
33602
|
+
*
|
|
33603
|
+
* The desktop uses this to retire a run's browser Stop control: that has to
|
|
33604
|
+
* outlive each individual action (the gap between two actions is when the
|
|
33605
|
+
* user most wants it) but must not outlive the run.
|
|
33606
|
+
*/
|
|
33607
|
+
/**
|
|
33608
|
+
* Fired when a run ends, carrying BOTH identifiers explicitly.
|
|
33609
|
+
*
|
|
33610
|
+
* They are different things and conflating them was a live bug: a chat
|
|
33611
|
+
* `sessionId` can contain many runs (hence `sessionTaskIds`), while
|
|
33612
|
+
* `Cascade.run()` mints a fresh random `taskId` per run — and it is the
|
|
33613
|
+
* taskId that tools see as `ToolExecuteOptions.sessionId`. Passing only the
|
|
33614
|
+
* chat id meant browser cleanup keyed on the task id matched nothing, so a
|
|
33615
|
+
* finished run kept its Stop banner armed and held its browser lease until
|
|
33616
|
+
* the deadlock ceiling.
|
|
33617
|
+
*/
|
|
33618
|
+
runEndedHook;
|
|
33619
|
+
onRunEnded(hook) {
|
|
33620
|
+
this.runEndedHook = hook;
|
|
33621
|
+
}
|
|
33289
33622
|
async start() {
|
|
33290
33623
|
const isLoopback = this.host === "127.0.0.1" || this.host === "::1" || this.host === "localhost";
|
|
33291
33624
|
if (!isLoopback) {
|
|
@@ -33476,6 +33809,8 @@ var DashboardServer = class {
|
|
|
33476
33809
|
}
|
|
33477
33810
|
/** Record run end: the assistant reply (when there is one) and the runtime row's final status. */
|
|
33478
33811
|
persistRunEnd(sessionId, title, latestPrompt, reply, status, result) {
|
|
33812
|
+
const endedTaskId = result?.taskId ?? this.sessionTaskIds.get(sessionId)?.at(-1);
|
|
33813
|
+
this.runEndedHook?.({ sessionId, ...endedTaskId ? { taskId: endedTaskId } : {} });
|
|
33479
33814
|
try {
|
|
33480
33815
|
if (reply && reply.trim()) {
|
|
33481
33816
|
this.store.addMessage({ id: randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -33569,6 +33904,7 @@ var DashboardServer = class {
|
|
|
33569
33904
|
const prompt = task.prompt;
|
|
33570
33905
|
const title = this.persistRunStart(sessionId, `[${task.name}] ${prompt}`);
|
|
33571
33906
|
const cascade = new Cascade(this.config, task.workspacePath ?? this.workspacePath, this.store);
|
|
33907
|
+
cascade.setUnattended(true);
|
|
33572
33908
|
this.activeSessions.set(sessionId, cascade);
|
|
33573
33909
|
cascade.on("tier:status", (e) => {
|
|
33574
33910
|
this.socket.broadcast("tier:status", { sessionId, ...e });
|
|
@@ -33576,13 +33912,16 @@ var DashboardServer = class {
|
|
|
33576
33912
|
cascade.on("peer:message", (e) => {
|
|
33577
33913
|
this.socket.emitPeerMessage(e);
|
|
33578
33914
|
});
|
|
33915
|
+
cascade.on("run:started", (e) => {
|
|
33916
|
+
const id = e.taskId;
|
|
33917
|
+
if (id) this.recordSessionTask(sessionId, id);
|
|
33918
|
+
});
|
|
33579
33919
|
try {
|
|
33580
33920
|
const result = await cascade.run({
|
|
33581
33921
|
prompt,
|
|
33582
33922
|
identityId: task.identityId,
|
|
33583
33923
|
approvalCallback: async () => ({ approved: true, always: false })
|
|
33584
33924
|
});
|
|
33585
|
-
this.recordSessionTask(sessionId, result.taskId);
|
|
33586
33925
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
33587
33926
|
this.captureWhy(sessionId, cascade, result);
|
|
33588
33927
|
this.socket.broadcast("session:complete", { sessionId, result });
|
|
@@ -34215,6 +34554,7 @@ ${prompt}`;
|
|
|
34215
34554
|
const cascade = new Cascade(this.config, this.workspacePath, this.store);
|
|
34216
34555
|
this.activeSessions.set(sessionId, cascade);
|
|
34217
34556
|
if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
|
|
34557
|
+
if (this.browserController) cascade.setBrowserController(this.browserController, this.browserActorRelease);
|
|
34218
34558
|
cascade.on("stream:token", (e) => {
|
|
34219
34559
|
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
34220
34560
|
});
|
|
@@ -34238,13 +34578,16 @@ ${prompt}`;
|
|
|
34238
34578
|
if (ev.requestId) this.clearPendingEscalation(ev.requestId);
|
|
34239
34579
|
this.socket.broadcastToRoom(`session:${sessionId}`, "escalation:timeout", { sessionId, ...e });
|
|
34240
34580
|
});
|
|
34581
|
+
cascade.on("run:started", (e) => {
|
|
34582
|
+
const id = e.taskId;
|
|
34583
|
+
if (id) this.recordSessionTask(sessionId, id);
|
|
34584
|
+
});
|
|
34241
34585
|
try {
|
|
34242
34586
|
const result = await cascade.run({
|
|
34243
34587
|
prompt: runPrompt,
|
|
34244
34588
|
identityId: body.identityId,
|
|
34245
34589
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
34246
34590
|
});
|
|
34247
|
-
this.recordSessionTask(sessionId, result.taskId);
|
|
34248
34591
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
34249
34592
|
this.captureWhy(sessionId, cascade, result);
|
|
34250
34593
|
this.socket.broadcast("cost:update", {
|
|
@@ -34633,6 +34976,694 @@ async function discoverMcpTools(servers) {
|
|
|
34633
34976
|
return results;
|
|
34634
34977
|
}
|
|
34635
34978
|
|
|
34979
|
+
// src/browser/lease.ts
|
|
34980
|
+
var LEASE_DEADLOCK_CEILING_MS = 3e5;
|
|
34981
|
+
var QUEUE_WAIT_MS = 18e4;
|
|
34982
|
+
var MAX_QUEUED = 8;
|
|
34983
|
+
var ACTION_HANDOFF_MS = 2e3;
|
|
34984
|
+
var ACTION_HANDOFF_POLL_MS = 25;
|
|
34985
|
+
var BrowserLease = class {
|
|
34986
|
+
actor = null;
|
|
34987
|
+
session = null;
|
|
34988
|
+
ceilingTimer = null;
|
|
34989
|
+
waiting = [];
|
|
34990
|
+
lifecycleWired = false;
|
|
34991
|
+
/**
|
|
34992
|
+
* The action holding the browser, if any.
|
|
34993
|
+
*
|
|
34994
|
+
* Needed alongside the lease for a reason the lease cannot cover: the lease
|
|
34995
|
+
* is RE-ENTRANT for its holder — that is what makes a sequence a sequence —
|
|
34996
|
+
* so nothing in it stops ONE actor running two actions at once. A retry
|
|
34997
|
+
* racing the call it retried is exactly that shape.
|
|
34998
|
+
*
|
|
34999
|
+
* A symbol because it identifies the individual call: only the action that
|
|
35000
|
+
* took the slot may release it.
|
|
35001
|
+
*/
|
|
35002
|
+
action = null;
|
|
35003
|
+
isRevoked;
|
|
35004
|
+
onChange;
|
|
35005
|
+
constructor(options = {}) {
|
|
35006
|
+
this.isRevoked = options.isRevoked ?? (() => false);
|
|
35007
|
+
this.onChange = options.onChange ?? (() => {
|
|
35008
|
+
});
|
|
35009
|
+
}
|
|
35010
|
+
get holderActor() {
|
|
35011
|
+
return this.actor;
|
|
35012
|
+
}
|
|
35013
|
+
get holderSession() {
|
|
35014
|
+
return this.session;
|
|
35015
|
+
}
|
|
35016
|
+
get queueDepth() {
|
|
35017
|
+
return this.waiting.length;
|
|
35018
|
+
}
|
|
35019
|
+
get actionInFlight() {
|
|
35020
|
+
return this.action !== null;
|
|
35021
|
+
}
|
|
35022
|
+
/**
|
|
35023
|
+
* Declare that this host reports worker terminal states.
|
|
35024
|
+
*
|
|
35025
|
+
* When it does, timers stop being ownership boundaries entirely. When it does
|
|
35026
|
+
* not — an embedder that wired a controller but not the release — the
|
|
35027
|
+
* fallbacks stay, because something has to break a deadlock.
|
|
35028
|
+
*/
|
|
35029
|
+
setLifecycleReleaseWired(on) {
|
|
35030
|
+
this.lifecycleWired = on;
|
|
35031
|
+
}
|
|
35032
|
+
/**
|
|
35033
|
+
* Take the browser for this actor, waiting in line if someone else holds it.
|
|
35034
|
+
*
|
|
35035
|
+
* Re-entrant for the actor that already holds it: that is the sequence case —
|
|
35036
|
+
* the same worker coming back for its next step must not queue behind itself.
|
|
35037
|
+
*/
|
|
35038
|
+
async acquire(actorId, sessionId, signal) {
|
|
35039
|
+
if (this.actor === null || this.actor === actorId) {
|
|
35040
|
+
this.actor = actorId;
|
|
35041
|
+
this.session = sessionId;
|
|
35042
|
+
this.armCeiling();
|
|
35043
|
+
return "granted";
|
|
35044
|
+
}
|
|
35045
|
+
if (this.waiting.length >= MAX_QUEUED) return "busy";
|
|
35046
|
+
return this.enqueue(actorId, sessionId, signal);
|
|
35047
|
+
}
|
|
35048
|
+
/**
|
|
35049
|
+
* One worker has finished its whole sequence, so its hold ends.
|
|
35050
|
+
*
|
|
35051
|
+
* THIS is the semantic release. It comes from the worker's own terminal path,
|
|
35052
|
+
* so it means "this actor will never ask again" — which a timer can never
|
|
35053
|
+
* mean about a worker that is merely thinking.
|
|
35054
|
+
*
|
|
35055
|
+
* Idempotent, and safe for an actor that never touched the browser.
|
|
35056
|
+
*/
|
|
35057
|
+
actorEnded(actorId) {
|
|
35058
|
+
if (!actorId) return;
|
|
35059
|
+
for (let i = this.waiting.length - 1; i >= 0; i--) {
|
|
35060
|
+
const w = this.waiting[i];
|
|
35061
|
+
if (w.actorId === actorId) w.settle("cancelled");
|
|
35062
|
+
}
|
|
35063
|
+
this.releaseIfHeldBy(actorId);
|
|
35064
|
+
this.onChange();
|
|
35065
|
+
}
|
|
35066
|
+
/**
|
|
35067
|
+
* Give the browser up when about to return without using it.
|
|
35068
|
+
*
|
|
35069
|
+
* Two guards, both load-bearing. Never while an action is in flight, because
|
|
35070
|
+
* the lease is RE-ENTRANT: the action still running may be this same actor's
|
|
35071
|
+
* previous step, and taking the lease off it hands the page to a queued
|
|
35072
|
+
* worker mid-sequence. And only while still the holder, because by the time a
|
|
35073
|
+
* refusal path runs a Stop may already have handed the lease on.
|
|
35074
|
+
*/
|
|
35075
|
+
releaseIfHeldBy(actorId) {
|
|
35076
|
+
if (this.action) return;
|
|
35077
|
+
if (this.actor === actorId) this.release();
|
|
35078
|
+
}
|
|
35079
|
+
/** Drop every queued waiter belonging to a run, and the lease if it holds it. */
|
|
35080
|
+
dropRun(sessionId) {
|
|
35081
|
+
for (let i = this.waiting.length - 1; i >= 0; i--) {
|
|
35082
|
+
const w = this.waiting[i];
|
|
35083
|
+
if (w.sessionId === sessionId) w.settle("cancelled");
|
|
35084
|
+
}
|
|
35085
|
+
if (this.session === sessionId) this.release();
|
|
35086
|
+
}
|
|
35087
|
+
/** Turn everyone away — the capability itself has gone. */
|
|
35088
|
+
dropAll() {
|
|
35089
|
+
for (let i = this.waiting.length - 1; i >= 0; i--) this.waiting[i].settle("off");
|
|
35090
|
+
}
|
|
35091
|
+
/** Hand the browser to the next actor in line, if any. */
|
|
35092
|
+
release() {
|
|
35093
|
+
this.clearCeiling();
|
|
35094
|
+
this.actor = null;
|
|
35095
|
+
this.session = null;
|
|
35096
|
+
let next = this.waiting.shift();
|
|
35097
|
+
while (next && this.isRevoked(next.sessionId)) {
|
|
35098
|
+
next.settle("cancelled");
|
|
35099
|
+
next = this.waiting.shift();
|
|
35100
|
+
}
|
|
35101
|
+
if (next) {
|
|
35102
|
+
this.actor = next.actorId;
|
|
35103
|
+
this.session = next.sessionId;
|
|
35104
|
+
this.armCeiling();
|
|
35105
|
+
next.settle("granted");
|
|
35106
|
+
}
|
|
35107
|
+
this.onChange();
|
|
35108
|
+
}
|
|
35109
|
+
/** Arm the deadlock ceiling. Not a release schedule — see the constant. */
|
|
35110
|
+
armCeiling() {
|
|
35111
|
+
this.clearCeiling();
|
|
35112
|
+
if (this.lifecycleWired) return;
|
|
35113
|
+
this.ceilingTimer = setTimeout(() => this.release(), LEASE_DEADLOCK_CEILING_MS);
|
|
35114
|
+
this.ceilingTimer.unref?.();
|
|
35115
|
+
}
|
|
35116
|
+
clearCeiling() {
|
|
35117
|
+
if (this.ceilingTimer) {
|
|
35118
|
+
clearTimeout(this.ceilingTimer);
|
|
35119
|
+
this.ceilingTimer = null;
|
|
35120
|
+
}
|
|
35121
|
+
}
|
|
35122
|
+
/** Take the per-action slot, or null when one is already running. */
|
|
35123
|
+
beginAction() {
|
|
35124
|
+
if (this.action) return null;
|
|
35125
|
+
const token = /* @__PURE__ */ Symbol("browser-action");
|
|
35126
|
+
this.action = token;
|
|
35127
|
+
return token;
|
|
35128
|
+
}
|
|
35129
|
+
/** Release the slot, but only for the action that took it. */
|
|
35130
|
+
endAction(token) {
|
|
35131
|
+
if (this.action !== token) return false;
|
|
35132
|
+
this.action = null;
|
|
35133
|
+
return true;
|
|
35134
|
+
}
|
|
35135
|
+
/**
|
|
35136
|
+
* Wait, briefly, for an outgoing action to unwind.
|
|
35137
|
+
*
|
|
35138
|
+
* A release can hand the browser on while the previous holder's abort is
|
|
35139
|
+
* still propagating through an awaited navigation. That is a handoff, not a
|
|
35140
|
+
* conflict, so it is waited out rather than refused — but bounded, because a
|
|
35141
|
+
* wedged action must not pin the queue.
|
|
35142
|
+
*/
|
|
35143
|
+
async awaitActionSlot() {
|
|
35144
|
+
for (let waited = 0; waited < ACTION_HANDOFF_MS; waited += ACTION_HANDOFF_POLL_MS) {
|
|
35145
|
+
if (!this.action) return true;
|
|
35146
|
+
await new Promise((r) => setTimeout(r, ACTION_HANDOFF_POLL_MS));
|
|
35147
|
+
}
|
|
35148
|
+
return this.action === null;
|
|
35149
|
+
}
|
|
35150
|
+
/**
|
|
35151
|
+
* Queue one actor, resolving when it gets the browser or is turned away.
|
|
35152
|
+
*
|
|
35153
|
+
* `settle` is wrapped so it runs at most once and always takes the waiter out
|
|
35154
|
+
* of the line: the timeout, an abort and a handoff all race, and settling
|
|
35155
|
+
* twice would silently leave a dead entry holding a queue slot.
|
|
35156
|
+
*/
|
|
35157
|
+
enqueue(actorId, sessionId, signal) {
|
|
35158
|
+
return new Promise((resolve2) => {
|
|
35159
|
+
let done = false;
|
|
35160
|
+
const waiter = {
|
|
35161
|
+
actorId,
|
|
35162
|
+
sessionId,
|
|
35163
|
+
settle: (result) => {
|
|
35164
|
+
if (done) return;
|
|
35165
|
+
done = true;
|
|
35166
|
+
clearTimeout(timer);
|
|
35167
|
+
signal?.removeEventListener("abort", giveUp);
|
|
35168
|
+
const i = this.waiting.indexOf(waiter);
|
|
35169
|
+
if (i >= 0) this.waiting.splice(i, 1);
|
|
35170
|
+
resolve2(result);
|
|
35171
|
+
}
|
|
35172
|
+
};
|
|
35173
|
+
const timer = this.lifecycleWired ? void 0 : setTimeout(() => waiter.settle("busy"), QUEUE_WAIT_MS);
|
|
35174
|
+
timer?.unref?.();
|
|
35175
|
+
const giveUp = () => waiter.settle("cancelled");
|
|
35176
|
+
signal?.addEventListener("abort", giveUp, { once: true });
|
|
35177
|
+
this.waiting.push(waiter);
|
|
35178
|
+
});
|
|
35179
|
+
}
|
|
35180
|
+
};
|
|
35181
|
+
|
|
35182
|
+
// src/browser/remote/controller.ts
|
|
35183
|
+
var ACTION_TIMEOUT_MS = 3e4;
|
|
35184
|
+
var RemoteBrowserController = class {
|
|
35185
|
+
provider;
|
|
35186
|
+
maxSessions;
|
|
35187
|
+
/**
|
|
35188
|
+
* Live-view listeners, one per run.
|
|
35189
|
+
*
|
|
35190
|
+
* A single callback was fine while each run had its own controller. Now the
|
|
35191
|
+
* controller is shared across a deployment, so one callback would send run
|
|
35192
|
+
* A's live-view URL to whichever run registered last — and that URL is a
|
|
35193
|
+
* bearer capability for a browser somebody else is driving.
|
|
35194
|
+
*/
|
|
35195
|
+
liveViewListeners = /* @__PURE__ */ new Map();
|
|
35196
|
+
/** An embedder that wants every run's live view, told which run each is. */
|
|
35197
|
+
onLiveViewAll;
|
|
35198
|
+
runs = /* @__PURE__ */ new Map();
|
|
35199
|
+
/**
|
|
35200
|
+
* Runs that have a pool slot but no browser yet.
|
|
35201
|
+
*
|
|
35202
|
+
* Counted alongside `runs` against the cap. Without it the limit was a
|
|
35203
|
+
* check-then-act across four awaits, and simultaneous first uses both won.
|
|
35204
|
+
*/
|
|
35205
|
+
opening = /* @__PURE__ */ new Set();
|
|
35206
|
+
/**
|
|
35207
|
+
* A run's stop signal, created when its slot is reserved.
|
|
35208
|
+
*
|
|
35209
|
+
* Before this, Stop looked only in `runs` — so during the first `open()`,
|
|
35210
|
+
* while the run existed only in `opening` and four awaits were in flight, it
|
|
35211
|
+
* found nothing and returned. The open then completed, built a FRESH
|
|
35212
|
+
* unaborted controller, and the click the user had stopped went ahead. The
|
|
35213
|
+
* signal has to exist before the browser does.
|
|
35214
|
+
*/
|
|
35215
|
+
aborts = /* @__PURE__ */ new Map();
|
|
35216
|
+
/**
|
|
35217
|
+
* Runs whose provider session is being released right now.
|
|
35218
|
+
*
|
|
35219
|
+
* `stopRun`, `endRun`, `releaseIfIdle`, and the stale-page branch of `open`
|
|
35220
|
+
* all delete a run from `runs` synchronously and only THEN await
|
|
35221
|
+
* `disposeRun` settling — the browser disconnect, and `provider.endSession`,
|
|
35222
|
+
* which for Steel is a real HTTP `/release` call. Between that delete and
|
|
35223
|
+
* the call actually finishing, the admission check in `open()` — which
|
|
35224
|
+
* counts only `runs` and `opening` — saw the slot as free, so a second run
|
|
35225
|
+
* could pass admission and call `createSession` before the first run's
|
|
35226
|
+
* session was truly released. At `maxSessions: 1` that is two live, BILLED
|
|
35227
|
+
* provider sessions at once, exactly what the pool exists to prevent.
|
|
35228
|
+
* Counting this set alongside the other two closes the window.
|
|
35229
|
+
*
|
|
35230
|
+
* Keyed by run id, not a counter: every one of those four call sites checks
|
|
35231
|
+
* the run is still IN `runs` before deleting it and starting teardown, and
|
|
35232
|
+
* that delete is synchronous, so at most one teardown can ever be in flight
|
|
35233
|
+
* for a given run id at a time — a second caller finds the entry already
|
|
35234
|
+
* gone and never starts a competing one. The entry is removed only once its
|
|
35235
|
+
* own teardown settles, so a later, genuine re-open of the same run id
|
|
35236
|
+
* never collides with one still in flight.
|
|
35237
|
+
*/
|
|
35238
|
+
closing = /* @__PURE__ */ new Set();
|
|
35239
|
+
/** Runs the user has stopped, by run id — same meaning as on the desktop. */
|
|
35240
|
+
revoked = /* @__PURE__ */ new Set();
|
|
35241
|
+
/**
|
|
35242
|
+
* One lease PER RUN, because there is one browser per run.
|
|
35243
|
+
*
|
|
35244
|
+
* The desktop has a single lease because it has a single browser. Here each
|
|
35245
|
+
* run gets its own session, so a global lease would make run B queue behind
|
|
35246
|
+
* run A for a browser it was never going to use — and with no queue timeout,
|
|
35247
|
+
* queue for as long as A lasts. What limits runs against each other is the
|
|
35248
|
+
* session POOL, not the lease.
|
|
35249
|
+
*/
|
|
35250
|
+
leases = /* @__PURE__ */ new Map();
|
|
35251
|
+
constructor(options) {
|
|
35252
|
+
this.provider = options.provider;
|
|
35253
|
+
const asked = Math.max(1, options.maxSessions ?? 1);
|
|
35254
|
+
this.maxSessions = options.provider.isolatesSessions ? asked : 1;
|
|
35255
|
+
this.onLiveViewAll = options.onLiveView;
|
|
35256
|
+
}
|
|
35257
|
+
leaseFor(runId) {
|
|
35258
|
+
let lease = this.leases.get(runId);
|
|
35259
|
+
if (!lease) {
|
|
35260
|
+
lease = new BrowserLease({ isRevoked: (id) => this.revoked.has(id) });
|
|
35261
|
+
lease.setLifecycleReleaseWired(true);
|
|
35262
|
+
this.leases.set(runId, lease);
|
|
35263
|
+
}
|
|
35264
|
+
return lease;
|
|
35265
|
+
}
|
|
35266
|
+
/** The controller to hand to `Cascade.setBrowserController`. */
|
|
35267
|
+
get controller() {
|
|
35268
|
+
return (action, context) => this.act(action, context);
|
|
35269
|
+
}
|
|
35270
|
+
/** Watch one run's browser. The URL never goes to any other run's listener. */
|
|
35271
|
+
onLiveViewFor(runKey, listener) {
|
|
35272
|
+
this.liveViewListeners.set(runKey, listener);
|
|
35273
|
+
}
|
|
35274
|
+
offLiveViewFor(runKey) {
|
|
35275
|
+
this.liveViewListeners.delete(runKey);
|
|
35276
|
+
}
|
|
35277
|
+
/**
|
|
35278
|
+
* Tell a run's listener what its browser situation is.
|
|
35279
|
+
*
|
|
35280
|
+
* `active` is separate from the URL and both are needed: a provider with no
|
|
35281
|
+
* live view is attached-but-unwatchable, and a finished run is not attached
|
|
35282
|
+
* at all. Both have no URL, so a listener given only the URL cannot tell them
|
|
35283
|
+
* apart — and would drop the Stop control for the first as if it were the
|
|
35284
|
+
* second.
|
|
35285
|
+
*/
|
|
35286
|
+
announceLiveView(runId, liveViewUrl, active) {
|
|
35287
|
+
this.liveViewListeners.get(runId)?.({ active, ...liveViewUrl ? { liveViewUrl } : {} });
|
|
35288
|
+
this.onLiveViewAll?.(runId, liveViewUrl);
|
|
35289
|
+
}
|
|
35290
|
+
/**
|
|
35291
|
+
* Hand back a session the run turned out not to want.
|
|
35292
|
+
*
|
|
35293
|
+
* A browser opened for an action that is then refused would otherwise stay
|
|
35294
|
+
* allocated and billed until the whole run ended, for a run that has just
|
|
35295
|
+
* been stopped.
|
|
35296
|
+
*/
|
|
35297
|
+
async releaseIfIdle(runId) {
|
|
35298
|
+
const held = this.runs.get(runId);
|
|
35299
|
+
if (!held) return;
|
|
35300
|
+
this.runs.delete(runId);
|
|
35301
|
+
this.closing.add(runId);
|
|
35302
|
+
await this.teardown(runId, held);
|
|
35303
|
+
}
|
|
35304
|
+
/** Release every run's session. For when the deployment's config changes. */
|
|
35305
|
+
async dispose() {
|
|
35306
|
+
const ids = /* @__PURE__ */ new Set([...this.runs.keys(), ...this.opening]);
|
|
35307
|
+
await Promise.all([...ids].map((runId) => this.endRun(runId)));
|
|
35308
|
+
}
|
|
35309
|
+
/** A worker finished; it will never ask for the browser again. */
|
|
35310
|
+
actorEnded(actorId) {
|
|
35311
|
+
for (const lease of this.leases.values()) lease.actorEnded(actorId);
|
|
35312
|
+
}
|
|
35313
|
+
/** The user stopped this run. Refuses further actions and clears its queue. */
|
|
35314
|
+
stopRun(runId) {
|
|
35315
|
+
this.revoked.add(runId);
|
|
35316
|
+
this.leases.get(runId)?.dropRun(runId);
|
|
35317
|
+
this.aborts.get(runId)?.abort();
|
|
35318
|
+
const held = this.runs.get(runId);
|
|
35319
|
+
if (!held) return;
|
|
35320
|
+
this.runs.delete(runId);
|
|
35321
|
+
this.closing.add(runId);
|
|
35322
|
+
void this.teardown(runId, held);
|
|
35323
|
+
}
|
|
35324
|
+
/** A run ended: release its browser rather than pay for an idle session. */
|
|
35325
|
+
async endRun(runId) {
|
|
35326
|
+
this.leases.get(runId)?.dropRun(runId);
|
|
35327
|
+
this.aborts.get(runId)?.abort();
|
|
35328
|
+
const held = this.runs.get(runId);
|
|
35329
|
+
if (!held) {
|
|
35330
|
+
this.forgetRun(runId);
|
|
35331
|
+
return;
|
|
35332
|
+
}
|
|
35333
|
+
this.runs.delete(runId);
|
|
35334
|
+
this.closing.add(runId);
|
|
35335
|
+
await this.teardown(runId, held);
|
|
35336
|
+
this.forgetRun(runId);
|
|
35337
|
+
}
|
|
35338
|
+
/** Forget a finished run entirely, so nothing accumulates per run. */
|
|
35339
|
+
forgetRun(runId) {
|
|
35340
|
+
this.revoked.delete(runId);
|
|
35341
|
+
this.leases.delete(runId);
|
|
35342
|
+
this.aborts.delete(runId);
|
|
35343
|
+
this.liveViewListeners.delete(runId);
|
|
35344
|
+
}
|
|
35345
|
+
/**
|
|
35346
|
+
* Let go of one run's browser.
|
|
35347
|
+
*
|
|
35348
|
+
* Best-effort throughout: a run is already over by the time this runs, and a
|
|
35349
|
+
* provider that is briefly unreachable must not turn that into a failure the
|
|
35350
|
+
* user sees. The provider's own idle timeout collects anything left.
|
|
35351
|
+
*/
|
|
35352
|
+
async disposeRun(runId, held) {
|
|
35353
|
+
held.abort.abort();
|
|
35354
|
+
this.announceLiveView(runId, void 0, false);
|
|
35355
|
+
await held.ownedContext?.close().catch(() => {
|
|
35356
|
+
});
|
|
35357
|
+
await held.browser.close().catch(() => {
|
|
35358
|
+
});
|
|
35359
|
+
await this.provider.endSession(held.session.id).catch(() => {
|
|
35360
|
+
});
|
|
35361
|
+
}
|
|
35362
|
+
/**
|
|
35363
|
+
* Run `disposeRun`, with the run id counted in `closing` until it settles.
|
|
35364
|
+
*
|
|
35365
|
+
* The caller adds the id to `closing` itself, synchronously, in the same
|
|
35366
|
+
* breath as deleting the run from `runs` — see the field comment. This just
|
|
35367
|
+
* guarantees the removal happens, success or failure, so a disposal that
|
|
35368
|
+
* throws cannot leave the slot counted against the pool forever.
|
|
35369
|
+
*/
|
|
35370
|
+
async teardown(runId, held) {
|
|
35371
|
+
try {
|
|
35372
|
+
await this.disposeRun(runId, held);
|
|
35373
|
+
} finally {
|
|
35374
|
+
this.closing.delete(runId);
|
|
35375
|
+
}
|
|
35376
|
+
}
|
|
35377
|
+
async act(action, context) {
|
|
35378
|
+
const runId = context.sessionId;
|
|
35379
|
+
if (!runId) {
|
|
35380
|
+
return { ok: false, detail: "This browser action arrived without a run identity, so it was not run." };
|
|
35381
|
+
}
|
|
35382
|
+
const actor = context.actorId || runId;
|
|
35383
|
+
if (this.revoked.has(runId)) {
|
|
35384
|
+
return { ok: false, detail: "The user stopped browser control for this run." };
|
|
35385
|
+
}
|
|
35386
|
+
if (context.signal?.aborted) {
|
|
35387
|
+
return { ok: false, detail: "The run was cancelled." };
|
|
35388
|
+
}
|
|
35389
|
+
const planned = this.runs.get(runId)?.generation ?? 0;
|
|
35390
|
+
const lease_ = this.leaseFor(runId);
|
|
35391
|
+
const lease = await lease_.acquire(actor, runId, context.signal);
|
|
35392
|
+
if (lease !== "granted") {
|
|
35393
|
+
return { ok: false, detail: refusal(lease) };
|
|
35394
|
+
}
|
|
35395
|
+
if (this.revoked.has(runId)) {
|
|
35396
|
+
lease_.releaseIfHeldBy(actor);
|
|
35397
|
+
return { ok: false, detail: "The user stopped browser control for this run." };
|
|
35398
|
+
}
|
|
35399
|
+
if (context.signal?.aborted) {
|
|
35400
|
+
lease_.releaseIfHeldBy(actor);
|
|
35401
|
+
return { ok: false, detail: "The run was cancelled." };
|
|
35402
|
+
}
|
|
35403
|
+
if (!await lease_.awaitActionSlot()) {
|
|
35404
|
+
lease_.releaseIfHeldBy(actor);
|
|
35405
|
+
return { ok: false, detail: "Another browser action is still finishing. Try again." };
|
|
35406
|
+
}
|
|
35407
|
+
const token = lease_.beginAction();
|
|
35408
|
+
if (!token) {
|
|
35409
|
+
return { ok: false, detail: "Another browser action is already running. Wait for it to finish." };
|
|
35410
|
+
}
|
|
35411
|
+
try {
|
|
35412
|
+
const held = await this.open(runId, context.signal);
|
|
35413
|
+
if (this.revoked.has(runId)) {
|
|
35414
|
+
await this.releaseIfIdle(runId);
|
|
35415
|
+
return { ok: false, detail: "The user stopped browser control for this run." };
|
|
35416
|
+
}
|
|
35417
|
+
if (context.signal?.aborted || held.abort.signal.aborted) {
|
|
35418
|
+
await this.releaseIfIdle(runId);
|
|
35419
|
+
return { ok: false, detail: "The run was cancelled." };
|
|
35420
|
+
}
|
|
35421
|
+
return await this.perform(held, action, planned);
|
|
35422
|
+
} catch (err) {
|
|
35423
|
+
return { ok: false, detail: err instanceof Error ? err.message : String(err) };
|
|
35424
|
+
} finally {
|
|
35425
|
+
lease_.endAction(token);
|
|
35426
|
+
if (context.signal?.aborted || this.revoked.has(runId)) lease_.releaseIfHeldBy(actor);
|
|
35427
|
+
}
|
|
35428
|
+
}
|
|
35429
|
+
/**
|
|
35430
|
+
* The run's browser, opened on first use.
|
|
35431
|
+
*
|
|
35432
|
+
* The ONE place the session cap is enforced. An earlier version also checked
|
|
35433
|
+
* it before taking the lease, on the reasoning that a run which cannot get a
|
|
35434
|
+
* session would otherwise queue forever — true when every run shared one
|
|
35435
|
+
* lease, and false once each run got its own, because a second run no longer
|
|
35436
|
+
* queues behind the first at all. Its own revert-check proved it dead: taking
|
|
35437
|
+
* it out changed no test. Two copies of a cap invite them to disagree.
|
|
35438
|
+
*/
|
|
35439
|
+
async open(runId, signal) {
|
|
35440
|
+
const existing = this.runs.get(runId);
|
|
35441
|
+
if (existing && !existing.page.isClosed()) return existing;
|
|
35442
|
+
if (existing) {
|
|
35443
|
+
this.runs.delete(runId);
|
|
35444
|
+
this.closing.add(runId);
|
|
35445
|
+
await this.teardown(runId, existing);
|
|
35446
|
+
}
|
|
35447
|
+
if (this.runs.size + this.opening.size + this.closing.size >= this.maxSessions) {
|
|
35448
|
+
throw new Error(
|
|
35449
|
+
`All ${this.maxSessions} browser session${this.maxSessions === 1 ? "" : "s"} are in use by other runs. Try again when one finishes, or raise the session limit in settings.`
|
|
35450
|
+
);
|
|
35451
|
+
}
|
|
35452
|
+
this.opening.add(runId);
|
|
35453
|
+
if (!this.aborts.has(runId)) this.aborts.set(runId, new AbortController());
|
|
35454
|
+
try {
|
|
35455
|
+
return await this.openReserved(runId, signal);
|
|
35456
|
+
} finally {
|
|
35457
|
+
this.opening.delete(runId);
|
|
35458
|
+
}
|
|
35459
|
+
}
|
|
35460
|
+
/** The part that may fail, with the pool slot already reserved. */
|
|
35461
|
+
async openReserved(runId, signal) {
|
|
35462
|
+
const playwright = await loadPlaywright();
|
|
35463
|
+
const abort = this.aborts.get(runId)?.signal;
|
|
35464
|
+
const creationSignal = signal && abort ? AbortSignal.any([signal, abort]) : signal ?? abort;
|
|
35465
|
+
const session = await this.provider.createSession(creationSignal);
|
|
35466
|
+
this.announceLiveView(runId, session.liveViewUrl, true);
|
|
35467
|
+
let browser;
|
|
35468
|
+
let owned;
|
|
35469
|
+
try {
|
|
35470
|
+
browser = await playwright.chromium.connectOverCDP(session.cdpUrl);
|
|
35471
|
+
owned = this.provider.isolatesSessions ? void 0 : await browser.newContext();
|
|
35472
|
+
const context = owned ?? browser.contexts()[0] ?? await browser.newContext();
|
|
35473
|
+
const page2 = owned ? await owned.newPage() : context.pages()[0] ?? await context.newPage();
|
|
35474
|
+
const abort2 = this.aborts.get(runId);
|
|
35475
|
+
if (!abort2) throw new Error("The run ended while its browser was still opening.");
|
|
35476
|
+
const held = { session, browser, page: page2, generation: 0, abort: abort2, ...owned ? { ownedContext: owned } : {} };
|
|
35477
|
+
page2.on("framenavigated", ((frame) => {
|
|
35478
|
+
if (frame !== page2.mainFrame()) return;
|
|
35479
|
+
held.generation += 1;
|
|
35480
|
+
}));
|
|
35481
|
+
this.runs.set(runId, held);
|
|
35482
|
+
return held;
|
|
35483
|
+
} catch (err) {
|
|
35484
|
+
this.announceLiveView(runId, void 0, false);
|
|
35485
|
+
await owned?.close().catch(() => {
|
|
35486
|
+
});
|
|
35487
|
+
await browser?.close().catch(() => {
|
|
35488
|
+
});
|
|
35489
|
+
await this.provider.endSession(session.id).catch(() => {
|
|
35490
|
+
});
|
|
35491
|
+
throw err;
|
|
35492
|
+
}
|
|
35493
|
+
}
|
|
35494
|
+
async perform(held, action, planned) {
|
|
35495
|
+
const { page: page2 } = held;
|
|
35496
|
+
const stoppable = async (work) => {
|
|
35497
|
+
if (held.abort.signal.aborted) throw new Error("The user stopped browser control for this run.");
|
|
35498
|
+
let onAbort;
|
|
35499
|
+
try {
|
|
35500
|
+
return await Promise.race([
|
|
35501
|
+
work,
|
|
35502
|
+
new Promise((_, reject) => {
|
|
35503
|
+
onAbort = () => {
|
|
35504
|
+
void page2.close().catch(() => {
|
|
35505
|
+
});
|
|
35506
|
+
reject(new Error("The user stopped browser control for this run."));
|
|
35507
|
+
};
|
|
35508
|
+
held.abort.signal.addEventListener("abort", onAbort, { once: true });
|
|
35509
|
+
})
|
|
35510
|
+
]);
|
|
35511
|
+
} finally {
|
|
35512
|
+
held.abort.signal.removeEventListener("abort", onAbort);
|
|
35513
|
+
}
|
|
35514
|
+
};
|
|
35515
|
+
const timeout = Math.min(action.timeoutMs ?? 1e4, ACTION_TIMEOUT_MS);
|
|
35516
|
+
const where = async () => ({
|
|
35517
|
+
url: page2.url(),
|
|
35518
|
+
title: await page2.title().catch(() => "")
|
|
35519
|
+
});
|
|
35520
|
+
if (action.kind !== "navigate" && held.generation !== planned) {
|
|
35521
|
+
return {
|
|
35522
|
+
ok: false,
|
|
35523
|
+
detail: "The page changed while this action was waiting for the browser, so it was not run. Look at the page again before retrying.",
|
|
35524
|
+
...await where()
|
|
35525
|
+
};
|
|
35526
|
+
}
|
|
35527
|
+
switch (action.kind) {
|
|
35528
|
+
case "navigate": {
|
|
35529
|
+
const target = action.url ?? "";
|
|
35530
|
+
if (!/^https?:\/\//i.test(target)) {
|
|
35531
|
+
return { ok: false, detail: "Only http and https addresses can be opened." };
|
|
35532
|
+
}
|
|
35533
|
+
await stoppable(page2.goto(target, { timeout: ACTION_TIMEOUT_MS, waitUntil: "domcontentloaded" }));
|
|
35534
|
+
return { ok: true, detail: `Opened ${target}`, ...await where() };
|
|
35535
|
+
}
|
|
35536
|
+
case "click":
|
|
35537
|
+
await stoppable(page2.click(action.selector, { timeout }));
|
|
35538
|
+
return { ok: true, detail: `Clicked ${action.selector}`, ...await where() };
|
|
35539
|
+
case "fill":
|
|
35540
|
+
await stoppable(page2.fill(action.selector, action.value ?? "", { timeout }));
|
|
35541
|
+
return { ok: true, detail: `Filled ${action.selector}`, ...await where() };
|
|
35542
|
+
case "press": {
|
|
35543
|
+
if (action.selector) await stoppable(page2.press(action.selector, action.key, { timeout }));
|
|
35544
|
+
else await stoppable(page2.keyboard.press(action.key));
|
|
35545
|
+
return { ok: true, detail: `Pressed ${action.key}`, ...await where() };
|
|
35546
|
+
}
|
|
35547
|
+
case "wait_for":
|
|
35548
|
+
await stoppable(page2.waitForSelector(action.selector, { timeout }));
|
|
35549
|
+
return { ok: true, detail: `${action.selector} appeared`, ...await where() };
|
|
35550
|
+
case "extract_text": {
|
|
35551
|
+
const text = await stoppable(page2.innerText(action.selector ?? "body", { timeout }));
|
|
35552
|
+
return { ok: true, detail: text.slice(0, 2e5), ...await where() };
|
|
35553
|
+
}
|
|
35554
|
+
default:
|
|
35555
|
+
return { ok: false, detail: `Unsupported action: ${String(action.kind)}` };
|
|
35556
|
+
}
|
|
35557
|
+
}
|
|
35558
|
+
};
|
|
35559
|
+
function refusal(result) {
|
|
35560
|
+
if (result === "off") return "Browser control was turned off while this action waited for the browser.";
|
|
35561
|
+
if (result === "cancelled") return "The run was cancelled while waiting for the browser.";
|
|
35562
|
+
return "The browser is in use by another part of this run and did not come free in time. Try again, or do something else first.";
|
|
35563
|
+
}
|
|
35564
|
+
async function loadPlaywright() {
|
|
35565
|
+
try {
|
|
35566
|
+
return await import('playwright');
|
|
35567
|
+
} catch {
|
|
35568
|
+
throw new Error("Playwright is not installed. Run: npm install playwright (no browser download is needed \u2014 this connects to a remote browser).");
|
|
35569
|
+
}
|
|
35570
|
+
}
|
|
35571
|
+
|
|
35572
|
+
// src/browser/remote/generic-cdp.ts
|
|
35573
|
+
var GenericCdpProvider = class {
|
|
35574
|
+
name = "CDP endpoint";
|
|
35575
|
+
// The endpoint is a single browser. There is no session to allocate, so two
|
|
35576
|
+
// runs against it share a page — see the seam.
|
|
35577
|
+
isolatesSessions = false;
|
|
35578
|
+
endpoint;
|
|
35579
|
+
constructor(endpoint) {
|
|
35580
|
+
this.endpoint = endpoint;
|
|
35581
|
+
}
|
|
35582
|
+
/**
|
|
35583
|
+
* There is no session to create — the endpoint is the session.
|
|
35584
|
+
*
|
|
35585
|
+
* `id` is a constant rather than a generated one because there is nothing to
|
|
35586
|
+
* look up later: `endSession` has nothing to call. Inventing a unique id
|
|
35587
|
+
* would imply a lifecycle this provider does not have.
|
|
35588
|
+
*/
|
|
35589
|
+
async createSession() {
|
|
35590
|
+
return { id: "cdp", cdpUrl: this.endpoint };
|
|
35591
|
+
}
|
|
35592
|
+
/**
|
|
35593
|
+
* Deliberately nothing.
|
|
35594
|
+
*
|
|
35595
|
+
* The endpoint belongs to whoever runs it and outlives any single run, so
|
|
35596
|
+
* "ending" it is not ours to do — closing it would take the browser away from
|
|
35597
|
+
* every other user of a shared endpoint. The driver still closes its own
|
|
35598
|
+
* connection and its pages; this is only about the browser's lifetime.
|
|
35599
|
+
*/
|
|
35600
|
+
async endSession() {
|
|
35601
|
+
}
|
|
35602
|
+
};
|
|
35603
|
+
function isCdpEndpoint(url) {
|
|
35604
|
+
try {
|
|
35605
|
+
const u = new URL(url);
|
|
35606
|
+
return u.protocol === "ws:" || u.protocol === "wss:";
|
|
35607
|
+
} catch {
|
|
35608
|
+
return false;
|
|
35609
|
+
}
|
|
35610
|
+
}
|
|
35611
|
+
|
|
35612
|
+
// src/browser/remote/steel.ts
|
|
35613
|
+
var DEFAULT_BASE = "https://api.steel.dev";
|
|
35614
|
+
var CREATE_TIMEOUT_MS = 6e4;
|
|
35615
|
+
var SteelProvider = class {
|
|
35616
|
+
name = "Steel";
|
|
35617
|
+
// POST /v1/sessions allocates a browser per call, so concurrent runs are
|
|
35618
|
+
// genuinely separate.
|
|
35619
|
+
isolatesSessions = true;
|
|
35620
|
+
base;
|
|
35621
|
+
apiKey;
|
|
35622
|
+
constructor(opts = {}) {
|
|
35623
|
+
let base = opts.url || DEFAULT_BASE;
|
|
35624
|
+
while (base.endsWith("/")) base = base.slice(0, -1);
|
|
35625
|
+
this.base = base;
|
|
35626
|
+
this.apiKey = opts.apiKey;
|
|
35627
|
+
}
|
|
35628
|
+
async createSession(signal) {
|
|
35629
|
+
const details = await this.call("POST", "/v1/sessions", signal, {});
|
|
35630
|
+
if (!details.id || !details.websocketUrl) {
|
|
35631
|
+
throw new Error("Steel returned a session with no id or websocket URL.");
|
|
35632
|
+
}
|
|
35633
|
+
return {
|
|
35634
|
+
id: details.id,
|
|
35635
|
+
cdpUrl: details.websocketUrl,
|
|
35636
|
+
...details.debugUrl ? { liveViewUrl: details.debugUrl } : {}
|
|
35637
|
+
};
|
|
35638
|
+
}
|
|
35639
|
+
async endSession(id) {
|
|
35640
|
+
try {
|
|
35641
|
+
await this.call("POST", `/v1/sessions/${encodeURIComponent(id)}/release`, void 0, {});
|
|
35642
|
+
} catch {
|
|
35643
|
+
}
|
|
35644
|
+
}
|
|
35645
|
+
async call(method, path30, signal, body) {
|
|
35646
|
+
const timeout = AbortSignal.timeout(CREATE_TIMEOUT_MS);
|
|
35647
|
+
const composed = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
35648
|
+
const res = await fetch(`${this.base}${path30}`, {
|
|
35649
|
+
method,
|
|
35650
|
+
headers: {
|
|
35651
|
+
"content-type": "application/json",
|
|
35652
|
+
// Sent only when configured — the self-hosted API takes no key, and an
|
|
35653
|
+
// empty header is worse than none.
|
|
35654
|
+
...this.apiKey ? { "steel-api-key": this.apiKey } : {}
|
|
35655
|
+
},
|
|
35656
|
+
body: JSON.stringify(body),
|
|
35657
|
+
signal: composed
|
|
35658
|
+
});
|
|
35659
|
+
if (!res.ok) {
|
|
35660
|
+
const detail = await res.text().then((t) => t.slice(0, 300)).catch(() => "");
|
|
35661
|
+
throw new Error(`Steel ${method} ${path30} failed: ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
35662
|
+
}
|
|
35663
|
+
return await res.json();
|
|
35664
|
+
}
|
|
35665
|
+
};
|
|
35666
|
+
|
|
34636
35667
|
// src/retrieval/chunk.ts
|
|
34637
35668
|
var DEFAULTS2 = { targetChars: 2e3, overlapChars: 200, minChars: 80 };
|
|
34638
35669
|
function splitLongBlock(block, max) {
|
|
@@ -35284,6 +36315,6 @@ function applySyncBundle(bundle, config, cleanup) {
|
|
|
35284
36315
|
return next;
|
|
35285
36316
|
}
|
|
35286
36317
|
|
|
35287
|
-
export { AZURE_BASE_MODELS, AZURE_BASE_URL_TEMPLATE, AuditLogger, CASCADE_AUDIT_FILE, CASCADE_CONFIG_DIR, CASCADE_CONFIG_FILE, CASCADE_DASHBOARD_SECRET_FILE, CASCADE_DB_FILE, CASCADE_IGNORE_FILE, CASCADE_KEYSTORE_FILE, CASCADE_MD_FILE, CASCADE_VERSION, CHARS_PER_TOKEN2 as CHARS_PER_TOKEN, COMPLEXITY_T2_COUNT, Cascade, CascadeCancelledError, CascadeConfigSchema, CascadeIgnore, CascadeRouter, CascadeToolError, CloudClient, CodeSearchTool, ConfigManager, CurrentPageTool, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CLOUD_URL, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, DOCUMENT_MIME, DashboardServer, DependencyScheduler, FileMcpOAuthStore, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, GenerateDocumentTool, GraphRetriever, GraphSearchTool, HooksRunner, Keystore, LLMReranker, LM_STUDIO_BASE_URL, MAX_PAGE_TEXT_CHARS, MCP_TOOL_PREFIX, MODELS, McpClient, McpOAuthProvider, MemoryStore, OLLAMA_BASE_URL, OpenAICompatibleEmbedder, PROVIDER_DISPLAY_NAMES, RESUME_SCHEMA_VERSION, ResumeStore, Retriever, SqliteVectorStore, T1Administrator, T1_MODEL_PRIORITY, T2Manager, T2_MODEL_PRIORITY, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, TaskGraphValidationError, TaskScheduler, Telemetry, ToolRegistry, VISION_MODEL_PRIORITY, WorkspaceIndex, applyEndpointEdit, applyProviderApiKey, applyProviderCredential, applySettingsCredentials, applySettingsPayload, applySyncBundle, azureModelForDeployment, beginMcpOAuth, buildDocumentTools, buildManifest, buildSessionTranscript, bytesToBase64, cagCharBudget, chartKind, chartToTableRows, chatCompleterFromProviders, chunkCode, chunkText2 as chunkText, commitSettings, compileSectionGraph, compileSubtaskGraph, compileTaskGraph, completeMcpOAuth, connectMcpWithLoopbackOAuth, createCascade, credentialDispositionForEdit, credentialEndpointIdentity, decryptJSON as decryptSyncBlob, diffManifest, disambiguateMcpServerNames, discoverMcpAuthServer, discoverMcpTools, distillSessionFacts, embedderFromProviders, encryptJSON as encryptSyncBlob, endpointFromSettingsPayload, evaluateAcceptance, explainRefusal, extractCharts, failures, fileExt, fileOAuthProvider, gatherSyncBundle, hasDefaultEndpoint, hasProviderCredential, hasUsableProvider, hashContent, heuristicCodeChunker, inferAzureBaseModel, inlineRuns, isDocumentFormat, isMcpToolName, matchImageLine, mcpServerPrefix, mcpToolName, nodeHttpFetch, normalizeEndpoint, parseBlocks, parseChartSpec, parseDelimited, parseRankOrder, parseSlide, planRetrieval, preferIpv4Host, priorAzureRow, reciprocalRankFusion, refreshMcpToken, removeMcpServerDenials, renderDocument, renderDocx, renderPptx, renderXlsx, runCascade, sameAzureEndpoint, sameCredentialEndpoint, sameEndpoint, sessionWorthRemembering, settingsSnapshot, sniffImage, splitSlides, streamCascade, stripInline, summarizeCompleted, undecided, uniqueMcpServerName };
|
|
36318
|
+
export { AZURE_BASE_MODELS, AZURE_BASE_URL_TEMPLATE, AuditLogger, BrowserControlTool, BrowserLease, CASCADE_AUDIT_FILE, CASCADE_CONFIG_DIR, CASCADE_CONFIG_FILE, CASCADE_DASHBOARD_SECRET_FILE, CASCADE_DB_FILE, CASCADE_IGNORE_FILE, CASCADE_KEYSTORE_FILE, CASCADE_MD_FILE, CASCADE_VERSION, CHARS_PER_TOKEN2 as CHARS_PER_TOKEN, COMPLEXITY_T2_COUNT, Cascade, CascadeCancelledError, CascadeConfigSchema, CascadeIgnore, CascadeRouter, CascadeToolError, CloudClient, CodeSearchTool, ConfigManager, CurrentPageTool, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CLOUD_URL, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, DOCUMENT_MIME, DashboardServer, DependencyScheduler, FileMcpOAuthStore, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, GenerateDocumentTool, GenericCdpProvider, GraphRetriever, GraphSearchTool, HooksRunner, Keystore, LLMReranker, LM_STUDIO_BASE_URL, MAX_PAGE_TEXT_CHARS, MCP_TOOL_PREFIX, MODELS, McpClient, McpOAuthProvider, MemoryStore, OLLAMA_BASE_URL, OpenAICompatibleEmbedder, PROVIDER_DISPLAY_NAMES, RESUME_SCHEMA_VERSION, RemoteBrowserController, ResumeStore, Retriever, SqliteVectorStore, SteelProvider, T1Administrator, T1_MODEL_PRIORITY, T2Manager, T2_MODEL_PRIORITY, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, TaskGraphValidationError, TaskScheduler, Telemetry, ToolRegistry, VISION_MODEL_PRIORITY, WorkspaceIndex, applyEndpointEdit, applyProviderApiKey, applyProviderCredential, applySettingsCredentials, applySettingsPayload, applySyncBundle, azureModelForDeployment, beginMcpOAuth, buildDocumentTools, buildManifest, buildSessionTranscript, bytesToBase64, cagCharBudget, chartKind, chartToTableRows, chatCompleterFromProviders, chunkCode, chunkText2 as chunkText, commitSettings, compileSectionGraph, compileSubtaskGraph, compileTaskGraph, completeMcpOAuth, connectMcpWithLoopbackOAuth, createCascade, credentialDispositionForEdit, credentialEndpointIdentity, decryptJSON as decryptSyncBlob, diffManifest, disambiguateMcpServerNames, discoverMcpAuthServer, discoverMcpTools, distillSessionFacts, embedderFromProviders, encryptJSON as encryptSyncBlob, endpointFromSettingsPayload, evaluateAcceptance, explainRefusal, extractCharts, failures, fileExt, fileOAuthProvider, gatherSyncBundle, hasDefaultEndpoint, hasProviderCredential, hasUsableProvider, hashContent, heuristicCodeChunker, inferAzureBaseModel, inlineRuns, isCdpEndpoint, isDocumentFormat, isMcpToolName, matchImageLine, mcpServerPrefix, mcpToolName, nodeHttpFetch, normalizeEndpoint, parseBlocks, parseChartSpec, parseDelimited, parseRankOrder, parseSlide, planRetrieval, preferIpv4Host, priorAzureRow, reciprocalRankFusion, refreshMcpToken, removeMcpServerDenials, renderDocument, renderDocx, renderPptx, renderXlsx, runCascade, sameAzureEndpoint, sameCredentialEndpoint, sameEndpoint, sessionWorthRemembering, settingsSnapshot, sniffImage, splitSlides, streamCascade, stripInline, summarizeCompleted, undecided, uniqueMcpServerName };
|
|
35288
36319
|
//# sourceMappingURL=index.js.map
|
|
35289
36320
|
//# sourceMappingURL=index.js.map
|