@basein/runner 0.2.7 → 0.2.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 +64 -21
- package/dist/auth/client.d.ts +40 -1
- package/dist/auth/client.js +77 -9
- package/dist/bin/bir-hooks.d.ts +18 -3
- package/dist/bin/bir-hooks.js +124 -38
- package/dist/bin/bir.d.ts +2 -0
- package/dist/bin/bir.js +362 -39
- package/dist/bin/investigate.js +5 -1
- package/dist/bin/setup.d.ts +72 -0
- package/dist/bin/setup.js +286 -0
- package/dist/config/adapters/claude-code.d.ts +90 -4
- package/dist/config/adapters/claude-code.js +164 -16
- package/dist/config/generate.d.ts +93 -1
- package/dist/config/generate.js +90 -3
- package/dist/control/client.d.ts +5 -0
- package/dist/control/client.js +8 -0
- package/dist/control/daemon.d.ts +116 -0
- package/dist/control/daemon.js +339 -0
- package/dist/control/discovery.d.ts +26 -0
- package/dist/control/discovery.js +41 -9
- package/dist/control/ensure-hook.d.ts +39 -0
- package/dist/control/ensure-hook.js +98 -0
- package/dist/control/paths.d.ts +14 -0
- package/dist/control/paths.js +20 -0
- package/dist/control/server.d.ts +28 -0
- package/dist/control/server.js +22 -6
- package/dist/proxy/session.d.ts +8 -1
- package/dist/proxy/session.js +28 -6
- package/dist/replay/controller.d.ts +24 -1
- package/dist/replay/controller.js +76 -20
- package/dist/replay/handover.js +5 -0
- package/dist/replay/plan.d.ts +2 -0
- package/dist/replay/plan.js +53 -6
- package/dist/replay/pricing.d.ts +1 -1
- package/dist/replay/pricing.js +12 -4
- package/dist/replay/tool-error.d.ts +15 -0
- package/dist/replay/tool-error.js +17 -0
- package/dist/replay/types.d.ts +48 -1
- package/docs/calculatedReplayGuide.md +157 -68
- package/docs/installRun.md +457 -111
- package/docs/loginWeb.md +1 -1
- package/docs/quickstart.md +193 -158
- package/package.json +2 -1
- package/scripts/install.ps1 +669 -0
- package/scripts/install.sh +586 -0
package/dist/control/server.d.ts
CHANGED
|
@@ -53,6 +53,14 @@ export interface ControlServerOptions {
|
|
|
53
53
|
cwd: string;
|
|
54
54
|
/** Preferred port; 0 (or a busy port) falls back to an ephemeral one. */
|
|
55
55
|
port?: number;
|
|
56
|
+
/**
|
|
57
|
+
* Whether a busy `port` may fall back to an ephemeral one. Default true — a
|
|
58
|
+
* recorder in a terminal is better on some port than not at all. A background
|
|
59
|
+
* recorder whose port is written into a project's hooks passes false: on a
|
|
60
|
+
* fallback port it would answer SessionStart and then every other hook would
|
|
61
|
+
* post into the void, which is worse than exiting with a reason.
|
|
62
|
+
*/
|
|
63
|
+
portFallback?: boolean;
|
|
56
64
|
/** Loopback bearer token. Generated when omitted. */
|
|
57
65
|
token?: string;
|
|
58
66
|
/** Config keys of the servers a proxy wraps. Grows as proxies register. */
|
|
@@ -90,6 +98,26 @@ export interface ControlServerOptions {
|
|
|
90
98
|
* recording and nothing else happens.
|
|
91
99
|
*/
|
|
92
100
|
replay?: ReplayOptions;
|
|
101
|
+
/**
|
|
102
|
+
* Where the replay switches came from — the environment, the project's
|
|
103
|
+
* stored policy, or the defaults. Reported on `/health` so `bir doctor` can
|
|
104
|
+
* say which, because "the allow-list I set is gone" has exactly one cause.
|
|
105
|
+
*/
|
|
106
|
+
replaySource?: "env" | "sidecar" | "default";
|
|
107
|
+
/**
|
|
108
|
+
* Whose account the recordings land in, for `/health` and `bir doctor`. "The
|
|
109
|
+
* run is not in Recordings" is, more often than not, "it is in somebody
|
|
110
|
+
* else's", and nothing said which until now.
|
|
111
|
+
*/
|
|
112
|
+
account?: string;
|
|
113
|
+
/**
|
|
114
|
+
* What 'POST /control/stop' does after answering. 'bir-hooks' passes its own
|
|
115
|
+
* shutdown; a test passes nothing and the route merely says it would. The
|
|
116
|
+
* route exists so a background recorder can be stopped the way Ctrl-C stops
|
|
117
|
+
* one in a terminal — finishing the run and draining the queue — rather than
|
|
118
|
+
* killed, which on Windows is the only other option.
|
|
119
|
+
*/
|
|
120
|
+
onStopRequested?: () => void;
|
|
93
121
|
}
|
|
94
122
|
export interface ControlServerAddress {
|
|
95
123
|
url: string;
|
package/dist/control/server.js
CHANGED
|
@@ -38,6 +38,7 @@ import { calculateCostUsd } from "../replay/pricing.js";
|
|
|
38
38
|
import { logDetail, logLine, errText } from "../util/log.js";
|
|
39
39
|
import { journal } from "../util/journal.js";
|
|
40
40
|
import { packageVersion } from "../util/version.js";
|
|
41
|
+
import { resolveAuthUrl } from "../auth/client.js";
|
|
41
42
|
/** How long a `/tool/post` waits for the proxy's own report before recording its own view. */
|
|
42
43
|
const PROXY_REPORT_GRACE_MS = 1_500;
|
|
43
44
|
/** One MCP call, as seen from up to two sides. */
|
|
@@ -127,7 +128,7 @@ export class ControlServer {
|
|
|
127
128
|
resolve(this.address);
|
|
128
129
|
});
|
|
129
130
|
};
|
|
130
|
-
bind(preferred, preferred !== 0);
|
|
131
|
+
bind(preferred, preferred !== 0 && this.opts.portFallback !== false);
|
|
131
132
|
});
|
|
132
133
|
}
|
|
133
134
|
async close() {
|
|
@@ -150,6 +151,10 @@ export class ControlServer {
|
|
|
150
151
|
const sessions = [...this.sessions.values()].map((s) => ({
|
|
151
152
|
sessionId: s.sessionId,
|
|
152
153
|
runId: s.run?.runId,
|
|
154
|
+
// Mid-run right now. What 'ensureDaemon' reads before it dares to restart
|
|
155
|
+
// an out-of-date recorder: a restart between turns costs nothing, one
|
|
156
|
+
// during a turn loses the turn.
|
|
157
|
+
active: Boolean(s.run) && !s.run.finished,
|
|
153
158
|
steps: s.run?.ordering.next ?? 0,
|
|
154
159
|
recording: s.run?.recording ?? false,
|
|
155
160
|
replay: s.run?.replay
|
|
@@ -187,12 +192,13 @@ export class ControlServer {
|
|
|
187
192
|
tier: "bound",
|
|
188
193
|
// The authoritative answer to "is anything actually being saved?".
|
|
189
194
|
recording: this.opts.recording ?? !(this.recorder instanceof NullRecorder),
|
|
195
|
+
account: this.opts.account ?? null,
|
|
190
196
|
// Whether a handed-out segment may actually run mid-task, or whether the
|
|
191
197
|
// runner is only watching (R-OUT-10, R-LIFE-8). Observe-only must never
|
|
192
198
|
// be invisible: an operator has to be able to see which of the two this
|
|
193
199
|
// machine is doing without reading a log file.
|
|
194
200
|
segmentArm: this.replay.segmentArm,
|
|
195
|
-
authUrl:
|
|
201
|
+
authUrl: resolveAuthUrl() || null,
|
|
196
202
|
sessionId: this.sessionId,
|
|
197
203
|
pid: process.pid,
|
|
198
204
|
cwd: this.opts.cwd,
|
|
@@ -210,6 +216,7 @@ export class ControlServer {
|
|
|
210
216
|
// (docs/calculatedReplay.md §13.2, mitigation 3).
|
|
211
217
|
replay: {
|
|
212
218
|
enabled: this.replay.enabled,
|
|
219
|
+
source: this.opts.replaySource ?? null,
|
|
213
220
|
minSimilarity: this.opts.replay?.minSimilarity ?? null,
|
|
214
221
|
allowServers: this.opts.replay?.allowServers ? [...this.opts.replay.allowServers] : null,
|
|
215
222
|
deriveKey: Boolean(this.opts.replay?.apiKey ?? process.env.ANTHROPIC_API_KEY),
|
|
@@ -248,6 +255,12 @@ export class ControlServer {
|
|
|
248
255
|
}
|
|
249
256
|
const body = await this.readJson(req);
|
|
250
257
|
switch (route) {
|
|
258
|
+
case "/control/stop":
|
|
259
|
+
// Answer first, then leave: the caller is polling for the pid to go.
|
|
260
|
+
this.send(res, 200, { ok: Boolean(this.opts.onStopRequested) });
|
|
261
|
+
if (this.opts.onStopRequested)
|
|
262
|
+
setImmediate(() => this.opts.onStopRequested?.());
|
|
263
|
+
return;
|
|
251
264
|
case "/session/start":
|
|
252
265
|
this.send(res, 200, this.onSessionStart(body));
|
|
253
266
|
return;
|
|
@@ -944,11 +957,14 @@ export class ControlServer {
|
|
|
944
957
|
continue;
|
|
945
958
|
let position;
|
|
946
959
|
if (!state.plan) {
|
|
947
|
-
// Only a
|
|
948
|
-
//
|
|
949
|
-
// decline leaves the recording
|
|
950
|
-
|
|
960
|
+
// Only a first step the plan cannot start on — parked, or a judgement
|
|
961
|
+
// — means the turn is committed to doing this recording's work itself
|
|
962
|
+
// from position 0 (R-HIT-14); every other decline leaves the recording
|
|
963
|
+
// untouched.
|
|
964
|
+
if (state.declined === "known_bad_first_step" ||
|
|
965
|
+
state.declined === "nondeterministic_first_step") {
|
|
951
966
|
position = 0;
|
|
967
|
+
}
|
|
952
968
|
}
|
|
953
969
|
else if (state.handover) {
|
|
954
970
|
position = state.handover.stepIndex;
|
package/dist/proxy/session.d.ts
CHANGED
|
@@ -25,7 +25,14 @@ import type { UpstreamClient } from "../upstream/client.js";
|
|
|
25
25
|
import type { ProxyStepReport } from "../control/correlation.js";
|
|
26
26
|
import { type Recorder } from "../record/recorder.js";
|
|
27
27
|
/** How long a proxy waits for a control server before falling to Tier 2. */
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Ten seconds, not five: the recorder is now started by the SessionStart hook,
|
|
30
|
+
* which fires while the host is also spawning this proxy, and a cold start
|
|
31
|
+
* (sign-in refresh, the service's /health probe, then listen) has been
|
|
32
|
+
* measured at one to four seconds. Steps are buffered meanwhile, never
|
|
33
|
+
* dropped, and a host session is never waiting on this.
|
|
34
|
+
*/
|
|
35
|
+
export declare const DISCOVERY_WINDOW_MS = 10000;
|
|
29
36
|
export type Tier = "bound" | "standalone" | "pending";
|
|
30
37
|
export interface ProxySessionOptions {
|
|
31
38
|
serverName: string;
|
package/dist/proxy/session.js
CHANGED
|
@@ -26,7 +26,7 @@ import { hostname } from "node:os";
|
|
|
26
26
|
import { ControlClient } from "../control/client.js";
|
|
27
27
|
import { resolveControl } from "../control/discovery.js";
|
|
28
28
|
import { qualifyToolName } from "../control/correlation.js";
|
|
29
|
-
import { authenticate } from "../auth/client.js";
|
|
29
|
+
import { authenticate, resolveAuthUrl } from "../auth/client.js";
|
|
30
30
|
import { NullRecorder } from "../record/recorder.js";
|
|
31
31
|
import { RemoteRecorder } from "../record/remote-recorder.js";
|
|
32
32
|
import { StepQueue } from "../record/queue.js";
|
|
@@ -36,7 +36,14 @@ import { redact } from "../record/redact.js";
|
|
|
36
36
|
import { logLine, logDetail, errText } from "../util/log.js";
|
|
37
37
|
import { packageVersion } from "../util/version.js";
|
|
38
38
|
/** How long a proxy waits for a control server before falling to Tier 2. */
|
|
39
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Ten seconds, not five: the recorder is now started by the SessionStart hook,
|
|
41
|
+
* which fires while the host is also spawning this proxy, and a cold start
|
|
42
|
+
* (sign-in refresh, the service's /health probe, then listen) has been
|
|
43
|
+
* measured at one to four seconds. Steps are buffered meanwhile, never
|
|
44
|
+
* dropped, and a host session is never waiting on this.
|
|
45
|
+
*/
|
|
46
|
+
export const DISCOVERY_WINDOW_MS = 10_000;
|
|
40
47
|
export class ProxySession {
|
|
41
48
|
serverName;
|
|
42
49
|
/** This proxy process, as the control server tells proxies apart (replay/executor.ts). */
|
|
@@ -98,6 +105,7 @@ export class ProxySession {
|
|
|
98
105
|
return;
|
|
99
106
|
}
|
|
100
107
|
const window = this.opts.discoveryWindowMs ?? DISCOVERY_WINDOW_MS;
|
|
108
|
+
const deadline = Date.now() + window;
|
|
101
109
|
const found = await resolveControl(this.opts.cwd, window);
|
|
102
110
|
if (!found?.url) {
|
|
103
111
|
await this.becomeStandalone(`no control server within ${window}ms`);
|
|
@@ -108,12 +116,24 @@ export class ProxySession {
|
|
|
108
116
|
startedAt: this.startedAt,
|
|
109
117
|
pid: process.pid,
|
|
110
118
|
});
|
|
111
|
-
const
|
|
119
|
+
const info = {
|
|
112
120
|
serverName: this.serverName,
|
|
113
121
|
pid: process.pid,
|
|
114
122
|
cwd: this.opts.cwd,
|
|
115
123
|
version: packageVersion(),
|
|
116
|
-
}
|
|
124
|
+
};
|
|
125
|
+
// A control server that is still coming up — the SessionStart hook is
|
|
126
|
+
// starting it while the host spawns us — answers nothing for a moment.
|
|
127
|
+
// Keep trying inside the same window rather than settle for Tier 2 on the
|
|
128
|
+
// first refusal.
|
|
129
|
+
let registered = await client.register(info);
|
|
130
|
+
while (!registered && Date.now() < deadline) {
|
|
131
|
+
await new Promise((resolve) => {
|
|
132
|
+
const t = setTimeout(resolve, 250);
|
|
133
|
+
t.unref?.();
|
|
134
|
+
});
|
|
135
|
+
registered = await client.register(info);
|
|
136
|
+
}
|
|
117
137
|
if (!registered) {
|
|
118
138
|
await this.becomeStandalone("control server did not answer /proxy/register");
|
|
119
139
|
return;
|
|
@@ -216,9 +236,11 @@ export class ProxySession {
|
|
|
216
236
|
if (this.opts.recorderFactory) {
|
|
217
237
|
return (await this.opts.recorderFactory()) ?? new NullRecorder();
|
|
218
238
|
}
|
|
219
|
-
|
|
239
|
+
// BIR_AUTH_URL, or the address 'bir setup' stored — the same lookup the
|
|
240
|
+
// control server and the CLI make, so no two processes disagree about it.
|
|
241
|
+
const baseUrl = resolveAuthUrl();
|
|
220
242
|
if (!baseUrl) {
|
|
221
|
-
logLine("recorder.disabled", { why: "
|
|
243
|
+
logLine("recorder.disabled", { why: "no service address — run 'bir setup', or set BIR_AUTH_URL" });
|
|
222
244
|
return new NullRecorder();
|
|
223
245
|
}
|
|
224
246
|
// A proxy runs inside the host's process tree with its stdio bound to the
|
|
@@ -219,7 +219,7 @@ export interface ReplayState {
|
|
|
219
219
|
* Why a match did not arm. One code per gate of the ladder, so the caller can
|
|
220
220
|
* act on a decline without parsing the sentence that explains it.
|
|
221
221
|
*/
|
|
222
|
-
export type DeclineCode = "replay_disabled" | "not_ready" | "flatten_failed" | "similarity" | "known_bad_first_step" | "unusable_first_step" | "coverage" | "missing_target" | "no_derive_key";
|
|
222
|
+
export type DeclineCode = "replay_disabled" | "not_ready" | "flatten_failed" | "similarity" | "known_bad_first_step" | "nondeterministic_first_step" | "unusable_first_step" | "coverage" | "missing_target" | "no_derive_key";
|
|
223
223
|
/** What `PreToolUse` should do about this call. */
|
|
224
224
|
export type PreToolAction =
|
|
225
225
|
/** Pin the arguments and let the real tool run. */
|
|
@@ -417,6 +417,13 @@ export declare class ReplayController {
|
|
|
417
417
|
* scenario that did nothing has nothing to book, and is a baseline sample.
|
|
418
418
|
*/
|
|
419
419
|
private handOver;
|
|
420
|
+
/**
|
|
421
|
+
* Which kind of stop a plan's end is (plan-services.md D8). The stopping
|
|
422
|
+
* step's own served reason wins — the calculation's verdict is more exact
|
|
423
|
+
* than a counter — then a call the service could not answer, then a parked
|
|
424
|
+
* step.
|
|
425
|
+
*/
|
|
426
|
+
private stopKindOf;
|
|
420
427
|
private noteFor;
|
|
421
428
|
/**
|
|
422
429
|
* The executor handed to a plan: dispatch a step to the proxy that owns its
|
|
@@ -476,6 +483,22 @@ export interface RunMatchLike {
|
|
|
476
483
|
/** The frozen name, beside the runtime intent the plan runs on (R-INTENT-12). */
|
|
477
484
|
intentName?: string;
|
|
478
485
|
}
|
|
486
|
+
/**
|
|
487
|
+
* The old rule, for a service that sends no `stop`: the position of the first
|
|
488
|
+
* step whose failure count is above `maxStepFailures` (fallbk.md D3), or
|
|
489
|
+
* undefined when none is — including when the service sent no policy, which an
|
|
490
|
+
* older service never does.
|
|
491
|
+
*/
|
|
479
492
|
export declare function knownBadStepIndex(steps: readonly SerializedScenarioStep[], maxStepFailures: number | undefined): number | undefined;
|
|
493
|
+
/**
|
|
494
|
+
* Position of the first step the plan must stop in front of (plan-services.md
|
|
495
|
+
* D8), or undefined when there is none.
|
|
496
|
+
*
|
|
497
|
+
* A step that carries `stop` was decided by the service — `null` means run it,
|
|
498
|
+
* even when its count is past the limit: its retry is due. A step without the
|
|
499
|
+
* field came from an older service, and the old counter rule decides it. Both
|
|
500
|
+
* can sit in one chain, since a called segment is served on its own terms.
|
|
501
|
+
*/
|
|
502
|
+
export declare function stopIndex(steps: readonly SerializedScenarioStep[], maxStepFailures: number | undefined): number | undefined;
|
|
480
503
|
export {};
|
|
481
504
|
//# sourceMappingURL=controller.d.ts.map
|
|
@@ -204,20 +204,31 @@ export class ReplayController {
|
|
|
204
204
|
const entries = flat.entries;
|
|
205
205
|
if (entries.length === 0)
|
|
206
206
|
return decline("the chain has no runnable step", "not_ready");
|
|
207
|
-
// Gate 5 —
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
|
|
207
|
+
// Gate 5 — steps the plan must stop in front of (fallbk.md D3,
|
|
208
|
+
// plan-services.md D8): a step the service parked — failed more than its
|
|
209
|
+
// limit, retry not due — or one the calculation marked non-deterministic,
|
|
210
|
+
// on whichever scenario owns it (R-CALL-30). A call the service could not
|
|
211
|
+
// answer stops the plan the same way, and the earliest of them wins.
|
|
212
|
+
const badAt = stopIndex(entries.map((e) => e.step), match.fallback?.maxStepFailures);
|
|
212
213
|
const rawStop = [flat.stopAt, badAt].filter((n) => typeof n === "number");
|
|
213
214
|
// Clamped to the start of the outermost called frame it falls in: a plan
|
|
214
215
|
// never runs half a sub-task and leaves its result mapping unevaluated.
|
|
215
216
|
const stopAt = rawStop.length > 0 ? clampToFrameStart(entries, Math.min(...rawStop)) : undefined;
|
|
216
217
|
if (stopAt === 0) {
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
218
|
+
const unusable = flat.stopAt === 0 && flat.stopReason ? flat.stopReason : undefined;
|
|
219
|
+
// The step that stops the plan, before the clamp: the reason is its own.
|
|
220
|
+
const stopping = entries[Math.min(...rawStop)]?.step ?? entries[0].step;
|
|
221
|
+
const judgement = stopping.stop?.kind === "nondeterministic";
|
|
222
|
+
const why = unusable
|
|
223
|
+
? `its first step is a call that cannot run: ${unusable.reason}`
|
|
224
|
+
: judgement
|
|
225
|
+
? "its first step needs a judgement the plan cannot compute — the agent does this task"
|
|
226
|
+
: `its first step has failed ${stopping.failureCount ?? 0} times — the agent does this task`;
|
|
227
|
+
return decline(why, unusable
|
|
228
|
+
? "unusable_first_step"
|
|
229
|
+
: judgement
|
|
230
|
+
? "nondeterministic_first_step"
|
|
231
|
+
: "known_bad_first_step");
|
|
221
232
|
}
|
|
222
233
|
const planned = stopAt === undefined ? entries : entries.slice(0, stopAt);
|
|
223
234
|
// Gate 6 — tool coverage, over the steps that will actually run.
|
|
@@ -333,6 +344,8 @@ export class ReplayController {
|
|
|
333
344
|
// What this chain calls, and what answers each call right now (R-CALL-32).
|
|
334
345
|
calls: callLines(scenario),
|
|
335
346
|
unusable: flat.stopReason?.reason,
|
|
347
|
+
// Which steps the service told this plan to stop in front of, and why.
|
|
348
|
+
stops: stopLines(entries),
|
|
336
349
|
coverage: coverageOf(entries.map((e) => e.step), wrapped, this.opts.allowServers).join(","),
|
|
337
350
|
tools: entries.map((e) => e.step.toolName ?? "").join(","),
|
|
338
351
|
});
|
|
@@ -562,7 +575,7 @@ export class ReplayController {
|
|
|
562
575
|
// is a call the service could not answer (segmented.md R-CALL-29).
|
|
563
576
|
const parked = plan.stopStep();
|
|
564
577
|
this.handOver(state, {
|
|
565
|
-
kind: state.
|
|
578
|
+
kind: this.stopKindOf(state, plan.stopEntry()),
|
|
566
579
|
step: parked,
|
|
567
580
|
entry: plan.stopEntry(),
|
|
568
581
|
position: plan.stepCount,
|
|
@@ -639,7 +652,10 @@ export class ReplayController {
|
|
|
639
652
|
const position = stop.flatIndex ?? plan.stepCount;
|
|
640
653
|
const step = plan.allSteps()[position];
|
|
641
654
|
this.handOver(state, {
|
|
642
|
-
kind
|
|
655
|
+
// The plan only knows it stopped in front of its end; which kind of
|
|
656
|
+
// stop that is — parked, a judgement, a call that cannot run — is
|
|
657
|
+
// decided here, where the served reasons are.
|
|
658
|
+
kind: stop.kind === "known_bad_step" ? this.stopKindOf(state, plan.stopEntry()) : stop.kind,
|
|
643
659
|
step,
|
|
644
660
|
entry: plan.allEntries()[position],
|
|
645
661
|
position,
|
|
@@ -1070,7 +1086,7 @@ export class ReplayController {
|
|
|
1070
1086
|
let text = composed.text;
|
|
1071
1087
|
if (plan.stopsEarly()) {
|
|
1072
1088
|
const parked = plan.stopStep();
|
|
1073
|
-
const stopKind = state.
|
|
1089
|
+
const stopKind = this.stopKindOf(state, plan.stopEntry());
|
|
1074
1090
|
state.handover = {
|
|
1075
1091
|
kind: stopKind,
|
|
1076
1092
|
stepIndex: parked.stepIndex,
|
|
@@ -1132,9 +1148,22 @@ export class ReplayController {
|
|
|
1132
1148
|
? "a sub-task of this chain cannot run — the agent does that part itself"
|
|
1133
1149
|
: h.kind === "known_bad_step"
|
|
1134
1150
|
? "the next step has failed too often — the agent continues from here"
|
|
1135
|
-
:
|
|
1151
|
+
: h.kind === "nondeterministic_step"
|
|
1152
|
+
? "the next step needs a judgement the plan cannot compute — the agent makes it"
|
|
1153
|
+
: "a step broke — the agent continues from here",
|
|
1136
1154
|
});
|
|
1137
1155
|
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Which kind of stop a plan's end is (plan-services.md D8). The stopping
|
|
1158
|
+
* step's own served reason wins — the calculation's verdict is more exact
|
|
1159
|
+
* than a counter — then a call the service could not answer, then a parked
|
|
1160
|
+
* step.
|
|
1161
|
+
*/
|
|
1162
|
+
stopKindOf(state, entry) {
|
|
1163
|
+
if (entry?.step.stop?.kind === "nondeterministic")
|
|
1164
|
+
return "nondeterministic_step";
|
|
1165
|
+
return state.flatStop ? "unusable_call" : "known_bad_step";
|
|
1166
|
+
}
|
|
1138
1167
|
noteFor(state, h) {
|
|
1139
1168
|
const plan = state.plan;
|
|
1140
1169
|
const executed = h.ranThisStep ? h.position + 1 : h.position;
|
|
@@ -1243,6 +1272,9 @@ export class ReplayController {
|
|
|
1243
1272
|
stage: info.stage ?? (status === "skipped" ? "tool_call" : undefined),
|
|
1244
1273
|
error,
|
|
1245
1274
|
durationMs: info.ms,
|
|
1275
|
+
// The tool is gone from its server: the service repairs the plan without
|
|
1276
|
+
// waiting out the grace (plan-services.md D8, kind 2).
|
|
1277
|
+
toolMissing: info.toolMissing || undefined,
|
|
1246
1278
|
// A verdict about a called segment's step is counted on that segment, at
|
|
1247
1279
|
// the revision it was served (R-CALL-13).
|
|
1248
1280
|
scenarioId: info.entry && info.entry.depth > 0 ? info.entry.scenarioId : undefined,
|
|
@@ -1340,11 +1372,6 @@ function targetKeys(schema) {
|
|
|
1340
1372
|
return [];
|
|
1341
1373
|
return Object.keys(schema).filter((k) => schema[k]?.kind !== "setting");
|
|
1342
1374
|
}
|
|
1343
|
-
/**
|
|
1344
|
-
* Position of the first step whose failure count is above `maxStepFailures`
|
|
1345
|
-
* (fallbk.md D3), or undefined when none is — including when the service sent
|
|
1346
|
-
* no policy, which an older service never does.
|
|
1347
|
-
*/
|
|
1348
1375
|
/**
|
|
1349
1376
|
* The index a hand-over reports, in the *caller's* chain (R-CALL-32).
|
|
1350
1377
|
*
|
|
@@ -1369,10 +1396,39 @@ function callLines(scenario) {
|
|
|
1369
1396
|
.map((s) => `${s.segmentId ?? "-"}:${s.unusable ?? s.resolution?.state ?? "resolved"}`)
|
|
1370
1397
|
.join(",");
|
|
1371
1398
|
}
|
|
1399
|
+
/** The served stops in a flat chain, for the `plan.armed` line (plan-services.md D8). */
|
|
1400
|
+
function stopLines(entries) {
|
|
1401
|
+
const lines = entries
|
|
1402
|
+
.map((e, i) => e.step.stop ? `${i}:${e.step.stop.kind}${e.step.stop.repairDue ? "/repair" : ""}` : undefined)
|
|
1403
|
+
.filter((s) => s !== undefined);
|
|
1404
|
+
return lines.length > 0 ? lines.join(",") : undefined;
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* The old rule, for a service that sends no `stop`: the position of the first
|
|
1408
|
+
* step whose failure count is above `maxStepFailures` (fallbk.md D3), or
|
|
1409
|
+
* undefined when none is — including when the service sent no policy, which an
|
|
1410
|
+
* older service never does.
|
|
1411
|
+
*/
|
|
1372
1412
|
export function knownBadStepIndex(steps, maxStepFailures) {
|
|
1413
|
+
const i = steps.findIndex((s) => isKnownBad(s, maxStepFailures));
|
|
1414
|
+
return i < 0 ? undefined : i;
|
|
1415
|
+
}
|
|
1416
|
+
function isKnownBad(step, maxStepFailures) {
|
|
1373
1417
|
if (typeof maxStepFailures !== "number" || !Number.isFinite(maxStepFailures))
|
|
1374
|
-
return
|
|
1375
|
-
|
|
1418
|
+
return false;
|
|
1419
|
+
return (step.failureCount ?? 0) > maxStepFailures;
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Position of the first step the plan must stop in front of (plan-services.md
|
|
1423
|
+
* D8), or undefined when there is none.
|
|
1424
|
+
*
|
|
1425
|
+
* A step that carries `stop` was decided by the service — `null` means run it,
|
|
1426
|
+
* even when its count is past the limit: its retry is due. A step without the
|
|
1427
|
+
* field came from an older service, and the old counter rule decides it. Both
|
|
1428
|
+
* can sit in one chain, since a called segment is served on its own terms.
|
|
1429
|
+
*/
|
|
1430
|
+
export function stopIndex(steps, maxStepFailures) {
|
|
1431
|
+
const i = steps.findIndex((s) => s.stop !== undefined ? s.stop !== null : isKnownBad(s, maxStepFailures));
|
|
1376
1432
|
return i < 0 ? undefined : i;
|
|
1377
1433
|
}
|
|
1378
1434
|
//# sourceMappingURL=controller.js.map
|
package/dist/replay/handover.js
CHANGED
|
@@ -60,6 +60,11 @@ export function buildHandoverNote(n) {
|
|
|
60
60
|
: "";
|
|
61
61
|
lines.push(`Step ${human} of ${n.totalSteps}${tool.replace(/\)$/, `${owner})`)} did not run: it has failed${times} before, so the scenario stops in front of it.`);
|
|
62
62
|
}
|
|
63
|
+
else if (n.kind === "nondeterministic_step") {
|
|
64
|
+
// The calculation could not write code for this step's input: it is a
|
|
65
|
+
// judgement, and the agent is the one to make it (plan-services.md D8).
|
|
66
|
+
lines.push(`Step ${human} of ${n.totalSteps}${tool} did not run: its input needs a judgement the scenario cannot compute. Make that choice yourself and carry on.`);
|
|
67
|
+
}
|
|
63
68
|
else {
|
|
64
69
|
const error = n.error ? `: ${truncate(n.error, MAX_ERROR_CHARS)}` : "";
|
|
65
70
|
lines.push(`Step ${human} of ${n.totalSteps}${tool} failed${error}`);
|
package/dist/replay/plan.d.ts
CHANGED
|
@@ -65,6 +65,8 @@ export interface StepInfo {
|
|
|
65
65
|
error?: string;
|
|
66
66
|
derivedKeys?: string[];
|
|
67
67
|
ms: number;
|
|
68
|
+
/** The tool is gone from its server, read from the error (plan-services.md D8). */
|
|
69
|
+
toolMissing?: boolean;
|
|
68
70
|
}
|
|
69
71
|
export type StepObserver = (info: StepInfo) => void;
|
|
70
72
|
export interface ComposeResult {
|
package/dist/replay/plan.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import { evalParamMapLogic, evalParamsLogic, evalResponseParamsLogic, evalResultMapLogic, evalToolInputLogic, evalToolOutputLogic, } from "./logic.js";
|
|
24
24
|
import { flatEntriesOf } from "./flatten.js";
|
|
25
25
|
import { assembleBundle, bundleInput, MAX_REPLAY_REASON } from "./bundle.js";
|
|
26
|
-
import { toolResultError } from "./tool-error.js";
|
|
26
|
+
import { isMissingToolError, toolResultError } from "./tool-error.js";
|
|
27
27
|
/**
|
|
28
28
|
* The recorded values for the *settings* a mapping did not name (R-CALL-21).
|
|
29
29
|
*
|
|
@@ -433,13 +433,29 @@ export class ScenarioReplayPlan {
|
|
|
433
433
|
: undefined) ?? (await recordedOutputFor?.(entry).catch(() => undefined));
|
|
434
434
|
if (!fallback) {
|
|
435
435
|
skipped += 1;
|
|
436
|
-
onStep?.({
|
|
436
|
+
onStep?.({
|
|
437
|
+
step,
|
|
438
|
+
entry,
|
|
439
|
+
input: computed,
|
|
440
|
+
outcome: "skipped",
|
|
441
|
+
error,
|
|
442
|
+
toolMissing: isMissingToolError(error) || undefined,
|
|
443
|
+
ms: Date.now() - startedAt,
|
|
444
|
+
});
|
|
437
445
|
continue;
|
|
438
446
|
}
|
|
439
447
|
response = fallback;
|
|
440
448
|
recorded = true;
|
|
441
449
|
recordedCount += 1;
|
|
442
|
-
onStep?.({
|
|
450
|
+
onStep?.({
|
|
451
|
+
step,
|
|
452
|
+
entry,
|
|
453
|
+
input: computed,
|
|
454
|
+
outcome: "recorded",
|
|
455
|
+
error,
|
|
456
|
+
toolMissing: isMissingToolError(error) || undefined,
|
|
457
|
+
ms: Date.now() - startedAt,
|
|
458
|
+
});
|
|
443
459
|
}
|
|
444
460
|
// Thread the output for later steps' inputs. A recorded output threads
|
|
445
461
|
// too: `toolOutputLogic` was authored against exactly this shape, and a
|
|
@@ -465,6 +481,7 @@ export class ScenarioReplayPlan {
|
|
|
465
481
|
outcome: error ? "failed" : "executed",
|
|
466
482
|
stage: error ? "tool_call" : undefined,
|
|
467
483
|
error,
|
|
484
|
+
toolMissing: isMissingToolError(error) || undefined,
|
|
468
485
|
derivedKeys,
|
|
469
486
|
ms: Date.now() - startedAt,
|
|
470
487
|
});
|
|
@@ -518,6 +535,9 @@ export class ScenarioReplayPlan {
|
|
|
518
535
|
}
|
|
519
536
|
const entry = this.entries[this.stepIndex];
|
|
520
537
|
const step = entry.step;
|
|
538
|
+
// Where this step sits in the flat list — what a stop reports. Taken now,
|
|
539
|
+
// because the cursor has moved on by the time some stops are written.
|
|
540
|
+
const flatIndex = this.stepIndex;
|
|
521
541
|
const startedAt = Date.now();
|
|
522
542
|
let computed;
|
|
523
543
|
try {
|
|
@@ -529,6 +549,7 @@ export class ScenarioReplayPlan {
|
|
|
529
549
|
stopped = {
|
|
530
550
|
kind: "step_failed",
|
|
531
551
|
stepIndex: step.stepIndex,
|
|
552
|
+
flatIndex,
|
|
532
553
|
stage: "tool_input_logic",
|
|
533
554
|
error: errText(err),
|
|
534
555
|
};
|
|
@@ -547,7 +568,15 @@ export class ScenarioReplayPlan {
|
|
|
547
568
|
: undefined) ?? (await recordedOutputFor?.(entry).catch(() => undefined));
|
|
548
569
|
if (!fallback) {
|
|
549
570
|
skipped += 1;
|
|
550
|
-
onStep?.({
|
|
571
|
+
onStep?.({
|
|
572
|
+
step,
|
|
573
|
+
entry,
|
|
574
|
+
input: computed,
|
|
575
|
+
outcome: "skipped",
|
|
576
|
+
error,
|
|
577
|
+
toolMissing: isMissingToolError(error) || undefined,
|
|
578
|
+
ms: Date.now() - startedAt,
|
|
579
|
+
});
|
|
551
580
|
// Advance past a step that cannot run, without threading anything.
|
|
552
581
|
// The frame is still left behind it: a sub-task whose last step could
|
|
553
582
|
// not run here still hands back whatever its earlier steps emitted.
|
|
@@ -558,7 +587,15 @@ export class ScenarioReplayPlan {
|
|
|
558
587
|
response = fallback;
|
|
559
588
|
recorded = true;
|
|
560
589
|
recordedCount += 1;
|
|
561
|
-
onStep?.({
|
|
590
|
+
onStep?.({
|
|
591
|
+
step,
|
|
592
|
+
entry,
|
|
593
|
+
input: computed,
|
|
594
|
+
outcome: "recorded",
|
|
595
|
+
error,
|
|
596
|
+
toolMissing: isMissingToolError(error) || undefined,
|
|
597
|
+
ms: Date.now() - startedAt,
|
|
598
|
+
});
|
|
562
599
|
}
|
|
563
600
|
if (stopOnFailure && !recorded) {
|
|
564
601
|
// Judged before the output logic, which was written against a success
|
|
@@ -573,6 +610,7 @@ export class ScenarioReplayPlan {
|
|
|
573
610
|
outcome: "failed",
|
|
574
611
|
stage: "tool_call",
|
|
575
612
|
error: toolError,
|
|
613
|
+
toolMissing: isMissingToolError(toolError) || undefined,
|
|
576
614
|
ms: Date.now() - startedAt,
|
|
577
615
|
});
|
|
578
616
|
entries.push({
|
|
@@ -585,6 +623,7 @@ export class ScenarioReplayPlan {
|
|
|
585
623
|
stopped = {
|
|
586
624
|
kind: "step_failed",
|
|
587
625
|
stepIndex: step.stepIndex,
|
|
626
|
+
flatIndex,
|
|
588
627
|
stage: "tool_call",
|
|
589
628
|
error: toolError,
|
|
590
629
|
};
|
|
@@ -608,6 +647,7 @@ export class ScenarioReplayPlan {
|
|
|
608
647
|
stopped = {
|
|
609
648
|
kind: "step_failed",
|
|
610
649
|
stepIndex: step.stepIndex,
|
|
650
|
+
flatIndex,
|
|
611
651
|
stage: "tool_output_logic",
|
|
612
652
|
error: errText(err),
|
|
613
653
|
};
|
|
@@ -627,6 +667,7 @@ export class ScenarioReplayPlan {
|
|
|
627
667
|
outcome: error ? "failed" : "executed",
|
|
628
668
|
stage: error ? "tool_call" : undefined,
|
|
629
669
|
error,
|
|
670
|
+
toolMissing: isMissingToolError(error) || undefined,
|
|
630
671
|
derivedKeys,
|
|
631
672
|
ms: Date.now() - startedAt,
|
|
632
673
|
});
|
|
@@ -640,7 +681,13 @@ export class ScenarioReplayPlan {
|
|
|
640
681
|
}
|
|
641
682
|
// Every planned step ran and the plan ends in front of a parked one.
|
|
642
683
|
if (!stopped && !partial && this.stopsEarly() && this.isDone()) {
|
|
643
|
-
|
|
684
|
+
// `known_bad_step` here means "in front of the plan's end": the
|
|
685
|
+
// controller reads the served reason and names the kind (D8).
|
|
686
|
+
stopped = {
|
|
687
|
+
kind: "known_bad_step",
|
|
688
|
+
stepIndex: this.entries[this.stopAt].stepIndex,
|
|
689
|
+
flatIndex: this.stopAt,
|
|
690
|
+
};
|
|
644
691
|
}
|
|
645
692
|
return {
|
|
646
693
|
text: assembleBundle(entries, maxChars, {
|
package/dist/replay/pricing.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* Bump the version — on **every** side — whenever a row changes.
|
|
18
18
|
*/
|
|
19
19
|
/** Identifies {@link MODEL_PRICING}. Must equal the service's constant. */
|
|
20
|
-
export declare const PRICING_VERSION = "2026-
|
|
20
|
+
export declare const PRICING_VERSION = "2026-09-24";
|
|
21
21
|
/**
|
|
22
22
|
* Tolerate provider prefixes (`anthropic/`, `us.anthropic.`), date snapshots and
|
|
23
23
|
* unseen version bumps by falling back to the longest matching family, so a new
|
package/dist/replay/pricing.js
CHANGED
|
@@ -17,24 +17,32 @@
|
|
|
17
17
|
* Bump the version — on **every** side — whenever a row changes.
|
|
18
18
|
*/
|
|
19
19
|
/** Identifies {@link MODEL_PRICING}. Must equal the service's constant. */
|
|
20
|
-
export const PRICING_VERSION = "2026-
|
|
21
|
-
/**
|
|
22
|
-
|
|
20
|
+
export const PRICING_VERSION = "2026-09-24";
|
|
21
|
+
/**
|
|
22
|
+
* A 5-minute cache write bills at 1.25x input. Cache reads bill at 0.1x input,
|
|
23
|
+
* except on models whose list price says otherwise (Opus 5.5, Fable 5.1), which
|
|
24
|
+
* pass their read price in dollars.
|
|
25
|
+
*/
|
|
26
|
+
const priced = (input, output, cacheRead = input * 0.1) => ({
|
|
23
27
|
input,
|
|
24
28
|
output,
|
|
25
|
-
cacheRead
|
|
29
|
+
cacheRead,
|
|
26
30
|
cacheWrite: input * 1.25,
|
|
27
31
|
});
|
|
28
32
|
const MODEL_PRICING = {
|
|
29
33
|
"claude-haiku-4-5": priced(1.0, 5.0),
|
|
30
34
|
"claude-haiku-4-5-20251001": priced(1.0, 5.0),
|
|
35
|
+
"claude-sonnet-4-5": priced(3.0, 15.0),
|
|
31
36
|
"claude-sonnet-4-6": priced(3.0, 15.0),
|
|
32
37
|
"claude-sonnet-5": priced(2.0, 10.0),
|
|
38
|
+
"claude-opus-4-5": priced(5.0, 25.0),
|
|
33
39
|
"claude-opus-4-6": priced(5.0, 25.0),
|
|
34
40
|
"claude-opus-4-7": priced(5.0, 25.0),
|
|
35
41
|
"claude-opus-4-8": priced(5.0, 25.0),
|
|
36
42
|
"claude-opus-5": priced(5.0, 25.0),
|
|
43
|
+
"claude-opus-5-5": priced(4.0, 20.0, 0.2),
|
|
37
44
|
"claude-fable-5": priced(10.0, 50.0),
|
|
45
|
+
"claude-fable-5-1": priced(10.0, 50.0, 0.25),
|
|
38
46
|
};
|
|
39
47
|
/**
|
|
40
48
|
* Tolerate provider prefixes (`anthropic/`, `us.anthropic.`), date snapshots and
|