@quanta-intellect/vessel-browser 0.1.144 → 0.1.146
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/out/main/index.js
CHANGED
|
@@ -682,15 +682,6 @@ function loadTrustedAppURL(wc, url) {
|
|
|
682
682
|
}
|
|
683
683
|
return wc.loadURL(parsed.toString());
|
|
684
684
|
}
|
|
685
|
-
const urlSafety = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
686
|
-
__proto__: null,
|
|
687
|
-
assertPermittedNavigationURL,
|
|
688
|
-
assertSafeURL,
|
|
689
|
-
isSafeNavigationURL,
|
|
690
|
-
loadInternalDataURL,
|
|
691
|
-
loadPermittedNavigationURL,
|
|
692
|
-
loadTrustedAppURL
|
|
693
|
-
}, Symbol.toStringTag, { value: "Module" }));
|
|
694
685
|
const MAX_CUSTOM_HISTORY = 50;
|
|
695
686
|
const READER_MODE_DATA_URL_PREFIX = "data:text/html;charset=utf-8,";
|
|
696
687
|
const logger$B = createLogger("Tab");
|
|
@@ -1467,7 +1458,12 @@ function loadJsonFile({
|
|
|
1467
1458
|
const decoded = decodeStoredData(raw, secure);
|
|
1468
1459
|
return parse(JSON.parse(decoded));
|
|
1469
1460
|
} catch (err) {
|
|
1470
|
-
|
|
1461
|
+
const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
1462
|
+
if (isMissingFile) {
|
|
1463
|
+
logger$A.info(`Persistence file not found; using fallback defaults: ${filePath2}`);
|
|
1464
|
+
} else {
|
|
1465
|
+
logger$A.warn(`Failed to load ${filePath2}, using fallback:`, err);
|
|
1466
|
+
}
|
|
1471
1467
|
return fallback;
|
|
1472
1468
|
}
|
|
1473
1469
|
}
|
|
@@ -3867,7 +3863,14 @@ const AIChannels = {
|
|
|
3867
3863
|
AGENT_CHECKPOINT_UPDATE_NOTE: "agent:checkpoint-update-note",
|
|
3868
3864
|
AGENT_UNDO_LAST_ACTION: "agent:undo-last-action",
|
|
3869
3865
|
AGENT_SESSION_CAPTURE: "agent:session-capture",
|
|
3870
|
-
AGENT_SESSION_RESTORE: "agent:session-restore"
|
|
3866
|
+
AGENT_SESSION_RESTORE: "agent:session-restore",
|
|
3867
|
+
AGENT_TASK_START: "agent:task-start",
|
|
3868
|
+
AGENT_TASK_UPDATE: "agent:task-update",
|
|
3869
|
+
AGENT_TASK_NOTE: "agent:task-note",
|
|
3870
|
+
AGENT_TASK_BLOCKER: "agent:task-blocker",
|
|
3871
|
+
AGENT_TASK_RESOLVE: "agent:task-resolve",
|
|
3872
|
+
AGENT_TASK_ABANDON: "agent:task-abandon",
|
|
3873
|
+
AGENT_TASK_CLEAR: "agent:task-clear"
|
|
3871
3874
|
};
|
|
3872
3875
|
const AutofillChannels = {
|
|
3873
3876
|
AUTOFILL_LIST: "autofill:list",
|
|
@@ -7794,6 +7797,96 @@ function isClickReadLoop(names) {
|
|
|
7794
7797
|
}
|
|
7795
7798
|
return clickReadPairs >= 2;
|
|
7796
7799
|
}
|
|
7800
|
+
const CLICK_READ_LOOP_SUPPRESS_THRESHOLD = 3;
|
|
7801
|
+
function classifyClickFailure(output) {
|
|
7802
|
+
if (/Error\[hidden\]/i.test(output)) return "hidden";
|
|
7803
|
+
if (/Error\[stale-index\]/i.test(output)) return "stale";
|
|
7804
|
+
if (/^\s*Error:/i.test(output)) return "other";
|
|
7805
|
+
return null;
|
|
7806
|
+
}
|
|
7807
|
+
function buildClickReadLoopIntervention(strikes, lastClickFailureKind) {
|
|
7808
|
+
if (strikes <= 0) return null;
|
|
7809
|
+
if (strikes >= CLICK_READ_LOOP_SUPPRESS_THRESHOLD) {
|
|
7810
|
+
const lines = [
|
|
7811
|
+
`Error: Suppressed repeated click — you have alternated click and read_page ${strikes} times without making progress and the clicks are not landing.`,
|
|
7812
|
+
`Stop calling click. Instead do one of: scroll (scroll or scroll_to_element) to load more of the page then read_page to refresh, inspect_element on a specific indexed result, or answer from the results already visible in the conversation.`
|
|
7813
|
+
];
|
|
7814
|
+
if (lastClickFailureKind === "hidden") {
|
|
7815
|
+
lines.push(
|
|
7816
|
+
`The last click target was hidden / not laid out — scrolling toward it first usually reveals it.`
|
|
7817
|
+
);
|
|
7818
|
+
} else if (lastClickFailureKind === "stale") {
|
|
7819
|
+
lines.push(
|
|
7820
|
+
`The last click target was stale — refresh page state with read_page and choose a currently listed target before clicking again.`
|
|
7821
|
+
);
|
|
7822
|
+
}
|
|
7823
|
+
return { kind: "suppress", message: lines.join("\n") };
|
|
7824
|
+
}
|
|
7825
|
+
if (strikes >= 2) {
|
|
7826
|
+
const lines = [
|
|
7827
|
+
`[System] You are alternating between click and read_page without advancing the task, and the last click did not complete.`,
|
|
7828
|
+
`The click result already includes a page snapshot, so do not read_page after every click.`
|
|
7829
|
+
];
|
|
7830
|
+
if (lastClickFailureKind === "hidden") {
|
|
7831
|
+
lines.push(
|
|
7832
|
+
`The click failed on a hidden element — call scroll (scroll or scroll_to_element) to reveal it, then read_page to refresh visible elements, before clicking again.`
|
|
7833
|
+
);
|
|
7834
|
+
} else if (lastClickFailureKind === "stale") {
|
|
7835
|
+
lines.push(
|
|
7836
|
+
`The click failed on a stale element index — call read_page to refresh current indexes before clicking again.`
|
|
7837
|
+
);
|
|
7838
|
+
} else {
|
|
7839
|
+
lines.push(
|
|
7840
|
+
`If you need detail on a specific element, use inspect_element. Otherwise continue the original task directly.`
|
|
7841
|
+
);
|
|
7842
|
+
}
|
|
7843
|
+
return { kind: "nudge", message: lines.join("\n") };
|
|
7844
|
+
}
|
|
7845
|
+
return {
|
|
7846
|
+
kind: "nudge",
|
|
7847
|
+
message: `[System] You are alternating between click and read_page without advancing the task. The click result already includes a page snapshot when it navigates, so do not read_page after every click. If you need detail on a specific element, use inspect_element. Otherwise continue the original task directly.`
|
|
7848
|
+
};
|
|
7849
|
+
}
|
|
7850
|
+
class ClickReadLoopGuard {
|
|
7851
|
+
recentToolNames = [];
|
|
7852
|
+
strikes = 0;
|
|
7853
|
+
lastClickFailureKind = null;
|
|
7854
|
+
beforeTool(toolName) {
|
|
7855
|
+
if (toolName === "click" && this.strikes >= CLICK_READ_LOOP_SUPPRESS_THRESHOLD && isClickReadLoop(this.recentToolNames)) {
|
|
7856
|
+
return buildClickReadLoopIntervention(
|
|
7857
|
+
this.strikes,
|
|
7858
|
+
this.lastClickFailureKind
|
|
7859
|
+
);
|
|
7860
|
+
}
|
|
7861
|
+
return null;
|
|
7862
|
+
}
|
|
7863
|
+
afterToolResult(toolName, output, succeeded) {
|
|
7864
|
+
if (toolName === "click") {
|
|
7865
|
+
this.lastClickFailureKind = succeeded ? null : classifyClickFailure(output);
|
|
7866
|
+
}
|
|
7867
|
+
this.recentToolNames.push(toolName);
|
|
7868
|
+
if (this.recentToolNames.length > 8) this.recentToolNames.shift();
|
|
7869
|
+
if (toolName === "click" && succeeded) {
|
|
7870
|
+
this.strikes = 0;
|
|
7871
|
+
return null;
|
|
7872
|
+
}
|
|
7873
|
+
if (toolName !== "click" && toolName !== "read_page") {
|
|
7874
|
+
this.strikes = 0;
|
|
7875
|
+
return null;
|
|
7876
|
+
}
|
|
7877
|
+
if (isClickReadLoop(this.recentToolNames) && this.lastClickFailureKind) {
|
|
7878
|
+
this.strikes += 1;
|
|
7879
|
+
if (this.strikes >= CLICK_READ_LOOP_SUPPRESS_THRESHOLD) {
|
|
7880
|
+
return null;
|
|
7881
|
+
}
|
|
7882
|
+
return buildClickReadLoopIntervention(
|
|
7883
|
+
this.strikes,
|
|
7884
|
+
this.lastClickFailureKind
|
|
7885
|
+
);
|
|
7886
|
+
}
|
|
7887
|
+
return null;
|
|
7888
|
+
}
|
|
7889
|
+
}
|
|
7797
7890
|
const TERMINAL_TOOL_RESULT = "__VESSEL_TERMINAL_TOOL_RESULT__";
|
|
7798
7891
|
const logger$w = createLogger("PromptCache");
|
|
7799
7892
|
function shortHash(value) {
|
|
@@ -7954,8 +8047,7 @@ class AnthropicProvider {
|
|
|
7954
8047
|
try {
|
|
7955
8048
|
const maxIterations = getEffectiveMaxIterations();
|
|
7956
8049
|
let iterationsUsed = 0;
|
|
7957
|
-
const
|
|
7958
|
-
let clickReadLoopNudged = false;
|
|
8050
|
+
const clickReadLoopGuard = new ClickReadLoopGuard();
|
|
7959
8051
|
for (let i = 0; i < maxIterations; i++) {
|
|
7960
8052
|
iterationsUsed = i + 1;
|
|
7961
8053
|
const stream = this.client.messages.stream(
|
|
@@ -8062,6 +8154,7 @@ class AnthropicProvider {
|
|
|
8062
8154
|
break;
|
|
8063
8155
|
}
|
|
8064
8156
|
const toolResults = [];
|
|
8157
|
+
const loopNudges = [];
|
|
8065
8158
|
for (const tb of toolUseBlocks) {
|
|
8066
8159
|
if (tb._malformedArgs !== void 0) {
|
|
8067
8160
|
onChunk(`
|
|
@@ -8075,6 +8168,19 @@ class AnthropicProvider {
|
|
|
8075
8168
|
});
|
|
8076
8169
|
continue;
|
|
8077
8170
|
}
|
|
8171
|
+
const clickLoopPreflight = clickReadLoopGuard.beforeTool(tb.name);
|
|
8172
|
+
if (clickLoopPreflight?.kind === "suppress") {
|
|
8173
|
+
onChunk(`
|
|
8174
|
+
<<tool:click:↻ loop suppressed>>
|
|
8175
|
+
`);
|
|
8176
|
+
toolResults.push({
|
|
8177
|
+
type: "tool_result",
|
|
8178
|
+
tool_use_id: tb.id,
|
|
8179
|
+
content: clickLoopPreflight.message,
|
|
8180
|
+
is_error: true
|
|
8181
|
+
});
|
|
8182
|
+
continue;
|
|
8183
|
+
}
|
|
8078
8184
|
const argSummary = [tb.input.url, tb.input.query, tb.input.text, tb.input.direction].map((v) => typeof v === "string" ? v : "").find((v) => v.length > 0) ?? "";
|
|
8079
8185
|
onChunk(`
|
|
8080
8186
|
<<tool:${tb.name}${argSummary ? ":" + argSummary : ""}>>
|
|
@@ -8089,6 +8195,7 @@ class AnthropicProvider {
|
|
|
8089
8195
|
if (result === TERMINAL_TOOL_RESULT) {
|
|
8090
8196
|
return;
|
|
8091
8197
|
}
|
|
8198
|
+
const toolSucceeded = !/^Error:/i.test(result.trim());
|
|
8092
8199
|
let parsedRich = null;
|
|
8093
8200
|
try {
|
|
8094
8201
|
const parsed = JSON.parse(result);
|
|
@@ -8120,16 +8227,18 @@ class AnthropicProvider {
|
|
|
8120
8227
|
content: result
|
|
8121
8228
|
});
|
|
8122
8229
|
}
|
|
8123
|
-
|
|
8124
|
-
|
|
8230
|
+
const clickLoopIntervention = clickReadLoopGuard.afterToolResult(
|
|
8231
|
+
tb.name,
|
|
8232
|
+
result,
|
|
8233
|
+
toolSucceeded
|
|
8234
|
+
);
|
|
8235
|
+
if (clickLoopIntervention?.kind === "nudge") {
|
|
8236
|
+
loopNudges.push(clickLoopIntervention.message);
|
|
8237
|
+
}
|
|
8125
8238
|
}
|
|
8126
8239
|
messages.push({ role: "user", content: toolResults });
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
messages.push({
|
|
8130
|
-
role: "user",
|
|
8131
|
-
content: `You are alternating between click and read_page without advancing the task. The click result already includes a page snapshot when it navigates — you do not need read_page after every click. If you need detail on a specific element, use inspect_element instead. If you have enough context, proceed with the next action directly.`
|
|
8132
|
-
});
|
|
8240
|
+
for (const nudge of loopNudges) {
|
|
8241
|
+
messages.push({ role: "user", content: nudge });
|
|
8133
8242
|
}
|
|
8134
8243
|
}
|
|
8135
8244
|
if (iterationsUsed >= maxIterations) {
|
|
@@ -8683,6 +8792,106 @@ function recoverTextEncodedToolCalls(text, availableToolNames) {
|
|
|
8683
8792
|
}
|
|
8684
8793
|
return recovered;
|
|
8685
8794
|
}
|
|
8795
|
+
function findInlineToolMarkerBodies(text) {
|
|
8796
|
+
const bodies = [];
|
|
8797
|
+
const lowerText = text.toLowerCase();
|
|
8798
|
+
let searchIndex = 0;
|
|
8799
|
+
while (searchIndex < text.length) {
|
|
8800
|
+
const start = lowerText.indexOf("<<tool", searchIndex);
|
|
8801
|
+
if (start === -1) break;
|
|
8802
|
+
let quote = null;
|
|
8803
|
+
let escaped = false;
|
|
8804
|
+
let end = -1;
|
|
8805
|
+
for (let index = start + 2; index < text.length - 1; index += 1) {
|
|
8806
|
+
const char = text[index];
|
|
8807
|
+
if (quote) {
|
|
8808
|
+
if (escaped) {
|
|
8809
|
+
escaped = false;
|
|
8810
|
+
} else if (char === "\\") {
|
|
8811
|
+
escaped = true;
|
|
8812
|
+
} else if (char === quote) {
|
|
8813
|
+
quote = null;
|
|
8814
|
+
}
|
|
8815
|
+
continue;
|
|
8816
|
+
}
|
|
8817
|
+
if (char === '"' || char === "'") {
|
|
8818
|
+
quote = char;
|
|
8819
|
+
continue;
|
|
8820
|
+
}
|
|
8821
|
+
if (char === ">" && text[index + 1] === ">") {
|
|
8822
|
+
end = index;
|
|
8823
|
+
break;
|
|
8824
|
+
}
|
|
8825
|
+
}
|
|
8826
|
+
if (end === -1) {
|
|
8827
|
+
searchIndex = start + 2;
|
|
8828
|
+
continue;
|
|
8829
|
+
}
|
|
8830
|
+
bodies.push(text.slice(start + 2, end));
|
|
8831
|
+
searchIndex = end + 2;
|
|
8832
|
+
}
|
|
8833
|
+
return bodies;
|
|
8834
|
+
}
|
|
8835
|
+
function recoverInlineToolMarkerToolCalls(text, availableToolNames) {
|
|
8836
|
+
const recovered = [];
|
|
8837
|
+
for (const markerBody of findInlineToolMarkerBodies(text)) {
|
|
8838
|
+
const match = markerBody.trim().match(/^tool[:=]([a-z_][a-z0-9_]*)(?:[:=]([\s\S]*))?$/i);
|
|
8839
|
+
if (!match) continue;
|
|
8840
|
+
const rawName = match[1] ?? "";
|
|
8841
|
+
const rawArgs = (match[2] ?? "").trim();
|
|
8842
|
+
const resolvedName = resolveToolCallName(
|
|
8843
|
+
rawName,
|
|
8844
|
+
{},
|
|
8845
|
+
availableToolNames
|
|
8846
|
+
);
|
|
8847
|
+
if (!availableToolNames.has(resolvedName)) continue;
|
|
8848
|
+
let parsedArgs = null;
|
|
8849
|
+
if (rawArgs.startsWith("{")) {
|
|
8850
|
+
const repaired = parseToolArgsWithRepair(resolvedName, rawArgs);
|
|
8851
|
+
if (repaired) {
|
|
8852
|
+
parsedArgs = repaired.args;
|
|
8853
|
+
}
|
|
8854
|
+
}
|
|
8855
|
+
if (!parsedArgs) {
|
|
8856
|
+
const kvArgs = {};
|
|
8857
|
+
const kvPattern = /([a-z_][a-z0-9_]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^,\s]*))/gi;
|
|
8858
|
+
let kvMatch;
|
|
8859
|
+
while ((kvMatch = kvPattern.exec(rawArgs)) !== null) {
|
|
8860
|
+
const key2 = kvMatch[1];
|
|
8861
|
+
const value = kvMatch[2] ?? kvMatch[3] ?? kvMatch[4] ?? "";
|
|
8862
|
+
kvArgs[key2] = value;
|
|
8863
|
+
}
|
|
8864
|
+
if (Object.keys(kvArgs).length > 0) {
|
|
8865
|
+
parsedArgs = kvArgs;
|
|
8866
|
+
}
|
|
8867
|
+
}
|
|
8868
|
+
if (!parsedArgs && rawArgs) {
|
|
8869
|
+
const repaired = parseToolArgsWithRepair(resolvedName, rawArgs);
|
|
8870
|
+
if (repaired) {
|
|
8871
|
+
parsedArgs = repaired.args;
|
|
8872
|
+
}
|
|
8873
|
+
}
|
|
8874
|
+
if (!parsedArgs) {
|
|
8875
|
+
parsedArgs = {};
|
|
8876
|
+
}
|
|
8877
|
+
recovered.push({
|
|
8878
|
+
id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
8879
|
+
name: resolvedName,
|
|
8880
|
+
argsJson: JSON.stringify(parsedArgs)
|
|
8881
|
+
});
|
|
8882
|
+
}
|
|
8883
|
+
return recovered;
|
|
8884
|
+
}
|
|
8885
|
+
function recoverAssistantTextToolCalls(text, availableToolNames) {
|
|
8886
|
+
const textEncodedCalls = recoverTextEncodedToolCalls(text, availableToolNames);
|
|
8887
|
+
if (textEncodedCalls.length > 0) return textEncodedCalls;
|
|
8888
|
+
const inlineMarkerCalls = recoverInlineToolMarkerToolCalls(
|
|
8889
|
+
text,
|
|
8890
|
+
availableToolNames
|
|
8891
|
+
);
|
|
8892
|
+
if (inlineMarkerCalls.length > 0) return inlineMarkerCalls;
|
|
8893
|
+
return recoverNarratedActionToolCalls(text, availableToolNames);
|
|
8894
|
+
}
|
|
8686
8895
|
function recoverNarratedActionToolCalls(text, availableToolNames) {
|
|
8687
8896
|
const trimmed = text.trim();
|
|
8688
8897
|
if (!trimmed) return [];
|
|
@@ -9320,10 +9529,9 @@ class OpenAICompatProvider {
|
|
|
9320
9529
|
let highlightCompletionRecoveryCount = 0;
|
|
9321
9530
|
let compactCorrectionCount = 0;
|
|
9322
9531
|
const recentCompactToolSignatures = [];
|
|
9323
|
-
const recentToolNames = [];
|
|
9324
9532
|
const successfulToolNames = [];
|
|
9325
9533
|
const searchLoopGuard = new SearchLoopGuard(isSearchContextResettingTool);
|
|
9326
|
-
|
|
9534
|
+
const clickReadLoopGuard = new ClickReadLoopGuard();
|
|
9327
9535
|
for (let i = 0; i < maxIterations; i++) {
|
|
9328
9536
|
iterationsUsed = i + 1;
|
|
9329
9537
|
let textAccum = "";
|
|
@@ -9398,22 +9606,13 @@ class OpenAICompatProvider {
|
|
|
9398
9606
|
tc.name = resolveToolCallName(tc.name, parsedArgs, availableToolNames);
|
|
9399
9607
|
}
|
|
9400
9608
|
if (toolCalls.length === 0) {
|
|
9401
|
-
const recoveredToolCalls =
|
|
9609
|
+
const recoveredToolCalls = recoverAssistantTextToolCalls(
|
|
9402
9610
|
textAccum,
|
|
9403
9611
|
availableToolNames
|
|
9404
9612
|
);
|
|
9405
9613
|
if (recoveredToolCalls.length > 0) {
|
|
9406
9614
|
toolCalls = recoveredToolCalls;
|
|
9407
9615
|
if (textAccum.trim()) onChunk("<<erase_prev>>");
|
|
9408
|
-
} else {
|
|
9409
|
-
const narratedToolCalls = recoverNarratedActionToolCalls(
|
|
9410
|
-
textAccum,
|
|
9411
|
-
availableToolNames
|
|
9412
|
-
);
|
|
9413
|
-
if (narratedToolCalls.length > 0) {
|
|
9414
|
-
toolCalls = narratedToolCalls;
|
|
9415
|
-
if (textAccum.trim()) onChunk("<<erase_prev>>");
|
|
9416
|
-
}
|
|
9417
9616
|
}
|
|
9418
9617
|
}
|
|
9419
9618
|
logAgentLoopDebug({
|
|
@@ -9622,6 +9821,19 @@ class OpenAICompatProvider {
|
|
|
9622
9821
|
}
|
|
9623
9822
|
continue;
|
|
9624
9823
|
}
|
|
9824
|
+
const clickLoopPreflight = clickReadLoopGuard.beforeTool(tc.name);
|
|
9825
|
+
if (clickLoopPreflight?.kind === "suppress") {
|
|
9826
|
+
onChunk(`
|
|
9827
|
+
<<tool:click:↻ loop suppressed>>
|
|
9828
|
+
`);
|
|
9829
|
+
messages.push({
|
|
9830
|
+
role: "tool",
|
|
9831
|
+
tool_call_id: tc.id,
|
|
9832
|
+
content: clickLoopPreflight.message
|
|
9833
|
+
});
|
|
9834
|
+
compactCorrectionCount += 1;
|
|
9835
|
+
continue;
|
|
9836
|
+
}
|
|
9625
9837
|
const argSummary = [args.url, args.query, args.text, args.direction].map((v) => typeof v === "string" ? v : "").find((v) => v.length > 0) ?? "";
|
|
9626
9838
|
onChunk(`
|
|
9627
9839
|
<<tool:${tc.name}${argSummary ? ":" + argSummary : ""}>>
|
|
@@ -9659,15 +9871,11 @@ class OpenAICompatProvider {
|
|
|
9659
9871
|
searchToolQuery,
|
|
9660
9872
|
toolSucceeded
|
|
9661
9873
|
);
|
|
9662
|
-
|
|
9663
|
-
|
|
9664
|
-
|
|
9665
|
-
|
|
9666
|
-
|
|
9667
|
-
role: "user",
|
|
9668
|
-
content: `[System] You are alternating between click and read_page without advancing the task. The click result already includes a page snapshot when it navigates — you do not need read_page after every click. If you need detail on a specific element, use inspect_element instead. If you have enough context, proceed with the next action directly.`
|
|
9669
|
-
});
|
|
9670
|
-
}
|
|
9874
|
+
const clickLoopIntervention = clickReadLoopGuard.afterToolResult(
|
|
9875
|
+
tc.name,
|
|
9876
|
+
toolContent,
|
|
9877
|
+
toolSucceeded
|
|
9878
|
+
);
|
|
9671
9879
|
compactCorrectionCount = 0;
|
|
9672
9880
|
iterationToolResultPreviews.push(toolContent);
|
|
9673
9881
|
messages.push({
|
|
@@ -9675,6 +9883,9 @@ class OpenAICompatProvider {
|
|
|
9675
9883
|
tool_call_id: tc.id,
|
|
9676
9884
|
content: toolContent
|
|
9677
9885
|
});
|
|
9886
|
+
if (clickLoopIntervention?.kind === "nudge") {
|
|
9887
|
+
messages.push({ role: "user", content: clickLoopIntervention.message });
|
|
9888
|
+
}
|
|
9678
9889
|
}
|
|
9679
9890
|
const followUpReminder = followUpReminderForProfile(
|
|
9680
9891
|
this.agentToolProfile,
|
|
@@ -10346,12 +10557,25 @@ function buildCodexFlightPriceEvidenceRecoveryInput(userMessage, assistantText,
|
|
|
10346
10557
|
]
|
|
10347
10558
|
};
|
|
10348
10559
|
}
|
|
10349
|
-
function buildCodexFailedClickRecoveryInput(attemptedTarget, latestToolResultPreview, failedClickCount = 1) {
|
|
10560
|
+
function buildCodexFailedClickRecoveryInput(attemptedTarget, latestToolResultPreview, failedClickCount = 1, errorOutput = "") {
|
|
10350
10561
|
const stateReminder = buildLatestStateReminder(latestToolResultPreview);
|
|
10562
|
+
const clickFailureKind = classifyClickFailure(errorOutput);
|
|
10351
10563
|
const lines = [
|
|
10352
|
-
`[System] The previous click did not complete${attemptedTarget ? ` for ${attemptedTarget}` : ""}
|
|
10353
|
-
`Take the next step yourself: try a different target, refresh the page state with read_page, call inspect_element on the intended element, or answer from the results already visible in the conversation. Do not ask the user to inspect or click the result for you.`
|
|
10564
|
+
`[System] The previous click did not complete${attemptedTarget ? ` for ${attemptedTarget}` : ""}.`
|
|
10354
10565
|
];
|
|
10566
|
+
if (clickFailureKind === "hidden") {
|
|
10567
|
+
lines.push(
|
|
10568
|
+
`The click target was hidden / not laid out (collapsed, lazy-loaded, or virtual-scroll). Call scroll or scroll_to_element toward it to reveal it, then read_page to refresh visible elements, before clicking again — or inspect_element on the intended index, or answer from the results already visible. Do not ask the user to inspect or click the result for you.`
|
|
10569
|
+
);
|
|
10570
|
+
} else if (clickFailureKind === "stale") {
|
|
10571
|
+
lines.push(
|
|
10572
|
+
`The click target was stale — the page changed since the last snapshot. Call read_page to refresh current indexes, then choose a currently listed target, inspect_element on the intended element, or answer from the results already visible. Do not ask the user to inspect or click the result for you.`
|
|
10573
|
+
);
|
|
10574
|
+
} else {
|
|
10575
|
+
lines.push(
|
|
10576
|
+
`Take the next step yourself: try a different target, refresh the page state with read_page, call inspect_element on the intended element, or answer from the results already visible in the conversation. Do not ask the user to inspect or click the result for you.`
|
|
10577
|
+
);
|
|
10578
|
+
}
|
|
10355
10579
|
if (failedClickCount >= 2) {
|
|
10356
10580
|
lines.push(
|
|
10357
10581
|
`You have already had multiple failed clicks without making page progress. Do not keep clicking similar search result titles. Use the latest read_page/search result text to answer, or inspect a specific indexed result/control only if essential.`
|
|
@@ -10562,8 +10786,7 @@ class CodexProvider {
|
|
|
10562
10786
|
let flightPriceEvidenceRecoveryCount = 0;
|
|
10563
10787
|
let correctionCount = 0;
|
|
10564
10788
|
const recentToolSignatures = [];
|
|
10565
|
-
const
|
|
10566
|
-
let clickReadLoopNudged = false;
|
|
10789
|
+
const clickReadLoopGuard = new ClickReadLoopGuard();
|
|
10567
10790
|
let latestToolResultPreview = null;
|
|
10568
10791
|
let failedClickCountSinceProgress = 0;
|
|
10569
10792
|
const searchLoopGuard = new SearchLoopGuard(isRealProgressTool);
|
|
@@ -10587,11 +10810,10 @@ class CodexProvider {
|
|
|
10587
10810
|
(item) => item.type === "function_call"
|
|
10588
10811
|
);
|
|
10589
10812
|
if (functionCalls.length === 0) {
|
|
10590
|
-
const
|
|
10813
|
+
const recoveredCalls = recoverAssistantTextToolCalls(
|
|
10591
10814
|
result.text,
|
|
10592
10815
|
availableToolNames
|
|
10593
10816
|
);
|
|
10594
|
-
const recoveredCalls = recoveredTextCalls.length > 0 ? recoveredTextCalls : recoverNarratedActionToolCalls(result.text, availableToolNames);
|
|
10595
10817
|
if (recoveredCalls.length > 0) {
|
|
10596
10818
|
if (result.text.trim()) onChunk("<<erase_prev>>");
|
|
10597
10819
|
functionCalls = recoveredCalls.map((toolCall) => ({
|
|
@@ -10720,6 +10942,22 @@ ${latestToolResultPreview || ""}`
|
|
|
10720
10942
|
correctionCount += 1;
|
|
10721
10943
|
continue;
|
|
10722
10944
|
}
|
|
10945
|
+
const clickLoopPreflight = clickReadLoopGuard.beforeTool(
|
|
10946
|
+
prepared.prepared.name
|
|
10947
|
+
);
|
|
10948
|
+
if (clickLoopPreflight?.kind === "suppress") {
|
|
10949
|
+
onChunk(`
|
|
10950
|
+
<<tool:click:↻ loop suppressed>>
|
|
10951
|
+
`);
|
|
10952
|
+
const suppressed = createCodexToolOutput(
|
|
10953
|
+
prepared.prepared.callId,
|
|
10954
|
+
clickLoopPreflight.message
|
|
10955
|
+
);
|
|
10956
|
+
currentInput.push(suppressed);
|
|
10957
|
+
latestToolResultPreview = previewToolResult(suppressed.output);
|
|
10958
|
+
correctionCount += 1;
|
|
10959
|
+
continue;
|
|
10960
|
+
}
|
|
10723
10961
|
const output = await executePreparedCodexFunctionCall(
|
|
10724
10962
|
prepared.prepared,
|
|
10725
10963
|
onChunk,
|
|
@@ -10747,7 +10985,8 @@ ${latestToolResultPreview || ""}`
|
|
|
10747
10985
|
buildCodexFailedClickRecoveryInput(
|
|
10748
10986
|
summarizeToolArg(prepared.prepared.args),
|
|
10749
10987
|
latestToolResultPreview,
|
|
10750
|
-
failedClickCountSinceProgress
|
|
10988
|
+
failedClickCountSinceProgress,
|
|
10989
|
+
outputText
|
|
10751
10990
|
)
|
|
10752
10991
|
);
|
|
10753
10992
|
}
|
|
@@ -10755,17 +10994,16 @@ ${latestToolResultPreview || ""}`
|
|
|
10755
10994
|
if (recentToolSignatures.length > 4) {
|
|
10756
10995
|
recentToolSignatures.shift();
|
|
10757
10996
|
}
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
|
|
10761
|
-
|
|
10997
|
+
const clickLoopIntervention = clickReadLoopGuard.afterToolResult(
|
|
10998
|
+
prepared.prepared.name,
|
|
10999
|
+
outputText,
|
|
11000
|
+
toolSucceeded
|
|
11001
|
+
);
|
|
11002
|
+
if (clickLoopIntervention?.kind === "nudge") {
|
|
10762
11003
|
currentInput.push({
|
|
10763
11004
|
type: "message",
|
|
10764
11005
|
role: "user",
|
|
10765
|
-
content: [{
|
|
10766
|
-
type: "input_text",
|
|
10767
|
-
text: `[System] You are alternating between click and read_page without advancing the task. The click result already includes a page snapshot when it navigates, so do not read_page after every click. If you need detail on a specific element, use inspect_element. Otherwise continue the original task directly.`
|
|
10768
|
-
}]
|
|
11006
|
+
content: [{ type: "input_text", text: clickLoopIntervention.message }]
|
|
10769
11007
|
});
|
|
10770
11008
|
}
|
|
10771
11009
|
correctionCount = 0;
|
|
@@ -11989,8 +12227,7 @@ function pageBusyError(action) {
|
|
|
11989
12227
|
return `Error: Page is still busy; ${action} timed out waiting for page scripts. Retry in a moment.`;
|
|
11990
12228
|
}
|
|
11991
12229
|
async function loadPermittedUrl(wc, url) {
|
|
11992
|
-
|
|
11993
|
-
assertPermittedNavigationURL2(url);
|
|
12230
|
+
assertPermittedNavigationURL(url);
|
|
11994
12231
|
await wc.loadURL(url);
|
|
11995
12232
|
}
|
|
11996
12233
|
async function executePageScript(wc, script, options) {
|
|
@@ -16330,6 +16567,60 @@ async function clickElement(wc, selector) {
|
|
|
16330
16567
|
}));
|
|
16331
16568
|
}
|
|
16332
16569
|
|
|
16570
|
+
// Sum offsetTop up the offsetParent chain until reaching "container",
|
|
16571
|
+
// giving the element's vertical position within that scroll container.
|
|
16572
|
+
function offsetTopWithin(el, container) {
|
|
16573
|
+
let top = 0;
|
|
16574
|
+
let node = el;
|
|
16575
|
+
while (node && node !== container) {
|
|
16576
|
+
top += node.offsetTop || 0;
|
|
16577
|
+
node = node.offsetParent;
|
|
16578
|
+
}
|
|
16579
|
+
return top;
|
|
16580
|
+
}
|
|
16581
|
+
|
|
16582
|
+
function nearestScrollableAncestor(el) {
|
|
16583
|
+
let node = el.parentElement;
|
|
16584
|
+
while (node) {
|
|
16585
|
+
if (node instanceof HTMLElement) {
|
|
16586
|
+
const style = window.getComputedStyle(node);
|
|
16587
|
+
if (
|
|
16588
|
+
(style.overflowY === "auto" || style.overflowY === "scroll") &&
|
|
16589
|
+
node.scrollHeight > node.clientHeight
|
|
16590
|
+
) {
|
|
16591
|
+
return node;
|
|
16592
|
+
}
|
|
16593
|
+
}
|
|
16594
|
+
node = node.parentElement;
|
|
16595
|
+
}
|
|
16596
|
+
return null;
|
|
16597
|
+
}
|
|
16598
|
+
|
|
16599
|
+
// Wait for the element to gain a non-zero layout box, polling for up to
|
|
16600
|
+
// maxFrames animation frames. Lazy / virtual-scroll renderers
|
|
16601
|
+
// (content-visibility, intersection-triggered list items) often lay out
|
|
16602
|
+
// a frame or two after the scroller moves. Falls back to setTimeout when
|
|
16603
|
+
// the window is hidden (requestAnimationFrame does not fire then).
|
|
16604
|
+
function waitForBox(el, maxFrames) {
|
|
16605
|
+
return new Promise((resolve) => {
|
|
16606
|
+
let frames = 0;
|
|
16607
|
+
const rafAvailable =
|
|
16608
|
+
typeof requestAnimationFrame === "function" &&
|
|
16609
|
+
document.visibilityState === "visible";
|
|
16610
|
+
const schedule = rafAvailable
|
|
16611
|
+
? (cb) => requestAnimationFrame(cb)
|
|
16612
|
+
: (cb) => setTimeout(cb, 16);
|
|
16613
|
+
const check = () => {
|
|
16614
|
+
const r = el.getBoundingClientRect();
|
|
16615
|
+
if (r.width > 0 && r.height > 0) return resolve(true);
|
|
16616
|
+
if (frames >= maxFrames) return resolve(false);
|
|
16617
|
+
frames += 1;
|
|
16618
|
+
schedule(check);
|
|
16619
|
+
};
|
|
16620
|
+
check();
|
|
16621
|
+
});
|
|
16622
|
+
}
|
|
16623
|
+
|
|
16333
16624
|
const el = document.querySelector(${JSON.stringify(selector)});
|
|
16334
16625
|
if (!el) return { error: "Error[stale-index]: Element not found — the page may have changed. Call read_page to refresh." };
|
|
16335
16626
|
|
|
@@ -16337,21 +16628,26 @@ async function clickElement(wc, selector) {
|
|
|
16337
16628
|
el.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
|
|
16338
16629
|
}
|
|
16339
16630
|
|
|
16340
|
-
|
|
16341
|
-
|
|
16342
|
-
|
|
16343
|
-
|
|
16344
|
-
|
|
16345
|
-
|
|
16346
|
-
|
|
16347
|
-
|
|
16348
|
-
|
|
16349
|
-
|
|
16350
|
-
)
|
|
16351
|
-
|
|
16631
|
+
// Give the renderer a brief grace to lay the element out after the
|
|
16632
|
+
// initial scroll. Already-visible elements resolve on the first check.
|
|
16633
|
+
let revealed = await waitForBox(el, 4);
|
|
16634
|
+
|
|
16635
|
+
// scrollIntoView is a no-op on zero-rect elements (collapsed, lazy, or
|
|
16636
|
+
// virtual-scroll content). Force the nearest scrollable ancestor to bring
|
|
16637
|
+
// the element's offset position into view, then wait longer for the
|
|
16638
|
+
// renderer to produce a layout box. This recovers many hidden targets
|
|
16639
|
+
// without the model having to scroll manually.
|
|
16640
|
+
if (!revealed) {
|
|
16641
|
+
const scroller = nearestScrollableAncestor(el);
|
|
16642
|
+
if (scroller) {
|
|
16643
|
+
const targetTop = offsetTopWithin(el, scroller) - scroller.clientHeight / 2;
|
|
16644
|
+
scroller.scrollTop = Math.max(0, targetTop);
|
|
16352
16645
|
}
|
|
16353
|
-
|
|
16354
|
-
|
|
16646
|
+
if (el instanceof HTMLElement) {
|
|
16647
|
+
el.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
|
|
16648
|
+
}
|
|
16649
|
+
revealed = await waitForBox(el, 24);
|
|
16650
|
+
}
|
|
16355
16651
|
|
|
16356
16652
|
const rect = el.getBoundingClientRect();
|
|
16357
16653
|
if (rect.width <= 0 || rect.height <= 0) {
|
|
@@ -17914,9 +18210,13 @@ Go back and select a different product.`;
|
|
|
17914
18210
|
const clickText = `Clicked: ${elInfo.text}${tagLabel}`;
|
|
17915
18211
|
const clickResult = await clickElement(wc, selector);
|
|
17916
18212
|
if (clickResult.startsWith("Error:")) return clickResult;
|
|
17917
|
-
|
|
18213
|
+
const initialNavigationWaitMs = /DOM activation/i.test(clickResult) && !elInfo.href ? 800 : void 0;
|
|
18214
|
+
await waitForPotentialNavigation$1(wc, beforeUrl, initialNavigationWaitMs);
|
|
17918
18215
|
const afterUrl = wc.getURL();
|
|
17919
18216
|
if (afterUrl !== beforeUrl) {
|
|
18217
|
+
if (/DOM activation/i.test(clickResult)) {
|
|
18218
|
+
return `${clickText} -> ${afterUrl} (recovered via DOM activation)`;
|
|
18219
|
+
}
|
|
17920
18220
|
return `${clickText} -> ${afterUrl}`;
|
|
17921
18221
|
}
|
|
17922
18222
|
const overlayHint = await detectPostClickOverlay(wc);
|
|
@@ -17943,6 +18243,9 @@ ${overlayHint}`;
|
|
|
17943
18243
|
}
|
|
17944
18244
|
return `${clickText} (${clickResult})${await buildCartSuccessSuffix(wc, beforeUrl)}`;
|
|
17945
18245
|
}
|
|
18246
|
+
if (/DOM activation/i.test(clickResult) && (!elInfo.href || elInfo.target === "_blank")) {
|
|
18247
|
+
return `${clickText} (${clickResult})`;
|
|
18248
|
+
}
|
|
17946
18249
|
const activationResult = await activateElement(wc, selector);
|
|
17947
18250
|
if (!activationResult.startsWith("Error:")) {
|
|
17948
18251
|
await waitForPotentialNavigation$1(wc, beforeUrl);
|
|
@@ -23312,7 +23615,10 @@ Recent checkpoints:
|
|
|
23312
23615
|
${input.recentCheckpoints || "- none"}
|
|
23313
23616
|
|
|
23314
23617
|
Task tracker:
|
|
23315
|
-
${input.taskTrackerContext || "- none"}
|
|
23618
|
+
${input.taskTrackerContext || "- none"}
|
|
23619
|
+
|
|
23620
|
+
Task memory:
|
|
23621
|
+
${input.taskMemoryContext || "- none"}`;
|
|
23316
23622
|
}
|
|
23317
23623
|
function buildAgentSystemPrompt(input) {
|
|
23318
23624
|
const instructionBlocks = input.profile === "compact" ? [
|
|
@@ -23693,6 +23999,7 @@ async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd,
|
|
|
23693
23999
|
const runtimeState = runtime2.getState();
|
|
23694
24000
|
const recentCheckpoints = runtimeState.checkpoints.slice(-3).map((item) => `- ${item.name} (${item.id})`).join("\n");
|
|
23695
24001
|
const taskTrackerContext = runtime2.getTaskTrackerContext();
|
|
24002
|
+
const taskMemoryContext = runtime2.getTaskMemoryContext();
|
|
23696
24003
|
const activeTabTitle = pageContent.title || "(untitled)";
|
|
23697
24004
|
const activeTabUrl = pageContent.url || activeWebContents.getURL();
|
|
23698
24005
|
const allTabs = tabManager.getAllStates();
|
|
@@ -23711,7 +24018,8 @@ All open tabs: ${allTabs.map((t) => `${t.id === activeTabId ? "→ " : ""}${t.ti
|
|
|
23711
24018
|
approvalMode: runtimeState.supervisor.approvalMode,
|
|
23712
24019
|
pendingApprovals: runtimeState.supervisor.pendingApprovals.length,
|
|
23713
24020
|
recentCheckpoints: recentCheckpoints || "- none",
|
|
23714
|
-
taskTrackerContext: taskTrackerContext || "- none"
|
|
24021
|
+
taskTrackerContext: taskTrackerContext || "- none",
|
|
24022
|
+
taskMemoryContext: taskMemoryContext || "- none"
|
|
23715
24023
|
});
|
|
23716
24024
|
const actionCtx = {
|
|
23717
24025
|
tabManager,
|
|
@@ -23740,6 +24048,9 @@ All open tabs: ${allTabs.map((t) => `${t.id === activeTabId ? "→ " : ""}${t.ti
|
|
|
23740
24048
|
let isError = false;
|
|
23741
24049
|
try {
|
|
23742
24050
|
output = await executeAction(name, args, actionCtx);
|
|
24051
|
+
if (/^\s*Error:/i.test(output)) {
|
|
24052
|
+
isError = true;
|
|
24053
|
+
}
|
|
23743
24054
|
if (provider.agentToolProfile === "compact") {
|
|
23744
24055
|
runtime2.updateTaskTracker(name, output);
|
|
23745
24056
|
const trackerCtx = runtime2.getTaskTrackerContext();
|
|
@@ -24311,6 +24622,17 @@ function setMcpHealth(update) {
|
|
|
24311
24622
|
}
|
|
24312
24623
|
const ApprovalModeSchema = zod.z.enum(["auto", "confirm-dangerous", "manual"]);
|
|
24313
24624
|
const CheckpointIdSchema = zod.z.string().min(1);
|
|
24625
|
+
const TaskTextSchema = zod.z.string().trim().min(1).max(2e4);
|
|
24626
|
+
const OptionalTaskTextSchema = zod.z.string().trim().max(2e4).optional();
|
|
24627
|
+
const OptionalNullableTaskTextSchema = zod.z.string().trim().max(2e4).nullable().optional();
|
|
24628
|
+
const TaskFactsSchema = zod.z.record(
|
|
24629
|
+
zod.z.string().trim().min(1).max(200),
|
|
24630
|
+
zod.z.string().max(2e4)
|
|
24631
|
+
);
|
|
24632
|
+
const TaskMemoryPatchSchema = zod.z.object({
|
|
24633
|
+
nextStep: OptionalNullableTaskTextSchema,
|
|
24634
|
+
facts: TaskFactsSchema.optional()
|
|
24635
|
+
});
|
|
24314
24636
|
function registerAgentRuntimeHandlers(runtime2, chromeView, sidebarView, sendToRendererViews) {
|
|
24315
24637
|
let runtimeUpdateTimer = null;
|
|
24316
24638
|
let pendingRuntimeState = null;
|
|
@@ -24403,6 +24725,45 @@ function registerAgentRuntimeHandlers(runtime2, chromeView, sidebarView, sendToR
|
|
|
24403
24725
|
return runtime2.restoreSession(snapshot2);
|
|
24404
24726
|
}
|
|
24405
24727
|
);
|
|
24728
|
+
electron.ipcMain.handle(Channels.AGENT_TASK_START, (event, goal) => {
|
|
24729
|
+
assertTrustedIpcSender(event);
|
|
24730
|
+
return runtime2.startTaskMemory(parseIpc(TaskTextSchema, goal, "goal"));
|
|
24731
|
+
});
|
|
24732
|
+
electron.ipcMain.handle(
|
|
24733
|
+
Channels.AGENT_TASK_UPDATE,
|
|
24734
|
+
(event, patch) => {
|
|
24735
|
+
assertTrustedIpcSender(event);
|
|
24736
|
+
return runtime2.updateTaskMemory(
|
|
24737
|
+
parseIpc(TaskMemoryPatchSchema, patch ?? {}, "patch")
|
|
24738
|
+
);
|
|
24739
|
+
}
|
|
24740
|
+
);
|
|
24741
|
+
electron.ipcMain.handle(Channels.AGENT_TASK_NOTE, (event, text) => {
|
|
24742
|
+
assertTrustedIpcSender(event);
|
|
24743
|
+
return runtime2.addTaskNote(parseIpc(TaskTextSchema, text, "text"));
|
|
24744
|
+
});
|
|
24745
|
+
electron.ipcMain.handle(Channels.AGENT_TASK_BLOCKER, (event, blocker) => {
|
|
24746
|
+
assertTrustedIpcSender(event);
|
|
24747
|
+
const validated = blocker == null ? null : parseIpc(OptionalNullableTaskTextSchema, blocker, "blocker");
|
|
24748
|
+
return runtime2.setTaskBlocker(validated ?? null);
|
|
24749
|
+
});
|
|
24750
|
+
electron.ipcMain.handle(Channels.AGENT_TASK_RESOLVE, (event, summary) => {
|
|
24751
|
+
assertTrustedIpcSender(event);
|
|
24752
|
+
return runtime2.resolveTaskMemory(
|
|
24753
|
+
summary == null ? void 0 : parseIpc(OptionalTaskTextSchema, summary, "summary")
|
|
24754
|
+
);
|
|
24755
|
+
});
|
|
24756
|
+
electron.ipcMain.handle(Channels.AGENT_TASK_ABANDON, (event, reason) => {
|
|
24757
|
+
assertTrustedIpcSender(event);
|
|
24758
|
+
return runtime2.abandonTaskMemory(
|
|
24759
|
+
reason == null ? void 0 : parseIpc(OptionalTaskTextSchema, reason, "reason")
|
|
24760
|
+
);
|
|
24761
|
+
});
|
|
24762
|
+
electron.ipcMain.handle(Channels.AGENT_TASK_CLEAR, (event) => {
|
|
24763
|
+
assertTrustedIpcSender(event);
|
|
24764
|
+
runtime2.clearTaskMemory();
|
|
24765
|
+
return null;
|
|
24766
|
+
});
|
|
24406
24767
|
}
|
|
24407
24768
|
function asTextResponse$1(text) {
|
|
24408
24769
|
return { content: [{ type: "text", text }] };
|
|
@@ -28129,6 +28490,137 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
|
|
|
28129
28490
|
}
|
|
28130
28491
|
);
|
|
28131
28492
|
}
|
|
28493
|
+
function registerTaskMemoryTools(server, _tabManager, runtime2) {
|
|
28494
|
+
server.registerTool(
|
|
28495
|
+
"task_start",
|
|
28496
|
+
{
|
|
28497
|
+
title: "Start Task",
|
|
28498
|
+
description: "Start tracking a task. Creates a task memory record with a goal that persists across actions and browser navigation. Use this at the beginning of a multi-step task so the human supervisor can see what you are working on.",
|
|
28499
|
+
inputSchema: {
|
|
28500
|
+
goal: zod.z.string().describe("What this task aims to accomplish"),
|
|
28501
|
+
nextStep: zod.z.string().optional().describe("The first step you plan to take"),
|
|
28502
|
+
facts: zod.z.record(zod.z.string()).optional().describe("Key-value facts relevant to this task (e.g. { username: alice })")
|
|
28503
|
+
}
|
|
28504
|
+
},
|
|
28505
|
+
async ({ goal, nextStep, facts }) => {
|
|
28506
|
+
const task = runtime2.startTaskMemory(goal, {
|
|
28507
|
+
nextStep: nextStep ?? null,
|
|
28508
|
+
facts: facts ?? {}
|
|
28509
|
+
});
|
|
28510
|
+
return asTextResponse(
|
|
28511
|
+
`Task started: ${task.goal}
|
|
28512
|
+
Status: ${task.status}${task.nextStep ? `
|
|
28513
|
+
Next step: ${task.nextStep}` : ""}`
|
|
28514
|
+
);
|
|
28515
|
+
}
|
|
28516
|
+
);
|
|
28517
|
+
server.registerTool(
|
|
28518
|
+
"task_update",
|
|
28519
|
+
{
|
|
28520
|
+
title: "Update Task",
|
|
28521
|
+
description: "Update the current task's next step or facts. Facts are merged with existing facts. Use this to record progress and keep the human supervisor informed.",
|
|
28522
|
+
inputSchema: {
|
|
28523
|
+
nextStep: zod.z.string().optional().describe("The next step you plan to take"),
|
|
28524
|
+
facts: zod.z.record(zod.z.string()).optional().describe("Key-value facts to merge into the task (existing keys are overwritten)")
|
|
28525
|
+
}
|
|
28526
|
+
},
|
|
28527
|
+
async ({ nextStep, facts }) => {
|
|
28528
|
+
const updated = runtime2.updateTaskMemory({
|
|
28529
|
+
nextStep,
|
|
28530
|
+
facts
|
|
28531
|
+
});
|
|
28532
|
+
if (!updated) return asTextResponse("No active task to update. Start one with task_start first.");
|
|
28533
|
+
return asTextResponse(
|
|
28534
|
+
`Task updated: ${updated.goal}
|
|
28535
|
+
Status: ${updated.status}${updated.nextStep ? `
|
|
28536
|
+
Next step: ${updated.nextStep}` : ""}${Object.keys(updated.facts).length > 0 ? `
|
|
28537
|
+
Facts: ${Object.entries(updated.facts).map(([k, v]) => `${k}=${v}`).join(", ")}` : ""}`
|
|
28538
|
+
);
|
|
28539
|
+
}
|
|
28540
|
+
);
|
|
28541
|
+
server.registerTool(
|
|
28542
|
+
"task_note",
|
|
28543
|
+
{
|
|
28544
|
+
title: "Add Task Note",
|
|
28545
|
+
description: "Add a note to the current task. Use this to record observations, intermediate results, or context for the human supervisor.",
|
|
28546
|
+
inputSchema: {
|
|
28547
|
+
text: zod.z.string().describe("The note text to add")
|
|
28548
|
+
}
|
|
28549
|
+
},
|
|
28550
|
+
async ({ text }) => {
|
|
28551
|
+
const updated = runtime2.addTaskNote(text);
|
|
28552
|
+
if (!updated) return asTextResponse("No active task to add a note to. Start one with task_start first.");
|
|
28553
|
+
return asTextResponse(`Note added to task: ${updated.goal}`);
|
|
28554
|
+
}
|
|
28555
|
+
);
|
|
28556
|
+
server.registerTool(
|
|
28557
|
+
"task_blocker",
|
|
28558
|
+
{
|
|
28559
|
+
title: "Set or Clear Task Blocker",
|
|
28560
|
+
description: "Mark the task as blocked with a reason, or clear a blocker to resume. Use this when you are stuck and need human input to continue.",
|
|
28561
|
+
inputSchema: {
|
|
28562
|
+
blocker: zod.z.string().optional().describe("Description of what is blocking progress. Omit or empty string to clear a blocker.")
|
|
28563
|
+
}
|
|
28564
|
+
},
|
|
28565
|
+
async ({ blocker }) => {
|
|
28566
|
+
const updated = runtime2.setTaskBlocker(blocker?.trim() || null);
|
|
28567
|
+
if (!updated) return asTextResponse("No active task. Start one with task_start first.");
|
|
28568
|
+
if (updated.blocker) {
|
|
28569
|
+
return asTextResponse(`Task blocked: ${updated.blocker}
|
|
28570
|
+
Status: ${updated.status}`);
|
|
28571
|
+
}
|
|
28572
|
+
return asTextResponse(`Blocker cleared. Task: ${updated.goal}
|
|
28573
|
+
Status: ${updated.status}`);
|
|
28574
|
+
}
|
|
28575
|
+
);
|
|
28576
|
+
server.registerTool(
|
|
28577
|
+
"task_resolve",
|
|
28578
|
+
{
|
|
28579
|
+
title: "Resolve Task",
|
|
28580
|
+
description: "Mark the current task as completed. Optionally add a summary note. Use this when the task goal has been achieved.",
|
|
28581
|
+
inputSchema: {
|
|
28582
|
+
summary: zod.z.string().optional().describe("Brief summary of the completed task")
|
|
28583
|
+
}
|
|
28584
|
+
},
|
|
28585
|
+
async ({ summary }) => {
|
|
28586
|
+
const resolved = runtime2.resolveTaskMemory(summary);
|
|
28587
|
+
if (!resolved) return asTextResponse("No active task to resolve. Start one with task_start first.");
|
|
28588
|
+
return asTextResponse(
|
|
28589
|
+
`Task completed: ${resolved.goal}${resolved.notes.length > 0 ? `
|
|
28590
|
+
Notes: ${resolved.notes.length} note(s)` : ""}`
|
|
28591
|
+
);
|
|
28592
|
+
}
|
|
28593
|
+
);
|
|
28594
|
+
server.registerTool(
|
|
28595
|
+
"task_abandon",
|
|
28596
|
+
{
|
|
28597
|
+
title: "Abandon Task",
|
|
28598
|
+
description: "Mark the current task as abandoned. Use this when the task cannot be completed or is no longer relevant.",
|
|
28599
|
+
inputSchema: {
|
|
28600
|
+
reason: zod.z.string().optional().describe("Reason for abandoning the task")
|
|
28601
|
+
}
|
|
28602
|
+
},
|
|
28603
|
+
async ({ reason }) => {
|
|
28604
|
+
const abandoned = runtime2.abandonTaskMemory(reason);
|
|
28605
|
+
if (!abandoned) return asTextResponse("No active task to abandon. Start one with task_start first.");
|
|
28606
|
+
return asTextResponse(
|
|
28607
|
+
`Task abandoned: ${abandoned.goal}${reason ? ` (${reason})` : ""}`
|
|
28608
|
+
);
|
|
28609
|
+
}
|
|
28610
|
+
);
|
|
28611
|
+
server.registerTool(
|
|
28612
|
+
"task_status",
|
|
28613
|
+
{
|
|
28614
|
+
title: "Task Status",
|
|
28615
|
+
description: "Check the current task memory status including goal, progress, notes, and blocker."
|
|
28616
|
+
},
|
|
28617
|
+
async () => {
|
|
28618
|
+
const ctx = runtime2.getTaskMemoryContext();
|
|
28619
|
+
if (!ctx) return asTextResponse("No active task. Start one with task_start.");
|
|
28620
|
+
return asTextResponse(ctx);
|
|
28621
|
+
}
|
|
28622
|
+
);
|
|
28623
|
+
}
|
|
28132
28624
|
function registerMacroTools(server, tabManager, runtime2) {
|
|
28133
28625
|
server.registerTool(
|
|
28134
28626
|
"fill_form",
|
|
@@ -29429,6 +29921,7 @@ function registerTools(server, tabManager, runtime2) {
|
|
|
29429
29921
|
registerSessionTools(server, tabManager, runtime2);
|
|
29430
29922
|
registerMemoryTools(server, tabManager, runtime2);
|
|
29431
29923
|
registerFlowTools(server, tabManager, runtime2);
|
|
29924
|
+
registerTaskMemoryTools(server, tabManager, runtime2);
|
|
29432
29925
|
registerMacroTools(server, tabManager, runtime2);
|
|
29433
29926
|
registerVaultTools(server, tabManager);
|
|
29434
29927
|
registerMetricsTools(server, tabManager, runtime2);
|
|
@@ -32672,6 +33165,121 @@ function formatTaskTracker(state2) {
|
|
|
32672
33165
|
${lines.join("\n")}
|
|
32673
33166
|
---`;
|
|
32674
33167
|
}
|
|
33168
|
+
function createTaskMemory(goal, options = {}) {
|
|
33169
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
33170
|
+
return {
|
|
33171
|
+
id: crypto$1.randomUUID(),
|
|
33172
|
+
goal: goal.trim(),
|
|
33173
|
+
status: "active",
|
|
33174
|
+
blocker: null,
|
|
33175
|
+
notes: [],
|
|
33176
|
+
nextStep: options.nextStep?.trim() || null,
|
|
33177
|
+
facts: { ...options.facts ?? {} },
|
|
33178
|
+
startedAt: now,
|
|
33179
|
+
updatedAt: now,
|
|
33180
|
+
completedAt: null
|
|
33181
|
+
};
|
|
33182
|
+
}
|
|
33183
|
+
function updateTaskMemory(task, patch) {
|
|
33184
|
+
const updated = {
|
|
33185
|
+
...task,
|
|
33186
|
+
nextStep: patch.nextStep !== void 0 ? patch.nextStep : task.nextStep,
|
|
33187
|
+
facts: {
|
|
33188
|
+
...task.facts,
|
|
33189
|
+
...patch.facts ?? {}
|
|
33190
|
+
},
|
|
33191
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
33192
|
+
};
|
|
33193
|
+
return updated;
|
|
33194
|
+
}
|
|
33195
|
+
function addTaskNote(task, text) {
|
|
33196
|
+
const note = {
|
|
33197
|
+
id: crypto$1.randomUUID(),
|
|
33198
|
+
text: text.trim(),
|
|
33199
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
33200
|
+
};
|
|
33201
|
+
const notes = [...task.notes, note].slice(-50);
|
|
33202
|
+
return {
|
|
33203
|
+
...task,
|
|
33204
|
+
notes,
|
|
33205
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
33206
|
+
};
|
|
33207
|
+
}
|
|
33208
|
+
function setTaskBlocker(task, blocker) {
|
|
33209
|
+
const status = blocker ? "blocked" : task.status === "blocked" ? "active" : task.status;
|
|
33210
|
+
return {
|
|
33211
|
+
...task,
|
|
33212
|
+
status,
|
|
33213
|
+
blocker,
|
|
33214
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
33215
|
+
};
|
|
33216
|
+
}
|
|
33217
|
+
function resolveTaskMemory(task, summary) {
|
|
33218
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
33219
|
+
let notes = task.notes;
|
|
33220
|
+
if (summary?.trim()) {
|
|
33221
|
+
const note = {
|
|
33222
|
+
id: crypto$1.randomUUID(),
|
|
33223
|
+
text: summary.trim(),
|
|
33224
|
+
createdAt: now
|
|
33225
|
+
};
|
|
33226
|
+
notes = [...task.notes, note].slice(-50);
|
|
33227
|
+
}
|
|
33228
|
+
return {
|
|
33229
|
+
...task,
|
|
33230
|
+
status: "completed",
|
|
33231
|
+
blocker: null,
|
|
33232
|
+
notes,
|
|
33233
|
+
completedAt: now,
|
|
33234
|
+
updatedAt: now
|
|
33235
|
+
};
|
|
33236
|
+
}
|
|
33237
|
+
function abandonTaskMemory(task, reason) {
|
|
33238
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
33239
|
+
let notes = task.notes;
|
|
33240
|
+
if (reason?.trim()) {
|
|
33241
|
+
const note = {
|
|
33242
|
+
id: crypto$1.randomUUID(),
|
|
33243
|
+
text: `Abandoned: ${reason.trim()}`,
|
|
33244
|
+
createdAt: now
|
|
33245
|
+
};
|
|
33246
|
+
notes = [...task.notes, note].slice(-50);
|
|
33247
|
+
}
|
|
33248
|
+
return {
|
|
33249
|
+
...task,
|
|
33250
|
+
status: "abandoned",
|
|
33251
|
+
blocker: null,
|
|
33252
|
+
notes,
|
|
33253
|
+
completedAt: now,
|
|
33254
|
+
updatedAt: now
|
|
33255
|
+
};
|
|
33256
|
+
}
|
|
33257
|
+
function formatTaskMemory(task) {
|
|
33258
|
+
if (!task) return "";
|
|
33259
|
+
const lines = [
|
|
33260
|
+
"--- Task Memory ---",
|
|
33261
|
+
`Goal: ${task.goal}`,
|
|
33262
|
+
`Status: ${task.status}${task.blocker ? ` (blocked: ${task.blocker})` : ""}`
|
|
33263
|
+
];
|
|
33264
|
+
if (task.nextStep) {
|
|
33265
|
+
lines.push(`Next step: ${task.nextStep}`);
|
|
33266
|
+
}
|
|
33267
|
+
if (Object.keys(task.facts).length > 0) {
|
|
33268
|
+
lines.push("Facts:");
|
|
33269
|
+
for (const [key2, value] of Object.entries(task.facts)) {
|
|
33270
|
+
lines.push(` ${key2}: ${value}`);
|
|
33271
|
+
}
|
|
33272
|
+
}
|
|
33273
|
+
if (task.notes.length > 0) {
|
|
33274
|
+
lines.push("Notes:");
|
|
33275
|
+
for (const note of task.notes.slice(-10)) {
|
|
33276
|
+
const time = note.createdAt.slice(11, 16);
|
|
33277
|
+
lines.push(` [${time}] ${note.text}`);
|
|
33278
|
+
}
|
|
33279
|
+
}
|
|
33280
|
+
lines.push("---");
|
|
33281
|
+
return lines.join("\n");
|
|
33282
|
+
}
|
|
32675
33283
|
const MAX_TRANSCRIPT_TEXT_LENGTH = 8e3;
|
|
32676
33284
|
const PERSIST_DEBOUNCE_MS = 500;
|
|
32677
33285
|
const INTERRUPTED_ACTION_STATUSES = /* @__PURE__ */ new Set([
|
|
@@ -32701,6 +33309,7 @@ function getRuntimeStatePath() {
|
|
|
32701
33309
|
}
|
|
32702
33310
|
function sanitizePersistence(persisted) {
|
|
32703
33311
|
const recoveredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
33312
|
+
const persistedTaskMemory = persisted?.taskMemory?.completedAt ? null : persisted?.taskMemory ?? null;
|
|
32704
33313
|
const actions = Array.isArray(persisted?.actions) ? persisted.actions.slice(-120).map(
|
|
32705
33314
|
(action) => INTERRUPTED_ACTION_STATUSES.has(action.status) ? {
|
|
32706
33315
|
...action,
|
|
@@ -32722,7 +33331,8 @@ function sanitizePersistence(persisted) {
|
|
|
32722
33331
|
transcript: [],
|
|
32723
33332
|
mcpStatus: "stopped",
|
|
32724
33333
|
flowState: null,
|
|
32725
|
-
taskTracker: null
|
|
33334
|
+
taskTracker: null,
|
|
33335
|
+
taskMemory: persistedTaskMemory
|
|
32726
33336
|
};
|
|
32727
33337
|
}
|
|
32728
33338
|
class AgentRuntime {
|
|
@@ -32809,7 +33419,8 @@ class AgentRuntime {
|
|
|
32809
33419
|
name: name?.trim() || `Checkpoint ${this.state.checkpoints.length + 1}`,
|
|
32810
33420
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
32811
33421
|
note: note?.trim() || void 0,
|
|
32812
|
-
snapshot: snapshot2
|
|
33422
|
+
snapshot: snapshot2,
|
|
33423
|
+
taskMemory: this.state.taskMemory ? clone(this.state.taskMemory) : null
|
|
32813
33424
|
};
|
|
32814
33425
|
this.state.checkpoints = [...this.state.checkpoints, checkpoint].slice(
|
|
32815
33426
|
-20
|
|
@@ -32822,6 +33433,7 @@ class AgentRuntime {
|
|
|
32822
33433
|
const checkpoint = this.state.checkpoints.find((item) => item.id === checkpointId) || null;
|
|
32823
33434
|
if (!checkpoint) return null;
|
|
32824
33435
|
this.tabManager.restoreSession(checkpoint.snapshot);
|
|
33436
|
+
this.state.taskMemory = checkpoint.taskMemory ? clone(checkpoint.taskMemory) : null;
|
|
32825
33437
|
this.captureSession(`Restored ${checkpoint.name}`);
|
|
32826
33438
|
return clone(checkpoint);
|
|
32827
33439
|
}
|
|
@@ -32938,6 +33550,54 @@ class AgentRuntime {
|
|
|
32938
33550
|
getTaskTrackerContext() {
|
|
32939
33551
|
return formatTaskTracker(this.state.taskTracker);
|
|
32940
33552
|
}
|
|
33553
|
+
// --- Task Memory ---
|
|
33554
|
+
startTaskMemory(goal, options) {
|
|
33555
|
+
this.state.taskMemory = createTaskMemory(goal, options);
|
|
33556
|
+
this.emit();
|
|
33557
|
+
return clone(this.state.taskMemory);
|
|
33558
|
+
}
|
|
33559
|
+
updateTaskMemory(patch) {
|
|
33560
|
+
if (!this.state.taskMemory || this.state.taskMemory.completedAt) return null;
|
|
33561
|
+
this.state.taskMemory = updateTaskMemory(this.state.taskMemory, patch);
|
|
33562
|
+
this.emit();
|
|
33563
|
+
return clone(this.state.taskMemory);
|
|
33564
|
+
}
|
|
33565
|
+
addTaskNote(text) {
|
|
33566
|
+
if (!this.state.taskMemory || this.state.taskMemory.completedAt) return null;
|
|
33567
|
+
this.state.taskMemory = addTaskNote(this.state.taskMemory, text);
|
|
33568
|
+
this.emit();
|
|
33569
|
+
return clone(this.state.taskMemory);
|
|
33570
|
+
}
|
|
33571
|
+
setTaskBlocker(blocker) {
|
|
33572
|
+
if (!this.state.taskMemory || this.state.taskMemory.completedAt) return null;
|
|
33573
|
+
this.state.taskMemory = setTaskBlocker(
|
|
33574
|
+
this.state.taskMemory,
|
|
33575
|
+
blocker
|
|
33576
|
+
);
|
|
33577
|
+
this.emit();
|
|
33578
|
+
return clone(this.state.taskMemory);
|
|
33579
|
+
}
|
|
33580
|
+
resolveTaskMemory(summary) {
|
|
33581
|
+
if (!this.state.taskMemory || this.state.taskMemory.completedAt) return null;
|
|
33582
|
+
const resolved = resolveTaskMemory(this.state.taskMemory, summary);
|
|
33583
|
+
this.state.taskMemory = null;
|
|
33584
|
+
this.emit();
|
|
33585
|
+
return clone(resolved);
|
|
33586
|
+
}
|
|
33587
|
+
abandonTaskMemory(reason) {
|
|
33588
|
+
if (!this.state.taskMemory || this.state.taskMemory.completedAt) return null;
|
|
33589
|
+
const abandoned = abandonTaskMemory(this.state.taskMemory, reason);
|
|
33590
|
+
this.state.taskMemory = null;
|
|
33591
|
+
this.emit();
|
|
33592
|
+
return clone(abandoned);
|
|
33593
|
+
}
|
|
33594
|
+
clearTaskMemory() {
|
|
33595
|
+
this.state.taskMemory = null;
|
|
33596
|
+
this.emit();
|
|
33597
|
+
}
|
|
33598
|
+
getTaskMemoryContext() {
|
|
33599
|
+
return formatTaskMemory(this.state.taskMemory);
|
|
33600
|
+
}
|
|
32941
33601
|
// --- Speedee Flow State ---
|
|
32942
33602
|
startFlow(goal, steps, startUrl) {
|
|
32943
33603
|
const flow = {
|
|
@@ -33163,7 +33823,8 @@ ${progress}
|
|
|
33163
33823
|
lastError: this.state.supervisor.lastError
|
|
33164
33824
|
},
|
|
33165
33825
|
actions: this.state.actions.slice(-120),
|
|
33166
|
-
checkpoints: this.state.checkpoints.slice(-20)
|
|
33826
|
+
checkpoints: this.state.checkpoints.slice(-20),
|
|
33827
|
+
taskMemory: this.state.taskMemory
|
|
33167
33828
|
};
|
|
33168
33829
|
return fs$1.promises.mkdir(path.dirname(getRuntimeStatePath()), { recursive: true }).then(
|
|
33169
33830
|
() => fs$1.promises.writeFile(
|