@d3ara1n/pi-subagent 0.2.0 → 0.3.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
@@ -44,7 +44,7 @@ This means:
44
44
  ## TUI Display
45
45
 
46
46
  - **During execution**: Shows role, elapsed time, turn count, and live tool calls
47
- - **Collapsed result**: `✓ explorer · 找到了登录/注册/token三块逻辑` + recent tool calls + usage stats
47
+ - **Collapsed result**: `✓ explorer · Found login, registration, and token logic` + recent tool calls + usage stats
48
48
  - **Expanded result** (Ctrl+O): Full task text, all tool calls, final output as rendered Markdown, and usage details
49
49
 
50
50
  ## Requirements
@@ -56,7 +56,7 @@ This means:
56
56
  ## Installation
57
57
 
58
58
  ```bash
59
- pi extension add @d3ara1n/pi-subagent
59
+ pi install @d3ara1n/pi-subagent
60
60
  ```
61
61
 
62
62
  ## Configuration
@@ -70,7 +70,7 @@ Edit `~/.pi/agent/settings.json`:
70
70
  "timeoutMs": 300000,
71
71
 
72
72
  // Summary generation — uses a lightweight model to create
73
- // a one-line Chinese summary for the TUI display
73
+ // a one-line summary for the TUI display
74
74
  "summary": {
75
75
  "role": "utility", // pi-model-roles role for summarization
76
76
  "enabled": true // set false to disable
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@d3ara1n/pi-subagent",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
+ "type": "module",
4
5
  "description": "Role-based subagent orchestration for pi — delegates tasks to specialized pi child processes with configurable model roles",
5
6
  "main": "src/index.ts",
6
7
  "keywords": [
package/src/index.ts CHANGED
@@ -15,7 +15,7 @@ import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
15
15
  import { Type } from "typebox";
16
16
  import type { ModelRolesAPI } from "@d3ara1n/pi-model-roles";
17
17
  import { getModelRolesAPI } from "@d3ara1n/pi-model-roles";
18
- import type { SubagentConfig, SubagentDetails, SubagentResult, SubagentRole } from "./types.ts";
18
+ import type { SubagentConfig, SubagentDetails, SubagentResult, SubagentRole, ToolStatus, ActivityEntry } from "./types.ts";
19
19
  import { DEFAULT_CONFIG } from "./types.ts";
20
20
  import { loadSubagentConfig } from "./config.ts";
21
21
  import { BUILTIN_ROLES } from "./roles.ts";
@@ -44,21 +44,16 @@ function formatUsageStats(usage: SubagentResult["usage"], model?: string): strin
44
44
  }
45
45
 
46
46
  type DisplayItem =
47
- | { type: "text"; text: string }
48
- | { type: "toolCall"; name: string; args: Record<string, any> };
49
-
50
- function getDisplayItems(messages: SubagentResult["messages"]): DisplayItem[] {
51
- const items: DisplayItem[] = [];
52
- for (const msg of messages) {
53
- if (msg.role === "assistant") {
54
- for (const part of msg.content) {
55
- if (part.type === "text" && part.text) items.push({ type: "text", text: part.text });
56
- else if (part.type === "toolCall" && part.name)
57
- items.push({ type: "toolCall", name: part.name, args: part.arguments ?? {} });
58
- }
59
- }
60
- }
61
- return items;
47
+ | { type: "toolCall"; name: string; args: Record<string, any>; status?: ToolStatus }
48
+ | { type: "thinking"; status?: ToolStatus };
49
+
50
+ /** Map the real-time activity log into renderable display items (in order). */
51
+ function buildDisplayItems(activityLog: ActivityEntry[]): DisplayItem[] {
52
+ return activityLog.map((a) =>
53
+ a.kind === "thinking"
54
+ ? { type: "thinking", status: a.status }
55
+ : { type: "toolCall", name: a.toolName ?? "?", args: a.args ?? {}, status: a.status },
56
+ );
62
57
  }
63
58
 
64
59
  function shortenPath(p: string): string {
@@ -126,6 +121,35 @@ function formatToolCall(
126
121
  }
127
122
  }
128
123
 
124
+ /** Per-tool-call visual styling: prefix glyph + color function keyed by status. */
125
+ function statusStyle(
126
+ status: ToolStatus | undefined,
127
+ fg: (color: string, text: string) => string,
128
+ ): { prefix: string; color: (c: string, text: string) => string } {
129
+ switch (status) {
130
+ case "running":
131
+ return { prefix: fg("accent", "\u25CF "), color: fg };
132
+ case "failed":
133
+ return { prefix: fg("error", "\u2717 "), color: (_c, text) => fg("error", text) };
134
+ case "done":
135
+ default:
136
+ return { prefix: fg("dim", "\u2192 "), color: (_c, text) => fg("dim", text) };
137
+ }
138
+ }
139
+
140
+ /** Render a thinking-block row: diamond glyph + label, colored by status.
141
+ * Running = hollow diamond (unformed thought); done = solid diamond (settled). */
142
+ function formatThinking(
143
+ status: ToolStatus | undefined,
144
+ fg: (color: string, text: string) => string,
145
+ ): string {
146
+ if (status === "running") {
147
+ return fg("accent", "\u25C7 thinking");
148
+ }
149
+ // done (or unknown) — dim past tense, solid diamond
150
+ return fg("dim", "\u25C6 thought");
151
+ }
152
+
129
153
  function renderDisplayItems(
130
154
  items: DisplayItem[],
131
155
  limit: number | undefined,
@@ -136,11 +160,11 @@ function renderDisplayItems(
136
160
  let text = "";
137
161
  if (skipped > 0) text += fg("muted", `... ${skipped} earlier items\n`);
138
162
  for (const item of toShow) {
139
- if (item.type === "text") {
140
- const preview = item.text.split("\n").slice(0, 3).join("\n");
141
- text += `${fg("toolOutput", preview.length > 120 ? preview.slice(0, 120) + "..." : preview)}\n`;
163
+ if (item.type === "thinking") {
164
+ text += `${formatThinking(item.status, fg)}\n`;
142
165
  } else {
143
- text += `${fg("muted", "\u2192 ")}${formatToolCall(item.name, item.args, fg)}\n`;
166
+ const { prefix, color } = statusStyle(item.status, fg);
167
+ text += `${prefix}${formatToolCall(item.name, item.args, color)}\n`;
144
168
  }
145
169
  }
146
170
  return text.trimEnd();
@@ -187,8 +211,8 @@ async function generateSummary(
187
211
  resolved.model,
188
212
  {
189
213
  systemPrompt:
190
- "Summarize the following agent output in one concise Chinese sentence (max 60 characters). Focus on what was accomplished, not how. Output only the summary, no preamble.",
191
- messages: [{ role: "user", content: summaryInput }],
214
+ "Summarize the following agent output in one concise sentence (max 60 characters). Respond in the same language as the input. Focus on what was accomplished, not how. Output only the summary, no preamble.",
215
+ messages: [{ role: "user", content: summaryInput, timestamp: Date.now() }],
192
216
  },
193
217
  {
194
218
  maxTokens: 100,
@@ -338,6 +362,7 @@ export default function subagentExtension(pi: ExtensionAPI) {
338
362
  text: `Unknown subagent role: ${params.role}. Available: ${Object.keys(availableRoles).join(", ")}`,
339
363
  },
340
364
  ],
365
+ details: undefined as any,
341
366
  };
342
367
  }
343
368
 
@@ -348,6 +373,7 @@ export default function subagentExtension(pi: ExtensionAPI) {
348
373
  } catch {
349
374
  return {
350
375
  content: [{ type: "text", text: "pi-model-roles is not initialized. Cannot resolve model for subagent." }],
376
+ details: undefined as any,
351
377
  };
352
378
  }
353
379
 
@@ -355,6 +381,7 @@ export default function subagentExtension(pi: ExtensionAPI) {
355
381
  if (!resolved.model) {
356
382
  return {
357
383
  content: [{ type: "text", text: `Role "${roleDef.role}" could not be resolved. Model not available.` }],
384
+ details: undefined as any,
358
385
  };
359
386
  }
360
387
 
@@ -371,6 +398,7 @@ export default function subagentExtension(pi: ExtensionAPI) {
371
398
  output: "",
372
399
  stderr: "",
373
400
  usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
401
+ activityLog: [],
374
402
  };
375
403
  onUpdate({
376
404
  content: [{ type: "text", text: `${params.role}: running...` }],
@@ -407,6 +435,7 @@ export default function subagentExtension(pi: ExtensionAPI) {
407
435
  },
408
436
  model: partial.model,
409
437
  stopReason: partial.stopReason,
438
+ activityLog: partial.activityLog ?? [],
410
439
  };
411
440
  const statusText = `${params.role} ${elapsed}s ${liveResult.usage.turns} turn${liveResult.usage.turns !== 1 ? "s" : ""}`;
412
441
  onUpdate({
@@ -511,7 +540,7 @@ export default function subagentExtension(pi: ExtensionAPI) {
511
540
  } else {
512
541
  icon = theme.fg("success", "\u2713");
513
542
  }
514
- const displayItems = getDisplayItems(r.messages);
543
+ const displayItems = buildDisplayItems(r.activityLog);
515
544
  const finalOutput = getFinalOutput(r.messages);
516
545
  const mdTheme = getMarkdownTheme();
517
546
 
@@ -532,20 +561,21 @@ export default function subagentExtension(pi: ExtensionAPI) {
532
561
  }
533
562
 
534
563
  container.addChild(new Spacer(1));
535
- const toolCalls = displayItems.filter((item) => item.type === "toolCall");
536
- if (toolCalls.length === 0) {
564
+ const activity = displayItems.filter((item) => item.type === "toolCall" || item.type === "thinking");
565
+ if (activity.length === 0) {
537
566
  const runningLabel = isRunning ? "(waiting for first event...)" : "(none)";
538
567
  container.addChild(new Text(theme.fg("muted", runningLabel), 0, 0));
539
568
  } else {
540
- for (const item of toolCalls) {
541
- container.addChild(
542
- new Text(
543
- theme.fg("muted", "\u2192 ") +
544
- formatToolCall(item.name, item.args, theme.fg.bind(theme)),
545
- 0,
546
- 0,
547
- ),
548
- );
569
+ const fg = theme.fg.bind(theme) as (color: string, text: string) => string;
570
+ for (const item of activity) {
571
+ if (item.type === "thinking") {
572
+ container.addChild(new Text(formatThinking(item.status, fg), 0, 0));
573
+ } else {
574
+ const { prefix, color } = statusStyle(item.status, fg);
575
+ container.addChild(
576
+ new Text(prefix + formatToolCall(item.name, item.args, color), 0, 0),
577
+ );
578
+ }
549
579
  }
550
580
  }
551
581
 
@@ -569,11 +599,11 @@ export default function subagentExtension(pi: ExtensionAPI) {
569
599
 
570
600
  if (isRunning) {
571
601
  // Running: show recent tool calls only
572
- const toolCalls = displayItems.filter((item) => item.type === "toolCall");
573
- if (toolCalls.length === 0) {
602
+ const activity = displayItems.filter((item) => item.type === "toolCall" || item.type === "thinking");
603
+ if (activity.length === 0) {
574
604
  text += `\n${theme.fg("muted", "(running...)")}`;
575
605
  } else {
576
- const rendered = renderDisplayItems(toolCalls, 5, theme.fg.bind(theme));
606
+ const rendered = renderDisplayItems(activity, 5, theme.fg.bind(theme) as (color: string, text: string) => string);
577
607
  if (rendered) text += `\n${rendered}`;
578
608
  }
579
609
  } else {
package/src/spawn.ts CHANGED
@@ -140,6 +140,7 @@ export async function spawnSubagent(
140
140
  output: "",
141
141
  stderr: "",
142
142
  usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
143
+ activityLog: [],
143
144
  };
144
145
 
145
146
  let tmpDir: string | null = null;
@@ -183,9 +184,12 @@ export async function spawnSubagent(
183
184
  usage: { ...result.usage },
184
185
  model: result.model,
185
186
  stopReason: result.stopReason,
187
+ activityLog: result.activityLog.map((a) => ({ ...a })),
186
188
  });
187
189
  };
188
190
 
191
+ let thinkingCounter = 0;
192
+
189
193
  const processLine = (line: string) => {
190
194
  if (!line.trim()) return;
191
195
  let event: any;
@@ -225,10 +229,47 @@ export async function spawnSubagent(
225
229
  emitProgress();
226
230
  }
227
231
 
228
- if (event.type === "tool_result_end" && event.message) {
229
- result.messages.push(event.message as SubagentMessage);
232
+ // Activity log: track thinking blocks and tool calls in arrival order.
233
+ // Both update in place so the TUI reflects real-time state.
234
+ if (event.type === "tool_execution_start" && event.toolCallId) {
235
+ result.activityLog.push({
236
+ kind: "toolCall",
237
+ id: event.toolCallId,
238
+ status: "running",
239
+ toolName: event.toolName,
240
+ args: event.args ?? {},
241
+ });
242
+ emitProgress();
243
+ } else if (event.type === "tool_execution_end" && event.toolCallId) {
244
+ const entry = result.activityLog.find((a) => a.id === event.toolCallId);
245
+ if (entry) entry.status = event.isError ? "failed" : "done";
230
246
  emitProgress();
231
247
  }
248
+
249
+ // Thinking-block lifecycle: pi wraps thinking_start/end inside
250
+ // message_update.assistantMessageEvent. These arrive BEFORE message_end,
251
+ // so we can't rely on messages[] to show real-time thinking state —
252
+ // register them in the activity log directly.
253
+ const aev = event.assistantMessageEvent;
254
+ if (event.type === "message_update" && aev) {
255
+ if (aev.type === "thinking_start") {
256
+ result.activityLog.push({
257
+ kind: "thinking",
258
+ id: `thinking-${thinkingCounter++}`,
259
+ status: "running",
260
+ });
261
+ emitProgress();
262
+ } else if (aev.type === "thinking_end") {
263
+ // Mark the most recent still-running thinking block as done.
264
+ for (let i = result.activityLog.length - 1; i >= 0; i--) {
265
+ if (result.activityLog[i].kind === "thinking" && result.activityLog[i].status === "running") {
266
+ result.activityLog[i].status = "done";
267
+ break;
268
+ }
269
+ }
270
+ emitProgress();
271
+ }
272
+ }
232
273
  };
233
274
 
234
275
  // Build env with optional subagent allowlist and tmpdir for researcher role
package/src/types.ts CHANGED
@@ -45,6 +45,20 @@ export interface SubagentRole {
45
45
  fallbackRole?: string;
46
46
  }
47
47
 
48
+ /** Status of an individual tool call within a subagent run. */
49
+ export type ToolStatus = "running" | "done" | "failed";
50
+
51
+ /** A single entry in the real-time activity log (thinking block or tool call). */
52
+ export interface ActivityEntry {
53
+ kind: "thinking" | "toolCall";
54
+ /** Synthetic id (thinking-N) or the toolCallId from the event stream. */
55
+ id: string;
56
+ status: ToolStatus;
57
+ /** Tool name + args (toolCall only). */
58
+ toolName?: string;
59
+ args?: Record<string, any>;
60
+ }
61
+
48
62
  /** Usage statistics from a subagent execution. */
49
63
  export interface SubagentUsage {
50
64
  input: number;
@@ -104,6 +118,8 @@ export interface SubagentResult {
104
118
  stopReason?: string;
105
119
  /** Error message if failed */
106
120
  errorMessage?: string;
121
+ /** Real-time activity log: thinking blocks and tool calls in arrival order. */
122
+ activityLog: ActivityEntry[];
107
123
  }
108
124
 
109
125
  /** TUI details structure passed via tool result details. */