@fishwithcat/dsh-agent 0.0.0-stage → 0.2.2-alpha.2

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/lib/index.js ADDED
@@ -0,0 +1,917 @@
1
+ import { Service, getTraceable, symbols } from "@deepseek-ai/cordis";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ import { isPromise } from "node:util/types";
4
+ import { scopeTarget } from "@deepseek-ai/dsh-scope";
5
+ import { brandNumber } from "@deepseek-ai/dsh-brand";
6
+ import { z } from "zod";
7
+ import { AssistantStreamAccumulator, boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
8
+ import { ReasoningEffortId } from "@deepseek-ai/dsh-llm/brand";
9
+ //#region lib/types/archive-admission.js
10
+ /**
11
+ * The `turn` family of the Workspace registry's archive admission: whether a
12
+ * Session's own Agent is inside a turn, and its cancel when the Session is
13
+ * archived with its work. Installed by the registry's constructor, so it
14
+ * answers for every Agent the registry publishes.
15
+ *
16
+ * @module @deepseek-ai/dsh-agent
17
+ */
18
+ /**
19
+ * Answer `workspace/session-activity` with the `turn` family while the
20
+ * Session's Agent is running (a turn waiting for an approval or an answer
21
+ * included), and `workspace/session-stop` by cancelling that turn the way
22
+ * the user's own stop does — `agent.cancel({ kind: 'user' })`, but without
23
+ * the stop button's `keepInbox`, so queued input is discarded with a logged
24
+ * inbox splice instead of waking the archived Session later. Nothing is
25
+ * awaited to settlement. Both listeners live as long as `ctx`'s fiber.
26
+ * @param ctx - the registry's registration context.
27
+ * @param lookup - the registry's live-Agent lookup by Session id.
28
+ */
29
+ function installTurnArchiveAdmission(ctx, lookup) {
30
+ ctx.on("workspace/session-activity", async ({ sessionId }, next) => {
31
+ const running = lookup(sessionId)?.status === "running";
32
+ const rest = await next();
33
+ if (!running) return rest;
34
+ return [{ kind: "turn" }, ...rest];
35
+ });
36
+ ctx.on("workspace/session-stop", ({ sessionId }) => {
37
+ const agent = lookup(sessionId);
38
+ if (agent?.status === "running") agent.cancel({ kind: "user" });
39
+ });
40
+ }
41
+ //#endregion
42
+ //#region lib/types/projection.js
43
+ const sessionSeqSchema = z.number().int().nonnegative().transform(brandNumber);
44
+ /** Host projection of agent turn and step boundaries. */
45
+ const turnBoundaryProjectionDefinition = {
46
+ key: "turnBoundary",
47
+ stateVersion: 2,
48
+ stateSchema: z.object({
49
+ openTurnStartSeq: sessionSeqSchema.nullable(),
50
+ lastStepStartSeq: sessionSeqSchema.nullable(),
51
+ lastStepBoundary: z.object({
52
+ kind: z.union([z.literal("start"), z.literal("end")]),
53
+ seq: sessionSeqSchema
54
+ }).nullable(),
55
+ lastTurn: z.number().int().nonnegative()
56
+ }),
57
+ init: () => ({
58
+ openTurnStartSeq: null,
59
+ lastStepStartSeq: null,
60
+ lastStepBoundary: null,
61
+ lastTurn: 0
62
+ }),
63
+ apply: (state, event) => {
64
+ switch (event.type) {
65
+ case "turn/start": return {
66
+ ...state,
67
+ openTurnStartSeq: event.seq,
68
+ lastTurn: event.data.turn
69
+ };
70
+ case "turn/end": return {
71
+ ...state,
72
+ openTurnStartSeq: null
73
+ };
74
+ case "step/start": return {
75
+ ...state,
76
+ lastStepStartSeq: event.seq,
77
+ lastStepBoundary: {
78
+ kind: "start",
79
+ seq: event.seq
80
+ }
81
+ };
82
+ case "step/end": return {
83
+ ...state,
84
+ lastStepBoundary: {
85
+ kind: "end",
86
+ seq: event.seq
87
+ }
88
+ };
89
+ default: return state;
90
+ }
91
+ }
92
+ };
93
+ //#endregion
94
+ //#region lib/types/assistant-stream-state.js
95
+ /**
96
+ * Process-local assistant-attempt state retained for reconnecting presentation
97
+ * consumers. This fold is independent of any transport: the Web follow stream,
98
+ * the terminal watch stream, and other local viewers share it.
99
+ * @module @deepseek-ai/dsh-agent/assistant-stream-state
100
+ */
101
+ const EMPTY_BASELINE = { revision: 0 };
102
+ /**
103
+ * Folds dense Agent frames into the latest process-local attempt and
104
+ * materializes one shared immutable reconnect baseline per accepted revision.
105
+ */
106
+ var AssistantStreamState = class {
107
+ activeAttempt;
108
+ revision = 0;
109
+ snapshotValue = EMPTY_BASELINE;
110
+ dirty = false;
111
+ /**
112
+ * Fold one trusted frame from the current attached Agent lifecycle.
113
+ * @param frame - next dense process-local Assistant frame.
114
+ * @param durableCursor - last committed Session seq when this frame was observed.
115
+ */
116
+ accept(frame, durableCursor) {
117
+ if (frame.type === "start" && frame.revision === 1 && this.revision !== 0) {
118
+ this.activeAttempt = void 0;
119
+ this.revision = 0;
120
+ }
121
+ if (frame.revision !== this.revision + 1) {
122
+ this.activeAttempt = void 0;
123
+ this.revision = frame.revision;
124
+ this.dirty = true;
125
+ return;
126
+ }
127
+ this.revision = frame.revision;
128
+ switch (frame.type) {
129
+ case "start":
130
+ this.activeAttempt = {
131
+ attemptId: frame.attemptId,
132
+ startedAfterSeq: durableCursor,
133
+ turn: frame.turn,
134
+ step: frame.step,
135
+ stream: new AssistantStreamAccumulator(),
136
+ nextIndex: 0
137
+ };
138
+ break;
139
+ case "chunk": {
140
+ const attempt = this.activeAttempt;
141
+ if (attempt === void 0 || attempt.attemptId !== frame.attemptId || frame.index !== attempt.nextIndex) {
142
+ this.activeAttempt = void 0;
143
+ break;
144
+ }
145
+ attempt.stream.push({
146
+ time: frame.time,
147
+ chunk: frame.chunk
148
+ });
149
+ attempt.nextIndex += 1;
150
+ break;
151
+ }
152
+ case "end":
153
+ this.activeAttempt = void 0;
154
+ break;
155
+ }
156
+ this.dirty = true;
157
+ }
158
+ /**
159
+ * Read the cached reconnect baseline, materializing it after a state change.
160
+ * @returns the identity-stable baseline for the latest accepted revision.
161
+ */
162
+ snapshot() {
163
+ if (!this.dirty) return this.snapshotValue;
164
+ this.snapshotValue = {
165
+ revision: this.revision,
166
+ ...this.activeAttempt === void 0 ? {} : { activeAttempt: {
167
+ attemptId: this.activeAttempt.attemptId,
168
+ startedAfterSeq: this.activeAttempt.startedAfterSeq,
169
+ turn: this.activeAttempt.turn,
170
+ step: this.activeAttempt.step,
171
+ nextIndex: this.activeAttempt.nextIndex,
172
+ stream: this.activeAttempt.stream.snapshot()
173
+ } }
174
+ };
175
+ this.dirty = false;
176
+ return this.snapshotValue;
177
+ }
178
+ };
179
+ //#endregion
180
+ //#region lib/types/model-selection.js
181
+ /**
182
+ * Build a detached selection, omitting an absent effort.
183
+ * @param provider - provider route.
184
+ * @param model - provider-owned model id.
185
+ * @param reasoningEffort - explicit effort; omission follows adapter defaults.
186
+ * @returns a detached selection.
187
+ */
188
+ function toSelection(provider, model, reasoningEffort) {
189
+ return {
190
+ provider,
191
+ model,
192
+ ...reasoningEffort === void 0 ? {} : { reasoningEffort }
193
+ };
194
+ }
195
+ /**
196
+ * Compare provider and model without comparing reasoning effort.
197
+ * @param left - first selection.
198
+ * @param right - second selection.
199
+ * @returns whether both selections name the same route.
200
+ */
201
+ function sameRoute(left, right) {
202
+ return left.provider === right.provider && left.model === right.model;
203
+ }
204
+ /**
205
+ * Compare complete selections, treating absent effort as its own value.
206
+ * @param left - first selection, or no recorded selection.
207
+ * @param right - second selection, or no recorded selection.
208
+ * @returns whether both selections agree.
209
+ */
210
+ function sameModelSelection(left, right) {
211
+ return left === right || left !== null && right !== null && sameRoute(left, right) && left.reasoningEffort === right.reasoningEffort;
212
+ }
213
+ /** Selection a recorded request used, omitting an effort the adapter defaulted. */
214
+ function selectionOfHeader({ config, adapterDefaults }) {
215
+ return toSelection(config.provider, config.model, adapterDefaults?.reasoningEffort === true ? void 0 : config.reasoningEffort);
216
+ }
217
+ const selectionSchema = z.object({
218
+ provider: z.string().min(1),
219
+ model: z.string().min(1),
220
+ reasoningEffort: z.string().min(1).transform(ReasoningEffortId).exactOptional()
221
+ }).nullable();
222
+ /**
223
+ * Session selection fold: recorded selections become
224
+ * the pending choice, and each request header retires a matching one.
225
+ */
226
+ const modelSelectionProjectionDefinition = {
227
+ key: "modelSelection",
228
+ stateVersion: 3,
229
+ stateSchema: z.object({
230
+ lastUsed: selectionSchema,
231
+ pending: selectionSchema
232
+ }),
233
+ init: () => ({
234
+ lastUsed: null,
235
+ pending: null
236
+ }),
237
+ apply: (state, event) => {
238
+ if (event.type === "model/selection") return sameModelSelection(state.pending, event.data) ? state : {
239
+ ...state,
240
+ pending: event.data
241
+ };
242
+ if (event.type !== "request/header") return state;
243
+ const lastUsed = selectionOfHeader(event.data.header);
244
+ const pending = sameModelSelection(state.pending, lastUsed) ? null : state.pending;
245
+ return sameModelSelection(state.lastUsed, lastUsed) && pending === state.pending ? state : {
246
+ lastUsed,
247
+ pending
248
+ };
249
+ },
250
+ wire: {
251
+ viewSchema: z.object({
252
+ lastUsed: selectionSchema,
253
+ next: selectionSchema
254
+ }),
255
+ view: (state) => ({
256
+ lastUsed: state.lastUsed,
257
+ next: state.pending ?? state.lastUsed
258
+ })
259
+ }
260
+ };
261
+ /**
262
+ * Read and record one Agent's model choices without resolving adapter defaults.
263
+ * @param readState - driver-owned reader for this Session's selection projection.
264
+ * @param session - Session receiving durable model selections.
265
+ * @param options - live route fallback until a selection or request is recorded.
266
+ */
267
+ var AgentModel = class {
268
+ readState;
269
+ session;
270
+ options;
271
+ constructor(readState, session, options) {
272
+ this.readState = readState;
273
+ this.session = session;
274
+ this.options = options;
275
+ }
276
+ current() {
277
+ const recorded = this.recordedSelection();
278
+ if (recorded !== null) return { ...recorded };
279
+ const { provider, model, reasoningEffort } = this.options;
280
+ return provider && model ? toSelection(provider, model, reasoningEffort) : void 0;
281
+ }
282
+ select(selection) {
283
+ if (sameModelSelection(this.recordedSelection(), selection)) return;
284
+ this.session.append("model/selection", toSelection(selection.provider, selection.model, selection.reasoningEffort));
285
+ }
286
+ recordedSelection() {
287
+ const state = this.readState();
288
+ return state.pending ?? state.lastUsed;
289
+ }
290
+ };
291
+ function routeLabel(route, other) {
292
+ return route.provider === other.provider ? route.model : `${route.provider}/${route.model}`;
293
+ }
294
+ function modelSwitchNotice(previous, selected) {
295
+ const from = routeLabel(previous, selected);
296
+ const to = routeLabel(selected, previous);
297
+ return createUserMessage({
298
+ content: [{
299
+ type: "text",
300
+ text: `[model changed: assistant turns above this point were generated by ${from}; the session continues with ${to}]`
301
+ }],
302
+ source: {
303
+ kind: "model-selection",
304
+ form: "notice",
305
+ summary: boundContextSummary(`${from} → ${to}`)
306
+ }
307
+ });
308
+ }
309
+ /**
310
+ * Append a model-switch notice when the step's fixed route differs from the
311
+ * last request header. Effort-only changes and empty no-request decisions add
312
+ * no notice; a failure before header persistence repeats it on the next step.
313
+ * Install in the selected Agent's unpublished setup scope.
314
+ * @param agentCtx - The selected Agent's scoped context.
315
+ * @returns Disposer for the scoped pre-step listener.
316
+ */
317
+ function installModelSelection(agentCtx) {
318
+ return agentCtx.on("agent/pre-step", async ({ agent, messages, signal, step, modelSelection }, next) => {
319
+ const decision = await next();
320
+ if (decision.kind === "reject" || signal.aborted) return decision;
321
+ if (decision.messages.length === 0 && (step === 1 || messages.length > 0)) return decision;
322
+ const previous = agent.session.requestHeader()?.config;
323
+ if (previous === void 0 || modelSelection.provider === previous.provider && modelSelection.model === previous.model) return decision;
324
+ return {
325
+ ...decision,
326
+ messages: [...decision.messages, modelSwitchNotice(previous, modelSelection)]
327
+ };
328
+ }, { prepend: true });
329
+ }
330
+ //#endregion
331
+ //#region lib/types/consumed-work.js
332
+ /**
333
+ * How one agent log accounts for the work it consumed.
334
+ *
335
+ * The turn and step vocabulary alone cannot answer this. A turn that stops
336
+ * before its first step leaves a `turn/end` shaped exactly like the balanced
337
+ * no-op turns a rejection or an empty claim produces, so reading turns in
338
+ * isolation either credits cut-short work as finished or convicts every no-op.
339
+ * The missing fact is the inbox's own record: {@link Inbox} logs each mutation
340
+ * with `removedCount` and marks a cancellation `outcome: 'canceled'`, which
341
+ * separates a turn claiming its input from work being dropped unrun.
342
+ *
343
+ * @module @deepseek-ai/dsh-agent/consumed-work
344
+ */
345
+ /**
346
+ * Whether a turn that consumed input but never reached a step ends in a way
347
+ * that accounts for that input. Only a `completed` end does not: it had
348
+ * nothing left to run once its claim was rewritten away. A `blocked` end is
349
+ * that input's ending too — the pre-step rejection that produced it discarded
350
+ * the claimed messages, so the work it took will never run.
351
+ * @param reason - the turn's recorded ending.
352
+ * @returns whether the ending accounts for the input the turn took.
353
+ */
354
+ function accountsForClaim(reason) {
355
+ switch (reason.kind) {
356
+ case "completed": return false;
357
+ case "blocked":
358
+ case "aborted":
359
+ case "interrupted":
360
+ case "error": return true;
361
+ /* v8 ignore next 5 -- unreachable: `max-tokens` requires a step and short-circuits
362
+ * before this call; `forked` exists only in constructor seed history, while production
363
+ * callers fold an operation-owned suffix. `TurnEndReasonMap` is merge-extensible, so a
364
+ * backend-added variant cannot be listed; an unnameable ending over consumed input must
365
+ * not read as success. */
366
+ default: return true;
367
+ }
368
+ }
369
+ /**
370
+ * Fold one agent log, or an owned suffix of one, into its account of consumed
371
+ * work. Single pass, and every input is the log itself: no caller has to sample
372
+ * live state before cancelling, so a cancellation issued by anyone — the owner's
373
+ * teardown, an ancestor's interrupt, an unloading plugin — reads the same.
374
+ * @param events - the log, or an owned suffix, to fold.
375
+ * @returns the accounting turn when one closed, and whether work was dropped unrun after it.
376
+ */
377
+ function foldConsumedWork(events) {
378
+ const stepped = /* @__PURE__ */ new Set();
379
+ const claimed = /* @__PURE__ */ new Set();
380
+ let open;
381
+ let end;
382
+ let droppedUnrun = false;
383
+ for (const event of events) switch (event.type) {
384
+ case "turn/start":
385
+ open = event.data.turn;
386
+ break;
387
+ case "step/start":
388
+ stepped.add(event.data.turn);
389
+ break;
390
+ case "agent/inbox/spliced": {
391
+ const { removedCount, outcome, inserted } = event.data;
392
+ if (removedCount === void 0) break;
393
+ if (outcome === "canceled") droppedUnrun ||= inserted.length === 0;
394
+ else if (open !== void 0) claimed.add(open);
395
+ break;
396
+ }
397
+ case "turn/end": {
398
+ const { turn, reason } = event.data;
399
+ open = void 0;
400
+ if (stepped.delete(turn) || claimed.delete(turn) && accountsForClaim(reason)) {
401
+ end = event;
402
+ droppedUnrun = false;
403
+ }
404
+ break;
405
+ }
406
+ default: break;
407
+ }
408
+ return {
409
+ ...end === void 0 ? {} : { end },
410
+ droppedUnrun
411
+ };
412
+ }
413
+ //#endregion
414
+ //#region lib/types/dispatch.js
415
+ /**
416
+ * Agent-scoped dispatch and prompt assembly helpers. The fused dispatcher
417
+ * {@link agentEvents} couples the agent subject to its scope carrier, so the
418
+ * scope key and the payload's `agent` cannot diverge; repeat dispatchers (the
419
+ * loop driver) build it once in the agent's constructor and reuse it.
420
+ * @module @deepseek-ai/dsh-agent/dispatch
421
+ */
422
+ /**
423
+ * Build the fused scope carrier for one agent subject.
424
+ *
425
+ * The carrier is a stateless routing object. {@link agentEvents} accepts an
426
+ * existing carrier, so callers that dispatch repeatedly for the same agent
427
+ * (the loop driver) build it once in the agent's constructor and reuse it,
428
+ * keeping hot-path dispatches allocation-free.
429
+ * @param agent - the subject agent and scope key.
430
+ * @returns the carrier passed as the event dispatcher `this` value.
431
+ */
432
+ function agentCarrier(agent) {
433
+ return scopeTarget(agent, agent);
434
+ }
435
+ /**
436
+ * Build a dispatcher that couples the agent subject to its scope carrier.
437
+ * @param ctx - the context to dispatch through (any context of the app).
438
+ * @param agent - the subject agent; also the scope-carrier key.
439
+ * @param carrier - the scope carrier to dispatch through; defaults to
440
+ * {@link agentCarrier} for the agent. Pass a constructor-built carrier to
441
+ * avoid rebuilding it for every dispatch.
442
+ * @returns the fused dispatcher.
443
+ */
444
+ function agentEvents(ctx, agent, carrier = agentCarrier(agent)) {
445
+ const fused = (payload) => ({
446
+ ...payload,
447
+ agent
448
+ });
449
+ return {
450
+ emit(name, payload) {
451
+ const args = [
452
+ carrier,
453
+ name,
454
+ fused(payload)
455
+ ];
456
+ const callbacks = ctx.events.dispatch("emit", args);
457
+ for (const callback of callbacks) try {
458
+ const returned = callback(...args);
459
+ Promise.resolve(returned).catch((error) => {
460
+ ctx.logger.warn(`agent event "${name}" listener rejected: ${String(error)}`);
461
+ });
462
+ } catch (error) {
463
+ ctx.logger.warn(`agent event "${name}" listener threw: ${String(error)}`);
464
+ }
465
+ },
466
+ async serial(name, payload) {
467
+ const serial = ctx.serial;
468
+ return await serial(carrier, name, fused(payload));
469
+ },
470
+ waterfall(name, payload, ...rest) {
471
+ const waterfall = ctx.waterfall;
472
+ return waterfall(carrier, name, fused(payload), ...rest);
473
+ }
474
+ };
475
+ }
476
+ /**
477
+ * Emit one contained agent notification without allocating a retained dispatcher.
478
+ * @param ctx - the context to dispatch through.
479
+ * @param agent - the subject agent and scope key.
480
+ * @param name - the agent-subject event to emit.
481
+ * @param payload - the event's payload fields; `agent` is injected.
482
+ */
483
+ function emitAgentEvent(ctx, agent, name, payload) {
484
+ agentEvents(ctx, agent).emit(name, payload);
485
+ }
486
+ /**
487
+ * Build the prompt assembly context with agent and scope set together, so
488
+ * agent-scoped prompt and tool contributions cannot be silently omitted.
489
+ * @param agent - the agent the assembly is for.
490
+ * @param signal - the current turn's explicit control signal, when assembly belongs to a turn.
491
+ * @returns the context to pass to `assemble()`.
492
+ */
493
+ function assembleContextFor(agent, signal) {
494
+ return {
495
+ agent,
496
+ scope: agent,
497
+ ...signal === void 0 ? {} : { signal }
498
+ };
499
+ }
500
+ //#endregion
501
+ //#region lib/types/index.js
502
+ /**
503
+ * Agent service: live registry, factory delegation, and process-local
504
+ * initiator scope. Concrete creation and driving belong to the loop.
505
+ *
506
+ * @module @deepseek-ai/dsh-agent
507
+ */
508
+ /** Thrown when create/resume is called before an agent factory is registered. */
509
+ const NO_FACTORY_MESSAGE = "no agent factory registered (load an agent-loop plugin)";
510
+ const NO_INITIATOR_MESSAGE = "no initiating agent is active";
511
+ const DISPOSED_INITIATOR_MESSAGE = "agent initiator scope is disposed";
512
+ /**
513
+ * Agent service (`ctx.agents`): tracks live agents and carries the initiating
514
+ * Agent through one process-local asynchronous driver chain. Agent *creation*
515
+ * is provided by whichever plugin implements the {@link AgentFactory}
516
+ * (`@deepseek-ai/dsh-agent-loop`), registered via {@link setFactory}.
517
+ *
518
+ * Initiator methods provide same-process causal attribution only. Ambient
519
+ * presence is neither liveness proof nor authorization; subjects and owners
520
+ * remain explicit, as does identity at worker, process, persistence, and wire
521
+ * boundaries. Returned Promise boundaries drain during teardown, except a
522
+ * nested lineage that starts an owning-fiber unload is excluded from its own drain.
523
+ */
524
+ var AgentRegistry = class extends Service {
525
+ static inject = ["sessionProjections"];
526
+ turnBoundaryProjectionHandle;
527
+ store = /* @__PURE__ */ new Map();
528
+ factory;
529
+ initiators = new AsyncLocalStorage();
530
+ initiatorRuns = new AsyncLocalStorage();
531
+ initiatorState = "active";
532
+ activeInitiatorRuns = 0;
533
+ initiatorDrain;
534
+ initiatorDisposal;
535
+ constructor(ctx) {
536
+ super(ctx, "agents");
537
+ ctx.inject(["typert"], (typeCtx) => {
538
+ typeCtx.typert.lookups.register("agent", {
539
+ parameter: "agent",
540
+ wire: "agentId",
541
+ resource: "session",
542
+ hostTypeSymbol: "@deepseek-ai/dsh-agent#Agent",
543
+ wireTypeSymbol: "@deepseek-ai/dsh-session/types#SessionId",
544
+ resolve: (sessionId) => this.get(sessionId)
545
+ });
546
+ typeCtx.typert.contexts.registerHost("agent", {
547
+ wire: "agentId",
548
+ wireTypeSymbol: "@deepseek-ai/dsh-session/types#SessionId",
549
+ resource: "session",
550
+ resolve: (sessionId) => this.get(sessionId)?.ctx
551
+ });
552
+ });
553
+ ctx.on("internal/status", (fiber) => {
554
+ if (fiber.state === 5 && this.hasLifecycleAncestor(fiber)) this.closeInitiators();
555
+ });
556
+ ctx.effect(function* () {
557
+ yield () => this.disposeInitiators();
558
+ yield () => {
559
+ this.closeInitiators();
560
+ };
561
+ }.bind(this), "agents.initiatorLifecycle()");
562
+ installTurnArchiveAdmission(ctx, (sessionId) => this.get(sessionId));
563
+ }
564
+ async [Service.init]() {
565
+ this.turnBoundaryProjectionHandle = await this.ctx.sessionProjections.register(turnBoundaryProjectionDefinition);
566
+ }
567
+ /**
568
+ * Read the turn and step positions recorded in a Session.
569
+ * @param session - Session whose committed events are read.
570
+ * @returns the current turn and step positions; do not mutate the returned value.
571
+ */
572
+ turnBoundaryOf(session) {
573
+ return this.turnBoundaryProjectionHandle.stateOf(session);
574
+ }
575
+ /**
576
+ * Read the Agent that initiated the inherited asynchronous driver chain.
577
+ * Use this optional form for logging, tracing, metrics, or host attribution
578
+ * that also supports agentless calls. When a parent creates a child, setup
579
+ * reports the causal parent while the setup callback's Agent parameter
580
+ * identifies the child.
581
+ * @returns the inherited Agent, or `undefined` outside an initiator boundary
582
+ * and inside an explicit clearing boundary.
583
+ * @throws when this service instance has been disposed.
584
+ */
585
+ currentInitiator() {
586
+ this.assertInitiatorsReadable();
587
+ return this.initiators.getStore();
588
+ }
589
+ /**
590
+ * Read the initiating Agent and fail when no initiator boundary is active.
591
+ * Use this for private helpers contractually below a driver, or for a
592
+ * deployment-owned outbound request whose contract forbids agentless calls.
593
+ * Generic or direct-call paths use optional lookup or explicit request fields.
594
+ * @returns the inherited Agent.
595
+ * @throws when no initiator is active or this service instance has been disposed.
596
+ */
597
+ requireInitiator() {
598
+ const agent = this.currentInitiator();
599
+ if (agent === void 0) throw new Error(NO_INITIATOR_MESSAGE);
600
+ return agent;
601
+ }
602
+ /**
603
+ * Run an operation with one exact Agent as its process-local initiator. The
604
+ * exact synchronous value or Promise returned by the operation is preserved.
605
+ * Custom drivers and test harnesses wrap their complete returned foreground
606
+ * lifetime.
607
+ * A queue or wire receiver may establish this boundary only after validating
608
+ * explicit identity and resolving the exact live Agent; this method does neither.
609
+ * Detached work remains owned by the subsystem that starts it.
610
+ * @param agent - initiating Agent to inherit; presence is neither liveness proof nor authorization.
611
+ * @param operation - synchronous or asynchronous operation to invoke.
612
+ * @returns the exact value returned by `operation`.
613
+ * @throws when the initiator scope is closing/disposed, or when `operation` throws.
614
+ */
615
+ withInitiator(agent, operation) {
616
+ return this.runWithInitiator(agent, operation);
617
+ }
618
+ /**
619
+ * Run an operation inside a boundary that hides any inherited initiating
620
+ * Agent. The exact synchronous value or Promise is preserved.
621
+ * Use this while creating lazy shared timers, queue pumps, pool maintenance,
622
+ * watchers, or exporters so they do not inherit the first Agent that happens
623
+ * to initialize them. It clears only initiator attribution, not explicit
624
+ * fields, and does not own or drain detached resources.
625
+ * @param operation - synchronous or asynchronous operation to invoke without an initiator.
626
+ * @returns the exact value returned by `operation`.
627
+ * @throws when the initiator scope is closing/disposed, or when `operation` throws.
628
+ */
629
+ withoutInitiator(operation) {
630
+ return this.runWithInitiator(void 0, operation);
631
+ }
632
+ /**
633
+ * Register the agent-creation factory (the loop calls this on construction,
634
+ * effect-scoped). A traced Cordis service is canonicalized to its concrete
635
+ * target; each create/resume call is then traced through that caller's
636
+ * context so ownership follows the caller without stacking proxy layers.
637
+ * Throws if a factory is already registered. Returns the disposer; on
638
+ * dispose the factory slot is cleared.
639
+ * @param factory - the loop-owned factory {@link create}/{@link resume} delegate to.
640
+ * @returns the disposer that clears the factory slot. The exact
641
+ * Cordis effect disposer (single-shot): composite (generator) effects may
642
+ * yield it directly — exact identity nests the teardown in order.
643
+ */
644
+ setFactory(factory) {
645
+ return this.ctx.effect(() => {
646
+ if (this.factory !== void 0) throw new Error("an agent factory is already registered");
647
+ const target = factory[symbols.original] ?? factory;
648
+ this.factory = { target };
649
+ return () => {
650
+ this.factory = void 0;
651
+ };
652
+ }, "agents.setFactory()");
653
+ }
654
+ /** Return the active creation factory. */
655
+ requireFactory() {
656
+ if (this.factory === void 0) throw new Error(NO_FACTORY_MESSAGE);
657
+ return this.factory;
658
+ }
659
+ /**
660
+ * Create and publish a new agent through the registered factory.
661
+ * Distinct from {@link register} (which records an already-constructed
662
+ * agent): this constructs the agent and its session. Rejects if no factory is
663
+ * registered or creation/setup fails. The resolved {@link AgentHandle} lets
664
+ * the owner tear down exactly this agent.
665
+ * @param options - shared identity, optional live parent, session seed/metadata, and agent options.
666
+ * @returns the handle after setup, rollback-covered publication, and loop start complete.
667
+ */
668
+ async create(options) {
669
+ const ownerCtx = this.ctx;
670
+ const { target } = this.requireFactory();
671
+ const receiver = getTraceable(ownerCtx, target);
672
+ return Reflect.apply(target.createAgent, receiver, [ownerCtx, options]);
673
+ }
674
+ /**
675
+ * Load a persisted session and resume an agent on it through the registered
676
+ * factory. Rejects if no factory is registered; the factory rejects if
677
+ * session persistence is not configured or persistence/setup fails.
678
+ * @param options - persisted identity, optional live parent, configuration, and setup.
679
+ * @returns the handle after setup, rollback-covered publication, and loop start complete.
680
+ */
681
+ async resume(options) {
682
+ const ownerCtx = this.ctx;
683
+ const { target } = this.requireFactory();
684
+ const receiver = getTraceable(ownerCtx, target);
685
+ return Reflect.apply(target.resume, receiver, [ownerCtx, options]);
686
+ }
687
+ /**
688
+ * Register a live agent with source `startup`. Rejects if the id is already registered or a
689
+ * serial `agent/created` listener fails. Emits `agent/disposed`
690
+ * when the calling fiber is disposed — both with the agent's scope carrier
691
+ * (`scopeTarget(agent, agent)`): the subject is the agent in hand, so the
692
+ * emits are scope-filtered regardless of which context invoked `register`
693
+ * (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always
694
+ * requires passing the carrier). The entry is a runtime root; factory-backed
695
+ * creation uses `options.parentAgent` for child ownership. Await the registration before using the agent.
696
+ * @param agent - the already-constructed agent to record in the store.
697
+ * @returns the awaitable Cordis effect disposer (single-shot; a repeat call
698
+ * returns undefined without awaiting an in-flight teardown). Exact
699
+ * identity is load-bearing: a composite (generator) effect that owns a
700
+ * teardown ORDER — the agent factory's lifecycle chain — must yield THIS
701
+ * function so Cordis nests the unregistration at that yield position;
702
+ * yielding a wrapper would leave it disposing as a concurrent sibling on
703
+ * owner unload, unregistering the agent (and emitting `agent/disposed`)
704
+ * while its final turn is still draining.
705
+ */
706
+ register(agent) {
707
+ return this.ctx.effect(async function* () {
708
+ yield this.enter(agent, void 0);
709
+ await this.announce(agent, "startup");
710
+ }.bind(this), "agents.register()");
711
+ }
712
+ /**
713
+ * Insert an already-constructed agent without announcing it. This is the
714
+ * advanced ordered-lifecycle primitive used by the async agent factory: it
715
+ * first completes setup while the agent is unpublished, then assigns the
716
+ * returned detach closure into its pre-installed composite teardown before
717
+ * calling {@link announce}. Ordinary callers use {@link register}.
718
+ * @param agent - the prepared, unpublished agent.
719
+ * @param owner - explicitly supplied live runtime owner, or
720
+ * undefined for a top-level runtime root. This is runtime ownership, not
721
+ * the resumed session's durable parent lineage.
722
+ * @returns an idempotent closure that removes this exact entry and emits
723
+ * `agent/disposed` with listener failures contained. When called from a
724
+ * `agent/created` listener, removal and disposal wait until the serial
725
+ * creation dispatch settles.
726
+ */
727
+ enter(agent, owner) {
728
+ const id = agent.id;
729
+ if (id !== agent.session.id) throw new Error(`agent id "${id}" does not match session id "${agent.session.id}"`);
730
+ const carrier = scopeTarget(agent, agent);
731
+ if (this.store.has(id)) throw new Error(`agent "${id}" is already registered`);
732
+ const entry = {
733
+ id,
734
+ agent,
735
+ owner,
736
+ carrier,
737
+ announced: false,
738
+ announcing: false,
739
+ detachRequested: false
740
+ };
741
+ this.store.set(id, entry);
742
+ let entered = true;
743
+ const detach = () => {
744
+ if (!entered) return;
745
+ entered = false;
746
+ if (entry.announcing) {
747
+ entry.detachRequested = true;
748
+ return;
749
+ }
750
+ this.detachEntered(entry);
751
+ };
752
+ return detach;
753
+ }
754
+ /** Remove one exact entered agent and emit its paired disposal when announced. */
755
+ detachEntered(entry) {
756
+ entry.detachRequested = false;
757
+ /* v8 ignore next -- enter() rejects replacement while this single-shot detach capability is live. */
758
+ if (this.store.get(entry.id) !== entry) return;
759
+ this.store.delete(entry.id);
760
+ if (!entry.announced) return;
761
+ this.emitDisposed(entry);
762
+ }
763
+ /** Emit the paired disposal edge through the entry's stable carrier. */
764
+ emitDisposed(entry) {
765
+ const args = [
766
+ entry.carrier,
767
+ "agent/disposed",
768
+ { agent: entry.agent }
769
+ ];
770
+ for (const callback of this.ctx.events.dispatch("emit", args)) try {
771
+ const returned = callback(...args);
772
+ Promise.resolve(returned).catch((error) => {
773
+ this.ctx.logger.warn(`agent "${entry.id}": agent/disposed listener rejected: ${String(error)}`);
774
+ });
775
+ } catch (error) {
776
+ this.ctx.logger.warn(`agent "${entry.id}": agent/disposed listener threw: ${String(error)}`);
777
+ }
778
+ }
779
+ /**
780
+ * Announce an agent previously inserted with {@link enter}.
781
+ * @param agent - the live inserted agent to announce.
782
+ * @param source - fresh creation, resume, clear, or compaction source.
783
+ * @param signal - optional factory initialization cancellation signal passed to listeners.
784
+ * @returns completion of the serial creation listeners; a listener failure rejects.
785
+ * @throws if `agent` is not the exact live registry entry for its id, or its
786
+ * creation announcement already began (including a reentrant call from a
787
+ * creation listener).
788
+ */
789
+ async announce(agent, source, signal) {
790
+ const entry = this.store.get(agent.id);
791
+ if (entry === void 0 || entry.agent !== agent) throw new Error(`agent "${agent.id}" is not live in this registry`);
792
+ if (entry.announced || entry.announcing) throw new Error(`agent "${entry.id}" was already announced`);
793
+ entry.announcing = true;
794
+ entry.announced = true;
795
+ try {
796
+ await this.ctx.serial(entry.carrier, "agent/created", {
797
+ agent: entry.agent,
798
+ source,
799
+ ...signal === void 0 ? {} : { signal }
800
+ });
801
+ } finally {
802
+ entry.announcing = false;
803
+ if (entry.detachRequested) this.detachEntered(entry);
804
+ }
805
+ }
806
+ /**
807
+ * Look up a live agent.
808
+ * @param id - the shared agent/session id to look up.
809
+ * @returns the agent, or undefined when no live agent has that id.
810
+ */
811
+ get(id) {
812
+ return this.store.get(id)?.agent;
813
+ }
814
+ /**
815
+ * Test whether a live agent was created through one exact parent agent's
816
+ * scoped context. Runtime ownership is independent of durable session
817
+ * lineage and remains unambiguous when unrelated providers reuse an id.
818
+ * @param id - the candidate child agent's shared agent/session id.
819
+ * @param owner - the expected runtime creator agent.
820
+ * @returns true only while the exact child entry is live under that owner.
821
+ */
822
+ isOwnedBy(id, owner) {
823
+ return this.store.get(id)?.owner === owner;
824
+ }
825
+ /**
826
+ * All live agents, in registration order.
827
+ * @returns a fresh array; mutating it does not affect the registry.
828
+ */
829
+ list() {
830
+ return [...this.store.values()].map((entry) => entry.agent);
831
+ }
832
+ /**
833
+ * All live top-level agents in registration order. A top-level agent was
834
+ * created without an owning agent context; durable session lineage does not
835
+ * affect this runtime relation, so a resumed fork may still be a root.
836
+ * @returns a fresh array; mutating it does not affect the registry.
837
+ */
838
+ roots() {
839
+ return [...this.store.values()].filter((entry) => entry.owner === void 0).map((entry) => entry.agent);
840
+ }
841
+ /** Reject new initiator boundaries while inherited continuations drain. */
842
+ closeInitiators() {
843
+ if (this.initiatorState === "active") this.initiatorState = "closing";
844
+ }
845
+ /** Wait for returned-Promise boundaries, then invalidate retained references. */
846
+ disposeInitiators() {
847
+ return this.initiatorDisposal ??= (async () => {
848
+ this.closeInitiators();
849
+ this.releaseReentrantInitiatorRuns();
850
+ if (this.activeInitiatorRuns !== 0) {
851
+ this.initiatorDrain ??= Promise.withResolvers();
852
+ await this.initiatorDrain.promise;
853
+ }
854
+ this.initiatorState = "disposed";
855
+ this.initiators.disable();
856
+ this.initiatorRuns.disable();
857
+ })();
858
+ }
859
+ /** Establish one tracked initiator or clearing boundary. */
860
+ runWithInitiator(agent, operation) {
861
+ if (this.initiatorState !== "active") throw new Error(DISPOSED_INITIATOR_MESSAGE);
862
+ const run = {
863
+ active: true,
864
+ parent: this.initiatorRuns.getStore()
865
+ };
866
+ this.activeInitiatorRuns += 1;
867
+ let result;
868
+ try {
869
+ result = this.initiatorRuns.run(run, () => this.initiators.run(agent, operation));
870
+ } catch (error) {
871
+ this.releaseInitiatorRun(run);
872
+ throw error;
873
+ }
874
+ if (isPromise(result)) try {
875
+ Promise.prototype.then.call(result, () => {
876
+ this.releaseInitiatorRun(run);
877
+ }, () => {
878
+ this.releaseInitiatorRun(run);
879
+ });
880
+ } catch {
881
+ this.releaseInitiatorRun(run);
882
+ }
883
+ else this.releaseInitiatorRun(run);
884
+ return result;
885
+ }
886
+ /** Whether one unloading fiber owns this service's lifecycle. */
887
+ hasLifecycleAncestor(candidate) {
888
+ let fiber = this.ctx.fiber;
889
+ while (true) {
890
+ if (fiber === candidate) return true;
891
+ const parent = fiber.parent.fiber;
892
+ if (parent === fiber) return false;
893
+ fiber = parent;
894
+ }
895
+ }
896
+ assertInitiatorsReadable() {
897
+ if (this.initiatorState === "disposed") throw new Error(DISPOSED_INITIATOR_MESSAGE);
898
+ }
899
+ /** Exclude the boundary chain that initiated this teardown from its own drain. */
900
+ releaseReentrantInitiatorRuns() {
901
+ let run = this.initiatorRuns.getStore();
902
+ while (run !== void 0) {
903
+ this.releaseInitiatorRun(run);
904
+ run = run.parent;
905
+ }
906
+ }
907
+ releaseInitiatorRun(run) {
908
+ if (!run.active) return;
909
+ run.active = false;
910
+ this.activeInitiatorRuns -= 1;
911
+ if (this.activeInitiatorRuns !== 0) return;
912
+ this.initiatorDrain?.resolve();
913
+ this.initiatorDrain = void 0;
914
+ }
915
+ };
916
+ //#endregion
917
+ export { AgentModel, AgentRegistry, AgentRegistry as default, AssistantStreamState, agentCarrier, agentEvents, assembleContextFor, emitAgentEvent, foldConsumedWork, installModelSelection, modelSelectionProjectionDefinition };