@celestea/studio 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/dist/app.d.ts +111 -0
- package/dist/app.js +219 -0
- package/dist/auth/api-token.d.ts +76 -0
- package/dist/auth/api-token.js +172 -0
- package/dist/auth/htpasswd.d.ts +22 -0
- package/dist/auth/htpasswd.js +61 -0
- package/dist/auth/index.d.ts +14 -0
- package/dist/auth/index.js +14 -0
- package/dist/auth/page.d.ts +19 -0
- package/dist/auth/page.js +73 -0
- package/dist/auth/rate-limit.d.ts +27 -0
- package/dist/auth/rate-limit.js +47 -0
- package/dist/auth/token.d.ts +49 -0
- package/dist/auth/token.js +112 -0
- package/dist/config.d.ts +78 -0
- package/dist/config.js +72 -0
- package/dist/deployment.d.ts +92 -0
- package/dist/deployment.js +154 -0
- package/dist/fake-runtime-adapter.d.ts +37 -0
- package/dist/fake-runtime-adapter.js +276 -0
- package/dist/handlers/auth.d.ts +24 -0
- package/dist/handlers/auth.js +155 -0
- package/dist/handlers/common.d.ts +76 -0
- package/dist/handlers/common.js +127 -0
- package/dist/handlers/config-shape.d.ts +76 -0
- package/dist/handlers/config-shape.js +227 -0
- package/dist/handlers/config.d.ts +26 -0
- package/dist/handlers/config.js +161 -0
- package/dist/handlers/context-shape.d.ts +33 -0
- package/dist/handlers/context-shape.js +59 -0
- package/dist/handlers/dialog.d.ts +22 -0
- package/dist/handlers/dialog.js +292 -0
- package/dist/handlers/exec.d.ts +22 -0
- package/dist/handlers/exec.js +131 -0
- package/dist/handlers/fs-read.d.ts +63 -0
- package/dist/handlers/fs-read.js +159 -0
- package/dist/handlers/fs.d.ts +57 -0
- package/dist/handlers/fs.js +153 -0
- package/dist/handlers/grants-shape.d.ts +45 -0
- package/dist/handlers/grants-shape.js +130 -0
- package/dist/handlers/grants.d.ts +21 -0
- package/dist/handlers/grants.js +267 -0
- package/dist/handlers/health.d.ts +37 -0
- package/dist/handlers/health.js +139 -0
- package/dist/handlers/index.d.ts +40 -0
- package/dist/handlers/index.js +85 -0
- package/dist/handlers/permissions.d.ts +9 -0
- package/dist/handlers/permissions.js +183 -0
- package/dist/handlers/plugins.d.ts +18 -0
- package/dist/handlers/plugins.js +23 -0
- package/dist/handlers/prompts.d.ts +12 -0
- package/dist/handlers/prompts.js +120 -0
- package/dist/handlers/providers.d.ts +12 -0
- package/dist/handlers/providers.js +178 -0
- package/dist/handlers/questions.d.ts +23 -0
- package/dist/handlers/questions.js +115 -0
- package/dist/handlers/session-model.d.ts +38 -0
- package/dist/handlers/session-model.js +105 -0
- package/dist/handlers/session-move.d.ts +22 -0
- package/dist/handlers/session-move.js +188 -0
- package/dist/handlers/session-tools.d.ts +19 -0
- package/dist/handlers/session-tools.js +75 -0
- package/dist/handlers/sessions.d.ts +32 -0
- package/dist/handlers/sessions.js +242 -0
- package/dist/handlers/usage.d.ts +26 -0
- package/dist/handlers/usage.js +77 -0
- package/dist/handlers/worker.d.ts +12 -0
- package/dist/handlers/worker.js +82 -0
- package/dist/handlers/workspaces.d.ts +12 -0
- package/dist/handlers/workspaces.js +88 -0
- package/dist/harness.test-util.d.ts +101 -0
- package/dist/harness.test-util.js +176 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +33 -0
- package/dist/main.d.ts +13 -0
- package/dist/main.js +35 -0
- package/dist/plugins.d.ts +105 -0
- package/dist/plugins.js +97 -0
- package/dist/question-registry.d.ts +124 -0
- package/dist/question-registry.js +212 -0
- package/dist/question-rows.d.ts +32 -0
- package/dist/question-rows.js +43 -0
- package/dist/replay/compare.d.ts +47 -0
- package/dist/replay/compare.js +89 -0
- package/dist/replay/e2e-replay.d.ts +46 -0
- package/dist/replay/e2e-replay.js +91 -0
- package/dist/replay/expect-compact.d.ts +31 -0
- package/dist/replay/expect-compact.js +92 -0
- package/dist/replay/fixtures.d.ts +52 -0
- package/dist/replay/fixtures.js +53 -0
- package/dist/replay/host.d.ts +32 -0
- package/dist/replay/host.js +62 -0
- package/dist/replay/index.d.ts +16 -0
- package/dist/replay/index.js +16 -0
- package/dist/replay/probes.d.ts +55 -0
- package/dist/replay/probes.js +225 -0
- package/dist/replay/report.d.ts +13 -0
- package/dist/replay/report.js +103 -0
- package/dist/replay/session-e2e.d.ts +44 -0
- package/dist/replay/session-e2e.js +91 -0
- package/dist/routes.d.ts +47 -0
- package/dist/routes.js +59 -0
- package/dist/runtime/attachments-llm.d.ts +13 -0
- package/dist/runtime/attachments-llm.js +37 -0
- package/dist/runtime/boot-recovery.d.ts +32 -0
- package/dist/runtime/boot-recovery.js +71 -0
- package/dist/runtime/context-snapshot.d.ts +59 -0
- package/dist/runtime/context-snapshot.js +113 -0
- package/dist/runtime/engine-grants.d.ts +121 -0
- package/dist/runtime/engine-grants.js +338 -0
- package/dist/runtime/engine-permissions.d.ts +29 -0
- package/dist/runtime/engine-permissions.js +92 -0
- package/dist/runtime/engine-plugins.d.ts +189 -0
- package/dist/runtime/engine-plugins.js +292 -0
- package/dist/runtime/engine-profile.d.ts +28 -0
- package/dist/runtime/engine-profile.js +88 -0
- package/dist/runtime/engine-session.d.ts +9 -0
- package/dist/runtime/engine-session.js +9 -0
- package/dist/runtime/fallback-contract.d.ts +32 -0
- package/dist/runtime/fallback-contract.js +10 -0
- package/dist/runtime/fallback-host.d.ts +134 -0
- package/dist/runtime/fallback-host.js +299 -0
- package/dist/runtime/host-autowake.d.ts +79 -0
- package/dist/runtime/host-autowake.js +86 -0
- package/dist/runtime/image-downgrade.d.ts +49 -0
- package/dist/runtime/image-downgrade.js +90 -0
- package/dist/runtime/inbox-message.d.ts +11 -0
- package/dist/runtime/inbox-message.js +20 -0
- package/dist/runtime/index.d.ts +17 -0
- package/dist/runtime/index.js +17 -0
- package/dist/runtime/ledger-view.d.ts +40 -0
- package/dist/runtime/ledger-view.js +56 -0
- package/dist/runtime/llm-assembly.d.ts +66 -0
- package/dist/runtime/llm-assembly.js +114 -0
- package/dist/runtime/offline-llm.d.ts +71 -0
- package/dist/runtime/offline-llm.js +132 -0
- package/dist/runtime/provider-target.d.ts +10 -0
- package/dist/runtime/provider-target.js +10 -0
- package/dist/runtime/question-host.d.ts +33 -0
- package/dist/runtime/question-host.js +41 -0
- package/dist/runtime/question-view.d.ts +48 -0
- package/dist/runtime/question-view.js +76 -0
- package/dist/runtime/real-runtime-adapter.d.ts +95 -0
- package/dist/runtime/real-runtime-adapter.js +553 -0
- package/dist/runtime/recovery-audit.d.ts +81 -0
- package/dist/runtime/recovery-audit.js +95 -0
- package/dist/runtime/recovery-view.d.ts +36 -0
- package/dist/runtime/recovery-view.js +63 -0
- package/dist/runtime/session-compose.d.ts +235 -0
- package/dist/runtime/session-compose.js +350 -0
- package/dist/runtime/session-grants.d.ts +36 -0
- package/dist/runtime/session-grants.js +69 -0
- package/dist/runtime/session-lifecycle.d.ts +40 -0
- package/dist/runtime/session-lifecycle.js +86 -0
- package/dist/runtime/session-publisher.d.ts +40 -0
- package/dist/runtime/session-publisher.js +48 -0
- package/dist/runtime/session-release.d.ts +53 -0
- package/dist/runtime/session-release.js +80 -0
- package/dist/runtime/test-util.d.ts +78 -0
- package/dist/runtime/test-util.js +162 -0
- package/dist/runtime/watchdog-view.d.ts +33 -0
- package/dist/runtime/watchdog-view.js +45 -0
- package/dist/runtime/worker-bridge.d.ts +76 -0
- package/dist/runtime/worker-bridge.js +156 -0
- package/dist/runtime/worker-live.d.ts +22 -0
- package/dist/runtime/worker-live.js +25 -0
- package/dist/runtime/worker-recovery.d.ts +35 -0
- package/dist/runtime/worker-recovery.js +64 -0
- package/dist/runtime/worker-table.d.ts +58 -0
- package/dist/runtime/worker-table.js +70 -0
- package/dist/runtime-adapter.d.ts +421 -0
- package/dist/runtime-adapter.js +76 -0
- package/dist/serial-queue.d.ts +16 -0
- package/dist/serial-queue.js +22 -0
- package/dist/server.d.ts +44 -0
- package/dist/server.js +119 -0
- package/dist/settings.d.ts +23 -0
- package/dist/settings.js +31 -0
- package/dist/sse.d.ts +67 -0
- package/dist/sse.js +186 -0
- package/dist/static.d.ts +20 -0
- package/dist/static.js +120 -0
- package/dist/store/builtin-sections.d.ts +52 -0
- package/dist/store/builtin-sections.js +118 -0
- package/dist/store/celestea-home.d.ts +12 -0
- package/dist/store/celestea-home.js +11 -0
- package/dist/store/fs-json.d.ts +51 -0
- package/dist/store/fs-json.js +119 -0
- package/dist/store/grants-audit.d.ts +84 -0
- package/dist/store/grants-audit.js +109 -0
- package/dist/store/grants-service.d.ts +44 -0
- package/dist/store/grants-service.js +41 -0
- package/dist/store/grants-tokens.d.ts +62 -0
- package/dist/store/grants-tokens.js +105 -0
- package/dist/store/grants.d.ts +113 -0
- package/dist/store/grants.js +238 -0
- package/dist/store/index.d.ts +36 -0
- package/dist/store/index.js +36 -0
- package/dist/store/mode.d.ts +35 -0
- package/dist/store/mode.js +41 -0
- package/dist/store/permissions.d.ts +61 -0
- package/dist/store/permissions.js +139 -0
- package/dist/store/prompts-compose.d.ts +56 -0
- package/dist/store/prompts-compose.js +62 -0
- package/dist/store/prompts-template.d.ts +26 -0
- package/dist/store/prompts-template.js +99 -0
- package/dist/store/prompts.d.ts +142 -0
- package/dist/store/prompts.js +273 -0
- package/dist/store/provider-probe.d.ts +81 -0
- package/dist/store/provider-probe.js +154 -0
- package/dist/store/providers.d.ts +97 -0
- package/dist/store/providers.js +219 -0
- package/dist/store/result.d.ts +29 -0
- package/dist/store/result.js +30 -0
- package/dist/store/session-id.d.ts +119 -0
- package/dist/store/session-id.js +220 -0
- package/dist/store/session-meta.d.ts +41 -0
- package/dist/store/session-meta.js +66 -0
- package/dist/store/session-ops.d.ts +109 -0
- package/dist/store/session-ops.js +264 -0
- package/dist/store/session-tools.d.ts +42 -0
- package/dist/store/session-tools.js +69 -0
- package/dist/store/sessions.d.ts +162 -0
- package/dist/store/sessions.js +307 -0
- package/dist/store/validate.d.ts +13 -0
- package/dist/store/validate.js +38 -0
- package/dist/store/workspaces.d.ts +67 -0
- package/dist/store/workspaces.js +228 -0
- package/dist/user-questions.d.ts +58 -0
- package/dist/user-questions.js +157 -0
- package/dist/version.d.ts +14 -0
- package/dist/version.js +52 -0
- package/package.json +38 -0
- package/webdist/assets/index-Bngs7gUm.js +98 -0
- package/webdist/assets/index-Bu6ci_rN.css +1 -0
- package/webdist/assets/katex-CCbS2qAY.js +257 -0
- package/webdist/assets/mhchem-CnUN8HwY.js +1 -0
- package/webdist/build-meta.json +7 -0
- package/webdist/index.html +261 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `startStudioServer` (H): the ONE reusable server bootstrap.
|
|
3
|
+
*
|
|
4
|
+
* `apps/studio/src/main.ts` (source checkout) and the `celestea` CLI
|
|
5
|
+
* (`packages/cli`) both call this, so the startup log lines and the W742
|
|
6
|
+
* graceful-teardown order exist in exactly one place. `main.ts` stays a thin
|
|
7
|
+
* wrapper that pins the historical source defaults; the CLI passes its own
|
|
8
|
+
* flags.
|
|
9
|
+
*
|
|
10
|
+
* Teardown order is the W742 contract (do not reorder):
|
|
11
|
+
* signal -> stop accepting traffic (bounded grace) -> flush grants audit ->
|
|
12
|
+
* engine down (workers settled, logs closed) -> let the loop drain.
|
|
13
|
+
*/
|
|
14
|
+
import { serve } from "@hono/node-server";
|
|
15
|
+
import { createStudioApp } from "./app.js";
|
|
16
|
+
import { loadStudioConfig } from "./config.js";
|
|
17
|
+
import { assertBindIsSafe } from "./auth/api-token.js";
|
|
18
|
+
import { engineLlmView } from "./runtime/llm-assembly.js";
|
|
19
|
+
import { autowakeEnabled, ENV_AUTOWAKE } from "@celestea/runtime";
|
|
20
|
+
/** Env knob: drain window before leftover sockets are cut. */
|
|
21
|
+
export const ENV_DRAIN_MS = "CELESTEA_SHUTDOWN_DRAIN_MS";
|
|
22
|
+
/** Env knob: ceiling for the whole teardown. */
|
|
23
|
+
export const ENV_TEARDOWN_MS = "CELESTEA_SHUTDOWN_TIMEOUT_MS";
|
|
24
|
+
/** The one startup banner (kept out of `startStudioServer` for the size rule). */
|
|
25
|
+
function logListeningBanner(info) {
|
|
26
|
+
console.log(`[celestea-studio-ts] listening on http://${info.hostname}:${info.port} (${info.endpointCount} contract endpoints)`);
|
|
27
|
+
const v = info.view;
|
|
28
|
+
console.log(`[celestea-studio-ts] llm: mode=${v.mode} model=${v.model} base_url=${v.baseUrl} ` +
|
|
29
|
+
`key=${v.hasApiKey ? "set" : "missing"} context_window=${v.contextWindow ?? "n/a"} ` +
|
|
30
|
+
`timeouts(c/r/i)=${v.timeouts.connectMs ?? "off"}/${v.timeouts.responseMs ?? "off"}/${v.timeouts.idleMs ?? "off"}ms`);
|
|
31
|
+
console.log(`[celestea-studio-ts] reasoning_effort=${v.reasoningEffort ?? "off"} max_output_tokens=${v.maxOutputTokens ?? "off"}`);
|
|
32
|
+
console.log(`[celestea-studio-ts] api_key_env=${info.apiKeyEnv} (key read from the environment only)`);
|
|
33
|
+
console.log(autowakeEnabled(info.env)
|
|
34
|
+
? `[celestea-studio-ts] autowake: enabled (a worker receipt wakes its host session; ${ENV_AUTOWAKE}=0 disables)`
|
|
35
|
+
: `[celestea-studio-ts] autowake: disabled by ${ENV_AUTOWAKE}`);
|
|
36
|
+
}
|
|
37
|
+
/** Boot the studio HTTP server; returns a handle whose `stop` is the teardown. */
|
|
38
|
+
export function startStudioServer(options) {
|
|
39
|
+
const env = options.env ?? process.env;
|
|
40
|
+
// H: resolve the config HERE so the listening callback can write the ACTUAL
|
|
41
|
+
// address back into it. `services.config` is the same object, so
|
|
42
|
+
// `GET /api/health.bind` can never disagree with the socket the process is on
|
|
43
|
+
// (the "never lie to the operator" rule); `--port 0` reports the real port.
|
|
44
|
+
const config = options.config ?? loadStudioConfig({ cwd: options.cwd, env });
|
|
45
|
+
// H-security: a non-loopback listener with no token is a remote-shell hole
|
|
46
|
+
// (POST /api/exec). Refuse BEFORE composing or binding; never degrade silently.
|
|
47
|
+
assertBindIsSafe(options.hostname, config.authToken);
|
|
48
|
+
const { app, routes, services } = createStudioApp({ ...options, config });
|
|
49
|
+
const profile = services.runtime.profile();
|
|
50
|
+
const view = engineLlmView(profile, env);
|
|
51
|
+
const loud = options.log ?? true;
|
|
52
|
+
const drainMs = Number.parseInt(env[ENV_DRAIN_MS] ?? "2000", 10);
|
|
53
|
+
const teardownMs = Number.parseInt(env[ENV_TEARDOWN_MS] ?? "5000", 10);
|
|
54
|
+
let boundPort = options.port;
|
|
55
|
+
let announceListening = () => { };
|
|
56
|
+
const listening = new Promise((resolve) => {
|
|
57
|
+
announceListening = resolve;
|
|
58
|
+
});
|
|
59
|
+
const server = serve({ fetch: app.fetch, port: options.port, hostname: options.hostname }, (info) => {
|
|
60
|
+
boundPort = info.port;
|
|
61
|
+
config.bind = `${options.hostname}:${info.port}`;
|
|
62
|
+
announceListening({ port: info.port, hostname: options.hostname });
|
|
63
|
+
if (loud) {
|
|
64
|
+
logListeningBanner({ hostname: options.hostname, port: info.port, endpointCount: routes.length, view, apiKeyEnv: profile.api_key_env, env });
|
|
65
|
+
}
|
|
66
|
+
options.onListening?.({ port: info.port, hostname: options.hostname, endpointCount: routes.length });
|
|
67
|
+
});
|
|
68
|
+
const engine = services.runtime;
|
|
69
|
+
let stopping = false;
|
|
70
|
+
const log = (line) => {
|
|
71
|
+
if (loud)
|
|
72
|
+
console.log(`[celestea-studio-ts] ${line}`);
|
|
73
|
+
};
|
|
74
|
+
async function within(work, ms) {
|
|
75
|
+
let timer = null;
|
|
76
|
+
const deadline = new Promise((resolve) => {
|
|
77
|
+
timer = setTimeout(resolve, ms);
|
|
78
|
+
});
|
|
79
|
+
try {
|
|
80
|
+
await Promise.race([Promise.resolve(work), deadline]);
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
log(`teardown step failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
if (timer !== null)
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function stopTraffic() {
|
|
91
|
+
await within(new Promise((resolve) => {
|
|
92
|
+
server.close(() => resolve());
|
|
93
|
+
}), drainMs);
|
|
94
|
+
server.closeAllConnections?.();
|
|
95
|
+
}
|
|
96
|
+
async function stop(signal) {
|
|
97
|
+
if (stopping) {
|
|
98
|
+
log(`${signal} again — exiting now (in-flight work is dropped)`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
stopping = true;
|
|
102
|
+
log(`${signal} received — draining (grace ${drainMs}ms)`);
|
|
103
|
+
await stopTraffic();
|
|
104
|
+
log("traffic stopped (listener closed, leftover sockets cut)");
|
|
105
|
+
await within(services.grants.audit.flush(), drainMs);
|
|
106
|
+
log("audit flushed");
|
|
107
|
+
await within(engine.shutdown?.(), teardownMs);
|
|
108
|
+
log("engine stopped (workers settled, session logs closed) — loop may drain");
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
get port() {
|
|
112
|
+
return boundPort;
|
|
113
|
+
},
|
|
114
|
+
hostname: options.hostname,
|
|
115
|
+
endpointCount: routes.length,
|
|
116
|
+
listening,
|
|
117
|
+
stop,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side mutable settings (the `USER_OVERRIDE` slot + the base-url override).
|
|
3
|
+
*
|
|
4
|
+
* Two things are NOT engine state but must survive between requests:
|
|
5
|
+
* - `system_prompt` (POST /api/config): a non-empty value bypasses the whole
|
|
6
|
+
* prompt registry assembly; an empty value clears the override and falls
|
|
7
|
+
* back to `build_gen` (`src/prompts.rs` memoryOverride);
|
|
8
|
+
* - `base_url` (POST /api/config): an empty value clears the override so the
|
|
9
|
+
* env / provider default chain applies again.
|
|
10
|
+
*
|
|
11
|
+
* The api_key is deliberately NOT here: it goes straight into the process env
|
|
12
|
+
* and is never held in a field that could be serialized by accident.
|
|
13
|
+
*/
|
|
14
|
+
export declare class StudioSettings {
|
|
15
|
+
private systemPrompt;
|
|
16
|
+
private baseUrl;
|
|
17
|
+
systemPromptOverride(): string | null;
|
|
18
|
+
/** "" clears the override (fall back to the registry assembly). */
|
|
19
|
+
setSystemPromptOverride(value: string): void;
|
|
20
|
+
baseUrlOverride(): string | null;
|
|
21
|
+
/** "" clears the override (fall back to env / provider chain). */
|
|
22
|
+
setBaseUrlOverride(value: string): void;
|
|
23
|
+
}
|
package/dist/settings.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side mutable settings (the `USER_OVERRIDE` slot + the base-url override).
|
|
3
|
+
*
|
|
4
|
+
* Two things are NOT engine state but must survive between requests:
|
|
5
|
+
* - `system_prompt` (POST /api/config): a non-empty value bypasses the whole
|
|
6
|
+
* prompt registry assembly; an empty value clears the override and falls
|
|
7
|
+
* back to `build_gen` (`src/prompts.rs` memoryOverride);
|
|
8
|
+
* - `base_url` (POST /api/config): an empty value clears the override so the
|
|
9
|
+
* env / provider default chain applies again.
|
|
10
|
+
*
|
|
11
|
+
* The api_key is deliberately NOT here: it goes straight into the process env
|
|
12
|
+
* and is never held in a field that could be serialized by accident.
|
|
13
|
+
*/
|
|
14
|
+
export class StudioSettings {
|
|
15
|
+
systemPrompt = null;
|
|
16
|
+
baseUrl = null;
|
|
17
|
+
systemPromptOverride() {
|
|
18
|
+
return this.systemPrompt;
|
|
19
|
+
}
|
|
20
|
+
/** "" clears the override (fall back to the registry assembly). */
|
|
21
|
+
setSystemPromptOverride(value) {
|
|
22
|
+
this.systemPrompt = value.trim() === "" ? null : value;
|
|
23
|
+
}
|
|
24
|
+
baseUrlOverride() {
|
|
25
|
+
return this.baseUrl;
|
|
26
|
+
}
|
|
27
|
+
/** "" clears the override (fall back to env / provider chain). */
|
|
28
|
+
setBaseUrlOverride(value) {
|
|
29
|
+
this.baseUrl = value === "" ? null : value;
|
|
30
|
+
}
|
|
31
|
+
}
|
package/dist/sse.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Studio SSE bus — the host side of `GET /api/events` (W513).
|
|
3
|
+
*
|
|
4
|
+
* Contract points implemented here:
|
|
5
|
+
* - envelope `{v:2, session, turn, seq, payload}`: `session` is the session the
|
|
6
|
+
* frame belongs to (`null` = process-level), `turn` is the SESSION-local turn
|
|
7
|
+
* number, `seq` stays a monotonic process-global counter and `payload` is
|
|
8
|
+
* byte-identical to the frozen 8-event contract;
|
|
9
|
+
* - exactly 8 event names (`SSE_EVENT_NAMES` lives in `@celestea/core`);
|
|
10
|
+
* - default = one connection receives EVERY session (the client routes
|
|
11
|
+
* locally); `subscribe({session})` = server-side split for narrow clients;
|
|
12
|
+
* - back-pressure is PER SESSION BUCKET: a background session's text flood can
|
|
13
|
+
* no longer evict the focused session's frames. Only the overflowing bucket
|
|
14
|
+
* is dropped, and the marker carries `{session, dropped, hint}`.
|
|
15
|
+
*
|
|
16
|
+
* NOTE (W513): the design sketch also proposed coalescing `status` frames per
|
|
17
|
+
* session. That is NOT done here: the P5 replay harness compares the frame
|
|
18
|
+
* stream byte-for-byte against the derived transcript, and dropping a status
|
|
19
|
+
* frame silently is indistinguishable from losing a real frame. Per-session
|
|
20
|
+
* bucketing alone provides the isolation the feature needs.
|
|
21
|
+
*
|
|
22
|
+
* The bus is push-based (each subscriber owns a bounded queue plus a waiter) so
|
|
23
|
+
* a Hono `streamSSE` handler awaits frames instead of polling. Delivery is
|
|
24
|
+
* deliberately lossy under back-pressure: that IS the contract.
|
|
25
|
+
*/
|
|
26
|
+
import { LAGGED_HINT, type SseEnvelope, type SseEventName, type Statusline } from "@celestea/core";
|
|
27
|
+
export { LAGGED_HINT };
|
|
28
|
+
/** Core keeps the canonical bus capacity; the host re-exports it under its own name. */
|
|
29
|
+
export declare const SSE_BUS_CAPACITY = 512;
|
|
30
|
+
/** Envelope version this bus writes (`2` = per-session envelope). */
|
|
31
|
+
export declare const SSE_ENVELOPE_VERSION = 2;
|
|
32
|
+
/** Smallest per-session bucket (never starve one session in a busy process). */
|
|
33
|
+
export declare const MIN_BUCKET_CAPACITY = 64;
|
|
34
|
+
export interface BusFrame {
|
|
35
|
+
event: SseEventName;
|
|
36
|
+
envelope: SseEnvelope;
|
|
37
|
+
}
|
|
38
|
+
export interface BusSubscription {
|
|
39
|
+
/** Await the next frame; resolves to null once the subscription is closed. */
|
|
40
|
+
next(): Promise<BusFrame | null>;
|
|
41
|
+
close(): void;
|
|
42
|
+
/** Number of times this subscriber fell behind (one lagged frame each). */
|
|
43
|
+
dropped(): number;
|
|
44
|
+
}
|
|
45
|
+
/** Server-side split: keep only these sessions (plus process-level frames). */
|
|
46
|
+
export interface SubscribeOptions {
|
|
47
|
+
/** One session id (`null` keeps only process-level frames). */
|
|
48
|
+
session?: string | null;
|
|
49
|
+
/** Several session ids (repeatable `?session=`). */
|
|
50
|
+
sessions?: readonly string[];
|
|
51
|
+
}
|
|
52
|
+
export interface StudioBus {
|
|
53
|
+
emit(event: SseEventName, turn: number, payload: Record<string, unknown>, session?: string | null): BusFrame;
|
|
54
|
+
subscribe(opts?: SubscribeOptions): BusSubscription;
|
|
55
|
+
/** Current global sequence counter (next value to be handed out). */
|
|
56
|
+
seq(): number;
|
|
57
|
+
subscriberCount(): number;
|
|
58
|
+
}
|
|
59
|
+
export interface StudioBusOptions {
|
|
60
|
+
capacity?: number;
|
|
61
|
+
/** Per-session bucket floor (default [MIN_BUCKET_CAPACITY], clamped to capacity). */
|
|
62
|
+
minBucket?: number;
|
|
63
|
+
/** Statusline snapshot embedded in the `lagged` marker. */
|
|
64
|
+
statusline?: () => Statusline | Record<string, unknown>;
|
|
65
|
+
}
|
|
66
|
+
/** One bus per Studio app; adapters and handlers share it. */
|
|
67
|
+
export declare function createStudioBus(opts?: StudioBusOptions): StudioBus;
|
package/dist/sse.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Studio SSE bus — the host side of `GET /api/events` (W513).
|
|
3
|
+
*
|
|
4
|
+
* Contract points implemented here:
|
|
5
|
+
* - envelope `{v:2, session, turn, seq, payload}`: `session` is the session the
|
|
6
|
+
* frame belongs to (`null` = process-level), `turn` is the SESSION-local turn
|
|
7
|
+
* number, `seq` stays a monotonic process-global counter and `payload` is
|
|
8
|
+
* byte-identical to the frozen 8-event contract;
|
|
9
|
+
* - exactly 8 event names (`SSE_EVENT_NAMES` lives in `@celestea/core`);
|
|
10
|
+
* - default = one connection receives EVERY session (the client routes
|
|
11
|
+
* locally); `subscribe({session})` = server-side split for narrow clients;
|
|
12
|
+
* - back-pressure is PER SESSION BUCKET: a background session's text flood can
|
|
13
|
+
* no longer evict the focused session's frames. Only the overflowing bucket
|
|
14
|
+
* is dropped, and the marker carries `{session, dropped, hint}`.
|
|
15
|
+
*
|
|
16
|
+
* NOTE (W513): the design sketch also proposed coalescing `status` frames per
|
|
17
|
+
* session. That is NOT done here: the P5 replay harness compares the frame
|
|
18
|
+
* stream byte-for-byte against the derived transcript, and dropping a status
|
|
19
|
+
* frame silently is indistinguishable from losing a real frame. Per-session
|
|
20
|
+
* bucketing alone provides the isolation the feature needs.
|
|
21
|
+
*
|
|
22
|
+
* The bus is push-based (each subscriber owns a bounded queue plus a waiter) so
|
|
23
|
+
* a Hono `streamSSE` handler awaits frames instead of polling. Delivery is
|
|
24
|
+
* deliberately lossy under back-pressure: that IS the contract.
|
|
25
|
+
*/
|
|
26
|
+
import { BUS_CAPACITY as CORE_CAPACITY, LAGGED_HINT, SSE_EVENT_NAMES, } from "@celestea/core";
|
|
27
|
+
export { LAGGED_HINT };
|
|
28
|
+
/** Core keeps the canonical bus capacity; the host re-exports it under its own name. */
|
|
29
|
+
export const SSE_BUS_CAPACITY = CORE_CAPACITY;
|
|
30
|
+
/** Envelope version this bus writes (`2` = per-session envelope). */
|
|
31
|
+
export const SSE_ENVELOPE_VERSION = 2;
|
|
32
|
+
/** Smallest per-session bucket (never starve one session in a busy process). */
|
|
33
|
+
export const MIN_BUCKET_CAPACITY = 64;
|
|
34
|
+
function bucketKey(session) {
|
|
35
|
+
return session ?? "";
|
|
36
|
+
}
|
|
37
|
+
/** One subscriber: an ordered frame list plus per-session occupancy counts. */
|
|
38
|
+
class SessionBuckets {
|
|
39
|
+
capacity;
|
|
40
|
+
minBucket;
|
|
41
|
+
accept;
|
|
42
|
+
lagged;
|
|
43
|
+
frames = [];
|
|
44
|
+
counts = new Map();
|
|
45
|
+
waiter = null;
|
|
46
|
+
closed = false;
|
|
47
|
+
dropCount = 0;
|
|
48
|
+
constructor(capacity, minBucket, accept, lagged) {
|
|
49
|
+
this.capacity = capacity;
|
|
50
|
+
this.minBucket = minBucket;
|
|
51
|
+
this.accept = accept;
|
|
52
|
+
this.lagged = lagged;
|
|
53
|
+
}
|
|
54
|
+
push(frame) {
|
|
55
|
+
if (this.closed || !this.accept(frame))
|
|
56
|
+
return;
|
|
57
|
+
const key = bucketKey(frame.envelope.session);
|
|
58
|
+
if ((this.counts.get(key) ?? 0) >= this.bucketCap()) {
|
|
59
|
+
this.dropCount += 1;
|
|
60
|
+
this.append(this.lagged(frame.envelope.session, this.dropBucket(key) + 1));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
this.append(frame);
|
|
64
|
+
}
|
|
65
|
+
next() {
|
|
66
|
+
const frame = this.shift();
|
|
67
|
+
if (frame !== undefined)
|
|
68
|
+
return Promise.resolve(frame);
|
|
69
|
+
if (this.closed)
|
|
70
|
+
return Promise.resolve(null);
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
this.waiter = { resolve };
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
close() {
|
|
76
|
+
this.closed = true;
|
|
77
|
+
this.frames.length = 0;
|
|
78
|
+
const waiter = this.waiter;
|
|
79
|
+
this.waiter = null;
|
|
80
|
+
waiter?.resolve(null);
|
|
81
|
+
}
|
|
82
|
+
dropped() {
|
|
83
|
+
return this.dropCount;
|
|
84
|
+
}
|
|
85
|
+
/** Per-session bucket cap: the fair share, never below `minBucket`. */
|
|
86
|
+
bucketCap() {
|
|
87
|
+
const buckets = Math.max(1, this.counts.size);
|
|
88
|
+
return Math.max(Math.min(this.minBucket, this.capacity), Math.floor(this.capacity / buckets));
|
|
89
|
+
}
|
|
90
|
+
append(frame) {
|
|
91
|
+
this.frames.push(frame);
|
|
92
|
+
const key = bucketKey(frame.envelope.session);
|
|
93
|
+
this.counts.set(key, (this.counts.get(key) ?? 0) + 1);
|
|
94
|
+
this.wake();
|
|
95
|
+
}
|
|
96
|
+
/** Drop ONE session's backlog; returns how many frames were discarded. */
|
|
97
|
+
dropBucket(key) {
|
|
98
|
+
const kept = [];
|
|
99
|
+
let removed = 0;
|
|
100
|
+
for (const frame of this.frames) {
|
|
101
|
+
if (bucketKey(frame.envelope.session) === key)
|
|
102
|
+
removed += 1;
|
|
103
|
+
else
|
|
104
|
+
kept.push(frame);
|
|
105
|
+
}
|
|
106
|
+
this.frames.length = 0;
|
|
107
|
+
this.frames.push(...kept);
|
|
108
|
+
this.counts.set(key, 0);
|
|
109
|
+
return removed;
|
|
110
|
+
}
|
|
111
|
+
/** Dequeue one frame and release its bucket slot. */
|
|
112
|
+
shift() {
|
|
113
|
+
const frame = this.frames.shift();
|
|
114
|
+
if (frame === undefined)
|
|
115
|
+
return undefined;
|
|
116
|
+
const key = bucketKey(frame.envelope.session);
|
|
117
|
+
this.counts.set(key, Math.max(0, (this.counts.get(key) ?? 1) - 1));
|
|
118
|
+
return frame;
|
|
119
|
+
}
|
|
120
|
+
wake() {
|
|
121
|
+
const waiter = this.waiter;
|
|
122
|
+
if (waiter === null)
|
|
123
|
+
return;
|
|
124
|
+
this.waiter = null;
|
|
125
|
+
waiter.resolve(this.shift() ?? null);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function assertEventName(event) {
|
|
129
|
+
if (!SSE_EVENT_NAMES.includes(event)) {
|
|
130
|
+
throw new Error(`unknown SSE event '${event}': the contract freezes ${SSE_EVENT_NAMES.length} names`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** The `?session=` filter: matching sessions plus process-level frames. */
|
|
134
|
+
function sessionFilter(opts) {
|
|
135
|
+
const one = opts.session;
|
|
136
|
+
const many = opts.sessions;
|
|
137
|
+
if (one === undefined && many === undefined)
|
|
138
|
+
return () => true;
|
|
139
|
+
const wanted = new Set(many ?? []);
|
|
140
|
+
if (one !== undefined && one !== null)
|
|
141
|
+
wanted.add(one);
|
|
142
|
+
return (frame) => frame.envelope.session === null || wanted.has(frame.envelope.session);
|
|
143
|
+
}
|
|
144
|
+
/** One bus per Studio app; adapters and handlers share it. */
|
|
145
|
+
export function createStudioBus(opts = {}) {
|
|
146
|
+
const capacity = opts.capacity ?? SSE_BUS_CAPACITY;
|
|
147
|
+
const minBucket = Math.min(opts.minBucket ?? MIN_BUCKET_CAPACITY, capacity);
|
|
148
|
+
const queues = new Set();
|
|
149
|
+
let seq = 0;
|
|
150
|
+
const laggedFrame = (session, dropped) => ({
|
|
151
|
+
event: "status",
|
|
152
|
+
envelope: {
|
|
153
|
+
v: SSE_ENVELOPE_VERSION,
|
|
154
|
+
session,
|
|
155
|
+
turn: 0,
|
|
156
|
+
seq: seq++,
|
|
157
|
+
payload: {
|
|
158
|
+
phase: "lagged",
|
|
159
|
+
hint: LAGGED_HINT,
|
|
160
|
+
session,
|
|
161
|
+
dropped,
|
|
162
|
+
statusline: opts.statusline ? opts.statusline() : {},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
function emit(event, turn, payload, session = null) {
|
|
167
|
+
assertEventName(event);
|
|
168
|
+
const frame = { event, envelope: { v: SSE_ENVELOPE_VERSION, session, turn, seq: seq++, payload } };
|
|
169
|
+
for (const q of queues)
|
|
170
|
+
q.push(frame);
|
|
171
|
+
return frame;
|
|
172
|
+
}
|
|
173
|
+
function subscribe(sub) {
|
|
174
|
+
const q = new SessionBuckets(capacity, minBucket, sessionFilter(sub ?? {}), laggedFrame);
|
|
175
|
+
queues.add(q);
|
|
176
|
+
return {
|
|
177
|
+
next: () => q.next(),
|
|
178
|
+
close: () => {
|
|
179
|
+
q.close();
|
|
180
|
+
queues.delete(q);
|
|
181
|
+
},
|
|
182
|
+
dropped: () => q.dropped(),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return { emit, subscribe, seq: () => seq, subscriberCount: () => queues.size };
|
|
186
|
+
}
|
package/dist/static.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static file serving + SPA fallback (`src/main.rs:735-847`).
|
|
3
|
+
*
|
|
4
|
+
* The Vite build under `STUDIO_STATIC_ROOT` is served READ-ONLY and hardened
|
|
5
|
+
* twice: `sanitizeRel` refuses `..`, absolute and prefix components on the
|
|
6
|
+
* request path, and the resolved path is then re-checked to sit inside the
|
|
7
|
+
* root (so a symlink cannot escape either). Unknown `/api/*` paths never reach
|
|
8
|
+
* this handler — they are 404 JSON, which is why the API 404 is registered
|
|
9
|
+
* before the fallback. A missing build serves the "build the frontend first"
|
|
10
|
+
* hint page instead.
|
|
11
|
+
*/
|
|
12
|
+
import type { Hono } from "hono";
|
|
13
|
+
export declare function contentTypeFor(rel: string): string;
|
|
14
|
+
/** Allow normal components only: reject `..`, absolute and prefix components. */
|
|
15
|
+
export declare function sanitizeRel(rel: string): string | null;
|
|
16
|
+
/** Resolve inside the root or refuse (catches symlink escapes too). */
|
|
17
|
+
export declare function resolveWithinRoot(root: string, rel: string): string | null;
|
|
18
|
+
/** The static/SPA handler; `/api/*` is answered by the caller's 404 route. */
|
|
19
|
+
export declare function serveStaticPath(root: string, rawPath: string): Response;
|
|
20
|
+
export declare function registerStatic(app: Hono, root: string): void;
|
package/dist/static.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static file serving + SPA fallback (`src/main.rs:735-847`).
|
|
3
|
+
*
|
|
4
|
+
* The Vite build under `STUDIO_STATIC_ROOT` is served READ-ONLY and hardened
|
|
5
|
+
* twice: `sanitizeRel` refuses `..`, absolute and prefix components on the
|
|
6
|
+
* request path, and the resolved path is then re-checked to sit inside the
|
|
7
|
+
* root (so a symlink cannot escape either). Unknown `/api/*` paths never reach
|
|
8
|
+
* this handler — they are 404 JSON, which is why the API 404 is registered
|
|
9
|
+
* before the fallback. A missing build serves the "build the frontend first"
|
|
10
|
+
* hint page instead.
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync, statSync, realpathSync } from "node:fs";
|
|
13
|
+
import { extname, resolve, sep } from "node:path";
|
|
14
|
+
const MIME = {
|
|
15
|
+
".html": "text/html; charset=utf-8",
|
|
16
|
+
".js": "text/javascript; charset=utf-8",
|
|
17
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
18
|
+
".css": "text/css; charset=utf-8",
|
|
19
|
+
".json": "application/json",
|
|
20
|
+
".svg": "image/svg+xml",
|
|
21
|
+
".png": "image/png",
|
|
22
|
+
".jpg": "image/jpeg",
|
|
23
|
+
".jpeg": "image/jpeg",
|
|
24
|
+
".gif": "image/gif",
|
|
25
|
+
".ico": "image/x-icon",
|
|
26
|
+
".webp": "image/webp",
|
|
27
|
+
".woff": "font/woff",
|
|
28
|
+
".woff2": "font/woff2",
|
|
29
|
+
".ttf": "font/ttf",
|
|
30
|
+
".map": "application/json",
|
|
31
|
+
".txt": "text/plain; charset=utf-8",
|
|
32
|
+
".webmanifest": "application/manifest+json",
|
|
33
|
+
};
|
|
34
|
+
const HINT_PAGE = `<!doctype html><meta charset="utf-8"><title>celestea-studio</title>
|
|
35
|
+
<body style="font-family:system-ui;padding:2rem">
|
|
36
|
+
<h1>celestea-studio TS</h1>
|
|
37
|
+
<p>frontend/dist is missing — build the frontend first (<code>pnpm build</code> in frontend/).</p>
|
|
38
|
+
<p>The HTTP API is available at <code>/api/*</code>.</p>
|
|
39
|
+
</body>`;
|
|
40
|
+
export function contentTypeFor(rel) {
|
|
41
|
+
return MIME[extname(rel).toLowerCase()] ?? "application/octet-stream";
|
|
42
|
+
}
|
|
43
|
+
/** Allow normal components only: reject `..`, absolute and prefix components. */
|
|
44
|
+
export function sanitizeRel(rel) {
|
|
45
|
+
const parts = [];
|
|
46
|
+
for (const raw of rel.split("/")) {
|
|
47
|
+
if (raw === "" || raw === ".")
|
|
48
|
+
continue;
|
|
49
|
+
if (raw === ".." || raw.includes("\\") || raw.includes("\0"))
|
|
50
|
+
return null;
|
|
51
|
+
parts.push(raw);
|
|
52
|
+
}
|
|
53
|
+
if (parts.length === 0)
|
|
54
|
+
return null;
|
|
55
|
+
return parts.join("/");
|
|
56
|
+
}
|
|
57
|
+
/** Resolve inside the root or refuse (catches symlink escapes too). */
|
|
58
|
+
export function resolveWithinRoot(root, rel) {
|
|
59
|
+
const target = resolve(root, rel);
|
|
60
|
+
const realRoot = (() => {
|
|
61
|
+
try {
|
|
62
|
+
return realpathSync(root);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return resolve(root);
|
|
66
|
+
}
|
|
67
|
+
})();
|
|
68
|
+
const real = (() => {
|
|
69
|
+
try {
|
|
70
|
+
return realpathSync(target);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return target;
|
|
74
|
+
}
|
|
75
|
+
})();
|
|
76
|
+
if (real !== realRoot && !real.startsWith(realRoot.endsWith(sep) ? realRoot : `${realRoot}${sep}`))
|
|
77
|
+
return null;
|
|
78
|
+
return target;
|
|
79
|
+
}
|
|
80
|
+
function readIfFile(path) {
|
|
81
|
+
try {
|
|
82
|
+
if (!statSync(path).isFile())
|
|
83
|
+
return null;
|
|
84
|
+
return path;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function readStatic(root, rel) {
|
|
91
|
+
const file = readIfFile(resolveWithinRoot(root, rel) ?? "");
|
|
92
|
+
if (file === null)
|
|
93
|
+
return null;
|
|
94
|
+
return { body: readFileSync(file), contentType: contentTypeFor(rel) };
|
|
95
|
+
}
|
|
96
|
+
function bytesResponse(body, contentType, status = 200) {
|
|
97
|
+
return new Response(body, { status, headers: { "content-type": contentType, "cache-control": "no-cache" } });
|
|
98
|
+
}
|
|
99
|
+
function htmlResponse(body, status = 200) {
|
|
100
|
+
return new Response(body, { status, headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-cache" } });
|
|
101
|
+
}
|
|
102
|
+
/** The static/SPA handler; `/api/*` is answered by the caller's 404 route. */
|
|
103
|
+
export function serveStaticPath(root, rawPath) {
|
|
104
|
+
if (rawPath.startsWith("/api/"))
|
|
105
|
+
return Response.json({ error: "not found" }, { status: 404 });
|
|
106
|
+
const rel = sanitizeRel(rawPath.replace(/^\/+/, "") === "" ? "index.html" : rawPath.replace(/^\/+/, ""));
|
|
107
|
+
if (rel === null)
|
|
108
|
+
return Response.json({ error: "not found" }, { status: 404 });
|
|
109
|
+
const file = readStatic(root, rel);
|
|
110
|
+
if (file !== null)
|
|
111
|
+
return bytesResponse(file.body, file.contentType);
|
|
112
|
+
if (rel === "index.html" || !rel.includes(".")) {
|
|
113
|
+
const index = readStatic(root, "index.html");
|
|
114
|
+
return index === null ? htmlResponse(HINT_PAGE) : bytesResponse(index.body, "text/html; charset=utf-8");
|
|
115
|
+
}
|
|
116
|
+
return Response.json({ error: "not found" }, { status: 404 });
|
|
117
|
+
}
|
|
118
|
+
export function registerStatic(app, root) {
|
|
119
|
+
app.get("*", (c) => serveStaticPath(root, new URL(c.req.url).pathname));
|
|
120
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builtin prompt sections — the frozen 10-row registry (order 100..1000).
|
|
3
|
+
*
|
|
4
|
+
* Ported verbatim from `fixtures/live/prompts.json` (a read-only live capture
|
|
5
|
+
* of GET /api/prompts), which mirrors `src/prompts.rs:89-99` BUILTIN_SECTIONS.
|
|
6
|
+
* W782 is the one deliberate edit since: `environment` (order 200) no longer
|
|
7
|
+
* spells out this checkout's paths/port/unit name — it renders `{{studio_*}}`
|
|
8
|
+
* variables derived at runtime, and no template may carry an absolute path.
|
|
9
|
+
* These are DATA, not logic: the registry stores them as `source: "builtin"`
|
|
10
|
+
* and a global/workspace row with the same id only swaps the template.
|
|
11
|
+
*
|
|
12
|
+
* W729 (P0, K6/D6): the registry is still exactly 10 rows with the SAME order
|
|
13
|
+
* array; the session mode is expressed as a TEMPLATE VARIANT of `tool_access`
|
|
14
|
+
* (order 300), never as an 11th section. `BUILTIN_SECTIONS` carries variant A
|
|
15
|
+
* (the `standard` text) and [TOOL_ACCESS_VARIANTS] carries both, so the
|
|
16
|
+
* mode-aware assembly picks one and every other consumer still sees a
|
|
17
|
+
* 10-row table.
|
|
18
|
+
*/
|
|
19
|
+
import { type SessionMode } from "./mode.js";
|
|
20
|
+
/**
|
|
21
|
+
* The `tool_access` (order 300) template variants — `docs/modes-standard-vs-execution.md`
|
|
22
|
+
* §1.3, verbatim. K3: the variant table is module-level data, never inlined at a
|
|
23
|
+
* call site; K6: it is the ONLY difference between the two mode assemblies.
|
|
24
|
+
*
|
|
25
|
+
* A `standard` — direct calls are the normal path; `run_code` is available.
|
|
26
|
+
* B `execution` — one program per dependent sequence + the hard limits.
|
|
27
|
+
*
|
|
28
|
+
* Byte budget: A = 353 B, B = 1068 B (the frozen 10-section assembly is 3835 B
|
|
29
|
+
* before this change, PROMPT_MAX_LEN is 8192 B — both variants stay far below).
|
|
30
|
+
*/
|
|
31
|
+
export declare const TOOL_ACCESS_VARIANTS: Readonly<Record<SessionMode, string>>;
|
|
32
|
+
export interface BuiltinSection {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
order: number;
|
|
36
|
+
template: string;
|
|
37
|
+
}
|
|
38
|
+
export declare const BUILTIN_SECTIONS: readonly BuiltinSection[];
|
|
39
|
+
/**
|
|
40
|
+
* Section id -> builtin template (the last-resort fallback) under one mode: the
|
|
41
|
+
* `tool_access` row is swapped for that mode's variant, every other row is the
|
|
42
|
+
* frozen template. A user override (global/workspace/bound) still wins,
|
|
43
|
+
* because the overlay replaces the template this function produced (R4).
|
|
44
|
+
*/
|
|
45
|
+
export declare function builtinTemplate(id: string, mode?: SessionMode): string | undefined;
|
|
46
|
+
/** The builtin rows of one mode, copied (a caller can never mutate the table). */
|
|
47
|
+
export declare function builtinRowsFor(mode: SessionMode): Array<{
|
|
48
|
+
id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
template: string;
|
|
51
|
+
order: number;
|
|
52
|
+
}>;
|