@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.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The `turn` family of the Workspace registry's archive admission: whether a
3
+ * Session's own Agent is inside a turn, and its cancel when the Session is
4
+ * archived with its work. Installed by the registry's constructor, so it
5
+ * answers for every Agent the registry publishes.
6
+ *
7
+ * @module @qilin-agent/agent
8
+ */
9
+ import type { Context } from '@qilin-agent/kylin';
10
+ import type { SessionId } from '@qilin-agent/session';
11
+ import type { Agent } from './types.ts';
12
+ /**
13
+ * Answer `workspace/session-activity` with the `turn` family while the
14
+ * Session's Agent is running (a turn waiting for an approval or an answer
15
+ * included), and `workspace/session-stop` by cancelling that turn the way
16
+ * the user's own stop does — `agent.cancel({ kind: 'user' })`, but without
17
+ * the stop button's `keepInbox`, so queued input is discarded with a logged
18
+ * inbox splice instead of waking the archived Session later. Nothing is
19
+ * awaited to settlement. Both listeners live as long as `ctx`'s fiber.
20
+ * @param ctx - the registry's registration context.
21
+ * @param lookup - the registry's live-Agent lookup by Session id.
22
+ */
23
+ export declare function installTurnArchiveAdmission(ctx: Context, lookup: (sessionId: SessionId) => Agent | undefined): void;
24
+ //# sourceMappingURL=archive-admission.d.ts.map
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The `turn` family of the Workspace registry's archive admission: whether a
3
+ * Session's own Agent is inside a turn, and its cancel when the Session is
4
+ * archived with its work. Installed by the registry's constructor, so it
5
+ * answers for every Agent the registry publishes.
6
+ *
7
+ * @module @qilin-agent/agent
8
+ */
9
+ /**
10
+ * Answer `workspace/session-activity` with the `turn` family while the
11
+ * Session's Agent is running (a turn waiting for an approval or an answer
12
+ * included), and `workspace/session-stop` by cancelling that turn the way
13
+ * the user's own stop does — `agent.cancel({ kind: 'user' })`, but without
14
+ * the stop button's `keepInbox`, so queued input is discarded with a logged
15
+ * inbox splice instead of waking the archived Session later. Nothing is
16
+ * awaited to settlement. Both listeners live as long as `ctx`'s fiber.
17
+ * @param ctx - the registry's registration context.
18
+ * @param lookup - the registry's live-Agent lookup by Session id.
19
+ */
20
+ export function installTurnArchiveAdmission(ctx, lookup) {
21
+ ctx.on('workspace/session-activity', async ({ sessionId }, next) => {
22
+ const running = lookup(sessionId)?.status === 'running';
23
+ const rest = await next();
24
+ if (!running)
25
+ return rest;
26
+ const own = { kind: 'turn' };
27
+ return [own, ...rest];
28
+ });
29
+ ctx.on('workspace/session-stop', ({ sessionId }) => {
30
+ const agent = lookup(sessionId);
31
+ if (agent?.status === 'running')
32
+ agent.cancel({ kind: 'user' });
33
+ });
34
+ }
35
+ //# sourceMappingURL=archive-admission.js.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * How one agent log accounts for the work it consumed.
3
+ *
4
+ * The turn and step vocabulary alone cannot answer this. A turn that stops
5
+ * before its first step leaves a `turn/end` shaped exactly like the balanced
6
+ * no-op turns a rejection or an empty claim produces, so reading turns in
7
+ * isolation either credits cut-short work as finished or convicts every no-op.
8
+ * The missing fact is the inbox's own record: {@link Inbox} logs each mutation
9
+ * with `removedCount` and marks a cancellation `outcome: 'canceled'`, which
10
+ * separates a turn claiming its input from work being dropped unrun.
11
+ *
12
+ * @module @qilin-agent/agent/consumed-work
13
+ */
14
+ import type { SessionEvent } from '@qilin-agent/session';
15
+ /** How one agent log accounts for the work it consumed. */
16
+ export interface ConsumedWork {
17
+ /**
18
+ * The latest closed turn that accounts for consumed work: one that entered a
19
+ * model step, or one that claimed inbox input and then failed, was stopped,
20
+ * or was rejected. Absent when no turn closed over any work.
21
+ */
22
+ readonly end?: SessionEvent<'turn/end'>;
23
+ /**
24
+ * Whether accepted work was cancelled out of the inbox, unrun, after that
25
+ * turn. This is the only account of input a cancellation took before any turn
26
+ * could open over it — no `turn/end` describes it.
27
+ */
28
+ readonly droppedUnrun: boolean;
29
+ }
30
+ /**
31
+ * Fold one agent log, or an owned suffix of one, into its account of consumed
32
+ * work. Single pass, and every input is the log itself: no caller has to sample
33
+ * live state before cancelling, so a cancellation issued by anyone — the owner's
34
+ * teardown, an ancestor's interrupt, an unloading plugin — reads the same.
35
+ * @param events - the log, or an owned suffix, to fold.
36
+ * @returns the accounting turn when one closed, and whether work was dropped unrun after it.
37
+ */
38
+ export declare function foldConsumedWork(events: readonly SessionEvent[]): ConsumedWork;
39
+ //# sourceMappingURL=consumed-work.d.ts.map
@@ -0,0 +1,93 @@
1
+ /**
2
+ * How one agent log accounts for the work it consumed.
3
+ *
4
+ * The turn and step vocabulary alone cannot answer this. A turn that stops
5
+ * before its first step leaves a `turn/end` shaped exactly like the balanced
6
+ * no-op turns a rejection or an empty claim produces, so reading turns in
7
+ * isolation either credits cut-short work as finished or convicts every no-op.
8
+ * The missing fact is the inbox's own record: {@link Inbox} logs each mutation
9
+ * with `removedCount` and marks a cancellation `outcome: 'canceled'`, which
10
+ * separates a turn claiming its input from work being dropped unrun.
11
+ *
12
+ * @module @qilin-agent/agent/consumed-work
13
+ */
14
+ /**
15
+ * Whether a turn that consumed input but never reached a step ends in a way
16
+ * that accounts for that input. Only a `completed` end does not: it had
17
+ * nothing left to run once its claim was rewritten away. A `blocked` end is
18
+ * that input's ending too — the pre-step rejection that produced it discarded
19
+ * the claimed messages, so the work it took will never run.
20
+ * @param reason - the turn's recorded ending.
21
+ * @returns whether the ending accounts for the input the turn took.
22
+ */
23
+ function accountsForClaim(reason) {
24
+ switch (reason.kind) {
25
+ case 'completed':
26
+ return false;
27
+ case 'blocked':
28
+ case 'aborted':
29
+ case 'interrupted':
30
+ case 'error':
31
+ return true;
32
+ /* v8 ignore next 5 -- unreachable: `max-tokens` requires a step and short-circuits
33
+ * before this call; `forked` exists only in constructor seed history, while production
34
+ * callers fold an operation-owned suffix. `TurnEndReasonMap` is merge-extensible, so a
35
+ * backend-added variant cannot be listed; an unnameable ending over consumed input must
36
+ * not read as success. */
37
+ default:
38
+ return true;
39
+ }
40
+ }
41
+ /**
42
+ * Fold one agent log, or an owned suffix of one, into its account of consumed
43
+ * work. Single pass, and every input is the log itself: no caller has to sample
44
+ * live state before cancelling, so a cancellation issued by anyone — the owner's
45
+ * teardown, an ancestor's interrupt, an unloading plugin — reads the same.
46
+ * @param events - the log, or an owned suffix, to fold.
47
+ * @returns the accounting turn when one closed, and whether work was dropped unrun after it.
48
+ */
49
+ export function foldConsumedWork(events) {
50
+ const stepped = new Set();
51
+ const claimed = new Set();
52
+ let open;
53
+ let end;
54
+ let droppedUnrun = false;
55
+ for (const event of events) {
56
+ switch (event.type) {
57
+ case 'turn/start':
58
+ open = event.data.turn;
59
+ break;
60
+ case 'step/start':
61
+ stepped.add(event.data.turn);
62
+ break;
63
+ case 'agent/inbox/spliced': {
64
+ const { removedCount, outcome, inserted } = event.data;
65
+ if (removedCount === undefined)
66
+ break;
67
+ // A replacement keeps the work pending under a new identity, so only a
68
+ // cancellation that leaves nothing behind drops it.
69
+ if (outcome === 'canceled')
70
+ droppedUnrun ||= inserted.length === 0;
71
+ // Claims are the loop's own step-boundary reads, always inside a turn.
72
+ else if (open !== undefined)
73
+ claimed.add(open);
74
+ break;
75
+ }
76
+ case 'turn/end': {
77
+ const { turn, reason } = event.data;
78
+ open = undefined;
79
+ if (stepped.delete(turn) || (claimed.delete(turn) && accountsForClaim(reason))) {
80
+ end = event;
81
+ // Anything dropped before this turn closed is what its own ending
82
+ // reports; only a later drop is still unaccounted for.
83
+ droppedUnrun = false;
84
+ }
85
+ break;
86
+ }
87
+ default:
88
+ break;
89
+ }
90
+ }
91
+ return { ...end === undefined ? {} : { end }, droppedUnrun };
92
+ }
93
+ //# sourceMappingURL=consumed-work.js.map
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Agent-scoped dispatch and prompt assembly helpers. The fused dispatcher
3
+ * {@link agentEvents} couples the agent subject to its scope carrier, so the
4
+ * scope key and the payload's `agent` cannot diverge; repeat dispatchers (the
5
+ * loop driver) build it once in the agent's constructor and reuse it.
6
+ * @module @qilin-agent/agent/dispatch
7
+ */
8
+ import type { Context, Events } from '@qilin-agent/kylin';
9
+ import type { Scoped } from '@qilin-agent/scope';
10
+ import type { AssembleContext } from '@qilin-agent/system-prompt';
11
+ import type { Agent } from './types.ts';
12
+ /** Extract the parameter tuple from an event handler type (its `this` is not part of the tuple). */
13
+ type Params<F> = F extends (...args: infer P) => unknown ? P : never;
14
+ /** Extract the return type from an event handler type. */
15
+ type Return<F> = F extends (...args: never[]) => infer R ? R : never;
16
+ /**
17
+ * The event names whose subject is an agent: the handler's first parameter is
18
+ * a payload object carrying the `agent` subject AND the handler declares a
19
+ * `Scoped<Agent>` `this` (the scope-carrier contract). The `this` check keeps
20
+ * accidental payload-happens-to-carry-an-Agent events (or zero-arg events,
21
+ * whose parameter tuple would satisfy a bare rest-tuple check via callability)
22
+ * out of the fused-dispatch surface.
23
+ */
24
+ export type AgentSubjectEvent = {
25
+ [K in keyof Events]: Events[K] extends (this: Scoped<Agent>, ...args: infer P) => unknown ? P extends [infer Payload, ...unknown[]] ? Payload extends {
26
+ agent: Agent;
27
+ } ? K : never : never : never;
28
+ }[keyof Events];
29
+ /** The full payload object of one agent-subject event. */
30
+ type PayloadOf<K extends AgentSubjectEvent> = Params<Events[K]> extends [infer Payload, ...unknown[]] ? Payload : never;
31
+ /** The event arguments AFTER the payload: the waterfall `next` when present. */
32
+ type Tail<K extends AgentSubjectEvent> = Params<Events[K]> extends [unknown, ...infer R] ? R : never;
33
+ /**
34
+ * The payload as emit-side callers pass it: the full payload minus the agent
35
+ * field, which the fused dispatcher injects so subject and scope key cannot
36
+ * diverge.
37
+ */
38
+ type PayloadRest<K extends AgentSubjectEvent> = Omit<PayloadOf<K> & object, 'agent'>;
39
+ /**
40
+ * The fused dispatcher {@link agentEvents} returns: each method dispatches the
41
+ * named agent-subject event with the agent's scope carrier as `thisArg` and
42
+ * the agent itself injected into the payload.
43
+ */
44
+ export interface AgentEventDispatch {
45
+ /**
46
+ * Fire-and-forget notification in the agent's scope. Every listener is
47
+ * invoked; synchronous throws and returned-promise rejections are logged and
48
+ * contained per listener, so a notification cannot veto lifecycle progress
49
+ * or starve a later observer.
50
+ * @param name - the agent-subject event to emit.
51
+ * @param payload - the event's payload fields; `agent` is injected.
52
+ */
53
+ emit<K extends AgentSubjectEvent>(name: K, payload: PayloadRest<K>): void;
54
+ /**
55
+ * Awaited in-order dispatch (Cordis `serial`) in the agent's scope.
56
+ * @param name - the agent-subject event to dispatch.
57
+ * @param payload - the event's payload fields; `agent` is injected.
58
+ * @returns the serial chain's result (the first bail value, if any).
59
+ */
60
+ serial<K extends AgentSubjectEvent>(name: K, payload: PayloadRest<K>): Promise<Awaited<Return<Events[K]>>>;
61
+ /**
62
+ * Around-middleware dispatch (Cordis `waterfall`) in the agent's scope. The
63
+ * declared event parameters already end with the `next` callback, so `rest`
64
+ * is exactly the event's arguments after the payload — the final element
65
+ * being the innermost `next` (the default the listener chain wraps).
66
+ * @param name - the agent-subject event to dispatch.
67
+ * @param payload - the event's payload fields; `agent` is injected.
68
+ * @param rest - the event's arguments after the payload (the `next` callback).
69
+ * @returns the waterfall's composed result.
70
+ */
71
+ waterfall<K extends AgentSubjectEvent>(name: K, payload: PayloadRest<K>, ...rest: Tail<K>): Return<Events[K]>;
72
+ }
73
+ /**
74
+ * Build the fused scope carrier for one agent subject.
75
+ *
76
+ * The carrier is a stateless routing object. {@link agentEvents} accepts an
77
+ * existing carrier, so callers that dispatch repeatedly for the same agent
78
+ * (the loop driver) build it once in the agent's constructor and reuse it,
79
+ * keeping hot-path dispatches allocation-free.
80
+ * @param agent - the subject agent and scope key.
81
+ * @returns the carrier passed as the event dispatcher `this` value.
82
+ */
83
+ export declare function agentCarrier(agent: Agent): Scoped<Agent>;
84
+ /**
85
+ * Build a dispatcher that couples the agent subject to its scope carrier.
86
+ * @param ctx - the context to dispatch through (any context of the app).
87
+ * @param agent - the subject agent; also the scope-carrier key.
88
+ * @param carrier - the scope carrier to dispatch through; defaults to
89
+ * {@link agentCarrier} for the agent. Pass a constructor-built carrier to
90
+ * avoid rebuilding it for every dispatch.
91
+ * @returns the fused dispatcher.
92
+ */
93
+ export declare function agentEvents(ctx: Context, agent: Agent, carrier?: Scoped<Agent>): AgentEventDispatch;
94
+ /**
95
+ * Emit one contained agent notification without allocating a retained dispatcher.
96
+ * @param ctx - the context to dispatch through.
97
+ * @param agent - the subject agent and scope key.
98
+ * @param name - the agent-subject event to emit.
99
+ * @param payload - the event's payload fields; `agent` is injected.
100
+ */
101
+ export declare function emitAgentEvent<K extends AgentSubjectEvent>(ctx: Context, agent: Agent, name: K, payload: PayloadRest<K>): void;
102
+ /**
103
+ * Build the prompt assembly context with agent and scope set together, so
104
+ * agent-scoped prompt and tool contributions cannot be silently omitted.
105
+ * @param agent - the agent the assembly is for.
106
+ * @param signal - the current turn's explicit control signal, when assembly belongs to a turn.
107
+ * @returns the context to pass to `assemble()`.
108
+ */
109
+ export declare function assembleContextFor(agent: Agent, signal?: AbortSignal): AssembleContext;
110
+ export {};
111
+ //# sourceMappingURL=dispatch.d.ts.map
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Agent-scoped dispatch and prompt assembly helpers. The fused dispatcher
3
+ * {@link agentEvents} couples the agent subject to its scope carrier, so the
4
+ * scope key and the payload's `agent` cannot diverge; repeat dispatchers (the
5
+ * loop driver) build it once in the agent's constructor and reuse it.
6
+ * @module @qilin-agent/agent/dispatch
7
+ */
8
+ import { scopeTarget } from '@qilin-agent/scope';
9
+ /**
10
+ * Build the fused scope carrier for one agent subject.
11
+ *
12
+ * The carrier is a stateless routing object. {@link agentEvents} accepts an
13
+ * existing carrier, so callers that dispatch repeatedly for the same agent
14
+ * (the loop driver) build it once in the agent's constructor and reuse it,
15
+ * keeping hot-path dispatches allocation-free.
16
+ * @param agent - the subject agent and scope key.
17
+ * @returns the carrier passed as the event dispatcher `this` value.
18
+ */
19
+ export function agentCarrier(agent) {
20
+ return scopeTarget(agent, agent);
21
+ }
22
+ /**
23
+ * Build a dispatcher that couples the agent subject to its scope carrier.
24
+ * @param ctx - the context to dispatch through (any context of the app).
25
+ * @param agent - the subject agent; also the scope-carrier key.
26
+ * @param carrier - the scope carrier to dispatch through; defaults to
27
+ * {@link agentCarrier} for the agent. Pass a constructor-built carrier to
28
+ * avoid rebuilding it for every dispatch.
29
+ * @returns the fused dispatcher.
30
+ */
31
+ export function agentEvents(ctx, agent, carrier = agentCarrier(agent)) {
32
+ // The ordinary dispatch methods forward through Cordis' variadic mixins. The
33
+ // fused (carrier, name, payload, ...rest) tuple is provably a valid argument
34
+ // list for the matching thisArg overload, but TypeScript cannot relate the
35
+ // generic Tail<K> spread back to that overload's conditional parameter
36
+ // tuple — hence one contained, shape-preserving cast per method.
37
+ const fused = (payload) =>
38
+ // The dispatcher owns the subject injection; callers pass PayloadRest, so
39
+ // the fused record is exactly the declared payload. The spread comes
40
+ // first, so a structurally acceptable payload that happens to carry an
41
+ // `agent` field can never override the injected subject.
42
+ ({ ...payload, agent });
43
+ return {
44
+ emit(name, payload) {
45
+ // Cordis emit invokes callbacks through Array.map: one synchronous throw
46
+ // starves later listeners, and returned promises are discarded. Agent
47
+ // notifications are non-vetoing, so resolve the same filtered callback
48
+ // set ourselves and contain both failure modes independently.
49
+ const args = [carrier, name, fused(payload)];
50
+ const callbacks = ctx.events.dispatch('emit', args);
51
+ for (const callback of callbacks) {
52
+ try {
53
+ const returned = callback(...args);
54
+ void Promise.resolve(returned).catch((error) => {
55
+ ctx.logger.warn(`agent event "${name}" listener rejected: ${String(error)}`);
56
+ });
57
+ }
58
+ catch (error) {
59
+ ctx.logger.warn(`agent event "${name}" listener threw: ${String(error)}`);
60
+ }
61
+ }
62
+ },
63
+ async serial(name, payload) {
64
+ // oxlint-disable-next-line typescript/unbound-method -- the events mixin accessor returns a pre-bound function
65
+ const serial = ctx.serial;
66
+ return await serial(carrier, name, fused(payload));
67
+ },
68
+ waterfall(name, payload, ...rest) {
69
+ // oxlint-disable-next-line typescript/unbound-method -- the events mixin accessor returns a pre-bound function
70
+ const waterfall = ctx.waterfall;
71
+ return waterfall(carrier, name, fused(payload), ...rest);
72
+ },
73
+ };
74
+ }
75
+ /**
76
+ * Emit one contained agent notification without allocating a retained dispatcher.
77
+ * @param ctx - the context to dispatch through.
78
+ * @param agent - the subject agent and scope key.
79
+ * @param name - the agent-subject event to emit.
80
+ * @param payload - the event's payload fields; `agent` is injected.
81
+ */
82
+ export function emitAgentEvent(ctx, agent, name, payload) {
83
+ agentEvents(ctx, agent).emit(name, payload);
84
+ }
85
+ /**
86
+ * Build the prompt assembly context with agent and scope set together, so
87
+ * agent-scoped prompt and tool contributions cannot be silently omitted.
88
+ * @param agent - the agent the assembly is for.
89
+ * @param signal - the current turn's explicit control signal, when assembly belongs to a turn.
90
+ * @returns the context to pass to `assemble()`.
91
+ */
92
+ export function assembleContextFor(agent, signal) {
93
+ return { agent, scope: agent, ...signal === undefined ? {} : { signal } };
94
+ }
95
+ //# sourceMappingURL=dispatch.js.map