@ian-pascoe/pi-minimal-subagents 0.12.0 → 0.14.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/README.md +115 -26
- package/package.json +2 -2
- package/src/minimal-subagents-capabilities.ts +12 -1
- package/src/minimal-subagents-config.ts +11 -6
- package/src/minimal-subagents-context.ts +180 -4
- package/src/minimal-subagents-coordinator.ts +303 -32
- package/src/minimal-subagents-delivery-ledger.ts +14 -13
- package/src/minimal-subagents-extension.ts +62 -2
- package/src/minimal-subagents-registry-wire.ts +1 -0
- package/src/minimal-subagents-registry.ts +6 -1
- package/src/minimal-subagents-render-contract.ts +33 -2
- package/src/minimal-subagents-rendering.ts +321 -52
- package/src/minimal-subagents-sessions.ts +3 -1
- package/src/minimal-subagents-status-panel.ts +26 -203
- package/src/minimal-subagents-tool-schemas.ts +20 -3
- package/src/minimal-subagents-tools.ts +109 -35
- package/src/minimal-subagents-transcript.ts +296 -0
- package/src/minimal-subagents-types.ts +28 -5
- package/src/minimal-subagents-ui.ts +1 -1
- package/src/minimal-subagents-usage.ts +13 -2
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from "node:util";
|
|
2
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import { contentText } from "@earendil-works/pi-ai";
|
|
4
|
+
import {
|
|
5
|
+
AssistantMessageComponent,
|
|
6
|
+
BashExecutionComponent,
|
|
7
|
+
BranchSummaryMessageComponent,
|
|
8
|
+
CompactionSummaryMessageComponent,
|
|
9
|
+
CustomMessageComponent,
|
|
10
|
+
ToolExecutionComponent,
|
|
11
|
+
UserMessageComponent,
|
|
12
|
+
type TruncationResult,
|
|
13
|
+
} from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import { Container, Text, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
15
|
+
import {
|
|
16
|
+
ACTIVITY_RAIL_WIDTH,
|
|
17
|
+
drawActivityRail,
|
|
18
|
+
renderMinimalSubagentsMessage,
|
|
19
|
+
renderMinimalSubagentsResult,
|
|
20
|
+
type MinimalSubagentsRenderTheme,
|
|
21
|
+
} from "./minimal-subagents-rendering.js";
|
|
22
|
+
import type { ChildAgentTranscriptSnapshot } from "./minimal-subagents-types.js";
|
|
23
|
+
|
|
24
|
+
interface CachedTranscriptMessage {
|
|
25
|
+
container: Container;
|
|
26
|
+
tools: Map<string, ToolExecutionComponent>;
|
|
27
|
+
expanded: boolean;
|
|
28
|
+
streaming: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Rendered transcript lines and the first line of each message. */
|
|
32
|
+
export interface TranscriptLayout {
|
|
33
|
+
lines: string[];
|
|
34
|
+
messageStarts: number[];
|
|
35
|
+
/** First line of each native component, such as one assistant reply or one tool execution. */
|
|
36
|
+
itemStarts: number[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Native components reused across renders while their source messages are unchanged. */
|
|
40
|
+
export interface TranscriptRenderCache {
|
|
41
|
+
messages: WeakMap<AgentMessage, CachedTranscriptMessage>;
|
|
42
|
+
results: WeakMap<ToolExecutionComponent, AgentMessage>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Render a Child Session Transcript with Pi's native message and tool components. */
|
|
46
|
+
export function renderTranscriptSnapshot(
|
|
47
|
+
snapshot: ChildAgentTranscriptSnapshot,
|
|
48
|
+
tui: TUI,
|
|
49
|
+
cwd: string,
|
|
50
|
+
expanded: boolean,
|
|
51
|
+
width: number,
|
|
52
|
+
cache: TranscriptRenderCache,
|
|
53
|
+
): TranscriptLayout {
|
|
54
|
+
if (snapshot.messages.length === 0) {
|
|
55
|
+
return {
|
|
56
|
+
lines: new Text(snapshot.fallback || "No conversation messages yet.", 3, 0).render(width),
|
|
57
|
+
messageStarts: [0],
|
|
58
|
+
itemStarts: [0],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const blocks: Container[] = [];
|
|
62
|
+
const tools = new Map(
|
|
63
|
+
snapshot.toolDefinitions.map((definition) => [definition.name, definition]),
|
|
64
|
+
);
|
|
65
|
+
const pendingTools = new Map<string, ToolExecutionComponent>();
|
|
66
|
+
const currentMessages = new Set(snapshot.messages);
|
|
67
|
+
|
|
68
|
+
for (const [messageIndex, message] of snapshot.messages.entries()) {
|
|
69
|
+
if (message.role === "toolResult") {
|
|
70
|
+
const paired = pendingTools.get(message.toolCallId);
|
|
71
|
+
if (paired) {
|
|
72
|
+
if (cache.results.get(paired) !== message) {
|
|
73
|
+
paired.updateResult(message);
|
|
74
|
+
cache.results.set(paired, message);
|
|
75
|
+
}
|
|
76
|
+
pendingTools.delete(message.toolCallId);
|
|
77
|
+
blocks.push(new Container());
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const streaming = messageIndex === snapshot.streamingAssistantIndex;
|
|
82
|
+
const cached = cache.messages.get(message);
|
|
83
|
+
const staleResult =
|
|
84
|
+
cached &&
|
|
85
|
+
[...cached.tools.values()].some((tool) => {
|
|
86
|
+
const result = cache.results.get(tool);
|
|
87
|
+
return result !== undefined && !currentMessages.has(result);
|
|
88
|
+
});
|
|
89
|
+
if (cached && !staleResult && cached.expanded === expanded && cached.streaming === streaming) {
|
|
90
|
+
blocks.push(cached.container);
|
|
91
|
+
for (const [id, tool] of cached.tools) pendingTools.set(id, tool);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const container = new Container();
|
|
95
|
+
const messageTools = new Map<string, ToolExecutionComponent>();
|
|
96
|
+
switch (message.role) {
|
|
97
|
+
case "user": {
|
|
98
|
+
const text = contentText(message.content, "\n\n");
|
|
99
|
+
if (text) container.addChild(new UserMessageComponent(text));
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
case "assistant": {
|
|
103
|
+
const assistant = new AssistantMessageComponent(message);
|
|
104
|
+
assistant.updateContent(message, messageIndex === snapshot.streamingAssistantIndex);
|
|
105
|
+
container.addChild(assistant);
|
|
106
|
+
for (const content of message.content) {
|
|
107
|
+
if (content.type !== "toolCall") continue;
|
|
108
|
+
const tool = new ToolExecutionComponent(
|
|
109
|
+
content.name,
|
|
110
|
+
content.id,
|
|
111
|
+
content.arguments,
|
|
112
|
+
{ showImages: false },
|
|
113
|
+
tools.get(content.name) ?? {},
|
|
114
|
+
tui,
|
|
115
|
+
cwd,
|
|
116
|
+
);
|
|
117
|
+
tool.setExpanded(expanded);
|
|
118
|
+
container.addChild(tool);
|
|
119
|
+
if (message.stopReason === "aborted" || message.stopReason === "error") {
|
|
120
|
+
tool.updateResult({
|
|
121
|
+
content: [
|
|
122
|
+
{
|
|
123
|
+
type: "text",
|
|
124
|
+
text:
|
|
125
|
+
message.stopReason === "aborted"
|
|
126
|
+
? "Operation aborted"
|
|
127
|
+
: (message.errorMessage ?? "Error"),
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
isError: true,
|
|
131
|
+
});
|
|
132
|
+
} else {
|
|
133
|
+
pendingTools.set(content.id, tool);
|
|
134
|
+
messageTools.set(content.id, tool);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
case "toolResult": {
|
|
140
|
+
const inherited = new ToolExecutionComponent(
|
|
141
|
+
message.toolName,
|
|
142
|
+
message.toolCallId,
|
|
143
|
+
{},
|
|
144
|
+
{ showImages: false },
|
|
145
|
+
{},
|
|
146
|
+
tui,
|
|
147
|
+
cwd,
|
|
148
|
+
);
|
|
149
|
+
inherited.setExpanded(expanded);
|
|
150
|
+
inherited.updateResult(message);
|
|
151
|
+
container.addChild(inherited);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case "custom": {
|
|
155
|
+
if (!message.display) break;
|
|
156
|
+
const renderer =
|
|
157
|
+
message.customType === "minimal-subagents.message"
|
|
158
|
+
? renderMinimalSubagentsMessage
|
|
159
|
+
: message.customType === "minimal-subagents.result"
|
|
160
|
+
? renderMinimalSubagentsResult
|
|
161
|
+
: undefined;
|
|
162
|
+
const custom = new CustomMessageComponent(message, renderer);
|
|
163
|
+
custom.setExpanded(expanded);
|
|
164
|
+
container.addChild(custom);
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case "bashExecution": {
|
|
168
|
+
const bash = new BashExecutionComponent(message.command, tui, message.excludeFromContext);
|
|
169
|
+
if (message.output) bash.appendOutput(message.output);
|
|
170
|
+
bash.setComplete(
|
|
171
|
+
message.exitCode,
|
|
172
|
+
message.cancelled,
|
|
173
|
+
// SAFETY: Persisted bash messages retain only the truncation flag; BashExecutionComponent reads that flag here.
|
|
174
|
+
message.truncated ? ({ truncated: true } as TruncationResult) : undefined,
|
|
175
|
+
message.fullOutputPath,
|
|
176
|
+
);
|
|
177
|
+
bash.setExpanded(expanded);
|
|
178
|
+
container.addChild(bash);
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
case "branchSummary": {
|
|
182
|
+
const summary = new BranchSummaryMessageComponent(message);
|
|
183
|
+
summary.setExpanded(expanded);
|
|
184
|
+
container.addChild(summary);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
case "compactionSummary": {
|
|
188
|
+
const summary = new CompactionSummaryMessageComponent(message);
|
|
189
|
+
summary.setExpanded(expanded);
|
|
190
|
+
container.addChild(summary);
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
cache.messages.set(message, { container, tools: messageTools, expanded, streaming });
|
|
195
|
+
blocks.push(container);
|
|
196
|
+
}
|
|
197
|
+
let length = 0;
|
|
198
|
+
const messageStarts: number[] = [];
|
|
199
|
+
const itemStarts: number[] = [];
|
|
200
|
+
// Containers concatenate their children, so rendering children one by one yields the same lines.
|
|
201
|
+
const lines = blocks.flatMap((block) => {
|
|
202
|
+
messageStarts.push(length);
|
|
203
|
+
return block.children.flatMap((child) => {
|
|
204
|
+
itemStarts.push(length);
|
|
205
|
+
// Native user-message prompt zones belong to the main terminal, not an embedded overlay.
|
|
206
|
+
const rendered = child
|
|
207
|
+
.render(width)
|
|
208
|
+
.map((line) =>
|
|
209
|
+
line
|
|
210
|
+
.replaceAll("\x1b]133;A\x07", "")
|
|
211
|
+
.replaceAll("\x1b]133;B\x07", "")
|
|
212
|
+
.replaceAll("\x1b]133;C\x07", ""),
|
|
213
|
+
);
|
|
214
|
+
length += rendered.length;
|
|
215
|
+
return rendered;
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
return { lines, messageStarts, itemStarts };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Create an empty cache for {@link renderTranscriptSnapshot}. */
|
|
222
|
+
export function createTranscriptRenderCache(): TranscriptRenderCache {
|
|
223
|
+
return { messages: new WeakMap(), results: new WeakMap() };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Collapsed live views keep at most this many lines of the child's recent work. */
|
|
227
|
+
const COLLAPSED_TRANSCRIPT_RAIL_LINES = 20;
|
|
228
|
+
|
|
229
|
+
function isSpacerLine(line: string): boolean {
|
|
230
|
+
// Includes a tool box's coloured padding rows, so each rail connector meets visible content.
|
|
231
|
+
return stripVTControlCharacters(line).trim().length === 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function withoutSpacerEdges(lines: string[]): string[] {
|
|
235
|
+
const start = lines.findIndex((line) => !isSpacerLine(line));
|
|
236
|
+
if (start < 0) return [];
|
|
237
|
+
const end = lines.findLastIndex((line) => !isSpacerLine(line));
|
|
238
|
+
return lines.slice(start, end + 1);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* A child's recent transcript drawn with Pi's native message and tool components, one rail item
|
|
243
|
+
* per component. Collapsed, it keeps the latest items that fit the line budget.
|
|
244
|
+
*/
|
|
245
|
+
export class TranscriptRail implements Component {
|
|
246
|
+
constructor(
|
|
247
|
+
private readonly snapshot: ChildAgentTranscriptSnapshot,
|
|
248
|
+
private readonly tui: TUI,
|
|
249
|
+
private readonly cwd: string,
|
|
250
|
+
private readonly expanded: boolean,
|
|
251
|
+
private readonly cache: TranscriptRenderCache,
|
|
252
|
+
private readonly theme: MinimalSubagentsRenderTheme,
|
|
253
|
+
) {}
|
|
254
|
+
|
|
255
|
+
render(width: number): string[] {
|
|
256
|
+
const layout = renderTranscriptSnapshot(
|
|
257
|
+
this.snapshot,
|
|
258
|
+
this.tui,
|
|
259
|
+
this.cwd,
|
|
260
|
+
this.expanded,
|
|
261
|
+
Math.max(1, width - ACTIVITY_RAIL_WIDTH),
|
|
262
|
+
this.cache,
|
|
263
|
+
);
|
|
264
|
+
const items = layout.itemStarts
|
|
265
|
+
.map((start, index) =>
|
|
266
|
+
withoutSpacerEdges(layout.lines.slice(start, layout.itemStarts[index + 1])),
|
|
267
|
+
)
|
|
268
|
+
.filter((lines) => lines.length > 0);
|
|
269
|
+
if (this.expanded) return drawActivityRail(items, this.theme);
|
|
270
|
+
let shown = 0;
|
|
271
|
+
let lineCount = 0;
|
|
272
|
+
for (const item of items.toReversed()) {
|
|
273
|
+
if (shown > 0 && lineCount + item.length > COLLAPSED_TRANSCRIPT_RAIL_LINES) break;
|
|
274
|
+
shown++;
|
|
275
|
+
lineCount += item.length;
|
|
276
|
+
}
|
|
277
|
+
const kept = items
|
|
278
|
+
.slice(items.length - shown)
|
|
279
|
+
.map((lines) => lines.slice(0, COLLAPSED_TRANSCRIPT_RAIL_LINES));
|
|
280
|
+
const earlier = items.length - shown;
|
|
281
|
+
return drawActivityRail(
|
|
282
|
+
earlier > 0
|
|
283
|
+
? [
|
|
284
|
+
[this.theme.fg("dim", `… ${earlier} earlier ${earlier === 1 ? "step" : "steps"}`)],
|
|
285
|
+
...kept,
|
|
286
|
+
]
|
|
287
|
+
: kept,
|
|
288
|
+
this.theme,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
invalidate(): void {
|
|
293
|
+
this.cache.messages = new WeakMap();
|
|
294
|
+
this.cache.results = new WeakMap();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type { Usage } from "@earendil-works/pi-ai";
|
|
3
3
|
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { MinimalSubagentsModelRole } from "./minimal-subagents-config.js";
|
|
4
5
|
|
|
5
6
|
/** Controls how much committed caller conversation enters a new child session. */
|
|
6
7
|
export type SessionContextMode = "inherit" | "compact" | "omit";
|
|
@@ -35,17 +36,24 @@ export interface SpawnParameters {
|
|
|
35
36
|
agent_id?: string;
|
|
36
37
|
session_context?: SessionContextMode;
|
|
37
38
|
project_context?: ProjectContextMode;
|
|
39
|
+
/** Configured model role whose model and thinking level apply unless overridden below. */
|
|
40
|
+
role?: string;
|
|
38
41
|
model?: string;
|
|
39
42
|
thinking_level?: ThinkingLevel;
|
|
40
43
|
tools?: ToolSelection;
|
|
41
44
|
delegation?: DelegationMode;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
/** Returns persistent agent and turn identities immediately after launch scheduling. */
|
|
45
|
-
export interface SpawnResult {
|
|
47
|
+
/** Returns persistent agent and turn identities plus the resolved grant immediately after launch scheduling. */
|
|
48
|
+
export interface SpawnResult extends RuntimeProfile {
|
|
46
49
|
agent_id: string;
|
|
47
50
|
turn_id: string;
|
|
48
51
|
status: "running";
|
|
52
|
+
/** Concrete ordinary tools granted by the Launch Contract. */
|
|
53
|
+
tools: string[];
|
|
54
|
+
delegation: DelegationMode;
|
|
55
|
+
/** Non-fatal tool-resolution warnings, such as skipped configured tools. */
|
|
56
|
+
warnings?: string[];
|
|
49
57
|
}
|
|
50
58
|
|
|
51
59
|
/** Retains one keyed terminal turn result for waits and durable delivery recovery. */
|
|
@@ -59,13 +67,18 @@ export interface TurnResult {
|
|
|
59
67
|
elapsed_ms?: number;
|
|
60
68
|
}
|
|
61
69
|
|
|
62
|
-
/**
|
|
63
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Reports whether one direct message was handed to a wait, queued into an active turn, started a
|
|
72
|
+
* new turn on an idle recipient, or failed.
|
|
73
|
+
*/
|
|
74
|
+
export type AgentMessageDisposition = "delivered-via-wait" | "queued" | "started-turn" | "failed";
|
|
64
75
|
|
|
65
76
|
export interface AgentMessageResult {
|
|
66
77
|
agent_id: string;
|
|
67
78
|
message_id: string;
|
|
68
79
|
disposition: AgentMessageDisposition;
|
|
80
|
+
/** Turn started on an idle recipient; present only for `started-turn`. */
|
|
81
|
+
turn_id?: string;
|
|
69
82
|
error?: string;
|
|
70
83
|
}
|
|
71
84
|
|
|
@@ -112,7 +125,8 @@ export interface AgentSummary extends RuntimeProfile {
|
|
|
112
125
|
latest_activity_at?: string;
|
|
113
126
|
task?: string;
|
|
114
127
|
child_count: number;
|
|
115
|
-
|
|
128
|
+
/** Nested summaries; omitted from caller-scoped status, which reports only `child_count`. */
|
|
129
|
+
children?: AgentSummary[];
|
|
116
130
|
}
|
|
117
131
|
|
|
118
132
|
/** Provides bounded recent child conversation text for detailed status. */
|
|
@@ -129,6 +143,12 @@ export interface RecentAgentActivity {
|
|
|
129
143
|
truncated: boolean;
|
|
130
144
|
}
|
|
131
145
|
|
|
146
|
+
/** Live progress of a Child Agent's running turn, shown while its parent waits. */
|
|
147
|
+
export interface ActiveTurnProgress {
|
|
148
|
+
turn_id: string;
|
|
149
|
+
tool_calls: number;
|
|
150
|
+
}
|
|
151
|
+
|
|
132
152
|
/** Holds the complete selected-branch Child Session Transcript for trusted status UI. */
|
|
133
153
|
export interface ChildAgentTranscriptSnapshot {
|
|
134
154
|
messages: AgentMessage[];
|
|
@@ -182,6 +202,8 @@ export interface DeleteResult {
|
|
|
182
202
|
|
|
183
203
|
/** Persists immutable context, model, thinking, and ordinary-tool launch choices. */
|
|
184
204
|
export interface LaunchContract extends RuntimeProfile {
|
|
205
|
+
/** Configured model role the caller named at launch; absent when none was used. */
|
|
206
|
+
role?: string;
|
|
185
207
|
session_context: SessionContextMode;
|
|
186
208
|
project_context: ProjectContextMode;
|
|
187
209
|
tools: ToolSelection | undefined;
|
|
@@ -407,6 +429,7 @@ export interface CoordinatorDependencies {
|
|
|
407
429
|
root: RootConversationEndpoint;
|
|
408
430
|
maxSubagentDepth?: number;
|
|
409
431
|
toolsets?: MinimalSubagentsToolsets;
|
|
432
|
+
modelRoles?: readonly MinimalSubagentsModelRole[];
|
|
410
433
|
now?: () => Date;
|
|
411
434
|
automaticDeliveryGraceMs?: number;
|
|
412
435
|
notify?: (notification: CoordinatorNotification) => void;
|
|
@@ -59,7 +59,7 @@ function flattenAgentHierarchy(agents: readonly AgentSummary[]): FlattenedAgentS
|
|
|
59
59
|
const flattened: FlattenedAgentSummary[] = [];
|
|
60
60
|
const visit = (agent: AgentSummary, depth: number, parentId?: string) => {
|
|
61
61
|
flattened.push({ agent, depth, parentId, order: flattened.length });
|
|
62
|
-
for (const child of agent.children) visit(child, depth + 1, agent.agent_id);
|
|
62
|
+
for (const child of agent.children ?? []) visit(child, depth + 1, agent.agent_id);
|
|
63
63
|
};
|
|
64
64
|
for (const agent of agents) visit(agent, 0);
|
|
65
65
|
return flattened;
|
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
import type { Usage } from "@earendil-works/pi-ai";
|
|
2
2
|
|
|
3
|
+
/** Return a clone with optional counters present, so every reported usage has one shape. */
|
|
4
|
+
export function normalizeMinimalSubagentsUsage(usage: Usage): Usage {
|
|
5
|
+
return {
|
|
6
|
+
...structuredClone(usage),
|
|
7
|
+
cacheWrite1h: usage.cacheWrite1h ?? 0,
|
|
8
|
+
reasoning: usage.reasoning ?? 0,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
3
12
|
/** Add two optional Pi usage totals without retaining mutable references to either input. */
|
|
4
13
|
export function addMinimalSubagentsUsage(
|
|
5
14
|
left: Usage | undefined,
|
|
6
15
|
right: Usage | undefined,
|
|
7
16
|
): Usage | undefined {
|
|
8
|
-
if (!left) return right ?
|
|
9
|
-
if (!right) return
|
|
17
|
+
if (!left) return right ? normalizeMinimalSubagentsUsage(right) : undefined;
|
|
18
|
+
if (!right) return normalizeMinimalSubagentsUsage(left);
|
|
10
19
|
return {
|
|
11
20
|
input: left.input + right.input,
|
|
12
21
|
output: left.output + right.output,
|
|
13
22
|
cacheRead: left.cacheRead + right.cacheRead,
|
|
14
23
|
cacheWrite: left.cacheWrite + right.cacheWrite,
|
|
24
|
+
cacheWrite1h: (left.cacheWrite1h ?? 0) + (right.cacheWrite1h ?? 0),
|
|
25
|
+
reasoning: (left.reasoning ?? 0) + (right.reasoning ?? 0),
|
|
15
26
|
totalTokens: left.totalTokens + right.totalTokens,
|
|
16
27
|
cost: {
|
|
17
28
|
input: left.cost.input + right.cost.input,
|