pi-crew 0.1.16 → 0.1.18
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 +17 -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 +36 -7
- package/src/extension/team-recommendation.ts +9 -3
- package/src/extension/team-tool.ts +61 -4
- package/src/runtime/child-pi.ts +25 -3
- package/src/runtime/crew-agent-records.ts +8 -0
- package/src/runtime/crew-agent-runtime.ts +3 -0
- package/src/runtime/pi-json-output.ts +3 -2
- package/src/runtime/policy-engine.ts +1 -1
- package/src/runtime/task-display.ts +38 -0
- package/src/runtime/task-runner.ts +19 -2
- package/src/runtime/team-runner.ts +242 -240
- package/src/ui/live-run-sidebar.ts +95 -0
- package/src/ui/powerbar-publisher.ts +20 -5
- package/src/ui/run-dashboard.ts +13 -3
- package/teams/parallel-research.team.md +14 -0
- package/workflows/parallel-research.workflow.md +50 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.18
|
|
4
|
+
|
|
5
|
+
- Added a built-in `parallel-research` team/workflow for map-reduce style source audits with dynamic `Source/pi-*` fanout and parallel explorer shards.
|
|
6
|
+
- 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.
|
|
7
|
+
- Added live sidebar sections for active agents, waiting tasks, completed agents, task graph, model, tool, and token/usage details.
|
|
8
|
+
- Stopped materializing queued dependency tasks as child-process agents; status now separates active agents, waiting tasks, and completed agents.
|
|
9
|
+
- Added workflow-aware default concurrency so research/parallel-research can use ready parallel work instead of always running one worker.
|
|
10
|
+
- Dropped user/system prompt messages from child event persistence to avoid prompt/context leakage in agent event logs.
|
|
11
|
+
- Tightened child event compaction with separate assistant/tool input/tool result caps and improved powerbar active/waiting/model/token summaries.
|
|
12
|
+
|
|
13
|
+
## 0.1.17
|
|
14
|
+
|
|
15
|
+
- Fixed terminal/completed workers being incorrectly escalated as stale heartbeat blockers after all tasks completed.
|
|
16
|
+
- Cleaned child-process result extraction so result artifacts prefer final assistant output and no longer include worker prompt/context.
|
|
17
|
+
- Made `/team-dashboard` visibly render as a top-right sidebar by default with explicit right-sidebar title text.
|
|
18
|
+
- Added per-subagent model and usage fields to agent records, status output, and dashboard fallbacks so model/token totals stay visible while and after workers run.
|
|
19
|
+
|
|
3
20
|
## 0.1.16
|
|
4
21
|
|
|
5
22
|
- Added right-side `/team-dashboard` placement with model, token, and tool detail rows for subagents.
|
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": true,
|
|
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; foreground runs auto-open a live top-right sidebar when the terminal is wide enough.
|
|
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": true,
|
|
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": true, "description": "Automatically open the live right sidebar for foreground runs when UI is available." },
|
|
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,
|
|
@@ -7,8 +7,9 @@ import { notifyActiveRuns } from "./session-summary.ts";
|
|
|
7
7
|
import { piTeamsHelp } from "./help.ts";
|
|
8
8
|
import { handleTeamManagerCommand } from "./team-manager-command.ts";
|
|
9
9
|
import { handleTeamTool, type TeamToolDetails } from "./team-tool.ts";
|
|
10
|
-
import {
|
|
10
|
+
import { listRecentRuns } from "./run-index.ts";
|
|
11
11
|
import { RunDashboard, type RunDashboardSelection } from "../ui/run-dashboard.ts";
|
|
12
|
+
import { LiveRunSidebar } from "../ui/live-run-sidebar.ts";
|
|
12
13
|
import { registerPiCrewRpc, type PiCrewRpcHandle } from "./cross-extension-rpc.ts";
|
|
13
14
|
import { stopCrewWidget, updateCrewWidget, type CrewWidgetState } from "../ui/crew-widget.ts";
|
|
14
15
|
import { clearPiCrewPowerbar, registerPiCrewPowerbarSegments, updatePiCrewPowerbar } from "../ui/powerbar-publisher.ts";
|
|
@@ -112,6 +113,29 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
112
113
|
let cleanedUp = false;
|
|
113
114
|
const widgetState: CrewWidgetState = { frame: 0 };
|
|
114
115
|
const foregroundControllers = new Set<AbortController>();
|
|
116
|
+
let liveSidebarRunId: string | undefined;
|
|
117
|
+
let liveSidebarTimer: ReturnType<typeof setInterval> | undefined;
|
|
118
|
+
const requestRender = (ctx: ExtensionContext): void => (ctx.ui as { requestRender?: () => void }).requestRender?.();
|
|
119
|
+
const openLiveSidebar = (ctx: ExtensionContext, runId: string): void => {
|
|
120
|
+
const uiConfig = loadConfig(ctx.cwd).config.ui;
|
|
121
|
+
const autoOpen = uiConfig?.autoOpenDashboard ?? true;
|
|
122
|
+
const foregroundAutoOpen = uiConfig?.autoOpenDashboardForForegroundRuns ?? true;
|
|
123
|
+
if (!ctx.hasUI || !autoOpen || !foregroundAutoOpen || (uiConfig?.dashboardPlacement ?? "right") !== "right") return;
|
|
124
|
+
if (liveSidebarRunId === runId) return;
|
|
125
|
+
if (liveSidebarTimer) clearInterval(liveSidebarTimer);
|
|
126
|
+
liveSidebarRunId = runId;
|
|
127
|
+
const width = Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? 56));
|
|
128
|
+
liveSidebarTimer = setInterval(() => requestRender(ctx), uiConfig?.dashboardLiveRefreshMs ?? 1000);
|
|
129
|
+
liveSidebarTimer.unref?.();
|
|
130
|
+
void ctx.ui.custom<undefined>((_tui, theme, _keybindings, done) => new LiveRunSidebar({ cwd: ctx.cwd, runId, done, theme, config: uiConfig }), {
|
|
131
|
+
overlay: true,
|
|
132
|
+
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 },
|
|
133
|
+
}).finally(() => {
|
|
134
|
+
if (liveSidebarRunId === runId) liveSidebarRunId = undefined;
|
|
135
|
+
if (liveSidebarTimer) clearInterval(liveSidebarTimer);
|
|
136
|
+
liveSidebarTimer = undefined;
|
|
137
|
+
});
|
|
138
|
+
};
|
|
115
139
|
const startForegroundRun = (ctx: ExtensionContext, runner: (signal?: AbortSignal) => Promise<void>): void => {
|
|
116
140
|
const controller = new AbortController();
|
|
117
141
|
foregroundControllers.add(controller);
|
|
@@ -141,6 +165,9 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
141
165
|
terminateActiveChildPiProcesses();
|
|
142
166
|
stopAsyncRunNotifier(notifierState);
|
|
143
167
|
stopCrewWidget(currentCtx, widgetState, currentCtx ? loadConfig(currentCtx.cwd).config.ui : undefined);
|
|
168
|
+
if (liveSidebarTimer) clearInterval(liveSidebarTimer);
|
|
169
|
+
liveSidebarTimer = undefined;
|
|
170
|
+
liveSidebarRunId = undefined;
|
|
144
171
|
clearPiCrewPowerbar(pi.events);
|
|
145
172
|
rpcHandle?.unsubscribe();
|
|
146
173
|
rpcHandle = undefined;
|
|
@@ -182,7 +209,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
182
209
|
const abort = (): void => controller.abort();
|
|
183
210
|
signal?.addEventListener("abort", abort, { once: true });
|
|
184
211
|
try {
|
|
185
|
-
const output = await handleTeamTool(params as TeamToolParamsValue, { ...ctx, signal: controller.signal, startForegroundRun: (runner) => startForegroundRun(ctx, runner) });
|
|
212
|
+
const output = await handleTeamTool(params as TeamToolParamsValue, { ...ctx, signal: controller.signal, startForegroundRun: (runner) => startForegroundRun(ctx, runner), onRunStarted: (runId) => openLiveSidebar(ctx, runId) });
|
|
186
213
|
const config = loadConfig(ctx.cwd).config.ui;
|
|
187
214
|
updateCrewWidget(ctx, widgetState, config);
|
|
188
215
|
updatePiCrewPowerbar(pi.events, ctx.cwd, config);
|
|
@@ -207,7 +234,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
207
234
|
pi.registerCommand("team-run", {
|
|
208
235
|
description: "Manually start a pi-crew run (agent may also use the team tool autonomously)",
|
|
209
236
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
210
|
-
const result = await handleTeamTool(parseRunArgs(args), { ...ctx, startForegroundRun: (runner) => startForegroundRun(ctx as ExtensionContext, runner) });
|
|
237
|
+
const result = await handleTeamTool(parseRunArgs(args), { ...ctx, startForegroundRun: (runner) => startForegroundRun(ctx as ExtensionContext, runner), onRunStarted: (runId) => openLiveSidebar(ctx as ExtensionContext, runId) });
|
|
211
238
|
await notifyCommandResult(ctx, commandText(result));
|
|
212
239
|
},
|
|
213
240
|
});
|
|
@@ -381,13 +408,15 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
381
408
|
description: "Open a pi-crew run dashboard overlay",
|
|
382
409
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
383
410
|
for (;;) {
|
|
384
|
-
const runs =
|
|
411
|
+
const runs = listRecentRuns(ctx.cwd, 50);
|
|
385
412
|
const uiConfig = loadConfig(ctx.cwd).config.ui;
|
|
386
413
|
const rightPanel = uiConfig?.dashboardPlacement !== "center";
|
|
387
|
-
const width = rightPanel ? Math.min(
|
|
388
|
-
const selection = await ctx.ui.custom<RunDashboardSelection | undefined>((_tui, theme, _keybindings, done) => new RunDashboard(runs, done, theme, { showModel: uiConfig?.showModel, showTokens: uiConfig?.showTokens, showTools: uiConfig?.showTools }), {
|
|
414
|
+
const width = rightPanel ? Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? 56)) : "90%";
|
|
415
|
+
const selection = await ctx.ui.custom<RunDashboardSelection | undefined>((_tui, theme, _keybindings, done) => new RunDashboard(runs, done, theme, { placement: rightPanel ? "right" : "center", showModel: uiConfig?.showModel, showTokens: uiConfig?.showTokens, showTools: uiConfig?.showTools }), {
|
|
389
416
|
overlay: true,
|
|
390
|
-
overlayOptions:
|
|
417
|
+
overlayOptions: rightPanel
|
|
418
|
+
? { width, minWidth: 40, maxHeight: "100%", anchor: "top-right", offsetX: 0, offsetY: 0, margin: { top: 0, right: 0, bottom: 0, left: 0 } }
|
|
419
|
+
: { width, maxHeight: "90%", anchor: "center", margin: 2 },
|
|
391
420
|
});
|
|
392
421
|
if (!selection) return;
|
|
393
422
|
if (selection.action === "reload") continue;
|
|
@@ -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;
|
|
@@ -58,6 +60,7 @@ type TeamContext = Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext
|
|
|
58
60
|
events?: { emit?: (event: string, data: unknown) => void };
|
|
59
61
|
signal?: AbortSignal;
|
|
60
62
|
startForegroundRun?: (runner: (signal?: AbortSignal) => Promise<void>) => 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 {
|
|
@@ -259,8 +304,9 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
259
304
|
const team = teams.find((item) => item.name === teamName);
|
|
260
305
|
if (!team) return result(`Team '${teamName}' not found.`, { action: "run", status: "error" }, true);
|
|
261
306
|
const workflowName = params.workflow ?? team.defaultWorkflow ?? "default";
|
|
262
|
-
const
|
|
263
|
-
if (!
|
|
307
|
+
const baseWorkflow = workflows.find((item) => item.name === workflowName);
|
|
308
|
+
if (!baseWorkflow) return result(`Workflow '${workflowName}' not found.`, { action: "run", status: "error" }, true);
|
|
309
|
+
const workflow = expandParallelResearchWorkflow(baseWorkflow, ctx.cwd);
|
|
264
310
|
|
|
265
311
|
const validationErrors = validateWorkflowForTeam(workflow, team);
|
|
266
312
|
if (validationErrors.length > 0) {
|
|
@@ -309,6 +355,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
309
355
|
const executeWorkers = runtime.kind === "child-process";
|
|
310
356
|
const executedConfig = effectiveRunConfig(loadedConfig.config, params.config);
|
|
311
357
|
if (executeWorkers && ctx.startForegroundRun) {
|
|
358
|
+
ctx.onRunStarted?.(updatedManifest.runId);
|
|
312
359
|
ctx.startForegroundRun(async (signal) => {
|
|
313
360
|
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
361
|
});
|
|
@@ -368,6 +415,10 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
368
415
|
const crewAgents = readCrewAgents(manifest).map((agent) => applyAttentionState(manifest, agent, controlConfig));
|
|
369
416
|
const artifactLines = manifest.artifacts.slice(-10).map((artifact) => `- ${artifact.kind}: ${artifact.path}${artifact.sizeBytes !== undefined ? ` (${artifact.sizeBytes} bytes)` : ""}`);
|
|
370
417
|
const totalUsage = aggregateUsage(tasks);
|
|
418
|
+
const activeAgents = crewAgents.filter((agent) => agent.status === "running");
|
|
419
|
+
const completedAgents = crewAgents.filter((agent) => agent.status !== "running");
|
|
420
|
+
const waitingTasks = tasks.filter((task) => task.status === "queued");
|
|
421
|
+
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
422
|
const lines = [
|
|
372
423
|
`Run: ${manifest.runId}`,
|
|
373
424
|
`Team: ${manifest.team}`,
|
|
@@ -380,11 +431,17 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
380
431
|
`State: ${manifest.stateRoot}`,
|
|
381
432
|
`Artifacts: ${manifest.artifactsRoot}`,
|
|
382
433
|
...(asyncLivenessLine ? [asyncLivenessLine] : []),
|
|
434
|
+
"Task graph:",
|
|
435
|
+
...formatTaskGraphLines(tasks),
|
|
383
436
|
"Tasks:",
|
|
384
437
|
...(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
438
|
`Task counts: ${[...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none"}`,
|
|
386
|
-
"
|
|
387
|
-
...(
|
|
439
|
+
"Active agents:",
|
|
440
|
+
...(activeAgents.length ? activeAgents.map(agentLine) : ["- (none)"]),
|
|
441
|
+
"Waiting tasks:",
|
|
442
|
+
...(waitingTasks.length ? waitingTasks.map((task) => `- ${task.id} [queued] ${task.role} -> ${task.agent} ${waitingReason(task, tasks) ?? "waiting"}`) : ["- (none)"]),
|
|
443
|
+
"Completed agents:",
|
|
444
|
+
...(completedAgents.length ? completedAgents.map(agentLine) : ["- (none)"]),
|
|
388
445
|
"Policy decisions:",
|
|
389
446
|
...(manifest.policyDecisions?.length ? manifest.policyDecisions.map((item) => `- ${item.action} (${item.reason})${item.taskId ? ` ${item.taskId}` : ""}: ${item.message}`) : ["- (none)"]),
|
|
390
447
|
`Total usage: ${formatUsage(totalUsage)}`,
|
package/src/runtime/child-pi.ts
CHANGED
|
@@ -9,6 +9,10 @@ const POST_EXIT_STDIO_GUARD_MS = 3000;
|
|
|
9
9
|
const FINAL_DRAIN_MS = 5000;
|
|
10
10
|
const HARD_KILL_MS = 3000;
|
|
11
11
|
const MAX_CAPTURE_BYTES = 256 * 1024;
|
|
12
|
+
const MAX_ASSISTANT_TEXT_CHARS = 8192;
|
|
13
|
+
const MAX_TOOL_RESULT_CHARS = 1024;
|
|
14
|
+
const MAX_TOOL_INPUT_CHARS = 2048;
|
|
15
|
+
const MAX_COMPACT_CONTENT_CHARS = 4096;
|
|
12
16
|
const activeChildProcesses = new Map<number, ChildProcess>();
|
|
13
17
|
|
|
14
18
|
function appendBoundedTail(current: string, chunk: string, maxBytes = MAX_CAPTURE_BYTES): string {
|
|
@@ -68,12 +72,27 @@ function appendTranscript(input: ChildPiRunInput, line: string): void {
|
|
|
68
72
|
fs.appendFileSync(input.transcriptPath, `${line}\n`, "utf-8");
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
function compactString(value: string, maxChars = MAX_COMPACT_CONTENT_CHARS): string {
|
|
76
|
+
if (value.length <= maxChars) return value;
|
|
77
|
+
return `${value.slice(0, maxChars)}\n[pi-crew compacted ${value.length - maxChars} chars]`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function compactValue(value: unknown): unknown {
|
|
81
|
+
if (typeof value === "string") return compactString(value);
|
|
82
|
+
if (Array.isArray(value)) return value.slice(0, 20).map(compactValue);
|
|
83
|
+
const record = asRecord(value);
|
|
84
|
+
if (!record) return value;
|
|
85
|
+
const compacted: Record<string, unknown> = {};
|
|
86
|
+
for (const [key, entry] of Object.entries(record).slice(0, 20)) compacted[key] = compactValue(entry);
|
|
87
|
+
return compacted;
|
|
88
|
+
}
|
|
89
|
+
|
|
71
90
|
function compactContentPart(part: unknown): unknown | undefined {
|
|
72
91
|
const record = asRecord(part);
|
|
73
92
|
if (!record) return undefined;
|
|
74
|
-
if (record.type === "text") return { type: "text", text: typeof record.text === "string" ? record.text : "" };
|
|
75
|
-
if (record.type === "toolCall") return { type: "toolCall", name: record.name, input: record.input };
|
|
76
|
-
if (record.type === "toolResult") return { type: "toolResult", name: record.name, content: record.content };
|
|
93
|
+
if (record.type === "text") return { type: "text", text: typeof record.text === "string" ? compactString(record.text, MAX_ASSISTANT_TEXT_CHARS) : "" };
|
|
94
|
+
if (record.type === "toolCall") return { type: "toolCall", name: record.name, input: compactValue(typeof record.input === "string" ? compactString(record.input, MAX_TOOL_INPUT_CHARS) : record.input) };
|
|
95
|
+
if (record.type === "toolResult") return { type: "toolResult", name: record.name, content: compactValue(typeof record.content === "string" ? compactString(record.content, MAX_TOOL_RESULT_CHARS) : record.content) };
|
|
77
96
|
return undefined;
|
|
78
97
|
}
|
|
79
98
|
|
|
@@ -86,6 +105,7 @@ function compactChildPiEvent(event: unknown): unknown | undefined {
|
|
|
86
105
|
}
|
|
87
106
|
if (record.type === "tool_result_end" || record.type === "message_end" || record.type === "message") {
|
|
88
107
|
const message = asRecord(record.message);
|
|
108
|
+
if (message?.role === "user" || message?.role === "system") return undefined;
|
|
89
109
|
const content = Array.isArray(message?.content) ? message.content.map(compactContentPart).filter((part) => part !== undefined) : undefined;
|
|
90
110
|
return {
|
|
91
111
|
type: record.type,
|
|
@@ -106,7 +126,9 @@ function displayTextFromCompactEvent(event: unknown): string | undefined {
|
|
|
106
126
|
if (record.type === "tool_execution_start") {
|
|
107
127
|
return typeof record.toolName === "string" ? `tool: ${record.toolName}` : "tool started";
|
|
108
128
|
}
|
|
129
|
+
if (record.type !== "message" && record.type !== "message_end") return undefined;
|
|
109
130
|
const message = asRecord(record.message);
|
|
131
|
+
if (message?.role !== undefined && message.role !== "assistant") return undefined;
|
|
110
132
|
const content = Array.isArray(message?.content) ? message.content : [];
|
|
111
133
|
const text = content.flatMap((part) => {
|
|
112
134
|
const item = asRecord(part);
|
|
@@ -124,6 +124,12 @@ export function emptyCrewAgentProgress(): CrewAgentProgress {
|
|
|
124
124
|
return { recentTools: [], recentOutput: [], toolCount: 0 };
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
function modelFromTask(task: TeamTaskState): string | undefined {
|
|
128
|
+
const attempts = task.modelAttempts;
|
|
129
|
+
if (!attempts?.length) return undefined;
|
|
130
|
+
return attempts.find((attempt) => attempt.success)?.model ?? attempts.at(-1)?.model;
|
|
131
|
+
}
|
|
132
|
+
|
|
127
133
|
export function recordFromTask(manifest: TeamRunManifest, task: TeamTaskState, runtime: CrewRuntimeKind): CrewAgentRecord {
|
|
128
134
|
return {
|
|
129
135
|
id: `${manifest.runId}:${task.id}`,
|
|
@@ -142,6 +148,8 @@ export function recordFromTask(manifest: TeamRunManifest, task: TeamTaskState, r
|
|
|
142
148
|
outputPath: agentOutputPath(manifest, task.id),
|
|
143
149
|
toolUses: task.agentProgress?.toolCount,
|
|
144
150
|
jsonEvents: task.jsonEvents,
|
|
151
|
+
model: modelFromTask(task),
|
|
152
|
+
usage: task.usage,
|
|
145
153
|
progress: task.agentProgress,
|
|
146
154
|
error: task.error,
|
|
147
155
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TeamTaskStatus } from "../state/contracts.ts";
|
|
2
|
+
import type { UsageState } from "../state/types.ts";
|
|
2
3
|
|
|
3
4
|
export type CrewRuntimeKind = "scaffold" | "child-process" | "live-session";
|
|
4
5
|
export type CrewAgentStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "stopped";
|
|
@@ -41,6 +42,8 @@ export interface CrewAgentRecord {
|
|
|
41
42
|
outputPath?: string;
|
|
42
43
|
toolUses?: number;
|
|
43
44
|
jsonEvents?: number;
|
|
45
|
+
model?: string;
|
|
46
|
+
usage?: UsageState;
|
|
44
47
|
progress?: CrewAgentProgress;
|
|
45
48
|
error?: string;
|
|
46
49
|
}
|
|
@@ -72,13 +72,14 @@ function textFromContent(content: unknown): string[] {
|
|
|
72
72
|
function extractText(value: unknown): string[] {
|
|
73
73
|
const obj = asRecord(value);
|
|
74
74
|
if (!obj) return [];
|
|
75
|
+
const message = asRecord(obj.message);
|
|
76
|
+
if (message?.role !== undefined && message.role !== "assistant") return [];
|
|
75
77
|
const text: string[] = [];
|
|
76
78
|
if (typeof obj.text === "string") text.push(obj.text);
|
|
77
79
|
if (typeof obj.output === "string") text.push(obj.output);
|
|
78
80
|
if (typeof obj.finalOutput === "string") text.push(obj.finalOutput);
|
|
79
81
|
if (typeof obj.final_output === "string") text.push(obj.final_output);
|
|
80
|
-
text.push(...textFromContent(obj.content));
|
|
81
|
-
const message = asRecord(obj.message);
|
|
82
|
+
if (!message) text.push(...textFromContent(obj.content));
|
|
82
83
|
if (message) text.push(...textFromContent(message.content));
|
|
83
84
|
return text.filter((entry) => entry.trim().length > 0);
|
|
84
85
|
}
|
|
@@ -56,7 +56,7 @@ export function evaluateCrewPolicy(input: PolicyEngineInput): PolicyDecision[] {
|
|
|
56
56
|
const maxRetries = input.limits?.maxRetriesPerTask ?? 0;
|
|
57
57
|
decisions.push(decision(retryCount < maxRetries ? "retry" : "escalate", "task_failed", task.error ? `Task failed: ${task.error}` : "Task failed.", task.id));
|
|
58
58
|
}
|
|
59
|
-
if (task.heartbeat && isWorkerHeartbeatStale(task.heartbeat, input.limits?.heartbeatStaleMs ?? 60_000, input.now)) {
|
|
59
|
+
if ((task.status === "running" || task.status === "queued") && task.heartbeat && task.heartbeat.alive !== false && isWorkerHeartbeatStale(task.heartbeat, input.limits?.heartbeatStaleMs ?? 60_000, input.now)) {
|
|
60
60
|
decisions.push(decision("escalate", "worker_stale", "Worker heartbeat is stale.", task.id));
|
|
61
61
|
}
|
|
62
62
|
if (task.taskPacket?.verification) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { TeamTaskState } from "../state/types.ts";
|
|
2
|
+
import type { CrewAgentRecord, CrewRuntimeKind } from "./crew-agent-runtime.ts";
|
|
3
|
+
import { recordFromTask } from "./crew-agent-records.ts";
|
|
4
|
+
import type { TeamRunManifest } from "../state/types.ts";
|
|
5
|
+
|
|
6
|
+
export function shouldMaterializeAgent(task: TeamTaskState): boolean {
|
|
7
|
+
return task.status !== "queued" && task.status !== "skipped";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function recordsForMaterializedTasks(manifest: TeamRunManifest, tasks: TeamTaskState[], runtime: CrewRuntimeKind): CrewAgentRecord[] {
|
|
11
|
+
return tasks.filter(shouldMaterializeAgent).map((task) => recordFromTask(manifest, task, runtime));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function taskById(tasks: TeamTaskState[]): Map<string, TeamTaskState> {
|
|
15
|
+
const map = new Map<string, TeamTaskState>();
|
|
16
|
+
for (const task of tasks) {
|
|
17
|
+
map.set(task.id, task);
|
|
18
|
+
if (task.stepId) map.set(task.stepId, task);
|
|
19
|
+
}
|
|
20
|
+
return map;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function waitingReason(task: TeamTaskState, tasks: TeamTaskState[]): string | undefined {
|
|
24
|
+
if (task.status !== "queued") return undefined;
|
|
25
|
+
const byId = taskById(tasks);
|
|
26
|
+
const waiting = task.dependsOn.map((id) => byId.get(id)?.id ?? id).filter((id) => byId.get(id)?.status !== "completed");
|
|
27
|
+
if (waiting.length === 0) return "ready";
|
|
28
|
+
return `waiting for ${waiting.join(", ")}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function formatTaskGraphLines(tasks: TeamTaskState[]): string[] {
|
|
32
|
+
if (tasks.length === 0) return ["- (none)"];
|
|
33
|
+
return tasks.map((task) => {
|
|
34
|
+
const icon = task.status === "completed" ? "✓" : task.status === "running" ? "⠋" : task.status === "failed" ? "✗" : task.status === "cancelled" || task.status === "skipped" ? "■" : "◦";
|
|
35
|
+
const wait = waitingReason(task, tasks);
|
|
36
|
+
return `- ${icon} ${task.id} [${task.status}] ${task.role}->${task.agent}${wait && wait !== "ready" ? ` (${wait})` : ""}`;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -190,6 +190,17 @@ function shouldFlushProgressEvent(event: unknown): boolean {
|
|
|
190
190
|
return type === "tool_execution_start" || type === "tool_execution_end" || type === "message_end" || type === "tool_result_end";
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
function cleanResultText(text: string | undefined): string | undefined {
|
|
194
|
+
const trimmed = text?.trim();
|
|
195
|
+
if (!trimmed) return undefined;
|
|
196
|
+
const doneIndex = trimmed.lastIndexOf("\nDONE\n");
|
|
197
|
+
if (doneIndex >= 0) return trimmed.slice(doneIndex + 1).trim();
|
|
198
|
+
if (trimmed === "DONE" || trimmed.startsWith("DONE\n")) return trimmed;
|
|
199
|
+
const fencedPromptIndex = trimmed.lastIndexOf("</file>");
|
|
200
|
+
if (fencedPromptIndex >= 0 && fencedPromptIndex < trimmed.length - 7) return trimmed.slice(fencedPromptIndex + 7).trim() || trimmed;
|
|
201
|
+
return trimmed;
|
|
202
|
+
}
|
|
203
|
+
|
|
193
204
|
function progressEventSummary(task: TeamTaskState, event: unknown): Record<string, unknown> {
|
|
194
205
|
const type = asRecord(event)?.type;
|
|
195
206
|
return {
|
|
@@ -309,6 +320,10 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
309
320
|
for (let i = 0; i < attemptModels.length; i++) {
|
|
310
321
|
const model = attemptModels[i];
|
|
311
322
|
const attemptStartedAt = new Date();
|
|
323
|
+
const pendingAttempt: ModelAttemptSummary = { model: model ?? "default", success: false };
|
|
324
|
+
task = { ...task, modelAttempts: [...modelAttempts, pendingAttempt] };
|
|
325
|
+
tasks = updateTask(tasks, task);
|
|
326
|
+
upsertCrewAgent(manifest, recordFromTask(manifest, task, "child-process"));
|
|
312
327
|
const childResult = await runChildPi({
|
|
313
328
|
cwd: task.cwd,
|
|
314
329
|
task: prompt,
|
|
@@ -329,11 +344,13 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
329
344
|
exitCode = childResult.exitCode;
|
|
330
345
|
finalStdout = childResult.stdout;
|
|
331
346
|
finalStderr = childResult.stderr;
|
|
332
|
-
parsedOutput = parsePiJsonOutput(childResult.stdout);
|
|
347
|
+
parsedOutput = parsePiJsonOutput(fs.existsSync(transcriptPath) ? fs.readFileSync(transcriptPath, "utf-8") : childResult.stdout);
|
|
333
348
|
error = childResult.error || (childResult.exitCode && childResult.exitCode !== 0 ? childResult.stderr || `Child Pi exited with ${childResult.exitCode}` : undefined);
|
|
334
349
|
persistChildProgress({ type: "attempt_finished" }, true);
|
|
335
350
|
const attempt: ModelAttemptSummary = { model: model ?? "default", success: !error, exitCode, error };
|
|
336
351
|
modelAttempts.push(attempt);
|
|
352
|
+
task = { ...task, modelAttempts: [...modelAttempts] };
|
|
353
|
+
tasks = updateTask(tasks, task);
|
|
337
354
|
logs.push(`MODEL ATTEMPT ${i + 1}: ${attempt.model}`, `success=${attempt.success}`, `exitCode=${attempt.exitCode ?? "null"}`, attempt.error ? `error=${attempt.error}` : "", "");
|
|
338
355
|
if (!error) break;
|
|
339
356
|
const nextModel = attemptModels[i + 1];
|
|
@@ -343,7 +360,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
343
360
|
resultArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
344
361
|
kind: "result",
|
|
345
362
|
relativePath: `results/${task.id}.txt`,
|
|
346
|
-
content: parsedOutput?.finalText
|
|
363
|
+
content: cleanResultText(parsedOutput?.finalText) ?? cleanResultText(finalStdout) ?? cleanResultText(finalStderr) ?? "(no output)",
|
|
347
364
|
producer: task.id,
|
|
348
365
|
});
|
|
349
366
|
logArtifact = writeArtifact(manifest.artifactsRoot, {
|