pi-crew 0.1.28 → 0.1.30
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 +42 -0
- package/NOTICE.md +1 -0
- package/docs/architecture.md +164 -92
- package/docs/refactor-tasks-phase6.md +662 -0
- package/docs/runtime-flow.md +148 -0
- package/package.json +1 -1
- package/schema.json +1 -0
- package/skills/git-master/SKILL.md +19 -0
- package/skills/read-only-explorer/SKILL.md +21 -0
- package/skills/safe-bash/SKILL.md +16 -0
- package/skills/task-packet/SKILL.md +23 -0
- package/skills/verify-evidence/SKILL.md +22 -0
- package/src/config/config.ts +2 -0
- package/src/config/defaults.ts +1 -0
- package/src/extension/async-notifier.ts +33 -4
- package/src/extension/register.ts +15 -522
- package/src/extension/registration/artifact-cleanup.ts +14 -0
- package/src/extension/registration/commands.ts +208 -0
- package/src/extension/registration/subagent-helpers.ts +1 -1
- package/src/extension/registration/subagent-tools.ts +110 -0
- package/src/extension/registration/team-tool.ts +44 -0
- package/src/extension/team-tool/api.ts +4 -4
- package/src/extension/team-tool/cancel.ts +31 -0
- package/src/extension/team-tool/inspect.ts +41 -0
- package/src/extension/team-tool/lifecycle-actions.ts +79 -0
- package/src/extension/team-tool/plan.ts +19 -0
- package/src/extension/team-tool/run.ts +41 -3
- package/src/extension/team-tool/status.ts +73 -0
- package/src/extension/team-tool.ts +57 -224
- package/src/runtime/async-marker.ts +26 -0
- package/src/runtime/async-runner.ts +44 -9
- package/src/runtime/background-runner.ts +2 -0
- package/src/runtime/child-pi.ts +5 -1
- package/src/runtime/concurrency.ts +9 -3
- package/src/runtime/crew-agent-records.ts +1 -0
- package/src/runtime/crew-agent-runtime.ts +2 -1
- package/src/runtime/model-fallback.ts +21 -4
- package/src/runtime/pi-args.ts +2 -0
- package/src/runtime/process-status.ts +1 -0
- package/src/runtime/role-permission.ts +11 -0
- package/src/runtime/task-runner/live-executor.ts +98 -0
- package/src/runtime/task-runner/progress.ts +111 -0
- package/src/runtime/task-runner/prompt-builder.ts +72 -0
- package/src/runtime/task-runner/result-utils.ts +14 -0
- package/src/runtime/task-runner/state-helpers.ts +22 -0
- package/src/runtime/task-runner.ts +38 -283
- package/src/runtime/team-runner.ts +116 -7
- package/src/schema/config-schema.ts +1 -0
- package/src/state/mailbox.ts +28 -0
- package/src/state/types.ts +16 -0
- package/src/subagents/async-entry.ts +1 -0
- package/src/subagents/index.ts +3 -0
- package/src/subagents/live/control.ts +1 -0
- package/src/subagents/live/manager.ts +1 -0
- package/src/subagents/live/realtime.ts +1 -0
- package/src/subagents/live/session-runtime.ts +1 -0
- package/src/subagents/manager.ts +1 -0
- package/src/subagents/spawn.ts +1 -0
- package/src/ui/live-run-sidebar.ts +1 -1
|
@@ -4,15 +4,15 @@ import type { CrewLimitsConfig, CrewRuntimeConfig } from "../config/config.ts";
|
|
|
4
4
|
import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
|
|
5
5
|
import { writeArtifact } from "../state/artifact-store.ts";
|
|
6
6
|
import { appendEvent } from "../state/event-log.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { saveRunManifest } from "../state/state-store.ts";
|
|
8
8
|
import { createTaskClaim } from "../state/task-claims.ts";
|
|
9
9
|
import { createWorkerHeartbeat, touchWorkerHeartbeat } from "./worker-heartbeat.ts";
|
|
10
10
|
import type { WorkflowStep } from "../workflows/workflow-config.ts";
|
|
11
11
|
import { captureWorktreeDiff, captureWorktreeDiffStat, prepareTaskWorkspace } from "../worktree/worktree-manager.ts";
|
|
12
|
-
import {
|
|
12
|
+
import { buildConfiguredModelRouting, formatModelAttemptNote, isRetryableModelFailure, type ModelAttemptSummary } from "./model-fallback.ts";
|
|
13
13
|
import { parsePiJsonOutput, type ParsedPiJsonOutput } from "./pi-json-output.ts";
|
|
14
14
|
import { runChildPi } from "./child-pi.ts";
|
|
15
|
-
import { buildTaskPacket
|
|
15
|
+
import { buildTaskPacket } from "./task-packet.ts";
|
|
16
16
|
import { createVerificationEvidence } from "./green-contract.ts";
|
|
17
17
|
import { createStartupEvidence } from "./worker-startup.ts";
|
|
18
18
|
import { permissionForRole } from "./role-permission.ts";
|
|
@@ -20,9 +20,11 @@ import { collectDependencyOutputContext, renderDependencyOutputContext, writeTas
|
|
|
20
20
|
import { appendCrewAgentEvent, appendCrewAgentOutput, emptyCrewAgentProgress, recordFromTask, upsertCrewAgent } from "./crew-agent-records.ts";
|
|
21
21
|
import { parseSessionUsage } from "./session-usage.ts";
|
|
22
22
|
import type { CrewAgentProgress, CrewRuntimeKind } from "./crew-agent-runtime.ts";
|
|
23
|
-
import { buildMemoryBlock } from "./agent-memory.ts";
|
|
24
|
-
import { runLiveSessionTask } from "./live-session-runtime.ts";
|
|
25
23
|
import { shouldAppendProgressEventUpdate, type ProgressEventSummary } from "./progress-event-coalescer.ts";
|
|
24
|
+
import { coordinationBridgeInstructions, renderTaskPrompt } from "./task-runner/prompt-builder.ts";
|
|
25
|
+
import { applyAgentProgressEvent, applyUsageToProgress, progressEventSummary, shouldFlushProgressEvent } from "./task-runner/progress.ts";
|
|
26
|
+
import { checkpointTask, persistSingleTaskUpdate, updateTask } from "./task-runner/state-helpers.ts";
|
|
27
|
+
import { cleanResultText, isFinalChildEvent } from "./task-runner/result-utils.ts";
|
|
26
28
|
|
|
27
29
|
export interface TaskRunnerInput {
|
|
28
30
|
manifest: TeamRunManifest;
|
|
@@ -42,218 +44,6 @@ export interface TaskRunnerInput {
|
|
|
42
44
|
dependencyContextText?: string;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
function readOnlyRoleInstructions(role: string): string {
|
|
46
|
-
if (permissionForRole(role) !== "read_only") return "";
|
|
47
|
-
return [
|
|
48
|
-
"# READ-ONLY ROLE CONTRACT",
|
|
49
|
-
"You are running in READ-ONLY mode for this task.",
|
|
50
|
-
"- Do not create, modify, delete, move, or copy files.",
|
|
51
|
-
"- Do not use shell redirects, heredocs, in-place edits, package installs, git commit/merge/rebase/reset/checkout, or other state-mutating commands.",
|
|
52
|
-
"- If implementation changes are needed, report exact recommendations instead of applying them.",
|
|
53
|
-
"- Prefer read/grep/find/listing tools and read-only git inspection commands.",
|
|
54
|
-
].join("\n");
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function coordinationBridgeInstructions(task: TeamTaskState): string {
|
|
58
|
-
return [
|
|
59
|
-
"# Crew Coordination Channel",
|
|
60
|
-
`Mailbox target for this task: ${task.id}`,
|
|
61
|
-
"Use the run mailbox contract for coordination with the leader/orchestrator:",
|
|
62
|
-
"- If blocked or uncertain, report the blocker in your final result and, when mailbox tools/API are available, send an inbox/outbox message addressed to the leader.",
|
|
63
|
-
"- If nudged, answer with current status, blocker, or smallest next step.",
|
|
64
|
-
"- Treat inherited/dependency context as reference-only; do not continue the parent conversation directly.",
|
|
65
|
-
"- Completion handoff should include: DONE/FAILED, summary, changed/read files, verification evidence, and remaining risks.",
|
|
66
|
-
].join("\n");
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function renderTaskPrompt(manifest: TeamRunManifest, step: WorkflowStep, task: TeamTaskState, agent?: AgentConfig): string {
|
|
70
|
-
const memoryBlock = agent?.memory ? buildMemoryBlock(agent.name, agent.memory, task.cwd, Boolean(agent.tools?.some((tool) => tool === "write" || tool === "edit"))) : "";
|
|
71
|
-
return [
|
|
72
|
-
"# pi-crew Worker Runtime Context",
|
|
73
|
-
`Run ID: ${manifest.runId}`,
|
|
74
|
-
`Team: ${manifest.team}`,
|
|
75
|
-
`Workflow: ${manifest.workflow ?? "(none)"}`,
|
|
76
|
-
`State root: ${manifest.stateRoot}`,
|
|
77
|
-
`Artifacts root: ${manifest.artifactsRoot}`,
|
|
78
|
-
`Events path: ${manifest.eventsPath}`,
|
|
79
|
-
`Task ID: ${task.id}`,
|
|
80
|
-
`Task cwd: ${task.cwd}`,
|
|
81
|
-
`Workspace mode: ${manifest.workspaceMode}`,
|
|
82
|
-
"",
|
|
83
|
-
`Goal:\n${manifest.goal}`,
|
|
84
|
-
"",
|
|
85
|
-
`Step: ${step.id}`,
|
|
86
|
-
`Role: ${step.role}`,
|
|
87
|
-
"",
|
|
88
|
-
"Protocol:",
|
|
89
|
-
"- Stay within the task scope unless the prompt explicitly says otherwise.",
|
|
90
|
-
"- Report blockers and verification evidence in the final result.",
|
|
91
|
-
"- Do not claim completion without evidence.",
|
|
92
|
-
"- Follow the Task Packet contract below; escalate if any contract field is impossible to satisfy.",
|
|
93
|
-
"",
|
|
94
|
-
readOnlyRoleInstructions(task.role),
|
|
95
|
-
"",
|
|
96
|
-
coordinationBridgeInstructions(task),
|
|
97
|
-
"",
|
|
98
|
-
task.taskPacket ? renderTaskPacket(task.taskPacket) : "",
|
|
99
|
-
"",
|
|
100
|
-
(inputDependencyContext(task) || ""),
|
|
101
|
-
memoryBlock,
|
|
102
|
-
"Task:",
|
|
103
|
-
step.task.replaceAll("{goal}", manifest.goal),
|
|
104
|
-
].join("\n");
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function inputDependencyContext(task: TeamTaskState): string {
|
|
108
|
-
return (task as TeamTaskState & { dependencyContextText?: string }).dependencyContextText ?? "";
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function updateTask(tasks: TeamTaskState[], updated: TeamTaskState): TeamTaskState[] {
|
|
112
|
-
return tasks.map((task) => task.id === updated.id ? updated : task);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function persistSingleTaskUpdate(manifest: TeamRunManifest, fallbackTasks: TeamTaskState[], updated: TeamTaskState): TeamTaskState[] {
|
|
116
|
-
const latest = loadRunManifestById(manifest.cwd, manifest.runId)?.tasks ?? fallbackTasks;
|
|
117
|
-
const merged = updateTask(latest, updated);
|
|
118
|
-
saveRunTasks(manifest, merged);
|
|
119
|
-
return merged;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
123
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function textFromContent(content: unknown): string[] {
|
|
127
|
-
if (typeof content === "string") return [content];
|
|
128
|
-
if (!Array.isArray(content)) return [];
|
|
129
|
-
const text: string[] = [];
|
|
130
|
-
for (const part of content) {
|
|
131
|
-
const obj = asRecord(part);
|
|
132
|
-
if (!obj) continue;
|
|
133
|
-
if (obj.type === "text" && typeof obj.text === "string") text.push(obj.text);
|
|
134
|
-
else if (typeof obj.content === "string") text.push(obj.content);
|
|
135
|
-
}
|
|
136
|
-
return text;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function eventText(event: unknown): string[] {
|
|
140
|
-
const obj = asRecord(event);
|
|
141
|
-
if (!obj) return [];
|
|
142
|
-
const text: string[] = [];
|
|
143
|
-
if (typeof obj.text === "string") text.push(obj.text);
|
|
144
|
-
if (typeof obj.output === "string") text.push(obj.output);
|
|
145
|
-
text.push(...textFromContent(obj.content));
|
|
146
|
-
const message = asRecord(obj.message);
|
|
147
|
-
if (message) text.push(...textFromContent(message.content));
|
|
148
|
-
return text.filter((entry) => entry.trim());
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function numberField(obj: Record<string, unknown>, keys: string[]): number | undefined {
|
|
152
|
-
for (const key of keys) {
|
|
153
|
-
const value = obj[key];
|
|
154
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
155
|
-
}
|
|
156
|
-
return undefined;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function eventUsage(event: unknown): { input?: number; output?: number; turns?: number } | undefined {
|
|
160
|
-
const obj = asRecord(event);
|
|
161
|
-
if (!obj) return undefined;
|
|
162
|
-
const direct = {
|
|
163
|
-
input: numberField(obj, ["input", "inputTokens", "input_tokens"]),
|
|
164
|
-
output: numberField(obj, ["output", "outputTokens", "output_tokens"]),
|
|
165
|
-
turns: numberField(obj, ["turns", "turnCount", "turn_count"]),
|
|
166
|
-
};
|
|
167
|
-
if (Object.values(direct).some((value) => value !== undefined)) return direct;
|
|
168
|
-
for (const key of ["usage", "tokenUsage", "tokens", "stats"]) {
|
|
169
|
-
const nested = eventUsage(obj[key]);
|
|
170
|
-
if (nested) return nested;
|
|
171
|
-
}
|
|
172
|
-
const message = asRecord(obj.message);
|
|
173
|
-
return message ? eventUsage(message.usage) : undefined;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function previewArgs(args: unknown): string | undefined {
|
|
177
|
-
if (!args) return undefined;
|
|
178
|
-
try {
|
|
179
|
-
const text = typeof args === "string" ? args : JSON.stringify(args);
|
|
180
|
-
return text.length > 240 ? `${text.slice(0, 240)}…` : text;
|
|
181
|
-
} catch {
|
|
182
|
-
return undefined;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function applyUsageToProgress(progress: CrewAgentProgress | undefined, usage: UsageState | undefined): CrewAgentProgress | undefined {
|
|
187
|
-
if (!usage) return progress;
|
|
188
|
-
const base = progress ?? emptyCrewAgentProgress();
|
|
189
|
-
return {
|
|
190
|
-
...base,
|
|
191
|
-
tokens: (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0),
|
|
192
|
-
turns: usage.turns ?? base.turns,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function shouldFlushProgressEvent(event: unknown): boolean {
|
|
197
|
-
const type = asRecord(event)?.type;
|
|
198
|
-
return type === "tool_execution_start" || type === "tool_execution_end" || type === "message_end" || type === "tool_result_end";
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function cleanResultText(text: string | undefined): string | undefined {
|
|
202
|
-
const trimmed = text?.trim();
|
|
203
|
-
if (!trimmed) return undefined;
|
|
204
|
-
const doneIndex = trimmed.lastIndexOf("\nDONE\n");
|
|
205
|
-
if (doneIndex >= 0) return trimmed.slice(doneIndex + 1).trim();
|
|
206
|
-
if (trimmed === "DONE" || trimmed.startsWith("DONE\n")) return trimmed;
|
|
207
|
-
const fencedPromptIndex = trimmed.lastIndexOf("</file>");
|
|
208
|
-
if (fencedPromptIndex >= 0 && fencedPromptIndex < trimmed.length - 7) return trimmed.slice(fencedPromptIndex + 7).trim() || trimmed;
|
|
209
|
-
return trimmed;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function progressEventSummary(task: TeamTaskState, event: unknown): ProgressEventSummary {
|
|
213
|
-
const type = asRecord(event)?.type;
|
|
214
|
-
return {
|
|
215
|
-
eventType: typeof type === "string" ? type : "event",
|
|
216
|
-
currentTool: task.agentProgress?.currentTool,
|
|
217
|
-
toolCount: task.agentProgress?.toolCount,
|
|
218
|
-
tokens: task.agentProgress?.tokens,
|
|
219
|
-
turns: task.agentProgress?.turns,
|
|
220
|
-
activityState: task.agentProgress?.activityState,
|
|
221
|
-
lastActivityAt: task.agentProgress?.lastActivityAt,
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function applyAgentProgressEvent(progress: CrewAgentProgress, event: unknown, startedAt: string | undefined): CrewAgentProgress {
|
|
226
|
-
const obj = asRecord(event);
|
|
227
|
-
const now = new Date().toISOString();
|
|
228
|
-
const next: CrewAgentProgress = { ...progress, recentTools: [...progress.recentTools], recentOutput: [...progress.recentOutput], lastActivityAt: now, activityState: "active" };
|
|
229
|
-
if (startedAt) next.durationMs = Date.now() - new Date(startedAt).getTime();
|
|
230
|
-
if (obj?.type === "tool_execution_start") {
|
|
231
|
-
next.toolCount += 1;
|
|
232
|
-
next.currentTool = typeof obj.toolName === "string" ? obj.toolName : typeof obj.name === "string" ? obj.name : "tool";
|
|
233
|
-
next.currentToolArgs = previewArgs(obj.args);
|
|
234
|
-
next.currentToolStartedAt = now;
|
|
235
|
-
}
|
|
236
|
-
if (obj?.type === "tool_execution_end") {
|
|
237
|
-
if (next.currentTool) next.recentTools.push({ tool: next.currentTool, args: next.currentToolArgs, endedAt: now });
|
|
238
|
-
next.currentTool = undefined;
|
|
239
|
-
next.currentToolArgs = undefined;
|
|
240
|
-
next.currentToolStartedAt = undefined;
|
|
241
|
-
}
|
|
242
|
-
if ((obj?.type === "tool_execution_error" || obj?.type === "tool_execution_failed") && next.currentTool) {
|
|
243
|
-
next.failedTool = next.currentTool;
|
|
244
|
-
}
|
|
245
|
-
const usage = eventUsage(event);
|
|
246
|
-
if (usage) {
|
|
247
|
-
next.tokens = (usage.input ?? 0) + (usage.output ?? 0);
|
|
248
|
-
next.turns = usage.turns ?? next.turns;
|
|
249
|
-
}
|
|
250
|
-
const text = eventText(event);
|
|
251
|
-
if (text.length > 0) next.recentOutput.push(...text.flatMap((entry) => entry.split(/\r?\n/)).filter(Boolean).slice(-10));
|
|
252
|
-
if (next.recentTools.length > 25) next.recentTools.splice(0, next.recentTools.length - 25);
|
|
253
|
-
if (next.recentOutput.length > 50) next.recentOutput.splice(0, next.recentOutput.length - 50);
|
|
254
|
-
return next;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
47
|
export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
|
|
258
48
|
let manifest = input.manifest;
|
|
259
49
|
const workspace = prepareTaskWorkspace(manifest, input.task);
|
|
@@ -276,6 +66,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
276
66
|
let tasks = updateTask(input.tasks, task);
|
|
277
67
|
const runtimeKind = input.runtimeKind ?? (input.executeWorkers ? "child-process" : "scaffold");
|
|
278
68
|
tasks = persistSingleTaskUpdate(manifest, tasks, task);
|
|
69
|
+
if (runtimeKind === "child-process") ({ task, tasks } = checkpointTask(manifest, tasks, task, "started"));
|
|
279
70
|
upsertCrewAgent(manifest, recordFromTask(manifest, task, runtimeKind));
|
|
280
71
|
appendEvent(manifest.eventsPath, { type: "task.started", runId: manifest.runId, taskId: task.id, data: { role: task.role, agent: task.agent, runtime: runtimeKind, cwd: task.cwd, worktreePath: workspace.worktreePath, worktreeBranch: workspace.branch, worktreeReused: workspace.reused } });
|
|
281
72
|
const permissionMode = permissionForRole(task.role);
|
|
@@ -305,13 +96,15 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
305
96
|
producer: task.id,
|
|
306
97
|
});
|
|
307
98
|
if (runtimeKind === "child-process") {
|
|
308
|
-
const
|
|
99
|
+
const modelRoutingPlan = buildConfiguredModelRouting({ overrideModel: input.modelOverride, stepModel: input.step.model, agentModel: input.agent.model, fallbackModels: input.agent.fallbackModels, parentModel: input.parentModel, modelRegistry: input.modelRegistry, cwd: manifest.cwd });
|
|
100
|
+
const candidates = modelRoutingPlan.candidates;
|
|
309
101
|
const attemptModels = candidates.length > 0 ? candidates : [undefined];
|
|
310
102
|
const logs: string[] = [];
|
|
311
103
|
let finalStdout = "";
|
|
312
104
|
let finalStderr = "";
|
|
313
105
|
modelAttempts = [];
|
|
314
106
|
const transcriptPath = `${manifest.artifactsRoot}/transcripts/${task.id}.jsonl`;
|
|
107
|
+
let finalCheckpointWritten = false;
|
|
315
108
|
let lastAgentRecordPersistedAt = 0;
|
|
316
109
|
let lastRunProgressPersistedAt = 0;
|
|
317
110
|
let lastRunProgressSummary: ProgressEventSummary | undefined;
|
|
@@ -344,11 +137,18 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
344
137
|
signal: input.signal,
|
|
345
138
|
transcriptPath,
|
|
346
139
|
maxDepth: input.limits?.maxTaskDepth,
|
|
140
|
+
onSpawn: (pid) => {
|
|
141
|
+
({ task, tasks } = checkpointTask(manifest, tasks, task, "child-spawned", pid));
|
|
142
|
+
},
|
|
347
143
|
onStdoutLine: (line) => appendCrewAgentOutput(manifest, task.id, line),
|
|
348
144
|
onJsonEvent: (event) => {
|
|
349
145
|
appendCrewAgentEvent(manifest, task.id, event);
|
|
350
146
|
task = { ...task, agentProgress: applyAgentProgressEvent(task.agentProgress ?? emptyCrewAgentProgress(), event, task.startedAt) };
|
|
351
147
|
tasks = updateTask(tasks, task);
|
|
148
|
+
if (!finalCheckpointWritten && isFinalChildEvent(event)) {
|
|
149
|
+
finalCheckpointWritten = true;
|
|
150
|
+
({ task, tasks } = checkpointTask(manifest, tasks, task, "child-stdout-final"));
|
|
151
|
+
}
|
|
352
152
|
persistChildProgress(event);
|
|
353
153
|
},
|
|
354
154
|
});
|
|
@@ -381,6 +181,12 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
381
181
|
content: [...logs, `finalExitCode=${exitCode ?? "null"}`, `jsonEvents=${parsedOutput?.jsonEvents ?? 0}`, parsedOutput?.usage ? `usage=${JSON.stringify(parsedOutput.usage)}` : "", "", "STDOUT:", finalStdout, "", "STDERR:", finalStderr].join("\n"),
|
|
382
182
|
producer: task.id,
|
|
383
183
|
});
|
|
184
|
+
const successfulAttemptIndex = modelAttempts.findIndex((attempt) => attempt.success);
|
|
185
|
+
const usedAttempt = successfulAttemptIndex === -1 ? Math.max(0, modelAttempts.length - 1) : successfulAttemptIndex;
|
|
186
|
+
const resolvedModel = modelAttempts[usedAttempt]?.model ?? candidates[0] ?? "default";
|
|
187
|
+
const fallbackReason = usedAttempt > 0 ? modelAttempts[usedAttempt - 1]?.error : undefined;
|
|
188
|
+
task = { ...task, modelRouting: { requested: modelRoutingPlan.requested, resolved: resolvedModel, fallbackChain: candidates, reason: fallbackReason ?? modelRoutingPlan.reason, usedAttempt } };
|
|
189
|
+
tasks = updateTask(tasks, task);
|
|
384
190
|
const sessionUsage = parseSessionUsage(transcriptPath);
|
|
385
191
|
const effectiveUsage = parsedOutput?.usage ?? sessionUsage;
|
|
386
192
|
if (effectiveUsage) {
|
|
@@ -397,72 +203,21 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
397
203
|
producer: task.id,
|
|
398
204
|
});
|
|
399
205
|
}
|
|
206
|
+
task = { ...task, resultArtifact, ...(logArtifact ? { logArtifact } : {}), ...(transcriptArtifact ? { transcriptArtifact } : {}) };
|
|
207
|
+
tasks = updateTask(tasks, task);
|
|
208
|
+
({ task, tasks } = checkpointTask(manifest, tasks, task, "artifact-written"));
|
|
400
209
|
} else if (runtimeKind === "live-session") {
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const decision = shouldAppendProgressEventUpdate({ previous: lastRunProgressSummary, next: summary, nowMs: now, lastAppendMs: lastRunProgressPersistedAt || undefined, minIntervalMs: 1000, force });
|
|
413
|
-
if (decision.shouldAppend) {
|
|
414
|
-
appendEvent(manifest.eventsPath, { type: "task.progress", runId: manifest.runId, taskId: task.id, data: { ...summary, coalesceReason: decision.reason } });
|
|
415
|
-
lastRunProgressSummary = summary;
|
|
416
|
-
lastRunProgressPersistedAt = now;
|
|
417
|
-
}
|
|
418
|
-
};
|
|
419
|
-
const attemptStartedAt = new Date();
|
|
420
|
-
const liveResult = await runLiveSessionTask({
|
|
421
|
-
manifest,
|
|
422
|
-
task,
|
|
423
|
-
step: input.step,
|
|
424
|
-
agent: input.agent,
|
|
425
|
-
prompt,
|
|
426
|
-
signal: input.signal,
|
|
427
|
-
transcriptPath,
|
|
428
|
-
runtimeConfig: input.runtimeConfig,
|
|
429
|
-
parentContext: input.parentContext,
|
|
430
|
-
parentModel: input.parentModel,
|
|
431
|
-
modelRegistry: input.modelRegistry,
|
|
432
|
-
onOutput: (text) => appendCrewAgentOutput(manifest, task.id, text),
|
|
433
|
-
onEvent: (event) => {
|
|
434
|
-
appendCrewAgentEvent(manifest, task.id, event);
|
|
435
|
-
task = { ...task, agentProgress: applyAgentProgressEvent(task.agentProgress ?? emptyCrewAgentProgress(), event, task.startedAt) };
|
|
436
|
-
tasks = updateTask(tasks, task);
|
|
437
|
-
persistLiveProgress(event);
|
|
438
|
-
},
|
|
439
|
-
});
|
|
440
|
-
startupEvidence = createStartupEvidence({ command: "live-session", startedAt: attemptStartedAt, finishedAt: new Date(), promptSentAt: attemptStartedAt, promptAccepted: liveResult.exitCode === 0 && !liveResult.error, stderr: liveResult.stderr, error: liveResult.error, exitCode: liveResult.exitCode });
|
|
441
|
-
exitCode = liveResult.exitCode;
|
|
442
|
-
error = liveResult.error || (liveResult.exitCode && liveResult.exitCode !== 0 ? liveResult.stderr || `Live session exited with ${liveResult.exitCode}` : undefined);
|
|
443
|
-
parsedOutput = { finalText: liveResult.stdout, textEvents: liveResult.stdout ? [liveResult.stdout] : [], jsonEvents: liveResult.jsonEvents, usage: liveResult.usage };
|
|
444
|
-
if (liveResult.usage) task = { ...task, usage: liveResult.usage, agentProgress: applyUsageToProgress(task.agentProgress, liveResult.usage) };
|
|
445
|
-
persistLiveProgress({ type: "attempt_finished" }, true);
|
|
446
|
-
resultArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
447
|
-
kind: "result",
|
|
448
|
-
relativePath: `results/${task.id}.txt`,
|
|
449
|
-
content: liveResult.stdout || liveResult.stderr || "(no output)",
|
|
450
|
-
producer: task.id,
|
|
451
|
-
});
|
|
452
|
-
logArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
453
|
-
kind: "log",
|
|
454
|
-
relativePath: `logs/${task.id}.log`,
|
|
455
|
-
content: [`runtime=live-session`, `finalExitCode=${exitCode ?? "null"}`, `jsonEvents=${liveResult.jsonEvents}`, liveResult.usage ? `usage=${JSON.stringify(liveResult.usage)}` : "", "", "STDOUT:", liveResult.stdout, "", "STDERR:", liveResult.stderr].join("\n"),
|
|
456
|
-
producer: task.id,
|
|
457
|
-
});
|
|
458
|
-
if (fs.existsSync(transcriptPath)) {
|
|
459
|
-
transcriptArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
460
|
-
kind: "log",
|
|
461
|
-
relativePath: `transcripts/${task.id}.jsonl`,
|
|
462
|
-
content: fs.readFileSync(transcriptPath, "utf-8"),
|
|
463
|
-
producer: task.id,
|
|
464
|
-
});
|
|
465
|
-
}
|
|
210
|
+
const { runLiveTask } = await import("./task-runner/live-executor.ts");
|
|
211
|
+
const live = await runLiveTask({ manifest, tasks, task, step: input.step, agent: input.agent, prompt, signal: input.signal, runtimeConfig: input.runtimeConfig, parentContext: input.parentContext, parentModel: input.parentModel, modelRegistry: input.modelRegistry });
|
|
212
|
+
task = live.task;
|
|
213
|
+
tasks = live.tasks;
|
|
214
|
+
startupEvidence = live.startupEvidence;
|
|
215
|
+
exitCode = live.exitCode;
|
|
216
|
+
error = live.error;
|
|
217
|
+
parsedOutput = live.parsedOutput;
|
|
218
|
+
resultArtifact = live.resultArtifact;
|
|
219
|
+
logArtifact = live.logArtifact;
|
|
220
|
+
transcriptArtifact = live.transcriptArtifact;
|
|
466
221
|
} else {
|
|
467
222
|
resultArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
468
223
|
kind: "result",
|
|
@@ -6,7 +6,7 @@ import { writeArtifact } from "../state/artifact-store.ts";
|
|
|
6
6
|
import { appendEvent } from "../state/event-log.ts";
|
|
7
7
|
import type { TeamConfig } from "../teams/team-config.ts";
|
|
8
8
|
import type { ArtifactDescriptor, PolicyDecision, TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
9
|
-
import { saveRunManifestAsync, saveRunTasksAsync, updateRunStatus } from "../state/state-store.ts";
|
|
9
|
+
import { saveRunManifest, saveRunManifestAsync, saveRunTasksAsync, updateRunStatus } from "../state/state-store.ts";
|
|
10
10
|
import { aggregateUsage, formatUsage } from "../state/usage.ts";
|
|
11
11
|
import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
|
|
12
12
|
import { evaluateCrewPolicy, summarizePolicyDecisions } from "./policy-engine.ts";
|
|
@@ -109,10 +109,14 @@ function slug(value: string): string {
|
|
|
109
109
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32) || "task";
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
function extractAdaptivePlanJson(text: string): string | undefined {
|
|
113
113
|
const markerMatch = text.match(/ADAPTIVE_PLAN_JSON_START\s*([\s\S]*?)\s*ADAPTIVE_PLAN_JSON_END/);
|
|
114
114
|
const fencedMatch = markerMatch ? undefined : text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
115
|
-
|
|
115
|
+
return markerMatch?.[1] ?? fencedMatch?.[1];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function __test__parseAdaptivePlan(text: string, allowedRoles: string[]): AdaptivePlan | undefined {
|
|
119
|
+
const raw = extractAdaptivePlanJson(text);
|
|
116
120
|
if (!raw) return undefined;
|
|
117
121
|
let parsed: unknown;
|
|
118
122
|
try { parsed = JSON.parse(raw); } catch { return undefined; }
|
|
@@ -141,6 +145,98 @@ export function __test__parseAdaptivePlan(text: string, allowedRoles: string[]):
|
|
|
141
145
|
return phases.length ? { phases } : undefined;
|
|
142
146
|
}
|
|
143
147
|
|
|
148
|
+
function closeUnbalancedJson(raw: string): string {
|
|
149
|
+
let result = raw.trim();
|
|
150
|
+
const stack: string[] = [];
|
|
151
|
+
let inString = false;
|
|
152
|
+
let escaped = false;
|
|
153
|
+
for (const char of result) {
|
|
154
|
+
if (escaped) {
|
|
155
|
+
escaped = false;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (char === "\\" && inString) {
|
|
159
|
+
escaped = true;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (char === '"') {
|
|
163
|
+
inString = !inString;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (inString) continue;
|
|
167
|
+
if (char === "{") stack.push("}");
|
|
168
|
+
else if (char === "[") stack.push("]");
|
|
169
|
+
else if ((char === "}" || char === "]") && stack.at(-1) === char) stack.pop();
|
|
170
|
+
}
|
|
171
|
+
while (stack.length) result += stack.pop();
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function adaptiveRoleAlias(role: string, allowed: Set<string>): string | undefined {
|
|
176
|
+
if (allowed.has(role)) return role;
|
|
177
|
+
const normalized = slug(role);
|
|
178
|
+
const aliases: Record<string, string[]> = {
|
|
179
|
+
reviewer: ["code-reviewer", "review", "code-review", "critic"],
|
|
180
|
+
"security-reviewer": ["security", "security-review", "sec-review"],
|
|
181
|
+
"test-engineer": ["tester", "qa", "test"],
|
|
182
|
+
executor: ["developer", "implementer", "coder", "engineer"],
|
|
183
|
+
explorer: ["researcher", "scout"],
|
|
184
|
+
analyst: ["analysis", "analyzer"],
|
|
185
|
+
};
|
|
186
|
+
for (const [target, names] of Object.entries(aliases)) if (allowed.has(target) && names.includes(normalized)) return target;
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function __test__repairAdaptivePlan(text: string, allowedRoles: string[]): { plan?: AdaptivePlan; repaired: boolean; reason?: string } {
|
|
191
|
+
const raw = extractAdaptivePlanJson(text);
|
|
192
|
+
if (!raw) return { repaired: false, reason: "missing-json" };
|
|
193
|
+
const candidates = [raw, closeUnbalancedJson(raw)];
|
|
194
|
+
let parsed: unknown;
|
|
195
|
+
for (const candidate of candidates) {
|
|
196
|
+
try {
|
|
197
|
+
parsed = JSON.parse(candidate);
|
|
198
|
+
break;
|
|
199
|
+
} catch {
|
|
200
|
+
// Try the next repair candidate.
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { repaired: false, reason: "invalid-json" };
|
|
204
|
+
const phasesRaw = Array.isArray((parsed as { phases?: unknown }).phases) ? (parsed as { phases: unknown[] }).phases : Array.isArray((parsed as { tasks?: unknown }).tasks) ? [{ name: "adaptive", tasks: (parsed as { tasks: unknown[] }).tasks }] : undefined;
|
|
205
|
+
if (!phasesRaw) return { repaired: false, reason: "missing-phases" };
|
|
206
|
+
const allowed = new Set(allowedRoles);
|
|
207
|
+
const phases: AdaptivePlanPhase[] = [];
|
|
208
|
+
let total = 0;
|
|
209
|
+
let repaired = raw !== closeUnbalancedJson(raw);
|
|
210
|
+
for (const [phaseIndex, phaseRaw] of phasesRaw.entries()) {
|
|
211
|
+
if (!phaseRaw || typeof phaseRaw !== "object" || Array.isArray(phaseRaw)) continue;
|
|
212
|
+
const phaseObj = phaseRaw as { name?: unknown; tasks?: unknown };
|
|
213
|
+
if (!Array.isArray(phaseObj.tasks)) continue;
|
|
214
|
+
const tasks: AdaptivePlanTask[] = [];
|
|
215
|
+
for (const taskRaw of phaseObj.tasks) {
|
|
216
|
+
if (total >= MAX_ADAPTIVE_TASKS) {
|
|
217
|
+
repaired = true;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
if (!taskRaw || typeof taskRaw !== "object" || Array.isArray(taskRaw)) {
|
|
221
|
+
repaired = true;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const taskObj = taskRaw as { role?: unknown; title?: unknown; task?: unknown };
|
|
225
|
+
const role = typeof taskObj.role === "string" ? adaptiveRoleAlias(taskObj.role, allowed) : undefined;
|
|
226
|
+
const taskText = typeof taskObj.task === "string" ? taskObj.task.trim() : "";
|
|
227
|
+
if (!role || !taskText) {
|
|
228
|
+
repaired = true;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
tasks.push({ role, title: typeof taskObj.title === "string" ? taskObj.title : undefined, task: taskText });
|
|
232
|
+
total++;
|
|
233
|
+
}
|
|
234
|
+
if (tasks.length) phases.push({ name: typeof phaseObj.name === "string" && phaseObj.name.trim() ? phaseObj.name.trim() : `phase-${phaseIndex + 1}`, tasks });
|
|
235
|
+
if (total >= MAX_ADAPTIVE_TASKS) break;
|
|
236
|
+
}
|
|
237
|
+
return phases.length ? { plan: { phases }, repaired: true, reason: repaired ? "repaired" : "normalized" } : { repaired: false, reason: "empty-plan" };
|
|
238
|
+
}
|
|
239
|
+
|
|
144
240
|
function reconstructAdaptiveWorkflow(workflow: WorkflowConfig, tasks: TeamTaskState[]): WorkflowConfig {
|
|
145
241
|
const existing = new Set(workflow.steps.map((step) => step.id));
|
|
146
242
|
const steps: WorkflowStep[] = [];
|
|
@@ -167,10 +263,20 @@ function injectAdaptivePlanIfReady(input: { manifest: TeamRunManifest; tasks: Te
|
|
|
167
263
|
appendEvent(input.manifest.eventsPath, { type: "adaptive.plan_missing", runId: input.manifest.runId, taskId: assessTask.id, message: "Adaptive planner result artifact could not be read." });
|
|
168
264
|
return { tasks: input.tasks, workflow: input.workflow, injected: false, missingPlan: true };
|
|
169
265
|
}
|
|
170
|
-
const
|
|
266
|
+
const allowedRoles = input.team.roles.map((role) => role.name);
|
|
267
|
+
let plan = __test__parseAdaptivePlan(text, allowedRoles);
|
|
171
268
|
if (!plan) {
|
|
172
|
-
|
|
173
|
-
|
|
269
|
+
const repair = process.env.PI_CREW_ADAPTIVE_REPAIR === "0" || process.env.PI_TEAMS_ADAPTIVE_REPAIR === "0" ? { repaired: false, reason: "disabled" } : __test__repairAdaptivePlan(text, allowedRoles);
|
|
270
|
+
if (repair.plan) {
|
|
271
|
+
plan = repair.plan;
|
|
272
|
+
const repairArtifact = writeArtifact(input.manifest.artifactsRoot, { kind: "metadata", relativePath: "metadata/adaptive-repair.json", producer: assessTask.id, content: `${JSON.stringify({ reason: repair.reason, phases: repair.plan.phases.map((phase) => ({ name: phase.name, count: phase.tasks.length, roles: phase.tasks.map((task) => task.role) })) }, null, 2)}\n` });
|
|
273
|
+
saveRunManifest({ ...input.manifest, updatedAt: new Date().toISOString(), artifacts: [...input.manifest.artifacts, repairArtifact] });
|
|
274
|
+
appendEvent(input.manifest.eventsPath, { type: "adaptive.plan_repaired", runId: input.manifest.runId, taskId: assessTask.id, message: "Adaptive planner output was repaired before dynamic subagents were spawned.", data: { reason: repair.reason } });
|
|
275
|
+
} else {
|
|
276
|
+
appendEvent(input.manifest.eventsPath, { type: "adaptive.plan_repair_failed", runId: input.manifest.runId, taskId: assessTask.id, message: "Adaptive planner output could not be repaired.", data: { reason: repair.reason } });
|
|
277
|
+
appendEvent(input.manifest.eventsPath, { type: "adaptive.plan_missing", runId: input.manifest.runId, taskId: assessTask.id, message: "Adaptive planner did not produce a valid plan; no dynamic subagents were spawned." });
|
|
278
|
+
return { tasks: input.tasks, workflow: input.workflow, injected: false, missingPlan: true };
|
|
279
|
+
}
|
|
174
280
|
}
|
|
175
281
|
const steps: WorkflowStep[] = [];
|
|
176
282
|
const tasks: TeamTaskState[] = [];
|
|
@@ -327,7 +433,10 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
|
|
|
327
433
|
|
|
328
434
|
const snapshot = taskGraphSnapshot(tasks, queueIndex);
|
|
329
435
|
const readyRoles = snapshot.ready.map((taskId) => tasks.find((task) => task.id === taskId)?.role).filter((role): role is string => Boolean(role));
|
|
330
|
-
const concurrency = resolveBatchConcurrency({ workflowName: workflow.name, workflowMaxConcurrency: workflow.maxConcurrency, teamMaxConcurrency: input.team.maxConcurrency, limitMaxConcurrentWorkers: input.limits?.maxConcurrentWorkers, readyCount: snapshot.ready.length, workspaceMode: manifest.workspaceMode, readyRoles });
|
|
436
|
+
const concurrency = resolveBatchConcurrency({ workflowName: workflow.name, workflowMaxConcurrency: workflow.maxConcurrency, teamMaxConcurrency: input.team.maxConcurrency, limitMaxConcurrentWorkers: input.limits?.maxConcurrentWorkers, allowUnboundedConcurrency: input.limits?.allowUnboundedConcurrency, readyCount: snapshot.ready.length, workspaceMode: manifest.workspaceMode, readyRoles });
|
|
437
|
+
if (concurrency.reason.includes(";unbounded:")) {
|
|
438
|
+
appendEvent(manifest.eventsPath, { type: "limits.unbounded", runId: manifest.runId, message: "Unbounded worker concurrency was explicitly enabled for this run.", data: { concurrencyReason: concurrency.reason, maxConcurrent: concurrency.maxConcurrent } });
|
|
439
|
+
}
|
|
331
440
|
const readyBatch = getReadyTasks(tasks, concurrency.selectedCount, queueIndex);
|
|
332
441
|
if (readyBatch.length === 0) {
|
|
333
442
|
tasks = markBlocked(tasks, "No ready queued task; dependency graph may be invalid.");
|
|
@@ -18,6 +18,7 @@ export const PiTeamsAutonomousConfigSchema = Type.Object({
|
|
|
18
18
|
|
|
19
19
|
export const PiTeamsLimitsConfigSchema = Type.Object({
|
|
20
20
|
maxConcurrentWorkers: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
21
|
+
allowUnboundedConcurrency: Type.Optional(Type.Boolean()),
|
|
21
22
|
maxTaskDepth: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
22
23
|
maxChildrenPerTask: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
23
24
|
maxRunMinutes: Type.Optional(Type.Integer({ minimum: 1 })),
|
package/src/state/mailbox.ts
CHANGED
|
@@ -34,6 +34,11 @@ export interface MailboxValidationReport {
|
|
|
34
34
|
repaired: string[];
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export interface MailboxReplayResult {
|
|
38
|
+
messages: MailboxMessage[];
|
|
39
|
+
updatedAt: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
37
42
|
function mailboxDir(manifest: TeamRunManifest): string {
|
|
38
43
|
return path.join(manifest.stateRoot, "mailbox");
|
|
39
44
|
}
|
|
@@ -110,6 +115,18 @@ export function readMailbox(manifest: TeamRunManifest, direction?: MailboxDirect
|
|
|
110
115
|
return directions.flatMap((item) => safeReadMailboxFile(mailboxPath(manifest, item, taskId), item)).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
111
116
|
}
|
|
112
117
|
|
|
118
|
+
function readAllInboxMessages(manifest: TeamRunManifest): MailboxMessage[] {
|
|
119
|
+
const messages = [...safeReadMailboxFile(mailboxPath(manifest, "inbox"), "inbox")];
|
|
120
|
+
const tasksDir = path.join(mailboxDir(manifest), "tasks");
|
|
121
|
+
if (fs.existsSync(tasksDir)) {
|
|
122
|
+
for (const entry of fs.readdirSync(tasksDir, { withFileTypes: true })) {
|
|
123
|
+
if (!entry.isDirectory()) continue;
|
|
124
|
+
messages.push(...safeReadMailboxFile(mailboxPath(manifest, "inbox", entry.name), "inbox"));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return messages.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
128
|
+
}
|
|
129
|
+
|
|
113
130
|
export function readDeliveryState(manifest: TeamRunManifest): MailboxDeliveryState {
|
|
114
131
|
ensureRunMailbox(manifest);
|
|
115
132
|
try {
|
|
@@ -163,6 +180,17 @@ export function acknowledgeMailboxMessage(manifest: TeamRunManifest, messageId:
|
|
|
163
180
|
return delivery;
|
|
164
181
|
}
|
|
165
182
|
|
|
183
|
+
export function replayPendingMailboxMessages(manifest: TeamRunManifest): MailboxReplayResult {
|
|
184
|
+
const delivery = readDeliveryState(manifest);
|
|
185
|
+
const pending = readAllInboxMessages(manifest).filter((message) => message.status !== "acknowledged" && delivery.messages[message.id] !== "acknowledged");
|
|
186
|
+
if (!pending.length) return { messages: [], updatedAt: delivery.updatedAt };
|
|
187
|
+
const updatedAt = new Date().toISOString();
|
|
188
|
+
for (const message of pending) delivery.messages[message.id] = "delivered";
|
|
189
|
+
delivery.updatedAt = updatedAt;
|
|
190
|
+
writeDeliveryState(manifest, delivery);
|
|
191
|
+
return { messages: pending, updatedAt };
|
|
192
|
+
}
|
|
193
|
+
|
|
166
194
|
export function validateMailbox(manifest: TeamRunManifest, options: { repair?: boolean } = {}): MailboxValidationReport {
|
|
167
195
|
ensureRunMailbox(manifest);
|
|
168
196
|
const issues: MailboxValidationIssue[] = [];
|
package/src/state/types.ts
CHANGED
|
@@ -118,12 +118,26 @@ export interface ModelAttemptState {
|
|
|
118
118
|
error?: string;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
export interface ModelRoutingState {
|
|
122
|
+
requested?: string;
|
|
123
|
+
resolved: string;
|
|
124
|
+
fallbackChain: string[];
|
|
125
|
+
reason?: string;
|
|
126
|
+
usedAttempt: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
121
129
|
export interface TaskWorktreeState {
|
|
122
130
|
path: string;
|
|
123
131
|
branch: string;
|
|
124
132
|
reused: boolean;
|
|
125
133
|
}
|
|
126
134
|
|
|
135
|
+
export interface TaskCheckpointState {
|
|
136
|
+
phase: "started" | "child-spawned" | "child-stdout-final" | "artifact-written";
|
|
137
|
+
updatedAt: string;
|
|
138
|
+
childPid?: number;
|
|
139
|
+
}
|
|
140
|
+
|
|
127
141
|
export interface TeamTaskState {
|
|
128
142
|
id: string;
|
|
129
143
|
runId: string;
|
|
@@ -144,12 +158,14 @@ export interface TeamTaskState {
|
|
|
144
158
|
exitCode?: number | null;
|
|
145
159
|
model?: string;
|
|
146
160
|
modelAttempts?: ModelAttemptState[];
|
|
161
|
+
modelRouting?: ModelRoutingState;
|
|
147
162
|
usage?: UsageState;
|
|
148
163
|
jsonEvents?: number;
|
|
149
164
|
agentProgress?: CrewAgentProgress;
|
|
150
165
|
error?: string;
|
|
151
166
|
claim?: TaskClaimState;
|
|
152
167
|
heartbeat?: WorkerHeartbeatState;
|
|
168
|
+
checkpoint?: TaskCheckpointState;
|
|
153
169
|
taskPacket?: TaskPacket;
|
|
154
170
|
verification?: VerificationEvidence;
|
|
155
171
|
graph?: TaskGraphNode;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../runtime/async-runner.ts";
|