@celestea/core 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.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +95 -0
  3. package/contracts/data-files/checkpoint.schema.json +111 -0
  4. package/contracts/data-files/cli-main-jsonl-precompact.schema.json +27 -0
  5. package/contracts/data-files/cli-main-jsonl.schema.json +22 -0
  6. package/contracts/data-files/fallbacks.schema.json +71 -0
  7. package/contracts/data-files/index.json +124 -0
  8. package/contracts/data-files/pricing.schema.json +65 -0
  9. package/contracts/data-files/prompts.schema.json +130 -0
  10. package/contracts/data-files/providers.schema.json +177 -0
  11. package/contracts/data-files/registry-tsv.schema.json +74 -0
  12. package/contracts/data-files/session.schema.json +51 -0
  13. package/contracts/data-files/usage-ledger.schema.json +112 -0
  14. package/contracts/data-files/workspaces.schema.json +63 -0
  15. package/contracts/endpoints.json +4390 -0
  16. package/contracts/probe-evidence.json +219 -0
  17. package/contracts/route-table.snapshot.json +377 -0
  18. package/contracts/scope-hash-vectors.json +273 -0
  19. package/contracts/session-event.schema.json +441 -0
  20. package/contracts/sse-events.json +202 -0
  21. package/contracts/tools.json +730 -0
  22. package/dist/agent.d.ts +65 -0
  23. package/dist/agent.js +36 -0
  24. package/dist/celestea-home.d.ts +63 -0
  25. package/dist/celestea-home.js +96 -0
  26. package/dist/celestea-sources.d.ts +53 -0
  27. package/dist/celestea-sources.js +61 -0
  28. package/dist/context.d.ts +33 -0
  29. package/dist/context.js +55 -0
  30. package/dist/contracts/index.d.ts +234 -0
  31. package/dist/contracts/index.js +159 -0
  32. package/dist/errors.d.ts +16 -0
  33. package/dist/errors.js +22 -0
  34. package/dist/event-bus.d.ts +60 -0
  35. package/dist/event-bus.js +100 -0
  36. package/dist/index.d.ts +66 -0
  37. package/dist/index.js +66 -0
  38. package/dist/injection.d.ts +61 -0
  39. package/dist/injection.js +27 -0
  40. package/dist/json.d.ts +34 -0
  41. package/dist/json.js +127 -0
  42. package/dist/llm.d.ts +34 -0
  43. package/dist/llm.js +41 -0
  44. package/dist/memory.d.ts +72 -0
  45. package/dist/memory.js +123 -0
  46. package/dist/message.d.ts +189 -0
  47. package/dist/message.js +252 -0
  48. package/dist/plugin.d.ts +38 -0
  49. package/dist/plugin.js +49 -0
  50. package/dist/projection.d.ts +67 -0
  51. package/dist/projection.js +168 -0
  52. package/dist/question.d.ts +154 -0
  53. package/dist/question.js +82 -0
  54. package/dist/redact.d.ts +40 -0
  55. package/dist/redact.js +185 -0
  56. package/dist/repo.d.ts +14 -0
  57. package/dist/repo.js +87 -0
  58. package/dist/sandbox.d.ts +182 -0
  59. package/dist/sandbox.js +78 -0
  60. package/dist/session-event.d.ts +57 -0
  61. package/dist/session-event.js +425 -0
  62. package/dist/session-log.d.ts +71 -0
  63. package/dist/session-log.js +66 -0
  64. package/dist/skill-catalog.d.ts +29 -0
  65. package/dist/skill-catalog.js +52 -0
  66. package/dist/skills.d.ts +116 -0
  67. package/dist/skills.js +273 -0
  68. package/dist/sse-bus.d.ts +40 -0
  69. package/dist/sse-bus.js +105 -0
  70. package/dist/stream.d.ts +115 -0
  71. package/dist/stream.js +52 -0
  72. package/dist/tool-surface.d.ts +45 -0
  73. package/dist/tool-surface.js +98 -0
  74. package/dist/tool.d.ts +77 -0
  75. package/dist/tool.js +15 -0
  76. package/dist/turn-id.d.ts +37 -0
  77. package/dist/turn-id.js +76 -0
  78. package/dist/types.d.ts +396 -0
  79. package/dist/types.js +58 -0
  80. package/package.json +27 -0
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Machine-readable contract loader (P0; hardened in W807).
3
+ *
4
+ * Everything under contracts/ is frozen data; this module only reads and
5
+ * validates it. Counts are asserted here so a drifted contract fails loudly.
6
+ *
7
+ * W807 -- READ ONCE, THEN TRUST THE SNAPSHOT. The loader used to re-read every
8
+ * JSON file on every call while comparing it against counts baked in at module
9
+ * load time. A running process was therefore a contradiction waiting to happen:
10
+ * 2026-09-16 W804 bumped contracts/tools.json from 11 to 12 while production had
11
+ * 11 in memory, so every compose 500'd ("tools contract must hold 11 tools, got
12
+ * 12") until an operator restarted. A store now validates a contract file ONCE
13
+ * and caches it for the process lifetime, so a later disk edit cannot make a
14
+ * running process contradict itself. verifyContractsAtStartup() is the explicit
15
+ * boot gate: it refuses to start on a mismatch (naming file, expected and
16
+ * actual) instead of booting into a later 500.
17
+ */
18
+ import type { ToolSpec } from "../types.js";
19
+ export interface EndpointField {
20
+ name: string;
21
+ type: string;
22
+ required?: boolean;
23
+ /** Present only in some response variants (e.g. kept_turns when compacted). */
24
+ optional?: boolean;
25
+ note?: string;
26
+ }
27
+ export interface EndpointRequest {
28
+ kind: "none" | "json" | "query";
29
+ fields: EndpointField[];
30
+ note?: string;
31
+ }
32
+ export interface EndpointResponse {
33
+ status: number;
34
+ shape: string;
35
+ fields: EndpointField[];
36
+ contentType?: string;
37
+ publicView?: {
38
+ excluded: string[];
39
+ note: string;
40
+ };
41
+ }
42
+ export interface EndpointError {
43
+ status: number;
44
+ error: string;
45
+ note?: string;
46
+ }
47
+ export interface EndpointProbe {
48
+ checked: boolean;
49
+ mode?: string;
50
+ server?: string;
51
+ reason?: string;
52
+ [k: string]: unknown;
53
+ }
54
+ export interface EndpointContract {
55
+ id: string;
56
+ method: "GET" | "POST" | "DELETE" | "PUT";
57
+ path: string;
58
+ group: string;
59
+ rustHandler: string;
60
+ docRef: string;
61
+ request: EndpointRequest;
62
+ response: EndpointResponse;
63
+ errors: EndpointError[];
64
+ notes?: string[];
65
+ probe?: EndpointProbe;
66
+ }
67
+ export interface EndpointsContract {
68
+ title: string;
69
+ generatedAt: string;
70
+ source: Record<string, string>;
71
+ conventions: Record<string, string>;
72
+ errorCodes: Record<string, string>;
73
+ count: number;
74
+ endpoints: EndpointContract[];
75
+ }
76
+ export interface SseEventContract {
77
+ name: string;
78
+ payload: Record<string, string>;
79
+ source: string;
80
+ codeRef: string;
81
+ frontendListens: boolean;
82
+ note?: string;
83
+ }
84
+ export interface SseContract {
85
+ transport: {
86
+ contentType: string;
87
+ keepAlive: boolean;
88
+ busCapacity: number;
89
+ envelope: Record<string, string>;
90
+ frameFormat: string;
91
+ };
92
+ lagged: {
93
+ trigger: string;
94
+ event: string;
95
+ payload: Record<string, string>;
96
+ semantics: string;
97
+ codeRef: string;
98
+ };
99
+ count: number;
100
+ events: SseEventContract[];
101
+ }
102
+ export interface RouteSnapshotEntry {
103
+ method: string;
104
+ path: string;
105
+ rustHandler: string;
106
+ }
107
+ export interface RouteSnapshot {
108
+ routeDeclarations: number;
109
+ methodPathCombos: number;
110
+ apiEndpoints: number;
111
+ staticRoutes: RouteSnapshotEntry[];
112
+ routes: RouteSnapshotEntry[];
113
+ /**
114
+ * W516: routes that exist ONLY in the TypeScript backend (no legacy
115
+ * counterpart). The legacy extraction above stays intact; a contract endpoint
116
+ * must appear in routes or here.
117
+ */
118
+ tsOnlyRoutes?: RouteSnapshotEntry[];
119
+ tsApiEndpoints?: number;
120
+ tsMethodPathCombos?: number;
121
+ }
122
+ export interface ToolsContract {
123
+ count: number;
124
+ tools: Array<ToolSpec & {
125
+ sourceRef: string;
126
+ }>;
127
+ }
128
+ export interface DataFileEntry {
129
+ file: string;
130
+ schema: string;
131
+ version: string;
132
+ mode: string;
133
+ secret?: boolean;
134
+ /** W787: free-form ownership / path note (optional, purely documentary). */
135
+ note?: string;
136
+ }
137
+ export interface DataFilesIndex {
138
+ freezeRule: string;
139
+ files: DataFileEntry[];
140
+ durability: Record<string, string>;
141
+ roundTripRequirement: string;
142
+ /**
143
+ * Per-capability implementation notes (iteration E): which part of a design
144
+ * section is implemented and which is explicitly deferred. Optional, because
145
+ * a capability that added no data file has nothing to report here.
146
+ */
147
+ recovery?: {
148
+ implemented: string;
149
+ notImplemented: string;
150
+ };
151
+ /** W787 (capability 2): the worker table's own implementation split. */
152
+ workerRegistry?: {
153
+ implemented: string;
154
+ notImplemented: string;
155
+ };
156
+ /** W804 (multimodal P0): the per-session attachment object store. */
157
+ attachments?: {
158
+ location: string;
159
+ kind: string;
160
+ introduced: string;
161
+ lifecycle: string;
162
+ fixtures: string;
163
+ schema: string;
164
+ };
165
+ }
166
+ /**
167
+ * The frozen counts. These are the module-load-time constants a drifted
168
+ * contracts file used to contradict (W804). They are deliberately NOT derived
169
+ * from the files, and must never be edited to paper over a contract edit -- the
170
+ * file change is what gets reviewed and the counts only follow a deliberate
171
+ * freeze revision.
172
+ */
173
+ export declare const FROZEN_COUNTS: {
174
+ readonly endpoints: 64;
175
+ readonly sseEvents: 9;
176
+ readonly tools: 18;
177
+ };
178
+ /** One frozen-count divergence, with everything an operator needs to act. */
179
+ export interface ContractMismatch {
180
+ /** File name relative to the contracts directory (e.g. tools.json). */
181
+ file: string;
182
+ /** Absolute path of the file, so the operator can open it directly. */
183
+ path: string;
184
+ /** Which number diverged: the declared count field, or the array length. */
185
+ field: string;
186
+ expected: number;
187
+ actual: number;
188
+ }
189
+ /** Raised when a contract file contradicts a frozen count. */
190
+ export declare class ContractValidationError extends Error {
191
+ readonly mismatches: ContractMismatch[];
192
+ readonly dir: string;
193
+ constructor(dir: string, mismatches: ContractMismatch[]);
194
+ }
195
+ /**
196
+ * A contract store bound to one directory. Every value is read from disk at
197
+ * most once: the first (validated) read wins and is reused for the rest of the
198
+ * process lifetime. Tests point a store at a throwaway copy; the process-wide
199
+ * contractStore() points at the real repository.
200
+ */
201
+ export interface ContractStore {
202
+ readonly dir: string;
203
+ loadEndpoints(): EndpointsContract;
204
+ loadSse(): SseContract;
205
+ loadRouteSnapshot(): RouteSnapshot;
206
+ loadTools(): ToolsContract;
207
+ loadSessionEventSchema(): Record<string, unknown>;
208
+ loadDataFilesIndex(): DataFilesIndex;
209
+ loadDataFileSchema(name: string): Record<string, unknown>;
210
+ /** Validate the frozen files from disk NOW and prime the cache. Throws on drift. */
211
+ verifyAtStartup(): void;
212
+ }
213
+ export declare function createContractStore(dir: string): ContractStore;
214
+ /** The process-wide store: one validated snapshot, reused for the process lifetime. */
215
+ export declare function contractStore(): ContractStore;
216
+ export declare function loadEndpoints(): EndpointsContract;
217
+ export declare function loadSse(): SseContract;
218
+ export declare function loadRouteSnapshot(): RouteSnapshot;
219
+ export declare function loadTools(): ToolsContract;
220
+ export declare function loadSessionEventSchema(): Record<string, unknown>;
221
+ export declare function loadDataFilesIndex(): DataFilesIndex;
222
+ export declare function loadDataFileSchema(name: string): Record<string, unknown>;
223
+ /**
224
+ * The explicit startup gate (W807): Studio calls this once at boot, before it
225
+ * binds a port. It reads the frozen contract files from disk, throws a
226
+ * ContractValidationError naming file / expected / actual on any drift, and on
227
+ * success primes the cache with the exact snapshot that was validated -- so the
228
+ * running process stays internally consistent for its whole lifetime even if
229
+ * contracts/*.json changes underneath it.
230
+ *
231
+ * A drifted file is a REFUSAL TO START, not a warning: the previous behaviour
232
+ * was to boot and then 500 on the first request that touched the contract.
233
+ */
234
+ export declare function verifyContractsAtStartup(): void;
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Machine-readable contract loader (P0; hardened in W807).
3
+ *
4
+ * Everything under contracts/ is frozen data; this module only reads and
5
+ * validates it. Counts are asserted here so a drifted contract fails loudly.
6
+ *
7
+ * W807 -- READ ONCE, THEN TRUST THE SNAPSHOT. The loader used to re-read every
8
+ * JSON file on every call while comparing it against counts baked in at module
9
+ * load time. A running process was therefore a contradiction waiting to happen:
10
+ * 2026-09-16 W804 bumped contracts/tools.json from 11 to 12 while production had
11
+ * 11 in memory, so every compose 500'd ("tools contract must hold 11 tools, got
12
+ * 12") until an operator restarted. A store now validates a contract file ONCE
13
+ * and caches it for the process lifetime, so a later disk edit cannot make a
14
+ * running process contradict itself. verifyContractsAtStartup() is the explicit
15
+ * boot gate: it refuses to start on a mismatch (naming file, expected and
16
+ * actual) instead of booting into a later 500.
17
+ */
18
+ import { readFileSync } from "node:fs";
19
+ import { resolve } from "node:path";
20
+ import { contractsDir } from "../repo.js";
21
+ /**
22
+ * The frozen counts. These are the module-load-time constants a drifted
23
+ * contracts file used to contradict (W804). They are deliberately NOT derived
24
+ * from the files, and must never be edited to paper over a contract edit -- the
25
+ * file change is what gets reviewed and the counts only follow a deliberate
26
+ * freeze revision.
27
+ */
28
+ export const FROZEN_COUNTS = {
29
+ // W860: 57 -> 60 (GET|PUT /api/sessions/{id}/tools + GET /api/plugins).
30
+ // W870: 60 -> 61 (PUT /api/sessions/{id}/model, the session-scoped model switch).
31
+ // G5: 61 -> 62 (GET /api/fs/list, the Win-style file-manager listing);
32
+ // G2: 62 -> 63 (POST /api/exec, immediate shell execution without the model).
33
+ // G5 follow-up: 63 -> 64 (GET /api/fs/read, the file manager viewer).
34
+ endpoints: 64,
35
+ sseEvents: 9,
36
+ // W884: 13 -> 14 (`load_skill`, the on-demand half of skill progressive
37
+ // disclosure; the catalog half adds no tool).
38
+ // F4: 14 -> 16 (`browser_open` + `browser_act`, the session browser tools).
39
+ // B2 (F3 P1): 16 -> 18 (`remember` + `forget`, the workspace-memory write pair).
40
+ tools: 18,
41
+ };
42
+ /** Raised when a contract file contradicts a frozen count. */
43
+ export class ContractValidationError extends Error {
44
+ mismatches;
45
+ dir;
46
+ constructor(dir, mismatches) {
47
+ const detail = mismatches
48
+ .map((m) => m.file + " (" + m.field + "): expected " + m.expected + ", got " + m.actual + " [" + m.path + "]")
49
+ .join("; ");
50
+ super("frozen contract violated in " + dir + " -- refusing to start: " + detail);
51
+ this.name = "ContractValidationError";
52
+ this.dir = dir;
53
+ this.mismatches = mismatches;
54
+ }
55
+ }
56
+ /** Every frozen file is checked twice: its declared count and its array length. */
57
+ const FROZEN_CHECKS = [
58
+ { file: "endpoints.json", field: "count", expected: FROZEN_COUNTS.endpoints, measure: (d) => d.count },
59
+ { file: "endpoints.json", field: "endpoints[]", expected: FROZEN_COUNTS.endpoints, measure: (d) => d.endpoints.length },
60
+ { file: "sse-events.json", field: "count", expected: FROZEN_COUNTS.sseEvents, measure: (d) => d.count },
61
+ { file: "sse-events.json", field: "events[]", expected: FROZEN_COUNTS.sseEvents, measure: (d) => d.events.length },
62
+ { file: "tools.json", field: "count", expected: FROZEN_COUNTS.tools, measure: (d) => d.count },
63
+ { file: "tools.json", field: "tools[]", expected: FROZEN_COUNTS.tools, measure: (d) => d.tools.length },
64
+ ];
65
+ const FROZEN_FILES = [...new Set(FROZEN_CHECKS.map((c) => c.file))];
66
+ function checkFrozen(file, doc, dir) {
67
+ return FROZEN_CHECKS.filter((c) => c.file === file)
68
+ .map((c) => ({ file, path: resolve(dir, file), field: c.field, expected: c.expected, actual: c.measure(doc) }))
69
+ .filter((m) => m.actual !== m.expected);
70
+ }
71
+ export function createContractStore(dir) {
72
+ const cache = new Map();
73
+ function readJson(...parts) {
74
+ return JSON.parse(readFileSync(resolve(dir, ...parts), "utf8"));
75
+ }
76
+ function cached(key, load) {
77
+ const hit = cache.get(key);
78
+ if (hit !== undefined)
79
+ return hit;
80
+ const value = load();
81
+ cache.set(key, value);
82
+ return value;
83
+ }
84
+ function loadChecked(file) {
85
+ return cached(file, () => {
86
+ const value = readJson(file);
87
+ const mismatches = checkFrozen(file, value, dir);
88
+ if (mismatches.length > 0)
89
+ throw new ContractValidationError(dir, mismatches);
90
+ return value;
91
+ });
92
+ }
93
+ function verifyAtStartup() {
94
+ const mismatches = [];
95
+ const snapshot = new Map();
96
+ for (const file of FROZEN_FILES) {
97
+ const value = readJson(file);
98
+ mismatches.push(...checkFrozen(file, value, dir));
99
+ snapshot.set(file, value);
100
+ }
101
+ if (mismatches.length > 0)
102
+ throw new ContractValidationError(dir, mismatches);
103
+ for (const [file, value] of snapshot)
104
+ cache.set(file, value);
105
+ }
106
+ return {
107
+ dir,
108
+ loadEndpoints: () => loadChecked("endpoints.json"),
109
+ loadSse: () => loadChecked("sse-events.json"),
110
+ loadTools: () => loadChecked("tools.json"),
111
+ loadRouteSnapshot: () => cached("route-table.snapshot.json", () => readJson("route-table.snapshot.json")),
112
+ loadSessionEventSchema: () => cached("session-event.schema.json", () => readJson("session-event.schema.json")),
113
+ loadDataFilesIndex: () => cached("data-files/index.json", () => readJson("data-files", "index.json")),
114
+ loadDataFileSchema: (name) => cached("data-files/" + name, () => readJson("data-files", name)),
115
+ verifyAtStartup,
116
+ };
117
+ }
118
+ let singleton = null;
119
+ /** The process-wide store: one validated snapshot, reused for the process lifetime. */
120
+ export function contractStore() {
121
+ if (singleton === null)
122
+ singleton = createContractStore(contractsDir());
123
+ return singleton;
124
+ }
125
+ export function loadEndpoints() {
126
+ return contractStore().loadEndpoints();
127
+ }
128
+ export function loadSse() {
129
+ return contractStore().loadSse();
130
+ }
131
+ export function loadRouteSnapshot() {
132
+ return contractStore().loadRouteSnapshot();
133
+ }
134
+ export function loadTools() {
135
+ return contractStore().loadTools();
136
+ }
137
+ export function loadSessionEventSchema() {
138
+ return contractStore().loadSessionEventSchema();
139
+ }
140
+ export function loadDataFilesIndex() {
141
+ return contractStore().loadDataFilesIndex();
142
+ }
143
+ export function loadDataFileSchema(name) {
144
+ return contractStore().loadDataFileSchema(name);
145
+ }
146
+ /**
147
+ * The explicit startup gate (W807): Studio calls this once at boot, before it
148
+ * binds a port. It reads the frozen contract files from disk, throws a
149
+ * ContractValidationError naming file / expected / actual on any drift, and on
150
+ * success primes the cache with the exact snapshot that was validated -- so the
151
+ * running process stays internally consistent for its whole lifetime even if
152
+ * contracts/*.json changes underneath it.
153
+ *
154
+ * A drifted file is a REFUSAL TO START, not a warning: the previous behaviour
155
+ * was to boot and then 500 on the first request that touched the contract.
156
+ */
157
+ export function verifyContractsAtStartup() {
158
+ contractStore().verifyAtStartup();
159
+ }
@@ -0,0 +1,16 @@
1
+ /** HTTP error contract helpers (the {ok:false,error} envelope). */
2
+ import type { ErrorEnvelope } from "./types.js";
3
+ export declare class StudioError extends Error {
4
+ readonly status: number;
5
+ readonly detail?: unknown;
6
+ constructor(status: number, message: string, detail?: unknown);
7
+ toEnvelope(): ErrorEnvelope;
8
+ }
9
+ /** Shared session-id resolution errors (src/workspaces.rs:611-652). */
10
+ export declare const SessionIdErrors: {
11
+ readonly malformed: (id: string) => StudioError;
12
+ readonly unknownWorkspace: (name: string) => StudioError;
13
+ readonly invalid: (id: string) => StudioError;
14
+ readonly unknownSession: (id: string) => StudioError;
15
+ readonly busy: (what: string) => StudioError;
16
+ };
package/dist/errors.js ADDED
@@ -0,0 +1,22 @@
1
+ /** HTTP error contract helpers (the {ok:false,error} envelope). */
2
+ export class StudioError extends Error {
3
+ status;
4
+ detail;
5
+ constructor(status, message, detail) {
6
+ super(message);
7
+ this.name = "StudioError";
8
+ this.status = status;
9
+ this.detail = detail;
10
+ }
11
+ toEnvelope() {
12
+ return { ok: false, error: this.message };
13
+ }
14
+ }
15
+ /** Shared session-id resolution errors (src/workspaces.rs:611-652). */
16
+ export const SessionIdErrors = {
17
+ malformed: (id) => new StudioError(400, `invalid session id '${id}': expected '<workspace>/<session>'`),
18
+ unknownWorkspace: (name) => new StudioError(404, `unknown workspace '${name}'`),
19
+ invalid: (id) => new StudioError(400, `invalid session id '${id}'`),
20
+ unknownSession: (id) => new StudioError(404, `unknown session '${id}'`),
21
+ busy: (what) => new StudioError(409, `turn in progress; ${what} applies between turns`),
22
+ };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * EventBus seam — port of `crates/core/src/event_bus.rs`.
3
+ *
4
+ * A typed bus with three independent dispatch modes, keyed by event type:
5
+ * - on / emit observe-only broadcast;
6
+ * - bail / runBail intercept chain: first non-`undefined` answer
7
+ * short-circuits (the guard primitive);
8
+ * - waterfall / runWaterfall
9
+ * transform chain: each listener maps the running value
10
+ * handed to the next one.
11
+ *
12
+ * The three modes live in separate maps, so a listener registered in one mode
13
+ * never interferes with another. The event type is an explicit token — a
14
+ * well-known string, a symbol, or a class constructor used by identity (see
15
+ * `context.ts:ServiceToken`).
16
+ *
17
+ * `undefined` is `None`: a bail listener that returns `undefined` passes, and a
18
+ * `null`/`false`/`0` answer short-circuits (unlike a truthiness check).
19
+ * `runWaterfall` cannot verify at runtime that every listener shares one value
20
+ * type (the legacy engine panics on a failed downcast); the TS generic makes
21
+ * one value type per event type a compile-time contract — pass a `transform`
22
+ * that keeps it.
23
+ */
24
+ import type { ServiceToken } from "./context.js";
25
+ export type EventKey<E> = ServiceToken<E>;
26
+ export interface EventBus {
27
+ /** Observe-only broadcast listener. */
28
+ on<E>(key: EventKey<E>, listener: (event: E) => void): void;
29
+ /** Deliver to every listener registered with `on` for this key. */
30
+ emit<E>(key: EventKey<E>, event: E): void;
31
+ /** Intercept listener; returning `undefined` passes to the next one. */
32
+ bail<E, R>(key: EventKey<E>, listener: (event: E) => R | undefined): void;
33
+ /** First non-`undefined` answer in registration order, else undefined. */
34
+ runBail<E, R>(key: EventKey<E>, event: E): R | undefined;
35
+ /** Transform listener; each layer receives the previous layer's value. */
36
+ waterfall<E, R>(key: EventKey<E>, listener: (event: E, value: R) => R): void;
37
+ /** The value after every waterfall listener has run, in order. */
38
+ runWaterfall<E, R>(key: EventKey<E>, event: E, init: R): R;
39
+ /**
40
+ * W783: ASYNC delegate chain. Each layer receives `(event, next)`: returning a
41
+ * value CLAIMS the request, calling `next()` delegates to the layer behind it.
42
+ * This is the cordis waterfall the sync `waterfall` cannot express, because a
43
+ * claiming layer may have to PARK on a promise (user questions, §5.2).
44
+ */
45
+ waterfallAsync<E, R>(key: EventKey<E>, listener: (event: E, next: () => Promise<R>) => Promise<R>): void;
46
+ /**
47
+ * W783: run the async chain outermost-first. `init` is the bottom of the
48
+ * chain (the fallback), reached only when every layer delegates.
49
+ */
50
+ runWaterfallAsync<E, R>(key: EventKey<E>, event: E, init: () => Promise<R>): Promise<R>;
51
+ /** Registered listener counts per mode (diagnostics / tests). */
52
+ counts(key: EventKey<unknown>): {
53
+ on: number;
54
+ bail: number;
55
+ waterfall: number;
56
+ };
57
+ }
58
+ export declare function createEventBus(): EventBus;
59
+ /** Well-known token for the engine event bus service. */
60
+ export declare const EVENT_BUS_SERVICE = "celestea.core.EventBus";
@@ -0,0 +1,100 @@
1
+ /**
2
+ * EventBus seam — port of `crates/core/src/event_bus.rs`.
3
+ *
4
+ * A typed bus with three independent dispatch modes, keyed by event type:
5
+ * - on / emit observe-only broadcast;
6
+ * - bail / runBail intercept chain: first non-`undefined` answer
7
+ * short-circuits (the guard primitive);
8
+ * - waterfall / runWaterfall
9
+ * transform chain: each listener maps the running value
10
+ * handed to the next one.
11
+ *
12
+ * The three modes live in separate maps, so a listener registered in one mode
13
+ * never interferes with another. The event type is an explicit token — a
14
+ * well-known string, a symbol, or a class constructor used by identity (see
15
+ * `context.ts:ServiceToken`).
16
+ *
17
+ * `undefined` is `None`: a bail listener that returns `undefined` passes, and a
18
+ * `null`/`false`/`0` answer short-circuits (unlike a truthiness check).
19
+ * `runWaterfall` cannot verify at runtime that every listener shares one value
20
+ * type (the legacy engine panics on a failed downcast); the TS generic makes
21
+ * one value type per event type a compile-time contract — pass a `transform`
22
+ * that keeps it.
23
+ */
24
+ function busKey(key) {
25
+ return typeof key === "string" ? `event:${key}` : key;
26
+ }
27
+ export function createEventBus() {
28
+ const subs = new Map();
29
+ const bailers = new Map();
30
+ const waterfalls = new Map();
31
+ // W783: the async delegate chain lives in its own map, so a listener
32
+ // registered in one mode can never interfere with another (same rule as the
33
+ // three original modes).
34
+ const asyncWaterfalls = new Map();
35
+ const push = (map, key, fn) => {
36
+ const k = busKey(key);
37
+ const list = map.get(k);
38
+ if (list)
39
+ list.push(fn);
40
+ else
41
+ map.set(k, [fn]);
42
+ };
43
+ return {
44
+ on(key, listener) {
45
+ push(subs, key, listener);
46
+ },
47
+ emit(key, event) {
48
+ for (const fn of subs.get(busKey(key)) ?? [])
49
+ fn(event);
50
+ },
51
+ bail(key, listener) {
52
+ push(bailers, key, listener);
53
+ },
54
+ runBail(key, event) {
55
+ for (const fn of bailers.get(busKey(key)) ?? []) {
56
+ const answer = fn(event);
57
+ if (answer !== undefined)
58
+ return answer;
59
+ }
60
+ return undefined;
61
+ },
62
+ waterfall(key, listener) {
63
+ push(waterfalls, key, listener);
64
+ },
65
+ runWaterfall(key, event, init) {
66
+ let value = init;
67
+ for (const fn of waterfalls.get(busKey(key)) ?? []) {
68
+ value = fn(event, value);
69
+ }
70
+ return value;
71
+ },
72
+ waterfallAsync(key, listener) {
73
+ push(asyncWaterfalls, key, listener);
74
+ },
75
+ runWaterfallAsync(key, event, init) {
76
+ // Outermost-first: each layer gets a `next` that walks to the layer
77
+ // behind it and finally to `init`. Building the chain lazily (inside
78
+ // `next`) keeps a delegating layer from running anything downstream
79
+ // until it actually delegates.
80
+ const layers = asyncWaterfalls.get(busKey(key)) ?? [];
81
+ const step = (index) => {
82
+ const fn = layers[index];
83
+ if (fn === undefined)
84
+ return init();
85
+ return fn(event, () => step(index + 1));
86
+ };
87
+ return step(0);
88
+ },
89
+ counts(key) {
90
+ const k = busKey(key);
91
+ return {
92
+ on: (subs.get(k) ?? []).length,
93
+ bail: (bailers.get(k) ?? []).length,
94
+ waterfall: (waterfalls.get(k) ?? []).length + (asyncWaterfalls.get(k) ?? []).length,
95
+ };
96
+ },
97
+ };
98
+ }
99
+ /** Well-known token for the engine event bus service. */
100
+ export const EVENT_BUS_SERVICE = "celestea.core.EventBus";
@@ -0,0 +1,66 @@
1
+ /**
2
+ * `@celestea/core` — the semantic kernel: the frozen contract types, the
3
+ * serde-exact SessionEvent codec, the model-visible Message model, and the
4
+ * plugin seams (Plugin / Context / EventBus / SessionLog / Llm / ToolGuard).
5
+ *
6
+ * Dependency direction: core imports NOTHING from the other packages. Every
7
+ * concrete implementation (session log, llm provider, tools, agent loop) is a
8
+ * plugin mounted into a Context at compose time.
9
+ *
10
+ * Module map:
11
+ * types.ts frozen P0 contracts (SessionEvent union, SSE, endpoints…)
12
+ * message.ts Role / Content / ToolCall / Message / Usage (message.rs)
13
+ * stream.ts ModelRequest / StreamEvent / LlmError (message.rs, llm.rs)
14
+ * session-event.ts SessionEvent JSONL codec (validate / serialize) (session_log.rs)
15
+ * session-log.ts SessionLog seam + storage half + default projection (A2)
16
+ * projection.ts derive_messages / balance_tool_calls (session/log.rs, A2)
17
+ * turn-id.ts turn id math + audit (A2)
18
+ * injection.ts mid-turn delivery seam: lanes / placement / envelope (W513)
19
+ * plugin.ts Plugin seam + NamedRegistry (plugin.rs)
20
+ * context.ts Context service container (context.rs)
21
+ * event-bus.ts EventBus seam (on/bail/waterfall/waterfallAsync) (event_bus.rs)
22
+ * question.ts user-question seam: service iface + error codes (W783)
23
+ * llm.ts Llm seam + LlmRegistry (llm.rs)
24
+ * tool.ts Tool / ToolGuard / ToolRegistry seams (tool.rs)
25
+ * sandbox.ts Sandbox seam (execution boundary) (tools/src/sandbox.rs)
26
+ * agent.ts AgentLoop seam + AgentConfig (agent.rs)
27
+ * json.ts JSON helpers + serde-exact text
28
+ * sse-bus.ts SDK-side SSE broadcast bus
29
+ * redact.ts secret redaction for fixtures / reports
30
+ * repo.ts repository-relative path helpers
31
+ * celestea-home.ts CELESTEA_HOME data-root resolution (W880)
32
+ * celestea-sources.ts project/global source layers (W882)
33
+ * skills.ts skill discovery + frontmatter contract (W882)
34
+ * skill-catalog.ts skill catalog text for the per-turn injection (W884)
35
+ * memory.ts workspace MEMORY.md turn-context injection (F3)
36
+ * errors.ts shared error types
37
+ * contracts/ contract-file loaders (frozen data in contracts/)
38
+ */
39
+ export * from "./types.js";
40
+ export * from "./message.js";
41
+ export * from "./stream.js";
42
+ export * from "./session-event.js";
43
+ export * from "./session-log.js";
44
+ export * from "./projection.js";
45
+ export * from "./tool-surface.js";
46
+ export * from "./turn-id.js";
47
+ export * from "./injection.js";
48
+ export * from "./plugin.js";
49
+ export * from "./context.js";
50
+ export * from "./event-bus.js";
51
+ export * from "./question.js";
52
+ export * from "./llm.js";
53
+ export * from "./tool.js";
54
+ export * from "./sandbox.js";
55
+ export * from "./agent.js";
56
+ export * from "./json.js";
57
+ export * from "./sse-bus.js";
58
+ export * from "./errors.js";
59
+ export * from "./redact.js";
60
+ export * from "./repo.js";
61
+ export * from "./celestea-home.js";
62
+ export * from "./celestea-sources.js";
63
+ export * from "./skills.js";
64
+ export * from "./skill-catalog.js";
65
+ export * from "./memory.js";
66
+ export * from "./contracts/index.js";