@mystilleef/pi-subagent 0.6.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 +1 -1
- package/src/agent/agent-cache.ts +225 -8
- package/src/agent/agents.ts +83 -47
- package/src/child/child-events.ts +90 -3
- package/src/child/process.ts +191 -101
- package/src/index.ts +36 -17
- package/src/orchestration/subagent-orchestrator.ts +228 -99
- package/src/output/normalize.ts +35 -28
- package/src/output/ui.ts +25 -20
- package/src/progress/progress-state.ts +163 -51
- package/src/progress/progress.ts +19 -19
- package/src/progress/result-details.ts +46 -8
- package/src/shared/types.ts +10 -0
package/README.md
CHANGED
|
@@ -151,7 +151,7 @@ executable automation.
|
|
|
151
151
|
**Environment variables:**
|
|
152
152
|
|
|
153
153
|
- `PI_SUBAGENT_DEPTH`: nested subagent depth guard. Nested calls stop at
|
|
154
|
-
depth `
|
|
154
|
+
depth `2`.
|
|
155
155
|
- `PI_SUBAGENT_MAX_OUTPUT_BYTES`: max returned output bytes. Default:
|
|
156
156
|
`50000`.
|
|
157
157
|
- `PI_SUBAGENT_MAX_OUTPUT_LINES`: max returned output lines. Default:
|
package/package.json
CHANGED
package/src/agent/agent-cache.ts
CHANGED
|
@@ -1,32 +1,249 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as fsPromises from "node:fs/promises";
|
|
1
3
|
import path from "node:path";
|
|
2
4
|
import {
|
|
3
5
|
type AgentDiscoveryResult,
|
|
6
|
+
type AgentDiscoveryScopeResult,
|
|
4
7
|
type AgentScope,
|
|
8
|
+
type AgentSource,
|
|
5
9
|
discoverAgentsAsync,
|
|
10
|
+
emptyScopeResult,
|
|
11
|
+
getUserAgentsDir,
|
|
12
|
+
readMarkdownDirEntriesAsync,
|
|
6
13
|
} from "./agents.js";
|
|
7
14
|
|
|
8
|
-
|
|
15
|
+
interface AgentDiscoveryScopeSnapshot {
|
|
16
|
+
markdownFiles: string[];
|
|
17
|
+
fileHashes: Record<string, string | null>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type AgentDiscoverySnapshots = Record<AgentSource, AgentDiscoveryScopeSnapshot>;
|
|
21
|
+
|
|
22
|
+
export type AgentDiscoveryCacheEntry = AgentDiscoveryResult & {
|
|
23
|
+
ts: number;
|
|
24
|
+
snapshots?: AgentDiscoverySnapshots;
|
|
25
|
+
};
|
|
9
26
|
export type AgentDiscoveryCache = Map<string, AgentDiscoveryCacheEntry>;
|
|
10
27
|
export const AGENT_DISCOVERY_CACHE_TTL_MS = 300_000;
|
|
11
28
|
const sharedAgentDiscoveryCache: AgentDiscoveryCache = new Map();
|
|
29
|
+
const AGENT_SOURCES = ["user", "project"] as const;
|
|
30
|
+
|
|
31
|
+
interface CacheOperationContext {
|
|
32
|
+
cwd: string;
|
|
33
|
+
cache: AgentDiscoveryCache;
|
|
34
|
+
ts: number;
|
|
35
|
+
cacheTtlMs: number;
|
|
36
|
+
}
|
|
12
37
|
|
|
13
38
|
export function resetAgentDiscoveryCache(): void {
|
|
14
39
|
sharedAgentDiscoveryCache.clear();
|
|
15
40
|
}
|
|
16
41
|
|
|
42
|
+
function cacheKey(cwd: string, scope: AgentScope): string {
|
|
43
|
+
return `${path.resolve(cwd)}\0${scope}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getFreshCacheEntry(
|
|
47
|
+
ctx: CacheOperationContext,
|
|
48
|
+
key: string,
|
|
49
|
+
): AgentDiscoveryCacheEntry | undefined {
|
|
50
|
+
const entry = ctx.cache.get(key);
|
|
51
|
+
return entry && ctx.ts - entry.ts <= ctx.cacheTtlMs ? entry : undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function emptySnapshot(): AgentDiscoveryScopeSnapshot {
|
|
55
|
+
return { markdownFiles: [], fileHashes: {} };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function cloneScopeSnapshot(
|
|
59
|
+
snapshot: AgentDiscoveryScopeSnapshot | undefined,
|
|
60
|
+
): AgentDiscoveryScopeSnapshot {
|
|
61
|
+
if (!snapshot) return emptySnapshot();
|
|
62
|
+
return {
|
|
63
|
+
markdownFiles: [...snapshot.markdownFiles],
|
|
64
|
+
fileHashes: { ...snapshot.fileHashes },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function equalStringSets(a: string[], b: string[]): boolean {
|
|
69
|
+
if (a.length !== b.length) return false;
|
|
70
|
+
const set = new Set(a);
|
|
71
|
+
return b.every((value) => set.has(value));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function snapshotsEqual(
|
|
75
|
+
left: AgentDiscoveryScopeSnapshot,
|
|
76
|
+
right: AgentDiscoveryScopeSnapshot,
|
|
77
|
+
): boolean {
|
|
78
|
+
if (!equalStringSets(left.markdownFiles, right.markdownFiles)) return false;
|
|
79
|
+
return left.markdownFiles.every(
|
|
80
|
+
(fileName) => left.fileHashes[fileName] === right.fileHashes[fileName],
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function scopeAgentsMatchListing(
|
|
85
|
+
scopeResult: AgentDiscoveryScopeResult,
|
|
86
|
+
source: AgentSource,
|
|
87
|
+
dir: string | null,
|
|
88
|
+
): boolean {
|
|
89
|
+
if (!dir)
|
|
90
|
+
return (
|
|
91
|
+
scopeResult.agents.length === 0 && scopeResult.markdownFiles.length === 0
|
|
92
|
+
);
|
|
93
|
+
const listedFiles = new Set(scopeResult.markdownFiles);
|
|
94
|
+
const resolvedDir = path.resolve(dir);
|
|
95
|
+
return scopeResult.agents.every((agent) => {
|
|
96
|
+
const fileName = path.basename(agent.filePath);
|
|
97
|
+
return (
|
|
98
|
+
agent.source === source &&
|
|
99
|
+
listedFiles.has(fileName) &&
|
|
100
|
+
path.resolve(path.dirname(agent.filePath)) === resolvedDir
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function hashMarkdownFileAsync(
|
|
106
|
+
dir: string,
|
|
107
|
+
fileName: string,
|
|
108
|
+
): Promise<string | null> {
|
|
109
|
+
try {
|
|
110
|
+
return createHash("sha256")
|
|
111
|
+
.update(await fsPromises.readFile(path.join(dir, fileName)))
|
|
112
|
+
.digest("hex");
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function buildScopeSnapshotAsync(
|
|
119
|
+
dir: string | null,
|
|
120
|
+
): Promise<AgentDiscoveryScopeSnapshot> {
|
|
121
|
+
if (!dir) return emptySnapshot();
|
|
122
|
+
const entries = await readMarkdownDirEntriesAsync(dir);
|
|
123
|
+
const markdownFiles = entries.map((entry) => entry.name);
|
|
124
|
+
const hashPairs = await Promise.all(
|
|
125
|
+
markdownFiles.map(
|
|
126
|
+
async (fileName) =>
|
|
127
|
+
[fileName, await hashMarkdownFileAsync(dir, fileName)] as const,
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
return { markdownFiles, fileHashes: Object.fromEntries(hashPairs) };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function buildCacheSnapshotsAsync(
|
|
134
|
+
discovery: AgentDiscoveryResult,
|
|
135
|
+
): Promise<AgentDiscoverySnapshots> {
|
|
136
|
+
const [user, project] = await Promise.all([
|
|
137
|
+
buildScopeSnapshotAsync(getUserAgentsDir()),
|
|
138
|
+
buildScopeSnapshotAsync(discovery.projectAgentsDir),
|
|
139
|
+
]);
|
|
140
|
+
return { user, project };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function canTrustDerivedScopeAsync(
|
|
144
|
+
source: AgentSource,
|
|
145
|
+
bothEntry: AgentDiscoveryCacheEntry,
|
|
146
|
+
): Promise<boolean> {
|
|
147
|
+
const dir =
|
|
148
|
+
source === "user" ? getUserAgentsDir() : bothEntry.projectAgentsDir;
|
|
149
|
+
const scopeResult = bothEntry.scopes[source];
|
|
150
|
+
const cachedSnapshot = bothEntry.snapshots?.[source];
|
|
151
|
+
if (!cachedSnapshot) return false;
|
|
152
|
+
if (!scopeAgentsMatchListing(scopeResult, source, dir)) return false;
|
|
153
|
+
if (!equalStringSets(scopeResult.markdownFiles, cachedSnapshot.markdownFiles))
|
|
154
|
+
return false;
|
|
155
|
+
return snapshotsEqual(cachedSnapshot, await buildScopeSnapshotAsync(dir));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function buildSourceRecord<T>(
|
|
159
|
+
source: AgentSource,
|
|
160
|
+
value: T,
|
|
161
|
+
empty: () => T,
|
|
162
|
+
): Record<AgentSource, T> {
|
|
163
|
+
const record: Record<AgentSource, T> = { user: empty(), project: empty() };
|
|
164
|
+
record[source] = value;
|
|
165
|
+
return record;
|
|
166
|
+
}
|
|
167
|
+
function createDerivedCacheEntry(
|
|
168
|
+
bothEntry: AgentDiscoveryCacheEntry,
|
|
169
|
+
source: AgentSource,
|
|
170
|
+
): AgentDiscoveryCacheEntry {
|
|
171
|
+
const { agents, markdownFiles } = bothEntry.scopes[source];
|
|
172
|
+
const clonedScope: AgentDiscoveryScopeResult = {
|
|
173
|
+
agents: [...agents],
|
|
174
|
+
markdownFiles: [...markdownFiles],
|
|
175
|
+
};
|
|
176
|
+
return {
|
|
177
|
+
agents: clonedScope.agents,
|
|
178
|
+
projectAgentsDir: bothEntry.projectAgentsDir,
|
|
179
|
+
scopes: buildSourceRecord(source, clonedScope, emptyScopeResult),
|
|
180
|
+
ts: bothEntry.ts,
|
|
181
|
+
snapshots: buildSourceRecord(
|
|
182
|
+
source,
|
|
183
|
+
cloneScopeSnapshot(bothEntry.snapshots?.[source]),
|
|
184
|
+
emptySnapshot,
|
|
185
|
+
),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function discoverAndCacheAsync(
|
|
190
|
+
ctx: CacheOperationContext,
|
|
191
|
+
scope: AgentScope,
|
|
192
|
+
): Promise<AgentDiscoveryCacheEntry> {
|
|
193
|
+
const discovery = await discoverAgentsAsync(ctx.cwd, scope);
|
|
194
|
+
const entry: AgentDiscoveryCacheEntry = {
|
|
195
|
+
...discovery,
|
|
196
|
+
ts: ctx.ts,
|
|
197
|
+
snapshots: await buildCacheSnapshotsAsync(discovery),
|
|
198
|
+
};
|
|
199
|
+
ctx.cache.set(cacheKey(ctx.cwd, scope), entry);
|
|
200
|
+
if (scope === "both") {
|
|
201
|
+
await primeScopedCacheEntriesAsync(ctx, entry);
|
|
202
|
+
}
|
|
203
|
+
return entry;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function deriveOrDiscoverScopedEntryAsync(
|
|
207
|
+
ctx: CacheOperationContext,
|
|
208
|
+
source: AgentSource,
|
|
209
|
+
bothEntry: AgentDiscoveryCacheEntry,
|
|
210
|
+
): Promise<AgentDiscoveryCacheEntry> {
|
|
211
|
+
if (await canTrustDerivedScopeAsync(source, bothEntry)) {
|
|
212
|
+
const entry = createDerivedCacheEntry(bothEntry, source);
|
|
213
|
+
ctx.cache.set(cacheKey(ctx.cwd, source), entry);
|
|
214
|
+
return entry;
|
|
215
|
+
}
|
|
216
|
+
return discoverAndCacheAsync(ctx, source);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function primeScopedCacheEntriesAsync(
|
|
220
|
+
ctx: CacheOperationContext,
|
|
221
|
+
bothEntry: AgentDiscoveryCacheEntry,
|
|
222
|
+
): Promise<void> {
|
|
223
|
+
for (const source of AGENT_SOURCES) {
|
|
224
|
+
const scopedKey = cacheKey(ctx.cwd, source);
|
|
225
|
+
if (getFreshCacheEntry(ctx, scopedKey)) continue;
|
|
226
|
+
await deriveOrDiscoverScopedEntryAsync(ctx, source, bothEntry);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
17
230
|
export async function getCachedAgentDiscovery(
|
|
18
231
|
cwd: string,
|
|
19
232
|
scope: AgentScope,
|
|
20
233
|
cache: AgentDiscoveryCache = sharedAgentDiscoveryCache,
|
|
21
234
|
cacheTtlMs = AGENT_DISCOVERY_CACHE_TTL_MS,
|
|
22
235
|
): Promise<AgentDiscoveryCacheEntry> {
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const entry =
|
|
26
|
-
if (entry
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
236
|
+
const ctx: CacheOperationContext = { cwd, cache, ts: Date.now(), cacheTtlMs };
|
|
237
|
+
const key = cacheKey(cwd, scope);
|
|
238
|
+
const entry = getFreshCacheEntry(ctx, key);
|
|
239
|
+
if (entry) return entry;
|
|
240
|
+
if (scope !== "both") {
|
|
241
|
+
const bothEntry = getFreshCacheEntry(ctx, cacheKey(cwd, "both"));
|
|
242
|
+
if (bothEntry) {
|
|
243
|
+
return deriveOrDiscoverScopedEntryAsync(ctx, scope, bothEntry);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return discoverAndCacheAsync(ctx, scope);
|
|
30
247
|
}
|
|
31
248
|
|
|
32
249
|
export async function getCachedAgentCompletions(
|
package/src/agent/agents.ts
CHANGED
|
@@ -1,21 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agent discovery and configuration
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
1
|
import type { Dirent } from "node:fs";
|
|
6
2
|
import * as fsPromises from "node:fs/promises";
|
|
7
3
|
import * as path from "node:path";
|
|
8
4
|
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
9
5
|
|
|
10
|
-
export type
|
|
11
|
-
|
|
12
|
-
export type ThinkingLevel =
|
|
13
|
-
| "off"
|
|
14
|
-
| "minimal"
|
|
15
|
-
| "low"
|
|
16
|
-
| "medium"
|
|
17
|
-
| "high"
|
|
18
|
-
| "xhigh";
|
|
6
|
+
export type AgentSource = "user" | "project";
|
|
7
|
+
export type AgentScope = AgentSource | "both";
|
|
19
8
|
|
|
20
9
|
const THINKING_LEVELS = [
|
|
21
10
|
"off",
|
|
@@ -26,6 +15,8 @@ const THINKING_LEVELS = [
|
|
|
26
15
|
"xhigh",
|
|
27
16
|
] as const;
|
|
28
17
|
|
|
18
|
+
export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
19
|
+
|
|
29
20
|
export interface AgentConfig {
|
|
30
21
|
name: string;
|
|
31
22
|
description: string;
|
|
@@ -33,24 +24,37 @@ export interface AgentConfig {
|
|
|
33
24
|
skills?: string[];
|
|
34
25
|
thinking?: ThinkingLevel;
|
|
35
26
|
systemPrompt: string;
|
|
36
|
-
source:
|
|
27
|
+
source: AgentSource;
|
|
37
28
|
filePath: string;
|
|
38
29
|
}
|
|
39
30
|
|
|
31
|
+
export interface AgentDiscoveryScopeResult {
|
|
32
|
+
agents: AgentConfig[];
|
|
33
|
+
markdownFiles: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
40
36
|
export interface AgentDiscoveryResult {
|
|
41
37
|
agents: AgentConfig[];
|
|
42
38
|
projectAgentsDir: string | null;
|
|
39
|
+
scopes: Record<AgentSource, AgentDiscoveryScopeResult>;
|
|
43
40
|
}
|
|
44
41
|
|
|
45
42
|
function mergeAgentLists(
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
userResult: AgentDiscoveryScopeResult,
|
|
44
|
+
projectResult: AgentDiscoveryScopeResult,
|
|
48
45
|
projectAgentsDir: string | null,
|
|
49
46
|
): AgentDiscoveryResult {
|
|
50
47
|
const agentMap = new Map<string, AgentConfig>();
|
|
51
|
-
for (const agent of
|
|
52
|
-
for (const agent of
|
|
53
|
-
return {
|
|
48
|
+
for (const agent of userResult.agents) agentMap.set(agent.name, agent);
|
|
49
|
+
for (const agent of projectResult.agents) agentMap.set(agent.name, agent);
|
|
50
|
+
return {
|
|
51
|
+
agents: Array.from(agentMap.values()),
|
|
52
|
+
projectAgentsDir,
|
|
53
|
+
scopes: {
|
|
54
|
+
user: userResult,
|
|
55
|
+
project: projectResult,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
function parseCommaList(raw: unknown): string[] | undefined {
|
|
@@ -72,7 +76,7 @@ function parseThinkingLevel(raw: unknown): ThinkingLevel | undefined {
|
|
|
72
76
|
|
|
73
77
|
function parseAgentConfig(
|
|
74
78
|
content: string,
|
|
75
|
-
source:
|
|
79
|
+
source: AgentSource,
|
|
76
80
|
filePath: string,
|
|
77
81
|
): AgentConfig | null {
|
|
78
82
|
let parsed: ReturnType<typeof parseFrontmatter<Record<string, unknown>>>;
|
|
@@ -116,35 +120,65 @@ function parseAgentConfig(
|
|
|
116
120
|
};
|
|
117
121
|
}
|
|
118
122
|
|
|
119
|
-
async function
|
|
123
|
+
async function loadAgentEntryAsync(
|
|
120
124
|
dir: string,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
let
|
|
125
|
+
entryName: string,
|
|
126
|
+
source: AgentSource,
|
|
127
|
+
): Promise<AgentConfig | null> {
|
|
128
|
+
const filePath = path.join(dir, entryName);
|
|
129
|
+
let content: string;
|
|
126
130
|
try {
|
|
127
|
-
|
|
131
|
+
content = await fsPromises.readFile(filePath, "utf-8");
|
|
128
132
|
} catch {
|
|
129
|
-
return
|
|
133
|
+
return null;
|
|
130
134
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
135
|
+
return parseAgentConfig(content, source, filePath);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function emptyScopeResult(): AgentDiscoveryScopeResult {
|
|
139
|
+
return { agents: [], markdownFiles: [] };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function loadAgentsFromDirAsync(
|
|
143
|
+
dir: string,
|
|
144
|
+
source: AgentSource,
|
|
145
|
+
): Promise<AgentDiscoveryScopeResult> {
|
|
146
|
+
const markdownEntries = await readMarkdownDirEntriesAsync(dir);
|
|
147
|
+
const markdownFiles = markdownEntries.map((entry) => entry.name);
|
|
148
|
+
const parsedAgents = await Promise.all(
|
|
149
|
+
markdownEntries.map((entry) =>
|
|
150
|
+
loadAgentEntryAsync(dir, entry.name, source),
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
const agents = parsedAgents.filter(
|
|
154
|
+
(agent): agent is AgentConfig => agent !== null,
|
|
155
|
+
);
|
|
156
|
+
return { agents, markdownFiles };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function isMarkdownDirent(entry: Dirent): boolean {
|
|
160
|
+
return (
|
|
161
|
+
entry.name.endsWith(".md") && (entry.isFile() || entry.isSymbolicLink())
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function readMarkdownDirEntriesAsync(
|
|
166
|
+
dir: string | null,
|
|
167
|
+
): Promise<Dirent[]> {
|
|
168
|
+
if (!dir) return [];
|
|
169
|
+
try {
|
|
170
|
+
const entries = await fsPromises.readdir(dir, { withFileTypes: true });
|
|
171
|
+
return entries.filter(isMarkdownDirent);
|
|
172
|
+
} catch {
|
|
173
|
+
return [];
|
|
143
174
|
}
|
|
144
|
-
return agents;
|
|
145
175
|
}
|
|
146
176
|
|
|
147
|
-
|
|
177
|
+
export function getUserAgentsDir(): string {
|
|
178
|
+
return path.join(getAgentDir(), "agents");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function isDirectoryAsync(p: string): Promise<boolean> {
|
|
148
182
|
try {
|
|
149
183
|
return (await fsPromises.stat(p)).isDirectory();
|
|
150
184
|
} catch {
|
|
@@ -169,15 +203,17 @@ export async function discoverAgentsAsync(
|
|
|
169
203
|
cwd: string,
|
|
170
204
|
scope: AgentScope,
|
|
171
205
|
): Promise<AgentDiscoveryResult> {
|
|
172
|
-
const userDir =
|
|
206
|
+
const userDir = getUserAgentsDir();
|
|
173
207
|
const projectAgentsDir = await findNearestProjectAgentsDirAsync(cwd);
|
|
174
|
-
const [
|
|
175
|
-
scope === "project"
|
|
208
|
+
const [userDiscovery, projectDiscovery] = await Promise.all([
|
|
209
|
+
scope === "project"
|
|
210
|
+
? emptyScopeResult()
|
|
211
|
+
: loadAgentsFromDirAsync(userDir, "user"),
|
|
176
212
|
scope === "user" || !projectAgentsDir
|
|
177
|
-
?
|
|
213
|
+
? emptyScopeResult()
|
|
178
214
|
: loadAgentsFromDirAsync(projectAgentsDir, "project"),
|
|
179
215
|
]);
|
|
180
|
-
return mergeAgentLists(
|
|
216
|
+
return mergeAgentLists(userDiscovery, projectDiscovery, projectAgentsDir);
|
|
181
217
|
}
|
|
182
218
|
|
|
183
219
|
export function formatAgentList(
|
|
@@ -1,14 +1,87 @@
|
|
|
1
|
-
|
|
1
|
+
import { makeToolPreview } from "../output/normalize.js";
|
|
2
|
+
import type { ToolActivity } from "../shared/types.js";
|
|
3
|
+
|
|
4
|
+
// Extracts results[0] from details; null-safe for malformed input.
|
|
5
|
+
function tryFirstResult(details: unknown): Record<string, unknown> | null {
|
|
6
|
+
try {
|
|
7
|
+
if (typeof details !== "object" || details === null) return null;
|
|
8
|
+
const results = (details as Record<string, unknown>).results;
|
|
9
|
+
if (!Array.isArray(results) || results.length === 0) return null;
|
|
10
|
+
const nested = results[0];
|
|
11
|
+
if (typeof nested !== "object" || nested === null) return null;
|
|
12
|
+
return nested as Record<string, unknown>;
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Malformed details (null result) falls back to { toolName }.
|
|
19
|
+
function parseToolActivity(
|
|
20
|
+
toolName: string,
|
|
21
|
+
partialResult: { content?: unknown; details?: unknown },
|
|
22
|
+
): ToolActivity {
|
|
23
|
+
const nestedRecord = tryFirstResult(partialResult.details);
|
|
24
|
+
if (!nestedRecord) return { toolName, inputSummary: toolName };
|
|
25
|
+
const isSubagent = toolName === "subagent";
|
|
26
|
+
const activity: ToolActivity = { toolName };
|
|
27
|
+
const agent = typeof nestedRecord.agent === "string" && nestedRecord.agent;
|
|
28
|
+
activity.inputSummary =
|
|
29
|
+
isSubagent && agent ? makeToolPreview(toolName, nestedRecord) : toolName;
|
|
30
|
+
if (
|
|
31
|
+
typeof nestedRecord.instanceName === "string" &&
|
|
32
|
+
nestedRecord.instanceName
|
|
33
|
+
) {
|
|
34
|
+
activity.instanceName = nestedRecord.instanceName;
|
|
35
|
+
}
|
|
36
|
+
const progress = nestedRecord.progress;
|
|
37
|
+
if (typeof progress === "object" && progress !== null) {
|
|
38
|
+
const activeToolActivity = (progress as Record<string, unknown>)
|
|
39
|
+
.activeToolActivity;
|
|
40
|
+
if (
|
|
41
|
+
typeof activeToolActivity === "object" &&
|
|
42
|
+
activeToolActivity !== null &&
|
|
43
|
+
typeof (activeToolActivity as Record<string, unknown>).toolName ===
|
|
44
|
+
"string"
|
|
45
|
+
) {
|
|
46
|
+
const childActivity = activeToolActivity as ToolActivity;
|
|
47
|
+
activity.child = childActivity;
|
|
48
|
+
// Subagent delegates inputSummary to its own agent name, not child
|
|
49
|
+
if (
|
|
50
|
+
!isSubagent &&
|
|
51
|
+
typeof childActivity.inputSummary === "string" &&
|
|
52
|
+
childActivity.inputSummary
|
|
53
|
+
) {
|
|
54
|
+
activity.inputSummary = childActivity.inputSummary;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return activity;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type ChildKnownEvent =
|
|
2
62
|
| { type: "message_end"; message: unknown }
|
|
3
63
|
| { type: "tool_result_end"; message: unknown }
|
|
4
|
-
| { type: "agent_end"; messages?: unknown; stopReason?: string }
|
|
64
|
+
| { type: "agent_end"; messages?: unknown; stopReason?: string }
|
|
65
|
+
| {
|
|
66
|
+
type: "tool_execution_update";
|
|
67
|
+
toolName: string;
|
|
68
|
+
partialResult: { content?: unknown; details?: unknown };
|
|
69
|
+
toolActivity: ToolActivity;
|
|
70
|
+
};
|
|
5
71
|
|
|
6
72
|
export type ChildEventParseResult =
|
|
7
73
|
| { kind: "known"; event: ChildKnownEvent }
|
|
8
74
|
| { kind: "unknown"; event: unknown }
|
|
9
75
|
| { kind: "invalid"; line: string };
|
|
10
76
|
|
|
11
|
-
const
|
|
77
|
+
export const TOOL_EXECUTION_UPDATE_EVENT = "tool_execution_update" as const;
|
|
78
|
+
|
|
79
|
+
const KNOWN_TYPES = new Set([
|
|
80
|
+
"message_end",
|
|
81
|
+
"tool_result_end",
|
|
82
|
+
"agent_end",
|
|
83
|
+
TOOL_EXECUTION_UPDATE_EVENT,
|
|
84
|
+
]);
|
|
12
85
|
|
|
13
86
|
export function parseChildEventLine(line: string): ChildEventParseResult {
|
|
14
87
|
if (typeof line !== "string" || !line.trim())
|
|
@@ -26,6 +99,20 @@ export function parseChildEventLine(line: string): ChildEventParseResult {
|
|
|
26
99
|
typeof (event as Record<string, unknown>).type === "string" &&
|
|
27
100
|
KNOWN_TYPES.has((event as Record<string, unknown>).type as string)
|
|
28
101
|
) {
|
|
102
|
+
const record = event as Record<string, unknown>;
|
|
103
|
+
if (record.type === TOOL_EXECUTION_UPDATE_EVENT) {
|
|
104
|
+
if (
|
|
105
|
+
typeof record.toolName !== "string" ||
|
|
106
|
+
typeof record.partialResult !== "object" ||
|
|
107
|
+
record.partialResult === null
|
|
108
|
+
) {
|
|
109
|
+
return { kind: "unknown", event };
|
|
110
|
+
}
|
|
111
|
+
record.toolActivity = parseToolActivity(
|
|
112
|
+
record.toolName as string,
|
|
113
|
+
record.partialResult as { content?: unknown; details?: unknown },
|
|
114
|
+
);
|
|
115
|
+
}
|
|
29
116
|
return { kind: "known", event: event as ChildKnownEvent };
|
|
30
117
|
}
|
|
31
118
|
return { kind: "unknown", event };
|