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/src/runtime/child-pi.ts
CHANGED
|
@@ -9,6 +9,9 @@ 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;
|
|
12
15
|
const MAX_COMPACT_CONTENT_CHARS = 4096;
|
|
13
16
|
const activeChildProcesses = new Map<number, ChildProcess>();
|
|
14
17
|
|
|
@@ -87,9 +90,9 @@ function compactValue(value: unknown): unknown {
|
|
|
87
90
|
function compactContentPart(part: unknown): unknown | undefined {
|
|
88
91
|
const record = asRecord(part);
|
|
89
92
|
if (!record) return undefined;
|
|
90
|
-
if (record.type === "text") return { type: "text", text: typeof record.text === "string" ? compactString(record.text) : "" };
|
|
91
|
-
if (record.type === "toolCall") return { type: "toolCall", name: record.name, input: compactValue(record.input) };
|
|
92
|
-
if (record.type === "toolResult") return { type: "toolResult", name: record.name, content: compactValue(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) };
|
|
93
96
|
return undefined;
|
|
94
97
|
}
|
|
95
98
|
|
|
@@ -102,6 +105,7 @@ function compactChildPiEvent(event: unknown): unknown | undefined {
|
|
|
102
105
|
}
|
|
103
106
|
if (record.type === "tool_result_end" || record.type === "message_end" || record.type === "message") {
|
|
104
107
|
const message = asRecord(record.message);
|
|
108
|
+
if (message?.role === "user" || message?.role === "system") return undefined;
|
|
105
109
|
const content = Array.isArray(message?.content) ? message.content.map(compactContentPart).filter((part) => part !== undefined) : undefined;
|
|
106
110
|
return {
|
|
107
111
|
type: record.type,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface ResolveBatchConcurrencyInput {
|
|
2
|
+
workflowName: string;
|
|
3
|
+
teamMaxConcurrency?: number;
|
|
4
|
+
limitMaxConcurrentWorkers?: number;
|
|
5
|
+
readyCount: number;
|
|
6
|
+
workspaceMode?: "single" | "worktree";
|
|
7
|
+
readyRoles?: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface BatchConcurrencyDecision {
|
|
11
|
+
maxConcurrent: number;
|
|
12
|
+
selectedCount: number;
|
|
13
|
+
defaultConcurrency: number;
|
|
14
|
+
reason: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function defaultWorkflowConcurrency(workflowName: string): number {
|
|
18
|
+
if (workflowName === "parallel-research") return 4;
|
|
19
|
+
if (workflowName === "research") return 2;
|
|
20
|
+
if (workflowName === "implementation" || workflowName === "review" || workflowName === "default") return 2;
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function positiveInteger(value: number | undefined): number | undefined {
|
|
25
|
+
if (value === undefined || !Number.isFinite(value)) return undefined;
|
|
26
|
+
return Math.max(1, Math.trunc(value));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveBatchConcurrency(input: ResolveBatchConcurrencyInput): BatchConcurrencyDecision {
|
|
30
|
+
const defaultConcurrency = defaultWorkflowConcurrency(input.workflowName);
|
|
31
|
+
const limitMax = positiveInteger(input.limitMaxConcurrentWorkers);
|
|
32
|
+
const teamMax = positiveInteger(input.teamMaxConcurrency);
|
|
33
|
+
const requested = limitMax ?? teamMax ?? defaultConcurrency;
|
|
34
|
+
const source = limitMax !== undefined ? "limit" : teamMax !== undefined ? "team" : "workflow";
|
|
35
|
+
const readyCount = Math.max(0, Math.trunc(input.readyCount));
|
|
36
|
+
return {
|
|
37
|
+
maxConcurrent: requested,
|
|
38
|
+
selectedCount: readyCount === 0 ? 0 : Math.min(readyCount, requested),
|
|
39
|
+
defaultConcurrency,
|
|
40
|
+
reason: `${source}:${requested};ready:${readyCount}`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface ProgressEventSummary {
|
|
2
|
+
eventType: string;
|
|
3
|
+
currentTool?: string;
|
|
4
|
+
toolCount?: number;
|
|
5
|
+
tokens?: number;
|
|
6
|
+
turns?: number;
|
|
7
|
+
activityState?: string;
|
|
8
|
+
lastActivityAt?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ProgressEventCoalesceDecision {
|
|
12
|
+
shouldAppend: boolean;
|
|
13
|
+
reason: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ProgressEventCoalesceInput {
|
|
17
|
+
previous?: ProgressEventSummary;
|
|
18
|
+
next: ProgressEventSummary;
|
|
19
|
+
nowMs: number;
|
|
20
|
+
lastAppendMs?: number;
|
|
21
|
+
minIntervalMs: number;
|
|
22
|
+
force?: boolean;
|
|
23
|
+
tokenThreshold?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DEFAULT_TOKEN_THRESHOLD = 256;
|
|
27
|
+
|
|
28
|
+
function numericIncrease(previous: number | undefined, next: number | undefined): number {
|
|
29
|
+
return next !== undefined && previous !== undefined ? next - previous : next !== undefined ? next : 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function shouldAppendProgressEventUpdate(input: ProgressEventCoalesceInput): ProgressEventCoalesceDecision {
|
|
33
|
+
if (input.force) return { shouldAppend: true, reason: "force" };
|
|
34
|
+
if (!input.previous) return { shouldAppend: true, reason: "first" };
|
|
35
|
+
if (input.previous.activityState !== input.next.activityState) return { shouldAppend: true, reason: "activity_changed" };
|
|
36
|
+
if (input.previous.currentTool !== input.next.currentTool) return { shouldAppend: true, reason: "tool_changed" };
|
|
37
|
+
if (numericIncrease(input.previous.toolCount, input.next.toolCount) > 0) return { shouldAppend: true, reason: "tool_count_increased" };
|
|
38
|
+
if (numericIncrease(input.previous.turns, input.next.turns) > 0) return { shouldAppend: true, reason: "turns_increased" };
|
|
39
|
+
const tokenIncrease = numericIncrease(input.previous.tokens, input.next.tokens);
|
|
40
|
+
if (tokenIncrease >= (input.tokenThreshold ?? DEFAULT_TOKEN_THRESHOLD)) return { shouldAppend: true, reason: "tokens_increased" };
|
|
41
|
+
if (input.lastAppendMs === undefined || input.nowMs - input.lastAppendMs >= input.minIntervalMs) return { shouldAppend: true, reason: "interval" };
|
|
42
|
+
return { shouldAppend: false, reason: "coalesced" };
|
|
43
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { loadRunManifestById } from "../state/state-store.ts";
|
|
2
|
+
import type { PiTeamsToolResult } from "../extension/tool-result.ts";
|
|
3
|
+
|
|
4
|
+
export type SubagentStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "error" | "stopped";
|
|
5
|
+
|
|
6
|
+
export interface SubagentSpawnOptions {
|
|
7
|
+
cwd: string;
|
|
8
|
+
type: string;
|
|
9
|
+
description: string;
|
|
10
|
+
prompt: string;
|
|
11
|
+
background: boolean;
|
|
12
|
+
model?: string;
|
|
13
|
+
maxTurns?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SubagentRecord {
|
|
17
|
+
id: string;
|
|
18
|
+
runId?: string;
|
|
19
|
+
type: string;
|
|
20
|
+
description: string;
|
|
21
|
+
prompt: string;
|
|
22
|
+
status: SubagentStatus;
|
|
23
|
+
startedAt: number;
|
|
24
|
+
completedAt?: number;
|
|
25
|
+
result?: string;
|
|
26
|
+
error?: string;
|
|
27
|
+
resultConsumed?: boolean;
|
|
28
|
+
model?: string;
|
|
29
|
+
background: boolean;
|
|
30
|
+
promise?: Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type SpawnRunner = (options: SubagentSpawnOptions, signal?: AbortSignal) => Promise<PiTeamsToolResult>;
|
|
34
|
+
type Notify = (record: SubagentRecord) => void;
|
|
35
|
+
|
|
36
|
+
interface QueuedSpawn {
|
|
37
|
+
record: SubagentRecord;
|
|
38
|
+
options: SubagentSpawnOptions;
|
|
39
|
+
runner: SpawnRunner;
|
|
40
|
+
signal?: AbortSignal;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const TERMINAL_RUN_STATUS = new Set(["completed", "failed", "cancelled", "blocked"]);
|
|
44
|
+
|
|
45
|
+
function resultText(result: PiTeamsToolResult): string {
|
|
46
|
+
return result.content?.map((item) => item.type === "text" ? item.text : "").filter(Boolean).join("\n") ?? "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function detailsRunId(result: PiTeamsToolResult): string | undefined {
|
|
50
|
+
const details = result.details as { runId?: unknown } | undefined;
|
|
51
|
+
return typeof details?.runId === "string" ? details.runId : undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class SubagentManager {
|
|
55
|
+
private readonly records = new Map<string, SubagentRecord>();
|
|
56
|
+
private queue: QueuedSpawn[] = [];
|
|
57
|
+
private runningBackground = 0;
|
|
58
|
+
private counter = 0;
|
|
59
|
+
private maxConcurrent: number;
|
|
60
|
+
private readonly onComplete?: Notify;
|
|
61
|
+
private readonly pollIntervalMs: number;
|
|
62
|
+
|
|
63
|
+
constructor(maxConcurrent = 4, onComplete?: Notify, pollIntervalMs = 1000) {
|
|
64
|
+
this.maxConcurrent = maxConcurrent;
|
|
65
|
+
this.onComplete = onComplete;
|
|
66
|
+
this.pollIntervalMs = pollIntervalMs;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
spawn(options: SubagentSpawnOptions, runner: SpawnRunner, signal?: AbortSignal): SubagentRecord {
|
|
70
|
+
const record: SubagentRecord = {
|
|
71
|
+
id: `agent_${Date.now().toString(36)}_${(++this.counter).toString(36)}`,
|
|
72
|
+
type: options.type,
|
|
73
|
+
description: options.description,
|
|
74
|
+
prompt: options.prompt,
|
|
75
|
+
status: options.background && this.runningBackground >= this.maxConcurrent ? "queued" : "running",
|
|
76
|
+
startedAt: Date.now(),
|
|
77
|
+
model: options.model,
|
|
78
|
+
background: options.background,
|
|
79
|
+
};
|
|
80
|
+
this.records.set(record.id, record);
|
|
81
|
+
if (record.status === "queued") {
|
|
82
|
+
this.queue.push({ record, options, runner, signal });
|
|
83
|
+
return record;
|
|
84
|
+
}
|
|
85
|
+
this.start(record, options, runner, signal);
|
|
86
|
+
return record;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getRecord(id: string): SubagentRecord | undefined {
|
|
90
|
+
return this.records.get(id);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
listAgents(): SubagentRecord[] {
|
|
94
|
+
return [...this.records.values()].sort((a, b) => b.startedAt - a.startedAt);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
abort(id: string): boolean {
|
|
98
|
+
const record = this.records.get(id);
|
|
99
|
+
if (!record) return false;
|
|
100
|
+
if (record.status === "queued") {
|
|
101
|
+
this.queue = this.queue.filter((entry) => entry.record.id !== id);
|
|
102
|
+
record.status = "stopped";
|
|
103
|
+
record.completedAt = Date.now();
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
if (record.status !== "running") return false;
|
|
107
|
+
record.status = "stopped";
|
|
108
|
+
record.completedAt = Date.now();
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
abortAll(): number {
|
|
113
|
+
let count = 0;
|
|
114
|
+
for (const entry of this.queue) {
|
|
115
|
+
entry.record.status = "stopped";
|
|
116
|
+
entry.record.completedAt = Date.now();
|
|
117
|
+
count++;
|
|
118
|
+
}
|
|
119
|
+
this.queue = [];
|
|
120
|
+
for (const record of this.records.values()) {
|
|
121
|
+
if (record.status === "running") {
|
|
122
|
+
record.status = "stopped";
|
|
123
|
+
record.completedAt = Date.now();
|
|
124
|
+
count++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return count;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async waitForAll(): Promise<void> {
|
|
131
|
+
while (true) {
|
|
132
|
+
this.drainQueue();
|
|
133
|
+
const pending = this.listAgents().filter((record) => record.status === "running" || record.status === "queued").map((record) => record.promise).filter((promise): promise is Promise<void> => Boolean(promise));
|
|
134
|
+
if (!pending.length) break;
|
|
135
|
+
await Promise.allSettled(pending);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async waitForRecord(id: string): Promise<SubagentRecord | undefined> {
|
|
140
|
+
while (true) {
|
|
141
|
+
const record = this.records.get(id);
|
|
142
|
+
if (!record) return undefined;
|
|
143
|
+
if (record.status !== "running" && record.status !== "queued") return record;
|
|
144
|
+
if (record.promise) await record.promise;
|
|
145
|
+
else await new Promise((resolve) => setTimeout(resolve, 100));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
setMaxConcurrent(value: number): void {
|
|
150
|
+
this.maxConcurrent = Math.max(1, Math.floor(value));
|
|
151
|
+
this.drainQueue();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private start(record: SubagentRecord, options: SubagentSpawnOptions, runner: SpawnRunner, signal?: AbortSignal): void {
|
|
155
|
+
if (options.background) this.runningBackground++;
|
|
156
|
+
record.status = "running";
|
|
157
|
+
record.startedAt = Date.now();
|
|
158
|
+
record.promise = (async () => {
|
|
159
|
+
try {
|
|
160
|
+
const result = await runner(options, signal);
|
|
161
|
+
record.runId = detailsRunId(result);
|
|
162
|
+
record.result = resultText(result);
|
|
163
|
+
if (result.isError) {
|
|
164
|
+
record.status = "error";
|
|
165
|
+
record.error = record.result;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (record.runId) await this.pollRunToTerminal(options.cwd, record);
|
|
169
|
+
else record.status = "completed";
|
|
170
|
+
} catch (error) {
|
|
171
|
+
record.status = "error";
|
|
172
|
+
record.error = error instanceof Error ? error.message : String(error);
|
|
173
|
+
} finally {
|
|
174
|
+
if (options.background) this.runningBackground = Math.max(0, this.runningBackground - 1);
|
|
175
|
+
record.completedAt = record.completedAt ?? Date.now();
|
|
176
|
+
this.onComplete?.(record);
|
|
177
|
+
this.drainQueue();
|
|
178
|
+
}
|
|
179
|
+
})();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private drainQueue(): void {
|
|
183
|
+
while (this.queue.length > 0 && this.runningBackground < this.maxConcurrent) {
|
|
184
|
+
const next = this.queue.shift();
|
|
185
|
+
if (!next || next.record.status !== "queued") continue;
|
|
186
|
+
this.start(next.record, next.options, next.runner, next.signal);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private async pollRunToTerminal(cwd: string, record: SubagentRecord): Promise<void> {
|
|
191
|
+
while (record.runId && record.status === "running") {
|
|
192
|
+
const loaded = loadRunManifestById(cwd, record.runId);
|
|
193
|
+
if (loaded && TERMINAL_RUN_STATUS.has(loaded.manifest.status)) {
|
|
194
|
+
record.status = loaded.manifest.status === "completed" ? "completed" : loaded.manifest.status === "cancelled" ? "cancelled" : "failed";
|
|
195
|
+
record.error = record.status === "completed" ? undefined : loaded.manifest.summary;
|
|
196
|
+
record.completedAt = Date.now();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -22,6 +22,7 @@ import { parseSessionUsage } from "./session-usage.ts";
|
|
|
22
22
|
import type { CrewAgentProgress, CrewRuntimeKind } from "./crew-agent-runtime.ts";
|
|
23
23
|
import { buildMemoryBlock } from "./agent-memory.ts";
|
|
24
24
|
import { runLiveSessionTask } from "./live-session-runtime.ts";
|
|
25
|
+
import { shouldAppendProgressEventUpdate, type ProgressEventSummary } from "./progress-event-coalescer.ts";
|
|
25
26
|
|
|
26
27
|
export interface TaskRunnerInput {
|
|
27
28
|
manifest: TeamRunManifest;
|
|
@@ -201,7 +202,7 @@ function cleanResultText(text: string | undefined): string | undefined {
|
|
|
201
202
|
return trimmed;
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
function progressEventSummary(task: TeamTaskState, event: unknown):
|
|
205
|
+
function progressEventSummary(task: TeamTaskState, event: unknown): ProgressEventSummary {
|
|
205
206
|
const type = asRecord(event)?.type;
|
|
206
207
|
return {
|
|
207
208
|
eventType: typeof type === "string" ? type : "event",
|
|
@@ -306,14 +307,18 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
306
307
|
const transcriptPath = `${manifest.artifactsRoot}/transcripts/${task.id}.jsonl`;
|
|
307
308
|
let lastAgentRecordPersistedAt = 0;
|
|
308
309
|
let lastRunProgressPersistedAt = 0;
|
|
310
|
+
let lastRunProgressSummary: ProgressEventSummary | undefined;
|
|
309
311
|
const persistChildProgress = (event: unknown, force = false): void => {
|
|
310
312
|
const now = Date.now();
|
|
311
313
|
if (force || shouldFlushProgressEvent(event) || now - lastAgentRecordPersistedAt >= 500) {
|
|
312
314
|
upsertCrewAgent(manifest, recordFromTask(manifest, task, "child-process"));
|
|
313
315
|
lastAgentRecordPersistedAt = now;
|
|
314
316
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
+
const summary = progressEventSummary(task, event);
|
|
318
|
+
const decision = shouldAppendProgressEventUpdate({ previous: lastRunProgressSummary, next: summary, nowMs: now, lastAppendMs: lastRunProgressPersistedAt || undefined, minIntervalMs: 1000, force });
|
|
319
|
+
if (decision.shouldAppend) {
|
|
320
|
+
appendEvent(manifest.eventsPath, { type: "task.progress", runId: manifest.runId, taskId: task.id, data: { ...summary, coalesceReason: decision.reason } });
|
|
321
|
+
lastRunProgressSummary = summary;
|
|
317
322
|
lastRunProgressPersistedAt = now;
|
|
318
323
|
}
|
|
319
324
|
};
|
|
@@ -389,14 +394,18 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
389
394
|
const transcriptPath = `${manifest.artifactsRoot}/transcripts/${task.id}.jsonl`;
|
|
390
395
|
let lastAgentRecordPersistedAt = 0;
|
|
391
396
|
let lastRunProgressPersistedAt = 0;
|
|
397
|
+
let lastRunProgressSummary: ProgressEventSummary | undefined;
|
|
392
398
|
const persistLiveProgress = (event: unknown, force = false): void => {
|
|
393
399
|
const now = Date.now();
|
|
394
400
|
if (force || shouldFlushProgressEvent(event) || now - lastAgentRecordPersistedAt >= 500) {
|
|
395
401
|
upsertCrewAgent(manifest, recordFromTask(manifest, task, "live-session"));
|
|
396
402
|
lastAgentRecordPersistedAt = now;
|
|
397
403
|
}
|
|
398
|
-
|
|
399
|
-
|
|
404
|
+
const summary = progressEventSummary(task, event);
|
|
405
|
+
const decision = shouldAppendProgressEventUpdate({ previous: lastRunProgressSummary, next: summary, nowMs: now, lastAppendMs: lastRunProgressPersistedAt || undefined, minIntervalMs: 1000, force });
|
|
406
|
+
if (decision.shouldAppend) {
|
|
407
|
+
appendEvent(manifest.eventsPath, { type: "task.progress", runId: manifest.runId, taskId: task.id, data: { ...summary, coalesceReason: decision.reason } });
|
|
408
|
+
lastRunProgressSummary = summary;
|
|
400
409
|
lastRunProgressPersistedAt = now;
|
|
401
410
|
}
|
|
402
411
|
};
|