@yaag/extension 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.
package/README.md CHANGED
@@ -138,6 +138,8 @@ export default defineRun({
138
138
  run: async (ctx) => {
139
139
  const agent = await ctx.spawn({
140
140
  name: "reviewer",
141
+ model: ["anthropic/claude-opus-4:medium", "anthropic/claude-haiku-4"],
142
+ thinking: (model) => (model.includes("haiku") ? "low" : "high"),
141
143
  tools: ["read", "grep"],
142
144
  disallowedTools: ["yaag_run"],
143
145
  skills: ["review"],
@@ -173,6 +175,14 @@ is recoverable and the Handle can be asked again. `wrapUpPrompt` replaces the
173
175
  default steering message. This is intentionally different from `timeoutMs`, the
174
176
  destructive fallback that rejects with `ASK_TIMEOUT` and closes the Agent.
175
177
 
178
+ `model` takes one pattern, an ordered list, or a function of the failures so
179
+ far; `thinking` takes a level or a function of the settled model. A pattern can
180
+ end with a thinking suffix (`"opus-5:medium"`), and the suffix wins over
181
+ `thinking`. The fallback rules — trigger classes, retry, termination — are
182
+ stated on the types in `<program dir>/.yaag/types/runtime/index.d.ts` and in
183
+ the root [`README.md`](../../README.md#models-and-fallback); the sequences are
184
+ in [`../../docs/architecture.md`](../../docs/architecture.md) §4 and §6.
185
+
176
186
  The tool call:
177
187
 
178
188
  ```json
@@ -231,3 +241,6 @@ Provided commands: `/yaag-status`, `/yaag-setup-workspace [dir]`.
231
241
  - [`../../docs/architecture.md`](../../docs/architecture.md) — the diagrams
232
242
  - [`../../docs/adr/`](../../docs/adr) — why it is built this way
233
243
  - [ADR-0018: foreign workspaces use CLI aliases and vendored types](../../docs/adr/0018-foreign-workspaces-use-cli-aliases-and-vendored-types.md) — workspace setup and foreign-program support
244
+ - [ADR-0037: model resolution triggers are read from pi's stderr diagnostic](../../docs/adr/0037-model-resolution-triggers-are-read-from-pis-stderr-diagnostic.md) — which failures start a fallback
245
+ - [ADR-0038: a mid-Ask model swap is a yaag-side match, then `set_model`](../../docs/adr/0038-a-mid-ask-model-swap-is-a-yaag-side-match-then-set-model.md) — how a live Agent changes model
246
+ - [ADR-0039: replay adopts the recorded resolved model](../../docs/adr/0039-replay-adopts-the-recorded-resolved-model.md) — why a replay skips the resolution loop
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaag/extension",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -24,9 +24,9 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "@earendil-works/pi-tui": "^0.84.0",
27
- "@yaag/cli": "0.6.2",
28
- "@yaag/runtime": "0.6.2",
29
- "@yaag/tui": "0.6.2",
27
+ "@yaag/cli": "0.7.0",
28
+ "@yaag/runtime": "0.7.0",
29
+ "@yaag/tui": "0.7.0",
30
30
  "nanoid": "^6.0.1"
31
31
  },
32
32
  "peerDependencies": {
@@ -2,6 +2,7 @@ import type {
2
2
  AgentActivity,
3
3
  AgentInfo,
4
4
  AgentState,
5
+ ModelFallbackInfo,
5
6
  NodeInfo,
6
7
  NodeState,
7
8
  RunOutcome,
@@ -56,6 +57,7 @@ interface SummaryBase {
56
57
  readonly incomplete: boolean;
57
58
  readonly durationMs: number;
58
59
  readonly worstFrameGapMs: number;
60
+ readonly modelFallbacks: number;
59
61
  }
60
62
 
61
63
  function parseBase(stored: Record<string, unknown>): SummaryBase | null {
@@ -88,6 +90,9 @@ function parseBase(stored: Record<string, unknown>): SummaryBase | null {
88
90
  incomplete: stored.incomplete,
89
91
  durationMs: stored.durationMs,
90
92
  worstFrameGapMs: stored.worstFrameGapMs,
93
+ // Tolerant, unlike the strict fields above: a record written before the
94
+ // fallback counter existed must still load instead of disappearing.
95
+ modelFallbacks: typeof stored.modelFallbacks === "number" ? stored.modelFallbacks : 0,
91
96
  };
92
97
  }
93
98
 
@@ -110,7 +115,9 @@ function parseAgent(value: unknown): AgentInfo | null {
110
115
  const nodes = parseNodes(stored.nodes);
111
116
  const tokens = parseTokens(stored.tokens);
112
117
  const activity = parseActivity(stored.activity);
118
+ const fallbacks = parseFallbacks(stored.modelFallbacks);
113
119
  if (
120
+ fallbacks === null ||
114
121
  nodes === null ||
115
122
  tokens === undefined ||
116
123
  activity === undefined ||
@@ -142,6 +149,10 @@ function parseAgent(value: unknown): AgentInfo | null {
142
149
  askStartedAt: stored.askStartedAt,
143
150
  nodes,
144
151
  finishedNodesPruned: stored.finishedNodesPruned,
152
+ modelFallbacks: fallbacks,
153
+ // Tolerant for the same reason as the Run-level counter.
154
+ modelFallbacksPruned:
155
+ typeof stored.modelFallbacksPruned === "number" ? stored.modelFallbacksPruned : 0,
145
156
  };
146
157
  if (state === "asking") {
147
158
  if (typeof stored.askIndex !== "number") return null;
@@ -189,6 +200,37 @@ function parseNodes(value: unknown): NodeInfo[] | null {
189
200
  return nodes;
190
201
  }
191
202
 
203
+ /**
204
+ * The per-Agent fallback table: `[]` when a record predates it, `null` when the
205
+ * stored value is malformed.
206
+ */
207
+ function parseFallbacks(value: unknown): ModelFallbackInfo[] | null {
208
+ if (value === undefined) return [];
209
+ if (!Array.isArray(value)) return null;
210
+ const fallbacks: ModelFallbackInfo[] = [];
211
+ for (const entry of value) {
212
+ const stored = asRecord(entry);
213
+ if (
214
+ stored === null ||
215
+ typeof stored.failedModel !== "string" ||
216
+ typeof stored.resolvedModel !== "string" ||
217
+ typeof stored.attempt !== "number" ||
218
+ !isReason(stored.reason) ||
219
+ !isNullableNumber(stored.at)
220
+ ) {
221
+ return null;
222
+ }
223
+ fallbacks.push({
224
+ failedModel: stored.failedModel,
225
+ resolvedModel: stored.resolvedModel,
226
+ attempt: stored.attempt,
227
+ reason: stored.reason,
228
+ at: stored.at,
229
+ });
230
+ }
231
+ return fallbacks;
232
+ }
233
+
192
234
  /**
193
235
  * `undefined` means the stored value is malformed or missing; a stored `null`
194
236
  * is an Agent or Node whose usage was never reported.
@@ -250,6 +292,10 @@ function isAgentState(value: unknown): value is AgentState {
250
292
  return value === "idle" || value === "asking" || value === "exited";
251
293
  }
252
294
 
295
+ function isReason(value: unknown): value is ModelFallbackInfo["reason"] {
296
+ return value === "not_found" || value === "auth" || value === "rate_limited";
297
+ }
298
+
253
299
  function isNodeState(value: unknown): value is NodeState {
254
300
  return value === "running" || value === "exited" || value === "failed";
255
301
  }
@@ -20,6 +20,12 @@ const DESCRIPTION = [
20
20
  "handle.ask supports maxTurns, maxToolCalls, maxDurationMs, and wrapUpPrompt as recoverable",
21
21
  "ASK_LIMIT controls. These differ from timeoutMs, the destructive Agent-killing fallback.",
22
22
  "",
23
+ "model takes one pattern, an ordered list of patterns, or a function of the failures so far;",
24
+ "thinking takes a level or a function of the settled model. A pattern can end with a thinking",
25
+ 'suffix ("opus-5:medium"), which wins over thinking. yaag falls back only on not_found, auth,',
26
+ "and rate_limited, at spawn and inside an Ask. Exhausted candidates fail with",
27
+ "MODEL_RESOLUTION_FAILED.",
28
+ "",
23
29
  "Describing imports the program and executes its module top level. Programs should keep",
24
30
  "module top level side-effect free; use this only for reviewed, user-authored files.",
25
31
  ].join("\n");
@@ -24,7 +24,7 @@ export function yaagPromptBlock(directories: readonly string[]): string {
24
24
  "});",
25
25
  "```",
26
26
  "",
27
- "Full authoring surface (defineAgent, args schemas, ask limits, worktrees):",
27
+ "Full authoring surface (defineAgent, args schemas, ask limits, worktrees, model fallback):",
28
28
  "read `<program dir>/.yaag/types/runtime/index.d.ts`.",
29
29
  ].join("\n");
30
30
  }
@@ -2,6 +2,7 @@ import type {
2
2
  AgentActivity,
3
3
  AgentInfo,
4
4
  LifecycleEvent,
5
+ ModelFallbackInfo,
5
6
  NodeInfo,
6
7
  RunSummary,
7
8
  TokenBreakdown,
@@ -58,7 +59,8 @@ function isSummaryBase(value: Record<string, unknown>): boolean {
58
59
  nullableTokens(value.tokens) &&
59
60
  typeof value.incomplete === "boolean" &&
60
61
  natural(value.durationMs) &&
61
- natural(value.worstFrameGapMs)
62
+ natural(value.worstFrameGapMs) &&
63
+ (value.modelFallbacks === undefined || natural(value.modelFallbacks))
62
64
  );
63
65
  }
64
66
 
@@ -78,9 +80,11 @@ function normalizeAgents(value: RunSummary): RunSummary {
78
80
  ...agent,
79
81
  nodes: agent.nodes ?? [],
80
82
  finishedNodesPruned: agent.finishedNodesPruned ?? 0,
83
+ modelFallbacks: agent.modelFallbacks ?? [],
84
+ modelFallbacksPruned: agent.modelFallbacksPruned ?? 0,
81
85
  };
82
86
  }
83
- return { ...value, agents };
87
+ return { ...value, agents, modelFallbacks: value.modelFallbacks ?? 0 };
84
88
  }
85
89
 
86
90
  function optionalNodes(value: unknown): boolean {
@@ -103,6 +107,24 @@ function isNodeState(value: unknown): boolean {
103
107
  return value === "running" || value === "exited" || value === "failed";
104
108
  }
105
109
 
110
+ function optionalFallbacks(value: unknown): boolean {
111
+ return value === undefined || (Array.isArray(value) && value.every(isFallback));
112
+ }
113
+
114
+ function isFallback(value: unknown): value is ModelFallbackInfo {
115
+ return (
116
+ isRecord(value) &&
117
+ strings(value.failedModel, value.resolvedModel) &&
118
+ isReason(value.reason) &&
119
+ natural(value.attempt) &&
120
+ nullableNumber(value.at)
121
+ );
122
+ }
123
+
124
+ function isReason(value: unknown): boolean {
125
+ return value === "not_found" || value === "auth" || value === "rate_limited";
126
+ }
127
+
106
128
  function optionalNodeUsage(value: unknown): boolean {
107
129
  if (value === undefined) return true;
108
130
  if (!isRecord(value)) return false;
@@ -142,7 +164,9 @@ function isAgentBase(value: Record<string, unknown>): boolean {
142
164
  nullableNumber(value.usageUpdatedAt) &&
143
165
  nullableNumber(value.askStartedAt) &&
144
166
  optionalNodes(value.nodes) &&
145
- (value.finishedNodesPruned === undefined || natural(value.finishedNodesPruned))
167
+ (value.finishedNodesPruned === undefined || natural(value.finishedNodesPruned)) &&
168
+ optionalFallbacks(value.modelFallbacks) &&
169
+ (value.modelFallbacksPruned === undefined || natural(value.modelFallbacksPruned))
146
170
  );
147
171
  }
148
172
 
@@ -191,6 +215,12 @@ function isEvent(value: unknown): value is LifecycleEvent {
191
215
  typeof value.ok === "boolean" &&
192
216
  (value.maxFrameGapMs === undefined || natural(value.maxFrameGapMs))
193
217
  );
218
+ case "model_fallback":
219
+ return (
220
+ strings(value.agent, value.failedModel, value.resolvedModel) &&
221
+ natural(value.attempt) &&
222
+ isReason(value.reason)
223
+ );
194
224
  case "agent_usage":
195
225
  return strings(value.agent) && isTokens(value.tokens) && number(value.cost);
196
226
  case "agent_exit":