@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/inbox.d.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionInbox — the per-session delivery queue with TWO LANES (W513 + W515 §1).
|
|
3
|
+
*
|
|
4
|
+
* One inbox per session runtime instance. It is the ONE place a message can be
|
|
5
|
+
* delivered into a session, and the lane it lands in decides WHEN it is seen:
|
|
6
|
+
*
|
|
7
|
+
* - `next-turn` — drained by the turn driver at the TURN START, before the
|
|
8
|
+
* turn's own input (a follow-up sent while the session is idle; a receipt
|
|
9
|
+
* that arrived while nothing was running). Reported as `placement: "queued"`.
|
|
10
|
+
* - `next-step` — drained by the loop at EVERY STEP BOUNDARY, right before the
|
|
11
|
+
* next model call (a user interjection sent while the turn is running; a
|
|
12
|
+
* receipt that arrives mid-turn). Reported as `placement: "steering"`.
|
|
13
|
+
*
|
|
14
|
+
* Invariants:
|
|
15
|
+
* - a message with an `id` already accepted is DROPPED as a duplicate (a
|
|
16
|
+
* receipt delivered twice is injected once);
|
|
17
|
+
* - while `next-step` is non-empty the turn must not reach its terminal state
|
|
18
|
+
* (enforced by the loop, see `@celestea/agent-loop`);
|
|
19
|
+
* - both transport paths (a host API call and a session-mailbox receipt) end
|
|
20
|
+
* in the same lanes, so the injection mechanism is literally the same one.
|
|
21
|
+
*
|
|
22
|
+
* The two hooks (`onQueued`, `onDelivered`) let the host publish the placement
|
|
23
|
+
* over SSE without the inbox knowing anything about a transport.
|
|
24
|
+
*
|
|
25
|
+
* E §1.3 P1 ①: the lanes and the dedup ledger are PERSISTABLE. `bindPersistence`
|
|
26
|
+
* attaches a sink (the session's `checkpoint.json`, see `inbox-checkpoint.ts`);
|
|
27
|
+
* binding RESTORES whatever the previous process left queued and then writes on
|
|
28
|
+
* every change, so "accepted but not yet injected" survives a crash — the
|
|
29
|
+
* G1-4 failure mode, where a user's message vanished silently.
|
|
30
|
+
*
|
|
31
|
+
* Restoration is DELIBERATELY silent (no `onQueued` hook): a message queued by a
|
|
32
|
+
* process that died was already announced by that process, and re-announcing it
|
|
33
|
+
* would show the client a placement that did not change.
|
|
34
|
+
*/
|
|
35
|
+
import type { DeliverySource, InjectionKind, InjectionLane, InjectionPlacement } from "@celestea/core";
|
|
36
|
+
/** One message waiting to be appended to the session log. */
|
|
37
|
+
export interface InjectedMessage {
|
|
38
|
+
text: string;
|
|
39
|
+
/** Attribution label (`""` = the user, otherwise `[from <label>] text`). */
|
|
40
|
+
from: string;
|
|
41
|
+
/** Arrival time (diagnostics / tests). */
|
|
42
|
+
at: number;
|
|
43
|
+
/** Lane the message waits in. */
|
|
44
|
+
lane: InjectionLane;
|
|
45
|
+
kind: InjectionKind;
|
|
46
|
+
/** Idempotency key (`""` = never deduplicated). */
|
|
47
|
+
id: string;
|
|
48
|
+
/** Envelope, so a settlement notice is not mistaken for a deliberate relay. */
|
|
49
|
+
source: DeliverySource;
|
|
50
|
+
/** True when this push was dropped because the id was already accepted. */
|
|
51
|
+
duplicate: boolean;
|
|
52
|
+
}
|
|
53
|
+
export interface InboxHooks {
|
|
54
|
+
/** A message was accepted into a lane (placement `queued` / `steering`). */
|
|
55
|
+
onQueued?: (message: InjectedMessage, placement: InjectionPlacement) => void;
|
|
56
|
+
/** A message left a lane and is now part of the model-visible log. */
|
|
57
|
+
onDelivered?: (message: InjectedMessage) => void;
|
|
58
|
+
}
|
|
59
|
+
export interface SessionInbox {
|
|
60
|
+
/** Move one message into `lane`; a duplicate id is dropped (`duplicate: true`). */
|
|
61
|
+
push(text: string, lane: InjectionLane, opts?: InboxPushOptions): InjectedMessage;
|
|
62
|
+
/** Take everything waiting in one lane, in arrival order. */
|
|
63
|
+
drain(lane: InjectionLane): InjectedMessage[];
|
|
64
|
+
/** Messages waiting (one lane, or both). */
|
|
65
|
+
pending(lane?: InjectionLane): number;
|
|
66
|
+
/** E §1.3 P1 ①: restore from, and persist every change to, `sink`. */
|
|
67
|
+
bindPersistence(sink: InboxSink): void;
|
|
68
|
+
/** The persistable state: both lanes plus the bounded delivered-id ledger. */
|
|
69
|
+
snapshot(): InboxSnapshot;
|
|
70
|
+
}
|
|
71
|
+
/** Both lanes plus the accepted-id memory, exactly as they persist. */
|
|
72
|
+
export interface InboxSnapshot {
|
|
73
|
+
next_turn: InjectedMessage[];
|
|
74
|
+
next_step: InjectedMessage[];
|
|
75
|
+
delivered_ids: string[];
|
|
76
|
+
}
|
|
77
|
+
/** Where a snapshot goes (the session's checkpoint sidecar in production). */
|
|
78
|
+
export interface InboxSink {
|
|
79
|
+
/** The snapshot of the PREVIOUS process, or null when there is none usable. */
|
|
80
|
+
load(): InboxSnapshot | null;
|
|
81
|
+
save(snapshot: InboxSnapshot): void;
|
|
82
|
+
}
|
|
83
|
+
export interface InboxPushOptions {
|
|
84
|
+
from?: string;
|
|
85
|
+
id?: string;
|
|
86
|
+
kind?: InjectionKind;
|
|
87
|
+
source?: DeliverySource;
|
|
88
|
+
}
|
|
89
|
+
/** How many ids are remembered for duplicate detection (bounded memory). */
|
|
90
|
+
export declare const DELIVERED_ID_MEMORY = 512;
|
|
91
|
+
/** Placement a lane implies, before the drain: queued vs steering. */
|
|
92
|
+
export declare function placementOfLane(lane: InjectionLane): InjectionPlacement;
|
|
93
|
+
/** Build one inbox; `now` and the placement hooks are injectable. */
|
|
94
|
+
export declare function createSessionInbox(now?: () => number, hooks?: InboxHooks): SessionInbox;
|
package/dist/inbox.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionInbox — the per-session delivery queue with TWO LANES (W513 + W515 §1).
|
|
3
|
+
*
|
|
4
|
+
* One inbox per session runtime instance. It is the ONE place a message can be
|
|
5
|
+
* delivered into a session, and the lane it lands in decides WHEN it is seen:
|
|
6
|
+
*
|
|
7
|
+
* - `next-turn` — drained by the turn driver at the TURN START, before the
|
|
8
|
+
* turn's own input (a follow-up sent while the session is idle; a receipt
|
|
9
|
+
* that arrived while nothing was running). Reported as `placement: "queued"`.
|
|
10
|
+
* - `next-step` — drained by the loop at EVERY STEP BOUNDARY, right before the
|
|
11
|
+
* next model call (a user interjection sent while the turn is running; a
|
|
12
|
+
* receipt that arrives mid-turn). Reported as `placement: "steering"`.
|
|
13
|
+
*
|
|
14
|
+
* Invariants:
|
|
15
|
+
* - a message with an `id` already accepted is DROPPED as a duplicate (a
|
|
16
|
+
* receipt delivered twice is injected once);
|
|
17
|
+
* - while `next-step` is non-empty the turn must not reach its terminal state
|
|
18
|
+
* (enforced by the loop, see `@celestea/agent-loop`);
|
|
19
|
+
* - both transport paths (a host API call and a session-mailbox receipt) end
|
|
20
|
+
* in the same lanes, so the injection mechanism is literally the same one.
|
|
21
|
+
*
|
|
22
|
+
* The two hooks (`onQueued`, `onDelivered`) let the host publish the placement
|
|
23
|
+
* over SSE without the inbox knowing anything about a transport.
|
|
24
|
+
*
|
|
25
|
+
* E §1.3 P1 ①: the lanes and the dedup ledger are PERSISTABLE. `bindPersistence`
|
|
26
|
+
* attaches a sink (the session's `checkpoint.json`, see `inbox-checkpoint.ts`);
|
|
27
|
+
* binding RESTORES whatever the previous process left queued and then writes on
|
|
28
|
+
* every change, so "accepted but not yet injected" survives a crash — the
|
|
29
|
+
* G1-4 failure mode, where a user's message vanished silently.
|
|
30
|
+
*
|
|
31
|
+
* Restoration is DELIBERATELY silent (no `onQueued` hook): a message queued by a
|
|
32
|
+
* process that died was already announced by that process, and re-announcing it
|
|
33
|
+
* would show the client a placement that did not change.
|
|
34
|
+
*/
|
|
35
|
+
/** How many ids are remembered for duplicate detection (bounded memory). */
|
|
36
|
+
export const DELIVERED_ID_MEMORY = 512;
|
|
37
|
+
/** Placement a lane implies, before the drain: queued vs steering. */
|
|
38
|
+
export function placementOfLane(lane) {
|
|
39
|
+
return lane === "next-step" ? "steering" : "queued";
|
|
40
|
+
}
|
|
41
|
+
/** Build one inbox; `now` and the placement hooks are injectable. */
|
|
42
|
+
export function createSessionInbox(now = Date.now, hooks = {}) {
|
|
43
|
+
const lanes = { "next-turn": [], "next-step": [] };
|
|
44
|
+
const seen = [];
|
|
45
|
+
const seenSet = new Set();
|
|
46
|
+
let sink = null;
|
|
47
|
+
/** Bounded FIFO memory of accepted ids (oldest evicted first). */
|
|
48
|
+
const remember = (id) => {
|
|
49
|
+
if (id === "" || !seenSet.has(id)) {
|
|
50
|
+
if (id !== "") {
|
|
51
|
+
seenSet.add(id);
|
|
52
|
+
seen.push(id);
|
|
53
|
+
if (seen.length > DELIVERED_ID_MEMORY) {
|
|
54
|
+
const oldest = seen.shift();
|
|
55
|
+
if (oldest !== undefined)
|
|
56
|
+
seenSet.delete(oldest);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
};
|
|
63
|
+
const snapshot = () => ({
|
|
64
|
+
next_turn: lanes["next-turn"].map((m) => ({ ...m })),
|
|
65
|
+
next_step: lanes["next-step"].map((m) => ({ ...m })),
|
|
66
|
+
delivered_ids: [...seen],
|
|
67
|
+
});
|
|
68
|
+
const save = () => {
|
|
69
|
+
try {
|
|
70
|
+
sink?.save(snapshot());
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
// Persistence is observation: a sidecar that cannot be written must never
|
|
74
|
+
// lose the message that is still safely in memory (the checkpoint's rule).
|
|
75
|
+
process.stderr.write(`[celestea-runtime] inbox not persisted: ${String(e)}\n`);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const restore = (state) => {
|
|
79
|
+
for (const message of state.next_turn)
|
|
80
|
+
lanes["next-turn"].push(message);
|
|
81
|
+
for (const message of state.next_step)
|
|
82
|
+
lanes["next-step"].push(message);
|
|
83
|
+
// A restored message was ALREADY accepted once: re-remembering its id keeps
|
|
84
|
+
// the duplicate rule valid across the restart (and across processes).
|
|
85
|
+
for (const message of [...lanes["next-turn"], ...lanes["next-step"]])
|
|
86
|
+
remember(message.id);
|
|
87
|
+
for (const id of state.delivered_ids)
|
|
88
|
+
remember(id);
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
bindPersistence(next) {
|
|
92
|
+
sink = next;
|
|
93
|
+
// Fail-safe on BOTH sides: an unreadable queue is an EMPTY queue (the same
|
|
94
|
+
// discipline the recovery decision table uses) — a broken sidecar must
|
|
95
|
+
// never stop a session from composing.
|
|
96
|
+
try {
|
|
97
|
+
const state = next.load();
|
|
98
|
+
if (state !== null)
|
|
99
|
+
restore(state);
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
process.stderr.write(`[celestea-runtime] inbox not restored: ${String(e)}\n`);
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
snapshot,
|
|
106
|
+
push(text, lane, opts = {}) {
|
|
107
|
+
const id = opts.id ?? "";
|
|
108
|
+
const message = {
|
|
109
|
+
text,
|
|
110
|
+
from: opts.from ?? "",
|
|
111
|
+
at: now(),
|
|
112
|
+
lane,
|
|
113
|
+
kind: opts.kind ?? "user",
|
|
114
|
+
id,
|
|
115
|
+
source: opts.source ?? { kind: "user", form: "message" },
|
|
116
|
+
duplicate: remember(id),
|
|
117
|
+
};
|
|
118
|
+
if (!message.duplicate) {
|
|
119
|
+
lanes[lane].push(message);
|
|
120
|
+
save();
|
|
121
|
+
hooks.onQueued?.(message, placementOfLane(lane));
|
|
122
|
+
}
|
|
123
|
+
return message;
|
|
124
|
+
},
|
|
125
|
+
drain(lane) {
|
|
126
|
+
const taken = lanes[lane].splice(0, lanes[lane].length);
|
|
127
|
+
if (taken.length > 0)
|
|
128
|
+
save();
|
|
129
|
+
for (const message of taken)
|
|
130
|
+
hooks.onDelivered?.(message);
|
|
131
|
+
return taken;
|
|
132
|
+
},
|
|
133
|
+
pending(lane) {
|
|
134
|
+
if (lane !== undefined)
|
|
135
|
+
return lanes[lane].length;
|
|
136
|
+
return lanes["next-turn"].length + lanes["next-step"].length;
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@celestea/runtime` — the assembly layer (L2): `compose(profile)` mounts every
|
|
3
|
+
* plugin into one `Context` and hands back a wired, runnable engine generation.
|
|
4
|
+
*
|
|
5
|
+
* Responsibility: assembly (plugin mount order, seam resolution, sanitized
|
|
6
|
+
* config), lifecycle (hot swap, session rebind, idempotent shutdown, explicit
|
|
7
|
+
* release), turn driving (single concurrency slot, frame stream,
|
|
8
|
+
* cancellation, terminal state from the log), and the statusline
|
|
9
|
+
* (`StatusTracker` + `UsageTracker`).
|
|
10
|
+
*
|
|
11
|
+
* Dependency direction: runtime -> core + L1 packages. Assembly stays
|
|
12
|
+
* injection-based (the concrete agent loop arrives through
|
|
13
|
+
* `ComposeConfig.loopFactory`, the frame mapper through `frameMapper`, the
|
|
14
|
+
* session log through `sessionBinding`/plugins, and the worker registry either
|
|
15
|
+
* through a host plugin or the built-in worker wiring); the two L1 edges this
|
|
16
|
+
* package does import are the worker seam (`@celestea/workers`) and the
|
|
17
|
+
* session-log/checkpoint contract (`@celestea/session`, E §1.3 P0).
|
|
18
|
+
*
|
|
19
|
+
* Module map (legacy -> TS):
|
|
20
|
+
* profile.ts frozen 12-key profile + compose step list (runtime/config.rs)
|
|
21
|
+
* agent-config.ts profile -> AgentConfig + step floor (compose.rs:196-204)
|
|
22
|
+
* sanitize.ts sanitized config projection for /api/config
|
|
23
|
+
* usage.ts UsageTracker (latest/total/cache_hit_ratio) (agent-loop/loop.rs)
|
|
24
|
+
* status.ts StatusTracker + statusline payload (studio/main.rs:253-620)
|
|
25
|
+
* frames.ts LoopEvent -> SSE frame mapping (studio/main.rs:667-713)
|
|
26
|
+
* session-binding.ts session id/dir + log opener + rebind (compose.rs:131-146)
|
|
27
|
+
* turn-runner.ts one turn: busy slot, sink, cancel, outcome (runtime/run.rs)
|
|
28
|
+
* inbox.ts per-session mid-turn injection queue (W513)
|
|
29
|
+
* inbox-checkpoint.ts W787: queue + delivered ids -> checkpoint.json (§1.3 P1)
|
|
30
|
+
* session-registry.ts session id -> independent Runtime (W513)
|
|
31
|
+
* worker-wiring.ts worker driver seams + host receipt drain (compose.rs:148-193)
|
|
32
|
+
* watchdog-mount.ts W740: mount the liveness watchdog (workers/watchdog.ts)
|
|
33
|
+
* runtime.ts Runtime handles + lifecycle (compose.rs:44-281)
|
|
34
|
+
* recovery.ts boot recovery: close a turn a crash left open (E §1.3)
|
|
35
|
+
* gen.ts Gen + GenerationHub (hot swap) (studio/main.rs:340-420)
|
|
36
|
+
* compose.ts compose(config) — mount order (runtime/compose.rs)
|
|
37
|
+
* host/ W747: the ENGINE-side host assembly (log/binding +
|
|
38
|
+
* provider targeting); see host/index.ts (apps/studio/src/runtime)
|
|
39
|
+
* compact/ context compaction (W259) (studio/src/compact.rs)
|
|
40
|
+
* tokens.ts runtime service tokens
|
|
41
|
+
* errors.ts TurnBusyError / RuntimeReleasedError / ComposeError
|
|
42
|
+
*
|
|
43
|
+
* Public API = this file. Everything else is an internal module.
|
|
44
|
+
*/
|
|
45
|
+
export * from "./profile.js";
|
|
46
|
+
export * from "./tokens.js";
|
|
47
|
+
export * from "./errors.js";
|
|
48
|
+
export * from "./agent-config.js";
|
|
49
|
+
export * from "./sanitize.js";
|
|
50
|
+
export * from "./usage.js";
|
|
51
|
+
export * from "./pricing.js";
|
|
52
|
+
export * from "./ledger.js";
|
|
53
|
+
export * from "./ledger-llm.js";
|
|
54
|
+
export * from "./ledger-query.js";
|
|
55
|
+
export * from "./status.js";
|
|
56
|
+
export * from "./frames.js";
|
|
57
|
+
export * from "./session-binding.js";
|
|
58
|
+
export * from "./retention.js";
|
|
59
|
+
export * from "./turn-runner.js";
|
|
60
|
+
export * from "./inbox.js";
|
|
61
|
+
export * from "./inbox-checkpoint.js";
|
|
62
|
+
export * from "./session-registry.js";
|
|
63
|
+
export * from "./recovery.js";
|
|
64
|
+
export * from "./worker-wiring.js";
|
|
65
|
+
export * from "./watchdog-mount.js";
|
|
66
|
+
export * from "./runtime.js";
|
|
67
|
+
export * from "./gen.js";
|
|
68
|
+
export * from "./autowake.js";
|
|
69
|
+
export * from "./compose.js";
|
|
70
|
+
export * from "./host/index.js";
|
|
71
|
+
export * from "./compact/index.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@celestea/runtime` — the assembly layer (L2): `compose(profile)` mounts every
|
|
3
|
+
* plugin into one `Context` and hands back a wired, runnable engine generation.
|
|
4
|
+
*
|
|
5
|
+
* Responsibility: assembly (plugin mount order, seam resolution, sanitized
|
|
6
|
+
* config), lifecycle (hot swap, session rebind, idempotent shutdown, explicit
|
|
7
|
+
* release), turn driving (single concurrency slot, frame stream,
|
|
8
|
+
* cancellation, terminal state from the log), and the statusline
|
|
9
|
+
* (`StatusTracker` + `UsageTracker`).
|
|
10
|
+
*
|
|
11
|
+
* Dependency direction: runtime -> core + L1 packages. Assembly stays
|
|
12
|
+
* injection-based (the concrete agent loop arrives through
|
|
13
|
+
* `ComposeConfig.loopFactory`, the frame mapper through `frameMapper`, the
|
|
14
|
+
* session log through `sessionBinding`/plugins, and the worker registry either
|
|
15
|
+
* through a host plugin or the built-in worker wiring); the two L1 edges this
|
|
16
|
+
* package does import are the worker seam (`@celestea/workers`) and the
|
|
17
|
+
* session-log/checkpoint contract (`@celestea/session`, E §1.3 P0).
|
|
18
|
+
*
|
|
19
|
+
* Module map (legacy -> TS):
|
|
20
|
+
* profile.ts frozen 12-key profile + compose step list (runtime/config.rs)
|
|
21
|
+
* agent-config.ts profile -> AgentConfig + step floor (compose.rs:196-204)
|
|
22
|
+
* sanitize.ts sanitized config projection for /api/config
|
|
23
|
+
* usage.ts UsageTracker (latest/total/cache_hit_ratio) (agent-loop/loop.rs)
|
|
24
|
+
* status.ts StatusTracker + statusline payload (studio/main.rs:253-620)
|
|
25
|
+
* frames.ts LoopEvent -> SSE frame mapping (studio/main.rs:667-713)
|
|
26
|
+
* session-binding.ts session id/dir + log opener + rebind (compose.rs:131-146)
|
|
27
|
+
* turn-runner.ts one turn: busy slot, sink, cancel, outcome (runtime/run.rs)
|
|
28
|
+
* inbox.ts per-session mid-turn injection queue (W513)
|
|
29
|
+
* inbox-checkpoint.ts W787: queue + delivered ids -> checkpoint.json (§1.3 P1)
|
|
30
|
+
* session-registry.ts session id -> independent Runtime (W513)
|
|
31
|
+
* worker-wiring.ts worker driver seams + host receipt drain (compose.rs:148-193)
|
|
32
|
+
* watchdog-mount.ts W740: mount the liveness watchdog (workers/watchdog.ts)
|
|
33
|
+
* runtime.ts Runtime handles + lifecycle (compose.rs:44-281)
|
|
34
|
+
* recovery.ts boot recovery: close a turn a crash left open (E §1.3)
|
|
35
|
+
* gen.ts Gen + GenerationHub (hot swap) (studio/main.rs:340-420)
|
|
36
|
+
* compose.ts compose(config) — mount order (runtime/compose.rs)
|
|
37
|
+
* host/ W747: the ENGINE-side host assembly (log/binding +
|
|
38
|
+
* provider targeting); see host/index.ts (apps/studio/src/runtime)
|
|
39
|
+
* compact/ context compaction (W259) (studio/src/compact.rs)
|
|
40
|
+
* tokens.ts runtime service tokens
|
|
41
|
+
* errors.ts TurnBusyError / RuntimeReleasedError / ComposeError
|
|
42
|
+
*
|
|
43
|
+
* Public API = this file. Everything else is an internal module.
|
|
44
|
+
*/
|
|
45
|
+
export * from "./profile.js";
|
|
46
|
+
export * from "./tokens.js";
|
|
47
|
+
export * from "./errors.js";
|
|
48
|
+
export * from "./agent-config.js";
|
|
49
|
+
export * from "./sanitize.js";
|
|
50
|
+
export * from "./usage.js";
|
|
51
|
+
export * from "./pricing.js";
|
|
52
|
+
export * from "./ledger.js";
|
|
53
|
+
export * from "./ledger-llm.js";
|
|
54
|
+
export * from "./ledger-query.js";
|
|
55
|
+
export * from "./status.js";
|
|
56
|
+
export * from "./frames.js";
|
|
57
|
+
export * from "./session-binding.js";
|
|
58
|
+
export * from "./retention.js";
|
|
59
|
+
export * from "./turn-runner.js";
|
|
60
|
+
export * from "./inbox.js";
|
|
61
|
+
export * from "./inbox-checkpoint.js";
|
|
62
|
+
export * from "./session-registry.js";
|
|
63
|
+
export * from "./recovery.js";
|
|
64
|
+
export * from "./worker-wiring.js";
|
|
65
|
+
export * from "./watchdog-mount.js";
|
|
66
|
+
export * from "./runtime.js";
|
|
67
|
+
export * from "./gen.js";
|
|
68
|
+
export * from "./autowake.js";
|
|
69
|
+
export * from "./compose.js";
|
|
70
|
+
export * from "./host/index.js";
|
|
71
|
+
export * from "./compact/index.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal helpers of the usage ledger (W836 R3 batch F), split out so
|
|
3
|
+
* `ledger.ts` stays inside the repository file budget:
|
|
4
|
+
* - [readLedgerRecords] parses ONE ledger file (current or rolled `.1`);
|
|
5
|
+
* - [LedgerKeySet] is the BOUNDED idempotency-key memory (P2-5): keys are
|
|
6
|
+
* evicted per closed turn, with a size cap as the backstop.
|
|
7
|
+
*/
|
|
8
|
+
/** Every readable record of one ledger path (an unparsable line is skipped). */
|
|
9
|
+
export declare function readLedgerRecords<T>(path: string): T[];
|
|
10
|
+
/** Bounded in-process idempotency keys: per-turn eviction + a size cap. */
|
|
11
|
+
export declare class LedgerKeySet {
|
|
12
|
+
private readonly max;
|
|
13
|
+
private readonly keys;
|
|
14
|
+
constructor(max: number);
|
|
15
|
+
/** True when this writer already booked the key (the row must be skipped). */
|
|
16
|
+
has(key: string): boolean;
|
|
17
|
+
/** Size of the memory (bounded-memory diagnostics / tests). */
|
|
18
|
+
get size(): number;
|
|
19
|
+
/** Remember one booked key, trimming the OLDEST keys past the cap. */
|
|
20
|
+
add(key: string): void;
|
|
21
|
+
/**
|
|
22
|
+
* Drop the keys of ONE closed turn: an in-flight turn keeps its keys, so a
|
|
23
|
+
* duplicate step booked while it is still open is still refused. A `null`
|
|
24
|
+
* turn id (an out-of-turn row) is left to the size cap.
|
|
25
|
+
*/
|
|
26
|
+
evictTurn(session: string, turnId: string | null): void;
|
|
27
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal helpers of the usage ledger (W836 R3 batch F), split out so
|
|
3
|
+
* `ledger.ts` stays inside the repository file budget:
|
|
4
|
+
* - [readLedgerRecords] parses ONE ledger file (current or rolled `.1`);
|
|
5
|
+
* - [LedgerKeySet] is the BOUNDED idempotency-key memory (P2-5): keys are
|
|
6
|
+
* evicted per closed turn, with a size cap as the backstop.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
/** Every readable record of one ledger path (an unparsable line is skipped). */
|
|
10
|
+
export function readLedgerRecords(path) {
|
|
11
|
+
let raw;
|
|
12
|
+
try {
|
|
13
|
+
raw = readFileSync(path, "utf8");
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
const out = [];
|
|
19
|
+
for (const line of raw.split("\n")) {
|
|
20
|
+
if (line.trim() === "")
|
|
21
|
+
continue;
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(line);
|
|
24
|
+
if (typeof parsed === "object" && parsed !== null)
|
|
25
|
+
out.push(parsed);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// A torn/foreign line never hides the rows around it.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
/** Bounded in-process idempotency keys: per-turn eviction + a size cap. */
|
|
34
|
+
export class LedgerKeySet {
|
|
35
|
+
max;
|
|
36
|
+
keys = new Set();
|
|
37
|
+
constructor(max) {
|
|
38
|
+
this.max = max;
|
|
39
|
+
}
|
|
40
|
+
/** True when this writer already booked the key (the row must be skipped). */
|
|
41
|
+
has(key) {
|
|
42
|
+
return this.keys.has(key);
|
|
43
|
+
}
|
|
44
|
+
/** Size of the memory (bounded-memory diagnostics / tests). */
|
|
45
|
+
get size() {
|
|
46
|
+
return this.keys.size;
|
|
47
|
+
}
|
|
48
|
+
/** Remember one booked key, trimming the OLDEST keys past the cap. */
|
|
49
|
+
add(key) {
|
|
50
|
+
this.keys.add(key);
|
|
51
|
+
if (this.keys.size <= this.max)
|
|
52
|
+
return;
|
|
53
|
+
let excess = this.keys.size - this.max;
|
|
54
|
+
for (const key2 of this.keys) {
|
|
55
|
+
if (excess <= 0)
|
|
56
|
+
break;
|
|
57
|
+
this.keys.delete(key2);
|
|
58
|
+
excess -= 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Drop the keys of ONE closed turn: an in-flight turn keeps its keys, so a
|
|
63
|
+
* duplicate step booked while it is still open is still refused. A `null`
|
|
64
|
+
* turn id (an out-of-turn row) is left to the size cap.
|
|
65
|
+
*/
|
|
66
|
+
evictTurn(session, turnId) {
|
|
67
|
+
if (turnId === null)
|
|
68
|
+
return;
|
|
69
|
+
const prefix = `${session}|${turnId}|`;
|
|
70
|
+
for (const key of this.keys)
|
|
71
|
+
if (key.startsWith(prefix))
|
|
72
|
+
this.keys.delete(key);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `Llm` step observer (iteration E §3.2.3, W728 P0) — one ledger step per
|
|
3
|
+
* model call, including the calls that FAIL.
|
|
4
|
+
*
|
|
5
|
+
* A decorator, not a new seam: `generate` and every stream event pass through
|
|
6
|
+
* byte-for-byte unchanged, and a rejection is re-thrown untouched. What it adds
|
|
7
|
+
* is the two facts only this seam can see:
|
|
8
|
+
* - the step BOUNDARIES (a stream that tears after a usage frame is one error
|
|
9
|
+
* row, not an ok row plus an error row, §3.2.3);
|
|
10
|
+
* - the structured failure cause — W723's `LlmError.httpStatus/retryable`
|
|
11
|
+
* (`packages/llm/src/errors.ts`), read structurally so this package keeps
|
|
12
|
+
* importing nothing from `@celestea/llm` (runtime composes L1, it does not
|
|
13
|
+
* depend on it).
|
|
14
|
+
*
|
|
15
|
+
* This is observation only: failures and retries book a row, they never change
|
|
16
|
+
* what the engine does with the response (§3.5, "纯新增观测").
|
|
17
|
+
*/
|
|
18
|
+
import type { Llm } from "@celestea/core";
|
|
19
|
+
import type { LedgerStepOutcome, LedgerStepSink } from "./ledger.js";
|
|
20
|
+
/** What the observer knows about the target it wraps (config, not per-call). */
|
|
21
|
+
export interface LedgerLlmOptions {
|
|
22
|
+
inner: Llm;
|
|
23
|
+
sink: LedgerStepSink;
|
|
24
|
+
/** Provider row id (`providers.json`), when the host resolved one. */
|
|
25
|
+
provider?: string | null;
|
|
26
|
+
/** Model of the composed profile; the request's own model wins per call. */
|
|
27
|
+
model?: string | null;
|
|
28
|
+
base_url_host?: string | null;
|
|
29
|
+
/** 0 = first attempt (§5.2); P1's fallback decorator passes the real value. */
|
|
30
|
+
attempt?: number;
|
|
31
|
+
/** Target name this attempt failed over from (P1); null in P0. */
|
|
32
|
+
fallback_from?: string | null;
|
|
33
|
+
}
|
|
34
|
+
/** Wrap one `Llm` so every model step is booked (success, failure and abort). */
|
|
35
|
+
export declare function createLedgerLlm(opts: LedgerLlmOptions): Llm;
|
|
36
|
+
/**
|
|
37
|
+
* The structured failure of a thrown value. W723's `LlmError` carries
|
|
38
|
+
* `kind`/`httpStatus`/`retryable`; anything else is an unclassified local
|
|
39
|
+
* failure, reported as the turn-outcome kind the loop will use ("generate").
|
|
40
|
+
*/
|
|
41
|
+
export declare function failureOf(error: unknown): LedgerStepOutcome;
|
|
42
|
+
/** A mid-stream terminal failure: no HTTP status exists (the response was 2xx). */
|
|
43
|
+
export declare function streamFailure(event: {
|
|
44
|
+
kindOf: string;
|
|
45
|
+
message: string;
|
|
46
|
+
}): LedgerStepOutcome;
|
|
47
|
+
/** Host of a base_url (`api.deepseek.com`), or null when it is not a URL. */
|
|
48
|
+
export declare function hostOf(baseUrl: string | null): string | null;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `Llm` step observer (iteration E §3.2.3, W728 P0) — one ledger step per
|
|
3
|
+
* model call, including the calls that FAIL.
|
|
4
|
+
*
|
|
5
|
+
* A decorator, not a new seam: `generate` and every stream event pass through
|
|
6
|
+
* byte-for-byte unchanged, and a rejection is re-thrown untouched. What it adds
|
|
7
|
+
* is the two facts only this seam can see:
|
|
8
|
+
* - the step BOUNDARIES (a stream that tears after a usage frame is one error
|
|
9
|
+
* row, not an ok row plus an error row, §3.2.3);
|
|
10
|
+
* - the structured failure cause — W723's `LlmError.httpStatus/retryable`
|
|
11
|
+
* (`packages/llm/src/errors.ts`), read structurally so this package keeps
|
|
12
|
+
* importing nothing from `@celestea/llm` (runtime composes L1, it does not
|
|
13
|
+
* depend on it).
|
|
14
|
+
*
|
|
15
|
+
* This is observation only: failures and retries book a row, they never change
|
|
16
|
+
* what the engine does with the response (§3.5, "纯新增观测").
|
|
17
|
+
*/
|
|
18
|
+
/** Wrap one `Llm` so every model step is booked (success, failure and abort). */
|
|
19
|
+
export function createLedgerLlm(opts) {
|
|
20
|
+
return {
|
|
21
|
+
async generate(req) {
|
|
22
|
+
const step = opts.sink.beginStep({
|
|
23
|
+
provider: opts.provider ?? null,
|
|
24
|
+
model: req.model === "" ? (opts.model ?? null) : req.model,
|
|
25
|
+
base_url_host: opts.base_url_host ?? null,
|
|
26
|
+
attempt: opts.attempt ?? 0,
|
|
27
|
+
fallback_from: opts.fallback_from ?? null,
|
|
28
|
+
});
|
|
29
|
+
let stream;
|
|
30
|
+
try {
|
|
31
|
+
stream = await opts.inner.generate(req);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
step.close(failureOf(error));
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
return observeStep(stream, step);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** The stream of one step, forwarded unchanged and closed exactly once. */
|
|
42
|
+
function observeStep(stream, step) {
|
|
43
|
+
const inner = stream[Symbol.asyncIterator]();
|
|
44
|
+
const iterator = {
|
|
45
|
+
async next() {
|
|
46
|
+
let result;
|
|
47
|
+
try {
|
|
48
|
+
result = await inner.next();
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
step.close(failureOf(error));
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
if (result.done === true) {
|
|
55
|
+
step.close({ kind: "ok" });
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
const event = result.value;
|
|
59
|
+
if (event.kind === "usage")
|
|
60
|
+
step.record(event.usage);
|
|
61
|
+
else if (event.kind === "failed")
|
|
62
|
+
step.close(streamFailure(event));
|
|
63
|
+
else if (event.kind === "interrupted")
|
|
64
|
+
step.close({ kind: "error", error_kind: "stream" });
|
|
65
|
+
return result;
|
|
66
|
+
},
|
|
67
|
+
async return() {
|
|
68
|
+
// The consumer abandoned the step (cancel/close): whatever usage it
|
|
69
|
+
// already produced is real money and keeps its row (§3.2.3 "取消").
|
|
70
|
+
step.close({ kind: "ok" });
|
|
71
|
+
await inner.return?.();
|
|
72
|
+
return { done: true, value: undefined };
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
return { [Symbol.asyncIterator]: () => iterator };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The structured failure of a thrown value. W723's `LlmError` carries
|
|
79
|
+
* `kind`/`httpStatus`/`retryable`; anything else is an unclassified local
|
|
80
|
+
* failure, reported as the turn-outcome kind the loop will use ("generate").
|
|
81
|
+
*/
|
|
82
|
+
export function failureOf(error) {
|
|
83
|
+
const rec = typeof error === "object" && error !== null ? error : {};
|
|
84
|
+
const kind = rec["kind"];
|
|
85
|
+
const status = rec["httpStatus"];
|
|
86
|
+
const retryable = rec["retryable"];
|
|
87
|
+
return {
|
|
88
|
+
kind: "error",
|
|
89
|
+
error_kind: typeof kind === "string" ? kind : "generate",
|
|
90
|
+
http_status: typeof status === "number" ? status : null,
|
|
91
|
+
retryable: typeof retryable === "boolean" ? retryable : null,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** A mid-stream terminal failure: no HTTP status exists (the response was 2xx). */
|
|
95
|
+
export function streamFailure(event) {
|
|
96
|
+
return {
|
|
97
|
+
kind: "error",
|
|
98
|
+
error_kind: event.kindOf,
|
|
99
|
+
http_status: null,
|
|
100
|
+
// The canonical timeout prefix survives into the message (W511/W723), so an
|
|
101
|
+
// idle-stall is still recognisable as retryable.
|
|
102
|
+
retryable: event.message.startsWith("llm timeout") ? true : null,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** Host of a base_url (`api.deepseek.com`), or null when it is not a URL. */
|
|
106
|
+
export function hostOf(baseUrl) {
|
|
107
|
+
if (baseUrl === null || baseUrl === "")
|
|
108
|
+
return null;
|
|
109
|
+
try {
|
|
110
|
+
return new URL(baseUrl).host;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|