@yaag/runtime 0.10.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-request.ts +12 -1
- package/src/agent/spawn.ts +25 -197
- package/src/cassette/cassette-replay.ts +33 -1
- package/src/cassette/cassette-schema.ts +20 -0
- package/src/cassette/cassette.ts +69 -6
- package/src/cassette/index.ts +1 -0
- package/src/cassette/recording-transport.ts +13 -0
- package/src/cassette/replay-divergence.ts +51 -2
- 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 +26 -1
- package/src/index.ts +6 -0
- package/src/summary/index.ts +1 -0
- package/src/summary/summary-agent.ts +38 -0
- package/src/summary/summary.ts +20 -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 +63 -0
- package/src/transport/worktree-transport.ts +39 -6
- package/src/types.ts +42 -0
|
@@ -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
|
@@ -22,7 +22,7 @@ export type RunOutcome = "completed" | "failed" | "stopped" | "paused" | "interr
|
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* How an Agent came to be. An absent value on an event means "spawn"; "fork"
|
|
25
|
-
*
|
|
25
|
+
* marks an Agent created from a copy of another Agent's session (ADR-0044).
|
|
26
26
|
*/
|
|
27
27
|
export type SpawnOrigin = "spawn" | "fork";
|
|
28
28
|
|
|
@@ -169,6 +169,31 @@ export type LifecycleEventBody =
|
|
|
169
169
|
/** Cumulative assistant-completion cost observed so far. */
|
|
170
170
|
readonly cost: number;
|
|
171
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
|
+
}
|
|
172
197
|
| {
|
|
173
198
|
readonly type: "agent_exit";
|
|
174
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,
|
|
@@ -79,6 +80,7 @@ export type {
|
|
|
79
80
|
AgentInfo,
|
|
80
81
|
AgentState,
|
|
81
82
|
AskingAgentInfo,
|
|
83
|
+
CompactionInfo,
|
|
82
84
|
EndedRunSummary,
|
|
83
85
|
ExitedAgentInfo,
|
|
84
86
|
IdleAgentInfo,
|
|
@@ -96,6 +98,9 @@ export type {
|
|
|
96
98
|
AskMarker,
|
|
97
99
|
AskMarkerContext,
|
|
98
100
|
AskPlayback,
|
|
101
|
+
CompactionMarker,
|
|
102
|
+
CompactionPlayback,
|
|
103
|
+
CompactionResult,
|
|
99
104
|
DiscoveredSkill,
|
|
100
105
|
Frame,
|
|
101
106
|
ReapPath,
|
|
@@ -110,6 +115,7 @@ export type {
|
|
|
110
115
|
export { readSystemPromptSidecar, reap, worktreeTransport } from "./transport/index.ts";
|
|
111
116
|
export type {
|
|
112
117
|
AskOptions,
|
|
118
|
+
ForkOptions,
|
|
113
119
|
Handle,
|
|
114
120
|
ResolvedSpawnOptions,
|
|
115
121
|
SpawnOptions,
|
package/src/summary/index.ts
CHANGED
|
@@ -7,6 +7,15 @@ 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
|
|
|
@@ -34,6 +43,10 @@ interface AgentInfoBase {
|
|
|
34
43
|
/** Timestamp ordering only cumulative live accounting, never lifecycle state. */
|
|
35
44
|
readonly usageUpdatedAt: number | null;
|
|
36
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;
|
|
37
50
|
/** This Agent's bounded Nested Node table, in first-seen order (spec §3). */
|
|
38
51
|
readonly nodes: readonly NodeInfo[];
|
|
39
52
|
/** Exited Nested Nodes dropped to keep the table bounded. */
|
|
@@ -103,6 +116,8 @@ export function placeholderAgent(): IdleAgentInfo {
|
|
|
103
116
|
sessionFile: null,
|
|
104
117
|
parent: null,
|
|
105
118
|
origin: "spawn",
|
|
119
|
+
compactions: 0,
|
|
120
|
+
lastCompaction: null,
|
|
106
121
|
state: "idle",
|
|
107
122
|
askIndex: null,
|
|
108
123
|
promptGist: null,
|
|
@@ -264,6 +279,29 @@ export function setUsage(
|
|
|
264
279
|
return { ...agent, tokens: observation.tokens, cost: observation.cost, usageUpdatedAt: at };
|
|
265
280
|
}
|
|
266
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
|
+
|
|
267
305
|
/**
|
|
268
306
|
* Folds authoritative shutdown accounting into the terminal Agent state.
|
|
269
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,6 +23,7 @@ export type {
|
|
|
22
23
|
AgentInfo,
|
|
23
24
|
AgentState,
|
|
24
25
|
AskingAgentInfo,
|
|
26
|
+
CompactionInfo,
|
|
25
27
|
ExitedAgentInfo,
|
|
26
28
|
IdleAgentInfo,
|
|
27
29
|
ModelFallbackInfo,
|
|
@@ -51,6 +53,8 @@ interface RunSummaryBase {
|
|
|
51
53
|
readonly worstFrameGapMs: number;
|
|
52
54
|
/** Run-wide Model Resolution fallbacks, pruned per-Agent entries included. */
|
|
53
55
|
readonly modelFallbacks: number;
|
|
56
|
+
/** Compactions across every Agent of this Run (ADR-0043). */
|
|
57
|
+
readonly compactions: number;
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
/** A Run that has not settled; it has neither outcome nor compatibility result. */
|
|
@@ -109,6 +113,7 @@ export function initialSummary(): RunningRunSummary {
|
|
|
109
113
|
durationMs: 0,
|
|
110
114
|
worstFrameGapMs: 0,
|
|
111
115
|
modelFallbacks: 0,
|
|
116
|
+
compactions: 0,
|
|
112
117
|
ok: null,
|
|
113
118
|
};
|
|
114
119
|
}
|
|
@@ -160,6 +165,21 @@ export function applyEvent(
|
|
|
160
165
|
event.agent,
|
|
161
166
|
applyAgentModel(summary.agents[event.agent] ?? placeholderAgent(), event),
|
|
162
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
|
+
);
|
|
163
183
|
case "agent_usage":
|
|
164
184
|
return withAgent(summary, event.agent, setUsage(summary.agents[event.agent], event, at));
|
|
165
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;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { REPORT_RESULT_EXTENSION_PATH, REPORT_RESULT_TOOL_NAME } from "../ask-contract/index.ts";
|
|
2
2
|
import { SYSTEM_PROMPT_RECORDER_EXTENSION_PATH } from "./system-prompt-recorder.ts";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
AgentStats,
|
|
5
|
+
CompactionResult,
|
|
6
|
+
Frame,
|
|
7
|
+
OpenOptions,
|
|
8
|
+
TokenBreakdown,
|
|
9
|
+
} from "./transport.ts";
|
|
4
10
|
|
|
5
11
|
/** What the Agent reported about its own work, read from a `get_state` probe. */
|
|
6
12
|
export interface AgentProgress {
|
|
@@ -23,7 +29,10 @@ export function piCommand(options: OpenOptions, toolProbeExtensionPath?: string)
|
|
|
23
29
|
}
|
|
24
30
|
appendCapabilities(cmd, options, toolProbeExtensionPath);
|
|
25
31
|
if (options.sessionDir) cmd.push("--session-dir", options.sessionDir);
|
|
32
|
+
// `--session` and `--fork` are mutually exclusive in pi, and a resume is the
|
|
33
|
+
// stronger claim: it names the very session this Agent must continue.
|
|
26
34
|
if (options.sessionFile) cmd.push("--session", options.sessionFile);
|
|
35
|
+
else if (options.forkSession) cmd.push("--fork", options.forkSession);
|
|
27
36
|
// No --approve: Agents inherit the user's saved project-trust decision (ADR-0009).
|
|
28
37
|
return cmd;
|
|
29
38
|
}
|
|
@@ -154,6 +163,47 @@ export function readStats(response: Frame): AgentStats {
|
|
|
154
163
|
return { tokens: readBreakdown("tokens" in data ? data.tokens : null), cost };
|
|
155
164
|
}
|
|
156
165
|
|
|
166
|
+
/**
|
|
167
|
+
* What a `compact` response reports (ADR-0043).
|
|
168
|
+
*
|
|
169
|
+
* A missing field becomes null rather than 0, so "unknown" and "free" stay
|
|
170
|
+
* distinguishable: a custom compaction handler may answer without usage.
|
|
171
|
+
*/
|
|
172
|
+
export function readCompaction(response: Frame): CompactionResult {
|
|
173
|
+
const data: unknown = response.data;
|
|
174
|
+
if (typeof data !== "object" || data === null) {
|
|
175
|
+
return { tokensBefore: null, tokensAfter: null, tokens: null, cost: null };
|
|
176
|
+
}
|
|
177
|
+
const record: Record<string, unknown> = { ...data };
|
|
178
|
+
const usage: unknown = record.usage;
|
|
179
|
+
const usageRecord: Record<string, unknown> =
|
|
180
|
+
typeof usage === "object" && usage !== null ? { ...usage } : {};
|
|
181
|
+
const cost: unknown = usageRecord.cost;
|
|
182
|
+
return {
|
|
183
|
+
tokensBefore: typeof record.tokensBefore === "number" ? record.tokensBefore : null,
|
|
184
|
+
tokensAfter:
|
|
185
|
+
typeof record.estimatedTokensAfter === "number" ? record.estimatedTokensAfter : null,
|
|
186
|
+
tokens: readCompactionTokens(usageRecord),
|
|
187
|
+
cost:
|
|
188
|
+
typeof cost === "object" &&
|
|
189
|
+
cost !== null &&
|
|
190
|
+
typeof (cost as { total?: unknown }).total === "number"
|
|
191
|
+
? (cost as { total: number }).total
|
|
192
|
+
: null,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The summary call's own token breakdown, or null when pi reported none. */
|
|
197
|
+
function readCompactionTokens(usage: Record<string, unknown>): TokenBreakdown | null {
|
|
198
|
+
return readBreakdown({
|
|
199
|
+
input: usage.input,
|
|
200
|
+
output: usage.output,
|
|
201
|
+
cacheRead: usage.cacheRead,
|
|
202
|
+
cacheWrite: usage.cacheWrite,
|
|
203
|
+
total: usage.totalTokens,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
157
207
|
function readBreakdown(tokens: unknown): TokenBreakdown | null {
|
|
158
208
|
if (typeof tokens !== "object" || tokens === null) return null;
|
|
159
209
|
const record: Record<string, unknown> = { ...tokens };
|
|
@@ -31,6 +31,23 @@ export interface AgentStats {
|
|
|
31
31
|
readonly cost: number | null;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* What one compaction of an Agent's context reported (ADR-0043).
|
|
36
|
+
*
|
|
37
|
+
* Every field is null when pi reported nothing for it: a custom compaction
|
|
38
|
+
* handler may answer without usage, and "unknown" must stay distinct from 0.
|
|
39
|
+
*/
|
|
40
|
+
export interface CompactionResult {
|
|
41
|
+
/** Context tokens before the summary replaced the transcript. */
|
|
42
|
+
readonly tokensBefore: number | null;
|
|
43
|
+
/** Estimated context tokens after the summary replaced the transcript. */
|
|
44
|
+
readonly tokensAfter: number | null;
|
|
45
|
+
/** Token breakdown of the summary call itself. */
|
|
46
|
+
readonly tokens: TokenBreakdown | null;
|
|
47
|
+
/** Cost of the summary call itself. */
|
|
48
|
+
readonly cost: number | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
34
51
|
/** Recorded inputs used to explain an Ask-hash mismatch without changing identity. */
|
|
35
52
|
export interface AskMarkerContext {
|
|
36
53
|
readonly prompt: string;
|
|
@@ -93,6 +110,27 @@ export interface AskCompletion {
|
|
|
93
110
|
readonly recovered?: true;
|
|
94
111
|
}
|
|
95
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Opaque marker naming one compaction of an Agent's context.
|
|
115
|
+
*
|
|
116
|
+
* A compaction sits between two Asks, so it cannot ride the Agent-level frame
|
|
117
|
+
* streams: a Cassette replay switches those cursors to an Ask and never
|
|
118
|
+
* switches back. The marker gives the exchange its own recorded streams.
|
|
119
|
+
*/
|
|
120
|
+
export interface CompactionMarker {
|
|
121
|
+
/** Monotonic per Agent, from 0. */
|
|
122
|
+
readonly index: number;
|
|
123
|
+
/** sha256 of the settled-Ask count and the custom instructions, when given. */
|
|
124
|
+
readonly hash: string;
|
|
125
|
+
/** Settled Asks of this Agent at the compaction point. */
|
|
126
|
+
readonly afterAsks: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Presence identifies Cassette playback of one compaction. */
|
|
130
|
+
export interface CompactionPlayback {
|
|
131
|
+
readonly result: CompactionResult;
|
|
132
|
+
}
|
|
133
|
+
|
|
96
134
|
/** Presence identifies Cassette playback, including recorded successful Asks. */
|
|
97
135
|
export interface AskPlayback {
|
|
98
136
|
/** Limit outcome recorded for this Ask, if it rejected with ASK_LIMIT. */
|
|
@@ -143,6 +181,15 @@ export interface AgentTransport {
|
|
|
143
181
|
/** Reports surfaced live outcomes; replay ignores completion. */
|
|
144
182
|
finishAsk(completion: AskCompletion): void;
|
|
145
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Begins one compaction. Live transports return undefined; replay transports
|
|
186
|
+
* return the recorded result.
|
|
187
|
+
*/
|
|
188
|
+
beginCompaction(marker: CompactionMarker): CompactionPlayback | undefined;
|
|
189
|
+
|
|
190
|
+
/** Ends the compaction exchange, so later frames belong to the Agent again. */
|
|
191
|
+
finishCompaction(result: CompactionResult | undefined): void;
|
|
192
|
+
|
|
146
193
|
/**
|
|
147
194
|
* The extraction policy recorded for the Ask at `index`, when a Cassette
|
|
148
195
|
* backs it. An Ask recorded under an older policy keeps that policy, so its
|
|
@@ -200,6 +247,22 @@ export interface OpenOptions {
|
|
|
200
247
|
readonly sessionDir?: string;
|
|
201
248
|
/** Resumes an existing pi session, translated to `--session <path>`. */
|
|
202
249
|
readonly sessionFile?: string;
|
|
250
|
+
/** How the Agent came to be; absent means an ordinary spawn. Spawn identity. */
|
|
251
|
+
readonly origin?: "fork";
|
|
252
|
+
/** Name of the Agent this one was forked from. Spawn identity; never argv. */
|
|
253
|
+
readonly forkOf?: string;
|
|
254
|
+
/** Settled Asks of the fork source at the fork point. Spawn identity. */
|
|
255
|
+
readonly forkAsks?: number;
|
|
256
|
+
/**
|
|
257
|
+
* The fork source's session file, translated to `pi --fork <path>`.
|
|
258
|
+
* Never spawn identity: the path is machine-specific.
|
|
259
|
+
*/
|
|
260
|
+
readonly forkSession?: string;
|
|
261
|
+
/**
|
|
262
|
+
* The fork source's worktree, so the fork branches from it instead of from a
|
|
263
|
+
* clean base tree. Never spawn identity.
|
|
264
|
+
*/
|
|
265
|
+
readonly worktreeFrom?: WorktreeResolution;
|
|
203
266
|
}
|
|
204
267
|
|
|
205
268
|
/** Nondeterministic worktree identity resolved below the transport seam. */
|
|
@@ -28,9 +28,17 @@ export function worktreeTransport(inner: TransportFactory): TransportFactory {
|
|
|
28
28
|
observeStartup?: TransportStartupObserver,
|
|
29
29
|
): Promise<AgentTransport> {
|
|
30
30
|
if (options.worktree !== true) return inner.open(options, observeStartup);
|
|
31
|
-
const
|
|
32
|
-
await
|
|
33
|
-
|
|
31
|
+
const from = options.worktreeFrom;
|
|
32
|
+
const root = await repositoryRoot(from?.cwd ?? options.cwd, options.name);
|
|
33
|
+
// A fork branches from what its source already committed, so the source's
|
|
34
|
+
// dirty files stay behind and no clean check applies (ADR-0044).
|
|
35
|
+
if (from === undefined) await requireClean(root, options.name);
|
|
36
|
+
const resolution = await addWorktree({
|
|
37
|
+
root,
|
|
38
|
+
name: options.name,
|
|
39
|
+
namingRoot: await namingRoot(root),
|
|
40
|
+
startPoint: from?.branch ?? "HEAD",
|
|
41
|
+
});
|
|
34
42
|
const { worktree: _worktree, ...innerOptions } = options;
|
|
35
43
|
let reported = false;
|
|
36
44
|
const transport = await inner.open(
|
|
@@ -69,18 +77,43 @@ async function requireClean(cwd: string, name: string): Promise<void> {
|
|
|
69
77
|
}
|
|
70
78
|
}
|
|
71
79
|
|
|
72
|
-
|
|
80
|
+
/**
|
|
81
|
+
* The repository the worktree directory is named after.
|
|
82
|
+
*
|
|
83
|
+
* For a worktree it is the main repository, not the worktree itself, so a fork
|
|
84
|
+
* of a Worktree Agent does not nest `repo-worktrees/a1-worktrees/a2`.
|
|
85
|
+
*/
|
|
86
|
+
async function namingRoot(root: string): Promise<string> {
|
|
87
|
+
const result = await git(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
88
|
+
const common = result.stdout.trim();
|
|
89
|
+
if (result.code !== 0 || common === "" || !common.endsWith("/.git")) return root;
|
|
90
|
+
return common.slice(0, -"/.git".length);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function addWorktree(options: {
|
|
94
|
+
readonly root: string;
|
|
95
|
+
readonly name: string;
|
|
96
|
+
readonly namingRoot: string;
|
|
97
|
+
readonly startPoint: string;
|
|
98
|
+
}): Promise<{
|
|
73
99
|
readonly cwd: string;
|
|
74
100
|
readonly branch: string;
|
|
75
101
|
}> {
|
|
76
|
-
const parent = join(dirname(options.
|
|
102
|
+
const parent = join(dirname(options.namingRoot), `${basename(options.namingRoot)}-worktrees`);
|
|
77
103
|
await mkdir(parent, { recursive: true });
|
|
78
104
|
for (let suffix = 1; ; suffix += 1) {
|
|
79
105
|
const leaf = suffix === 1 ? options.name : `${options.name}-${suffix}`;
|
|
80
106
|
const branch = `yaag/${leaf}`;
|
|
81
107
|
const cwd = join(parent, leaf);
|
|
82
108
|
if (await candidateTaken(options.root, branch, cwd)) continue;
|
|
83
|
-
const result = await git(options.root, [
|
|
109
|
+
const result = await git(options.root, [
|
|
110
|
+
"worktree",
|
|
111
|
+
"add",
|
|
112
|
+
"-b",
|
|
113
|
+
branch,
|
|
114
|
+
cwd,
|
|
115
|
+
options.startPoint,
|
|
116
|
+
]);
|
|
84
117
|
if (result.code === 0) return { cwd, branch };
|
|
85
118
|
if (isCollision(result)) continue;
|
|
86
119
|
throw new Error(`git worktree add failed: ${result.stderr.trim() || result.stdout.trim()}`);
|