@basein/runner 0.3.14 → 0.3.15
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/dist/control/server.d.ts +25 -10
- package/dist/control/server.js +50 -18
- package/dist/record/recorder.d.ts +6 -3
- package/dist/replay/controller.d.ts +18 -0
- package/dist/replay/controller.js +15 -0
- package/package.json +1 -1
package/dist/control/server.d.ts
CHANGED
|
@@ -1075,21 +1075,36 @@ export declare class ControlServer {
|
|
|
1075
1075
|
private applyFragmentCreation;
|
|
1076
1076
|
/**
|
|
1077
1077
|
* Whether this matched turn's task is now the model's own, so its work is
|
|
1078
|
-
* the repeat the service offered to record
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1078
|
+
* the repeat the service offered to record: offered, nothing recording, no
|
|
1079
|
+
* fragment, and the prompt match's plan done with — retired, not running,
|
|
1080
|
+
* not diverging — and either:
|
|
1081
|
+
*
|
|
1082
|
+
* - it ran no step: the model does the whole task (newestRun.md N1); or
|
|
1083
|
+
* - it ran to its end, and the model, told to answer from its results,
|
|
1084
|
+
* called tools of its own instead (N7). Before 0.3.15 that work was
|
|
1085
|
+
* lost: a model that did not take the plan's result as the answer and
|
|
1086
|
+
* did the task again left nothing to fix the plan from.
|
|
1087
|
+
*
|
|
1088
|
+
* A plan that handed over is the fragment's to record (fallbk.md D7). One
|
|
1089
|
+
* that ran part of the task and stopped without a hand-over records
|
|
1090
|
+
* nothing, as before: the call where it stopped ran while it was still
|
|
1091
|
+
* live, so a run of what came after would lack the step that matters.
|
|
1082
1092
|
*/
|
|
1083
1093
|
private repeatDue;
|
|
1084
1094
|
/**
|
|
1085
|
-
*
|
|
1095
|
+
* Whether the prompt match's plan ran to its end before the model went on
|
|
1096
|
+
* by itself (newestRun.md N7). False when it ran no step, which is N1.
|
|
1097
|
+
*/
|
|
1098
|
+
private redoneAfterPlan;
|
|
1099
|
+
/**
|
|
1100
|
+
* Open the repeat run and record from this call on (newestRun.md N1, N7).
|
|
1086
1101
|
*
|
|
1087
1102
|
* The same swap as {@link openFragment}: nothing allocated before it belongs
|
|
1088
|
-
* to it. Unlike a fragment it carries the turn's prompt
|
|
1089
|
-
* duration are the whole turn's
|
|
1090
|
-
*
|
|
1091
|
-
*
|
|
1092
|
-
* them.
|
|
1103
|
+
* to it. Unlike a fragment it carries the turn's prompt. When no plan did
|
|
1104
|
+
* any of the task, its cost and duration are the whole turn's; after a plan
|
|
1105
|
+
* that ran to its end, they start here, as a fragment's do. Nothing waits
|
|
1106
|
+
* for the service's answer: the steps queue behind the create, and a create
|
|
1107
|
+
* answered `id: null` — the service records no repeat of it — drops them.
|
|
1093
1108
|
*/
|
|
1094
1109
|
private openRepeat;
|
|
1095
1110
|
/**
|
package/dist/control/server.js
CHANGED
|
@@ -2292,12 +2292,15 @@ export class ControlServer {
|
|
|
2292
2292
|
this.reportExecution(session, run, durationMs, cost, cutShort);
|
|
2293
2293
|
// A fragment is recorded from the hand-over on (fallbk.md D7), so its cost
|
|
2294
2294
|
// and duration are the model's own work, not the steps the scenario did.
|
|
2295
|
-
//
|
|
2296
|
-
//
|
|
2297
|
-
// spent after the
|
|
2295
|
+
// So is a repeat opened after the plan ran to its end (newestRun.md N7). Another host
|
|
2296
|
+
// hands over one number for the whole run, and it is the fragment's: the
|
|
2297
|
+
// plan's own steps made no model call, so all of it was spent after the
|
|
2298
|
+
// hand-over.
|
|
2298
2299
|
const fragment = run.fragment?.state === "open" ? run.fragment : undefined;
|
|
2299
|
-
const
|
|
2300
|
-
const
|
|
2300
|
+
const repeatAfterPlan = !fragment && run.repeat?.state === "open" && run.repeat.startedAtMs !== undefined ? run.repeat : undefined;
|
|
2301
|
+
const ownWork = fragment ?? repeatAfterPlan;
|
|
2302
|
+
const recordedCost = ownWork && !other ? this.costSince(session, ownWork.usageMark) : cost;
|
|
2303
|
+
const recordedMs = ownWork?.startedAtMs ? Date.now() - ownWork.startedAtMs : durationMs;
|
|
2301
2304
|
if (run.recording) {
|
|
2302
2305
|
const transcriptPath = session.transcriptPath;
|
|
2303
2306
|
// The answer is the host's when it handed one over, and the
|
|
@@ -4032,10 +4035,20 @@ export class ControlServer {
|
|
|
4032
4035
|
}
|
|
4033
4036
|
/**
|
|
4034
4037
|
* Whether this matched turn's task is now the model's own, so its work is
|
|
4035
|
-
* the repeat the service offered to record
|
|
4036
|
-
*
|
|
4037
|
-
*
|
|
4038
|
-
*
|
|
4038
|
+
* the repeat the service offered to record: offered, nothing recording, no
|
|
4039
|
+
* fragment, and the prompt match's plan done with — retired, not running,
|
|
4040
|
+
* not diverging — and either:
|
|
4041
|
+
*
|
|
4042
|
+
* - it ran no step: the model does the whole task (newestRun.md N1); or
|
|
4043
|
+
* - it ran to its end, and the model, told to answer from its results,
|
|
4044
|
+
* called tools of its own instead (N7). Before 0.3.15 that work was
|
|
4045
|
+
* lost: a model that did not take the plan's result as the answer and
|
|
4046
|
+
* did the task again left nothing to fix the plan from.
|
|
4047
|
+
*
|
|
4048
|
+
* A plan that handed over is the fragment's to record (fallbk.md D7). One
|
|
4049
|
+
* that ran part of the task and stopped without a hand-over records
|
|
4050
|
+
* nothing, as before: the call where it stopped ran while it was still
|
|
4051
|
+
* live, so a run of what came after would lack the step that matters.
|
|
4039
4052
|
*/
|
|
4040
4053
|
repeatDue(run) {
|
|
4041
4054
|
const r = run.repeat;
|
|
@@ -4044,23 +4057,34 @@ export class ControlServer {
|
|
|
4044
4057
|
const prompt = run.replays.find((s) => s.armedBy === "prompt");
|
|
4045
4058
|
if (!prompt?.plan)
|
|
4046
4059
|
return true;
|
|
4047
|
-
|
|
4060
|
+
if (!prompt.retired || prompt.diverging || this.runningPlans.has(prompt) || prompt.handover)
|
|
4061
|
+
return false;
|
|
4062
|
+
return prompt.stepsPinned === 0 || prompt.ranToEnd === true;
|
|
4063
|
+
}
|
|
4064
|
+
/**
|
|
4065
|
+
* Whether the prompt match's plan ran to its end before the model went on
|
|
4066
|
+
* by itself (newestRun.md N7). False when it ran no step, which is N1.
|
|
4067
|
+
*/
|
|
4068
|
+
redoneAfterPlan(run) {
|
|
4069
|
+
const prompt = run.replays.find((s) => s.armedBy === "prompt");
|
|
4070
|
+
return prompt?.plan !== undefined && prompt.ranToEnd === true;
|
|
4048
4071
|
}
|
|
4049
4072
|
/**
|
|
4050
|
-
* Open the repeat run and record from this call on (newestRun.md N1).
|
|
4073
|
+
* Open the repeat run and record from this call on (newestRun.md N1, N7).
|
|
4051
4074
|
*
|
|
4052
4075
|
* The same swap as {@link openFragment}: nothing allocated before it belongs
|
|
4053
|
-
* to it. Unlike a fragment it carries the turn's prompt
|
|
4054
|
-
* duration are the whole turn's
|
|
4055
|
-
*
|
|
4056
|
-
*
|
|
4057
|
-
* them.
|
|
4076
|
+
* to it. Unlike a fragment it carries the turn's prompt. When no plan did
|
|
4077
|
+
* any of the task, its cost and duration are the whole turn's; after a plan
|
|
4078
|
+
* that ran to its end, they start here, as a fragment's do. Nothing waits
|
|
4079
|
+
* for the service's answer: the steps queue behind the create, and a create
|
|
4080
|
+
* answered `id: null` — the service records no repeat of it — drops them.
|
|
4058
4081
|
*/
|
|
4059
4082
|
openRepeat(session, run) {
|
|
4060
4083
|
const r = run.repeat;
|
|
4061
4084
|
if (!r || r.state !== "offered")
|
|
4062
4085
|
return;
|
|
4063
|
-
const
|
|
4086
|
+
const redone = this.redoneAfterPlan(run);
|
|
4087
|
+
const reason = redone ? "redone" : this.repeatReason(run);
|
|
4064
4088
|
const repeatOf = reason ? { runId: r.runId, reason } : { runId: r.runId };
|
|
4065
4089
|
// The repeat is a run of the same host as the turn it records.
|
|
4066
4090
|
const framework = frameworkOf(session);
|
|
@@ -4075,11 +4099,19 @@ export class ControlServer {
|
|
|
4075
4099
|
run.ordering = new StepIndexAllocator();
|
|
4076
4100
|
run.recording = true;
|
|
4077
4101
|
r.state = "open";
|
|
4102
|
+
if (redone) {
|
|
4103
|
+
// The plan's steps are not the model's work: this run is billed and
|
|
4104
|
+
// timed from its own first call on, as a fragment is.
|
|
4105
|
+
r.usageMark = markTranscriptUsage(session.transcriptPath);
|
|
4106
|
+
r.startedAtMs = Date.now();
|
|
4107
|
+
}
|
|
4078
4108
|
logLine("run.repeat", {
|
|
4079
4109
|
run: id,
|
|
4080
4110
|
of: r.runId,
|
|
4081
4111
|
reason,
|
|
4082
|
-
why:
|
|
4112
|
+
why: redone
|
|
4113
|
+
? "the plan ran to its end, and the agent went on with calls of its own instead of answering from its results — recorded as that recording's newest run"
|
|
4114
|
+
: "the agent is doing a recorded task itself — recorded as that recording's newest run",
|
|
4083
4115
|
});
|
|
4084
4116
|
if (!isRunCreationAware(this.recorder))
|
|
4085
4117
|
return;
|
|
@@ -13,9 +13,12 @@ export interface StartRunOptions {
|
|
|
13
13
|
};
|
|
14
14
|
/**
|
|
15
15
|
* Marks a *repeat* (the service's newestRun.md): a matched turn whose plan
|
|
16
|
-
* did not run, so the model is doing the recording's task itself
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* did not run, so the model is doing the recording's task itself — or whose
|
|
17
|
+
* plan ran to its end, after which the model went on with calls of its own
|
|
18
|
+
* instead of answering from its results (N7). Sent only when the match
|
|
19
|
+
* offered it ({@link RunMatch.repeat}). `reason` is why the plan did not
|
|
20
|
+
* run, or `redone` after a plan that ran to its end. The service never
|
|
21
|
+
* prompt-matches a repeat.
|
|
19
22
|
*/
|
|
20
23
|
repeatOf?: {
|
|
21
24
|
runId: string;
|
|
@@ -435,6 +435,24 @@ export interface ReplayState {
|
|
|
435
435
|
sharedWith: string[];
|
|
436
436
|
/** Set when the plan handed the task to the model part-way (fallbk.md). */
|
|
437
437
|
handover?: Handover;
|
|
438
|
+
/**
|
|
439
|
+
* Every planned step ran and the model was handed the results to answer
|
|
440
|
+
* from: a steered plan whose last step threaded, a direct run that ran to
|
|
441
|
+
* its end, or a divergence whose bundle carried the rest of the chain. A
|
|
442
|
+
* call the model makes of its own after this is the task done again — it
|
|
443
|
+
* did not take the plan's result as the answer (the service's newestRun.md
|
|
444
|
+
* N7). Never set on a plan that handed over: the hand-over says the rest
|
|
445
|
+
* is the model's.
|
|
446
|
+
*/
|
|
447
|
+
ranToEnd?: boolean;
|
|
448
|
+
/**
|
|
449
|
+
* A divergence is running the plan's remaining steps for real (§8). The
|
|
450
|
+
* plan is already retired by then, so this is what tells a call made beside
|
|
451
|
+
* that work from one made after it: a repeat never opens while steps are
|
|
452
|
+
* still landing, and whether the plan ran to its end or handed over is not
|
|
453
|
+
* known yet (the service's newestRun.md N7).
|
|
454
|
+
*/
|
|
455
|
+
diverging?: boolean;
|
|
438
456
|
/**
|
|
439
457
|
* Which gate declined, as a fixed code (segmented.md R-HIT-14). The prose
|
|
440
458
|
* `why` that `decline()` logs stays free text; this is what code reads — the
|
|
@@ -1375,6 +1375,7 @@ export class ReplayController {
|
|
|
1375
1375
|
}
|
|
1376
1376
|
if (done) {
|
|
1377
1377
|
this.upgrade(state, "steered_full");
|
|
1378
|
+
state.ranToEnd = true;
|
|
1378
1379
|
// A completing retire: the last step threaded and the plan's work is over
|
|
1379
1380
|
// (segmented.md R-MONEY-3). Everything the agent spends after this is the
|
|
1380
1381
|
// agent's own, not this plan's.
|
|
@@ -1687,6 +1688,7 @@ export class ReplayController {
|
|
|
1687
1688
|
// `runToCompletion` returned without a stop: the plan ran to its end, so
|
|
1688
1689
|
// this is a completing retire (R-MONEY-3). The stopped path handed over
|
|
1689
1690
|
// above and took no mark.
|
|
1691
|
+
state.ranToEnd = true;
|
|
1690
1692
|
this.retire(state, undefined, true);
|
|
1691
1693
|
logLine("replay.done", {
|
|
1692
1694
|
scenario: state.scenarioId ?? undefined,
|
|
@@ -2075,6 +2077,9 @@ export class ReplayController {
|
|
|
2075
2077
|
step: `${plan.currentStepIndex}/${plan.stepCount}`,
|
|
2076
2078
|
why,
|
|
2077
2079
|
});
|
|
2080
|
+
// Retired, and still working: until the bundle is composed, a call the
|
|
2081
|
+
// model makes beside this one is beside the plan, not after it.
|
|
2082
|
+
state.diverging = true;
|
|
2078
2083
|
this.retire(state, undefined);
|
|
2079
2084
|
// The bundle, and the note in front of it, must fit the channel it goes
|
|
2080
2085
|
// out on whole: the host cuts a long Bash output in the middle.
|
|
@@ -2099,6 +2104,11 @@ export class ReplayController {
|
|
|
2099
2104
|
});
|
|
2100
2105
|
return { kind: "abort" };
|
|
2101
2106
|
}
|
|
2107
|
+
finally {
|
|
2108
|
+
// Nothing below waits: what this divergence ends in — a hand-over, or
|
|
2109
|
+
// a bundle that ran to the end — is settled before any other hook runs.
|
|
2110
|
+
state.diverging = false;
|
|
2111
|
+
}
|
|
2102
2112
|
const stop = composed.stopped;
|
|
2103
2113
|
// Same rule as `runArmed`: a recovered chain whose tools errored is
|
|
2104
2114
|
// `failed`, and so is one that stopped at a step that broke — its logic
|
|
@@ -2175,6 +2185,11 @@ export class ReplayController {
|
|
|
2175
2185
|
};
|
|
2176
2186
|
text = `${state.handover.note}\n\n${composed.text}`;
|
|
2177
2187
|
}
|
|
2188
|
+
else if (!stop) {
|
|
2189
|
+
// The bundle carries the rest of the chain, and its header tells the
|
|
2190
|
+
// model the work is done: from here the model answers from it.
|
|
2191
|
+
state.ranToEnd = true;
|
|
2192
|
+
}
|
|
2178
2193
|
// Bash-clean delivery: the model reaches for `Bash` when it cannot call a
|
|
2179
2194
|
// scripted tool, and a genuine command output is trusted where a `deny`
|
|
2180
2195
|
// reason is read as adversarial interception.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basein/runner",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.15",
|
|
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",
|