@yaag/cli 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.
Files changed (50) hide show
  1. package/README.md +31 -0
  2. package/assets/types/runtime/agent/agent.d.ts +16 -1
  3. package/assets/types/runtime/agent/define-agent.d.ts +20 -3
  4. package/assets/types/runtime/agent/spawn-extensions.d.ts +39 -0
  5. package/assets/types/runtime/agent/spawn-request.d.ts +24 -0
  6. package/assets/types/runtime/agent/spawn.d.ts +3 -0
  7. package/assets/types/runtime/ask/ask-exchange-events.d.ts +7 -1
  8. package/assets/types/runtime/ask/ask-exchange-options.d.ts +12 -0
  9. package/assets/types/runtime/ask/index.d.ts +1 -0
  10. package/assets/types/runtime/cassette/cassette-schema.d.ts +1 -0
  11. package/assets/types/runtime/cassette/cassette.d.ts +5 -0
  12. package/assets/types/runtime/cassette/replay-divergence.d.ts +10 -2
  13. package/assets/types/runtime/config/config-file.d.ts +22 -0
  14. package/assets/types/runtime/config/config-issues.d.ts +12 -0
  15. package/assets/types/runtime/config/config-paths.d.ts +22 -0
  16. package/assets/types/runtime/config/config-schema.d.ts +17 -0
  17. package/assets/types/runtime/config/effective-config.d.ts +42 -0
  18. package/assets/types/runtime/config/index.d.ts +8 -0
  19. package/assets/types/runtime/errors.d.ts +16 -2
  20. package/assets/types/runtime/events.d.ts +18 -0
  21. package/assets/types/runtime/extension/extension-paths.d.ts +10 -2
  22. package/assets/types/runtime/index.d.ts +4 -2
  23. package/assets/types/runtime/model/index.d.ts +8 -1
  24. package/assets/types/runtime/model/model-error-history.d.ts +20 -0
  25. package/assets/types/runtime/model/model-failure.d.ts +18 -0
  26. package/assets/types/runtime/model/model-fallback.d.ts +13 -0
  27. package/assets/types/runtime/model/model-match.d.ts +31 -0
  28. package/assets/types/runtime/model/model-resolution.d.ts +44 -6
  29. package/assets/types/runtime/model/model-swap.d.ts +27 -0
  30. package/assets/types/runtime/model/recorded-resolution.d.ts +37 -0
  31. package/assets/types/runtime/model/resolution-loop.d.ts +47 -0
  32. package/assets/types/runtime/run/run.d.ts +7 -0
  33. package/assets/types/runtime/summary/index.d.ts +1 -1
  34. package/assets/types/runtime/summary/summary-agent.d.ts +8 -0
  35. package/assets/types/runtime/summary/summary-fallbacks.d.ts +30 -0
  36. package/assets/types/runtime/summary/summary.d.ts +3 -1
  37. package/assets/types/runtime/transport/fake-transport.d.ts +9 -1
  38. package/assets/types/runtime/transport/index.d.ts +1 -0
  39. package/assets/types/runtime/transport/stderr-tail.d.ts +12 -0
  40. package/assets/types/runtime/transport/transport.d.ts +15 -0
  41. package/assets/types/runtime/types.d.ts +33 -5
  42. package/assets/types/runtime/wire-constants.d.ts +2 -0
  43. package/package.json +3 -3
  44. package/src/argv.ts +23 -4
  45. package/src/cli.ts +9 -0
  46. package/src/config/index.ts +8 -0
  47. package/src/config/program-directory.ts +34 -0
  48. package/src/config/run-config.ts +54 -0
  49. package/src/terminal/render.ts +3 -0
  50. package/src/terminal/run-invocation.ts +4 -1
@@ -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>;
@@ -1,4 +1,5 @@
1
1
  import { type Cassette, type CassetteSink } from "../cassette/index.ts";
2
+ import { type EffectiveConfig } from "../config/index.ts";
2
3
  import type { StampedEventSink } from "../events.ts";
3
4
  import type { TransportFactory } from "../transport/index.ts";
4
5
  import { type SkillProbeFactory } from "../transport/index.ts";
@@ -33,6 +34,12 @@ export interface RunOptions {
33
34
  readonly signal?: AbortSignal;
34
35
  /** Overrides the default checkpoint directory a stopped Run publishes into (ADR-0021). */
35
36
  readonly checkpointDir?: string;
37
+ /**
38
+ * Effective Config for this Run, read once by the launcher (ADR-0040). The
39
+ * Run never loads it itself: omission runs config-free, so no Run picks up a
40
+ * config it was not given, and every test stays hermetic.
41
+ */
42
+ readonly config?: EffectiveConfig;
36
43
  }
37
44
  /**
38
45
  * Executes one Orchestration Program and resolves with its return value.
@@ -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
@@ -156,6 +157,11 @@ export interface OpenOptions {
156
157
  readonly resolvedSkillPaths?: readonly string[];
157
158
  /** Absolute launch-ready extension paths resolved above the seam, emitted as repeated `-e` flags. */
158
159
  readonly resolvedExtensionPaths?: readonly string[];
160
+ /**
161
+ * Declared extensions in final concatenation order, unresolved (ADR-0040).
162
+ * Spawn identity only: it never becomes argv, and it is absent when empty.
163
+ */
164
+ readonly declaredExtensions?: readonly string[];
159
165
  /** Session storage directory. Used by the e2e suite to stay out of ~/.pi (ticket 06). */
160
166
  readonly sessionDir?: string;
161
167
  /** Resumes an existing pi session, translated to `--session <path>`. */
@@ -176,4 +182,13 @@ export type TransportStartupObserver = (startup: TransportStartup) => void;
176
182
  /** How the layer above obtains transports. Swapped wholesale for replay. */
177
183
  export interface TransportFactory {
178
184
  open(options: OpenOptions, observeStartup?: TransportStartupObserver): Promise<AgentTransport>;
185
+ /**
186
+ * The resolved selection this factory recorded for the Agent it would open
187
+ * next, so a Cassette-backed spawn adopts the recorded outcome instead of
188
+ * re-running Model Resolution (ADR-0037, ADR-0038, ADR-0039).
189
+ *
190
+ * A pure peek: it must not advance the factory's spawn cursor. Live factories
191
+ * record nothing and omit it.
192
+ */
193
+ recordedSpawn?(options: OpenOptions): RecordedSpawnSelection | undefined;
179
194
  }
@@ -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`). */
@@ -32,6 +36,11 @@ export interface SpawnOptions {
32
36
  * resolve from the Orchestration Program file; each becomes `pi -e <path>`.
33
37
  */
34
38
  readonly extensions?: readonly string[];
39
+ /**
40
+ * Load the Effective Config's `agents.extensions` for this Agent. Default
41
+ * true; false spawns with only the extensions this call declares (ADR-0040).
42
+ */
43
+ readonly configExtensions?: boolean;
35
44
  /**
36
45
  * Tool allowlist layered over the ADR-0026 baseline; omission is tool-free when hermetic.
37
46
  * `disallowedTools` applies last. A non-empty surviving allowlist is verified after startup;
@@ -56,7 +65,12 @@ export interface SpawnOptions {
56
65
  /** Request a fresh Git worktree. The requested cwd remains the base until spawn resolves. */
57
66
  readonly worktree?: boolean;
58
67
  }
59
- /** Spawn options after Model Resolution has settled one model and thinking level. */
68
+ /**
69
+ * Spawn options after Model Resolution has settled one model and thinking level.
70
+ *
71
+ * This is the settled shape, and it is what the Cassette identity hashes
72
+ * (ADR-0039).
73
+ */
60
74
  export interface ResolvedSpawnOptions extends Omit<SpawnOptions, "model" | "thinking"> {
61
75
  readonly model?: string;
62
76
  readonly thinking?: ThinkingLevel;
@@ -79,6 +93,11 @@ export interface SpawnOverrides {
79
93
  * Agent, permits one further turn (or a fixed duration grace), then aborts and
80
94
  * rejects with `ASK_LIMIT` if it has not settled. This leaves the Handle alive.
81
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.
82
101
  */
83
102
  export interface AskOptions {
84
103
  /** Reject and kill the Agent if it has not settled in time. Unset = no bound. */
@@ -101,7 +120,10 @@ export interface AskOptions {
101
120
  * live Ask by default (ADR-0029). On expiry yaag probes the Agent and either
102
121
  * recovers a missed settlement or rejects with `ASK_STALLED`. Omission uses
103
122
  * the 10-minute default. `false`, and any value that is not above zero,
104
- * 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).
105
127
  */
106
128
  readonly stallMs?: number | false;
107
129
  /** Per-Ask replacement for the runtime's wrap-up steering message. */
@@ -134,7 +156,10 @@ export interface Handle {
134
156
  readonly cwd: string;
135
157
  /** Fresh branch for a worktree Agent; undefined for ordinary Agents. */
136
158
  readonly branch: string | undefined;
137
- /** Model id as reported by the Agent's own `get_state` — not the requested pattern. */
159
+ /**
160
+ * Model id as reported by the Agent's own `get_state` — not the requested
161
+ * pattern. A mid-Ask model swap updates it (ADR-0038).
162
+ */
138
163
  readonly model: string;
139
164
  /**
140
165
  * Sends a prompt and resolves after the Agent's turn settles.
@@ -147,6 +172,9 @@ export interface Handle {
147
172
  * (default 3) and then rejects recoverably with `ASK_INVALID_OUTPUT` after an
148
173
  * abort settlement; a reported value the schema rejects fails the same way
149
174
  * without a correction. These recoverable outcomes leave the Handle reusable.
175
+ * A retry after a Model Fallback discards the result the failed attempt
176
+ * reported, and arms the collector again, so a stale result cannot settle the
177
+ * retried Ask (ADR-0032, ADR-0038).
150
178
  * A concurrent call rejects with `AGENT_BUSY`.
151
179
  */
152
180
  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.8.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.8.0",
25
+ "@yaag/tui": "0.8.0",
26
26
  "typebox": "1.3.7"
27
27
  }
28
28
  }
package/src/argv.ts CHANGED
@@ -5,9 +5,9 @@
5
5
  import { resolve } from "node:path";
6
6
 
7
7
  export const USAGE = `usage:
8
- yaag run <program.ts> [--quiet] [--args <json>] [--record <file>] [--replay <file>] [--resume <file>] [--events-fd <n>]
9
- yaag run --eval <source> [--quiet] [--args <json>] [--record <file>] [--replay <file>] [--resume <file>] [--events-fd <n>]
10
- yaag run --eval-fd <n> [--quiet] [--args <json>] [--record <file>] [--replay <file>] [--resume <file>] [--events-fd <n>]
8
+ yaag run <program.ts> [--quiet] [--args <json>] [--record <file>] [--replay <file>] [--resume <file>] [--events-fd <n>] [--config <file>] [--no-config]
9
+ yaag run --eval <source> [--quiet] [--args <json>] [--record <file>] [--replay <file>] [--resume <file>] [--events-fd <n>] [--config <file>] [--no-config]
10
+ yaag run --eval-fd <n> [--quiet] [--args <json>] [--record <file>] [--replay <file>] [--resume <file>] [--events-fd <n>] [--config <file>] [--no-config]
11
11
  yaag describe <program.ts>
12
12
  yaag setup-workspace [dir]
13
13
 
@@ -20,6 +20,12 @@ must be a file.
20
20
  close that descriptor. It keeps the source out of the process argument list,
21
21
  where every local user can read it. The yaag extension always uses it.
22
22
 
23
+ --config <file> reads one more config file for this Run. Give it one time only.
24
+ yaag reads it after the global config and after the project config.
25
+
26
+ --no-config tells yaag to ignore the global config and the project config. It
27
+ does not ignore --config.
28
+
23
29
  A --resume or --replay Run also needs the program: give the program file,
24
30
  --eval <source>, or --eval-fd <n>. A Cassette holds the history of a Run, and never the program
25
31
  to run.
@@ -44,6 +50,8 @@ export type ParsedArgv =
44
50
  readonly record?: string;
45
51
  readonly replay?: string;
46
52
  readonly resume?: string;
53
+ readonly config?: string;
54
+ readonly noConfig?: boolean;
47
55
  }
48
56
  | { readonly ok: true; readonly command: "describe"; readonly file: string }
49
57
  | { readonly ok: true; readonly command: "setup-workspace"; readonly dir: string }
@@ -72,12 +80,16 @@ function parseRun(tokens: readonly string[]): ParsedArgv {
72
80
  let record: string | undefined;
73
81
  let replay: string | undefined;
74
82
  let resume: string | undefined;
83
+ let config: string | undefined;
84
+ let noConfig = false;
75
85
  let quiet = false;
76
86
 
77
87
  for (let index = 0; index < tokens.length; index += 1) {
78
88
  const token = tokens[index] ?? "";
79
89
  if (token === "--quiet") {
80
90
  quiet = true;
91
+ } else if (token === "--no-config") {
92
+ noConfig = true;
81
93
  } else if (
82
94
  token === "--args" ||
83
95
  token === "--eval" ||
@@ -85,7 +97,8 @@ function parseRun(tokens: readonly string[]): ParsedArgv {
85
97
  token === "--events-fd" ||
86
98
  token === "--record" ||
87
99
  token === "--replay" ||
88
- token === "--resume"
100
+ token === "--resume" ||
101
+ token === "--config"
89
102
  ) {
90
103
  const value = tokens[index + 1];
91
104
  if (value === undefined) return failure(`${token} needs a value\n${USAGE}`);
@@ -108,6 +121,10 @@ function parseRun(tokens: readonly string[]): ParsedArgv {
108
121
  record = value;
109
122
  } else if (token === "--replay") {
110
123
  replay = value;
124
+ } else if (token === "--config") {
125
+ // One Run Config only: two of them would leave the merge order unstated.
126
+ if (config !== undefined) return failure(`--config may be given once\n${USAGE}`);
127
+ config = value;
111
128
  } else {
112
129
  resume = value;
113
130
  }
@@ -134,6 +151,8 @@ function parseRun(tokens: readonly string[]): ParsedArgv {
134
151
  ...(record === undefined ? {} : { record }),
135
152
  ...(replay === undefined ? {} : { replay }),
136
153
  ...(resume === undefined ? {} : { resume }),
154
+ ...(config === undefined ? {} : { config }),
155
+ ...(noConfig ? { noConfig: true } : {}),
137
156
  };
138
157
  }
139
158
 
package/src/cli.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  type StampedEventSink,
11
11
  } from "@yaag/runtime";
12
12
  import { parseArgv } from "./argv.ts";
13
+ import { loadRunConfig } from "./config/index.ts";
13
14
  import { loadProgram, loadRunProgram, registerRuntimeAlias } from "./program/index.ts";
14
15
  import { setupWorkspace } from "./setup-workspace.ts";
15
16
  import {
@@ -47,6 +48,13 @@ export async function main(argv: readonly string[]): Promise<number> {
47
48
  await loadCassette(parsed.resume);
48
49
  }
49
50
  if (parsed.command === "describe") return describe(await loadProgram(resolve(parsed.file)));
51
+ // Before the program import: a bad config must fail the Run at start, and
52
+ // never after an arbitrary program module ran its top level (ADR-0040).
53
+ const config = await loadRunConfig({
54
+ programFile: parsed.program.kind === "file" ? resolve(parsed.program.file) : undefined,
55
+ ...(parsed.config === undefined ? {} : { configPath: parsed.config }),
56
+ ...(parsed.noConfig === true ? { noConfig: true } : {}),
57
+ });
50
58
  const { program, programFile, programSource } = await loadRunProgram(parsed.program);
51
59
  return await run(program, {
52
60
  programFile,
@@ -56,6 +64,7 @@ export async function main(argv: readonly string[]): Promise<number> {
56
64
  record: parsed.record,
57
65
  replay: parsed.replay,
58
66
  resume: parsed.resume,
67
+ config,
59
68
  quiet: parsed.quiet,
60
69
  });
61
70
  } catch (error) {