pi-subagents 0.11.7 → 0.11.9
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/CHANGELOG.md +21 -0
- package/README.md +8 -4
- package/index.ts +84 -5
- package/package.json +1 -1
- package/prompt-template-bridge.ts +114 -11
- package/render.ts +30 -0
- package/slash-bridge.ts +174 -0
- package/slash-commands.ts +187 -80
- package/slash-live-state.ts +294 -0
- package/subagent-executor.ts +15 -3
- package/types.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.11.9] - 2026-03-21
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- `/agents` overlay launches (single, chain, parallel) and slash commands (`/run`, `/chain`, `/parallel`) now render an inline result card in chat instead of relaying through `sendUserMessage`.
|
|
9
|
+
- `/agents` overlay chain launches no longer bypass the executor for async fallback, fixing a path where async chain errors were silently swallowed.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- All slash and overlay subagent execution now routes through an event bus request/response protocol (`slash-bridge.ts`), matching the pattern used by pi-prompt-template-model. This replaces both the old `sendUserMessage` relay and the direct `executeChain` call in the overlay handler.
|
|
13
|
+
- Slash launches show a live inline card immediately on start that streams current tool, recent tools, and output in real time, rather than appearing only after completion.
|
|
14
|
+
- `/parallel` now uses the native `tasks` parameter directly instead of wrapping through `{ chain: [{ parallel: tasks }] }`.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- `slash-bridge.ts` — event bus bridge for slash command execution. Manages AbortController lifecycle, cancel-before-start races, and progress streaming via `subagent:slash:*` events.
|
|
18
|
+
- `slash-live-state.ts` — request-id keyed snapshot store that drives live inline card rendering during execution and restores finalized results from session entries on reload.
|
|
19
|
+
- Clarified README Usage section to distinguish LLM tool parameters from user-facing slash commands.
|
|
20
|
+
|
|
21
|
+
## [0.11.8] - 2026-03-21
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- Prompt-template delegation bridge now supports parallel task execution: accepts `tasks` array payloads, emits per-task `parallelResults` with individual error/success states, and streams per-task progress updates with `taskProgress` entries.
|
|
25
|
+
|
|
5
26
|
## [0.11.7] - 2026-03-20
|
|
6
27
|
|
|
7
28
|
### Changed
|
package/README.md
CHANGED
|
@@ -97,9 +97,11 @@ Semantics:
|
|
|
97
97
|
|
|
98
98
|
When `extensions` is present, it takes precedence over extension paths implied by `tools` entries.
|
|
99
99
|
|
|
100
|
-
**MCP Tools**
|
|
100
|
+
**MCP Tools (optional)**
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
If you have the [pi-mcp-adapter](https://github.com/nicobailon/pi-mcp-adapter) extension installed, subagents can use MCP server tools directly. Without that extension, everything below is ignored — MCP integration is entirely optional.
|
|
103
|
+
|
|
104
|
+
Add `mcp:` prefixed entries to the `tools` field in agent frontmatter:
|
|
103
105
|
|
|
104
106
|
```yaml
|
|
105
107
|
# All tools from a server
|
|
@@ -118,7 +120,7 @@ The `mcp:` items are additive — they don't affect which builtins the agent get
|
|
|
118
120
|
|
|
119
121
|
Subagents only get direct MCP tools when `mcp:` items are explicitly listed. Even if your `mcp.json` has `directTools: true` globally, a subagent without `mcp:` in its frontmatter won't get any direct tools — keeping it lean. The `mcp` proxy tool is still available for discovery if needed.
|
|
120
122
|
|
|
121
|
-
The MCP adapter
|
|
123
|
+
> **First-run caveat:** The MCP adapter caches tool metadata at startup. The first time you connect to a new MCP server, that cache is empty, so tools are only available through the generic `mcp` proxy. After that first session, restart pi and direct tools become available.
|
|
122
124
|
|
|
123
125
|
**Resolution priority:** step override > agent frontmatter > disabled
|
|
124
126
|
|
|
@@ -425,7 +427,9 @@ Skills are specialized instructions loaded from SKILL.md files and injected into
|
|
|
425
427
|
|
|
426
428
|
## Usage
|
|
427
429
|
|
|
428
|
-
**subagent tool
|
|
430
|
+
These are the parameters the **LLM agent** passes when it calls the `subagent` tool — not something you type directly. The agent decides to use these based on your conversation. For user-facing commands, see [Quick Commands](#quick-commands) above.
|
|
431
|
+
|
|
432
|
+
**subagent tool parameters:**
|
|
429
433
|
```typescript
|
|
430
434
|
// Single agent
|
|
431
435
|
{ agent: "worker", task: "refactor auth" }
|
package/index.ts
CHANGED
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
import * as fs from "node:fs";
|
|
16
16
|
import * as os from "node:os";
|
|
17
17
|
import * as path from "node:path";
|
|
18
|
+
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
18
19
|
import { type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
19
|
-
import { Text } from "@mariozechner/pi-tui";
|
|
20
|
+
import { Box, Container, Spacer, Text } from "@mariozechner/pi-tui";
|
|
20
21
|
import { discoverAgents } from "./agents.js";
|
|
21
22
|
import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "./artifacts.js";
|
|
22
23
|
import { cleanupOldChainDirs } from "./settings.js";
|
|
@@ -28,6 +29,8 @@ import { createAsyncJobTracker } from "./async-job-tracker.js";
|
|
|
28
29
|
import { createResultWatcher } from "./result-watcher.js";
|
|
29
30
|
import { registerSlashCommands } from "./slash-commands.js";
|
|
30
31
|
import { registerPromptTemplateDelegationBridge } from "./prompt-template-bridge.js";
|
|
32
|
+
import { registerSlashSubagentBridge } from "./slash-bridge.js";
|
|
33
|
+
import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "./slash-live-state.js";
|
|
31
34
|
import {
|
|
32
35
|
type Details,
|
|
33
36
|
type ExtensionConfig,
|
|
@@ -35,6 +38,7 @@ import {
|
|
|
35
38
|
ASYNC_DIR,
|
|
36
39
|
DEFAULT_ARTIFACT_CONFIG,
|
|
37
40
|
RESULTS_DIR,
|
|
41
|
+
SLASH_RESULT_TYPE,
|
|
38
42
|
WIDGET_KEY,
|
|
39
43
|
} from "./types.js";
|
|
40
44
|
|
|
@@ -92,6 +96,48 @@ function ensureAccessibleDir(dirPath: string): void {
|
|
|
92
96
|
}
|
|
93
97
|
}
|
|
94
98
|
|
|
99
|
+
function isSlashResultRunning(result: { details?: Details }): boolean {
|
|
100
|
+
return result.details?.progress?.some((entry) => entry.status === "running")
|
|
101
|
+
|| result.details?.results.some((entry) => entry.progress?.status === "running")
|
|
102
|
+
|| false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isSlashResultError(result: { details?: Details }): boolean {
|
|
106
|
+
return result.details?.results.some((entry) => entry.exitCode !== 0 && entry.progress?.status !== "running") || false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function rebuildSlashResultContainer(
|
|
110
|
+
container: Container,
|
|
111
|
+
result: AgentToolResult<Details>,
|
|
112
|
+
options: { expanded: boolean },
|
|
113
|
+
theme: ExtensionContext["ui"]["theme"],
|
|
114
|
+
): void {
|
|
115
|
+
container.clear();
|
|
116
|
+
container.addChild(new Spacer(1));
|
|
117
|
+
const boxTheme = isSlashResultRunning(result) ? "toolPendingBg" : isSlashResultError(result) ? "toolErrorBg" : "toolSuccessBg";
|
|
118
|
+
const box = new Box(1, 1, (text: string) => theme.bg(boxTheme, text));
|
|
119
|
+
box.addChild(renderSubagentResult(result, options, theme));
|
|
120
|
+
container.addChild(box);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function createSlashResultComponent(
|
|
124
|
+
details: SlashMessageDetails,
|
|
125
|
+
options: { expanded: boolean },
|
|
126
|
+
theme: ExtensionContext["ui"]["theme"],
|
|
127
|
+
): Container {
|
|
128
|
+
const container = new Container();
|
|
129
|
+
let lastVersion = -1;
|
|
130
|
+
container.render = (width: number): string[] => {
|
|
131
|
+
const snapshot = getSlashRenderableSnapshot(details);
|
|
132
|
+
if (snapshot.version !== lastVersion) {
|
|
133
|
+
lastVersion = snapshot.version;
|
|
134
|
+
rebuildSlashResultContainer(container, snapshot.result, options, theme);
|
|
135
|
+
}
|
|
136
|
+
return Container.prototype.render.call(container, width);
|
|
137
|
+
};
|
|
138
|
+
return container;
|
|
139
|
+
}
|
|
140
|
+
|
|
95
141
|
export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
96
142
|
ensureAccessibleDir(RESULTS_DIR);
|
|
97
143
|
ensureAccessibleDir(ASYNC_DIR);
|
|
@@ -139,11 +185,39 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
139
185
|
discoverAgents,
|
|
140
186
|
});
|
|
141
187
|
|
|
188
|
+
pi.registerMessageRenderer<SlashMessageDetails>(SLASH_RESULT_TYPE, (message, options, theme) => {
|
|
189
|
+
const details = resolveSlashMessageDetails(message.details);
|
|
190
|
+
if (!details) return undefined;
|
|
191
|
+
return createSlashResultComponent(details, options, theme);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const slashBridge = registerSlashSubagentBridge({
|
|
195
|
+
events: pi.events,
|
|
196
|
+
getContext: () => state.lastUiContext,
|
|
197
|
+
execute: (id, params, signal, onUpdate, ctx) =>
|
|
198
|
+
executor.execute(id, params, signal, onUpdate, ctx),
|
|
199
|
+
});
|
|
200
|
+
|
|
142
201
|
const promptTemplateBridge = registerPromptTemplateDelegationBridge({
|
|
143
202
|
events: pi.events,
|
|
144
203
|
getContext: () => state.lastUiContext,
|
|
145
|
-
execute: async (requestId, request, signal, ctx, onUpdate) =>
|
|
146
|
-
|
|
204
|
+
execute: async (requestId, request, signal, ctx, onUpdate) => {
|
|
205
|
+
if (request.tasks && request.tasks.length > 0) {
|
|
206
|
+
return executor.execute(
|
|
207
|
+
requestId,
|
|
208
|
+
{
|
|
209
|
+
tasks: request.tasks,
|
|
210
|
+
context: request.context,
|
|
211
|
+
cwd: request.cwd,
|
|
212
|
+
async: false,
|
|
213
|
+
clarify: false,
|
|
214
|
+
},
|
|
215
|
+
signal,
|
|
216
|
+
onUpdate,
|
|
217
|
+
ctx,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
return executor.execute(
|
|
147
221
|
requestId,
|
|
148
222
|
{
|
|
149
223
|
agent: request.agent,
|
|
@@ -157,7 +231,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
157
231
|
signal,
|
|
158
232
|
onUpdate,
|
|
159
233
|
ctx,
|
|
160
|
-
)
|
|
234
|
+
);
|
|
235
|
+
},
|
|
161
236
|
});
|
|
162
237
|
|
|
163
238
|
const tool: ToolDefinition<typeof SubagentParams, Details> = {
|
|
@@ -324,7 +399,7 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
|
|
|
324
399
|
|
|
325
400
|
pi.registerTool(tool);
|
|
326
401
|
pi.registerTool(statusTool);
|
|
327
|
-
registerSlashCommands(pi, state
|
|
402
|
+
registerSlashCommands(pi, state);
|
|
328
403
|
|
|
329
404
|
pi.events.on("subagent:started", handleStarted);
|
|
330
405
|
pi.events.on("subagent:complete", handleComplete);
|
|
@@ -356,6 +431,7 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
|
|
|
356
431
|
state.lastUiContext = ctx;
|
|
357
432
|
cleanupSessionArtifacts(ctx);
|
|
358
433
|
resetJobs(ctx);
|
|
434
|
+
restoreSlashFinalSnapshots(ctx.sessionManager.getEntries());
|
|
359
435
|
};
|
|
360
436
|
|
|
361
437
|
pi.on("session_start", (_event, ctx) => {
|
|
@@ -376,6 +452,9 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
|
|
|
376
452
|
}
|
|
377
453
|
state.cleanupTimers.clear();
|
|
378
454
|
state.asyncJobs.clear();
|
|
455
|
+
clearSlashSnapshots();
|
|
456
|
+
slashBridge.cancelAll();
|
|
457
|
+
slashBridge.dispose();
|
|
379
458
|
promptTemplateBridge.cancelAll();
|
|
380
459
|
promptTemplateBridge.dispose();
|
|
381
460
|
if (state.lastUiContext?.hasUI) {
|
package/package.json
CHANGED
|
@@ -4,10 +4,24 @@ export const PROMPT_TEMPLATE_SUBAGENT_RESPONSE_EVENT = "prompt-template:subagent
|
|
|
4
4
|
export const PROMPT_TEMPLATE_SUBAGENT_UPDATE_EVENT = "prompt-template:subagent:update";
|
|
5
5
|
export const PROMPT_TEMPLATE_SUBAGENT_CANCEL_EVENT = "prompt-template:subagent:cancel";
|
|
6
6
|
|
|
7
|
+
export interface PromptTemplateDelegationTask {
|
|
8
|
+
agent: string;
|
|
9
|
+
task: string;
|
|
10
|
+
model?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PromptTemplateDelegationParallelResult {
|
|
14
|
+
agent: string;
|
|
15
|
+
messages: unknown[];
|
|
16
|
+
isError: boolean;
|
|
17
|
+
errorText?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
7
20
|
export interface PromptTemplateDelegationRequest {
|
|
8
21
|
requestId: string;
|
|
9
22
|
agent: string;
|
|
10
23
|
task: string;
|
|
24
|
+
tasks?: PromptTemplateDelegationTask[];
|
|
11
25
|
context: "fresh" | "fork";
|
|
12
26
|
model: string;
|
|
13
27
|
cwd: string;
|
|
@@ -15,10 +29,23 @@ export interface PromptTemplateDelegationRequest {
|
|
|
15
29
|
|
|
16
30
|
export interface PromptTemplateDelegationResponse extends PromptTemplateDelegationRequest {
|
|
17
31
|
messages: unknown[];
|
|
32
|
+
parallelResults?: PromptTemplateDelegationParallelResult[];
|
|
18
33
|
isError: boolean;
|
|
19
34
|
errorText?: string;
|
|
20
35
|
}
|
|
21
36
|
|
|
37
|
+
export interface PromptTemplateDelegationTaskProgress {
|
|
38
|
+
index?: number;
|
|
39
|
+
agent: string;
|
|
40
|
+
status?: string;
|
|
41
|
+
currentTool?: string;
|
|
42
|
+
currentToolArgs?: string;
|
|
43
|
+
recentOutput?: string;
|
|
44
|
+
toolCount?: number;
|
|
45
|
+
durationMs?: number;
|
|
46
|
+
tokens?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
22
49
|
export interface PromptTemplateDelegationUpdate {
|
|
23
50
|
requestId: string;
|
|
24
51
|
currentTool?: string;
|
|
@@ -27,6 +54,7 @@ export interface PromptTemplateDelegationUpdate {
|
|
|
27
54
|
toolCount?: number;
|
|
28
55
|
durationMs?: number;
|
|
29
56
|
tokens?: number;
|
|
57
|
+
taskProgress?: PromptTemplateDelegationTaskProgress[];
|
|
30
58
|
}
|
|
31
59
|
|
|
32
60
|
export interface PromptTemplateBridgeEvents {
|
|
@@ -39,13 +67,21 @@ interface PromptTemplateBridgeResult {
|
|
|
39
67
|
content?: unknown;
|
|
40
68
|
details?: {
|
|
41
69
|
results?: Array<{
|
|
70
|
+
agent?: string;
|
|
42
71
|
messages?: unknown[];
|
|
72
|
+
exitCode?: number;
|
|
73
|
+
error?: string;
|
|
43
74
|
}>;
|
|
44
75
|
progress?: Array<{
|
|
76
|
+
index?: number;
|
|
77
|
+
agent?: string;
|
|
78
|
+
status?: string;
|
|
45
79
|
currentTool?: string;
|
|
46
80
|
currentToolArgs?: string;
|
|
47
81
|
recentOutput?: string[];
|
|
48
82
|
toolCount?: number;
|
|
83
|
+
durationMs?: number;
|
|
84
|
+
tokens?: number;
|
|
49
85
|
}>;
|
|
50
86
|
};
|
|
51
87
|
}
|
|
@@ -62,15 +98,45 @@ export interface PromptTemplateBridgeOptions<Ctx extends { cwd?: string }> {
|
|
|
62
98
|
) => Promise<PromptTemplateBridgeResult>;
|
|
63
99
|
}
|
|
64
100
|
|
|
101
|
+
function parseDelegationTasks(tasks: unknown): PromptTemplateDelegationTask[] {
|
|
102
|
+
if (!Array.isArray(tasks)) return [];
|
|
103
|
+
const parsed: PromptTemplateDelegationTask[] = [];
|
|
104
|
+
for (const item of tasks) {
|
|
105
|
+
if (!item || typeof item !== "object") return [];
|
|
106
|
+
const value = item as Partial<PromptTemplateDelegationTask>;
|
|
107
|
+
if (typeof value.agent !== "string" || !value.agent.trim()) return [];
|
|
108
|
+
if (typeof value.task !== "string" || !value.task.trim()) return [];
|
|
109
|
+
const model = typeof value.model === "string" && value.model.trim().length > 0 ? value.model : undefined;
|
|
110
|
+
parsed.push({
|
|
111
|
+
agent: value.agent,
|
|
112
|
+
task: value.task,
|
|
113
|
+
...(model ? { model } : {}),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return parsed;
|
|
117
|
+
}
|
|
118
|
+
|
|
65
119
|
export function parsePromptTemplateRequest(data: unknown): PromptTemplateDelegationRequest | undefined {
|
|
66
120
|
if (!data || typeof data !== "object") return undefined;
|
|
67
|
-
const value = data as Partial<PromptTemplateDelegationRequest
|
|
68
|
-
if (
|
|
121
|
+
const value = data as Partial<PromptTemplateDelegationRequest> & { tasks?: unknown };
|
|
122
|
+
if (typeof value.requestId !== "string" || !value.requestId) return undefined;
|
|
123
|
+
if (typeof value.model !== "string" || !value.model) return undefined;
|
|
124
|
+
if (typeof value.cwd !== "string" || !value.cwd) return undefined;
|
|
69
125
|
if (value.context !== "fresh" && value.context !== "fork") return undefined;
|
|
126
|
+
const tasks = parseDelegationTasks(value.tasks);
|
|
127
|
+
const hasSingle =
|
|
128
|
+
typeof value.agent === "string" &&
|
|
129
|
+
value.agent.length > 0 &&
|
|
130
|
+
typeof value.task === "string" &&
|
|
131
|
+
value.task.length > 0;
|
|
132
|
+
if (!hasSingle && tasks.length === 0) return undefined;
|
|
133
|
+
|
|
134
|
+
const fallbackTask = tasks[0];
|
|
70
135
|
return {
|
|
71
136
|
requestId: value.requestId,
|
|
72
|
-
agent: value.agent,
|
|
73
|
-
task: value.task,
|
|
137
|
+
agent: hasSingle ? value.agent : fallbackTask!.agent,
|
|
138
|
+
task: hasSingle ? value.task : fallbackTask!.task,
|
|
139
|
+
...(tasks.length > 0 ? { tasks } : {}),
|
|
74
140
|
context: value.context,
|
|
75
141
|
model: value.model,
|
|
76
142
|
cwd: value.cwd,
|
|
@@ -90,16 +156,31 @@ export function firstTextContent(content: unknown): string | undefined {
|
|
|
90
156
|
|
|
91
157
|
function toDelegationUpdate(requestId: string, update: PromptTemplateBridgeResult): PromptTemplateDelegationUpdate | undefined {
|
|
92
158
|
const progress = update.details?.progress?.[0];
|
|
93
|
-
|
|
94
|
-
|
|
159
|
+
const taskProgress = update.details?.progress?.map((entry) => {
|
|
160
|
+
const lastOutput = entry.recentOutput?.[entry.recentOutput.length - 1];
|
|
161
|
+
return {
|
|
162
|
+
index: entry.index,
|
|
163
|
+
agent: entry.agent ?? "delegate",
|
|
164
|
+
status: entry.status,
|
|
165
|
+
currentTool: entry.currentTool,
|
|
166
|
+
currentToolArgs: entry.currentToolArgs,
|
|
167
|
+
recentOutput: lastOutput && lastOutput !== "(running...)" ? lastOutput : undefined,
|
|
168
|
+
toolCount: entry.toolCount,
|
|
169
|
+
durationMs: entry.durationMs,
|
|
170
|
+
tokens: entry.tokens,
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
if (!progress && (!taskProgress || taskProgress.length === 0)) return undefined;
|
|
174
|
+
const lastOutput = progress?.recentOutput?.[progress.recentOutput.length - 1];
|
|
95
175
|
return {
|
|
96
176
|
requestId,
|
|
97
|
-
currentTool: progress
|
|
98
|
-
currentToolArgs: progress
|
|
177
|
+
currentTool: progress?.currentTool,
|
|
178
|
+
currentToolArgs: progress?.currentToolArgs,
|
|
99
179
|
recentOutput: lastOutput && lastOutput !== "(running...)" ? lastOutput : undefined,
|
|
100
|
-
toolCount: progress
|
|
101
|
-
durationMs:
|
|
102
|
-
tokens:
|
|
180
|
+
toolCount: progress?.toolCount,
|
|
181
|
+
durationMs: progress?.durationMs,
|
|
182
|
+
tokens: progress?.tokens,
|
|
183
|
+
taskProgress,
|
|
103
184
|
};
|
|
104
185
|
}
|
|
105
186
|
|
|
@@ -177,9 +258,31 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
|
|
|
177
258
|
},
|
|
178
259
|
);
|
|
179
260
|
const messages = result.details?.results?.[0]?.messages ?? [];
|
|
261
|
+
const parallelResults = request.tasks
|
|
262
|
+
? request.tasks.map<PromptTemplateDelegationParallelResult>((task, index) => {
|
|
263
|
+
const step = result.details?.results?.[index];
|
|
264
|
+
if (!step) {
|
|
265
|
+
return {
|
|
266
|
+
agent: task.agent,
|
|
267
|
+
messages: [],
|
|
268
|
+
isError: true,
|
|
269
|
+
errorText: "Missing result for delegated parallel task.",
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const exitCode = typeof step.exitCode === "number" ? step.exitCode : undefined;
|
|
273
|
+
const errorText = step.error;
|
|
274
|
+
return {
|
|
275
|
+
agent: step.agent ?? task.agent,
|
|
276
|
+
messages: step.messages ?? [],
|
|
277
|
+
isError: (exitCode !== undefined && exitCode !== 0) || !!errorText,
|
|
278
|
+
errorText: errorText || undefined,
|
|
279
|
+
};
|
|
280
|
+
})
|
|
281
|
+
: undefined;
|
|
180
282
|
const response: PromptTemplateDelegationResponse = {
|
|
181
283
|
...request,
|
|
182
284
|
messages,
|
|
285
|
+
...(parallelResults ? { parallelResults } : {}),
|
|
183
286
|
isError: result.isError === true,
|
|
184
287
|
errorText: result.isError ? firstTextContent(result.content) : undefined,
|
|
185
288
|
};
|
package/render.ts
CHANGED
|
@@ -220,6 +220,36 @@ export function renderSubagentResult(
|
|
|
220
220
|
);
|
|
221
221
|
c.addChild(new Spacer(1));
|
|
222
222
|
|
|
223
|
+
if (isRunning && r.progress) {
|
|
224
|
+
if (r.progress.currentTool) {
|
|
225
|
+
const maxToolArgsLen = Math.max(50, w - 20);
|
|
226
|
+
const toolArgsPreview = r.progress.currentToolArgs
|
|
227
|
+
? (r.progress.currentToolArgs.length > maxToolArgsLen
|
|
228
|
+
? `${r.progress.currentToolArgs.slice(0, maxToolArgsLen)}...`
|
|
229
|
+
: r.progress.currentToolArgs)
|
|
230
|
+
: "";
|
|
231
|
+
const toolLine = toolArgsPreview
|
|
232
|
+
? `${r.progress.currentTool}: ${toolArgsPreview}`
|
|
233
|
+
: r.progress.currentTool;
|
|
234
|
+
c.addChild(new Text(truncLine(theme.fg("warning", `> ${toolLine}`), w), 0, 0));
|
|
235
|
+
}
|
|
236
|
+
if (r.progress.recentTools?.length) {
|
|
237
|
+
for (const t of r.progress.recentTools.slice(-3)) {
|
|
238
|
+
const maxArgsLen = Math.max(40, w - 24);
|
|
239
|
+
const argsPreview = t.args.length > maxArgsLen
|
|
240
|
+
? `${t.args.slice(0, maxArgsLen)}...`
|
|
241
|
+
: t.args;
|
|
242
|
+
c.addChild(new Text(truncLine(theme.fg("dim", `${t.tool}: ${argsPreview}`), w), 0, 0));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
for (const line of (r.progress.recentOutput ?? []).slice(-5)) {
|
|
246
|
+
c.addChild(new Text(truncLine(theme.fg("dim", ` ${line}`), w), 0, 0));
|
|
247
|
+
}
|
|
248
|
+
if (r.progress.currentTool || r.progress.recentTools?.length || r.progress.recentOutput?.length) {
|
|
249
|
+
c.addChild(new Spacer(1));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
223
253
|
const items = getDisplayItems(r.messages);
|
|
224
254
|
for (const item of items) {
|
|
225
255
|
if (item.type === "tool")
|
package/slash-bridge.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
2
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import type { SubagentParamsLike } from "./subagent-executor.js";
|
|
4
|
+
import {
|
|
5
|
+
SLASH_SUBAGENT_CANCEL_EVENT,
|
|
6
|
+
SLASH_SUBAGENT_REQUEST_EVENT,
|
|
7
|
+
SLASH_SUBAGENT_RESPONSE_EVENT,
|
|
8
|
+
SLASH_SUBAGENT_STARTED_EVENT,
|
|
9
|
+
SLASH_SUBAGENT_UPDATE_EVENT,
|
|
10
|
+
type Details,
|
|
11
|
+
} from "./types.js";
|
|
12
|
+
|
|
13
|
+
export interface SlashSubagentRequest {
|
|
14
|
+
requestId: string;
|
|
15
|
+
params: SubagentParamsLike;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SlashSubagentResponse {
|
|
19
|
+
requestId: string;
|
|
20
|
+
result: AgentToolResult<Details>;
|
|
21
|
+
isError: boolean;
|
|
22
|
+
errorText?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SlashSubagentUpdate {
|
|
26
|
+
requestId: string;
|
|
27
|
+
progress?: Details["progress"];
|
|
28
|
+
currentTool?: string;
|
|
29
|
+
toolCount?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface EventBus {
|
|
33
|
+
on(event: string, handler: (data: unknown) => void): (() => void) | void;
|
|
34
|
+
emit(event: string, data: unknown): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface SlashBridgeOptions {
|
|
38
|
+
events: EventBus;
|
|
39
|
+
getContext: () => ExtensionContext | null;
|
|
40
|
+
execute: (
|
|
41
|
+
id: string,
|
|
42
|
+
params: SubagentParamsLike,
|
|
43
|
+
signal: AbortSignal,
|
|
44
|
+
onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
|
|
45
|
+
ctx: ExtensionContext,
|
|
46
|
+
) => Promise<AgentToolResult<Details>>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function registerSlashSubagentBridge(options: SlashBridgeOptions): {
|
|
50
|
+
cancelAll: () => void;
|
|
51
|
+
dispose: () => void;
|
|
52
|
+
} {
|
|
53
|
+
const controllers = new Map<string, AbortController>();
|
|
54
|
+
const pendingCancels = new Set<string>();
|
|
55
|
+
const subscriptions: Array<() => void> = [];
|
|
56
|
+
|
|
57
|
+
const subscribe = (event: string, handler: (data: unknown) => void): void => {
|
|
58
|
+
const unsubscribe = options.events.on(event, handler);
|
|
59
|
+
if (typeof unsubscribe === "function") subscriptions.push(unsubscribe);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
subscribe(SLASH_SUBAGENT_CANCEL_EVENT, (data) => {
|
|
63
|
+
if (!data || typeof data !== "object") return;
|
|
64
|
+
const requestId = (data as { requestId?: unknown }).requestId;
|
|
65
|
+
if (typeof requestId !== "string") return;
|
|
66
|
+
const controller = controllers.get(requestId);
|
|
67
|
+
if (controller) {
|
|
68
|
+
controller.abort();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
pendingCancels.add(requestId);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
subscribe(SLASH_SUBAGENT_REQUEST_EVENT, async (data) => {
|
|
75
|
+
if (!data || typeof data !== "object") return;
|
|
76
|
+
const request = data as Partial<SlashSubagentRequest>;
|
|
77
|
+
if (typeof request.requestId !== "string" || !request.params) return;
|
|
78
|
+
const { requestId, params } = request as SlashSubagentRequest;
|
|
79
|
+
|
|
80
|
+
const ctx = options.getContext();
|
|
81
|
+
if (!ctx) {
|
|
82
|
+
const response: SlashSubagentResponse = {
|
|
83
|
+
requestId,
|
|
84
|
+
result: {
|
|
85
|
+
content: [{ type: "text", text: "No active extension context for slash subagent execution." }],
|
|
86
|
+
details: { mode: "single" as const, results: [] },
|
|
87
|
+
},
|
|
88
|
+
isError: true,
|
|
89
|
+
errorText: "No active extension context.",
|
|
90
|
+
};
|
|
91
|
+
options.events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, response);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const controller = new AbortController();
|
|
96
|
+
controllers.set(requestId, controller);
|
|
97
|
+
|
|
98
|
+
if (pendingCancels.delete(requestId)) {
|
|
99
|
+
controller.abort();
|
|
100
|
+
const response: SlashSubagentResponse = {
|
|
101
|
+
requestId,
|
|
102
|
+
result: {
|
|
103
|
+
content: [{ type: "text", text: "Cancelled." }],
|
|
104
|
+
details: { mode: "single" as const, results: [] },
|
|
105
|
+
},
|
|
106
|
+
isError: true,
|
|
107
|
+
errorText: "Cancelled before start.",
|
|
108
|
+
};
|
|
109
|
+
options.events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, response);
|
|
110
|
+
controllers.delete(requestId);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
options.events.emit(SLASH_SUBAGENT_STARTED_EVENT, { requestId });
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const result = await options.execute(
|
|
118
|
+
requestId,
|
|
119
|
+
params,
|
|
120
|
+
controller.signal,
|
|
121
|
+
(update) => {
|
|
122
|
+
const progress = update.details?.progress;
|
|
123
|
+
const first = progress?.[0];
|
|
124
|
+
const payload: SlashSubagentUpdate = {
|
|
125
|
+
requestId,
|
|
126
|
+
progress,
|
|
127
|
+
currentTool: first?.currentTool,
|
|
128
|
+
toolCount: first?.toolCount,
|
|
129
|
+
};
|
|
130
|
+
options.events.emit(SLASH_SUBAGENT_UPDATE_EVENT, payload);
|
|
131
|
+
},
|
|
132
|
+
ctx,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const response: SlashSubagentResponse = {
|
|
136
|
+
requestId,
|
|
137
|
+
result,
|
|
138
|
+
isError: (result as { isError?: boolean }).isError === true,
|
|
139
|
+
errorText: (result as { isError?: boolean }).isError
|
|
140
|
+
? result.content.find((c) => c.type === "text")?.text
|
|
141
|
+
: undefined,
|
|
142
|
+
};
|
|
143
|
+
options.events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, response);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
const response: SlashSubagentResponse = {
|
|
146
|
+
requestId,
|
|
147
|
+
result: {
|
|
148
|
+
content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
|
|
149
|
+
details: { mode: "single" as const, results: [] },
|
|
150
|
+
},
|
|
151
|
+
isError: true,
|
|
152
|
+
errorText: error instanceof Error ? error.message : String(error),
|
|
153
|
+
};
|
|
154
|
+
options.events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, response);
|
|
155
|
+
} finally {
|
|
156
|
+
controllers.delete(requestId);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
cancelAll: () => {
|
|
162
|
+
for (const controller of controllers.values()) {
|
|
163
|
+
controller.abort();
|
|
164
|
+
}
|
|
165
|
+
controllers.clear();
|
|
166
|
+
pendingCancels.clear();
|
|
167
|
+
},
|
|
168
|
+
dispose: () => {
|
|
169
|
+
for (const unsubscribe of subscriptions) unsubscribe();
|
|
170
|
+
subscriptions.length = 0;
|
|
171
|
+
pendingCancels.clear();
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
package/slash-commands.ts
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import * as path from "node:path";
|
|
4
2
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { Key, matchesKey } from "@mariozechner/pi-tui";
|
|
5
4
|
import { discoverAgents, discoverAgentsAll } from "./agents.js";
|
|
6
|
-
import { executeAsyncChain, isAsyncAvailable } from "./async-execution.js";
|
|
7
|
-
import { executeChain } from "./chain-execution.js";
|
|
8
5
|
import { AgentManagerComponent, type ManagerResult } from "./agent-manager.js";
|
|
9
6
|
import { discoverAvailableSkills } from "./skills.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
7
|
+
import type { SubagentParamsLike } from "./subagent-executor.js";
|
|
8
|
+
import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.js";
|
|
9
|
+
import {
|
|
10
|
+
applySlashUpdate,
|
|
11
|
+
buildSlashInitialResult,
|
|
12
|
+
failSlashResult,
|
|
13
|
+
finalizeSlashResult,
|
|
14
|
+
} from "./slash-live-state.js";
|
|
15
|
+
import {
|
|
16
|
+
MAX_PARALLEL,
|
|
17
|
+
SLASH_RESULT_TYPE,
|
|
18
|
+
SLASH_SUBAGENT_CANCEL_EVENT,
|
|
19
|
+
SLASH_SUBAGENT_REQUEST_EVENT,
|
|
20
|
+
SLASH_SUBAGENT_RESPONSE_EVENT,
|
|
21
|
+
SLASH_SUBAGENT_STARTED_EVENT,
|
|
22
|
+
SLASH_SUBAGENT_UPDATE_EVENT,
|
|
23
|
+
type SubagentState,
|
|
24
|
+
} from "./types.js";
|
|
13
25
|
|
|
14
26
|
interface InlineConfig {
|
|
15
27
|
output?: string | false;
|
|
@@ -71,25 +83,6 @@ const extractExecutionFlags = (rawArgs: string): { args: string; bg: boolean; fo
|
|
|
71
83
|
return { args, bg, fork };
|
|
72
84
|
};
|
|
73
85
|
|
|
74
|
-
function setupDirectRun(ctx: ExtensionContext, getSubagentSessionRoot: (parentSessionFile: string | null) => string) {
|
|
75
|
-
const runId = randomUUID().slice(0, 8);
|
|
76
|
-
const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
|
|
77
|
-
const sessionRoot = path.join(getSubagentSessionRoot(parentSessionFile), runId);
|
|
78
|
-
try {
|
|
79
|
-
fs.mkdirSync(sessionRoot, { recursive: true });
|
|
80
|
-
} catch (error) {
|
|
81
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
82
|
-
throw new Error(`Failed to create session directory '${sessionRoot}': ${message}`);
|
|
83
|
-
}
|
|
84
|
-
return {
|
|
85
|
-
runId,
|
|
86
|
-
shareEnabled: false,
|
|
87
|
-
sessionDirForIndex: (idx?: number) => path.join(sessionRoot, `run-${idx ?? 0}`),
|
|
88
|
-
artifactsDir: getArtifactsDir(parentSessionFile),
|
|
89
|
-
artifactConfig: { ...DEFAULT_ARTIFACT_CONFIG } as ArtifactConfig,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
86
|
const makeAgentCompletions = (state: SubagentState, multiAgent: boolean) => (prefix: string) => {
|
|
94
87
|
const agents = discoverAgents(state.baseCwd, "both").agents;
|
|
95
88
|
if (!multiAgent) {
|
|
@@ -111,11 +104,145 @@ const makeAgentCompletions = (state: SubagentState, multiAgent: boolean) => (pre
|
|
|
111
104
|
return agents.filter((a) => a.name.startsWith(lastWord)).map((a) => ({ value: `${beforeLastWord}${a.name}`, label: a.name }));
|
|
112
105
|
};
|
|
113
106
|
|
|
107
|
+
async function requestSlashRun(
|
|
108
|
+
pi: ExtensionAPI,
|
|
109
|
+
ctx: ExtensionContext,
|
|
110
|
+
requestId: string,
|
|
111
|
+
params: SubagentParamsLike,
|
|
112
|
+
): Promise<SlashSubagentResponse> {
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
let done = false;
|
|
115
|
+
let started = false;
|
|
116
|
+
|
|
117
|
+
const startTimeoutMs = 15_000;
|
|
118
|
+
const startTimeout = setTimeout(() => {
|
|
119
|
+
finish(() => reject(new Error(
|
|
120
|
+
"Slash subagent bridge did not start within 15s. Ensure the extension is loaded correctly.",
|
|
121
|
+
)));
|
|
122
|
+
}, startTimeoutMs);
|
|
123
|
+
|
|
124
|
+
const onStarted = (data: unknown) => {
|
|
125
|
+
if (done || !data || typeof data !== "object") return;
|
|
126
|
+
if ((data as { requestId?: unknown }).requestId !== requestId) return;
|
|
127
|
+
started = true;
|
|
128
|
+
clearTimeout(startTimeout);
|
|
129
|
+
if (ctx.hasUI) ctx.ui.setStatus("subagent-slash", "running...");
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const onResponse = (data: unknown) => {
|
|
133
|
+
if (done || !data || typeof data !== "object") return;
|
|
134
|
+
const response = data as Partial<SlashSubagentResponse>;
|
|
135
|
+
if (response.requestId !== requestId) return;
|
|
136
|
+
clearTimeout(startTimeout);
|
|
137
|
+
finish(() => resolve(response as SlashSubagentResponse));
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const onUpdate = (data: unknown) => {
|
|
141
|
+
if (done || !data || typeof data !== "object") return;
|
|
142
|
+
const update = data as SlashSubagentUpdate;
|
|
143
|
+
if (update.requestId !== requestId) return;
|
|
144
|
+
applySlashUpdate(requestId, update);
|
|
145
|
+
if (!ctx.hasUI) return;
|
|
146
|
+
const tool = update.currentTool ? ` ${update.currentTool}` : "";
|
|
147
|
+
const count = update.toolCount ?? 0;
|
|
148
|
+
ctx.ui.setStatus("subagent-slash", `${count} tools${tool}`);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const onTerminalInput = ctx.hasUI
|
|
152
|
+
? ctx.ui.onTerminalInput((input) => {
|
|
153
|
+
if (!matchesKey(input, Key.escape)) return undefined;
|
|
154
|
+
pi.events.emit(SLASH_SUBAGENT_CANCEL_EVENT, { requestId });
|
|
155
|
+
finish(() => reject(new Error("Cancelled")));
|
|
156
|
+
return { consume: true };
|
|
157
|
+
})
|
|
158
|
+
: undefined;
|
|
159
|
+
|
|
160
|
+
const unsubStarted = pi.events.on(SLASH_SUBAGENT_STARTED_EVENT, onStarted);
|
|
161
|
+
const unsubResponse = pi.events.on(SLASH_SUBAGENT_RESPONSE_EVENT, onResponse);
|
|
162
|
+
const unsubUpdate = pi.events.on(SLASH_SUBAGENT_UPDATE_EVENT, onUpdate);
|
|
163
|
+
|
|
164
|
+
const finish = (next: () => void) => {
|
|
165
|
+
if (done) return;
|
|
166
|
+
done = true;
|
|
167
|
+
clearTimeout(startTimeout);
|
|
168
|
+
unsubStarted();
|
|
169
|
+
unsubResponse();
|
|
170
|
+
unsubUpdate();
|
|
171
|
+
onTerminalInput?.();
|
|
172
|
+
if (ctx.hasUI) ctx.ui.setStatus("subagent-slash", undefined);
|
|
173
|
+
next();
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
pi.events.emit(SLASH_SUBAGENT_REQUEST_EVENT, { requestId, params });
|
|
177
|
+
|
|
178
|
+
// Bridge emits STARTED synchronously during REQUEST emit.
|
|
179
|
+
// If not started, no bridge received the request.
|
|
180
|
+
if (!started && done) return;
|
|
181
|
+
if (!started) {
|
|
182
|
+
finish(() => reject(new Error(
|
|
183
|
+
"No slash subagent bridge responded. Ensure the subagent extension is loaded correctly.",
|
|
184
|
+
)));
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function extractSlashMessageText(content: string | Array<{ type?: string; text?: string }>): string {
|
|
190
|
+
if (typeof content === "string") return content;
|
|
191
|
+
if (!Array.isArray(content)) return "";
|
|
192
|
+
return content
|
|
193
|
+
.filter((part): part is { type: "text"; text: string } => part?.type === "text" && typeof part.text === "string")
|
|
194
|
+
.map((part) => part.text)
|
|
195
|
+
.join("\n");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function runSlashSubagent(
|
|
199
|
+
pi: ExtensionAPI,
|
|
200
|
+
ctx: ExtensionContext,
|
|
201
|
+
params: SubagentParamsLike,
|
|
202
|
+
): Promise<void> {
|
|
203
|
+
const requestId = randomUUID();
|
|
204
|
+
const initialDetails = buildSlashInitialResult(requestId, params);
|
|
205
|
+
const initialText = extractSlashMessageText(initialDetails.result.content) || "Running subagent...";
|
|
206
|
+
pi.sendMessage({
|
|
207
|
+
customType: SLASH_RESULT_TYPE,
|
|
208
|
+
content: initialText,
|
|
209
|
+
display: true,
|
|
210
|
+
details: initialDetails,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const response = await requestSlashRun(pi, ctx, requestId, params);
|
|
215
|
+
const finalDetails = finalizeSlashResult(response);
|
|
216
|
+
const text = extractSlashMessageText(response.result.content) || response.errorText || "(no output)";
|
|
217
|
+
pi.sendMessage({
|
|
218
|
+
customType: SLASH_RESULT_TYPE,
|
|
219
|
+
content: text,
|
|
220
|
+
display: false,
|
|
221
|
+
details: finalDetails,
|
|
222
|
+
});
|
|
223
|
+
if (response.isError && ctx.hasUI) {
|
|
224
|
+
ctx.ui.notify(response.errorText || "Subagent failed", "error");
|
|
225
|
+
}
|
|
226
|
+
} catch (error) {
|
|
227
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
228
|
+
const failedDetails = failSlashResult(requestId, params, message === "Cancelled" ? "Cancelled" : message);
|
|
229
|
+
pi.sendMessage({
|
|
230
|
+
customType: SLASH_RESULT_TYPE,
|
|
231
|
+
content: message,
|
|
232
|
+
display: false,
|
|
233
|
+
details: failedDetails,
|
|
234
|
+
});
|
|
235
|
+
if (message === "Cancelled") {
|
|
236
|
+
if (ctx.hasUI) ctx.ui.notify("Cancelled", "warning");
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (ctx.hasUI) ctx.ui.notify(message, "error");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
114
243
|
async function openAgentManager(
|
|
115
244
|
pi: ExtensionAPI,
|
|
116
|
-
state: SubagentState,
|
|
117
245
|
ctx: ExtensionContext,
|
|
118
|
-
getSubagentSessionRoot: (parentSessionFile: string | null) => string,
|
|
119
246
|
): Promise<void> {
|
|
120
247
|
const agentData = { ...discoverAgentsAll(ctx.cwd), cwd: ctx.cwd };
|
|
121
248
|
const models = ctx.modelRegistry.getAvailable().map((m) => ({
|
|
@@ -132,54 +259,26 @@ async function openAgentManager(
|
|
|
132
259
|
if (!result) return;
|
|
133
260
|
|
|
134
261
|
if (result.action === "chain") {
|
|
135
|
-
const
|
|
136
|
-
const exec = setupDirectRun(ctx, getSubagentSessionRoot);
|
|
137
|
-
const chain: SequentialStep[] = result.agents.map((name, i) => ({
|
|
262
|
+
const chain = result.agents.map((name, i) => ({
|
|
138
263
|
agent: name,
|
|
139
264
|
...(i === 0 ? { task: result.task } : {}),
|
|
140
265
|
}));
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
const id = randomUUID();
|
|
149
|
-
const asyncCtx = { pi, cwd: ctx.cwd, currentSessionId: ctx.sessionManager.getSessionId() ?? id };
|
|
150
|
-
const asyncSessionRoot = getSubagentSessionRoot(ctx.sessionManager.getSessionFile() ?? null);
|
|
151
|
-
fs.mkdirSync(asyncSessionRoot, { recursive: true });
|
|
152
|
-
executeAsyncChain(id, {
|
|
153
|
-
chain: r.requestedAsync.chain,
|
|
154
|
-
agents,
|
|
155
|
-
ctx: asyncCtx,
|
|
156
|
-
maxOutput: undefined,
|
|
157
|
-
artifactsDir: exec.artifactsDir,
|
|
158
|
-
artifactConfig: exec.artifactConfig,
|
|
159
|
-
shareEnabled: false,
|
|
160
|
-
sessionRoot: asyncSessionRoot,
|
|
161
|
-
chainSkills: r.requestedAsync.chainSkills,
|
|
162
|
-
}).then((asyncResult) => {
|
|
163
|
-
pi.sendUserMessage(asyncResult.content[0]?.text || "(launched in background)");
|
|
164
|
-
}).catch((err) => {
|
|
165
|
-
pi.sendUserMessage(`Async launch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
166
|
-
});
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
pi.sendUserMessage(r.content[0]?.text || "(no output)");
|
|
170
|
-
})
|
|
171
|
-
.catch((err) => pi.sendUserMessage(`Chain failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
266
|
+
await runSlashSubagent(pi, ctx, {
|
|
267
|
+
chain,
|
|
268
|
+
task: result.task,
|
|
269
|
+
clarify: true,
|
|
270
|
+
agentScope: "both",
|
|
271
|
+
});
|
|
172
272
|
return;
|
|
173
273
|
}
|
|
174
274
|
|
|
175
|
-
const sendToolCall = (params: Record<string, unknown>) => {
|
|
176
|
-
pi.sendUserMessage(
|
|
177
|
-
`Call the subagent tool with these exact parameters: ${JSON.stringify({ ...params, agentScope: "both" })}`,
|
|
178
|
-
);
|
|
179
|
-
};
|
|
180
|
-
|
|
181
275
|
if (result.action === "launch") {
|
|
182
|
-
|
|
276
|
+
await runSlashSubagent(pi, ctx, {
|
|
277
|
+
agent: result.agent,
|
|
278
|
+
task: result.task,
|
|
279
|
+
clarify: !result.skipClarify,
|
|
280
|
+
agentScope: "both",
|
|
281
|
+
});
|
|
183
282
|
} else if (result.action === "launch-chain") {
|
|
184
283
|
const chainParam = result.chain.steps.map((step) => ({
|
|
185
284
|
agent: step.agent,
|
|
@@ -190,9 +289,18 @@ async function openAgentManager(
|
|
|
190
289
|
skill: step.skills,
|
|
191
290
|
model: step.model,
|
|
192
291
|
}));
|
|
193
|
-
|
|
292
|
+
await runSlashSubagent(pi, ctx, {
|
|
293
|
+
chain: chainParam,
|
|
294
|
+
task: result.task,
|
|
295
|
+
clarify: !result.skipClarify,
|
|
296
|
+
agentScope: "both",
|
|
297
|
+
});
|
|
194
298
|
} else if (result.action === "parallel") {
|
|
195
|
-
|
|
299
|
+
await runSlashSubagent(pi, ctx, {
|
|
300
|
+
tasks: result.tasks,
|
|
301
|
+
clarify: !result.skipClarify,
|
|
302
|
+
agentScope: "both",
|
|
303
|
+
});
|
|
196
304
|
}
|
|
197
305
|
}
|
|
198
306
|
|
|
@@ -276,12 +384,11 @@ const parseAgentArgs = (
|
|
|
276
384
|
export function registerSlashCommands(
|
|
277
385
|
pi: ExtensionAPI,
|
|
278
386
|
state: SubagentState,
|
|
279
|
-
getSubagentSessionRoot: (parentSessionFile: string | null) => string,
|
|
280
387
|
): void {
|
|
281
388
|
pi.registerCommand("agents", {
|
|
282
389
|
description: "Open the Agents Manager",
|
|
283
390
|
handler: async (_args, ctx) => {
|
|
284
|
-
await openAgentManager(pi,
|
|
391
|
+
await openAgentManager(pi, ctx);
|
|
285
392
|
},
|
|
286
393
|
});
|
|
287
394
|
|
|
@@ -304,13 +411,13 @@ export function registerSlashCommands(
|
|
|
304
411
|
if (inline.reads && Array.isArray(inline.reads) && inline.reads.length > 0) {
|
|
305
412
|
finalTask = `[Read from: ${inline.reads.join(", ")}]\n\n${finalTask}`;
|
|
306
413
|
}
|
|
307
|
-
const params:
|
|
414
|
+
const params: SubagentParamsLike = { agent: agentName, task: finalTask, clarify: false, agentScope: "both" };
|
|
308
415
|
if (inline.output !== undefined) params.output = inline.output;
|
|
309
416
|
if (inline.skill !== undefined) params.skill = inline.skill;
|
|
310
417
|
if (inline.model) params.model = inline.model;
|
|
311
418
|
if (bg) params.async = true;
|
|
312
419
|
if (fork) params.context = "fork";
|
|
313
|
-
|
|
420
|
+
await runSlashSubagent(pi, ctx, params);
|
|
314
421
|
},
|
|
315
422
|
});
|
|
316
423
|
|
|
@@ -330,10 +437,10 @@ export function registerSlashCommands(
|
|
|
330
437
|
...(config.skill !== undefined ? { skill: config.skill } : {}),
|
|
331
438
|
...(config.progress !== undefined ? { progress: config.progress } : {}),
|
|
332
439
|
}));
|
|
333
|
-
const params:
|
|
440
|
+
const params: SubagentParamsLike = { chain, task: parsed.task, clarify: false, agentScope: "both" };
|
|
334
441
|
if (bg) params.async = true;
|
|
335
442
|
if (fork) params.context = "fork";
|
|
336
|
-
pi
|
|
443
|
+
await runSlashSubagent(pi, ctx, params);
|
|
337
444
|
},
|
|
338
445
|
});
|
|
339
446
|
|
|
@@ -354,16 +461,16 @@ export function registerSlashCommands(
|
|
|
354
461
|
...(config.skill !== undefined ? { skill: config.skill } : {}),
|
|
355
462
|
...(config.progress !== undefined ? { progress: config.progress } : {}),
|
|
356
463
|
}));
|
|
357
|
-
const params:
|
|
464
|
+
const params: SubagentParamsLike = { tasks, clarify: false, agentScope: "both" };
|
|
358
465
|
if (bg) params.async = true;
|
|
359
466
|
if (fork) params.context = "fork";
|
|
360
|
-
pi
|
|
467
|
+
await runSlashSubagent(pi, ctx, params);
|
|
361
468
|
},
|
|
362
469
|
});
|
|
363
470
|
|
|
364
471
|
pi.registerShortcut("ctrl+shift+a", {
|
|
365
472
|
handler: async (ctx) => {
|
|
366
|
-
await openAgentManager(pi,
|
|
473
|
+
await openAgentManager(pi, ctx);
|
|
367
474
|
},
|
|
368
475
|
});
|
|
369
476
|
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
2
|
+
import type { Message } from "@mariozechner/pi-ai";
|
|
3
|
+
import type { SubagentParamsLike } from "./subagent-executor.js";
|
|
4
|
+
import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.js";
|
|
5
|
+
import { type Details, type SingleResult, type Usage, SLASH_RESULT_TYPE } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export interface SlashMessageDetails {
|
|
8
|
+
requestId: string;
|
|
9
|
+
result: AgentToolResult<Details>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface SlashSnapshot {
|
|
13
|
+
result: AgentToolResult<Details>;
|
|
14
|
+
version: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface SequentialChainStepLike {
|
|
18
|
+
agent: string;
|
|
19
|
+
task?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ParallelChainStepLike {
|
|
23
|
+
parallel: Array<{ agent: string; task?: string }>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type ChainStepLike = SequentialChainStepLike | ParallelChainStepLike;
|
|
27
|
+
|
|
28
|
+
const liveSnapshots = new Map<string, SlashSnapshot>();
|
|
29
|
+
const finalSnapshots = new Map<string, SlashSnapshot>();
|
|
30
|
+
let versionCounter = 1;
|
|
31
|
+
|
|
32
|
+
const EMPTY_MESSAGES: Message[] = [];
|
|
33
|
+
const EMPTY_USAGE: Usage = {
|
|
34
|
+
input: 0,
|
|
35
|
+
output: 0,
|
|
36
|
+
cacheRead: 0,
|
|
37
|
+
cacheWrite: 0,
|
|
38
|
+
cost: 0,
|
|
39
|
+
turns: 0,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function nextVersion(): number {
|
|
43
|
+
return versionCounter++;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function cloneUsage(): Usage {
|
|
47
|
+
return { ...EMPTY_USAGE };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createPlaceholderResult(
|
|
51
|
+
agent: string,
|
|
52
|
+
task: string,
|
|
53
|
+
status: "pending" | "running",
|
|
54
|
+
index?: number,
|
|
55
|
+
): SingleResult {
|
|
56
|
+
return {
|
|
57
|
+
agent,
|
|
58
|
+
task,
|
|
59
|
+
exitCode: 0,
|
|
60
|
+
messages: EMPTY_MESSAGES,
|
|
61
|
+
usage: cloneUsage(),
|
|
62
|
+
progress: {
|
|
63
|
+
...(index !== undefined ? { index } : {}),
|
|
64
|
+
agent,
|
|
65
|
+
status,
|
|
66
|
+
task,
|
|
67
|
+
recentTools: [],
|
|
68
|
+
recentOutput: [],
|
|
69
|
+
toolCount: 0,
|
|
70
|
+
tokens: 0,
|
|
71
|
+
durationMs: 0,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function buildParallelInitialResult(params: SubagentParamsLike): AgentToolResult<Details> {
|
|
77
|
+
const tasks = params.tasks ?? [];
|
|
78
|
+
return {
|
|
79
|
+
content: [{ type: "text", text: tasks.map((task) => `${task.agent}: ${task.task}`).join("\n\n") }],
|
|
80
|
+
details: {
|
|
81
|
+
mode: "parallel",
|
|
82
|
+
...(params.context ? { context: params.context } : {}),
|
|
83
|
+
results: tasks.map((task, index) => createPlaceholderResult(task.agent, task.task, "running", index)),
|
|
84
|
+
progress: tasks.map((task, index) => ({
|
|
85
|
+
index,
|
|
86
|
+
agent: task.agent,
|
|
87
|
+
status: "running" as const,
|
|
88
|
+
task: task.task,
|
|
89
|
+
recentTools: [],
|
|
90
|
+
recentOutput: [],
|
|
91
|
+
toolCount: 0,
|
|
92
|
+
tokens: 0,
|
|
93
|
+
durationMs: 0,
|
|
94
|
+
})),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isParallelChainStep(step: ChainStepLike): step is ParallelChainStepLike {
|
|
100
|
+
return "parallel" in step && Array.isArray(step.parallel);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function chainStepLabel(step: ChainStepLike): string {
|
|
104
|
+
if (isParallelChainStep(step)) {
|
|
105
|
+
return `[${step.parallel.map((entry) => entry.agent).join("+")}]`;
|
|
106
|
+
}
|
|
107
|
+
return step.agent;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function flattenChainResults(chain: ChainStepLike[], fallbackTask: string | undefined): SingleResult[] {
|
|
111
|
+
const results: SingleResult[] = [];
|
|
112
|
+
let flatIndex = 0;
|
|
113
|
+
for (const step of chain) {
|
|
114
|
+
if (isParallelChainStep(step)) {
|
|
115
|
+
for (const task of step.parallel) {
|
|
116
|
+
results.push(createPlaceholderResult(task.agent, task.task ?? fallbackTask ?? "", results.length === 0 ? "running" : "pending", flatIndex));
|
|
117
|
+
flatIndex++;
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
results.push(createPlaceholderResult(step.agent, step.task ?? fallbackTask ?? "", results.length === 0 ? "running" : "pending", flatIndex));
|
|
122
|
+
flatIndex++;
|
|
123
|
+
}
|
|
124
|
+
return results;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function buildChainInitialResult(params: SubagentParamsLike): AgentToolResult<Details> {
|
|
128
|
+
const chain = (params.chain ?? []) as ChainStepLike[];
|
|
129
|
+
const results = flattenChainResults(chain, params.task);
|
|
130
|
+
return {
|
|
131
|
+
content: [{
|
|
132
|
+
type: "text",
|
|
133
|
+
text: results.map((result, index) => `Step ${index + 1}: ${result.agent}\n${result.task}`).join("\n\n"),
|
|
134
|
+
}],
|
|
135
|
+
details: {
|
|
136
|
+
mode: "chain",
|
|
137
|
+
...(params.context ? { context: params.context } : {}),
|
|
138
|
+
results,
|
|
139
|
+
progress: results.map((result, index) => ({
|
|
140
|
+
index,
|
|
141
|
+
agent: result.agent,
|
|
142
|
+
status: index === 0 ? "running" as const : "pending" as const,
|
|
143
|
+
task: result.task,
|
|
144
|
+
recentTools: [],
|
|
145
|
+
recentOutput: [],
|
|
146
|
+
toolCount: 0,
|
|
147
|
+
tokens: 0,
|
|
148
|
+
durationMs: 0,
|
|
149
|
+
})),
|
|
150
|
+
chainAgents: chain.map((step) => chainStepLabel(step)),
|
|
151
|
+
totalSteps: chain.length,
|
|
152
|
+
currentStepIndex: 0,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildSingleInitialResult(params: SubagentParamsLike): AgentToolResult<Details> {
|
|
158
|
+
const agent = params.agent ?? "subagent";
|
|
159
|
+
const task = params.task ?? "";
|
|
160
|
+
return {
|
|
161
|
+
content: [{ type: "text", text: task }],
|
|
162
|
+
details: {
|
|
163
|
+
mode: "single",
|
|
164
|
+
...(params.context ? { context: params.context } : {}),
|
|
165
|
+
results: [createPlaceholderResult(agent, task, "running")],
|
|
166
|
+
progress: [{
|
|
167
|
+
agent,
|
|
168
|
+
status: "running",
|
|
169
|
+
task,
|
|
170
|
+
recentTools: [],
|
|
171
|
+
recentOutput: [],
|
|
172
|
+
toolCount: 0,
|
|
173
|
+
tokens: 0,
|
|
174
|
+
durationMs: 0,
|
|
175
|
+
}],
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function buildSlashInitialResult(requestId: string, params: SubagentParamsLike): SlashMessageDetails {
|
|
181
|
+
const result = (params.tasks?.length ?? 0) > 0
|
|
182
|
+
? buildParallelInitialResult(params)
|
|
183
|
+
: (params.chain?.length ?? 0) > 0
|
|
184
|
+
? buildChainInitialResult(params)
|
|
185
|
+
: buildSingleInitialResult(params);
|
|
186
|
+
liveSnapshots.set(requestId, { result, version: nextVersion() });
|
|
187
|
+
finalSnapshots.delete(requestId);
|
|
188
|
+
return { requestId, result };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function cloneResultsWithProgress(
|
|
192
|
+
results: SingleResult[],
|
|
193
|
+
progress: NonNullable<Details["progress"]> | undefined,
|
|
194
|
+
): SingleResult[] {
|
|
195
|
+
return results.map((result, index) => {
|
|
196
|
+
const nextProgress = progress?.find((entry) => entry.index === index)
|
|
197
|
+
?? progress?.[index]
|
|
198
|
+
?? result.progress;
|
|
199
|
+
return nextProgress ? { ...result, progress: nextProgress } : result;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function applySlashUpdate(requestId: string, update: SlashSubagentUpdate): void {
|
|
204
|
+
const snapshot = liveSnapshots.get(requestId);
|
|
205
|
+
if (!snapshot) return;
|
|
206
|
+
const progress = update.progress;
|
|
207
|
+
if (!progress || !snapshot.result.details) return;
|
|
208
|
+
const currentStepIndex = progress.findIndex((entry) => entry.status === "running");
|
|
209
|
+
const nextDetails: Details = {
|
|
210
|
+
...snapshot.result.details,
|
|
211
|
+
progress,
|
|
212
|
+
results: cloneResultsWithProgress(snapshot.result.details.results, progress),
|
|
213
|
+
...(snapshot.result.details.mode === "chain" && currentStepIndex >= 0 ? { currentStepIndex } : {}),
|
|
214
|
+
};
|
|
215
|
+
liveSnapshots.set(requestId, {
|
|
216
|
+
result: {
|
|
217
|
+
...snapshot.result,
|
|
218
|
+
details: nextDetails,
|
|
219
|
+
},
|
|
220
|
+
version: nextVersion(),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function finalizeSlashResult(response: SlashSubagentResponse): SlashMessageDetails {
|
|
225
|
+
const snapshot = {
|
|
226
|
+
result: response.result,
|
|
227
|
+
version: nextVersion(),
|
|
228
|
+
};
|
|
229
|
+
finalSnapshots.set(response.requestId, snapshot);
|
|
230
|
+
liveSnapshots.delete(response.requestId);
|
|
231
|
+
return {
|
|
232
|
+
requestId: response.requestId,
|
|
233
|
+
result: response.result,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function failSlashResult(requestId: string, params: SubagentParamsLike, message: string): SlashMessageDetails {
|
|
238
|
+
const initial = buildSlashInitialResult(requestId, params).result;
|
|
239
|
+
const failedResults = initial.details.results.map((result) => ({
|
|
240
|
+
...result,
|
|
241
|
+
exitCode: 1,
|
|
242
|
+
error: message,
|
|
243
|
+
progress: result.progress ? { ...result.progress, status: "failed" as const } : result.progress,
|
|
244
|
+
}));
|
|
245
|
+
const result: AgentToolResult<Details> = {
|
|
246
|
+
content: [{ type: "text", text: message }],
|
|
247
|
+
details: {
|
|
248
|
+
...initial.details,
|
|
249
|
+
results: failedResults,
|
|
250
|
+
progress: failedResults.map((entry) => entry.progress!).filter(Boolean),
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
const snapshot = { result, version: nextVersion() };
|
|
254
|
+
finalSnapshots.set(requestId, snapshot);
|
|
255
|
+
liveSnapshots.delete(requestId);
|
|
256
|
+
return { requestId, result };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function isSlashMessageDetails(value: unknown): value is SlashMessageDetails {
|
|
260
|
+
if (!value || typeof value !== "object") return false;
|
|
261
|
+
const v = value as { requestId?: string; result?: { content?: unknown; details?: { results?: unknown } } };
|
|
262
|
+
if (typeof v.requestId !== "string" || !v.requestId) return false;
|
|
263
|
+
if (!v.result || !Array.isArray(v.result.content)) return false;
|
|
264
|
+
return !!v.result.details && Array.isArray(v.result.details.results);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function resolveSlashMessageDetails(value: unknown): SlashMessageDetails | undefined {
|
|
268
|
+
return isSlashMessageDetails(value) ? value : undefined;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function getSlashRenderableSnapshot(details: SlashMessageDetails): SlashSnapshot {
|
|
272
|
+
return finalSnapshots.get(details.requestId)
|
|
273
|
+
?? liveSnapshots.get(details.requestId)
|
|
274
|
+
?? { result: details.result, version: 0 };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function restoreSlashFinalSnapshots(entries: unknown[]): void {
|
|
278
|
+
liveSnapshots.clear();
|
|
279
|
+
finalSnapshots.clear();
|
|
280
|
+
for (const entry of entries) {
|
|
281
|
+
const e = entry as { type?: string; message?: { role?: string; customType?: string; display?: boolean; details?: unknown } };
|
|
282
|
+
if (e?.type !== "message") continue;
|
|
283
|
+
const m = e.message;
|
|
284
|
+
if (!m || m.role !== "custom" || m.customType !== SLASH_RESULT_TYPE || m.display !== false) continue;
|
|
285
|
+
const details = resolveSlashMessageDetails(m.details);
|
|
286
|
+
if (!details) continue;
|
|
287
|
+
finalSnapshots.set(details.requestId, { result: details.result, version: nextVersion() });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function clearSlashSnapshots(): void {
|
|
292
|
+
liveSnapshots.clear();
|
|
293
|
+
finalSnapshots.clear();
|
|
294
|
+
}
|
package/subagent-executor.ts
CHANGED
|
@@ -51,7 +51,7 @@ interface TaskParam {
|
|
|
51
51
|
progress?: boolean;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
interface SubagentParamsLike {
|
|
54
|
+
export interface SubagentParamsLike {
|
|
55
55
|
action?: string;
|
|
56
56
|
agent?: string;
|
|
57
57
|
task?: string;
|
|
@@ -107,6 +107,7 @@ function validateExecutionInput(
|
|
|
107
107
|
hasChain: boolean,
|
|
108
108
|
hasTasks: boolean,
|
|
109
109
|
hasSingle: boolean,
|
|
110
|
+
allowClarifyTaskPrompt: boolean,
|
|
110
111
|
): AgentToolResult<Details> | null {
|
|
111
112
|
if (Number(hasChain) + Number(hasTasks) + Number(hasSingle) !== 1) {
|
|
112
113
|
return {
|
|
@@ -139,7 +140,7 @@ function validateExecutionInput(
|
|
|
139
140
|
details: { mode: "chain" as const, results: [] },
|
|
140
141
|
};
|
|
141
142
|
}
|
|
142
|
-
} else if (!(firstStep as SequentialStep).task && !params.task) {
|
|
143
|
+
} else if (!(firstStep as SequentialStep).task && !params.task && !allowClarifyTaskPrompt) {
|
|
143
144
|
return {
|
|
144
145
|
content: [{ type: "text", text: "First step in chain must have a task" }],
|
|
145
146
|
isError: true,
|
|
@@ -806,8 +807,19 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
806
807
|
const hasChain = (params.chain?.length ?? 0) > 0;
|
|
807
808
|
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
|
808
809
|
const hasSingle = Boolean(params.agent && params.task);
|
|
810
|
+
const allowClarifyTaskPrompt = hasChain
|
|
811
|
+
&& params.clarify === true
|
|
812
|
+
&& ctx.hasUI
|
|
813
|
+
&& !(params.chain?.some(isParallelStep) ?? false);
|
|
809
814
|
|
|
810
|
-
const validationError = validateExecutionInput(
|
|
815
|
+
const validationError = validateExecutionInput(
|
|
816
|
+
params,
|
|
817
|
+
agents,
|
|
818
|
+
hasChain,
|
|
819
|
+
hasTasks,
|
|
820
|
+
hasSingle,
|
|
821
|
+
allowClarifyTaskPrompt,
|
|
822
|
+
);
|
|
811
823
|
if (validationError) return validationError;
|
|
812
824
|
|
|
813
825
|
let sessionFileForIndex: (idx?: number) => string | undefined = () => undefined;
|
package/types.ts
CHANGED
|
@@ -263,6 +263,12 @@ export const MAX_CONCURRENCY = 4;
|
|
|
263
263
|
export const RESULTS_DIR = path.join(os.tmpdir(), "pi-async-subagent-results");
|
|
264
264
|
export const ASYNC_DIR = path.join(os.tmpdir(), "pi-async-subagent-runs");
|
|
265
265
|
export const WIDGET_KEY = "subagent-async";
|
|
266
|
+
export const SLASH_RESULT_TYPE = "subagent-slash-result";
|
|
267
|
+
export const SLASH_SUBAGENT_REQUEST_EVENT = "subagent:slash:request";
|
|
268
|
+
export const SLASH_SUBAGENT_STARTED_EVENT = "subagent:slash:started";
|
|
269
|
+
export const SLASH_SUBAGENT_RESPONSE_EVENT = "subagent:slash:response";
|
|
270
|
+
export const SLASH_SUBAGENT_UPDATE_EVENT = "subagent:slash:update";
|
|
271
|
+
export const SLASH_SUBAGENT_CANCEL_EVENT = "subagent:slash:cancel";
|
|
266
272
|
export const POLL_INTERVAL_MS = 250;
|
|
267
273
|
export const MAX_WIDGET_JOBS = 4;
|
|
268
274
|
export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;
|