@pi-unipi/subagents 0.1.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/dist/agent-manager.d.ts +69 -0
- package/dist/agent-manager.d.ts.map +1 -0
- package/dist/agent-manager.js +240 -0
- package/dist/agent-manager.js.map +1 -0
- package/dist/agent-runner.d.ts +50 -0
- package/dist/agent-runner.d.ts.map +1 -0
- package/dist/agent-runner.js +238 -0
- package/dist/agent-runner.js.map +1 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +115 -0
- package/dist/config.js.map +1 -0
- package/dist/custom-agents.d.ts +14 -0
- package/dist/custom-agents.d.ts.map +1 -0
- package/dist/custom-agents.js +94 -0
- package/dist/custom-agents.js.map +1 -0
- package/dist/file-lock.d.ts +42 -0
- package/dist/file-lock.d.ts.map +1 -0
- package/dist/file-lock.js +91 -0
- package/dist/file-lock.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +270 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts.d.ts +13 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +31 -0
- package/dist/prompts.js.map +1 -0
- package/dist/types.d.ts +79 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/widget.d.ts +22 -0
- package/dist/widget.d.ts.map +1 -0
- package/dist/widget.js +108 -0
- package/dist/widget.js.map +1 -0
- package/package.json +30 -0
- package/src/agent-manager.ts +302 -0
- package/src/agent-runner.ts +306 -0
- package/src/config.ts +128 -0
- package/src/custom-agents.ts +106 -0
- package/src/file-lock.ts +102 -0
- package/src/index.ts +323 -0
- package/src/prompts.ts +39 -0
- package/src/skills/explore/SKILL.md +32 -0
- package/src/skills/work/SKILL.md +40 -0
- package/src/types.ts +86 -0
- package/src/widget.ts +123 -0
- package/tsconfig.json +19 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/subagents — Extension entry
|
|
3
|
+
*
|
|
4
|
+
* Tools: Agent, get_result
|
|
5
|
+
* ESC propagation: all children abort on parent ESC
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { defineTool, type ExtensionAPI, type ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
10
|
+
import { Type } from "@sinclair/typebox";
|
|
11
|
+
import { AgentManager } from "./agent-manager.js";
|
|
12
|
+
import { initConfig, saveGlobalConfig } from "./config.js";
|
|
13
|
+
import { type AgentActivity, type AgentRecord, BUILTIN_TYPES } from "./types.js";
|
|
14
|
+
import { AgentWidget } from "./widget.js";
|
|
15
|
+
|
|
16
|
+
/** Format tokens safely. */
|
|
17
|
+
function safeFormatTokens(session: any): string {
|
|
18
|
+
if (!session) return "";
|
|
19
|
+
try {
|
|
20
|
+
const stats = session.getSessionStats();
|
|
21
|
+
const total = stats.tokens?.total ?? 0;
|
|
22
|
+
if (total >= 1_000_000) return `${(total / 1_000_000).toFixed(1)}M`;
|
|
23
|
+
if (total >= 1_000) return `${(total / 1_000).toFixed(1)}k`;
|
|
24
|
+
return `${total}`;
|
|
25
|
+
} catch {
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Build result text. */
|
|
31
|
+
function textResult(msg: string, details?: any) {
|
|
32
|
+
return { content: [{ type: "text" as const, text: msg }], details };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default function (pi: ExtensionAPI) {
|
|
36
|
+
// Initialize config
|
|
37
|
+
const config = initConfig(process.cwd());
|
|
38
|
+
if (!config.enabled) return;
|
|
39
|
+
|
|
40
|
+
// Activity tracking for widget
|
|
41
|
+
const agentActivity = new Map<string, AgentActivity>();
|
|
42
|
+
|
|
43
|
+
// Create manager with completion callback
|
|
44
|
+
const manager = new AgentManager(
|
|
45
|
+
(record) => {
|
|
46
|
+
// On complete: clean up activity, emit event
|
|
47
|
+
agentActivity.delete(record.id);
|
|
48
|
+
widget.markFinished(record.id);
|
|
49
|
+
widget.update();
|
|
50
|
+
|
|
51
|
+
pi.events.emit("subagents:completed", {
|
|
52
|
+
id: record.id,
|
|
53
|
+
type: record.type,
|
|
54
|
+
description: record.description,
|
|
55
|
+
status: record.status,
|
|
56
|
+
result: record.result,
|
|
57
|
+
error: record.error,
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
config.maxConcurrent,
|
|
61
|
+
(record) => {
|
|
62
|
+
// On start: emit event
|
|
63
|
+
pi.events.emit("subagents:started", {
|
|
64
|
+
id: record.id,
|
|
65
|
+
type: record.type,
|
|
66
|
+
description: record.description,
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Create widget
|
|
72
|
+
const widget = new AgentWidget(manager, agentActivity);
|
|
73
|
+
|
|
74
|
+
// ESC propagation: abort all agents on session shutdown
|
|
75
|
+
pi.on("session_shutdown", async () => {
|
|
76
|
+
manager.abortAll();
|
|
77
|
+
manager.dispose();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Wire UI context for widget
|
|
81
|
+
pi.on("tool_execution_start", async (_event, ctx) => {
|
|
82
|
+
widget.setUICtx(ctx.ui);
|
|
83
|
+
widget.update();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Create activity tracker
|
|
87
|
+
function createActivityTracker(maxTurns?: number) {
|
|
88
|
+
const state: AgentActivity = {
|
|
89
|
+
activeTools: new Map(),
|
|
90
|
+
toolUses: 0,
|
|
91
|
+
turnCount: 1,
|
|
92
|
+
maxTurns,
|
|
93
|
+
tokens: "",
|
|
94
|
+
responseText: "",
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const callbacks = {
|
|
98
|
+
onToolActivity: (activity: { type: "start" | "end"; toolName: string }) => {
|
|
99
|
+
if (activity.type === "start") {
|
|
100
|
+
state.activeTools.set(activity.toolName + "_" + Date.now(), activity.toolName);
|
|
101
|
+
} else {
|
|
102
|
+
for (const [key, name] of state.activeTools) {
|
|
103
|
+
if (name === activity.toolName) {
|
|
104
|
+
state.activeTools.delete(key);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
state.toolUses++;
|
|
109
|
+
}
|
|
110
|
+
widget.update();
|
|
111
|
+
},
|
|
112
|
+
onTextDelta: (_delta: string, fullText: string) => {
|
|
113
|
+
state.responseText = fullText;
|
|
114
|
+
widget.update();
|
|
115
|
+
},
|
|
116
|
+
onTurnEnd: (turnCount: number) => {
|
|
117
|
+
state.turnCount = turnCount;
|
|
118
|
+
widget.update();
|
|
119
|
+
},
|
|
120
|
+
onSessionCreated: (session: any) => {
|
|
121
|
+
state.session = session;
|
|
122
|
+
state.tokens = safeFormatTokens(session);
|
|
123
|
+
widget.update();
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return { state, callbacks };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---- Agent tool ----
|
|
131
|
+
|
|
132
|
+
const builtinTypes = BUILTIN_TYPES.join(", ");
|
|
133
|
+
|
|
134
|
+
pi.registerTool(
|
|
135
|
+
defineTool({
|
|
136
|
+
name: "Agent",
|
|
137
|
+
label: "Agent",
|
|
138
|
+
description: `Launch a sub-agent for parallel work.
|
|
139
|
+
|
|
140
|
+
Available agent types: ${builtinTypes}
|
|
141
|
+
Custom types can be defined in .unipi/config/agents/<name>.md
|
|
142
|
+
|
|
143
|
+
Guidelines:
|
|
144
|
+
- Use "explore" for parallel file reads
|
|
145
|
+
- Use "work" for parallel file writes (transparent locking)
|
|
146
|
+
- Use run_in_background for work you don't need immediately
|
|
147
|
+
- ESC kills all running agents immediately
|
|
148
|
+
- Agents inherit the parent model by default`,
|
|
149
|
+
parameters: Type.Object({
|
|
150
|
+
type: Type.String({
|
|
151
|
+
description: `Agent type: ${builtinTypes}, or custom type from .unipc/config/agents/*.md`,
|
|
152
|
+
}),
|
|
153
|
+
prompt: Type.String({
|
|
154
|
+
description: "The task for the agent to perform.",
|
|
155
|
+
}),
|
|
156
|
+
description: Type.String({
|
|
157
|
+
description: "A short (3-5 word) description of the task.",
|
|
158
|
+
}),
|
|
159
|
+
run_in_background: Type.Optional(
|
|
160
|
+
Type.Boolean({
|
|
161
|
+
description: "Run in background. Returns agent ID immediately.",
|
|
162
|
+
}),
|
|
163
|
+
),
|
|
164
|
+
max_turns: Type.Optional(
|
|
165
|
+
Type.Number({
|
|
166
|
+
description: "Max agentic turns before stopping.",
|
|
167
|
+
minimum: 1,
|
|
168
|
+
}),
|
|
169
|
+
),
|
|
170
|
+
}),
|
|
171
|
+
|
|
172
|
+
execute: async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
173
|
+
widget.setUICtx(ctx.ui);
|
|
174
|
+
|
|
175
|
+
const type = params.type as string;
|
|
176
|
+
const prompt = params.prompt as string;
|
|
177
|
+
const description = params.description as string;
|
|
178
|
+
const runInBackground = params.run_in_background as boolean | undefined;
|
|
179
|
+
const maxTurns = params.max_turns as number | undefined;
|
|
180
|
+
|
|
181
|
+
// Create activity tracker
|
|
182
|
+
const { state: bgState, callbacks: bgCallbacks } = createActivityTracker(maxTurns);
|
|
183
|
+
|
|
184
|
+
if (runInBackground) {
|
|
185
|
+
// Background execution
|
|
186
|
+
const id = manager.spawn(pi, ctx, type, prompt, {
|
|
187
|
+
description,
|
|
188
|
+
maxTurns,
|
|
189
|
+
isBackground: true,
|
|
190
|
+
...bgCallbacks,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
agentActivity.set(id, bgState);
|
|
194
|
+
widget.ensureTimer();
|
|
195
|
+
widget.update();
|
|
196
|
+
|
|
197
|
+
const record = manager.getRecord(id);
|
|
198
|
+
const isQueued = record?.status === "queued";
|
|
199
|
+
|
|
200
|
+
return textResult(
|
|
201
|
+
`Agent ${isQueued ? "queued" : "started"} in background.\n` +
|
|
202
|
+
`ID: ${id}\n` +
|
|
203
|
+
`Type: ${type}\n` +
|
|
204
|
+
`Description: ${description}\n` +
|
|
205
|
+
(isQueued ? `Position: queued (max ${manager.getMaxConcurrent()} concurrent)\n` : "") +
|
|
206
|
+
`\nYou will be notified when this agent completes.\n` +
|
|
207
|
+
`Use get_result to retrieve full results.`,
|
|
208
|
+
{ status: "background", agentId: id },
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Foreground execution
|
|
213
|
+
let spinnerFrame = 0;
|
|
214
|
+
const startedAt = Date.now();
|
|
215
|
+
let fgId: string | undefined;
|
|
216
|
+
|
|
217
|
+
const streamUpdate = () => {
|
|
218
|
+
onUpdate?.({
|
|
219
|
+
content: [{ type: "text", text: `${bgState.toolUses} tool uses...` }],
|
|
220
|
+
details: {
|
|
221
|
+
status: "running",
|
|
222
|
+
toolUses: bgState.toolUses,
|
|
223
|
+
tokens: bgState.tokens,
|
|
224
|
+
turnCount: bgState.turnCount,
|
|
225
|
+
maxTurns: bgState.maxTurns,
|
|
226
|
+
durationMs: Date.now() - startedAt,
|
|
227
|
+
activity: bgState.responseText
|
|
228
|
+
? bgState.responseText.split("\n").pop()?.trim().slice(0, 60)
|
|
229
|
+
: "thinking…",
|
|
230
|
+
spinnerFrame: spinnerFrame % 10,
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const spinnerInterval = setInterval(() => {
|
|
236
|
+
spinnerFrame++;
|
|
237
|
+
streamUpdate();
|
|
238
|
+
}, 80);
|
|
239
|
+
|
|
240
|
+
streamUpdate();
|
|
241
|
+
|
|
242
|
+
const record = await manager.spawnAndWait(pi, ctx, type, prompt, {
|
|
243
|
+
description,
|
|
244
|
+
maxTurns,
|
|
245
|
+
...bgCallbacks,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
clearInterval(spinnerInterval);
|
|
249
|
+
|
|
250
|
+
if (fgId) {
|
|
251
|
+
agentActivity.delete(fgId);
|
|
252
|
+
widget.markFinished(fgId);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const tokenText = safeFormatTokens(bgState.session);
|
|
256
|
+
const durationMs = (record.completedAt ?? Date.now()) - record.startedAt;
|
|
257
|
+
|
|
258
|
+
if (record.status === "error") {
|
|
259
|
+
return textResult(`Agent failed: ${record.error}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return textResult(
|
|
263
|
+
`Agent completed in ${(durationMs / 1000).toFixed(1)}s (${record.toolUses} tool uses${tokenText ? `, ${tokenText} tokens` : ""}).\n\n` +
|
|
264
|
+
(record.result?.trim() || "No output."),
|
|
265
|
+
);
|
|
266
|
+
},
|
|
267
|
+
}),
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// ---- get_result tool ----
|
|
271
|
+
|
|
272
|
+
pi.registerTool(
|
|
273
|
+
defineTool({
|
|
274
|
+
name: "get_result",
|
|
275
|
+
label: "Get Agent Result",
|
|
276
|
+
description: "Check status and retrieve results from a background agent.",
|
|
277
|
+
parameters: Type.Object({
|
|
278
|
+
agent_id: Type.String({
|
|
279
|
+
description: "The agent ID to check.",
|
|
280
|
+
}),
|
|
281
|
+
wait: Type.Optional(
|
|
282
|
+
Type.Boolean({
|
|
283
|
+
description: "Wait for completion. Default: false.",
|
|
284
|
+
}),
|
|
285
|
+
),
|
|
286
|
+
}),
|
|
287
|
+
execute: async (_toolCallId, params) => {
|
|
288
|
+
const record = manager.getRecord(params.agent_id as string);
|
|
289
|
+
if (!record) {
|
|
290
|
+
return textResult(`Agent not found: "${params.agent_id}". It may have been cleaned up.`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (params.wait && record.status === "running" && record.promise) {
|
|
294
|
+
record.resultConsumed = true;
|
|
295
|
+
await record.promise;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const duration = record.completedAt
|
|
299
|
+
? `${((record.completedAt - record.startedAt) / 1000).toFixed(1)}s`
|
|
300
|
+
: "running";
|
|
301
|
+
|
|
302
|
+
let output =
|
|
303
|
+
`Agent: ${record.id}\n` +
|
|
304
|
+
`Type: ${record.type} | Status: ${record.status} | Tool uses: ${record.toolUses} | Duration: ${duration}\n` +
|
|
305
|
+
`Description: ${record.description}\n\n`;
|
|
306
|
+
|
|
307
|
+
if (record.status === "running") {
|
|
308
|
+
output += "Agent is still running. Use wait: true or check back later.";
|
|
309
|
+
} else if (record.status === "error") {
|
|
310
|
+
output += `Error: ${record.error}`;
|
|
311
|
+
} else {
|
|
312
|
+
output += record.result?.trim() || "No output.";
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (record.status !== "running" && record.status !== "queued") {
|
|
316
|
+
record.resultConsumed = true;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return textResult(output);
|
|
320
|
+
},
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
323
|
+
}
|
package/src/prompts.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/subagents — System prompt builder
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AgentConfig } from "./types.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build system prompt for an agent.
|
|
9
|
+
*/
|
|
10
|
+
export function buildAgentPrompt(
|
|
11
|
+
config: AgentConfig,
|
|
12
|
+
cwd: string,
|
|
13
|
+
env: { isGitRepo: boolean; branch: string; platform: string },
|
|
14
|
+
parentSystemPrompt: string,
|
|
15
|
+
): string {
|
|
16
|
+
if (config.promptMode === "append") {
|
|
17
|
+
// Append mode: parent prompt + agent additions
|
|
18
|
+
return [
|
|
19
|
+
parentSystemPrompt,
|
|
20
|
+
"",
|
|
21
|
+
"---",
|
|
22
|
+
"",
|
|
23
|
+
`## Agent Role: ${config.displayName ?? config.name}`,
|
|
24
|
+
config.systemPrompt,
|
|
25
|
+
].join("\n");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Replace mode: standalone prompt
|
|
29
|
+
return [
|
|
30
|
+
`# ${config.displayName ?? config.name}`,
|
|
31
|
+
"",
|
|
32
|
+
config.systemPrompt,
|
|
33
|
+
"",
|
|
34
|
+
"---",
|
|
35
|
+
"",
|
|
36
|
+
`Working directory: ${cwd}`,
|
|
37
|
+
`Git: ${env.isGitRepo ? `${env.branch} on ${env.platform}` : "not a git repo"}`,
|
|
38
|
+
].join("\n");
|
|
39
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: explore
|
|
3
|
+
description: "Fast parallel codebase exploration"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Explore Agent
|
|
7
|
+
|
|
8
|
+
Read-only agent for fast parallel codebase exploration.
|
|
9
|
+
|
|
10
|
+
## Capabilities
|
|
11
|
+
|
|
12
|
+
- Read files
|
|
13
|
+
- Search with grep, find, ls
|
|
14
|
+
- Run bash commands (read-only)
|
|
15
|
+
|
|
16
|
+
## Constraints
|
|
17
|
+
|
|
18
|
+
- Cannot write or edit files
|
|
19
|
+
- Cannot modify the codebase
|
|
20
|
+
- Report findings only
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
Spawn multiple explore agents to read different parts of the codebase in parallel.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
Agent({
|
|
28
|
+
type: "explore",
|
|
29
|
+
prompt: "Find all files related to authentication",
|
|
30
|
+
description: "Find auth files"
|
|
31
|
+
})
|
|
32
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: work
|
|
3
|
+
description: "Parallel file writes with transparent locking"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Work Agent
|
|
7
|
+
|
|
8
|
+
Read-write agent for parallel file modifications.
|
|
9
|
+
|
|
10
|
+
## Capabilities
|
|
11
|
+
|
|
12
|
+
- Read files
|
|
13
|
+
- Write and edit files
|
|
14
|
+
- Run bash commands
|
|
15
|
+
- Search with grep, find, ls
|
|
16
|
+
|
|
17
|
+
## File Locking
|
|
18
|
+
|
|
19
|
+
When writing a file, the lock is acquired automatically. If another agent holds the lock, your write waits transparently — you won't see errors.
|
|
20
|
+
|
|
21
|
+
- Per-file granularity: locking `src/auth.ts` doesn't block `src/login.ts`
|
|
22
|
+
- Locks release automatically when the write completes
|
|
23
|
+
- On abort, all locks are released
|
|
24
|
+
|
|
25
|
+
## Constraints
|
|
26
|
+
|
|
27
|
+
- Cannot spawn sub-agents (prevents nesting)
|
|
28
|
+
- Cannot modify other agents' locked files (waits instead)
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
Spawn work agents to modify different files in parallel.
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
Agent({
|
|
36
|
+
type: "work",
|
|
37
|
+
prompt: "Refactor src/auth.ts to use async/await",
|
|
38
|
+
description: "Refactor auth module"
|
|
39
|
+
})
|
|
40
|
+
```
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/subagents — Type definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
|
|
6
|
+
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
|
7
|
+
|
|
8
|
+
export type { ThinkingLevel };
|
|
9
|
+
|
|
10
|
+
/** Agent type name: built-in or user-defined. */
|
|
11
|
+
export type AgentType = string;
|
|
12
|
+
|
|
13
|
+
/** Built-in agent type names. */
|
|
14
|
+
export const BUILTIN_TYPES = ["explore", "work"] as const;
|
|
15
|
+
|
|
16
|
+
/** Memory scope for persistent agent memory. */
|
|
17
|
+
export type MemoryScope = "user" | "project" | "local";
|
|
18
|
+
|
|
19
|
+
/** Unified agent configuration. */
|
|
20
|
+
export interface AgentConfig {
|
|
21
|
+
name: string;
|
|
22
|
+
displayName?: string;
|
|
23
|
+
description: string;
|
|
24
|
+
builtinToolNames?: string[];
|
|
25
|
+
disallowedTools?: string[];
|
|
26
|
+
extensions: true | string[] | false;
|
|
27
|
+
skills: true | string[] | false;
|
|
28
|
+
model?: string;
|
|
29
|
+
thinking?: ThinkingLevel;
|
|
30
|
+
maxTurns?: number;
|
|
31
|
+
systemPrompt: string;
|
|
32
|
+
promptMode: "replace" | "append";
|
|
33
|
+
inheritContext?: boolean;
|
|
34
|
+
runInBackground?: boolean;
|
|
35
|
+
isolated?: boolean;
|
|
36
|
+
memory?: MemoryScope;
|
|
37
|
+
isDefault?: boolean;
|
|
38
|
+
enabled?: boolean;
|
|
39
|
+
source?: "builtin" | "project" | "global";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Agent record — tracks a running agent. */
|
|
43
|
+
export interface AgentRecord {
|
|
44
|
+
id: string;
|
|
45
|
+
type: AgentType;
|
|
46
|
+
description: string;
|
|
47
|
+
status: "queued" | "running" | "completed" | "aborted" | "stopped" | "error";
|
|
48
|
+
result?: string;
|
|
49
|
+
error?: string;
|
|
50
|
+
toolUses: number;
|
|
51
|
+
startedAt: number;
|
|
52
|
+
completedAt?: number;
|
|
53
|
+
session?: AgentSession;
|
|
54
|
+
abortController?: AbortController;
|
|
55
|
+
promise?: Promise<string>;
|
|
56
|
+
/** Set when result consumed via get_result — suppresses notification. */
|
|
57
|
+
resultConsumed?: boolean;
|
|
58
|
+
/** Files locked by this agent. */
|
|
59
|
+
lockedFiles: Set<string>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** File lock entry. */
|
|
63
|
+
export interface FileLockEntry {
|
|
64
|
+
agentId: string;
|
|
65
|
+
filePath: string;
|
|
66
|
+
promise: Promise<void>;
|
|
67
|
+
release: () => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Extension config. */
|
|
71
|
+
export interface SubagentsConfig {
|
|
72
|
+
maxConcurrent: number;
|
|
73
|
+
enabled: boolean;
|
|
74
|
+
types: Record<string, { enabled?: boolean }>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Agent activity for widget display. */
|
|
78
|
+
export interface AgentActivity {
|
|
79
|
+
activeTools: Map<string, string>;
|
|
80
|
+
toolUses: number;
|
|
81
|
+
turnCount: number;
|
|
82
|
+
maxTurns?: number;
|
|
83
|
+
tokens: string;
|
|
84
|
+
responseText: string;
|
|
85
|
+
session?: AgentSession;
|
|
86
|
+
}
|
package/src/widget.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/subagents — Live widget
|
|
3
|
+
*
|
|
4
|
+
* Shows running agents above the editor.
|
|
5
|
+
* Adapted from pi-subagents.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AgentManager } from "./agent-manager.js";
|
|
9
|
+
import type { AgentActivity } from "./types.js";
|
|
10
|
+
|
|
11
|
+
/** Spinner frames (braille). */
|
|
12
|
+
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
13
|
+
|
|
14
|
+
/** Format token count. */
|
|
15
|
+
function formatTokens(n: number): string {
|
|
16
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
17
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
|
|
18
|
+
return `${n}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Format duration. */
|
|
22
|
+
function formatMs(ms: number): string {
|
|
23
|
+
if (ms >= 60_000) return `${(ms / 60_000).toFixed(1)}m`;
|
|
24
|
+
if (ms >= 1_000) return `${(ms / 1_000).toFixed(1)}s`;
|
|
25
|
+
return `${ms}ms`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Format turns. */
|
|
29
|
+
function formatTurns(turn: number, max?: number): string {
|
|
30
|
+
return max ? `⟳${turn}≤${max}` : `⟳${turn}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Describe current activity from active tools. */
|
|
34
|
+
function describeActivity(activeTools: Map<string, string>, responseText: string): string {
|
|
35
|
+
if (activeTools.size > 0) {
|
|
36
|
+
const names = [...new Set(activeTools.values())];
|
|
37
|
+
return names.join(", ") + "…";
|
|
38
|
+
}
|
|
39
|
+
if (responseText) {
|
|
40
|
+
const lastLine = responseText.split("\n").pop()?.trim() ?? "";
|
|
41
|
+
if (lastLine.length > 0) return lastLine.slice(0, 60) + (lastLine.length > 60 ? "…" : "");
|
|
42
|
+
}
|
|
43
|
+
return "thinking…";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class AgentWidget {
|
|
47
|
+
private manager: AgentManager;
|
|
48
|
+
private activity: Map<string, AgentActivity>;
|
|
49
|
+
private spinnerFrame = 0;
|
|
50
|
+
private timer?: ReturnType<typeof setInterval>;
|
|
51
|
+
private uiCtx?: any;
|
|
52
|
+
|
|
53
|
+
constructor(manager: AgentManager, activity: Map<string, AgentActivity>) {
|
|
54
|
+
this.manager = manager;
|
|
55
|
+
this.activity = activity;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setUICtx(ctx: any) {
|
|
59
|
+
this.uiCtx = ctx;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
ensureTimer() {
|
|
63
|
+
if (this.timer) return;
|
|
64
|
+
this.timer = setInterval(() => {
|
|
65
|
+
this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER.length;
|
|
66
|
+
this.render();
|
|
67
|
+
}, 80);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
markFinished(_id: string) {
|
|
71
|
+
// Check if any agents still running
|
|
72
|
+
if (!this.manager.hasRunning()) {
|
|
73
|
+
if (this.timer) {
|
|
74
|
+
clearInterval(this.timer);
|
|
75
|
+
this.timer = undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
update() {
|
|
81
|
+
this.render();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private render() {
|
|
85
|
+
const agents = this.manager.listAgents();
|
|
86
|
+
const running = agents.filter((a) => a.status === "running" || a.status === "queued");
|
|
87
|
+
|
|
88
|
+
if (running.length === 0) {
|
|
89
|
+
// Clear widget
|
|
90
|
+
this.uiCtx?.setIndicator?.("");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const lines: string[] = [];
|
|
95
|
+
const frame = SPINNER[this.spinnerFrame];
|
|
96
|
+
|
|
97
|
+
for (const agent of running) {
|
|
98
|
+
const act = this.activity.get(agent.id);
|
|
99
|
+
const toolCount = agent.toolUses;
|
|
100
|
+
const tokens = act?.tokens ?? "";
|
|
101
|
+
const duration = formatMs(Date.now() - agent.startedAt);
|
|
102
|
+
const activity = act ? describeActivity(act.activeTools, act.responseText) : "starting…";
|
|
103
|
+
|
|
104
|
+
const parts: string[] = [];
|
|
105
|
+
if (act?.turnCount) parts.push(formatTurns(act.turnCount, act.maxTurns));
|
|
106
|
+
if (toolCount > 0) parts.push(`${toolCount} tool uses`);
|
|
107
|
+
if (tokens) parts.push(tokens);
|
|
108
|
+
parts.push(duration);
|
|
109
|
+
|
|
110
|
+
lines.push(
|
|
111
|
+
`${frame} ${agent.type} ${agent.description} · ${parts.join(" · ")}`,
|
|
112
|
+
` ⎿ ${activity}`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const queued = agents.filter((a) => a.status === "queued").length;
|
|
117
|
+
if (queued > 0) {
|
|
118
|
+
lines.push(` ${queued} queued`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.uiCtx?.setIndicator?.(lines.join("\n"));
|
|
122
|
+
}
|
|
123
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|