@ian-pascoe/pi-minimal-subagents 0.15.0 → 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 CHANGED
@@ -276,10 +276,10 @@ creation; reloading does not silently broaden them. The default maximum depth
276
276
  is two levels beneath the interactive Root Agent.
277
277
 
278
278
  The extension registers six coordinator tools for the Root Agent and fanout
279
- children: `subagent`, `agent_message`, `subagent_wait`, `subagent_status`,
280
- `subagent_cancel`, and `subagent_delete`. Ordinary non-fanout children receive
281
- only the three adjacent-coordination tools: `agent_message`, `subagent_wait`,
282
- and `subagent_status`.
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`.
283
283
 
284
284
  A running child's `latest_activity_at` advances with the timestamps of its latest
285
285
  messages and tool results. Targeted `subagent_status` includes `recent_activity`, a bounded tail of message
@@ -288,7 +288,9 @@ assistant message but omits image data. Timeout Wait Events carry only a compact
288
288
  progress snapshot. Children target only direct children; the Root Agent
289
289
  may inspect any descendant. Model-facing status reports a `child_count` but no
290
290
  nested `children` summaries. Reported `usage` always includes `cacheWrite1h`
291
- and `reasoning`, as `0` when the provider reports none. `subagent_cancel`
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`
292
294
  lists in `affected_agent_ids` only agents whose active turns it cancelled.
293
295
 
294
296
  `session_context` defaults to `omit`: the child starts with only its system
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian-pascoe/pi-minimal-subagents",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "private": false,
5
5
  "description": "Persistent nested subagents with bounded delegation for Pi",
6
6
  "keywords": [
@@ -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, subagent_wait, and subagent_status; wait and status target direct children only.";
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.",
@@ -272,7 +272,10 @@ const CoordinatorWaitSourceOutputSchema = {
272
272
  source_turn_id: Type.String(),
273
273
  };
274
274
 
275
- /** Final details schemas exposed to CodeMode for the six coordinator tools. */
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
+ */
276
279
  export const CoordinatorToolOutputSchemas = {
277
280
  subagent: Type.Object({
278
281
  agent_id: Type.String(),
@@ -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
- const ORDINARY_CHILD_COORDINATOR_TOOL_NAMES = new Set([
46
- "agent_message",
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
- const json = JSON.stringify(result, null, 2);
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,
@@ -380,7 +396,7 @@ export function createCoordinatorToolDefinitions(
380
396
  ? alreadyDeliveredContent(result)
381
397
  : structuredToolResult(result).content,
382
398
  details,
383
- structuredContent: JSON.parse(JSON.stringify(details)),
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) => ORDINARY_CHILD_COORDINATOR_TOOL_NAMES.has(tool.name));
502
+ : coordinatorTools.filter((tool) => NON_SPAWNING_CHILD_COORDINATOR_TOOL_NAMES.has(tool.name));
487
503
  }
@@ -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
+ }