@ian-pascoe/pi-minimal-subagents 0.14.1 → 0.15.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
|
@@ -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
|
|
|
@@ -279,10 +281,11 @@ children: `subagent`, `agent_message`, `subagent_wait`, `subagent_status`,
|
|
|
279
281
|
only the three adjacent-coordination tools: `agent_message`, `subagent_wait`,
|
|
280
282
|
and `subagent_status`.
|
|
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
291
|
and `reasoning`, as `0` when the provider reports none. `subagent_cancel`
|
|
@@ -351,8 +354,11 @@ not claim the turn, and `turn_id` rereads the full result. A
|
|
|
351
354
|
caller may have only one outstanding wait for the same source turn; a
|
|
352
355
|
concurrent duplicate is rejected instead of competing for one Wait Event.
|
|
353
356
|
When `timeout_ms` expires, the wait returns an observational `event: "timeout"`
|
|
354
|
-
with the requested turn identity and
|
|
355
|
-
|
|
357
|
+
with the requested turn identity and a compact, bounded progress snapshot
|
|
358
|
+
(`state`, `elapsed_ms`, `latest_activity_at`, `total_tokens`, and the labels of
|
|
359
|
+
the last three Recent Activity items as `recent_activity_labels`), under about 1 KB. It omits the task and
|
|
360
|
+
Launch Contract; targeted `subagent_status` returns the full detail.
|
|
361
|
+
It removes only the waiter, leaving the child
|
|
356
362
|
running and all pending delivery unclaimed. Abort signals remain errors.
|
|
357
363
|
|
|
358
364
|
The persisted Delivery Ledger records Coordination Messages, terminal results,
|
package/package.json
CHANGED
|
@@ -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({
|
|
@@ -313,7 +319,11 @@ export const CoordinatorToolOutputSchemas = {
|
|
|
313
319
|
agent_id: Type.String(),
|
|
314
320
|
turn_id: Type.String(),
|
|
315
321
|
timeout_ms: Type.Number(),
|
|
316
|
-
|
|
322
|
+
state: Type.Union([Type.Literal("running"), Type.Literal("idle")]),
|
|
323
|
+
elapsed_ms: Type.Optional(Type.Number()),
|
|
324
|
+
latest_activity_at: Type.Optional(Type.String()),
|
|
325
|
+
total_tokens: Type.Optional(Type.Number()),
|
|
326
|
+
recent_activity_labels: Type.Array(Type.String()),
|
|
317
327
|
...CoordinatorWaitSourceOutputSchema,
|
|
318
328
|
}),
|
|
319
329
|
]),
|
|
@@ -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) {
|
|
@@ -330,7 +330,7 @@ export function createCoordinatorToolDefinitions(
|
|
|
330
330
|
name: "subagent_wait",
|
|
331
331
|
label: "Subagent Wait",
|
|
332
332
|
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
|
|
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 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
334
|
promptSnippet: "Wait for one direct child's exact turn",
|
|
335
335
|
parameters: options.schemas.subagent_wait,
|
|
336
336
|
annotations: {
|
|
@@ -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. */
|