@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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine-side session binding helpers.
|
|
3
|
+
*
|
|
4
|
+
* The host owns the id space (`<workspace>/<session>`); the engine owns the
|
|
5
|
+
* file name: every session directory holds ONE append-only `cli-main.jsonl`
|
|
6
|
+
* (`celestea_studio/src/workspaces.rs` SESSION_FILE), so the log is always
|
|
7
|
+
* opened under the fixed session id `cli-main` and never under the host id —
|
|
8
|
+
* otherwise `file_name_for` would sanitize `ws/s1` into `ws_s1.jsonl` and the
|
|
9
|
+
* engine would talk to a file the host never reads.
|
|
10
|
+
*
|
|
11
|
+
* When nothing is active (or the id is unknown) the generation runs on an
|
|
12
|
+
* in-memory log: `/api/turn` still works, and the adapter never invents a
|
|
13
|
+
* directory on behalf of the operator.
|
|
14
|
+
*
|
|
15
|
+
* E §1.3 P0 ②: a persistent log is wrapped in a checkpoint decorator, so every
|
|
16
|
+
* `turn_start`/`turn_end` it records also updates `<dir>/checkpoint.json`. An
|
|
17
|
+
* in-memory (detached) session has no directory and therefore no sidecar.
|
|
18
|
+
*
|
|
19
|
+
* W747: moved verbatim from `apps/studio/src/runtime/engine-session.ts` into the
|
|
20
|
+
* runtime's host layer (behaviour, export names and log format unchanged; the old
|
|
21
|
+
* path is now a re-export shim). The only edit is the import of the intra-package
|
|
22
|
+
* session binding (`../session-binding.js` instead of the `@celestea/runtime`
|
|
23
|
+
* alias, which would be a package self-cycle). `sessionIdOfDir` came with it, out
|
|
24
|
+
* of the host's grants reader (`engine-grants.ts`), because the id space is what
|
|
25
|
+
* this module already documents; the old path re-exports it unchanged.
|
|
26
|
+
*/
|
|
27
|
+
import { type CheckpointIdentity } from "@celestea/session";
|
|
28
|
+
import type { SessionLog } from "@celestea/core";
|
|
29
|
+
import { type SessionBinding } from "../session-binding.js";
|
|
30
|
+
/** The engine's per-session log file name. */
|
|
31
|
+
export declare const SESSION_LOG_NAME = "cli-main.jsonl";
|
|
32
|
+
/** The session id the log is opened under (keeps the file name cli-main.jsonl). */
|
|
33
|
+
export declare const SESSION_LOG_ID = "cli-main";
|
|
34
|
+
/**
|
|
35
|
+
* W768: a session's workspace as ONE value — the display NAME the system prompt
|
|
36
|
+
* renders and the ROOT PATH the tools/sandbox must run in.
|
|
37
|
+
*
|
|
38
|
+
* They travel together because they are the same fact seen twice: two
|
|
39
|
+
* independent lookups (a prompt that says "CelesteaTeamAPI" and a shell that
|
|
40
|
+
* starts in whatever the process was launched from) drifted apart into a bug the
|
|
41
|
+
* model then reasoned from. A caller that needs either one resolves this value.
|
|
42
|
+
*/
|
|
43
|
+
export interface SessionWorkspace {
|
|
44
|
+
/** Workspace key: the basename of `path` (`CelesteaTeamAPI`). */
|
|
45
|
+
name: string;
|
|
46
|
+
/** Absolute workspace root — the session's cwd and containment root. */
|
|
47
|
+
path: string;
|
|
48
|
+
}
|
|
49
|
+
/** Where an active session lives (the host resolves the id; dir may be null). */
|
|
50
|
+
export interface SessionTarget {
|
|
51
|
+
sessionId: string;
|
|
52
|
+
dir: string | null;
|
|
53
|
+
/** W768: the session's workspace, resolved by the host alongside `dir`. */
|
|
54
|
+
workspace?: SessionWorkspace | null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Checkpoint wiring of one host process. `identity` is the `pid`/`boot_id` pair
|
|
58
|
+
* written into every sidecar (E §1.2.2); tests inject a fixed one so the written
|
|
59
|
+
* file is deterministic.
|
|
60
|
+
*/
|
|
61
|
+
export interface CheckpointWiring {
|
|
62
|
+
/**
|
|
63
|
+
* W878: the TRUSTED `<workspace>/<session>` id of this session, from the
|
|
64
|
+
* host's own `resolve()` (`ResolvedSession.id`). When present it is the
|
|
65
|
+
* sidecar's self-description; when absent the legacy `sessionIdOfDir(dir)`
|
|
66
|
+
* fallback below is used, which only holds while the session directory is a
|
|
67
|
+
* direct child of the workspace root. New callers must thread the id.
|
|
68
|
+
*/
|
|
69
|
+
sessionId?: string;
|
|
70
|
+
identity?: CheckpointIdentity;
|
|
71
|
+
now?: () => number;
|
|
72
|
+
warn?: (message: string) => void;
|
|
73
|
+
/**
|
|
74
|
+
* E §1.3 P1 ③: the audit channel of a DEGRADED log — the sidecar's
|
|
75
|
+
* `degraded.log_write_errors` just became non-zero, so disk and memory have
|
|
76
|
+
* forked. Called at most once per session store.
|
|
77
|
+
*/
|
|
78
|
+
onDegraded?: (info: {
|
|
79
|
+
session: string;
|
|
80
|
+
count: number;
|
|
81
|
+
}) => void;
|
|
82
|
+
}
|
|
83
|
+
/** `boot_id` is generated ONCE per process and never again while it lives. */
|
|
84
|
+
export declare const PROCESS_CHECKPOINT_IDENTITY: CheckpointIdentity;
|
|
85
|
+
/**
|
|
86
|
+
* `<workspace>/<session>` of a session directory (the file's self-description).
|
|
87
|
+
*
|
|
88
|
+
* W878 legacy: this infers the id from the PATH, so it is only correct while the
|
|
89
|
+
* session directory is a direct child of the workspace root. It stays exported
|
|
90
|
+
* for callers/tests that still derive the id, but inside the library it is no
|
|
91
|
+
* longer the source of truth — `openSessionLog` prefers the explicit
|
|
92
|
+
* `CheckpointWiring.sessionId` and only falls back here when none was threaded.
|
|
93
|
+
*/
|
|
94
|
+
export declare function sessionIdOfDir(sessionDir: string): string;
|
|
95
|
+
/**
|
|
96
|
+
* Open (replaying) the append-only log of a session directory, wrapped so the
|
|
97
|
+
* turn boundaries also land in `<dir>/checkpoint.json` (E §1.3 P0 ②). The
|
|
98
|
+
* wrapper is transparent: `path`, `close()` and `writeErrorCount()` still work
|
|
99
|
+
* for the host and for the registry's turn-counter restoration.
|
|
100
|
+
*/
|
|
101
|
+
export declare function openSessionLog(dir: string, wiring?: CheckpointWiring): SessionLog;
|
|
102
|
+
/** One in-memory log per detached session id, reused across rebinds. */
|
|
103
|
+
export declare function memoryBindingFor(logs: Map<string, SessionLog>, sessionId: string | null): SessionBinding;
|
|
104
|
+
/** The binding for a host session id (persistent when a directory is known). */
|
|
105
|
+
export declare function bindingFor(sessionId: string | null, target: SessionTarget | null, logs: Map<string, SessionLog>, wiring?: CheckpointWiring): SessionBinding;
|
|
106
|
+
/**
|
|
107
|
+
* Worker session-id prefix of one host session (W513).
|
|
108
|
+
*
|
|
109
|
+
* Every session runtime owns its OWN worker registry, and a registry mints
|
|
110
|
+
* `session-<n>` by default — which would collide across sessions in the merged
|
|
111
|
+
* `GET /api/sessions` view. The session id therefore prefixes the ids
|
|
112
|
+
* (`sample-ws_s1-session-0`); the detached runtime keeps the frozen
|
|
113
|
+
* `session-<n>` shape so single-session hosts and fixtures are unchanged.
|
|
114
|
+
*/
|
|
115
|
+
export declare function workerSessionPrefix(sessionId: string | null): string;
|
|
116
|
+
/** Close a session log when its implementation owns a descriptor (idempotent). */
|
|
117
|
+
export declare function closeLog(log: SessionLog | null | undefined): void;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine-side session binding helpers.
|
|
3
|
+
*
|
|
4
|
+
* The host owns the id space (`<workspace>/<session>`); the engine owns the
|
|
5
|
+
* file name: every session directory holds ONE append-only `cli-main.jsonl`
|
|
6
|
+
* (`celestea_studio/src/workspaces.rs` SESSION_FILE), so the log is always
|
|
7
|
+
* opened under the fixed session id `cli-main` and never under the host id —
|
|
8
|
+
* otherwise `file_name_for` would sanitize `ws/s1` into `ws_s1.jsonl` and the
|
|
9
|
+
* engine would talk to a file the host never reads.
|
|
10
|
+
*
|
|
11
|
+
* When nothing is active (or the id is unknown) the generation runs on an
|
|
12
|
+
* in-memory log: `/api/turn` still works, and the adapter never invents a
|
|
13
|
+
* directory on behalf of the operator.
|
|
14
|
+
*
|
|
15
|
+
* E §1.3 P0 ②: a persistent log is wrapped in a checkpoint decorator, so every
|
|
16
|
+
* `turn_start`/`turn_end` it records also updates `<dir>/checkpoint.json`. An
|
|
17
|
+
* in-memory (detached) session has no directory and therefore no sidecar.
|
|
18
|
+
*
|
|
19
|
+
* W747: moved verbatim from `apps/studio/src/runtime/engine-session.ts` into the
|
|
20
|
+
* runtime's host layer (behaviour, export names and log format unchanged; the old
|
|
21
|
+
* path is now a re-export shim). The only edit is the import of the intra-package
|
|
22
|
+
* session binding (`../session-binding.js` instead of the `@celestea/runtime`
|
|
23
|
+
* alias, which would be a package self-cycle). `sessionIdOfDir` came with it, out
|
|
24
|
+
* of the host's grants reader (`engine-grants.ts`), because the id space is what
|
|
25
|
+
* this module already documents; the old path re-exports it unchanged.
|
|
26
|
+
*/
|
|
27
|
+
import { basename, dirname } from "node:path";
|
|
28
|
+
import { InMemorySessionLog } from "@celestea/session";
|
|
29
|
+
import { PersistentSessionLog } from "@celestea/session";
|
|
30
|
+
import { checkpointedLog, CheckpointStore, currentProcessIdentity, writeErrorCountOf, } from "@celestea/session";
|
|
31
|
+
import { createSessionBinding } from "../session-binding.js";
|
|
32
|
+
/** The engine's per-session log file name. */
|
|
33
|
+
export const SESSION_LOG_NAME = "cli-main.jsonl";
|
|
34
|
+
/** The session id the log is opened under (keeps the file name cli-main.jsonl). */
|
|
35
|
+
export const SESSION_LOG_ID = "cli-main";
|
|
36
|
+
/** `boot_id` is generated ONCE per process and never again while it lives. */
|
|
37
|
+
export const PROCESS_CHECKPOINT_IDENTITY = currentProcessIdentity();
|
|
38
|
+
/**
|
|
39
|
+
* `<workspace>/<session>` of a session directory (the file's self-description).
|
|
40
|
+
*
|
|
41
|
+
* W878 legacy: this infers the id from the PATH, so it is only correct while the
|
|
42
|
+
* session directory is a direct child of the workspace root. It stays exported
|
|
43
|
+
* for callers/tests that still derive the id, but inside the library it is no
|
|
44
|
+
* longer the source of truth — `openSessionLog` prefers the explicit
|
|
45
|
+
* `CheckpointWiring.sessionId` and only falls back here when none was threaded.
|
|
46
|
+
*/
|
|
47
|
+
export function sessionIdOfDir(sessionDir) {
|
|
48
|
+
return `${basename(dirname(sessionDir))}/${basename(sessionDir)}`;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Open (replaying) the append-only log of a session directory, wrapped so the
|
|
52
|
+
* turn boundaries also land in `<dir>/checkpoint.json` (E §1.3 P0 ②). The
|
|
53
|
+
* wrapper is transparent: `path`, `close()` and `writeErrorCount()` still work
|
|
54
|
+
* for the host and for the registry's turn-counter restoration.
|
|
55
|
+
*/
|
|
56
|
+
export function openSessionLog(dir, wiring = {}) {
|
|
57
|
+
const log = PersistentSessionLog.open(dir, SESSION_LOG_ID);
|
|
58
|
+
const store = new CheckpointStore({
|
|
59
|
+
dir,
|
|
60
|
+
// Self-description `<workspace>/<session>` — the id grants.json also uses, so
|
|
61
|
+
// a sidecar found in a renamed directory is ignored instead of trusted.
|
|
62
|
+
// W878: the explicit id wins; `sessionIdOfDir` is only the legacy fallback
|
|
63
|
+
// for callers that have no trusted id (it is wrong once a session dir sinks
|
|
64
|
+
// below the workspace root, e.g. `<ws>/.celestea/sessions/<dir>`).
|
|
65
|
+
session: wiring.sessionId ?? sessionIdOfDir(dir),
|
|
66
|
+
identity: wiring.identity ?? PROCESS_CHECKPOINT_IDENTITY,
|
|
67
|
+
...(wiring.now === undefined ? {} : { now: wiring.now }),
|
|
68
|
+
...(wiring.warn === undefined ? {} : { warn: wiring.warn }),
|
|
69
|
+
logWriteErrors: () => writeErrorCountOf(log),
|
|
70
|
+
...(wiring.onDegraded === undefined ? {} : { onDegraded: wiring.onDegraded }),
|
|
71
|
+
});
|
|
72
|
+
return checkpointedLog(log, store);
|
|
73
|
+
}
|
|
74
|
+
/** One in-memory log per detached session id, reused across rebinds. */
|
|
75
|
+
export function memoryBindingFor(logs, sessionId) {
|
|
76
|
+
const key = sessionId ?? "<detached>";
|
|
77
|
+
const log = logs.get(key) ?? new InMemorySessionLog();
|
|
78
|
+
logs.set(key, log);
|
|
79
|
+
return createSessionBinding({ sessionId: key, dir: null, open: () => log });
|
|
80
|
+
}
|
|
81
|
+
/** The binding for a host session id (persistent when a directory is known). */
|
|
82
|
+
export function bindingFor(sessionId, target, logs, wiring = {}) {
|
|
83
|
+
if (sessionId === null || target === null || target.dir === null)
|
|
84
|
+
return memoryBindingFor(logs, sessionId);
|
|
85
|
+
const dir = target.dir;
|
|
86
|
+
// W878: thread the trusted id into the sidecar wiring. `sessionId` is narrowed
|
|
87
|
+
// to a string here, so the checkpoint store never has to infer it from `dir`.
|
|
88
|
+
return createSessionBinding({ sessionId, dir, open: () => openSessionLog(dir, { ...wiring, sessionId }) });
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Worker session-id prefix of one host session (W513).
|
|
92
|
+
*
|
|
93
|
+
* Every session runtime owns its OWN worker registry, and a registry mints
|
|
94
|
+
* `session-<n>` by default — which would collide across sessions in the merged
|
|
95
|
+
* `GET /api/sessions` view. The session id therefore prefixes the ids
|
|
96
|
+
* (`sample-ws_s1-session-0`); the detached runtime keeps the frozen
|
|
97
|
+
* `session-<n>` shape so single-session hosts and fixtures are unchanged.
|
|
98
|
+
*/
|
|
99
|
+
export function workerSessionPrefix(sessionId) {
|
|
100
|
+
if (sessionId === null)
|
|
101
|
+
return "session-";
|
|
102
|
+
return `${sessionId.replace(/[^A-Za-z0-9._-]/g, "_")}-session-`;
|
|
103
|
+
}
|
|
104
|
+
/** Close a session log when its implementation owns a descriptor (idempotent). */
|
|
105
|
+
export function closeLog(log) {
|
|
106
|
+
const close = log === null || log === undefined ? undefined : log.close;
|
|
107
|
+
if (typeof close === "function")
|
|
108
|
+
close.call(log);
|
|
109
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W747 — the host layer of the engine assembly.
|
|
3
|
+
*
|
|
4
|
+
* The engine's true composition root used to live entirely in `apps/studio`
|
|
5
|
+
* (`apps/studio/src/runtime/**`, ~2500 lines): profile resolution, provider
|
|
6
|
+
* targeting, session-log binding, tool/sandbox/guard assembly, grants policy.
|
|
7
|
+
* `@celestea/runtime` only mounted plugins. W732 §A2 / §F1 flagged that as the
|
|
8
|
+
* largest gap between the documented layering and the code, so the engine-side
|
|
9
|
+
* half moves here, one bounded cut at a time.
|
|
10
|
+
*
|
|
11
|
+
* Rules for this layer:
|
|
12
|
+
* 1. `packages/*` may never import `apps/*` (dependency-cruiser
|
|
13
|
+
* `no-packages-to-apps`): a module only moves here once it depends on
|
|
14
|
+
* nothing but `@celestea/core`, the L1 packages and `../` internals.
|
|
15
|
+
* 2. The host keeps its HTTP shapes and its data files. When a module needs a
|
|
16
|
+
* host-owned type, the MINIMAL structural slice it reads is declared here
|
|
17
|
+
* (see `ProfileSlot` in `provider-target.ts`) — never a copy of the host
|
|
18
|
+
* view, and never a host import.
|
|
19
|
+
* 3. Nothing is re-written while moving: behaviour, export names and error
|
|
20
|
+
* text stay byte-identical, and the old `apps/studio/src/runtime/<mod>.ts`
|
|
21
|
+
* path stays alive as a re-export shim so existing imports do not break.
|
|
22
|
+
*
|
|
23
|
+
* These modules are NOT a package subpath: everything below is re-exported by
|
|
24
|
+
* `../index.ts`, the package's only public API (cross-package deep imports are
|
|
25
|
+
* forbidden by `dependency-cruiser` and ESLint). A subpath export would need an
|
|
26
|
+
* exception in `.dependency-cruiser.cjs`; that is a separate decision.
|
|
27
|
+
*
|
|
28
|
+
* Moved so far (W747, first cut):
|
|
29
|
+
* engine-session.ts log/binding assembly + the `<ws>/<session>` id helper
|
|
30
|
+
* provider-target.ts startup model/base_url/api-key resolution (W511)
|
|
31
|
+
* Still in `apps/studio/src/runtime/` and blocked — see the W747 report:
|
|
32
|
+
* engine-profile.ts needs `EngineProfile`/`ProfilePatch` (apps runtime-adapter)
|
|
33
|
+
* and `MIN_STEPS`/`CONTEXT_WINDOW` (apps config)
|
|
34
|
+
* engine-plugins.ts needs `@celestea/tools` in packages/runtime/package.json
|
|
35
|
+
* llm-assembly.ts needs `@celestea/llm` in packages/runtime/package.json
|
|
36
|
+
* session-compose.ts needs `CapacityError` (apps runtime-adapter) + the above
|
|
37
|
+
*/
|
|
38
|
+
export * from "./engine-session.js";
|
|
39
|
+
export * from "./provider-target.js";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W747 — the host layer of the engine assembly.
|
|
3
|
+
*
|
|
4
|
+
* The engine's true composition root used to live entirely in `apps/studio`
|
|
5
|
+
* (`apps/studio/src/runtime/**`, ~2500 lines): profile resolution, provider
|
|
6
|
+
* targeting, session-log binding, tool/sandbox/guard assembly, grants policy.
|
|
7
|
+
* `@celestea/runtime` only mounted plugins. W732 §A2 / §F1 flagged that as the
|
|
8
|
+
* largest gap between the documented layering and the code, so the engine-side
|
|
9
|
+
* half moves here, one bounded cut at a time.
|
|
10
|
+
*
|
|
11
|
+
* Rules for this layer:
|
|
12
|
+
* 1. `packages/*` may never import `apps/*` (dependency-cruiser
|
|
13
|
+
* `no-packages-to-apps`): a module only moves here once it depends on
|
|
14
|
+
* nothing but `@celestea/core`, the L1 packages and `../` internals.
|
|
15
|
+
* 2. The host keeps its HTTP shapes and its data files. When a module needs a
|
|
16
|
+
* host-owned type, the MINIMAL structural slice it reads is declared here
|
|
17
|
+
* (see `ProfileSlot` in `provider-target.ts`) — never a copy of the host
|
|
18
|
+
* view, and never a host import.
|
|
19
|
+
* 3. Nothing is re-written while moving: behaviour, export names and error
|
|
20
|
+
* text stay byte-identical, and the old `apps/studio/src/runtime/<mod>.ts`
|
|
21
|
+
* path stays alive as a re-export shim so existing imports do not break.
|
|
22
|
+
*
|
|
23
|
+
* These modules are NOT a package subpath: everything below is re-exported by
|
|
24
|
+
* `../index.ts`, the package's only public API (cross-package deep imports are
|
|
25
|
+
* forbidden by `dependency-cruiser` and ESLint). A subpath export would need an
|
|
26
|
+
* exception in `.dependency-cruiser.cjs`; that is a separate decision.
|
|
27
|
+
*
|
|
28
|
+
* Moved so far (W747, first cut):
|
|
29
|
+
* engine-session.ts log/binding assembly + the `<ws>/<session>` id helper
|
|
30
|
+
* provider-target.ts startup model/base_url/api-key resolution (W511)
|
|
31
|
+
* Still in `apps/studio/src/runtime/` and blocked — see the W747 report:
|
|
32
|
+
* engine-profile.ts needs `EngineProfile`/`ProfilePatch` (apps runtime-adapter)
|
|
33
|
+
* and `MIN_STEPS`/`CONTEXT_WINDOW` (apps config)
|
|
34
|
+
* engine-plugins.ts needs `@celestea/tools` in packages/runtime/package.json
|
|
35
|
+
* llm-assembly.ts needs `@celestea/llm` in packages/runtime/package.json
|
|
36
|
+
* session-compose.ts needs `CapacityError` (apps runtime-adapter) + the above
|
|
37
|
+
*/
|
|
38
|
+
export * from "./engine-session.js";
|
|
39
|
+
export * from "./provider-target.js";
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup provider-target resolution (W511) — the TS port of the legacy
|
|
3
|
+
* `providers::apply_startup_default` + `resolve_base_url` + `resolve_api_key`.
|
|
4
|
+
*
|
|
5
|
+
* providers.json is the operator's registry of upstreams; resolving a target
|
|
6
|
+
* means answering three questions BEFORE the first compose (and again on every
|
|
7
|
+
* generation swap, from the host's own profile):
|
|
8
|
+
*
|
|
9
|
+
* model `default_model` IS IN the models list of some provider -> that id
|
|
10
|
+
* ("providers.json default_model");
|
|
11
|
+
* else env `CELESTEA_MODEL` -> "env CELESTEA_MODEL";
|
|
12
|
+
* else whatever the caller's profile already carried
|
|
13
|
+
* ("profile default", i.e. the celestea.toml slot).
|
|
14
|
+
* An unlisted `default_model` never wins: the contract validates it
|
|
15
|
+
* against the provider rows instead of trusting a stale string.
|
|
16
|
+
* base_url the provider that OWNS the resolved model, when its
|
|
17
|
+
* request_format is chat_completions and its base_url is non-empty
|
|
18
|
+
* (written into the profile before compose);
|
|
19
|
+
* else env `CELESTEA_BASE_URL` -> else the profile's own base_url.
|
|
20
|
+
* api key env[api_key_env] when non-empty; otherwise a plaintext key stored
|
|
21
|
+
* on the owning provider row is injected into the PROCESS ENV (the
|
|
22
|
+
* engine's only key channel, exactly like the engine's `env::set_var`).
|
|
23
|
+
* In-memory only: never written to a data file, never returned in a
|
|
24
|
+
* response, never logged.
|
|
25
|
+
*
|
|
26
|
+
* The module is store-free: it consumes the three `ProvidersStore` methods it
|
|
27
|
+
* needs, so the rules are unit-testable without a data file.
|
|
28
|
+
*
|
|
29
|
+
* W747: moved verbatim from `apps/studio/src/runtime/provider-target.ts` into the
|
|
30
|
+
* runtime's host layer (rules, export names and behaviour unchanged; the old
|
|
31
|
+
* path is now a re-export shim). The ONE edit is the profile slice below: the
|
|
32
|
+
* host view (`EngineProfile`) lives in `apps/studio/src/runtime-adapter.ts` and a
|
|
33
|
+
* package may not import an app, so the three fields this module actually reads
|
|
34
|
+
* are declared here as the minimal structural `ProfileSlot` that the host view
|
|
35
|
+
* already satisfies. `applyProviderTarget` is generic in it, so a caller holding
|
|
36
|
+
* an `EngineProfile` gets that exact type back.
|
|
37
|
+
*/
|
|
38
|
+
/** The startup-profile slice this module reads (satisfied by the host's view). */
|
|
39
|
+
export interface ProfileSlot {
|
|
40
|
+
model: string;
|
|
41
|
+
base_url: string;
|
|
42
|
+
api_key_env: string;
|
|
43
|
+
}
|
|
44
|
+
/** The fields of a providers.json row this module reads. */
|
|
45
|
+
export interface ProviderRef {
|
|
46
|
+
id: string;
|
|
47
|
+
base_url: string;
|
|
48
|
+
request_format: string;
|
|
49
|
+
api_key: string | null;
|
|
50
|
+
models: readonly {
|
|
51
|
+
id: string;
|
|
52
|
+
}[];
|
|
53
|
+
}
|
|
54
|
+
/** The `ProvidersStore` slice this module needs. */
|
|
55
|
+
export interface ProviderLookup {
|
|
56
|
+
rows(): readonly ProviderRef[];
|
|
57
|
+
defaultModel(): string | null;
|
|
58
|
+
}
|
|
59
|
+
/** Where the resolved model id came from (reported by the startup log). */
|
|
60
|
+
export type ModelSource = "providers.json default_model" | "env CELESTEA_MODEL" | "profile default";
|
|
61
|
+
/**
|
|
62
|
+
* Where the api key came from; "none" means unauthenticated requests.
|
|
63
|
+
*
|
|
64
|
+
* W747: named `ProviderKeySource` in the engine because `profile.ts` already
|
|
65
|
+
* exports a `KeySource` — the `resolve_api_key` ORDER (`env` / `api_key_file`
|
|
66
|
+
* / `home_config` / `provider_store` / `borrowed_engine_key` / `none`), which is a
|
|
67
|
+
* different, wider vocabulary. Two exports cannot share one name in a package's
|
|
68
|
+
* single public API, and the host's narrower 3-value union is what the startup
|
|
69
|
+
* log reports; the host-side shim re-exports it under its original name, so no
|
|
70
|
+
* `apps/studio` import changed.
|
|
71
|
+
*/
|
|
72
|
+
export type ProviderKeySource = "env" | "provider_store" | "none";
|
|
73
|
+
export interface ProviderTarget {
|
|
74
|
+
model: string;
|
|
75
|
+
base_url: string;
|
|
76
|
+
/** The provider row that lists `model` (null = no provider claims it). */
|
|
77
|
+
provider_id: string | null;
|
|
78
|
+
model_source: ModelSource;
|
|
79
|
+
key_source: ProviderKeySource;
|
|
80
|
+
}
|
|
81
|
+
/** The request format that has a live adapter today (`ENGINE_FORMAT`). */
|
|
82
|
+
export declare const CHAT_COMPLETIONS_FORMAT = "chat_completions";
|
|
83
|
+
/** Every model id any provider row lists (the `default_model` allow-list). */
|
|
84
|
+
export declare function listedModelIds(lookup: ProviderLookup): Set<string>;
|
|
85
|
+
/** Resolve the model id + its source. Pure: no env mutation, no I/O. */
|
|
86
|
+
export declare function resolveModel(lookup: ProviderLookup, env: NodeJS.ProcessEnv, fallbackModel: string): {
|
|
87
|
+
model: string;
|
|
88
|
+
source: ModelSource;
|
|
89
|
+
};
|
|
90
|
+
/** The first provider row listing `model` (`find_provider_with_model`). */
|
|
91
|
+
export declare function ownerOf(lookup: ProviderLookup, model: string): ProviderRef | null;
|
|
92
|
+
/** `resolve_base_url(profile, env)` restricted to the TS channels. */
|
|
93
|
+
export declare function resolveBaseUrl(owner: ProviderRef | null, env: NodeJS.ProcessEnv, profileBaseUrl: string): string;
|
|
94
|
+
/**
|
|
95
|
+
* The key to authenticate with: the deployment's env key wins, a keyless env
|
|
96
|
+
* borrows the owning provider's stored key (injected into the process env by
|
|
97
|
+
* the caller). Blank values never count as "configured".
|
|
98
|
+
*/
|
|
99
|
+
export declare function resolveProviderKey(owner: ProviderRef | null, env: NodeJS.ProcessEnv, apiKeyEnv: string): {
|
|
100
|
+
key: string | null;
|
|
101
|
+
source: ProviderKeySource;
|
|
102
|
+
};
|
|
103
|
+
/** The three answers together, from the host's startup profile. */
|
|
104
|
+
export declare function resolveProviderTarget(lookup: ProviderLookup, env: NodeJS.ProcessEnv, base: ProfileSlot): ProviderTarget;
|
|
105
|
+
/**
|
|
106
|
+
* Apply a target to the startup profile and hand the secret to the process env.
|
|
107
|
+
* The key is written to `env[api_key_env]` ONLY — the returned profile and every
|
|
108
|
+
* log line stay key-free. Returns the profile plus the applied target.
|
|
109
|
+
*/
|
|
110
|
+
export declare function applyProviderTarget<P extends ProfileSlot>(base: P, target: ProviderTarget, lookup: ProviderLookup, env: NodeJS.ProcessEnv): {
|
|
111
|
+
profile: P;
|
|
112
|
+
target: ProviderTarget;
|
|
113
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup provider-target resolution (W511) — the TS port of the legacy
|
|
3
|
+
* `providers::apply_startup_default` + `resolve_base_url` + `resolve_api_key`.
|
|
4
|
+
*
|
|
5
|
+
* providers.json is the operator's registry of upstreams; resolving a target
|
|
6
|
+
* means answering three questions BEFORE the first compose (and again on every
|
|
7
|
+
* generation swap, from the host's own profile):
|
|
8
|
+
*
|
|
9
|
+
* model `default_model` IS IN the models list of some provider -> that id
|
|
10
|
+
* ("providers.json default_model");
|
|
11
|
+
* else env `CELESTEA_MODEL` -> "env CELESTEA_MODEL";
|
|
12
|
+
* else whatever the caller's profile already carried
|
|
13
|
+
* ("profile default", i.e. the celestea.toml slot).
|
|
14
|
+
* An unlisted `default_model` never wins: the contract validates it
|
|
15
|
+
* against the provider rows instead of trusting a stale string.
|
|
16
|
+
* base_url the provider that OWNS the resolved model, when its
|
|
17
|
+
* request_format is chat_completions and its base_url is non-empty
|
|
18
|
+
* (written into the profile before compose);
|
|
19
|
+
* else env `CELESTEA_BASE_URL` -> else the profile's own base_url.
|
|
20
|
+
* api key env[api_key_env] when non-empty; otherwise a plaintext key stored
|
|
21
|
+
* on the owning provider row is injected into the PROCESS ENV (the
|
|
22
|
+
* engine's only key channel, exactly like the engine's `env::set_var`).
|
|
23
|
+
* In-memory only: never written to a data file, never returned in a
|
|
24
|
+
* response, never logged.
|
|
25
|
+
*
|
|
26
|
+
* The module is store-free: it consumes the three `ProvidersStore` methods it
|
|
27
|
+
* needs, so the rules are unit-testable without a data file.
|
|
28
|
+
*
|
|
29
|
+
* W747: moved verbatim from `apps/studio/src/runtime/provider-target.ts` into the
|
|
30
|
+
* runtime's host layer (rules, export names and behaviour unchanged; the old
|
|
31
|
+
* path is now a re-export shim). The ONE edit is the profile slice below: the
|
|
32
|
+
* host view (`EngineProfile`) lives in `apps/studio/src/runtime-adapter.ts` and a
|
|
33
|
+
* package may not import an app, so the three fields this module actually reads
|
|
34
|
+
* are declared here as the minimal structural `ProfileSlot` that the host view
|
|
35
|
+
* already satisfies. `applyProviderTarget` is generic in it, so a caller holding
|
|
36
|
+
* an `EngineProfile` gets that exact type back.
|
|
37
|
+
*/
|
|
38
|
+
/** The request format that has a live adapter today (`ENGINE_FORMAT`). */
|
|
39
|
+
export const CHAT_COMPLETIONS_FORMAT = "chat_completions";
|
|
40
|
+
function trimmed(value) {
|
|
41
|
+
return typeof value === "string" ? value.trim() : "";
|
|
42
|
+
}
|
|
43
|
+
/** Every model id any provider row lists (the `default_model` allow-list). */
|
|
44
|
+
export function listedModelIds(lookup) {
|
|
45
|
+
const ids = new Set();
|
|
46
|
+
for (const p of lookup.rows())
|
|
47
|
+
for (const m of p.models)
|
|
48
|
+
ids.add(m.id);
|
|
49
|
+
return ids;
|
|
50
|
+
}
|
|
51
|
+
/** Resolve the model id + its source. Pure: no env mutation, no I/O. */
|
|
52
|
+
export function resolveModel(lookup, env, fallbackModel) {
|
|
53
|
+
const declared = trimmed(lookup.defaultModel());
|
|
54
|
+
if (declared !== "" && listedModelIds(lookup).has(declared)) {
|
|
55
|
+
return { model: declared, source: "providers.json default_model" };
|
|
56
|
+
}
|
|
57
|
+
const fromEnv = trimmed(env["CELESTEA_MODEL"]);
|
|
58
|
+
if (fromEnv !== "")
|
|
59
|
+
return { model: fromEnv, source: "env CELESTEA_MODEL" };
|
|
60
|
+
return { model: fallbackModel, source: "profile default" };
|
|
61
|
+
}
|
|
62
|
+
/** The first provider row listing `model` (`find_provider_with_model`). */
|
|
63
|
+
export function ownerOf(lookup, model) {
|
|
64
|
+
for (const p of lookup.rows()) {
|
|
65
|
+
if (p.models.some((m) => m.id === model))
|
|
66
|
+
return p;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
/** `resolve_base_url(profile, env)` restricted to the TS channels. */
|
|
71
|
+
export function resolveBaseUrl(owner, env, profileBaseUrl) {
|
|
72
|
+
if (owner !== null && owner.request_format === CHAT_COMPLETIONS_FORMAT && trimmed(owner.base_url) !== "") {
|
|
73
|
+
return owner.base_url;
|
|
74
|
+
}
|
|
75
|
+
const fromEnv = trimmed(env["CELESTEA_BASE_URL"]);
|
|
76
|
+
return fromEnv === "" ? profileBaseUrl : fromEnv;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The key to authenticate with: the deployment's env key wins, a keyless env
|
|
80
|
+
* borrows the owning provider's stored key (injected into the process env by
|
|
81
|
+
* the caller). Blank values never count as "configured".
|
|
82
|
+
*/
|
|
83
|
+
export function resolveProviderKey(owner, env, apiKeyEnv) {
|
|
84
|
+
if (trimmed(env[apiKeyEnv]) !== "")
|
|
85
|
+
return { key: trimmed(env[apiKeyEnv]), source: "env" };
|
|
86
|
+
const stored = trimmed(owner?.api_key);
|
|
87
|
+
return stored === "" ? { key: null, source: "none" } : { key: stored, source: "provider_store" };
|
|
88
|
+
}
|
|
89
|
+
/** The three answers together, from the host's startup profile. */
|
|
90
|
+
export function resolveProviderTarget(lookup, env, base) {
|
|
91
|
+
const { model, source } = resolveModel(lookup, env, base.model);
|
|
92
|
+
const owner = ownerOf(lookup, model);
|
|
93
|
+
const { source: keySource } = resolveProviderKey(owner, env, base.api_key_env);
|
|
94
|
+
return {
|
|
95
|
+
model,
|
|
96
|
+
base_url: resolveBaseUrl(owner, env, base.base_url),
|
|
97
|
+
provider_id: owner?.id ?? null,
|
|
98
|
+
model_source: source,
|
|
99
|
+
key_source: keySource,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Apply a target to the startup profile and hand the secret to the process env.
|
|
104
|
+
* The key is written to `env[api_key_env]` ONLY — the returned profile and every
|
|
105
|
+
* log line stay key-free. Returns the profile plus the applied target.
|
|
106
|
+
*/
|
|
107
|
+
export function applyProviderTarget(base, target, lookup, env) {
|
|
108
|
+
const owner = target.provider_id === null ? null : ownerOf(lookup, target.model);
|
|
109
|
+
const { key, source } = resolveProviderKey(owner, env, base.api_key_env);
|
|
110
|
+
if (key !== null)
|
|
111
|
+
env[base.api_key_env] = key;
|
|
112
|
+
return {
|
|
113
|
+
profile: { ...base, model: target.model, base_url: target.base_url },
|
|
114
|
+
target: { ...target, key_source: source },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The inbox's persistence sink over one session's `checkpoint.json` (E §1.3 P1 ①).
|
|
3
|
+
*
|
|
4
|
+
* WHY the checkpoint and not a file of its own: §1.2.1 fixes the classification —
|
|
5
|
+
* "everything derivable from `cli-main.jsonl` is NOT persisted twice" — and the
|
|
6
|
+
* two queue lanes plus the accepted-id ledger are exactly the facts the log
|
|
7
|
+
* cannot express. Putting them in the sidecar also means ONE atomic write path
|
|
8
|
+
* (tmp-<pid> + rename, mode 0600) for every non-log fact of the session.
|
|
9
|
+
*
|
|
10
|
+
* The sink is intentionally tiny and total: `load()` returns null for a missing,
|
|
11
|
+
* corrupt or foreign sidecar (a queue that cannot be read is simply empty — the
|
|
12
|
+
* same fail-safe discipline the recovery decision table uses), and `save()`
|
|
13
|
+
* never throws (the store reports its own failures on stderr).
|
|
14
|
+
*/
|
|
15
|
+
import type { CheckpointStore } from "@celestea/session";
|
|
16
|
+
import type { InboxSink } from "./inbox.js";
|
|
17
|
+
/** Bind an inbox to the session sidecar's two lanes + delivered-id ledger. */
|
|
18
|
+
export declare function checkpointInboxSink(store: CheckpointStore): InboxSink;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The inbox's persistence sink over one session's `checkpoint.json` (E §1.3 P1 ①).
|
|
3
|
+
*
|
|
4
|
+
* WHY the checkpoint and not a file of its own: §1.2.1 fixes the classification —
|
|
5
|
+
* "everything derivable from `cli-main.jsonl` is NOT persisted twice" — and the
|
|
6
|
+
* two queue lanes plus the accepted-id ledger are exactly the facts the log
|
|
7
|
+
* cannot express. Putting them in the sidecar also means ONE atomic write path
|
|
8
|
+
* (tmp-<pid> + rename, mode 0600) for every non-log fact of the session.
|
|
9
|
+
*
|
|
10
|
+
* The sink is intentionally tiny and total: `load()` returns null for a missing,
|
|
11
|
+
* corrupt or foreign sidecar (a queue that cannot be read is simply empty — the
|
|
12
|
+
* same fail-safe discipline the recovery decision table uses), and `save()`
|
|
13
|
+
* never throws (the store reports its own failures on stderr).
|
|
14
|
+
*/
|
|
15
|
+
/** Bind an inbox to the session sidecar's two lanes + delivered-id ledger. */
|
|
16
|
+
export function checkpointInboxSink(store) {
|
|
17
|
+
return {
|
|
18
|
+
load() {
|
|
19
|
+
const persisted = store.persistedQueues();
|
|
20
|
+
if (persisted === null)
|
|
21
|
+
return null;
|
|
22
|
+
return {
|
|
23
|
+
// Writing is structurally compatible (a message IS a lane message); the
|
|
24
|
+
// read direction narrows `lane`/`kind`/`source`, which only the inbox can
|
|
25
|
+
// guarantee — hence exactly one cast, at the boundary (K1: no sibling
|
|
26
|
+
// import just to share the type).
|
|
27
|
+
next_turn: persisted.lanes.next_turn,
|
|
28
|
+
next_step: persisted.lanes.next_step,
|
|
29
|
+
delivered_ids: persisted.delivered_ids,
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
save(snapshot) {
|
|
33
|
+
const write = (messages) => [...messages];
|
|
34
|
+
store.lanesChanged(write(snapshot.next_turn), write(snapshot.next_step), snapshot.delivered_ids);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|