@yaag/runtime 0.6.2 → 0.8.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/agent.ts +38 -2
- package/src/agent/define-agent.ts +20 -3
- package/src/agent/spawn-extensions.ts +97 -0
- package/src/agent/spawn-request.ts +70 -0
- package/src/agent/spawn.ts +102 -62
- package/src/ask/ask-exchange-events.ts +10 -1
- package/src/ask/ask-exchange-options.ts +13 -0
- package/src/ask/ask-exchange.ts +77 -9
- package/src/ask/index.ts +1 -0
- package/src/cassette/cassette-publish.ts +5 -1
- package/src/cassette/cassette-replay.ts +23 -4
- package/src/cassette/cassette-schema.ts +1 -0
- package/src/cassette/cassette.ts +8 -0
- package/src/cassette/recording-transport.ts +7 -0
- package/src/cassette/replay-divergence.ts +86 -20
- package/src/cassette/replay-transport.ts +8 -1
- package/src/cassette/resume-transport.ts +14 -1
- package/src/config/config-file.ts +67 -0
- package/src/config/config-issues.ts +31 -0
- package/src/config/config-paths.ts +38 -0
- package/src/config/config-schema.ts +25 -0
- package/src/config/effective-config.ts +116 -0
- package/src/config/index.ts +22 -0
- package/src/errors.ts +56 -2
- package/src/events.ts +19 -0
- package/src/extension/extension-paths.ts +15 -6
- package/src/index.ts +14 -0
- package/src/model/index.ts +18 -0
- package/src/model/model-error-history.ts +36 -0
- package/src/model/model-failure.ts +78 -0
- package/src/model/model-fallback.ts +15 -0
- package/src/model/model-match.ts +99 -0
- package/src/model/model-resolution.ts +44 -6
- package/src/model/model-swap.ts +81 -0
- package/src/model/recorded-resolution.ts +61 -0
- package/src/model/resolution-loop.ts +115 -0
- package/src/run/run.ts +8 -0
- package/src/summary/index.ts +1 -0
- package/src/summary/summary-agent.ts +9 -1
- package/src/summary/summary-fallbacks.ts +50 -0
- package/src/summary/summary.ts +12 -0
- package/src/transport/fake-transport.ts +57 -1
- package/src/transport/index.ts +4 -0
- package/src/transport/live-transport.ts +37 -4
- package/src/transport/stderr-tail.ts +32 -0
- package/src/transport/transport.ts +16 -0
- package/src/types.ts +33 -5
- package/src/wire-constants.ts +3 -0
|
@@ -3,6 +3,7 @@ import { FrameQueue } from "./frame-queue.ts";
|
|
|
3
3
|
import { decodeFrames } from "./jsonl.ts";
|
|
4
4
|
import { piCommand, readModel, readSessionFile, readStats } from "./pi-state.ts";
|
|
5
5
|
import { reap } from "./reap.ts";
|
|
6
|
+
import { appendStderrTail, stderrDiagnostic } from "./stderr-tail.ts";
|
|
6
7
|
import {
|
|
7
8
|
createToolProbe,
|
|
8
9
|
TOOL_PROBE_EXTENSION_PATH,
|
|
@@ -25,6 +26,8 @@ import type {
|
|
|
25
26
|
const READY_TIMEOUT_MS = 30_000;
|
|
26
27
|
/** Cost is a local read answering in ~0ms, but the kill must never block on it. */
|
|
27
28
|
const STATS_TIMEOUT_MS = 2_000;
|
|
29
|
+
/** How long an exit message waits for stderr, which grandchildren may hold open. */
|
|
30
|
+
const STDERR_DRAIN_MS = 100;
|
|
28
31
|
|
|
29
32
|
/** Agents are real `pi --mode rpc` child processes (ADR-0001). */
|
|
30
33
|
export const liveTransport: TransportFactory = {
|
|
@@ -42,7 +45,11 @@ class LiveTransport implements AgentTransport {
|
|
|
42
45
|
model = "";
|
|
43
46
|
|
|
44
47
|
readonly #name: string;
|
|
45
|
-
readonly #process: Bun.Subprocess<"pipe", "pipe", "
|
|
48
|
+
readonly #process: Bun.Subprocess<"pipe", "pipe", "pipe">;
|
|
49
|
+
/** Last bytes pi wrote to stderr; the only classification signal a spawn gets (ADR-0037). */
|
|
50
|
+
#stderrTail = "";
|
|
51
|
+
/** Settles when stderr reaches EOF, so an exit message can wait for the diagnostic. */
|
|
52
|
+
readonly #stderrDone: Promise<void>;
|
|
46
53
|
readonly #queue = new FrameQueue();
|
|
47
54
|
readonly #pending = new Map<string, (frame: Frame) => void>();
|
|
48
55
|
/** Both halves of tool verification, or null when the spawn promised nothing. */
|
|
@@ -60,12 +67,29 @@ class LiveTransport implements AgentTransport {
|
|
|
60
67
|
cwd: options.cwd,
|
|
61
68
|
stdin: "pipe",
|
|
62
69
|
stdout: "pipe",
|
|
63
|
-
stderr
|
|
70
|
+
// pi prints model and startup diagnostics to stderr and exits 1 (main() →
|
|
71
|
+
// reportDiagnostics → process.exit(1)), so stderr carries the only reason
|
|
72
|
+
// yaag can classify. The reader below drains it so pi never blocks.
|
|
73
|
+
stderr: "pipe",
|
|
64
74
|
// Its own process group, so the Agent sees stdin EOF rather than a signal
|
|
65
75
|
// when the Orchestrator dies — EOF is the path that reaps grandchildren.
|
|
66
76
|
detached: true,
|
|
67
77
|
});
|
|
68
78
|
void this.#read();
|
|
79
|
+
// A stream error must not surface as an unhandled rejection: the tail is a
|
|
80
|
+
// best-effort diagnostic, never a reason to fail an Agent that started.
|
|
81
|
+
this.#stderrDone = this.#readStderr().catch(() => undefined);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Drains stderr continuously, keeping only a bounded tail. */
|
|
85
|
+
async #readStderr(): Promise<void> {
|
|
86
|
+
const decoder = new TextDecoder();
|
|
87
|
+
for await (const chunk of readChunks(this.#process.stderr)) {
|
|
88
|
+
this.#stderrTail = appendStderrTail(
|
|
89
|
+
this.#stderrTail,
|
|
90
|
+
decoder.decode(chunk, { stream: true }),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
69
93
|
}
|
|
70
94
|
|
|
71
95
|
/** Confirms the Agent is up and speaking the protocol, and resolves its model. */
|
|
@@ -90,6 +114,8 @@ class LiveTransport implements AgentTransport {
|
|
|
90
114
|
// The original startup error remains the useful failure.
|
|
91
115
|
}
|
|
92
116
|
if (error instanceof YaagError) throw error;
|
|
117
|
+
// Only the exit path folds in stderr: a tool-contract or timeout failure
|
|
118
|
+
// must stay generic, whatever pi warned about earlier (ADR-0037).
|
|
93
119
|
const message = error instanceof Error ? error.message : "agent startup failed";
|
|
94
120
|
throw new YaagError("SPAWN_FAILED", message, this.#name);
|
|
95
121
|
}
|
|
@@ -174,8 +200,15 @@ class LiveTransport implements AgentTransport {
|
|
|
174
200
|
timeoutMs,
|
|
175
201
|
);
|
|
176
202
|
});
|
|
177
|
-
const death = this.#process.exited.then((code) => {
|
|
178
|
-
|
|
203
|
+
const death = this.#process.exited.then(async (code) => {
|
|
204
|
+
// Detached grandchildren can hold the pipe open, so the drain is bounded:
|
|
205
|
+
// an exit must reject fast, with whatever diagnostic already arrived.
|
|
206
|
+
await Promise.race([this.#stderrDone, Bun.sleep(STDERR_DRAIN_MS)]);
|
|
207
|
+
throw new YaagError(
|
|
208
|
+
"SPAWN_FAILED",
|
|
209
|
+
`agent "${this.#name}" exited (${code})${stderrDiagnostic(this.#stderrTail)}`,
|
|
210
|
+
this.#name,
|
|
211
|
+
);
|
|
179
212
|
});
|
|
180
213
|
return Promise.race([response, failure, death]).finally(() => clearTimeout(timer));
|
|
181
214
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Bound on the kept stderr tail, so a chatty Agent cannot grow the buffer. */
|
|
2
|
+
export const STDERR_TAIL_BYTES = 4_096;
|
|
3
|
+
/** Bound on the diagnostic folded into a failure message, so it stays readable. */
|
|
4
|
+
export const STDERR_MESSAGE_CHARS = 300;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Lines pi prints that are warnings, not the reason a process died.
|
|
8
|
+
* `No models match pattern "<p>"` is the dangerous one: it classifies as
|
|
9
|
+
* `not_found` (ADR-0037), so an unfiltered tail would turn an unrelated startup
|
|
10
|
+
* failure of the same Agent into a Model Resolution trigger.
|
|
11
|
+
*/
|
|
12
|
+
const WARNINGS: readonly RegExp[] = [/no models match pattern/i, /^\s*warn(ing)?\b/i];
|
|
13
|
+
|
|
14
|
+
/** Appends one chunk to a stderr tail, keeping only the last bounded window. */
|
|
15
|
+
export function appendStderrTail(tail: string, chunk: string): string {
|
|
16
|
+
return (tail + chunk).slice(-STDERR_TAIL_BYTES);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The exit diagnostic of a stderr tail as a message suffix, or "" when pi
|
|
21
|
+
* printed nothing fatal. Warning lines are dropped, the rest is collapsed to one
|
|
22
|
+
* line and capped, so a failure message stays readable.
|
|
23
|
+
*/
|
|
24
|
+
export function stderrDiagnostic(tail: string): string {
|
|
25
|
+
const lines = tail
|
|
26
|
+
.split("\n")
|
|
27
|
+
.map((line) => line.trim())
|
|
28
|
+
.filter((line) => line !== "" && !WARNINGS.some((pattern) => pattern.test(line)));
|
|
29
|
+
const text = lines.join(" ").replace(/\s+/g, " ").trim();
|
|
30
|
+
if (text === "") return "";
|
|
31
|
+
return `: ${text.length > STDERR_MESSAGE_CHARS ? text.slice(-STDERR_MESSAGE_CHARS) : text}`;
|
|
32
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CanonicalJsonObject, ReportedResult } from "../ask-contract/index.ts";
|
|
2
2
|
import type { AskLimitOutcome, AskStalledOutcome } from "../errors.ts";
|
|
3
|
+
import type { RecordedSpawnSelection } from "../model/index.ts";
|
|
3
4
|
import type { AskOptions, ResolvedSpawnOptions, ThinkingLevel } from "../types.ts";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -185,6 +186,11 @@ export interface OpenOptions {
|
|
|
185
186
|
readonly resolvedSkillPaths?: readonly string[];
|
|
186
187
|
/** Absolute launch-ready extension paths resolved above the seam, emitted as repeated `-e` flags. */
|
|
187
188
|
readonly resolvedExtensionPaths?: readonly string[];
|
|
189
|
+
/**
|
|
190
|
+
* Declared extensions in final concatenation order, unresolved (ADR-0040).
|
|
191
|
+
* Spawn identity only: it never becomes argv, and it is absent when empty.
|
|
192
|
+
*/
|
|
193
|
+
readonly declaredExtensions?: readonly string[];
|
|
188
194
|
/** Session storage directory. Used by the e2e suite to stay out of ~/.pi (ticket 06). */
|
|
189
195
|
readonly sessionDir?: string;
|
|
190
196
|
/** Resumes an existing pi session, translated to `--session <path>`. */
|
|
@@ -209,4 +215,14 @@ export type TransportStartupObserver = (startup: TransportStartup) => void;
|
|
|
209
215
|
/** How the layer above obtains transports. Swapped wholesale for replay. */
|
|
210
216
|
export interface TransportFactory {
|
|
211
217
|
open(options: OpenOptions, observeStartup?: TransportStartupObserver): Promise<AgentTransport>;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The resolved selection this factory recorded for the Agent it would open
|
|
221
|
+
* next, so a Cassette-backed spawn adopts the recorded outcome instead of
|
|
222
|
+
* re-running Model Resolution (ADR-0037, ADR-0038, ADR-0039).
|
|
223
|
+
*
|
|
224
|
+
* A pure peek: it must not advance the factory's spawn cursor. Live factories
|
|
225
|
+
* record nothing and omit it.
|
|
226
|
+
*/
|
|
227
|
+
recordedSpawn?(options: OpenOptions): RecordedSpawnSelection | undefined;
|
|
212
228
|
}
|
package/src/types.ts
CHANGED
|
@@ -12,7 +12,10 @@ export interface SpawnOptions {
|
|
|
12
12
|
*
|
|
13
13
|
* An array is an ordered fallback list and a function picks the next candidate
|
|
14
14
|
* from the failures so far. Any resolved pattern may carry an inline thinking
|
|
15
|
-
* suffix (`"opus-5:medium"`), which wins over `thinking`.
|
|
15
|
+
* suffix (`"opus-5:medium"`), which wins over `thinking`. Each attempt settles
|
|
16
|
+
* the model first, then the thinking level for that model. yaag starts a new
|
|
17
|
+
* attempt only when pi reports `not_found`, `auth`, or `rate_limited`. See
|
|
18
|
+
* `ModelSpec` and `ModelResolver` for the termination rules (ADR-0037).
|
|
16
19
|
*/
|
|
17
20
|
readonly model?: ModelSpec;
|
|
18
21
|
/** Replaces the default system prompt (`pi --system-prompt`). */
|
|
@@ -20,7 +23,8 @@ export interface SpawnOptions {
|
|
|
20
23
|
/**
|
|
21
24
|
* Sets pi's thinking level; omission preserves pi's default. A function picks
|
|
22
25
|
* the level from the settled model, and is not consulted for a model pattern
|
|
23
|
-
* that carries an inline thinking suffix.
|
|
26
|
+
* that carries an inline thinking suffix. The resolver runs again for each
|
|
27
|
+
* attempt, with the model that settled for that attempt (ADR-0037).
|
|
24
28
|
*/
|
|
25
29
|
readonly thinking?: ThinkingSpec;
|
|
26
30
|
/** Appends text to pi's system prompt (`pi --append-system-prompt`). */
|
|
@@ -34,6 +38,11 @@ export interface SpawnOptions {
|
|
|
34
38
|
* resolve from the Orchestration Program file; each becomes `pi -e <path>`.
|
|
35
39
|
*/
|
|
36
40
|
readonly extensions?: readonly string[];
|
|
41
|
+
/**
|
|
42
|
+
* Load the Effective Config's `agents.extensions` for this Agent. Default
|
|
43
|
+
* true; false spawns with only the extensions this call declares (ADR-0040).
|
|
44
|
+
*/
|
|
45
|
+
readonly configExtensions?: boolean;
|
|
37
46
|
/**
|
|
38
47
|
* Tool allowlist layered over the ADR-0026 baseline; omission is tool-free when hermetic.
|
|
39
48
|
* `disallowedTools` applies last. A non-empty surviving allowlist is verified after startup;
|
|
@@ -59,7 +68,12 @@ export interface SpawnOptions {
|
|
|
59
68
|
readonly worktree?: boolean;
|
|
60
69
|
}
|
|
61
70
|
|
|
62
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Spawn options after Model Resolution has settled one model and thinking level.
|
|
73
|
+
*
|
|
74
|
+
* This is the settled shape, and it is what the Cassette identity hashes
|
|
75
|
+
* (ADR-0039).
|
|
76
|
+
*/
|
|
63
77
|
export interface ResolvedSpawnOptions extends Omit<SpawnOptions, "model" | "thinking"> {
|
|
64
78
|
readonly model?: string;
|
|
65
79
|
readonly thinking?: ThinkingLevel;
|
|
@@ -84,6 +98,11 @@ export interface SpawnOverrides {
|
|
|
84
98
|
* Agent, permits one further turn (or a fixed duration grace), then aborts and
|
|
85
99
|
* rejects with `ASK_LIMIT` if it has not settled. This leaves the Handle alive.
|
|
86
100
|
* `timeoutMs` is independent: it kills the Agent and rejects with `ASK_TIMEOUT`.
|
|
101
|
+
*
|
|
102
|
+
* A model failure inside an Ask starts Model Fallback and retries the Ask
|
|
103
|
+
* (ADR-0038). Each attempt gets fresh soft limits, a fresh Stall Watchdog and a
|
|
104
|
+
* fresh output collector. `timeoutMs` is the exception: it is the hard ceiling
|
|
105
|
+
* of the whole Ask, and every attempt shares one deadline.
|
|
87
106
|
*/
|
|
88
107
|
export interface AskOptions {
|
|
89
108
|
/** Reject and kill the Agent if it has not settled in time. Unset = no bound. */
|
|
@@ -106,7 +125,10 @@ export interface AskOptions {
|
|
|
106
125
|
* live Ask by default (ADR-0029). On expiry yaag probes the Agent and either
|
|
107
126
|
* recovers a missed settlement or rejects with `ASK_STALLED`. Omission uses
|
|
108
127
|
* the 10-minute default. `false`, and any value that is not above zero,
|
|
109
|
-
* disable the watchdog for this Ask.
|
|
128
|
+
* disable the watchdog for this Ask. `ASK_STALLED` is a generic failure: it
|
|
129
|
+
* never starts Model Fallback. If the silence budget ends the Ask before pi
|
|
130
|
+
* reports a model failure, the Ask fails `ASK_STALLED` and no swap happens
|
|
131
|
+
* (ADR-0029).
|
|
110
132
|
*/
|
|
111
133
|
readonly stallMs?: number | false;
|
|
112
134
|
/** Per-Ask replacement for the runtime's wrap-up steering message. */
|
|
@@ -141,7 +163,10 @@ export interface Handle {
|
|
|
141
163
|
readonly cwd: string;
|
|
142
164
|
/** Fresh branch for a worktree Agent; undefined for ordinary Agents. */
|
|
143
165
|
readonly branch: string | undefined;
|
|
144
|
-
/**
|
|
166
|
+
/**
|
|
167
|
+
* Model id as reported by the Agent's own `get_state` — not the requested
|
|
168
|
+
* pattern. A mid-Ask model swap updates it (ADR-0038).
|
|
169
|
+
*/
|
|
145
170
|
readonly model: string;
|
|
146
171
|
|
|
147
172
|
/**
|
|
@@ -155,6 +180,9 @@ export interface Handle {
|
|
|
155
180
|
* (default 3) and then rejects recoverably with `ASK_INVALID_OUTPUT` after an
|
|
156
181
|
* abort settlement; a reported value the schema rejects fails the same way
|
|
157
182
|
* without a correction. These recoverable outcomes leave the Handle reusable.
|
|
183
|
+
* A retry after a Model Fallback discards the result the failed attempt
|
|
184
|
+
* reported, and arms the collector again, so a stale result cannot settle the
|
|
185
|
+
* retried Ask (ADR-0032, ADR-0038).
|
|
158
186
|
* A concurrent call rejects with `AGENT_BUSY`.
|
|
159
187
|
*/
|
|
160
188
|
ask<Schema extends TSchema>(
|
package/src/wire-constants.ts
CHANGED
|
@@ -20,5 +20,8 @@ export const NODE_GIST_MAX_CHARS = 120;
|
|
|
20
20
|
/** Maximum Nested Nodes the Summary keeps per Agent; past it, exited nodes prune oldest-first. */
|
|
21
21
|
export const AGENT_NODE_TABLE_MAX = 64;
|
|
22
22
|
|
|
23
|
+
/** Maximum Model Resolution fallbacks the Summary keeps per Agent; past it, the oldest prune. */
|
|
24
|
+
export const AGENT_MODEL_FALLBACK_MAX = 16;
|
|
25
|
+
|
|
23
26
|
/** Prefix marking that the oldest pending Ask output was dropped to fit the wire cap. */
|
|
24
27
|
export const ASK_OUTPUT_TRUNCATION_MARKER = "[…output truncated…]";
|