@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,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W740 — the watchdog at the composition root.
|
|
3
|
+
*
|
|
4
|
+
* W736 built the liveness adjudicator (`@celestea/workers` `watchdog.ts`:
|
|
5
|
+
* keep-running / done / grace-deferred / respawned / failed / probe-error, with
|
|
6
|
+
* `WorkerRegistry.finalize` as the single terminal write point) but never mounted
|
|
7
|
+
* it, so nothing swept the RUNNING rows of the engine's own worker registry in
|
|
8
|
+
* production. This module is that mount — and nothing else:
|
|
9
|
+
*
|
|
10
|
+
* - it resolves the cadence and the retry/grace budgets from the environment
|
|
11
|
+
* (`celesteaWatchdogSettings`) with SAFE defaults, so a bad value degrades to
|
|
12
|
+
* the default instead of killing startup or hot-looping;
|
|
13
|
+
* - it mounts `watchdogPlugin` into the session's Context, which both schedules
|
|
14
|
+
* the sweep (interval timer, `unref`ed) and provides `WATCHDOG_SERVICE` for
|
|
15
|
+
* tool/host readers;
|
|
16
|
+
* - it hands back a stop function, so shutdown/dispose clears the timer (a
|
|
17
|
+
* sweep timer must never outlive the generation that owns the registry).
|
|
18
|
+
*
|
|
19
|
+
* SCOPE (W740 boundary, mirrors W736): the registry watched here is the STUDIO
|
|
20
|
+
* ENGINE's per-session, in-process worker registry. It is NOT the DSH plugin's
|
|
21
|
+
* cross-process `registry.tsv`; no `proc=` liveness rule and no cross-instance
|
|
22
|
+
* judgement is introduced. The watchdog only ever adjudicates `ownEntries()`.
|
|
23
|
+
*/
|
|
24
|
+
import { type Context } from "@celestea/core";
|
|
25
|
+
import { type Watchdog, type WorkerRegistry } from "@celestea/workers";
|
|
26
|
+
/** Mount name of the watchdog (kept distinct from the workers plugin's name). */
|
|
27
|
+
export declare const WATCHDOG_PLUGIN_NAME = "celestea.runtime.watchdog";
|
|
28
|
+
/** `CELESTEA_WATCHDOG=off` (or `0`/`false`/`no`) disables the mount entirely. */
|
|
29
|
+
export declare const WATCHDOG_ENV = "CELESTEA_WATCHDOG";
|
|
30
|
+
/** `CELESTEA_WATCHDOG_INTERVAL_MS` — sweep period (0 = off). */
|
|
31
|
+
export declare const WATCHDOG_INTERVAL_ENV = "CELESTEA_WATCHDOG_INTERVAL_MS";
|
|
32
|
+
/** `CELESTEA_WATCHDOG_MAX_RETRIES` — re-dispatch ceiling per worker. */
|
|
33
|
+
export declare const WATCHDOG_MAX_RETRIES_ENV = "CELESTEA_WATCHDOG_MAX_RETRIES";
|
|
34
|
+
/** `CELESTEA_WATCHDOG_GRACE_MS` — fresh-spawn grace before an anomaly is judged. */
|
|
35
|
+
export declare const WATCHDOG_GRACE_ENV = "CELESTEA_WATCHDOG_GRACE_MS";
|
|
36
|
+
/** Cadence + budgets + on/off of the mounted watchdog. */
|
|
37
|
+
export interface WatchdogMountSettings {
|
|
38
|
+
/** Start sweeping at mount time (false = the caller ticks by hand). */
|
|
39
|
+
autostart: boolean;
|
|
40
|
+
/** Sweep period in ms (`> 0`; 0 is never produced — it means "off"). */
|
|
41
|
+
intervalMs: number;
|
|
42
|
+
maxRetries: number;
|
|
43
|
+
graceMs: number;
|
|
44
|
+
/** Append-only verdict log (`null` = the library default: no deployment path). */
|
|
45
|
+
watcherLog: string | null;
|
|
46
|
+
alertsLog: string | null;
|
|
47
|
+
}
|
|
48
|
+
/** Defaults: ON, W736's production cadence, library log paths (off). */
|
|
49
|
+
export declare const WATCHDOG_MOUNT_DEFAULTS: WatchdogMountSettings;
|
|
50
|
+
/** `off` / `0` / `false` / `no` (case-insensitive) mean "do not mount". */
|
|
51
|
+
export declare function watchdogDisabled(env: NodeJS.ProcessEnv): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Cadence and budgets from the environment. Every input is validated: a
|
|
54
|
+
* non-numeric, negative or non-positive-period value falls back to the default,
|
|
55
|
+
* so a typo can neither abort composition nor spin the sweep in a hot loop.
|
|
56
|
+
*/
|
|
57
|
+
export declare function celesteaWatchdogSettings(env: NodeJS.ProcessEnv): WatchdogMountSettings;
|
|
58
|
+
/** What the composition root keeps after mounting the watchdog. */
|
|
59
|
+
export interface MountedWatchdog {
|
|
60
|
+
/** The adjudicator the plugin provided (the host ticks it by hand in tests). */
|
|
61
|
+
watchdog: Watchdog;
|
|
62
|
+
/** Clear the sweep timer (idempotent; shutdown/dispose calls it). */
|
|
63
|
+
stop: () => void;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Mount the watchdog over one composed worker registry. Returns null when the
|
|
67
|
+
* watchdog is switched off — in that case NO service is provided and NO timer
|
|
68
|
+
* exists, which is what makes "off" observable rather than merely quiet.
|
|
69
|
+
*
|
|
70
|
+
* `settings.autostart: false` still mounts (and still provides the token); it
|
|
71
|
+
* only leaves the cadence to the caller, which is how a test drives `tick()`.
|
|
72
|
+
*/
|
|
73
|
+
export declare function mountWatchdog(ctx: Context, registry: WorkerRegistry, settings?: Partial<WatchdogMountSettings>): MountedWatchdog | null;
|
|
74
|
+
/**
|
|
75
|
+
* The shutdown hook of a mounted watchdog: clearing the interval is the whole
|
|
76
|
+
* job, and it is idempotent, so it can be registered as a host teardown hook
|
|
77
|
+
* without a second bookkeeping path. `null` (watchdog off) is a no-op.
|
|
78
|
+
*/
|
|
79
|
+
export declare function stopWatchdog(mounted: MountedWatchdog | null): () => void;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W740 — the watchdog at the composition root.
|
|
3
|
+
*
|
|
4
|
+
* W736 built the liveness adjudicator (`@celestea/workers` `watchdog.ts`:
|
|
5
|
+
* keep-running / done / grace-deferred / respawned / failed / probe-error, with
|
|
6
|
+
* `WorkerRegistry.finalize` as the single terminal write point) but never mounted
|
|
7
|
+
* it, so nothing swept the RUNNING rows of the engine's own worker registry in
|
|
8
|
+
* production. This module is that mount — and nothing else:
|
|
9
|
+
*
|
|
10
|
+
* - it resolves the cadence and the retry/grace budgets from the environment
|
|
11
|
+
* (`celesteaWatchdogSettings`) with SAFE defaults, so a bad value degrades to
|
|
12
|
+
* the default instead of killing startup or hot-looping;
|
|
13
|
+
* - it mounts `watchdogPlugin` into the session's Context, which both schedules
|
|
14
|
+
* the sweep (interval timer, `unref`ed) and provides `WATCHDOG_SERVICE` for
|
|
15
|
+
* tool/host readers;
|
|
16
|
+
* - it hands back a stop function, so shutdown/dispose clears the timer (a
|
|
17
|
+
* sweep timer must never outlive the generation that owns the registry).
|
|
18
|
+
*
|
|
19
|
+
* SCOPE (W740 boundary, mirrors W736): the registry watched here is the STUDIO
|
|
20
|
+
* ENGINE's per-session, in-process worker registry. It is NOT the DSH plugin's
|
|
21
|
+
* cross-process `registry.tsv`; no `proc=` liveness rule and no cross-instance
|
|
22
|
+
* judgement is introduced. The watchdog only ever adjudicates `ownEntries()`.
|
|
23
|
+
*/
|
|
24
|
+
import { mountPlugins } from "@celestea/core";
|
|
25
|
+
import { WATCHDOG_DEFAULTS, WATCHDOG_SERVICE, watchdogPlugin, } from "@celestea/workers";
|
|
26
|
+
/** Mount name of the watchdog (kept distinct from the workers plugin's name). */
|
|
27
|
+
export const WATCHDOG_PLUGIN_NAME = "celestea.runtime.watchdog";
|
|
28
|
+
/** `CELESTEA_WATCHDOG=off` (or `0`/`false`/`no`) disables the mount entirely. */
|
|
29
|
+
export const WATCHDOG_ENV = "CELESTEA_WATCHDOG";
|
|
30
|
+
/** `CELESTEA_WATCHDOG_INTERVAL_MS` — sweep period (0 = off). */
|
|
31
|
+
export const WATCHDOG_INTERVAL_ENV = "CELESTEA_WATCHDOG_INTERVAL_MS";
|
|
32
|
+
/** `CELESTEA_WATCHDOG_MAX_RETRIES` — re-dispatch ceiling per worker. */
|
|
33
|
+
export const WATCHDOG_MAX_RETRIES_ENV = "CELESTEA_WATCHDOG_MAX_RETRIES";
|
|
34
|
+
/** `CELESTEA_WATCHDOG_GRACE_MS` — fresh-spawn grace before an anomaly is judged. */
|
|
35
|
+
export const WATCHDOG_GRACE_ENV = "CELESTEA_WATCHDOG_GRACE_MS";
|
|
36
|
+
/** Defaults: ON, W736's production cadence, library log paths (off). */
|
|
37
|
+
export const WATCHDOG_MOUNT_DEFAULTS = {
|
|
38
|
+
autostart: true,
|
|
39
|
+
intervalMs: WATCHDOG_DEFAULTS.intervalMs,
|
|
40
|
+
maxRetries: WATCHDOG_DEFAULTS.maxRetries,
|
|
41
|
+
graceMs: WATCHDOG_DEFAULTS.graceMs,
|
|
42
|
+
watcherLog: null,
|
|
43
|
+
alertsLog: null,
|
|
44
|
+
};
|
|
45
|
+
/** `off` / `0` / `false` / `no` (case-insensitive) mean "do not mount". */
|
|
46
|
+
export function watchdogDisabled(env) {
|
|
47
|
+
const raw = (env[WATCHDOG_ENV] ?? "").trim().toLowerCase();
|
|
48
|
+
return raw === "off" || raw === "0" || raw === "false" || raw === "no";
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Cadence and budgets from the environment. Every input is validated: a
|
|
52
|
+
* non-numeric, negative or non-positive-period value falls back to the default,
|
|
53
|
+
* so a typo can neither abort composition nor spin the sweep in a hot loop.
|
|
54
|
+
*/
|
|
55
|
+
export function celesteaWatchdogSettings(env) {
|
|
56
|
+
if (watchdogDisabled(env)) {
|
|
57
|
+
return { ...WATCHDOG_MOUNT_DEFAULTS, autostart: false, intervalMs: 0 };
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
autostart: true,
|
|
61
|
+
intervalMs: positiveFrom(env, WATCHDOG_INTERVAL_ENV, WATCHDOG_MOUNT_DEFAULTS.intervalMs),
|
|
62
|
+
maxRetries: countFrom(env, WATCHDOG_MAX_RETRIES_ENV, WATCHDOG_MOUNT_DEFAULTS.maxRetries),
|
|
63
|
+
graceMs: positiveFrom(env, WATCHDOG_GRACE_ENV, WATCHDOG_MOUNT_DEFAULTS.graceMs),
|
|
64
|
+
watcherLog: null,
|
|
65
|
+
alertsLog: null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Mount the watchdog over one composed worker registry. Returns null when the
|
|
70
|
+
* watchdog is switched off — in that case NO service is provided and NO timer
|
|
71
|
+
* exists, which is what makes "off" observable rather than merely quiet.
|
|
72
|
+
*
|
|
73
|
+
* `settings.autostart: false` still mounts (and still provides the token); it
|
|
74
|
+
* only leaves the cadence to the caller, which is how a test drives `tick()`.
|
|
75
|
+
*/
|
|
76
|
+
export function mountWatchdog(ctx, registry, settings = {}) {
|
|
77
|
+
const cfg = { ...WATCHDOG_MOUNT_DEFAULTS, ...settings };
|
|
78
|
+
// P2-1 (W836): `autostart:false` still mounts (and still provides the token);
|
|
79
|
+
// only a non-positive period means "off". Returning null here contradicted the
|
|
80
|
+
// documented contract above and made a manual `tick()` impossible.
|
|
81
|
+
if (cfg.intervalMs <= 0)
|
|
82
|
+
return null;
|
|
83
|
+
const config = {
|
|
84
|
+
intervalMs: cfg.intervalMs,
|
|
85
|
+
resultsDir: registry.resultsDir,
|
|
86
|
+
maxRetries: cfg.maxRetries,
|
|
87
|
+
graceMs: cfg.graceMs,
|
|
88
|
+
watcherLog: cfg.watcherLog,
|
|
89
|
+
alertsLog: cfg.alertsLog,
|
|
90
|
+
};
|
|
91
|
+
mountPlugins(ctx, [watchdogPlugin({ registry, config, autostart: cfg.autostart, name: WATCHDOG_PLUGIN_NAME })]);
|
|
92
|
+
const watchdog = ctx.get(WATCHDOG_SERVICE);
|
|
93
|
+
if (watchdog === undefined)
|
|
94
|
+
return null;
|
|
95
|
+
return { watchdog, stop: () => watchdog.stop() };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The shutdown hook of a mounted watchdog: clearing the interval is the whole
|
|
99
|
+
* job, and it is idempotent, so it can be registered as a host teardown hook
|
|
100
|
+
* without a second bookkeeping path. `null` (watchdog off) is a no-op.
|
|
101
|
+
*/
|
|
102
|
+
export function stopWatchdog(mounted) {
|
|
103
|
+
return () => mounted?.stop();
|
|
104
|
+
}
|
|
105
|
+
/** Non-negative integer from the env, else the fallback (0 is allowed). */
|
|
106
|
+
function countFrom(env, name, fallback) {
|
|
107
|
+
const raw = (env[name] ?? "").trim();
|
|
108
|
+
if (raw === "")
|
|
109
|
+
return fallback;
|
|
110
|
+
const value = Number.parseInt(raw, 10);
|
|
111
|
+
return Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
112
|
+
}
|
|
113
|
+
/** Strictly positive integer from the env, else the fallback (0 means "off"). */
|
|
114
|
+
function positiveFrom(env, name, fallback) {
|
|
115
|
+
const raw = (env[name] ?? "").trim();
|
|
116
|
+
if (raw === "")
|
|
117
|
+
return fallback;
|
|
118
|
+
const value = Number.parseInt(raw, 10);
|
|
119
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
120
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker wiring — the part of composition that only `runtime` can do.
|
|
3
|
+
*
|
|
4
|
+
* Layer rules say an L1 package never imports another L1 package, so
|
|
5
|
+
* `packages/workers` cannot reach the `Llm` / `ToolRegistry` / `AgentLoop`
|
|
6
|
+
* implementations and cannot build a session log with a real projection. The
|
|
7
|
+
* composition root can: it resolves the three driver seams from the Context and
|
|
8
|
+
* hands them to the registry (`attach_drivers`, W206/W232), injects the
|
|
9
|
+
* worker session log factory, registers the HOST conversation so worker
|
|
10
|
+
* receipts have an address, and returns the drain hook the turn runner uses.
|
|
11
|
+
*
|
|
12
|
+
* Nothing here is mandatory: with `workers: false` the generation runs without
|
|
13
|
+
* orchestration and every hook degrades to a no-op.
|
|
14
|
+
*/
|
|
15
|
+
import { Context } from "@celestea/core";
|
|
16
|
+
import { WorkerRegistry, type SessionLogFactory, type Watchdog, type WorkerDrivers } from "@celestea/workers";
|
|
17
|
+
import type { PendingReceipt } from "./turn-runner.js";
|
|
18
|
+
export interface WorkerWiring {
|
|
19
|
+
/** `false` disables orchestration wiring entirely (default: enabled). */
|
|
20
|
+
enabled?: boolean;
|
|
21
|
+
/** Pre-built registry (the host owns it); otherwise one is created + mounted. */
|
|
22
|
+
registry?: WorkerRegistry;
|
|
23
|
+
/**
|
|
24
|
+
* `null` = in-memory table only. Omitted = `REGISTRY_TSV_PATH` (the legacy
|
|
25
|
+
* shared `/tmp` table). E §2.3 P0 ①: the studio passes an EXPLICIT path
|
|
26
|
+
* (`<data dir>/worker-registry.tsv`, env `CELESTEA_WORKER_REGISTRY`) so it can
|
|
27
|
+
* never write the table of the DSH-side fleet (B6/R2-1).
|
|
28
|
+
*/
|
|
29
|
+
tsvPath?: string | null;
|
|
30
|
+
resultsDir?: string;
|
|
31
|
+
sourceLabel?: string;
|
|
32
|
+
/** Worker session log factory (default: `InMemorySessionLog`). */
|
|
33
|
+
logFactory?: SessionLogFactory;
|
|
34
|
+
/** Host conversation id (default `cli-main`; the host passes the session id). */
|
|
35
|
+
hostSessionId?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Prefix of the worker session ids this registry mints (`session-` by
|
|
38
|
+
* default). Every session runtime owns its OWN registry since W513, so the
|
|
39
|
+
* host passes a session-derived prefix: `worker:<sid>` ids stay unique in the
|
|
40
|
+
* merged `GET /api/sessions` list.
|
|
41
|
+
*/
|
|
42
|
+
sessionIdPrefix?: string;
|
|
43
|
+
/** Model token recorded on the host session meta. */
|
|
44
|
+
hostModel?: string | null;
|
|
45
|
+
/**
|
|
46
|
+
* W729 §2.3: the host session's mode, inherited by every spawn that does not
|
|
47
|
+
* pass one. `null` = the host declared none (the spawn then records none).
|
|
48
|
+
*/
|
|
49
|
+
hostMode?: string | null;
|
|
50
|
+
}
|
|
51
|
+
export interface WorkerHost {
|
|
52
|
+
registry: WorkerRegistry;
|
|
53
|
+
hostSessionId: string;
|
|
54
|
+
/** Name of the plugin this wiring mounted (null when a host plugin provided it). */
|
|
55
|
+
mountedPlugin: string | null;
|
|
56
|
+
/** Poll the host mailbox: the messages to inject at the next boundary. */
|
|
57
|
+
drain: () => PendingReceipt[];
|
|
58
|
+
/** Messages waiting in the host mailbox (the loop's close guard reads it). */
|
|
59
|
+
pending: () => number;
|
|
60
|
+
/** Attach driver seams so a spawn is driven, not merely registered. */
|
|
61
|
+
attach: (drivers: WorkerDrivers | null) => boolean;
|
|
62
|
+
/**
|
|
63
|
+
* W740: the liveness watchdog mounted over THIS registry by the composition
|
|
64
|
+
* root (`compose()` attaches it after this wiring ran), or null when the
|
|
65
|
+
* watchdog is off. The service itself also lives in the Context under
|
|
66
|
+
* `WATCHDOG_SERVICE`; this handle is what a host ticks by hand.
|
|
67
|
+
*/
|
|
68
|
+
watchdog?: Watchdog | null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the registry: an already-mounted one wins (a host plugin provided it),
|
|
72
|
+
* otherwise create and mount the default alone. Returns null when disabled.
|
|
73
|
+
*/
|
|
74
|
+
export declare function ensureWorkerWiring(ctx: Context, wiring: WorkerWiring | false | undefined): WorkerHost | null;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker wiring — the part of composition that only `runtime` can do.
|
|
3
|
+
*
|
|
4
|
+
* Layer rules say an L1 package never imports another L1 package, so
|
|
5
|
+
* `packages/workers` cannot reach the `Llm` / `ToolRegistry` / `AgentLoop`
|
|
6
|
+
* implementations and cannot build a session log with a real projection. The
|
|
7
|
+
* composition root can: it resolves the three driver seams from the Context and
|
|
8
|
+
* hands them to the registry (`attach_drivers`, W206/W232), injects the
|
|
9
|
+
* worker session log factory, registers the HOST conversation so worker
|
|
10
|
+
* receipts have an address, and returns the drain hook the turn runner uses.
|
|
11
|
+
*
|
|
12
|
+
* Nothing here is mandatory: with `workers: false` the generation runs without
|
|
13
|
+
* orchestration and every hook degrades to a no-op.
|
|
14
|
+
*/
|
|
15
|
+
import { Context } from "@celestea/core";
|
|
16
|
+
import { InMemorySessionLog } from "@celestea/session";
|
|
17
|
+
import { WorkerRegistry, WORKER_REGISTRY_SERVICE, workersPlugin, } from "@celestea/workers";
|
|
18
|
+
import { mountPlugins } from "@celestea/core";
|
|
19
|
+
import { HOST_SESSION_ID, RESULTS_DIR } from "./tokens.js";
|
|
20
|
+
const DEFAULT_WORKER_PLUGIN = "celestea.runtime.workers";
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the registry: an already-mounted one wins (a host plugin provided it),
|
|
23
|
+
* otherwise create and mount the default alone. Returns null when disabled.
|
|
24
|
+
*/
|
|
25
|
+
export function ensureWorkerWiring(ctx, wiring) {
|
|
26
|
+
if (wiring === false || wiring?.enabled === false)
|
|
27
|
+
return null;
|
|
28
|
+
const provided = ctx.get(WORKER_REGISTRY_SERVICE);
|
|
29
|
+
const hostSessionId = wiring?.hostSessionId ?? HOST_SESSION_ID;
|
|
30
|
+
const registry = provided ?? mountDefault(ctx, wiring ?? {}, hostSessionId);
|
|
31
|
+
registerHost(registry, hostSessionId, wiring?.hostModel ?? null, wiring?.hostMode ?? null);
|
|
32
|
+
return {
|
|
33
|
+
registry,
|
|
34
|
+
hostSessionId,
|
|
35
|
+
mountedPlugin: provided === undefined ? DEFAULT_WORKER_PLUGIN : null,
|
|
36
|
+
drain: () => drainHost(registry, hostSessionId),
|
|
37
|
+
pending: () => registry.mailbox.pending(hostSessionId),
|
|
38
|
+
attach: (drivers) => attachDrivers(registry, drivers),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function mountDefault(ctx, wiring, hostSessionId) {
|
|
42
|
+
const registry = new WorkerRegistry({
|
|
43
|
+
tsvPath: wiring.tsvPath === undefined ? undefined : wiring.tsvPath,
|
|
44
|
+
// E §2.2.2 (`host=`): the row records which conversation dispatched it.
|
|
45
|
+
hostSessionId,
|
|
46
|
+
resultsDir: wiring.resultsDir ?? RESULTS_DIR,
|
|
47
|
+
sourceLabel: wiring.sourceLabel ?? "celestea.runtime",
|
|
48
|
+
logFactory: wiring.logFactory ?? (() => new InMemorySessionLog()),
|
|
49
|
+
...(wiring.sessionIdPrefix === undefined ? {} : { sessionIdPrefix: wiring.sessionIdPrefix }),
|
|
50
|
+
hostMode: wiring.hostMode ?? null,
|
|
51
|
+
});
|
|
52
|
+
mountPlugins(ctx, [workersPlugin({ registry, name: DEFAULT_WORKER_PLUGIN })]);
|
|
53
|
+
return registry;
|
|
54
|
+
}
|
|
55
|
+
/** Register the host conversation so receipts can be addressed to it (W232). */
|
|
56
|
+
function registerHost(registry, hostSessionId, model, mode) {
|
|
57
|
+
const host = {
|
|
58
|
+
meta: { id: hostSessionId, title: hostSessionId, workspace: null, model, mode },
|
|
59
|
+
log: new InMemorySessionLog(),
|
|
60
|
+
};
|
|
61
|
+
registry.registerHostSession(host);
|
|
62
|
+
// The spawn default: a worker inherits the mode of its owning session (W729).
|
|
63
|
+
registry.setHostMode(mode);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* FIFO drain of the host queue, annotated with the sender label, the envelope
|
|
67
|
+
* (`source`) and a stable key (W515 §3/§4).
|
|
68
|
+
*
|
|
69
|
+
* W840 (R2 N1): the key is NOT a duplicate filter on this path. The drained
|
|
70
|
+
* messages are concatenated straight into the turn at compose.ts (never pushed
|
|
71
|
+
* through `SessionInbox.push`), so the inbox B3 rule never sees them; the id only
|
|
72
|
+
* surfaces in the SSE `context` frame (session-publisher.ts). The single
|
|
73
|
+
* once-only gate for a receipt is WorkerRegistry.closeLoop's durable
|
|
74
|
+
* `receipt:<wid>:<attempt>` token (registry.ts, receiptDelivered).
|
|
75
|
+
*/
|
|
76
|
+
function drainHost(registry, hostSessionId) {
|
|
77
|
+
return registry.mailbox.poll(hostSessionId).map((m) => ({
|
|
78
|
+
text: m.content,
|
|
79
|
+
from: m.from_label,
|
|
80
|
+
id: idempotencyKeyOf(registry, m),
|
|
81
|
+
kind: m.kind,
|
|
82
|
+
source: m.source ?? { kind: "worker-relay", form: "message", senderSessionId: m.from_label },
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* E §2.2.3 (2-P1 ②): the stable key of a drained message.
|
|
87
|
+
*
|
|
88
|
+
* A RECEIPT is keyed by `receipt:<wid>:<attempt>` — stable across processes, so
|
|
89
|
+
* the SSE `context` frame for a replayed receipt carries the same id. W840
|
|
90
|
+
* (R2 N1): this key does NOT deduplicate on the production injection path — the
|
|
91
|
+
* drained receipts are appended straight to the turn (compose.ts), bypassing
|
|
92
|
+
* `SessionInbox.push` and its B3 rule; closeLoop's durable receiptDelivered token
|
|
93
|
+
* is the only once-only gate. Routing receipts through the inbox (to make B3
|
|
94
|
+
* apply here) is a semantics change left for adjudication. Every other message
|
|
95
|
+
* (a deliberate relay, a stimulus) keeps the in-process mailbox sequence: keying
|
|
96
|
+
* a relay by `(wid, attempt)` would silently drop a SECOND intentional message.
|
|
97
|
+
*/
|
|
98
|
+
function idempotencyKeyOf(registry, message) {
|
|
99
|
+
const receipt = message.kind === "receipt" ? registry.receiptKeyFor(message.from_label) : null;
|
|
100
|
+
return receipt ?? `mailbox:${message.id}`;
|
|
101
|
+
}
|
|
102
|
+
function attachDrivers(registry, drivers) {
|
|
103
|
+
if (drivers === null)
|
|
104
|
+
return false;
|
|
105
|
+
registry.attachDrivers(drivers);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@celestea/runtime",
|
|
3
|
+
"version": "2.7.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"default": "./dist/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@celestea/agent-loop": "2.7.1",
|
|
14
|
+
"@celestea/core": "2.7.1",
|
|
15
|
+
"@celestea/session": "2.7.1",
|
|
16
|
+
"@celestea/workers": "2.7.1"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"main": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
29
|
+
"build": "tsc -p tsconfig.build.json"
|
|
30
|
+
}
|
|
31
|
+
}
|