pi-crew 0.1.4 → 0.1.6
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/package.json +4 -2
- package/schema.json +24 -0
- package/src/config/config.ts +64 -0
- package/src/extension/team-tool.ts +79 -8
- package/src/runtime/agent-control.ts +64 -0
- package/src/runtime/async-runner.ts +30 -1
- package/src/runtime/background-runner.ts +4 -2
- package/src/runtime/child-pi.ts +78 -4
- package/src/runtime/crew-agent-records.ts +99 -0
- package/src/runtime/crew-agent-runtime.ts +53 -0
- package/src/runtime/group-join.ts +88 -0
- package/src/runtime/live-session-runtime.ts +33 -0
- package/src/runtime/policy-engine.ts +23 -0
- package/src/runtime/recovery-recipes.ts +74 -0
- package/src/runtime/role-permission.ts +28 -0
- package/src/runtime/runtime-resolver.ts +75 -0
- package/src/runtime/session-usage.ts +79 -0
- package/src/runtime/task-graph-scheduler.ts +107 -0
- package/src/runtime/task-output-context.ts +106 -0
- package/src/runtime/task-runner.ts +214 -4
- package/src/runtime/team-runner.ts +86 -14
- package/src/runtime/worker-startup.ts +57 -0
- package/src/state/contracts.ts +7 -0
- package/src/state/event-log.ts +84 -2
- package/src/state/state-store.ts +23 -2
- package/src/state/types.ts +3 -0
- package/src/worktree/branch-freshness.ts +45 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { TaskGraphNode, TeamTaskState } from "../state/types.ts";
|
|
2
|
+
|
|
3
|
+
export interface TaskGraphSchedulerSnapshot {
|
|
4
|
+
ready: string[];
|
|
5
|
+
blocked: string[];
|
|
6
|
+
running: string[];
|
|
7
|
+
done: string[];
|
|
8
|
+
failed: string[];
|
|
9
|
+
cancelled: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function completedStepIds(tasks: TeamTaskState[]): Set<string> {
|
|
13
|
+
return new Set(tasks.filter((task) => task.status === "completed").map((task) => task.stepId).filter((id): id is string => id !== undefined));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function taskById(tasks: TeamTaskState[]): Map<string, TeamTaskState> {
|
|
17
|
+
return new Map(tasks.map((task) => [task.id, task]));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function stepIdToTaskId(tasks: TeamTaskState[]): Map<string, string> {
|
|
21
|
+
return new Map(tasks.map((task) => [task.stepId, task.id]).filter((entry): entry is [string, string] => entry[0] !== undefined));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function dependencySatisfied(task: TeamTaskState, doneStepIds: Set<string>, idMap: Map<string, TeamTaskState>, stepMap: Map<string, string>): boolean {
|
|
25
|
+
return task.dependsOn.every((dependency) => {
|
|
26
|
+
if (doneStepIds.has(dependency)) return true;
|
|
27
|
+
const taskId = stepMap.get(dependency) ?? dependency;
|
|
28
|
+
return idMap.get(taskId)?.status === "completed";
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function withQueue(task: TeamTaskState, queue: TaskGraphNode["queue"]): TeamTaskState {
|
|
33
|
+
return task.graph ? { ...task, graph: { ...task.graph, queue } } : task;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function refreshTaskGraphQueues(tasks: TeamTaskState[]): TeamTaskState[] {
|
|
37
|
+
const doneSteps = completedStepIds(tasks);
|
|
38
|
+
const ids = taskById(tasks);
|
|
39
|
+
const steps = stepIdToTaskId(tasks);
|
|
40
|
+
return tasks.map((task) => {
|
|
41
|
+
if (task.status === "queued") return withQueue(task, dependencySatisfied(task, doneSteps, ids, steps) ? "ready" : "blocked");
|
|
42
|
+
if (task.status === "running") return withQueue(task, "running");
|
|
43
|
+
if (task.status === "completed" || task.status === "skipped") return withQueue(task, "done");
|
|
44
|
+
return withQueue(task, "blocked");
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getReadyTasks(tasks: TeamTaskState[], maxCount = 1): TeamTaskState[] {
|
|
49
|
+
return refreshTaskGraphQueues(tasks).filter((task) => task.status === "queued" && task.graph?.queue === "ready").slice(0, Math.max(0, maxCount));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function markTaskRunning(tasks: TeamTaskState[], taskId: string, now = new Date()): TeamTaskState[] {
|
|
53
|
+
return refreshTaskGraphQueues(tasks).map((task) => task.id === taskId ? withQueue({ ...task, status: "running", startedAt: task.startedAt ?? now.toISOString() }, "running") : task);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function markTaskDone(tasks: TeamTaskState[], taskId: string, now = new Date()): TeamTaskState[] {
|
|
57
|
+
return refreshTaskGraphQueues(tasks.map((task) => task.id === taskId ? { ...task, status: "completed", finishedAt: task.finishedAt ?? now.toISOString() } : task));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function cancelTaskSubtree(tasks: TeamTaskState[], rootTaskId: string, reason = "Cancelled by task graph scheduler.", now = new Date()): TeamTaskState[] {
|
|
61
|
+
const ids = taskById(tasks);
|
|
62
|
+
const toCancel = new Set<string>();
|
|
63
|
+
const stack = [rootTaskId];
|
|
64
|
+
while (stack.length) {
|
|
65
|
+
const current = stack.pop();
|
|
66
|
+
if (!current || toCancel.has(current)) continue;
|
|
67
|
+
toCancel.add(current);
|
|
68
|
+
const task = ids.get(current);
|
|
69
|
+
for (const child of task?.graph?.children ?? []) stack.push(child);
|
|
70
|
+
}
|
|
71
|
+
return refreshTaskGraphQueues(tasks.map((task) => {
|
|
72
|
+
if (!toCancel.has(task.id)) return task;
|
|
73
|
+
if (task.status === "completed") return task;
|
|
74
|
+
return { ...task, status: "cancelled", error: reason, finishedAt: task.finishedAt ?? now.toISOString() };
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function failTaskAndBlockChildren(tasks: TeamTaskState[], rootTaskId: string, reason: string, now = new Date()): TeamTaskState[] {
|
|
79
|
+
const ids = taskById(tasks);
|
|
80
|
+
const blocked = new Set<string>();
|
|
81
|
+
const root = ids.get(rootTaskId);
|
|
82
|
+
const stack = [...(root?.graph?.children ?? [])];
|
|
83
|
+
while (stack.length) {
|
|
84
|
+
const current = stack.pop();
|
|
85
|
+
if (!current || blocked.has(current)) continue;
|
|
86
|
+
blocked.add(current);
|
|
87
|
+
const task = ids.get(current);
|
|
88
|
+
for (const child of task?.graph?.children ?? []) stack.push(child);
|
|
89
|
+
}
|
|
90
|
+
return refreshTaskGraphQueues(tasks.map((task) => {
|
|
91
|
+
if (task.id === rootTaskId) return { ...task, status: "failed", error: reason, finishedAt: task.finishedAt ?? now.toISOString() };
|
|
92
|
+
if (blocked.has(task.id) && task.status === "queued") return { ...task, status: "skipped", error: `Blocked by failed task '${rootTaskId}'.`, finishedAt: task.finishedAt ?? now.toISOString() };
|
|
93
|
+
return task;
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function taskGraphSnapshot(tasks: TeamTaskState[]): TaskGraphSchedulerSnapshot {
|
|
98
|
+
const refreshed = refreshTaskGraphQueues(tasks);
|
|
99
|
+
return {
|
|
100
|
+
ready: refreshed.filter((task) => task.status === "queued" && task.graph?.queue === "ready").map((task) => task.id),
|
|
101
|
+
blocked: refreshed.filter((task) => task.status === "queued" && task.graph?.queue === "blocked").map((task) => task.id),
|
|
102
|
+
running: refreshed.filter((task) => task.status === "running").map((task) => task.id),
|
|
103
|
+
done: refreshed.filter((task) => task.status === "completed" || task.status === "skipped").map((task) => task.id),
|
|
104
|
+
failed: refreshed.filter((task) => task.status === "failed").map((task) => task.id),
|
|
105
|
+
cancelled: refreshed.filter((task) => task.status === "cancelled").map((task) => task.id),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
4
|
+
import { writeArtifact } from "../state/artifact-store.ts";
|
|
5
|
+
import type { WorkflowStep } from "../workflows/workflow-config.ts";
|
|
6
|
+
|
|
7
|
+
export interface DependencyOutputContext {
|
|
8
|
+
dependencies: Array<{ taskId: string; title: string; status: string; result?: string; resultPath?: string }>;
|
|
9
|
+
sharedReads: Array<{ name: string; path: string; content: string }>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function readIfSmall(filePath: string, maxBytes = 24_000): string | undefined {
|
|
13
|
+
try {
|
|
14
|
+
const stat = fs.statSync(filePath);
|
|
15
|
+
if (stat.size > maxBytes) return `${fs.readFileSync(filePath, "utf-8").slice(0, maxBytes)}\n\n...(truncated ${stat.size - maxBytes} bytes)`;
|
|
16
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
17
|
+
} catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function sharedPath(manifest: TeamRunManifest, name: string): string {
|
|
23
|
+
return path.join(manifest.artifactsRoot, "shared", name);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function collectDependencyOutputContext(manifest: TeamRunManifest, tasks: TeamTaskState[], task: TeamTaskState, step: WorkflowStep): DependencyOutputContext {
|
|
27
|
+
const byStep = new Map(tasks.map((item) => [item.stepId, item]).filter((entry): entry is [string, TeamTaskState] => Boolean(entry[0])));
|
|
28
|
+
const byId = new Map(tasks.map((item) => [item.id, item]));
|
|
29
|
+
const dependencies = task.dependsOn.map((dep) => byStep.get(dep) ?? byId.get(dep)).filter((item): item is TeamTaskState => Boolean(item)).map((item) => ({
|
|
30
|
+
taskId: item.id,
|
|
31
|
+
title: item.title,
|
|
32
|
+
status: item.status,
|
|
33
|
+
resultPath: item.resultArtifact?.path,
|
|
34
|
+
result: item.resultArtifact ? readIfSmall(item.resultArtifact.path) : undefined,
|
|
35
|
+
}));
|
|
36
|
+
const sharedReads = (step.reads === false ? [] : step.reads ?? []).map((name) => {
|
|
37
|
+
const filePath = sharedPath(manifest, name);
|
|
38
|
+
return { name, path: filePath, content: readIfSmall(filePath) ?? "" };
|
|
39
|
+
}).filter((item) => item.content.trim().length > 0);
|
|
40
|
+
return { dependencies, sharedReads };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function renderDependencyOutputContext(context: DependencyOutputContext): string {
|
|
44
|
+
const parts: string[] = [];
|
|
45
|
+
if (context.dependencies.length) {
|
|
46
|
+
parts.push("# Dependency Outputs", "");
|
|
47
|
+
for (const dep of context.dependencies) {
|
|
48
|
+
parts.push(`## ${dep.taskId} (${dep.title})`, `Status: ${dep.status}`, dep.resultPath ? `Result artifact: ${dep.resultPath}` : "", "", dep.result?.trim() || "(no result output)", "");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (context.sharedReads.length) {
|
|
52
|
+
parts.push("# Shared Run Context Reads", "");
|
|
53
|
+
for (const read of context.sharedReads) parts.push(`## shared/${read.name}`, `Path: ${read.path}`, "", read.content.trim(), "");
|
|
54
|
+
}
|
|
55
|
+
return parts.join("\n").trim();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function writeTaskSharedOutput(manifest: TeamRunManifest, step: WorkflowStep, task: TeamTaskState): ArtifactDescriptor | undefined {
|
|
59
|
+
if (step.output === false) return undefined;
|
|
60
|
+
const name = step.output || `${task.id}.md`;
|
|
61
|
+
const source = task.resultArtifact ? readIfSmall(task.resultArtifact.path, 80_000) : undefined;
|
|
62
|
+
if (!source) return undefined;
|
|
63
|
+
return writeArtifact(manifest.artifactsRoot, {
|
|
64
|
+
kind: "metadata",
|
|
65
|
+
relativePath: `shared/${name}`,
|
|
66
|
+
producer: task.id,
|
|
67
|
+
content: source.endsWith("\n") ? source : `${source}\n`,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function writeTaskInputsArtifact(manifest: TeamRunManifest, task: TeamTaskState, context: DependencyOutputContext): ArtifactDescriptor {
|
|
72
|
+
return writeArtifact(manifest.artifactsRoot, {
|
|
73
|
+
kind: "metadata",
|
|
74
|
+
relativePath: `metadata/${task.id}.inputs.json`,
|
|
75
|
+
producer: task.id,
|
|
76
|
+
content: `${JSON.stringify(context, null, 2)}\n`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function aggregateTaskOutputs(tasks: TeamTaskState[]): string {
|
|
81
|
+
return tasks.map((task, index) => {
|
|
82
|
+
const body = task.resultArtifact ? readIfSmall(task.resultArtifact.path, 40_000) : undefined;
|
|
83
|
+
const hasBody = Boolean(body?.trim());
|
|
84
|
+
const expectedMissing = task.resultArtifact && !fs.existsSync(task.resultArtifact.path);
|
|
85
|
+
const status = task.status === "skipped"
|
|
86
|
+
? "SKIPPED"
|
|
87
|
+
: task.status === "failed"
|
|
88
|
+
? `FAILED${task.exitCode !== undefined ? ` (exit code ${task.exitCode ?? "null"})` : ""}${task.error ? `: ${task.error}` : ""}`
|
|
89
|
+
: expectedMissing
|
|
90
|
+
? `EMPTY OUTPUT (expected result artifact missing: ${task.resultArtifact?.path})`
|
|
91
|
+
: !hasBody
|
|
92
|
+
? "EMPTY OUTPUT (no textual response returned)"
|
|
93
|
+
: task.status.toUpperCase();
|
|
94
|
+
return [
|
|
95
|
+
`=== Task ${index + 1}: ${task.id} (${task.agent}) ===`,
|
|
96
|
+
`Status: ${status}`,
|
|
97
|
+
task.role ? `Role: ${task.role}` : "",
|
|
98
|
+
task.resultArtifact?.path ? `Result artifact: ${task.resultArtifact.path}` : "",
|
|
99
|
+
task.logArtifact?.path ? `Log artifact: ${task.logArtifact.path}` : "",
|
|
100
|
+
task.transcriptArtifact?.path ? `Transcript: ${task.transcriptArtifact.path}` : "",
|
|
101
|
+
task.usage ? `Usage: ${JSON.stringify(task.usage)}` : "",
|
|
102
|
+
"",
|
|
103
|
+
hasBody ? body!.trim() : status,
|
|
104
|
+
].filter(Boolean).join("\n");
|
|
105
|
+
}).join("\n\n");
|
|
106
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
1
2
|
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
2
|
-
import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
3
|
+
import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
|
|
3
4
|
import { writeArtifact } from "../state/artifact-store.ts";
|
|
4
5
|
import { appendEvent } from "../state/event-log.ts";
|
|
5
6
|
import { saveRunManifest, saveRunTasks } from "../state/state-store.ts";
|
|
@@ -12,6 +13,12 @@ import { parsePiJsonOutput, type ParsedPiJsonOutput } from "./pi-json-output.ts"
|
|
|
12
13
|
import { runChildPi } from "./child-pi.ts";
|
|
13
14
|
import { buildTaskPacket, renderTaskPacket } from "./task-packet.ts";
|
|
14
15
|
import { createVerificationEvidence } from "./green-contract.ts";
|
|
16
|
+
import { createStartupEvidence } from "./worker-startup.ts";
|
|
17
|
+
import { permissionForRole } from "./role-permission.ts";
|
|
18
|
+
import { collectDependencyOutputContext, renderDependencyOutputContext, writeTaskInputsArtifact, writeTaskSharedOutput } from "./task-output-context.ts";
|
|
19
|
+
import { appendCrewAgentEvent, appendCrewAgentOutput, emptyCrewAgentProgress, recordFromTask, upsertCrewAgent } from "./crew-agent-records.ts";
|
|
20
|
+
import { parseSessionUsage } from "./session-usage.ts";
|
|
21
|
+
import type { CrewAgentProgress } from "./crew-agent-runtime.ts";
|
|
15
22
|
|
|
16
23
|
export interface TaskRunnerInput {
|
|
17
24
|
manifest: TeamRunManifest;
|
|
@@ -21,6 +28,31 @@ export interface TaskRunnerInput {
|
|
|
21
28
|
agent: AgentConfig;
|
|
22
29
|
signal?: AbortSignal;
|
|
23
30
|
executeWorkers: boolean;
|
|
31
|
+
dependencyContextText?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readOnlyRoleInstructions(role: string): string {
|
|
35
|
+
if (permissionForRole(role) !== "read_only") return "";
|
|
36
|
+
return [
|
|
37
|
+
"# READ-ONLY ROLE CONTRACT",
|
|
38
|
+
"You are running in READ-ONLY mode for this task.",
|
|
39
|
+
"- Do not create, modify, delete, move, or copy files.",
|
|
40
|
+
"- Do not use shell redirects, heredocs, in-place edits, package installs, git commit/merge/rebase/reset/checkout, or other state-mutating commands.",
|
|
41
|
+
"- If implementation changes are needed, report exact recommendations instead of applying them.",
|
|
42
|
+
"- Prefer read/grep/find/listing tools and read-only git inspection commands.",
|
|
43
|
+
].join("\n");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function coordinationBridgeInstructions(task: TeamTaskState): string {
|
|
47
|
+
return [
|
|
48
|
+
"# Crew Coordination Channel",
|
|
49
|
+
`Mailbox target for this task: ${task.id}`,
|
|
50
|
+
"Use the run mailbox contract for coordination with the leader/orchestrator:",
|
|
51
|
+
"- 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.",
|
|
52
|
+
"- If nudged, answer with current status, blocker, or smallest next step.",
|
|
53
|
+
"- Treat inherited/dependency context as reference-only; do not continue the parent conversation directly.",
|
|
54
|
+
"- Completion handoff should include: DONE/FAILED, summary, changed/read files, verification evidence, and remaining risks.",
|
|
55
|
+
].join("\n");
|
|
24
56
|
}
|
|
25
57
|
|
|
26
58
|
function renderTaskPrompt(manifest: TeamRunManifest, step: WorkflowStep, task: TeamTaskState): string {
|
|
@@ -47,21 +79,136 @@ function renderTaskPrompt(manifest: TeamRunManifest, step: WorkflowStep, task: T
|
|
|
47
79
|
"- Do not claim completion without evidence.",
|
|
48
80
|
"- Follow the Task Packet contract below; escalate if any contract field is impossible to satisfy.",
|
|
49
81
|
"",
|
|
82
|
+
readOnlyRoleInstructions(task.role),
|
|
83
|
+
"",
|
|
84
|
+
coordinationBridgeInstructions(task),
|
|
85
|
+
"",
|
|
50
86
|
task.taskPacket ? renderTaskPacket(task.taskPacket) : "",
|
|
87
|
+
"",
|
|
88
|
+
(inputDependencyContext(task) || ""),
|
|
51
89
|
"Task:",
|
|
52
90
|
step.task.replaceAll("{goal}", manifest.goal),
|
|
53
91
|
].join("\n");
|
|
54
92
|
}
|
|
55
93
|
|
|
94
|
+
function inputDependencyContext(task: TeamTaskState): string {
|
|
95
|
+
return (task as TeamTaskState & { dependencyContextText?: string }).dependencyContextText ?? "";
|
|
96
|
+
}
|
|
97
|
+
|
|
56
98
|
function updateTask(tasks: TeamTaskState[], updated: TeamTaskState): TeamTaskState[] {
|
|
57
99
|
return tasks.map((task) => task.id === updated.id ? updated : task);
|
|
58
100
|
}
|
|
59
101
|
|
|
102
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
103
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function textFromContent(content: unknown): string[] {
|
|
107
|
+
if (typeof content === "string") return [content];
|
|
108
|
+
if (!Array.isArray(content)) return [];
|
|
109
|
+
const text: string[] = [];
|
|
110
|
+
for (const part of content) {
|
|
111
|
+
const obj = asRecord(part);
|
|
112
|
+
if (!obj) continue;
|
|
113
|
+
if (obj.type === "text" && typeof obj.text === "string") text.push(obj.text);
|
|
114
|
+
else if (typeof obj.content === "string") text.push(obj.content);
|
|
115
|
+
}
|
|
116
|
+
return text;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function eventText(event: unknown): string[] {
|
|
120
|
+
const obj = asRecord(event);
|
|
121
|
+
if (!obj) return [];
|
|
122
|
+
const text: string[] = [];
|
|
123
|
+
if (typeof obj.text === "string") text.push(obj.text);
|
|
124
|
+
if (typeof obj.output === "string") text.push(obj.output);
|
|
125
|
+
text.push(...textFromContent(obj.content));
|
|
126
|
+
const message = asRecord(obj.message);
|
|
127
|
+
if (message) text.push(...textFromContent(message.content));
|
|
128
|
+
return text.filter((entry) => entry.trim());
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function numberField(obj: Record<string, unknown>, keys: string[]): number | undefined {
|
|
132
|
+
for (const key of keys) {
|
|
133
|
+
const value = obj[key];
|
|
134
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function eventUsage(event: unknown): { input?: number; output?: number; turns?: number } | undefined {
|
|
140
|
+
const obj = asRecord(event);
|
|
141
|
+
if (!obj) return undefined;
|
|
142
|
+
const direct = {
|
|
143
|
+
input: numberField(obj, ["input", "inputTokens", "input_tokens"]),
|
|
144
|
+
output: numberField(obj, ["output", "outputTokens", "output_tokens"]),
|
|
145
|
+
turns: numberField(obj, ["turns", "turnCount", "turn_count"]),
|
|
146
|
+
};
|
|
147
|
+
if (Object.values(direct).some((value) => value !== undefined)) return direct;
|
|
148
|
+
for (const key of ["usage", "tokenUsage", "tokens", "stats"]) {
|
|
149
|
+
const nested = eventUsage(obj[key]);
|
|
150
|
+
if (nested) return nested;
|
|
151
|
+
}
|
|
152
|
+
const message = asRecord(obj.message);
|
|
153
|
+
return message ? eventUsage(message.usage) : undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function previewArgs(args: unknown): string | undefined {
|
|
157
|
+
if (!args) return undefined;
|
|
158
|
+
try {
|
|
159
|
+
const text = typeof args === "string" ? args : JSON.stringify(args);
|
|
160
|
+
return text.length > 240 ? `${text.slice(0, 240)}…` : text;
|
|
161
|
+
} catch {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function applyUsageToProgress(progress: CrewAgentProgress | undefined, usage: UsageState | undefined): CrewAgentProgress | undefined {
|
|
167
|
+
if (!usage) return progress;
|
|
168
|
+
const base = progress ?? emptyCrewAgentProgress();
|
|
169
|
+
return {
|
|
170
|
+
...base,
|
|
171
|
+
tokens: (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0),
|
|
172
|
+
turns: usage.turns ?? base.turns,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function applyAgentProgressEvent(progress: CrewAgentProgress, event: unknown, startedAt: string | undefined): CrewAgentProgress {
|
|
177
|
+
const obj = asRecord(event);
|
|
178
|
+
const now = new Date().toISOString();
|
|
179
|
+
const next: CrewAgentProgress = { ...progress, recentTools: [...progress.recentTools], recentOutput: [...progress.recentOutput], lastActivityAt: now, activityState: "active" };
|
|
180
|
+
if (startedAt) next.durationMs = Date.now() - new Date(startedAt).getTime();
|
|
181
|
+
if (obj?.type === "tool_execution_start") {
|
|
182
|
+
next.toolCount += 1;
|
|
183
|
+
next.currentTool = typeof obj.toolName === "string" ? obj.toolName : typeof obj.name === "string" ? obj.name : "tool";
|
|
184
|
+
next.currentToolArgs = previewArgs(obj.args);
|
|
185
|
+
next.currentToolStartedAt = now;
|
|
186
|
+
}
|
|
187
|
+
if (obj?.type === "tool_execution_end") {
|
|
188
|
+
if (next.currentTool) next.recentTools.push({ tool: next.currentTool, args: next.currentToolArgs, endedAt: now });
|
|
189
|
+
next.currentTool = undefined;
|
|
190
|
+
next.currentToolArgs = undefined;
|
|
191
|
+
next.currentToolStartedAt = undefined;
|
|
192
|
+
}
|
|
193
|
+
const usage = eventUsage(event);
|
|
194
|
+
if (usage) {
|
|
195
|
+
next.tokens = (usage.input ?? 0) + (usage.output ?? 0);
|
|
196
|
+
next.turns = usage.turns ?? next.turns;
|
|
197
|
+
}
|
|
198
|
+
const text = eventText(event);
|
|
199
|
+
if (text.length > 0) next.recentOutput.push(...text.flatMap((entry) => entry.split(/\r?\n/)).filter(Boolean).slice(-10));
|
|
200
|
+
if (next.recentTools.length > 25) next.recentTools.splice(0, next.recentTools.length - 25);
|
|
201
|
+
if (next.recentOutput.length > 50) next.recentOutput.splice(0, next.recentOutput.length - 50);
|
|
202
|
+
return next;
|
|
203
|
+
}
|
|
204
|
+
|
|
60
205
|
export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
|
|
61
206
|
let manifest = input.manifest;
|
|
62
207
|
const workspace = prepareTaskWorkspace(manifest, input.task);
|
|
63
208
|
const worktree = workspace.worktreePath && workspace.branch ? { path: workspace.worktreePath, branch: workspace.branch, reused: workspace.reused ?? false } : input.task.worktree;
|
|
64
209
|
const taskPacket = buildTaskPacket({ manifest, step: input.step, taskId: input.task.id, cwd: workspace.cwd, worktreePath: worktree?.path });
|
|
210
|
+
const dependencyContext = collectDependencyOutputContext(manifest, input.tasks, input.task, input.step);
|
|
211
|
+
const dependencyContextText = input.dependencyContextText ?? renderDependencyOutputContext(dependencyContext);
|
|
65
212
|
let task: TeamTaskState = {
|
|
66
213
|
...input.task,
|
|
67
214
|
cwd: workspace.cwd,
|
|
@@ -71,10 +218,14 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
71
218
|
startedAt: new Date().toISOString(),
|
|
72
219
|
claim: createTaskClaim(`task-runner:${input.task.id}`),
|
|
73
220
|
heartbeat: createWorkerHeartbeat(input.task.id),
|
|
74
|
-
|
|
221
|
+
agentProgress: input.task.agentProgress ?? emptyCrewAgentProgress(),
|
|
222
|
+
...(dependencyContextText ? { dependencyContextText } : {}),
|
|
223
|
+
} as TeamTaskState;
|
|
75
224
|
let tasks = updateTask(input.tasks, task);
|
|
76
225
|
saveRunTasks(manifest, tasks);
|
|
226
|
+
upsertCrewAgent(manifest, recordFromTask(manifest, task, input.executeWorkers ? "child-process" : "scaffold"));
|
|
77
227
|
appendEvent(manifest.eventsPath, { type: "task.started", runId: manifest.runId, taskId: task.id, data: { role: task.role, agent: task.agent, cwd: task.cwd, worktreePath: workspace.worktreePath, worktreeBranch: workspace.branch, worktreeReused: workspace.reused } });
|
|
228
|
+
const permissionMode = permissionForRole(task.role);
|
|
78
229
|
|
|
79
230
|
const prompt = renderTaskPrompt(manifest, input.step, task);
|
|
80
231
|
const promptArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
@@ -86,11 +237,20 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
86
237
|
|
|
87
238
|
let resultArtifact: ArtifactDescriptor;
|
|
88
239
|
let logArtifact: ArtifactDescriptor | undefined;
|
|
240
|
+
let transcriptArtifact: ArtifactDescriptor | undefined;
|
|
89
241
|
let exitCode: number | null = 0;
|
|
90
242
|
let error: string | undefined;
|
|
91
243
|
let modelAttempts: ModelAttemptSummary[] | undefined;
|
|
92
244
|
let parsedOutput: ParsedPiJsonOutput | undefined;
|
|
93
245
|
|
|
246
|
+
let startupEvidence = createStartupEvidence({ command: input.executeWorkers ? "pi" : "safe-scaffold", startedAt: new Date(task.startedAt ?? new Date().toISOString()), finishedAt: new Date(), promptSentAt: new Date(task.startedAt ?? new Date().toISOString()), promptAccepted: true, exitCode: 0 });
|
|
247
|
+
const inputsArtifact = writeTaskInputsArtifact(manifest, task, dependencyContext);
|
|
248
|
+
const coordinationArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
249
|
+
kind: "metadata",
|
|
250
|
+
relativePath: `metadata/${task.id}.coordination-bridge.md`,
|
|
251
|
+
content: `${coordinationBridgeInstructions(task)}\n`,
|
|
252
|
+
producer: task.id,
|
|
253
|
+
});
|
|
94
254
|
if (input.executeWorkers) {
|
|
95
255
|
const candidates = buildModelCandidates(input.step.model ?? input.agent.model, input.agent.fallbackModels, undefined);
|
|
96
256
|
const attemptModels = candidates.length > 0 ? candidates : [input.step.model ?? input.agent.model];
|
|
@@ -98,9 +258,27 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
98
258
|
let finalStdout = "";
|
|
99
259
|
let finalStderr = "";
|
|
100
260
|
modelAttempts = [];
|
|
261
|
+
const transcriptPath = `${manifest.artifactsRoot}/transcripts/${task.id}.jsonl`;
|
|
101
262
|
for (let i = 0; i < attemptModels.length; i++) {
|
|
102
263
|
const model = attemptModels[i];
|
|
103
|
-
const
|
|
264
|
+
const attemptStartedAt = new Date();
|
|
265
|
+
const childResult = await runChildPi({
|
|
266
|
+
cwd: task.cwd,
|
|
267
|
+
task: prompt,
|
|
268
|
+
agent: input.agent,
|
|
269
|
+
model,
|
|
270
|
+
signal: input.signal,
|
|
271
|
+
transcriptPath,
|
|
272
|
+
onStdoutLine: (line) => appendCrewAgentOutput(manifest, task.id, line),
|
|
273
|
+
onJsonEvent: (event) => {
|
|
274
|
+
appendCrewAgentEvent(manifest, task.id, event);
|
|
275
|
+
task = { ...task, agentProgress: applyAgentProgressEvent(task.agentProgress ?? emptyCrewAgentProgress(), event, task.startedAt) };
|
|
276
|
+
tasks = updateTask(tasks, task);
|
|
277
|
+
upsertCrewAgent(manifest, recordFromTask(manifest, task, "child-process"));
|
|
278
|
+
appendEvent(manifest.eventsPath, { type: "task.progress", runId: manifest.runId, taskId: task.id, data: { event } });
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
startupEvidence = createStartupEvidence({ command: "pi", startedAt: attemptStartedAt, finishedAt: new Date(), promptSentAt: attemptStartedAt, promptAccepted: childResult.exitCode === 0 && !childResult.error, stderr: childResult.stderr, error: childResult.error, exitCode: childResult.exitCode });
|
|
104
282
|
exitCode = childResult.exitCode;
|
|
105
283
|
finalStdout = childResult.stdout;
|
|
106
284
|
finalStderr = childResult.stderr;
|
|
@@ -126,6 +304,22 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
126
304
|
content: [...logs, `finalExitCode=${exitCode ?? "null"}`, `jsonEvents=${parsedOutput?.jsonEvents ?? 0}`, parsedOutput?.usage ? `usage=${JSON.stringify(parsedOutput.usage)}` : "", "", "STDOUT:", finalStdout, "", "STDERR:", finalStderr].join("\n"),
|
|
127
305
|
producer: task.id,
|
|
128
306
|
});
|
|
307
|
+
const sessionUsage = parseSessionUsage(transcriptPath);
|
|
308
|
+
const effectiveUsage = parsedOutput?.usage ?? sessionUsage;
|
|
309
|
+
if (effectiveUsage) {
|
|
310
|
+
parsedOutput = { ...(parsedOutput ?? { jsonEvents: 0, textEvents: [] }), usage: effectiveUsage };
|
|
311
|
+
task = { ...task, usage: effectiveUsage, agentProgress: applyUsageToProgress(task.agentProgress, effectiveUsage) };
|
|
312
|
+
tasks = updateTask(tasks, task);
|
|
313
|
+
upsertCrewAgent(manifest, recordFromTask(manifest, task, "child-process"));
|
|
314
|
+
}
|
|
315
|
+
if (fs.existsSync(transcriptPath)) {
|
|
316
|
+
transcriptArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
317
|
+
kind: "log",
|
|
318
|
+
relativePath: `transcripts/${task.id}.jsonl`,
|
|
319
|
+
content: fs.readFileSync(transcriptPath, "utf-8"),
|
|
320
|
+
producer: task.id,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
129
323
|
} else {
|
|
130
324
|
resultArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
131
325
|
kind: "result",
|
|
@@ -155,6 +349,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
155
349
|
modelAttempts,
|
|
156
350
|
usage: parsedOutput?.usage,
|
|
157
351
|
jsonEvents: parsedOutput?.jsonEvents,
|
|
352
|
+
agentProgress: task.agentProgress,
|
|
158
353
|
error,
|
|
159
354
|
verification: createVerificationEvidence(taskPacket.verification, !error, error ? `Task failed: ${error}` : input.executeWorkers ? "Worker finished without reporting a verification failure." : "Safe scaffold mode; verification commands were not executed."),
|
|
160
355
|
promptArtifact,
|
|
@@ -162,6 +357,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
162
357
|
claim: undefined,
|
|
163
358
|
heartbeat: touchWorkerHeartbeat(task.heartbeat ?? createWorkerHeartbeat(task.id), { alive: false }),
|
|
164
359
|
...(logArtifact ? { logArtifact } : {}),
|
|
360
|
+
...(transcriptArtifact ? { transcriptArtifact } : {}),
|
|
165
361
|
};
|
|
166
362
|
tasks = updateTask(tasks, task);
|
|
167
363
|
const packetArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
@@ -176,9 +372,23 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
176
372
|
content: `${JSON.stringify(task.verification, null, 2)}\n`,
|
|
177
373
|
producer: task.id,
|
|
178
374
|
});
|
|
179
|
-
|
|
375
|
+
const sharedOutputArtifact = writeTaskSharedOutput(manifest, input.step, task);
|
|
376
|
+
const startupArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
377
|
+
kind: "metadata",
|
|
378
|
+
relativePath: `metadata/${task.id}.startup-evidence.json`,
|
|
379
|
+
content: `${JSON.stringify(startupEvidence, null, 2)}\n`,
|
|
380
|
+
producer: task.id,
|
|
381
|
+
});
|
|
382
|
+
const permissionArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
383
|
+
kind: "metadata",
|
|
384
|
+
relativePath: `metadata/${task.id}.permission.json`,
|
|
385
|
+
content: `${JSON.stringify({ role: task.role, permissionMode }, null, 2)}\n`,
|
|
386
|
+
producer: task.id,
|
|
387
|
+
});
|
|
388
|
+
manifest = { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts, promptArtifact, resultArtifact, inputsArtifact, coordinationArtifact, packetArtifact, verificationArtifact, startupArtifact, permissionArtifact, ...(sharedOutputArtifact ? [sharedOutputArtifact] : []), ...(logArtifact ? [logArtifact] : []), ...(transcriptArtifact ? [transcriptArtifact] : []), ...(diffArtifact ? [diffArtifact] : [])] };
|
|
180
389
|
saveRunManifest(manifest);
|
|
181
390
|
saveRunTasks(manifest, tasks);
|
|
391
|
+
upsertCrewAgent(manifest, recordFromTask(manifest, task, input.executeWorkers ? "child-process" : "scaffold"));
|
|
182
392
|
appendEvent(manifest.eventsPath, { type: error ? "task.failed" : "task.completed", runId: manifest.runId, taskId: task.id, message: error });
|
|
183
393
|
return { manifest, tasks };
|
|
184
394
|
}
|