@yaag/runtime 0.8.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaag/runtime",
3
- "version": "0.8.3",
3
+ "version": "0.9.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -16,7 +16,7 @@ export interface AskEndOutcome {
16
16
 
17
17
  /**
18
18
  * Emits the Ask-scoped Lifecycle Events for one exchange, plus the Agent-scoped
19
- * `model_fallback` this exchange's fallback loop reports.
19
+ * `model_fallback` and `agent_model` this exchange's fallback loop reports.
20
20
  */
21
21
  export class AskEvents {
22
22
  readonly #emit: EventSink;
@@ -66,6 +66,15 @@ export class AskEvents {
66
66
  this.#emit({ type: "model_fallback", agent: this.#agent, ...fallback });
67
67
  };
68
68
 
69
+ /**
70
+ * The concrete model the Agent runs after a swap that landed (ADR-0041).
71
+ * Agent-scoped like `fallback`: the pattern side of the story stays in
72
+ * `model_fallback`, and this carries pi's own `provider/id`.
73
+ */
74
+ model = (model: string): void => {
75
+ this.#emit({ type: "agent_model", agent: this.#agent, model });
76
+ };
77
+
69
78
  /** A `normal` cause is the absent default, so ordinary settlements stay lean. */
70
79
  end(outcome: AskEndOutcome): void {
71
80
  const { durationMs, ok, maxFrameGapMs, cause = "normal" } = outcome;
@@ -112,6 +112,9 @@ class AskExchange {
112
112
  selection,
113
113
  });
114
114
  fallback.onSwapped(selection.model ?? swapped.model, swapped.model);
115
+ // Only a swap that landed reports a model: a refused `set_model` throws
116
+ // above and re-enters the loop, so the loop itself stays event-free.
117
+ this.#events.model(swapped.model);
115
118
  },
116
119
  });
117
120
  }
package/src/events.ts CHANGED
@@ -137,6 +137,20 @@ export type LifecycleEventBody =
137
137
  /** The candidate resolution picked next. */
138
138
  readonly resolvedModel: string;
139
139
  }
140
+ | {
141
+ /**
142
+ * The concrete model an Agent runs from now on, reported when it changes
143
+ * after spawn. Today the only producer is a successful mid-Ask model swap
144
+ * (ADR-0038, ADR-0041): `model_fallback` names the pattern the resolver
145
+ * picked, this names the `provider/id` pi landed on, so `agent.model`
146
+ * stays concrete. Spawn needs none: `agent_spawn.model` is already
147
+ * concrete.
148
+ */
149
+ readonly type: "agent_model";
150
+ readonly agent: string;
151
+ /** pi's concrete `provider/id` for the model the Agent runs now. */
152
+ readonly model: string;
153
+ }
140
154
  | {
141
155
  readonly type: "agent_usage";
142
156
  readonly agent: string;
@@ -7,7 +7,10 @@ export interface ModelFallback {
7
7
  readonly reason: ModelErrorReason;
8
8
  /** 0-based index of the failed attempt in the Agent's shared history. */
9
9
  readonly attempt: number;
10
- /** The candidate the resolver picked next. */
10
+ /**
11
+ * The candidate the resolver picked next. It is a yaag pattern, not a
12
+ * concrete `provider/id`, so it is never an Agent's model (ADR-0041).
13
+ */
11
14
  readonly resolvedModel: string;
12
15
  }
13
16
 
@@ -12,6 +12,11 @@ export type AgentState = "idle" | "asking" | "exited";
12
12
 
13
13
  /** Identity and accounting facts that apply in every observer Agent state. */
14
14
  interface AgentInfoBase {
15
+ /**
16
+ * Always the concrete `provider/id` pi reports — from `agent_spawn` at spawn
17
+ * and from `agent_model` after every Model Fallback that landed (ADR-0041).
18
+ * A `resolvedModel` pattern never lands here.
19
+ */
15
20
  readonly model: string | null;
16
21
  readonly cwd: string | null;
17
22
  readonly branch: string | null;
@@ -24,9 +24,8 @@ type ModelFallbackEvent = Extract<LifecycleEventBody, { readonly type: "model_fa
24
24
  * The Agent's `model` is left alone. `resolvedModel` is a yaag pattern, which
25
25
  * can be partial, and the event reports it before the swap is applied, while
26
26
  * `model` is pi's resolved `provider/id` for the model the Agent really runs.
27
- * `agent_spawn` is the only writer of that field: a mid-Ask swap updates the
28
- * Handle alone and reports no Lifecycle Event, so the Summary keeps the
29
- * spawn-time id.
27
+ * Only `agent_spawn` and `agent_model` write that field: a mid-Ask swap that
28
+ * lands reports the concrete model in its own `agent_model` event (ADR-0041).
30
29
  */
31
30
  export function applyModelFallback(
32
31
  agent: AgentRecord,
@@ -0,0 +1,18 @@
1
+ import type { LifecycleEventBody } from "../events.ts";
2
+ import type { AgentRecord } from "./summary-agent.ts";
3
+
4
+ type AgentModelEvent = Extract<LifecycleEventBody, { readonly type: "agent_model" }>;
5
+
6
+ /**
7
+ * Folds the concrete model an Agent runs now (ADR-0041).
8
+ *
9
+ * The event reports pi's `provider/id` after a swap landed, so it replaces
10
+ * `model` and nothing else: a model change is not a lifecycle state change, and
11
+ * `state`, `askIndex`, `activity`, `stateChangedAt` and the bounded fallback
12
+ * table stay as they are. An exited Agent is skipped, because its model is
13
+ * frozen together with its final accounting.
14
+ */
15
+ export function applyAgentModel(agent: AgentRecord, event: AgentModelEvent): AgentRecord {
16
+ if (agent.state === "exited") return agent;
17
+ return { ...agent, model: event.model };
18
+ }
@@ -13,6 +13,7 @@ import {
13
13
  totalsFromAgents,
14
14
  } from "./summary-agent.ts";
15
15
  import { applyModelFallback } from "./summary-fallbacks.ts";
16
+ import { applyAgentModel } from "./summary-model.ts";
16
17
  import { applyNodeUpdate } from "./summary-nodes.ts";
17
18
 
18
19
  export type { NodeState, NodeUsage, RunOutcome } from "../events.ts";
@@ -152,6 +153,12 @@ export function applyEvent(
152
153
  event.agent,
153
154
  applyModelFallback(summary.agents[event.agent] ?? placeholderAgent(), event, at),
154
155
  );
156
+ case "agent_model":
157
+ return withAgent(
158
+ summary,
159
+ event.agent,
160
+ applyAgentModel(summary.agents[event.agent] ?? placeholderAgent(), event),
161
+ );
155
162
  case "agent_usage":
156
163
  return withAgent(summary, event.agent, setUsage(summary.agents[event.agent], event, at));
157
164
  case "ask_end":
@@ -54,6 +54,8 @@ export interface FakeTransportOptions extends FakePromptScript {
54
54
  readonly schemaCommandError?: string;
55
55
  /** The snapshot answered to `get_available_models`; defaults to this fake's own model. */
56
56
  readonly models?: readonly AvailableModel[];
57
+ /** The model this fake reports before any swap; defaults to a placeholder id. */
58
+ readonly model?: string;
57
59
  /** Makes a `set_model` command fail, as pi does for a pair it does not know. */
58
60
  readonly setModelError?: string;
59
61
  /** Makes a `set_thinking_level` command fail. */
@@ -75,7 +77,7 @@ export interface FakeTransportOptions extends FakePromptScript {
75
77
  * It never spawns or closes a real Agent process.
76
78
  */
77
79
  export class FakeTransport implements AgentTransport {
78
- #model = "test/model";
80
+ #model: string;
79
81
 
80
82
  /** The model this fake currently reports, which a `set_model` swap updates. */
81
83
  get model(): string {
@@ -101,6 +103,7 @@ export class FakeTransport implements AgentTransport {
101
103
 
102
104
  constructor(options: FakeTransportOptions = {}) {
103
105
  this.#options = options;
106
+ this.#model = options.model ?? "test/model";
104
107
  }
105
108
 
106
109
  send(frame: Frame): void {