@basein/runner 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +276 -0
  3. package/dist/auth/client.d.ts +85 -0
  4. package/dist/auth/client.js +284 -0
  5. package/dist/bin/bir-hooks.d.ts +48 -0
  6. package/dist/bin/bir-hooks.js +201 -0
  7. package/dist/bin/bir-proxy.d.ts +45 -0
  8. package/dist/bin/bir-proxy.js +207 -0
  9. package/dist/bin/bir-scenario.d.ts +24 -0
  10. package/dist/bin/bir-scenario.js +177 -0
  11. package/dist/bin/bir.d.ts +21 -0
  12. package/dist/bin/bir.js +876 -0
  13. package/dist/config/adapters/claude-code.d.ts +76 -0
  14. package/dist/config/adapters/claude-code.js +181 -0
  15. package/dist/config/adapters/generic.d.ts +17 -0
  16. package/dist/config/adapters/generic.js +36 -0
  17. package/dist/config/generate.d.ts +127 -0
  18. package/dist/config/generate.js +114 -0
  19. package/dist/config/resolve.d.ts +68 -0
  20. package/dist/config/resolve.js +132 -0
  21. package/dist/control/client.d.ts +56 -0
  22. package/dist/control/client.js +86 -0
  23. package/dist/control/correlation.d.ts +86 -0
  24. package/dist/control/correlation.js +0 -0
  25. package/dist/control/discovery.d.ts +50 -0
  26. package/dist/control/discovery.js +123 -0
  27. package/dist/control/ordering.d.ts +38 -0
  28. package/dist/control/ordering.js +44 -0
  29. package/dist/control/paths.d.ts +32 -0
  30. package/dist/control/paths.js +56 -0
  31. package/dist/control/server.d.ts +272 -0
  32. package/dist/control/server.js +1131 -0
  33. package/dist/control/transcript.d.ts +75 -0
  34. package/dist/control/transcript.js +241 -0
  35. package/dist/index.d.ts +37 -0
  36. package/dist/index.js +32 -0
  37. package/dist/jsonrpc/framing.d.ts +49 -0
  38. package/dist/jsonrpc/framing.js +143 -0
  39. package/dist/jsonrpc/types.d.ts +52 -0
  40. package/dist/jsonrpc/types.js +46 -0
  41. package/dist/proxy/intercept.d.ts +55 -0
  42. package/dist/proxy/intercept.js +147 -0
  43. package/dist/proxy/relay.d.ts +97 -0
  44. package/dist/proxy/relay.js +166 -0
  45. package/dist/proxy/session.d.ts +116 -0
  46. package/dist/proxy/session.js +319 -0
  47. package/dist/record/housekeeping.d.ts +34 -0
  48. package/dist/record/housekeeping.js +39 -0
  49. package/dist/record/queue.d.ts +48 -0
  50. package/dist/record/queue.js +96 -0
  51. package/dist/record/recorder.d.ts +111 -0
  52. package/dist/record/recorder.js +39 -0
  53. package/dist/record/redact.d.ts +37 -0
  54. package/dist/record/redact.js +119 -0
  55. package/dist/record/remote-recorder.d.ts +110 -0
  56. package/dist/record/remote-recorder.js +301 -0
  57. package/dist/record/truncate.d.ts +36 -0
  58. package/dist/record/truncate.js +85 -0
  59. package/dist/replay/bundle.d.ts +36 -0
  60. package/dist/replay/bundle.js +89 -0
  61. package/dist/replay/controller.d.ts +300 -0
  62. package/dist/replay/controller.js +807 -0
  63. package/dist/replay/coverage.d.ts +41 -0
  64. package/dist/replay/coverage.js +56 -0
  65. package/dist/replay/derive.d.ts +58 -0
  66. package/dist/replay/derive.js +166 -0
  67. package/dist/replay/executor.d.ts +78 -0
  68. package/dist/replay/executor.js +233 -0
  69. package/dist/replay/logic.d.ts +31 -0
  70. package/dist/replay/logic.js +50 -0
  71. package/dist/replay/plan.d.ts +181 -0
  72. package/dist/replay/plan.js +397 -0
  73. package/dist/replay/pricing.d.ts +41 -0
  74. package/dist/replay/pricing.js +76 -0
  75. package/dist/replay/source-run.d.ts +50 -0
  76. package/dist/replay/source-run.js +98 -0
  77. package/dist/replay/tool-error.d.ts +22 -0
  78. package/dist/replay/tool-error.js +60 -0
  79. package/dist/replay/types.d.ts +116 -0
  80. package/dist/replay/types.js +35 -0
  81. package/dist/upstream/client.d.ts +78 -0
  82. package/dist/upstream/client.js +114 -0
  83. package/dist/upstream/http-client.d.ts +78 -0
  84. package/dist/upstream/http-client.js +261 -0
  85. package/dist/upstream/lazy-client.d.ts +31 -0
  86. package/dist/upstream/lazy-client.js +53 -0
  87. package/dist/upstream/stdio-client.d.ts +57 -0
  88. package/dist/upstream/stdio-client.js +203 -0
  89. package/dist/util/log.d.ts +27 -0
  90. package/dist/util/log.js +51 -0
  91. package/dist/util/version.d.ts +2 -0
  92. package/dist/util/version.js +40 -0
  93. package/docs/BaseInstRunner.md +621 -0
  94. package/docs/calculatedReplay.md +1185 -0
  95. package/docs/calculatedReplayGuide.md +448 -0
  96. package/docs/installRun.md +413 -0
  97. package/docs/mcpmark.md +752 -0
  98. package/docs/quickstart.md +201 -0
  99. package/docs/t-bench.md +394 -0
  100. package/package.json +56 -0
@@ -0,0 +1,76 @@
1
+ /**
2
+ * pricing — token cost, in $/million tokens (docs/calculatedReplay.md §11.3).
3
+ *
4
+ * COPIED FROM THE SERVICE, NOT FROM RRepeat. A saving is `baseline − actual`,
5
+ * where the baseline is priced by BaseIn's `src/scenarios/cost.ts` and the actual
6
+ * by this file. The two are maintained by hand in separate repositories, so any
7
+ * drift between them is fake money — and {@link PRICING_VERSION} is stamped on
8
+ * every execution report so the server can store a mismatched one *unmeasured*
9
+ * rather than silently differencing two different tables.
10
+ *
11
+ * The service owns the baseline, so the service's table is the one to match.
12
+ * That is not a stylistic preference: at the time of writing, RRepeat and BaseIn
13
+ * both declare version `2026-08-30` while pricing `claude-opus-4-6` at 3.0/15.0
14
+ * and 5.0/25.0 respectively. Equal versions are supposed to certify that two
15
+ * numbers are comparable. For that row, right now, they are not.
16
+ *
17
+ * Bump the version — on **every** side — whenever a row changes.
18
+ */
19
+ /** Identifies {@link MODEL_PRICING}. Must equal the service's constant. */
20
+ export const PRICING_VERSION = "2026-08-30";
21
+ /** Cache reads bill at 0.1x input; a 5-minute cache write at 1.25x. */
22
+ const priced = (input, output) => ({
23
+ input,
24
+ output,
25
+ cacheRead: input * 0.1,
26
+ cacheWrite: input * 1.25,
27
+ });
28
+ const MODEL_PRICING = {
29
+ "claude-haiku-4-5": priced(1.0, 5.0),
30
+ "claude-haiku-4-5-20251001": priced(1.0, 5.0),
31
+ "claude-sonnet-4-6": priced(3.0, 15.0),
32
+ "claude-sonnet-5": priced(2.0, 10.0),
33
+ "claude-opus-4-6": priced(5.0, 25.0),
34
+ "claude-opus-4-7": priced(5.0, 25.0),
35
+ "claude-opus-4-8": priced(5.0, 25.0),
36
+ "claude-opus-5": priced(5.0, 25.0),
37
+ "claude-fable-5": priced(10.0, 50.0),
38
+ };
39
+ /**
40
+ * Tolerate provider prefixes (`anthropic/`, `us.anthropic.`), date snapshots and
41
+ * unseen version bumps by falling back to the longest matching family, so a new
42
+ * snapshot never silently prices at $0.
43
+ */
44
+ export function resolveModelPricing(model) {
45
+ const exact = MODEL_PRICING[model];
46
+ if (exact)
47
+ return exact;
48
+ // Strip a provider prefix: "us.anthropic.claude-haiku-4-5-v1:0" → "claude-haiku-4-5-v1:0".
49
+ const idx = model.indexOf("claude-");
50
+ const bare = idx >= 0 ? model.slice(idx) : model;
51
+ if (MODEL_PRICING[bare])
52
+ return MODEL_PRICING[bare];
53
+ // Longest declared family that prefixes the name — so a dated snapshot of a
54
+ // known model prices as that model rather than as nothing.
55
+ let best;
56
+ for (const key of Object.keys(MODEL_PRICING)) {
57
+ if (bare.startsWith(key) && (!best || key.length > best.length))
58
+ best = key;
59
+ }
60
+ return best ? MODEL_PRICING[best] : undefined;
61
+ }
62
+ export function hasModelPricing(model) {
63
+ return resolveModelPricing(model) !== undefined;
64
+ }
65
+ /** Cost in USD. An unpriced model yields 0 — the caller reports `measured: false`. */
66
+ export function calculateCostUsd(model, usage) {
67
+ const p = resolveModelPricing(model);
68
+ if (!p)
69
+ return 0;
70
+ const perToken = 1e-6;
71
+ return (usage.inputTokens * p.input * perToken +
72
+ usage.outputTokens * p.output * perToken +
73
+ (usage.cacheReadTokens ?? 0) * p.cacheRead * perToken +
74
+ (usage.cacheCreationTokens ?? 0) * p.cacheWrite * perToken);
75
+ }
76
+ //# sourceMappingURL=pricing.js.map
@@ -0,0 +1,50 @@
1
+ /**
2
+ * source-run — recorded outputs for steps that cannot be executed here
3
+ * (docs/calculatedReplay.md §8.1).
4
+ *
5
+ * RRepeat reads these from `SerializedScenarioStep.recordedOutput`, a field whose
6
+ * own comment says it is optional because *"it is the server's payload that
7
+ * decides whether to send it, and older payloads do not."* Verify that before
8
+ * relying on it: **no payload does.** BaseIn's `scenario_steps` table has
9
+ * `tool_input_logic`, `tool_output_logic`, `reasoning` and `reasoning_vector` —
10
+ * and no `recorded_output`. RRepeat's fallback is unreachable in production.
11
+ *
12
+ * There is a path that needs no server change, and the match payload already
13
+ * carries its key: `matched.runId` is the canonical run the scenario was
14
+ * calculated from, and `GET /recordings/runs/:runId` returns its steps with
15
+ * their `tool_output`.
16
+ *
17
+ * The queue semantics deliberately match the server's own dry replay
18
+ * (`recordedOutputsByTool` in `scenarios/replay.ts`): a FIFO per tool name, drawn
19
+ * in order, with a per-tool cursor. A scenario that behaved one way under
20
+ * `bir scenario replay --dry` and another way here would be untestable.
21
+ *
22
+ * Fetched **lazily** — only when a step actually needs it — and cached for the
23
+ * turn, so a fully-executable replay never makes this call at all.
24
+ */
25
+ import type { SerializedScenarioStep } from "./types.js";
26
+ export interface SourceRunOptions {
27
+ baseUrl: string;
28
+ runId: string;
29
+ /** Bearer token; refreshed by the caller, read fresh on each fetch. */
30
+ token: () => string;
31
+ fetchImpl?: typeof fetch;
32
+ timeoutMs?: number;
33
+ }
34
+ export declare class SourceRunOutputs {
35
+ private readonly opts;
36
+ /** toolName → recorded outputs, in run order. */
37
+ private byTool?;
38
+ private cursor;
39
+ private loading?;
40
+ constructor(opts: SourceRunOptions);
41
+ /**
42
+ * The next recorded output for `step`'s tool, or undefined when the run has
43
+ * none left (or could not be fetched). Never throws — a missing recorded
44
+ * output means the step is skipped, which the caller already handles.
45
+ */
46
+ outputFor(step: SerializedScenarioStep): Promise<string | undefined>;
47
+ private load;
48
+ private fetchRun;
49
+ }
50
+ //# sourceMappingURL=source-run.d.ts.map
@@ -0,0 +1,98 @@
1
+ /**
2
+ * source-run — recorded outputs for steps that cannot be executed here
3
+ * (docs/calculatedReplay.md §8.1).
4
+ *
5
+ * RRepeat reads these from `SerializedScenarioStep.recordedOutput`, a field whose
6
+ * own comment says it is optional because *"it is the server's payload that
7
+ * decides whether to send it, and older payloads do not."* Verify that before
8
+ * relying on it: **no payload does.** BaseIn's `scenario_steps` table has
9
+ * `tool_input_logic`, `tool_output_logic`, `reasoning` and `reasoning_vector` —
10
+ * and no `recorded_output`. RRepeat's fallback is unreachable in production.
11
+ *
12
+ * There is a path that needs no server change, and the match payload already
13
+ * carries its key: `matched.runId` is the canonical run the scenario was
14
+ * calculated from, and `GET /recordings/runs/:runId` returns its steps with
15
+ * their `tool_output`.
16
+ *
17
+ * The queue semantics deliberately match the server's own dry replay
18
+ * (`recordedOutputsByTool` in `scenarios/replay.ts`): a FIFO per tool name, drawn
19
+ * in order, with a per-tool cursor. A scenario that behaved one way under
20
+ * `bir scenario replay --dry` and another way here would be untestable.
21
+ *
22
+ * Fetched **lazily** — only when a step actually needs it — and cached for the
23
+ * turn, so a fully-executable replay never makes this call at all.
24
+ */
25
+ import { logDetail, logLine, errText } from "../util/log.js";
26
+ export class SourceRunOutputs {
27
+ opts;
28
+ /** toolName → recorded outputs, in run order. */
29
+ byTool;
30
+ cursor = new Map();
31
+ loading;
32
+ constructor(opts) {
33
+ this.opts = opts;
34
+ }
35
+ /**
36
+ * The next recorded output for `step`'s tool, or undefined when the run has
37
+ * none left (or could not be fetched). Never throws — a missing recorded
38
+ * output means the step is skipped, which the caller already handles.
39
+ */
40
+ async outputFor(step) {
41
+ try {
42
+ await this.load();
43
+ }
44
+ catch (err) {
45
+ logLine("replay.source_run_failed", {
46
+ run: this.opts.runId,
47
+ why: "no recorded outputs available for steps that cannot run here",
48
+ error: errText(err),
49
+ });
50
+ return undefined;
51
+ }
52
+ const list = this.byTool?.get(step.toolName);
53
+ if (!list || list.length === 0)
54
+ return undefined;
55
+ const at = this.cursor.get(step.toolName) ?? 0;
56
+ this.cursor.set(step.toolName, at + 1);
57
+ return list[at];
58
+ }
59
+ load() {
60
+ if (!this.loading) {
61
+ this.loading = this.fetchRun();
62
+ }
63
+ return this.loading;
64
+ }
65
+ async fetchRun() {
66
+ const doFetch = this.opts.fetchImpl ?? fetch;
67
+ const controller = new AbortController();
68
+ const timer = setTimeout(() => controller.abort(), this.opts.timeoutMs ?? 10_000);
69
+ timer.unref?.();
70
+ try {
71
+ const res = await doFetch(`${this.opts.baseUrl.replace(/\/+$/, "")}/recordings/runs/${this.opts.runId}`, {
72
+ headers: { authorization: `Bearer ${this.opts.token()}` },
73
+ signal: controller.signal,
74
+ });
75
+ if (!res.ok)
76
+ throw new Error(`HTTP ${res.status}`);
77
+ const body = (await res.json());
78
+ const map = new Map();
79
+ for (const row of body.steps ?? []) {
80
+ if (row.type !== "tool_response" || !row.tool_name)
81
+ continue;
82
+ const list = map.get(row.tool_name) ?? [];
83
+ list.push(row.tool_output ?? "{}");
84
+ map.set(row.tool_name, list);
85
+ }
86
+ this.byTool = map;
87
+ logDetail("replay.source_run_loaded", {
88
+ run: this.opts.runId,
89
+ tools: map.size,
90
+ outputs: [...map.values()].reduce((n, l) => n + l.length, 0),
91
+ });
92
+ }
93
+ finally {
94
+ clearTimeout(timer);
95
+ }
96
+ }
97
+ }
98
+ //# sourceMappingURL=source-run.js.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * tool-error — telling a tool that ran and *failed* from one that ran and worked.
3
+ *
4
+ * A step's tool call resolving is not the same as the step's work happening.
5
+ * `executeStep` rejects only when a tool could not be run here; a tool that ran
6
+ * and answered `Error: relation does not exist` resolves normally, with that
7
+ * error as its response — which is right for threading (the output logic may
8
+ * want to see it) and wrong for the verdict. Eight such steps once logged
9
+ * `ok=true` each, the plan reported `steered_full`, and the ledger booked its
10
+ * largest saving of the day on a replay that created nothing (docs/mcpmark.md §13).
11
+ *
12
+ * The signal is the MCP result itself: `isError: true` per the spec, or — for
13
+ * servers that report failures as ordinary text, `postgres-mcp` among them — a
14
+ * first text block that begins with the word "Error". Nothing else is read, and
15
+ * a built-in's hook-shaped response is never judged.
16
+ */
17
+ /**
18
+ * The error a serialized `CallToolResult` reports, or undefined when it does
19
+ * not. `serialized` is the proxy's own serialization — the whole result, as JSON.
20
+ */
21
+ export declare function toolResultError(serialized: string): string | undefined;
22
+ //# sourceMappingURL=tool-error.d.ts.map
@@ -0,0 +1,60 @@
1
+ /**
2
+ * tool-error — telling a tool that ran and *failed* from one that ran and worked.
3
+ *
4
+ * A step's tool call resolving is not the same as the step's work happening.
5
+ * `executeStep` rejects only when a tool could not be run here; a tool that ran
6
+ * and answered `Error: relation does not exist` resolves normally, with that
7
+ * error as its response — which is right for threading (the output logic may
8
+ * want to see it) and wrong for the verdict. Eight such steps once logged
9
+ * `ok=true` each, the plan reported `steered_full`, and the ledger booked its
10
+ * largest saving of the day on a replay that created nothing (docs/mcpmark.md §13).
11
+ *
12
+ * The signal is the MCP result itself: `isError: true` per the spec, or — for
13
+ * servers that report failures as ordinary text, `postgres-mcp` among them — a
14
+ * first text block that begins with the word "Error". Nothing else is read, and
15
+ * a built-in's hook-shaped response is never judged.
16
+ */
17
+ const MAX_ERROR_CHARS = 500;
18
+ /**
19
+ * The error a serialized `CallToolResult` reports, or undefined when it does
20
+ * not. `serialized` is the proxy's own serialization — the whole result, as JSON.
21
+ */
22
+ export function toolResultError(serialized) {
23
+ let parsed;
24
+ try {
25
+ parsed = JSON.parse(serialized);
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ if (typeof parsed === "string")
31
+ return looksLikeError(parsed) ? clip(parsed) : undefined;
32
+ if (!parsed || typeof parsed !== "object")
33
+ return undefined;
34
+ const result = parsed;
35
+ const text = firstText(result.content);
36
+ if (result.isError === true)
37
+ return clip(text ?? "the tool reported an error");
38
+ if (text !== undefined && looksLikeError(text))
39
+ return clip(text);
40
+ return undefined;
41
+ }
42
+ function firstText(content) {
43
+ if (!Array.isArray(content))
44
+ return undefined;
45
+ for (const block of content) {
46
+ const text = block?.text;
47
+ if (typeof text === "string")
48
+ return text;
49
+ }
50
+ return undefined;
51
+ }
52
+ /** "Error: …" / "ERROR: …" / "error occurred" — not "Errors were fixed". */
53
+ function looksLikeError(text) {
54
+ return /^\s*error\b/i.test(text);
55
+ }
56
+ function clip(text) {
57
+ const trimmed = text.trim();
58
+ return trimmed.length > MAX_ERROR_CHARS ? `${trimmed.slice(0, MAX_ERROR_CHARS)}…` : trimmed;
59
+ }
60
+ //# sourceMappingURL=tool-error.js.map
@@ -0,0 +1,116 @@
1
+ /**
2
+ * The scenario payload, exactly as the BaseIn service serializes it.
3
+ *
4
+ * Shapes mirror `serializeScenario` in the service's `src/scenarios/repo.ts`.
5
+ * They arrive on a similar-prompt hit, embedded in `POST /recordings/runs`'s
6
+ * `matched.scenario` — there is no follow-up fetch, so this is the whole
7
+ * contract (docs/calculatedReplay.md §2).
8
+ *
9
+ * Every field is typed as the service actually sends it, which means `null` is
10
+ * everywhere and nothing is assumed present. A scenario whose `state` is not
11
+ * `"ready"` carries an **empty** `steps` array, by the service's own rule.
12
+ */
13
+ /** One step: the tool, and the JS logic that recomputes its arguments. */
14
+ export interface SerializedScenarioStep {
15
+ stepIndex: number;
16
+ /** As the recorder wrote it — `mcp__<server>__<tool>` for an MCP call. */
17
+ toolName: string;
18
+ /** JS body: `(parameters, intent, respParams) => object`. Required. */
19
+ toolInputLogic: string;
20
+ /** JS body: `(toolOutput, parameters, intent, respParams) => object`. */
21
+ toolOutputLogic: string | null;
22
+ /** The model's reasoning for this step, used in the steering directive. */
23
+ reasoning: string | null;
24
+ /**
25
+ * The literal output recorded for this step.
26
+ *
27
+ * **The service does not send this today** — `scenario_steps` has no such
28
+ * column (docs/calculatedReplay.md §8.1). Kept in the type because the field
29
+ * is part of the wider contract and costs nothing to honour if it appears;
30
+ * {@link ../replay/source-run.ts} is what actually supplies recorded outputs.
31
+ */
32
+ recordedOutput?: string | null;
33
+ }
34
+ /** The `paramsObject` schema: one entry per parameter the scenario takes. */
35
+ export type ParamsSchema = Record<string, {
36
+ sampleValue: unknown;
37
+ description: string;
38
+ }>;
39
+ /** A `ready` scenario, as serialized by the BaseIn service on a match. */
40
+ export interface SerializedScenario {
41
+ id: string;
42
+ runId: string;
43
+ state: string;
44
+ intent: string;
45
+ parameters: unknown;
46
+ responseParameters: unknown;
47
+ /** name → { sampleValue, description }. The schema derivation fills in. */
48
+ paramsObject: ParamsSchema | null;
49
+ toolParameterMap: unknown;
50
+ toolOutputParameterMap: unknown;
51
+ /** JS body: `(parameters, intent) => object`. Applied once, before step 0. */
52
+ paramsLogic: string | null;
53
+ /** JS body: `(respParams, parameters, intent) => object`. The final model. */
54
+ responseParamsLogic: string | null;
55
+ outParamsObject?: unknown;
56
+ originalCostUsd: number | null;
57
+ originalMs: number | null;
58
+ calcCostUsd: number | null;
59
+ baselineCostUsd?: number | null;
60
+ baselineSamples?: number | null;
61
+ steps: SerializedScenarioStep[];
62
+ }
63
+ /**
64
+ * How a matched run's execution is classified when it is reported back.
65
+ * Mirrors the service's zod enum on `POST /scenarios/:id/executions`.
66
+ */
67
+ export type ExecutionOutcome = "steered_full" | "diverged" | "not_steered" | "failed";
68
+ /**
69
+ * Upgrade order. `not_steered` is the floor — the control group, and the safe
70
+ * answer if a turn dies mid-flight — and each later decision point can only move
71
+ * the classification forward, never back (docs/calculatedReplay.md §11.1).
72
+ */
73
+ export declare const OUTCOME_RANK: Record<ExecutionOutcome, number>;
74
+ /**
75
+ * What became of one step of a replay.
76
+ *
77
+ * `ok` — its tool ran here and its output threaded.
78
+ * `recorded` — its tool could not run here, so its recorded output stood in.
79
+ * A pass, with an asterisk: the values are from the source run.
80
+ * `skipped` — its tool could not run and there was no recorded output. The
81
+ * step did not happen at all; later steps read nothing from it.
82
+ * `failed` — the *scenario's own logic* threw, or its tool ran and reported
83
+ * an error (stage `tool_call`). Either way the step's work did
84
+ * not happen, and a plan cannot claim success past it.
85
+ *
86
+ * A step the run never reached is simply absent from the report. The console
87
+ * renders those as "not run" — asserting anything about a step that never
88
+ * started would be inventing a result.
89
+ */
90
+ export type ExecutionStepStatus = "ok" | "recorded" | "skipped" | "failed";
91
+ /**
92
+ * Where inside a step (or a run) something broke. Mirrors the service's
93
+ * `ExecutionStage` (its `src/db.ts`) — a closed vocabulary, so the console can
94
+ * say which part failed without parsing an error message.
95
+ */
96
+ export type ExecutionStage = "derive_params" | "params_logic" | "tool_input_logic" | "tool_call" | "tool_output_logic" | "response_logic" | "setup" | "unknown";
97
+ /**
98
+ * One step's verdict, as reported to the service alongside the execution.
99
+ *
100
+ * `stepIndex` is the scenario's own `scenario_steps.step_index`, not a position
101
+ * in this array: a diverged turn reports only the steps it reached, and the
102
+ * console lines them up against the full chain by index.
103
+ */
104
+ export interface ExecutionStepResult {
105
+ stepIndex: number;
106
+ toolName: string;
107
+ status: ExecutionStepStatus;
108
+ /** Only for `failed` / `skipped`. */
109
+ stage?: ExecutionStage;
110
+ /** Message only — never a tool payload; this is rendered in a web page. */
111
+ error?: string;
112
+ durationMs?: number;
113
+ }
114
+ /** Narrowing guard for a payload that is worth arming a plan from. */
115
+ export declare function isReadyScenario(value: unknown): value is SerializedScenario;
116
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The scenario payload, exactly as the BaseIn service serializes it.
3
+ *
4
+ * Shapes mirror `serializeScenario` in the service's `src/scenarios/repo.ts`.
5
+ * They arrive on a similar-prompt hit, embedded in `POST /recordings/runs`'s
6
+ * `matched.scenario` — there is no follow-up fetch, so this is the whole
7
+ * contract (docs/calculatedReplay.md §2).
8
+ *
9
+ * Every field is typed as the service actually sends it, which means `null` is
10
+ * everywhere and nothing is assumed present. A scenario whose `state` is not
11
+ * `"ready"` carries an **empty** `steps` array, by the service's own rule.
12
+ */
13
+ /**
14
+ * Upgrade order. `not_steered` is the floor — the control group, and the safe
15
+ * answer if a turn dies mid-flight — and each later decision point can only move
16
+ * the classification forward, never back (docs/calculatedReplay.md §11.1).
17
+ */
18
+ export const OUTCOME_RANK = {
19
+ not_steered: 0,
20
+ failed: 1,
21
+ diverged: 2,
22
+ steered_full: 3,
23
+ };
24
+ /** Narrowing guard for a payload that is worth arming a plan from. */
25
+ export function isReadyScenario(value) {
26
+ if (!value || typeof value !== "object")
27
+ return false;
28
+ const s = value;
29
+ return (typeof s.id === "string" &&
30
+ s.state === "ready" &&
31
+ Array.isArray(s.steps) &&
32
+ s.steps.length > 0 &&
33
+ s.steps.every((step) => typeof step?.toolName === "string" && typeof step?.toolInputLogic === "string"));
34
+ }
35
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,78 @@
1
+ /**
2
+ * UpstreamClient — the transport contract every upstream implements.
3
+ *
4
+ * DEVIATION FROM §5 OF THE DESIGN, AND WHY. The design sketches
5
+ * `request(method, params)` / `notify(...)` / `onIncoming(...)`. That surface
6
+ * cannot express the one thing D7 requires: sending a **response** toward the
7
+ * upstream. An upstream that issues `sampling/createMessage` or
8
+ * `elicitation/create` is waiting for the host's answer, and there is no
9
+ * `request`/`notify` shape that carries it — so a relay built on that interface
10
+ * would hang exactly the servers §8 says must work.
11
+ *
12
+ * The primitive here is therefore message-level: {@link send} takes any
13
+ * `JsonRpcMessage` verbatim and {@link onMessage} hands back any message the
14
+ * upstream originates, in either role. `request`/`notify` remain, implemented on
15
+ * top, for the few places that genuinely want a call (`bir doctor`, tests) — and
16
+ * they allocate ids in the reserved `bir:` space so they can never collide with a
17
+ * host id passing through (Phase 1.3).
18
+ */
19
+ import type { JsonRpcMessage } from "../jsonrpc/types.js";
20
+ /** Why the upstream connection ended. */
21
+ export interface UpstreamCloseInfo {
22
+ code?: number | null;
23
+ signal?: NodeJS.Signals | null;
24
+ error?: Error;
25
+ }
26
+ export interface UpstreamClient {
27
+ /** Resolves when the transport is usable; rejects if the upstream is unusable. */
28
+ readonly ready: Promise<void>;
29
+ /** True once the upstream has died or `close()` was called. */
30
+ readonly dead: boolean;
31
+ /** Send any JSON-RPC message toward the upstream, verbatim. */
32
+ send(msg: JsonRpcMessage): void;
33
+ /** Every message the upstream originates (requests, notifications, responses). */
34
+ onMessage(handler: (msg: JsonRpcMessage) => void): void;
35
+ /** Called once when the upstream connection ends, for any reason. */
36
+ onClose(handler: (info: UpstreamCloseInfo) => void): void;
37
+ /** Convenience call in the reserved `bir:` id space. Never used by the relay. */
38
+ request(method: string, params?: unknown, signal?: AbortSignal): Promise<unknown>;
39
+ /** Fire-and-forget notification toward the upstream. */
40
+ notify(method: string, params?: unknown): void;
41
+ close(): void;
42
+ }
43
+ /**
44
+ * The id prefix reserved for requests the proxy itself originates. Host ids pass
45
+ * through unchanged (the relay is 1:1), so the only way a collision could happen
46
+ * is a proxy-originated id colliding with a host one — impossible while every
47
+ * proxy id lives in this disjoint string space.
48
+ */
49
+ export declare const BIR_ID_PREFIX = "bir:";
50
+ export declare function isBirId(id: unknown): boolean;
51
+ /**
52
+ * Shared plumbing for the transports: `bir:`-id request tracking, handler
53
+ * fan-out, and one-shot close. Subclasses implement {@link deliver} (bytes out)
54
+ * and call {@link accept} (bytes in) / {@link closed} (connection over).
55
+ */
56
+ export declare abstract class BaseUpstreamClient implements UpstreamClient {
57
+ abstract readonly ready: Promise<void>;
58
+ private readonly messageHandlers;
59
+ private readonly closeHandlers;
60
+ private readonly pending;
61
+ private nextId;
62
+ private isDead;
63
+ get dead(): boolean;
64
+ abstract send(msg: JsonRpcMessage): void;
65
+ abstract close(): void;
66
+ onMessage(handler: (msg: JsonRpcMessage) => void): void;
67
+ onClose(handler: (info: UpstreamCloseInfo) => void): void;
68
+ request(method: string, params?: unknown, signal?: AbortSignal): Promise<unknown>;
69
+ notify(method: string, params?: unknown): void;
70
+ /**
71
+ * Route one upstream message. Responses to proxy-originated (`bir:`) ids are
72
+ * consumed here and never reach the relay; everything else fans out verbatim.
73
+ */
74
+ protected accept(msg: JsonRpcMessage): void;
75
+ /** Mark the connection over. Idempotent; fails every in-flight `request()`. */
76
+ protected closed(info: UpstreamCloseInfo): void;
77
+ }
78
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1,114 @@
1
+ /**
2
+ * UpstreamClient — the transport contract every upstream implements.
3
+ *
4
+ * DEVIATION FROM §5 OF THE DESIGN, AND WHY. The design sketches
5
+ * `request(method, params)` / `notify(...)` / `onIncoming(...)`. That surface
6
+ * cannot express the one thing D7 requires: sending a **response** toward the
7
+ * upstream. An upstream that issues `sampling/createMessage` or
8
+ * `elicitation/create` is waiting for the host's answer, and there is no
9
+ * `request`/`notify` shape that carries it — so a relay built on that interface
10
+ * would hang exactly the servers §8 says must work.
11
+ *
12
+ * The primitive here is therefore message-level: {@link send} takes any
13
+ * `JsonRpcMessage` verbatim and {@link onMessage} hands back any message the
14
+ * upstream originates, in either role. `request`/`notify` remain, implemented on
15
+ * top, for the few places that genuinely want a call (`bir doctor`, tests) — and
16
+ * they allocate ids in the reserved `bir:` space so they can never collide with a
17
+ * host id passing through (Phase 1.3).
18
+ */
19
+ /**
20
+ * The id prefix reserved for requests the proxy itself originates. Host ids pass
21
+ * through unchanged (the relay is 1:1), so the only way a collision could happen
22
+ * is a proxy-originated id colliding with a host one — impossible while every
23
+ * proxy id lives in this disjoint string space.
24
+ */
25
+ export const BIR_ID_PREFIX = "bir:";
26
+ export function isBirId(id) {
27
+ return typeof id === "string" && id.startsWith(BIR_ID_PREFIX);
28
+ }
29
+ /**
30
+ * Shared plumbing for the transports: `bir:`-id request tracking, handler
31
+ * fan-out, and one-shot close. Subclasses implement {@link deliver} (bytes out)
32
+ * and call {@link accept} (bytes in) / {@link closed} (connection over).
33
+ */
34
+ export class BaseUpstreamClient {
35
+ messageHandlers = [];
36
+ closeHandlers = [];
37
+ pending = new Map();
38
+ nextId = 1;
39
+ isDead = false;
40
+ get dead() {
41
+ return this.isDead;
42
+ }
43
+ onMessage(handler) {
44
+ this.messageHandlers.push(handler);
45
+ }
46
+ onClose(handler) {
47
+ if (this.isDead) {
48
+ handler({});
49
+ return;
50
+ }
51
+ this.closeHandlers.push(handler);
52
+ }
53
+ request(method, params, signal) {
54
+ if (this.isDead)
55
+ return Promise.reject(new Error("upstream is not available"));
56
+ const id = `${BIR_ID_PREFIX}${this.nextId++}`;
57
+ return new Promise((resolve, reject) => {
58
+ const onAbort = () => {
59
+ this.pending.delete(id);
60
+ reject(new Error(`${method} aborted`));
61
+ };
62
+ signal?.addEventListener("abort", onAbort, { once: true });
63
+ this.pending.set(id, {
64
+ resolve,
65
+ reject,
66
+ cleanup: () => signal?.removeEventListener("abort", onAbort),
67
+ });
68
+ this.send({ jsonrpc: "2.0", id, method, params });
69
+ });
70
+ }
71
+ notify(method, params) {
72
+ if (this.isDead)
73
+ return;
74
+ this.send({ jsonrpc: "2.0", method, params });
75
+ }
76
+ /**
77
+ * Route one upstream message. Responses to proxy-originated (`bir:`) ids are
78
+ * consumed here and never reach the relay; everything else fans out verbatim.
79
+ */
80
+ accept(msg) {
81
+ const id = msg.id;
82
+ if (isBirId(id)) {
83
+ const waiter = this.pending.get(id);
84
+ if (waiter) {
85
+ this.pending.delete(id);
86
+ waiter.cleanup();
87
+ const res = msg;
88
+ if (res.error)
89
+ waiter.reject(new Error(res.error.message ?? "upstream error"));
90
+ else
91
+ waiter.resolve(res.result);
92
+ }
93
+ return;
94
+ }
95
+ for (const handler of this.messageHandlers)
96
+ handler(msg);
97
+ }
98
+ /** Mark the connection over. Idempotent; fails every in-flight `request()`. */
99
+ closed(info) {
100
+ if (this.isDead)
101
+ return;
102
+ this.isDead = true;
103
+ const err = info.error ?? new Error("upstream closed");
104
+ for (const [, waiter] of this.pending) {
105
+ waiter.cleanup();
106
+ waiter.reject(err);
107
+ }
108
+ this.pending.clear();
109
+ for (const handler of this.closeHandlers)
110
+ handler(info);
111
+ this.closeHandlers.length = 0;
112
+ }
113
+ }
114
+ //# sourceMappingURL=client.js.map