Skip to content

Commit cdbaf44

Browse files
committed
delegate: exact-repeat detection + serve-from-cache (fix #4)
Before executing each function call, computes a signature (name + JSON.stringify(args)) and checks whether it was already made this run. An exact repeat is served from resultCache instead of re-executed at all -- kills the "burns budget re-running an identical read" case for free. repeatCounts is incremented regardless (used below to detect a step that consisted ENTIRELY of repeats -- the real stuck-loop signal).
1 parent f7fbcdd commit cdbaf44

1 file changed

Lines changed: 37 additions & 16 deletions

File tree

connectors/gemini/delegate.js

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,26 +1077,47 @@ export async function runInvestigation({ task, max_steps = 20, resume_run_id })
10771077
// guards against.
10781078
const results = await Promise.all(functionCalls.map(async (part) => {
10791079
const { name, args, id } = part.functionCall;
1080-
const fn = FUNCTIONS.find((f) => f.name === name);
1080+
// Stuck-loop detection (fix #4): a signature identifies an exact
1081+
// repeat of a call already made this run. `isRepeat` reflects
1082+
// whether this signature has been SEEN before (checked before the
1083+
// increment below); repeatCounts itself is incremented regardless
1084+
// of whether it's a repeat, purely for observability/debugging --
1085+
// only the boolean matters to the stuck-loop logic further down.
1086+
const signature = `${name}:${JSON.stringify(args || {})}`;
1087+
const isRepeat = repeatCounts.has(signature);
1088+
repeatCounts.set(signature, (repeatCounts.get(signature) || 0) + 1);
1089+
10811090
let resultText;
1082-
if (!fn) {
1083-
resultText = `Error: unknown function "${name}".`;
1091+
let servedFromCache = false;
1092+
if (isRepeat && resultCache.has(signature)) {
1093+
// Exact repeat -- don't re-execute at all, just return what this
1094+
// same call returned last time. This is the free win: no network
1095+
// call, no wasted budget, regardless of whether the run as a
1096+
// whole turns out to be stuck (see allRepeatsThisStep below).
1097+
resultText = resultCache.get(signature);
1098+
servedFromCache = true;
10841099
} else {
1085-
try {
1086-
resultText = await fn.execute(args || {});
1087-
} catch (err) {
1088-
resultText = `Error: ${err?.message ?? String(err)}`;
1100+
const fn = FUNCTIONS.find((f) => f.name === name);
1101+
if (!fn) {
1102+
resultText = `Error: unknown function "${name}".`;
1103+
} else {
1104+
try {
1105+
resultText = await fn.execute(args || {});
1106+
} catch (err) {
1107+
resultText = `Error: ${err?.message ?? String(err)}`;
1108+
}
10891109
}
1110+
// Defensive: every FUNCTIONS[].execute() is expected to return a
1111+
// string. Guard against a future one accidentally returning
1112+
// something else (object, undefined, etc.) so this can't throw
1113+
// mid-transcript and take down the whole step -- see the outer
1114+
// catch below for why that matters.
1115+
if (typeof resultText !== "string") {
1116+
resultText = `Error: ${name} returned a non-string result (${typeof resultText}); this is a bug in the function's execute().`;
1117+
}
1118+
resultCache.set(signature, resultText);
10901119
}
1091-
// Defensive: every FUNCTIONS[].execute() is expected to return a
1092-
// string. Guard against a future one accidentally returning
1093-
// something else (object, undefined, etc.) so this can't throw
1094-
// mid-transcript and take down the whole step -- see the outer
1095-
// catch below for why that matters.
1096-
if (typeof resultText !== "string") {
1097-
resultText = `Error: ${name} returned a non-string result (${typeof resultText}); this is a bug in the function's execute().`;
1098-
}
1099-
return { name, args, id, resultText };
1120+
return { name, args, id, resultText, isRepeat, servedFromCache };
11001121
}));
11011122

11021123
for (const r of results) {

0 commit comments

Comments
 (0)