@yaag/runtime 0.8.3 → 0.10.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 +1 -1
- package/src/agent/spawn-parent.ts +21 -0
- package/src/agent/spawn-request.ts +5 -1
- package/src/agent/spawn.ts +33 -7
- package/src/ask/ask-exchange-events.ts +10 -1
- package/src/ask/ask-exchange.ts +3 -0
- package/src/cassette/cassette-schema.ts +1 -0
- package/src/cassette/cassette.ts +6 -0
- package/src/cassette/replay-divergence.ts +7 -1
- package/src/events.ts +24 -0
- package/src/index.ts +1 -0
- package/src/model/model-fallback.ts +4 -1
- package/src/summary/index.ts +1 -0
- package/src/summary/summary-agent.ts +22 -2
- package/src/summary/summary-fallbacks.ts +2 -3
- package/src/summary/summary-model.ts +18 -0
- package/src/summary/summary.ts +8 -0
- package/src/transport/fake-transport.ts +4 -1
- package/src/transport/transport.ts +5 -0
- package/src/types.ts +12 -1
package/package.json
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Agent } from "./agent.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolves a Parent Link to the parent Agent's name.
|
|
5
|
+
*
|
|
6
|
+
* Only a Handle this Run already spawned is accepted, which is what makes a
|
|
7
|
+
* lineage cycle impossible by construction: a Handle exists only after its own
|
|
8
|
+
* spawn resolved. Liveness is deliberately not checked — a cleanly exited Agent
|
|
9
|
+
* remains a valid parent, because the link states tree position, not a
|
|
10
|
+
* dependency on a live process.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveParent(parent: unknown, agents: readonly Agent[]): string | undefined {
|
|
13
|
+
if (parent === undefined) return undefined;
|
|
14
|
+
const known = agents.find((agent) => agent === parent);
|
|
15
|
+
if (known === undefined) {
|
|
16
|
+
throw new TypeError(
|
|
17
|
+
'spawn option "parent" must be a Handle returned by ctx.spawn() in this Run',
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return known.name;
|
|
21
|
+
}
|
|
@@ -9,6 +9,8 @@ export interface OpenRequestOptions {
|
|
|
9
9
|
readonly spawnOptions: SpawnOptions;
|
|
10
10
|
readonly resolvedExtensionPaths?: readonly string[];
|
|
11
11
|
readonly declaredExtensions?: readonly string[];
|
|
12
|
+
/** Resolved parent Agent name (Parent Link); spawn identity only, never argv. */
|
|
13
|
+
readonly parent?: string;
|
|
12
14
|
readonly sessionDir: string | undefined;
|
|
13
15
|
}
|
|
14
16
|
|
|
@@ -51,6 +53,7 @@ export function openRequest(options: OpenRequestOptions): OpenOptions {
|
|
|
51
53
|
...(options.declaredExtensions === undefined || options.declaredExtensions.length === 0
|
|
52
54
|
? {}
|
|
53
55
|
: { declaredExtensions: options.declaredExtensions }),
|
|
56
|
+
...(options.parent === undefined ? {} : { parent: options.parent }),
|
|
54
57
|
...(spawnOptions.worktree === true ? { worktree: true as const } : {}),
|
|
55
58
|
...(options.sessionDir === undefined ? {} : { sessionDir: options.sessionDir }),
|
|
56
59
|
};
|
|
@@ -61,7 +64,8 @@ export function withSelection(
|
|
|
61
64
|
options: SpawnOptions,
|
|
62
65
|
selection: ModelSelection,
|
|
63
66
|
): ResolvedSpawnOptions {
|
|
64
|
-
|
|
67
|
+
// `parent` is dropped here as well: a Handle must never reach recorded options.
|
|
68
|
+
const { model: _model, thinking: _thinking, parent: _parent, ...rest } = options;
|
|
65
69
|
return {
|
|
66
70
|
...rest,
|
|
67
71
|
...(selection.model === undefined ? {} : { model: selection.model }),
|
package/src/agent/spawn.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { Agent } from "./agent.ts";
|
|
|
22
22
|
import { uniqueAgentName } from "./agent-names.ts";
|
|
23
23
|
import { type AgentDefinition, agentDefinitionConfig, isAgentDefinition } from "./define-agent.ts";
|
|
24
24
|
import { resolveSpawnExtensions } from "./spawn-extensions.ts";
|
|
25
|
+
import { resolveParent } from "./spawn-parent.ts";
|
|
25
26
|
import { openRequest, withSelection } from "./spawn-request.ts";
|
|
26
27
|
|
|
27
28
|
/** Dependencies for one Run's Agent-spawn gate. */
|
|
@@ -58,6 +59,8 @@ export function makeSpawn(deps: SpawnDependencies): SpawnGate {
|
|
|
58
59
|
const request = resolveRequest(definitionOrOptions, overrides);
|
|
59
60
|
const cwd = resolve(request.spawnOptions.cwd ?? process.cwd());
|
|
60
61
|
const name = uniqueAgentName(request.spawnOptions.name, deps.agents.length, taken);
|
|
62
|
+
// Resolved before any transport work, so a bad Parent Link costs nothing.
|
|
63
|
+
const parent = resolveParent(request.parent, deps.agents);
|
|
61
64
|
const resolution = normalizeModelResolution(request.spawnOptions);
|
|
62
65
|
// One history per Agent: its spawn loop and every mid-Ask fallback loop
|
|
63
66
|
// append to it, so a resolver sees every candidate that already failed.
|
|
@@ -94,6 +97,7 @@ export function makeSpawn(deps: SpawnDependencies): SpawnGate {
|
|
|
94
97
|
cwd,
|
|
95
98
|
spawnOptions: settled,
|
|
96
99
|
...extensionFields,
|
|
100
|
+
...(parent === undefined ? {} : { parent }),
|
|
97
101
|
sessionDir: deps.sessionDir,
|
|
98
102
|
}),
|
|
99
103
|
spawnOptions: settled,
|
|
@@ -107,6 +111,7 @@ export function makeSpawn(deps: SpawnDependencies): SpawnGate {
|
|
|
107
111
|
cwd,
|
|
108
112
|
spawnOptions: request.spawnOptions,
|
|
109
113
|
...extensionFields,
|
|
114
|
+
...(parent === undefined ? {} : { parent }),
|
|
110
115
|
sessionDir: deps.sessionDir,
|
|
111
116
|
}),
|
|
112
117
|
);
|
|
@@ -157,6 +162,7 @@ export function makeSpawn(deps: SpawnDependencies): SpawnGate {
|
|
|
157
162
|
cwd: resolvedCwd,
|
|
158
163
|
...(branch === undefined ? {} : { branch }),
|
|
159
164
|
...(sessionFile === undefined ? {} : { sessionFile }),
|
|
165
|
+
...(parent === undefined ? {} : { parent }),
|
|
160
166
|
});
|
|
161
167
|
return agent;
|
|
162
168
|
} catch (error) {
|
|
@@ -178,10 +184,22 @@ export function makeSpawn(deps: SpawnDependencies): SpawnGate {
|
|
|
178
184
|
};
|
|
179
185
|
}
|
|
180
186
|
|
|
181
|
-
|
|
187
|
+
/** Topology overrides after validation; `parent` stays raw for `resolveParent`. */
|
|
188
|
+
type SpawnTopology = Omit<SpawnOverrides, "parent">;
|
|
189
|
+
|
|
190
|
+
type MutableSpawnOverrides = { -readonly [Key in keyof SpawnTopology]: SpawnTopology[Key] };
|
|
191
|
+
|
|
192
|
+
/** One validated override object: checked topology plus the unchecked Parent Link. */
|
|
193
|
+
interface ValidatedOverrides {
|
|
194
|
+
readonly topology: SpawnTopology;
|
|
195
|
+
readonly parent: unknown;
|
|
196
|
+
}
|
|
182
197
|
|
|
183
198
|
interface SpawnRequest {
|
|
199
|
+
/** Never carries `parent`: a Handle must not reach the recorded options. */
|
|
184
200
|
readonly spawnOptions: SpawnOptions;
|
|
201
|
+
/** The raw Parent Link option, validated by `resolveParent`. */
|
|
202
|
+
readonly parent: unknown;
|
|
185
203
|
readonly askDefaults: AskOptions | undefined;
|
|
186
204
|
/** Definition identity stays separate from a topology-overridden Agent name. */
|
|
187
205
|
readonly definitionName: string | undefined;
|
|
@@ -192,9 +210,10 @@ function resolveRequest(
|
|
|
192
210
|
overrides: SpawnOverrides | undefined,
|
|
193
211
|
): SpawnRequest {
|
|
194
212
|
if (!isAgentDefinition(definitionOrOptions)) {
|
|
195
|
-
|
|
213
|
+
const { parent, ...spawnOptions } = definitionOrOptions;
|
|
214
|
+
return { spawnOptions, parent, askDefaults: undefined, definitionName: undefined };
|
|
196
215
|
}
|
|
197
|
-
const topology = validateOverrides(overrides);
|
|
216
|
+
const { topology, parent } = validateOverrides(overrides);
|
|
198
217
|
const config = agentDefinitionConfig(definitionOrOptions);
|
|
199
218
|
return {
|
|
200
219
|
spawnOptions: {
|
|
@@ -218,27 +237,32 @@ function resolveRequest(
|
|
|
218
237
|
? { systemPrompt: config.prompt }
|
|
219
238
|
: { appendSystemPrompt: config.prompt }),
|
|
220
239
|
},
|
|
240
|
+
parent,
|
|
221
241
|
askDefaults: config.askDefaults,
|
|
222
242
|
definitionName: config.name,
|
|
223
243
|
};
|
|
224
244
|
}
|
|
225
245
|
|
|
226
|
-
function validateOverrides(overrides: unknown):
|
|
227
|
-
if (overrides === undefined) return {};
|
|
246
|
+
function validateOverrides(overrides: unknown): ValidatedOverrides {
|
|
247
|
+
if (overrides === undefined) return { topology: {}, parent: undefined };
|
|
228
248
|
if (typeof overrides !== "object" || overrides === null || Array.isArray(overrides)) {
|
|
229
249
|
throw new TypeError(
|
|
230
250
|
"spawn overrides must be an object: definitions own policy and spawn overrides own topology",
|
|
231
251
|
);
|
|
232
252
|
}
|
|
233
253
|
for (const key of Object.keys(overrides)) {
|
|
234
|
-
if (key !== "name" && key !== "cwd" && key !== "worktree") {
|
|
254
|
+
if (key !== "name" && key !== "cwd" && key !== "worktree" && key !== "parent") {
|
|
235
255
|
throw new TypeError(
|
|
236
256
|
`spawn override "${key}" is not allowed: definitions own policy and spawn overrides own topology`,
|
|
237
257
|
);
|
|
238
258
|
}
|
|
239
259
|
}
|
|
240
260
|
const topology: MutableSpawnOverrides = {};
|
|
261
|
+
// `parent` is never narrowed here: `resolveParent` owns that check, so the
|
|
262
|
+
// error text stays in one place and no unchecked value becomes a Handle.
|
|
263
|
+
let parent: unknown;
|
|
241
264
|
for (const [key, value] of Object.entries(overrides)) {
|
|
265
|
+
if (key === "parent") parent = value;
|
|
242
266
|
if (key === "name") {
|
|
243
267
|
if (value !== undefined && typeof value !== "string") {
|
|
244
268
|
throw new TypeError('spawn override "name" must be a string when present');
|
|
@@ -258,7 +282,7 @@ function validateOverrides(overrides: unknown): SpawnOverrides {
|
|
|
258
282
|
topology.worktree = value;
|
|
259
283
|
}
|
|
260
284
|
}
|
|
261
|
-
return topology;
|
|
285
|
+
return { topology, parent };
|
|
262
286
|
}
|
|
263
287
|
|
|
264
288
|
function isSpawnFailure(error: unknown): error is YaagError {
|
|
@@ -272,6 +296,7 @@ interface OpenTransportOptions {
|
|
|
272
296
|
readonly spawnOptions: ResolvedSpawnOptions;
|
|
273
297
|
readonly resolvedExtensionPaths?: readonly string[];
|
|
274
298
|
readonly declaredExtensions?: readonly string[];
|
|
299
|
+
readonly parent?: string;
|
|
275
300
|
readonly sessionDir: string | undefined;
|
|
276
301
|
}
|
|
277
302
|
|
|
@@ -294,6 +319,7 @@ async function openTransport(options: OpenTransportOptions): Promise<OpenedTrans
|
|
|
294
319
|
...(options.declaredExtensions === undefined
|
|
295
320
|
? {}
|
|
296
321
|
: { declaredExtensions: options.declaredExtensions }),
|
|
322
|
+
...(options.parent === undefined ? {} : { parent: options.parent }),
|
|
297
323
|
sessionDir: options.sessionDir,
|
|
298
324
|
}),
|
|
299
325
|
(report) => Object.assign(startup, report),
|
|
@@ -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;
|
package/src/ask/ask-exchange.ts
CHANGED
|
@@ -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/cassette/cassette.ts
CHANGED
|
@@ -88,6 +88,11 @@ export interface CassetteSpawn {
|
|
|
88
88
|
* across machines (ADR-0040). Absent when the Agent ran no extension.
|
|
89
89
|
*/
|
|
90
90
|
readonly declaredExtensions?: readonly string[];
|
|
91
|
+
/**
|
|
92
|
+
* Resolved name of the Agent named as this Agent's parent (Parent Link).
|
|
93
|
+
* Part of spawn identity; absent for a root Agent and for older Cassettes.
|
|
94
|
+
*/
|
|
95
|
+
readonly parent?: string;
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
/** The frames attributed to one Ask marker. */
|
|
@@ -272,6 +277,7 @@ function spawnIdentity(options: OpenOptions): CassetteSpawn {
|
|
|
272
277
|
...(options.declaredExtensions === undefined || options.declaredExtensions.length === 0
|
|
273
278
|
? {}
|
|
274
279
|
: { declaredExtensions: [...options.declaredExtensions] }),
|
|
280
|
+
...(options.parent === undefined ? {} : { parent: options.parent }),
|
|
275
281
|
...(options.worktree === true ? { worktree: true } : {}),
|
|
276
282
|
};
|
|
277
283
|
}
|
|
@@ -145,7 +145,12 @@ const SPAWN_IDENTITY_FIELDS = [
|
|
|
145
145
|
] as const;
|
|
146
146
|
|
|
147
147
|
/** The spawn identity fields plus the Agent name, which only an open request carries. */
|
|
148
|
-
const SPAWN_OPEN_FIELDS = [
|
|
148
|
+
const SPAWN_OPEN_FIELDS = [
|
|
149
|
+
"name",
|
|
150
|
+
...SPAWN_IDENTITY_FIELDS,
|
|
151
|
+
"declaredExtensions",
|
|
152
|
+
"parent",
|
|
153
|
+
] as const;
|
|
149
154
|
|
|
150
155
|
/** The listed fields whose canonical JSON differs between two identity records. */
|
|
151
156
|
function changedAmong<Key extends string>(
|
|
@@ -216,6 +221,7 @@ function spawnHash(options: CassetteSpawn | OpenOptions): string {
|
|
|
216
221
|
...(options.declaredExtensions === undefined || options.declaredExtensions.length === 0
|
|
217
222
|
? {}
|
|
218
223
|
: { declaredExtensions: options.declaredExtensions }),
|
|
224
|
+
...(options.parent === undefined ? {} : { parent: options.parent }),
|
|
219
225
|
}),
|
|
220
226
|
);
|
|
221
227
|
return hasher.digest("hex");
|
package/src/events.ts
CHANGED
|
@@ -20,6 +20,12 @@ export type { ModelErrorReason } from "./model/index.ts";
|
|
|
20
20
|
*/
|
|
21
21
|
export type RunOutcome = "completed" | "failed" | "stopped" | "paused" | "interrupted";
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* How an Agent came to be. An absent value on an event means "spawn"; "fork"
|
|
25
|
+
* arrives with the forking spec.
|
|
26
|
+
*/
|
|
27
|
+
export type SpawnOrigin = "spawn" | "fork";
|
|
28
|
+
|
|
23
29
|
/** The current, Ask-scoped observer projection derived from Agent frames. */
|
|
24
30
|
export type AgentActivity =
|
|
25
31
|
| { readonly type: "thinking" }
|
|
@@ -65,6 +71,10 @@ export type LifecycleEventBody =
|
|
|
65
71
|
* Cassette-playback Agents and events from older CLIs.
|
|
66
72
|
*/
|
|
67
73
|
readonly sessionFile?: string;
|
|
74
|
+
/** Resolved name of the Agent named as this Agent's parent (Parent Link). */
|
|
75
|
+
readonly parent?: string;
|
|
76
|
+
/** How the Agent came to be; absent means "spawn". */
|
|
77
|
+
readonly origin?: SpawnOrigin;
|
|
68
78
|
}
|
|
69
79
|
| {
|
|
70
80
|
readonly type: "ask_start";
|
|
@@ -137,6 +147,20 @@ export type LifecycleEventBody =
|
|
|
137
147
|
/** The candidate resolution picked next. */
|
|
138
148
|
readonly resolvedModel: string;
|
|
139
149
|
}
|
|
150
|
+
| {
|
|
151
|
+
/**
|
|
152
|
+
* The concrete model an Agent runs from now on, reported when it changes
|
|
153
|
+
* after spawn. Today the only producer is a successful mid-Ask model swap
|
|
154
|
+
* (ADR-0038, ADR-0041): `model_fallback` names the pattern the resolver
|
|
155
|
+
* picked, this names the `provider/id` pi landed on, so `agent.model`
|
|
156
|
+
* stays concrete. Spawn needs none: `agent_spawn.model` is already
|
|
157
|
+
* concrete.
|
|
158
|
+
*/
|
|
159
|
+
readonly type: "agent_model";
|
|
160
|
+
readonly agent: string;
|
|
161
|
+
/** pi's concrete `provider/id` for the model the Agent runs now. */
|
|
162
|
+
readonly model: string;
|
|
163
|
+
}
|
|
140
164
|
| {
|
|
141
165
|
readonly type: "agent_usage";
|
|
142
166
|
readonly agent: string;
|
package/src/index.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
package/src/summary/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { AgentActivity } from "../events.ts";
|
|
1
|
+
import type { AgentActivity, SpawnOrigin } from "../events.ts";
|
|
2
2
|
import type { TokenBreakdown, WorktreeResolution } from "../transport/index.ts";
|
|
3
3
|
import type { ModelFallbackInfo } from "./summary-fallbacks.ts";
|
|
4
4
|
import type { NodeInfo } from "./summary-nodes.ts";
|
|
5
5
|
|
|
6
|
-
export type { AgentActivity } from "../events.ts";
|
|
6
|
+
export type { AgentActivity, SpawnOrigin } from "../events.ts";
|
|
7
7
|
export type { ModelFallbackInfo } from "./summary-fallbacks.ts";
|
|
8
8
|
export type { NodeInfo } from "./summary-nodes.ts";
|
|
9
9
|
|
|
@@ -12,11 +12,20 @@ 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;
|
|
18
23
|
/** pi's session file for this Agent, when the spawn reported one; a Peek reads it. */
|
|
19
24
|
readonly sessionFile: string | null;
|
|
25
|
+
/** The Agent named as this one's parent (Parent Link), or null for a root Agent. */
|
|
26
|
+
readonly parent: string | null;
|
|
27
|
+
/** How the Agent came to be; "spawn" until forking ships. */
|
|
28
|
+
readonly origin: SpawnOrigin;
|
|
20
29
|
readonly activity: AgentActivity | null;
|
|
21
30
|
readonly tokens: TokenBreakdown | null;
|
|
22
31
|
readonly cost: number | null;
|
|
@@ -92,6 +101,8 @@ export function placeholderAgent(): IdleAgentInfo {
|
|
|
92
101
|
cwd: null,
|
|
93
102
|
branch: null,
|
|
94
103
|
sessionFile: null,
|
|
104
|
+
parent: null,
|
|
105
|
+
origin: "spawn",
|
|
95
106
|
state: "idle",
|
|
96
107
|
askIndex: null,
|
|
97
108
|
promptGist: null,
|
|
@@ -120,6 +131,8 @@ export function spawnAgent(
|
|
|
120
131
|
readonly cwd: string;
|
|
121
132
|
readonly branch?: string;
|
|
122
133
|
readonly sessionFile?: string;
|
|
134
|
+
readonly parent?: string;
|
|
135
|
+
readonly origin?: SpawnOrigin;
|
|
123
136
|
},
|
|
124
137
|
at: number | null,
|
|
125
138
|
): AgentRecord {
|
|
@@ -130,6 +143,8 @@ export function spawnAgent(
|
|
|
130
143
|
cwd: identity.cwd,
|
|
131
144
|
branch: identity.branch ?? null,
|
|
132
145
|
sessionFile: identity.sessionFile ?? null,
|
|
146
|
+
parent: identity.parent ?? null,
|
|
147
|
+
origin: identity.origin ?? "spawn",
|
|
133
148
|
stateChangedAt: at,
|
|
134
149
|
};
|
|
135
150
|
}
|
|
@@ -139,6 +154,11 @@ export function spawnAgent(
|
|
|
139
154
|
cwd: current.cwd ?? identity.cwd,
|
|
140
155
|
branch: current.branch ?? identity.branch ?? null,
|
|
141
156
|
sessionFile: current.sessionFile ?? identity.sessionFile ?? null,
|
|
157
|
+
// Lineage follows the rule above it: a fact already folded wins, and a late
|
|
158
|
+
// spawn only fills what is still missing. `origin` has no missing value —
|
|
159
|
+
// it defaults to "spawn" — so the spawn event is its only authority.
|
|
160
|
+
parent: current.parent ?? identity.parent ?? null,
|
|
161
|
+
origin: identity.origin ?? current.origin,
|
|
142
162
|
};
|
|
143
163
|
}
|
|
144
164
|
|
|
@@ -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`
|
|
28
|
-
*
|
|
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
|
+
}
|
package/src/summary/summary.ts
CHANGED
|
@@ -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";
|
|
@@ -25,6 +26,7 @@ export type {
|
|
|
25
26
|
IdleAgentInfo,
|
|
26
27
|
ModelFallbackInfo,
|
|
27
28
|
NodeInfo,
|
|
29
|
+
SpawnOrigin,
|
|
28
30
|
} from "./summary-agent.ts";
|
|
29
31
|
|
|
30
32
|
/** The observer-facing lifecycle state of a Run. */
|
|
@@ -152,6 +154,12 @@ export function applyEvent(
|
|
|
152
154
|
event.agent,
|
|
153
155
|
applyModelFallback(summary.agents[event.agent] ?? placeholderAgent(), event, at),
|
|
154
156
|
);
|
|
157
|
+
case "agent_model":
|
|
158
|
+
return withAgent(
|
|
159
|
+
summary,
|
|
160
|
+
event.agent,
|
|
161
|
+
applyAgentModel(summary.agents[event.agent] ?? placeholderAgent(), event),
|
|
162
|
+
);
|
|
155
163
|
case "agent_usage":
|
|
156
164
|
return withAgent(summary, event.agent, setUsage(summary.agents[event.agent], event, at));
|
|
157
165
|
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
|
|
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 {
|
|
@@ -191,6 +191,11 @@ export interface OpenOptions {
|
|
|
191
191
|
* Spawn identity only: it never becomes argv, and it is absent when empty.
|
|
192
192
|
*/
|
|
193
193
|
readonly declaredExtensions?: readonly string[];
|
|
194
|
+
/**
|
|
195
|
+
* Resolved name of the Agent named as this Agent's parent (Parent Link).
|
|
196
|
+
* Spawn identity only: it never becomes argv.
|
|
197
|
+
*/
|
|
198
|
+
readonly parent?: string;
|
|
194
199
|
/** Session storage directory. Used by the e2e suite to stay out of ~/.pi (ticket 06). */
|
|
195
200
|
readonly sessionDir?: string;
|
|
196
201
|
/** Resumes an existing pi session, translated to `--session <path>`. */
|
package/src/types.ts
CHANGED
|
@@ -66,6 +66,11 @@ export interface SpawnOptions {
|
|
|
66
66
|
readonly name?: string;
|
|
67
67
|
/** Request a fresh Git worktree. The requested cwd remains the base until spawn resolves. */
|
|
68
68
|
readonly worktree?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Names this Agent's parent in the Run tree (Parent Link). Data only: it is
|
|
71
|
+
* no conversation channel and no lifetime rule.
|
|
72
|
+
*/
|
|
73
|
+
readonly parent?: Handle;
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
/**
|
|
@@ -73,8 +78,12 @@ export interface SpawnOptions {
|
|
|
73
78
|
*
|
|
74
79
|
* This is the settled shape, and it is what the Cassette identity hashes
|
|
75
80
|
* (ADR-0039).
|
|
81
|
+
*
|
|
82
|
+
* `parent` is omitted on purpose: a Handle must never reach this shape, because
|
|
83
|
+
* an Ask records these options into the Cassette. Spawn resolves the Parent
|
|
84
|
+
* Link to a name, which travels in `OpenOptions` and stays out of Ask identity.
|
|
76
85
|
*/
|
|
77
|
-
export interface ResolvedSpawnOptions extends Omit<SpawnOptions, "model" | "thinking"> {
|
|
86
|
+
export interface ResolvedSpawnOptions extends Omit<SpawnOptions, "model" | "thinking" | "parent"> {
|
|
78
87
|
readonly model?: string;
|
|
79
88
|
readonly thinking?: ThinkingLevel;
|
|
80
89
|
}
|
|
@@ -87,6 +96,8 @@ export interface SpawnOverrides {
|
|
|
87
96
|
readonly cwd?: string;
|
|
88
97
|
/** Request a fresh Git worktree. */
|
|
89
98
|
readonly worktree?: boolean;
|
|
99
|
+
/** Names this Agent's parent in the Run tree (Parent Link). Data only. */
|
|
100
|
+
readonly parent?: Handle;
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
/**
|