@celestea/runtime 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/agent-config.d.ts +18 -0
- package/dist/agent-config.js +31 -0
- package/dist/autowake.d.ts +141 -0
- package/dist/autowake.js +262 -0
- package/dist/compact/index.d.ts +13 -0
- package/dist/compact/index.js +13 -0
- package/dist/compact/plan.d.ts +51 -0
- package/dist/compact/plan.js +98 -0
- package/dist/compact/rewrite.d.ts +23 -0
- package/dist/compact/rewrite.js +79 -0
- package/dist/compact/run.d.ts +44 -0
- package/dist/compact/run.js +59 -0
- package/dist/compact/summarize.d.ts +30 -0
- package/dist/compact/summarize.js +70 -0
- package/dist/compact/transcript.d.ts +35 -0
- package/dist/compact/transcript.js +88 -0
- package/dist/compose.d.ts +117 -0
- package/dist/compose.js +191 -0
- package/dist/errors.d.ts +25 -0
- package/dist/errors.js +34 -0
- package/dist/frames.d.ts +46 -0
- package/dist/frames.js +62 -0
- package/dist/gen.d.ts +86 -0
- package/dist/gen.js +129 -0
- package/dist/host/engine-session.d.ts +117 -0
- package/dist/host/engine-session.js +109 -0
- package/dist/host/index.d.ts +39 -0
- package/dist/host/index.js +39 -0
- package/dist/host/provider-target.d.ts +113 -0
- package/dist/host/provider-target.js +116 -0
- package/dist/inbox-checkpoint.d.ts +18 -0
- package/dist/inbox-checkpoint.js +37 -0
- package/dist/inbox.d.ts +94 -0
- package/dist/inbox.js +139 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +71 -0
- package/dist/ledger-io.d.ts +27 -0
- package/dist/ledger-io.js +74 -0
- package/dist/ledger-llm.d.ts +48 -0
- package/dist/ledger-llm.js +115 -0
- package/dist/ledger-query.d.ts +91 -0
- package/dist/ledger-query.js +153 -0
- package/dist/ledger.d.ts +271 -0
- package/dist/ledger.js +444 -0
- package/dist/pricing.d.ts +100 -0
- package/dist/pricing.js +167 -0
- package/dist/profile.d.ts +26 -0
- package/dist/profile.js +39 -0
- package/dist/recovery.d.ts +56 -0
- package/dist/recovery.js +91 -0
- package/dist/retention.d.ts +49 -0
- package/dist/retention.js +119 -0
- package/dist/runtime.d.ts +197 -0
- package/dist/runtime.js +347 -0
- package/dist/sanitize.d.ts +35 -0
- package/dist/sanitize.js +36 -0
- package/dist/session-binding.d.ts +36 -0
- package/dist/session-binding.js +33 -0
- package/dist/session-registry.d.ts +238 -0
- package/dist/session-registry.js +388 -0
- package/dist/status.d.ts +279 -0
- package/dist/status.js +411 -0
- package/dist/tokens.d.ts +25 -0
- package/dist/tokens.js +25 -0
- package/dist/turn-runner.d.ts +169 -0
- package/dist/turn-runner.js +242 -0
- package/dist/usage.d.ts +64 -0
- package/dist/usage.js +88 -0
- package/dist/watchdog-mount.d.ts +79 -0
- package/dist/watchdog-mount.js +120 -0
- package/dist/worker-wiring.d.ts +74 -0
- package/dist/worker-wiring.js +107 -0
- package/package.json +31 -0
package/dist/tokens.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Well-known service tokens the runtime owns.
|
|
3
|
+
*
|
|
4
|
+
* `core` freezes the seam tokens (`SESSION_LOG_SERVICE`, `LLM_SERVICE`,
|
|
5
|
+
* `TOOL_REGISTRY_SERVICE`, `AGENT_LOOP_SERVICE`, `EVENT_BUS_SERVICE`). The
|
|
6
|
+
* runtime adds the tokens that only exist because it drives turns: the
|
|
7
|
+
* per-turn abort signal, the per-turn frame sink, and the two accounting
|
|
8
|
+
* services. They follow the same convention (a stable namespaced string), so a
|
|
9
|
+
* Context-aware `AgentLoop` implementation can resolve them without importing
|
|
10
|
+
* this package (which would be an L1 -> L2 jump, ARCHITECTURE.md §1.3 D3).
|
|
11
|
+
*/
|
|
12
|
+
/** Per-turn `AbortSignal` (cooperative cancellation), provided on the turn scope. */
|
|
13
|
+
export declare const TURN_ABORT_SERVICE = "celestea.runtime.TurnAbort";
|
|
14
|
+
/** Per-turn frame sink: `(frame: TurnFrame) => void` (SSE-shaped, in log order). */
|
|
15
|
+
export declare const TURN_SINK_SERVICE = "celestea.runtime.TurnSink";
|
|
16
|
+
/** Shared usage accounting (`UsageAccounting`): latest + cumulative. */
|
|
17
|
+
export declare const USAGE_TRACKER_SERVICE = "celestea.runtime.UsageTracker";
|
|
18
|
+
/** Shared statusline tracker (`StatusTracker`): steps + delta rate. */
|
|
19
|
+
export declare const STATUS_TRACKER_SERVICE = "celestea.runtime.StatusTracker";
|
|
20
|
+
/** The host (coordinator) conversation id: registry / mailbox queue key. */
|
|
21
|
+
export declare const HOST_SESSION_ID = "cli-main";
|
|
22
|
+
/** Default directory the worker receipt protocol writes `results/<wid>-<short>.md` into. */
|
|
23
|
+
export declare const RESULTS_DIR = "results";
|
|
24
|
+
/** W218: context window used for the estimated ratio (contract display default). */
|
|
25
|
+
export declare const CONTEXT_WINDOW_FALLBACK = 1000000;
|
package/dist/tokens.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Well-known service tokens the runtime owns.
|
|
3
|
+
*
|
|
4
|
+
* `core` freezes the seam tokens (`SESSION_LOG_SERVICE`, `LLM_SERVICE`,
|
|
5
|
+
* `TOOL_REGISTRY_SERVICE`, `AGENT_LOOP_SERVICE`, `EVENT_BUS_SERVICE`). The
|
|
6
|
+
* runtime adds the tokens that only exist because it drives turns: the
|
|
7
|
+
* per-turn abort signal, the per-turn frame sink, and the two accounting
|
|
8
|
+
* services. They follow the same convention (a stable namespaced string), so a
|
|
9
|
+
* Context-aware `AgentLoop` implementation can resolve them without importing
|
|
10
|
+
* this package (which would be an L1 -> L2 jump, ARCHITECTURE.md §1.3 D3).
|
|
11
|
+
*/
|
|
12
|
+
/** Per-turn `AbortSignal` (cooperative cancellation), provided on the turn scope. */
|
|
13
|
+
export const TURN_ABORT_SERVICE = "celestea.runtime.TurnAbort";
|
|
14
|
+
/** Per-turn frame sink: `(frame: TurnFrame) => void` (SSE-shaped, in log order). */
|
|
15
|
+
export const TURN_SINK_SERVICE = "celestea.runtime.TurnSink";
|
|
16
|
+
/** Shared usage accounting (`UsageAccounting`): latest + cumulative. */
|
|
17
|
+
export const USAGE_TRACKER_SERVICE = "celestea.runtime.UsageTracker";
|
|
18
|
+
/** Shared statusline tracker (`StatusTracker`): steps + delta rate. */
|
|
19
|
+
export const STATUS_TRACKER_SERVICE = "celestea.runtime.StatusTracker";
|
|
20
|
+
/** The host (coordinator) conversation id: registry / mailbox queue key. */
|
|
21
|
+
export const HOST_SESSION_ID = "cli-main";
|
|
22
|
+
/** Default directory the worker receipt protocol writes `results/<wid>-<short>.md` into. */
|
|
23
|
+
export const RESULTS_DIR = "results";
|
|
24
|
+
/** W218: context window used for the estimated ratio (contract display default). */
|
|
25
|
+
export const CONTEXT_WINDOW_FALLBACK = 1_000_000;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One turn, driven through the `AgentLoop` seam (`runtime/src/run.rs`).
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities, in order:
|
|
5
|
+
* 1. **single concurrency slot** — a Runtime generation runs at most one turn;
|
|
6
|
+
* a second `runTurn` while busy is a [TurnBusyError] (409), never a queue;
|
|
7
|
+
* 2. **receipt drain** — pending worker receipts are polled out of the host
|
|
8
|
+
* mailbox and appended to the session log BEFORE the input, so a receipt is
|
|
9
|
+
* real, model-visible history on the host's next turn (W232);
|
|
10
|
+
* 3. **stream mapping** — every `LoopEvent` the loop emits is fed to the
|
|
11
|
+
* statusline tracker and mapped to one host frame, in log order;
|
|
12
|
+
* 4. **cancel propagation** — the caller's `AbortSignal` is linked to the
|
|
13
|
+
* turn's own signal, which is (a) passed to the loop bindings and
|
|
14
|
+
* (b) provided on the turn scope under `TURN_ABORT_SERVICE`;
|
|
15
|
+
* 5. **terminal state** — read back from the session log's own `turn_end`
|
|
16
|
+
* (the log is the single source of truth), never invented by the runtime.
|
|
17
|
+
* 6. **usage ledger observation** (W728) — when a ledger is wired, the turn
|
|
18
|
+
* boundary is announced to it (pure observation; it can never throw).
|
|
19
|
+
*/
|
|
20
|
+
import { type AgentConfig, type AgentLoop, type Context, type ImageRef, type InjectionSource, type PendingInjection, type SessionEvent, type SessionLog, type TurnOutcome } from "@celestea/core";
|
|
21
|
+
import type { FrameMapper, LoopEventSink, TurnFrame } from "./frames.js";
|
|
22
|
+
import type { TurnLedgerHooks } from "./ledger.js";
|
|
23
|
+
import type { StatusTracker } from "./status.js";
|
|
24
|
+
import type { UsageAccounting } from "./usage.js";
|
|
25
|
+
/** Host-side frame consumer (SSE publisher, CLI renderer, test collector). */
|
|
26
|
+
export type FrameSink = (frame: TurnFrame) => void;
|
|
27
|
+
/**
|
|
28
|
+
* W888: one engine-owned turn-context row. The origin is what the transcript
|
|
29
|
+
* uses to label the injected block instead of showing it as a user bubble.
|
|
30
|
+
*/
|
|
31
|
+
export interface TurnContextRow {
|
|
32
|
+
readonly text: string;
|
|
33
|
+
readonly origin: "skill" | "memory";
|
|
34
|
+
}
|
|
35
|
+
export interface TurnOptions {
|
|
36
|
+
/** Caller cancellation (linked into the turn's own signal). */
|
|
37
|
+
signal?: AbortSignal;
|
|
38
|
+
/** Frame consumer for this turn; absent = frames are dropped. */
|
|
39
|
+
sink?: FrameSink;
|
|
40
|
+
/**
|
|
41
|
+
* W804: content-addressed image references for THIS turn's user message. The
|
|
42
|
+
* loop writes them onto the `user_message` row; bytes never enter the log.
|
|
43
|
+
*/
|
|
44
|
+
attachments?: readonly ImageRef[];
|
|
45
|
+
}
|
|
46
|
+
/** Collaborators handed to a per-turn loop instance (`with_bindings`). */
|
|
47
|
+
export interface LoopBindings {
|
|
48
|
+
config: AgentConfig;
|
|
49
|
+
signal: AbortSignal;
|
|
50
|
+
sink: LoopEventSink;
|
|
51
|
+
usage: UsageAccounting;
|
|
52
|
+
/** Mid-turn injection source (absent = nothing can be injected). */
|
|
53
|
+
injections?: InjectionSource;
|
|
54
|
+
}
|
|
55
|
+
/** Builds the per-turn `AgentLoop`; the host injects its concrete loop here. */
|
|
56
|
+
export type LoopFactory = (bindings: LoopBindings) => AgentLoop;
|
|
57
|
+
/** Anything waiting to be appended to the log (user text, receipt, relay). */
|
|
58
|
+
export type PendingReceipt = PendingInjection;
|
|
59
|
+
export interface TurnRunnerDeps {
|
|
60
|
+
ctx: Context;
|
|
61
|
+
/** Current session log (a rebind swaps the producer, so this is a getter). */
|
|
62
|
+
session: () => SessionLog;
|
|
63
|
+
status: StatusTracker;
|
|
64
|
+
usage: UsageAccounting;
|
|
65
|
+
agentConfig: AgentConfig;
|
|
66
|
+
frameMapper: FrameMapper;
|
|
67
|
+
/**
|
|
68
|
+
* Usage ledger hooks (W728 §3 P0): the turn boundary is only known HERE, and
|
|
69
|
+
* the ledger must not guess it from a counter. Observation only — the ledger
|
|
70
|
+
* swallows its own IO failures, so a turn cannot fail because of bookkeeping.
|
|
71
|
+
*/
|
|
72
|
+
ledger?: TurnLedgerHooks;
|
|
73
|
+
/** Absent = the loop is resolved from `AGENT_LOOP_SERVICE` in the Context. */
|
|
74
|
+
loopFactory?: LoopFactory;
|
|
75
|
+
/**
|
|
76
|
+
* The TURN-START drain (`next-turn` lane + session mailbox): receipts precede
|
|
77
|
+
* the input (W232) and a follow-up queued while the session was idle is
|
|
78
|
+
* appended before it (W515 §1: `placement: "queued"`).
|
|
79
|
+
*/
|
|
80
|
+
drainPending?: () => PendingReceipt[];
|
|
81
|
+
/**
|
|
82
|
+
* The STEP-BOUNDARY source handed to the loop (`next-step` lane + session
|
|
83
|
+
* mailbox). It carries `pending()` so the loop can refuse to close a turn
|
|
84
|
+
* while a steering message is still waiting (W515 §1 invariant).
|
|
85
|
+
*/
|
|
86
|
+
injections?: InjectionSource;
|
|
87
|
+
/**
|
|
88
|
+
* W884: durable, ENGINE-OWNED turn context — the skill catalog (name +
|
|
89
|
+
* description only) and the F3 workspace MEMORY.md. Evaluated at EVERY turn
|
|
90
|
+
* start, before the receipts and the input, and appended as ordinary user-role
|
|
91
|
+
* history so it stays resident and participates in trimming/compaction like any
|
|
92
|
+
* other message. Returning `[]` costs nothing (a workspace with neither).
|
|
93
|
+
*
|
|
94
|
+
* W888: each row carries its own `origin` so the transcript can label an
|
|
95
|
+
* injected block ('skill' catalog vs 'memory') instead of showing it as a
|
|
96
|
+
* typed user bubble.
|
|
97
|
+
*/
|
|
98
|
+
turnContext?: () => readonly TurnContextRow[];
|
|
99
|
+
}
|
|
100
|
+
export declare class TurnRunner {
|
|
101
|
+
private readonly deps;
|
|
102
|
+
private busy;
|
|
103
|
+
private controller;
|
|
104
|
+
private turnNo;
|
|
105
|
+
private released;
|
|
106
|
+
/** The in-flight turn promise, so shutdown can wait for it (P1-5, W836). */
|
|
107
|
+
private inFlight;
|
|
108
|
+
constructor(deps: TurnRunnerDeps);
|
|
109
|
+
get isBusy(): boolean;
|
|
110
|
+
/** The in-flight turn's signal, or null between turns. */
|
|
111
|
+
get currentSignal(): AbortSignal | null;
|
|
112
|
+
/** Turns started by this runner (diagnostics / tests). */
|
|
113
|
+
get turnCount(): number;
|
|
114
|
+
/** Cancel the in-flight turn; returns false when nothing was running. */
|
|
115
|
+
cancel(): boolean;
|
|
116
|
+
/** Shutdown hook: stop driving, but keep no other state. */
|
|
117
|
+
stop(): void;
|
|
118
|
+
runTurn(input: string | null, opts?: TurnOptions): Promise<TurnOutcome>;
|
|
119
|
+
/**
|
|
120
|
+
* P1-5 (W836): wait for the in-flight turn to settle. `stop()` only ABORTS it;
|
|
121
|
+
* a shutdown that claims a clean exit must first see the turn's own terminal
|
|
122
|
+
* write, or a crash inside that window strands the turn forever.
|
|
123
|
+
*/
|
|
124
|
+
join(): Promise<void>;
|
|
125
|
+
private drive;
|
|
126
|
+
/** Feed the tracker, then map the event onto one host frame. */
|
|
127
|
+
private makeSink;
|
|
128
|
+
/** W263口径: a step per tool CALL; deltas feed the rate window. */
|
|
129
|
+
private observe;
|
|
130
|
+
/**
|
|
131
|
+
* The turn scope: the root context plus the per-turn services. A loop
|
|
132
|
+
* constructed from the Context (no factory) can resolve the signal and sink
|
|
133
|
+
* here without importing this package.
|
|
134
|
+
*/
|
|
135
|
+
private turnScope;
|
|
136
|
+
private resolveLoop;
|
|
137
|
+
/**
|
|
138
|
+
* W884: append the engine-owned turn context (the skill catalog). Blank rows
|
|
139
|
+
* are dropped, so a provider that has nothing to say is free to return [""].
|
|
140
|
+
*/
|
|
141
|
+
private injectTurnContext;
|
|
142
|
+
/** Turn-start drain: receipts and interjections land BEFORE the input. */
|
|
143
|
+
private injectReceipts;
|
|
144
|
+
/** The single drain function shared by turn start and the step boundary. */
|
|
145
|
+
private drainPending;
|
|
146
|
+
}
|
|
147
|
+
/** `[from W1] content` — the receipt attribution the host log shows verbatim. */
|
|
148
|
+
export declare function formatReceipt(receipt: PendingReceipt): string;
|
|
149
|
+
/**
|
|
150
|
+
* Link a caller signal into the turn's controller. Returns the cleanup that
|
|
151
|
+
* removes the listener again (P1-7, W836): `once:true` only removes it when the
|
|
152
|
+
* source DOES abort, so a host signal that outlives many turns would otherwise
|
|
153
|
+
* accumulate one listener per turn.
|
|
154
|
+
*/
|
|
155
|
+
export declare function linkAbort(source: AbortSignal, target: AbortController): () => void;
|
|
156
|
+
/**
|
|
157
|
+
* The terminal state of the turn that started at `from`: the LAST `turn_end`
|
|
158
|
+
* appended after that index, with legacy rows (missing outcome) defaulting to
|
|
159
|
+
* `completed` exactly like the JSONL codec does.
|
|
160
|
+
*/
|
|
161
|
+
export declare function lastTurnEndOutcome(events: readonly SessionEvent[], from: number): TurnOutcome | null;
|
|
162
|
+
/**
|
|
163
|
+
* Resolve the outcome of a finished turn:
|
|
164
|
+
* 1. a `turn_end` written by the loop wins — the log is the source of truth;
|
|
165
|
+
* 2. otherwise the turn never terminated: a thrown error propagates (a wiring
|
|
166
|
+
* failure is not a terminal state), an aborted turn is `cancelled`, and a
|
|
167
|
+
* silently-stopped turn is `interrupted` (a torn turn is never `completed`).
|
|
168
|
+
*/
|
|
169
|
+
export declare function resolveOutcome(events: readonly SessionEvent[], from: number, signal: AbortSignal, failure: unknown): TurnOutcome;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One turn, driven through the `AgentLoop` seam (`runtime/src/run.rs`).
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities, in order:
|
|
5
|
+
* 1. **single concurrency slot** — a Runtime generation runs at most one turn;
|
|
6
|
+
* a second `runTurn` while busy is a [TurnBusyError] (409), never a queue;
|
|
7
|
+
* 2. **receipt drain** — pending worker receipts are polled out of the host
|
|
8
|
+
* mailbox and appended to the session log BEFORE the input, so a receipt is
|
|
9
|
+
* real, model-visible history on the host's next turn (W232);
|
|
10
|
+
* 3. **stream mapping** — every `LoopEvent` the loop emits is fed to the
|
|
11
|
+
* statusline tracker and mapped to one host frame, in log order;
|
|
12
|
+
* 4. **cancel propagation** — the caller's `AbortSignal` is linked to the
|
|
13
|
+
* turn's own signal, which is (a) passed to the loop bindings and
|
|
14
|
+
* (b) provided on the turn scope under `TURN_ABORT_SERVICE`;
|
|
15
|
+
* 5. **terminal state** — read back from the session log's own `turn_end`
|
|
16
|
+
* (the log is the single source of truth), never invented by the runtime.
|
|
17
|
+
* 6. **usage ledger observation** (W728) — when a ledger is wired, the turn
|
|
18
|
+
* boundary is announced to it (pure observation; it can never throw).
|
|
19
|
+
*/
|
|
20
|
+
import { AGENT_LOOP_SERVICE, } from "@celestea/core";
|
|
21
|
+
import { ComposeError, RuntimeReleasedError, TurnBusyError } from "./errors.js";
|
|
22
|
+
import { TURN_ABORT_SERVICE, TURN_SINK_SERVICE, USAGE_TRACKER_SERVICE } from "./tokens.js";
|
|
23
|
+
export class TurnRunner {
|
|
24
|
+
deps;
|
|
25
|
+
busy = false;
|
|
26
|
+
controller = null;
|
|
27
|
+
turnNo = 0;
|
|
28
|
+
released = false;
|
|
29
|
+
/** The in-flight turn promise, so shutdown can wait for it (P1-5, W836). */
|
|
30
|
+
inFlight = null;
|
|
31
|
+
constructor(deps) {
|
|
32
|
+
this.deps = deps;
|
|
33
|
+
}
|
|
34
|
+
get isBusy() {
|
|
35
|
+
return this.busy;
|
|
36
|
+
}
|
|
37
|
+
/** The in-flight turn's signal, or null between turns. */
|
|
38
|
+
get currentSignal() {
|
|
39
|
+
return this.controller?.signal ?? null;
|
|
40
|
+
}
|
|
41
|
+
/** Turns started by this runner (diagnostics / tests). */
|
|
42
|
+
get turnCount() {
|
|
43
|
+
return this.turnNo;
|
|
44
|
+
}
|
|
45
|
+
/** Cancel the in-flight turn; returns false when nothing was running. */
|
|
46
|
+
cancel() {
|
|
47
|
+
if (this.controller === null)
|
|
48
|
+
return false;
|
|
49
|
+
this.controller.abort();
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
/** Shutdown hook: stop driving, but keep no other state. */
|
|
53
|
+
stop() {
|
|
54
|
+
this.released = true;
|
|
55
|
+
this.cancel();
|
|
56
|
+
}
|
|
57
|
+
async runTurn(input, opts = {}) {
|
|
58
|
+
if (this.released)
|
|
59
|
+
throw new RuntimeReleasedError("the turn runner was stopped");
|
|
60
|
+
if (this.busy)
|
|
61
|
+
throw new TurnBusyError();
|
|
62
|
+
this.busy = true;
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
const unlink = opts.signal === undefined ? null : linkAbort(opts.signal, controller);
|
|
65
|
+
this.controller = controller;
|
|
66
|
+
const run = this.drive(input, opts, controller.signal);
|
|
67
|
+
this.inFlight = run;
|
|
68
|
+
try {
|
|
69
|
+
return await run;
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
unlink?.();
|
|
73
|
+
this.controller = null;
|
|
74
|
+
this.busy = false;
|
|
75
|
+
this.inFlight = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* P1-5 (W836): wait for the in-flight turn to settle. `stop()` only ABORTS it;
|
|
80
|
+
* a shutdown that claims a clean exit must first see the turn's own terminal
|
|
81
|
+
* write, or a crash inside that window strands the turn forever.
|
|
82
|
+
*/
|
|
83
|
+
async join() {
|
|
84
|
+
const run = this.inFlight;
|
|
85
|
+
if (run === null)
|
|
86
|
+
return;
|
|
87
|
+
try {
|
|
88
|
+
await run;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// The starter observes the outcome; shutdown only needs the turn stopped.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async drive(input, opts, signal) {
|
|
95
|
+
this.turnNo += 1;
|
|
96
|
+
this.deps.status.beginTurn();
|
|
97
|
+
const sink = this.makeSink(opts.sink);
|
|
98
|
+
const scope = this.turnScope(signal, sink);
|
|
99
|
+
const log = this.deps.session();
|
|
100
|
+
// W884: the skill catalog is standing context, so it lands BEFORE the
|
|
101
|
+
// receipts (which are addressed messages and belong nearest the input).
|
|
102
|
+
this.injectTurnContext(log);
|
|
103
|
+
this.injectReceipts(log);
|
|
104
|
+
const start = log.events().length;
|
|
105
|
+
const loop = this.resolveLoop(signal, sink);
|
|
106
|
+
this.deps.ledger?.beginTurn(log);
|
|
107
|
+
let failure = null;
|
|
108
|
+
try {
|
|
109
|
+
await loop.runTurn(scope, input, opts.attachments);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
failure = error;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const outcome = resolveOutcome(log.events(), start, signal, failure);
|
|
116
|
+
this.deps.ledger?.endTurn(outcome);
|
|
117
|
+
return outcome;
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
// A wiring failure still closes the ledger's turn before it propagates:
|
|
121
|
+
// the usage already booked belongs to a turn that will have no total.
|
|
122
|
+
this.deps.ledger?.endTurn("interrupted");
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Feed the tracker, then map the event onto one host frame. */
|
|
127
|
+
makeSink(userSink) {
|
|
128
|
+
return (event) => {
|
|
129
|
+
this.observe(event);
|
|
130
|
+
userSink?.(this.deps.frameMapper(event));
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** W263口径: a step per tool CALL; deltas feed the rate window. */
|
|
134
|
+
observe(event) {
|
|
135
|
+
if (event.kind === "text")
|
|
136
|
+
this.deps.status.addChars(event.delta.length);
|
|
137
|
+
else if (event.kind === "thinking")
|
|
138
|
+
this.deps.status.addChars(event.delta.length);
|
|
139
|
+
else if (event.kind === "tool_call")
|
|
140
|
+
this.deps.status.addStep();
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The turn scope: the root context plus the per-turn services. A loop
|
|
144
|
+
* constructed from the Context (no factory) can resolve the signal and sink
|
|
145
|
+
* here without importing this package.
|
|
146
|
+
*/
|
|
147
|
+
turnScope(signal, sink) {
|
|
148
|
+
const scope = this.deps.ctx.scoped();
|
|
149
|
+
scope.provide(TURN_ABORT_SERVICE, signal);
|
|
150
|
+
scope.provide(TURN_SINK_SERVICE, sink);
|
|
151
|
+
scope.provide(USAGE_TRACKER_SERVICE, this.deps.usage);
|
|
152
|
+
return scope;
|
|
153
|
+
}
|
|
154
|
+
resolveLoop(signal, sink) {
|
|
155
|
+
const factory = this.deps.loopFactory;
|
|
156
|
+
if (factory !== undefined) {
|
|
157
|
+
const injections = this.deps.injections;
|
|
158
|
+
return factory({
|
|
159
|
+
config: this.deps.agentConfig,
|
|
160
|
+
signal,
|
|
161
|
+
sink,
|
|
162
|
+
usage: this.deps.usage,
|
|
163
|
+
...(injections === undefined ? {} : { injections }),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const loop = this.deps.ctx.get(AGENT_LOOP_SERVICE);
|
|
167
|
+
if (loop === undefined) {
|
|
168
|
+
throw new ComposeError("no AgentLoop: pass a loopFactory or mount an agentLoopPlugin");
|
|
169
|
+
}
|
|
170
|
+
return loop;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* W884: append the engine-owned turn context (the skill catalog). Blank rows
|
|
174
|
+
* are dropped, so a provider that has nothing to say is free to return [""].
|
|
175
|
+
*/
|
|
176
|
+
injectTurnContext(log) {
|
|
177
|
+
for (const row of this.deps.turnContext?.() ?? []) {
|
|
178
|
+
if (row.text === "")
|
|
179
|
+
continue;
|
|
180
|
+
// W888: the origin travels with the row so the projection can label it.
|
|
181
|
+
log.append({ type: "user_message", text: row.text, origin: row.origin });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** Turn-start drain: receipts and interjections land BEFORE the input. */
|
|
185
|
+
injectReceipts(log) {
|
|
186
|
+
for (const receipt of this.drainPending()) {
|
|
187
|
+
// W888: a worker receipt is NOT the human's voice.
|
|
188
|
+
log.append({ type: "user_message", text: formatReceipt(receipt), origin: "receipt" });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** The single drain function shared by turn start and the step boundary. */
|
|
192
|
+
drainPending() {
|
|
193
|
+
return this.deps.drainPending?.() ?? [];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** `[from W1] content` — the receipt attribution the host log shows verbatim. */
|
|
197
|
+
export function formatReceipt(receipt) {
|
|
198
|
+
return receipt.from === "" ? receipt.text : `[from ${receipt.from}] ${receipt.text}`;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Link a caller signal into the turn's controller. Returns the cleanup that
|
|
202
|
+
* removes the listener again (P1-7, W836): `once:true` only removes it when the
|
|
203
|
+
* source DOES abort, so a host signal that outlives many turns would otherwise
|
|
204
|
+
* accumulate one listener per turn.
|
|
205
|
+
*/
|
|
206
|
+
export function linkAbort(source, target) {
|
|
207
|
+
if (source.aborted) {
|
|
208
|
+
target.abort();
|
|
209
|
+
return () => undefined;
|
|
210
|
+
}
|
|
211
|
+
const onAbort = () => target.abort();
|
|
212
|
+
source.addEventListener("abort", onAbort, { once: true });
|
|
213
|
+
return () => source.removeEventListener("abort", onAbort);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* The terminal state of the turn that started at `from`: the LAST `turn_end`
|
|
217
|
+
* appended after that index, with legacy rows (missing outcome) defaulting to
|
|
218
|
+
* `completed` exactly like the JSONL codec does.
|
|
219
|
+
*/
|
|
220
|
+
export function lastTurnEndOutcome(events, from) {
|
|
221
|
+
for (let i = events.length - 1; i >= from; i--) {
|
|
222
|
+
const ev = events[i];
|
|
223
|
+
if (ev !== undefined && ev.type === "turn_end")
|
|
224
|
+
return ev.outcome ?? "completed";
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Resolve the outcome of a finished turn:
|
|
230
|
+
* 1. a `turn_end` written by the loop wins — the log is the source of truth;
|
|
231
|
+
* 2. otherwise the turn never terminated: a thrown error propagates (a wiring
|
|
232
|
+
* failure is not a terminal state), an aborted turn is `cancelled`, and a
|
|
233
|
+
* silently-stopped turn is `interrupted` (a torn turn is never `completed`).
|
|
234
|
+
*/
|
|
235
|
+
export function resolveOutcome(events, from, signal, failure) {
|
|
236
|
+
const fromLog = lastTurnEndOutcome(events, from);
|
|
237
|
+
if (fromLog !== null)
|
|
238
|
+
return fromLog;
|
|
239
|
+
if (failure !== null && failure !== undefined)
|
|
240
|
+
throw failure;
|
|
241
|
+
return signal.aborted ? "cancelled" : "interrupted";
|
|
242
|
+
}
|
package/dist/usage.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage accounting — the `usage` half of the statusline (W220/W263).
|
|
3
|
+
*
|
|
4
|
+
* The `AgentLoop` records the provider's `usage` stream event for every LLM
|
|
5
|
+
* response; the host reads two views:
|
|
6
|
+
* - `latest()` — the most recent response (the context-usage surface is the
|
|
7
|
+
* REAL prompt size of that request: `usage_prompt_tokens`, estimated:false);
|
|
8
|
+
* - `total()` — cumulative across every response of this generation.
|
|
9
|
+
*
|
|
10
|
+
* Both accessors return copies, so a caller can never mutate tracked state
|
|
11
|
+
* (`Usage` is `Copy` in the legacy engine).
|
|
12
|
+
*
|
|
13
|
+
* The seam is structural (`UsageRecorder` / `UsageAccounting`), not a class
|
|
14
|
+
* identity: `packages/agent-loop` ships its own `UsageTracker` with the same
|
|
15
|
+
* three methods, and the composition root may pass that instance in — the
|
|
16
|
+
* runtime then observes the very same object the loop writes to, with no
|
|
17
|
+
* cross-package type dependency.
|
|
18
|
+
*/
|
|
19
|
+
import { type Usage, type UsageBlock } from "@celestea/core";
|
|
20
|
+
/** Write side of the seam: what an agent loop needs. */
|
|
21
|
+
export interface UsageRecorder {
|
|
22
|
+
record(usage: Usage): void;
|
|
23
|
+
}
|
|
24
|
+
/** Read side: what the statusline / host needs in addition. */
|
|
25
|
+
export interface UsageAccounting extends UsageRecorder {
|
|
26
|
+
latest(): Usage;
|
|
27
|
+
total(): Usage;
|
|
28
|
+
}
|
|
29
|
+
/** In-memory latest + cumulative tracker (the runtime's default implementation). */
|
|
30
|
+
export declare class UsageTracker implements UsageAccounting {
|
|
31
|
+
private latestUsage;
|
|
32
|
+
private totalUsage;
|
|
33
|
+
record(usage: Usage): void;
|
|
34
|
+
latest(): Usage;
|
|
35
|
+
total(): Usage;
|
|
36
|
+
/** New-turn / new-generation baseline (the tracker keeps no history). */
|
|
37
|
+
reset(): void;
|
|
38
|
+
}
|
|
39
|
+
/** Factory form (ARCHITECTURE.md §6.1 `createXxx` convention). */
|
|
40
|
+
export declare function createUsageTracker(): UsageTracker;
|
|
41
|
+
/**
|
|
42
|
+
* `cache_read / prompt_tokens`, clamped to [0,1] and rounded to 4 decimals;
|
|
43
|
+
* 0 when the denominator is 0 (no usage recorded yet). `cache_hit_ratio`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function cacheHitRatioRounded(u: Usage): number;
|
|
46
|
+
/** One usage block: raw counters plus the derived cache-hit ratio. */
|
|
47
|
+
export declare function usageBlock(u: Usage): UsageBlock;
|
|
48
|
+
/**
|
|
49
|
+
* The statusline `usage` field: latest block + the same shape under `total`.
|
|
50
|
+
*
|
|
51
|
+
* W755 GUARDRAIL — `total` is a BILLING-shaped sum, NOT an occupancy reading.
|
|
52
|
+
* Every step of a turn re-sends the whole prompt, so `total.prompt_tokens` is
|
|
53
|
+
* O(steps x prompt): the DSH host's own ledger shows the same shape (13.7M
|
|
54
|
+
* uncached input / 1.09G cache-read over one long session) and it is several
|
|
55
|
+
* orders of magnitude away from the context window. Context occupancy is
|
|
56
|
+
* `context_usage` and ONLY `context_usage`
|
|
57
|
+
* (`packages/runtime/src/status.ts::contextUsage`, which uses `latest()` plus
|
|
58
|
+
* the visible growth since that sample). Never divide `total.prompt_tokens`,
|
|
59
|
+
* `total.cache_read` or `total.total_tokens` by a context window, and never
|
|
60
|
+
* render a context meter from this block.
|
|
61
|
+
*/
|
|
62
|
+
export declare function usageStatus(u: UsageAccounting): UsageBlock & {
|
|
63
|
+
total: UsageBlock;
|
|
64
|
+
};
|
package/dist/usage.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage accounting — the `usage` half of the statusline (W220/W263).
|
|
3
|
+
*
|
|
4
|
+
* The `AgentLoop` records the provider's `usage` stream event for every LLM
|
|
5
|
+
* response; the host reads two views:
|
|
6
|
+
* - `latest()` — the most recent response (the context-usage surface is the
|
|
7
|
+
* REAL prompt size of that request: `usage_prompt_tokens`, estimated:false);
|
|
8
|
+
* - `total()` — cumulative across every response of this generation.
|
|
9
|
+
*
|
|
10
|
+
* Both accessors return copies, so a caller can never mutate tracked state
|
|
11
|
+
* (`Usage` is `Copy` in the legacy engine).
|
|
12
|
+
*
|
|
13
|
+
* The seam is structural (`UsageRecorder` / `UsageAccounting`), not a class
|
|
14
|
+
* identity: `packages/agent-loop` ships its own `UsageTracker` with the same
|
|
15
|
+
* three methods, and the composition root may pass that instance in — the
|
|
16
|
+
* runtime then observes the very same object the loop writes to, with no
|
|
17
|
+
* cross-package type dependency.
|
|
18
|
+
*/
|
|
19
|
+
import { usageAdd, zeroUsage } from "@celestea/core";
|
|
20
|
+
/** In-memory latest + cumulative tracker (the runtime's default implementation). */
|
|
21
|
+
export class UsageTracker {
|
|
22
|
+
latestUsage = zeroUsage();
|
|
23
|
+
totalUsage = zeroUsage();
|
|
24
|
+
record(usage) {
|
|
25
|
+
this.totalUsage = usageAdd(this.totalUsage, usage);
|
|
26
|
+
this.latestUsage = { ...usage };
|
|
27
|
+
}
|
|
28
|
+
latest() {
|
|
29
|
+
return { ...this.latestUsage };
|
|
30
|
+
}
|
|
31
|
+
total() {
|
|
32
|
+
return { ...this.totalUsage };
|
|
33
|
+
}
|
|
34
|
+
/** New-turn / new-generation baseline (the tracker keeps no history). */
|
|
35
|
+
reset() {
|
|
36
|
+
this.latestUsage = zeroUsage();
|
|
37
|
+
this.totalUsage = zeroUsage();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Factory form (ARCHITECTURE.md §6.1 `createXxx` convention). */
|
|
41
|
+
export function createUsageTracker() {
|
|
42
|
+
return new UsageTracker();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* `cache_read / prompt_tokens`, clamped to [0,1] and rounded to 4 decimals;
|
|
46
|
+
* 0 when the denominator is 0 (no usage recorded yet). `cache_hit_ratio`.
|
|
47
|
+
*/
|
|
48
|
+
export function cacheHitRatioRounded(u) {
|
|
49
|
+
if (u.prompt_tokens === 0)
|
|
50
|
+
return 0;
|
|
51
|
+
return round4(clamp01(u.cache_read / u.prompt_tokens));
|
|
52
|
+
}
|
|
53
|
+
/** One usage block: raw counters plus the derived cache-hit ratio. */
|
|
54
|
+
export function usageBlock(u) {
|
|
55
|
+
return {
|
|
56
|
+
prompt_tokens: u.prompt_tokens,
|
|
57
|
+
completion_tokens: u.completion_tokens,
|
|
58
|
+
total_tokens: u.total_tokens,
|
|
59
|
+
cache_read: u.cache_read,
|
|
60
|
+
cache_hit_ratio: cacheHitRatioRounded(u),
|
|
61
|
+
reasoning_tokens: u.reasoning_tokens,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The statusline `usage` field: latest block + the same shape under `total`.
|
|
66
|
+
*
|
|
67
|
+
* W755 GUARDRAIL — `total` is a BILLING-shaped sum, NOT an occupancy reading.
|
|
68
|
+
* Every step of a turn re-sends the whole prompt, so `total.prompt_tokens` is
|
|
69
|
+
* O(steps x prompt): the DSH host's own ledger shows the same shape (13.7M
|
|
70
|
+
* uncached input / 1.09G cache-read over one long session) and it is several
|
|
71
|
+
* orders of magnitude away from the context window. Context occupancy is
|
|
72
|
+
* `context_usage` and ONLY `context_usage`
|
|
73
|
+
* (`packages/runtime/src/status.ts::contextUsage`, which uses `latest()` plus
|
|
74
|
+
* the visible growth since that sample). Never divide `total.prompt_tokens`,
|
|
75
|
+
* `total.cache_read` or `total.total_tokens` by a context window, and never
|
|
76
|
+
* render a context meter from this block.
|
|
77
|
+
*/
|
|
78
|
+
export function usageStatus(u) {
|
|
79
|
+
return { ...usageBlock(u.latest()), total: usageBlock(u.total()) };
|
|
80
|
+
}
|
|
81
|
+
function clamp01(v) {
|
|
82
|
+
if (!Number.isFinite(v) || v < 0)
|
|
83
|
+
return 0;
|
|
84
|
+
return v > 1 ? 1 : v;
|
|
85
|
+
}
|
|
86
|
+
function round4(v) {
|
|
87
|
+
return Math.round(v * 10_000) / 10_000;
|
|
88
|
+
}
|