@basein/runner 0.3.9 → 0.3.11

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 (50) hide show
  1. package/README.md +47 -6
  2. package/dist/auth/client.d.ts +15 -0
  3. package/dist/auth/client.js +34 -12
  4. package/dist/bin/bir-hooks.d.ts +1 -1
  5. package/dist/bin/bir-hooks.js +1 -1
  6. package/dist/bin/bir.d.ts +2 -0
  7. package/dist/bin/bir.js +60 -6
  8. package/dist/bin/investigate.d.ts +5 -0
  9. package/dist/bin/investigate.js +117 -15
  10. package/dist/bin/setup.d.ts +12 -0
  11. package/dist/bin/setup.js +185 -56
  12. package/dist/bin/update.d.ts +81 -0
  13. package/dist/bin/update.js +194 -0
  14. package/dist/config/adapters/claude-code.d.ts +18 -1
  15. package/dist/config/adapters/claude-code.js +30 -3
  16. package/dist/config/generate.d.ts +14 -0
  17. package/dist/control/daemon.d.ts +26 -1
  18. package/dist/control/daemon.js +53 -3
  19. package/dist/control/pin-guard.d.ts +6 -1
  20. package/dist/control/pin-guard.js +15 -1
  21. package/dist/control/recorder-keeper.js +2 -1
  22. package/dist/control/server.d.ts +205 -2
  23. package/dist/control/server.js +766 -56
  24. package/dist/control/transcript.d.ts +4 -0
  25. package/dist/control/transcript.js +4 -0
  26. package/dist/proxy/session.js +1 -0
  27. package/dist/record/housekeeping.d.ts +7 -1
  28. package/dist/record/housekeeping.js +11 -3
  29. package/dist/record/recorder.d.ts +34 -0
  30. package/dist/record/remote-recorder.d.ts +6 -0
  31. package/dist/record/remote-recorder.js +31 -4
  32. package/dist/record/switchable-recorder.js +1 -0
  33. package/dist/replay/controller.d.ts +139 -3
  34. package/dist/replay/controller.js +248 -24
  35. package/dist/replay/derive.js +2 -0
  36. package/dist/replay/handover.d.ts +13 -1
  37. package/dist/replay/handover.js +19 -0
  38. package/dist/replay/safety.d.ts +15 -4
  39. package/dist/replay/safety.js +19 -6
  40. package/dist/replay/source-run.js +2 -1
  41. package/dist/util/service-headers.d.ts +24 -0
  42. package/dist/util/service-headers.js +47 -0
  43. package/docs/BaseInstRunner.md +115 -1
  44. package/docs/calculatedReplay.md +209 -32
  45. package/docs/calculatedReplayGuide.md +133 -12
  46. package/docs/installRun.md +91 -10
  47. package/docs/quickstart.md +64 -9
  48. package/package.json +1 -1
  49. package/production.md +83 -19
  50. package/scripts/install.ps1 +25 -1
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The contract this runner follows. It goes up by one, together with the
3
+ * service's own number, when the service starts to rely on something a runner
4
+ * must do — never for a field a runner may ignore.
5
+ *
6
+ * - **1** — stops in front of a step that carries `stop`, and reads
7
+ * `withheld: "runner_update_required"` on a match.
8
+ */
9
+ export declare const CONTRACT = 1;
10
+ /** Spread into the `headers` of a request to the service. */
11
+ export declare function serviceHeaders(): Record<string, string>;
12
+ /**
13
+ * `bir doctor`'s line for a service that hands plans only to a newer runner
14
+ * than this one: its `/health` names the lowest contract it serves
15
+ * (`minRunner`). Undefined when this runner is new enough, and when the
16
+ * service is an older one that names no minimum.
17
+ */
18
+ export declare function runnerOutdated(service: {
19
+ minRunner?: number;
20
+ } | undefined, ours?: {
21
+ contract: number;
22
+ version: string;
23
+ }): string | undefined;
24
+ //# sourceMappingURL=service-headers.d.ts.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The two headers every call to the BaseIn service carries, so the service
3
+ * knows which runner it is talking to.
4
+ *
5
+ * WHY. The service hands every runner the same plan, and a safety mark added to
6
+ * a plan later works only in a runner that knows it. With these the service can
7
+ * count the versions in use, and hand no plan to a runner too old for it — the
8
+ * agent then does the task, and `bir doctor` says to update.
9
+ *
10
+ * `x-bir-version` is this package's version. `x-bir-contract` is
11
+ * {@link CONTRACT}: the newest set of rules this runner follows.
12
+ *
13
+ * On every call to the service, signed in or not, and never on a call to
14
+ * anyone else: not to Anthropic (replay/derive.ts), not to an upstream MCP
15
+ * server, not to the local control server.
16
+ */
17
+ import { packageVersion } from "./version.js";
18
+ /**
19
+ * The contract this runner follows. It goes up by one, together with the
20
+ * service's own number, when the service starts to rely on something a runner
21
+ * must do — never for a field a runner may ignore.
22
+ *
23
+ * - **1** — stops in front of a step that carries `stop`, and reads
24
+ * `withheld: "runner_update_required"` on a match.
25
+ */
26
+ export const CONTRACT = 1;
27
+ /** Spread into the `headers` of a request to the service. */
28
+ export function serviceHeaders() {
29
+ return {
30
+ "x-bir-version": packageVersion(),
31
+ "x-bir-contract": String(CONTRACT),
32
+ };
33
+ }
34
+ /**
35
+ * `bir doctor`'s line for a service that hands plans only to a newer runner
36
+ * than this one: its `/health` names the lowest contract it serves
37
+ * (`minRunner`). Undefined when this runner is new enough, and when the
38
+ * service is an older one that names no minimum.
39
+ */
40
+ export function runnerOutdated(service, ours = { contract: CONTRACT, version: packageVersion() }) {
41
+ if (service?.minRunner === undefined || service.minRunner <= ours.contract)
42
+ return undefined;
43
+ return (`the BaseIn service hands plans only to a newer runner than this one (${ours.version}): ` +
44
+ "it still records, but nothing is replayed until it is updated — run `bir update`, " +
45
+ "then `bir setup` in each project that uses it");
46
+ }
47
+ //# sourceMappingURL=service-headers.js.map
@@ -411,7 +411,7 @@ the discovery file required on every route):
411
411
  | `POST /session/end` | hook | `finishRun` + flush |
412
412
  | `POST /proxy/register` | proxy | `{ serverName, pid, cwd }` → `{ runId, sessionId }` |
413
413
  | `POST /proxy/step` | proxy | A `ProxyStepReport`; allocates `stepIndex` |
414
- | `GET /health` | CLI | `bir doctor` |
414
+ | `GET /health` | CLI, adapter | `bir doctor`; the adapter of another host reads `hostFields` (below) |
415
415
 
416
416
  **Ordering.** `ordering.ts` hands out `stepIndex` from one counter shared by hook
417
417
  and proxy reports. Allocation happens at *report* time, not call time, so
@@ -424,6 +424,99 @@ what actually happened.
424
424
  `PostToolUse` shape, and `SubagentStart` / `SubagentStop` (which carry `agent_id`)
425
425
  to attribute subagent steps correctly.
426
426
 
427
+ **A host that is not Claude Code.** The same routes serve an agent that runs
428
+ somewhere else. The one host `bir setup --host` knows is a LangChain
429
+ application: a Python program whose agent is built with LangChain's
430
+ `create_agent` and the package `langchain-basic2202`. The package (the adapter)
431
+ is a LangChain middleware. It runs inside the application, sees every tool call
432
+ itself, and finds the recorder by the folder's discovery file. The folder is set
433
+ up with `bir setup --host langchain`, which gives it a port and a token of its
434
+ own and starts the recorder. It writes no hooks and wraps no server
435
+ ([production.md](../production.md) §3.2). The how-to is `langChainGuide.md` in
436
+ the repository's root; it is not in the npm package, and the Python package is
437
+ not on PyPI yet.
438
+
439
+ Claude Code's hooks are unchanged, and every new field is optional. A session
440
+ whose requests carry `host` with an `app` other than `claude-code` is a session
441
+ of another host. What such a host adds:
442
+
443
+ | Route | Added | Meaning |
444
+ |---|---|---|
445
+ | every request | `host: { app, version?, adapter?, scope? }` | The host the session runs in. The session keeps the first one it names |
446
+ | `POST /session/prompt` | `replay: true` | The adapter can step through a plan, and asks for one |
447
+ | `POST /session/prompt` | `sinks: { command?: string[], write?: string[] }` | The host's tools whose input is run, or written into a file |
448
+ | `POST /session/prompt` | answer: `plan: { scenarioId, mode, steps: [{ stepIndex, toolName }], stopAt? }` | The plan as data, beside the directive |
449
+ | `POST /tool/pre` | `context: string`, `thinking: string` | What the model wrote, and what it reasoned, before the call |
450
+ | `POST /tool/pre` | `scripted: true` | The adapter asks for the next step's arguments |
451
+ | `POST /tool/pre` | `tools: [{ name }]` | The tools the agent holds, with the first such request of a run |
452
+ | `POST /tool/pre` | answer: `noPlan: true`, or `declined: "host_tool_missing"` | Why a request for a step got none |
453
+ | `POST /tool/post` | `scripted: true` | The result of a call the adapter made for a step of the plan |
454
+ | `POST /tool/post` | `host_stop: "rejected" \| "edited"` | The host stopped the step itself |
455
+ | `POST /session/stop` | `final_answer: string` | The run's final answer |
456
+ | `POST /session/stop` | `usage: [{ model, input_tokens, output_tokens, cache_read_input_tokens?, cache_creation_input_tokens? }]` | The tokens the run's model calls used, per model |
457
+ | `POST /session/stop` | `failed: string`, `incomplete: string` | The run failed, or its recording is not the whole run, and why |
458
+ | `GET /health` | answer: `hostFields: 2` | How much of this protocol the recorder understands. An adapter reads it before it relies on a field: with 1 it records, from 2 it may also ask for a plan and step through it |
459
+
460
+ A prompt's `recent_results` (the conversation's earlier tool results, which an
461
+ adapter used to send for the reading of new values) is no longer read. A new
462
+ prompt's values are read from the prompt alone: an earlier turn's results are
463
+ not this task's (the service's segmented.md, R-PARAM-2a).
464
+
465
+ What the control server does for such a session:
466
+
467
+ - **The run.** It is created with `framework` set to the app, or `app:scope`
468
+ (`langchain`, `langchain:billing`), and with the host in `metadata.host`. A
469
+ Claude Code run is created as before (`framework: "baseinstrunner"`). Only
470
+ `/session/prompt` opens a run. Any other request that finds none open records
471
+ nothing and is answered `{}`, or `{ noPlan: true }` when it asked for a step.
472
+ - **The scope.** It keeps plans apart inside one account, and is part of the
473
+ run's `framework`. It is 1 to 64 characters and is never cut or dropped. A
474
+ session that names one the recorder cannot keep (empty, longer, or not a
475
+ text) has every request answered `{}`, and nothing of it is recorded or
476
+ replayed (`session.host_refused`).
477
+ - **The plan.** One starts only for a prompt that said `replay: true`.
478
+ Otherwise a match is declined with `host_records_only`. The plan's mode is
479
+ always `steer`, and the runner runs no step itself: the host runs its own
480
+ tools. The prompt's answer carries the plan as data (`plan`), because the
481
+ directive beside it is a text written for a model.
482
+ - **Tool names.** They carry no Claude Code meaning. `Bash`, `Read`,
483
+ `TodoWrite` or `ToolSearch` are the application's own tools there, and
484
+ nothing is skipped as housekeeping except the `bir` server's own tools.
485
+ - **A step.** The adapter asks for each one (`scripted: true`). The answer is
486
+ the step's arguments (`hookSpecificOutput.updatedInput`), or
487
+ `{ noPlan: true }` when the recorder holds no live plan, or
488
+ `{ declined: "host_tool_missing" }` when the plan names a tool that is not in
489
+ `tools`.
490
+ - **The model's own call.** A `/tool/pre` without `scripted` is the model's own
491
+ call, and is never given the plan's arguments. A plan that is live then is
492
+ handed over, and the hand-over note comes with that call's `/tool/post`.
493
+ - **A step the host stopped.** `host_stop: "rejected"` means a person said no,
494
+ or answered in the tool's place, or a call limit blocked the call. `"edited"`
495
+ means a person changed the call. Either way the step's verdict is `skipped`,
496
+ not `failed`, and nothing is counted against the plan. After `rejected` the
497
+ report is `measured: false`; after `edited` the cost stays measured. Both are
498
+ `baselineEligible: false`.
499
+ - **The cost.** It comes from `usage`, not from a transcript. It is measured
500
+ only when at least one entry has tokens and every model in the list has a
501
+ price. With more than one plan in one run, none is measured.
502
+ - **The end.** `failed` closes the run with that text as its `error` (cleaned
503
+ of secrets, cut) and no final answer. `incomplete` marks the finish
504
+ `incomplete: { lostSteps, lossy }`; the finish carries that mark too when the
505
+ recorder itself lost one of the run's step sends. A run sealed after two
506
+ hours of silence is closed with `error: "turn ended early: idle"`. A run that
507
+ is closed while its plan is still live has the plan handed over first, so the
508
+ report says `fell_back` (a step ran) or `failed` (none ran), with
509
+ `measured: false` and `baselineEligible: false`.
510
+ - **Sinks.** A value that a plan reads from the prompt is checked before it goes
511
+ into a tool the host declared in `sinks`, or one whose plain name has a
512
+ command word (`sql`, `query`, `exec`, `shell`, `command`, `script` and `eval`
513
+ among them).
514
+
515
+ On the service's side, matching stays inside a host: a run created with
516
+ `framework` `langchain` or `langchain:<scope>` is matched only against
517
+ recordings and plans of that same text. Everything else is Claude Code's pool,
518
+ as before. That part of the service is built and not deployed yet.
519
+
427
520
  **Acceptance:** a Claude Code session running one built-in and one MCP tool
428
521
  produces a single run whose steps are in true execution order.
429
522
 
@@ -574,6 +667,11 @@ Run `metadata` carries what is new, and what BaseIn-side consumers will need:
574
667
  `tier` is the field that stops a Tier 2 partial trace being mistaken for a
575
668
  complete one.
576
669
 
670
+ A run of a host that is not Claude Code (Phase 5) names that host in
671
+ `metadata.host` (`{ "app": "langchain", … }`), and is created with a `framework`
672
+ of its own: the app, or `app:scope`. Its finish may carry an `error`, and the
673
+ `incomplete` mark.
674
+
577
675
  ---
578
676
 
579
677
  ## 8. Full-passthrough method map
@@ -618,6 +716,11 @@ observable.
618
716
  - Discovery files are mode `0600`.
619
717
  - `bir doctor` prints exactly which servers are wrapped, so a proxy can never sit
620
718
  in the path invisibly.
719
+ - A recorder started for a folder that is set up for another host and not for
720
+ Claude Code (Phase 5) gets a small environment, not the whole environment of
721
+ the window that started it. A plan's step code runs inside the recorder with
722
+ no sandbox, so a model key or a database password of the application must not
723
+ reach it. The names it keeps are in [production.md](../production.md) §9.
621
724
 
622
725
  ---
623
726
 
@@ -630,6 +733,7 @@ The governing rule: **the host session must never fail because of BaseInstRunner
630
733
  | Control server unreachable at startup | Buffer <= 10 s while retrying, then Tier 2 (passthrough for an entry that may not record here, §0.1). Keep looking every 15 s; bind when a recorder answers |
631
734
  | Control server dies mid-session | Log `tier.downgrade`; what follows goes to a Tier 2 run of the proxy's own (at most one per proxy; none where the entry may not record), flagged `lossy`, until a recorder answers again. A bound proxy in an installed project whose recorder died (pid gone, port refusing) starts it again in the background (`bir-hooks revive`); a recorder stopped with `bir down` stays stopped |
632
735
  | Recorder HTTP fails | Drop the send, log once per run, continue. Never retry inline: a background send is retried once, and only after a 429/503 or a connection that never reached the service. After a failure, hooks stop waiting on the service for a cool-down (5 s, doubling to 60 s) |
736
+ | The service answers `429`: the account is over one of its limits (these are built in the service, and not deployed yet) | Send the create, the step or the report once more, at most 2 s later, then drop it (`recorder.send_failed` with `HTTP 429`). A dropped step is a hole in its recording, and a run whose create got a `429` runs without its plan. A `429` that the second try did not cure starts the same cool-down. The limits are in [production.md](../production.md) §3.3 |
633
737
  | Upstream fails to start, or exits | Answer the requests in flight, then exit with the upstream's status (1 if it had none). The host shows that one server failed, with a reconnect offered, as it would unwrapped. `BIR_PROXY_STAY_UP=1` keeps the proxy up instead, answering every request `MCP server "x" is not available` |
634
738
  | A stdio upstream dies mid-call | Return `isError: true` with `MCP server "x" is not available` as content — a tool failure the model can react to, not a protocol error — then exit as above |
635
739
  | Remote upstream unreachable or refusing | The proxy stays up and answers each request with a short JSON-RPC error that never echoes the server's body (`upstream unreachable: …`, `upstream requires sign-in (HTTP 401)`). Only connecting is bounded (10 s, `BIR_UPSTREAM_CONNECT_TIMEOUT_MS`); a slow tool is the host's to time out |
@@ -659,6 +763,9 @@ The governing rule: **the host session must never fail because of BaseInstRunner
659
763
  6. **Redaction** — assert no fixture secret reaches the recorder.
660
764
  7. **Integration** — a real Claude Code session against `fake-upstream`, asserting
661
765
  run shape end to end. The only test that catches host-behaviour drift.
766
+ 8. **Another host** — `examples/langchain-proof/`, in the repository and not in
767
+ the npm package: a real LangChain agent against the real recorder, with a
768
+ stand-in service and a fake model (Phase 5). Its README lists the cases.
662
769
 
663
770
  ---
664
771
 
@@ -683,6 +790,13 @@ ship it before touching the control plane.
683
790
  `metadata` enough for the savings pipeline to exclude partial traces from
684
791
  baselines? A Tier 2 run has no prompt, so similarity matching on `input` behaves
685
792
  differently.
793
+ *Answered in part.* The service's new build (built, not deployed yet) lets a
794
+ finish say that the recording is not whole: `incomplete: { lostSteps, lossy }`.
795
+ A run with an `error`, or with that mark, is never picked for automatic
796
+ calculation and no longer catches its prompt, unless its owner preferred it
797
+ or it already has a plan. Today only a run of a host that is not Claude Code
798
+ (Phase 5) sends either: a Claude Code run that was interrupted or lost steps
799
+ is finished as before. The runner still sends `tier` in `metadata` only.
686
800
  2. **Coexistence with RRepeat.** If both are installed, both record the same session
687
801
  into the same service. Should `bir-hooks` detect `rrepeat-hooks` and refuse, or
688
802
  should runs carry a `recorder` discriminator and be deduped server-side? §7's