@basein/runner 0.3.23 → 0.3.25

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.
@@ -21,7 +21,7 @@
21
21
  * output, and skipping.
22
22
  */
23
23
  import { evalParamsLogic, evalResponseParamsLogic, evalToolInputLogic, evalToolOutputLogic, evalWhenLogic, LogicTimeoutError, } from "./logic.js";
24
- import { assembleBundle, bundleInput, MAX_REPLAY_REASON, testWords } from "./bundle.js";
24
+ import { assembleBundle, bundleInput, MAX_REPLAY_REASON, testWords, wentWell, } from "./bundle.js";
25
25
  import { redact, redactText } from "../record/redact.js";
26
26
  import { sinkOfPlan, unsafeParameter } from "./safety.js";
27
27
  import { testedOnlyTargets } from "./targets.js";
@@ -883,6 +883,29 @@ export class ScenarioReplayPlan {
883
883
  // The intent is a paragraph the analyser wrote; it is context, not an
884
884
  // instruction, so it is clipped rather than dumped whole into the prompt.
885
885
  const intent = this.intent.length > 240 ? `${this.intent.slice(0, 240)}…` : this.intent;
886
+ if (this.mode === "direct") {
887
+ // The steps are not listed. The model does not call them, and what the
888
+ // tool returns names every call and says whether the run was whole or
889
+ // stopped short — what a list here once told it ahead of time. The list
890
+ // cost the agent up to 2,000 characters in the first call of every turn
891
+ // a plan was armed for (docs/calculatedReplay.md, "How a bundle reads"),
892
+ // and the tool's result shows an operator watching the session what ran.
893
+ return [
894
+ "[BaseInstRunner calculated replay] A known-good tool sequence was recorded",
895
+ `for this request${intent ? ` (intent: ${intent})` : ""}.`,
896
+ "",
897
+ `Call ${directToolName} once, with no arguments, before any other tool. It runs the`,
898
+ "sequence and returns what each call did.",
899
+ subTask
900
+ ? "Then continue your task from the results it returns, calling tools as needed."
901
+ : this.stopsEarly()
902
+ ? "Then continue the user's request from the results it returns."
903
+ : "Then answer the user's request from the results it returns.",
904
+ // A parked step (fallbk.md D3): the model is told up front that the
905
+ // plan is partial, so the hand-over that follows is expected.
906
+ ...(this.stopsEarly() ? ["", "The system will then hand the rest of the task back to you."] : []),
907
+ ].join("\n");
908
+ }
886
909
  // Each step's reasoning is another session's narration ("I'll now drop the
887
910
  // temp table…"), injected into this one. It is a label here, not a
888
911
  // paragraph: its first line, clipped. A chain long enough that even the
@@ -931,30 +954,6 @@ export class ScenarioReplayPlan {
931
954
  const tail = this.stopsEarly()
932
955
  ? ["", "The system will then hand the rest of the task back to you."]
933
956
  : [];
934
- if (this.mode === "direct") {
935
- // The sequence is listed even though the model does not call it itself:
936
- // it is about to receive these results, and knowing what was run is what
937
- // lets it tell a complete answer from a partial one. It is also the only
938
- // place an operator watching the session can see what a replay is doing.
939
- return [
940
- "[BaseInstRunner calculated replay] A known-good tool sequence was recorded",
941
- `for this request${intent ? ` (intent: ${intent})` : ""}.`,
942
- "",
943
- anyTest
944
- ? 'It runs these tools, in order, and returns their results. A step marked "only when" runs only when this request needs it, and the results say which ones did not:'
945
- : "It runs these tools, in order, and returns their results:",
946
- "",
947
- ...lines,
948
- "",
949
- `Call ${directToolName} once, with no arguments, before any other tool.`,
950
- subTask
951
- ? "Then continue your task from the results it returns, calling tools as needed."
952
- : this.stopsEarly()
953
- ? "Then continue the user's request from the results it returns."
954
- : "Then answer the user's request from the results it returns.",
955
- ...tail,
956
- ].join("\n");
957
- }
958
957
  return [
959
958
  "[BaseInstRunner calculated replay] A known-good tool sequence was previously",
960
959
  "recorded for this request. Call exactly these tools, in order, one at a time:",
@@ -1343,12 +1342,29 @@ export class ScenarioReplayPlan {
1343
1342
  position: this.stopAt,
1344
1343
  };
1345
1344
  }
1345
+ const layout = {
1346
+ handover: stopped !== undefined,
1347
+ subTask: this.subTask,
1348
+ reportTool: opts.reportTool,
1349
+ };
1350
+ // Brief only for a run that went entirely well (bundle.ts, `wentWell`). A
1351
+ // tool error that is not an MCP `isError` — a JSON-RPC error, which the
1352
+ // bundle cannot tell from a result — is counted in `errored`, and is just
1353
+ // as much a reason for the agent to see everything.
1354
+ const brief = opts.brief === true && errored === 0 && wentWell(entries, layout);
1355
+ let summaryError;
1356
+ if (brief) {
1357
+ let summary;
1358
+ try {
1359
+ summary = this.responseModel();
1360
+ }
1361
+ catch (err) {
1362
+ summaryError = errText(err);
1363
+ }
1364
+ layout.brief = { summary };
1365
+ }
1346
1366
  return {
1347
- text: assembleBundle(entries, maxChars, {
1348
- handover: stopped !== undefined,
1349
- subTask: this.subTask,
1350
- reportTool: opts.reportTool,
1351
- }),
1367
+ text: assembleBundle(entries, maxChars, layout),
1352
1368
  executed,
1353
1369
  recorded: recordedCount,
1354
1370
  skipped,
@@ -1359,6 +1375,8 @@ export class ScenarioReplayPlan {
1359
1375
  stopped,
1360
1376
  aborted: aborted || undefined,
1361
1377
  endedAt: this.stepIndex,
1378
+ brief: brief || undefined,
1379
+ summaryError,
1362
1380
  };
1363
1381
  }
1364
1382
  /**
@@ -771,6 +771,38 @@ Delivery, best channel first. (A direct plan's own results go through `run_scena
771
771
  (`assembleBundle`/`truncate`, portable verbatim from `payload-executor.ts`). Works for any tool,
772
772
  and is read as adversarial — hence last.
773
773
 
774
+ **How a bundle reads** ([`bundle.ts`](../src/replay/bundle.ts); the same in a direct run's answer,
775
+ §6). The agent pays once to take the bundle in, as new input, and on a long plan that was most of a
776
+ replayed task's bill — so nothing in it repeats what the model already has. Each result is shown as
777
+ its text, not as MCP's `CallToolResult` JSON (the envelope and its second layer of escaping told the
778
+ model nothing), with `(error)` when `isError` is set; a result with anything but text — an image, a
779
+ resource, structured content — is shown as it came. Consecutive calls to one tool share a section,
780
+ `--- Steps a-b: tool (n calls) ---`, with one numbered line per call, `n: input -> result`. Input
781
+ fields with the same value in every call are said once above them (`In every call: {…}`), and so is
782
+ an answer every call got (`Every call's result: …`). A step on its own, and a step left out, keep
783
+ `--- Step n: tool ---`. Nothing is dropped, and each result keeps its fair share of the cap. On
784
+ B2's length tests this made bundles 27% shorter, 33% at 50–60 steps.
785
+
786
+ **A run that went well reads briefly** (`AssembleOptions.brief`, asked for by a direct run, §6). When
787
+ every step ran live and succeeded and the agent is to answer now, its job is to see that the steps
788
+ were right and say what was done — not to read every word of every result. So the header says every
789
+ call succeeded; a result up to 300 characters is shown whole; a longer one that is JSON keeps every
790
+ record and field, and only its text longer than 40 characters is cut, with "…" (`cutLongText`: the
791
+ IDs, dates, senders and names the agent checks the steps against are short; an e-mail's body is
792
+ not). A long result in words is shown whole — it may be the very answer. Showing only the values the
793
+ plan took from a long result was tried first: it left the agent unable to check the plan's choices
794
+ ("I couldn't confirm these were the newest") in one reply in six on B2's length tests, and once it
795
+ reported a correct run as wrong. The plan's summary (its response model) is said in a few words
796
+ (short lists whole, long ones counted), in the bundle, above the footer — so `bir-scenario` gets
797
+ `responseModel: {}` and does not append it again — and is left out when it only says it was left
798
+ out. The footer asks for
799
+ the answer in a few sentences: the reply is output, the dearest tokens an agent spends. Anything
800
+ that went wrong — a step that reported an error, a run that stopped short, a recorded stand-in, a
801
+ sub-task the agent carries on with — gets the full bundle above, because that is what the agent
802
+ works from next. For the same reason a direct run's directive (`steeringDirective`) no longer lists
803
+ the planned steps: the model calls none of them, and the answer names every call and says whether
804
+ the run was whole. An operator's `bir replay` and a divergence bundle are always full.
805
+
774
806
  **Tier 2 — abort.** Composition itself threw (`replay.compose_failed`). Retire the plan, return
775
807
  `{}`, continue as an ordinary turn. Outcome `failed`.
776
808
 
@@ -373,7 +373,7 @@ so there is exactly one switch and it cannot get out of step with itself.
373
373
  | `BIR_BASH_ALLOW` | *(unset: any command)* | Command prefixes a runner-side `Bash` step may run (`*` for all). Unset means any command; `none` means no command, and `Bash` steps are steered in the session. A plan's Bash steps share a working directory, exactly as the host's own Bash tool does — variables and functions do not persist, also as the host's. A call with `run_in_background`, or with its own `timeout` longer than the step's clock (`BIR_STEP_TIMEOUT_MS`), is never started on the runner: the plan hands over in front of it |
374
374
  | `BIR_PATH_ALLOW` | *(unset: any folder, from 0.3.21)* | Folders a runner-side `Read`, `Glob`, `Grep`, `Edit` or `Write` may touch (`*` for all). Unset means any path, as the agent's own tools reach it — a plan that reads the agent's memory file under `~/.claude` runs. `project` is the session's project folder alone (the rule up to 0.3.20); add folders after a comma, `~` for the home folder: `project,~/.claude`. Under a list, a path outside it, symlinks included, is refused and the plan hands over in front of it. `none` means no folder, and the file steps are steered in the session. `Bash` is `BIR_BASH_ALLOW`'s |
375
375
  | `BIR_INTENT_ARM` | *(unset: off)* | `1` lets a whole plan whose intent matches the agent's reasoning mid-turn arm in the calling tool's place, which means waiting for it (`BIR_INTENT_ARM_BUDGET_MS`, 500). Unset or `0`: off — no probe is sent and nothing arms mid-turn, as up to 0.3.18 |
376
- | `BIR_CONTINUATIONS` | *(unset: on, from 0.3.23)* | After a plan hands the task to the agent at a step, the agent's first call asks the service for the plan of the work recorded after an earlier hand-over at that step — once per hand-over, waiting 1.5 s at most for the answer and up to 7 s for its values — and runs it in that call's place only when the call is its first step (the same tool, on the same thing). Its saving is counted on the plan that handed over. `0`: off, and the agent's work is recorded as before. `BIR_INTENT_MATCH=0` turns it off too |
376
+ | `BIR_CONTINUATIONS` | *(unset: on, from 0.3.23)* | After a plan hands the task to the agent at a step, the agent's first call asks the service for the plan of the work recorded after an earlier hand-over at that step — once per hand-over, waiting 1.5 s at most for the answer and up to 7 s for its values — and that plan does the rest of the task, whatever the agent's call was (from 0.3.24; 0.3.23 ran it only when the call was its first step). A call to one of the plan's own tools is answered in its place, so the plan never makes it twice; any other call runs as the agent wrote it, and the agent is told of the plan on its result. Its saving is counted on the plan that handed over. `0`: off, and the agent's work is recorded as before. `BIR_INTENT_MATCH=0` turns it off too |
377
377
  | `BIR_VERBOSE` | *(unset)* | `1` adds per-step `replay.*` detail lines |
378
378
 
379
379
  The `bir` server reads its own, from the environment Claude Code starts it with:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basein/runner",
3
- "version": "0.3.23",
3
+ "version": "0.3.25",
4
4
  "description": "A recording MCP proxy: sits between any MCP client and its MCP servers, executes each call on the client's behalf, and records the run as a reusable BaseIn scenario.",
5
5
  "type": "module",
6
6
  "license": "MIT",