@yaag/runtime 0.9.0 → 0.11.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/agent-compaction.ts +80 -0
- package/src/agent/agent-usage.ts +11 -0
- package/src/agent/agent.ts +94 -2
- package/src/agent/fork.ts +74 -0
- package/src/agent/index.ts +1 -0
- package/src/agent/spawn-open.ts +249 -0
- package/src/agent/spawn-parent.ts +21 -0
- package/src/agent/spawn-request.ts +17 -2
- package/src/agent/spawn.ts +45 -191
- package/src/cassette/cassette-replay.ts +33 -1
- package/src/cassette/cassette-schema.ts +21 -0
- package/src/cassette/cassette.ts +75 -6
- package/src/cassette/index.ts +1 -0
- package/src/cassette/recording-transport.ts +13 -0
- package/src/cassette/replay-divergence.ts +58 -3
- package/src/cassette/replay-transport.ts +22 -1
- package/src/cassette/resume-transport.ts +27 -2
- package/src/errors.ts +4 -0
- package/src/events.ts +35 -0
- package/src/index.ts +7 -0
- package/src/summary/index.ts +2 -0
- package/src/summary/summary-agent.ts +55 -2
- package/src/summary/summary.ts +21 -0
- package/src/transport/fake-transport.ts +35 -1
- package/src/transport/index.ts +9 -1
- package/src/transport/live-transport.ts +9 -0
- package/src/transport/pi-state.ts +51 -1
- package/src/transport/transport.ts +68 -0
- package/src/transport/worktree-transport.ts +39 -6
- package/src/types.ts +54 -1
|
@@ -2,6 +2,7 @@ import { YaagError } from "../errors.ts";
|
|
|
2
2
|
import type {
|
|
3
3
|
AskMarker,
|
|
4
4
|
AskMarkerContext,
|
|
5
|
+
CompactionMarker,
|
|
5
6
|
OpenOptions,
|
|
6
7
|
RecordedSpawnSelection,
|
|
7
8
|
} from "../transport/index.ts";
|
|
@@ -9,7 +10,13 @@ import type { CassetteAgent, CassetteAsk, CassetteSpawn } from "./cassette.ts";
|
|
|
9
10
|
|
|
10
11
|
/** A pure description of the first strict replay identity mismatch. */
|
|
11
12
|
export interface ReplayMismatch {
|
|
12
|
-
readonly kind:
|
|
13
|
+
readonly kind:
|
|
14
|
+
| "unexpected-spawn"
|
|
15
|
+
| "spawn-options"
|
|
16
|
+
| "changed-ask"
|
|
17
|
+
| "extra-ask"
|
|
18
|
+
| "changed-compaction"
|
|
19
|
+
| "extra-compaction";
|
|
13
20
|
readonly agent: string;
|
|
14
21
|
readonly index?: number;
|
|
15
22
|
readonly expectedHash: string;
|
|
@@ -71,6 +78,32 @@ export const replayMismatch = {
|
|
|
71
78
|
}),
|
|
72
79
|
};
|
|
73
80
|
},
|
|
81
|
+
|
|
82
|
+
compaction(
|
|
83
|
+
agent: CassetteAgent,
|
|
84
|
+
cursor: number,
|
|
85
|
+
actual: CompactionMarker,
|
|
86
|
+
): ReplayMismatch | null {
|
|
87
|
+
const expected = agent.compactions?.[cursor];
|
|
88
|
+
if (!expected) {
|
|
89
|
+
return {
|
|
90
|
+
kind: "extra-compaction",
|
|
91
|
+
agent: agent.spawn.name,
|
|
92
|
+
index: actual.index,
|
|
93
|
+
expectedHash: "<none>",
|
|
94
|
+
actualHash: actual.hash,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return expected.index === actual.index && expected.hash === actual.hash
|
|
98
|
+
? null
|
|
99
|
+
: {
|
|
100
|
+
kind: "changed-compaction",
|
|
101
|
+
agent: agent.spawn.name,
|
|
102
|
+
index: actual.index,
|
|
103
|
+
expectedHash: expected.hash,
|
|
104
|
+
actualHash: actual.hash,
|
|
105
|
+
};
|
|
106
|
+
},
|
|
74
107
|
};
|
|
75
108
|
|
|
76
109
|
/** Where a user reads what a Divergence means and what to do about it. */
|
|
@@ -86,7 +119,8 @@ export function strictReplay(mismatch: ReplayMismatch): never {
|
|
|
86
119
|
mismatch.agent,
|
|
87
120
|
);
|
|
88
121
|
}
|
|
89
|
-
const at =
|
|
122
|
+
const at =
|
|
123
|
+
compactionAt(mismatch) ?? (mismatch.index === undefined ? "spawn" : `Ask ${mismatch.index}`);
|
|
90
124
|
const changed =
|
|
91
125
|
mismatch.kind === "spawn-options" && mismatch.changedFields !== undefined
|
|
92
126
|
? ` changed ${mismatch.changedFields.join(", ")};`
|
|
@@ -98,6 +132,14 @@ export function strictReplay(mismatch: ReplayMismatch): never {
|
|
|
98
132
|
);
|
|
99
133
|
}
|
|
100
134
|
|
|
135
|
+
/** Names the compaction seam of a compaction mismatch, or nothing for other kinds. */
|
|
136
|
+
function compactionAt(mismatch: ReplayMismatch): string | undefined {
|
|
137
|
+
if (mismatch.kind !== "changed-compaction" && mismatch.kind !== "extra-compaction") {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
return `compaction #${mismatch.index}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
101
143
|
/** The identity fields a changed spawn altered, for the Divergence report (ADR-0039). */
|
|
102
144
|
function spawnChangedFields(expected: CassetteSpawn, actual: OpenOptions): readonly string[] {
|
|
103
145
|
return changedAmong(SPAWN_OPEN_FIELDS, expected, actual);
|
|
@@ -145,7 +187,15 @@ const SPAWN_IDENTITY_FIELDS = [
|
|
|
145
187
|
] as const;
|
|
146
188
|
|
|
147
189
|
/** The spawn identity fields plus the Agent name, which only an open request carries. */
|
|
148
|
-
const SPAWN_OPEN_FIELDS = [
|
|
190
|
+
const SPAWN_OPEN_FIELDS = [
|
|
191
|
+
"name",
|
|
192
|
+
...SPAWN_IDENTITY_FIELDS,
|
|
193
|
+
"declaredExtensions",
|
|
194
|
+
"parent",
|
|
195
|
+
"origin",
|
|
196
|
+
"forkOf",
|
|
197
|
+
"forkAsks",
|
|
198
|
+
] as const;
|
|
149
199
|
|
|
150
200
|
/** The listed fields whose canonical JSON differs between two identity records. */
|
|
151
201
|
function changedAmong<Key extends string>(
|
|
@@ -216,6 +266,11 @@ function spawnHash(options: CassetteSpawn | OpenOptions): string {
|
|
|
216
266
|
...(options.declaredExtensions === undefined || options.declaredExtensions.length === 0
|
|
217
267
|
? {}
|
|
218
268
|
: { declaredExtensions: options.declaredExtensions }),
|
|
269
|
+
...(options.parent === undefined ? {} : { parent: options.parent }),
|
|
270
|
+
// Conditional, so a Cassette recorded before forking keeps its hash.
|
|
271
|
+
...(options.origin === undefined ? {} : { origin: options.origin }),
|
|
272
|
+
...(options.forkOf === undefined ? {} : { forkOf: options.forkOf }),
|
|
273
|
+
...(options.forkAsks === undefined ? {} : { forkAsks: options.forkAsks }),
|
|
219
274
|
}),
|
|
220
275
|
);
|
|
221
276
|
return hasher.digest("hex");
|
|
@@ -3,6 +3,8 @@ import type {
|
|
|
3
3
|
AgentTransport,
|
|
4
4
|
AskMarker,
|
|
5
5
|
AskPlayback,
|
|
6
|
+
CompactionMarker,
|
|
7
|
+
CompactionPlayback,
|
|
6
8
|
Frame,
|
|
7
9
|
OpenOptions,
|
|
8
10
|
RecordedSpawnSelection,
|
|
@@ -30,7 +32,14 @@ export function replayTransport(cassette: Cassette): TransportFactory {
|
|
|
30
32
|
const mismatch = replayMismatch.spawn(agent, options);
|
|
31
33
|
if (mismatch) strictReplay(mismatch);
|
|
32
34
|
spawnCursor += 1;
|
|
33
|
-
|
|
35
|
+
// The recorded session file rides along, so a replayed Agent can be
|
|
36
|
+
// forked exactly where the recording forked it (ADR-0044).
|
|
37
|
+
if (agent.worktree !== undefined || agent.sessionFile !== undefined) {
|
|
38
|
+
observeStartup?.({
|
|
39
|
+
...(agent.worktree === undefined ? {} : { worktree: agent.worktree }),
|
|
40
|
+
...(agent.sessionFile === undefined ? {} : { sessionFile: agent.sessionFile }),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
34
43
|
return new ReplayTransport(new CassetteReplay(agent));
|
|
35
44
|
},
|
|
36
45
|
|
|
@@ -46,6 +55,7 @@ class ReplayTransport implements AgentTransport {
|
|
|
46
55
|
readonly model: string;
|
|
47
56
|
readonly #replay: CassetteReplay;
|
|
48
57
|
#askCursor = 0;
|
|
58
|
+
#compactionCursor = 0;
|
|
49
59
|
#closed: Promise<AgentStats> | null = null;
|
|
50
60
|
|
|
51
61
|
constructor(replay: CassetteReplay) {
|
|
@@ -72,6 +82,17 @@ class ReplayTransport implements AgentTransport {
|
|
|
72
82
|
// A completed replay does not alter the recorded Cassette.
|
|
73
83
|
}
|
|
74
84
|
|
|
85
|
+
beginCompaction(marker: CompactionMarker): CompactionPlayback {
|
|
86
|
+
const mismatch = replayMismatch.compaction(this.#replay.agent, this.#compactionCursor, marker);
|
|
87
|
+
if (mismatch) strictReplay(mismatch);
|
|
88
|
+
this.#compactionCursor += 1;
|
|
89
|
+
return this.#replay.beginCompaction(marker);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
finishCompaction(): void {
|
|
93
|
+
// A completed replay does not alter the recorded Cassette.
|
|
94
|
+
}
|
|
95
|
+
|
|
75
96
|
recordedExtractionPolicy(index: number): string | undefined {
|
|
76
97
|
return this.#replay.agent.asks[index]?.extractionPolicy;
|
|
77
98
|
}
|
|
@@ -4,6 +4,9 @@ import type {
|
|
|
4
4
|
AskCompletion,
|
|
5
5
|
AskMarker,
|
|
6
6
|
AskPlayback,
|
|
7
|
+
CompactionMarker,
|
|
8
|
+
CompactionPlayback,
|
|
9
|
+
CompactionResult,
|
|
7
10
|
Frame,
|
|
8
11
|
OpenOptions,
|
|
9
12
|
RecordedSpawnSelection,
|
|
@@ -64,6 +67,7 @@ class ResumeTransport implements AgentTransport {
|
|
|
64
67
|
readonly #frames = new FrameQueue();
|
|
65
68
|
readonly #buffered: Frame[] = [];
|
|
66
69
|
#askCursor = 0;
|
|
70
|
+
#compactionCursor = 0;
|
|
67
71
|
#mode: "replay" | "activating" | "live" = "replay";
|
|
68
72
|
#live: AgentTransport | null = null;
|
|
69
73
|
#activation: Promise<void> | null = null;
|
|
@@ -112,6 +116,27 @@ class ResumeTransport implements AgentTransport {
|
|
|
112
116
|
if (this.#mode === "live") this.#live?.finishAsk(completion);
|
|
113
117
|
}
|
|
114
118
|
|
|
119
|
+
/**
|
|
120
|
+
* A compaction the Cassette does not hold activates live, exactly as a
|
|
121
|
+
* diverged Ask does: the recorded prefix cannot answer it (ADR-0043).
|
|
122
|
+
*/
|
|
123
|
+
beginCompaction(marker: CompactionMarker): CompactionPlayback | undefined {
|
|
124
|
+
if (this.#mode === "live") return this.#live?.beginCompaction(marker);
|
|
125
|
+
if (this.#mode === "activating") return undefined;
|
|
126
|
+
const mismatch = replayMismatch.compaction(this.#agent, this.#compactionCursor, marker);
|
|
127
|
+
if (!mismatch) {
|
|
128
|
+
this.#compactionCursor += 1;
|
|
129
|
+
return this.#replay.beginCompaction(marker);
|
|
130
|
+
}
|
|
131
|
+
this.#mode = "activating";
|
|
132
|
+
this.#activation = this.#activate(null);
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
finishCompaction(result: CompactionResult | undefined): void {
|
|
137
|
+
if (this.#mode === "live") this.#live?.finishCompaction(result);
|
|
138
|
+
}
|
|
139
|
+
|
|
115
140
|
recordedExtractionPolicy(index: number): string | undefined {
|
|
116
141
|
return this.#mode === "replay" ? this.#agent.asks[index]?.extractionPolicy : undefined;
|
|
117
142
|
}
|
|
@@ -121,7 +146,7 @@ class ResumeTransport implements AgentTransport {
|
|
|
121
146
|
return this.#closed;
|
|
122
147
|
}
|
|
123
148
|
|
|
124
|
-
async #activate(marker: AskMarker): Promise<void> {
|
|
149
|
+
async #activate(marker: AskMarker | null): Promise<void> {
|
|
125
150
|
// End the old source before attaching the new one, but keep the outward
|
|
126
151
|
// queue open so Connection remains attached across the handoff.
|
|
127
152
|
this.#replay.finish();
|
|
@@ -131,7 +156,7 @@ class ResumeTransport implements AgentTransport {
|
|
|
131
156
|
try {
|
|
132
157
|
const live = await this.#liveFactory.open(this.#continuation);
|
|
133
158
|
this.#live = live;
|
|
134
|
-
live.beginAsk(marker);
|
|
159
|
+
if (marker !== null) live.beginAsk(marker);
|
|
135
160
|
for (const frame of this.#buffered.splice(0)) live.send(frame);
|
|
136
161
|
this.#mode = "live";
|
|
137
162
|
void this.#forward(live.frames());
|
package/src/errors.ts
CHANGED
|
@@ -44,6 +44,10 @@ export type YaagErrorCode =
|
|
|
44
44
|
| "ASK_TIMEOUT" // timeoutMs elapsed
|
|
45
45
|
| "ASK_LIMIT" // soft Ask budget exceeded after grace
|
|
46
46
|
| "ASK_STALLED" // no frame arrived within the silence budget; escalated, then kill
|
|
47
|
+
| "COMPACT_DURING_ASK" // compact() was called while an Ask was in flight
|
|
48
|
+
| "COMPACT_FAILED" // pi refused or could not summarize the context
|
|
49
|
+
| "FORK_DURING_ASK" // fork() was called while the source Ask was in flight
|
|
50
|
+
| "FORK_REFUSED" // the fork source died mid-Ask, or holds no session file
|
|
47
51
|
| "ASK_INVALID_OUTPUT" // settled text could not be extracted or satisfy outputSchema
|
|
48
52
|
| "ARGS_INVALID" // arguments failed schema validation before the Run started
|
|
49
53
|
| "CONFIG_INVALID" // a config layer is missing, unparsable, or violates the schema
|
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
|
+
* marks an Agent created from a copy of another Agent's session (ADR-0044).
|
|
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";
|
|
@@ -159,6 +169,31 @@ export type LifecycleEventBody =
|
|
|
159
169
|
/** Cumulative assistant-completion cost observed so far. */
|
|
160
170
|
readonly cost: number;
|
|
161
171
|
}
|
|
172
|
+
| {
|
|
173
|
+
/**
|
|
174
|
+
* One compaction of an Agent's context (ADR-0043).
|
|
175
|
+
*
|
|
176
|
+
* It carries the summary call's own spend, so a Run Summary does not
|
|
177
|
+
* silently lose it. `agent_exit` stays authoritative for the Agent's
|
|
178
|
+
* total: pi's `get_session_stats` already includes compaction spend.
|
|
179
|
+
* The custom instructions never ride the wire (ticket 09); only the
|
|
180
|
+
* `custom` flag says that the program supplied some.
|
|
181
|
+
*/
|
|
182
|
+
readonly type: "agent_compaction";
|
|
183
|
+
readonly agent: string;
|
|
184
|
+
/** Monotonic per Agent, from 0. */
|
|
185
|
+
readonly index: number;
|
|
186
|
+
/** Context tokens before the summary replaced the transcript; null when unknown. */
|
|
187
|
+
readonly tokensBefore: number | null;
|
|
188
|
+
/** Estimated context tokens after the summary; null when unknown. */
|
|
189
|
+
readonly tokensAfter: number | null;
|
|
190
|
+
/** The summary call's own token breakdown; null when unknown. */
|
|
191
|
+
readonly tokens: TokenBreakdown | null;
|
|
192
|
+
/** The summary call's own cost; null when unknown. */
|
|
193
|
+
readonly cost: number | null;
|
|
194
|
+
/** The program supplied custom compaction instructions. */
|
|
195
|
+
readonly custom: boolean;
|
|
196
|
+
}
|
|
162
197
|
| {
|
|
163
198
|
readonly type: "agent_exit";
|
|
164
199
|
readonly agent: string;
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export type {
|
|
|
5
5
|
CassetteAgent,
|
|
6
6
|
CassetteArtifact,
|
|
7
7
|
CassetteAsk,
|
|
8
|
+
CassetteCompaction,
|
|
8
9
|
CassetteGit,
|
|
9
10
|
CassetteRun,
|
|
10
11
|
CassetteSink,
|
|
@@ -47,6 +48,7 @@ export type {
|
|
|
47
48
|
LifecycleEventBody,
|
|
48
49
|
NodeState,
|
|
49
50
|
NodeUsage,
|
|
51
|
+
SpawnOrigin,
|
|
50
52
|
StampedEventSink,
|
|
51
53
|
} from "./events.ts";
|
|
52
54
|
export type {
|
|
@@ -78,6 +80,7 @@ export type {
|
|
|
78
80
|
AgentInfo,
|
|
79
81
|
AgentState,
|
|
80
82
|
AskingAgentInfo,
|
|
83
|
+
CompactionInfo,
|
|
81
84
|
EndedRunSummary,
|
|
82
85
|
ExitedAgentInfo,
|
|
83
86
|
IdleAgentInfo,
|
|
@@ -95,6 +98,9 @@ export type {
|
|
|
95
98
|
AskMarker,
|
|
96
99
|
AskMarkerContext,
|
|
97
100
|
AskPlayback,
|
|
101
|
+
CompactionMarker,
|
|
102
|
+
CompactionPlayback,
|
|
103
|
+
CompactionResult,
|
|
98
104
|
DiscoveredSkill,
|
|
99
105
|
Frame,
|
|
100
106
|
ReapPath,
|
|
@@ -109,6 +115,7 @@ export type {
|
|
|
109
115
|
export { readSystemPromptSidecar, reap, worktreeTransport } from "./transport/index.ts";
|
|
110
116
|
export type {
|
|
111
117
|
AskOptions,
|
|
118
|
+
ForkOptions,
|
|
112
119
|
Handle,
|
|
113
120
|
ResolvedSpawnOptions,
|
|
114
121
|
SpawnOptions,
|
package/src/summary/index.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
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
|
|
|
10
|
+
/** What one compaction of an Agent's context reported (ADR-0043). */
|
|
11
|
+
export interface CompactionInfo {
|
|
12
|
+
readonly tokensBefore: number | null;
|
|
13
|
+
readonly tokensAfter: number | null;
|
|
14
|
+
/** The summary call's own cost, already included in `agent_exit` accounting. */
|
|
15
|
+
readonly cost: number | null;
|
|
16
|
+
readonly at: number | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
10
19
|
/** The observer-facing lifecycle state of an Agent. */
|
|
11
20
|
export type AgentState = "idle" | "asking" | "exited";
|
|
12
21
|
|
|
@@ -22,6 +31,10 @@ interface AgentInfoBase {
|
|
|
22
31
|
readonly branch: string | null;
|
|
23
32
|
/** pi's session file for this Agent, when the spawn reported one; a Peek reads it. */
|
|
24
33
|
readonly sessionFile: string | null;
|
|
34
|
+
/** The Agent named as this one's parent (Parent Link), or null for a root Agent. */
|
|
35
|
+
readonly parent: string | null;
|
|
36
|
+
/** How the Agent came to be; "spawn" until forking ships. */
|
|
37
|
+
readonly origin: SpawnOrigin;
|
|
25
38
|
readonly activity: AgentActivity | null;
|
|
26
39
|
readonly tokens: TokenBreakdown | null;
|
|
27
40
|
readonly cost: number | null;
|
|
@@ -30,6 +43,10 @@ interface AgentInfoBase {
|
|
|
30
43
|
/** Timestamp ordering only cumulative live accounting, never lifecycle state. */
|
|
31
44
|
readonly usageUpdatedAt: number | null;
|
|
32
45
|
readonly askStartedAt: number | null;
|
|
46
|
+
/** Compactions of this Agent's context so far (ADR-0043). */
|
|
47
|
+
readonly compactions: number;
|
|
48
|
+
/** What the newest compaction reported, or null when the Agent compacted none. */
|
|
49
|
+
readonly lastCompaction: CompactionInfo | null;
|
|
33
50
|
/** This Agent's bounded Nested Node table, in first-seen order (spec §3). */
|
|
34
51
|
readonly nodes: readonly NodeInfo[];
|
|
35
52
|
/** Exited Nested Nodes dropped to keep the table bounded. */
|
|
@@ -97,6 +114,10 @@ export function placeholderAgent(): IdleAgentInfo {
|
|
|
97
114
|
cwd: null,
|
|
98
115
|
branch: null,
|
|
99
116
|
sessionFile: null,
|
|
117
|
+
parent: null,
|
|
118
|
+
origin: "spawn",
|
|
119
|
+
compactions: 0,
|
|
120
|
+
lastCompaction: null,
|
|
100
121
|
state: "idle",
|
|
101
122
|
askIndex: null,
|
|
102
123
|
promptGist: null,
|
|
@@ -125,6 +146,8 @@ export function spawnAgent(
|
|
|
125
146
|
readonly cwd: string;
|
|
126
147
|
readonly branch?: string;
|
|
127
148
|
readonly sessionFile?: string;
|
|
149
|
+
readonly parent?: string;
|
|
150
|
+
readonly origin?: SpawnOrigin;
|
|
128
151
|
},
|
|
129
152
|
at: number | null,
|
|
130
153
|
): AgentRecord {
|
|
@@ -135,6 +158,8 @@ export function spawnAgent(
|
|
|
135
158
|
cwd: identity.cwd,
|
|
136
159
|
branch: identity.branch ?? null,
|
|
137
160
|
sessionFile: identity.sessionFile ?? null,
|
|
161
|
+
parent: identity.parent ?? null,
|
|
162
|
+
origin: identity.origin ?? "spawn",
|
|
138
163
|
stateChangedAt: at,
|
|
139
164
|
};
|
|
140
165
|
}
|
|
@@ -144,6 +169,11 @@ export function spawnAgent(
|
|
|
144
169
|
cwd: current.cwd ?? identity.cwd,
|
|
145
170
|
branch: current.branch ?? identity.branch ?? null,
|
|
146
171
|
sessionFile: current.sessionFile ?? identity.sessionFile ?? null,
|
|
172
|
+
// Lineage follows the rule above it: a fact already folded wins, and a late
|
|
173
|
+
// spawn only fills what is still missing. `origin` has no missing value —
|
|
174
|
+
// it defaults to "spawn" — so the spawn event is its only authority.
|
|
175
|
+
parent: current.parent ?? identity.parent ?? null,
|
|
176
|
+
origin: identity.origin ?? current.origin,
|
|
147
177
|
};
|
|
148
178
|
}
|
|
149
179
|
|
|
@@ -249,6 +279,29 @@ export function setUsage(
|
|
|
249
279
|
return { ...agent, tokens: observation.tokens, cost: observation.cost, usageUpdatedAt: at };
|
|
250
280
|
}
|
|
251
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Counts one compaction and keeps what it reported.
|
|
284
|
+
*
|
|
285
|
+
* It folds no cost: the summary call's spend reaches the Summary through
|
|
286
|
+
* `agent_usage`, and `agent_exit` stays authoritative (ADR-0043). A terminal
|
|
287
|
+
* Agent still counts a late compaction, because the count is a fact about the
|
|
288
|
+
* conversation rather than a lifecycle state.
|
|
289
|
+
*/
|
|
290
|
+
export function compactAgent(
|
|
291
|
+
current: AgentRecord | undefined,
|
|
292
|
+
observation: CompactionInfo,
|
|
293
|
+
at: number | null,
|
|
294
|
+
): AgentRecord {
|
|
295
|
+
const agent = current ?? placeholderAgent();
|
|
296
|
+
const stale =
|
|
297
|
+
at !== null && agent.lastCompaction?.at !== null && at < (agent.lastCompaction?.at ?? at);
|
|
298
|
+
return {
|
|
299
|
+
...agent,
|
|
300
|
+
compactions: agent.compactions + 1,
|
|
301
|
+
lastCompaction: stale ? agent.lastCompaction : { ...observation, at },
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
252
305
|
/**
|
|
253
306
|
* Folds authoritative shutdown accounting into the terminal Agent state.
|
|
254
307
|
*
|
package/src/summary/summary.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { TokenBreakdown } from "../transport/index.ts";
|
|
|
3
3
|
import type { AgentInfo } from "./summary-agent.ts";
|
|
4
4
|
import {
|
|
5
5
|
type AgentRecord,
|
|
6
|
+
compactAgent,
|
|
6
7
|
endAsk,
|
|
7
8
|
exitAgent,
|
|
8
9
|
placeholderAgent,
|
|
@@ -22,10 +23,12 @@ export type {
|
|
|
22
23
|
AgentInfo,
|
|
23
24
|
AgentState,
|
|
24
25
|
AskingAgentInfo,
|
|
26
|
+
CompactionInfo,
|
|
25
27
|
ExitedAgentInfo,
|
|
26
28
|
IdleAgentInfo,
|
|
27
29
|
ModelFallbackInfo,
|
|
28
30
|
NodeInfo,
|
|
31
|
+
SpawnOrigin,
|
|
29
32
|
} from "./summary-agent.ts";
|
|
30
33
|
|
|
31
34
|
/** The observer-facing lifecycle state of a Run. */
|
|
@@ -50,6 +53,8 @@ interface RunSummaryBase {
|
|
|
50
53
|
readonly worstFrameGapMs: number;
|
|
51
54
|
/** Run-wide Model Resolution fallbacks, pruned per-Agent entries included. */
|
|
52
55
|
readonly modelFallbacks: number;
|
|
56
|
+
/** Compactions across every Agent of this Run (ADR-0043). */
|
|
57
|
+
readonly compactions: number;
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
/** A Run that has not settled; it has neither outcome nor compatibility result. */
|
|
@@ -108,6 +113,7 @@ export function initialSummary(): RunningRunSummary {
|
|
|
108
113
|
durationMs: 0,
|
|
109
114
|
worstFrameGapMs: 0,
|
|
110
115
|
modelFallbacks: 0,
|
|
116
|
+
compactions: 0,
|
|
111
117
|
ok: null,
|
|
112
118
|
};
|
|
113
119
|
}
|
|
@@ -159,6 +165,21 @@ export function applyEvent(
|
|
|
159
165
|
event.agent,
|
|
160
166
|
applyAgentModel(summary.agents[event.agent] ?? placeholderAgent(), event),
|
|
161
167
|
);
|
|
168
|
+
case "agent_compaction":
|
|
169
|
+
return withAgent(
|
|
170
|
+
{ ...summary, compactions: summary.compactions + 1 },
|
|
171
|
+
event.agent,
|
|
172
|
+
compactAgent(
|
|
173
|
+
summary.agents[event.agent],
|
|
174
|
+
{
|
|
175
|
+
tokensBefore: event.tokensBefore,
|
|
176
|
+
tokensAfter: event.tokensAfter,
|
|
177
|
+
cost: event.cost,
|
|
178
|
+
at,
|
|
179
|
+
},
|
|
180
|
+
at,
|
|
181
|
+
),
|
|
182
|
+
);
|
|
162
183
|
case "agent_usage":
|
|
163
184
|
return withAgent(summary, event.agent, setUsage(summary.agents[event.agent], event, at));
|
|
164
185
|
case "ask_end":
|
|
@@ -6,7 +6,13 @@ import {
|
|
|
6
6
|
import type { AvailableModel } from "../model/index.ts";
|
|
7
7
|
import { FrameQueue } from "./frame-queue.ts";
|
|
8
8
|
import { parseFrame } from "./jsonl.ts";
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
AgentStats,
|
|
11
|
+
AgentTransport,
|
|
12
|
+
AskMarker,
|
|
13
|
+
CompactionMarker,
|
|
14
|
+
Frame,
|
|
15
|
+
} from "./transport.ts";
|
|
10
16
|
|
|
11
17
|
/**
|
|
12
18
|
* Scripted frame playback for one prompt sent through a FakeTransport.
|
|
@@ -50,6 +56,10 @@ export interface FakeTransportOptions extends FakePromptScript {
|
|
|
50
56
|
readonly steerError?: string;
|
|
51
57
|
/** Makes an abort RPC response fail without embedding a limit decision in playback. */
|
|
52
58
|
readonly abortError?: string;
|
|
59
|
+
/** Payload of a `compact` command response; omission answers a bare success. */
|
|
60
|
+
readonly compaction?: Record<string, unknown>;
|
|
61
|
+
/** Makes a `compact` command response fail, as pi does when it cannot summarize. */
|
|
62
|
+
readonly compactError?: string;
|
|
53
63
|
/** Makes the private report_result schema command fail. */
|
|
54
64
|
readonly schemaCommandError?: string;
|
|
55
65
|
/** The snapshot answered to `get_available_models`; defaults to this fake's own model. */
|
|
@@ -87,6 +97,7 @@ export class FakeTransport implements AgentTransport {
|
|
|
87
97
|
/** Everything the runtime wrote, in order. */
|
|
88
98
|
readonly sent: Frame[] = [];
|
|
89
99
|
readonly asks: AskMarker[] = [];
|
|
100
|
+
readonly compactions: CompactionMarker[] = [];
|
|
90
101
|
readonly #queue = new FrameQueue();
|
|
91
102
|
readonly #options: FakeTransportOptions;
|
|
92
103
|
#closed = false;
|
|
@@ -113,6 +124,7 @@ export class FakeTransport implements AgentTransport {
|
|
|
113
124
|
if (frame.type === "abort") void this.#answerAbort(frame);
|
|
114
125
|
if (frame.type === "get_last_assistant_text") this.#answerLastText(frame);
|
|
115
126
|
if (frame.type === "get_state") this.#answerState(frame);
|
|
127
|
+
if (frame.type === "compact") this.#answerCompact(frame);
|
|
116
128
|
if (frame.type === "get_available_models") this.#answerAvailableModels(frame);
|
|
117
129
|
if (frame.type === "set_model") this.#answerSetModel(frame);
|
|
118
130
|
if (frame.type === "set_thinking_level")
|
|
@@ -133,6 +145,15 @@ export class FakeTransport implements AgentTransport {
|
|
|
133
145
|
// This live test transport does not persist Ask outcomes.
|
|
134
146
|
}
|
|
135
147
|
|
|
148
|
+
beginCompaction(marker: CompactionMarker): undefined {
|
|
149
|
+
this.compactions.push(marker);
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
finishCompaction(): void {
|
|
154
|
+
// This live test transport does not persist compaction outcomes.
|
|
155
|
+
}
|
|
156
|
+
|
|
136
157
|
/** True once the runtime shut this Agent down. */
|
|
137
158
|
get closed(): boolean {
|
|
138
159
|
return this.#closed;
|
|
@@ -295,6 +316,19 @@ export class FakeTransport implements AgentTransport {
|
|
|
295
316
|
});
|
|
296
317
|
}
|
|
297
318
|
|
|
319
|
+
#answerCompact(frame: Frame): void {
|
|
320
|
+
const error = this.#options.compactError;
|
|
321
|
+
this.#queue.push({
|
|
322
|
+
type: "response",
|
|
323
|
+
command: "compact",
|
|
324
|
+
id: frame.id,
|
|
325
|
+
success: error === undefined,
|
|
326
|
+
...(error === undefined
|
|
327
|
+
? { data: this.#options.compaction ?? { summary: "summary" } }
|
|
328
|
+
: { error }),
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
298
332
|
#answerState(frame: Frame): void {
|
|
299
333
|
if (this.#options.stateSilent === true) return;
|
|
300
334
|
this.#queue.push({
|
package/src/transport/index.ts
CHANGED
|
@@ -17,7 +17,12 @@ export { FrameGapTracker } from "./frame-gap.ts";
|
|
|
17
17
|
export { FrameQueue } from "./frame-queue.ts";
|
|
18
18
|
export { decodeFrames } from "./jsonl.ts";
|
|
19
19
|
export { liveTransport } from "./live-transport.ts";
|
|
20
|
-
export {
|
|
20
|
+
export {
|
|
21
|
+
type AgentProgress,
|
|
22
|
+
piCommand,
|
|
23
|
+
readAgentProgress,
|
|
24
|
+
readCompaction,
|
|
25
|
+
} from "./pi-state.ts";
|
|
21
26
|
export { type ReapPath, type ReapTarget, reap } from "./reap.ts";
|
|
22
27
|
export { type DiscoveredSkill, liveSkillProbe, type SkillProbeFactory } from "./skill-probe.ts";
|
|
23
28
|
export { skillRestrictionTransport } from "./skill-restriction-transport.ts";
|
|
@@ -34,6 +39,9 @@ export type {
|
|
|
34
39
|
AskMarker,
|
|
35
40
|
AskMarkerContext,
|
|
36
41
|
AskPlayback,
|
|
42
|
+
CompactionMarker,
|
|
43
|
+
CompactionPlayback,
|
|
44
|
+
CompactionResult,
|
|
37
45
|
Frame,
|
|
38
46
|
OpenOptions,
|
|
39
47
|
TokenBreakdown,
|
|
@@ -140,6 +140,15 @@ class LiveTransport implements AgentTransport {
|
|
|
140
140
|
// Live transport has no Cassette outcome to complete.
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
beginCompaction(): undefined {
|
|
144
|
+
// Nothing to do live; a recording implementation groups frames by this.
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
finishCompaction(): void {
|
|
149
|
+
// Live transport has no Cassette compaction to complete.
|
|
150
|
+
}
|
|
151
|
+
|
|
143
152
|
close(): Promise<AgentStats> {
|
|
144
153
|
this.#closing ??= this.#shutdown();
|
|
145
154
|
return this.#closing;
|