@howarewoo/pi-subagents 0.1.1 → 0.2.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.
@@ -0,0 +1,111 @@
1
+ import type {
2
+ AgentSession,
3
+ AgentSessionEvent,
4
+ ExtensionToolContext,
5
+ Skill,
6
+ ToolDefinition,
7
+ } from "@earendil-works/pi-coding-agent";
8
+ import type { CapabilityDenial, ToolMetadata } from "./capabilities.ts";
9
+ import type { StructuredOutcome } from "./outcomes.ts";
10
+ import type { ModelSelection } from "./model-policy.ts";
11
+ import type { ExecutionLimits } from "./scheduling.ts";
12
+ import type { BuiltinTool, Profile } from "./profiles.ts";
13
+
14
+ export type DelegationMessage = {
15
+ id: string;
16
+ sender: ChildRef | "root";
17
+ recipient: ChildRef | "root";
18
+ text: string;
19
+ mode: "inbox" | "steer" | "followUp";
20
+ status: "accepted" | "queued" | "handled" | "delivered" | "not-running" | "error";
21
+ error?: string;
22
+ };
23
+ export type CoordinationWait =
24
+ | { status: "timeout" | "cancelled" | "shutdown" }
25
+ | { status: "messages"; messages: DelegationMessage[] }
26
+ | { status: "message"; message: DelegationMessage }
27
+ | {
28
+ status: "outcome";
29
+ outcome: Pick<ChildSnapshot, "id" | "generation" | "status" | "limitReason" | "pauseReason">;
30
+ };
31
+ export type ChildRef = Readonly<{ id: string; generation: number }>;
32
+ export type ChildSnapshot = Readonly<
33
+ ChildRef & {
34
+ parentId: string;
35
+ parentGeneration?: number;
36
+ depth: number;
37
+ writerLease: boolean;
38
+ limits: ExecutionLimits;
39
+ limitReason?: string;
40
+ toolCalls: number;
41
+ spawned?: number;
42
+ pauseReason?: string;
43
+ retainedContext?: boolean;
44
+ window?: number;
45
+ status:
46
+ | "queued"
47
+ | "setup"
48
+ | "running"
49
+ | "pausing"
50
+ | "paused"
51
+ | "draining"
52
+ | "settled"
53
+ | "failed"
54
+ | "cancelled";
55
+ profile: string;
56
+ tools: readonly BuiltinTool[];
57
+ leafTools?: readonly string[];
58
+ skills?: readonly string[];
59
+ capabilityDenials?: readonly CapabilityDenial[];
60
+ cwd?: string;
61
+ assignment: string;
62
+ activity: string;
63
+ progress: string;
64
+ result: string;
65
+ model: { provider: string; id: string };
66
+ effort: string | null;
67
+ prewalk?: {
68
+ target: ModelSelection;
69
+ status: "armed" | "handed-off" | "suppressed-exact" | "no-op";
70
+ };
71
+ telemetry: {
72
+ requests: number | null;
73
+ tokens: ReturnType<AgentSession["getSessionStats"]>["tokens"] | null;
74
+ cost: number | null;
75
+ context: ReturnType<AgentSession["getContextUsage"]> | null;
76
+ };
77
+ outcome?: StructuredOutcome;
78
+ artifacts?: { text?: string; structured?: string; error?: string };
79
+ error?: string;
80
+ transcript?: string;
81
+ disposed: boolean;
82
+ createdAt: number;
83
+ endedAt: number | null;
84
+ }
85
+ >;
86
+ export type RegistryEvent = Readonly<{
87
+ type: "changed" | "sdk";
88
+ child: ChildSnapshot;
89
+ event?: AgentSessionEvent;
90
+ }>;
91
+ export type StartTask = {
92
+ cwd?: string;
93
+ model?: ModelSelection;
94
+ resultSchema?: unknown;
95
+ assignment: string;
96
+ profile: Profile;
97
+ skills: Skill[];
98
+ context: ExtensionToolContext;
99
+ assertTools: (names: readonly string[]) => void;
100
+ toolMetadata?: () => readonly ToolMetadata[];
101
+ /** Live parent codemode availability; never a grant to forward its dispatcher. */
102
+ codemodeEnabled?: () => boolean;
103
+ availableSkills?: () => readonly Skill[];
104
+ capabilityDenials?: CapabilityDenial[];
105
+ leafProvenance?: Readonly<Record<string, string>>;
106
+ callerId: string;
107
+ parent?: ChildRef;
108
+ limits?: Partial<ExecutionLimits>;
109
+ coordinationTools?: (actor: ChildRef) => ToolDefinition[];
110
+ nestedTool?: (parent: ChildRef) => ToolDefinition;
111
+ };
package/src/hub.ts CHANGED
@@ -3,7 +3,6 @@ import {
3
3
  Input,
4
4
  matchesKey,
5
5
  oklchColor,
6
- sliceByColumn,
7
6
  truncateToWidth,
8
7
  visibleWidth,
9
8
  wrapTextWithAnsi,
@@ -15,229 +14,23 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
15
14
  import type { ChildRef, ChildSnapshot, DelegationCoordinator } from "./coordinator.ts";
16
15
  import { createNativeThread, safe, type LiveTool, type ThreadDependencies } from "./thread.ts";
17
16
  export { safe } from "./thread.ts";
17
+ export { createChildWidget } from "./subagent-widget.ts";
18
+ export {
19
+ agentLabel,
20
+ fleetSummary,
21
+ taskCard,
22
+ transcriptLines,
23
+ writerWarning,
24
+ } from "./task-presentation.ts";
25
+ import {
26
+ agentLabel,
27
+ assignmentHeadline,
28
+ lineText,
29
+ taskDetails,
30
+ transcriptLines,
31
+ } from "./task-presentation.ts";
18
32
 
19
33
  type Palette = Pick<Theme, "fg" | "style"> & { colors: Pick<Theme["colors"], "customMessageBg"> };
20
- const lineText = (text: string) => safe(text).replaceAll("\n", " ");
21
- const assignmentHeadline = (child: ChildSnapshot) =>
22
- safe(child.assignment)
23
- .split("\n")
24
- .map((line) => line.trim())
25
- .find(Boolean)
26
- ?.replace(/^#{1,6}\s+/, "") || "Assignment";
27
- export const agentLabel = (child: ChildSnapshot) =>
28
- `${assignmentHeadline(child)} · ${lineText(child.profile)}`;
29
- const active = (child: ChildSnapshot) => !child.disposed;
30
- const activity = (child: ChildSnapshot) =>
31
- child.error ||
32
- (active(child) ? child.activity || child.progress : child.result) ||
33
- child.progress ||
34
- "Waiting for SDK activity";
35
- export const writerWarning = "Model-tool writer only: no conflicting manual !/!! or external edits";
36
- const statuses = [
37
- "queued",
38
- "setup",
39
- "running",
40
- "draining",
41
- "settled",
42
- "failed",
43
- "cancelled",
44
- ] as const;
45
- export function fleetSummary(children: ChildSnapshot[]): string {
46
- return children.length
47
- ? statuses
48
- .map((status) => `${status} ${children.filter((child) => child.status === status).length}`)
49
- .join(" · ")
50
- : "No tasks · ask the model to delegate_task";
51
- }
52
-
53
- /** Event-driven, bounded current-agent tree; history and full metrics remain in the Hub. */
54
- export function createChildWidget(
55
- snapshot: () => ChildSnapshot[],
56
- theme: Pick<Theme, "fg"> & Partial<Pick<Theme, "bold">>,
57
- availableRows = () => 30,
58
- ): Component {
59
- return {
60
- render(width) {
61
- width = Math.max(0, width);
62
- const children = snapshot();
63
- if (!children.length) return [];
64
- const queued = children.filter((child) => child.status === "queued").length;
65
- const priority = (child: ChildSnapshot) =>
66
- child.status === "draining"
67
- ? 0
68
- : child.status === "running"
69
- ? 1
70
- : child.status === "setup"
71
- ? 2
72
- : 3;
73
- const live = children
74
- .filter(
75
- (child) =>
76
- !child.disposed &&
77
- (child.status === "setup" || child.status === "running" || child.status === "draining"),
78
- )
79
- .sort((a, b) => priority(a) - priority(b));
80
- const writer = children.some((child) => child.writerLease);
81
- const budget = Math.max(1, Math.min(5, Math.floor(availableRows() / 4)));
82
- const counts =
83
- width < 60
84
- ? `${live.length} active · ${queued} queued`
85
- : statuses
86
- .flatMap((status) => {
87
- const count = children.filter((child) => child.status === status).length;
88
- return count ? [`${count} ${status}`] : [];
89
- })
90
- .join(" · ");
91
- const rows = [
92
- theme.fg("accent", `● Subagents ${children.length}`) + theme.fg("muted", ` · ${counts}`),
93
- ];
94
- if (writer && budget === 1)
95
- rows[0] = theme.fg(
96
- "warning",
97
- `● Subagents ${children.length} · Model writer: !/!! + external unsafe · ${queued} queued`,
98
- );
99
- const room = Math.max(0, budget - 1 - (writer && budget > 1 ? 1 : 0));
100
- const activityRows = live.length && room >= (live.length > 1 ? 3 : 2) ? 1 : 0;
101
- const slots = room - activityRows;
102
- const overflow = live.length > slots;
103
- const shown = live.slice(0, Math.max(0, slots - (overflow ? 1 : 0)));
104
- shown.forEach((child, index) => {
105
- const last = index === shown.length - 1 && shown.length === live.length;
106
- const color = child.status === "draining" ? "warning" : "accent";
107
- const role = lineText(child.profile);
108
- const indent = " ".repeat(Math.min(3, Math.max(0, child.depth - 1)));
109
- const prefix =
110
- theme.fg("muted", `${indent}${last ? "└─" : "├─"} `) +
111
- theme.fg(color, "● ") +
112
- (theme.bold?.(role) ?? role) +
113
- " ";
114
- const elapsed = Math.max(
115
- 0,
116
- Math.floor(((child.endedAt ?? Date.now()) - child.createdAt) / 1000),
117
- );
118
- const stats = [
119
- child.status,
120
- `${elapsed}s`,
121
- `${child.toolCalls} tools`,
122
- ...(child.telemetry.requests === null ? [] : [`${child.telemetry.requests} req`]),
123
- ...(child.telemetry.tokens ? [`${child.telemetry.tokens.total} tok`] : []),
124
- ...(child.telemetry.cost === null ? [] : [`$${child.telemetry.cost.toFixed(3)}`]),
125
- ].join(" · ");
126
- const metric = truncateToWidth(stats, Math.max(8, Math.floor(width / 2)));
127
- const headline = assignmentHeadline(child);
128
- rows.push(
129
- prefix +
130
- theme.fg(
131
- "text",
132
- truncateToWidth(
133
- headline,
134
- Math.max(4, width - visibleWidth(prefix) - visibleWidth(metric) - 3),
135
- ),
136
- ) +
137
- theme.fg("muted", " · " + metric),
138
- );
139
- if (!index && activityRows) {
140
- const prefix = `${indent}${last ? " " : "│ "} └ `;
141
- const text = lineText(activity(child));
142
- const columns = Math.max(0, width - visibleWidth(prefix));
143
- const length = visibleWidth(text);
144
- // Progress is an accumulated tail; keep its newest text, not a frozen opening paragraph.
145
- const preview =
146
- length > columns
147
- ? "…" +
148
- sliceByColumn(text, length - Math.max(0, columns - 1), Math.max(0, columns - 1))
149
- : text;
150
- rows.push(theme.fg("muted", prefix + preview));
151
- }
152
- });
153
- if (shown.length < live.length && room)
154
- rows.push(theme.fg("muted", `└─ ${live.length - shown.length} more · /subagent-hub`));
155
- if (writer && budget > 1)
156
- rows.push(
157
- theme.fg("warning", width < 75 ? "Model writer: !/!! + external unsafe" : writerWarning),
158
- );
159
- return rows.slice(0, budget).map((line) => truncateToWidth(line, width));
160
- },
161
- invalidate() {},
162
- };
163
- }
164
-
165
- export function taskCard(child: ChildSnapshot | undefined, expanded = false): string[] {
166
- if (!child)
167
- return [
168
- "Task identity unavailable",
169
- "See tool error; /subagent-hub lists current runtime tasks",
170
- ];
171
- return [
172
- `${truncateToWidth(agentLabel(child), 240, "… [preview truncated]")} #${child.generation} · ${child.status}`,
173
- `Identity ${child.id} #${child.generation}`,
174
- `${child.model.provider}/${child.model.id} · effort ${child.effort ?? "unavailable"}`,
175
- truncateToWidth(lineText(activity(child)), 240, "… [preview truncated]"),
176
- `Prewalk ${child.prewalk ? JSON.stringify(child.prewalk) : "off"} · advisor unavailable`,
177
- `Parent ${child.parentId}${child.parentGeneration ? ` #${child.parentGeneration}` : ""} · depth ${child.depth} · writer lease ${child.writerLease}`,
178
- ...(child.outcome
179
- ? [
180
- `Structured ${child.outcome.validation} · ${child.outcome.finalized ? "finalized" : "provisional"} · attempts ${child.outcome.attempts}`,
181
- ]
182
- : []),
183
- ...(child.limitReason ? [`Limit: ${child.limitReason}`] : []),
184
- ...(child.artifacts ? [`Artifacts: ${JSON.stringify(child.artifacts)}`] : []),
185
- ...(expanded
186
- ? [
187
- "Assignment: " +
188
- truncateToWidth(lineText(child.assignment), 480, "… [preview truncated]"),
189
- "Result: " +
190
- truncateToWidth(
191
- lineText(child.result || child.error || "Not available yet"),
192
- 480,
193
- "… [preview truncated]",
194
- ),
195
- ]
196
- : []),
197
- child.transcript
198
- ? `Full transcript: ${child.transcript} · /subagent-hub`
199
- : "Transcript unavailable (setup) · /subagent-hub",
200
- ].map(lineText);
201
- }
202
-
203
- /** Read-only content projection: never dumps provider/auth objects, image payloads or opaque signatures. */
204
- export function transcriptLines(
205
- messages: ReturnType<DelegationCoordinator["messages"]>,
206
- toolsOnly = false,
207
- ): string[] {
208
- const lines: string[] = [];
209
- for (const message of messages) {
210
- if (message.role === "system") continue;
211
- const start = lines.length;
212
- const blocks = "content" in message ? message.content : undefined;
213
- if (!toolsOnly || message.role === "toolResult") {
214
- lines.push(
215
- `[${message.role}${message.role === "toolResult" ? ` ${message.toolName} ${message.toolCallId} ${message.isError ? "ERROR" : "result"}` : ""}]`,
216
- );
217
- if (typeof blocks === "string") lines.push(safe(blocks));
218
- }
219
- if (Array.isArray(blocks))
220
- for (const block of blocks) {
221
- if (block.type === "toolCall")
222
- lines.push(
223
- `Tool ${block.name} ${block.id}\nArguments: ${safe(JSON.stringify(block.arguments, null, 2))}`,
224
- );
225
- else if (!toolsOnly || message.role === "toolResult") {
226
- if (block.type === "text") lines.push(safe(block.text));
227
- else if (block.type === "thinking") lines.push("Thinking: " + safe(block.thinking));
228
- else if (block.type === "image")
229
- lines.push(`[image ${safe(block.mimeType)}; payload omitted]`);
230
- }
231
- }
232
- if (message.role === "assistant" && message.errorMessage)
233
- lines.push("Error: " + safe(message.errorMessage));
234
- if (start && lines.length > start) lines.splice(start, 0, "");
235
- }
236
- return lines.length
237
- ? lines.map(safe)
238
- : [toolsOnly ? "No tool calls yet" : "Waiting for SDK messages"];
239
- }
240
-
241
34
  /** Observation only: closing or disposing never touches child execution. */
242
35
  export function createHub(
243
36
  coordinator: DelegationCoordinator,
@@ -337,6 +130,7 @@ export function createHub(
337
130
  !child ||
338
131
  child.disposed ||
339
132
  child.status === "draining" ||
133
+ child.status === "pausing" ||
340
134
  (text !== undefined && child.status !== "running")
341
135
  )
342
136
  throw new Error(
@@ -452,8 +246,6 @@ export function createHub(
452
246
  footer.push(
453
247
  theme.fg("muted", `Filter: ${lineText(query)} · ${children.length} generations`),
454
248
  );
455
- if (current.some((item) => item.writerLease))
456
- footer.push(theme.fg("warning", "WRITER: model tools only; no manual/external writes"));
457
249
  if (notice) {
458
250
  const lines = wrapTextWithAnsi(lineText(notice), Math.max(1, inner));
459
251
  footer.push(
@@ -517,40 +309,13 @@ export function createHub(
517
309
  let nativeLines: string[] | undefined;
518
310
  if (child) {
519
311
  if (tab === "details") {
520
- const metric = (value: unknown) =>
521
- value === null || value === undefined ? "unavailable" : JSON.stringify(value);
522
- detail = [
523
- `Identity ${child.id} #${child.generation}`,
524
- "",
525
- "Assignment",
526
- safe(child.assignment),
527
- "",
528
- "Result",
529
- safe(child.result || "Not available yet"),
530
- "",
531
- ...(child.error ? ["Error", safe(child.error), ""] : []),
532
- ...(child.outcome
533
- ? ["Structured outcome", JSON.stringify(child.outcome, null, 2), ""]
534
- : []),
535
- ...(child.artifacts
536
- ? ["Saved artifacts", JSON.stringify(child.artifacts, null, 2), ""]
537
- : []),
538
- `Workspace: ${safe(child.cwd ?? "parent workspace")}`,
539
- `Effective builtin tools: ${child.tools.join(", ")}`,
540
- `${child.model.provider}/${child.model.id} · effort ${child.effort ?? "unavailable"}`,
541
- `Prewalk ${child.prewalk ? JSON.stringify(child.prewalk) : "off"} · advisor unavailable`,
542
- `Presence ${coordinator.agents().find((agent) => agent.id === child.id && agent.generation === child.generation)?.presence ?? "historical"} · resources ${child.disposed ? "disposed" : "live"}`,
543
- `Elapsed ${Math.max(0, Math.floor(((child.endedAt ?? Date.now()) - child.createdAt) / 1000))}s`,
544
- `Direct requests ${metric(child.telemetry.requests)} · cost ${metric(child.telemetry.cost)}`,
545
- `Direct tokens ${metric(child.telemetry.tokens)}`,
546
- `Context ${metric(child.telemetry.context)}`,
547
- `Parent ${child.parentId} #${child.parentGeneration ?? "root"} · depth ${child.depth} · direct generation only`,
548
- `Limits ${JSON.stringify(child.limits)} · calls ${child.toolCalls} · reason ${child.limitReason ?? "none"}`,
549
- "Activity: " + safe(activity(child)),
550
- "Full transcript: " + safe(child.transcript ?? "unavailable (queued/setup)"),
551
- "Followup/revive: native delegate_coordinate only (Hub has no tool dispatcher)",
552
- ...(child.writerLease ? [writerWarning] : []),
553
- ];
312
+ detail = taskDetails(
313
+ child,
314
+ coordinator
315
+ .agents()
316
+ .find((agent) => agent.id === child.id && agent.generation === child.generation)
317
+ ?.presence,
318
+ );
554
319
  } else {
555
320
  try {
556
321
  const messages = coordinator.messages(child);
@@ -626,7 +391,7 @@ export function createHub(
626
391
  ? "success"
627
392
  : "accent";
628
393
  const selectedLabel = child
629
- ? `${agentLabel(child)} #${child.generation} · ${child.status}${historical(child) ? " · history" : ""}`
394
+ ? `${agentLabel(child)} · ${child.status}${historical(child) ? " · history" : ""}`
630
395
  : "No selection";
631
396
  const viewLabel = `${tab.toUpperCase()} · Tab: ${tab === "thread" ? "details" : tab === "details" ? "tools" : "thread"}`;
632
397
  const position = `${tab === "thread" ? (following ? "LIVE · " : "READING · ") : ""}${offset + 1}–${Math.min(wrapped.length, offset + viewport)}/${wrapped.length} lines${tab === "thread" ? (following ? " · k/PgUp read" : " · End: live") : offset < maxOffset ? " · more ↓" : ""}`;
@@ -636,16 +401,21 @@ export function createHub(
636
401
  const start = Math.max(0, index - slots + 1);
637
402
  const left = [
638
403
  theme.fg("accent", `SUBAGENTS · ${children.length} generations`),
639
- ...children
640
- .slice(start, start + slots)
641
- .flatMap((item) => [
404
+ ...children.slice(start, start + slots).flatMap((item) => {
405
+ const indent = " ".repeat(Math.min(4, Math.max(0, item.depth - 1)));
406
+ const prefix = `${identity(item) === selected ? ">" : " "} ${indent}└ `;
407
+ return [
642
408
  theme.fg(
643
409
  identity(item) === selected ? "accent" : "text",
644
- `${identity(item) === selected ? ">" : " "} ${" ".repeat(Math.min(4, Math.max(0, item.depth - 1)))}└ ${agentLabel(item)} #${item.generation}${historical(item) ? " · history" : ""}`,
410
+ `${prefix}${assignmentHeadline(item)}${historical(item) ? " · history" : ""}`,
411
+ ),
412
+ theme.fg(
413
+ statusColor(item),
414
+ `${" ".repeat(visibleWidth(prefix))}${lineText(item.profile)} · ${item.status}`,
645
415
  ),
646
- theme.fg(statusColor(item), ` ${item.profile} · ${item.status}`),
647
- theme.fg("muted", ` #${item.generation}${historical(item) ? " · history" : ""}`),
648
- ]),
416
+ "", // Separate agents without exposing internal generation numbers.
417
+ ];
418
+ }),
649
419
  ];
650
420
  const right = [
651
421
  theme.fg("accent", lineText(selectedLabel)),
@@ -674,7 +444,7 @@ export function createHub(
674
444
  ),
675
445
  );
676
446
  } else {
677
- const suffix = child ? ` #${child.generation} · ${index + 1}/${children.length}` : "";
447
+ const suffix = child ? ` · ${index + 1}/${children.length}` : "";
678
448
  pane.push(
679
449
  theme.fg(
680
450
  "accent",
@@ -798,6 +568,7 @@ export function createHub(
798
568
  child &&
799
569
  !child.disposed &&
800
570
  child.status !== "draining" &&
571
+ child.status !== "pausing" &&
801
572
  (data === "x" || child.status === "running")
802
573
  ) {
803
574
  mode = data === "s" ? "steer" : data === "m" ? "message" : "stop";