@yaag/runtime 0.6.2 → 0.7.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.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/src/agent/agent.ts +38 -2
  3. package/src/agent/define-agent.ts +13 -3
  4. package/src/agent/spawn-request.ts +64 -0
  5. package/src/agent/spawn.ts +84 -63
  6. package/src/ask/ask-exchange-events.ts +10 -1
  7. package/src/ask/ask-exchange-options.ts +13 -0
  8. package/src/ask/ask-exchange.ts +77 -9
  9. package/src/ask/index.ts +1 -0
  10. package/src/cassette/cassette-replay.ts +23 -4
  11. package/src/cassette/recording-transport.ts +7 -0
  12. package/src/cassette/replay-divergence.ts +75 -19
  13. package/src/cassette/replay-transport.ts +8 -1
  14. package/src/cassette/resume-transport.ts +14 -1
  15. package/src/errors.ts +55 -2
  16. package/src/events.ts +19 -0
  17. package/src/index.ts +2 -0
  18. package/src/model/index.ts +18 -0
  19. package/src/model/model-error-history.ts +36 -0
  20. package/src/model/model-failure.ts +78 -0
  21. package/src/model/model-fallback.ts +15 -0
  22. package/src/model/model-match.ts +99 -0
  23. package/src/model/model-resolution.ts +44 -6
  24. package/src/model/model-swap.ts +81 -0
  25. package/src/model/recorded-resolution.ts +61 -0
  26. package/src/model/resolution-loop.ts +115 -0
  27. package/src/summary/index.ts +1 -0
  28. package/src/summary/summary-agent.ts +9 -1
  29. package/src/summary/summary-fallbacks.ts +50 -0
  30. package/src/summary/summary.ts +12 -0
  31. package/src/transport/fake-transport.ts +57 -1
  32. package/src/transport/index.ts +4 -0
  33. package/src/transport/live-transport.ts +37 -4
  34. package/src/transport/stderr-tail.ts +32 -0
  35. package/src/transport/transport.ts +11 -0
  36. package/src/types.ts +28 -5
  37. package/src/wire-constants.ts +3 -0
@@ -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
  /**
@@ -209,4 +210,14 @@ export type TransportStartupObserver = (startup: TransportStartup) => void;
209
210
  /** How the layer above obtains transports. Swapped wholesale for replay. */
210
211
  export interface TransportFactory {
211
212
  open(options: OpenOptions, observeStartup?: TransportStartupObserver): Promise<AgentTransport>;
213
+
214
+ /**
215
+ * The resolved selection this factory recorded for the Agent it would open
216
+ * next, so a Cassette-backed spawn adopts the recorded outcome instead of
217
+ * re-running Model Resolution (ADR-0037, ADR-0038, ADR-0039).
218
+ *
219
+ * A pure peek: it must not advance the factory's spawn cursor. Live factories
220
+ * record nothing and omit it.
221
+ */
222
+ recordedSpawn?(options: OpenOptions): RecordedSpawnSelection | undefined;
212
223
  }
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`). */
@@ -59,7 +63,12 @@ export interface SpawnOptions {
59
63
  readonly worktree?: boolean;
60
64
  }
61
65
 
62
- /** Spawn options after Model Resolution has settled one model and thinking level. */
66
+ /**
67
+ * Spawn options after Model Resolution has settled one model and thinking level.
68
+ *
69
+ * This is the settled shape, and it is what the Cassette identity hashes
70
+ * (ADR-0039).
71
+ */
63
72
  export interface ResolvedSpawnOptions extends Omit<SpawnOptions, "model" | "thinking"> {
64
73
  readonly model?: string;
65
74
  readonly thinking?: ThinkingLevel;
@@ -84,6 +93,11 @@ export interface SpawnOverrides {
84
93
  * Agent, permits one further turn (or a fixed duration grace), then aborts and
85
94
  * rejects with `ASK_LIMIT` if it has not settled. This leaves the Handle alive.
86
95
  * `timeoutMs` is independent: it kills the Agent and rejects with `ASK_TIMEOUT`.
96
+ *
97
+ * A model failure inside an Ask starts Model Fallback and retries the Ask
98
+ * (ADR-0038). Each attempt gets fresh soft limits, a fresh Stall Watchdog and a
99
+ * fresh output collector. `timeoutMs` is the exception: it is the hard ceiling
100
+ * of the whole Ask, and every attempt shares one deadline.
87
101
  */
88
102
  export interface AskOptions {
89
103
  /** Reject and kill the Agent if it has not settled in time. Unset = no bound. */
@@ -106,7 +120,10 @@ export interface AskOptions {
106
120
  * live Ask by default (ADR-0029). On expiry yaag probes the Agent and either
107
121
  * recovers a missed settlement or rejects with `ASK_STALLED`. Omission uses
108
122
  * the 10-minute default. `false`, and any value that is not above zero,
109
- * disable the watchdog for this Ask.
123
+ * disable the watchdog for this Ask. `ASK_STALLED` is a generic failure: it
124
+ * never starts Model Fallback. If the silence budget ends the Ask before pi
125
+ * reports a model failure, the Ask fails `ASK_STALLED` and no swap happens
126
+ * (ADR-0029).
110
127
  */
111
128
  readonly stallMs?: number | false;
112
129
  /** Per-Ask replacement for the runtime's wrap-up steering message. */
@@ -141,7 +158,10 @@ export interface Handle {
141
158
  readonly cwd: string;
142
159
  /** Fresh branch for a worktree Agent; undefined for ordinary Agents. */
143
160
  readonly branch: string | undefined;
144
- /** Model id as reported by the Agent's own `get_state` — not the requested pattern. */
161
+ /**
162
+ * Model id as reported by the Agent's own `get_state` — not the requested
163
+ * pattern. A mid-Ask model swap updates it (ADR-0038).
164
+ */
145
165
  readonly model: string;
146
166
 
147
167
  /**
@@ -155,6 +175,9 @@ export interface Handle {
155
175
  * (default 3) and then rejects recoverably with `ASK_INVALID_OUTPUT` after an
156
176
  * abort settlement; a reported value the schema rejects fails the same way
157
177
  * without a correction. These recoverable outcomes leave the Handle reusable.
178
+ * A retry after a Model Fallback discards the result the failed attempt
179
+ * reported, and arms the collector again, so a stale result cannot settle the
180
+ * retried Ask (ADR-0032, ADR-0038).
158
181
  * A concurrent call rejects with `AGENT_BUSY`.
159
182
  */
160
183
  ask<Schema extends TSchema>(
@@ -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…]";