@yaag/runtime 0.9.0 → 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/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 +10 -0
- package/src/index.ts +1 -0
- package/src/summary/index.ts +1 -0
- package/src/summary/summary-agent.ts +17 -2
- package/src/summary/summary.ts +1 -0
- 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),
|
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";
|
package/src/index.ts
CHANGED
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
|
|
|
@@ -22,6 +22,10 @@ interface AgentInfoBase {
|
|
|
22
22
|
readonly branch: string | null;
|
|
23
23
|
/** pi's session file for this Agent, when the spawn reported one; a Peek reads it. */
|
|
24
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;
|
|
25
29
|
readonly activity: AgentActivity | null;
|
|
26
30
|
readonly tokens: TokenBreakdown | null;
|
|
27
31
|
readonly cost: number | null;
|
|
@@ -97,6 +101,8 @@ export function placeholderAgent(): IdleAgentInfo {
|
|
|
97
101
|
cwd: null,
|
|
98
102
|
branch: null,
|
|
99
103
|
sessionFile: null,
|
|
104
|
+
parent: null,
|
|
105
|
+
origin: "spawn",
|
|
100
106
|
state: "idle",
|
|
101
107
|
askIndex: null,
|
|
102
108
|
promptGist: null,
|
|
@@ -125,6 +131,8 @@ export function spawnAgent(
|
|
|
125
131
|
readonly cwd: string;
|
|
126
132
|
readonly branch?: string;
|
|
127
133
|
readonly sessionFile?: string;
|
|
134
|
+
readonly parent?: string;
|
|
135
|
+
readonly origin?: SpawnOrigin;
|
|
128
136
|
},
|
|
129
137
|
at: number | null,
|
|
130
138
|
): AgentRecord {
|
|
@@ -135,6 +143,8 @@ export function spawnAgent(
|
|
|
135
143
|
cwd: identity.cwd,
|
|
136
144
|
branch: identity.branch ?? null,
|
|
137
145
|
sessionFile: identity.sessionFile ?? null,
|
|
146
|
+
parent: identity.parent ?? null,
|
|
147
|
+
origin: identity.origin ?? "spawn",
|
|
138
148
|
stateChangedAt: at,
|
|
139
149
|
};
|
|
140
150
|
}
|
|
@@ -144,6 +154,11 @@ export function spawnAgent(
|
|
|
144
154
|
cwd: current.cwd ?? identity.cwd,
|
|
145
155
|
branch: current.branch ?? identity.branch ?? null,
|
|
146
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,
|
|
147
162
|
};
|
|
148
163
|
}
|
|
149
164
|
|
package/src/summary/summary.ts
CHANGED
|
@@ -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
|
/**
|