@k2b/nessi 0.10.0-rc.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/LICENSE +21 -0
- package/README.md +251 -0
- package/aggregates.d.ts +7 -0
- package/aggregates.js +115 -0
- package/ai/complete-from-stream.d.ts +2 -0
- package/ai/complete-from-stream.js +36 -0
- package/ai/index.d.ts +10 -0
- package/ai/index.js +9 -0
- package/ai/providers/anthropic.d.ts +13 -0
- package/ai/providers/anthropic.js +266 -0
- package/ai/providers/gemini.d.ts +12 -0
- package/ai/providers/gemini.js +192 -0
- package/ai/providers/mistral.d.ts +12 -0
- package/ai/providers/mistral.js +287 -0
- package/ai/providers/ollama.d.ts +10 -0
- package/ai/providers/ollama.js +241 -0
- package/ai/providers/openai-compatible.d.ts +2 -0
- package/ai/providers/openai-compatible.js +349 -0
- package/ai/providers/openai.d.ts +12 -0
- package/ai/providers/openai.js +22 -0
- package/ai/providers/openrouter.d.ts +13 -0
- package/ai/providers/openrouter.js +28 -0
- package/ai/providers/vllm.d.ts +11 -0
- package/ai/providers/vllm.js +22 -0
- package/ai/shared/errors.d.ts +15 -0
- package/ai/shared/errors.js +56 -0
- package/ai/shared/json.d.ts +3 -0
- package/ai/shared/json.js +15 -0
- package/ai/shared/messages.d.ts +15 -0
- package/ai/shared/messages.js +58 -0
- package/ai/shared/ndjson.d.ts +4 -0
- package/ai/shared/ndjson.js +60 -0
- package/ai/shared/sse.d.ts +15 -0
- package/ai/shared/sse.js +79 -0
- package/ai/shared/stream-helpers.d.ts +13 -0
- package/ai/shared/stream-helpers.js +105 -0
- package/ai/shared/tool-call-ids.d.ts +5 -0
- package/ai/shared/tool-call-ids.js +38 -0
- package/ai/shared/tool-stream-normalizer.d.ts +6 -0
- package/ai/shared/tool-stream-normalizer.js +271 -0
- package/ai/shared/tools.d.ts +29 -0
- package/ai/shared/tools.js +25 -0
- package/ai/shared/usage.d.ts +3 -0
- package/ai/shared/usage.js +5 -0
- package/ai/types.d.ts +252 -0
- package/ai/types.js +0 -0
- package/compact.d.ts +5 -0
- package/compact.js +108 -0
- package/index.d.ts +11 -0
- package/index.js +12 -0
- package/nessi.d.ts +2 -0
- package/nessi.js +1250 -0
- package/package.json +80 -0
- package/providers/ollama.d.ts +2 -0
- package/providers/ollama.js +1 -0
- package/providers/openai.d.ts +2 -0
- package/providers/openai.js +1 -0
- package/providers/openrouter.d.ts +2 -0
- package/providers/openrouter.js +1 -0
- package/stores.d.ts +11 -0
- package/stores.js +42 -0
- package/structured.d.ts +9 -0
- package/structured.js +413 -0
- package/tools.d.ts +25 -0
- package/tools.js +36 -0
- package/types.d.ts +290 -0
- package/types.js +3 -0
- package/utils.d.ts +15 -0
- package/utils.js +47 -0
package/nessi.js
ADDED
|
@@ -0,0 +1,1250 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// nessi - Core Loop
|
|
3
|
+
// ============================================================================
|
|
4
|
+
import { aggregateFromTurns, buildLoopTiming, cloneUsage } from "./aggregates.js";
|
|
5
|
+
import { appendAssistantContentBlock, buildAssistantMessageFromContent } from "./ai/shared/messages.js";
|
|
6
|
+
import { toolToSpec } from "./tools.js";
|
|
7
|
+
import { createLoopId, projectHistoricalToolResults, toErrorMessage, truncateToolResults, zeroUsage, } from "./utils.js";
|
|
8
|
+
class PullCancelledError extends Error {
|
|
9
|
+
constructor() {
|
|
10
|
+
super("channel pull cancelled");
|
|
11
|
+
this.name = "PullCancelledError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
class ToolExecutionFailure extends Error {
|
|
15
|
+
issue;
|
|
16
|
+
constructor(issue) {
|
|
17
|
+
super(issue.message);
|
|
18
|
+
this.name = "ToolExecutionFailure";
|
|
19
|
+
this.issue = issue;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const createChannel = () => {
|
|
23
|
+
const queue = [];
|
|
24
|
+
const waiters = [];
|
|
25
|
+
return {
|
|
26
|
+
push(value) {
|
|
27
|
+
const waiter = waiters.shift();
|
|
28
|
+
if (waiter) {
|
|
29
|
+
waiter.cleanup?.();
|
|
30
|
+
waiter.resolve(value);
|
|
31
|
+
}
|
|
32
|
+
else
|
|
33
|
+
queue.push(value);
|
|
34
|
+
},
|
|
35
|
+
pull(signal) {
|
|
36
|
+
const queued = queue.shift();
|
|
37
|
+
if (queued !== undefined)
|
|
38
|
+
return Promise.resolve(queued);
|
|
39
|
+
if (signal?.aborted)
|
|
40
|
+
return Promise.reject(new PullCancelledError());
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const waiter = { resolve, reject };
|
|
43
|
+
const cancel = () => {
|
|
44
|
+
const index = waiters.indexOf(waiter);
|
|
45
|
+
if (index >= 0)
|
|
46
|
+
waiters.splice(index, 1);
|
|
47
|
+
waiter.cleanup?.();
|
|
48
|
+
reject(new PullCancelledError());
|
|
49
|
+
};
|
|
50
|
+
if (signal) {
|
|
51
|
+
waiter.cleanup = () => signal.removeEventListener("abort", cancel);
|
|
52
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
53
|
+
}
|
|
54
|
+
waiters.push(waiter);
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
drain() {
|
|
58
|
+
return queue.splice(0, queue.length);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
// ----------------------------------------------------------------------------
|
|
63
|
+
// Input normalization
|
|
64
|
+
// ----------------------------------------------------------------------------
|
|
65
|
+
const normalizeInput = (input) => {
|
|
66
|
+
if (typeof input === "string")
|
|
67
|
+
return { role: "user", content: [{ type: "text", text: input }] };
|
|
68
|
+
return {
|
|
69
|
+
role: "user",
|
|
70
|
+
content: input.map((part) => (typeof part === "string" ? { type: "text", text: part } : part)),
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
// ----------------------------------------------------------------------------
|
|
74
|
+
// Debug and issue helpers
|
|
75
|
+
// ----------------------------------------------------------------------------
|
|
76
|
+
const formatDebugJson = (value, maxLength = 2400) => {
|
|
77
|
+
try {
|
|
78
|
+
const text = JSON.stringify(value, null, 2) ?? String(value);
|
|
79
|
+
if (text.length <= maxLength)
|
|
80
|
+
return text;
|
|
81
|
+
return `${text.slice(0, maxLength)}\n... truncated`;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return String(value);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const formatToolValidationError = (tool, args, error) => {
|
|
88
|
+
const issues = error.issues.length > 0
|
|
89
|
+
? error.issues
|
|
90
|
+
.map((rawIssue, index) => {
|
|
91
|
+
const issue = rawIssue;
|
|
92
|
+
const path = Array.isArray(issue.path) && issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
93
|
+
const code = typeof issue.code === "string" ? issue.code : "unknown";
|
|
94
|
+
const message = typeof issue.message === "string" ? issue.message : "Validation failed";
|
|
95
|
+
const expected = typeof issue.expected === "string" ? `, expected ${issue.expected}` : "";
|
|
96
|
+
const received = Object.prototype.hasOwnProperty.call(issue, "input")
|
|
97
|
+
? `, received ${formatDebugJson(issue.input, 200).replace(/\s+/g, " ")}`
|
|
98
|
+
: "";
|
|
99
|
+
return `${index + 1}. ${path}: ${message} [${code}${expected}${received}]`;
|
|
100
|
+
})
|
|
101
|
+
.join("\n")
|
|
102
|
+
: "No detailed issues reported.";
|
|
103
|
+
return [
|
|
104
|
+
`Validation error for tool "${tool.def.name}"`,
|
|
105
|
+
"",
|
|
106
|
+
"Issues:",
|
|
107
|
+
issues,
|
|
108
|
+
"",
|
|
109
|
+
"Received args:",
|
|
110
|
+
formatDebugJson(args),
|
|
111
|
+
"",
|
|
112
|
+
"Expected input schema:",
|
|
113
|
+
formatDebugJson(toolToSpec(tool).inputSchema),
|
|
114
|
+
].join("\n");
|
|
115
|
+
};
|
|
116
|
+
const createTurnId = (loopId, turnIndex, suffix = "turn") => `${loopId}:${suffix}:${turnIndex}`;
|
|
117
|
+
const isToolStreamIssue = (issue) => issue.kind === "malformed_tool_call" || issue.kind === "cancelled_tool_call";
|
|
118
|
+
const toolExecutionIssue = (reason, message, call) => ({
|
|
119
|
+
kind: "tool_execution_error",
|
|
120
|
+
reason,
|
|
121
|
+
message,
|
|
122
|
+
retryable: false,
|
|
123
|
+
callId: call.id,
|
|
124
|
+
name: call.name,
|
|
125
|
+
});
|
|
126
|
+
const toolTimeoutIssue = (call, timeoutMs) => ({
|
|
127
|
+
kind: "timeout",
|
|
128
|
+
scope: "tool",
|
|
129
|
+
message: `Tool "${call.name}" timed out after ${timeoutMs}ms.`,
|
|
130
|
+
retryable: false,
|
|
131
|
+
callId: call.id,
|
|
132
|
+
name: call.name,
|
|
133
|
+
});
|
|
134
|
+
const runtimeIssue = (error) => ({
|
|
135
|
+
kind: "runtime_error",
|
|
136
|
+
message: toErrorMessage(error),
|
|
137
|
+
retryable: false,
|
|
138
|
+
});
|
|
139
|
+
const issueToToolResult = (issue) => issue.message;
|
|
140
|
+
const timeoutMsFor = (tool) => {
|
|
141
|
+
const timeoutMs = tool.def.timeoutMs;
|
|
142
|
+
return typeof timeoutMs === "number" && timeoutMs > 0 ? timeoutMs : undefined;
|
|
143
|
+
};
|
|
144
|
+
const withTimeout = async (run, timeoutMs, onTimeout) => {
|
|
145
|
+
if (!timeoutMs)
|
|
146
|
+
return { ok: true, value: await run() };
|
|
147
|
+
const timeoutController = new AbortController();
|
|
148
|
+
let timeout;
|
|
149
|
+
let timedOut = false;
|
|
150
|
+
try {
|
|
151
|
+
timeout = setTimeout(() => {
|
|
152
|
+
timedOut = true;
|
|
153
|
+
onTimeout();
|
|
154
|
+
timeoutController.abort();
|
|
155
|
+
}, timeoutMs);
|
|
156
|
+
return { ok: true, value: await run(timeoutController.signal) };
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
if (timedOut && error instanceof PullCancelledError)
|
|
160
|
+
return { ok: false };
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
if (timeout)
|
|
165
|
+
clearTimeout(timeout);
|
|
166
|
+
timeoutController.abort();
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
class LoopAbortedError extends Error {
|
|
170
|
+
constructor() {
|
|
171
|
+
super("nessi loop aborted");
|
|
172
|
+
this.name = "LoopAbortedError";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const linkedAbortSignal = (signals) => {
|
|
176
|
+
const activeSignals = signals.filter((signal) => Boolean(signal));
|
|
177
|
+
const controller = new AbortController();
|
|
178
|
+
const listeners = [];
|
|
179
|
+
for (const signal of activeSignals) {
|
|
180
|
+
const abort = () => controller.abort(signal.reason);
|
|
181
|
+
if (signal.aborted) {
|
|
182
|
+
abort();
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
186
|
+
listeners.push(() => signal.removeEventListener("abort", abort));
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
signal: controller.signal,
|
|
190
|
+
cleanup() {
|
|
191
|
+
for (const remove of listeners)
|
|
192
|
+
remove();
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
// ----------------------------------------------------------------------------
|
|
197
|
+
// Aggregate reconstruction
|
|
198
|
+
// ----------------------------------------------------------------------------
|
|
199
|
+
const aggregateTurnsFromEntries = (entries) => {
|
|
200
|
+
const messages = entries.filter((entry) => entry.kind === "message").map((entry) => entry.message);
|
|
201
|
+
const lastAssistantIdx = messages.findLastIndex((message) => message.role === "assistant");
|
|
202
|
+
if (lastAssistantIdx < 0)
|
|
203
|
+
return [];
|
|
204
|
+
let start = 0;
|
|
205
|
+
for (let i = lastAssistantIdx - 1; i >= 0; i--) {
|
|
206
|
+
if (messages[i]?.role === "user") {
|
|
207
|
+
start = i + 1;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const turns = [];
|
|
212
|
+
for (let i = start; i < messages.length; i++) {
|
|
213
|
+
const message = messages[i];
|
|
214
|
+
if (message?.role !== "assistant")
|
|
215
|
+
continue;
|
|
216
|
+
const toolCalls = message.content
|
|
217
|
+
.filter((block) => block.type === "tool_call")
|
|
218
|
+
.map((block) => ({
|
|
219
|
+
callId: block.id,
|
|
220
|
+
name: block.name,
|
|
221
|
+
args: block.args,
|
|
222
|
+
}));
|
|
223
|
+
const byId = new Map(toolCalls.map((toolCall) => [toolCall.callId, toolCall]));
|
|
224
|
+
for (let j = i + 1; j < messages.length; j++) {
|
|
225
|
+
const next = messages[j];
|
|
226
|
+
if (!next || next.role === "assistant" || next.role === "user")
|
|
227
|
+
break;
|
|
228
|
+
const toolCall = byId.get(next.callId);
|
|
229
|
+
if (toolCall) {
|
|
230
|
+
toolCall.result = next.result;
|
|
231
|
+
toolCall.isError = next.isError;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
turns.push({
|
|
235
|
+
message,
|
|
236
|
+
usage: cloneUsage(message.usage),
|
|
237
|
+
stopReason: message.stopReason,
|
|
238
|
+
toolCalls,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return turns;
|
|
242
|
+
};
|
|
243
|
+
const canMergeDelta = (left, right) => left.agentId === right.agentId
|
|
244
|
+
&& left.loopId === right.loopId
|
|
245
|
+
&& left.turnId === right.turnId
|
|
246
|
+
&& left.blockId === right.blockId;
|
|
247
|
+
const coalesceOutboundEvents = async function* (source, options) {
|
|
248
|
+
const maxChars = typeof options.maxChars === "number" && options.maxChars > 0 ? options.maxChars : undefined;
|
|
249
|
+
const ms = typeof options.ms === "number" && options.ms > 0 ? options.ms : undefined;
|
|
250
|
+
if (!maxChars && !ms) {
|
|
251
|
+
yield* source;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
255
|
+
let next = iterator.next();
|
|
256
|
+
let buffer;
|
|
257
|
+
let timer;
|
|
258
|
+
let timerSeq = 0;
|
|
259
|
+
const clearTimer = () => {
|
|
260
|
+
timer = undefined;
|
|
261
|
+
timerSeq++;
|
|
262
|
+
};
|
|
263
|
+
const startTimer = () => {
|
|
264
|
+
if (!ms || timer)
|
|
265
|
+
return;
|
|
266
|
+
const seq = ++timerSeq;
|
|
267
|
+
timer = new Promise((resolve) => setTimeout(() => resolve({ type: "timer", seq }), ms));
|
|
268
|
+
};
|
|
269
|
+
const flush = function* () {
|
|
270
|
+
if (!buffer)
|
|
271
|
+
return;
|
|
272
|
+
const event = buffer;
|
|
273
|
+
buffer = undefined;
|
|
274
|
+
clearTimer();
|
|
275
|
+
yield event;
|
|
276
|
+
};
|
|
277
|
+
while (true) {
|
|
278
|
+
const raced = await (timer
|
|
279
|
+
? Promise.race([
|
|
280
|
+
next.then((result) => ({ type: "event", result })),
|
|
281
|
+
timer,
|
|
282
|
+
])
|
|
283
|
+
: next.then((result) => ({ type: "event", result })));
|
|
284
|
+
if (raced.type === "timer") {
|
|
285
|
+
if (raced.seq !== timerSeq)
|
|
286
|
+
continue;
|
|
287
|
+
yield* flush();
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const { result } = raced;
|
|
291
|
+
if (result.done) {
|
|
292
|
+
yield* flush();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
next = iterator.next();
|
|
296
|
+
const event = result.value;
|
|
297
|
+
if (event.type !== "block_delta") {
|
|
298
|
+
yield* flush();
|
|
299
|
+
yield event;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (!buffer) {
|
|
303
|
+
buffer = event;
|
|
304
|
+
startTimer();
|
|
305
|
+
}
|
|
306
|
+
else if (canMergeDelta(buffer, event)) {
|
|
307
|
+
buffer = { ...buffer, delta: buffer.delta + event.delta };
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
yield* flush();
|
|
311
|
+
buffer = event;
|
|
312
|
+
startTimer();
|
|
313
|
+
}
|
|
314
|
+
if (maxChars && buffer.delta.length >= maxChars) {
|
|
315
|
+
yield* flush();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
// ----------------------------------------------------------------------------
|
|
320
|
+
// nessi()
|
|
321
|
+
// ----------------------------------------------------------------------------
|
|
322
|
+
export const nessi = (options) => {
|
|
323
|
+
const { agentId = "main", loopId: requestedLoopId, input, provider, systemPrompt, tools = [], store, creditStore, compact, steering, maxTurns = Infinity, temperature, maxOutputTokens, disableReasoning, coalesce, maxToolResultChars, signal: externalSignal, } = options;
|
|
324
|
+
const channel = createChannel();
|
|
325
|
+
const deferredInbound = [];
|
|
326
|
+
const steerQueue = [];
|
|
327
|
+
const subscribers = [];
|
|
328
|
+
const abortController = new AbortController();
|
|
329
|
+
let lastUsage = zeroUsage();
|
|
330
|
+
const loopTurns = [];
|
|
331
|
+
const loopIssues = [];
|
|
332
|
+
const loopId = requestedLoopId?.trim() ? requestedLoopId : createLoopId();
|
|
333
|
+
const timing = {
|
|
334
|
+
generationMs: 0,
|
|
335
|
+
toolExecutionMs: 0,
|
|
336
|
+
actionWaitMs: 0,
|
|
337
|
+
};
|
|
338
|
+
const measureGeneration = async (run) => {
|
|
339
|
+
const startedAt = nowMs();
|
|
340
|
+
try {
|
|
341
|
+
return await run();
|
|
342
|
+
}
|
|
343
|
+
finally {
|
|
344
|
+
timing.generationMs += elapsedSince(startedAt);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
const waitForActionResponse = async (startedAt, run) => {
|
|
348
|
+
try {
|
|
349
|
+
return await run();
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
if (startedAt !== undefined)
|
|
353
|
+
timing.actionWaitMs += elapsedSince(startedAt);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
const recordToolExecution = (startedAt, actionWaitMsAtStart) => {
|
|
357
|
+
const elapsedMs = elapsedSince(startedAt);
|
|
358
|
+
const nestedActionWaitMs = Math.max(0, timing.actionWaitMs - actionWaitMsAtStart);
|
|
359
|
+
timing.toolExecutionMs += Math.max(0, elapsedMs - nestedActionWaitMs);
|
|
360
|
+
};
|
|
361
|
+
const snapshotAggregate = () => {
|
|
362
|
+
const aggregate = aggregateFromTurns(loopTurns, loopIssues);
|
|
363
|
+
return { ...aggregate, timing: snapshotTiming(timing, aggregate.usage) };
|
|
364
|
+
};
|
|
365
|
+
const loopEndEvent = (reason) => ({
|
|
366
|
+
type: "loop_end",
|
|
367
|
+
agentId,
|
|
368
|
+
loopId,
|
|
369
|
+
reason,
|
|
370
|
+
aggregate: snapshotAggregate(),
|
|
371
|
+
});
|
|
372
|
+
const recordIssue = (issue, turn) => {
|
|
373
|
+
loopIssues.push({ ...issue });
|
|
374
|
+
if (turn) {
|
|
375
|
+
turn.issues.push({ ...issue });
|
|
376
|
+
if (isToolStreamIssue(issue))
|
|
377
|
+
turn.toolIssues.push({ ...issue });
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
const issueEvent = (issue, turn) => ({
|
|
381
|
+
type: "issue",
|
|
382
|
+
agentId,
|
|
383
|
+
loopId,
|
|
384
|
+
issue,
|
|
385
|
+
...(turn ? { turnId: turn.turnId, turnIndex: turn.turnIndex } : {}),
|
|
386
|
+
});
|
|
387
|
+
const recordAssistantTurn = (message, usage, toolCalls, issues = [], toolIssues = []) => {
|
|
388
|
+
const turn = {
|
|
389
|
+
message,
|
|
390
|
+
usage: cloneUsage(usage),
|
|
391
|
+
stopReason: message.stopReason,
|
|
392
|
+
toolCalls,
|
|
393
|
+
...(issues.length > 0 ? { issues: issues.map((issue) => ({ ...issue })) } : {}),
|
|
394
|
+
...(toolIssues.length > 0 ? { toolIssues: toolIssues.map((issue) => ({ ...issue })) } : {}),
|
|
395
|
+
};
|
|
396
|
+
loopTurns.push(turn);
|
|
397
|
+
};
|
|
398
|
+
const hasBufferedInbound = (match) => {
|
|
399
|
+
deferredInbound.push(...channel.drain());
|
|
400
|
+
return deferredInbound.some(match);
|
|
401
|
+
};
|
|
402
|
+
const pullMatching = async (match, localSignal) => {
|
|
403
|
+
while (true) {
|
|
404
|
+
const bufferedIdx = deferredInbound.findIndex(match);
|
|
405
|
+
if (bufferedIdx >= 0)
|
|
406
|
+
return deferredInbound.splice(bufferedIdx, 1)[0];
|
|
407
|
+
if (abortController.signal.aborted)
|
|
408
|
+
throw new LoopAbortedError();
|
|
409
|
+
const linked = linkedAbortSignal([abortController.signal, localSignal]);
|
|
410
|
+
let inbound;
|
|
411
|
+
try {
|
|
412
|
+
inbound = await channel.pull(linked.signal);
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
if (error instanceof PullCancelledError && abortController.signal.aborted) {
|
|
416
|
+
throw new LoopAbortedError();
|
|
417
|
+
}
|
|
418
|
+
throw error;
|
|
419
|
+
}
|
|
420
|
+
finally {
|
|
421
|
+
linked.cleanup();
|
|
422
|
+
}
|
|
423
|
+
if (match(inbound))
|
|
424
|
+
return inbound;
|
|
425
|
+
deferredInbound.push(inbound);
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
if (externalSignal) {
|
|
429
|
+
if (externalSignal.aborted)
|
|
430
|
+
abortController.abort();
|
|
431
|
+
else
|
|
432
|
+
externalSignal.addEventListener("abort", () => abortController.abort(), { once: true });
|
|
433
|
+
}
|
|
434
|
+
const signal = abortController.signal;
|
|
435
|
+
async function* applyPendingSteering() {
|
|
436
|
+
const pending = steerQueue.splice(0);
|
|
437
|
+
const supplied = await steering?.({ agentId, loopId, signal });
|
|
438
|
+
if (typeof supplied === "string")
|
|
439
|
+
pending.push(supplied);
|
|
440
|
+
else if (supplied)
|
|
441
|
+
pending.push(...supplied);
|
|
442
|
+
pending.push(...steerQueue.splice(0));
|
|
443
|
+
let applied = false;
|
|
444
|
+
for (const text of pending) {
|
|
445
|
+
if (!text.trim())
|
|
446
|
+
continue;
|
|
447
|
+
const steerMessage = { role: "user", content: [{ type: "text", text }] };
|
|
448
|
+
await store.append(steerMessage);
|
|
449
|
+
applied = true;
|
|
450
|
+
yield { type: "steer_applied", agentId, loopId, message: text };
|
|
451
|
+
}
|
|
452
|
+
return applied;
|
|
453
|
+
}
|
|
454
|
+
const names = tools.map((tool) => tool.def.name);
|
|
455
|
+
if (new Set(names).size !== names.length) {
|
|
456
|
+
const dup = names.find((name, index) => names.indexOf(name) !== index);
|
|
457
|
+
throw new Error(`Duplicate tool name: ${dup}`);
|
|
458
|
+
}
|
|
459
|
+
const toolMap = new Map(tools.map((tool) => [tool.def.name, tool]));
|
|
460
|
+
const isTerminalTool = (name) => Boolean(toolMap.get(name)?.def?.terminal);
|
|
461
|
+
const appendToolResult = async (callId, name, result, isError = false) => {
|
|
462
|
+
const msg = { role: "tool_result", callId, name, result, isError };
|
|
463
|
+
await store.append(msg);
|
|
464
|
+
return msg;
|
|
465
|
+
};
|
|
466
|
+
const appendSuccessfulToolResult = async (tool, call, input, output) => {
|
|
467
|
+
let historicalResult;
|
|
468
|
+
try {
|
|
469
|
+
const value = await tool.def.toHistoricalResult?.({ input, output, callId: call.id });
|
|
470
|
+
if (value !== undefined)
|
|
471
|
+
historicalResult = { originLoopId: loopId, value };
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
const issue = {
|
|
475
|
+
kind: "tool_historical_result_error",
|
|
476
|
+
message: `Historical result projection failed for tool "${call.name}": ${toErrorMessage(error)}`,
|
|
477
|
+
retryable: false,
|
|
478
|
+
callId: call.id,
|
|
479
|
+
name: call.name,
|
|
480
|
+
};
|
|
481
|
+
await store.append({ role: "tool_result", callId: call.id, name: call.name, result: output, isError: false });
|
|
482
|
+
return issue;
|
|
483
|
+
}
|
|
484
|
+
await store.append({
|
|
485
|
+
role: "tool_result",
|
|
486
|
+
callId: call.id,
|
|
487
|
+
name: call.name,
|
|
488
|
+
result: output,
|
|
489
|
+
...(historicalResult ? { historicalResult } : {}),
|
|
490
|
+
isError: false,
|
|
491
|
+
});
|
|
492
|
+
return undefined;
|
|
493
|
+
};
|
|
494
|
+
async function* failToolCall(tc, turnCtx, updateAggregateToolCall, issue, turnIssues) {
|
|
495
|
+
const result = issueToToolResult(issue);
|
|
496
|
+
await appendToolResult(tc.id, tc.name, result, true);
|
|
497
|
+
updateAggregateToolCall(tc.id, { result, isError: true });
|
|
498
|
+
recordIssue(issue, turnIssues);
|
|
499
|
+
yield issueEvent(issue, turnCtx);
|
|
500
|
+
yield {
|
|
501
|
+
type: "tool_execution_end",
|
|
502
|
+
agentId,
|
|
503
|
+
loopId,
|
|
504
|
+
...turnCtx,
|
|
505
|
+
callId: tc.id,
|
|
506
|
+
name: tc.name,
|
|
507
|
+
result,
|
|
508
|
+
isError: true,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
const nowMs = () => Date.now();
|
|
512
|
+
const elapsedSince = (startedAt) => Math.max(0, nowMs() - startedAt);
|
|
513
|
+
const snapshotTiming = (timing, usage) => buildLoopTiming({
|
|
514
|
+
wallMs: timing.loopStartedAt === undefined ? 0 : elapsedSince(timing.loopStartedAt),
|
|
515
|
+
generationMs: timing.generationMs,
|
|
516
|
+
toolExecutionMs: timing.toolExecutionMs,
|
|
517
|
+
actionWaitMs: timing.actionWaitMs,
|
|
518
|
+
}, usage);
|
|
519
|
+
async function* executeToolCall(tc, turnCtx, updateAggregateToolCall, turnIssues) {
|
|
520
|
+
const eventFields = { agentId, loopId, ...turnCtx };
|
|
521
|
+
yield { type: "tool_execution_start", ...eventFields, callId: tc.id, name: tc.name, args: tc.args };
|
|
522
|
+
const tool = toolMap.get(tc.name);
|
|
523
|
+
if (!tool) {
|
|
524
|
+
yield* failToolCall(tc, turnCtx, updateAggregateToolCall, toolExecutionIssue("unknown_tool", `Unknown tool: ${tc.name}`, tc), turnIssues);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const inputResult = tool.def.inputSchema.safeParse(tc.args);
|
|
528
|
+
if (!inputResult.success) {
|
|
529
|
+
yield* failToolCall(tc, turnCtx, updateAggregateToolCall, toolExecutionIssue("input_validation_failed", formatToolValidationError(tool, tc.args, inputResult.error), tc), turnIssues);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const validatedInput = inputResult.data;
|
|
533
|
+
updateAggregateToolCall(tc.id, { args: validatedInput });
|
|
534
|
+
const timeoutMs = timeoutMsFor(tool);
|
|
535
|
+
const timeoutIssue = () => toolTimeoutIssue(tc, timeoutMs ?? 0);
|
|
536
|
+
if (tool.kind === "client") {
|
|
537
|
+
const matchesToolResult = (event) => event.type === "tool_result" && event.callId === tc.id;
|
|
538
|
+
let actionWaitStartedAt;
|
|
539
|
+
if (!hasBufferedInbound(matchesToolResult)) {
|
|
540
|
+
actionWaitStartedAt = nowMs();
|
|
541
|
+
yield {
|
|
542
|
+
type: "tool_action_request",
|
|
543
|
+
...eventFields,
|
|
544
|
+
kind: "client_tool",
|
|
545
|
+
callId: tc.id,
|
|
546
|
+
name: tc.name,
|
|
547
|
+
args: validatedInput,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
const pulled = await waitForActionResponse(actionWaitStartedAt, () => withTimeout((signal) => pullMatching(matchesToolResult, signal), timeoutMs, () => { }));
|
|
551
|
+
if (!pulled.ok) {
|
|
552
|
+
const issue = timeoutIssue();
|
|
553
|
+
const result = issueToToolResult(issue);
|
|
554
|
+
await appendToolResult(tc.id, tc.name, result, true);
|
|
555
|
+
updateAggregateToolCall(tc.id, { result, isError: true });
|
|
556
|
+
recordIssue(issue, turnIssues);
|
|
557
|
+
yield issueEvent(issue, turnCtx);
|
|
558
|
+
yield { type: "tool_execution_end", ...eventFields, callId: tc.id, name: tc.name, result, isError: true };
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const output = pulled.value.result;
|
|
562
|
+
if (tool.def.outputSchema) {
|
|
563
|
+
const outputResult = tool.def.outputSchema.safeParse(output);
|
|
564
|
+
if (!outputResult.success) {
|
|
565
|
+
const issue = toolExecutionIssue("output_validation_failed", `Output validation error for tool "${tc.name}": ${outputResult.error.message}`, tc);
|
|
566
|
+
const result = issueToToolResult(issue);
|
|
567
|
+
await appendToolResult(tc.id, tc.name, result, true);
|
|
568
|
+
updateAggregateToolCall(tc.id, { result, isError: true });
|
|
569
|
+
recordIssue(issue, turnIssues);
|
|
570
|
+
yield issueEvent(issue, turnCtx);
|
|
571
|
+
yield { type: "tool_execution_end", ...eventFields, callId: tc.id, name: tc.name, result, isError: true };
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const historicalIssue = await appendSuccessfulToolResult(tool, tc, validatedInput, output);
|
|
576
|
+
updateAggregateToolCall(tc.id, { result: output });
|
|
577
|
+
if (historicalIssue) {
|
|
578
|
+
recordIssue(historicalIssue, turnIssues);
|
|
579
|
+
yield issueEvent(historicalIssue, turnCtx);
|
|
580
|
+
}
|
|
581
|
+
yield { type: "tool_execution_end", ...eventFields, callId: tc.id, name: tc.name, result: output };
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (tool.def.needsApproval) {
|
|
585
|
+
const matchesApproval = (event) => event.type === "approval_response" && event.callId === tc.id;
|
|
586
|
+
let actionWaitStartedAt;
|
|
587
|
+
if (!hasBufferedInbound(matchesApproval)) {
|
|
588
|
+
actionWaitStartedAt = nowMs();
|
|
589
|
+
yield {
|
|
590
|
+
type: "tool_action_request",
|
|
591
|
+
...eventFields,
|
|
592
|
+
kind: "approval",
|
|
593
|
+
callId: tc.id,
|
|
594
|
+
name: tc.name,
|
|
595
|
+
args: validatedInput,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
const pulled = await waitForActionResponse(actionWaitStartedAt, () => withTimeout((signal) => pullMatching(matchesApproval, signal), timeoutMs, () => { }));
|
|
599
|
+
if (!pulled.ok) {
|
|
600
|
+
const issue = timeoutIssue();
|
|
601
|
+
const result = issueToToolResult(issue);
|
|
602
|
+
await appendToolResult(tc.id, tc.name, result, true);
|
|
603
|
+
updateAggregateToolCall(tc.id, { result, isError: true });
|
|
604
|
+
recordIssue(issue, turnIssues);
|
|
605
|
+
yield issueEvent(issue, turnCtx);
|
|
606
|
+
yield { type: "tool_execution_end", ...eventFields, callId: tc.id, name: tc.name, result, isError: true };
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (!pulled.value.approved) {
|
|
610
|
+
const issue = toolExecutionIssue("approval_denied", "User denied this action", tc);
|
|
611
|
+
const result = issueToToolResult(issue);
|
|
612
|
+
await appendToolResult(tc.id, tc.name, result, true);
|
|
613
|
+
updateAggregateToolCall(tc.id, { result, isError: true });
|
|
614
|
+
recordIssue(issue, turnIssues);
|
|
615
|
+
yield issueEvent(issue, turnCtx);
|
|
616
|
+
yield { type: "tool_execution_end", ...eventFields, callId: tc.id, name: tc.name, result, isError: true };
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const toolAbort = new AbortController();
|
|
621
|
+
const abortTool = () => toolAbort.abort();
|
|
622
|
+
if (signal.aborted)
|
|
623
|
+
abortTool();
|
|
624
|
+
else
|
|
625
|
+
signal.addEventListener("abort", abortTool, { once: true });
|
|
626
|
+
let timeoutHandle;
|
|
627
|
+
const toolStartedAt = nowMs();
|
|
628
|
+
const actionWaitMsAtToolStart = timing.actionWaitMs;
|
|
629
|
+
let toolExecutionRecorded = false;
|
|
630
|
+
const finishToolExecution = () => {
|
|
631
|
+
if (toolExecutionRecorded)
|
|
632
|
+
return;
|
|
633
|
+
toolExecutionRecorded = true;
|
|
634
|
+
recordToolExecution(toolStartedAt, actionWaitMsAtToolStart);
|
|
635
|
+
};
|
|
636
|
+
try {
|
|
637
|
+
const approvalQueue = [];
|
|
638
|
+
const clientToolQueue = [];
|
|
639
|
+
let queueNotify = null;
|
|
640
|
+
let approvalCounter = 0;
|
|
641
|
+
let clientToolCounter = 0;
|
|
642
|
+
const ctx = {
|
|
643
|
+
signal: toolAbort.signal,
|
|
644
|
+
requestApproval(message) {
|
|
645
|
+
return new Promise((resolve) => {
|
|
646
|
+
const id = `${tc.id}-approval-${approvalCounter++}`;
|
|
647
|
+
approvalQueue.push({ id, message, resolve });
|
|
648
|
+
queueNotify?.();
|
|
649
|
+
});
|
|
650
|
+
},
|
|
651
|
+
requestClientTool(name, args) {
|
|
652
|
+
return new Promise((resolve, reject) => {
|
|
653
|
+
const id = `${tc.id}-client-${clientToolCounter++}`;
|
|
654
|
+
clientToolQueue.push({
|
|
655
|
+
id,
|
|
656
|
+
name,
|
|
657
|
+
args,
|
|
658
|
+
resolve: resolve,
|
|
659
|
+
reject,
|
|
660
|
+
});
|
|
661
|
+
queueNotify?.();
|
|
662
|
+
});
|
|
663
|
+
},
|
|
664
|
+
};
|
|
665
|
+
const resultPromise = tool.execute(validatedInput, ctx);
|
|
666
|
+
let timeout;
|
|
667
|
+
if (timeoutMs) {
|
|
668
|
+
timeout = new Promise((resolve) => {
|
|
669
|
+
timeoutHandle = setTimeout(() => {
|
|
670
|
+
toolAbort.abort();
|
|
671
|
+
resolve({ kind: "timeout" });
|
|
672
|
+
}, timeoutMs);
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
let result;
|
|
676
|
+
let done = false;
|
|
677
|
+
while (!done) {
|
|
678
|
+
const waitForQueue = new Promise((resolve) => {
|
|
679
|
+
if (approvalQueue.length > 0 || clientToolQueue.length > 0)
|
|
680
|
+
resolve({ kind: "queue" });
|
|
681
|
+
else
|
|
682
|
+
queueNotify = () => resolve({ kind: "queue" });
|
|
683
|
+
});
|
|
684
|
+
const settled = await Promise.race([
|
|
685
|
+
resultPromise.then((value) => ({ kind: "done", result: value })),
|
|
686
|
+
waitForQueue,
|
|
687
|
+
...(timeout ? [timeout] : []),
|
|
688
|
+
]);
|
|
689
|
+
if (settled.kind === "timeout") {
|
|
690
|
+
const issue = timeoutIssue();
|
|
691
|
+
const timeoutResult = issueToToolResult(issue);
|
|
692
|
+
await appendToolResult(tc.id, tc.name, timeoutResult, true);
|
|
693
|
+
updateAggregateToolCall(tc.id, { result: timeoutResult, isError: true });
|
|
694
|
+
recordIssue(issue, turnIssues);
|
|
695
|
+
yield issueEvent(issue, turnCtx);
|
|
696
|
+
finishToolExecution();
|
|
697
|
+
yield { type: "tool_execution_end", ...eventFields, callId: tc.id, name: tc.name, result: timeoutResult, isError: true };
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
if (settled.kind === "done") {
|
|
701
|
+
result = settled.result;
|
|
702
|
+
done = true;
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
while (approvalQueue.length > 0) {
|
|
706
|
+
const req = approvalQueue.shift();
|
|
707
|
+
const matchesCustomApproval = (event) => event.type === "approval_response" && event.callId === req.id;
|
|
708
|
+
let actionWaitStartedAt;
|
|
709
|
+
if (!hasBufferedInbound(matchesCustomApproval)) {
|
|
710
|
+
actionWaitStartedAt = nowMs();
|
|
711
|
+
yield {
|
|
712
|
+
type: "tool_action_request",
|
|
713
|
+
...eventFields,
|
|
714
|
+
kind: "custom_approval",
|
|
715
|
+
callId: req.id,
|
|
716
|
+
name: tc.name,
|
|
717
|
+
args: validatedInput,
|
|
718
|
+
message: req.message,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
const response = await waitForActionResponse(actionWaitStartedAt, () => withTimeout((signal) => pullMatching(matchesCustomApproval, signal), timeoutMs, () => toolAbort.abort()));
|
|
722
|
+
if (!response.ok) {
|
|
723
|
+
const issue = timeoutIssue();
|
|
724
|
+
const timeoutResult = issueToToolResult(issue);
|
|
725
|
+
await appendToolResult(tc.id, tc.name, timeoutResult, true);
|
|
726
|
+
updateAggregateToolCall(tc.id, { result: timeoutResult, isError: true });
|
|
727
|
+
recordIssue(issue, turnIssues);
|
|
728
|
+
yield issueEvent(issue, turnCtx);
|
|
729
|
+
finishToolExecution();
|
|
730
|
+
yield { type: "tool_execution_end", ...eventFields, callId: tc.id, name: tc.name, result: timeoutResult, isError: true };
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
req.resolve(response.value.approved);
|
|
734
|
+
}
|
|
735
|
+
while (clientToolQueue.length > 0) {
|
|
736
|
+
const req = clientToolQueue.shift();
|
|
737
|
+
const bridgeTool = toolMap.get(req.name);
|
|
738
|
+
let requestArgs = req.args;
|
|
739
|
+
if (bridgeTool) {
|
|
740
|
+
if (bridgeTool.kind !== "client") {
|
|
741
|
+
req.reject(new ToolExecutionFailure(toolExecutionIssue("unknown_tool", `Client tool "${req.name}" is not registered as a client tool.`, { id: req.id, name: req.name })));
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
const inputResult = bridgeTool.def.inputSchema.safeParse(req.args);
|
|
745
|
+
if (!inputResult.success) {
|
|
746
|
+
req.reject(new ToolExecutionFailure(toolExecutionIssue("input_validation_failed", formatToolValidationError(bridgeTool, req.args, inputResult.error), { id: req.id, name: req.name })));
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
requestArgs = inputResult.data;
|
|
750
|
+
}
|
|
751
|
+
const matchesClientResult = (event) => event.type === "tool_result" && event.callId === req.id;
|
|
752
|
+
let actionWaitStartedAt;
|
|
753
|
+
if (!hasBufferedInbound(matchesClientResult)) {
|
|
754
|
+
actionWaitStartedAt = nowMs();
|
|
755
|
+
yield {
|
|
756
|
+
type: "tool_action_request",
|
|
757
|
+
...eventFields,
|
|
758
|
+
kind: "client_tool",
|
|
759
|
+
callId: req.id,
|
|
760
|
+
name: req.name,
|
|
761
|
+
args: requestArgs,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
const response = await waitForActionResponse(actionWaitStartedAt, () => withTimeout((signal) => pullMatching(matchesClientResult, signal), timeoutMs, () => toolAbort.abort()));
|
|
765
|
+
if (!response.ok) {
|
|
766
|
+
const issue = timeoutIssue();
|
|
767
|
+
const timeoutResult = issueToToolResult(issue);
|
|
768
|
+
await appendToolResult(tc.id, tc.name, timeoutResult, true);
|
|
769
|
+
updateAggregateToolCall(tc.id, { result: timeoutResult, isError: true });
|
|
770
|
+
recordIssue(issue, turnIssues);
|
|
771
|
+
yield issueEvent(issue, turnCtx);
|
|
772
|
+
finishToolExecution();
|
|
773
|
+
yield { type: "tool_execution_end", ...eventFields, callId: tc.id, name: tc.name, result: timeoutResult, isError: true };
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
let output = response.value.result;
|
|
777
|
+
if (bridgeTool?.kind === "client" && bridgeTool.def.outputSchema) {
|
|
778
|
+
const outputResult = bridgeTool.def.outputSchema.safeParse(output);
|
|
779
|
+
if (!outputResult.success) {
|
|
780
|
+
req.reject(new ToolExecutionFailure(toolExecutionIssue("output_validation_failed", `Output validation error for client tool "${req.name}": ${outputResult.error.message}`, { id: req.id, name: req.name })));
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
output = outputResult.data;
|
|
784
|
+
}
|
|
785
|
+
req.resolve(output);
|
|
786
|
+
}
|
|
787
|
+
queueNotify = null;
|
|
788
|
+
}
|
|
789
|
+
if (tool.def.outputSchema) {
|
|
790
|
+
const outputResult = tool.def.outputSchema.safeParse(result);
|
|
791
|
+
if (!outputResult.success) {
|
|
792
|
+
const issue = toolExecutionIssue("output_validation_failed", `Output validation error for tool "${tc.name}": ${outputResult.error.message}`, tc);
|
|
793
|
+
const output = issueToToolResult(issue);
|
|
794
|
+
await appendToolResult(tc.id, tc.name, output, true);
|
|
795
|
+
updateAggregateToolCall(tc.id, { result: output, isError: true });
|
|
796
|
+
recordIssue(issue, turnIssues);
|
|
797
|
+
yield issueEvent(issue, turnCtx);
|
|
798
|
+
finishToolExecution();
|
|
799
|
+
yield { type: "tool_execution_end", ...eventFields, callId: tc.id, name: tc.name, result: output, isError: true };
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
const historicalIssue = await appendSuccessfulToolResult(tool, tc, validatedInput, result);
|
|
804
|
+
updateAggregateToolCall(tc.id, { result });
|
|
805
|
+
if (historicalIssue) {
|
|
806
|
+
recordIssue(historicalIssue, turnIssues);
|
|
807
|
+
yield issueEvent(historicalIssue, turnCtx);
|
|
808
|
+
}
|
|
809
|
+
finishToolExecution();
|
|
810
|
+
yield { type: "tool_execution_end", ...eventFields, callId: tc.id, name: tc.name, result };
|
|
811
|
+
}
|
|
812
|
+
catch (error) {
|
|
813
|
+
if (error instanceof LoopAbortedError || signal.aborted)
|
|
814
|
+
throw error;
|
|
815
|
+
const issue = error instanceof ToolExecutionFailure
|
|
816
|
+
? error.issue
|
|
817
|
+
: toolExecutionIssue("execution_failed", toErrorMessage(error), tc);
|
|
818
|
+
const result = issueToToolResult(issue);
|
|
819
|
+
await appendToolResult(tc.id, tc.name, result, true);
|
|
820
|
+
updateAggregateToolCall(tc.id, { result, isError: true });
|
|
821
|
+
recordIssue(issue, turnIssues);
|
|
822
|
+
yield issueEvent(issue, turnCtx);
|
|
823
|
+
finishToolExecution();
|
|
824
|
+
yield { type: "tool_execution_end", ...eventFields, callId: tc.id, name: tc.name, result, isError: true };
|
|
825
|
+
}
|
|
826
|
+
finally {
|
|
827
|
+
finishToolExecution();
|
|
828
|
+
if (timeoutHandle)
|
|
829
|
+
clearTimeout(timeoutHandle);
|
|
830
|
+
signal.removeEventListener("abort", abortTool);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
const noopAggregateUpdate = () => { };
|
|
834
|
+
async function* runCompaction(operation) {
|
|
835
|
+
yield { type: "compaction_start", agentId, loopId };
|
|
836
|
+
try {
|
|
837
|
+
await operation;
|
|
838
|
+
}
|
|
839
|
+
finally {
|
|
840
|
+
yield { type: "compaction_end", agentId, loopId };
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
async function* resumePendingToolCalls(turnCtx) {
|
|
844
|
+
const entries = await store.load();
|
|
845
|
+
const seeded = aggregateTurnsFromEntries(entries);
|
|
846
|
+
loopTurns.splice(0, loopTurns.length, ...seeded);
|
|
847
|
+
let lastAssistantIdx = -1;
|
|
848
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
849
|
+
const role = entries[i].message.role;
|
|
850
|
+
if (role === "user")
|
|
851
|
+
return;
|
|
852
|
+
if (role === "assistant") {
|
|
853
|
+
lastAssistantIdx = i;
|
|
854
|
+
break;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
if (lastAssistantIdx < 0)
|
|
858
|
+
return;
|
|
859
|
+
const entry = entries[lastAssistantIdx];
|
|
860
|
+
if (entry.kind === "summary")
|
|
861
|
+
return;
|
|
862
|
+
const assistantMessage = entry.message;
|
|
863
|
+
const toolCallBlocks = assistantMessage.content.filter((block) => block.type === "tool_call");
|
|
864
|
+
if (toolCallBlocks.length === 0)
|
|
865
|
+
return;
|
|
866
|
+
const resolvedCallIds = new Set();
|
|
867
|
+
for (let i = lastAssistantIdx + 1; i < entries.length; i++) {
|
|
868
|
+
const message = entries[i].message;
|
|
869
|
+
if (message.role === "tool_result")
|
|
870
|
+
resolvedCallIds.add(message.callId);
|
|
871
|
+
}
|
|
872
|
+
const pending = toolCallBlocks.filter((block) => !resolvedCallIds.has(block.id));
|
|
873
|
+
if (pending.length === 0)
|
|
874
|
+
return;
|
|
875
|
+
const aggregateTurn = loopTurns.findLast((turn) => turn.message === assistantMessage);
|
|
876
|
+
const aggregateToolCallMap = new Map((aggregateTurn?.toolCalls ?? []).map((toolCall) => [toolCall.callId, toolCall]));
|
|
877
|
+
const updateAggregateToolCall = aggregateTurn
|
|
878
|
+
? (callId, patch) => {
|
|
879
|
+
const aggregateToolCall = aggregateToolCallMap.get(callId);
|
|
880
|
+
if (aggregateToolCall)
|
|
881
|
+
Object.assign(aggregateToolCall, patch);
|
|
882
|
+
}
|
|
883
|
+
: noopAggregateUpdate;
|
|
884
|
+
const turnIssues = { issues: [], toolIssues: [] };
|
|
885
|
+
yield { type: "turn_start", agentId, loopId, ...turnCtx, resumed: true };
|
|
886
|
+
for (const tc of pending) {
|
|
887
|
+
if (signal.aborted)
|
|
888
|
+
return;
|
|
889
|
+
yield* executeToolCall(tc, turnCtx, updateAggregateToolCall, turnIssues);
|
|
890
|
+
}
|
|
891
|
+
if (aggregateTurn && turnIssues.issues.length > 0) {
|
|
892
|
+
aggregateTurn.issues = [...(aggregateTurn.issues ?? []), ...turnIssues.issues.map((issue) => ({ ...issue }))];
|
|
893
|
+
aggregateTurn.toolIssues = [
|
|
894
|
+
...(aggregateTurn.toolIssues ?? []),
|
|
895
|
+
...turnIssues.toolIssues.map((issue) => ({ ...issue })),
|
|
896
|
+
];
|
|
897
|
+
}
|
|
898
|
+
yield { type: "turn_end", agentId, loopId, ...turnCtx, message: assistantMessage };
|
|
899
|
+
}
|
|
900
|
+
async function* run() {
|
|
901
|
+
let providerTurn = 0;
|
|
902
|
+
let eventTurnIndex = 0;
|
|
903
|
+
let compactionRetried = false;
|
|
904
|
+
const providerTools = tools.map(toolToSpec);
|
|
905
|
+
const prepareProviderMessages = (sourceEntries) => {
|
|
906
|
+
const rawMessages = sourceEntries.map((entry) => entry.message);
|
|
907
|
+
const projectedMessages = projectHistoricalToolResults(rawMessages, loopId);
|
|
908
|
+
return typeof maxToolResultChars === "number"
|
|
909
|
+
? truncateToolResults(projectedMessages, maxToolResultChars)
|
|
910
|
+
: projectedMessages;
|
|
911
|
+
};
|
|
912
|
+
timing.loopStartedAt = nowMs();
|
|
913
|
+
yield { type: "loop_start", agentId, loopId };
|
|
914
|
+
try {
|
|
915
|
+
if (input !== undefined) {
|
|
916
|
+
await store.append(normalizeInput(input));
|
|
917
|
+
}
|
|
918
|
+
else {
|
|
919
|
+
const resumeCtx = { turnId: createTurnId(loopId, eventTurnIndex, "resume"), turnIndex: eventTurnIndex };
|
|
920
|
+
let emittedResumeTurn = false;
|
|
921
|
+
for await (const event of resumePendingToolCalls(resumeCtx)) {
|
|
922
|
+
emittedResumeTurn = true;
|
|
923
|
+
yield event;
|
|
924
|
+
}
|
|
925
|
+
if (emittedResumeTurn)
|
|
926
|
+
eventTurnIndex++;
|
|
927
|
+
if (signal.aborted) {
|
|
928
|
+
yield loopEndEvent("aborted");
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
while (true) {
|
|
933
|
+
if (signal.aborted) {
|
|
934
|
+
yield loopEndEvent("aborted");
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
if (creditStore) {
|
|
938
|
+
const remaining = await creditStore.remaining();
|
|
939
|
+
if (remaining <= 0) {
|
|
940
|
+
yield loopEndEvent("no_credits");
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
const steeringApplied = yield* applyPendingSteering();
|
|
945
|
+
if (steeringApplied) {
|
|
946
|
+
providerTurn = 0;
|
|
947
|
+
compactionRetried = false;
|
|
948
|
+
}
|
|
949
|
+
if (providerTurn >= maxTurns) {
|
|
950
|
+
yield loopEndEvent("max_turns");
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
let entries = await store.load();
|
|
954
|
+
let messages = prepareProviderMessages(entries);
|
|
955
|
+
const contextWindow = provider.contextWindow;
|
|
956
|
+
const computeFillRatio = (providerMessages) => {
|
|
957
|
+
if (typeof contextWindow !== "number" || contextWindow <= 0)
|
|
958
|
+
return undefined;
|
|
959
|
+
const estimatedTokens = Math.ceil(JSON.stringify({ systemPrompt, messages: providerMessages, tools: providerTools }).length / 4);
|
|
960
|
+
const tokens = lastUsage.input > 0 ? Math.max(lastUsage.input, estimatedTokens) : estimatedTokens;
|
|
961
|
+
return tokens / contextWindow;
|
|
962
|
+
};
|
|
963
|
+
if (compact && !compactionRetried) {
|
|
964
|
+
const fillRatio = computeFillRatio(messages);
|
|
965
|
+
const shouldForce = typeof fillRatio === "number" && fillRatio >= 0.85;
|
|
966
|
+
const compaction = compact({
|
|
967
|
+
entries,
|
|
968
|
+
store,
|
|
969
|
+
provider,
|
|
970
|
+
usage: lastUsage,
|
|
971
|
+
force: shouldForce,
|
|
972
|
+
fillRatio,
|
|
973
|
+
});
|
|
974
|
+
if (compaction) {
|
|
975
|
+
yield* runCompaction(compaction);
|
|
976
|
+
entries = await store.load();
|
|
977
|
+
messages = prepareProviderMessages(entries);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
const turnCtx = { turnId: createTurnId(loopId, eventTurnIndex), turnIndex: eventTurnIndex };
|
|
981
|
+
eventTurnIndex++;
|
|
982
|
+
yield { type: "turn_start", agentId, loopId, ...turnCtx };
|
|
983
|
+
let turnUsage = zeroUsage();
|
|
984
|
+
let turnUsageReported = false;
|
|
985
|
+
let stopReason = "stop";
|
|
986
|
+
const assistantBlocks = [];
|
|
987
|
+
const openBlocks = new Map();
|
|
988
|
+
const toolCalls = [];
|
|
989
|
+
const turnIssues = { issues: [], toolIssues: [] };
|
|
990
|
+
let hadContextOverflow = false;
|
|
991
|
+
let overflowRatio;
|
|
992
|
+
let providerFailure;
|
|
993
|
+
const eventFields = { agentId, loopId, ...turnCtx };
|
|
994
|
+
const makePartialMessage = (reason) => {
|
|
995
|
+
const content = [...assistantBlocks];
|
|
996
|
+
const pendingBlocks = [...openBlocks.entries()]
|
|
997
|
+
.sort((left, right) => left[1].index - right[1].index)
|
|
998
|
+
.map(([, block]) => block.kind === "thinking"
|
|
999
|
+
? { type: "thinking", thinking: block.text }
|
|
1000
|
+
: { type: "text", text: block.text });
|
|
1001
|
+
for (const block of pendingBlocks)
|
|
1002
|
+
appendAssistantContentBlock(content, block);
|
|
1003
|
+
return buildAssistantMessageFromContent(provider.model, content, turnUsage, reason);
|
|
1004
|
+
};
|
|
1005
|
+
try {
|
|
1006
|
+
const providerIterator = provider.stream({
|
|
1007
|
+
systemPrompt,
|
|
1008
|
+
messages,
|
|
1009
|
+
tools: providerTools,
|
|
1010
|
+
temperature,
|
|
1011
|
+
maxOutputTokens,
|
|
1012
|
+
disableReasoning,
|
|
1013
|
+
signal,
|
|
1014
|
+
})[Symbol.asyncIterator]();
|
|
1015
|
+
try {
|
|
1016
|
+
streamLoop: while (true) {
|
|
1017
|
+
const next = await measureGeneration(() => providerIterator.next());
|
|
1018
|
+
if (next.done)
|
|
1019
|
+
break;
|
|
1020
|
+
const event = next.value;
|
|
1021
|
+
if (signal.aborted)
|
|
1022
|
+
break;
|
|
1023
|
+
switch (event.type) {
|
|
1024
|
+
case "block_start":
|
|
1025
|
+
if (event.kind === "text" || event.kind === "thinking") {
|
|
1026
|
+
openBlocks.set(event.blockId, { index: event.index, kind: event.kind, text: "" });
|
|
1027
|
+
}
|
|
1028
|
+
yield { ...event, ...eventFields };
|
|
1029
|
+
break;
|
|
1030
|
+
case "block_delta": {
|
|
1031
|
+
const open = openBlocks.get(event.blockId);
|
|
1032
|
+
if (open)
|
|
1033
|
+
open.text += event.delta;
|
|
1034
|
+
yield { ...event, ...eventFields };
|
|
1035
|
+
break;
|
|
1036
|
+
}
|
|
1037
|
+
case "block_end":
|
|
1038
|
+
openBlocks.delete(event.blockId);
|
|
1039
|
+
appendAssistantContentBlock(assistantBlocks, event.block);
|
|
1040
|
+
if (event.block.type === "tool_call") {
|
|
1041
|
+
toolCalls.push(event.block);
|
|
1042
|
+
stopReason = "tool_use";
|
|
1043
|
+
}
|
|
1044
|
+
yield { ...event, ...eventFields };
|
|
1045
|
+
break;
|
|
1046
|
+
case "usage":
|
|
1047
|
+
turnUsage = event.usage;
|
|
1048
|
+
turnUsageReported = true;
|
|
1049
|
+
stopReason = event.finishReason ?? stopReason;
|
|
1050
|
+
yield { ...event, ...eventFields };
|
|
1051
|
+
break;
|
|
1052
|
+
case "issue":
|
|
1053
|
+
recordIssue(event.issue, turnIssues);
|
|
1054
|
+
yield issueEvent(event.issue, turnCtx);
|
|
1055
|
+
if (event.issue.kind === "provider_error" && event.issue.contextOverflow) {
|
|
1056
|
+
hadContextOverflow = true;
|
|
1057
|
+
overflowRatio = event.issue.overflowRatio;
|
|
1058
|
+
break;
|
|
1059
|
+
}
|
|
1060
|
+
if (event.issue.kind === "provider_error"
|
|
1061
|
+
|| (event.issue.kind === "timeout" && event.issue.scope !== "tool")
|
|
1062
|
+
|| event.issue.kind === "runtime_error") {
|
|
1063
|
+
providerFailure = event.issue;
|
|
1064
|
+
break streamLoop;
|
|
1065
|
+
}
|
|
1066
|
+
break;
|
|
1067
|
+
default: {
|
|
1068
|
+
const unsupported = event;
|
|
1069
|
+
const issue = {
|
|
1070
|
+
kind: "runtime_error",
|
|
1071
|
+
message: `Unsupported provider event type: ${String(unsupported.type)}`,
|
|
1072
|
+
retryable: false,
|
|
1073
|
+
};
|
|
1074
|
+
recordIssue(issue, turnIssues);
|
|
1075
|
+
yield issueEvent(issue, turnCtx);
|
|
1076
|
+
providerFailure = issue;
|
|
1077
|
+
break streamLoop;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
finally {
|
|
1083
|
+
await providerIterator.return?.();
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
catch (error) {
|
|
1087
|
+
if (signal.aborted) {
|
|
1088
|
+
const msg = makePartialMessage("interrupted");
|
|
1089
|
+
if (msg.content.length > 0) {
|
|
1090
|
+
await store.append(msg);
|
|
1091
|
+
recordAssistantTurn(msg, turnUsageReported ? turnUsage : undefined, toolCalls.map((toolCall) => ({ callId: toolCall.id, name: toolCall.name, args: toolCall.args })), turnIssues.issues, turnIssues.toolIssues);
|
|
1092
|
+
yield { type: "turn_end", agentId, loopId, ...turnCtx, message: msg };
|
|
1093
|
+
}
|
|
1094
|
+
yield loopEndEvent("aborted");
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
const issue = runtimeIssue(error);
|
|
1098
|
+
recordIssue(issue, turnIssues);
|
|
1099
|
+
yield issueEvent(issue, turnCtx);
|
|
1100
|
+
yield loopEndEvent("error");
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
if (hadContextOverflow) {
|
|
1104
|
+
if (compact && !compactionRetried) {
|
|
1105
|
+
const estimatedFillRatio = computeFillRatio(messages);
|
|
1106
|
+
const fillRatio = typeof overflowRatio === "number"
|
|
1107
|
+
? Math.max(estimatedFillRatio ?? 0, overflowRatio)
|
|
1108
|
+
: estimatedFillRatio;
|
|
1109
|
+
const compaction = compact({
|
|
1110
|
+
entries,
|
|
1111
|
+
store,
|
|
1112
|
+
provider,
|
|
1113
|
+
usage: lastUsage,
|
|
1114
|
+
force: true,
|
|
1115
|
+
fillRatio,
|
|
1116
|
+
});
|
|
1117
|
+
if (compaction) {
|
|
1118
|
+
yield* runCompaction(compaction);
|
|
1119
|
+
compactionRetried = true;
|
|
1120
|
+
continue;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
yield loopEndEvent("context_overflow");
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
if (providerFailure) {
|
|
1127
|
+
yield loopEndEvent("error");
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
if (signal.aborted) {
|
|
1131
|
+
const msg = makePartialMessage("interrupted");
|
|
1132
|
+
if (msg.content.length > 0) {
|
|
1133
|
+
await store.append(msg);
|
|
1134
|
+
recordAssistantTurn(msg, turnUsageReported ? turnUsage : undefined, toolCalls.map((toolCall) => ({ callId: toolCall.id, name: toolCall.name, args: toolCall.args })), turnIssues.issues, turnIssues.toolIssues);
|
|
1135
|
+
yield { type: "turn_end", agentId, loopId, ...turnCtx, message: msg };
|
|
1136
|
+
}
|
|
1137
|
+
yield loopEndEvent("aborted");
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
const assistantMessage = buildAssistantMessageFromContent(provider.model, assistantBlocks, turnUsage, stopReason);
|
|
1141
|
+
await store.append(assistantMessage);
|
|
1142
|
+
lastUsage = turnUsage;
|
|
1143
|
+
const aggregateToolCalls = toolCalls.map((toolCall) => ({
|
|
1144
|
+
callId: toolCall.id,
|
|
1145
|
+
name: toolCall.name,
|
|
1146
|
+
args: toolCall.args,
|
|
1147
|
+
}));
|
|
1148
|
+
recordAssistantTurn(assistantMessage, turnUsageReported ? turnUsage : undefined, aggregateToolCalls, turnIssues.issues, turnIssues.toolIssues);
|
|
1149
|
+
if (creditStore && turnUsage.creditsUsed && turnUsage.creditsUsed > 0) {
|
|
1150
|
+
await creditStore.deduct(turnUsage.creditsUsed);
|
|
1151
|
+
}
|
|
1152
|
+
if (toolCalls.length === 0) {
|
|
1153
|
+
yield { type: "turn_end", agentId, loopId, ...turnCtx, message: assistantMessage };
|
|
1154
|
+
const lateSteeringApplied = yield* applyPendingSteering();
|
|
1155
|
+
if (lateSteeringApplied) {
|
|
1156
|
+
providerTurn = 0;
|
|
1157
|
+
compactionRetried = false;
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1160
|
+
yield loopEndEvent("stop");
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
const aggregateToolCallMap = new Map(aggregateToolCalls.map((toolCall) => [toolCall.callId, toolCall]));
|
|
1164
|
+
const updateAggregateToolCall = (callId, patch) => {
|
|
1165
|
+
const aggregateToolCall = aggregateToolCallMap.get(callId);
|
|
1166
|
+
if (aggregateToolCall)
|
|
1167
|
+
Object.assign(aggregateToolCall, patch);
|
|
1168
|
+
};
|
|
1169
|
+
let terminalToolCompleted = false;
|
|
1170
|
+
for (const tc of toolCalls) {
|
|
1171
|
+
yield* executeToolCall(tc, turnCtx, updateAggregateToolCall, turnIssues);
|
|
1172
|
+
const aggregateToolCall = aggregateToolCallMap.get(tc.id);
|
|
1173
|
+
if (isTerminalTool(tc.name) && aggregateToolCall && !aggregateToolCall.isError) {
|
|
1174
|
+
terminalToolCompleted = true;
|
|
1175
|
+
break;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
const recordedTurn = loopTurns[loopTurns.length - 1];
|
|
1179
|
+
if (recordedTurn?.message === assistantMessage) {
|
|
1180
|
+
recordedTurn.issues = turnIssues.issues.map((issue) => ({ ...issue }));
|
|
1181
|
+
recordedTurn.toolIssues = turnIssues.toolIssues.map((issue) => ({ ...issue }));
|
|
1182
|
+
}
|
|
1183
|
+
yield { type: "turn_end", agentId, loopId, ...turnCtx, message: assistantMessage };
|
|
1184
|
+
if (terminalToolCompleted) {
|
|
1185
|
+
const lateSteeringApplied = yield* applyPendingSteering();
|
|
1186
|
+
if (lateSteeringApplied) {
|
|
1187
|
+
providerTurn = 0;
|
|
1188
|
+
compactionRetried = false;
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
yield loopEndEvent("stop");
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
providerTurn++;
|
|
1195
|
+
compactionRetried = false;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
catch (error) {
|
|
1199
|
+
if (signal.aborted) {
|
|
1200
|
+
yield loopEndEvent("aborted");
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
const issue = runtimeIssue(error);
|
|
1204
|
+
recordIssue(issue);
|
|
1205
|
+
yield issueEvent(issue);
|
|
1206
|
+
yield loopEndEvent("error");
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
const eventSource = coalesce ? coalesceOutboundEvents(run(), coalesce) : run();
|
|
1210
|
+
const generator = eventSource[Symbol.asyncIterator]();
|
|
1211
|
+
const loop = {
|
|
1212
|
+
[Symbol.asyncIterator]() {
|
|
1213
|
+
return {
|
|
1214
|
+
async next() {
|
|
1215
|
+
const result = await generator.next();
|
|
1216
|
+
if (!result.done && result.value) {
|
|
1217
|
+
for (const listener of subscribers)
|
|
1218
|
+
listener(result.value);
|
|
1219
|
+
}
|
|
1220
|
+
return result;
|
|
1221
|
+
},
|
|
1222
|
+
async return(value) {
|
|
1223
|
+
return generator.return(value);
|
|
1224
|
+
},
|
|
1225
|
+
async throw(error) {
|
|
1226
|
+
return generator.throw(error);
|
|
1227
|
+
},
|
|
1228
|
+
};
|
|
1229
|
+
},
|
|
1230
|
+
subscribe(listener) {
|
|
1231
|
+
subscribers.push(listener);
|
|
1232
|
+
return () => {
|
|
1233
|
+
const idx = subscribers.indexOf(listener);
|
|
1234
|
+
if (idx >= 0)
|
|
1235
|
+
subscribers.splice(idx, 1);
|
|
1236
|
+
};
|
|
1237
|
+
},
|
|
1238
|
+
push(event) {
|
|
1239
|
+
channel.push(event);
|
|
1240
|
+
},
|
|
1241
|
+
steer(message) {
|
|
1242
|
+
if (message.trim())
|
|
1243
|
+
steerQueue.push(message);
|
|
1244
|
+
},
|
|
1245
|
+
abort() {
|
|
1246
|
+
abortController.abort();
|
|
1247
|
+
},
|
|
1248
|
+
};
|
|
1249
|
+
return loop;
|
|
1250
|
+
};
|