pi-crew 0.1.17 → 0.1.19
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 +18 -0
- package/README.md +8 -2
- package/docs/usage.md +4 -1
- package/package.json +1 -1
- package/schema.json +6 -3
- package/src/config/config.ts +6 -0
- package/src/extension/register.ts +217 -7
- package/src/extension/team-recommendation.ts +9 -3
- package/src/extension/team-tool.ts +83 -10
- package/src/runtime/child-pi.ts +7 -3
- package/src/runtime/concurrency.ts +42 -0
- package/src/runtime/progress-event-coalescer.ts +43 -0
- package/src/runtime/subagent-manager.ts +202 -0
- package/src/runtime/task-display.ts +38 -0
- package/src/runtime/task-runner.ts +14 -5
- package/src/runtime/team-runner.ts +244 -240
- package/src/ui/live-run-sidebar.ts +95 -0
- package/src/ui/powerbar-publisher.ts +22 -6
- package/teams/parallel-research.team.md +14 -0
- package/workflows/parallel-research.workflow.md +50 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.19
|
|
4
|
+
|
|
5
|
+
- Added Claude-style `Agent`, `get_subagent_result`, and `steer_subagent` tools backed by pi-crew's durable worker runtime, plus conflict-safe `crew_agent`, `crew_agent_result`, and `crew_agent_steer` aliases.
|
|
6
|
+
- Added a durable subagent manager with background queueing, completion notifications, result joins, session-bound cleanup, and direct single-agent runs via `team run agent=...`.
|
|
7
|
+
- Disabled risky auto-opening of the right sidebar by default, added foreground completion notifications, and reduced duplicate widget/sidebar UI.
|
|
8
|
+
- Added progress coalescing and workflow concurrency helpers to keep foreground sessions responsive during busy worker output.
|
|
9
|
+
- Fixed live-session runs being classified as scaffold when workers are enabled and hardened session switch/shutdown cleanup for foreground child processes.
|
|
10
|
+
|
|
11
|
+
## 0.1.18
|
|
12
|
+
|
|
13
|
+
- Added a built-in `parallel-research` team/workflow for map-reduce style source audits with dynamic `Source/pi-*` fanout and parallel explorer shards.
|
|
14
|
+
- Made the live right sidebar the default foreground UI: active foreground runs auto-open a top-right live sidebar when the terminal is wide enough.
|
|
15
|
+
- Added live sidebar sections for active agents, waiting tasks, completed agents, task graph, model, tool, and token/usage details.
|
|
16
|
+
- Stopped materializing queued dependency tasks as child-process agents; status now separates active agents, waiting tasks, and completed agents.
|
|
17
|
+
- Added workflow-aware default concurrency so research/parallel-research can use ready parallel work instead of always running one worker.
|
|
18
|
+
- Dropped user/system prompt messages from child event persistence to avoid prompt/context leakage in agent event logs.
|
|
19
|
+
- Tightened child event compaction with separate assistant/tool input/tool result caps and improved powerbar active/waiting/model/token summaries.
|
|
20
|
+
|
|
3
21
|
## 0.1.17
|
|
4
22
|
|
|
5
23
|
- Fixed terminal/completed workers being incorrectly escalated as stale heartbeat blockers after all tasks completed.
|
package/README.md
CHANGED
|
@@ -52,6 +52,7 @@ Current highlights:
|
|
|
52
52
|
- run-level and task-level mailbox files with validation/repair support
|
|
53
53
|
- `/team-manager` interactive helper
|
|
54
54
|
- `/team-dashboard` custom TUI overlay with progress preview, action shortcuts, and reload
|
|
55
|
+
- `parallel-research` team/workflow for dynamic `Source/pi-*` fanout and parallel shard exploration
|
|
55
56
|
- package polish: `schema.json`, TypeScript semantic check, strip-types import smoke, cross-platform CI workflow, dry-run package verification
|
|
56
57
|
|
|
57
58
|
## Install
|
|
@@ -171,7 +172,10 @@ Supported config:
|
|
|
171
172
|
"widgetMaxLines": 8,
|
|
172
173
|
"powerbar": true,
|
|
173
174
|
"dashboardPlacement": "right",
|
|
174
|
-
"dashboardWidth":
|
|
175
|
+
"dashboardWidth": 56,
|
|
176
|
+
"dashboardLiveRefreshMs": 1000,
|
|
177
|
+
"autoOpenDashboard": false,
|
|
178
|
+
"autoOpenDashboardForForegroundRuns": true,
|
|
175
179
|
"showModel": true,
|
|
176
180
|
"showTokens": true,
|
|
177
181
|
"showTools": true
|
|
@@ -186,7 +190,9 @@ Safety notes:
|
|
|
186
190
|
UI notes:
|
|
187
191
|
|
|
188
192
|
- `widgetPlacement`/`widgetMaxLines` keep the persistent active-run widget compact.
|
|
189
|
-
- `dashboardPlacement: "right"`
|
|
193
|
+
- `dashboardPlacement: "right"` is the default for `/team-dashboard`; automatic overlay opening is opt-in because Pi custom overlays can be modal/focus-capturing in some terminals.
|
|
194
|
+
- `autoOpenDashboard`/`autoOpenDashboardForForegroundRuns` control whether the live sidebar opens automatically.
|
|
195
|
+
- `dashboardLiveRefreshMs` controls the live sidebar refresh cadence.
|
|
190
196
|
- `showModel`, `showTokens`, and `showTools` show worker model attempts, token usage, and tool activity in dashboard agent rows.
|
|
191
197
|
|
|
192
198
|
Show config:
|
package/docs/usage.md
CHANGED
|
@@ -34,7 +34,10 @@ Supported fields:
|
|
|
34
34
|
"widgetMaxLines": 8,
|
|
35
35
|
"powerbar": true,
|
|
36
36
|
"dashboardPlacement": "right",
|
|
37
|
-
"dashboardWidth":
|
|
37
|
+
"dashboardWidth": 56,
|
|
38
|
+
"dashboardLiveRefreshMs": 1000,
|
|
39
|
+
"autoOpenDashboard": false,
|
|
40
|
+
"autoOpenDashboardForForegroundRuns": true,
|
|
38
41
|
"showModel": true,
|
|
39
42
|
"showTokens": true,
|
|
40
43
|
"showTools": true
|
package/package.json
CHANGED
package/schema.json
CHANGED
|
@@ -87,9 +87,12 @@
|
|
|
87
87
|
"widgetPlacement": { "type": "string", "enum": ["aboveEditor", "belowEditor"] },
|
|
88
88
|
"widgetMaxLines": { "type": "integer", "minimum": 1, "maximum": 50 },
|
|
89
89
|
"powerbar": { "type": "boolean" },
|
|
90
|
-
"dashboardPlacement": { "type": "string", "enum": ["center", "right"], "description": "Place /team-dashboard as a centered overlay or right-side panel." },
|
|
91
|
-
"dashboardWidth": { "type": "integer", "minimum": 32, "maximum": 120 },
|
|
92
|
-
"
|
|
90
|
+
"dashboardPlacement": { "type": "string", "enum": ["center", "right"], "default": "right", "description": "Place /team-dashboard as a centered overlay or right-side panel." },
|
|
91
|
+
"dashboardWidth": { "type": "integer", "minimum": 32, "maximum": 120, "default": 56 },
|
|
92
|
+
"dashboardLiveRefreshMs": { "type": "integer", "minimum": 250, "maximum": 60000, "default": 1000 },
|
|
93
|
+
"autoOpenDashboard": { "type": "boolean", "default": false, "description": "Opt in to automatically opening the live right sidebar for foreground runs when UI is available. Disabled by default because Pi overlays are modal in some terminals." },
|
|
94
|
+
"autoOpenDashboardForForegroundRuns": { "type": "boolean", "default": true },
|
|
95
|
+
"showModel": { "type": "boolean", "default": true, "description": "Show worker model attempts in dashboard agent rows." },
|
|
93
96
|
"showTokens": { "type": "boolean", "description": "Show token usage in dashboard agent rows." },
|
|
94
97
|
"showTools": { "type": "boolean", "description": "Show tool activity in dashboard agent rows." }
|
|
95
98
|
}
|
package/src/config/config.ts
CHANGED
|
@@ -53,6 +53,9 @@ export interface CrewUiConfig {
|
|
|
53
53
|
powerbar?: boolean;
|
|
54
54
|
dashboardPlacement?: "center" | "right";
|
|
55
55
|
dashboardWidth?: number;
|
|
56
|
+
dashboardLiveRefreshMs?: number;
|
|
57
|
+
autoOpenDashboard?: boolean;
|
|
58
|
+
autoOpenDashboardForForegroundRuns?: boolean;
|
|
56
59
|
showModel?: boolean;
|
|
57
60
|
showTokens?: boolean;
|
|
58
61
|
showTools?: boolean;
|
|
@@ -316,6 +319,9 @@ function parseUiConfig(value: unknown): CrewUiConfig | undefined {
|
|
|
316
319
|
powerbar: typeof obj.powerbar === "boolean" ? obj.powerbar : undefined,
|
|
317
320
|
dashboardPlacement: obj.dashboardPlacement === "center" || obj.dashboardPlacement === "right" ? obj.dashboardPlacement : undefined,
|
|
318
321
|
dashboardWidth: parsePositiveInteger(obj.dashboardWidth, 120),
|
|
322
|
+
dashboardLiveRefreshMs: parsePositiveInteger(obj.dashboardLiveRefreshMs, 60_000),
|
|
323
|
+
autoOpenDashboard: typeof obj.autoOpenDashboard === "boolean" ? obj.autoOpenDashboard : undefined,
|
|
324
|
+
autoOpenDashboardForForegroundRuns: typeof obj.autoOpenDashboardForForegroundRuns === "boolean" ? obj.autoOpenDashboardForForegroundRuns : undefined,
|
|
319
325
|
showModel: typeof obj.showModel === "boolean" ? obj.showModel : undefined,
|
|
320
326
|
showTokens: typeof obj.showTokens === "boolean" ? obj.showTokens : undefined,
|
|
321
327
|
showTools: typeof obj.showTools === "boolean" ? obj.showTools : undefined,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
1
2
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { Type } from "typebox";
|
|
2
4
|
import { loadConfig } from "../config/config.ts";
|
|
3
5
|
import { registerAutonomousPolicy } from "./autonomous-policy.ts";
|
|
4
6
|
import { TeamToolParams, type TeamToolParamsValue } from "../schema/team-tool-schema.ts";
|
|
@@ -9,6 +11,7 @@ import { handleTeamManagerCommand } from "./team-manager-command.ts";
|
|
|
9
11
|
import { handleTeamTool, type TeamToolDetails } from "./team-tool.ts";
|
|
10
12
|
import { listRecentRuns } from "./run-index.ts";
|
|
11
13
|
import { RunDashboard, type RunDashboardSelection } from "../ui/run-dashboard.ts";
|
|
14
|
+
import { LiveRunSidebar } from "../ui/live-run-sidebar.ts";
|
|
12
15
|
import { registerPiCrewRpc, type PiCrewRpcHandle } from "./cross-extension-rpc.ts";
|
|
13
16
|
import { stopCrewWidget, updateCrewWidget, type CrewWidgetState } from "../ui/crew-widget.ts";
|
|
14
17
|
import { clearPiCrewPowerbar, registerPiCrewPowerbarSegments, updatePiCrewPowerbar } from "../ui/powerbar-publisher.ts";
|
|
@@ -16,6 +19,7 @@ import { DurableTextViewer, DurableTranscriptViewer } from "../ui/transcript-vie
|
|
|
16
19
|
import { loadRunManifestById } from "../state/state-store.ts";
|
|
17
20
|
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
18
21
|
import { terminateActiveChildPiProcesses } from "../runtime/child-pi.ts";
|
|
22
|
+
import { SubagentManager, type SubagentRecord, type SubagentSpawnOptions } from "../runtime/subagent-manager.ts";
|
|
19
23
|
|
|
20
24
|
function parseRunArgs(args: string): TeamToolParamsValue {
|
|
21
25
|
const tokens = args.match(/"[^"]*"|'[^']*'|\S+/g)?.map((token) => token.replace(/^['"]|['"]$/g, "")) ?? [];
|
|
@@ -99,6 +103,55 @@ function setNestedConfig(config: Record<string, unknown>, key: string, value: un
|
|
|
99
103
|
target[parts[parts.length - 1]!] = value;
|
|
100
104
|
}
|
|
101
105
|
|
|
106
|
+
function sendFollowUp(pi: ExtensionAPI, content: string): void {
|
|
107
|
+
const sender = (pi as unknown as { sendMessage?: (message: unknown, options?: unknown) => void }).sendMessage;
|
|
108
|
+
if (typeof sender !== "function") return;
|
|
109
|
+
sender.call(pi, { customType: "pi-crew-subagent-notification", content, display: true }, { deliverAs: "followUp", triggerTurn: true });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function formatSubagentRecord(record: SubagentRecord): string {
|
|
113
|
+
const duration = record.completedAt ? `${Math.round((record.completedAt - record.startedAt) / 1000)}s` : "running";
|
|
114
|
+
return [
|
|
115
|
+
`Agent: ${record.id}`,
|
|
116
|
+
`Type: ${record.type}`,
|
|
117
|
+
`Status: ${record.status}`,
|
|
118
|
+
record.runId ? `Run: ${record.runId}` : undefined,
|
|
119
|
+
`Description: ${record.description}`,
|
|
120
|
+
record.model ? `Model: ${record.model}` : undefined,
|
|
121
|
+
`Duration: ${duration}`,
|
|
122
|
+
record.error ? `Error: ${record.error}` : undefined,
|
|
123
|
+
].filter((line): line is string => Boolean(line)).join("\n");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function readSubagentRunResult(ctx: ExtensionContext | ExtensionCommandContext, record: SubagentRecord): string | undefined {
|
|
127
|
+
if (!record.runId) return record.result;
|
|
128
|
+
const loaded = loadRunManifestById(ctx.cwd, record.runId);
|
|
129
|
+
const task = loaded?.tasks.find((item) => item.resultArtifact) ?? loaded?.tasks[0];
|
|
130
|
+
const path = task?.resultArtifact?.path;
|
|
131
|
+
if (!path) return undefined;
|
|
132
|
+
try {
|
|
133
|
+
return fs.readFileSync(path, "utf-8").trim();
|
|
134
|
+
} catch {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function subagentToolResult(text: string, details: Record<string, unknown> = {}, isError = false) {
|
|
140
|
+
return { content: [{ type: "text" as const, text }], details, isError };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function __test__subagentSpawnParams(params: Record<string, unknown>, ctx: Pick<ExtensionContext, "cwd">): SubagentSpawnOptions {
|
|
144
|
+
return {
|
|
145
|
+
cwd: ctx.cwd,
|
|
146
|
+
type: typeof params.subagent_type === "string" && params.subagent_type.trim() ? params.subagent_type.trim() : "executor",
|
|
147
|
+
description: typeof params.description === "string" && params.description.trim() ? params.description.trim() : "pi-crew subagent",
|
|
148
|
+
prompt: typeof params.prompt === "string" ? params.prompt : "",
|
|
149
|
+
background: params.run_in_background === true,
|
|
150
|
+
model: typeof params.model === "string" && params.model.trim() ? params.model.trim() : undefined,
|
|
151
|
+
maxTurns: typeof params.max_turns === "number" && Number.isFinite(params.max_turns) ? params.max_turns : undefined,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
102
155
|
export function registerPiTeams(pi: ExtensionAPI): void {
|
|
103
156
|
const globalStore = globalThis as Record<string, unknown>;
|
|
104
157
|
const runtimeCleanupStoreKey = "__piCrewRuntimeCleanup";
|
|
@@ -111,8 +164,51 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
111
164
|
let rpcHandle: PiCrewRpcHandle | undefined;
|
|
112
165
|
let cleanedUp = false;
|
|
113
166
|
const widgetState: CrewWidgetState = { frame: 0 };
|
|
167
|
+
const subagentManager = new SubagentManager(4, (record) => {
|
|
168
|
+
if (!record.background || record.resultConsumed) return;
|
|
169
|
+
if (record.status === "completed" || record.status === "failed" || record.status === "cancelled" || record.status === "error") {
|
|
170
|
+
sendFollowUp(pi, [`pi-crew subagent ${record.id} ${record.status}.`, record.runId ? `Run: ${record.runId}` : undefined, `Use get_subagent_result with agent_id=${record.id} for output.`].filter((line): line is string => Boolean(line)).join("\n"));
|
|
171
|
+
}
|
|
172
|
+
});
|
|
114
173
|
const foregroundControllers = new Set<AbortController>();
|
|
115
|
-
|
|
174
|
+
let liveSidebarRunId: string | undefined;
|
|
175
|
+
let liveSidebarTimer: ReturnType<typeof setInterval> | undefined;
|
|
176
|
+
const requestRender = (ctx: ExtensionContext): void => (ctx.ui as { requestRender?: () => void }).requestRender?.();
|
|
177
|
+
const stopSessionBoundSubagents = (): void => {
|
|
178
|
+
for (const controller of foregroundControllers) controller.abort();
|
|
179
|
+
foregroundControllers.clear();
|
|
180
|
+
subagentManager.abortAll();
|
|
181
|
+
terminateActiveChildPiProcesses();
|
|
182
|
+
if (liveSidebarTimer) clearInterval(liveSidebarTimer);
|
|
183
|
+
liveSidebarTimer = undefined;
|
|
184
|
+
liveSidebarRunId = undefined;
|
|
185
|
+
if (currentCtx) stopCrewWidget(currentCtx, widgetState, loadConfig(currentCtx.cwd).config.ui);
|
|
186
|
+
clearPiCrewPowerbar(pi.events);
|
|
187
|
+
};
|
|
188
|
+
const openLiveSidebar = (ctx: ExtensionContext, runId: string): void => {
|
|
189
|
+
const uiConfig = loadConfig(ctx.cwd).config.ui;
|
|
190
|
+
const autoOpen = uiConfig?.autoOpenDashboard === true;
|
|
191
|
+
const foregroundAutoOpen = uiConfig?.autoOpenDashboardForForegroundRuns !== false;
|
|
192
|
+
if (!ctx.hasUI || !autoOpen || !foregroundAutoOpen || (uiConfig?.dashboardPlacement ?? "right") !== "right") return;
|
|
193
|
+
if (liveSidebarRunId === runId) return;
|
|
194
|
+
if (liveSidebarTimer) clearInterval(liveSidebarTimer);
|
|
195
|
+
liveSidebarRunId = runId;
|
|
196
|
+
ctx.ui.setWidget("pi-crew", undefined, { placement: uiConfig?.widgetPlacement ?? "aboveEditor" });
|
|
197
|
+
ctx.ui.setWidget("pi-crew-active", undefined, { placement: uiConfig?.widgetPlacement ?? "aboveEditor" });
|
|
198
|
+
const width = Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? 56));
|
|
199
|
+
liveSidebarTimer = setInterval(() => requestRender(ctx), uiConfig?.dashboardLiveRefreshMs ?? 1000);
|
|
200
|
+
liveSidebarTimer.unref?.();
|
|
201
|
+
void ctx.ui.custom<undefined>((_tui, theme, _keybindings, done) => new LiveRunSidebar({ cwd: ctx.cwd, runId, done, theme, config: uiConfig }), {
|
|
202
|
+
overlay: true,
|
|
203
|
+
overlayOptions: { width, minWidth: 40, maxHeight: "100%", anchor: "top-right", offsetX: 0, offsetY: 0, margin: { top: 0, right: 0, bottom: 0, left: 0 }, visible: (termWidth: number) => termWidth >= 100 },
|
|
204
|
+
}).finally(() => {
|
|
205
|
+
if (liveSidebarRunId === runId) liveSidebarRunId = undefined;
|
|
206
|
+
if (liveSidebarTimer) clearInterval(liveSidebarTimer);
|
|
207
|
+
liveSidebarTimer = undefined;
|
|
208
|
+
updateCrewWidget(ctx, widgetState, loadConfig(ctx.cwd).config.ui);
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
const startForegroundRun = (ctx: ExtensionContext, runner: (signal?: AbortSignal) => Promise<void>, runId?: string): void => {
|
|
116
212
|
const controller = new AbortController();
|
|
117
213
|
foregroundControllers.add(controller);
|
|
118
214
|
setImmediate(() => {
|
|
@@ -123,6 +219,12 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
123
219
|
})
|
|
124
220
|
.finally(() => {
|
|
125
221
|
foregroundControllers.delete(controller);
|
|
222
|
+
if (runId) {
|
|
223
|
+
const loaded = loadRunManifestById(ctx.cwd, runId);
|
|
224
|
+
const status = loaded?.manifest.status ?? "finished";
|
|
225
|
+
const level = status === "failed" || status === "blocked" ? "error" : status === "cancelled" ? "warning" : "info";
|
|
226
|
+
ctx.ui.notify(`pi-crew run ${runId} ${status}. Use /team-summary ${runId} or /team-status ${runId}.`, level as "info" | "warning" | "error");
|
|
227
|
+
}
|
|
126
228
|
if (currentCtx) {
|
|
127
229
|
const config = loadConfig(currentCtx.cwd).config.ui;
|
|
128
230
|
updateCrewWidget(currentCtx, widgetState, config);
|
|
@@ -136,9 +238,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
136
238
|
const cleanupRuntime = (): void => {
|
|
137
239
|
if (cleanedUp) return;
|
|
138
240
|
cleanedUp = true;
|
|
139
|
-
|
|
140
|
-
foregroundControllers.clear();
|
|
141
|
-
terminateActiveChildPiProcesses();
|
|
241
|
+
stopSessionBoundSubagents();
|
|
142
242
|
stopAsyncRunNotifier(notifierState);
|
|
143
243
|
stopCrewWidget(currentCtx, widgetState, currentCtx ? loadConfig(currentCtx.cwd).config.ui : undefined);
|
|
144
244
|
clearPiCrewPowerbar(pi.events);
|
|
@@ -152,6 +252,8 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
152
252
|
pi.on("session_start", (_event, ctx) => {
|
|
153
253
|
cleanedUp = false;
|
|
154
254
|
currentCtx = ctx;
|
|
255
|
+
if (widgetState.interval) clearInterval(widgetState.interval);
|
|
256
|
+
widgetState.interval = undefined;
|
|
155
257
|
notifyActiveRuns(ctx);
|
|
156
258
|
const loadedConfig = loadConfig(ctx.cwd);
|
|
157
259
|
registerPiCrewPowerbarSegments(pi.events, loadedConfig.config.ui);
|
|
@@ -161,11 +263,19 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
161
263
|
widgetState.interval = setInterval(() => {
|
|
162
264
|
if (!currentCtx) return;
|
|
163
265
|
const config = loadConfig(currentCtx.cwd).config.ui;
|
|
164
|
-
|
|
266
|
+
if (liveSidebarRunId) {
|
|
267
|
+
currentCtx.ui.setWidget("pi-crew", undefined, { placement: config?.widgetPlacement ?? "aboveEditor" });
|
|
268
|
+
currentCtx.ui.setWidget("pi-crew-active", undefined, { placement: config?.widgetPlacement ?? "aboveEditor" });
|
|
269
|
+
} else {
|
|
270
|
+
updateCrewWidget(currentCtx, widgetState, config);
|
|
271
|
+
}
|
|
165
272
|
updatePiCrewPowerbar(pi.events, currentCtx.cwd, config);
|
|
166
273
|
}, 1000);
|
|
167
274
|
widgetState.interval.unref?.();
|
|
168
275
|
});
|
|
276
|
+
pi.on("session_before_switch", () => {
|
|
277
|
+
stopSessionBoundSubagents();
|
|
278
|
+
});
|
|
169
279
|
pi.on("session_shutdown", () => {
|
|
170
280
|
cleanupRuntime();
|
|
171
281
|
});
|
|
@@ -182,7 +292,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
182
292
|
const abort = (): void => controller.abort();
|
|
183
293
|
signal?.addEventListener("abort", abort, { once: true });
|
|
184
294
|
try {
|
|
185
|
-
const output = await handleTeamTool(params as TeamToolParamsValue, { ...ctx, signal: controller.signal, startForegroundRun: (runner) => startForegroundRun(ctx, runner) });
|
|
295
|
+
const output = await handleTeamTool(params as TeamToolParamsValue, { ...ctx, signal: controller.signal, startForegroundRun: (runner, runId) => startForegroundRun(ctx, runner, runId), onRunStarted: (runId) => openLiveSidebar(ctx, runId) });
|
|
186
296
|
const config = loadConfig(ctx.cwd).config.ui;
|
|
187
297
|
updateCrewWidget(ctx, widgetState, config);
|
|
188
298
|
updatePiCrewPowerbar(pi.events, ctx.cwd, config);
|
|
@@ -196,6 +306,106 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
196
306
|
|
|
197
307
|
pi.registerTool(tool);
|
|
198
308
|
|
|
309
|
+
const agentTool: ToolDefinition = {
|
|
310
|
+
name: "Agent",
|
|
311
|
+
label: "Agent",
|
|
312
|
+
description: "Launch a real pi-crew subagent. Uses pi-crew's durable child-process runtime by default; set run_in_background=true for parallel/background work, then use get_subagent_result.",
|
|
313
|
+
promptSnippet: "Use Agent to delegate focused work to a real pi-crew subagent. Use run_in_background=true for parallel work and get_subagent_result to join results.",
|
|
314
|
+
promptGuidelines: [
|
|
315
|
+
"Use Agent for independent exploration, review, verification, or implementation subtasks instead of doing all work in the parent turn.",
|
|
316
|
+
"For parallel work, launch multiple Agent calls with run_in_background=true, then call get_subagent_result for each result.",
|
|
317
|
+
"Available pi-crew subagent types include explorer, planner, analyst, executor, reviewer, verifier, writer, security-reviewer, and test-engineer.",
|
|
318
|
+
],
|
|
319
|
+
parameters: Type.Object({
|
|
320
|
+
prompt: Type.String({ description: "The task for the subagent to perform." }),
|
|
321
|
+
description: Type.String({ description: "Short 3-5 word task description." }),
|
|
322
|
+
subagent_type: Type.String({ description: "pi-crew agent name, e.g. explorer, planner, executor, reviewer, verifier, writer, security-reviewer, test-engineer." }),
|
|
323
|
+
model: Type.Optional(Type.String({ description: "Optional model override. If omitted, pi-crew uses Pi-configured model fallback." })),
|
|
324
|
+
max_turns: Type.Optional(Type.Number({ description: "Reserved for live-session subagents; child-process runtime may ignore this." })),
|
|
325
|
+
run_in_background: Type.Optional(Type.Boolean({ description: "Run in background and return an agent ID immediately." })),
|
|
326
|
+
}) as never,
|
|
327
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
328
|
+
const options = __test__subagentSpawnParams(params as Record<string, unknown>, ctx);
|
|
329
|
+
if (!options.prompt.trim()) return subagentToolResult("Agent requires prompt.", {}, true);
|
|
330
|
+
const runner = async (spawnOptions: SubagentSpawnOptions, childSignal?: AbortSignal) => handleTeamTool({
|
|
331
|
+
action: "run",
|
|
332
|
+
agent: spawnOptions.type,
|
|
333
|
+
goal: spawnOptions.prompt,
|
|
334
|
+
model: spawnOptions.model,
|
|
335
|
+
async: false,
|
|
336
|
+
}, spawnOptions.background ? { ...ctx, signal: childSignal, startForegroundRun: (run, runId) => startForegroundRun(ctx, run, runId), onRunStarted: (runId) => openLiveSidebar(ctx, runId) } : { ...ctx, signal: childSignal });
|
|
337
|
+
const record = subagentManager.spawn(options, runner, options.background ? undefined : signal);
|
|
338
|
+
if (options.background || record.status === "queued") {
|
|
339
|
+
return subagentToolResult([`Agent ${record.status === "queued" ? "queued" : "started"}.`, `Agent ID: ${record.id}`, `Type: ${record.type}`, `Description: ${record.description}`, "Use get_subagent_result to retrieve output. Do not duplicate this agent's work."].join("\n"), { agentId: record.id, status: record.status });
|
|
340
|
+
}
|
|
341
|
+
await record.promise;
|
|
342
|
+
const output = readSubagentRunResult(ctx, record) ?? record.result ?? "No output.";
|
|
343
|
+
return subagentToolResult([`Agent ${record.id} ${record.status}.`, "", output].join("\n"), { agentId: record.id, runId: record.runId, status: record.status }, record.status === "failed" || record.status === "error");
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const getSubagentResultTool: ToolDefinition = {
|
|
348
|
+
name: "get_subagent_result",
|
|
349
|
+
label: "Get Agent Result",
|
|
350
|
+
description: "Check status and retrieve results from a pi-crew background subagent.",
|
|
351
|
+
parameters: Type.Object({
|
|
352
|
+
agent_id: Type.String({ description: "Agent ID returned by Agent." }),
|
|
353
|
+
wait: Type.Optional(Type.Boolean({ description: "Wait for completion before returning." })),
|
|
354
|
+
verbose: Type.Optional(Type.Boolean({ description: "Include status metadata before output." })),
|
|
355
|
+
}) as never,
|
|
356
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
357
|
+
const p = params as { agent_id?: string; wait?: boolean; verbose?: boolean };
|
|
358
|
+
if (!p.agent_id) return subagentToolResult("get_subagent_result requires agent_id.", {}, true);
|
|
359
|
+
const record = subagentManager.getRecord(p.agent_id);
|
|
360
|
+
if (!record) return subagentToolResult(`Agent not found: ${p.agent_id}`, {}, true);
|
|
361
|
+
let current = record;
|
|
362
|
+
if (p.wait && (current.status === "running" || current.status === "queued")) {
|
|
363
|
+
current.resultConsumed = true;
|
|
364
|
+
current = await subagentManager.waitForRecord(current.id) ?? current;
|
|
365
|
+
}
|
|
366
|
+
const output = readSubagentRunResult(ctx, current);
|
|
367
|
+
if (current.status !== "running" && current.status !== "queued") current.resultConsumed = true;
|
|
368
|
+
const text = [p.verbose ? formatSubagentRecord(current) : undefined, output ? `${p.verbose ? "\n" : ""}${output}` : current.status === "running" || current.status === "queued" ? "Agent is still running. Use wait=true or check again later." : current.error ?? "No output."].filter((line): line is string => Boolean(line)).join("\n");
|
|
369
|
+
return subagentToolResult(text, { agentId: current.id, runId: current.runId, status: current.status }, current.status === "failed" || current.status === "error");
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const steerSubagentTool: ToolDefinition = {
|
|
374
|
+
name: "steer_subagent",
|
|
375
|
+
label: "Steer Agent",
|
|
376
|
+
description: "Send a steering note to a running pi-crew subagent. Live-session steering is planned; child-process runs expose durable status and can be cancelled if needed.",
|
|
377
|
+
parameters: Type.Object({ agent_id: Type.String(), message: Type.String() }) as never,
|
|
378
|
+
async execute(_id, params) {
|
|
379
|
+
const p = params as { agent_id?: string; message?: string };
|
|
380
|
+
const record = p.agent_id ? subagentManager.getRecord(p.agent_id) : undefined;
|
|
381
|
+
if (!record) return subagentToolResult(`Agent not found: ${p.agent_id ?? ""}`, {}, true);
|
|
382
|
+
return subagentToolResult([`Steering request noted for ${record.id}.`, "Current default pi-crew backend is child-process, so mid-turn session.steer is not available yet.", record.runId ? `Use team cancel runId=${record.runId} if the agent must be interrupted.` : undefined].filter((line): line is string => Boolean(line)).join("\n"), { agentId: record.id, runId: record.runId, status: record.status });
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const crewAgentTool: ToolDefinition = {
|
|
387
|
+
...agentTool,
|
|
388
|
+
name: "crew_agent",
|
|
389
|
+
label: "Crew Agent",
|
|
390
|
+
description: "Launch a real pi-crew subagent using a conflict-safe pi-crew-specific tool name.",
|
|
391
|
+
promptSnippet: "Use crew_agent when you need pi-crew subagents and another extension may own the generic Agent tool.",
|
|
392
|
+
};
|
|
393
|
+
const crewAgentResultTool: ToolDefinition = {
|
|
394
|
+
...getSubagentResultTool,
|
|
395
|
+
name: "crew_agent_result",
|
|
396
|
+
label: "Get Crew Agent Result",
|
|
397
|
+
description: "Check status and retrieve results from a pi-crew subagent using the conflict-safe tool name.",
|
|
398
|
+
};
|
|
399
|
+
const crewAgentSteerTool: ToolDefinition = {
|
|
400
|
+
...steerSubagentTool,
|
|
401
|
+
name: "crew_agent_steer",
|
|
402
|
+
label: "Steer Crew Agent",
|
|
403
|
+
description: "Send a steering note to a pi-crew subagent using the conflict-safe tool name.",
|
|
404
|
+
};
|
|
405
|
+
for (const extraTool of [agentTool, getSubagentResultTool, steerSubagentTool, crewAgentTool, crewAgentResultTool, crewAgentSteerTool]) {
|
|
406
|
+
pi.registerTool(extraTool);
|
|
407
|
+
}
|
|
408
|
+
|
|
199
409
|
pi.registerCommand("teams", {
|
|
200
410
|
description: "List pi-crew teams, workflows, and agents",
|
|
201
411
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
@@ -207,7 +417,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
207
417
|
pi.registerCommand("team-run", {
|
|
208
418
|
description: "Manually start a pi-crew run (agent may also use the team tool autonomously)",
|
|
209
419
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
210
|
-
const result = await handleTeamTool(parseRunArgs(args), { ...ctx, startForegroundRun: (runner) => startForegroundRun(ctx as ExtensionContext, runner) });
|
|
420
|
+
const result = await handleTeamTool(parseRunArgs(args), { ...ctx, startForegroundRun: (runner, runId) => startForegroundRun(ctx as ExtensionContext, runner, runId), onRunStarted: (runId) => openLiveSidebar(ctx as ExtensionContext, runId) });
|
|
211
421
|
await notifyCommandResult(ctx, commandText(result));
|
|
212
422
|
},
|
|
213
423
|
});
|
|
@@ -12,8 +12,8 @@ export interface RecommendedSubtask {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export interface TeamRecommendation {
|
|
15
|
-
team:
|
|
16
|
-
workflow:
|
|
15
|
+
team: string;
|
|
16
|
+
workflow: string;
|
|
17
17
|
action: "plan" | "run";
|
|
18
18
|
async: boolean;
|
|
19
19
|
workspaceMode: "single" | "worktree";
|
|
@@ -23,7 +23,8 @@ export interface TeamRecommendation {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const REVIEW_TERMS = ["review", "audit", "security", "vulnerability", "diff", "pr", "pull request"];
|
|
26
|
-
const RESEARCH_TERMS = ["research", "investigate", "compare", "analyze", "document", "docs", "explain", "architecture"];
|
|
26
|
+
const RESEARCH_TERMS = ["research", "investigate", "compare", "analyze", "document", "docs", "explain", "architecture", "đọc sâu", "source", "projects"];
|
|
27
|
+
const PARALLEL_RESEARCH_RE = /(?:đọc sâu|deep read|deep research|source audit|multiple projects|các project|pi-\*|source\/|@source)/i;
|
|
27
28
|
const FAST_FIX_TERMS = ["quick fix", "fast-fix", "small bug", "typo", "one-line", "minor", "lint"];
|
|
28
29
|
const IMPLEMENTATION_TERMS = ["implement", "refactor", "migrate", "feature", "tests", "test", "integration", "upgrade", "build", "create", "add"];
|
|
29
30
|
const RISKY_TERMS = ["migration", "refactor", "large", "multiple", "parallel", "concurrent", "risky", "critical"];
|
|
@@ -122,6 +123,11 @@ export function recommendTeam(goal: string, config: PiTeamsAutonomousConfig = {}
|
|
|
122
123
|
workflow = "review";
|
|
123
124
|
confidence = "high";
|
|
124
125
|
reasons.push(`Review/audit terms detected: ${reviewMatches.join(", ") || "explicit review intent"}.`);
|
|
126
|
+
} else if (PARALLEL_RESEARCH_RE.test(goal) || (researchMatches.length >= 2 && (normalized.includes("multiple") || normalized.includes("source") || normalized.includes("project") || normalized.includes("pi-")))) {
|
|
127
|
+
team = "parallel-research";
|
|
128
|
+
workflow = "parallel-research";
|
|
129
|
+
confidence = "high";
|
|
130
|
+
reasons.push("Deep/multi-source research detected; use parallel shard exploration.");
|
|
125
131
|
} else if (intents.includes("research") || (researchMatches.length > 0 && implementationMatches.length === 0)) {
|
|
126
132
|
team = "research";
|
|
127
133
|
workflow = "research";
|
|
@@ -5,6 +5,7 @@ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
|
5
5
|
import { allAgents, discoverAgents } from "../agents/discover-agents.ts";
|
|
6
6
|
import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
|
|
7
7
|
import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows.ts";
|
|
8
|
+
import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
|
|
8
9
|
import { effectiveAutonomousConfig, loadConfig, updateAutonomousConfig, updateConfig, type PiTeamsAutonomousConfig, type PiTeamsConfig } from "../config/config.ts";
|
|
9
10
|
import { projectPiRoot, userPiRoot } from "../utils/paths.ts";
|
|
10
11
|
import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
|
|
@@ -44,6 +45,7 @@ import { readForegroundControlStatus, writeForegroundInterruptRequest } from "..
|
|
|
44
45
|
import { listLiveAgents, resumeLiveAgent, steerLiveAgent, stopLiveAgent } from "../runtime/live-agent-manager.ts";
|
|
45
46
|
import { appendLiveAgentControlRequest } from "../runtime/live-agent-control.ts";
|
|
46
47
|
import { liveControlRealtimeMessage, publishLiveControlRealtime } from "../runtime/live-control-realtime.ts";
|
|
48
|
+
import { formatTaskGraphLines, waitingReason } from "../runtime/task-display.ts";
|
|
47
49
|
|
|
48
50
|
export interface TeamToolDetails {
|
|
49
51
|
action: string;
|
|
@@ -57,7 +59,8 @@ type TeamContext = Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext
|
|
|
57
59
|
sessionManager?: { getBranch?: () => unknown[] };
|
|
58
60
|
events?: { emit?: (event: string, data: unknown) => void };
|
|
59
61
|
signal?: AbortSignal;
|
|
60
|
-
startForegroundRun?: (runner: (signal?: AbortSignal) => Promise<void
|
|
62
|
+
startForegroundRun?: (runner: (signal?: AbortSignal) => Promise<void>, runId?: string) => void;
|
|
63
|
+
onRunStarted?: (runId: string) => void;
|
|
61
64
|
};
|
|
62
65
|
|
|
63
66
|
function result(text: string, details: TeamToolDetails, isError = false): PiTeamsToolResult {
|
|
@@ -168,6 +171,48 @@ function commandExists(command: string, args: string[]): { ok: boolean; detail:
|
|
|
168
171
|
return { ok: false, detail: output.error?.message ?? firstOutputLine(output.stdout, output.stderr) };
|
|
169
172
|
}
|
|
170
173
|
|
|
174
|
+
function sourcePiProjects(cwd: string): string[] {
|
|
175
|
+
const sourceDir = path.join(cwd, "Source");
|
|
176
|
+
try {
|
|
177
|
+
return fs.readdirSync(sourceDir, { withFileTypes: true })
|
|
178
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith("pi-"))
|
|
179
|
+
.map((entry) => `Source/${entry.name}`)
|
|
180
|
+
.sort();
|
|
181
|
+
} catch {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function chunkProjects(projects: string[], target = 4): string[][] {
|
|
187
|
+
const chunks = Array.from({ length: Math.min(Math.max(1, target), Math.max(1, projects.length)) }, () => [] as string[]);
|
|
188
|
+
projects.forEach((project, index) => chunks[index % chunks.length]!.push(project));
|
|
189
|
+
return chunks.filter((chunk) => chunk.length > 0);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function expandParallelResearchWorkflow(workflow: WorkflowConfig, cwd: string): WorkflowConfig {
|
|
193
|
+
if (workflow.name !== "parallel-research") return workflow;
|
|
194
|
+
const projects = sourcePiProjects(cwd);
|
|
195
|
+
if (projects.length === 0) return workflow;
|
|
196
|
+
const chunks = chunkProjects(projects, Math.min(6, Math.max(4, Math.ceil(projects.length / 4))));
|
|
197
|
+
const exploreSteps: WorkflowStep[] = chunks.map((paths, index) => ({
|
|
198
|
+
id: `explore-shard-${index + 1}`,
|
|
199
|
+
role: "explorer",
|
|
200
|
+
dependsOn: ["discover"],
|
|
201
|
+
parallelGroup: "explore",
|
|
202
|
+
reads: paths,
|
|
203
|
+
task: [`Explore this dynamic shard for: {goal}`, "", "Paths:", ...paths.map((item) => `- ${item}`), "", "Focus on purpose, architecture, runtime/UI patterns, package config, docs, and lessons for pi-crew."].join("\n"),
|
|
204
|
+
}));
|
|
205
|
+
return {
|
|
206
|
+
...workflow,
|
|
207
|
+
steps: [
|
|
208
|
+
{ id: "discover", role: "explorer", task: `Discover and validate ${projects.length} pi-* projects for: {goal}\n\nProjects:\n${projects.map((item) => `- ${item}`).join("\n")}` },
|
|
209
|
+
...exploreSteps,
|
|
210
|
+
{ id: "synthesize", role: "analyst", dependsOn: exploreSteps.map((step) => step.id), task: "Synthesize all dynamic shard findings. Identify common patterns, gaps, and concrete recommendations." },
|
|
211
|
+
{ id: "write", role: "writer", dependsOn: ["synthesize"], output: "research-summary.md", task: "Write a concise final summary with evidence, risks, and actionable next steps." },
|
|
212
|
+
],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
171
216
|
function effectiveRunConfig(base: PiTeamsConfig, rawOverride: unknown): PiTeamsConfig {
|
|
172
217
|
const patch = configPatchFromConfig(rawOverride);
|
|
173
218
|
return {
|
|
@@ -255,12 +300,29 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
255
300
|
const teams = allTeams(discoverTeams(ctx.cwd));
|
|
256
301
|
const workflows = allWorkflows(discoverWorkflows(ctx.cwd));
|
|
257
302
|
const agents = allAgents(discoverAgents(ctx.cwd));
|
|
303
|
+
const directAgent = params.agent ? agents.find((item) => item.name === params.agent) : undefined;
|
|
304
|
+
if (params.agent && !directAgent) return result(`Agent '${params.agent}' not found.`, { action: "run", status: "error" }, true);
|
|
258
305
|
const teamName = params.team ?? "default";
|
|
259
|
-
const team =
|
|
306
|
+
const team = directAgent ? {
|
|
307
|
+
name: `direct-${directAgent.name}`,
|
|
308
|
+
description: `Direct subagent run for ${directAgent.name}`,
|
|
309
|
+
source: "builtin" as const,
|
|
310
|
+
filePath: "<generated>",
|
|
311
|
+
roles: [{ name: params.role ?? "agent", agent: directAgent.name, description: directAgent.description }],
|
|
312
|
+
defaultWorkflow: "direct-agent",
|
|
313
|
+
workspaceMode: params.workspaceMode,
|
|
314
|
+
} : teams.find((item) => item.name === teamName);
|
|
260
315
|
if (!team) return result(`Team '${teamName}' not found.`, { action: "run", status: "error" }, true);
|
|
261
|
-
const workflowName = params.workflow ?? team.defaultWorkflow ?? "default";
|
|
262
|
-
const
|
|
263
|
-
|
|
316
|
+
const workflowName = directAgent ? "direct-agent" : params.workflow ?? team.defaultWorkflow ?? "default";
|
|
317
|
+
const baseWorkflow = directAgent ? {
|
|
318
|
+
name: "direct-agent",
|
|
319
|
+
description: `Direct task for ${directAgent.name}`,
|
|
320
|
+
source: "builtin" as const,
|
|
321
|
+
filePath: "<generated>",
|
|
322
|
+
steps: [{ id: "01_agent", role: params.role ?? "agent", task: "{goal}", model: params.model }],
|
|
323
|
+
} : workflows.find((item) => item.name === workflowName);
|
|
324
|
+
if (!baseWorkflow) return result(`Workflow '${workflowName}' not found.`, { action: "run", status: "error" }, true);
|
|
325
|
+
const workflow = directAgent ? baseWorkflow : expandParallelResearchWorkflow(baseWorkflow, ctx.cwd);
|
|
264
326
|
|
|
265
327
|
const validationErrors = validateWorkflowForTeam(workflow, team);
|
|
266
328
|
if (validationErrors.length > 0) {
|
|
@@ -306,12 +368,13 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
306
368
|
}
|
|
307
369
|
|
|
308
370
|
const runtime = await resolveCrewRuntime(effectiveRunConfig(loadedConfig.config, params.config));
|
|
309
|
-
const executeWorkers = runtime.kind
|
|
371
|
+
const executeWorkers = runtime.kind !== "scaffold";
|
|
310
372
|
const executedConfig = effectiveRunConfig(loadedConfig.config, params.config);
|
|
311
373
|
if (executeWorkers && ctx.startForegroundRun) {
|
|
374
|
+
ctx.onRunStarted?.(updatedManifest.runId);
|
|
312
375
|
ctx.startForegroundRun(async (signal) => {
|
|
313
376
|
await executeTeamRun({ manifest: updatedManifest, tasks, team, workflow, agents, executeWorkers, limits: executedConfig.limits, runtime, runtimeConfig: executedConfig.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, modelOverride: params.model, signal });
|
|
314
|
-
});
|
|
377
|
+
}, updatedManifest.runId);
|
|
315
378
|
const text = [
|
|
316
379
|
`Started foreground pi-crew run ${updatedManifest.runId}.`,
|
|
317
380
|
`Team: ${team.name}`,
|
|
@@ -368,6 +431,10 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
368
431
|
const crewAgents = readCrewAgents(manifest).map((agent) => applyAttentionState(manifest, agent, controlConfig));
|
|
369
432
|
const artifactLines = manifest.artifacts.slice(-10).map((artifact) => `- ${artifact.kind}: ${artifact.path}${artifact.sizeBytes !== undefined ? ` (${artifact.sizeBytes} bytes)` : ""}`);
|
|
370
433
|
const totalUsage = aggregateUsage(tasks);
|
|
434
|
+
const activeAgents = crewAgents.filter((agent) => agent.status === "running");
|
|
435
|
+
const completedAgents = crewAgents.filter((agent) => agent.status !== "running");
|
|
436
|
+
const waitingTasks = tasks.filter((task) => task.status === "queued");
|
|
437
|
+
const agentLine = (agent: typeof crewAgents[number]): string => `- ${agent.id} [${agent.status}] ${agent.role} -> ${agent.agent} runtime=${agent.runtime}${agent.model ? ` model=${agent.model}` : ""}${agent.usage ? ` usage=${formatUsage(agent.usage)}` : ""}${agent.progress?.activityState === "needs_attention" ? " needs_attention" : ""}${formatActivityAge(agent) ? ` activity=${formatActivityAge(agent)}` : ""}${agent.progress?.currentTool ? ` tool=${agent.progress.currentTool}` : ""}${agent.toolUses ? ` tools=${agent.toolUses}` : ""}${!agent.usage && agent.progress?.tokens ? ` tokens=${agent.progress.tokens}` : ""}${agent.progress?.turns ? ` turns=${agent.progress.turns}` : ""}${agent.jsonEvents !== undefined ? ` jsonEvents=${agent.jsonEvents}` : ""}${agent.statusPath ? ` status=${agent.statusPath}` : ""}${agent.error ? ` error=${agent.error}` : ""}`;
|
|
371
438
|
const lines = [
|
|
372
439
|
`Run: ${manifest.runId}`,
|
|
373
440
|
`Team: ${manifest.team}`,
|
|
@@ -380,11 +447,17 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
380
447
|
`State: ${manifest.stateRoot}`,
|
|
381
448
|
`Artifacts: ${manifest.artifactsRoot}`,
|
|
382
449
|
...(asyncLivenessLine ? [asyncLivenessLine] : []),
|
|
450
|
+
"Task graph:",
|
|
451
|
+
...formatTaskGraphLines(tasks),
|
|
383
452
|
"Tasks:",
|
|
384
453
|
...(tasks.length ? tasks.map((task) => `- ${task.id} [${task.status}] ${task.role} -> ${task.agent}${task.taskPacket ? ` scope=${task.taskPacket.scope}` : ""}${task.verification ? ` green=${task.verification.observedGreenLevel}/${task.verification.requiredGreenLevel}` : ""}${task.modelAttempts?.length ? ` attempts=${task.modelAttempts.length}` : ""}${task.jsonEvents !== undefined ? ` jsonEvents=${task.jsonEvents}` : ""}${task.usage ? ` usage=${JSON.stringify(task.usage)}` : ""}${task.worktree ? ` worktree=${task.worktree.path}` : ""}${task.error ? ` error=${task.error}` : ""}`) : ["- (none)"]),
|
|
385
454
|
`Task counts: ${[...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none"}`,
|
|
386
|
-
"
|
|
387
|
-
...(
|
|
455
|
+
"Active agents:",
|
|
456
|
+
...(activeAgents.length ? activeAgents.map(agentLine) : ["- (none)"]),
|
|
457
|
+
"Waiting tasks:",
|
|
458
|
+
...(waitingTasks.length ? waitingTasks.map((task) => `- ${task.id} [queued] ${task.role} -> ${task.agent} ${waitingReason(task, tasks) ?? "waiting"}`) : ["- (none)"]),
|
|
459
|
+
"Completed agents:",
|
|
460
|
+
...(completedAgents.length ? completedAgents.map(agentLine) : ["- (none)"]),
|
|
388
461
|
"Policy decisions:",
|
|
389
462
|
...(manifest.policyDecisions?.length ? manifest.policyDecisions.map((item) => `- ${item.action} (${item.reason})${item.taskId ? ` ${item.taskId}` : ""}: ${item.message}`) : ["- (none)"]),
|
|
390
463
|
`Total usage: ${formatUsage(totalUsage)}`,
|
|
@@ -450,7 +523,7 @@ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
450
523
|
appendEvent(loaded.manifest.eventsPath, { type: "run.resume_requested", runId: loaded.manifest.runId });
|
|
451
524
|
const loadedConfig = loadConfig(ctx.cwd);
|
|
452
525
|
const runtime = await resolveCrewRuntime(loadedConfig.config);
|
|
453
|
-
const executeWorkers = runtime.kind
|
|
526
|
+
const executeWorkers = runtime.kind !== "scaffold";
|
|
454
527
|
const executed = await executeTeamRun({ manifest: loaded.manifest, tasks: resetTasks, team, workflow, agents: allAgents(discoverAgents(ctx.cwd)), executeWorkers, limits: loadedConfig.config.limits, runtime, runtimeConfig: loadedConfig.config.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, modelOverride: params.model, signal: ctx.signal });
|
|
455
528
|
return result([`Resumed run ${executed.manifest.runId}.`, `Status: ${executed.manifest.status}`, `Tasks: ${executed.tasks.length}`, `Artifacts: ${executed.manifest.artifactsRoot}`].join("\n"), { action: "resume", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot }, executed.manifest.status === "failed");
|
|
456
529
|
});
|