@yaag/runtime 0.1.4 → 0.2.0
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/package.json +1 -1
- package/src/agent.ts +11 -3
- package/src/ask-contract-identity.ts +23 -5
- package/src/ask-exchange-events.ts +14 -1
- package/src/ask-exchange-options.ts +7 -2
- package/src/ask-exchange.ts +198 -26
- package/src/ask-hash.ts +7 -3
- package/src/ask-limit.ts +27 -11
- package/src/ask-output-steering.ts +1 -3
- package/src/ask-output.ts +20 -4
- package/src/ask-turn.ts +11 -0
- package/src/cassette-loader.ts +24 -0
- package/src/cassette-replay.ts +76 -7
- package/src/cassette-schema.ts +17 -2
- package/src/cassette.ts +10 -9
- package/src/checkpoint-flush.ts +101 -0
- package/src/connection.ts +20 -0
- package/src/define-agent.ts +11 -5
- package/src/errors.ts +11 -2
- package/src/events.ts +29 -3
- package/src/fake-transport.ts +55 -0
- package/src/frame-queue.ts +5 -0
- package/src/index.ts +11 -1
- package/src/model-resolution.ts +119 -0
- package/src/model-suffix.ts +24 -0
- package/src/pi-state.ts +63 -8
- package/src/recording-transport.ts +8 -8
- package/src/replay-divergence.ts +1 -0
- package/src/replay-transport.ts +4 -0
- package/src/report-result-extension.ts +128 -0
- package/src/report-result-output.ts +73 -0
- package/src/report-result-steering.ts +83 -0
- package/src/report-result.ts +122 -0
- package/src/resume-transport.ts +26 -8
- package/src/run-checkpoint.ts +93 -41
- package/src/run.ts +86 -32
- package/src/spawn.ts +29 -4
- package/src/stall-watchdog.ts +193 -0
- package/src/summary-agent.ts +11 -2
- package/src/summary.ts +23 -2
- package/src/thinking-level.ts +32 -0
- package/src/transport.ts +43 -8
- package/src/types.ts +50 -14
- package/src/validation-errors.ts +10 -4
package/src/run.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
1
2
|
import type { TSchema } from "typebox";
|
|
2
3
|
import type { Agent } from "./agent.ts";
|
|
3
4
|
import { validateArgs } from "./args-validation.ts";
|
|
4
|
-
import { type Cassette, CassetteCollector } from "./cassette.ts";
|
|
5
|
-
import { loadCassette } from "./cassette-loader.ts";
|
|
6
|
-
import {
|
|
5
|
+
import { type Cassette, CassetteCollector, type CassetteSink } from "./cassette.ts";
|
|
6
|
+
import { assertReplayable, interruptedResumeWarning, loadCassette } from "./cassette-loader.ts";
|
|
7
|
+
import {
|
|
8
|
+
checkpointFileName,
|
|
9
|
+
cleanStaleCheckpointTemp,
|
|
10
|
+
resolveCheckpointDirectory,
|
|
11
|
+
} from "./checkpoint-dir.ts";
|
|
12
|
+
import { createCheckpointFlusher, flushingSink } from "./checkpoint-flush.ts";
|
|
7
13
|
import { type OrchestrationProgram, programDefinition } from "./define-run.ts";
|
|
8
14
|
import { isYaagError, YaagError } from "./errors.ts";
|
|
9
15
|
import type { EventSink, LifecycleEvent, RunOutcome, StampedEventSink } from "./events.ts";
|
|
@@ -11,7 +17,12 @@ import { liveTransport } from "./live-transport.ts";
|
|
|
11
17
|
import { recordingTransport } from "./recording-transport.ts";
|
|
12
18
|
import { replayTransport } from "./replay-transport.ts";
|
|
13
19
|
import { resumeTransport } from "./resume-transport.ts";
|
|
14
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
publishRunCheckpoint,
|
|
22
|
+
type RunCheckpointResult,
|
|
23
|
+
resolveRunIdentity,
|
|
24
|
+
writeRecordingDiagnostic,
|
|
25
|
+
} from "./run-checkpoint.ts";
|
|
15
26
|
import type { RunContext } from "./run-context.ts";
|
|
16
27
|
import { liveSkillProbe, type SkillProbeFactory } from "./skill-probe.ts";
|
|
17
28
|
import { skillRestrictionTransport } from "./skill-restriction-transport.ts";
|
|
@@ -72,26 +83,28 @@ export async function executeRun<Args, Result>(
|
|
|
72
83
|
} catch (error) {
|
|
73
84
|
writeRecordingDiagnostic(error);
|
|
74
85
|
}
|
|
75
|
-
const replay =
|
|
76
|
-
const resume =
|
|
86
|
+
const replay = await loadReplay(options.replay);
|
|
87
|
+
const resume = await loadResume(options.resume);
|
|
77
88
|
const definition = programDefinition(program);
|
|
78
|
-
// Collection is universal so every Run can checkpoint
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
89
|
+
// Collection is universal so every Run can checkpoint, and every Run flushes
|
|
90
|
+
// that collection at each Ask boundary (ADR-0031). Only a stopped Run and a
|
|
91
|
+
// --record Run keep the artifact once the Run settles (ADR-0021). That memory
|
|
92
|
+
// is potentially unbounded over arbitrarily many Asks — yaag has no Run token
|
|
93
|
+
// budget (ADR-0012/0017), and these ADRs do not pretend one exists.
|
|
82
94
|
const collector = new CassetteCollector();
|
|
83
95
|
const args: unknown = Object.hasOwn(options, "args") ? options.args : {};
|
|
96
|
+
// Fixed here, so every Ask-boundary flush and the settlement write one file,
|
|
97
|
+
// and so run_start can name it (ADR-0031).
|
|
98
|
+
const destination = options.record ?? join(checkpointDir, checkpointFileName());
|
|
99
|
+
const identity = await resolveRunIdentity(options.programFile, args);
|
|
100
|
+
const checkpoint = { collector, record: options.record, destination, identity };
|
|
84
101
|
try {
|
|
85
102
|
assertArgs<Args>(definition.args, args);
|
|
86
103
|
} catch (error) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
checkpointDir,
|
|
92
|
-
programFile: options.programFile,
|
|
93
|
-
args,
|
|
94
|
-
});
|
|
104
|
+
// The result is discarded on purpose: this rejection precedes run_start, and
|
|
105
|
+
// no event may precede an ARGS_INVALID rejection, so a lost Checkpoint here
|
|
106
|
+
// stays a stderr diagnostic (ticket 04).
|
|
107
|
+
await publishRunCheckpoint({ outcome: "failed", ...checkpoint });
|
|
95
108
|
throw error;
|
|
96
109
|
}
|
|
97
110
|
|
|
@@ -105,7 +118,13 @@ export async function executeRun<Args, Result>(
|
|
|
105
118
|
summary = applyEvent(summary, event);
|
|
106
119
|
sink(event);
|
|
107
120
|
};
|
|
108
|
-
const
|
|
121
|
+
const flusher = createCheckpointFlusher(checkpoint);
|
|
122
|
+
const factory = selectFactory({
|
|
123
|
+
options,
|
|
124
|
+
collector: flushingSink(collector, flusher),
|
|
125
|
+
replay,
|
|
126
|
+
resume,
|
|
127
|
+
});
|
|
109
128
|
const agents: Agent[] = [];
|
|
110
129
|
const startedAt = Date.now();
|
|
111
130
|
const spawnGate = makeSpawn({
|
|
@@ -117,7 +136,7 @@ export async function executeRun<Args, Result>(
|
|
|
117
136
|
});
|
|
118
137
|
|
|
119
138
|
const ctx: RunContext<Args> = { args, spawn: spawnGate.spawn };
|
|
120
|
-
emit({ type: "run_start", program: definition.name ?? "program" });
|
|
139
|
+
emit({ type: "run_start", program: definition.name ?? "program", artifact: destination });
|
|
121
140
|
|
|
122
141
|
// The program's settlement is held rather than rethrown from a `finally`, so
|
|
123
142
|
// publication can displace it when the checkpoint fails (ADR-0021).
|
|
@@ -133,27 +152,40 @@ export async function executeRun<Args, Result>(
|
|
|
133
152
|
}
|
|
134
153
|
spawnGate.close();
|
|
135
154
|
await reapAll({ agents, emit });
|
|
155
|
+
// No flush may land after the settled artifact, or an acknowledged Run would
|
|
156
|
+
// be overwritten by an `interrupted` one.
|
|
157
|
+
await flusher.quiesce();
|
|
136
158
|
// run_end is the acknowledgement: nothing may report an outcome before the
|
|
137
159
|
// checkpoint's destination-directory fsync has landed (ADR-0021).
|
|
138
|
-
const
|
|
139
|
-
outcome,
|
|
140
|
-
collector,
|
|
141
|
-
record: options.record,
|
|
142
|
-
checkpointDir,
|
|
143
|
-
programFile: options.programFile,
|
|
144
|
-
args,
|
|
145
|
-
});
|
|
160
|
+
const publication = await publishRunCheckpoint({ outcome, ...checkpoint });
|
|
146
161
|
emitRunEnd({
|
|
147
162
|
emit,
|
|
148
|
-
outcome: failure === null ? outcome : "failed",
|
|
149
163
|
durationMs: Date.now() - startedAt,
|
|
150
164
|
summary,
|
|
165
|
+
...endingFrom(publication, outcome),
|
|
151
166
|
});
|
|
152
|
-
if (
|
|
167
|
+
if (publication.kind === "displaced") throw publication.error;
|
|
153
168
|
if (!settled.ok) throw settled.error;
|
|
154
169
|
return settled.value;
|
|
155
170
|
}
|
|
156
171
|
|
|
172
|
+
/** Loads a strict-replay source, which must be a settled Cassette (ADR-0031). */
|
|
173
|
+
async function loadReplay(path: string | undefined): Promise<Cassette | null> {
|
|
174
|
+
if (path === undefined) return null;
|
|
175
|
+
const cassette = await loadCassette(path);
|
|
176
|
+
assertReplayable(cassette, path);
|
|
177
|
+
return cassette;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Loads a resume source, warning once when it came from a hard death (ADR-0031). */
|
|
181
|
+
async function loadResume(path: string | undefined): Promise<Cassette | null> {
|
|
182
|
+
if (path === undefined) return null;
|
|
183
|
+
const cassette = await loadCassette(path);
|
|
184
|
+
const warning = interruptedResumeWarning(cassette, path);
|
|
185
|
+
if (warning !== null) writeRecordingDiagnostic(warning);
|
|
186
|
+
return cassette;
|
|
187
|
+
}
|
|
188
|
+
|
|
157
189
|
/** What the Orchestration Program body produced, before the Run acknowledges it. */
|
|
158
190
|
type Settlement<Result> =
|
|
159
191
|
| { readonly ok: true; readonly value: Result }
|
|
@@ -182,7 +214,8 @@ function assertArgs<Args>(schema: TSchema | undefined, value: unknown): asserts
|
|
|
182
214
|
*/
|
|
183
215
|
export interface FactorySelection {
|
|
184
216
|
readonly options: RunOptions;
|
|
185
|
-
|
|
217
|
+
/** Receives every frame; wrapped so Ask boundaries flush the Checkpoint (ADR-0031). */
|
|
218
|
+
readonly collector: CassetteSink;
|
|
186
219
|
readonly replay: Cassette | null;
|
|
187
220
|
readonly resume: Cassette | null;
|
|
188
221
|
}
|
|
@@ -257,12 +290,33 @@ interface RunEndOptions {
|
|
|
257
290
|
readonly durationMs: number;
|
|
258
291
|
/** Settled fold: every Agent is reaped and the checkpoint is published by now. */
|
|
259
292
|
readonly summary: RunSummary;
|
|
293
|
+
/** Publication error text when a requested Checkpoint was lost (ticket 04). */
|
|
294
|
+
readonly checkpointLost?: string;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Folds one publication result into how the Run ends. A displaced publication
|
|
299
|
+
* takes the outcome; a lost Checkpoint keeps it and reports the loss instead.
|
|
300
|
+
*/
|
|
301
|
+
function endingFrom(
|
|
302
|
+
publication: RunCheckpointResult,
|
|
303
|
+
outcome: RunOutcome,
|
|
304
|
+
): { readonly outcome: RunOutcome; readonly checkpointLost?: string } {
|
|
305
|
+
switch (publication.kind) {
|
|
306
|
+
case "published":
|
|
307
|
+
return { outcome };
|
|
308
|
+
case "lost":
|
|
309
|
+
return { outcome, checkpointLost: publication.message };
|
|
310
|
+
case "displaced":
|
|
311
|
+
return { outcome: "failed" };
|
|
312
|
+
}
|
|
260
313
|
}
|
|
261
314
|
|
|
262
315
|
/** Emits the Run's acknowledgement, the last Lifecycle Event of every Run. */
|
|
263
|
-
function emitRunEnd({ emit, outcome, durationMs, summary }: RunEndOptions): void {
|
|
316
|
+
function emitRunEnd({ emit, outcome, durationMs, summary, checkpointLost }: RunEndOptions): void {
|
|
264
317
|
emit({
|
|
265
318
|
type: "run_end",
|
|
319
|
+
...(checkpointLost === undefined ? {} : { checkpointLost }),
|
|
266
320
|
ok: outcome === "completed",
|
|
267
321
|
outcome,
|
|
268
322
|
durationMs,
|
package/src/spawn.ts
CHANGED
|
@@ -5,9 +5,16 @@ import { type AgentDefinition, agentDefinitionConfig, isAgentDefinition } from "
|
|
|
5
5
|
import { YaagError } from "./errors.ts";
|
|
6
6
|
import type { EventSink } from "./events.ts";
|
|
7
7
|
import { resolveExtensionPaths } from "./extension-paths.ts";
|
|
8
|
+
import { normalizeModelResolution } from "./model-resolution.ts";
|
|
8
9
|
import type { RunContext } from "./run-context.ts";
|
|
9
10
|
import type { AgentTransport, TransportFactory, TransportStartup } from "./transport.ts";
|
|
10
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
AskOptions,
|
|
13
|
+
Handle,
|
|
14
|
+
ResolvedSpawnOptions,
|
|
15
|
+
SpawnOptions,
|
|
16
|
+
SpawnOverrides,
|
|
17
|
+
} from "./types.ts";
|
|
11
18
|
|
|
12
19
|
/** Dependencies for one Run's Agent-spawn gate. */
|
|
13
20
|
export interface SpawnDependencies {
|
|
@@ -41,12 +48,13 @@ export function makeSpawn(deps: SpawnDependencies): SpawnGate {
|
|
|
41
48
|
const request = resolveRequest(definitionOrOptions, overrides);
|
|
42
49
|
const cwd = resolve(request.spawnOptions.cwd ?? process.cwd());
|
|
43
50
|
const name = uniqueAgentName(request.spawnOptions.name, deps.agents.length, taken);
|
|
51
|
+
const spawnOptions = resolveSpawnSelection(request.spawnOptions, name);
|
|
44
52
|
try {
|
|
45
53
|
const opened = await openTransport({
|
|
46
54
|
factory: deps.factory,
|
|
47
55
|
name,
|
|
48
56
|
cwd,
|
|
49
|
-
spawnOptions
|
|
57
|
+
spawnOptions,
|
|
50
58
|
sessionDir: deps.sessionDir,
|
|
51
59
|
programFile: deps.programFile,
|
|
52
60
|
});
|
|
@@ -59,7 +67,7 @@ export function makeSpawn(deps: SpawnDependencies): SpawnGate {
|
|
|
59
67
|
branch,
|
|
60
68
|
transport: opened.transport,
|
|
61
69
|
emit: deps.emit,
|
|
62
|
-
spawnOptions
|
|
70
|
+
spawnOptions,
|
|
63
71
|
...(request.askDefaults === undefined ? {} : { askDefaults: request.askDefaults }),
|
|
64
72
|
...(request.definitionName === undefined ? {} : { definitionName: request.definitionName }),
|
|
65
73
|
});
|
|
@@ -92,6 +100,23 @@ export function makeSpawn(deps: SpawnDependencies): SpawnGate {
|
|
|
92
100
|
};
|
|
93
101
|
}
|
|
94
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Attempt-0 shim for Model Resolution: one selection, no retry loop.
|
|
105
|
+
* Ticket 02 replaces this with the fallback loop and MODEL_RESOLUTION_FAILED.
|
|
106
|
+
*/
|
|
107
|
+
function resolveSpawnSelection(options: SpawnOptions, name: string): ResolvedSpawnOptions {
|
|
108
|
+
const selection = normalizeModelResolution(options).resolve([]);
|
|
109
|
+
if (selection === undefined) {
|
|
110
|
+
throw new YaagError("SPAWN_FAILED", `agent "${name}": no model candidate to try`, name);
|
|
111
|
+
}
|
|
112
|
+
const { model: _model, thinking: _thinking, ...rest } = options;
|
|
113
|
+
return {
|
|
114
|
+
...rest,
|
|
115
|
+
...(selection.model === undefined ? {} : { model: selection.model }),
|
|
116
|
+
...(selection.thinking === undefined ? {} : { thinking: selection.thinking }),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
95
120
|
type MutableSpawnOverrides = { -readonly [Key in keyof SpawnOverrides]: SpawnOverrides[Key] };
|
|
96
121
|
|
|
97
122
|
interface SpawnRequest {
|
|
@@ -180,7 +205,7 @@ interface OpenTransportOptions {
|
|
|
180
205
|
readonly factory: TransportFactory;
|
|
181
206
|
readonly name: string;
|
|
182
207
|
readonly cwd: string;
|
|
183
|
-
readonly spawnOptions:
|
|
208
|
+
readonly spawnOptions: ResolvedSpawnOptions;
|
|
184
209
|
readonly sessionDir: string | undefined;
|
|
185
210
|
readonly programFile: string | undefined;
|
|
186
211
|
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { CommandResponse } from "./connection.ts";
|
|
2
|
+
import type { AskStalledOutcome } from "./errors.ts";
|
|
3
|
+
import { readAgentProgress } from "./pi-state.ts";
|
|
4
|
+
import type { Frame } from "./transport.ts";
|
|
5
|
+
|
|
6
|
+
/** Silence allowed on a live Ask before the Stall Watchdog probes the Agent. */
|
|
7
|
+
export const DEFAULT_STALL_MS = 600_000;
|
|
8
|
+
/** Time allowed for the `get_state` probe to answer before yaag kills the Agent. */
|
|
9
|
+
export const STALL_PROBE_SETTLE_MS = 45_000;
|
|
10
|
+
|
|
11
|
+
/** Why an Ask settled: normal frames, watchdog recovery, or a stall rejection. */
|
|
12
|
+
export type SettlementCause = "normal" | "recovered" | "stalled";
|
|
13
|
+
|
|
14
|
+
/** Rejection carried by {@link StallWatchdog.failed} for both stall outcomes. */
|
|
15
|
+
export class StallSignal extends Error {
|
|
16
|
+
readonly outcome: AskStalledOutcome;
|
|
17
|
+
|
|
18
|
+
constructor(outcome: AskStalledOutcome) {
|
|
19
|
+
super(
|
|
20
|
+
`no frame for ${outcome.idleMs}ms and the state probe ${
|
|
21
|
+
outcome.destructive ? "did not answer" : "reported work in progress"
|
|
22
|
+
}`,
|
|
23
|
+
);
|
|
24
|
+
this.name = "StallSignal";
|
|
25
|
+
this.outcome = outcome;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface StallWatchdogOptions {
|
|
30
|
+
/** Silence, in milliseconds, that arms the probe. */
|
|
31
|
+
readonly stallMs: number;
|
|
32
|
+
/** Bounded wait for the probe response; defaults to 45s. */
|
|
33
|
+
readonly probeSettleMs?: number;
|
|
34
|
+
/** Sends one command to the Agent and resolves with its response. */
|
|
35
|
+
readonly command: (frame: Frame) => Promise<CommandResponse>;
|
|
36
|
+
/** True when this Ask already observed a terminal assistant `message_end`. */
|
|
37
|
+
readonly terminal: () => boolean;
|
|
38
|
+
/** Settles the Ask from the observed terminal state. */
|
|
39
|
+
readonly recover: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Default liveness guard for every live Ask (ADR-0029).
|
|
44
|
+
*
|
|
45
|
+
* It measures silence, not wall time: any Agent frame resets the timer, so a
|
|
46
|
+
* legitimately long Ask is never punished. On expiry it sends one `get_state`
|
|
47
|
+
* probe and takes one of three exits:
|
|
48
|
+
* - the probe answers, the Agent is not working, and this Ask already saw a
|
|
49
|
+
* terminal assistant `message_end` — the Ask settles as `recovered`;
|
|
50
|
+
* - the probe answers and the Agent is still working — the Ask fails with a
|
|
51
|
+
* recoverable `ASK_STALLED` and the Agent stays alive;
|
|
52
|
+
* - the probe stays silent — the Ask fails with a destructive `ASK_STALLED`
|
|
53
|
+
* and the Agent is killed.
|
|
54
|
+
*/
|
|
55
|
+
export class StallWatchdog {
|
|
56
|
+
readonly #stallMs: number;
|
|
57
|
+
readonly #probeSettleMs: number;
|
|
58
|
+
readonly #command: (frame: Frame) => Promise<CommandResponse>;
|
|
59
|
+
readonly #terminal: () => boolean;
|
|
60
|
+
readonly #recover: () => void;
|
|
61
|
+
readonly #failure: Promise<never>;
|
|
62
|
+
#reject: (reason: unknown) => void = () => {};
|
|
63
|
+
#stallTimer: ReturnType<typeof setTimeout> | undefined;
|
|
64
|
+
#probeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
65
|
+
#tripped = false;
|
|
66
|
+
#done = false;
|
|
67
|
+
#cause: SettlementCause = "normal";
|
|
68
|
+
#result: AskStalledOutcome | null = null;
|
|
69
|
+
|
|
70
|
+
constructor(options: StallWatchdogOptions) {
|
|
71
|
+
this.#stallMs = options.stallMs;
|
|
72
|
+
this.#probeSettleMs = options.probeSettleMs ?? STALL_PROBE_SETTLE_MS;
|
|
73
|
+
this.#command = options.command;
|
|
74
|
+
this.#terminal = options.terminal;
|
|
75
|
+
this.#recover = options.recover;
|
|
76
|
+
this.#failure = new Promise<never>((_resolve, reject) => {
|
|
77
|
+
this.#reject = reject;
|
|
78
|
+
});
|
|
79
|
+
// The race may settle before the Ask starts awaiting it.
|
|
80
|
+
void this.#failure.catch(() => {});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Arms the silence timer immediately before the prompt command is sent. */
|
|
84
|
+
start(): void {
|
|
85
|
+
this.#arm();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Resets the silence timer. Frames that arrive after the probe are ignored. */
|
|
89
|
+
observe(_frame: Frame): void {
|
|
90
|
+
if (this.#done || this.#tripped) return;
|
|
91
|
+
this.#arm();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Disarms the watchdog for a settled turn, so the commands that follow
|
|
96
|
+
* settlement — `get_last_assistant_text` and output steering — can never be
|
|
97
|
+
* mistaken for Agent silence. Idempotent.
|
|
98
|
+
*/
|
|
99
|
+
settled(): void {
|
|
100
|
+
if (this.#tripped) return;
|
|
101
|
+
this.cleanup();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Re-arms the timer for a further effort in the same Ask. Inert after a trip. */
|
|
105
|
+
rearm(): void {
|
|
106
|
+
if (this.#done || this.#tripped) return;
|
|
107
|
+
this.#arm();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Cancels every timer once the Ask leaves. Idempotent. */
|
|
111
|
+
cleanup(): void {
|
|
112
|
+
clearTimeout(this.#stallTimer);
|
|
113
|
+
clearTimeout(this.#probeTimer);
|
|
114
|
+
this.#stallTimer = undefined;
|
|
115
|
+
this.#probeTimer = undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** How this Ask settled, as reported in its `ask_end` Lifecycle Event. */
|
|
119
|
+
get cause(): SettlementCause {
|
|
120
|
+
return this.#cause;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The stalled outcome, once the probe protocol resolved either way. */
|
|
124
|
+
get result(): AskStalledOutcome | null {
|
|
125
|
+
return this.#result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Rejects with a {@link StallSignal} on both stall exits. */
|
|
129
|
+
get failed(): Promise<never> {
|
|
130
|
+
return this.#failure;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#arm(): void {
|
|
134
|
+
clearTimeout(this.#stallTimer);
|
|
135
|
+
this.#stallTimer = setTimeout(() => this.#trip(), this.#stallMs);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#trip(): void {
|
|
139
|
+
if (this.#done || this.#tripped) return;
|
|
140
|
+
this.#tripped = true;
|
|
141
|
+
clearTimeout(this.#stallTimer);
|
|
142
|
+
this.#stallTimer = undefined;
|
|
143
|
+
this.#probeTimer = setTimeout(() => this.#fail(true), this.#probeSettleMs);
|
|
144
|
+
void this.#probe();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async #probe(): Promise<void> {
|
|
148
|
+
let response: CommandResponse;
|
|
149
|
+
try {
|
|
150
|
+
response = await this.#command({ type: "get_state" });
|
|
151
|
+
} catch {
|
|
152
|
+
// A dead Agent or a refused probe is silence: the kill timer decides.
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (this.#done) return;
|
|
156
|
+
const progress = readAgentProgress(response.data);
|
|
157
|
+
// Unreadable state is treated as work in progress: yaag never invents a
|
|
158
|
+
// settlement it did not observe.
|
|
159
|
+
if (progress !== null && !progress.working && this.#terminal()) {
|
|
160
|
+
this.#done = true;
|
|
161
|
+
this.#cause = "recovered";
|
|
162
|
+
this.cleanup();
|
|
163
|
+
this.#recover();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.#fail(false, progress ?? undefined);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#fail(destructive: boolean, state?: AskStalledOutcome["state"]): void {
|
|
170
|
+
if (this.#done) return;
|
|
171
|
+
this.#done = true;
|
|
172
|
+
this.#cause = "stalled";
|
|
173
|
+
this.#result = {
|
|
174
|
+
idleMs: this.#stallMs,
|
|
175
|
+
destructive,
|
|
176
|
+
...(state === undefined ? {} : { state }),
|
|
177
|
+
};
|
|
178
|
+
this.cleanup();
|
|
179
|
+
this.#reject(new StallSignal(this.#result));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Resolves the effective stall budget.
|
|
185
|
+
*
|
|
186
|
+
* `false` and any value that is not above zero disable the watchdog for one
|
|
187
|
+
* Ask. An absent value takes the 10-minute default.
|
|
188
|
+
*/
|
|
189
|
+
export function stallBudgetMs(stallMs: number | false | undefined): number | null {
|
|
190
|
+
if (stallMs === false) return null;
|
|
191
|
+
if (stallMs === undefined) return DEFAULT_STALL_MS;
|
|
192
|
+
return stallMs > 0 ? stallMs : null;
|
|
193
|
+
}
|
package/src/summary-agent.ts
CHANGED
|
@@ -184,11 +184,19 @@ export function setActivity(
|
|
|
184
184
|
* Folds an Ask settlement into the idle arm, retaining the settled Ask identity.
|
|
185
185
|
* Older stamped events are ignored and legacy unstamped events use stream order.
|
|
186
186
|
*/
|
|
187
|
+
/** One Ask settlement as the fold sees it. */
|
|
188
|
+
export interface AskSettlement {
|
|
189
|
+
readonly index: number;
|
|
190
|
+
/** A recovered settlement makes this Agent's accounting a floor, not a total. */
|
|
191
|
+
readonly recovered: boolean;
|
|
192
|
+
}
|
|
193
|
+
|
|
187
194
|
export function endAsk(
|
|
188
195
|
current: AgentRecord | undefined,
|
|
189
|
-
|
|
196
|
+
settlement: AskSettlement,
|
|
190
197
|
at: number | null,
|
|
191
198
|
): AgentRecord {
|
|
199
|
+
const { index, recovered } = settlement;
|
|
192
200
|
const agent = current ?? placeholderAgent();
|
|
193
201
|
if (
|
|
194
202
|
!canChangeAskState(agent, at) ||
|
|
@@ -202,6 +210,7 @@ export function endAsk(
|
|
|
202
210
|
state: "idle",
|
|
203
211
|
askIndex: index,
|
|
204
212
|
activity: null,
|
|
213
|
+
incomplete: agent.incomplete || recovered,
|
|
205
214
|
stateChangedAt: at,
|
|
206
215
|
};
|
|
207
216
|
}
|
|
@@ -268,7 +277,7 @@ export function totalsFromAgents(agents: Readonly<Record<string, AgentRecord>>):
|
|
|
268
277
|
? null
|
|
269
278
|
: records.reduce(addTokens, zeroTokens()),
|
|
270
279
|
incomplete: records.some(
|
|
271
|
-
(agent) => agent.state === "exited" &&
|
|
280
|
+
(agent) => agent.incomplete || (agent.state === "exited" && agent.cost === null),
|
|
272
281
|
),
|
|
273
282
|
};
|
|
274
283
|
}
|
package/src/summary.ts
CHANGED
|
@@ -31,6 +31,11 @@ export type RunState = "running" | "ended";
|
|
|
31
31
|
interface RunSummaryBase {
|
|
32
32
|
readonly program: string;
|
|
33
33
|
readonly startedAt: number | null;
|
|
34
|
+
/**
|
|
35
|
+
* Path of the Run's Checkpoint, as `run_start` named it (ADR-0031). It is
|
|
36
|
+
* null for a Run whose CLI is older than that event field.
|
|
37
|
+
*/
|
|
38
|
+
readonly artifact: string | null;
|
|
34
39
|
readonly agents: Readonly<Record<string, AgentInfo>>;
|
|
35
40
|
readonly asksStarted: number;
|
|
36
41
|
readonly asksSettled: number;
|
|
@@ -55,6 +60,11 @@ export interface EndedRunSummary extends RunSummaryBase {
|
|
|
55
60
|
readonly ok: boolean;
|
|
56
61
|
/** The `run_end.at` event fact used to reject strictly older stamped endings. */
|
|
57
62
|
readonly endedAt: number | null;
|
|
63
|
+
/**
|
|
64
|
+
* The Run asked for a Checkpoint with `--record`, failed, and lost the
|
|
65
|
+
* artifact too. Carries the publication error text (ticket 04).
|
|
66
|
+
*/
|
|
67
|
+
readonly checkpointLost?: string;
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
/**
|
|
@@ -82,6 +92,7 @@ export function initialSummary(): RunningRunSummary {
|
|
|
82
92
|
runState: "running",
|
|
83
93
|
outcome: null,
|
|
84
94
|
startedAt: null,
|
|
95
|
+
artifact: null,
|
|
85
96
|
agents: {},
|
|
86
97
|
asksStarted: 0,
|
|
87
98
|
asksSettled: 0,
|
|
@@ -111,7 +122,12 @@ export function applyEvent(
|
|
|
111
122
|
const at = "at" in event ? event.at : null;
|
|
112
123
|
switch (event.type) {
|
|
113
124
|
case "run_start":
|
|
114
|
-
return {
|
|
125
|
+
return {
|
|
126
|
+
...summary,
|
|
127
|
+
program: event.program,
|
|
128
|
+
startedAt: at,
|
|
129
|
+
artifact: event.artifact ?? null,
|
|
130
|
+
};
|
|
115
131
|
case "agent_spawn":
|
|
116
132
|
return withAgent(summary, event.agent, spawnAgent(summary.agents[event.agent], event, at));
|
|
117
133
|
case "ask_start":
|
|
@@ -158,6 +174,7 @@ function endRun(
|
|
|
158
174
|
endedAt: at,
|
|
159
175
|
durationMs: event.durationMs,
|
|
160
176
|
worstFrameGapMs: event.worstFrameGapMs,
|
|
177
|
+
...(event.checkpointLost === undefined ? {} : { checkpointLost: event.checkpointLost }),
|
|
161
178
|
};
|
|
162
179
|
}
|
|
163
180
|
|
|
@@ -195,7 +212,11 @@ function settleAsk(
|
|
|
195
212
|
const next = withAgent(
|
|
196
213
|
{ ...summary, asksSettled: summary.asksSettled + 1 },
|
|
197
214
|
event.agent,
|
|
198
|
-
endAsk(
|
|
215
|
+
endAsk(
|
|
216
|
+
summary.agents[event.agent],
|
|
217
|
+
{ index: event.index, recovered: event.cause === "recovered" },
|
|
218
|
+
at,
|
|
219
|
+
),
|
|
199
220
|
);
|
|
200
221
|
return {
|
|
201
222
|
...next,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi's supported thinking levels.
|
|
3
|
+
*
|
|
4
|
+
* Lives in its own leaf module so both `types.ts` and the Model Resolution
|
|
5
|
+
* modules can depend on it without forming an import cycle.
|
|
6
|
+
*/
|
|
7
|
+
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The closed level list, authored as a total record so that adding a level to
|
|
11
|
+
* `ThinkingLevel` breaks this build instead of silently omitting the level.
|
|
12
|
+
* Authority: pi `docs/rpc.md` "Levels:".
|
|
13
|
+
*/
|
|
14
|
+
const levels: Record<ThinkingLevel, true> = {
|
|
15
|
+
off: true,
|
|
16
|
+
minimal: true,
|
|
17
|
+
low: true,
|
|
18
|
+
medium: true,
|
|
19
|
+
high: true,
|
|
20
|
+
xhigh: true,
|
|
21
|
+
max: true,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Every thinking level pi accepts, in documented order. */
|
|
25
|
+
export const THINKING_LEVELS: readonly ThinkingLevel[] = Object.freeze(
|
|
26
|
+
Object.keys(levels) as ThinkingLevel[],
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
/** True only for one of pi's thinking levels; case-sensitive and total. */
|
|
30
|
+
export function isThinkingLevel(value: unknown): value is ThinkingLevel {
|
|
31
|
+
return typeof value === "string" && Object.hasOwn(levels, value);
|
|
32
|
+
}
|