@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.
- package/LICENSE +21 -0
- package/README.md +95 -0
- package/contracts/data-files/checkpoint.schema.json +111 -0
- package/contracts/data-files/cli-main-jsonl-precompact.schema.json +27 -0
- package/contracts/data-files/cli-main-jsonl.schema.json +22 -0
- package/contracts/data-files/fallbacks.schema.json +71 -0
- package/contracts/data-files/index.json +124 -0
- package/contracts/data-files/pricing.schema.json +65 -0
- package/contracts/data-files/prompts.schema.json +130 -0
- package/contracts/data-files/providers.schema.json +177 -0
- package/contracts/data-files/registry-tsv.schema.json +74 -0
- package/contracts/data-files/session.schema.json +51 -0
- package/contracts/data-files/usage-ledger.schema.json +112 -0
- package/contracts/data-files/workspaces.schema.json +63 -0
- package/contracts/endpoints.json +4390 -0
- package/contracts/probe-evidence.json +219 -0
- package/contracts/route-table.snapshot.json +377 -0
- package/contracts/scope-hash-vectors.json +273 -0
- package/contracts/session-event.schema.json +441 -0
- package/contracts/sse-events.json +202 -0
- package/contracts/tools.json +730 -0
- package/dist/agent.d.ts +65 -0
- package/dist/agent.js +36 -0
- package/dist/celestea-home.d.ts +63 -0
- package/dist/celestea-home.js +96 -0
- package/dist/celestea-sources.d.ts +53 -0
- package/dist/celestea-sources.js +61 -0
- package/dist/context.d.ts +33 -0
- package/dist/context.js +55 -0
- package/dist/contracts/index.d.ts +234 -0
- package/dist/contracts/index.js +159 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +22 -0
- package/dist/event-bus.d.ts +60 -0
- package/dist/event-bus.js +100 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +66 -0
- package/dist/injection.d.ts +61 -0
- package/dist/injection.js +27 -0
- package/dist/json.d.ts +34 -0
- package/dist/json.js +127 -0
- package/dist/llm.d.ts +34 -0
- package/dist/llm.js +41 -0
- package/dist/memory.d.ts +72 -0
- package/dist/memory.js +123 -0
- package/dist/message.d.ts +189 -0
- package/dist/message.js +252 -0
- package/dist/plugin.d.ts +38 -0
- package/dist/plugin.js +49 -0
- package/dist/projection.d.ts +67 -0
- package/dist/projection.js +168 -0
- package/dist/question.d.ts +154 -0
- package/dist/question.js +82 -0
- package/dist/redact.d.ts +40 -0
- package/dist/redact.js +185 -0
- package/dist/repo.d.ts +14 -0
- package/dist/repo.js +87 -0
- package/dist/sandbox.d.ts +182 -0
- package/dist/sandbox.js +78 -0
- package/dist/session-event.d.ts +57 -0
- package/dist/session-event.js +425 -0
- package/dist/session-log.d.ts +71 -0
- package/dist/session-log.js +66 -0
- package/dist/skill-catalog.d.ts +29 -0
- package/dist/skill-catalog.js +52 -0
- package/dist/skills.d.ts +116 -0
- package/dist/skills.js +273 -0
- package/dist/sse-bus.d.ts +40 -0
- package/dist/sse-bus.js +105 -0
- package/dist/stream.d.ts +115 -0
- package/dist/stream.js +52 -0
- package/dist/tool-surface.d.ts +45 -0
- package/dist/tool-surface.js +98 -0
- package/dist/tool.d.ts +77 -0
- package/dist/tool.js +15 -0
- package/dist/turn-id.d.ts +37 -0
- package/dist/turn-id.js +76 -0
- package/dist/types.d.ts +396 -0
- package/dist/types.js +58 -0
- 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
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -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";
|
package/dist/index.d.ts
ADDED
|
@@ -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";
|