@stackstackstack/dsh-agent-loop 0.1.5
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.i18n.yaml +6 -0
- package/README.md +134 -0
- package/README.zh.md +134 -0
- package/lib/index.js +1295 -0
- package/lib/invariant.js +42 -0
- package/lib/types/agent.d.ts +61 -0
- package/lib/types/constants.d.ts +6 -0
- package/lib/types/index.d.ts +155 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/runtime-context.d.ts +26 -0
- package/lib/types/tool-calls.d.ts +38 -0
- package/package.json +61 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,1295 @@
|
|
|
1
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import z from "@deepseek-ai/schemastery";
|
|
4
|
+
import { Inbox, agentEvents, assembleContextFor, emitAgentEvent } from "@stackstackstack/dsh-agent";
|
|
5
|
+
import { BlockAssembler, LlmError, assertNever, createAssistantMessage, createToolResultMessage, createUserMessage, deepFreeze, errorChain, markAgentLoopRequest } from "@stackstackstack/dsh-llm";
|
|
6
|
+
import { installSettingsSection, settingsNamespace } from "@stackstackstack/dsh-settings";
|
|
7
|
+
import { SessionId, SessionPreparation, canonicalHeader, headerEquals, isReplacementSurfaceEvent } from "@stackstackstack/dsh-session";
|
|
8
|
+
import { createScope } from "@stackstackstack/dsh-scope";
|
|
9
|
+
import { joinContextSections, renderContextSections, renderPrompt } from "@stackstackstack/dsh-system-prompt";
|
|
10
|
+
import { TOOL_ABORTED_BEFORE_DISPATCH, TOOL_RUNTIME_SCHEDULER } from "@stackstackstack/dsh-tools";
|
|
11
|
+
//#region lib/types/runtime-context.js
|
|
12
|
+
/**
|
|
13
|
+
* Durable projection state for dynamic runtime context.
|
|
14
|
+
* @module @stackstackstack/dsh-agent-loop/runtime-context
|
|
15
|
+
*/
|
|
16
|
+
const SOURCE = "@stackstackstack/dsh-system-prompt";
|
|
17
|
+
const CLEARED = "Current runtime context: none. Earlier runtime-context snapshots no longer apply.";
|
|
18
|
+
function isOwned(message) {
|
|
19
|
+
return message.source.kind === "plugin" && message.source.plugin === SOURCE;
|
|
20
|
+
}
|
|
21
|
+
function textOf(message) {
|
|
22
|
+
const [block] = message.content;
|
|
23
|
+
return message.content.length === 1 && block?.type === "text" ? block.text : void 0;
|
|
24
|
+
}
|
|
25
|
+
/** Tracks the last retained runtime-context snapshot without owning its commit. */
|
|
26
|
+
var RuntimeContextProjection = class {
|
|
27
|
+
/** `undefined` means no snapshot ever existed; `null` means none is retained. */
|
|
28
|
+
retained;
|
|
29
|
+
/**
|
|
30
|
+
* Restore projection state once, then follow authoritative session events.
|
|
31
|
+
* @param ctx - agent-scoped event context.
|
|
32
|
+
* @param session - session receiving projected messages.
|
|
33
|
+
*/
|
|
34
|
+
constructor(ctx, session) {
|
|
35
|
+
const surface = new Set(session.surface.nodes);
|
|
36
|
+
for (let index = session.events.length - 1; index >= 0; index -= 1) {
|
|
37
|
+
const event = session.events[index];
|
|
38
|
+
if (event?.type !== "user/message" || !isOwned(event.data)) continue;
|
|
39
|
+
this.retained ??= null;
|
|
40
|
+
if (surface.has(event.seq)) {
|
|
41
|
+
this.retained = {
|
|
42
|
+
seq: event.seq,
|
|
43
|
+
text: textOf(event.data)
|
|
44
|
+
};
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
ctx.on("session/event", (subject, event) => {
|
|
49
|
+
if (subject !== session) return;
|
|
50
|
+
if (event.type === "user/message" && isOwned(event.data)) this.retained = {
|
|
51
|
+
seq: event.seq,
|
|
52
|
+
text: textOf(event.data)
|
|
53
|
+
};
|
|
54
|
+
else if (this.retained && isReplacementSurfaceEvent(event) && event.sourceEventSeqs?.includes(this.retained.seq) === true) this.retained = null;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Create an uncommitted snapshot only when the retained value differs.
|
|
59
|
+
* @param current - fully rendered dynamic context.
|
|
60
|
+
* @param sections - named contributions that formed the current snapshot.
|
|
61
|
+
* @returns a candidate user message, or `undefined` when no update is needed.
|
|
62
|
+
*/
|
|
63
|
+
project(current, sections) {
|
|
64
|
+
if (this.retained === void 0 && current.length === 0) return;
|
|
65
|
+
const snapshot = current.length === 0 ? CLEARED : current;
|
|
66
|
+
if (this.retained?.text === snapshot) return;
|
|
67
|
+
return createUserMessage({
|
|
68
|
+
content: [{
|
|
69
|
+
type: "text",
|
|
70
|
+
text: snapshot
|
|
71
|
+
}],
|
|
72
|
+
source: sections.length === 0 ? {
|
|
73
|
+
kind: "plugin",
|
|
74
|
+
plugin: SOURCE
|
|
75
|
+
} : {
|
|
76
|
+
kind: "plugin",
|
|
77
|
+
plugin: SOURCE,
|
|
78
|
+
form: "snapshot",
|
|
79
|
+
sections
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region lib/types/tool-calls.js
|
|
86
|
+
/**
|
|
87
|
+
* Schedules one assistant step's tool calls. Exclusive calls form barriers;
|
|
88
|
+
* parallel calls use a bounded rolling pool and are reclassified before start.
|
|
89
|
+
* Dispatch may overlap, while policy, results, and result context remain
|
|
90
|
+
* model-ordered. Abort or an internal scheduler failure stops replenishment
|
|
91
|
+
* and drains started calls.
|
|
92
|
+
*
|
|
93
|
+
* Abort records synthetic error results for skipped calls so replay stays
|
|
94
|
+
* valid. A terminal scheduler failure preserves already-recorded `tool/call`
|
|
95
|
+
* events without fabricating results.
|
|
96
|
+
* @module dsh-agent-loop/tool-calls
|
|
97
|
+
*/
|
|
98
|
+
/**
|
|
99
|
+
* Schedule one assistant step's tool calls by their live concurrency mode.
|
|
100
|
+
* Ordinary completion and abort commit started-call results in order. Abort
|
|
101
|
+
* drains them, records synthetic results for unstarted calls, and returns with
|
|
102
|
+
* the signal still aborted after accepting started-call context through the
|
|
103
|
+
* caller-supplied acceptor (the machine stages it in its next-step inbox for the
|
|
104
|
+
* step boundary). An internal scheduler failure stops new dispatches, drains
|
|
105
|
+
* already-started dispatches, and rejects with the first failure without
|
|
106
|
+
* fabricating tool results.
|
|
107
|
+
* The committed step's AgentLoop driver boundary supplies the initiating Agent
|
|
108
|
+
* that becomes each explicit {@link ToolExecutionInput.agent}.
|
|
109
|
+
*
|
|
110
|
+
* @param ctx - loop context that owns the tool registry and carries the initiating Agent.
|
|
111
|
+
* @param turn - current turn number.
|
|
112
|
+
* @param step - current step number.
|
|
113
|
+
* @param toolCalls - assistant calls in model order.
|
|
114
|
+
* @param signal - abort signal shared by the step.
|
|
115
|
+
* @param acceptContext - accepts committed result context for the next step boundary.
|
|
116
|
+
*/
|
|
117
|
+
async function executeToolCalls(ctx, turn, step, toolCalls, signal, acceptContext) {
|
|
118
|
+
const agent = ctx.agents.requireInitiator();
|
|
119
|
+
const { session } = agent;
|
|
120
|
+
const planned = toolCalls.map((block) => ({
|
|
121
|
+
block,
|
|
122
|
+
exec: {
|
|
123
|
+
callId: block.id,
|
|
124
|
+
name: block.name,
|
|
125
|
+
arguments: parseArguments(block.arguments),
|
|
126
|
+
agent,
|
|
127
|
+
signal
|
|
128
|
+
}
|
|
129
|
+
}));
|
|
130
|
+
let next = 0;
|
|
131
|
+
let concluded = false;
|
|
132
|
+
while (next < planned.length) {
|
|
133
|
+
const first = planned[next];
|
|
134
|
+
const mode = ctx.tools.executionMode(first.exec).kind;
|
|
135
|
+
const outcome = await runGroup(ctx, turn, step, mode === "parallel" ? planned.slice(next) : [first], mode, signal, acceptContext);
|
|
136
|
+
next += outcome.consumed;
|
|
137
|
+
concluded ||= outcome.concluded;
|
|
138
|
+
if (outcome.aborted) {
|
|
139
|
+
for (const call of planned.slice(next)) appendSkippedToolCall(session, turn, step, call.block);
|
|
140
|
+
return { concluded };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { concluded };
|
|
144
|
+
}
|
|
145
|
+
/** Parse model arguments, preserving invalid JSON as text and mapping empty input to `{}`. */
|
|
146
|
+
function parseArguments(raw) {
|
|
147
|
+
try {
|
|
148
|
+
return raw ? JSON.parse(raw) : {};
|
|
149
|
+
} catch {
|
|
150
|
+
return raw;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Run one exclusive barrier or parallel pool. Later calls are reclassified
|
|
155
|
+
* before start; an exclusive reclassification waits for the current pool to
|
|
156
|
+
* drain and remains for the caller's next barrier. Results and contexts commit
|
|
157
|
+
* in model order. Abort stops starts, drains and commits started calls, accepts
|
|
158
|
+
* their contexts into the owning batch, records results for skipped calls, and
|
|
159
|
+
* returns an aborted outcome. Scheduler failure drains dispatches without
|
|
160
|
+
* committing synthetic recovery results.
|
|
161
|
+
*/
|
|
162
|
+
async function runGroup(ctx, turn, step, group, mode, signal, acceptContext) {
|
|
163
|
+
const { session } = ctx.agents.requireInitiator();
|
|
164
|
+
const { maxParallelToolCalls } = ctx.agentLoop.config;
|
|
165
|
+
const slots = group.map(() => void 0);
|
|
166
|
+
const callSeqs = group.map(() => -1);
|
|
167
|
+
let nextToStart = 0;
|
|
168
|
+
let committed = 0;
|
|
169
|
+
let started = 0;
|
|
170
|
+
let aborted = signal.aborted;
|
|
171
|
+
let concluded = false;
|
|
172
|
+
let schedulerFailure;
|
|
173
|
+
const throwSchedulerFailure = () => {
|
|
174
|
+
if (schedulerFailure !== void 0) throw schedulerFailure.error;
|
|
175
|
+
};
|
|
176
|
+
const commitReady = async () => {
|
|
177
|
+
while (committed < group.length) {
|
|
178
|
+
const slot = slots[committed];
|
|
179
|
+
if (slot === void 0) break;
|
|
180
|
+
const call = group[committed];
|
|
181
|
+
const result = slot.needsPost ? await ctx.tools[TOOL_RUNTIME_SCHEDULER].finalize(slot.exec, slot.result) : ctx.tools[TOOL_RUNTIME_SCHEDULER].finish(slot.exec, slot.result);
|
|
182
|
+
appendToolResult(session, turn, step, call.block, result, callSeqs[committed]);
|
|
183
|
+
for (const context of result.additionalContexts ?? []) acceptContext(context);
|
|
184
|
+
concluded ||= result.concludesTurn === true;
|
|
185
|
+
committed++;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
189
|
+
const startCall = async (index) => {
|
|
190
|
+
const call = group[index];
|
|
191
|
+
callSeqs[index] = appendToolCall(session, turn, step, call.block);
|
|
192
|
+
started++;
|
|
193
|
+
const prepared = await ctx.tools[TOOL_RUNTIME_SCHEDULER].prepare(call.exec);
|
|
194
|
+
throwSchedulerFailure();
|
|
195
|
+
switch (prepared.kind) {
|
|
196
|
+
case "dispatch": {
|
|
197
|
+
const promise = ctx.tools[TOOL_RUNTIME_SCHEDULER].dispatch(prepared.exec).then((outcome) => {
|
|
198
|
+
slots[index] = {
|
|
199
|
+
exec: prepared.exec,
|
|
200
|
+
result: outcome.result,
|
|
201
|
+
needsPost: outcome.kind === "post-result"
|
|
202
|
+
};
|
|
203
|
+
return index;
|
|
204
|
+
}, (error) => {
|
|
205
|
+
schedulerFailure ??= { error };
|
|
206
|
+
return index;
|
|
207
|
+
});
|
|
208
|
+
inFlight.set(index, promise);
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
case "post-result":
|
|
212
|
+
slots[index] = {
|
|
213
|
+
exec: prepared.exec,
|
|
214
|
+
result: prepared.result,
|
|
215
|
+
needsPost: true
|
|
216
|
+
};
|
|
217
|
+
break;
|
|
218
|
+
case "final-result":
|
|
219
|
+
slots[index] = {
|
|
220
|
+
exec: prepared.exec,
|
|
221
|
+
result: prepared.result,
|
|
222
|
+
needsPost: false
|
|
223
|
+
};
|
|
224
|
+
break;
|
|
225
|
+
/* v8 ignore next -- closed-union exhaustiveness guard */
|
|
226
|
+
default: assertNever(prepared, "tool-call scheduler prepare result");
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
const fillPool = async () => {
|
|
230
|
+
while (!aborted && nextToStart < group.length && inFlight.size < maxParallelToolCalls) {
|
|
231
|
+
const nextCall = group[nextToStart];
|
|
232
|
+
if (nextToStart > 0 && mode === "parallel" && ctx.tools.executionMode(nextCall.exec).kind !== "parallel") break;
|
|
233
|
+
await startCall(nextToStart);
|
|
234
|
+
nextToStart++;
|
|
235
|
+
throwSchedulerFailure();
|
|
236
|
+
await commitReady();
|
|
237
|
+
throwSchedulerFailure();
|
|
238
|
+
if (signal.aborted) aborted = true;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
try {
|
|
242
|
+
await fillPool();
|
|
243
|
+
while (inFlight.size > 0) {
|
|
244
|
+
const settledIndex = await Promise.race(inFlight.values());
|
|
245
|
+
inFlight.delete(settledIndex);
|
|
246
|
+
throwSchedulerFailure();
|
|
247
|
+
await commitReady();
|
|
248
|
+
throwSchedulerFailure();
|
|
249
|
+
if (signal.aborted) aborted = true;
|
|
250
|
+
await fillPool();
|
|
251
|
+
}
|
|
252
|
+
} catch (error) {
|
|
253
|
+
schedulerFailure ??= { error };
|
|
254
|
+
await Promise.allSettled(inFlight.values());
|
|
255
|
+
throw schedulerFailure.error;
|
|
256
|
+
}
|
|
257
|
+
if (aborted) {
|
|
258
|
+
for (const call of group.slice(started)) appendSkippedToolCall(session, turn, step, call.block);
|
|
259
|
+
return {
|
|
260
|
+
consumed: group.length,
|
|
261
|
+
aborted: true,
|
|
262
|
+
concluded
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/* v8 ignore next -- unreachable: a non-aborted group commits every started call */
|
|
266
|
+
if (committed !== started) throw new Error("tool-call scheduler: uncommitted settled calls");
|
|
267
|
+
return {
|
|
268
|
+
consumed: started,
|
|
269
|
+
aborted: false,
|
|
270
|
+
concluded
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/** Append the durable call/result pair for a model call skipped after cancellation. */
|
|
274
|
+
function appendSkippedToolCall(session, turn, step, block) {
|
|
275
|
+
const callSeq = appendToolCall(session, turn, step, block);
|
|
276
|
+
appendToolResult(session, turn, step, block, {
|
|
277
|
+
content: [{
|
|
278
|
+
type: "text",
|
|
279
|
+
text: "Error: tool call aborted before dispatch"
|
|
280
|
+
}],
|
|
281
|
+
isError: true,
|
|
282
|
+
error: {
|
|
283
|
+
message: "tool call aborted before dispatch",
|
|
284
|
+
info: {
|
|
285
|
+
name: "AbortError",
|
|
286
|
+
code: TOOL_ABORTED_BEFORE_DISPATCH
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}, callSeq);
|
|
290
|
+
}
|
|
291
|
+
/** Append a started call and return the event seq that its result must cite. */
|
|
292
|
+
function appendToolCall(session, turn, step, block) {
|
|
293
|
+
return session.append("tool/call", {
|
|
294
|
+
turn,
|
|
295
|
+
step,
|
|
296
|
+
callId: block.id,
|
|
297
|
+
name: block.name,
|
|
298
|
+
arguments: block.arguments
|
|
299
|
+
}).seq;
|
|
300
|
+
}
|
|
301
|
+
/** Append a model-ordered result linked to its call event. */
|
|
302
|
+
function appendToolResult(session, turn, step, block, result, callSeq) {
|
|
303
|
+
const message = createToolResultMessage({
|
|
304
|
+
callId: block.id,
|
|
305
|
+
content: result.content,
|
|
306
|
+
isError: result.isError
|
|
307
|
+
});
|
|
308
|
+
session.append("tool/result", {
|
|
309
|
+
turn,
|
|
310
|
+
step,
|
|
311
|
+
message,
|
|
312
|
+
...result.error?.info ? { error: result.error.info } : {},
|
|
313
|
+
...result.meta !== void 0 ? { meta: result.meta } : {}
|
|
314
|
+
}, {
|
|
315
|
+
surfaceOp: "append",
|
|
316
|
+
sourceEventSeqs: [callSeq]
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region lib/types/agent.js
|
|
321
|
+
/**
|
|
322
|
+
* Default Agent driver over queued turns and step-boundary input. Every request
|
|
323
|
+
* is derived from the session log.
|
|
324
|
+
* @module dsh-agent-loop/agent
|
|
325
|
+
*/
|
|
326
|
+
/** Remove adapter-derived values before plugins propose the next request config. */
|
|
327
|
+
function requestProposal(header) {
|
|
328
|
+
if (header.adapterDefaults === void 0) return header.config;
|
|
329
|
+
const proposal = { ...header.config };
|
|
330
|
+
if (header.adapterDefaults.reasoningEffort === true) delete proposal.reasoningEffort;
|
|
331
|
+
if (header.adapterDefaults.maxTokens === true) delete proposal.maxTokens;
|
|
332
|
+
return proposal;
|
|
333
|
+
}
|
|
334
|
+
/** Drives one session through turn and step boundaries. */
|
|
335
|
+
var ReactLoopAgent = class {
|
|
336
|
+
loopCtx;
|
|
337
|
+
id;
|
|
338
|
+
options;
|
|
339
|
+
session;
|
|
340
|
+
inbox;
|
|
341
|
+
phase;
|
|
342
|
+
activityDone = Promise.resolve();
|
|
343
|
+
/** The agent-scoped registration boundary; the lifecycle owner unwinds it after the driver exits. */
|
|
344
|
+
scope;
|
|
345
|
+
ctx;
|
|
346
|
+
/** Fused dispatcher, built once in the constructor so hot-path dispatches never allocate. */
|
|
347
|
+
dispatch;
|
|
348
|
+
/** Whether this loop instance has appended its initial/resume request anchor. */
|
|
349
|
+
requestHeaderLogged = false;
|
|
350
|
+
runtimeContext;
|
|
351
|
+
constructor(loopCtx, id, options, session) {
|
|
352
|
+
this.loopCtx = loopCtx;
|
|
353
|
+
this.id = id;
|
|
354
|
+
this.options = options;
|
|
355
|
+
this.session = session;
|
|
356
|
+
this.dispatch = agentEvents(loopCtx, this);
|
|
357
|
+
this.inbox = new Inbox(session, {
|
|
358
|
+
inserted: (message) => {
|
|
359
|
+
this.dispatch.emit("agent/inbox/inserted", { message });
|
|
360
|
+
},
|
|
361
|
+
discarded: (message) => {
|
|
362
|
+
this.dispatch.emit("agent/inbox/discarded", { message });
|
|
363
|
+
},
|
|
364
|
+
claimed: (message, turn) => {
|
|
365
|
+
this.dispatch.emit("agent/inbox/claimed", {
|
|
366
|
+
message,
|
|
367
|
+
turn
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
const lastTurn = session.events.findLast((event) => event.type === "turn/start")?.data.turn ?? 0;
|
|
372
|
+
this.phase = {
|
|
373
|
+
kind: "idle",
|
|
374
|
+
lastTurn
|
|
375
|
+
};
|
|
376
|
+
this.scope = createScope(loopCtx, this);
|
|
377
|
+
this.ctx = this.scope.ctx.extend({ agent: this });
|
|
378
|
+
this.runtimeContext = new RuntimeContextProjection(this.ctx, session);
|
|
379
|
+
}
|
|
380
|
+
get status() {
|
|
381
|
+
return this.phase.kind === "idle" || this.phase.kind === "maintenance" ? "idle" : "running";
|
|
382
|
+
}
|
|
383
|
+
/** Commit a phase and publish its externally visible status transition. */
|
|
384
|
+
setPhase(next) {
|
|
385
|
+
const previousStatus = this.status;
|
|
386
|
+
this.phase = next;
|
|
387
|
+
const status = this.status;
|
|
388
|
+
if (status !== previousStatus) this.dispatch.emit("agent/status", { status });
|
|
389
|
+
}
|
|
390
|
+
send(message, target, wakeup) {
|
|
391
|
+
const wakingAfterAbort = wakeup && this.phase.kind !== "idle" && this.phase.abort.signal.aborted;
|
|
392
|
+
const resolvedTarget = wakingAfterAbort ? "next-turn" : target;
|
|
393
|
+
this.inbox.splice(resolvedTarget, Infinity, 0, [message]);
|
|
394
|
+
if (wakeup) this.wakeDriver(wakingAfterAbort);
|
|
395
|
+
}
|
|
396
|
+
followup(input) {
|
|
397
|
+
this.send(input, "next-turn", true);
|
|
398
|
+
}
|
|
399
|
+
steer(input) {
|
|
400
|
+
this.send(input, "next-step", true);
|
|
401
|
+
}
|
|
402
|
+
inject(input) {
|
|
403
|
+
this.send(input, "next-step", false);
|
|
404
|
+
}
|
|
405
|
+
cancel(cause, options = {}) {
|
|
406
|
+
if (!options.keepInbox) {
|
|
407
|
+
this.inbox.clear();
|
|
408
|
+
if (this.phase.kind !== "idle") this.phase.wakeRequested = false;
|
|
409
|
+
}
|
|
410
|
+
if (this.phase.kind !== "idle") this.phase.abort.abort(cause);
|
|
411
|
+
}
|
|
412
|
+
runMaintenance(job) {
|
|
413
|
+
if (this.phase.kind !== "idle") throw new Error(`agent "${this.id}" already has active work`);
|
|
414
|
+
const done = Promise.withResolvers();
|
|
415
|
+
const maintenance = {
|
|
416
|
+
kind: "maintenance",
|
|
417
|
+
abort: new AbortController(),
|
|
418
|
+
lastTurn: this.phase.lastTurn,
|
|
419
|
+
wakeRequested: false
|
|
420
|
+
};
|
|
421
|
+
this.setPhase(maintenance);
|
|
422
|
+
this.activityDone = done.promise;
|
|
423
|
+
return (async () => {
|
|
424
|
+
try {
|
|
425
|
+
return await job(maintenance.abort.signal);
|
|
426
|
+
} finally {
|
|
427
|
+
this.setPhase({
|
|
428
|
+
kind: "idle",
|
|
429
|
+
lastTurn: maintenance.lastTurn
|
|
430
|
+
});
|
|
431
|
+
if (maintenance.wakeRequested && this.inbox.hasPending) this.wakeDriver();
|
|
432
|
+
done.resolve();
|
|
433
|
+
}
|
|
434
|
+
})();
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Start one driver, or latch its wake behind maintenance or an aborted
|
|
438
|
+
* activity. A wake sent while idle always opens its turn boundary, even
|
|
439
|
+
* when its message was cleared; only a latched replay is suppressed when
|
|
440
|
+
* the queue no longer holds the wake.
|
|
441
|
+
* @param wakeAfterAbort - the {@link send} classification, captured before
|
|
442
|
+
* the inbox insertion so a reentrant cancel cannot reclassify it.
|
|
443
|
+
*/
|
|
444
|
+
wakeDriver(wakeAfterAbort = false) {
|
|
445
|
+
if (this.phase.kind !== "idle") {
|
|
446
|
+
if (this.phase.abort.signal.reason?.kind !== "disposed" && (this.phase.kind === "maintenance" || wakeAfterAbort)) this.phase.wakeRequested = true;
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const driver = Promise.withResolvers();
|
|
450
|
+
this.activityDone = driver.promise;
|
|
451
|
+
this.setPhase({
|
|
452
|
+
kind: "running",
|
|
453
|
+
abort: new AbortController(),
|
|
454
|
+
turn: this.phase.lastTurn,
|
|
455
|
+
step: 0,
|
|
456
|
+
wakeRequested: false
|
|
457
|
+
});
|
|
458
|
+
this.loopCtx.agents.withInitiator(this, () => this.kick()).then(driver.resolve, driver.reject);
|
|
459
|
+
}
|
|
460
|
+
async whenIdle() {
|
|
461
|
+
let activity;
|
|
462
|
+
do
|
|
463
|
+
await (activity = this.activityDone);
|
|
464
|
+
while (activity !== this.activityDone);
|
|
465
|
+
}
|
|
466
|
+
/** Report one failure at its live boundary, then preserve it for driver containment. */
|
|
467
|
+
throwError(error) {
|
|
468
|
+
const turn = this.phase.kind === "running" ? this.phase.turn : this.phase.lastTurn;
|
|
469
|
+
const step = this.phase.kind === "running" ? this.phase.step : 0;
|
|
470
|
+
this.dispatch.emit("agent/error", {
|
|
471
|
+
turn,
|
|
472
|
+
step,
|
|
473
|
+
error
|
|
474
|
+
});
|
|
475
|
+
throw error;
|
|
476
|
+
}
|
|
477
|
+
async kick() {
|
|
478
|
+
try {
|
|
479
|
+
while (await this.turn());
|
|
480
|
+
} catch (_error) {} finally {
|
|
481
|
+
/* v8 ignore next -- kick owns a running phase until this driver boundary */
|
|
482
|
+
if (this.phase.kind === "running") {
|
|
483
|
+
const { turn, wakeRequested } = this.phase;
|
|
484
|
+
this.setPhase({
|
|
485
|
+
kind: "idle",
|
|
486
|
+
lastTurn: turn
|
|
487
|
+
});
|
|
488
|
+
if (wakeRequested && this.inbox.hasPending) this.wakeDriver();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
async preStep(target, position) {
|
|
493
|
+
/* v8 ignore next -- private callers establish the running phase before proposing a step */
|
|
494
|
+
if (this.phase.kind !== "running") throw new Error(`agent "${this.id}": pre-step outside running phase`);
|
|
495
|
+
const signal = this.phase.abort.signal;
|
|
496
|
+
const claimed = this.inbox.claim(target, position.turn);
|
|
497
|
+
const assembly = await this.loopCtx.systemPrompt.assemble(assembleContextFor(this, signal));
|
|
498
|
+
signal.throwIfAborted();
|
|
499
|
+
const sections = renderContextSections(assembly);
|
|
500
|
+
const context = this.runtimeContext.project(joinContextSections(sections), sections);
|
|
501
|
+
const decision = await this.dispatch.waterfall("agent/pre-step", {
|
|
502
|
+
messages: claimed,
|
|
503
|
+
...position,
|
|
504
|
+
signal
|
|
505
|
+
}, () => Promise.resolve({
|
|
506
|
+
kind: "enter",
|
|
507
|
+
messages: context === void 0 ? claimed : [...claimed, context]
|
|
508
|
+
}));
|
|
509
|
+
signal.throwIfAborted();
|
|
510
|
+
return decision.kind === "reject" ? decision : {
|
|
511
|
+
...decision,
|
|
512
|
+
assembly
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
/** Open one turn before claiming its first proposed step. */
|
|
516
|
+
async turn() {
|
|
517
|
+
if (this.phase.kind !== "running") this.throwError(/* @__PURE__ */ new Error(`agent "${this.id}": turn without driver reservation`));
|
|
518
|
+
const phase = this.phase;
|
|
519
|
+
const { signal } = phase.abort;
|
|
520
|
+
signal.throwIfAborted();
|
|
521
|
+
const turn = phase.turn + 1;
|
|
522
|
+
try {
|
|
523
|
+
this.session.append("turn/start", { turn });
|
|
524
|
+
} catch (error) {
|
|
525
|
+
this.throwError(error);
|
|
526
|
+
}
|
|
527
|
+
phase.turn = turn;
|
|
528
|
+
let turnEnds = null;
|
|
529
|
+
let target = "next-turn";
|
|
530
|
+
try {
|
|
531
|
+
while (true) {
|
|
532
|
+
signal.throwIfAborted();
|
|
533
|
+
const step = phase.step + 1;
|
|
534
|
+
const decision = await this.preStep(target, {
|
|
535
|
+
turn,
|
|
536
|
+
step
|
|
537
|
+
});
|
|
538
|
+
if (decision.kind === "reject") {
|
|
539
|
+
turnEnds = { kind: "blocked" };
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
if (turnEnds && decision.messages.length === 0) break;
|
|
543
|
+
if (phase.step === 0 && decision.messages.length === 0) {
|
|
544
|
+
turnEnds = { kind: "completed" };
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
signal.throwIfAborted();
|
|
548
|
+
this.session.append("step/start", {
|
|
549
|
+
turn,
|
|
550
|
+
step
|
|
551
|
+
});
|
|
552
|
+
phase.step = step;
|
|
553
|
+
try {
|
|
554
|
+
for (const message of decision.messages) this.session.append("user/message", message, { surfaceOp: "append" });
|
|
555
|
+
const stepEnd = await this.step(decision.assembly);
|
|
556
|
+
if (turnEnds === null || turnEnds.kind !== "max-tokens") turnEnds = stepEnd;
|
|
557
|
+
} finally {
|
|
558
|
+
this.session.append("step/end", {
|
|
559
|
+
turn,
|
|
560
|
+
step
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
signal.throwIfAborted();
|
|
564
|
+
if (turnEnds && this.inbox.nextStep.length === 0) {
|
|
565
|
+
await this.dispatch.serial("agent/turn-stopping", {
|
|
566
|
+
turn,
|
|
567
|
+
signal
|
|
568
|
+
});
|
|
569
|
+
signal.throwIfAborted();
|
|
570
|
+
}
|
|
571
|
+
if (turnEnds && this.inbox.nextStep.length === 0) break;
|
|
572
|
+
target = "next-step";
|
|
573
|
+
}
|
|
574
|
+
} catch (error) {
|
|
575
|
+
if (signal.aborted) {
|
|
576
|
+
turnEnds = {
|
|
577
|
+
kind: "aborted",
|
|
578
|
+
reason: signal.reason
|
|
579
|
+
};
|
|
580
|
+
throw error;
|
|
581
|
+
}
|
|
582
|
+
turnEnds = {
|
|
583
|
+
kind: "error",
|
|
584
|
+
error: error instanceof LlmError ? error.failure : {
|
|
585
|
+
message: errorChain(error),
|
|
586
|
+
code: "UNKNOWN"
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
this.throwError(error);
|
|
590
|
+
} finally {
|
|
591
|
+
try {
|
|
592
|
+
this.session.append("turn/end", {
|
|
593
|
+
turn,
|
|
594
|
+
reason: turnEnds
|
|
595
|
+
});
|
|
596
|
+
} catch (error) {
|
|
597
|
+
this.throwError(error);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (!this.inbox.hasPending) return false;
|
|
601
|
+
phase.abort = new AbortController();
|
|
602
|
+
phase.wakeRequested = false;
|
|
603
|
+
phase.step = 0;
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
async step(assembly) {
|
|
607
|
+
/* v8 ignore next -- private callers establish the running phase before executing a step */
|
|
608
|
+
if (this.phase.kind !== "running") throw new Error(`agent "${this.id}": step outside running phase`);
|
|
609
|
+
const { turn, step, abort: { signal } } = this.phase;
|
|
610
|
+
signal.throwIfAborted();
|
|
611
|
+
const system = renderPrompt(assembly);
|
|
612
|
+
while (true) {
|
|
613
|
+
const { request, preparedCall } = await this.buildRequest(turn, step, assembly.tools, system, this.session.deriveMessages(), signal);
|
|
614
|
+
const assembler = new BlockAssembler();
|
|
615
|
+
const chunkSeqs = [];
|
|
616
|
+
const stream = preparedCall?.stream(request) ?? this.loopCtx.llm.stream(request);
|
|
617
|
+
signal.throwIfAborted();
|
|
618
|
+
for await (const chunk of stream) {
|
|
619
|
+
signal.throwIfAborted();
|
|
620
|
+
chunkSeqs.push(this.session.append("assistant/chunk", {
|
|
621
|
+
turn,
|
|
622
|
+
step,
|
|
623
|
+
chunk
|
|
624
|
+
}).seq);
|
|
625
|
+
assembler.push(chunk);
|
|
626
|
+
}
|
|
627
|
+
signal.throwIfAborted();
|
|
628
|
+
const finish = assembler.finish;
|
|
629
|
+
if (finish.kind === "error" || finish.kind === "aborted") {
|
|
630
|
+
const action = await this.dispatch.waterfall("agent/request-error", {
|
|
631
|
+
turn,
|
|
632
|
+
step,
|
|
633
|
+
provider: request.provider,
|
|
634
|
+
failure: finish.failure,
|
|
635
|
+
retryPolicy: preparedCall?.retryPolicy,
|
|
636
|
+
signal
|
|
637
|
+
}, () => Promise.resolve(void 0));
|
|
638
|
+
signal.throwIfAborted();
|
|
639
|
+
if (action?.kind !== "retry") throw new LlmError(finish.failure.message, finish.failure.code, finish.failure);
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
const message = createAssistantMessage({
|
|
643
|
+
content: assembler.blocks(),
|
|
644
|
+
source: {
|
|
645
|
+
provider: request.provider,
|
|
646
|
+
model: request.model,
|
|
647
|
+
...assembler.replayState !== void 0 ? { replayState: assembler.replayState } : {}
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
this.session.append("assistant/message", {
|
|
651
|
+
turn,
|
|
652
|
+
step,
|
|
653
|
+
message,
|
|
654
|
+
...assembler.usage === void 0 ? {} : { usage: assembler.usage }
|
|
655
|
+
}, {
|
|
656
|
+
surfaceOp: "append",
|
|
657
|
+
sourceEventSeqs: chunkSeqs
|
|
658
|
+
});
|
|
659
|
+
if (finish.kind === "max-tokens") return { kind: "max-tokens" };
|
|
660
|
+
const toolCalls = message.content.filter((block) => block.type === "tool-call");
|
|
661
|
+
if (toolCalls.length === 0) return { kind: "completed" };
|
|
662
|
+
const { concluded } = await executeToolCalls(this.loopCtx, turn, step, toolCalls, signal, (context) => this.inbox.splice("next-step", this.inbox.nextStep.length, 0, [context]));
|
|
663
|
+
return concluded ? { kind: "completed" } : null;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Compose one frozen request and bind it to the adapter registration that
|
|
668
|
+
* resolved its exact-model defaults.
|
|
669
|
+
*/
|
|
670
|
+
async buildRequest(turn, step, tools, system, boundaryMessages, signal) {
|
|
671
|
+
const { session } = this;
|
|
672
|
+
const persistedHeader = session.requestHeader();
|
|
673
|
+
const persistedConfig = persistedHeader?.config;
|
|
674
|
+
const route = {
|
|
675
|
+
provider: this.options.provider ?? "",
|
|
676
|
+
model: this.options.model ?? ""
|
|
677
|
+
};
|
|
678
|
+
const reasoningEffort = persistedConfig?.provider === route.provider && persistedConfig.model === route.model && persistedHeader?.adapterDefaults?.reasoningEffort !== true ? persistedConfig.reasoningEffort : void 0;
|
|
679
|
+
const maxTokens = this.options.maxTokens;
|
|
680
|
+
const seedConfig = deepFreeze(structuredClone(this.requestHeaderLogged ? requestProposal(persistedHeader) : {
|
|
681
|
+
...route,
|
|
682
|
+
...reasoningEffort === void 0 ? {} : { reasoningEffort },
|
|
683
|
+
...maxTokens === void 0 ? {} : { maxTokens }
|
|
684
|
+
}));
|
|
685
|
+
const proposedConfig = await this.dispatch.waterfall("agent/request", {
|
|
686
|
+
turn,
|
|
687
|
+
step,
|
|
688
|
+
signal
|
|
689
|
+
}, () => Promise.resolve(seedConfig));
|
|
690
|
+
signal.throwIfAborted();
|
|
691
|
+
if (!proposedConfig.provider || !proposedConfig.model) throw new Error(`agent "${this.id}" has no provider/model: set AgentOptions.provider and AgentOptions.model or supply both via the agent/request waterfall`);
|
|
692
|
+
let config;
|
|
693
|
+
let preparedCall;
|
|
694
|
+
try {
|
|
695
|
+
preparedCall = await this.loopCtx.llm.prepareCall(proposedConfig, signal);
|
|
696
|
+
config = preparedCall.config;
|
|
697
|
+
} catch (error) {
|
|
698
|
+
if (!(error instanceof LlmError) || error.code !== "NO_ADAPTER") throw error;
|
|
699
|
+
config = proposedConfig;
|
|
700
|
+
}
|
|
701
|
+
signal.throwIfAborted();
|
|
702
|
+
const header = canonicalHeader({
|
|
703
|
+
config,
|
|
704
|
+
...preparedCall === void 0 ? {} : { adapterDefaults: preparedCall.adapterDefaults },
|
|
705
|
+
...system ? { system } : {},
|
|
706
|
+
...tools.length > 0 ? { tools } : {}
|
|
707
|
+
});
|
|
708
|
+
const baseline = this.session.requestHeader();
|
|
709
|
+
if (!this.requestHeaderLogged) {
|
|
710
|
+
this.session.append("request/header", {
|
|
711
|
+
header,
|
|
712
|
+
reason: baseline === void 0 ? "initial" : "resume"
|
|
713
|
+
});
|
|
714
|
+
this.requestHeaderLogged = true;
|
|
715
|
+
} else if (baseline === void 0 || !headerEquals(baseline, header)) this.session.append("request/header", {
|
|
716
|
+
header,
|
|
717
|
+
reason: "change"
|
|
718
|
+
});
|
|
719
|
+
const contextWindow = preparedCall?.context?.contextWindow;
|
|
720
|
+
const requestContext = {
|
|
721
|
+
provider: config.provider,
|
|
722
|
+
model: config.model,
|
|
723
|
+
...contextWindow === void 0 ? {} : { contextWindow }
|
|
724
|
+
};
|
|
725
|
+
const previousContext = session.requestContext();
|
|
726
|
+
if (previousContext?.provider !== requestContext.provider || previousContext.model !== requestContext.model || previousContext.contextWindow !== requestContext.contextWindow) session.append("request/context", requestContext);
|
|
727
|
+
signal.throwIfAborted();
|
|
728
|
+
return {
|
|
729
|
+
request: markAgentLoopRequest(deepFreeze({
|
|
730
|
+
...header.config,
|
|
731
|
+
messages: boundaryMessages,
|
|
732
|
+
...header.system !== void 0 ? { system: header.system } : {},
|
|
733
|
+
...header.tools !== void 0 ? { tools: header.tools } : {},
|
|
734
|
+
sessionId: this.session.id,
|
|
735
|
+
signal
|
|
736
|
+
})),
|
|
737
|
+
...preparedCall === void 0 ? {} : { preparedCall }
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
//#endregion
|
|
742
|
+
//#region lib/types/constants.js
|
|
743
|
+
/** Shared agent-loop scheduler defaults.
|
|
744
|
+
* @module dsh-agent-loop/constants
|
|
745
|
+
*/
|
|
746
|
+
/** Default maximum in-flight parallel-safe calls per agent step. */
|
|
747
|
+
const DEFAULT_MAX_PARALLEL_TOOL_CALLS = 10;
|
|
748
|
+
//#endregion
|
|
749
|
+
//#region lib/types/index.js
|
|
750
|
+
/**
|
|
751
|
+
* Concrete agent-loop plugin: creates scoped ReactLoopAgents, publishes them
|
|
752
|
+
* through the agent/session registries, and owns their ordered teardown.
|
|
753
|
+
*
|
|
754
|
+
* @module @stackstackstack/dsh-agent-loop
|
|
755
|
+
*/
|
|
756
|
+
var __addDisposableResource = function(env, value, async) {
|
|
757
|
+
if (value !== null && value !== void 0) {
|
|
758
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
759
|
+
var dispose, inner;
|
|
760
|
+
if (async) {
|
|
761
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
762
|
+
dispose = value[Symbol.asyncDispose];
|
|
763
|
+
}
|
|
764
|
+
if (dispose === void 0) {
|
|
765
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
766
|
+
dispose = value[Symbol.dispose];
|
|
767
|
+
if (async) inner = dispose;
|
|
768
|
+
}
|
|
769
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
770
|
+
if (inner) dispose = function() {
|
|
771
|
+
try {
|
|
772
|
+
inner.call(this);
|
|
773
|
+
} catch (e) {
|
|
774
|
+
return Promise.reject(e);
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
env.stack.push({
|
|
778
|
+
value,
|
|
779
|
+
dispose,
|
|
780
|
+
async
|
|
781
|
+
});
|
|
782
|
+
} else if (async) env.stack.push({ async: true });
|
|
783
|
+
return value;
|
|
784
|
+
};
|
|
785
|
+
var __disposeResources = (function(SuppressedError) {
|
|
786
|
+
return function(env) {
|
|
787
|
+
function fail(e) {
|
|
788
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
789
|
+
env.hasError = true;
|
|
790
|
+
}
|
|
791
|
+
var r, s = 0;
|
|
792
|
+
function next() {
|
|
793
|
+
while (r = env.stack.pop()) try {
|
|
794
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
795
|
+
if (r.dispose) {
|
|
796
|
+
var result = r.dispose.call(r.value);
|
|
797
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) {
|
|
798
|
+
fail(e);
|
|
799
|
+
return next();
|
|
800
|
+
});
|
|
801
|
+
} else s |= 1;
|
|
802
|
+
} catch (e) {
|
|
803
|
+
fail(e);
|
|
804
|
+
}
|
|
805
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
806
|
+
if (env.hasError) throw env.error;
|
|
807
|
+
}
|
|
808
|
+
return next();
|
|
809
|
+
};
|
|
810
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) {
|
|
811
|
+
var e = new Error(message);
|
|
812
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
813
|
+
});
|
|
814
|
+
/** Fiber states that cannot own or serve a new lifecycle. */
|
|
815
|
+
const INACTIVE_STATES = new Set([
|
|
816
|
+
5,
|
|
817
|
+
4,
|
|
818
|
+
3
|
|
819
|
+
]);
|
|
820
|
+
/** Factory-level ownership: live agent teardowns plus config startup work. */
|
|
821
|
+
var FactoryOwnership = class {
|
|
822
|
+
fiber;
|
|
823
|
+
accepting = true;
|
|
824
|
+
teardown = new AbortController();
|
|
825
|
+
inactive = Promise.withResolvers();
|
|
826
|
+
liveAgents = /* @__PURE__ */ new Set();
|
|
827
|
+
startupTasks = /* @__PURE__ */ new Set();
|
|
828
|
+
constructor(fiber) {
|
|
829
|
+
this.fiber = fiber;
|
|
830
|
+
}
|
|
831
|
+
/** Aborts (reason: `agent loop is not active` error) when factory teardown begins. */
|
|
832
|
+
get signal() {
|
|
833
|
+
return this.teardown.signal;
|
|
834
|
+
}
|
|
835
|
+
isActive() {
|
|
836
|
+
return this.accepting && !INACTIVE_STATES.has(this.fiber.state);
|
|
837
|
+
}
|
|
838
|
+
/** Track one live agent's shared teardown until it has run. */
|
|
839
|
+
track(dispose) {
|
|
840
|
+
this.liveAgents.add(dispose);
|
|
841
|
+
return () => {
|
|
842
|
+
this.liveAgents.delete(dispose);
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
/** Join config startup work that begins before an agent exists. */
|
|
846
|
+
trackStartup(job) {
|
|
847
|
+
this.startupTasks.add(job);
|
|
848
|
+
const forget = () => {
|
|
849
|
+
this.startupTasks.delete(job);
|
|
850
|
+
};
|
|
851
|
+
job.then(forget, forget);
|
|
852
|
+
}
|
|
853
|
+
/** Join one public create/resume continuation; factory dispose awaits its settlement. */
|
|
854
|
+
trackWrapper(job) {
|
|
855
|
+
this.trackStartup(job.then(() => void 0, () => void 0));
|
|
856
|
+
}
|
|
857
|
+
/** Resolve `task`, or stop waiting when factory teardown begins. */
|
|
858
|
+
async waitWhileActive(job) {
|
|
859
|
+
await Promise.race([job, this.inactive.promise]);
|
|
860
|
+
}
|
|
861
|
+
async dispose() {
|
|
862
|
+
this.accepting = false;
|
|
863
|
+
this.teardown.abort(/* @__PURE__ */ new Error("agent loop is not active"));
|
|
864
|
+
this.inactive.resolve();
|
|
865
|
+
await Promise.all([...[...this.liveAgents].map((dispose) => dispose()), ...this.startupTasks]);
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
/** Await `operation`, or throw the signal's reason as soon as it aborts. */
|
|
869
|
+
async function raceAbort(operation, signal, id) {
|
|
870
|
+
const toAbortError = () => signal.reason instanceof Error ? signal.reason : new Error(`agent "${id}" creation aborted`, { cause: signal.reason });
|
|
871
|
+
if (signal.aborted) throw toAbortError();
|
|
872
|
+
const aborted = Promise.withResolvers();
|
|
873
|
+
const listener = () => {
|
|
874
|
+
aborted.reject(toAbortError());
|
|
875
|
+
};
|
|
876
|
+
signal.addEventListener("abort", listener, { once: true });
|
|
877
|
+
try {
|
|
878
|
+
return await Promise.race([Promise.resolve(operation), aborted.promise]);
|
|
879
|
+
} finally {
|
|
880
|
+
signal.removeEventListener("abort", listener);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
/** Start an abortable operation and release a value that arrives after cancellation. */
|
|
884
|
+
async function raceAbortCall(operation, signal, id, releaseAbandoned) {
|
|
885
|
+
if (signal.aborted) throw signal.reason instanceof Error ? signal.reason : new Error(`agent "${id}" creation aborted`, { cause: signal.reason });
|
|
886
|
+
const pending = Promise.resolve().then(operation);
|
|
887
|
+
try {
|
|
888
|
+
return await raceAbort(pending, signal, id);
|
|
889
|
+
} catch (error) {
|
|
890
|
+
if (signal.aborted && releaseAbandoned !== void 0) pending.then(releaseAbandoned, () => void 0);
|
|
891
|
+
throw error;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
/** Resolve the deployment-wide scheduler cap at the owning config boundary. */
|
|
895
|
+
function resolveMaxParallelToolCalls(value) {
|
|
896
|
+
const maxParallelToolCalls = value ?? 10;
|
|
897
|
+
if (!Number.isInteger(maxParallelToolCalls) || maxParallelToolCalls < 1) throw new Error("maxParallelToolCalls must be a positive integer");
|
|
898
|
+
return maxParallelToolCalls;
|
|
899
|
+
}
|
|
900
|
+
/** Reject an output-token cap that cannot be represented exactly on the request wire. */
|
|
901
|
+
function assertAgentOptions(options) {
|
|
902
|
+
if (options.maxTokens !== void 0 && (!Number.isSafeInteger(options.maxTokens) || options.maxTokens <= 0)) throw new TypeError("agent maxTokens must be a positive safe integer");
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Context key a launcher sets before any Loader entry mounts
|
|
906
|
+
* (`ctx.provide(CONFIGURED_AGENT_IDENTITIES_KEY, identities)`) to fix
|
|
907
|
+
* configured agents' session identities without a config key, so an overlay
|
|
908
|
+
* repointing the row's model route cannot drop them.
|
|
909
|
+
*/
|
|
910
|
+
const CONFIGURED_AGENT_IDENTITIES_KEY = "configuredAgentIdentities";
|
|
911
|
+
/**
|
|
912
|
+
* Apply launcher-owned identities over the configured agents, replacing both
|
|
913
|
+
* identity keys for every entry the launcher named so a config-supplied
|
|
914
|
+
* identity can never survive alongside a launcher-supplied one.
|
|
915
|
+
* @param agents - the configured agent entries.
|
|
916
|
+
* @param identities - launcher identities keyed by configured agent `id`, or `undefined`.
|
|
917
|
+
* @returns the entries with launcher-owned identities applied.
|
|
918
|
+
*/
|
|
919
|
+
function applyLauncherIdentities(agents, identities) {
|
|
920
|
+
if (identities === void 0) return agents;
|
|
921
|
+
return agents.map((agent) => {
|
|
922
|
+
const identity = identities[agent.id];
|
|
923
|
+
if (identity === void 0) return agent;
|
|
924
|
+
const { sessionId: _sessionId, resumeSessionId: _resumeSessionId, ...rest } = agent;
|
|
925
|
+
return identity.resume ? {
|
|
926
|
+
...rest,
|
|
927
|
+
resumeSessionId: identity.id
|
|
928
|
+
} : {
|
|
929
|
+
...rest,
|
|
930
|
+
sessionId: identity.id
|
|
931
|
+
};
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
/** Settings namespace carrying the tool-call parallelism a user owns. */
|
|
935
|
+
const AGENT_LOOP_SETTINGS_NAMESPACE = settingsNamespace("agent-loop");
|
|
936
|
+
/** Schema of the agent-loop settings section. */
|
|
937
|
+
const AGENT_LOOP_SETTINGS_SCHEMA = z.object({ maxParallelToolCalls: z.number().step(1).min(1).default(10) });
|
|
938
|
+
/** Reject self-contained identity conflicts before any configured agent starts. */
|
|
939
|
+
function validateConfiguredAgents(agents) {
|
|
940
|
+
const exactIdentities = /* @__PURE__ */ new Map();
|
|
941
|
+
for (const { id, sessionId, resumeSessionId } of agents) {
|
|
942
|
+
const hasResumeId = resumeSessionId !== void 0 && resumeSessionId !== "";
|
|
943
|
+
if (sessionId !== void 0 && hasResumeId) throw new Error(`agent "${id}": sessionId and resumeSessionId are mutually exclusive`);
|
|
944
|
+
const exactIdentity = hasResumeId ? resumeSessionId : sessionId;
|
|
945
|
+
if (exactIdentity === void 0) continue;
|
|
946
|
+
const firstId = exactIdentities.get(exactIdentity);
|
|
947
|
+
if (firstId !== void 0) throw new Error(`agents "${firstId}" and "${id}" use duplicate exact session identity "${exactIdentity}"`);
|
|
948
|
+
exactIdentities.set(exactIdentity, id);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
/** Concrete agent factory and driver service. */
|
|
952
|
+
var AgentLoop = class extends Service {
|
|
953
|
+
static inject = [
|
|
954
|
+
"agents",
|
|
955
|
+
"sessions",
|
|
956
|
+
"llm",
|
|
957
|
+
"tools",
|
|
958
|
+
"systemPrompt"
|
|
959
|
+
];
|
|
960
|
+
/** Runtime schema for declarative agents. */
|
|
961
|
+
static Config = z.object({
|
|
962
|
+
maxParallelToolCalls: z.number().step(1).min(1).default(10),
|
|
963
|
+
agents: z.array(z.object({
|
|
964
|
+
id: z.string().required(),
|
|
965
|
+
sessionId: z.string().min(1),
|
|
966
|
+
provider: z.string(),
|
|
967
|
+
model: z.string(),
|
|
968
|
+
maxTokens: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER),
|
|
969
|
+
cwd: z.string(),
|
|
970
|
+
resumeSessionId: z.string()
|
|
971
|
+
})).default([])
|
|
972
|
+
});
|
|
973
|
+
/** Validated configuration owned by the agent-loop service. */
|
|
974
|
+
config;
|
|
975
|
+
ownership;
|
|
976
|
+
/** Plain holder prevents Cordis from re-tracing the factory's dependency context through a caller shadow. */
|
|
977
|
+
runtime;
|
|
978
|
+
constructor(ctx, config) {
|
|
979
|
+
super(ctx, "agentLoop");
|
|
980
|
+
const entry = { maxParallelToolCalls: resolveMaxParallelToolCalls(config.maxParallelToolCalls) };
|
|
981
|
+
let source = () => entry;
|
|
982
|
+
this.config = {
|
|
983
|
+
...config,
|
|
984
|
+
agents: applyLauncherIdentities(config.agents, ctx.get(CONFIGURED_AGENT_IDENTITIES_KEY)),
|
|
985
|
+
get maxParallelToolCalls() {
|
|
986
|
+
return source().maxParallelToolCalls;
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
installSettingsSection(ctx, AGENT_LOOP_SETTINGS_NAMESPACE, AGENT_LOOP_SETTINGS_SCHEMA, entry, {
|
|
990
|
+
validate: (value) => void resolveMaxParallelToolCalls(value.maxParallelToolCalls),
|
|
991
|
+
setSource: (current) => {
|
|
992
|
+
source = current;
|
|
993
|
+
},
|
|
994
|
+
onChange: () => {}
|
|
995
|
+
});
|
|
996
|
+
validateConfiguredAgents(this.config.agents);
|
|
997
|
+
this.ownership = new FactoryOwnership(ctx.fiber);
|
|
998
|
+
this.runtime = { ctx };
|
|
999
|
+
ctx.effect(() => () => this.ownership.dispose(), "agentLoop.transactions()");
|
|
1000
|
+
ctx.effect(() => ctx.agents.setFactory(this), "agentLoop.setFactory()");
|
|
1001
|
+
ctx.systemPrompt.variable("provider", (context) => context.agent?.options.provider);
|
|
1002
|
+
ctx.systemPrompt.variable("model", (context) => context.agent?.options.model);
|
|
1003
|
+
ctx.systemPrompt.variable("cwd", (context) => context.agent?.session.header.cwd);
|
|
1004
|
+
for (const { id, sessionId, cwd, resumeSessionId, ...options } of this.config.agents) {
|
|
1005
|
+
const meta = cwd === void 0 ? {} : { cwd };
|
|
1006
|
+
if (resumeSessionId === void 0 || resumeSessionId === "") {
|
|
1007
|
+
const configuredId = sessionId ?? SessionId(`${id}-session-${randomUUID()}`);
|
|
1008
|
+
const persistence = sessionId === void 0 ? void 0 : ctx.get("sessionPersistence");
|
|
1009
|
+
if (persistence === void 0) this.create(configuredId, options, meta);
|
|
1010
|
+
else {
|
|
1011
|
+
const startup = this.restoreOrCreateConfigured(ctx, persistence, configuredId, options, meta).catch((error) => {
|
|
1012
|
+
this.reportConfiguredStartupFailure(id, "restore", configuredId, error);
|
|
1013
|
+
});
|
|
1014
|
+
this.ownership.trackStartup(startup);
|
|
1015
|
+
}
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
ctx.effect(() => {
|
|
1019
|
+
return ctx.inject(["sessionPersistence"], (childCtx) => {
|
|
1020
|
+
this.resumeWith(ctx, childCtx.sessionPersistence, {
|
|
1021
|
+
resumeSessionId,
|
|
1022
|
+
agentOptions: options
|
|
1023
|
+
}).catch((error) => {
|
|
1024
|
+
this.reportConfiguredStartupFailure(id, "resume", resumeSessionId, error);
|
|
1025
|
+
});
|
|
1026
|
+
}).dispose;
|
|
1027
|
+
}, `agentLoop.resume(${id})`);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
/** Report a contained declarative-start failure to identity-bound consumers. */
|
|
1031
|
+
reportConfiguredStartupFailure(configId, action, sessionId, error) {
|
|
1032
|
+
if (!this.ownership.isActive()) return;
|
|
1033
|
+
this.ctx.logger.warn(`agent "${configId}": config-driven ${action} of "${sessionId}" failed: ${errorChain(error)}`);
|
|
1034
|
+
const args = ["agent-loop/config-start-failed", {
|
|
1035
|
+
sessionId,
|
|
1036
|
+
error
|
|
1037
|
+
}];
|
|
1038
|
+
for (const callback of this.ctx.events.dispatch("emit", args)) try {
|
|
1039
|
+
const returned = callback(...args);
|
|
1040
|
+
Promise.resolve(returned).catch((listenerError) => {
|
|
1041
|
+
this.ctx.logger.warn(`agent "${configId}": config-start-failed listener rejected: ${errorChain(listenerError)}`);
|
|
1042
|
+
});
|
|
1043
|
+
} catch (listenerError) {
|
|
1044
|
+
this.ctx.logger.warn(`agent "${configId}": config-start-failed listener threw: ${errorChain(listenerError)}`);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
/** Restore a materialized exact config identity on remount, or create it on first use. */
|
|
1048
|
+
async restoreOrCreateConfigured(ownerCtx, persistence, sessionId, agentOptions, meta) {
|
|
1049
|
+
await this.waitForDrainingConfiguredIdentity(ownerCtx, sessionId);
|
|
1050
|
+
if (!this.ownership.isActive()) return;
|
|
1051
|
+
try {
|
|
1052
|
+
await this.resumeWith(ownerCtx, persistence, {
|
|
1053
|
+
resumeSessionId: sessionId,
|
|
1054
|
+
agentOptions
|
|
1055
|
+
});
|
|
1056
|
+
return;
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
if (!this.ownership.isActive()) return;
|
|
1059
|
+
if ((await persistence.list()).some((header) => header.id === sessionId)) throw error;
|
|
1060
|
+
}
|
|
1061
|
+
this.create(sessionId, agentOptions, meta);
|
|
1062
|
+
}
|
|
1063
|
+
/** Wait for a draining same-id lifecycle to finish registry teardown. */
|
|
1064
|
+
async waitForDrainingConfiguredIdentity(ownerCtx, sessionId) {
|
|
1065
|
+
if (ownerCtx.agents.get(sessionId) === void 0 && ownerCtx.sessions.get(sessionId) === void 0) return;
|
|
1066
|
+
const released = Promise.withResolvers();
|
|
1067
|
+
const checkReleased = () => {
|
|
1068
|
+
if (ownerCtx.agents.get(sessionId) === void 0 && ownerCtx.sessions.get(sessionId) === void 0) released.resolve();
|
|
1069
|
+
};
|
|
1070
|
+
const disposeAgentListener = ownerCtx.on("agent/disposed", () => {
|
|
1071
|
+
checkReleased();
|
|
1072
|
+
});
|
|
1073
|
+
const disposeSessionListener = ownerCtx.on("session/disposed", checkReleased);
|
|
1074
|
+
try {
|
|
1075
|
+
checkReleased();
|
|
1076
|
+
await this.ownership.waitWhileActive(released.promise);
|
|
1077
|
+
} finally {
|
|
1078
|
+
disposeAgentListener();
|
|
1079
|
+
disposeSessionListener();
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Construct the driver, scope, and one memoized reverse teardown for a new
|
|
1084
|
+
* agent. The teardown is registered with the factory and the owner fiber
|
|
1085
|
+
* BEFORE publication, so a mid-setup unload rolls everything back; `signal`
|
|
1086
|
+
* fuses caller cancellation with lifecycle teardown for setup awaits.
|
|
1087
|
+
*/
|
|
1088
|
+
prepare(ownerCtx, id, options, session, callerSignal) {
|
|
1089
|
+
assertAgentOptions(options);
|
|
1090
|
+
ownerCtx.fiber.assertActive();
|
|
1091
|
+
/* v8 ignore next -- unreachable backstop, see above */
|
|
1092
|
+
if (!this.ownership.isActive()) throw new Error("agent loop is not active");
|
|
1093
|
+
if (callerSignal?.aborted) throw callerSignal.reason instanceof Error ? callerSignal.reason : new Error(`agent "${id}" creation aborted`, { cause: callerSignal.reason });
|
|
1094
|
+
const loopCtx = this.runtime.ctx;
|
|
1095
|
+
const abort = new AbortController();
|
|
1096
|
+
const onCallerAbort = () => {
|
|
1097
|
+
abort.abort(callerSignal?.reason instanceof Error ? callerSignal.reason : new Error(`agent "${id}" creation aborted`, { cause: callerSignal?.reason }));
|
|
1098
|
+
};
|
|
1099
|
+
const onFactoryTeardown = () => {
|
|
1100
|
+
abort.abort(this.ownership.signal.reason);
|
|
1101
|
+
};
|
|
1102
|
+
callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
1103
|
+
this.ownership.signal.addEventListener("abort", onFactoryTeardown, { once: true });
|
|
1104
|
+
let machine;
|
|
1105
|
+
let detachSession;
|
|
1106
|
+
let detachAgent;
|
|
1107
|
+
let disposing;
|
|
1108
|
+
const machineReady = Promise.withResolvers();
|
|
1109
|
+
const dispose = (ownerTriggered = false) => disposing ??= (async () => {
|
|
1110
|
+
abort.abort(/* @__PURE__ */ new Error(`agent "${id}" lifecycle disposed`));
|
|
1111
|
+
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
1112
|
+
this.ownership.signal.removeEventListener("abort", onFactoryTeardown);
|
|
1113
|
+
try {
|
|
1114
|
+
if (machine === void 0) await machineReady.promise;
|
|
1115
|
+
if (machine !== void 0) {
|
|
1116
|
+
machine.cancel({ kind: "disposed" });
|
|
1117
|
+
await machine.whenIdle();
|
|
1118
|
+
await machine.scope.dispose();
|
|
1119
|
+
}
|
|
1120
|
+
} finally {
|
|
1121
|
+
try {
|
|
1122
|
+
detachAgent?.();
|
|
1123
|
+
detachSession?.();
|
|
1124
|
+
} finally {
|
|
1125
|
+
untrack();
|
|
1126
|
+
if (!ownerTriggered) await unfollowOwner();
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
})();
|
|
1130
|
+
const untrack = this.ownership.track(dispose);
|
|
1131
|
+
let unfollowOwner;
|
|
1132
|
+
try {
|
|
1133
|
+
unfollowOwner = ownerCtx.effect(() => () => {
|
|
1134
|
+
if (disposing !== void 0) return;
|
|
1135
|
+
abort.abort(/* @__PURE__ */ new Error(`agent "${id}" setup aborted: owner disposed during setup`));
|
|
1136
|
+
return dispose(true);
|
|
1137
|
+
}, `agentLoop.lifecycle(${id})`);
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
untrack();
|
|
1140
|
+
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
1141
|
+
this.ownership.signal.removeEventListener("abort", onFactoryTeardown);
|
|
1142
|
+
throw error;
|
|
1143
|
+
}
|
|
1144
|
+
/* v8 ignore stop */
|
|
1145
|
+
const assertLive = () => {
|
|
1146
|
+
if (!abort.signal.aborted) return;
|
|
1147
|
+
/* v8 ignore next -- unreachable String() arm, see above */
|
|
1148
|
+
throw abort.signal.reason instanceof Error ? abort.signal.reason : new Error(String(abort.signal.reason));
|
|
1149
|
+
};
|
|
1150
|
+
try {
|
|
1151
|
+
const agent = machine = new ReactLoopAgent(loopCtx, id, options, session);
|
|
1152
|
+
machineReady.resolve();
|
|
1153
|
+
assertLive();
|
|
1154
|
+
return {
|
|
1155
|
+
agent,
|
|
1156
|
+
signal: abort.signal,
|
|
1157
|
+
publish: (source) => {
|
|
1158
|
+
assertLive();
|
|
1159
|
+
detachSession = agent.ctx.sessions.enter(session);
|
|
1160
|
+
detachAgent = loopCtx.agents.enter(agent, ownerCtx.agent);
|
|
1161
|
+
agent.ctx.sessions.announce(session);
|
|
1162
|
+
assertLive();
|
|
1163
|
+
loopCtx.agents.announce(agent);
|
|
1164
|
+
assertLive();
|
|
1165
|
+
emitAgentEvent(loopCtx, agent, "agent/session-start", { source });
|
|
1166
|
+
assertLive();
|
|
1167
|
+
return {
|
|
1168
|
+
agent,
|
|
1169
|
+
dispose
|
|
1170
|
+
};
|
|
1171
|
+
},
|
|
1172
|
+
dispose
|
|
1173
|
+
};
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
machineReady.resolve();
|
|
1176
|
+
dispose();
|
|
1177
|
+
throw error;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Create an agent and session under one caller-supplied identity, owned by
|
|
1182
|
+
* the accessing fiber. Constructor-driven config calls mint a fresh combined
|
|
1183
|
+
* id before entering this boundary.
|
|
1184
|
+
* @param id - shared agent/session identity.
|
|
1185
|
+
* @param options - concrete loop options.
|
|
1186
|
+
* @param meta - optional fresh-session workspace metadata.
|
|
1187
|
+
* @returns the published running agent.
|
|
1188
|
+
*/
|
|
1189
|
+
create(id, options = {}, meta = {}) {
|
|
1190
|
+
const env_1 = {
|
|
1191
|
+
stack: [],
|
|
1192
|
+
error: void 0,
|
|
1193
|
+
hasError: false
|
|
1194
|
+
};
|
|
1195
|
+
try {
|
|
1196
|
+
const preparation = __addDisposableResource(env_1, SessionPreparation.create(this.runtime.ctx.sessions.prepare(id, { meta })), false);
|
|
1197
|
+
const prepared = this.prepare(this.ctx, id, options, preparation.session);
|
|
1198
|
+
try {
|
|
1199
|
+
return prepared.publish("startup").agent;
|
|
1200
|
+
} catch (error) {
|
|
1201
|
+
prepared.dispose();
|
|
1202
|
+
throw error;
|
|
1203
|
+
}
|
|
1204
|
+
} catch (e_1) {
|
|
1205
|
+
env_1.error = e_1;
|
|
1206
|
+
env_1.hasError = true;
|
|
1207
|
+
} finally {
|
|
1208
|
+
__disposeResources(env_1);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Create an owned agent on a caller-supplied session id.
|
|
1213
|
+
* @param ownerCtx - caller context that structurally owns the lifecycle.
|
|
1214
|
+
* @param options - identities, session seed/metadata, loop options, setup, and cancellation.
|
|
1215
|
+
* @returns the published handle.
|
|
1216
|
+
*/
|
|
1217
|
+
async createAgent(ownerCtx, options) {
|
|
1218
|
+
const preparation = SessionPreparation.create(this.runtime.ctx.sessions.prepare(options.sessionId, {
|
|
1219
|
+
...options.seed === void 0 ? {} : { seed: options.seed },
|
|
1220
|
+
...options.meta === void 0 ? {} : { meta: options.meta }
|
|
1221
|
+
}));
|
|
1222
|
+
const published = this.setupAndPublish(ownerCtx, options.sessionId, preparation, options.agentOptions ?? {}, options.setup, options.signal, "startup");
|
|
1223
|
+
this.ownership.trackWrapper(published);
|
|
1224
|
+
return published;
|
|
1225
|
+
}
|
|
1226
|
+
/** Prepare one Agent around an acquired Session, run setup, and publish it. */
|
|
1227
|
+
async setupAndPublish(ownerCtx, id, preparation, agentOptions, setup, signal, source) {
|
|
1228
|
+
const env_2 = {
|
|
1229
|
+
stack: [],
|
|
1230
|
+
error: void 0,
|
|
1231
|
+
hasError: false
|
|
1232
|
+
};
|
|
1233
|
+
try {
|
|
1234
|
+
const session = __addDisposableResource(env_2, preparation, false).session;
|
|
1235
|
+
const prepared = this.prepare(ownerCtx, id, agentOptions, session, signal);
|
|
1236
|
+
try {
|
|
1237
|
+
(await raceAbort(setup?.(prepared.agent.ctx), prepared.signal, id))?.commit();
|
|
1238
|
+
return prepared.publish(source);
|
|
1239
|
+
} catch (error) {
|
|
1240
|
+
await prepared.dispose();
|
|
1241
|
+
throw error;
|
|
1242
|
+
}
|
|
1243
|
+
} catch (e_2) {
|
|
1244
|
+
env_2.error = e_2;
|
|
1245
|
+
env_2.hasError = true;
|
|
1246
|
+
} finally {
|
|
1247
|
+
__disposeResources(env_2);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Resume an owned agent from the configured persistence service.
|
|
1252
|
+
* @param ownerCtx - caller context that owns load, setup, and the live lifecycle.
|
|
1253
|
+
* @param options - persisted identity, loop options, setup, and cancellation.
|
|
1254
|
+
* @returns the published handle.
|
|
1255
|
+
*/
|
|
1256
|
+
async resume(ownerCtx, options) {
|
|
1257
|
+
const persistence = this.runtime.ctx.get("sessionPersistence");
|
|
1258
|
+
if (persistence === void 0) throw new Error("cannot resume: session persistence is not configured (load a dsh-session-persistence backend)");
|
|
1259
|
+
return this.resumeWith(ownerCtx, persistence, options);
|
|
1260
|
+
}
|
|
1261
|
+
/** Resume through an explicit persistence handle used by the deferred config path. */
|
|
1262
|
+
resumeWith(ownerCtx, persistence, options) {
|
|
1263
|
+
const id = options.resumeSessionId;
|
|
1264
|
+
const published = (async () => {
|
|
1265
|
+
const ownerAbort = new AbortController();
|
|
1266
|
+
const unfollowOwner = ownerCtx.effect(() => () => {
|
|
1267
|
+
ownerAbort.abort(/* @__PURE__ */ new Error(`agent "${id}" setup aborted: owner disposed during setup`));
|
|
1268
|
+
}, `agentLoop.resume-load(${id})`);
|
|
1269
|
+
const fused = AbortSignal.any([
|
|
1270
|
+
...options.signal === void 0 ? [] : [options.signal],
|
|
1271
|
+
ownerAbort.signal,
|
|
1272
|
+
this.ownership.signal
|
|
1273
|
+
]);
|
|
1274
|
+
let preparation;
|
|
1275
|
+
try {
|
|
1276
|
+
try {
|
|
1277
|
+
preparation = await raceAbortCall(() => persistence.prepare(id, fused), fused, id, (abandoned) => {
|
|
1278
|
+
abandoned[Symbol.dispose]();
|
|
1279
|
+
});
|
|
1280
|
+
} finally {
|
|
1281
|
+
await unfollowOwner();
|
|
1282
|
+
}
|
|
1283
|
+
ownerCtx.fiber.assertActive();
|
|
1284
|
+
if (!this.ownership.isActive()) throw new Error("agent loop is not active");
|
|
1285
|
+
return await this.setupAndPublish(ownerCtx, id, preparation, options.agentOptions ?? {}, options.setup, options.signal, "resume");
|
|
1286
|
+
} finally {
|
|
1287
|
+
preparation?.[Symbol.dispose]();
|
|
1288
|
+
}
|
|
1289
|
+
})();
|
|
1290
|
+
this.ownership.trackWrapper(published);
|
|
1291
|
+
return published;
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
//#endregion
|
|
1295
|
+
export { AGENT_LOOP_SETTINGS_NAMESPACE, AGENT_LOOP_SETTINGS_SCHEMA, AgentLoop, AgentLoop as default, CONFIGURED_AGENT_IDENTITIES_KEY, DEFAULT_MAX_PARALLEL_TOOL_CALLS };
|