@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
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W769 — the studio host's auto-wake wiring.
|
|
3
|
+
*
|
|
4
|
+
* The loop itself is `@celestea/runtime`'s `AutowakeLoop` (mailbox → busy check →
|
|
5
|
+
* wake). This module owns the studio-specific part: WHICH conversations have a
|
|
6
|
+
* loop (one per host session, keyed by session id / `cli-main` for the detached
|
|
7
|
+
* generation) and how the loop reads the state of the generation in force.
|
|
8
|
+
*
|
|
9
|
+
* It lives outside the adapter on purpose: `real-runtime-adapter.ts` sits at the
|
|
10
|
+
* eslint size budget, and "which sessions can be woken" is a policy of its own —
|
|
11
|
+
* a worker's receipt is addressed to the session that spawned it, so the set of
|
|
12
|
+
* queues that can carry one is exactly the set of live host sessions.
|
|
13
|
+
*/
|
|
14
|
+
import { AutowakeLoop, HOST_SESSION_ID, keyOfSession } from "@celestea/runtime";
|
|
15
|
+
/** Mailbox + busy + the USER's next-turn lane depth, from one entry (or null). */
|
|
16
|
+
export function autowakeStateOf(entry) {
|
|
17
|
+
return entry === null
|
|
18
|
+
? null
|
|
19
|
+
: {
|
|
20
|
+
mailbox: entry.runtime.workers?.mailbox ?? null,
|
|
21
|
+
busy: entry.inFlight,
|
|
22
|
+
userPending: entry.runtime.pendingInjections("next-turn"),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** The studio's log line for auto-wake decisions (stderr, like boot recovery). */
|
|
26
|
+
export function autowakeLog(session, line) {
|
|
27
|
+
process.stderr.write(`[celestea-studio-ts] autowake ${session ?? HOST_SESSION_ID}: ${line}\n`);
|
|
28
|
+
}
|
|
29
|
+
export class HostAutowake {
|
|
30
|
+
loops = new Map();
|
|
31
|
+
opts;
|
|
32
|
+
constructor(opts) {
|
|
33
|
+
this.opts = opts;
|
|
34
|
+
}
|
|
35
|
+
/** Is auto-wake on? (`CELESTEA_AUTOWAKE`, default on.) */
|
|
36
|
+
get running() {
|
|
37
|
+
return this.opts.enabled;
|
|
38
|
+
}
|
|
39
|
+
/** Live loops (diagnostics / tests). */
|
|
40
|
+
get count() {
|
|
41
|
+
return this.loops.size;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Mount the (idempotent) loop of one host conversation. Called from the
|
|
45
|
+
* generation's build callback: the hooks read the CURRENT entry on every pass,
|
|
46
|
+
* so a rebuilt instance is picked up without re-mounting anything.
|
|
47
|
+
*/
|
|
48
|
+
ensure(session) {
|
|
49
|
+
if (!this.opts.enabled)
|
|
50
|
+
return;
|
|
51
|
+
const key = keyOfSession(session);
|
|
52
|
+
if (this.loops.has(key))
|
|
53
|
+
return;
|
|
54
|
+
const loop = new AutowakeLoop({
|
|
55
|
+
queueKey: session ?? HOST_SESSION_ID,
|
|
56
|
+
mailbox: () => this.opts.lookup(session)?.mailbox ?? null,
|
|
57
|
+
isBusy: () => this.opts.lookup(session)?.busy ?? false,
|
|
58
|
+
userPending: () => this.opts.lookup(session)?.userPending ?? 0,
|
|
59
|
+
wake: (input) => this.opts.wake(session, input),
|
|
60
|
+
log: (line) => autowakeLog(session, line),
|
|
61
|
+
});
|
|
62
|
+
this.loops.set(key, loop);
|
|
63
|
+
loop.start();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* W794: a DELETED session never comes back under that id, so its loop is
|
|
67
|
+
* unparked for good — otherwise it keeps re-binding to a generation that can
|
|
68
|
+
* never exist again and re-logs `no live generation to bind` forever (the
|
|
69
|
+
* timer is real). A rebuild / idle eviction must NOT call this: the same id
|
|
70
|
+
* recomposes, and its loop has to be waiting for it. Idempotent.
|
|
71
|
+
*/
|
|
72
|
+
async forget(session) {
|
|
73
|
+
const key = keyOfSession(session);
|
|
74
|
+
const loop = this.loops.get(key);
|
|
75
|
+
if (loop === undefined)
|
|
76
|
+
return;
|
|
77
|
+
this.loops.delete(key);
|
|
78
|
+
await loop.stop();
|
|
79
|
+
}
|
|
80
|
+
/** Unpark every loop (process shutdown; idempotent). */
|
|
81
|
+
async stop() {
|
|
82
|
+
const loops = [...this.loops.values()];
|
|
83
|
+
this.loops.clear();
|
|
84
|
+
await Promise.all(loops.map((loop) => loop.stop()));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W804 (multimodal P0 section 7.6): the host-visible report of ONE image
|
|
3
|
+
* downgrade. Extracted from the runtime adapter so that file stays inside its
|
|
4
|
+
* line budget.
|
|
5
|
+
*
|
|
6
|
+
* Three channels: a `status` frame (the statusline and the info block share it)
|
|
7
|
+
* and an audit line on the process log. The turn itself is NOT failed.
|
|
8
|
+
*
|
|
9
|
+
* W863: the report is DEDUPLICATED per session. The decorator fires on EVERY
|
|
10
|
+
* image-bearing request (`packages/llm/src/image-fallback.ts`), and a multi-step
|
|
11
|
+
* turn re-sends the same history image at every step — so one turn used to emit
|
|
12
|
+
* N identical frames and the UI stacked N identical blocks. The memo below
|
|
13
|
+
* lives in a per-adapter reporter (never module state): one entry per session,
|
|
14
|
+
* keyed by the (model, cause) signature that defines "this is NEWS".
|
|
15
|
+
*/
|
|
16
|
+
import type { ImageDowngradeInfo } from "@celestea/llm";
|
|
17
|
+
import type { StudioBus } from "../sse.js";
|
|
18
|
+
/**
|
|
19
|
+
* W863: the dedupe signature of one downgrade — exactly the (model, cause) pair
|
|
20
|
+
* the requirement fixes. `cause` is normalized the same way `copyOf` does, so a
|
|
21
|
+
* pre-W855 producer (cause absent) and an explicit `upstream_rejected` are the
|
|
22
|
+
* SAME signature. Deliberately NOT keyed on httpStatus/message/placeholder: for
|
|
23
|
+
* one (model, cause) the visible copy is identical in shape, and the verbatim
|
|
24
|
+
* upstream body is not what the user acts on.
|
|
25
|
+
*/
|
|
26
|
+
export declare function downgradeSignature(info: ImageDowngradeInfo): string;
|
|
27
|
+
/** One reporter per host adapter: the per-session memo + the two channels. */
|
|
28
|
+
export interface ImageDowngradeReporter {
|
|
29
|
+
/** Returns true when this report was NEW (emitted + audited), false when deduped. */
|
|
30
|
+
report(sessionId: string | null, info: ImageDowngradeInfo): boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* W863 (§A): make the host report ONE downgrade per session per (model, cause).
|
|
34
|
+
* - every later frame with the same signature is swallowed ENTIRELY (no emit
|
|
35
|
+
* AND no audit line: the process log must never claim what the UI did not
|
|
36
|
+
* show);
|
|
37
|
+
* - a new model or a new cause is news again (the user must see that the
|
|
38
|
+
* situation changed);
|
|
39
|
+
* - `sessionId` is the partition key (`null` = the detached session), so two
|
|
40
|
+
* sessions never silence each other. State is per reporter instance and JS
|
|
41
|
+
* is single-threaded, so there is no cross-session race to guard.
|
|
42
|
+
* Scope is the SESSION (not the turn): see the W863 report for the boundary.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createImageDowngradeReporter(deps: {
|
|
45
|
+
bus: () => StudioBus | null;
|
|
46
|
+
/** Audit sink (defaults to console.warn); injected so tests can count lines. */
|
|
47
|
+
warn?: (line: string) => void;
|
|
48
|
+
}): ImageDowngradeReporter;
|
|
49
|
+
export declare function reportImageDowngrade(bus: StudioBus | null, sessionId: string | null, info: ImageDowngradeInfo, warn?: (line: string) => void): void;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W804 (multimodal P0 section 7.6): the host-visible report of ONE image
|
|
3
|
+
* downgrade. Extracted from the runtime adapter so that file stays inside its
|
|
4
|
+
* line budget.
|
|
5
|
+
*
|
|
6
|
+
* Three channels: a `status` frame (the statusline and the info block share it)
|
|
7
|
+
* and an audit line on the process log. The turn itself is NOT failed.
|
|
8
|
+
*
|
|
9
|
+
* W863: the report is DEDUPLICATED per session. The decorator fires on EVERY
|
|
10
|
+
* image-bearing request (`packages/llm/src/image-fallback.ts`), and a multi-step
|
|
11
|
+
* turn re-sends the same history image at every step — so one turn used to emit
|
|
12
|
+
* N identical frames and the UI stacked N identical blocks. The memo below
|
|
13
|
+
* lives in a per-adapter reporter (never module state): one entry per session,
|
|
14
|
+
* keyed by the (model, cause) signature that defines "this is NEWS".
|
|
15
|
+
*/
|
|
16
|
+
/** W855: the user-facing message + executable hint of one downgrade cause. */
|
|
17
|
+
function copyOf(info, cause) {
|
|
18
|
+
if (cause === "timeout") {
|
|
19
|
+
return {
|
|
20
|
+
message: '模型 "' + info.model + '" 未在超时时间内响应图像输入,本轮已自动降级为「仅文本 + 图片占位」并重试;图片内容未送达模型(超时原文:' + info.message + ')。',
|
|
21
|
+
hint: '下一步:切换到支持图像输入的模型,或确认该模型 input_modalities 含 "image";若上游持续超时,请检查网络/网关后重试。',
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (cause === "configured_text_only") {
|
|
25
|
+
return {
|
|
26
|
+
message: '模型 "' + info.model + '" 已按配置声明为纯文本(input_modalities 不含 "image"),本轮已自动降级为「仅文本 + 图片占位」,未向上游发送图像。',
|
|
27
|
+
hint: '下一步:确认该模型 input_modalities 含 "image" 才能接收图片;否则请切换到支持图像输入的模型。',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
message: '模型 "' + info.model + '" 拒绝了图像输入(上游 ' + String(info.httpStatus ?? 400) + '),本轮已自动降级为「仅文本 + 图片占位」继续,图片内容未送达模型。',
|
|
32
|
+
hint: '下一步:切换到支持图像输入的模型,或确认该模型 input_modalities 含 "image";若确实不支持请设为 ["text"]。',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* W863: the dedupe signature of one downgrade — exactly the (model, cause) pair
|
|
37
|
+
* the requirement fixes. `cause` is normalized the same way `copyOf` does, so a
|
|
38
|
+
* pre-W855 producer (cause absent) and an explicit `upstream_rejected` are the
|
|
39
|
+
* SAME signature. Deliberately NOT keyed on httpStatus/message/placeholder: for
|
|
40
|
+
* one (model, cause) the visible copy is identical in shape, and the verbatim
|
|
41
|
+
* upstream body is not what the user acts on.
|
|
42
|
+
*/
|
|
43
|
+
export function downgradeSignature(info) {
|
|
44
|
+
return info.model + "\u0000" + (info.cause ?? "upstream_rejected");
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* W863 (§A): make the host report ONE downgrade per session per (model, cause).
|
|
48
|
+
* - every later frame with the same signature is swallowed ENTIRELY (no emit
|
|
49
|
+
* AND no audit line: the process log must never claim what the UI did not
|
|
50
|
+
* show);
|
|
51
|
+
* - a new model or a new cause is news again (the user must see that the
|
|
52
|
+
* situation changed);
|
|
53
|
+
* - `sessionId` is the partition key (`null` = the detached session), so two
|
|
54
|
+
* sessions never silence each other. State is per reporter instance and JS
|
|
55
|
+
* is single-threaded, so there is no cross-session race to guard.
|
|
56
|
+
* Scope is the SESSION (not the turn): see the W863 report for the boundary.
|
|
57
|
+
*/
|
|
58
|
+
export function createImageDowngradeReporter(deps) {
|
|
59
|
+
const lastBySession = new Map();
|
|
60
|
+
const warn = deps.warn ?? ((line) => console.warn(line));
|
|
61
|
+
return {
|
|
62
|
+
report(sessionId, info) {
|
|
63
|
+
const key = sessionId ?? "";
|
|
64
|
+
const signature = downgradeSignature(info);
|
|
65
|
+
if (lastBySession.get(key) === signature)
|
|
66
|
+
return false;
|
|
67
|
+
lastBySession.set(key, signature);
|
|
68
|
+
reportImageDowngrade(deps.bus(), sessionId, info, warn);
|
|
69
|
+
return true;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function reportImageDowngrade(bus, sessionId, info, warn = (line) => console.warn(line)) {
|
|
74
|
+
// W855: an absent cause = a pre-W855 producer -> the historical 400 wording.
|
|
75
|
+
const cause = info.cause ?? "upstream_rejected";
|
|
76
|
+
const { message, hint } = copyOf(info, cause);
|
|
77
|
+
bus?.emit("status", 0, {
|
|
78
|
+
phase: "error",
|
|
79
|
+
model: info.model,
|
|
80
|
+
reason: info.reason,
|
|
81
|
+
cause,
|
|
82
|
+
http_status: info.httpStatus,
|
|
83
|
+
message,
|
|
84
|
+
hint,
|
|
85
|
+
placeholder: info.placeholder,
|
|
86
|
+
}, sessionId);
|
|
87
|
+
// Audit: the process log (the session log keeps its frozen event vocabulary).
|
|
88
|
+
warn("[W804/W855] image downgrade session=" + (sessionId ?? "-") +
|
|
89
|
+
" model=" + info.model + " cause=" + cause + " status=" + String(info.httpStatus));
|
|
90
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a drained injection into the inbox shape the SSE publisher expects.
|
|
3
|
+
*
|
|
4
|
+
* The inbox's own message type is richer than the wire payload (`at`, defaulted
|
|
5
|
+
* `lane`/`kind`/`source`), and the host needs the WIRE shape to publish a
|
|
6
|
+
* `placement: "context"` frame (W515 §2). Keeping the mapping here — instead of
|
|
7
|
+
* inline in the adapter — keeps the adapter about the HTTP contract.
|
|
8
|
+
*/
|
|
9
|
+
import type { PendingInjection } from "@celestea/core";
|
|
10
|
+
import type { InjectedMessage } from "@celestea/runtime";
|
|
11
|
+
export declare function inboxMessageOf(message: PendingInjection): InjectedMessage;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a drained injection into the inbox shape the SSE publisher expects.
|
|
3
|
+
*
|
|
4
|
+
* The inbox's own message type is richer than the wire payload (`at`, defaulted
|
|
5
|
+
* `lane`/`kind`/`source`), and the host needs the WIRE shape to publish a
|
|
6
|
+
* `placement: "context"` frame (W515 §2). Keeping the mapping here — instead of
|
|
7
|
+
* inline in the adapter — keeps the adapter about the HTTP contract.
|
|
8
|
+
*/
|
|
9
|
+
export function inboxMessageOf(message) {
|
|
10
|
+
return {
|
|
11
|
+
text: message.text,
|
|
12
|
+
from: message.from,
|
|
13
|
+
at: 0,
|
|
14
|
+
lane: message.lane ?? "next-turn",
|
|
15
|
+
kind: message.kind ?? "user",
|
|
16
|
+
id: message.id ?? "",
|
|
17
|
+
source: message.source ?? { kind: "user", form: "message" },
|
|
18
|
+
duplicate: false,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The real engine assembly of `apps/studio` (P5 integration).
|
|
3
|
+
*
|
|
4
|
+
* `createRealRuntimeAdapter` is the production `RuntimeAdapter`: it composes
|
|
5
|
+
* `packages/runtime` (real agent loop + real tool registry + real session log)
|
|
6
|
+
* with an OFFLINE `Llm` seam, so the whole HTTP contract can be exercised end to
|
|
7
|
+
* end without a provider. The P4 fake adapter stays in
|
|
8
|
+
* `../fake-runtime-adapter.ts` for deterministic HTTP-layer tests.
|
|
9
|
+
*/
|
|
10
|
+
export * from "./engine-profile.js";
|
|
11
|
+
export * from "./engine-session.js";
|
|
12
|
+
export * from "./engine-plugins.js";
|
|
13
|
+
export * from "./llm-assembly.js";
|
|
14
|
+
export * from "./provider-target.js";
|
|
15
|
+
export * from "./offline-llm.js";
|
|
16
|
+
export * from "./worker-bridge.js";
|
|
17
|
+
export * from "./real-runtime-adapter.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The real engine assembly of `apps/studio` (P5 integration).
|
|
3
|
+
*
|
|
4
|
+
* `createRealRuntimeAdapter` is the production `RuntimeAdapter`: it composes
|
|
5
|
+
* `packages/runtime` (real agent loop + real tool registry + real session log)
|
|
6
|
+
* with an OFFLINE `Llm` seam, so the whole HTTP contract can be exercised end to
|
|
7
|
+
* end without a provider. The P4 fake adapter stays in
|
|
8
|
+
* `../fake-runtime-adapter.ts` for deterministic HTTP-layer tests.
|
|
9
|
+
*/
|
|
10
|
+
export * from "./engine-profile.js";
|
|
11
|
+
export * from "./engine-session.js";
|
|
12
|
+
export * from "./engine-plugins.js";
|
|
13
|
+
export * from "./llm-assembly.js";
|
|
14
|
+
export * from "./provider-target.js";
|
|
15
|
+
export * from "./offline-llm.js";
|
|
16
|
+
export * from "./worker-bridge.js";
|
|
17
|
+
export * from "./real-runtime-adapter.js";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The usage ledger's HOST-side views (E-P1, capability 3, W785).
|
|
3
|
+
*
|
|
4
|
+
* `RealEngine` (real-runtime-adapter.ts) owns the lifecycle of the process-wide
|
|
5
|
+
* ledger file, but it does not own the SHAPES the HTTP layer promises. Those live
|
|
6
|
+
* here as three pure functions so the adapter stays a thin seam (and stays inside
|
|
7
|
+
* the §4.1 file budget):
|
|
8
|
+
*
|
|
9
|
+
* - [usageLedgerView] — `GET /api/usage/ledger`: the runtime's own `queryLedger`
|
|
10
|
+
* over the file's records, or the honest `ok:false` when there is no ledger;
|
|
11
|
+
* - [costBlockView] — `/api/status.cost`: one session's block, `null` when
|
|
12
|
+
* there is no ledger (the handler then omits the optional key);
|
|
13
|
+
* - [ledgerLabel] — the label rows are booked under: the TRUSTED session
|
|
14
|
+
* id the caller resolved (`<workspace>/<session>`, the SAME label
|
|
15
|
+
* `session-compose.ts` books under), `sessionId ?? "cli-main"` when it is
|
|
16
|
+
* null. W878: never inferred from `dir`.
|
|
17
|
+
*
|
|
18
|
+
* Both readers call `file.readAll()` on every request — the current file plus
|
|
19
|
+
* the rolled `.1` segment (P2-2), so a rotation cannot zero the cumulative
|
|
20
|
+
* view. The ledger stays the single source of truth, so nothing is cached and
|
|
21
|
+
* a row booked a millisecond ago is visible to the next poll.
|
|
22
|
+
*/
|
|
23
|
+
import { type LedgerCostBlock, type LedgerQuery, type LedgerQueryResult, type UsageLedgerFile } from "@celestea/runtime";
|
|
24
|
+
/** `ok:false` = there is no ledger to read here; never a fabricated empty view. */
|
|
25
|
+
export type LedgerUnavailable = {
|
|
26
|
+
ok: false;
|
|
27
|
+
error: string;
|
|
28
|
+
};
|
|
29
|
+
/** `GET /api/usage/ledger` over the process ledger (see the module header). */
|
|
30
|
+
export declare function usageLedgerView(file: UsageLedgerFile | null, q: LedgerQuery): LedgerQueryResult | LedgerUnavailable;
|
|
31
|
+
/** `/api/status.cost` over the process ledger (see the module header). */
|
|
32
|
+
export declare function costBlockView(file: UsageLedgerFile | null, session: string | null, dir: string | null): LedgerCostBlock | null;
|
|
33
|
+
/**
|
|
34
|
+
* The ledger label of a session — the same one its rows are booked under.
|
|
35
|
+
*
|
|
36
|
+
* W878: the id is an INPUT, not something to re-derive from `dir`. The `_dir`
|
|
37
|
+
* parameter is retained only because the adapter's `costBlock` seam already
|
|
38
|
+
* holds it; it is deliberately ignored.
|
|
39
|
+
*/
|
|
40
|
+
export declare function ledgerLabel(session: string | null, _dir: string | null): string;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The usage ledger's HOST-side views (E-P1, capability 3, W785).
|
|
3
|
+
*
|
|
4
|
+
* `RealEngine` (real-runtime-adapter.ts) owns the lifecycle of the process-wide
|
|
5
|
+
* ledger file, but it does not own the SHAPES the HTTP layer promises. Those live
|
|
6
|
+
* here as three pure functions so the adapter stays a thin seam (and stays inside
|
|
7
|
+
* the §4.1 file budget):
|
|
8
|
+
*
|
|
9
|
+
* - [usageLedgerView] — `GET /api/usage/ledger`: the runtime's own `queryLedger`
|
|
10
|
+
* over the file's records, or the honest `ok:false` when there is no ledger;
|
|
11
|
+
* - [costBlockView] — `/api/status.cost`: one session's block, `null` when
|
|
12
|
+
* there is no ledger (the handler then omits the optional key);
|
|
13
|
+
* - [ledgerLabel] — the label rows are booked under: the TRUSTED session
|
|
14
|
+
* id the caller resolved (`<workspace>/<session>`, the SAME label
|
|
15
|
+
* `session-compose.ts` books under), `sessionId ?? "cli-main"` when it is
|
|
16
|
+
* null. W878: never inferred from `dir`.
|
|
17
|
+
*
|
|
18
|
+
* Both readers call `file.readAll()` on every request — the current file plus
|
|
19
|
+
* the rolled `.1` segment (P2-2), so a rotation cannot zero the cumulative
|
|
20
|
+
* view. The ledger stays the single source of truth, so nothing is cached and
|
|
21
|
+
* a row booked a millisecond ago is visible to the next poll.
|
|
22
|
+
*/
|
|
23
|
+
import { HOST_SESSION_ID, ledgerCostBlock, queryLedger, } from "@celestea/runtime";
|
|
24
|
+
/** `GET /api/usage/ledger` over the process ledger (see the module header). */
|
|
25
|
+
export function usageLedgerView(file, q) {
|
|
26
|
+
if (file === null)
|
|
27
|
+
return { ok: false, error: "usage ledger disabled" };
|
|
28
|
+
try {
|
|
29
|
+
return queryLedger(file.readAll(), q);
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** `/api/status.cost` over the process ledger (see the module header). */
|
|
36
|
+
export function costBlockView(file, session, dir) {
|
|
37
|
+
if (file === null)
|
|
38
|
+
return null;
|
|
39
|
+
try {
|
|
40
|
+
return ledgerCostBlock(file.readAll(), ledgerLabel(session, dir));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Cost is a view: a broken ledger must not fail a status poll.
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The ledger label of a session — the same one its rows are booked under.
|
|
49
|
+
*
|
|
50
|
+
* W878: the id is an INPUT, not something to re-derive from `dir`. The `_dir`
|
|
51
|
+
* parameter is retained only because the adapter's `costBlock` seam already
|
|
52
|
+
* holds it; it is deliberately ignored.
|
|
53
|
+
*/
|
|
54
|
+
export function ledgerLabel(session, _dir) {
|
|
55
|
+
return session ?? HOST_SESSION_ID;
|
|
56
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine LLM assembly (W511): profile -> `Llm`, live by default.
|
|
3
|
+
*
|
|
4
|
+
* Two seams, one switch:
|
|
5
|
+
* - LIVE `@celestea/llm`'s OpenAI-compatible client, assembled from the
|
|
6
|
+
* engine profile (model / base_url / reasoning_effort /
|
|
7
|
+
* max_output_tokens) + the environment (api key, the three
|
|
8
|
+
* CELESTEA_LLM_* timeouts);
|
|
9
|
+
* - OFFLINE the deterministic in-process seam (`createOfflineLlm`), still
|
|
10
|
+
* available for tests and replay via `CELESTEA_LLM_MODE=offline`
|
|
11
|
+
* or an injected `llm` factory on the adapter.
|
|
12
|
+
*
|
|
13
|
+
* `@celestea/llm` speaks its own (parity) seam types; core owns the seam the
|
|
14
|
+
* engine consumes. `liveEngineLlm` is the ONE adapter between them: requests
|
|
15
|
+
* pass through unchanged (identical shapes), stream events are copied field by
|
|
16
|
+
* field, and a provider stream-idle failure (`kindOf: "timeout"`) is reported
|
|
17
|
+
* as core's `"stream"` terminal — the message keeps the `llm timeout:` prefix,
|
|
18
|
+
* so the distinction survives in the transcript.
|
|
19
|
+
*/
|
|
20
|
+
import { type LiveLlmView, type LlmMode, type LlmTarget } from "@celestea/llm";
|
|
21
|
+
import type { Llm } from "@celestea/core";
|
|
22
|
+
import type { Profile } from "@celestea/runtime";
|
|
23
|
+
import type { EngineProfile } from "../runtime-adapter.js";
|
|
24
|
+
import { type ProviderLookup, type ProviderTarget } from "./provider-target.js";
|
|
25
|
+
import type { Llm as ProviderLlm } from "@celestea/llm";
|
|
26
|
+
/**
|
|
27
|
+
* The profile fields a live client needs, from either profile shape (`Profile`
|
|
28
|
+
* carries `context_window_tokens`, the host view `context_window`).
|
|
29
|
+
*/
|
|
30
|
+
export declare function llmProfileOf(profile: Profile | EngineProfile): {
|
|
31
|
+
model: string;
|
|
32
|
+
base_url: string;
|
|
33
|
+
api_key_env: string;
|
|
34
|
+
reasoning_effort: string | null;
|
|
35
|
+
max_output_tokens: number | null;
|
|
36
|
+
context_window_tokens: number;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The ONE provider-seam -> core-seam bridge: requests pass through unchanged and
|
|
40
|
+
* every event is copied field by field. Shared by [liveEngineLlm] and by the
|
|
41
|
+
* fallback decorator (E §4 P1), so a fallback turn is bridged exactly once and
|
|
42
|
+
* the two paths cannot drift.
|
|
43
|
+
*/
|
|
44
|
+
export declare function bridgeProviderLlm(inner: ProviderLlm): Llm;
|
|
45
|
+
/** The live provider behind the core `Llm` seam. */
|
|
46
|
+
export declare function liveEngineLlm(profile: Profile, env: NodeJS.ProcessEnv): Llm;
|
|
47
|
+
/**
|
|
48
|
+
* ONE fallback target's client (E §4.2.1 P1): the composed profile with that
|
|
49
|
+
* target's own `model` / `base_url` / key env applied, so each target keeps its
|
|
50
|
+
* own credential and its own three timeout tiers (§4.2.1 "三档超时语义逐字不变").
|
|
51
|
+
* Only env var NAMES travel here; the value is resolved inside `createLiveLlm`.
|
|
52
|
+
*/
|
|
53
|
+
export declare function liveEngineLlmFor(base: Profile, target: LlmTarget, env: NodeJS.ProcessEnv): Llm;
|
|
54
|
+
/** The engine's `Llm` for this generation: live, or the offline test seam. */
|
|
55
|
+
export declare function createEngineLlm(profile: Profile, env: NodeJS.ProcessEnv, mode?: LlmMode): Llm;
|
|
56
|
+
/** Secret-free description of the live adapter (startup logging / diagnostics). */
|
|
57
|
+
export declare function engineLlmView(profile: Profile | EngineProfile, env: NodeJS.ProcessEnv, mode?: LlmMode): LiveLlmView;
|
|
58
|
+
/**
|
|
59
|
+
* The startup profile: the host constants + env overrides ([defaultEngineProfile])
|
|
60
|
+
* with providers.json applied on top (model, base_url, and the api key into the
|
|
61
|
+
* process env — in memory only).
|
|
62
|
+
*/
|
|
63
|
+
export declare function startupEngineProfile(lookup: ProviderLookup, env: NodeJS.ProcessEnv, apiKeyEnv: string): {
|
|
64
|
+
profile: EngineProfile;
|
|
65
|
+
target: ProviderTarget;
|
|
66
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine LLM assembly (W511): profile -> `Llm`, live by default.
|
|
3
|
+
*
|
|
4
|
+
* Two seams, one switch:
|
|
5
|
+
* - LIVE `@celestea/llm`'s OpenAI-compatible client, assembled from the
|
|
6
|
+
* engine profile (model / base_url / reasoning_effort /
|
|
7
|
+
* max_output_tokens) + the environment (api key, the three
|
|
8
|
+
* CELESTEA_LLM_* timeouts);
|
|
9
|
+
* - OFFLINE the deterministic in-process seam (`createOfflineLlm`), still
|
|
10
|
+
* available for tests and replay via `CELESTEA_LLM_MODE=offline`
|
|
11
|
+
* or an injected `llm` factory on the adapter.
|
|
12
|
+
*
|
|
13
|
+
* `@celestea/llm` speaks its own (parity) seam types; core owns the seam the
|
|
14
|
+
* engine consumes. `liveEngineLlm` is the ONE adapter between them: requests
|
|
15
|
+
* pass through unchanged (identical shapes), stream events are copied field by
|
|
16
|
+
* field, and a provider stream-idle failure (`kindOf: "timeout"`) is reported
|
|
17
|
+
* as core's `"stream"` terminal — the message keeps the `llm timeout:` prefix,
|
|
18
|
+
* so the distinction survives in the transcript.
|
|
19
|
+
*/
|
|
20
|
+
import { createLiveLlm, liveLlmView, resolveLlmMode, } from "@celestea/llm";
|
|
21
|
+
import { defaultEngineProfile } from "./engine-profile.js";
|
|
22
|
+
import { createOfflineLlm } from "./offline-llm.js";
|
|
23
|
+
import { applyProviderTarget, resolveProviderTarget, } from "./provider-target.js";
|
|
24
|
+
/**
|
|
25
|
+
* The profile fields a live client needs, from either profile shape (`Profile`
|
|
26
|
+
* carries `context_window_tokens`, the host view `context_window`).
|
|
27
|
+
*/
|
|
28
|
+
export function llmProfileOf(profile) {
|
|
29
|
+
return {
|
|
30
|
+
model: profile.model,
|
|
31
|
+
base_url: profile.base_url,
|
|
32
|
+
api_key_env: profile.api_key_env,
|
|
33
|
+
reasoning_effort: profile.reasoning_effort,
|
|
34
|
+
max_output_tokens: profile.max_output_tokens,
|
|
35
|
+
context_window_tokens: "context_window_tokens" in profile ? profile.context_window_tokens : profile.context_window,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** One provider stream event -> the core event the engine consumes. */
|
|
39
|
+
function coreEvent(event) {
|
|
40
|
+
switch (event.kind) {
|
|
41
|
+
case "text":
|
|
42
|
+
return { kind: "text", text: event.text };
|
|
43
|
+
case "thinking":
|
|
44
|
+
return { kind: "thinking", text: event.text };
|
|
45
|
+
case "usage":
|
|
46
|
+
return { kind: "usage", usage: event.usage };
|
|
47
|
+
case "done":
|
|
48
|
+
return { kind: "done", message: event.message };
|
|
49
|
+
case "interrupted":
|
|
50
|
+
return { kind: "interrupted" };
|
|
51
|
+
case "failed":
|
|
52
|
+
return {
|
|
53
|
+
kind: "failed",
|
|
54
|
+
// core's union has no "timeout" member: an SSE idle guard is a broken
|
|
55
|
+
// stream there, and the "llm timeout:" prefix keeps the detail.
|
|
56
|
+
kindOf: event.kindOf === "generate" ? "generate" : "stream",
|
|
57
|
+
message: event.message,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Re-yield a provider stream as a core stream. */
|
|
62
|
+
async function* coreStream(stream) {
|
|
63
|
+
for await (const event of stream)
|
|
64
|
+
yield coreEvent(event);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The ONE provider-seam -> core-seam bridge: requests pass through unchanged and
|
|
68
|
+
* every event is copied field by field. Shared by [liveEngineLlm] and by the
|
|
69
|
+
* fallback decorator (E §4 P1), so a fallback turn is bridged exactly once and
|
|
70
|
+
* the two paths cannot drift.
|
|
71
|
+
*/
|
|
72
|
+
export function bridgeProviderLlm(inner) {
|
|
73
|
+
return {
|
|
74
|
+
async generate(req) {
|
|
75
|
+
return coreStream(await inner.generate(req));
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** The live provider behind the core `Llm` seam. */
|
|
80
|
+
export function liveEngineLlm(profile, env) {
|
|
81
|
+
return bridgeProviderLlm(createLiveLlm(llmProfileOf(profile), env));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* ONE fallback target's client (E §4.2.1 P1): the composed profile with that
|
|
85
|
+
* target's own `model` / `base_url` / key env applied, so each target keeps its
|
|
86
|
+
* own credential and its own three timeout tiers (§4.2.1 "三档超时语义逐字不变").
|
|
87
|
+
* Only env var NAMES travel here; the value is resolved inside `createLiveLlm`.
|
|
88
|
+
*/
|
|
89
|
+
export function liveEngineLlmFor(base, target, env) {
|
|
90
|
+
return liveEngineLlm({
|
|
91
|
+
...base,
|
|
92
|
+
model: target.model,
|
|
93
|
+
base_url: target.baseUrl ?? base.base_url,
|
|
94
|
+
api_key_env: target.apiKeyEnv ?? base.api_key_env,
|
|
95
|
+
}, env);
|
|
96
|
+
}
|
|
97
|
+
/** The engine's `Llm` for this generation: live, or the offline test seam. */
|
|
98
|
+
export function createEngineLlm(profile, env, mode = resolveLlmMode(env)) {
|
|
99
|
+
return mode === "offline" ? createOfflineLlm() : liveEngineLlm(profile, env);
|
|
100
|
+
}
|
|
101
|
+
/** Secret-free description of the live adapter (startup logging / diagnostics). */
|
|
102
|
+
export function engineLlmView(profile, env, mode) {
|
|
103
|
+
return liveLlmView(llmProfileOf(profile), env, mode);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The startup profile: the host constants + env overrides ([defaultEngineProfile])
|
|
107
|
+
* with providers.json applied on top (model, base_url, and the api key into the
|
|
108
|
+
* process env — in memory only).
|
|
109
|
+
*/
|
|
110
|
+
export function startupEngineProfile(lookup, env, apiKeyEnv) {
|
|
111
|
+
const base = defaultEngineProfile(env, apiKeyEnv);
|
|
112
|
+
const target = resolveProviderTarget(lookup, env, base);
|
|
113
|
+
return applyProviderTarget(base, target, lookup, env);
|
|
114
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OFFLINE engine LLM — a deterministic, in-process `Llm` seam implementation.
|
|
3
|
+
*
|
|
4
|
+
* Purpose: the TS host must be testable and replayable with ZERO network access
|
|
5
|
+
* (`禁真网`). This adapter answers every `generate()` from a local script, so a
|
|
6
|
+
* real turn (real `DefaultAgentLoop`, real `ToolRegistry`, real session log)
|
|
7
|
+
* runs end to end without a provider.
|
|
8
|
+
*
|
|
9
|
+
* It is not a stub that fakes the loop: the loop, the tool dispatch, the log and
|
|
10
|
+
* the SSE frames are the production ones. Only the model is local.
|
|
11
|
+
*
|
|
12
|
+
* Two behaviours:
|
|
13
|
+
* - a normal turn answers with the next scripted step (text / thinking /
|
|
14
|
+
* tool calls / usage), defaulting to a deterministic `echo:` reply;
|
|
15
|
+
* - a COMPACTION request (the four-section summarizer prompt) answers with a
|
|
16
|
+
* deterministic four-section summary derived from the transcript, so a
|
|
17
|
+
* compaction is reproducible byte for byte across runs.
|
|
18
|
+
*
|
|
19
|
+
* `deltaMs` interleaves real delays between frames: a test can abort mid-stream
|
|
20
|
+
* and observe the cooperative cancel path instead of a turn that already
|
|
21
|
+
* finished.
|
|
22
|
+
*/
|
|
23
|
+
import { type Llm, type Message, type ModelRequest, type StreamEvent, type ToolCall, type Usage } from "@celestea/core";
|
|
24
|
+
/** One scripted model step. */
|
|
25
|
+
export interface OfflineStep {
|
|
26
|
+
thinking?: string;
|
|
27
|
+
text?: string;
|
|
28
|
+
tool_calls?: readonly ToolCall[];
|
|
29
|
+
/** Answer with a provider failure instead of a done frame. */
|
|
30
|
+
fail?: string;
|
|
31
|
+
/** Answer with a torn stream (the `interrupted` terminal state). */
|
|
32
|
+
interrupted?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface OfflineLlmOptions {
|
|
35
|
+
/**
|
|
36
|
+
* Steps consumed in order (a caller-supplied array is kept LIVE, so a test can
|
|
37
|
+
* append steps after the engine was composed); once exhausted the default
|
|
38
|
+
* deterministic echo step is used.
|
|
39
|
+
*/
|
|
40
|
+
script?: OfflineStep[];
|
|
41
|
+
/** Delay between frames in ms (0 = as fast as the microtask queue allows). */
|
|
42
|
+
deltaMs?: number;
|
|
43
|
+
/** Text chunk size per `text` delta frame. */
|
|
44
|
+
chunkChars?: number;
|
|
45
|
+
/** Summary text override for compaction requests. */
|
|
46
|
+
summary?: (transcript: string) => string;
|
|
47
|
+
/**
|
|
48
|
+
* W729: every request the engine actually sent (`system` + messages), so a
|
|
49
|
+
* test can assert WHICH system prompt each session's turn carried instead of
|
|
50
|
+
* re-deriving it. Shared across instances when the caller passes one array.
|
|
51
|
+
*/
|
|
52
|
+
onRequest?: (req: ModelRequest) => void;
|
|
53
|
+
}
|
|
54
|
+
export interface OfflineLlm extends Llm {
|
|
55
|
+
/** How many `generate()` calls were served (diagnostics / assertions). */
|
|
56
|
+
readonly calls: number;
|
|
57
|
+
}
|
|
58
|
+
/** Deterministic usage so `cache_hit_ratio` / `context_usage` are assertable. */
|
|
59
|
+
export declare function offlineUsage(req: ModelRequest, answer: string): Usage;
|
|
60
|
+
/** True when the request is the compaction summarizer (not a chat turn). */
|
|
61
|
+
export declare function isCompactionRequest(req: ModelRequest): boolean;
|
|
62
|
+
/** The default offline summary: four sections + a stable digest of the input. */
|
|
63
|
+
export declare function offlineSummary(transcript: string): string;
|
|
64
|
+
/** Split text into fixed-size chunks (never splitting a code point). */
|
|
65
|
+
export declare function chunkText(text: string, size: number): string[];
|
|
66
|
+
/** Assistant message of a step (tool calls win over text, like the provider). */
|
|
67
|
+
export declare function stepMessage(step: OfflineStep): Message;
|
|
68
|
+
/** The frame list of one scripted step (before timing is applied). */
|
|
69
|
+
export declare function stepFrames(step: OfflineStep, req: ModelRequest, chunk: number): StreamEvent[];
|
|
70
|
+
/** Build the deterministic offline engine LLM. */
|
|
71
|
+
export declare function createOfflineLlm(opts?: OfflineLlmOptions): OfflineLlm;
|