@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.
- package/README.md +1 -1
- package/package.json +13 -11
- package/src/agent/agent-cache.ts +258 -0
- package/src/agent/agents.ts +232 -0
- package/src/child/child-events.ts +119 -0
- package/src/{process.ts → child/process.ts} +268 -122
- package/src/{prompt-contract.ts → child/prompt-contract.ts} +2 -0
- package/src/index.ts +43 -24
- package/src/{jobs-command.ts → orchestration/jobs-command.ts} +2 -3
- package/src/{run.ts → orchestration/run.ts} +2 -2
- package/src/{subagent-orchestrator.ts → orchestration/subagent-orchestrator.ts} +246 -124
- package/src/{normalize.ts → output/normalize.ts} +35 -49
- package/src/{summary.ts → output/summary.ts} +4 -2
- package/src/{ui.ts → output/ui.ts} +45 -48
- package/src/{progress-state.ts → progress/progress-state.ts} +164 -52
- package/src/{progress.ts → progress/progress.ts} +21 -21
- package/src/{result-details.ts → progress/result-details.ts} +55 -17
- package/src/shared/instance-name.ts +117 -0
- package/src/{types.ts → shared/types.ts} +13 -2
- package/src/{utils.ts → shared/utils.ts} +4 -6
- package/src/agent-cache.ts +0 -41
- package/src/agents.ts +0 -178
- package/src/child-events.ts +0 -32
- package/src/instance-name.ts +0 -164
- /package/src/{termination.ts → child/termination.ts} +0 -0
- /package/src/{cancel-command.ts → orchestration/cancel-command.ts} +0 -0
- /package/src/{run-command.ts → orchestration/run-command.ts} +0 -0
- /package/src/{run-registry.ts → orchestration/run-registry.ts} +0 -0
|
@@ -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 "
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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 "
|
|
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 "
|
|
5
|
+
} from "../output/summary.js";
|
|
11
6
|
import type {
|
|
12
7
|
SingleResult,
|
|
13
8
|
SubagentDetails,
|
|
14
9
|
SubagentToolResult,
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
62
|
-
const {
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 "
|
|
3
|
-
import type { TerminationMetadata } from "
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
}
|
package/src/agent-cache.ts
DELETED
|
@@ -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
|
-
}
|
package/src/child-events.ts
DELETED
|
@@ -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
|
-
}
|