@tangle-network/agent-runtime 0.108.1 → 0.109.1
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/README.md +7 -6
- package/dist/{activation-DRpnplEm.js → activation-CM-L6TtL.js} +2 -2
- package/dist/{activation-DRpnplEm.js.map → activation-CM-L6TtL.js.map} +1 -1
- package/dist/agent.d.ts +2 -2
- package/dist/agent.js +2 -2
- package/dist/analyst-loop.d.ts +1 -1
- package/dist/{environment-provider-D0NXc4Qz.d.ts → environment-provider-CMqSAp-O.d.ts} +3 -2
- package/dist/environment-provider.d.ts +1 -1
- package/dist/{improvement-cycle-CRnDDdX0.js → improvement-cycle-9O4iVFOA.js} +3 -3
- package/dist/{improvement-cycle-CRnDDdX0.js.map → improvement-cycle-9O4iVFOA.js.map} +1 -1
- package/dist/{index-DQp3BPeC.d.ts → index-Bur6uUF4.d.ts} +6 -152
- package/dist/{index-cdkwHruJ.d.ts → index-Cn5TtHUG.d.ts} +3 -3
- package/dist/{index-BZbJWqoZ.d.ts → index-bXiMVrUv.d.ts} +8 -7
- package/dist/index.d.ts +8 -6
- package/dist/index.js +11 -635
- package/dist/index.js.map +1 -1
- package/dist/intelligence.d.ts +2 -2
- package/dist/intelligence.js +2 -2
- package/dist/{loops.d.ts → kernel.d.ts} +3 -3
- package/dist/{loops.js → kernel.js} +4 -4
- package/dist/{knowledge-rdrpPIXs.js → knowledge-1a38bcUT.js} +3 -3
- package/dist/{knowledge-rdrpPIXs.js.map → knowledge-1a38bcUT.js.map} +1 -1
- package/dist/knowledge.d.ts +1 -1
- package/dist/knowledge.js +1 -1
- package/dist/{loop-runner-bin-9-JlGqN7.d.ts → loop-runner-bin-CSzrWlgE.d.ts} +3 -3
- package/dist/{loop-runner-bin-B1V-XS5A.js → loop-runner-bin-V4EN9aNT.js} +3 -3
- package/dist/{loop-runner-bin-B1V-XS5A.js.map → loop-runner-bin-V4EN9aNT.js.map} +1 -1
- package/dist/loop-runner-bin.d.ts +1 -1
- package/dist/loop-runner-bin.js +1 -1
- package/dist/mcp/bin.js +1 -1
- package/dist/mcp/index.d.ts +2 -2
- package/dist/mcp/index.js +4 -4
- package/dist/{openai-tools-BjQq2TTL.js → openai-tools-DT9FH2_t.js} +2 -2
- package/dist/{openai-tools-BjQq2TTL.js.map → openai-tools-DT9FH2_t.js.map} +1 -1
- package/dist/primeintellect/index.d.ts +1 -1
- package/dist/profiles.d.ts +1 -1
- package/dist/profiles.js.map +1 -1
- package/dist/{runtime-CDekUROm.js → runtime-BVMyqgct.js} +8 -7
- package/dist/{runtime-CDekUROm.js.map → runtime-BVMyqgct.js.map} +1 -1
- package/dist/runtime-hooks-C7iJOWm3.js +99 -0
- package/dist/runtime-hooks-C7iJOWm3.js.map +1 -0
- package/dist/runtime-hooks-sbRpjStq.d.ts +88 -0
- package/dist/{structural-rollout-BC81Otmc.js → structural-rollout-ASQLr4-v.js} +2 -2
- package/dist/{structural-rollout-BC81Otmc.js.map → structural-rollout-ASQLr4-v.js.map} +1 -1
- package/dist/{supervise-JfKPwIlO.js → supervise-DyPmmhJ6.js} +6 -5
- package/dist/supervise-DyPmmhJ6.js.map +1 -0
- package/dist/{supervisor-B2LzaWRb.js → supervisor-sTJC9psT.js} +3 -98
- package/dist/supervisor-sTJC9psT.js.map +1 -0
- package/dist/testing.js +8 -8
- package/dist/tool-loop.d.ts +149 -0
- package/dist/tool-loop.js +629 -0
- package/dist/tool-loop.js.map +1 -0
- package/dist/{types-BevOjfTY.d.ts → types-DnNGJ5Gz.d.ts} +3 -88
- package/package.json +21 -10
- package/dist/supervise-JfKPwIlO.js.map +0 -1
- package/dist/supervisor-B2LzaWRb.js.map +0 -1
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
import { i as notifyRuntimeHookEvent, r as notifyRuntimeDecisionPoint } from "./runtime-hooks-C7iJOWm3.js";
|
|
2
|
+
//#region src/tool-loop.ts
|
|
3
|
+
/** Runaway-backstop: stops an infinite tool loop where cost is unmetered. Set
|
|
4
|
+
* far above any legitimate workflow — this is a watchdog, not a policy cap.
|
|
5
|
+
* Legitimate per-call budgets come from `maxCostUsd` + `costOf`. */
|
|
6
|
+
const RUNAWAY_BACKSTOP_TURNS = 200;
|
|
7
|
+
const DEFAULT_DECISION_CONTEXT_CHARS = 12e3;
|
|
8
|
+
const FAILURE_RECOVERY_ACTIONS = [
|
|
9
|
+
"retry",
|
|
10
|
+
"verify",
|
|
11
|
+
"continue",
|
|
12
|
+
"stop"
|
|
13
|
+
];
|
|
14
|
+
/** Consecutive identical calls (same tool + canonical-JSON args) that trigger
|
|
15
|
+
* stuck-loop detection. The window resets on any different call. */
|
|
16
|
+
const STUCK_LOOP_THRESHOLD = 3;
|
|
17
|
+
/** A tool-call id is required to key a `role: 'tool'` result back to its call.
|
|
18
|
+
* When the model omitted one, derive a stable id from the tool name so the
|
|
19
|
+
* assistant `tool_calls` entry and its `tool` result still match. */
|
|
20
|
+
function toolCallId(call) {
|
|
21
|
+
return call.toolCallId ?? `call_${call.toolName}`;
|
|
22
|
+
}
|
|
23
|
+
/** The assistant turn that emitted `pending`, in OpenAI shape: text content
|
|
24
|
+
* (null when the turn was tool-only) plus its `tool_calls` array. */
|
|
25
|
+
function assistantToolCallMessage(turnText, pending) {
|
|
26
|
+
return {
|
|
27
|
+
role: "assistant",
|
|
28
|
+
content: turnText.trim() || null,
|
|
29
|
+
tool_calls: pending.map((call) => ({
|
|
30
|
+
id: toolCallId(call),
|
|
31
|
+
type: "function",
|
|
32
|
+
function: {
|
|
33
|
+
name: call.toolName,
|
|
34
|
+
arguments: JSON.stringify(call.args)
|
|
35
|
+
}
|
|
36
|
+
}))
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** One `role: 'tool'` result message keyed to its call by `tool_call_id`. */
|
|
40
|
+
function toolResultMessage(call, content) {
|
|
41
|
+
return {
|
|
42
|
+
role: "tool",
|
|
43
|
+
tool_call_id: toolCallId(call),
|
|
44
|
+
content
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function defaultRender(label, outcome) {
|
|
48
|
+
if (outcome.ok) return `- ${label} → ok: ${JSON.stringify(outcome.result)}`;
|
|
49
|
+
return `- ${label} → failed (${outcome.code}): ${outcome.message}`;
|
|
50
|
+
}
|
|
51
|
+
/** Run the bounded tool loop and return the final text + every executed tool
|
|
52
|
+
* outcome. Awaitable — callers needing to stream events to a UI use
|
|
53
|
+
* {@link streamToolLoop}. */
|
|
54
|
+
async function runToolLoop(opts) {
|
|
55
|
+
const backstop = opts.maxToolTurns ?? RUNAWAY_BACKSTOP_TURNS;
|
|
56
|
+
const render = opts.renderResult ?? defaultRender;
|
|
57
|
+
const labelFor = opts.labelFor ?? ((c) => c.toolName);
|
|
58
|
+
const runId = opts.runId ?? `agent-run-${randomSuffix()}`;
|
|
59
|
+
const messages = [
|
|
60
|
+
{
|
|
61
|
+
role: "system",
|
|
62
|
+
content: opts.systemPrompt
|
|
63
|
+
},
|
|
64
|
+
...opts.priorMessages ?? [],
|
|
65
|
+
{
|
|
66
|
+
role: "user",
|
|
67
|
+
content: opts.userMessage
|
|
68
|
+
}
|
|
69
|
+
];
|
|
70
|
+
const observer = createToolLoopObserver(opts.hooks, runId, opts.scenarioId);
|
|
71
|
+
const toolResults = [];
|
|
72
|
+
let finalText = "";
|
|
73
|
+
let turns = 0;
|
|
74
|
+
let accumulatedCostUsd = 0;
|
|
75
|
+
let lastCallHash = null;
|
|
76
|
+
let consecutiveCount = 0;
|
|
77
|
+
observer.loopBefore(backstop, messages.length);
|
|
78
|
+
for (let toolTurn = 0;; toolTurn++) {
|
|
79
|
+
turns++;
|
|
80
|
+
if (opts.deadlineMs !== void 0 && Date.now() >= opts.deadlineMs) {
|
|
81
|
+
observer.loopAfter({
|
|
82
|
+
turns,
|
|
83
|
+
toolResults: toolResults.length,
|
|
84
|
+
stopReason: "deadline"
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
finalText,
|
|
88
|
+
toolResults,
|
|
89
|
+
turns,
|
|
90
|
+
stopReason: "deadline",
|
|
91
|
+
cappedOut: true
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
let turnText = "";
|
|
95
|
+
const pending = [];
|
|
96
|
+
const turnEventId = observer.turnBefore(toolTurn, messages.length);
|
|
97
|
+
for await (const ev of opts.streamTurn([...messages])) if (ev.type === "text") {
|
|
98
|
+
turnText += ev.text;
|
|
99
|
+
finalText += ev.text;
|
|
100
|
+
} else if (ev.type === "tool_call" && opts.isExecutableTool(ev.call.toolName)) pending.push(ev.call);
|
|
101
|
+
if (pending.length === 0) {
|
|
102
|
+
observer.turnAfter(toolTurn, turnEventId, {
|
|
103
|
+
pendingToolCalls: 0,
|
|
104
|
+
finalTextChars: finalText.length
|
|
105
|
+
});
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
if (toolTurn >= backstop) {
|
|
109
|
+
observer.turnAfter(toolTurn, turnEventId, {
|
|
110
|
+
pendingToolCalls: pending.length,
|
|
111
|
+
stopReason: "backstop"
|
|
112
|
+
});
|
|
113
|
+
observer.loopAfter({
|
|
114
|
+
turns,
|
|
115
|
+
toolResults: toolResults.length,
|
|
116
|
+
stopReason: "backstop"
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
finalText,
|
|
120
|
+
toolResults,
|
|
121
|
+
turns,
|
|
122
|
+
stopReason: "backstop",
|
|
123
|
+
cappedOut: true
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
messages.push(assistantToolCallMessage(turnText, pending));
|
|
127
|
+
const outcomes = [];
|
|
128
|
+
for (const [callIndex, call] of pending.entries()) {
|
|
129
|
+
const callHash = canonicalCallHash(call);
|
|
130
|
+
if (callHash === lastCallHash) consecutiveCount++;
|
|
131
|
+
else {
|
|
132
|
+
lastCallHash = callHash;
|
|
133
|
+
consecutiveCount = 1;
|
|
134
|
+
}
|
|
135
|
+
if (consecutiveCount >= STUCK_LOOP_THRESHOLD) {
|
|
136
|
+
observer.turnAfter(toolTurn, turnEventId, {
|
|
137
|
+
pendingToolCalls: pending.length,
|
|
138
|
+
stopReason: "stuck-loop"
|
|
139
|
+
});
|
|
140
|
+
observer.loopAfter({
|
|
141
|
+
turns,
|
|
142
|
+
toolResults: toolResults.length,
|
|
143
|
+
stopReason: "stuck-loop"
|
|
144
|
+
});
|
|
145
|
+
return {
|
|
146
|
+
finalText,
|
|
147
|
+
toolResults,
|
|
148
|
+
turns,
|
|
149
|
+
stopReason: "stuck-loop",
|
|
150
|
+
cappedOut: true
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const callEventId = observer.toolCallBefore(toolTurn, turnEventId, callIndex, call);
|
|
154
|
+
let outcome;
|
|
155
|
+
try {
|
|
156
|
+
outcome = await opts.executeToolCall(call);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
outcome = {
|
|
159
|
+
ok: false,
|
|
160
|
+
code: "executor_error",
|
|
161
|
+
message: err instanceof Error ? err.message : String(err)
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (opts.maxCostUsd !== void 0 && opts.costOf !== void 0) {
|
|
165
|
+
accumulatedCostUsd += opts.costOf(call, outcome);
|
|
166
|
+
if (accumulatedCostUsd >= opts.maxCostUsd) {
|
|
167
|
+
const label = labelFor(call);
|
|
168
|
+
toolResults.push({
|
|
169
|
+
call,
|
|
170
|
+
label,
|
|
171
|
+
outcome
|
|
172
|
+
});
|
|
173
|
+
messages.push(toolResultMessage(call, render(label, outcome)));
|
|
174
|
+
observer.toolCallAfter(toolTurn, callEventId, call, outcome);
|
|
175
|
+
observer.turnAfter(toolTurn, turnEventId, {
|
|
176
|
+
pendingToolCalls: pending.length,
|
|
177
|
+
stopReason: "budget"
|
|
178
|
+
});
|
|
179
|
+
observer.loopAfter({
|
|
180
|
+
turns,
|
|
181
|
+
toolResults: toolResults.length,
|
|
182
|
+
stopReason: "budget"
|
|
183
|
+
});
|
|
184
|
+
return {
|
|
185
|
+
finalText,
|
|
186
|
+
toolResults,
|
|
187
|
+
turns,
|
|
188
|
+
stopReason: "budget",
|
|
189
|
+
cappedOut: true
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const label = labelFor(call);
|
|
194
|
+
const rendered = render(label, outcome);
|
|
195
|
+
toolResults.push({
|
|
196
|
+
call,
|
|
197
|
+
label,
|
|
198
|
+
outcome
|
|
199
|
+
});
|
|
200
|
+
outcomes.push({
|
|
201
|
+
call,
|
|
202
|
+
label,
|
|
203
|
+
outcome,
|
|
204
|
+
rendered
|
|
205
|
+
});
|
|
206
|
+
messages.push(toolResultMessage(call, rendered));
|
|
207
|
+
observer.toolCallAfter(toolTurn, callEventId, call, outcome);
|
|
208
|
+
}
|
|
209
|
+
observer.failureRecovery({
|
|
210
|
+
toolTurn,
|
|
211
|
+
messages,
|
|
212
|
+
turnText,
|
|
213
|
+
outcomes
|
|
214
|
+
});
|
|
215
|
+
observer.turnAfter(toolTurn, turnEventId, {
|
|
216
|
+
pendingToolCalls: pending.length,
|
|
217
|
+
toolResults: outcomes.map((item) => ({
|
|
218
|
+
toolName: item.call.toolName,
|
|
219
|
+
toolCallId: item.call.toolCallId,
|
|
220
|
+
ok: item.outcome.ok
|
|
221
|
+
})),
|
|
222
|
+
failedToolCalls: outcomes.filter((item) => !item.outcome.ok).length
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
observer.loopAfter({
|
|
226
|
+
turns,
|
|
227
|
+
toolResults: toolResults.length,
|
|
228
|
+
stopReason: "completed"
|
|
229
|
+
});
|
|
230
|
+
return {
|
|
231
|
+
finalText,
|
|
232
|
+
toolResults,
|
|
233
|
+
turns,
|
|
234
|
+
stopReason: "completed",
|
|
235
|
+
cappedOut: false
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
/** Streaming bounded tool loop: yields each raw turn event (the caller maps +
|
|
239
|
+
* telemetries + re-emits it) and each executed `tool_result`; emits one
|
|
240
|
+
* `capped` if it stops for any non-completed reason with calls still pending. */
|
|
241
|
+
async function* streamToolLoop(opts) {
|
|
242
|
+
const backstop = opts.maxToolTurns ?? RUNAWAY_BACKSTOP_TURNS;
|
|
243
|
+
const render = opts.renderResult ?? defaultRender;
|
|
244
|
+
const labelFor = opts.labelFor ?? ((c) => c.toolName);
|
|
245
|
+
const runId = opts.runId ?? `agent-run-${randomSuffix()}`;
|
|
246
|
+
const messages = [
|
|
247
|
+
{
|
|
248
|
+
role: "system",
|
|
249
|
+
content: opts.systemPrompt
|
|
250
|
+
},
|
|
251
|
+
...opts.priorMessages ?? [],
|
|
252
|
+
{
|
|
253
|
+
role: "user",
|
|
254
|
+
content: opts.userMessage
|
|
255
|
+
}
|
|
256
|
+
];
|
|
257
|
+
const observer = createToolLoopObserver(opts.hooks, runId, opts.scenarioId);
|
|
258
|
+
let accumulatedCostUsd = 0;
|
|
259
|
+
let lastCallHash = null;
|
|
260
|
+
let consecutiveCount = 0;
|
|
261
|
+
observer.loopBefore(backstop, messages.length);
|
|
262
|
+
for (let toolTurn = 0;; toolTurn++) {
|
|
263
|
+
if (opts.deadlineMs !== void 0 && Date.now() >= opts.deadlineMs) {
|
|
264
|
+
observer.loopAfter({
|
|
265
|
+
turns: toolTurn + 1,
|
|
266
|
+
stopReason: "deadline"
|
|
267
|
+
});
|
|
268
|
+
yield {
|
|
269
|
+
kind: "capped",
|
|
270
|
+
pending: 0,
|
|
271
|
+
stopReason: "deadline"
|
|
272
|
+
};
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
let turnText = "";
|
|
276
|
+
const pending = [];
|
|
277
|
+
const turnEventId = observer.turnBefore(toolTurn, messages.length);
|
|
278
|
+
for await (const event of opts.streamTurn([...messages])) {
|
|
279
|
+
yield {
|
|
280
|
+
kind: "event",
|
|
281
|
+
event
|
|
282
|
+
};
|
|
283
|
+
turnText += opts.extractText(event);
|
|
284
|
+
const call = opts.extractToolCall(event);
|
|
285
|
+
if (call && opts.isExecutableTool(call.toolName)) pending.push(call);
|
|
286
|
+
}
|
|
287
|
+
if (pending.length === 0) {
|
|
288
|
+
observer.turnAfter(toolTurn, turnEventId, { pendingToolCalls: 0 });
|
|
289
|
+
observer.loopAfter({
|
|
290
|
+
turns: toolTurn + 1,
|
|
291
|
+
stopReason: "completed"
|
|
292
|
+
});
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (toolTurn >= backstop) {
|
|
296
|
+
observer.turnAfter(toolTurn, turnEventId, {
|
|
297
|
+
pendingToolCalls: pending.length,
|
|
298
|
+
stopReason: "backstop"
|
|
299
|
+
});
|
|
300
|
+
observer.loopAfter({
|
|
301
|
+
turns: toolTurn + 1,
|
|
302
|
+
stopReason: "backstop"
|
|
303
|
+
});
|
|
304
|
+
yield {
|
|
305
|
+
kind: "capped",
|
|
306
|
+
pending: pending.length,
|
|
307
|
+
stopReason: "backstop"
|
|
308
|
+
};
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
messages.push(assistantToolCallMessage(turnText, pending));
|
|
312
|
+
const outcomes = [];
|
|
313
|
+
for (const [callIndex, call] of pending.entries()) {
|
|
314
|
+
const callHash = canonicalCallHash(call);
|
|
315
|
+
if (callHash === lastCallHash) consecutiveCount++;
|
|
316
|
+
else {
|
|
317
|
+
lastCallHash = callHash;
|
|
318
|
+
consecutiveCount = 1;
|
|
319
|
+
}
|
|
320
|
+
if (consecutiveCount >= STUCK_LOOP_THRESHOLD) {
|
|
321
|
+
observer.turnAfter(toolTurn, turnEventId, {
|
|
322
|
+
pendingToolCalls: pending.length,
|
|
323
|
+
stopReason: "stuck-loop"
|
|
324
|
+
});
|
|
325
|
+
observer.loopAfter({
|
|
326
|
+
turns: toolTurn + 1,
|
|
327
|
+
stopReason: "stuck-loop"
|
|
328
|
+
});
|
|
329
|
+
yield {
|
|
330
|
+
kind: "capped",
|
|
331
|
+
pending: pending.length,
|
|
332
|
+
stopReason: "stuck-loop"
|
|
333
|
+
};
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const callEventId = observer.toolCallBefore(toolTurn, turnEventId, callIndex, call);
|
|
337
|
+
let outcome;
|
|
338
|
+
try {
|
|
339
|
+
outcome = await opts.executeToolCall(call);
|
|
340
|
+
} catch (err) {
|
|
341
|
+
outcome = {
|
|
342
|
+
ok: false,
|
|
343
|
+
code: "executor_error",
|
|
344
|
+
message: err instanceof Error ? err.message : String(err)
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
if (opts.maxCostUsd !== void 0 && opts.costOf !== void 0) {
|
|
348
|
+
accumulatedCostUsd += opts.costOf(call, outcome);
|
|
349
|
+
if (accumulatedCostUsd >= opts.maxCostUsd) {
|
|
350
|
+
const label = labelFor(call);
|
|
351
|
+
yield {
|
|
352
|
+
kind: "tool_result",
|
|
353
|
+
toolName: call.toolName,
|
|
354
|
+
toolCallId: call.toolCallId,
|
|
355
|
+
label,
|
|
356
|
+
outcome
|
|
357
|
+
};
|
|
358
|
+
messages.push(toolResultMessage(call, render(label, outcome)));
|
|
359
|
+
observer.toolCallAfter(toolTurn, callEventId, call, outcome);
|
|
360
|
+
observer.turnAfter(toolTurn, turnEventId, {
|
|
361
|
+
pendingToolCalls: pending.length,
|
|
362
|
+
stopReason: "budget"
|
|
363
|
+
});
|
|
364
|
+
observer.loopAfter({
|
|
365
|
+
turns: toolTurn + 1,
|
|
366
|
+
stopReason: "budget"
|
|
367
|
+
});
|
|
368
|
+
yield {
|
|
369
|
+
kind: "capped",
|
|
370
|
+
pending: pending.length,
|
|
371
|
+
stopReason: "budget"
|
|
372
|
+
};
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const label = labelFor(call);
|
|
377
|
+
yield {
|
|
378
|
+
kind: "tool_result",
|
|
379
|
+
toolName: call.toolName,
|
|
380
|
+
toolCallId: call.toolCallId,
|
|
381
|
+
label,
|
|
382
|
+
outcome
|
|
383
|
+
};
|
|
384
|
+
const rendered = render(label, outcome);
|
|
385
|
+
outcomes.push({
|
|
386
|
+
call,
|
|
387
|
+
label,
|
|
388
|
+
outcome,
|
|
389
|
+
rendered
|
|
390
|
+
});
|
|
391
|
+
messages.push(toolResultMessage(call, rendered));
|
|
392
|
+
observer.toolCallAfter(toolTurn, callEventId, call, outcome);
|
|
393
|
+
}
|
|
394
|
+
observer.failureRecovery({
|
|
395
|
+
toolTurn,
|
|
396
|
+
messages,
|
|
397
|
+
turnText,
|
|
398
|
+
outcomes
|
|
399
|
+
});
|
|
400
|
+
observer.turnAfter(toolTurn, turnEventId, {
|
|
401
|
+
pendingToolCalls: pending.length,
|
|
402
|
+
toolResults: outcomes.map((item) => ({
|
|
403
|
+
toolName: item.call.toolName,
|
|
404
|
+
toolCallId: item.call.toolCallId,
|
|
405
|
+
ok: item.outcome.ok
|
|
406
|
+
})),
|
|
407
|
+
failedToolCalls: outcomes.filter((item) => !item.outcome.ok).length
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function createToolLoopObserver(hooks, runId, scenarioId) {
|
|
412
|
+
const loopEventId = `${runId}:agent.run`;
|
|
413
|
+
return {
|
|
414
|
+
loopBefore: (maxToolTurns, messageCount) => {
|
|
415
|
+
notifyToolLoopEvent({
|
|
416
|
+
hooks,
|
|
417
|
+
runId,
|
|
418
|
+
scenarioId,
|
|
419
|
+
target: "agent.run",
|
|
420
|
+
phase: "before",
|
|
421
|
+
id: `${loopEventId}:before`,
|
|
422
|
+
payload: {
|
|
423
|
+
maxToolTurns,
|
|
424
|
+
messageCount
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
},
|
|
428
|
+
loopAfter: (payload) => {
|
|
429
|
+
notifyToolLoopEvent({
|
|
430
|
+
hooks,
|
|
431
|
+
runId,
|
|
432
|
+
scenarioId,
|
|
433
|
+
target: "agent.run",
|
|
434
|
+
phase: "after",
|
|
435
|
+
id: `${loopEventId}:after`,
|
|
436
|
+
payload
|
|
437
|
+
});
|
|
438
|
+
},
|
|
439
|
+
turnBefore: (toolTurn, messageCount) => {
|
|
440
|
+
const turnEventId = `${loopEventId}:${toolTurn}`;
|
|
441
|
+
notifyToolLoopEvent({
|
|
442
|
+
hooks,
|
|
443
|
+
runId,
|
|
444
|
+
scenarioId,
|
|
445
|
+
target: "agent.turn",
|
|
446
|
+
phase: "before",
|
|
447
|
+
id: turnEventId,
|
|
448
|
+
stepIndex: toolTurn,
|
|
449
|
+
parentId: loopEventId,
|
|
450
|
+
payload: { messageCount }
|
|
451
|
+
});
|
|
452
|
+
return turnEventId;
|
|
453
|
+
},
|
|
454
|
+
turnAfter: (toolTurn, turnEventId, payload) => {
|
|
455
|
+
notifyToolLoopEvent({
|
|
456
|
+
hooks,
|
|
457
|
+
runId,
|
|
458
|
+
scenarioId,
|
|
459
|
+
target: "agent.turn",
|
|
460
|
+
phase: "after",
|
|
461
|
+
id: `${turnEventId}:after`,
|
|
462
|
+
stepIndex: toolTurn,
|
|
463
|
+
parentId: turnEventId,
|
|
464
|
+
payload
|
|
465
|
+
});
|
|
466
|
+
},
|
|
467
|
+
toolCallBefore: (toolTurn, turnEventId, callIndex, call) => {
|
|
468
|
+
const callEventId = `${turnEventId}:tool-call:${callIndex}`;
|
|
469
|
+
notifyToolLoopEvent({
|
|
470
|
+
hooks,
|
|
471
|
+
runId,
|
|
472
|
+
scenarioId,
|
|
473
|
+
target: "agent.tool_call",
|
|
474
|
+
phase: "before",
|
|
475
|
+
id: callEventId,
|
|
476
|
+
stepIndex: toolTurn,
|
|
477
|
+
parentId: turnEventId,
|
|
478
|
+
payload: toolCallPayload(call)
|
|
479
|
+
});
|
|
480
|
+
return callEventId;
|
|
481
|
+
},
|
|
482
|
+
toolCallAfter: (toolTurn, callEventId, call, outcome) => {
|
|
483
|
+
notifyToolLoopEvent({
|
|
484
|
+
hooks,
|
|
485
|
+
runId,
|
|
486
|
+
scenarioId,
|
|
487
|
+
target: "agent.tool_call",
|
|
488
|
+
phase: "after",
|
|
489
|
+
id: `${callEventId}:after`,
|
|
490
|
+
stepIndex: toolTurn,
|
|
491
|
+
parentId: callEventId,
|
|
492
|
+
payload: {
|
|
493
|
+
...toolCallPayload(call),
|
|
494
|
+
outcome: outcomePayload(outcome)
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
},
|
|
498
|
+
failureRecovery: (options) => {
|
|
499
|
+
notifyToolFailureRecovery({
|
|
500
|
+
hooks,
|
|
501
|
+
runId,
|
|
502
|
+
scenarioId,
|
|
503
|
+
stepIndex: options.toolTurn,
|
|
504
|
+
messages: options.messages,
|
|
505
|
+
turnText: options.turnText,
|
|
506
|
+
outcomes: options.outcomes
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function notifyToolLoopEvent(options) {
|
|
512
|
+
notifyRuntimeHookEvent(options.hooks, {
|
|
513
|
+
id: options.id ?? `${options.runId}:${options.target}:${options.phase}`,
|
|
514
|
+
runId: options.runId,
|
|
515
|
+
scenarioId: options.scenarioId,
|
|
516
|
+
target: options.target,
|
|
517
|
+
phase: options.phase,
|
|
518
|
+
timestamp: Date.now(),
|
|
519
|
+
stepIndex: options.stepIndex,
|
|
520
|
+
parentId: options.parentId,
|
|
521
|
+
payload: options.payload,
|
|
522
|
+
metadata: {
|
|
523
|
+
producer: "tool-loop",
|
|
524
|
+
...options.metadata
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
function notifyToolFailureRecovery(options) {
|
|
529
|
+
const failed = options.outcomes.filter((item) => !item.outcome.ok);
|
|
530
|
+
if (failed.length === 0) return;
|
|
531
|
+
const evidence = [];
|
|
532
|
+
for (const item of failed) {
|
|
533
|
+
const id = item.call.toolCallId ?? `${options.stepIndex}:${item.label}`;
|
|
534
|
+
evidence.push({
|
|
535
|
+
source: "tool_call",
|
|
536
|
+
id,
|
|
537
|
+
detail: `${item.call.toolName} ${stringifySafe(item.call.args, 2e3)}`,
|
|
538
|
+
metadata: {
|
|
539
|
+
toolName: item.call.toolName,
|
|
540
|
+
label: item.label
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
evidence.push({
|
|
544
|
+
source: "tool_result",
|
|
545
|
+
id: `${id}:result`,
|
|
546
|
+
detail: item.rendered,
|
|
547
|
+
metadata: failureMetadata(item.outcome)
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
notifyRuntimeDecisionPoint(options.hooks, {
|
|
551
|
+
id: `${options.runId}:agent.turn:${options.stepIndex}:failure-recovery`,
|
|
552
|
+
runId: options.runId,
|
|
553
|
+
scenarioId: options.scenarioId,
|
|
554
|
+
stepIndex: options.stepIndex,
|
|
555
|
+
kind: "retry",
|
|
556
|
+
candidateActions: [...FAILURE_RECOVERY_ACTIONS],
|
|
557
|
+
context: renderDecisionContext(options.messages, options.turnText, options.outcomes),
|
|
558
|
+
evidence,
|
|
559
|
+
metadata: {
|
|
560
|
+
target: "failure-recovery",
|
|
561
|
+
source: "agent.turn",
|
|
562
|
+
failedToolCount: failed.length,
|
|
563
|
+
toolNames: failed.map((item) => item.call.toolName)
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
function toolCallPayload(call) {
|
|
568
|
+
return {
|
|
569
|
+
toolName: call.toolName,
|
|
570
|
+
toolCallId: call.toolCallId,
|
|
571
|
+
argsPreview: stringifySafe(call.args, 2e3)
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
function outcomePayload(outcome) {
|
|
575
|
+
if (!outcome.ok) return {
|
|
576
|
+
ok: false,
|
|
577
|
+
code: outcome.code,
|
|
578
|
+
message: trimText(outcome.message, 2e3),
|
|
579
|
+
status: outcome.status
|
|
580
|
+
};
|
|
581
|
+
return {
|
|
582
|
+
ok: true,
|
|
583
|
+
resultPreview: stringifySafe(outcome.result, 2e3)
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
function failureMetadata(outcome) {
|
|
587
|
+
if (outcome.ok) return void 0;
|
|
588
|
+
return {
|
|
589
|
+
code: outcome.code,
|
|
590
|
+
message: outcome.message,
|
|
591
|
+
status: outcome.status
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function renderDecisionContext(messages, turnText, outcomes) {
|
|
595
|
+
const recent = messages.slice(-6).map((message) => `[${message.role}]\n${message.content ?? ""}`);
|
|
596
|
+
const assistant = turnText.trim() ? [`[assistant]\n${turnText}`] : [];
|
|
597
|
+
const toolResults = [`[tool results]\n${outcomes.map((item) => item.rendered).join("\n")}`];
|
|
598
|
+
return trimText([
|
|
599
|
+
...recent,
|
|
600
|
+
...assistant,
|
|
601
|
+
...toolResults
|
|
602
|
+
].join("\n\n"), DEFAULT_DECISION_CONTEXT_CHARS);
|
|
603
|
+
}
|
|
604
|
+
/** Canonical identifier for a tool call used by stuck-loop detection.
|
|
605
|
+
* Keys are sorted so `{b:1,a:2}` and `{a:2,b:1}` produce the same hash. */
|
|
606
|
+
function canonicalCallHash(call) {
|
|
607
|
+
const sortedArgs = Object.fromEntries(Object.entries(call.args).sort(([a], [b]) => a.localeCompare(b)));
|
|
608
|
+
return `${call.toolName}:${JSON.stringify(sortedArgs)}`;
|
|
609
|
+
}
|
|
610
|
+
function stringifySafe(value, max) {
|
|
611
|
+
let text;
|
|
612
|
+
try {
|
|
613
|
+
text = JSON.stringify(value) ?? String(value);
|
|
614
|
+
} catch {
|
|
615
|
+
text = String(value);
|
|
616
|
+
}
|
|
617
|
+
return trimText(text, max);
|
|
618
|
+
}
|
|
619
|
+
function trimText(text, max) {
|
|
620
|
+
if (text.length <= max) return text;
|
|
621
|
+
return `${text.slice(0, max)}…`;
|
|
622
|
+
}
|
|
623
|
+
function randomSuffix(len = 8) {
|
|
624
|
+
return Math.random().toString(36).slice(2, 2 + len);
|
|
625
|
+
}
|
|
626
|
+
//#endregion
|
|
627
|
+
export { runToolLoop, streamToolLoop };
|
|
628
|
+
|
|
629
|
+
//# sourceMappingURL=tool-loop.js.map
|