@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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/agent-config.d.ts +18 -0
  4. package/dist/agent-config.js +31 -0
  5. package/dist/autowake.d.ts +141 -0
  6. package/dist/autowake.js +262 -0
  7. package/dist/compact/index.d.ts +13 -0
  8. package/dist/compact/index.js +13 -0
  9. package/dist/compact/plan.d.ts +51 -0
  10. package/dist/compact/plan.js +98 -0
  11. package/dist/compact/rewrite.d.ts +23 -0
  12. package/dist/compact/rewrite.js +79 -0
  13. package/dist/compact/run.d.ts +44 -0
  14. package/dist/compact/run.js +59 -0
  15. package/dist/compact/summarize.d.ts +30 -0
  16. package/dist/compact/summarize.js +70 -0
  17. package/dist/compact/transcript.d.ts +35 -0
  18. package/dist/compact/transcript.js +88 -0
  19. package/dist/compose.d.ts +117 -0
  20. package/dist/compose.js +191 -0
  21. package/dist/errors.d.ts +25 -0
  22. package/dist/errors.js +34 -0
  23. package/dist/frames.d.ts +46 -0
  24. package/dist/frames.js +62 -0
  25. package/dist/gen.d.ts +86 -0
  26. package/dist/gen.js +129 -0
  27. package/dist/host/engine-session.d.ts +117 -0
  28. package/dist/host/engine-session.js +109 -0
  29. package/dist/host/index.d.ts +39 -0
  30. package/dist/host/index.js +39 -0
  31. package/dist/host/provider-target.d.ts +113 -0
  32. package/dist/host/provider-target.js +116 -0
  33. package/dist/inbox-checkpoint.d.ts +18 -0
  34. package/dist/inbox-checkpoint.js +37 -0
  35. package/dist/inbox.d.ts +94 -0
  36. package/dist/inbox.js +139 -0
  37. package/dist/index.d.ts +71 -0
  38. package/dist/index.js +71 -0
  39. package/dist/ledger-io.d.ts +27 -0
  40. package/dist/ledger-io.js +74 -0
  41. package/dist/ledger-llm.d.ts +48 -0
  42. package/dist/ledger-llm.js +115 -0
  43. package/dist/ledger-query.d.ts +91 -0
  44. package/dist/ledger-query.js +153 -0
  45. package/dist/ledger.d.ts +271 -0
  46. package/dist/ledger.js +444 -0
  47. package/dist/pricing.d.ts +100 -0
  48. package/dist/pricing.js +167 -0
  49. package/dist/profile.d.ts +26 -0
  50. package/dist/profile.js +39 -0
  51. package/dist/recovery.d.ts +56 -0
  52. package/dist/recovery.js +91 -0
  53. package/dist/retention.d.ts +49 -0
  54. package/dist/retention.js +119 -0
  55. package/dist/runtime.d.ts +197 -0
  56. package/dist/runtime.js +347 -0
  57. package/dist/sanitize.d.ts +35 -0
  58. package/dist/sanitize.js +36 -0
  59. package/dist/session-binding.d.ts +36 -0
  60. package/dist/session-binding.js +33 -0
  61. package/dist/session-registry.d.ts +238 -0
  62. package/dist/session-registry.js +388 -0
  63. package/dist/status.d.ts +279 -0
  64. package/dist/status.js +411 -0
  65. package/dist/tokens.d.ts +25 -0
  66. package/dist/tokens.js +25 -0
  67. package/dist/turn-runner.d.ts +169 -0
  68. package/dist/turn-runner.js +242 -0
  69. package/dist/usage.d.ts +64 -0
  70. package/dist/usage.js +88 -0
  71. package/dist/watchdog-mount.d.ts +79 -0
  72. package/dist/watchdog-mount.js +120 -0
  73. package/dist/worker-wiring.d.ts +74 -0
  74. package/dist/worker-wiring.js +107 -0
  75. 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
+ }