@mystilleef/pi-subagent 0.5.0 → 0.7.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.
@@ -18,19 +18,20 @@
18
18
 
19
19
  import type { Component } from "@earendil-works/pi-tui";
20
20
  import { Box, Text } from "@earendil-works/pi-tui";
21
+ import { formatSubagentTitle, type SubagentTheme } from "../output/ui.js";
21
22
  import {
22
23
  formatHeaderStats,
23
24
  getProgressState,
24
25
  type ProgressStatus,
26
+ renderToolActivityForDisplay,
25
27
  STATUS_BG,
26
28
  STATUS_COLOR,
27
29
  STATUS_ICON,
28
30
  type SubagentProgressState,
29
31
  type ThemeBg,
30
32
  } from "./progress-state.js";
31
- import { formatSubagentTitle, type SubagentTheme } from "./ui.js";
32
33
 
33
- export { makeToolPreview } from "./normalize.js";
34
+ export { makeToolPreview } from "../output/normalize.js";
34
35
  export {
35
36
  cancelProgressState,
36
37
  clearProgressState,
@@ -41,11 +42,12 @@ export {
41
42
  formatContextPercent,
42
43
  formatElapsed,
43
44
  formatHeaderStats,
44
- formatTokenCount,
45
45
  getProgressState,
46
46
  makeTaskPreview,
47
47
  type ProgressStatus,
48
48
  patchProgressState,
49
+ renderToolActivity,
50
+ renderToolActivityForDisplay,
49
51
  resetProgressStore,
50
52
  STATUS_COLOR,
51
53
  STATUS_ICON,
@@ -96,7 +98,9 @@ class DynamicSubagentProgressText implements Component {
96
98
  render(width: number): string[] {
97
99
  const state = getProgressState(this.requestId);
98
100
  if (!state) return [];
99
- return renderProgressBox(state, this.options, this.theme).render(width);
101
+ return renderProgressBox(state, this.options, this.theme, width).render(
102
+ width,
103
+ );
100
104
  }
101
105
  }
102
106
 
@@ -108,6 +112,7 @@ function renderProgressBox(
108
112
  state: SubagentProgressState,
109
113
  options: { expanded: boolean },
110
114
  theme: SubagentTheme,
115
+ width: number,
111
116
  ): Box {
112
117
  const status = state.status;
113
118
  const title = formatSubagentTitle(state.agent, state.instanceName, theme);
@@ -116,28 +121,19 @@ function renderProgressBox(
116
121
  theme.bg(getProgressBackground(status), line),
117
122
  );
118
123
  box.addChild(new Text(header, 0, 0));
119
- addProgressBody(box, state, options, theme);
120
- return box;
121
- }
122
-
123
- function addProgressBody(
124
- box: Box,
125
- state: SubagentProgressState,
126
- options: { expanded: boolean },
127
- theme: SubagentTheme,
128
- ): void {
129
- const body = makeProgressBody(state, options, theme);
130
- if (body.length === 0) return;
124
+ const body = makeProgressBody(state, options, theme, width);
131
125
  for (const line of body) box.addChild(line);
126
+ return box;
132
127
  }
133
128
 
134
129
  function makeProgressBody(
135
130
  state: SubagentProgressState,
136
131
  options: { expanded: boolean },
137
132
  theme: SubagentTheme,
133
+ width: number,
138
134
  ): Text[] {
139
135
  if (state.status === "running")
140
- return makeRunningProgressBody(state, options, theme);
136
+ return makeRunningProgressBody(state, options, theme, width);
141
137
  if (state.status === "error" || state.status === "cancelled") {
142
138
  return makeStoppedProgressBody(state, options, theme);
143
139
  }
@@ -150,12 +146,16 @@ function makeRunningProgressBody(
150
146
  state: SubagentProgressState,
151
147
  options: { expanded: boolean },
152
148
  theme: SubagentTheme,
149
+ width: number,
153
150
  ): Text[] {
154
151
  const body: Text[] = [];
155
- if (state.lastToolPreview) {
156
- body.push(
157
- new Text(formatRunningToolPreview(state.lastToolPreview, theme), 2, 0),
158
- );
152
+ const activityBudget = Math.max(0, width - 8);
153
+ const activityPreview = renderToolActivityForDisplay(
154
+ state.activeToolActivity,
155
+ activityBudget,
156
+ );
157
+ if (activityPreview) {
158
+ body.push(new Text(formatRunningToolPreview(activityPreview, theme), 2, 0));
159
159
  }
160
160
  if (options.expanded)
161
161
  body.push(new Text(theme.fg("dim", state.taskPreview), 2, 0));
@@ -1,19 +1,21 @@
1
- import { TOOL_RESULT_FAILED_MESSAGE } from "./process.js";
2
- import {
3
- extractProgressFromDetails,
4
- getProgressState,
5
- patchProgressState,
6
- } from "./progress.js";
1
+ import { TOOL_RESULT_FAILED_MESSAGE } from "../child/process.js";
7
2
  import {
8
3
  formatSubagentResultForParent,
9
4
  summarizeFeedbackUiFinalOutput,
10
- } from "./summary.js";
5
+ } from "../output/summary.js";
11
6
  import type {
12
7
  SingleResult,
13
8
  SubagentDetails,
14
9
  SubagentToolResult,
15
- } from "./types.js";
16
- import { detectMessageError } from "./utils.js";
10
+ ToolActivity,
11
+ } from "../shared/types.js";
12
+ import { detectMessageError } from "../shared/utils.js";
13
+ import {
14
+ extractProgressFromDetails,
15
+ getProgressState,
16
+ patchProgressState,
17
+ renderToolActivity,
18
+ } from "./progress.js";
17
19
 
18
20
  export function hasSubagentFailed(result: SingleResult): boolean {
19
21
  return (
@@ -53,22 +55,56 @@ export function sanitizeDetailsForDisplay(
53
55
  };
54
56
  }
55
57
 
58
+ export function getLatestResult(
59
+ details: SubagentDetails,
60
+ ): SingleResult | undefined {
61
+ return details.results[0];
62
+ }
63
+
56
64
  export function patchProgressFromDetails(
57
65
  requestId: string,
58
66
  details: SubagentDetails,
59
67
  seenToolCallIds: Set<string>,
60
68
  ): void {
61
- const latestResult = details.results[0];
62
- const { newToolCallIds, lastToolPreview } = extractProgressFromDetails(
63
- details,
64
- seenToolCallIds,
65
- );
69
+ const latestResult = getLatestResult(details);
70
+ const {
71
+ newToolCallIds,
72
+ lastToolPreview,
73
+ activityText,
74
+ activeToolActivity,
75
+ toolResultCompleted,
76
+ } = extractProgressFromDetails(details, seenToolCallIds);
66
77
  const current = getProgressState(requestId);
67
78
  if (!current) return;
68
79
  const patch: Record<string, unknown> = {
69
80
  toolCount: current.toolCount + newToolCallIds.length,
70
81
  };
71
- if (lastToolPreview) patch.lastToolPreview = lastToolPreview;
82
+ let nextActivity: ToolActivity | undefined;
83
+ if (newToolCallIds.length > 0 && lastToolPreview) {
84
+ nextActivity = { toolName: "tool", inputSummary: lastToolPreview };
85
+ } else if (activeToolActivity) {
86
+ nextActivity = activeToolActivity;
87
+ } else if (activityText) {
88
+ nextActivity = { toolName: "tool", inputSummary: activityText };
89
+ } else if (current.activeToolActivity) {
90
+ nextActivity = current.activeToolActivity;
91
+ }
92
+ if (toolResultCompleted && nextActivity?.child) {
93
+ nextActivity = { ...nextActivity, child: undefined };
94
+ } else if (toolResultCompleted) {
95
+ nextActivity = undefined;
96
+ }
97
+ patch.activeToolActivity = nextActivity;
98
+ const renderedPreview = renderToolActivity(nextActivity);
99
+ if (renderedPreview) {
100
+ patch.lastToolPreview = renderedPreview;
101
+ } else if (toolResultCompleted && !nextActivity) {
102
+ patch.lastToolPreview = undefined;
103
+ }
104
+ if (toolResultCompleted) {
105
+ patch.toolResultCompleted = true;
106
+ }
107
+ // Token accounting always applies when usage data is available
72
108
  if (latestResult?.usage) {
73
109
  patch.inputTokens = latestResult.usage.input;
74
110
  patch.outputTokens = latestResult.usage.output;
@@ -86,11 +122,13 @@ export function getSubagentText(result: SubagentToolResult): string {
86
122
  }
87
123
 
88
124
  export function getResultDisplayText(result: SubagentToolResult): string {
89
- return result.details.results[0]?.finalOutput ?? getSubagentText(result);
125
+ return (
126
+ getLatestResult(result.details)?.finalOutput ?? getSubagentText(result)
127
+ );
90
128
  }
91
129
 
92
130
  export function getFeedbackSummaryText(result: SubagentToolResult): string {
93
- const rawFinalOutput = result.details.results[0]?.finalOutput;
131
+ const rawFinalOutput = getLatestResult(result.details)?.finalOutput;
94
132
  if (rawFinalOutput?.trim())
95
133
  return summarizeFeedbackUiFinalOutput(rawFinalOutput);
96
134
  return getSubagentText(result).trim() || "(no output)";
@@ -0,0 +1,117 @@
1
+ const DEFAULT_ADJECTIVES = [
2
+ "able",
3
+ "agile",
4
+ "alert",
5
+ "amber",
6
+ "ample",
7
+ "apt",
8
+ "arctic",
9
+ "avid",
10
+ "bold",
11
+ "brave",
12
+ "bright",
13
+ "brisk",
14
+ "calm",
15
+ "clever",
16
+ "cosmic",
17
+ "crisp",
18
+ "daring",
19
+ "dawn",
20
+ "eager",
21
+ "early",
22
+ "fair",
23
+ "fast",
24
+ "fierce",
25
+ "fine",
26
+ "fresh",
27
+ "gentle",
28
+ "golden",
29
+ "grand",
30
+ "happy",
31
+ "honest",
32
+ "jolly",
33
+ "keen",
34
+ "kind",
35
+ "lively",
36
+ "lucky",
37
+ "merry",
38
+ "mighty",
39
+ "nimble",
40
+ "noble",
41
+ "novel",
42
+ "patient",
43
+ "proud",
44
+ "quick",
45
+ "quiet",
46
+ "rapid",
47
+ "ready",
48
+ "sharp",
49
+ "smart",
50
+ "solid",
51
+ "steady",
52
+ "swift",
53
+ "tidy",
54
+ "vivid",
55
+ "warm",
56
+ "wise",
57
+ ] as const;
58
+
59
+ const DEFAULT_NOUNS = [
60
+ "badger",
61
+ "beacon",
62
+ "bison",
63
+ "brook",
64
+ "cedar",
65
+ "comet",
66
+ "coral",
67
+ "coyote",
68
+ "crane",
69
+ "dolphin",
70
+ "eagle",
71
+ "ember",
72
+ "falcon",
73
+ "finch",
74
+ "forest",
75
+ "fox",
76
+ "gecko",
77
+ "glade",
78
+ "harbor",
79
+ "hawk",
80
+ "heron",
81
+ "island",
82
+ "jaguar",
83
+ "koala",
84
+ "lagoon",
85
+ "lemur",
86
+ "lynx",
87
+ "maple",
88
+ "meadow",
89
+ "otter",
90
+ "panda",
91
+ "panther",
92
+ "pelican",
93
+ "phoenix",
94
+ "puma",
95
+ "raven",
96
+ "reef",
97
+ "river",
98
+ "salmon",
99
+ "sparrow",
100
+ "summit",
101
+ "tiger",
102
+ "valley",
103
+ "violet",
104
+ "walrus",
105
+ "willow",
106
+ "wolf",
107
+ "wren",
108
+ "yak",
109
+ "zephyr",
110
+ ] as const;
111
+
112
+ export function generateSubagentInstanceName(): string {
113
+ const adj =
114
+ DEFAULT_ADJECTIVES[Math.floor(Math.random() * DEFAULT_ADJECTIVES.length)];
115
+ const noun = DEFAULT_NOUNS[Math.floor(Math.random() * DEFAULT_NOUNS.length)];
116
+ return `${adj}-${noun}`;
117
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Message } from "@earendil-works/pi-ai";
2
- import type { AgentScope } from "./agents.js";
3
- import type { TerminationMetadata } from "./termination.js";
2
+ import type { AgentScope } from "../agent/agents.js";
3
+ import type { TerminationMetadata } from "../child/termination.js";
4
4
 
5
5
  export interface UsageStats {
6
6
  input: number;
@@ -13,6 +13,13 @@ export interface UsageStats {
13
13
  turns: number;
14
14
  }
15
15
 
16
+ export interface ToolActivity {
17
+ toolName: string;
18
+ inputSummary?: string;
19
+ instanceName?: string;
20
+ child?: ToolActivity;
21
+ }
22
+
16
23
  export interface StreamingProgressToolCall {
17
24
  id: string;
18
25
  preview: string;
@@ -20,8 +27,10 @@ export interface StreamingProgressToolCall {
20
27
 
21
28
  export interface StreamingProgress {
22
29
  activityText?: string;
30
+ activeToolActivity?: ToolActivity;
23
31
  toolCalls: StreamingProgressToolCall[];
24
32
  lastToolPreview?: string;
33
+ toolResultCompleted?: boolean;
25
34
  }
26
35
 
27
36
  export interface SingleResult {
@@ -40,6 +49,7 @@ export interface SingleResult {
40
49
  progress?: StreamingProgress;
41
50
  messages?: Message[];
42
51
  termination?: TerminationMetadata;
52
+ thinkingWarning?: string;
43
53
  }
44
54
 
45
55
  export interface SubagentDetails {
@@ -47,6 +57,7 @@ export interface SubagentDetails {
47
57
  agentScope: AgentScope;
48
58
  projectAgentsDir: string | null;
49
59
  results: SingleResult[];
60
+ renderedByMessage?: true;
50
61
  }
51
62
 
52
63
  export interface SubagentToolResult {
@@ -5,7 +5,6 @@ import type { Message } from "@earendil-works/pi-ai";
5
5
  import {
6
6
  DefaultResourceLoader,
7
7
  getAgentDir,
8
- withFileMutationQueue,
9
8
  } from "@earendil-works/pi-coding-agent";
10
9
 
11
10
  export const DEFAULT_MAX_OUTPUT_BYTES = 50_000;
@@ -66,11 +65,10 @@ export async function writePromptToTempFile(
66
65
  );
67
66
  const safeName = agentName.replace(/[^\w.-]+/g, "_");
68
67
  const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
69
- await withFileMutationQueue(filePath, async () => {
70
- await fs.promises.writeFile(filePath, prompt, {
71
- encoding: "utf-8",
72
- mode: 0o600,
73
- });
68
+ // mkdtemp guarantees a unique directory per call; no concurrent writer can hold this path.
69
+ await fs.promises.writeFile(filePath, prompt, {
70
+ encoding: "utf-8",
71
+ mode: 0o600,
74
72
  });
75
73
  return { dir: tmpDir, filePath };
76
74
  }
@@ -1,41 +0,0 @@
1
- import path from "node:path";
2
- import {
3
- type AgentDiscoveryResult,
4
- type AgentScope,
5
- discoverAgents,
6
- } from "./agents.js";
7
-
8
- export type AgentDiscoveryCacheEntry = AgentDiscoveryResult & { ts: number };
9
- export type AgentDiscoveryCache = Map<string, AgentDiscoveryCacheEntry>;
10
- export const AGENT_DISCOVERY_CACHE_TTL_MS = 3_000;
11
- const sharedAgentDiscoveryCache: AgentDiscoveryCache = new Map();
12
-
13
- export function resetAgentDiscoveryCache(): void {
14
- sharedAgentDiscoveryCache.clear();
15
- }
16
-
17
- export function getCachedAgentDiscovery(
18
- cwd: string,
19
- scope: AgentScope,
20
- cache: AgentDiscoveryCache = sharedAgentDiscoveryCache,
21
- cacheTtlMs = AGENT_DISCOVERY_CACHE_TTL_MS,
22
- ): AgentDiscoveryCacheEntry {
23
- const key = `${path.resolve(cwd)}\0${scope}`;
24
- const now = Date.now();
25
- const entry = cache.get(key);
26
- if (entry && now - entry.ts <= cacheTtlMs) return entry;
27
- const nextEntry = { ...discoverAgents(cwd, scope), ts: now };
28
- cache.set(key, nextEntry);
29
- return nextEntry;
30
- }
31
-
32
- export function getCachedAgentCompletions(
33
- prefix: string,
34
- cwd = process.cwd(),
35
- cache: AgentDiscoveryCache = sharedAgentDiscoveryCache,
36
- cacheTtlMs = AGENT_DISCOVERY_CACHE_TTL_MS,
37
- ): { value: string; label: string }[] {
38
- return getCachedAgentDiscovery(cwd, "both", cache, cacheTtlMs)
39
- .agents.filter((agent) => agent.name.startsWith(prefix))
40
- .map((agent) => ({ value: agent.name, label: agent.name }));
41
- }
package/src/agents.ts DELETED
@@ -1,178 +0,0 @@
1
- /**
2
- * Agent discovery and configuration
3
- */
4
-
5
- import * as fs from "node:fs";
6
- import * as path from "node:path";
7
- import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
8
-
9
- export type AgentScope = "user" | "project" | "both";
10
-
11
- export type ThinkingLevel =
12
- | "off"
13
- | "minimal"
14
- | "low"
15
- | "medium"
16
- | "high"
17
- | "xhigh";
18
-
19
- const THINKING_LEVELS = [
20
- "off",
21
- "minimal",
22
- "low",
23
- "medium",
24
- "high",
25
- "xhigh",
26
- ] as const;
27
-
28
- export interface AgentConfig {
29
- name: string;
30
- description: string;
31
- tools?: string[];
32
- skills?: string[];
33
- thinking?: ThinkingLevel;
34
- systemPrompt: string;
35
- source: "user" | "project";
36
- filePath: string;
37
- }
38
-
39
- export interface AgentDiscoveryResult {
40
- agents: AgentConfig[];
41
- projectAgentsDir: string | null;
42
- }
43
-
44
- function parseCommaList(raw: unknown): string[] | undefined {
45
- if (typeof raw !== "string") return undefined;
46
- const items = raw
47
- .split(",")
48
- .map((s) => s.trim())
49
- .filter(Boolean);
50
- return items.length > 0 ? items : undefined;
51
- }
52
-
53
- function parseThinkingLevel(raw: unknown): ThinkingLevel | undefined {
54
- if (typeof raw !== "string") return undefined;
55
- const normalized = raw.trim().toLowerCase();
56
- return (THINKING_LEVELS as readonly string[]).includes(normalized)
57
- ? (normalized as ThinkingLevel)
58
- : undefined;
59
- }
60
-
61
- function loadAgentsFromDir(
62
- dir: string,
63
- source: "user" | "project",
64
- ): AgentConfig[] {
65
- const agents: AgentConfig[] = [];
66
- if (!fs.existsSync(dir)) {
67
- return agents;
68
- }
69
- let entries: fs.Dirent[];
70
- try {
71
- entries = fs.readdirSync(dir, { withFileTypes: true });
72
- } catch {
73
- return agents;
74
- }
75
- for (const entry of entries) {
76
- if (!entry.name.endsWith(".md")) continue;
77
- if (!entry.isFile() && !entry.isSymbolicLink()) continue;
78
- const filePath = path.join(dir, entry.name);
79
- let content: string;
80
- try {
81
- content = fs.readFileSync(filePath, "utf-8");
82
- } catch {
83
- continue;
84
- }
85
- let parsed: ReturnType<typeof parseFrontmatter<Record<string, unknown>>>;
86
- try {
87
- parsed = parseFrontmatter<Record<string, unknown>>(content);
88
- } catch {
89
- continue;
90
- }
91
- const { frontmatter, body } = parsed;
92
- if (
93
- typeof frontmatter !== "object" ||
94
- frontmatter === null ||
95
- Array.isArray(frontmatter)
96
- )
97
- continue;
98
- const {
99
- name,
100
- description,
101
- tools: rawTools,
102
- skills: rawSkills,
103
- thinking: rawThinking,
104
- } = frontmatter;
105
- if (typeof name !== "string" || typeof description !== "string") continue;
106
- if (rawTools != null && typeof rawTools !== "string") continue;
107
- if (rawSkills != null && typeof rawSkills !== "string") continue;
108
- if (rawThinking != null && typeof rawThinking !== "string") continue;
109
- const tools = parseCommaList(rawTools);
110
- const skills = Object.hasOwn(frontmatter, "skills")
111
- ? (parseCommaList(rawSkills) ?? [])
112
- : undefined;
113
- const thinking = parseThinkingLevel(rawThinking);
114
- agents.push({
115
- name,
116
- description,
117
- tools,
118
- skills,
119
- thinking,
120
- systemPrompt: body,
121
- source,
122
- filePath,
123
- });
124
- }
125
- return agents;
126
- }
127
-
128
- function isDirectory(p: string): boolean {
129
- try {
130
- return fs.statSync(p).isDirectory();
131
- } catch {
132
- return false;
133
- }
134
- }
135
-
136
- function findNearestProjectAgentsDir(cwd: string): string | null {
137
- let currentDir = cwd;
138
- while (true) {
139
- const candidate = path.join(currentDir, ".pi", "agents");
140
- if (isDirectory(candidate)) return candidate;
141
- const parentDir = path.dirname(currentDir);
142
- if (parentDir === currentDir) return null;
143
- currentDir = parentDir;
144
- }
145
- }
146
-
147
- export function discoverAgents(
148
- cwd: string,
149
- scope: AgentScope,
150
- ): AgentDiscoveryResult {
151
- const userDir = path.join(getAgentDir(), "agents");
152
- const projectAgentsDir = findNearestProjectAgentsDir(cwd);
153
- const userAgents =
154
- scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
155
- const projectAgents =
156
- scope === "user" || !projectAgentsDir
157
- ? []
158
- : loadAgentsFromDir(projectAgentsDir, "project");
159
- const agentMap = new Map<string, AgentConfig>();
160
- for (const agent of userAgents) agentMap.set(agent.name, agent);
161
- for (const agent of projectAgents) agentMap.set(agent.name, agent);
162
- return { agents: Array.from(agentMap.values()), projectAgentsDir };
163
- }
164
-
165
- export function formatAgentList(
166
- agents: AgentConfig[],
167
- maxItems: number,
168
- ): { text: string; remaining: number } {
169
- if (agents.length === 0) return { text: "none", remaining: 0 };
170
- const listed = agents.slice(0, maxItems);
171
- const remaining = agents.length - listed.length;
172
- return {
173
- text: listed
174
- .map((a) => `${a.name} (${a.source}): ${a.description}`)
175
- .join("; "),
176
- remaining,
177
- };
178
- }
@@ -1,32 +0,0 @@
1
- type ChildKnownEvent =
2
- | { type: "message_end"; message: unknown }
3
- | { type: "tool_result_end"; message: unknown }
4
- | { type: "agent_end"; messages?: unknown; stopReason?: string };
5
-
6
- export type ChildEventParseResult =
7
- | { kind: "known"; event: ChildKnownEvent }
8
- | { kind: "unknown"; event: unknown }
9
- | { kind: "invalid"; line: string };
10
-
11
- const KNOWN_TYPES = new Set(["message_end", "tool_result_end", "agent_end"]);
12
-
13
- export function parseChildEventLine(line: string): ChildEventParseResult {
14
- if (typeof line !== "string" || !line.trim())
15
- return { kind: "invalid", line };
16
- let event: unknown;
17
- try {
18
- event = JSON.parse(line);
19
- } catch {
20
- return { kind: "invalid", line };
21
- }
22
- if (
23
- typeof event === "object" &&
24
- event !== null &&
25
- "type" in event &&
26
- typeof (event as Record<string, unknown>).type === "string" &&
27
- KNOWN_TYPES.has((event as Record<string, unknown>).type as string)
28
- ) {
29
- return { kind: "known", event: event as ChildKnownEvent };
30
- }
31
- return { kind: "unknown", event };
32
- }