@qilin-agent/agent 0.0.0-stage → 3.1.4
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 +91 -0
- package/README.md +184 -2
- package/README.zh.md +185 -0
- package/lib/index.js +700 -0
- package/lib/types/archive-admission.d.ts +24 -0
- package/lib/types/archive-admission.js +35 -0
- package/lib/types/consumed-work.d.ts +39 -0
- package/lib/types/consumed-work.js +93 -0
- package/lib/types/dispatch.d.ts +111 -0
- package/lib/types/dispatch.js +95 -0
- package/lib/types/index.d.ts +379 -0
- package/lib/types/index.js +468 -0
- package/lib/types/model-selection.d.ts +50 -0
- package/lib/types/model-selection.js +95 -0
- package/lib/types/projection.d.ts +9 -0
- package/lib/types/projection.js +2 -0
- package/lib/types/runtime-types.d.ts +410 -0
- package/lib/types/runtime-types.js +8 -0
- package/lib/types/types.d.ts +95 -0
- package/lib/types/types.js +7 -0
- package/package.json +55 -3
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent service: live registry, factory delegation, and process-local
|
|
3
|
+
* initiator scope. Concrete creation and driving belong to the loop.
|
|
4
|
+
*
|
|
5
|
+
* @module @qilin-agent/agent
|
|
6
|
+
*/
|
|
7
|
+
import { getTraceable, Service, symbols } from '@qilin-agent/kylin';
|
|
8
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
9
|
+
import { isPromise } from 'node:util/types';
|
|
10
|
+
import { scopeTarget } from '@qilin-agent/scope';
|
|
11
|
+
import { installTurnArchiveAdmission } from "./archive-admission.js";
|
|
12
|
+
export * from "./runtime-types.js";
|
|
13
|
+
export * from "./types.js";
|
|
14
|
+
export * from "./consumed-work.js";
|
|
15
|
+
export * from "./model-selection.js";
|
|
16
|
+
export { agentCarrier, agentEvents, assembleContextFor, emitAgentEvent } from "./dispatch.js";
|
|
17
|
+
/** Thrown when create/resume is called before an agent factory is registered. */
|
|
18
|
+
const NO_FACTORY_MESSAGE = 'no agent factory registered (load an agent-loop plugin)';
|
|
19
|
+
const NO_INITIATOR_MESSAGE = 'no initiating agent is active';
|
|
20
|
+
const DISPOSED_INITIATOR_MESSAGE = 'agent initiator scope is disposed';
|
|
21
|
+
/**
|
|
22
|
+
* Agent service (`ctx.agents`): tracks live agents and carries the initiating
|
|
23
|
+
* Agent through one process-local asynchronous driver chain. Agent *creation*
|
|
24
|
+
* is provided by whichever plugin implements the {@link AgentFactory}
|
|
25
|
+
* (`@qilin-agent/agent-loop`), registered via {@link setFactory}.
|
|
26
|
+
*
|
|
27
|
+
* Initiator methods provide same-process causal attribution only. Ambient
|
|
28
|
+
* presence is neither liveness proof nor authorization; subjects and owners
|
|
29
|
+
* remain explicit, as does identity at worker, process, persistence, and wire
|
|
30
|
+
* boundaries. Returned Promise boundaries drain during teardown, except a
|
|
31
|
+
* nested lineage that starts an owning-fiber unload is excluded from its own drain.
|
|
32
|
+
*/
|
|
33
|
+
export class AgentRegistry extends Service {
|
|
34
|
+
store = new Map();
|
|
35
|
+
factory;
|
|
36
|
+
initiators = new AsyncLocalStorage();
|
|
37
|
+
initiatorRuns = new AsyncLocalStorage();
|
|
38
|
+
initiatorState = 'active';
|
|
39
|
+
activeInitiatorRuns = 0;
|
|
40
|
+
initiatorDrain;
|
|
41
|
+
initiatorDisposal;
|
|
42
|
+
constructor(ctx) {
|
|
43
|
+
super(ctx, 'agents');
|
|
44
|
+
ctx.inject(['typert'], (typeCtx) => {
|
|
45
|
+
typeCtx.typert.lookups.register('agent', {
|
|
46
|
+
parameter: 'agent',
|
|
47
|
+
wire: 'agentId',
|
|
48
|
+
hostTypeSymbol: '@qilin-agent/agent#Agent',
|
|
49
|
+
wireTypeSymbol: '@qilin-agent/session/types#SessionId',
|
|
50
|
+
resolve: sessionId => this.get(sessionId),
|
|
51
|
+
});
|
|
52
|
+
typeCtx.typert.contexts.registerHost('agent', {
|
|
53
|
+
wire: 'agentId',
|
|
54
|
+
wireTypeSymbol: '@qilin-agent/session/types#SessionId',
|
|
55
|
+
resolve: sessionId => this.get(sessionId)?.ctx,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
ctx.on('internal/status', (fiber) => {
|
|
59
|
+
if (fiber.state === 5 /* FiberState.UNLOADING */ && this.hasLifecycleAncestor(fiber)) {
|
|
60
|
+
this.closeInitiators();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
ctx.effect(function* () {
|
|
64
|
+
yield () => this.disposeInitiators();
|
|
65
|
+
yield () => { this.closeInitiators(); };
|
|
66
|
+
}.bind(this), 'agents.initiatorLifecycle()');
|
|
67
|
+
// Archive admission: the Workspace registry asks what still runs for a
|
|
68
|
+
// Session before hiding it; a running turn answers here, for every Agent.
|
|
69
|
+
installTurnArchiveAdmission(ctx, sessionId => this.get(sessionId));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Read the Agent that initiated the inherited asynchronous driver chain.
|
|
73
|
+
* Use this optional form for logging, tracing, metrics, or host attribution
|
|
74
|
+
* that also supports agentless calls. When a parent creates a child, setup
|
|
75
|
+
* reports the causal parent while the setup callback's Agent parameter
|
|
76
|
+
* identifies the child.
|
|
77
|
+
* @returns the inherited Agent, or `undefined` outside an initiator boundary
|
|
78
|
+
* and inside an explicit clearing boundary.
|
|
79
|
+
* @throws when this service instance has been disposed.
|
|
80
|
+
*/
|
|
81
|
+
currentInitiator() {
|
|
82
|
+
this.assertInitiatorsReadable();
|
|
83
|
+
return this.initiators.getStore();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Read the initiating Agent and fail when no initiator boundary is active.
|
|
87
|
+
* Use this for private helpers contractually below a driver, or for a
|
|
88
|
+
* deployment-owned outbound request whose contract forbids agentless calls.
|
|
89
|
+
* Generic or direct-call paths use optional lookup or explicit request fields.
|
|
90
|
+
* @returns the inherited Agent.
|
|
91
|
+
* @throws when no initiator is active or this service instance has been disposed.
|
|
92
|
+
*/
|
|
93
|
+
requireInitiator() {
|
|
94
|
+
const agent = this.currentInitiator();
|
|
95
|
+
if (agent === undefined)
|
|
96
|
+
throw new Error(NO_INITIATOR_MESSAGE);
|
|
97
|
+
return agent;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Run an operation with one exact Agent as its process-local initiator. The
|
|
101
|
+
* exact synchronous value or Promise returned by the operation is preserved.
|
|
102
|
+
* Custom drivers and test harnesses wrap their complete returned foreground
|
|
103
|
+
* lifetime.
|
|
104
|
+
* A queue or wire receiver may establish this boundary only after validating
|
|
105
|
+
* explicit identity and resolving the exact live Agent; this method does neither.
|
|
106
|
+
* Detached work remains owned by the subsystem that starts it.
|
|
107
|
+
* @param agent - initiating Agent to inherit; presence is neither liveness proof nor authorization.
|
|
108
|
+
* @param operation - synchronous or asynchronous operation to invoke.
|
|
109
|
+
* @returns the exact value returned by `operation`.
|
|
110
|
+
* @throws when the initiator scope is closing/disposed, or when `operation` throws.
|
|
111
|
+
*/
|
|
112
|
+
withInitiator(agent, operation) {
|
|
113
|
+
return this.runWithInitiator(agent, operation);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Run an operation inside a boundary that hides any inherited initiating
|
|
117
|
+
* Agent. The exact synchronous value or Promise is preserved.
|
|
118
|
+
* Use this while creating lazy shared timers, queue pumps, pool maintenance,
|
|
119
|
+
* watchers, or exporters so they do not inherit the first Agent that happens
|
|
120
|
+
* to initialize them. It clears only initiator attribution, not explicit
|
|
121
|
+
* fields, and does not own or drain detached resources.
|
|
122
|
+
* @param operation - synchronous or asynchronous operation to invoke without an initiator.
|
|
123
|
+
* @returns the exact value returned by `operation`.
|
|
124
|
+
* @throws when the initiator scope is closing/disposed, or when `operation` throws.
|
|
125
|
+
*/
|
|
126
|
+
withoutInitiator(operation) {
|
|
127
|
+
return this.runWithInitiator(undefined, operation);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Register the agent-creation factory (the loop calls this on construction,
|
|
131
|
+
* effect-scoped). A traced Cordis service is canonicalized to its concrete
|
|
132
|
+
* target; each create/resume call is then traced through that caller's
|
|
133
|
+
* context so ownership follows the caller without stacking proxy layers.
|
|
134
|
+
* Throws if a factory is already registered. Returns the disposer; on
|
|
135
|
+
* dispose the factory slot is cleared.
|
|
136
|
+
* @param factory - the loop-owned factory {@link create}/{@link resume} delegate to.
|
|
137
|
+
* @returns the disposer that clears the factory slot. The exact
|
|
138
|
+
* Cordis effect disposer (single-shot): composite (generator) effects may
|
|
139
|
+
* yield it directly — exact identity nests the teardown in order.
|
|
140
|
+
*/
|
|
141
|
+
setFactory(factory) {
|
|
142
|
+
const dispose = this.ctx.effect(() => {
|
|
143
|
+
if (this.factory !== undefined)
|
|
144
|
+
throw new Error('an agent factory is already registered');
|
|
145
|
+
// Avoid stacking two Cordis shadow layers when a caller passes a Service
|
|
146
|
+
// already read through a context. Calls are re-traced through their
|
|
147
|
+
// actual owner context below.
|
|
148
|
+
const target = factory[symbols.original] ?? factory;
|
|
149
|
+
this.factory = { target };
|
|
150
|
+
return () => { this.factory = undefined; };
|
|
151
|
+
}, 'agents.setFactory()');
|
|
152
|
+
// The exact cordis effect disposer (the agents.register() convention): a
|
|
153
|
+
// caller's composite effect can yield it for in-order teardown; the
|
|
154
|
+
// loop's constructor effect returns it directly, identity-nesting the
|
|
155
|
+
// registration under that effect.
|
|
156
|
+
// oxlint-disable-next-line typescript/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity
|
|
157
|
+
return dispose;
|
|
158
|
+
}
|
|
159
|
+
/** Return the active creation factory. */
|
|
160
|
+
requireFactory() {
|
|
161
|
+
if (this.factory === undefined)
|
|
162
|
+
throw new Error(NO_FACTORY_MESSAGE);
|
|
163
|
+
return this.factory;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Create and publish a new agent through the registered factory.
|
|
167
|
+
* Distinct from {@link register} (which records an already-constructed
|
|
168
|
+
* agent): this constructs the agent and its session. Rejects if no factory is
|
|
169
|
+
* registered or creation/setup fails. The resolved {@link AgentHandle} lets
|
|
170
|
+
* the owner tear down exactly this agent.
|
|
171
|
+
* @param options - shared identity, optional live parent, session seed/metadata, and agent options.
|
|
172
|
+
* @returns the handle after setup, rollback-covered publication, and loop start complete.
|
|
173
|
+
*/
|
|
174
|
+
async create(options) {
|
|
175
|
+
const ownerCtx = this.ctx;
|
|
176
|
+
// Re-trace a Service-backed factory through the accessing context
|
|
177
|
+
// explicitly. This preserves AgentLoop's dependency origin while binding
|
|
178
|
+
// its effects to ownerCtx; plain factories receive ownerCtx as an explicit
|
|
179
|
+
// capability and need no Cordis tracker magic.
|
|
180
|
+
const { target } = this.requireFactory();
|
|
181
|
+
const receiver = getTraceable(ownerCtx, target);
|
|
182
|
+
// oxlint-disable-next-line typescript/unbound-method -- Reflect.apply intentionally supplies the caller-traced receiver
|
|
183
|
+
return Reflect.apply(target.createAgent, receiver, [ownerCtx, options]);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Load a persisted session and resume an agent on it through the registered
|
|
187
|
+
* factory. Rejects if no factory is registered; the factory rejects if
|
|
188
|
+
* session persistence is not configured or persistence/setup fails.
|
|
189
|
+
* @param options - persisted identity, optional live parent, configuration, and setup.
|
|
190
|
+
* @returns the handle after setup, rollback-covered publication, and loop start complete.
|
|
191
|
+
*/
|
|
192
|
+
async resume(options) {
|
|
193
|
+
const ownerCtx = this.ctx;
|
|
194
|
+
const { target } = this.requireFactory();
|
|
195
|
+
const receiver = getTraceable(ownerCtx, target);
|
|
196
|
+
// oxlint-disable-next-line typescript/unbound-method -- Reflect.apply intentionally supplies the caller-traced receiver
|
|
197
|
+
return Reflect.apply(target.resume, receiver, [ownerCtx, options]);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Register a live agent with source `startup`. Rejects if the id is already registered or a
|
|
201
|
+
* serial `agent/created` listener fails. Emits `agent/disposed`
|
|
202
|
+
* when the calling fiber is disposed — both with the agent's scope carrier
|
|
203
|
+
* (`scopeTarget(agent, agent)`): the subject is the agent in hand, so the
|
|
204
|
+
* emits are scope-filtered regardless of which context invoked `register`
|
|
205
|
+
* (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always
|
|
206
|
+
* requires passing the carrier). The entry is a runtime root; factory-backed
|
|
207
|
+
* creation uses `options.parentAgent` for child ownership. Await the registration before using the agent.
|
|
208
|
+
* @param agent - the already-constructed agent to record in the store.
|
|
209
|
+
* @returns the awaitable Cordis effect disposer (single-shot; a repeat call
|
|
210
|
+
* returns undefined without awaiting an in-flight teardown). Exact
|
|
211
|
+
* identity is load-bearing: a composite (generator) effect that owns a
|
|
212
|
+
* teardown ORDER — the agent factory's lifecycle chain — must yield THIS
|
|
213
|
+
* function so Cordis nests the unregistration at that yield position;
|
|
214
|
+
* yielding a wrapper would leave it disposing as a concurrent sibling on
|
|
215
|
+
* owner unload, unregistering the agent (and emitting `agent/disposed`)
|
|
216
|
+
* while its final turn is still draining.
|
|
217
|
+
*/
|
|
218
|
+
register(agent) {
|
|
219
|
+
return this.ctx.effect(async function* () {
|
|
220
|
+
yield this.enter(agent, undefined);
|
|
221
|
+
await this.announce(agent, 'startup');
|
|
222
|
+
}.bind(this), 'agents.register()');
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Insert an already-constructed agent without announcing it. This is the
|
|
226
|
+
* advanced ordered-lifecycle primitive used by the async agent factory: it
|
|
227
|
+
* first completes setup while the agent is unpublished, then assigns the
|
|
228
|
+
* returned detach closure into its pre-installed composite teardown before
|
|
229
|
+
* calling {@link announce}. Ordinary callers use {@link register}.
|
|
230
|
+
* @param agent - the prepared, unpublished agent.
|
|
231
|
+
* @param owner - explicitly supplied live runtime owner, or
|
|
232
|
+
* undefined for a top-level runtime root. This is runtime ownership, not
|
|
233
|
+
* the resumed session's durable parent lineage.
|
|
234
|
+
* @returns an idempotent closure that removes this exact entry and emits
|
|
235
|
+
* `agent/disposed` with listener failures contained. When called from a
|
|
236
|
+
* `agent/created` listener, removal and disposal wait until the serial
|
|
237
|
+
* creation dispatch settles.
|
|
238
|
+
*/
|
|
239
|
+
enter(agent, owner) {
|
|
240
|
+
const id = agent.id;
|
|
241
|
+
if (id !== agent.session.id) {
|
|
242
|
+
throw new Error(`agent id "${id}" does not match session id "${agent.session.id}"`);
|
|
243
|
+
}
|
|
244
|
+
const carrier = scopeTarget(agent, agent);
|
|
245
|
+
// This is the authoritative collision boundary. Concurrent create/resume
|
|
246
|
+
// operations may both prepare, but only one exact entry can publish.
|
|
247
|
+
if (this.store.has(id))
|
|
248
|
+
throw new Error(`agent "${id}" is already registered`);
|
|
249
|
+
const entry = {
|
|
250
|
+
id,
|
|
251
|
+
agent,
|
|
252
|
+
owner,
|
|
253
|
+
carrier,
|
|
254
|
+
announced: false,
|
|
255
|
+
announcing: false,
|
|
256
|
+
detachRequested: false,
|
|
257
|
+
};
|
|
258
|
+
this.store.set(id, entry);
|
|
259
|
+
let entered = true;
|
|
260
|
+
const detach = () => {
|
|
261
|
+
if (!entered)
|
|
262
|
+
return;
|
|
263
|
+
entered = false;
|
|
264
|
+
// Every callback reached by this creation dispatch must observe the same
|
|
265
|
+
// live entry, and disposal must follow creation. A listener may own
|
|
266
|
+
// the advanced detach capability, so make that ordering structural:
|
|
267
|
+
// visibility and the paired disposal are deferred until announce()'s
|
|
268
|
+
// serial dispatch has settled.
|
|
269
|
+
if (entry.announcing) {
|
|
270
|
+
entry.detachRequested = true;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
this.detachEntered(entry);
|
|
274
|
+
};
|
|
275
|
+
return detach;
|
|
276
|
+
}
|
|
277
|
+
/** Remove one exact entered agent and emit its paired disposal when announced. */
|
|
278
|
+
detachEntered(entry) {
|
|
279
|
+
entry.detachRequested = false;
|
|
280
|
+
// A stale capability can never delete a later same-id lifecycle. The
|
|
281
|
+
// captured entry identity is the final boundary.
|
|
282
|
+
/* v8 ignore next -- enter() rejects replacement while this single-shot detach capability is live. */
|
|
283
|
+
if (this.store.get(entry.id) !== entry)
|
|
284
|
+
return;
|
|
285
|
+
this.store.delete(entry.id);
|
|
286
|
+
// An insertion rolled back before announce was never externally created,
|
|
287
|
+
// so emitting disposed would invent an impossible lifecycle edge. Marking
|
|
288
|
+
// happens before the created emit: if a later created listener throws,
|
|
289
|
+
// earlier listeners may already have observed it and must see disposal.
|
|
290
|
+
if (!entry.announced)
|
|
291
|
+
return;
|
|
292
|
+
this.emitDisposed(entry);
|
|
293
|
+
}
|
|
294
|
+
/** Emit the paired disposal edge through the entry's stable carrier. */
|
|
295
|
+
emitDisposed(entry) {
|
|
296
|
+
const args = [entry.carrier, 'agent/disposed', { agent: entry.agent }];
|
|
297
|
+
for (const callback of this.ctx.events.dispatch('emit', args)) {
|
|
298
|
+
try {
|
|
299
|
+
const returned = callback(...args);
|
|
300
|
+
void Promise.resolve(returned).catch((error) => {
|
|
301
|
+
this.ctx.logger.warn(`agent "${entry.id}": agent/disposed listener rejected: ${String(error)}`);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
this.ctx.logger.warn(`agent "${entry.id}": agent/disposed listener threw: ${String(error)}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Announce an agent previously inserted with {@link enter}.
|
|
311
|
+
* @param agent - the live inserted agent to announce.
|
|
312
|
+
* @param source - fresh creation, resume, clear, or compaction source.
|
|
313
|
+
* @param signal - optional factory initialization cancellation signal passed to listeners.
|
|
314
|
+
* @returns completion of the serial creation listeners; a listener failure rejects.
|
|
315
|
+
* @throws if `agent` is not the exact live registry entry for its id, or its
|
|
316
|
+
* creation announcement already began (including a reentrant call from a
|
|
317
|
+
* creation listener).
|
|
318
|
+
*/
|
|
319
|
+
async announce(agent, source, signal) {
|
|
320
|
+
const entry = this.store.get(agent.id);
|
|
321
|
+
if (entry === undefined || entry.agent !== agent) {
|
|
322
|
+
throw new Error(`agent "${agent.id}" is not live in this registry`);
|
|
323
|
+
}
|
|
324
|
+
if (entry.announced || entry.announcing) {
|
|
325
|
+
throw new Error(`agent "${entry.id}" was already announced`);
|
|
326
|
+
}
|
|
327
|
+
// Mark before dispatch so a listener cannot recursively create a second
|
|
328
|
+
// lifecycle edge; detach still pairs a partially delivered first edge.
|
|
329
|
+
entry.announcing = true;
|
|
330
|
+
entry.announced = true;
|
|
331
|
+
try {
|
|
332
|
+
await this.ctx.serial(entry.carrier, 'agent/created', {
|
|
333
|
+
agent: entry.agent,
|
|
334
|
+
source,
|
|
335
|
+
...signal === undefined ? {} : { signal },
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
finally {
|
|
339
|
+
entry.announcing = false;
|
|
340
|
+
if (entry.detachRequested)
|
|
341
|
+
this.detachEntered(entry);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Look up a live agent.
|
|
346
|
+
* @param id - the shared agent/session id to look up.
|
|
347
|
+
* @returns the agent, or undefined when no live agent has that id.
|
|
348
|
+
*/
|
|
349
|
+
get(id) {
|
|
350
|
+
return this.store.get(id)?.agent;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Test whether a live agent was created through one exact parent agent's
|
|
354
|
+
* scoped context. Runtime ownership is independent of durable session
|
|
355
|
+
* lineage and remains unambiguous when unrelated providers reuse an id.
|
|
356
|
+
* @param id - the candidate child agent's shared agent/session id.
|
|
357
|
+
* @param owner - the expected runtime creator agent.
|
|
358
|
+
* @returns true only while the exact child entry is live under that owner.
|
|
359
|
+
*/
|
|
360
|
+
isOwnedBy(id, owner) {
|
|
361
|
+
return this.store.get(id)?.owner === owner;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* All live agents, in registration order.
|
|
365
|
+
* @returns a fresh array; mutating it does not affect the registry.
|
|
366
|
+
*/
|
|
367
|
+
list() {
|
|
368
|
+
return [...this.store.values()].map(entry => entry.agent);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* All live top-level agents in registration order. A top-level agent was
|
|
372
|
+
* created without an owning agent context; durable session lineage does not
|
|
373
|
+
* affect this runtime relation, so a resumed fork may still be a root.
|
|
374
|
+
* @returns a fresh array; mutating it does not affect the registry.
|
|
375
|
+
*/
|
|
376
|
+
roots() {
|
|
377
|
+
return [...this.store.values()]
|
|
378
|
+
.filter(entry => entry.owner === undefined)
|
|
379
|
+
.map(entry => entry.agent);
|
|
380
|
+
}
|
|
381
|
+
/** Reject new initiator boundaries while inherited continuations drain. */
|
|
382
|
+
closeInitiators() {
|
|
383
|
+
if (this.initiatorState === 'active')
|
|
384
|
+
this.initiatorState = 'closing';
|
|
385
|
+
}
|
|
386
|
+
/** Wait for returned-Promise boundaries, then invalidate retained references. */
|
|
387
|
+
disposeInitiators() {
|
|
388
|
+
return (this.initiatorDisposal ??= (async () => {
|
|
389
|
+
this.closeInitiators();
|
|
390
|
+
this.releaseReentrantInitiatorRuns();
|
|
391
|
+
if (this.activeInitiatorRuns !== 0) {
|
|
392
|
+
this.initiatorDrain ??= Promise.withResolvers();
|
|
393
|
+
await this.initiatorDrain.promise;
|
|
394
|
+
}
|
|
395
|
+
this.initiatorState = 'disposed';
|
|
396
|
+
this.initiators.disable();
|
|
397
|
+
this.initiatorRuns.disable();
|
|
398
|
+
})());
|
|
399
|
+
}
|
|
400
|
+
/** Establish one tracked initiator or clearing boundary. */
|
|
401
|
+
runWithInitiator(agent, operation) {
|
|
402
|
+
if (this.initiatorState !== 'active')
|
|
403
|
+
throw new Error(DISPOSED_INITIATOR_MESSAGE);
|
|
404
|
+
const run = {
|
|
405
|
+
active: true,
|
|
406
|
+
parent: this.initiatorRuns.getStore(),
|
|
407
|
+
};
|
|
408
|
+
this.activeInitiatorRuns += 1;
|
|
409
|
+
let result;
|
|
410
|
+
try {
|
|
411
|
+
result = this.initiatorRuns.run(run, () => this.initiators.run(agent, operation));
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
this.releaseInitiatorRun(run);
|
|
415
|
+
throw error;
|
|
416
|
+
}
|
|
417
|
+
if (isPromise(result)) {
|
|
418
|
+
try {
|
|
419
|
+
void Promise.prototype.then.call(result, () => { this.releaseInitiatorRun(run); }, () => { this.releaseInitiatorRun(run); });
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
// A branded Promise may expose a failing @@species. Observer setup did
|
|
423
|
+
// not attach, so preserve the exact return without leaking the run.
|
|
424
|
+
this.releaseInitiatorRun(run);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
this.releaseInitiatorRun(run);
|
|
429
|
+
}
|
|
430
|
+
return result;
|
|
431
|
+
}
|
|
432
|
+
/** Whether one unloading fiber owns this service's lifecycle. */
|
|
433
|
+
hasLifecycleAncestor(candidate) {
|
|
434
|
+
let fiber = this.ctx.fiber;
|
|
435
|
+
while (true) {
|
|
436
|
+
if (fiber === candidate)
|
|
437
|
+
return true;
|
|
438
|
+
const parent = fiber.parent.fiber;
|
|
439
|
+
if (parent === fiber)
|
|
440
|
+
return false;
|
|
441
|
+
fiber = parent;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
assertInitiatorsReadable() {
|
|
445
|
+
if (this.initiatorState === 'disposed')
|
|
446
|
+
throw new Error(DISPOSED_INITIATOR_MESSAGE);
|
|
447
|
+
}
|
|
448
|
+
/** Exclude the boundary chain that initiated this teardown from its own drain. */
|
|
449
|
+
releaseReentrantInitiatorRuns() {
|
|
450
|
+
let run = this.initiatorRuns.getStore();
|
|
451
|
+
while (run !== undefined) {
|
|
452
|
+
this.releaseInitiatorRun(run);
|
|
453
|
+
run = run.parent;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
releaseInitiatorRun(run) {
|
|
457
|
+
if (!run.active)
|
|
458
|
+
return;
|
|
459
|
+
run.active = false;
|
|
460
|
+
this.activeInitiatorRuns -= 1;
|
|
461
|
+
if (this.activeInitiatorRuns !== 0)
|
|
462
|
+
return;
|
|
463
|
+
this.initiatorDrain?.resolve();
|
|
464
|
+
this.initiatorDrain = undefined;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
export default AgentRegistry;
|
|
468
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-scoped model selection shared by runtime entry points.
|
|
3
|
+
* @module @qilin-agent/agent/model-selection
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@qilin-agent/kylin';
|
|
6
|
+
import { type ReasoningEffortId } from '@qilin-agent/llm';
|
|
7
|
+
import type { ContextFormed } from '@qilin-agent/llm';
|
|
8
|
+
declare module '@qilin-agent/llm' {
|
|
9
|
+
interface MessageSourceMap {
|
|
10
|
+
'model-selection': {
|
|
11
|
+
kind: 'model-selection';
|
|
12
|
+
} & ContextFormed;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Complete provider, model, and optional reasoning effort selected for one live Agent. */
|
|
16
|
+
export interface ModelSelection {
|
|
17
|
+
/** Registered provider route. */
|
|
18
|
+
provider: string;
|
|
19
|
+
/** Provider-owned model id. */
|
|
20
|
+
model: string;
|
|
21
|
+
/** Adapter-owned reasoning effort, or provider/default behavior when absent. */
|
|
22
|
+
reasoningEffort?: ReasoningEffortId;
|
|
23
|
+
}
|
|
24
|
+
/** Mutable model selection plus the value captured for the current step. */
|
|
25
|
+
export interface ModelSelectionRef {
|
|
26
|
+
/** Model selected for the next step that enters prompt assembly. */
|
|
27
|
+
current: ModelSelection | undefined;
|
|
28
|
+
/** Selection captured when the current step entered prompt assembly. */
|
|
29
|
+
assembled: ModelSelection | undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Couple one mutable selection to Agent-scoped prompt assembly and request routing.
|
|
33
|
+
* Prompt assembly snapshots the selected model before delegating, then applies
|
|
34
|
+
* its provider/model pair and effort to request config so a
|
|
35
|
+
* concurrent switch takes effect on a later step instead of splitting the two
|
|
36
|
+
* surfaces. An absent selected effort clears any inherited effort, restoring
|
|
37
|
+
* the selected model's provider/default behavior.
|
|
38
|
+
*
|
|
39
|
+
* A provider/model change appends a durable user-role notice to the next
|
|
40
|
+
* admitted request. It compares the assembled selection with the latest
|
|
41
|
+
* request header; effort-only changes and empty no-request decisions add no
|
|
42
|
+
* notice. Failure before header persistence repeats the notice on the next
|
|
43
|
+
* request.
|
|
44
|
+
*
|
|
45
|
+
* @param agentCtx - The selected Agent's scoped context.
|
|
46
|
+
* @param selection - Mutable selection owned by the calling entry point.
|
|
47
|
+
* @returns Disposer for all scoped waterfall listeners.
|
|
48
|
+
*/
|
|
49
|
+
export declare function installModelSelection(agentCtx: Context, selection: ModelSelectionRef): () => void;
|
|
50
|
+
//# sourceMappingURL=model-selection.d.ts.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-scoped model selection shared by runtime entry points.
|
|
3
|
+
* @module @qilin-agent/agent/model-selection
|
|
4
|
+
*/
|
|
5
|
+
import { boundContextSummary, createUserMessage, } from '@qilin-agent/llm';
|
|
6
|
+
function sameRoute(left, right) {
|
|
7
|
+
return left.provider === right.provider && left.model === right.model;
|
|
8
|
+
}
|
|
9
|
+
function routeLabel(route, other) {
|
|
10
|
+
return route.provider === other.provider ? route.model : `${route.provider}/${route.model}`;
|
|
11
|
+
}
|
|
12
|
+
function modelSwitchNotice(previous, selected) {
|
|
13
|
+
const from = routeLabel(previous, selected);
|
|
14
|
+
const to = routeLabel(selected, previous);
|
|
15
|
+
return createUserMessage({
|
|
16
|
+
content: [{
|
|
17
|
+
type: 'text',
|
|
18
|
+
text: `[model changed: assistant turns above this point were generated by ${from}; the session continues with ${to}]`,
|
|
19
|
+
}],
|
|
20
|
+
source: {
|
|
21
|
+
kind: 'model-selection',
|
|
22
|
+
form: 'notice',
|
|
23
|
+
summary: boundContextSummary(`${from} → ${to}`),
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Couple one mutable selection to Agent-scoped prompt assembly and request routing.
|
|
29
|
+
* Prompt assembly snapshots the selected model before delegating, then applies
|
|
30
|
+
* its provider/model pair and effort to request config so a
|
|
31
|
+
* concurrent switch takes effect on a later step instead of splitting the two
|
|
32
|
+
* surfaces. An absent selected effort clears any inherited effort, restoring
|
|
33
|
+
* the selected model's provider/default behavior.
|
|
34
|
+
*
|
|
35
|
+
* A provider/model change appends a durable user-role notice to the next
|
|
36
|
+
* admitted request. It compares the assembled selection with the latest
|
|
37
|
+
* request header; effort-only changes and empty no-request decisions add no
|
|
38
|
+
* notice. Failure before header persistence repeats the notice on the next
|
|
39
|
+
* request.
|
|
40
|
+
*
|
|
41
|
+
* @param agentCtx - The selected Agent's scoped context.
|
|
42
|
+
* @param selection - Mutable selection owned by the calling entry point.
|
|
43
|
+
* @returns Disposer for all scoped waterfall listeners.
|
|
44
|
+
*/
|
|
45
|
+
export function installModelSelection(agentCtx, selection) {
|
|
46
|
+
const disposeAssembly = agentCtx.on('system-prompt/assemble', async (_assembly, _context, next) => {
|
|
47
|
+
const selected = selection.current;
|
|
48
|
+
const assembled = await next();
|
|
49
|
+
selection.assembled = selected;
|
|
50
|
+
if (selected === undefined)
|
|
51
|
+
return assembled;
|
|
52
|
+
return {
|
|
53
|
+
...assembled,
|
|
54
|
+
variables: {
|
|
55
|
+
...assembled.variables,
|
|
56
|
+
provider: selected.provider,
|
|
57
|
+
model: selected.model,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
const disposeRequest = agentCtx.on('agent/request', async (_payload, next) => {
|
|
62
|
+
const resolved = await next();
|
|
63
|
+
const selected = selection.assembled;
|
|
64
|
+
if (selected === undefined)
|
|
65
|
+
return resolved;
|
|
66
|
+
const { reasoningEffort: _inheritedEffort, ...withoutInheritedEffort } = resolved;
|
|
67
|
+
return {
|
|
68
|
+
...withoutInheritedEffort,
|
|
69
|
+
provider: selected.provider,
|
|
70
|
+
model: selected.model,
|
|
71
|
+
...selected.reasoningEffort === undefined
|
|
72
|
+
? {}
|
|
73
|
+
: { reasoningEffort: selected.reasoningEffort },
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
const disposeNotice = agentCtx.on('agent/pre-step', async ({ agent, messages, signal, step }, next) => {
|
|
77
|
+
const decision = await next();
|
|
78
|
+
if (decision.kind === 'reject' || signal.aborted)
|
|
79
|
+
return decision;
|
|
80
|
+
// The loop skips an empty first step and an emptied offered continuation.
|
|
81
|
+
if (decision.messages.length === 0 && (step === 1 || messages.length > 0))
|
|
82
|
+
return decision;
|
|
83
|
+
const selected = selection.assembled;
|
|
84
|
+
const previous = agent.session.requestHeader()?.config;
|
|
85
|
+
if (selected === undefined || previous === undefined || sameRoute(selected, previous))
|
|
86
|
+
return decision;
|
|
87
|
+
return { ...decision, messages: [...decision.messages, modelSwitchNotice(previous, selected)] };
|
|
88
|
+
}, { prepend: true });
|
|
89
|
+
return () => {
|
|
90
|
+
disposeAssembly();
|
|
91
|
+
disposeRequest();
|
|
92
|
+
disposeNotice();
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=model-selection.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TurnBoundaryProjection } from './types.ts';
|
|
2
|
+
declare module '@qilin-agent/session-projection/types' {
|
|
3
|
+
interface SessionProjectionStateMap {
|
|
4
|
+
/** The agent session's open/last turn and step boundary facts (whole value). */
|
|
5
|
+
turnBoundary: TurnBoundaryProjection;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=projection.d.ts.map
|