@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,115 @@
1
+ import {
2
+ sliceByColumn,
3
+ truncateToWidth,
4
+ visibleWidth,
5
+ type Component,
6
+ } from "@earendil-works/pi-tui";
7
+ import type { Theme } from "@earendil-works/pi-coding-agent";
8
+ import type { ChildSnapshot } from "./coordinator.ts";
9
+ import { active, activity, assignmentHeadline, lineText, statuses } from "./task-presentation.ts";
10
+
11
+ /** Event-driven, bounded current-agent tree; history and full metrics remain in the Hub. */
12
+ export function createChildWidget(
13
+ snapshot: () => ChildSnapshot[],
14
+ theme: Pick<Theme, "fg"> & Partial<Pick<Theme, "bold">>,
15
+ availableRows = () => 30,
16
+ ): Component {
17
+ return {
18
+ render(width) {
19
+ width = Math.max(0, width);
20
+ const children = snapshot();
21
+ if (!children.length) return [];
22
+ const queued = children.filter((child) => child.status === "queued").length;
23
+ const priority = (child: ChildSnapshot) =>
24
+ child.status === "draining" || child.status === "pausing"
25
+ ? 0
26
+ : child.status === "running"
27
+ ? 1
28
+ : child.status === "setup"
29
+ ? 2
30
+ : 3;
31
+ const live = children
32
+ .filter(
33
+ (child) =>
34
+ !child.disposed &&
35
+ (child.status === "setup" ||
36
+ child.status === "running" ||
37
+ child.status === "draining" ||
38
+ child.status === "pausing"),
39
+ )
40
+ .sort((a, b) => priority(a) - priority(b));
41
+ const budget = Math.max(1, Math.min(5, Math.floor(availableRows() / 4)));
42
+ const counts =
43
+ width < 60
44
+ ? `${live.filter(active).length} active · ${children.filter((child) => child.status === "paused").length} paused · ${queued} queued`
45
+ : statuses
46
+ .flatMap((status) => {
47
+ const count = children.filter((child) => child.status === status).length;
48
+ return count ? [`${count} ${status}`] : [];
49
+ })
50
+ .join(" · ");
51
+ const rows = [
52
+ theme.fg("accent", `● Subagents ${children.length}`) + theme.fg("muted", ` · ${counts}`),
53
+ ];
54
+ const room = Math.max(0, budget - 1);
55
+ const activityRows = live.length && room >= (live.length > 1 ? 3 : 2) ? 1 : 0;
56
+ const slots = room - activityRows;
57
+ const overflow = live.length > slots;
58
+ const shown = live.slice(0, Math.max(0, slots - (overflow ? 1 : 0)));
59
+ shown.forEach((child, index) => {
60
+ const last = index === shown.length - 1 && shown.length === live.length;
61
+ const color =
62
+ child.status === "draining" || child.status === "pausing" ? "warning" : "accent";
63
+ const role = lineText(child.profile);
64
+ const indent = " ".repeat(Math.min(3, Math.max(0, child.depth - 1)));
65
+ const prefix =
66
+ theme.fg("muted", `${indent}${last ? "└─" : "├─"} `) +
67
+ theme.fg(color, "● ") +
68
+ (theme.bold?.(role) ?? role) +
69
+ " ";
70
+ const elapsed = Math.max(
71
+ 0,
72
+ Math.floor(((child.endedAt ?? Date.now()) - child.createdAt) / 1000),
73
+ );
74
+ const stats = [
75
+ child.status,
76
+ `${elapsed}s`,
77
+ `${child.toolCalls} tools`,
78
+ ...(child.telemetry.requests === null ? [] : [`${child.telemetry.requests} req`]),
79
+ ...(child.telemetry.tokens ? [`${child.telemetry.tokens.total} tok`] : []),
80
+ ...(child.telemetry.cost === null ? [] : [`$${child.telemetry.cost.toFixed(3)}`]),
81
+ ].join(" · ");
82
+ const metric = truncateToWidth(stats, Math.max(8, Math.floor(width / 2)));
83
+ const headline = assignmentHeadline(child);
84
+ rows.push(
85
+ prefix +
86
+ theme.fg(
87
+ "text",
88
+ truncateToWidth(
89
+ headline,
90
+ Math.max(4, width - visibleWidth(prefix) - visibleWidth(metric) - 3),
91
+ ),
92
+ ) +
93
+ theme.fg("muted", " · " + metric),
94
+ );
95
+ if (!index && activityRows) {
96
+ const prefix = `${indent}${last ? " " : "│ "} └ `;
97
+ const text = lineText(activity(child));
98
+ const columns = Math.max(0, width - visibleWidth(prefix));
99
+ const length = visibleWidth(text);
100
+ // Progress is an accumulated tail; keep its newest text, not a frozen opening paragraph.
101
+ const preview =
102
+ length > columns
103
+ ? "…" +
104
+ sliceByColumn(text, length - Math.max(0, columns - 1), Math.max(0, columns - 1))
105
+ : text;
106
+ rows.push(theme.fg("muted", prefix + preview));
107
+ }
108
+ });
109
+ if (shown.length < live.length && room)
110
+ rows.push(theme.fg("muted", `└─ ${live.length - shown.length} more · /subagent-hub`));
111
+ return rows.slice(0, budget).map((line) => truncateToWidth(line, width));
112
+ },
113
+ invalidate() {},
114
+ };
115
+ }
@@ -0,0 +1,26 @@
1
+ import { VERSION } from "@earendil-works/pi-coding-agent";
2
+
3
+ const MINIMUM_PI_VERSION = "1.5.2";
4
+ const minimum = MINIMUM_PI_VERSION.split(".").map(BigInt);
5
+
6
+ /** Gate execution, not registration: root diagnostics and cleanup must remain usable. */
7
+ export function requireSupportedHost(): void {
8
+ // Only stable SemVer, with optional build metadata (which has no precedence).
9
+ const stable =
10
+ typeof VERSION === "string"
11
+ ? /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(
12
+ VERSION,
13
+ )
14
+ : null;
15
+ if (stable && stable[0] === VERSION) {
16
+ // BigInt keeps numeric comparison exact without a version-component size limit.
17
+ const difference = stable
18
+ .slice(1, 4)
19
+ .map((part, index) => BigInt(part) - minimum[index]!)
20
+ .find((part) => part !== 0n);
21
+ if (difference === undefined || difference > 0n) return;
22
+ }
23
+ throw new Error(
24
+ `Delegated execution requires supported stable Pi >=${MINIMUM_PI_VERSION}; detected ${VERSION || "unknown"}. Install stable Pi ${MINIMUM_PI_VERSION} or newer and restart Pi after draining active tasks.`,
25
+ );
26
+ }
package/src/task-input.ts CHANGED
@@ -11,7 +11,7 @@ export async function readTaskInput(
11
11
  path: string,
12
12
  ctx: ExtensionToolContext,
13
13
  signal?: AbortSignal,
14
- beforeRead?: () => void,
14
+ beforeRead?: () => void | Promise<void>,
15
15
  cwd = ctx.cwd,
16
16
  ): Promise<string> {
17
17
  if (!path || isAbsolute(path) || path.includes("\0") || hasDeniedTaskComponent(path))
@@ -32,7 +32,8 @@ export async function readTaskInput(
32
32
  if (!before.isFile() || before.size === 0 || before.size > 50000)
33
33
  throw new Error("taskFile must be a nonempty regular text file of at most 50000 bytes");
34
34
  signal?.throwIfAborted();
35
- beforeRead?.();
35
+ await beforeRead?.();
36
+ signal?.throwIfAborted();
36
37
  const outcome = await ctx.executeTool("read", { path: target }, { signal });
37
38
  if (outcome.isError)
38
39
  throw new Error(
@@ -0,0 +1,165 @@
1
+ import { truncateToWidth } from "@earendil-works/pi-tui";
2
+ import type { ChildSnapshot, DelegationCoordinator } from "./coordinator.ts";
3
+ import { safe } from "./thread.ts";
4
+
5
+ export const lineText = (text: string) => safe(text).replaceAll("\n", " ");
6
+ export const assignmentHeadline = (child: ChildSnapshot) =>
7
+ safe(child.assignment)
8
+ .split("\n")
9
+ .map((line) => line.trim())
10
+ .find(Boolean)
11
+ ?.replace(/^#{1,6}\s+/, "") || "Assignment";
12
+ export const agentLabel = (child: ChildSnapshot) =>
13
+ `${assignmentHeadline(child)} · ${lineText(child.profile)}`;
14
+ export const active = (child: ChildSnapshot) => !child.disposed && child.status !== "paused";
15
+ export const activity = (child: ChildSnapshot) =>
16
+ (child.status === "paused"
17
+ ? `Paused: ${child.pauseReason ?? "deadline"} · context ${child.retainedContext ? "retained" : "not yet available"} · resume via model or stop in Hub`
18
+ : child.error) ||
19
+ (active(child) ? child.activity || child.progress : child.result) ||
20
+ child.progress ||
21
+ "Waiting for SDK activity";
22
+ export const writerWarning = "Model-tool writer only: no conflicting manual !/!! or external edits";
23
+ export const statuses = [
24
+ "queued",
25
+ "setup",
26
+ "running",
27
+ "draining",
28
+ "pausing",
29
+ "paused",
30
+ "settled",
31
+ "failed",
32
+ "cancelled",
33
+ ] as const;
34
+ export function fleetSummary(children: ChildSnapshot[]): string {
35
+ return children.length
36
+ ? statuses
37
+ .map((status) => `${status} ${children.filter((child) => child.status === status).length}`)
38
+ .join(" · ")
39
+ : "No tasks · ask the model to delegate_task";
40
+ }
41
+
42
+ export function taskCard(child: ChildSnapshot | undefined, expanded = false): string[] {
43
+ if (!child)
44
+ return [
45
+ "Task identity unavailable",
46
+ "See tool error; /subagent-hub lists current runtime tasks",
47
+ ];
48
+ const headline = `${truncateToWidth(assignmentHeadline(child), 240, "… [preview truncated]")} · ${lineText(child.profile)} #${child.generation} · ${child.status}`;
49
+ if (!expanded)
50
+ return [
51
+ headline,
52
+ ...(child.status === "paused"
53
+ ? [
54
+ `Pause: ${truncateToWidth(lineText(child.pauseReason ?? "deadline"), 240, "… [preview truncated]")}`,
55
+ ]
56
+ : child.error
57
+ ? [`Error: ${truncateToWidth(lineText(child.error), 240, "… [preview truncated]")}`]
58
+ : []),
59
+ ].map(lineText);
60
+ return [
61
+ headline,
62
+ `Identity ${child.id} #${child.generation}`,
63
+ `${child.model.provider}/${child.model.id} · effort ${child.effort ?? "unavailable"}`,
64
+ truncateToWidth(lineText(activity(child)), 240, "… [preview truncated]"),
65
+ `Prewalk ${child.prewalk ? JSON.stringify(child.prewalk) : "off"} · advisor unavailable`,
66
+ `Parent ${child.parentId}${child.parentGeneration ? ` #${child.parentGeneration}` : ""} · depth ${child.depth} · writer lease ${child.writerLease}`,
67
+ ...(child.outcome
68
+ ? [
69
+ `Structured ${child.outcome.validation} · ${child.outcome.finalized ? "finalized" : "provisional"} · attempts ${child.outcome.attempts}`,
70
+ ]
71
+ : []),
72
+ ...(child.limitReason ? [`Limit: ${child.limitReason}`] : []),
73
+ ...(child.pauseReason
74
+ ? [
75
+ `Pause: ${child.pauseReason} · context ${child.retainedContext ? "retained" : "not yet available"} · resume via model control after drain`,
76
+ ]
77
+ : []),
78
+ `Window ${child.window ?? 1} · cumulative requests ${child.telemetry.requests ?? 0}/${child.limits.requests} · tools ${child.toolCalls}/${child.limits.tools} · spawns ${child.spawned ?? 0}/${child.limits.spawns}`,
79
+ ...(child.artifacts ? [`Artifacts: ${JSON.stringify(child.artifacts)}`] : []),
80
+ "Assignment: " + truncateToWidth(lineText(child.assignment), 480, "… [preview truncated]"),
81
+ "Result: " +
82
+ truncateToWidth(
83
+ lineText(child.result || child.error || "Not available yet"),
84
+ 480,
85
+ "… [preview truncated]",
86
+ ),
87
+ child.transcript
88
+ ? `Full transcript: ${child.transcript} · /subagent-hub`
89
+ : "Transcript unavailable (setup) · /subagent-hub",
90
+ ].map(lineText);
91
+ }
92
+
93
+ /** Read-only content projection: never dumps provider/auth objects, image payloads or opaque signatures. */
94
+ export function transcriptLines(
95
+ messages: ReturnType<DelegationCoordinator["messages"]>,
96
+ toolsOnly = false,
97
+ ): string[] {
98
+ const lines: string[] = [];
99
+ for (const message of messages) {
100
+ if (message.role === "system") continue;
101
+ const start = lines.length;
102
+ const blocks = "content" in message ? message.content : undefined;
103
+ if (!toolsOnly || message.role === "toolResult") {
104
+ lines.push(
105
+ `[${message.role}${message.role === "toolResult" ? ` ${message.toolName} ${message.toolCallId} ${message.isError ? "ERROR" : "result"}` : ""}]`,
106
+ );
107
+ if (typeof blocks === "string") lines.push(safe(blocks));
108
+ }
109
+ if (Array.isArray(blocks))
110
+ for (const block of blocks) {
111
+ if (block.type === "toolCall")
112
+ lines.push(
113
+ `Tool ${block.name} ${block.id}\nArguments: ${safe(JSON.stringify(block.arguments, null, 2))}`,
114
+ );
115
+ else if (!toolsOnly || message.role === "toolResult") {
116
+ if (block.type === "text") lines.push(safe(block.text));
117
+ else if (block.type === "thinking") lines.push("Thinking: " + safe(block.thinking));
118
+ else if (block.type === "image")
119
+ lines.push(`[image ${safe(block.mimeType)}; payload omitted]`);
120
+ }
121
+ }
122
+ if (message.role === "assistant" && message.errorMessage)
123
+ lines.push("Error: " + safe(message.errorMessage));
124
+ if (start && lines.length > start) lines.splice(start, 0, "");
125
+ }
126
+ return lines.length
127
+ ? lines.map(safe)
128
+ : [toolsOnly ? "No tool calls yet" : "Waiting for SDK messages"];
129
+ }
130
+
131
+ export function taskDetails(
132
+ child: ChildSnapshot,
133
+ presence: ReturnType<DelegationCoordinator["agents"]>[number]["presence"],
134
+ ): string[] {
135
+ const metric = (value: unknown) =>
136
+ value === null || value === undefined ? "unavailable" : JSON.stringify(value);
137
+ return [
138
+ `Identity ${child.id} #${child.generation}`,
139
+ "",
140
+ "Assignment",
141
+ safe(child.assignment),
142
+ "",
143
+ "Result",
144
+ safe(child.result || "Not available yet"),
145
+ "",
146
+ ...(child.error ? ["Error", safe(child.error), ""] : []),
147
+ ...(child.outcome ? ["Structured outcome", JSON.stringify(child.outcome, null, 2), ""] : []),
148
+ ...(child.artifacts ? ["Saved artifacts", JSON.stringify(child.artifacts, null, 2), ""] : []),
149
+ `Workspace: ${safe(child.cwd ?? "parent workspace")}`,
150
+ `Effective builtin tools: ${child.tools.join(", ")}`,
151
+ `${child.model.provider}/${child.model.id} · effort ${child.effort ?? "unavailable"}`,
152
+ `Prewalk ${child.prewalk ? JSON.stringify(child.prewalk) : "off"} · advisor unavailable`,
153
+ `Presence ${presence ?? "historical"} · resources ${child.disposed ? "disposed" : child.status === "paused" ? "idle SDK; context retained; model resume required" : "live"}`,
154
+ `Elapsed ${Math.max(0, Math.floor(((child.endedAt ?? Date.now()) - child.createdAt) / 1000))}s`,
155
+ `Direct requests ${metric(child.telemetry.requests)} · cost ${metric(child.telemetry.cost)}`,
156
+ `Direct tokens ${metric(child.telemetry.tokens)}`,
157
+ `Context ${metric(child.telemetry.context)}`,
158
+ `Parent ${child.parentId} #${child.parentGeneration ?? "root"} · depth ${child.depth} · direct generation only`,
159
+ `Limits ${JSON.stringify(child.limits)} · calls ${child.toolCalls} · reason ${child.limitReason ?? "none"}`,
160
+ "Activity: " + safe(activity(child)),
161
+ "Full transcript: " + safe(child.transcript ?? "unavailable (queued/setup)"),
162
+ "Followup/revive: native delegate_coordinate only (Hub has no tool dispatcher)",
163
+ ...(child.writerLease ? [writerWarning] : []),
164
+ ];
165
+ }
@@ -0,0 +1,165 @@
1
+ import { Type } from "typebox";
2
+ import { EFFORTS } from "./model-policy.ts";
3
+ import { COORDINATION_ACTIONS } from "./profiles.ts";
4
+
5
+ const role = Type.Union([
6
+ Type.Literal("general"),
7
+ Type.Literal("scout"),
8
+ Type.Literal("review"),
9
+ Type.Literal("implementation"),
10
+ ]);
11
+ const limits = Type.Object(
12
+ {
13
+ depth: Type.Optional(Type.Integer({ minimum: 0 })),
14
+ spawns: Type.Optional(Type.Integer({ minimum: 0 })),
15
+ deadlineMs: Type.Optional(Type.Integer({ minimum: 1 })),
16
+ requests: Type.Optional(Type.Integer({ minimum: 1 })),
17
+ tools: Type.Optional(Type.Integer({ minimum: 1 })),
18
+ },
19
+ { additionalProperties: false },
20
+ );
21
+ const modelSelection = Type.Object(
22
+ {
23
+ provider: Type.String({ minLength: 1 }),
24
+ id: Type.String({ minLength: 1 }),
25
+ effort: Type.Enum(EFFORTS),
26
+ },
27
+ { additionalProperties: false },
28
+ );
29
+ const assignment = {
30
+ cwd: Type.Optional(
31
+ Type.String({
32
+ minLength: 1,
33
+ description:
34
+ "Existing workspace; absolute or relative to root parent cwd. No new permissions or sandbox.",
35
+ }),
36
+ ),
37
+ models: Type.Optional(Type.Array(modelSelection, { maxItems: 64 })),
38
+ model: Type.Optional(modelSelection),
39
+ assignment: Type.Optional(Type.String({ minLength: 1 })),
40
+ taskFile: Type.Optional(Type.String({ minLength: 1 })),
41
+ resultSchema: Type.Optional(Type.Unknown()),
42
+ profile: role,
43
+ spawn: Type.Optional(Type.Array(role)),
44
+ coordination: Type.Optional(
45
+ Type.Array(Type.Union(COORDINATION_ACTIONS.map((action) => Type.Literal(action)))),
46
+ ),
47
+ limits: Type.Optional(limits),
48
+ };
49
+ const item = Type.Object(assignment, { additionalProperties: false });
50
+ export const parameters = Type.Union([
51
+ Type.Object(
52
+ { ...assignment, background: Type.Optional(Type.Boolean()) },
53
+ { additionalProperties: false },
54
+ ),
55
+ Type.Object(
56
+ {
57
+ tasks: Type.Array(item, { minItems: 1, maxItems: 64 }),
58
+ background: Type.Optional(Type.Boolean()),
59
+ },
60
+ { additionalProperties: false },
61
+ ),
62
+ ]);
63
+
64
+ const identity = Type.Object(
65
+ { id: Type.String({ minLength: 1 }), generation: Type.Integer({ minimum: 1 }) },
66
+ { additionalProperties: false },
67
+ );
68
+ export const coordinationParameters = Type.Union([
69
+ Type.Object(
70
+ {
71
+ action: Type.Literal("message"),
72
+ target: Type.Optional(identity),
73
+ text: Type.String({ minLength: 1, maxLength: 4096 }),
74
+ mode: Type.Optional(
75
+ Type.Union([Type.Literal("inbox"), Type.Literal("steer"), Type.Literal("followUp")]),
76
+ ),
77
+ },
78
+ { additionalProperties: false },
79
+ ),
80
+ Type.Object({ action: Type.Literal("inbox") }, { additionalProperties: false }),
81
+ Type.Object(
82
+ {
83
+ action: Type.Literal("wait"),
84
+ target: Type.Optional(identity),
85
+ messageId: Type.Optional(Type.String({ minLength: 1 })),
86
+ timeoutMs: Type.Integer({ minimum: 1, maximum: 60000 }),
87
+ },
88
+ { additionalProperties: false },
89
+ ),
90
+ Type.Object(
91
+ {
92
+ action: Type.Union([Type.Literal("park"), Type.Literal("stop"), Type.Literal("status")]),
93
+ target: identity,
94
+ },
95
+ { additionalProperties: false },
96
+ ),
97
+ Type.Object(
98
+ {
99
+ action: Type.Union([Type.Literal("followup"), Type.Literal("revive")]),
100
+ target: identity,
101
+ assignment: Type.String({ minLength: 1 }),
102
+ resultSchema: Type.Optional(Type.Unknown()),
103
+ },
104
+ { additionalProperties: false },
105
+ ),
106
+ Type.Object(
107
+ {
108
+ action: Type.Literal("resume"),
109
+ target: identity,
110
+ assignment: Type.String({ minLength: 1 }),
111
+ },
112
+ { additionalProperties: false },
113
+ ),
114
+ ]);
115
+
116
+ const worker = Type.Union([role, Type.Literal("worker")]);
117
+ const compatibleItem = Type.Object(
118
+ {
119
+ agent: worker,
120
+ task: Type.String({ minLength: 1 }),
121
+ cwd: Type.Optional(
122
+ Type.String({
123
+ minLength: 1,
124
+ description:
125
+ "Existing workspace; absolute or relative to root parent cwd. Omit to inherit the call default.",
126
+ }),
127
+ ),
128
+ model: Type.Optional(Type.Union([modelSelection, Type.String({ minLength: 1 })])),
129
+ limits: Type.Optional(limits),
130
+ },
131
+ { additionalProperties: false },
132
+ );
133
+ export const compatibleParameters = Type.Union([
134
+ Type.Object(
135
+ { ...compatibleItem.properties, async: Type.Optional(Type.Boolean()) },
136
+ { additionalProperties: false },
137
+ ),
138
+ Type.Object(
139
+ {
140
+ tasks: Type.Array(compatibleItem, { minItems: 1, maxItems: 64 }),
141
+ async: Type.Optional(Type.Boolean()),
142
+ cwd: Type.Optional(Type.String({ minLength: 1 })),
143
+ },
144
+ { additionalProperties: false },
145
+ ),
146
+ Type.Object(
147
+ { action: Type.Union([Type.Literal("list"), Type.Literal("agents")]) },
148
+ { additionalProperties: false },
149
+ ),
150
+ Type.Object(
151
+ {
152
+ action: Type.Union([Type.Literal("status"), Type.Literal("stop")]),
153
+ id: Type.String({ minLength: 1 }),
154
+ },
155
+ { additionalProperties: false },
156
+ ),
157
+ Type.Object(
158
+ {
159
+ action: Type.Union([Type.Literal("steer"), Type.Literal("resume")]),
160
+ id: Type.String({ minLength: 1 }),
161
+ message: Type.String({ minLength: 1 }),
162
+ },
163
+ { additionalProperties: false },
164
+ ),
165
+ ]);