@basein/runner 0.3.9 → 0.3.10
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/README.md +46 -5
- package/dist/auth/client.d.ts +15 -0
- package/dist/auth/client.js +34 -12
- package/dist/bin/bir-hooks.d.ts +1 -1
- package/dist/bin/bir-hooks.js +1 -1
- package/dist/bin/bir.d.ts +2 -0
- package/dist/bin/bir.js +60 -6
- package/dist/bin/investigate.d.ts +5 -0
- package/dist/bin/investigate.js +117 -15
- package/dist/bin/setup.d.ts +12 -0
- package/dist/bin/setup.js +185 -56
- package/dist/bin/update.d.ts +81 -0
- package/dist/bin/update.js +194 -0
- package/dist/config/adapters/claude-code.d.ts +17 -0
- package/dist/config/adapters/claude-code.js +28 -1
- package/dist/config/generate.d.ts +14 -0
- package/dist/control/daemon.d.ts +26 -1
- package/dist/control/daemon.js +53 -3
- package/dist/control/pin-guard.d.ts +6 -1
- package/dist/control/pin-guard.js +15 -1
- package/dist/control/recorder-keeper.js +2 -1
- package/dist/control/server.d.ts +200 -2
- package/dist/control/server.js +741 -48
- package/dist/control/transcript.d.ts +4 -0
- package/dist/control/transcript.js +4 -0
- package/dist/proxy/session.js +1 -0
- package/dist/record/housekeeping.d.ts +7 -1
- package/dist/record/housekeeping.js +11 -3
- package/dist/record/recorder.d.ts +34 -0
- package/dist/record/remote-recorder.d.ts +6 -0
- package/dist/record/remote-recorder.js +31 -4
- package/dist/record/switchable-recorder.js +1 -0
- package/dist/replay/controller.d.ts +132 -3
- package/dist/replay/controller.js +248 -24
- package/dist/replay/derive.js +2 -0
- package/dist/replay/handover.d.ts +13 -1
- package/dist/replay/handover.js +19 -0
- package/dist/replay/safety.d.ts +15 -4
- package/dist/replay/safety.js +19 -6
- package/dist/replay/source-run.js +2 -1
- package/dist/util/service-headers.d.ts +24 -0
- package/dist/util/service-headers.js +47 -0
- package/docs/BaseInstRunner.md +115 -1
- package/docs/calculatedReplay.md +209 -32
- package/docs/calculatedReplayGuide.md +133 -12
- package/docs/installRun.md +91 -10
- package/docs/quickstart.md +64 -9
- package/package.json +1 -1
- package/production.md +81 -17
- 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
|
package/docs/BaseInstRunner.md
CHANGED
|
@@ -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
|