@yaag/cli 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 (31) hide show
  1. package/assets/types/runtime/agent/agent.d.ts +16 -1
  2. package/assets/types/runtime/agent/define-agent.d.ts +13 -3
  3. package/assets/types/runtime/agent/spawn-request.d.ts +23 -0
  4. package/assets/types/runtime/ask/ask-exchange-events.d.ts +7 -1
  5. package/assets/types/runtime/ask/ask-exchange-options.d.ts +12 -0
  6. package/assets/types/runtime/ask/index.d.ts +1 -0
  7. package/assets/types/runtime/cassette/replay-divergence.d.ts +10 -2
  8. package/assets/types/runtime/errors.d.ts +16 -2
  9. package/assets/types/runtime/events.d.ts +18 -0
  10. package/assets/types/runtime/index.d.ts +2 -2
  11. package/assets/types/runtime/model/index.d.ts +8 -1
  12. package/assets/types/runtime/model/model-error-history.d.ts +20 -0
  13. package/assets/types/runtime/model/model-failure.d.ts +18 -0
  14. package/assets/types/runtime/model/model-fallback.d.ts +13 -0
  15. package/assets/types/runtime/model/model-match.d.ts +31 -0
  16. package/assets/types/runtime/model/model-resolution.d.ts +44 -6
  17. package/assets/types/runtime/model/model-swap.d.ts +27 -0
  18. package/assets/types/runtime/model/recorded-resolution.d.ts +37 -0
  19. package/assets/types/runtime/model/resolution-loop.d.ts +47 -0
  20. package/assets/types/runtime/summary/index.d.ts +1 -1
  21. package/assets/types/runtime/summary/summary-agent.d.ts +8 -0
  22. package/assets/types/runtime/summary/summary-fallbacks.d.ts +30 -0
  23. package/assets/types/runtime/summary/summary.d.ts +3 -1
  24. package/assets/types/runtime/transport/fake-transport.d.ts +9 -1
  25. package/assets/types/runtime/transport/index.d.ts +1 -0
  26. package/assets/types/runtime/transport/stderr-tail.d.ts +12 -0
  27. package/assets/types/runtime/transport/transport.d.ts +10 -0
  28. package/assets/types/runtime/types.d.ts +28 -5
  29. package/assets/types/runtime/wire-constants.d.ts +2 -0
  30. package/package.json +3 -3
  31. package/src/terminal/render.ts +3 -0
@@ -1,7 +1,16 @@
1
1
  import type { Static, TSchema } from "typebox";
2
2
  import type { EventSink } from "../events.ts";
3
+ import type { ModelErrorHistory, ModelResolution } from "../model/index.ts";
3
4
  import type { AgentStats, AgentTransport } from "../transport/index.ts";
4
5
  import type { AskOptions, Handle, ResolvedSpawnOptions, StructuredAskOptions } from "../types.ts";
6
+ /** The mid-Ask Model Resolution one Agent's spawn handed it (ADR-0038). */
7
+ export interface AgentModelFallback {
8
+ readonly resolution: ModelResolution;
9
+ /** The Agent-wide attempt history, shared with its spawn loop. */
10
+ readonly history: ModelErrorHistory;
11
+ /** The model candidate this Agent's spawn settled on. */
12
+ readonly candidate: string;
13
+ }
5
14
  export interface AgentOptions {
6
15
  readonly name: string;
7
16
  readonly cwd: string;
@@ -10,6 +19,11 @@ export interface AgentOptions {
10
19
  readonly emit: EventSink;
11
20
  /** The options this Agent was spawned with, for the ADR-0014 Ask hash. */
12
21
  readonly spawnOptions: ResolvedSpawnOptions;
22
+ /**
23
+ * Mid-Ask Model Resolution for this Agent, sharing the spawn loop's history.
24
+ * Absent when the spawn named no candidate: there is nothing to fall back from.
25
+ */
26
+ readonly modelFallback?: AgentModelFallback;
13
27
  /** Definition-owned defaults merged below explicit per-Ask options. */
14
28
  readonly askDefaults?: AskOptions;
15
29
  /** Definition identity recorded on its Asks, outside replay identity. */
@@ -27,8 +41,9 @@ export declare class Agent implements Handle {
27
41
  readonly name: string;
28
42
  readonly cwd: string;
29
43
  readonly branch: string | undefined;
30
- readonly model: string;
31
44
  constructor(options: AgentOptions);
45
+ /** The model pi reports for this Agent now; a mid-Ask swap updates it. */
46
+ get model(): string;
32
47
  ask<Schema extends TSchema>(prompt: string, options: StructuredAskOptions<Schema>): Promise<Static<Schema>>;
33
48
  ask(prompt: string, options?: AskOptions): Promise<string>;
34
49
  /** True when the Agent was killed mid-Ask, so its cost is a floor (ADR-0012). */
@@ -12,10 +12,18 @@ export interface AgentConfig {
12
12
  /**
13
13
  * Model id handed to `pi --model`. Unset = pi's default. An array is an ordered
14
14
  * fallback list, a function picks the next candidate from the failures so far,
15
- * and any pattern may carry an inline thinking suffix (`"opus-5:medium"`).
15
+ * and any pattern may carry an inline thinking suffix (`"opus-5:medium"`),
16
+ * which wins over `thinking`. Each attempt settles the model first, then the
17
+ * thinking level for that model. yaag starts a new attempt only when pi
18
+ * reports `not_found`, `auth`, or `rate_limited` (ADR-0037).
16
19
  */
17
20
  readonly model?: ModelSpec;
18
- /** Thinking budget for the Agent's turns, as a level or a resolver. */
21
+ /**
22
+ * Thinking budget for the Agent's turns, as a level or a resolver. The
23
+ * resolver runs again for each attempt, with the model that settled for that
24
+ * attempt. A resolver kept in a definition is frozen policy, so it must stay
25
+ * pure and synchronous (ADR-0037).
26
+ */
19
27
  readonly thinking?: ThinkingSpec;
20
28
  /** Allow-list of tool names. Unset = pi's default tool set. */
21
29
  readonly tools?: readonly string[];
@@ -40,7 +48,9 @@ export interface AgentDefinition {
40
48
  * Throws a TypeError when `name` is missing or blank, or when `cwd`/`worktree`
41
49
  * appear — those are topology, chosen at spawn time, not baked into a definition.
42
50
  * The config is defensively copied and deep-frozen, so later mutation of the
43
- * caller's arrays cannot change the definition.
51
+ * caller's arrays cannot change the definition. `defineAgent` also copies and
52
+ * freezes a model array, so a later change of the caller's array cannot change
53
+ * Model Resolution (ADR-0037).
44
54
  */
45
55
  export declare function defineAgent(config: AgentConfig): AgentDefinition;
46
56
  /** True only for values produced by `defineAgent`. */
@@ -0,0 +1,23 @@
1
+ import type { ModelSelection } from "../model/index.ts";
2
+ import type { OpenOptions } from "../transport/index.ts";
3
+ import type { ResolvedSpawnOptions, SpawnOptions } from "../types.ts";
4
+ /** Everything one spawn needs to build its open request, before a model settles. */
5
+ export interface OpenRequestOptions {
6
+ readonly name: string;
7
+ readonly cwd: string;
8
+ readonly spawnOptions: SpawnOptions;
9
+ readonly resolvedExtensionPaths?: readonly string[];
10
+ readonly sessionDir: string | undefined;
11
+ }
12
+ /**
13
+ * Builds one spawn's open request from its options.
14
+ *
15
+ * A caller that has not settled a model yet passes the declared options: the
16
+ * request then carries a literal `model`/`thinking` and carries none for an
17
+ * array or resolver spec. That request is what a Cassette-backed factory peeks
18
+ * at, which is why the resume peek blanks model and thinking before it compares
19
+ * the request with the recorded spawn (ADR-0039).
20
+ */
21
+ export declare function openRequest(options: OpenRequestOptions): OpenOptions;
22
+ /** Replaces the caller's `model`/`thinking` forms with one attempt's settled selection. */
23
+ export declare function withSelection(options: SpawnOptions, selection: ModelSelection): ResolvedSpawnOptions;
@@ -1,4 +1,5 @@
1
1
  import type { AgentActivity, AskOutputChannel, EventSink } from "../events.ts";
2
+ import type { ModelFallback } from "../model/index.ts";
2
3
  import type { NodeSnapshot } from "../node/index.ts";
3
4
  import type { SettlementCause } from "./stall-watchdog.ts";
4
5
  /** What one finished Ask reports in its `ask_end` Lifecycle Event. */
@@ -9,7 +10,10 @@ export interface AskEndOutcome {
9
10
  readonly maxFrameGapMs: number | undefined;
10
11
  readonly cause?: SettlementCause;
11
12
  }
12
- /** Emits the four Ask-scoped Lifecycle Events for one exchange. */
13
+ /**
14
+ * Emits the Ask-scoped Lifecycle Events for one exchange, plus the Agent-scoped
15
+ * `model_fallback` this exchange's fallback loop reports.
16
+ */
13
17
  export declare class AskEvents {
14
18
  #private;
15
19
  constructor(emit: EventSink, agent: string, index: number);
@@ -18,6 +22,8 @@ export declare class AskEvents {
18
22
  output: (channel: AskOutputChannel, text: string) => void;
19
23
  /** Composes the Nested Node path from this Ask's identity, then reports it. */
20
24
  node: (snapshot: NodeSnapshot) => void;
25
+ /** Not Ask-scoped: a fallback names the Agent only, like the spawn-time loop. */
26
+ fallback: (fallback: ModelFallback) => void;
21
27
  /** A `normal` cause is the absent default, so ordinary settlements stay lean. */
22
28
  end(outcome: AskEndOutcome): void;
23
29
  }
@@ -1,5 +1,6 @@
1
1
  import type { ReportResultTool } from "../ask-contract/index.ts";
2
2
  import type { EventSink } from "../events.ts";
3
+ import type { ModelErrorHistory, ModelResolution } from "../model/index.ts";
3
4
  import type { AgentTransport, Connection, Frame } from "../transport/index.ts";
4
5
  import type { ResolvedSpawnOptions } from "../types.ts";
5
6
  import type { EffectiveAskOptions } from "./ask-hash.ts";
@@ -7,6 +8,15 @@ import type { EffectiveAskOptions } from "./ask-hash.ts";
7
8
  export interface FrameObserver {
8
9
  observe(frame: Frame): void;
9
10
  }
11
+ /** Mid-Ask Model Resolution for one Agent, shared with its spawn-time loop. */
12
+ export interface AskModelFallback {
13
+ readonly resolution: ModelResolution;
14
+ readonly history: ModelErrorHistory;
15
+ /** The model candidate the Agent runs right now; a failure is attributed to it. */
16
+ currentModel(): string;
17
+ /** Reports the candidate a successful swap applied, and the model pi now reports. */
18
+ onSwapped(candidate: string, reportedModel: string): void;
19
+ }
10
20
  /** Options that connect one Ask exchange to its Agent's identity and event stream. */
11
21
  export interface AskExchangeOptions {
12
22
  readonly agent: string;
@@ -23,6 +33,8 @@ export interface AskExchangeOptions {
23
33
  readonly emit: EventSink;
24
34
  /** The Agent's persistent usage accumulator; fed frames only during live Asks. */
25
35
  readonly usage: FrameObserver;
36
+ /** Mid-Ask Model Resolution; absent when this Agent's spawn named no candidate. */
37
+ readonly modelFallback: AskModelFallback | undefined;
26
38
  /** Kills the Agent on ASK_TIMEOUT and destructive stalls — the one upward capability. */
27
39
  readonly close: () => void;
28
40
  /** Test-only override for the fixed duration-limit grace. */
@@ -3,5 +3,6 @@
3
3
  * Files inside this directory import each other directly.
4
4
  */
5
5
  export { exchangeAsk } from "./ask-exchange.ts";
6
+ export type { AskModelFallback } from "./ask-exchange-options.ts";
6
7
  export { askHash, askMarkerContext, type EffectiveAskOptions } from "./ask-hash.ts";
7
8
  export type { SettlementCause } from "./stall-watchdog.ts";
@@ -1,5 +1,5 @@
1
- import type { AskMarker, OpenOptions } from "../transport/index.ts";
2
- import type { CassetteAgent } from "./cassette.ts";
1
+ import type { AskMarker, OpenOptions, RecordedSpawnSelection } from "../transport/index.ts";
2
+ import type { CassetteAgent, CassetteSpawn } from "./cassette.ts";
3
3
  /** A pure description of the first strict replay identity mismatch. */
4
4
  export interface ReplayMismatch {
5
5
  readonly kind: "unexpected-spawn" | "spawn-options" | "changed-ask" | "extra-ask";
@@ -17,3 +17,11 @@ export declare const replayMismatch: {
17
17
  };
18
18
  /** Converts a detected mismatch into strict replay's public failure. */
19
19
  export declare function strictReplay(mismatch: ReplayMismatch): never;
20
+ /**
21
+ * True when a live spawn matches the recorded one on every identity field
22
+ * except the resolved model and thinking, so a resume may adopt the recorded
23
+ * selection instead of re-running Model Resolution (ADR-0039).
24
+ */
25
+ export declare function spawnMatchesExceptModel(expected: CassetteSpawn, actual: OpenOptions): boolean;
26
+ /** The recorded resolved selection of one Agent, or `undefined` when none is recorded. */
27
+ export declare function recordedSpawnSelection(agent: CassetteAgent | null): RecordedSpawnSelection | undefined;
@@ -5,7 +5,9 @@ export interface AskLimitOutcome {
5
5
  readonly kind: AskLimitKind;
6
6
  readonly count: number;
7
7
  }
8
+ import type { ModelError } from "./model/index.ts";
8
9
  import type { AgentProgress } from "./transport/index.ts";
10
+ export type { ModelError } from "./model/index.ts";
9
11
  export type { AgentProgress } from "./transport/index.ts";
10
12
  /** The recorded result when yaag rejects an Ask because no frame arrived within `idleMs`. */
11
13
  export interface AskStalledOutcome {
@@ -23,8 +25,12 @@ export interface AskInvalidOutputOutcome {
23
25
  /** Final extraction or TypeBox diagnostics, localized to JSON paths. */
24
26
  readonly errors: readonly string[];
25
27
  }
28
+ /** The recorded history when every model candidate of one Model Resolution loop failed. */
29
+ export interface ModelResolutionOutcome {
30
+ readonly modelErrors: readonly ModelError[];
31
+ }
26
32
  /** Why an Ask, spawn, or Run failed. Programs may branch on this; most won't. */
27
- export type YaagErrorCode = "AGENT_FAILED" | "AGENT_DIED" | "AGENT_BUSY" | "ASK_TIMEOUT" | "ASK_LIMIT" | "ASK_STALLED" | "ASK_INVALID_OUTPUT" | "ARGS_INVALID" | "OPTIONS_CONFLICT" | "REPLAY_DIVERGED" | "RESUME_REFUSED" | "RUN_CLOSED" | "RUN_STOPPED" | "WORKTREE_REFUSED" | "SPAWN_FAILED";
33
+ export type YaagErrorCode = "AGENT_FAILED" | "AGENT_DIED" | "AGENT_BUSY" | "ASK_TIMEOUT" | "ASK_LIMIT" | "ASK_STALLED" | "ASK_INVALID_OUTPUT" | "ARGS_INVALID" | "OPTIONS_CONFLICT" | "REPLAY_DIVERGED" | "RESUME_REFUSED" | "RUN_CLOSED" | "RUN_STOPPED" | "WORKTREE_REFUSED" | "MODEL_RESOLUTION_FAILED" | "SPAWN_FAILED";
28
34
  /** The single error class of the runtime (ADR-0003). */
29
35
  export declare class YaagError extends Error {
30
36
  readonly code: YaagErrorCode;
@@ -44,7 +50,13 @@ export declare class YaagError extends Error {
44
50
  readonly steeringEfforts?: number;
45
51
  /** Localized extraction or schema errors, present only for `ASK_INVALID_OUTPUT`. */
46
52
  readonly errors?: readonly string[];
47
- constructor(code: YaagErrorCode, message: string, agent?: string, options?: AskLimitOutcome | AskStalledOutcome | AskInvalidOutputOutcome);
53
+ /**
54
+ * Failed model candidates, present only for `MODEL_RESOLUTION_FAILED`. It
55
+ * holds one entry for each attempt, oldest first. A candidate that already
56
+ * failed with `not_found` or `auth` is refused again (ADR-0037).
57
+ */
58
+ readonly modelErrors?: readonly ModelError[];
59
+ constructor(code: YaagErrorCode, message: string, agent?: string, options?: AskLimitOutcome | AskStalledOutcome | AskInvalidOutputOutcome | ModelResolutionOutcome);
48
60
  }
49
61
  /** Builds the runtime's uniform agent-scoped error message. */
50
62
  export declare function agentError(agent: string, code: YaagErrorCode, message: string): YaagError;
@@ -52,5 +64,7 @@ export declare function agentError(agent: string, code: YaagErrorCode, message:
52
64
  export declare function askLimitError(agent: string, outcome: AskLimitOutcome): YaagError;
53
65
  /** The ASK_STALLED rejection for one tripped idle watch. */
54
66
  export declare function askStalledError(agent: string, outcome: AskStalledOutcome): YaagError;
67
+ /** The MODEL_RESOLUTION_FAILED rejection for an exhausted or self-repeating resolver. */
68
+ export declare function modelResolutionError(agent: string, errors: readonly ModelError[], refusedCandidate?: string): YaagError;
55
69
  /** Narrows an unknown rejection reason to a YaagError. */
56
70
  export declare function isYaagError(value: unknown): value is YaagError;
@@ -7,8 +7,10 @@
7
7
  * dedicated fd arrives with the extension.
8
8
  */
9
9
  import type { SettlementCause } from "./ask/index.ts";
10
+ import type { ModelErrorReason } from "./model/index.ts";
10
11
  import type { TokenBreakdown, WorktreeResolution } from "./transport/index.ts";
11
12
  export type { SettlementCause } from "./ask/index.ts";
13
+ export type { ModelErrorReason } from "./model/index.ts";
12
14
  /**
13
15
  * A Run's outcome (ADR-0022). `paused` arrives with the pause slice.
14
16
  * `interrupted` is not terminal: only an in-flight Checkpoint carries it, and a
@@ -112,6 +114,22 @@ export type LifecycleEventBody = {
112
114
  * Watchdog settled it from observed state, so its usage is an undercount.
113
115
  */
114
116
  readonly cause?: SettlementCause;
117
+ } | {
118
+ /**
119
+ * One failed Model Resolution attempt and the candidate that replaced it.
120
+ * yaag emits it at spawn time and inside an Ask (ADR-0037, ADR-0038).
121
+ * A resolver that gives up emits none: the Run fails with
122
+ * MODEL_RESOLUTION_FAILED, which already carries the full history.
123
+ */
124
+ readonly type: "model_fallback";
125
+ readonly agent: string;
126
+ /** The candidate pattern that failed, after inline-suffix stripping. */
127
+ readonly failedModel: string;
128
+ readonly reason: ModelErrorReason;
129
+ /** 0-based index of the failed attempt, as the resolver's history numbers it. */
130
+ readonly attempt: number;
131
+ /** The candidate resolution picked next. */
132
+ readonly resolvedModel: string;
115
133
  } | {
116
134
  readonly type: "agent_usage";
117
135
  readonly agent: string;
@@ -2,7 +2,7 @@ export type { AgentConfig, AgentDefinition } from "./agent/index.ts";
2
2
  export { agentDefinitionConfig, defineAgent, isAgentDefinition } from "./agent/index.ts";
3
3
  export type { Cassette, CassetteAgent, CassetteArtifact, CassetteAsk, CassetteGit, CassetteRun, CassetteSink, CassetteSpawn, } from "./cassette/index.ts";
4
4
  export { assertReplayable, CASSETTE_VERSION, loadCassette, recordingTransport, replayTransport, resumeTransport, } from "./cassette/index.ts";
5
- export type { AskInvalidOutputOutcome, AskLimitKind, AskLimitOutcome, AskStalledOutcome, YaagErrorCode, } from "./errors.ts";
5
+ export type { AskInvalidOutputOutcome, AskLimitKind, AskLimitOutcome, AskStalledOutcome, ModelResolutionOutcome, YaagErrorCode, } from "./errors.ts";
6
6
  export { isYaagError, YaagError } from "./errors.ts";
7
7
  export type { AgentActivity, AskOutputChannel, EventSink, LifecycleEvent, LifecycleEventBody, NodeState, NodeUsage, StampedEventSink, } from "./events.ts";
8
8
  export type { ModelError, ModelErrorReason, ModelResolver, ModelSelection, ModelSpec, ThinkingResolver, ThinkingSpec, } from "./model/index.ts";
@@ -11,7 +11,7 @@ export { agentAskPath, childPath, DEFAULT_NODE_DECODERS, NodeTracker, sanitizeNo
11
11
  export { prompt, promptGist } from "./prompt/index.ts";
12
12
  export type { OrchestrationProgram, ProgramDefinition, RunContext, RunOptions, } from "./run/index.ts";
13
13
  export { defineRun, executeRun, isOrchestrationProgram, programDefinition } from "./run/index.ts";
14
- export type { AgentInfo, AgentState, AskingAgentInfo, EndedRunSummary, ExitedAgentInfo, IdleAgentInfo, NodeInfo, RunningRunSummary, RunOutcome, RunState, RunSummary, } from "./summary/index.ts";
14
+ export type { AgentInfo, AgentState, AskingAgentInfo, EndedRunSummary, ExitedAgentInfo, IdleAgentInfo, ModelFallbackInfo, NodeInfo, RunningRunSummary, RunOutcome, RunState, RunSummary, } from "./summary/index.ts";
15
15
  export { applyEvent, initialSummary } from "./summary/index.ts";
16
16
  export type { AgentStats, AgentTransport, AskMarker, AskMarkerContext, AskPlayback, DiscoveredSkill, Frame, ReapPath, ReapTarget, SkillProbeFactory, TokenBreakdown, TransportFactory, TransportStartup, TransportStartupObserver, WorktreeResolution, } from "./transport/index.ts";
17
17
  export { readSystemPromptSidecar, reap, worktreeTransport } from "./transport/index.ts";
@@ -2,5 +2,12 @@
2
2
  * Public surface of the `model/` module: model resolution.
3
3
  * Files inside this directory import each other directly.
4
4
  */
5
- export { type ModelError, type ModelErrorReason, type ModelResolver, type ModelSelection, type ModelSpec, normalizeModelResolution, type ThinkingResolver, type ThinkingSpec, } from "./model-resolution.ts";
5
+ export { ModelErrorHistory } from "./model-error-history.ts";
6
+ export { classifyAskFailure, classifyModelFailure } from "./model-failure.ts";
7
+ export type { ModelFallback, ModelFallbackSink } from "./model-fallback.ts";
8
+ export type { AvailableModel } from "./model-match.ts";
9
+ export { type ModelError, type ModelErrorReason, type ModelResolution, type ModelResolver, type ModelSelection, type ModelSpec, normalizeModelResolution, type ThinkingResolver, type ThinkingSpec, } from "./model-resolution.ts";
10
+ export { type ModelSwapResult, swapModel } from "./model-swap.ts";
11
+ export { type RecordedResolution, type RecordedResolutionOptions, type RecordedSpawnSelection, resolveRecordedModel, } from "./recorded-resolution.ts";
12
+ export { type FallbackLoopOptions, type ResolutionLoopOptions, resolveModel, retryOnModelFailure, } from "./resolution-loop.ts";
6
13
  export type { ThinkingLevel } from "./thinking-level.ts";
@@ -0,0 +1,20 @@
1
+ import type { ModelError, ModelErrorReason } from "./model-resolution.ts";
2
+ /**
3
+ * The Model Resolution attempt history of one Agent.
4
+ *
5
+ * The spawn-time loop and every mid-Ask fallback loop of the same Agent share
6
+ * one history, so a resolver sees every candidate that already failed, whenever
7
+ * it failed.
8
+ */
9
+ export declare class ModelErrorHistory {
10
+ #private;
11
+ /** Every failed attempt so far, oldest first. */
12
+ get errors(): readonly ModelError[];
13
+ /**
14
+ * Appends one failed attempt and returns it; the attempt number is the
15
+ * history length before the push.
16
+ */
17
+ record(reason: ModelErrorReason, failedModel: string): ModelError;
18
+ /** True when this candidate already failed for a reason a retry cannot fix. */
19
+ failedPermanently(candidate: string): boolean;
20
+ }
@@ -0,0 +1,18 @@
1
+ import type { ModelErrorReason } from "./model-resolution.ts";
2
+ /**
3
+ * Classifies a spawn or Ask failure into a Model Resolution trigger, or
4
+ * `undefined` for a generic failure that must stay an ordinary failure.
5
+ *
6
+ * Evaluation order is `not_found` → `auth` → `rate_limited`, first match wins:
7
+ * a "model not found" diagnostic may also mention API keys in its help text, so
8
+ * the most specific table has to be consulted first.
9
+ */
10
+ export declare function classifyModelFailure(error: unknown): ModelErrorReason | undefined;
11
+ /**
12
+ * Classifies one Ask rejection into a Model Resolution trigger.
13
+ *
14
+ * Only AGENT_FAILED can be a trigger: ASK_STALLED is generic and never enters
15
+ * resolution (ADR-0029), ASK_LIMIT/ASK_TIMEOUT/ASK_INVALID_OUTPUT are yaag's own
16
+ * verdicts, and AGENT_DIED leaves nothing to swap a model on.
17
+ */
18
+ export declare function classifyAskFailure(error: unknown): ModelErrorReason | undefined;
@@ -0,0 +1,13 @@
1
+ import type { ModelErrorReason } from "./model-resolution.ts";
2
+ /** One failed Model Resolution attempt and the candidate that replaced it. */
3
+ export interface ModelFallback {
4
+ /** The candidate pattern that failed, after inline-suffix stripping. */
5
+ readonly failedModel: string;
6
+ readonly reason: ModelErrorReason;
7
+ /** 0-based index of the failed attempt in the Agent's shared history. */
8
+ readonly attempt: number;
9
+ /** The candidate the resolver picked next. */
10
+ readonly resolvedModel: string;
11
+ }
12
+ /** Where a Model Resolution loop reports each fallback it takes. */
13
+ export type ModelFallbackSink = (fallback: ModelFallback) => void;
@@ -0,0 +1,31 @@
1
+ /** One entry of pi's `get_available_models` snapshot. */
2
+ export interface AvailableModel {
3
+ readonly provider: string;
4
+ readonly id: string;
5
+ readonly name?: string;
6
+ }
7
+ /** What matching one yaag model pattern against pi's snapshot produced. */
8
+ export type ModelMatch = {
9
+ readonly kind: "matched";
10
+ readonly model: AvailableModel;
11
+ } | {
12
+ readonly kind: "no_match";
13
+ } | {
14
+ readonly kind: "ambiguous";
15
+ readonly candidates: readonly string[];
16
+ };
17
+ /**
18
+ * Matches one yaag model pattern against the models pi reports as available.
19
+ *
20
+ * A reduced mirror of pi's `core/model-resolver.js` (`resolveCliModel` /
21
+ * `tryMatchModel`): exact `provider/id`, exact bare `id`, then a known provider
22
+ * prefix, then a partial `id`/`name` search. A bare id that exists under several
23
+ * providers is refused rather than guessed, because yaag cannot see pi's
24
+ * configured-auth ordering. No suffix parsing happens here: an inline thinking
25
+ * suffix is already stripped by `normalizeModelResolution`.
26
+ */
27
+ export declare function matchAvailableModel(pattern: string, models: readonly AvailableModel[]): ModelMatch;
28
+ /** Reads pi's `get_available_models` payload, or `null` when it is not one. */
29
+ export declare function readAvailableModels(data: unknown): readonly AvailableModel[] | null;
30
+ /** Reads one pi Model object, or `null` when the payload is not one. */
31
+ export declare function readAvailableModel(entry: unknown): AvailableModel | null;
@@ -1,7 +1,21 @@
1
1
  import { type ThinkingLevel } from "./thinking-level.ts";
2
- /** Why one model candidate was rejected by pi. */
2
+ /**
3
+ * Why one model candidate was rejected by pi (ADR-0037).
4
+ *
5
+ * `not_found`: the pattern matched no model, or pi refused the live swap.
6
+ * `auth`: the provider refused the credentials.
7
+ * `rate_limited`: the provider stayed rate-limited after pi's own retries.
8
+ *
9
+ * These three classes are the only triggers of Model Resolution. A different
10
+ * transport or turn failure stays an ordinary failure and starts no new attempt.
11
+ */
3
12
  export type ModelErrorReason = "not_found" | "auth" | "rate_limited";
4
- /** One failed Model Resolution attempt, handed back to the caller's resolver. */
13
+ /**
14
+ * One failed Model Resolution attempt, handed back to the caller's resolver.
15
+ *
16
+ * yaag gives the full history to each resolver call, oldest attempt first. The
17
+ * first call gets an empty list (ADR-0037).
18
+ */
5
19
  export interface ModelError {
6
20
  readonly reason: ModelErrorReason;
7
21
  /** The model pattern that failed, after inline-suffix stripping. */
@@ -9,13 +23,37 @@ export interface ModelError {
9
23
  /** 0-based index of the attempt that produced this error. */
10
24
  readonly attempt: number;
11
25
  }
12
- /** Picks the next model candidate, or `undefined` to give up. */
26
+ /**
27
+ * Picks the next model candidate, or `undefined` to give up (ADR-0037).
28
+ *
29
+ * When the resolver gives up, the spawn or the Ask fails with
30
+ * `MODEL_RESOLUTION_FAILED`, which carries the full error history. yaag also
31
+ * refuses a candidate that already failed with `not_found` or `auth` in the
32
+ * same loop, so a resolver cannot loop forever. The resolver must be pure and
33
+ * synchronous.
34
+ */
13
35
  export type ModelResolver = (errors: readonly ModelError[]) => string | undefined;
14
- /** Every accepted `model` form: one pattern, an ordered list, or a resolver. */
36
+ /**
37
+ * Every accepted `model` form: one pattern, an ordered list, or a resolver.
38
+ *
39
+ * An array is the resolver that reads the candidate at the attempt index, and
40
+ * gives `undefined` past the end. One pattern is the one-element list. So the
41
+ * termination rule of `ModelResolver` covers every form (ADR-0037).
42
+ */
15
43
  export type ModelSpec = string | readonly string[] | ModelResolver;
16
- /** Picks the thinking level for a settled model, or `undefined` for pi's default. */
44
+ /**
45
+ * Picks the thinking level for a settled model, or `undefined` for pi's default.
46
+ *
47
+ * `undefined` means "no thinking preference". It is not a failure, and it does
48
+ * not stop the attempt (ADR-0037).
49
+ */
17
50
  export type ThinkingResolver = (selectedModel: string, errors: readonly ModelError[]) => ThinkingLevel | undefined;
18
- /** Every accepted `thinking` form: one level or a resolver. */
51
+ /**
52
+ * Every accepted `thinking` form: one level or a resolver.
53
+ *
54
+ * An inline suffix on the resolved model pattern wins over this property
55
+ * (ADR-0037).
56
+ */
19
57
  export type ThinkingSpec = ThinkingLevel | ThinkingResolver;
20
58
  /** One attempt's settled selection; `model: undefined` inherits pi's default model. */
21
59
  export interface ModelSelection {
@@ -0,0 +1,27 @@
1
+ import type { CommandResponse, Frame } from "../transport/index.ts";
2
+ import type { ModelSelection } from "./model-resolution.ts";
3
+ /** Sends one command frame to the live Agent. The Connection satisfies it. */
4
+ export type ModelSwapCommand = (frame: Frame) => Promise<CommandResponse>;
5
+ /** What one successful swap left the Agent running. */
6
+ export interface ModelSwapResult {
7
+ /** `provider/id` pi reports for the new model. */
8
+ readonly model: string;
9
+ }
10
+ /**
11
+ * Swaps one live Agent's model in place; the conversation context survives.
12
+ *
13
+ * pi's `set_model` takes a concrete `{provider, modelId}` pair and no pattern, so
14
+ * the swap is two frames: `get_available_models` to read the snapshot, then
15
+ * `set_model` on the entry yaag matched itself. A thinking level is a third
16
+ * frame, sent only when the selection names one — `undefined` means "no
17
+ * preference", exactly as at spawn.
18
+ *
19
+ * Rejects with an AGENT_FAILED-shaped error carrying pi's own diagnostic, so
20
+ * `classifyAskFailure` can re-enter Model Resolution: a refused pattern and a
21
+ * refused `set_model` both classify as `not_found`.
22
+ */
23
+ export declare function swapModel(options: {
24
+ readonly agent: string;
25
+ readonly command: ModelSwapCommand;
26
+ readonly selection: ModelSelection;
27
+ }): Promise<ModelSwapResult>;
@@ -0,0 +1,37 @@
1
+ import type { ModelError, ModelResolution, ModelSelection } from "./model-resolution.ts";
2
+ import type { ThinkingLevel } from "./thinking-level.ts";
3
+ /**
4
+ * The model and thinking a Cassette recorded for one Agent's settled spawn.
5
+ *
6
+ * It lives in `model/` because `transport/` already depends on this directory,
7
+ * and never the other way round: one name for one concept (ADR-0039).
8
+ */
9
+ export interface RecordedSpawnSelection {
10
+ readonly model?: string;
11
+ readonly thinking?: ThinkingLevel;
12
+ }
13
+ /** What to replay: the declared resolution and the outcome the Cassette holds. */
14
+ export interface RecordedResolutionOptions {
15
+ readonly resolution: ModelResolution;
16
+ readonly recorded: RecordedSpawnSelection;
17
+ }
18
+ /** The adopted selection and the attempts the declared resolution skipped to reach it. */
19
+ export interface RecordedResolution {
20
+ readonly selection: ModelSelection;
21
+ /**
22
+ * The synthetic failures of every candidate offered before the recorded one.
23
+ * The caller seeds the Agent's shared history with them, so a later mid-Ask
24
+ * fallback re-resolves from the attempt index the recording reached.
25
+ */
26
+ readonly skipped: readonly ModelError[];
27
+ }
28
+ /**
29
+ * Replays Model Resolution against a recorded outcome, without any model call.
30
+ *
31
+ * Offers the declared resolution one candidate at a time, feeding every
32
+ * non-matching candidate back as a synthetic `not_found` failure, and settles on
33
+ * the first selection that names the recorded model. Returns `undefined` when
34
+ * the declared spec can no longer produce that outcome — the caller then runs
35
+ * the ordinary loop, and Cassette identity reports the Divergence (ADR-0039).
36
+ */
37
+ export declare function resolveRecordedModel(options: RecordedResolutionOptions): RecordedResolution | undefined;
@@ -0,0 +1,47 @@
1
+ import { ModelErrorHistory } from "./model-error-history.ts";
2
+ import type { ModelFallbackSink } from "./model-fallback.ts";
3
+ import type { ModelResolution, ModelSelection } from "./model-resolution.ts";
4
+ /** One Model Resolution loop: what to resolve, for whom, and how one attempt runs. */
5
+ export interface ResolutionLoopOptions<T> {
6
+ readonly resolution: ModelResolution;
7
+ readonly agent: string;
8
+ /** The Agent's shared attempt history; a fresh one when the caller keeps none. */
9
+ readonly history?: ModelErrorHistory;
10
+ /** Runs one attempt with the settled selection; rejects to feed classification. */
11
+ attempt(selection: ModelSelection): Promise<T>;
12
+ /** Reports each failed attempt whose resolution produced a next candidate. */
13
+ readonly onFallback?: ModelFallbackSink;
14
+ }
15
+ /**
16
+ * Runs the Model Resolution loop: resolve → attempt → classify → re-resolve.
17
+ *
18
+ * Rejects with MODEL_RESOLUTION_FAILED (carrying the full error history) when the
19
+ * resolver gives up, or when it returns a candidate that already failed
20
+ * permanently (`not_found`/`auth`) in this loop. Any failure that is not a Model
21
+ * Resolution trigger is rethrown unchanged, and so is any failure of an attempt
22
+ * that named no candidate at all (it inherited pi's default, so there is nothing
23
+ * to fall back from).
24
+ */
25
+ export declare function resolveModel<T>(options: ResolutionLoopOptions<T>): Promise<T>;
26
+ /** One mid-Ask fallback loop: the work already runs on a model, so resolution comes second. */
27
+ export interface FallbackLoopOptions<T> {
28
+ readonly resolution: ModelResolution;
29
+ readonly agent: string;
30
+ readonly history: ModelErrorHistory;
31
+ /** The model the Agent runs right now; a failure is attributed to it. */
32
+ currentModel(): string;
33
+ /** Runs one attempt on the current model; rejects to feed classification. */
34
+ attempt(): Promise<T>;
35
+ /** Applies a re-resolved selection to the live Agent; rejects to feed classification. */
36
+ swap(selection: ModelSelection): Promise<void>;
37
+ /** Reports each failed attempt or swap whose resolution produced a next candidate. */
38
+ readonly onFallback?: ModelFallbackSink;
39
+ }
40
+ /**
41
+ * Runs the mid-Ask Model Resolution loop: attempt → classify → re-resolve → swap → retry.
42
+ *
43
+ * Unlike `resolveModel`, the first attempt runs on the model already in place,
44
+ * and a failed swap is classified exactly like a failed attempt, so a pattern pi
45
+ * refuses re-enters the loop instead of ending the Ask.
46
+ */
47
+ export declare function retryOnModelFailure<T>(options: FallbackLoopOptions<T>): Promise<T>;
@@ -3,4 +3,4 @@
3
3
  * Files inside this directory import each other directly.
4
4
  */
5
5
  export { applyEvent, type EndedRunSummary, initialSummary, type RunningRunSummary, type RunOutcome, type RunState, type RunSummary, } from "./summary.ts";
6
- export type { AgentInfo, AgentState, AskingAgentInfo, ExitedAgentInfo, IdleAgentInfo, NodeInfo, } from "./summary-agent.ts";
6
+ export type { AgentInfo, AgentState, AskingAgentInfo, ExitedAgentInfo, IdleAgentInfo, ModelFallbackInfo, NodeInfo, } from "./summary-agent.ts";
@@ -1,7 +1,9 @@
1
1
  import type { AgentActivity } from "../events.ts";
2
2
  import type { TokenBreakdown, WorktreeResolution } from "../transport/index.ts";
3
+ import type { ModelFallbackInfo } from "./summary-fallbacks.ts";
3
4
  import type { NodeInfo } from "./summary-nodes.ts";
4
5
  export type { AgentActivity } from "../events.ts";
6
+ export type { ModelFallbackInfo } from "./summary-fallbacks.ts";
5
7
  export type { NodeInfo } from "./summary-nodes.ts";
6
8
  /** The observer-facing lifecycle state of an Agent. */
7
9
  export type AgentState = "idle" | "asking" | "exited";
@@ -24,6 +26,10 @@ interface AgentInfoBase {
24
26
  readonly nodes: readonly NodeInfo[];
25
27
  /** Exited Nested Nodes dropped to keep the table bounded. */
26
28
  readonly finishedNodesPruned: number;
29
+ /** This Agent's bounded Model Resolution fallback table, oldest first. */
30
+ readonly modelFallbacks: readonly ModelFallbackInfo[];
31
+ /** Fallbacks dropped to keep the table bounded. */
32
+ readonly modelFallbacksPruned: number;
27
33
  }
28
34
  /** An Agent between Asks; its latest settled Ask identity, if any, is retained. */
29
35
  export interface IdleAgentInfo extends AgentInfoBase {
@@ -68,6 +74,8 @@ export interface AgentExitObservation {
68
74
  readonly incomplete: boolean;
69
75
  readonly worktree?: WorktreeResolution;
70
76
  }
77
+ /** Produces an idle placeholder for lifecycle events received before a spawn. */
78
+ export declare function placeholderAgent(): IdleAgentInfo;
71
79
  /**
72
80
  * Reconciles spawn identity without reopening a terminal Agent or losing facts
73
81
  * learned from a prior exit. A late spawn fills only missing identity fields.
@@ -0,0 +1,30 @@
1
+ import type { LifecycleEventBody, ModelErrorReason } from "../events.ts";
2
+ import type { AgentRecord } from "./summary-agent.ts";
3
+ /** One failed Model Resolution attempt as the Summary keeps it. */
4
+ export interface ModelFallbackInfo {
5
+ readonly failedModel: string;
6
+ readonly reason: ModelErrorReason;
7
+ readonly attempt: number;
8
+ readonly resolvedModel: string;
9
+ readonly at: number | null;
10
+ }
11
+ type ModelFallbackEvent = Extract<LifecycleEventBody, {
12
+ readonly type: "model_fallback";
13
+ }>;
14
+ /**
15
+ * Folds one `model_fallback` into an Agent's bounded fallback table.
16
+ *
17
+ * A fallback is an append-only fact, not a state replacement, so entries keep
18
+ * arrival order (the loop already orders attempts) and no stamped entry is
19
+ * rejected. Past `AGENT_MODEL_FALLBACK_MAX` entries the oldest drop into
20
+ * `modelFallbacksPruned`.
21
+ *
22
+ * The Agent's `model` is left alone. `resolvedModel` is a yaag pattern, which
23
+ * can be partial, and the event reports it before the swap is applied, while
24
+ * `model` is pi's resolved `provider/id` for the model the Agent really runs.
25
+ * `agent_spawn` is the only writer of that field: a mid-Ask swap updates the
26
+ * Handle alone and reports no Lifecycle Event, so the Summary keeps the
27
+ * spawn-time id.
28
+ */
29
+ export declare function applyModelFallback(agent: AgentRecord, event: ModelFallbackEvent, at: number | null): AgentRecord;
30
+ export {};
@@ -2,7 +2,7 @@ import type { LifecycleEvent, LifecycleEventBody, RunOutcome } from "../events.t
2
2
  import type { TokenBreakdown } from "../transport/index.ts";
3
3
  import type { AgentInfo } from "./summary-agent.ts";
4
4
  export type { NodeState, NodeUsage, RunOutcome } from "../events.ts";
5
- export type { AgentActivity, AgentInfo, AgentState, AskingAgentInfo, ExitedAgentInfo, IdleAgentInfo, NodeInfo, } from "./summary-agent.ts";
5
+ export type { AgentActivity, AgentInfo, AgentState, AskingAgentInfo, ExitedAgentInfo, IdleAgentInfo, ModelFallbackInfo, NodeInfo, } from "./summary-agent.ts";
6
6
  /** The observer-facing lifecycle state of a Run. */
7
7
  export type RunState = "running" | "ended";
8
8
  /** Accounting and identity facts shared by all Run observer states. */
@@ -22,6 +22,8 @@ interface RunSummaryBase {
22
22
  readonly incomplete: boolean;
23
23
  readonly durationMs: number;
24
24
  readonly worstFrameGapMs: number;
25
+ /** Run-wide Model Resolution fallbacks, pruned per-Agent entries included. */
26
+ readonly modelFallbacks: number;
25
27
  }
26
28
  /** A Run that has not settled; it has neither outcome nor compatibility result. */
27
29
  export interface RunningRunSummary extends RunSummaryBase {
@@ -1,3 +1,4 @@
1
+ import type { AvailableModel } from "../model/index.ts";
1
2
  import type { AgentStats, AgentTransport, AskMarker, Frame } from "./transport.ts";
2
3
  /**
3
4
  * Scripted frame playback for one prompt sent through a FakeTransport.
@@ -42,6 +43,12 @@ export interface FakeTransportOptions extends FakePromptScript {
42
43
  readonly abortError?: string;
43
44
  /** Makes the private report_result schema command fail. */
44
45
  readonly schemaCommandError?: string;
46
+ /** The snapshot answered to `get_available_models`; defaults to this fake's own model. */
47
+ readonly models?: readonly AvailableModel[];
48
+ /** Makes a `set_model` command fail, as pi does for a pair it does not know. */
49
+ readonly setModelError?: string;
50
+ /** Makes a `set_thinking_level` command fail. */
51
+ readonly setThinkingError?: string;
45
52
  readonly stats?: AgentStats;
46
53
  /** pi can answer a command after `agent_settled` — it is last among events only. */
47
54
  readonly promptResponse?: "immediate" | "after-settle";
@@ -59,7 +66,8 @@ export interface FakeTransportOptions extends FakePromptScript {
59
66
  */
60
67
  export declare class FakeTransport implements AgentTransport {
61
68
  #private;
62
- readonly model = "test/model";
69
+ /** The model this fake currently reports, which a `set_model` swap updates. */
70
+ get model(): string;
63
71
  /** Everything the runtime wrote, in order. */
64
72
  readonly sent: Frame[];
65
73
  readonly asks: AskMarker[];
@@ -2,6 +2,7 @@
2
2
  * Public surface of the `transport/` module: the pi process seam and its probes.
3
3
  * Files inside this directory import each other directly.
4
4
  */
5
+ export type { RecordedSpawnSelection } from "../model/index.ts";
5
6
  export { type CommandResponse, Connection } from "./connection.ts";
6
7
  export { FakeTransport, type FakeTransportOptions, recordedFrames, reportResultFrames, } from "./fake-transport.ts";
7
8
  export { FrameGapTracker } from "./frame-gap.ts";
@@ -0,0 +1,12 @@
1
+ /** Bound on the kept stderr tail, so a chatty Agent cannot grow the buffer. */
2
+ export declare const STDERR_TAIL_BYTES = 4096;
3
+ /** Bound on the diagnostic folded into a failure message, so it stays readable. */
4
+ export declare const STDERR_MESSAGE_CHARS = 300;
5
+ /** Appends one chunk to a stderr tail, keeping only the last bounded window. */
6
+ export declare function appendStderrTail(tail: string, chunk: string): string;
7
+ /**
8
+ * The exit diagnostic of a stderr tail as a message suffix, or "" when pi
9
+ * printed nothing fatal. Warning lines are dropped, the rest is collapsed to one
10
+ * line and capped, so a failure message stays readable.
11
+ */
12
+ export declare function stderrDiagnostic(tail: string): string;
@@ -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
  * The one seam of the runtime (ticket 04). An AgentTransport represents a whole
@@ -176,4 +177,13 @@ export type TransportStartupObserver = (startup: TransportStartup) => void;
176
177
  /** How the layer above obtains transports. Swapped wholesale for replay. */
177
178
  export interface TransportFactory {
178
179
  open(options: OpenOptions, observeStartup?: TransportStartupObserver): Promise<AgentTransport>;
180
+ /**
181
+ * The resolved selection this factory recorded for the Agent it would open
182
+ * next, so a Cassette-backed spawn adopts the recorded outcome instead of
183
+ * re-running Model Resolution (ADR-0037, ADR-0038, ADR-0039).
184
+ *
185
+ * A pure peek: it must not advance the factory's spawn cursor. Live factories
186
+ * record nothing and omit it.
187
+ */
188
+ recordedSpawn?(options: OpenOptions): RecordedSpawnSelection | undefined;
179
189
  }
@@ -10,7 +10,10 @@ export interface SpawnOptions {
10
10
  *
11
11
  * An array is an ordered fallback list and a function picks the next candidate
12
12
  * from the failures so far. Any resolved pattern may carry an inline thinking
13
- * suffix (`"opus-5:medium"`), which wins over `thinking`.
13
+ * suffix (`"opus-5:medium"`), which wins over `thinking`. Each attempt settles
14
+ * the model first, then the thinking level for that model. yaag starts a new
15
+ * attempt only when pi reports `not_found`, `auth`, or `rate_limited`. See
16
+ * `ModelSpec` and `ModelResolver` for the termination rules (ADR-0037).
14
17
  */
15
18
  readonly model?: ModelSpec;
16
19
  /** Replaces the default system prompt (`pi --system-prompt`). */
@@ -18,7 +21,8 @@ export interface SpawnOptions {
18
21
  /**
19
22
  * Sets pi's thinking level; omission preserves pi's default. A function picks
20
23
  * the level from the settled model, and is not consulted for a model pattern
21
- * that carries an inline thinking suffix.
24
+ * that carries an inline thinking suffix. The resolver runs again for each
25
+ * attempt, with the model that settled for that attempt (ADR-0037).
22
26
  */
23
27
  readonly thinking?: ThinkingSpec;
24
28
  /** Appends text to pi's system prompt (`pi --append-system-prompt`). */
@@ -56,7 +60,12 @@ export interface SpawnOptions {
56
60
  /** Request a fresh Git worktree. The requested cwd remains the base until spawn resolves. */
57
61
  readonly worktree?: boolean;
58
62
  }
59
- /** Spawn options after Model Resolution has settled one model and thinking level. */
63
+ /**
64
+ * Spawn options after Model Resolution has settled one model and thinking level.
65
+ *
66
+ * This is the settled shape, and it is what the Cassette identity hashes
67
+ * (ADR-0039).
68
+ */
60
69
  export interface ResolvedSpawnOptions extends Omit<SpawnOptions, "model" | "thinking"> {
61
70
  readonly model?: string;
62
71
  readonly thinking?: ThinkingLevel;
@@ -79,6 +88,11 @@ export interface SpawnOverrides {
79
88
  * Agent, permits one further turn (or a fixed duration grace), then aborts and
80
89
  * rejects with `ASK_LIMIT` if it has not settled. This leaves the Handle alive.
81
90
  * `timeoutMs` is independent: it kills the Agent and rejects with `ASK_TIMEOUT`.
91
+ *
92
+ * A model failure inside an Ask starts Model Fallback and retries the Ask
93
+ * (ADR-0038). Each attempt gets fresh soft limits, a fresh Stall Watchdog and a
94
+ * fresh output collector. `timeoutMs` is the exception: it is the hard ceiling
95
+ * of the whole Ask, and every attempt shares one deadline.
82
96
  */
83
97
  export interface AskOptions {
84
98
  /** Reject and kill the Agent if it has not settled in time. Unset = no bound. */
@@ -101,7 +115,10 @@ export interface AskOptions {
101
115
  * live Ask by default (ADR-0029). On expiry yaag probes the Agent and either
102
116
  * recovers a missed settlement or rejects with `ASK_STALLED`. Omission uses
103
117
  * the 10-minute default. `false`, and any value that is not above zero,
104
- * disable the watchdog for this Ask.
118
+ * disable the watchdog for this Ask. `ASK_STALLED` is a generic failure: it
119
+ * never starts Model Fallback. If the silence budget ends the Ask before pi
120
+ * reports a model failure, the Ask fails `ASK_STALLED` and no swap happens
121
+ * (ADR-0029).
105
122
  */
106
123
  readonly stallMs?: number | false;
107
124
  /** Per-Ask replacement for the runtime's wrap-up steering message. */
@@ -134,7 +151,10 @@ export interface Handle {
134
151
  readonly cwd: string;
135
152
  /** Fresh branch for a worktree Agent; undefined for ordinary Agents. */
136
153
  readonly branch: string | undefined;
137
- /** Model id as reported by the Agent's own `get_state` — not the requested pattern. */
154
+ /**
155
+ * Model id as reported by the Agent's own `get_state` — not the requested
156
+ * pattern. A mid-Ask model swap updates it (ADR-0038).
157
+ */
138
158
  readonly model: string;
139
159
  /**
140
160
  * Sends a prompt and resolves after the Agent's turn settles.
@@ -147,6 +167,9 @@ export interface Handle {
147
167
  * (default 3) and then rejects recoverably with `ASK_INVALID_OUTPUT` after an
148
168
  * abort settlement; a reported value the schema rejects fails the same way
149
169
  * without a correction. These recoverable outcomes leave the Handle reusable.
170
+ * A retry after a Model Fallback discards the result the failed attempt
171
+ * reported, and arms the collector again, so a stale result cannot settle the
172
+ * retried Ask (ADR-0032, ADR-0038).
150
173
  * A concurrent call rejects with `AGENT_BUSY`.
151
174
  */
152
175
  ask<Schema extends TSchema>(prompt: string, options: StructuredAskOptions<Schema>): Promise<Static<Schema>>;
@@ -14,5 +14,7 @@ export declare const ASK_OUTPUT_MAX_BYTES: number;
14
14
  export declare const NODE_GIST_MAX_CHARS = 120;
15
15
  /** Maximum Nested Nodes the Summary keeps per Agent; past it, exited nodes prune oldest-first. */
16
16
  export declare const AGENT_NODE_TABLE_MAX = 64;
17
+ /** Maximum Model Resolution fallbacks the Summary keeps per Agent; past it, the oldest prune. */
18
+ export declare const AGENT_MODEL_FALLBACK_MAX = 16;
17
19
  /** Prefix marking that the oldest pending Ask output was dropped to fit the wire cap. */
18
20
  export declare const ASK_OUTPUT_TRUNCATION_MARKER = "[\u2026output truncated\u2026]";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaag/cli",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -21,8 +21,8 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@earendil-works/pi-tui": "^0.84.0",
24
- "@yaag/runtime": "0.6.2",
25
- "@yaag/tui": "0.6.2",
24
+ "@yaag/runtime": "0.7.0",
25
+ "@yaag/tui": "0.7.0",
26
26
  "typebox": "1.3.7"
27
27
  }
28
28
  }
@@ -39,6 +39,8 @@ function renderPlainEvent(event: LifecycleEventBody, mode: PlainRenderMode): str
39
39
  event.maxFrameGapMs === undefined ? "" : ` max gap ${seconds(event.maxFrameGapMs)}`;
40
40
  return `[${event.agent}] ask #${event.index + 1} ${event.ok ? "done" : "failed"} ${seconds(event.durationMs)}${gap}`;
41
41
  }
42
+ case "model_fallback":
43
+ return `[${event.agent}] model ${event.failedModel} ${event.reason} — falling back to ${event.resolvedModel}`;
42
44
  case "agent_exit":
43
45
  return `[${event.agent}] exit ${tokens(event.tokens)} ${cost(event.cost)}${event.incomplete ? " (killed mid-ask, cost incomplete)" : ""}`;
44
46
  case "run_end": {
@@ -58,6 +60,7 @@ function minimalEvent(type: LifecycleEventBody["type"]): boolean {
58
60
  type === "agent_spawn" ||
59
61
  type === "ask_start" ||
60
62
  type === "ask_end" ||
63
+ type === "model_fallback" ||
61
64
  type === "agent_exit" ||
62
65
  type === "run_end"
63
66
  );