@basein/runner 0.3.13 → 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
CHANGED
|
@@ -1073,6 +1073,46 @@ export declare class ControlServer {
|
|
|
1073
1073
|
private openFragment;
|
|
1074
1074
|
/** What the fragment's create call said: `created: false` is a hit, not a recording. */
|
|
1075
1075
|
private applyFragmentCreation;
|
|
1076
|
+
/**
|
|
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: 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.
|
|
1092
|
+
*/
|
|
1093
|
+
private repeatDue;
|
|
1094
|
+
/**
|
|
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).
|
|
1101
|
+
*
|
|
1102
|
+
* The same swap as {@link openFragment}: nothing allocated before it belongs
|
|
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.
|
|
1108
|
+
*/
|
|
1109
|
+
private openRepeat;
|
|
1110
|
+
/**
|
|
1111
|
+
* Why the matched turn's plan did not run, as the service shows it: what
|
|
1112
|
+
* the service withheld it for, else the code of the gate that declined it
|
|
1113
|
+
* here, a plan that was late included.
|
|
1114
|
+
*/
|
|
1115
|
+
private repeatReason;
|
|
1076
1116
|
/** Resolve with `p`, or null once `ms` passes or it rejects. */
|
|
1077
1117
|
private withinBudget;
|
|
1078
1118
|
/**
|
package/dist/control/server.js
CHANGED
|
@@ -697,6 +697,9 @@ export class ControlServer {
|
|
|
697
697
|
}
|
|
698
698
|
: null,
|
|
699
699
|
fragment: s.run?.fragment ? { state: s.run.fragment.state } : null,
|
|
700
|
+
// The service's offer to record the turn as a recording's newest run,
|
|
701
|
+
// and what became of it (newestRun.md).
|
|
702
|
+
repeat: s.run?.repeat ? { state: s.run.repeat.state, of: s.run.repeat.runId } : null,
|
|
700
703
|
}));
|
|
701
704
|
const sending = this.recorderBacklog();
|
|
702
705
|
return {
|
|
@@ -1673,6 +1676,12 @@ export class ControlServer {
|
|
|
1673
1676
|
if (!match || run.runId !== runId)
|
|
1674
1677
|
return;
|
|
1675
1678
|
run.recording = false;
|
|
1679
|
+
// The service's offer to record the turn after all, should the model end
|
|
1680
|
+
// up doing the task itself (newestRun.md N5). Taken up at the model's
|
|
1681
|
+
// first call of its own, once no plan of this match runs (`repeatDue`).
|
|
1682
|
+
if (match.repeat?.runId && !run.repeat) {
|
|
1683
|
+
run.repeat = { runId: match.repeat.runId, withheld: match.withheld ?? undefined, state: "offered" };
|
|
1684
|
+
}
|
|
1676
1685
|
logLine("run.matched", {
|
|
1677
1686
|
run: runId,
|
|
1678
1687
|
matchedRun: match.runId,
|
|
@@ -2283,12 +2292,15 @@ export class ControlServer {
|
|
|
2283
2292
|
this.reportExecution(session, run, durationMs, cost, cutShort);
|
|
2284
2293
|
// A fragment is recorded from the hand-over on (fallbk.md D7), so its cost
|
|
2285
2294
|
// and duration are the model's own work, not the steps the scenario did.
|
|
2286
|
-
//
|
|
2287
|
-
//
|
|
2288
|
-
// 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.
|
|
2289
2299
|
const fragment = run.fragment?.state === "open" ? run.fragment : undefined;
|
|
2290
|
-
const
|
|
2291
|
-
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;
|
|
2292
2304
|
if (run.recording) {
|
|
2293
2305
|
const transcriptPath = session.transcriptPath;
|
|
2294
2306
|
// The answer is the host's when it handed one over, and the
|
|
@@ -2934,6 +2946,12 @@ export class ControlServer {
|
|
|
2934
2946
|
this.noteHandover(run);
|
|
2935
2947
|
return this.preToolAnswer(this.replay.deliverInstead(note, toolName));
|
|
2936
2948
|
}
|
|
2949
|
+
// The model is doing the matched task itself, and the service asked for
|
|
2950
|
+
// that run to be recorded as the recording's newest (newestRun.md N1).
|
|
2951
|
+
// Opened before the probe below, so the turn is recorded from this call
|
|
2952
|
+
// on like any recording turn — a plan an intent match arms in it too.
|
|
2953
|
+
if (this.repeatDue(run))
|
|
2954
|
+
this.openRepeat(session, run);
|
|
2937
2955
|
// 2. The model is driving: look for a scenario by this iteration's intent.
|
|
2938
2956
|
// Never in plan mode, where nothing may run on the user's behalf.
|
|
2939
2957
|
// If the deadline answers first, it answers `{}`: the call runs as
|
|
@@ -3266,6 +3284,9 @@ export class ControlServer {
|
|
|
3266
3284
|
});
|
|
3267
3285
|
}
|
|
3268
3286
|
this.noteHandover(run);
|
|
3287
|
+
// The model doing the matched task itself, as in Claude Code (newestRun.md N1).
|
|
3288
|
+
if (this.repeatDue(run))
|
|
3289
|
+
this.openRepeat(session, run);
|
|
3269
3290
|
const words = this.hostWords(payload);
|
|
3270
3291
|
// Counted, never waited on, and never arming: see `tryIntentMatch`.
|
|
3271
3292
|
await this.tryIntentMatch(session, run, toolName, words.intent, modelArgs);
|
|
@@ -3546,6 +3567,10 @@ export class ControlServer {
|
|
|
3546
3567
|
out.add(prompt);
|
|
3547
3568
|
if (run.fragment?.hitRunId)
|
|
3548
3569
|
out.add(run.fragment.hitRunId);
|
|
3570
|
+
// The recording the match offered a repeat of: the same recording, and
|
|
3571
|
+
// known even when the match landed too late to be armed (newestRun.md).
|
|
3572
|
+
if (run.repeat)
|
|
3573
|
+
out.add(run.repeat.runId);
|
|
3549
3574
|
return [...out];
|
|
3550
3575
|
}
|
|
3551
3576
|
/**
|
|
@@ -3985,8 +4010,16 @@ export class ControlServer {
|
|
|
3985
4010
|
/** What the fragment's create call said: `created: false` is a hit, not a recording. */
|
|
3986
4011
|
applyFragmentCreation(run, id, created) {
|
|
3987
4012
|
const f = run.fragment;
|
|
3988
|
-
if (
|
|
4013
|
+
if (!f || run.runId !== id)
|
|
4014
|
+
return;
|
|
4015
|
+
if (created.created) {
|
|
4016
|
+
// Recorded as the newest run of the fragment it is a hit on (the
|
|
4017
|
+
// service's newestRun.md N2): still that fragment's repeat, so no step
|
|
4018
|
+
// hit may be counted against it (R-HIT-7).
|
|
4019
|
+
if (created.fragmentOf)
|
|
4020
|
+
f.hitRunId = created.fragmentOf.runId;
|
|
3989
4021
|
return;
|
|
4022
|
+
}
|
|
3990
4023
|
run.recording = false;
|
|
3991
4024
|
f.state = "hit";
|
|
3992
4025
|
// Which run it hit matters from here on: this turn must not count step
|
|
@@ -4000,6 +4033,112 @@ export class ControlServer {
|
|
|
4000
4033
|
why: "a hand-over at this step was recorded before — counted as a hit, not recorded",
|
|
4001
4034
|
});
|
|
4002
4035
|
}
|
|
4036
|
+
/**
|
|
4037
|
+
* Whether this matched turn's task is now the model's own, so its work is
|
|
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.
|
|
4052
|
+
*/
|
|
4053
|
+
repeatDue(run) {
|
|
4054
|
+
const r = run.repeat;
|
|
4055
|
+
if (!r || r.state !== "offered" || run.recording || run.finished || run.fragment)
|
|
4056
|
+
return false;
|
|
4057
|
+
const prompt = run.replays.find((s) => s.armedBy === "prompt");
|
|
4058
|
+
if (!prompt?.plan)
|
|
4059
|
+
return true;
|
|
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;
|
|
4071
|
+
}
|
|
4072
|
+
/**
|
|
4073
|
+
* Open the repeat run and record from this call on (newestRun.md N1, N7).
|
|
4074
|
+
*
|
|
4075
|
+
* The same swap as {@link openFragment}: nothing allocated before it belongs
|
|
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.
|
|
4081
|
+
*/
|
|
4082
|
+
openRepeat(session, run) {
|
|
4083
|
+
const r = run.repeat;
|
|
4084
|
+
if (!r || r.state !== "offered")
|
|
4085
|
+
return;
|
|
4086
|
+
const redone = this.redoneAfterPlan(run);
|
|
4087
|
+
const reason = redone ? "redone" : this.repeatReason(run);
|
|
4088
|
+
const repeatOf = reason ? { runId: r.runId, reason } : { runId: r.runId };
|
|
4089
|
+
// The repeat is a run of the same host as the turn it records.
|
|
4090
|
+
const framework = frameworkOf(session);
|
|
4091
|
+
const id = this.recorder.startRun(run.input, { ...this.runMetadata(session), repeatOf }, framework ? { repeatOf, framework } : { repeatOf });
|
|
4092
|
+
for (const c of run.correlations.values()) {
|
|
4093
|
+
c.settled = true;
|
|
4094
|
+
if (c.fallback)
|
|
4095
|
+
clearTimeout(c.fallback);
|
|
4096
|
+
}
|
|
4097
|
+
run.builtIns.clear();
|
|
4098
|
+
run.runId = id;
|
|
4099
|
+
run.ordering = new StepIndexAllocator();
|
|
4100
|
+
run.recording = true;
|
|
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
|
+
}
|
|
4108
|
+
logLine("run.repeat", {
|
|
4109
|
+
run: id,
|
|
4110
|
+
of: r.runId,
|
|
4111
|
+
reason,
|
|
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",
|
|
4115
|
+
});
|
|
4116
|
+
if (!isRunCreationAware(this.recorder))
|
|
4117
|
+
return;
|
|
4118
|
+
void this.recorder
|
|
4119
|
+
.runCreated(id)
|
|
4120
|
+
.catch(() => ({ created: true }))
|
|
4121
|
+
.then((created) => {
|
|
4122
|
+
if (created.created || run.runId !== id)
|
|
4123
|
+
return;
|
|
4124
|
+
run.recording = false;
|
|
4125
|
+
r.state = "refused";
|
|
4126
|
+
logLine("run.repeat_refused", {
|
|
4127
|
+
run: id,
|
|
4128
|
+
of: r.runId,
|
|
4129
|
+
why: "the service records no repeat of this recording — nothing recorded",
|
|
4130
|
+
});
|
|
4131
|
+
});
|
|
4132
|
+
}
|
|
4133
|
+
/**
|
|
4134
|
+
* Why the matched turn's plan did not run, as the service shows it: what
|
|
4135
|
+
* the service withheld it for, else the code of the gate that declined it
|
|
4136
|
+
* here, a plan that was late included.
|
|
4137
|
+
*/
|
|
4138
|
+
repeatReason(run) {
|
|
4139
|
+
const prompt = run.replays.find((s) => s.armedBy === "prompt") ?? run.lateMatch;
|
|
4140
|
+
return run.repeat?.withheld ?? prompt?.declined ?? undefined;
|
|
4141
|
+
}
|
|
4003
4142
|
/** Resolve with `p`, or null once `ms` passes or it rejects. */
|
|
4004
4143
|
async withinBudget(p, ms) {
|
|
4005
4144
|
let timer;
|
|
@@ -11,6 +11,19 @@ export interface StartRunOptions {
|
|
|
11
11
|
scenarioId: string;
|
|
12
12
|
stepIndex: number;
|
|
13
13
|
};
|
|
14
|
+
/**
|
|
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 — 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.
|
|
22
|
+
*/
|
|
23
|
+
repeatOf?: {
|
|
24
|
+
runId: string;
|
|
25
|
+
reason?: string;
|
|
26
|
+
};
|
|
14
27
|
/**
|
|
15
28
|
* The run's `framework`, when the session named a host that is not Claude
|
|
16
29
|
* Code: its `app`, or `app:scope` (`langchain`, `langchain:billing`). The
|
|
@@ -176,6 +189,15 @@ export interface RunMatch {
|
|
|
176
189
|
* ordinary hand-out, and from an older service.
|
|
177
190
|
*/
|
|
178
191
|
withheld?: string | null;
|
|
192
|
+
/**
|
|
193
|
+
* The service's offer (its newestRun.md N5): should the model end up doing
|
|
194
|
+
* this task itself — the plan declined, withheld, late or off — record the
|
|
195
|
+
* turn as a repeat of `runId`, the recording's newest run. Absent or null
|
|
196
|
+
* from a service that does not record repeats: then nothing is recorded.
|
|
197
|
+
*/
|
|
198
|
+
repeat?: {
|
|
199
|
+
runId: string;
|
|
200
|
+
} | null;
|
|
179
201
|
}
|
|
180
202
|
/** Optional capability: `null` means "fresh run, keep recording". */
|
|
181
203
|
export interface MatchAware {
|
|
@@ -202,10 +224,18 @@ export declare function isMatchAware(r: Recorder): r is Recorder & MatchAware;
|
|
|
202
224
|
/** What the create call said about a run: fresh, or a hit on a fragment. */
|
|
203
225
|
export interface RunCreation {
|
|
204
226
|
created: boolean;
|
|
205
|
-
/**
|
|
227
|
+
/**
|
|
228
|
+
* Set when this was a hit on an existing fragment (R-FALL-7, R-HIT-7) —
|
|
229
|
+
* with `created` too when the service recorded it all the same, as that
|
|
230
|
+
* fragment's newest run (its newestRun.md N2).
|
|
231
|
+
*/
|
|
206
232
|
fragmentOf?: {
|
|
207
233
|
runId: string;
|
|
208
234
|
};
|
|
235
|
+
/** Set when the service recorded the run as a repeat: the run it repeats. */
|
|
236
|
+
repeatOf?: {
|
|
237
|
+
runId: string;
|
|
238
|
+
};
|
|
209
239
|
}
|
|
210
240
|
export interface RunCreationAware {
|
|
211
241
|
runCreated(runId: string): Promise<RunCreation>;
|
|
@@ -475,6 +475,9 @@ export class RemoteRecorder {
|
|
|
475
475
|
// A fragment (fallbk.md D7). Omitted, not null, so an older service
|
|
476
476
|
// that does not know the field sees the body it always saw.
|
|
477
477
|
...(options?.fallbackOf ? { fallbackOf: options.fallbackOf } : {}),
|
|
478
|
+
// A repeat (the service's newestRun.md). Sent only after a match
|
|
479
|
+
// that offered it, so a service that would not know it never sees it.
|
|
480
|
+
...(options?.repeatOf ? { repeatOf: options.repeatOf } : {}),
|
|
478
481
|
// A chain with calls is inlined for this runner rather than stopped in
|
|
479
482
|
// front of its first call (segmented.md R-CALL-28).
|
|
480
483
|
supportsCalls: true,
|
|
@@ -496,7 +499,11 @@ export class RemoteRecorder {
|
|
|
496
499
|
why: "the create's first attempt failed, so the turn went ahead as a fresh one; the service kept its own run, and this one's steps are not sent",
|
|
497
500
|
});
|
|
498
501
|
}
|
|
499
|
-
entry.settle(b.matched ?? null, {
|
|
502
|
+
entry.settle(b.matched ?? null, {
|
|
503
|
+
created: b.id !== null,
|
|
504
|
+
fragmentOf: b.fragmentOf,
|
|
505
|
+
...(b.repeatOf ? { repeatOf: b.repeatOf } : {}),
|
|
506
|
+
});
|
|
500
507
|
}
|
|
501
508
|
catch (err) {
|
|
502
509
|
entry.settle(null, { created: true }); // fail-safe: on a create error, record normally
|
|
@@ -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",
|