cascade-ai 0.79.0 → 0.81.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 +310 -11
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +310 -11
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +1758 -79
- package/dist/index.cjs +1760 -81
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +832 -14
- package/dist/index.d.ts +832 -14
- package/dist/index.js +1757 -82
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -1849,7 +1849,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
|
|
|
1849
1849
|
var init_constants = __esm({
|
|
1850
1850
|
"src/constants.ts"() {
|
|
1851
1851
|
init_pricing();
|
|
1852
|
-
CASCADE_VERSION = "0.
|
|
1852
|
+
CASCADE_VERSION = "0.81.0";
|
|
1853
1853
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
1854
1854
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
1855
1855
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -31444,8 +31444,95 @@ function requiredFieldFor(kind, input) {
|
|
|
31444
31444
|
}
|
|
31445
31445
|
}
|
|
31446
31446
|
|
|
31447
|
+
// src/tools/ask-user.ts
|
|
31448
|
+
var MAX_QUESTIONS = 5;
|
|
31449
|
+
var MAX_OPTIONS = 6;
|
|
31450
|
+
var AskUserTool = class extends BaseTool {
|
|
31451
|
+
name = "ask_user";
|
|
31452
|
+
description = "Ask the person a short questionnaire when their request is genuinely ambiguous, instead of guessing. Use it when two readings of the request would produce materially different work \u2014 which account, which date range, whether to include drafts \u2014 and NOT for anything you can settle yourself or from the conversation. Ask everything you need in ONE call: several questions together read as a form, the same questions one per turn read as an interrogation. You may get no answer (nobody is watching, or they did not reply); if so, proceed on your best reading and say which assumption you made.";
|
|
31453
|
+
inputSchema = {
|
|
31454
|
+
type: "object",
|
|
31455
|
+
properties: {
|
|
31456
|
+
questions: {
|
|
31457
|
+
type: "array",
|
|
31458
|
+
minItems: 1,
|
|
31459
|
+
maxItems: MAX_QUESTIONS,
|
|
31460
|
+
description: `The questions to ask together (at most ${MAX_QUESTIONS}).`,
|
|
31461
|
+
items: {
|
|
31462
|
+
type: "object",
|
|
31463
|
+
properties: {
|
|
31464
|
+
prompt: { type: "string", description: "The question, as you would say it to them." },
|
|
31465
|
+
kind: {
|
|
31466
|
+
type: "string",
|
|
31467
|
+
enum: ["choice", "multi", "text"],
|
|
31468
|
+
description: "choice = pick one, multi = pick any that apply, text = free-form answer."
|
|
31469
|
+
},
|
|
31470
|
+
options: {
|
|
31471
|
+
type: "array",
|
|
31472
|
+
items: { type: "string" },
|
|
31473
|
+
description: `The answers to offer, for choice and multi (at most ${MAX_OPTIONS}). Omit for text.`
|
|
31474
|
+
}
|
|
31475
|
+
},
|
|
31476
|
+
required: ["prompt", "kind"]
|
|
31477
|
+
}
|
|
31478
|
+
}
|
|
31479
|
+
},
|
|
31480
|
+
required: ["questions"]
|
|
31481
|
+
};
|
|
31482
|
+
ask;
|
|
31483
|
+
constructor(ask) {
|
|
31484
|
+
super();
|
|
31485
|
+
this.ask = ask;
|
|
31486
|
+
}
|
|
31487
|
+
// Asking a question changes nothing, so it needs no approval gate: a prompt
|
|
31488
|
+
// asking permission to show a prompt is a tax on the behaviour we want.
|
|
31489
|
+
isDangerous() {
|
|
31490
|
+
return false;
|
|
31491
|
+
}
|
|
31492
|
+
async execute(input, options) {
|
|
31493
|
+
const raw = Array.isArray(input["questions"]) ? input["questions"] : [];
|
|
31494
|
+
const questions = [];
|
|
31495
|
+
for (const [i, q] of raw.slice(0, MAX_QUESTIONS).entries()) {
|
|
31496
|
+
const item = q ?? {};
|
|
31497
|
+
const prompt = typeof item["prompt"] === "string" ? item["prompt"].trim() : "";
|
|
31498
|
+
if (!prompt) continue;
|
|
31499
|
+
const kind = item["kind"] === "multi" || item["kind"] === "text" ? item["kind"] : "choice";
|
|
31500
|
+
const options_ = Array.isArray(item["options"]) ? item["options"].filter((o) => typeof o === "string" && o.trim() !== "").slice(0, MAX_OPTIONS) : [];
|
|
31501
|
+
const resolved = kind !== "text" && options_.length < 2 ? "text" : kind;
|
|
31502
|
+
questions.push({
|
|
31503
|
+
// Positional, and assigned HERE. Letting the model supply ids invites
|
|
31504
|
+
// two questions sharing one, and the second answer would overwrite the
|
|
31505
|
+
// first with nothing to say it had.
|
|
31506
|
+
id: `q${i + 1}`,
|
|
31507
|
+
prompt,
|
|
31508
|
+
kind: resolved,
|
|
31509
|
+
...resolved === "text" ? {} : { options: options_ }
|
|
31510
|
+
});
|
|
31511
|
+
}
|
|
31512
|
+
if (questions.length === 0) {
|
|
31513
|
+
return "Error: ask_user needs at least one question with a prompt.";
|
|
31514
|
+
}
|
|
31515
|
+
const result = await this.ask(questions, options.signal);
|
|
31516
|
+
if (result.outcome !== "answered" || result.answers.length === 0) {
|
|
31517
|
+
const why = result.outcome === "no-listener" ? "There is nobody watching this run to answer" : result.outcome === "aborted" ? "The run was stopped while the question was open" : result.outcome === "answered" ? "They saw the questions and chose not to answer" : "They did not answer in time";
|
|
31518
|
+
return `${why}. Proceed on your best reading of the request, and say plainly which assumption you made.`;
|
|
31519
|
+
}
|
|
31520
|
+
const byId = new Map(result.answers.map((a) => [a.id, a.value]));
|
|
31521
|
+
const lines = questions.map((q) => {
|
|
31522
|
+
const value = byId.get(q.id);
|
|
31523
|
+
const said = Array.isArray(value) ? value.join(", ") : value;
|
|
31524
|
+
return `${q.prompt}
|
|
31525
|
+
\u2192 ${said && said.length > 0 ? said : "(no answer given)"}`;
|
|
31526
|
+
});
|
|
31527
|
+
return `They answered:
|
|
31528
|
+
|
|
31529
|
+
${lines.join("\n\n")}`;
|
|
31530
|
+
}
|
|
31531
|
+
};
|
|
31532
|
+
|
|
31447
31533
|
// src/core/cascade.ts
|
|
31448
31534
|
var ESCALATION_DECISION_TIMEOUT_MS = 5 * 6e4;
|
|
31535
|
+
var CLARIFICATION_TIMEOUT_MS = 2 * 6e4;
|
|
31449
31536
|
function buildContextualPrompt(prompt, history = []) {
|
|
31450
31537
|
const recent = history.slice(-6);
|
|
31451
31538
|
if (recent.length === 0) return prompt;
|
|
@@ -31724,6 +31811,112 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
31724
31811
|
// timeout. Same class of bug as the twelve-failover dead-model race: a wave
|
|
31725
31812
|
// is concurrent, so per-run singletons are never safe inside it.
|
|
31726
31813
|
pendingEscalations = /* @__PURE__ */ new Map();
|
|
31814
|
+
/**
|
|
31815
|
+
* Questionnaires waiting on a person, keyed by request id.
|
|
31816
|
+
*
|
|
31817
|
+
* Exactly the shape `pendingEscalations` uses, and for the same reason: the
|
|
31818
|
+
* answer, the timeout and the abort all race, and one settle path guarded by
|
|
31819
|
+
* the map delete is what makes the first of them win.
|
|
31820
|
+
*/
|
|
31821
|
+
pendingClarifications = /* @__PURE__ */ new Map();
|
|
31822
|
+
/**
|
|
31823
|
+
* Put a gate's opening question to its listeners, and NOTICE when one fails.
|
|
31824
|
+
*
|
|
31825
|
+
* `emit` cannot do this job. It calls listeners synchronously and discards
|
|
31826
|
+
* what they return, so an `async` listener — the ordinary shape for anything
|
|
31827
|
+
* that has to reach a socket or a UI — does not fail in a way `emit` reports:
|
|
31828
|
+
* it returns a REJECTED PROMISE that nobody is holding. The `try` around the
|
|
31829
|
+
* emit sees nothing, the gate is never told delivery failed, and it parks for
|
|
31830
|
+
* its full timeout while Node raises an unhandled rejection that can take the
|
|
31831
|
+
* process down.
|
|
31832
|
+
*
|
|
31833
|
+
* `onFailed` is called for either shape, and it is safe to call more than
|
|
31834
|
+
* once: both gates route it to `settle`, which is guarded by a map delete, so
|
|
31835
|
+
* the first failure wins and the rest are no-ops.
|
|
31836
|
+
*
|
|
31837
|
+
* PER LISTENER, not around the loop. `emit` stops dispatching at the first
|
|
31838
|
+
* throw, so one broken host used to cost a working one its notification —
|
|
31839
|
+
* the same reason `announceLiveView` delivers its two listeners separately.
|
|
31840
|
+
* A synchronous failure settles the gate once the whole snapshot has been
|
|
31841
|
+
* delivered; an asynchronous one settles it when the rejection arrives, which
|
|
31842
|
+
* is still far better than the window-long park it replaces.
|
|
31843
|
+
*
|
|
31844
|
+
* SETTLED AFTER THE LOOP, not inside it, and that is not tidiness. `onFailed`
|
|
31845
|
+
* settles the gate, and settling emits the gate's CLOSURE — so failing
|
|
31846
|
+
* mid-dispatch announced "this request is over" and then went on handing the
|
|
31847
|
+
* OPENING event to the listeners after it. A host later in the list would
|
|
31848
|
+
* draw a form whose only closure notification had already gone past, and it
|
|
31849
|
+
* would stand there forever. Deliver to everyone, then decide.
|
|
31850
|
+
*
|
|
31851
|
+
* `rawListeners`, NOT `listeners`. Bypassing `emit` means taking on what
|
|
31852
|
+
* `emit` was doing, and `once` is the part that bites: `listeners()` hands
|
|
31853
|
+
* back the UNWRAPPED callback, so calling it never runs the wrapper that
|
|
31854
|
+
* deregisters it. A host that asked to be told once would be told every
|
|
31855
|
+
* time — and told through a transport it had set up for one gate only.
|
|
31856
|
+
* `rawListeners()` returns the wrapper, which removes the registration and
|
|
31857
|
+
* passes the underlying listener's return value straight through, so the
|
|
31858
|
+
* promise below is still the one being observed. Verified against Node.
|
|
31859
|
+
*
|
|
31860
|
+
* `.call(this, …)`, which is the third thing `emit` was doing. EventEmitter
|
|
31861
|
+
* invokes listeners with the emitter as the receiver, so
|
|
31862
|
+
* `function () { this.resolveClarification(...) }` is a legitimate host — and
|
|
31863
|
+
* calling it bare gave it `this === undefined`, which throws, which this
|
|
31864
|
+
* would then have read as the host failing to receive the question and
|
|
31865
|
+
* settled the gate unanswered. A working listener turned into a delivery
|
|
31866
|
+
* failure by the thing delivering it.
|
|
31867
|
+
*/
|
|
31868
|
+
deliverGate(event, payload, onFailed) {
|
|
31869
|
+
let delivered = false;
|
|
31870
|
+
let threw = false;
|
|
31871
|
+
const pending = [];
|
|
31872
|
+
for (const listener of this.rawListeners(event)) {
|
|
31873
|
+
try {
|
|
31874
|
+
const out = listener.call(this, payload);
|
|
31875
|
+
if (out && typeof out.then === "function") {
|
|
31876
|
+
pending.push(Promise.resolve(out).then(() => true, () => false));
|
|
31877
|
+
} else {
|
|
31878
|
+
delivered = true;
|
|
31879
|
+
}
|
|
31880
|
+
} catch {
|
|
31881
|
+
threw = true;
|
|
31882
|
+
}
|
|
31883
|
+
}
|
|
31884
|
+
if (delivered) return;
|
|
31885
|
+
if (pending.length === 0) {
|
|
31886
|
+
if (threw) onFailed();
|
|
31887
|
+
return;
|
|
31888
|
+
}
|
|
31889
|
+
void Promise.all(pending).then((results) => {
|
|
31890
|
+
if (!results.some(Boolean)) onFailed();
|
|
31891
|
+
});
|
|
31892
|
+
}
|
|
31893
|
+
/**
|
|
31894
|
+
* Tell listeners something that is over, and let none of them make it worse.
|
|
31895
|
+
*
|
|
31896
|
+
* The closing half of the gate family — `clarification:closed`,
|
|
31897
|
+
* `clarification:timeout`, `escalation:closed`, `escalation:timeout`. Each
|
|
31898
|
+
* was already wrapped in a `try` for the synchronous case, and each had the
|
|
31899
|
+
* same hole `deliverGate` had before last round: `emit` discards what a
|
|
31900
|
+
* listener returns, so an `async` host fails by returning a rejected promise
|
|
31901
|
+
* nobody is holding and Node raises an unhandled rejection.
|
|
31902
|
+
*
|
|
31903
|
+
* There is nothing to settle here — the gate is already closed, which is the
|
|
31904
|
+
* whole message — so unlike `deliverGate` this reports to no one. It exists
|
|
31905
|
+
* so that the four informational emits cannot take the process down, and so
|
|
31906
|
+
* the rule is written once rather than four times.
|
|
31907
|
+
*/
|
|
31908
|
+
announceGateClosed(event, payload) {
|
|
31909
|
+
for (const listener of this.rawListeners(event)) {
|
|
31910
|
+
try {
|
|
31911
|
+
const out = listener.call(this, payload);
|
|
31912
|
+
if (out && typeof out.then === "function") {
|
|
31913
|
+
void Promise.resolve(out).catch(() => {
|
|
31914
|
+
});
|
|
31915
|
+
}
|
|
31916
|
+
} catch {
|
|
31917
|
+
}
|
|
31918
|
+
}
|
|
31919
|
+
}
|
|
31727
31920
|
async requestEscalationDecision(ctx, taskId, signal) {
|
|
31728
31921
|
if (this.config.autonomy === "auto") return { action: "skip", automatic: true };
|
|
31729
31922
|
if (this.listenerCount("escalation:decision-required") === 0) return { action: "skip", automatic: true };
|
|
@@ -31736,23 +31929,28 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
31736
31929
|
if (timeout) clearTimeout(timeout);
|
|
31737
31930
|
signal?.removeEventListener("abort", onAbort);
|
|
31738
31931
|
resolve2(decision);
|
|
31932
|
+
this.announceGateClosed("escalation:closed", { taskId, requestId, sectionId: ctx.sectionId });
|
|
31739
31933
|
};
|
|
31740
31934
|
const onAbort = () => settle({ action: "skip", automatic: true });
|
|
31741
31935
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
31742
31936
|
timeout = setTimeout(() => {
|
|
31743
|
-
this.emit("escalation:timeout", { taskId, requestId, sectionId: ctx.sectionId });
|
|
31744
31937
|
settle({ action: "timeout" });
|
|
31938
|
+
this.announceGateClosed("escalation:timeout", { taskId, requestId, sectionId: ctx.sectionId });
|
|
31745
31939
|
}, ESCALATION_DECISION_TIMEOUT_MS);
|
|
31746
31940
|
this.pendingEscalations.set(requestId, settle);
|
|
31747
|
-
this.
|
|
31748
|
-
|
|
31749
|
-
|
|
31750
|
-
|
|
31751
|
-
|
|
31752
|
-
|
|
31753
|
-
|
|
31754
|
-
|
|
31755
|
-
|
|
31941
|
+
this.deliverGate(
|
|
31942
|
+
"escalation:decision-required",
|
|
31943
|
+
{
|
|
31944
|
+
taskId,
|
|
31945
|
+
requestId,
|
|
31946
|
+
sectionId: ctx.sectionId,
|
|
31947
|
+
sectionTitle: ctx.sectionTitle,
|
|
31948
|
+
issues: ctx.issues,
|
|
31949
|
+
summary: ctx.summary,
|
|
31950
|
+
timeoutMs: ESCALATION_DECISION_TIMEOUT_MS
|
|
31951
|
+
},
|
|
31952
|
+
() => settle({ action: "skip", automatic: true })
|
|
31953
|
+
);
|
|
31756
31954
|
});
|
|
31757
31955
|
}
|
|
31758
31956
|
/**
|
|
@@ -31789,6 +31987,73 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
31789
31987
|
releasePendingEscalations(action = "skip") {
|
|
31790
31988
|
for (const settle of Array.from(this.pendingEscalations.values())) settle({ action, automatic: true });
|
|
31791
31989
|
}
|
|
31990
|
+
// ── Clarification: asking instead of assuming ───────────────────────
|
|
31991
|
+
// The same gate shape as the escalation above, and for the same reasons. The
|
|
31992
|
+
// difference that matters: an unanswered escalation SKIPS a section, while an
|
|
31993
|
+
// unanswered question just means the model proceeds on its own reading — so
|
|
31994
|
+
// every non-answer here is benign, and none of them may park the run.
|
|
31995
|
+
/**
|
|
31996
|
+
* Put a questionnaire to whoever is watching, if anybody is.
|
|
31997
|
+
*
|
|
31998
|
+
* Returns `no-listener` rather than waiting when there is nothing attached to
|
|
31999
|
+
* answer — a scheduled run, an API caller with no socket, an autonomous run
|
|
32000
|
+
* that has already said it wants no prompts. That check is the whole reason
|
|
32001
|
+
* this can be offered to the model at all: a tool that might hang a headless
|
|
32002
|
+
* run forever is a tool that cannot be registered.
|
|
32003
|
+
*/
|
|
32004
|
+
async askUser(questions, signal) {
|
|
32005
|
+
const none = (outcome) => ({ outcome, answers: [] });
|
|
32006
|
+
if (this.unattended) return none("no-listener");
|
|
32007
|
+
if (this.config.autonomy === "auto") return none("no-listener");
|
|
32008
|
+
if (this.listenerCount("clarification:required") === 0) return none("no-listener");
|
|
32009
|
+
if (signal?.aborted) return none("aborted");
|
|
32010
|
+
const requestId = crypto.randomUUID();
|
|
32011
|
+
return await new Promise((resolve2) => {
|
|
32012
|
+
let timeout;
|
|
32013
|
+
const settle = (result) => {
|
|
32014
|
+
if (!this.pendingClarifications.delete(requestId)) return;
|
|
32015
|
+
if (timeout) clearTimeout(timeout);
|
|
32016
|
+
signal?.removeEventListener("abort", onAbort);
|
|
32017
|
+
resolve2(result);
|
|
32018
|
+
this.announceGateClosed("clarification:closed", { requestId });
|
|
32019
|
+
};
|
|
32020
|
+
const onAbort = () => settle(none("aborted"));
|
|
32021
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
32022
|
+
timeout = setTimeout(() => {
|
|
32023
|
+
settle(none("timeout"));
|
|
32024
|
+
this.announceGateClosed("clarification:timeout", { requestId });
|
|
32025
|
+
}, CLARIFICATION_TIMEOUT_MS);
|
|
32026
|
+
this.pendingClarifications.set(requestId, settle);
|
|
32027
|
+
this.deliverGate(
|
|
32028
|
+
"clarification:required",
|
|
32029
|
+
{ requestId, questions, timeoutMs: CLARIFICATION_TIMEOUT_MS },
|
|
32030
|
+
() => settle(none("no-listener"))
|
|
32031
|
+
);
|
|
32032
|
+
});
|
|
32033
|
+
}
|
|
32034
|
+
/**
|
|
32035
|
+
* Answer a questionnaire from a REPL / dashboard / cloud listener.
|
|
32036
|
+
*
|
|
32037
|
+
* `requestId` comes back on the `clarification:required` event and is what
|
|
32038
|
+
* makes an answer land on the question that asked. Optional for a host with
|
|
32039
|
+
* only one in flight, matching `resolveEscalation`: without it the oldest
|
|
32040
|
+
* outstanding request is answered, which is correct whenever there is one.
|
|
32041
|
+
*/
|
|
32042
|
+
resolveClarification(answers, requestId) {
|
|
32043
|
+
const result = { outcome: "answered", answers };
|
|
32044
|
+
if (requestId) {
|
|
32045
|
+
this.pendingClarifications.get(requestId)?.(result);
|
|
32046
|
+
return;
|
|
32047
|
+
}
|
|
32048
|
+
const oldest = this.pendingClarifications.keys().next();
|
|
32049
|
+
if (!oldest.done) this.pendingClarifications.get(oldest.value)?.(result);
|
|
32050
|
+
}
|
|
32051
|
+
/** Release every parked questionnaire — run teardown, so none outlive the run. */
|
|
32052
|
+
releasePendingClarifications() {
|
|
32053
|
+
for (const settle of Array.from(this.pendingClarifications.values())) {
|
|
32054
|
+
settle({ outcome: "aborted", answers: [] });
|
|
32055
|
+
}
|
|
32056
|
+
}
|
|
31792
32057
|
/**
|
|
31793
32058
|
* Resolve a pending boardroom plan approval from a REPL / dashboard listener.
|
|
31794
32059
|
* An optional `note` re-plans and re-asks; an optional `editedPlan` is applied
|
|
@@ -31813,12 +32078,14 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
31813
32078
|
const timeout = setTimeout(() => {
|
|
31814
32079
|
if (this.pendingContextApproval) {
|
|
31815
32080
|
this.pendingContextApproval = void 0;
|
|
32081
|
+
this.announceGateClosed("context:approval-closed", {});
|
|
31816
32082
|
resolve2(true);
|
|
31817
32083
|
}
|
|
31818
32084
|
}, 12e4);
|
|
31819
32085
|
this.pendingContextApproval = (approved) => {
|
|
31820
32086
|
clearTimeout(timeout);
|
|
31821
32087
|
this.pendingContextApproval = void 0;
|
|
32088
|
+
this.announceGateClosed("context:approval-closed", {});
|
|
31822
32089
|
resolve2(approved);
|
|
31823
32090
|
};
|
|
31824
32091
|
this.emit("context:approval-required", info);
|
|
@@ -32009,6 +32276,34 @@ ${input}` }],
|
|
|
32009
32276
|
if ((this.config.tools?.disabledTools ?? []).includes("browser_control")) return;
|
|
32010
32277
|
this.toolRegistry.register(new BrowserControlTool(controller, release));
|
|
32011
32278
|
}
|
|
32279
|
+
/**
|
|
32280
|
+
* Declare that this host can put a questionnaire to a person and answer it.
|
|
32281
|
+
*
|
|
32282
|
+
* Explicit, and NOT done at construction, because registering a tool is a
|
|
32283
|
+
* promise the host has to keep. A host that registers `ask_user` without
|
|
32284
|
+
* listening for `clarification:required` gives the model a tool that always
|
|
32285
|
+
* answers "there is nobody watching this run" — while a person sits at the
|
|
32286
|
+
* terminal looking at it. That is worse than not offering the tool at all:
|
|
32287
|
+
* it costs a round trip and then tells the model something false.
|
|
32288
|
+
*
|
|
32289
|
+
* Only `cloud/server` bridges the protocol today. The CLI REPL and the
|
|
32290
|
+
* dashboard do not, so they do not call this, and their models are not shown
|
|
32291
|
+
* a capability nobody can serve. Wiring either of them is a matter of
|
|
32292
|
+
* listening for the event and calling `resolveClarification`, then calling
|
|
32293
|
+
* this.
|
|
32294
|
+
*
|
|
32295
|
+
* Still gated on the same conditions as the call itself, and `askUser`
|
|
32296
|
+
* re-checks them: `setUnattended` can land after this, so the tool can be
|
|
32297
|
+
* registered for a run that later turns out to have nobody on it. Then it
|
|
32298
|
+
* answers `no-listener` and the model proceeds on its own reading, which is
|
|
32299
|
+
* the same defence-in-depth `BrowserControlTool.revoke` provides.
|
|
32300
|
+
*/
|
|
32301
|
+
enableClarification() {
|
|
32302
|
+
if (this.unattended) return;
|
|
32303
|
+
if (this.config.autonomy === "auto") return;
|
|
32304
|
+
if ((this.config.tools?.disabledTools ?? []).includes("ask_user")) return;
|
|
32305
|
+
this.toolRegistry.register(new AskUserTool((questions, signal) => this.askUser(questions, signal)));
|
|
32306
|
+
}
|
|
32012
32307
|
registerMediaTools(workspacePath) {
|
|
32013
32308
|
const denied = new Set(this.config.tools?.disabledTools ?? []);
|
|
32014
32309
|
const permitted = (name) => !denied.has(name);
|
|
@@ -32954,6 +33249,10 @@ ${finalOutput}` : ""
|
|
|
32954
33249
|
this.releasePendingEscalations();
|
|
32955
33250
|
} catch {
|
|
32956
33251
|
}
|
|
33252
|
+
try {
|
|
33253
|
+
this.releasePendingClarifications();
|
|
33254
|
+
} catch {
|
|
33255
|
+
}
|
|
32957
33256
|
this.router.restoreTierModels();
|
|
32958
33257
|
this.router.setRunSignal(void 0);
|
|
32959
33258
|
if (this.taskAnalyzer) {
|