@ian-pascoe/pi-minimal-subagents 0.14.1 → 0.16.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 +19 -11
- package/package.json +1 -1
- package/src/minimal-subagents-context.ts +1 -1
- package/src/minimal-subagents-coordinator.ts +58 -4
- package/src/minimal-subagents-render-contract.ts +16 -3
- package/src/minimal-subagents-rendering.ts +25 -8
- package/src/minimal-subagents-tools.ts +25 -9
- package/src/minimal-subagents-types.ts +8 -3
- package/src/minimal-subagents-usage.ts +30 -0
package/README.md
CHANGED
|
@@ -49,7 +49,9 @@ From this package checkout, load the source directly with
|
|
|
49
49
|
pass `turn_id` to re-read a turn whose result is still retained, claimed or
|
|
50
50
|
not.
|
|
51
51
|
4. `subagent_status` inspects children; `subagent_cancel` stops work but keeps
|
|
52
|
-
the session; `subagent_delete` removes it. Deleted IDs cannot be reused
|
|
52
|
+
the session; `subagent_delete` removes it. Deleted IDs cannot be reused, and
|
|
53
|
+
spawning with an existing `agent_id` fails with that child's state and the
|
|
54
|
+
next step (`agent_message`, `subagent_delete`, or another `agent_id`).
|
|
53
55
|
|
|
54
56
|
## Configuration
|
|
55
57
|
|
|
@@ -274,18 +276,21 @@ creation; reloading does not silently broaden them. The default maximum depth
|
|
|
274
276
|
is two levels beneath the interactive Root Agent.
|
|
275
277
|
|
|
276
278
|
The extension registers six coordinator tools for the Root Agent and fanout
|
|
277
|
-
children: `subagent`, `agent_message`, `subagent_wait`, `subagent_status`,
|
|
278
|
-
`subagent_cancel`, and `subagent_delete`.
|
|
279
|
-
|
|
280
|
-
|
|
279
|
+
children below the depth cap: `subagent`, `agent_message`, `subagent_wait`, `subagent_status`,
|
|
280
|
+
`subagent_cancel`, and `subagent_delete`. Children that cannot spawn (delegation
|
|
281
|
+
`none`, or fanout at the depth cap) have no children to wait on or inspect, so they
|
|
282
|
+
receive only `agent_message`.
|
|
281
283
|
|
|
282
|
-
|
|
284
|
+
A running child's `latest_activity_at` advances with the timestamps of its latest
|
|
285
|
+
messages and tool results. Targeted `subagent_status` includes `recent_activity`, a bounded tail of message
|
|
283
286
|
text, reasoning, tool calls, and tool results. It includes the current streaming
|
|
284
|
-
assistant message but omits image data. Timeout Wait Events
|
|
285
|
-
|
|
287
|
+
assistant message but omits image data. Timeout Wait Events carry only a compact
|
|
288
|
+
progress snapshot. Children target only direct children; the Root Agent
|
|
286
289
|
may inspect any descendant. Model-facing status reports a `child_count` but no
|
|
287
290
|
nested `children` summaries. Reported `usage` always includes `cacheWrite1h`
|
|
288
|
-
and `reasoning`, as `0` when the provider reports none.
|
|
291
|
+
and `reasoning`, as `0` when the provider reports none. Tool-result text and
|
|
292
|
+
`structuredContent` round every `usage.cost` field to six decimal places of USD;
|
|
293
|
+
exact values stay in session data. `subagent_cancel`
|
|
289
294
|
lists in `affected_agent_ids` only agents whose active turns it cancelled.
|
|
290
295
|
|
|
291
296
|
`session_context` defaults to `omit`: the child starts with only its system
|
|
@@ -351,8 +356,11 @@ not claim the turn, and `turn_id` rereads the full result. A
|
|
|
351
356
|
caller may have only one outstanding wait for the same source turn; a
|
|
352
357
|
concurrent duplicate is rejected instead of competing for one Wait Event.
|
|
353
358
|
When `timeout_ms` expires, the wait returns an observational `event: "timeout"`
|
|
354
|
-
with the requested turn identity and
|
|
355
|
-
|
|
359
|
+
with the requested turn identity and a compact, bounded progress snapshot
|
|
360
|
+
(`state`, `elapsed_ms`, `latest_activity_at`, `total_tokens`, and the labels of
|
|
361
|
+
the last three Recent Activity items as `recent_activity_labels`), under about 1 KB. It omits the task and
|
|
362
|
+
Launch Contract; targeted `subagent_status` returns the full detail.
|
|
363
|
+
It removes only the waiter, leaving the child
|
|
356
364
|
running and all pending delivery unclaimed. Abort signals remain errors.
|
|
357
365
|
|
|
358
366
|
The persisted Delivery Ledger records Coordination Messages, terminal results,
|
package/package.json
CHANGED
|
@@ -353,7 +353,7 @@ export function buildSubagentSystemPrompt(
|
|
|
353
353
|
): string {
|
|
354
354
|
const coordinatorBoundary = options.canSpawn
|
|
355
355
|
? "Coordinator tools support subagent, agent_message, subagent_wait, subagent_status, subagent_cancel, and subagent_delete. Wait, status, cancel, and delete target direct children only; recursive cancel and delete may affect a child's subtree."
|
|
356
|
-
: "Coordinator tools support agent_message
|
|
356
|
+
: "Coordinator tools support only agent_message; you have no children to wait on or inspect.";
|
|
357
357
|
const delegationBoundary = options.canSpawn
|
|
358
358
|
? [
|
|
359
359
|
"You have explicit fanout responsibility for this assigned task.",
|
|
@@ -69,12 +69,17 @@ import type {
|
|
|
69
69
|
StatusResult,
|
|
70
70
|
TurnResult,
|
|
71
71
|
WaitMessageResult,
|
|
72
|
+
WaitTimeoutResult,
|
|
72
73
|
WaitDeliveredTurnResult,
|
|
73
74
|
WaitResult,
|
|
74
75
|
} from "./minimal-subagents-types.js";
|
|
75
76
|
|
|
76
77
|
const FRIENDLY_AGENT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
77
78
|
const RESERVED_AGENT_IDS = new Set(["root", "parent"]);
|
|
79
|
+
/** Recent Activity labels a timed-out wait reports. */
|
|
80
|
+
const WAIT_TIMEOUT_ACTIVITY_LIMIT = 3;
|
|
81
|
+
/** Characters kept of each label so a timed-out wait stays bounded whatever a tool is named. */
|
|
82
|
+
const WAIT_TIMEOUT_LABEL_MAX_LENGTH = 80;
|
|
78
83
|
const RECENT_MESSAGE_LIMIT = 20;
|
|
79
84
|
const DEFAULT_AUTOMATIC_DELIVERY_GRACE_MS = 1_000;
|
|
80
85
|
|
|
@@ -260,7 +265,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
260
265
|
);
|
|
261
266
|
}
|
|
262
267
|
if (this.agents.has(agentId) || this.pendingAgentIds.has(agentId)) {
|
|
263
|
-
throw new Error(
|
|
268
|
+
throw new Error(this.duplicateAgentIdMessage(agentId));
|
|
264
269
|
}
|
|
265
270
|
|
|
266
271
|
const sessionContext = parameters.session_context ?? "omit";
|
|
@@ -481,7 +486,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
481
486
|
}
|
|
482
487
|
}
|
|
483
488
|
|
|
484
|
-
/** Wait for one exact turn, returning
|
|
489
|
+
/** Wait for one exact turn, returning compact child progress if the waiter times out. */
|
|
485
490
|
wait(
|
|
486
491
|
callerId: string,
|
|
487
492
|
agentId: string,
|
|
@@ -546,7 +551,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
546
551
|
agent_id: agentId,
|
|
547
552
|
turn_id: turnId,
|
|
548
553
|
timeout_ms: timeoutMs,
|
|
549
|
-
|
|
554
|
+
...this.buildWaitTimeoutProgress(agent),
|
|
550
555
|
});
|
|
551
556
|
}, timeoutMs);
|
|
552
557
|
}
|
|
@@ -1838,6 +1843,20 @@ export class MinimalSubagentsCoordinator {
|
|
|
1838
1843
|
this.removeSettledEmptyTurnClaim(delivery.source_agent_id, delivery.source_turn_id);
|
|
1839
1844
|
}
|
|
1840
1845
|
|
|
1846
|
+
/** Name an existing agent ID's state and the next steps that are valid for that state. */
|
|
1847
|
+
private duplicateAgentIdMessage(agentId: string): string {
|
|
1848
|
+
const prefix = `Minimal subagents duplicate agent ID: ${agentId}`;
|
|
1849
|
+
const existing = this.agents.get(agentId);
|
|
1850
|
+
if (!existing) {
|
|
1851
|
+
return `${prefix} is still being created; retry shortly or choose another agent_id`;
|
|
1852
|
+
}
|
|
1853
|
+
if (existing.availability === "unavailable") {
|
|
1854
|
+
return `${prefix} already exists (unavailable). Use subagent_delete to remove it (a deleted ID cannot be reused), or choose another agent_id.`;
|
|
1855
|
+
}
|
|
1856
|
+
const state = existing.active_turn_id ? "running" : "idle";
|
|
1857
|
+
return `${prefix} already exists (${state}). Use agent_message to continue it, subagent_delete to remove it (a deleted ID cannot be reused), or choose another agent_id.`;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1841
1860
|
private buildAgentSummary(agent: PersistedAgent, includeDescendants = true): AgentSummary {
|
|
1842
1861
|
const directChildren = this.childrenOf(agent.agent_id);
|
|
1843
1862
|
const elapsed = agent.active_turn_started_at
|
|
@@ -1860,7 +1879,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1860
1879
|
...runtimeProfile,
|
|
1861
1880
|
tools: runtime?.getActiveToolNames?.() ?? [...agent.launch_contract.ordinary_tools],
|
|
1862
1881
|
elapsed_ms: elapsed ?? agent.latest_result?.elapsed_ms,
|
|
1863
|
-
latest_activity_at:
|
|
1882
|
+
latest_activity_at: this.latestActivityAt(agent, runtime),
|
|
1864
1883
|
task: agent.task,
|
|
1865
1884
|
child_count: directChildren.length,
|
|
1866
1885
|
};
|
|
@@ -1870,6 +1889,41 @@ export class MinimalSubagentsCoordinator {
|
|
|
1870
1889
|
return summary;
|
|
1871
1890
|
}
|
|
1872
1891
|
|
|
1892
|
+
/**
|
|
1893
|
+
* The persisted activity time, advanced by the timestamps of the messages and tool results a
|
|
1894
|
+
* running child has produced since. Only running children can have newer unpersisted activity.
|
|
1895
|
+
*/
|
|
1896
|
+
private latestActivityAt(agent: PersistedAgent, runtime: ChildAgentRuntime | undefined): string {
|
|
1897
|
+
const persisted = agent.latest_activity_at ?? agent.created_at;
|
|
1898
|
+
if (!agent.active_turn_id || !runtime) return persisted;
|
|
1899
|
+
const persistedMs = Date.parse(persisted);
|
|
1900
|
+
let latestMs = persistedMs;
|
|
1901
|
+
for (const { timestamp } of runtime.snapshotActivityMessages()) {
|
|
1902
|
+
if (Number.isFinite(timestamp) && timestamp > latestMs) latestMs = timestamp;
|
|
1903
|
+
}
|
|
1904
|
+
return latestMs > persistedMs ? new Date(latestMs).toISOString() : persisted;
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
/** The compact progress a timed-out wait reports; `subagent_status` has the full detail. */
|
|
1908
|
+
private buildWaitTimeoutProgress(
|
|
1909
|
+
agent: PersistedAgent,
|
|
1910
|
+
): Omit<WaitTimeoutResult, "event" | "agent_id" | "turn_id" | "timeout_ms"> {
|
|
1911
|
+
const summary = this.buildAgentSummary(agent, false);
|
|
1912
|
+
const runtime = this.runtimes.get(agent.agent_id);
|
|
1913
|
+
const messages =
|
|
1914
|
+
runtime?.snapshotActivityMessages() ?? this.inspectTranscript(agent.agent_id).messages;
|
|
1915
|
+
const usage = runtime?.getUsage() ?? agent.latest_result?.usage;
|
|
1916
|
+
return {
|
|
1917
|
+
state: summary.state,
|
|
1918
|
+
elapsed_ms: summary.elapsed_ms,
|
|
1919
|
+
latest_activity_at: summary.latest_activity_at,
|
|
1920
|
+
total_tokens: usage?.totalTokens,
|
|
1921
|
+
recent_activity_labels: buildRecentAgentActivity(messages)
|
|
1922
|
+
.slice(-WAIT_TIMEOUT_ACTIVITY_LIMIT)
|
|
1923
|
+
.map((activity) => activity.label.slice(0, WAIT_TIMEOUT_LABEL_MAX_LENGTH)),
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1873
1927
|
private buildAgentDetail(agent: PersistedAgent, includeDescendants = true): AgentDetail {
|
|
1874
1928
|
const summary = this.buildAgentSummary(agent, includeDescendants);
|
|
1875
1929
|
const runtime = this.runtimes.get(agent.agent_id);
|
|
@@ -154,7 +154,13 @@ const WaitTimeoutDetailsSchema = Type.Object({
|
|
|
154
154
|
agent_id: Type.String(),
|
|
155
155
|
turn_id: Type.String(),
|
|
156
156
|
timeout_ms: Type.Number(),
|
|
157
|
-
|
|
157
|
+
state: Type.Optional(Type.String()),
|
|
158
|
+
elapsed_ms: Type.Optional(Type.Number()),
|
|
159
|
+
latest_activity_at: Type.Optional(Type.String()),
|
|
160
|
+
total_tokens: Type.Optional(Type.Number()),
|
|
161
|
+
recent_activity_labels: Type.Optional(Type.Array(Type.String())),
|
|
162
|
+
/** Pre-compact timeout results carried the full child status. */
|
|
163
|
+
agent: Type.Optional(RenderStatusAgentSchema),
|
|
158
164
|
});
|
|
159
165
|
const StatusDetailsSchema = Type.Union([
|
|
160
166
|
Type.Object({
|
|
@@ -266,7 +272,10 @@ const CoordinatorWaitSourceOutputSchema = {
|
|
|
266
272
|
source_turn_id: Type.String(),
|
|
267
273
|
};
|
|
268
274
|
|
|
269
|
-
/**
|
|
275
|
+
/**
|
|
276
|
+
* Final details schemas exposed to CodeMode for the six coordinator tools. `structuredContent`
|
|
277
|
+
* matches them with every `usage.cost` field rounded for presentation.
|
|
278
|
+
*/
|
|
270
279
|
export const CoordinatorToolOutputSchemas = {
|
|
271
280
|
subagent: Type.Object({
|
|
272
281
|
agent_id: Type.String(),
|
|
@@ -313,7 +322,11 @@ export const CoordinatorToolOutputSchemas = {
|
|
|
313
322
|
agent_id: Type.String(),
|
|
314
323
|
turn_id: Type.String(),
|
|
315
324
|
timeout_ms: Type.Number(),
|
|
316
|
-
|
|
325
|
+
state: Type.Union([Type.Literal("running"), Type.Literal("idle")]),
|
|
326
|
+
elapsed_ms: Type.Optional(Type.Number()),
|
|
327
|
+
latest_activity_at: Type.Optional(Type.String()),
|
|
328
|
+
total_tokens: Type.Optional(Type.Number()),
|
|
329
|
+
recent_activity_labels: Type.Array(Type.String()),
|
|
317
330
|
...CoordinatorWaitSourceOutputSchema,
|
|
318
331
|
}),
|
|
319
332
|
]),
|
|
@@ -585,6 +585,16 @@ function renderWaitProgress(
|
|
|
585
585
|
/** Shown instead of the output of a result already delivered to the parent automatically. */
|
|
586
586
|
const ALREADY_DELIVERED_TEXT = "Already delivered automatically; wait with turn_id to reread it.";
|
|
587
587
|
|
|
588
|
+
/** The activity labels of a timeout result, including pre-compact results that carried the full status. */
|
|
589
|
+
function timeoutActivityLabels(
|
|
590
|
+
details: Extract<WaitRenderDetails, { event: "timeout" }>,
|
|
591
|
+
): readonly string[] {
|
|
592
|
+
return (
|
|
593
|
+
details.recent_activity_labels ??
|
|
594
|
+
latestProgressActivity(details.agent?.recent_activity ?? []).map((entry) => entry.label)
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
588
598
|
/** The collapsed body of a settled wait: what the child said, or why it stopped. */
|
|
589
599
|
function collapsedWaitBody(
|
|
590
600
|
details: WaitRenderDetails,
|
|
@@ -593,8 +603,10 @@ function collapsedWaitBody(
|
|
|
593
603
|
): Component | undefined {
|
|
594
604
|
if (details.event === "message") return collapsedTextPreview(details.message, theme);
|
|
595
605
|
if (details.event === "timeout") {
|
|
596
|
-
const
|
|
597
|
-
return
|
|
606
|
+
const labels = timeoutActivityLabels(details);
|
|
607
|
+
return labels.length > 0
|
|
608
|
+
? collapsedTextPreview(theme.fg("dim", labels.join(" \u00b7 ")), theme)
|
|
609
|
+
: undefined;
|
|
598
610
|
}
|
|
599
611
|
if (details.already_delivered) {
|
|
600
612
|
return collapsedTextPreview(theme.fg("muted", ALREADY_DELIVERED_TEXT), theme);
|
|
@@ -627,7 +639,9 @@ function renderWaitResult(
|
|
|
627
639
|
details.event === "timeout" ? details.timeout_ms : details.elapsed_ms,
|
|
628
640
|
);
|
|
629
641
|
const usage = details.event === "timeout" ? undefined : details.usage;
|
|
630
|
-
const tokens = formatSubagentTokenCount(
|
|
642
|
+
const tokens = formatSubagentTokenCount(
|
|
643
|
+
details.event === "timeout" ? details.total_tokens : usage?.totalTokens,
|
|
644
|
+
);
|
|
631
645
|
const drainedMessageCount =
|
|
632
646
|
details.event === "message" || details.event === "timeout"
|
|
633
647
|
? 0
|
|
@@ -658,12 +672,15 @@ function renderWaitResult(
|
|
|
658
672
|
return container;
|
|
659
673
|
}
|
|
660
674
|
if (details.event === "timeout") {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
theme,
|
|
664
|
-
"Child status",
|
|
665
|
-
renderDetailedStatusAgent(details.agent, options, theme),
|
|
675
|
+
container.addChild(
|
|
676
|
+
renderLabelValue(theme, "State", details.state ?? details.agent?.state ?? "unknown"),
|
|
666
677
|
);
|
|
678
|
+
if (details.latest_activity_at) {
|
|
679
|
+
container.addChild(renderLabelValue(theme, "Latest activity", details.latest_activity_at));
|
|
680
|
+
}
|
|
681
|
+
const labels = timeoutActivityLabels(details);
|
|
682
|
+
if (labels.length > 0)
|
|
683
|
+
appendTextSection(container, theme, "Recent activity", labels.join("\n"));
|
|
667
684
|
return container;
|
|
668
685
|
}
|
|
669
686
|
if (details.messages && details.messages.length > 0) {
|
|
@@ -13,6 +13,7 @@ import type { TUI } from "@earendil-works/pi-tui";
|
|
|
13
13
|
import type { MinimalSubagentsCoordinator } from "./minimal-subagents-coordinator.js";
|
|
14
14
|
import { withTroubleshootingHint } from "./troubleshooting-skill.js";
|
|
15
15
|
import type { MinimalSubagentsModelRole } from "./minimal-subagents-config.js";
|
|
16
|
+
import { roundMinimalSubagentsUsageCosts } from "./minimal-subagents-usage.js";
|
|
16
17
|
import {
|
|
17
18
|
renderCoordinatorToolCall,
|
|
18
19
|
renderCoordinatorToolResult,
|
|
@@ -42,11 +43,8 @@ import type {
|
|
|
42
43
|
WaitResult,
|
|
43
44
|
} from "./minimal-subagents-types.js";
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"subagent_wait",
|
|
48
|
-
"subagent_status",
|
|
49
|
-
]);
|
|
46
|
+
/** A child that cannot spawn can never have children, so only adjacent messaging is useful. */
|
|
47
|
+
const NON_SPAWNING_CHILD_COORDINATOR_TOOL_NAMES = new Set(["agent_message"]);
|
|
50
48
|
|
|
51
49
|
/** Coordinator operations consumed by the six public coordinator tool definitions. */
|
|
52
50
|
export type CoordinatorToolOperations = Pick<
|
|
@@ -183,10 +181,28 @@ function createCoordinatorToolRendering(
|
|
|
183
181
|
};
|
|
184
182
|
}
|
|
185
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Serialize a tool result with every nested `usage` cost rounded for presentation. Exact values
|
|
186
|
+
* stay in `details`, the Registry, and session data, which never pass through here.
|
|
187
|
+
*/
|
|
188
|
+
function stringifyWithRoundedUsageCosts(
|
|
189
|
+
result: CoordinatorToolResultDetails,
|
|
190
|
+
space?: number,
|
|
191
|
+
): string {
|
|
192
|
+
// `JSON.stringify` types replacer values as `any`; a usage is recognized by key and `cost` object.
|
|
193
|
+
return JSON.stringify(
|
|
194
|
+
result,
|
|
195
|
+
(key, value) =>
|
|
196
|
+
key === "usage" && value?.cost ? roundMinimalSubagentsUsageCosts(value) : value,
|
|
197
|
+
space,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
186
201
|
function structuredToolResult<TDetails extends CoordinatorToolResultDetails>(
|
|
187
202
|
result: TDetails,
|
|
188
203
|
): AgentToolResult<TDetails> {
|
|
189
|
-
|
|
204
|
+
// Presentation only: `details` keeps the exact values the session persists.
|
|
205
|
+
const json = stringifyWithRoundedUsageCosts(result, 2);
|
|
190
206
|
const truncated = truncateHead(json, {
|
|
191
207
|
maxBytes: DEFAULT_MAX_BYTES,
|
|
192
208
|
maxLines: DEFAULT_MAX_LINES,
|
|
@@ -330,7 +346,7 @@ export function createCoordinatorToolDefinitions(
|
|
|
330
346
|
name: "subagent_wait",
|
|
331
347
|
label: "Subagent Wait",
|
|
332
348
|
description:
|
|
333
|
-
"Wait for one direct child's oldest observable turn whose result you have neither claimed nor received, or select an exact retained turn_id. An active child may first return event=message; later unconsumed items still fall back automatically. A settled turn returns event=turn, with any queued messages in messages; waiting again returns a result you claimed in full. Without turn_id, a result already delivered to you automatically returns event=turn with already_delivered=true and no output; pass its turn_id to reread it. Timeout returns event=timeout
|
|
349
|
+
"Wait for one direct child's oldest observable turn whose result you have neither claimed nor received, or select an exact retained turn_id. An active child may first return event=message; later unconsumed items still fall back automatically. A settled turn returns event=turn, with any queued messages in messages; waiting again returns a result you claimed in full. Without turn_id, a result already delivered to you automatically returns event=turn with already_delivered=true and no output; pass its turn_id to reread it. Timeout returns a compact event=timeout (state, elapsed_ms, latest_activity_at, total_tokens, last few activity labels; use subagent_status for detail) and never cancels the child.",
|
|
334
350
|
promptSnippet: "Wait for one direct child's exact turn",
|
|
335
351
|
parameters: options.schemas.subagent_wait,
|
|
336
352
|
annotations: {
|
|
@@ -380,7 +396,7 @@ export function createCoordinatorToolDefinitions(
|
|
|
380
396
|
? alreadyDeliveredContent(result)
|
|
381
397
|
: structuredToolResult(result).content,
|
|
382
398
|
details,
|
|
383
|
-
structuredContent: JSON.parse(
|
|
399
|
+
structuredContent: JSON.parse(stringifyWithRoundedUsageCosts(details)),
|
|
384
400
|
};
|
|
385
401
|
});
|
|
386
402
|
} finally {
|
|
@@ -483,5 +499,5 @@ export function createCoordinatorToolDefinitions(
|
|
|
483
499
|
const allowFanoutTools = options.allowFanoutTools ?? options.callerId === "root";
|
|
484
500
|
return allowFanoutTools
|
|
485
501
|
? coordinatorTools
|
|
486
|
-
: coordinatorTools.filter((tool) =>
|
|
502
|
+
: coordinatorTools.filter((tool) => NON_SPAWNING_CHILD_COORDINATOR_TOOL_NAMES.has(tool.name));
|
|
487
503
|
}
|
|
@@ -111,15 +111,20 @@ export interface WaitDeliveredTurnResult extends Pick<
|
|
|
111
111
|
messages?: WaitMessageResult[];
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
/** Reports an observational wait timeout with
|
|
114
|
+
/** Reports an observational wait timeout with a compact, bounded child progress snapshot. */
|
|
115
115
|
export interface WaitTimeoutResult {
|
|
116
116
|
event: "timeout";
|
|
117
117
|
agent_id: string;
|
|
118
118
|
turn_id: string;
|
|
119
119
|
/** Requested timeout duration that expired. */
|
|
120
120
|
timeout_ms: number;
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
state: AgentState;
|
|
122
|
+
elapsed_ms?: number;
|
|
123
|
+
latest_activity_at?: string;
|
|
124
|
+
/** Total tokens the child has used, when usage is known. */
|
|
125
|
+
total_tokens?: number;
|
|
126
|
+
/** Labels of the child's last few Recent Activity items, oldest first. */
|
|
127
|
+
recent_activity_labels: string[];
|
|
123
128
|
}
|
|
124
129
|
|
|
125
130
|
/** Reports one message, terminal turn, or timeout returned by subagent_wait. */
|
|
@@ -33,3 +33,33 @@ export function addMinimalSubagentsUsage(
|
|
|
33
33
|
},
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* USD decimal places kept in model-visible and structured usage costs. Six places is one
|
|
39
|
+
* millionth of a dollar: finer than any single turn's cost is worth reading, yet coarse enough to
|
|
40
|
+
* drop the binary-float tails that summing many per-message costs produces.
|
|
41
|
+
*/
|
|
42
|
+
export const MINIMAL_SUBAGENTS_COST_DECIMALS = 6;
|
|
43
|
+
|
|
44
|
+
const COST_SCALE = 10 ** MINIMAL_SUBAGENTS_COST_DECIMALS;
|
|
45
|
+
|
|
46
|
+
function roundCost(value: number): number {
|
|
47
|
+
return Number.isFinite(value) ? Math.round(value * COST_SCALE) / COST_SCALE : value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Return a clone whose cost fields are rounded for presentation. Each field rounds independently
|
|
52
|
+
* from its exact value, so `total` stays within half a unit per summed field of the rounded sum.
|
|
53
|
+
*/
|
|
54
|
+
export function roundMinimalSubagentsUsageCosts(usage: Usage): Usage {
|
|
55
|
+
return {
|
|
56
|
+
...usage,
|
|
57
|
+
cost: {
|
|
58
|
+
input: roundCost(usage.cost.input),
|
|
59
|
+
output: roundCost(usage.cost.output),
|
|
60
|
+
cacheRead: roundCost(usage.cost.cacheRead),
|
|
61
|
+
cacheWrite: roundCost(usage.cost.cacheWrite),
|
|
62
|
+
total: roundCost(usage.cost.total),
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|