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
|
@@ -5,6 +5,8 @@ export interface ResolveBatchConcurrencyInput {
|
|
|
5
5
|
workflowMaxConcurrency?: number;
|
|
6
6
|
teamMaxConcurrency?: number;
|
|
7
7
|
limitMaxConcurrentWorkers?: number;
|
|
8
|
+
allowUnboundedConcurrency?: boolean;
|
|
9
|
+
hardCap?: number;
|
|
8
10
|
readyCount: number;
|
|
9
11
|
workspaceMode?: "single" | "worktree";
|
|
10
12
|
readyRoles?: string[];
|
|
@@ -40,11 +42,15 @@ export function resolveBatchConcurrency(input: ResolveBatchConcurrencyInput): Ba
|
|
|
40
42
|
if (limitMax !== undefined) source = "limit";
|
|
41
43
|
else if (teamMax !== undefined) source = "team";
|
|
42
44
|
else source = "workflow";
|
|
45
|
+
const hardCap = positiveInteger(input.hardCap) ?? DEFAULT_CONCURRENCY.hardCap;
|
|
46
|
+
const maxConcurrent = input.allowUnboundedConcurrency ? requested : Math.min(requested, hardCap);
|
|
43
47
|
const readyCount = Math.max(0, Math.trunc(input.readyCount));
|
|
48
|
+
const cappedReason = maxConcurrent < requested ? `;capped:${hardCap}` : "";
|
|
49
|
+
const unboundedReason = input.allowUnboundedConcurrency && requested > hardCap ? `;unbounded:${hardCap}` : "";
|
|
44
50
|
return {
|
|
45
|
-
maxConcurrent
|
|
46
|
-
selectedCount: readyCount === 0 ? 0 : Math.min(readyCount,
|
|
51
|
+
maxConcurrent,
|
|
52
|
+
selectedCount: readyCount === 0 ? 0 : Math.min(readyCount, maxConcurrent),
|
|
47
53
|
defaultConcurrency,
|
|
48
|
-
reason: `${source}:${requested};ready:${readyCount}`,
|
|
54
|
+
reason: `${source}:${requested}${cappedReason}${unboundedReason};ready:${readyCount}`,
|
|
49
55
|
};
|
|
50
56
|
}
|
|
@@ -159,6 +159,7 @@ export function recordFromTask(manifest: TeamRunManifest, task: TeamTaskState, r
|
|
|
159
159
|
toolUses: task.agentProgress?.toolCount,
|
|
160
160
|
jsonEvents: task.jsonEvents,
|
|
161
161
|
model: modelFromTask(task),
|
|
162
|
+
routing: task.modelRouting,
|
|
162
163
|
usage: task.usage,
|
|
163
164
|
progress: task.agentProgress,
|
|
164
165
|
error: task.error,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TeamTaskStatus } from "../state/contracts.ts";
|
|
2
|
-
import type { UsageState } from "../state/types.ts";
|
|
2
|
+
import type { ModelRoutingState, UsageState } from "../state/types.ts";
|
|
3
3
|
|
|
4
4
|
export type CrewRuntimeKind = "scaffold" | "child-process" | "live-session";
|
|
5
5
|
export type CrewAgentStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "stopped";
|
|
@@ -43,6 +43,7 @@ export interface CrewAgentRecord {
|
|
|
43
43
|
toolUses?: number;
|
|
44
44
|
jsonEvents?: number;
|
|
45
45
|
model?: string;
|
|
46
|
+
routing?: ModelRoutingState;
|
|
46
47
|
usage?: UsageState;
|
|
47
48
|
progress?: CrewAgentProgress;
|
|
48
49
|
error?: string;
|
|
@@ -219,7 +219,13 @@ function isAvailableModel(model: string, availableModels: AvailableModelInfo[] |
|
|
|
219
219
|
return availableModels.some((entry) => entry.id === baseModel);
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
export
|
|
222
|
+
export interface ConfiguredModelRouting {
|
|
223
|
+
requested?: string;
|
|
224
|
+
candidates: string[];
|
|
225
|
+
reason?: string;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function buildConfiguredModelRouting(input: {
|
|
223
229
|
overrideModel?: string;
|
|
224
230
|
stepModel?: string;
|
|
225
231
|
agentModel?: string;
|
|
@@ -227,18 +233,29 @@ export function buildConfiguredModelCandidates(input: {
|
|
|
227
233
|
parentModel?: unknown;
|
|
228
234
|
modelRegistry?: unknown;
|
|
229
235
|
cwd?: string;
|
|
230
|
-
}):
|
|
236
|
+
}): ConfiguredModelRouting {
|
|
231
237
|
const registryModels = availableModelInfosFromRegistry(input.modelRegistry);
|
|
232
238
|
const configModels = configuredModelInfosFromPiConfig(input.cwd);
|
|
233
239
|
const availableModels = registryModels && registryModels.length > 0 ? registryModels : configModels.length > 0 ? configModels : registryModels;
|
|
234
240
|
const parentModel = modelStringFromUnknown(input.parentModel);
|
|
235
241
|
const preferredProvider = parentModel?.split("/")[0] ?? availableModels?.[0]?.provider;
|
|
236
|
-
|
|
242
|
+
const requested = [input.overrideModel, input.stepModel, input.agentModel, parentModel].find((model): model is string => Boolean(model?.trim()));
|
|
243
|
+
if (availableModels && availableModels.length === 0) return { requested, candidates: [], reason: "no configured Pi models available" };
|
|
237
244
|
const rawModels = availableModels
|
|
238
245
|
? [input.overrideModel, input.stepModel, input.agentModel, ...(input.fallbackModels ?? []), parentModel, ...availableModels.map((model) => model.fullId)]
|
|
239
246
|
: [input.overrideModel, parentModel];
|
|
240
247
|
const configuredModels = rawModels
|
|
241
248
|
.filter((model): model is string => Boolean(model?.trim()))
|
|
242
249
|
.filter((model) => isAvailableModel(model.trim(), availableModels));
|
|
243
|
-
|
|
250
|
+
const candidates = buildModelCandidates(configuredModels[0], configuredModels.slice(1), availableModels, preferredProvider);
|
|
251
|
+
const reason = requested && candidates[0] && resolveModelCandidate(requested, availableModels, preferredProvider) !== candidates[0]
|
|
252
|
+
? "requested model unavailable; selected configured Pi fallback"
|
|
253
|
+
: candidates.length > 1
|
|
254
|
+
? "configured Pi fallback chain"
|
|
255
|
+
: undefined;
|
|
256
|
+
return { requested, candidates, reason };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function buildConfiguredModelCandidates(input: Parameters<typeof buildConfiguredModelRouting>[0]): string[] {
|
|
260
|
+
return buildConfiguredModelRouting(input).candidates;
|
|
244
261
|
}
|
package/src/runtime/pi-args.ts
CHANGED
|
@@ -93,10 +93,12 @@ export function buildPiWorkerArgs(input: BuildPiWorkerArgsInput): BuildPiWorkerA
|
|
|
93
93
|
PI_CREW_INHERIT_SKILLS: input.agent.inheritSkills ? "1" : "0",
|
|
94
94
|
PI_CREW_DEPTH: String(parentDepth + 1),
|
|
95
95
|
PI_CREW_MAX_DEPTH: String(maxDepth),
|
|
96
|
+
PI_CREW_ROLE: input.agent.name,
|
|
96
97
|
PI_TEAMS_INHERIT_PROJECT_CONTEXT: input.agent.inheritProjectContext ? "1" : "0",
|
|
97
98
|
PI_TEAMS_INHERIT_SKILLS: input.agent.inheritSkills ? "1" : "0",
|
|
98
99
|
PI_TEAMS_DEPTH: String(parentDepth + 1),
|
|
99
100
|
PI_TEAMS_MAX_DEPTH: String(maxDepth),
|
|
101
|
+
PI_TEAMS_ROLE: input.agent.name,
|
|
100
102
|
},
|
|
101
103
|
tempDir,
|
|
102
104
|
};
|
|
@@ -26,3 +26,14 @@ export function checkRolePermission(role: string, command: string): PermissionCh
|
|
|
26
26
|
if (mode === "read_only" && !isReadOnlyCommand(command)) return { allowed: false, mode, reason: `Role '${role}' is read-only and command may modify state.` };
|
|
27
27
|
return { allowed: true, mode };
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
export function currentCrewRole(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
31
|
+
return env.PI_CREW_ROLE?.trim() || env.PI_TEAMS_ROLE?.trim() || undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function checkSubagentSpawnPermission(role: string | undefined): PermissionCheckResult {
|
|
35
|
+
if (!role) return { allowed: true, mode: "workspace_write" };
|
|
36
|
+
const mode = permissionForRole(role);
|
|
37
|
+
if (mode === "read_only") return { allowed: false, mode, reason: `Role '${role}' is read-only and cannot spawn additional subagents.` };
|
|
38
|
+
return { allowed: true, mode };
|
|
39
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import type { AgentConfig } from "../../agents/agent-config.ts";
|
|
3
|
+
import type { CrewRuntimeConfig } from "../../config/config.ts";
|
|
4
|
+
import { writeArtifact } from "../../state/artifact-store.ts";
|
|
5
|
+
import { appendEvent } from "../../state/event-log.ts";
|
|
6
|
+
import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState } from "../../state/types.ts";
|
|
7
|
+
import type { WorkflowStep } from "../../workflows/workflow-config.ts";
|
|
8
|
+
import { appendCrewAgentEvent, appendCrewAgentOutput, emptyCrewAgentProgress, recordFromTask, upsertCrewAgent } from "../crew-agent-records.ts";
|
|
9
|
+
import { createStartupEvidence, type WorkerStartupEvidence } from "../worker-startup.ts";
|
|
10
|
+
import { runLiveSessionTask } from "../live-session-runtime.ts";
|
|
11
|
+
import { shouldAppendProgressEventUpdate, type ProgressEventSummary } from "../progress-event-coalescer.ts";
|
|
12
|
+
import { applyAgentProgressEvent, applyUsageToProgress, progressEventSummary, shouldFlushProgressEvent } from "./progress.ts";
|
|
13
|
+
import type { ParsedPiJsonOutput } from "../pi-json-output.ts";
|
|
14
|
+
|
|
15
|
+
export interface RunLiveTaskInput {
|
|
16
|
+
manifest: TeamRunManifest;
|
|
17
|
+
tasks: TeamTaskState[];
|
|
18
|
+
task: TeamTaskState;
|
|
19
|
+
step: WorkflowStep;
|
|
20
|
+
agent: AgentConfig;
|
|
21
|
+
prompt: string;
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
runtimeConfig?: CrewRuntimeConfig;
|
|
24
|
+
parentContext?: string;
|
|
25
|
+
parentModel?: unknown;
|
|
26
|
+
modelRegistry?: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RunLiveTaskOutput {
|
|
30
|
+
task: TeamTaskState;
|
|
31
|
+
tasks: TeamTaskState[];
|
|
32
|
+
startupEvidence: WorkerStartupEvidence;
|
|
33
|
+
exitCode: number | null;
|
|
34
|
+
error?: string;
|
|
35
|
+
parsedOutput?: ParsedPiJsonOutput;
|
|
36
|
+
resultArtifact: ArtifactDescriptor;
|
|
37
|
+
logArtifact?: ArtifactDescriptor;
|
|
38
|
+
transcriptArtifact?: ArtifactDescriptor;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function updateTask(tasks: TeamTaskState[], updated: TeamTaskState): TeamTaskState[] {
|
|
42
|
+
return tasks.map((task) => task.id === updated.id ? updated : task);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function runLiveTask(input: RunLiveTaskInput): Promise<RunLiveTaskOutput> {
|
|
46
|
+
const { manifest, step, agent, prompt } = input;
|
|
47
|
+
let task = input.task;
|
|
48
|
+
let tasks = input.tasks;
|
|
49
|
+
const transcriptPath = `${manifest.artifactsRoot}/transcripts/${task.id}.jsonl`;
|
|
50
|
+
let lastAgentRecordPersistedAt = 0;
|
|
51
|
+
let lastRunProgressPersistedAt = 0;
|
|
52
|
+
let lastRunProgressSummary: ProgressEventSummary | undefined;
|
|
53
|
+
const persistLiveProgress = (event: unknown, force = false): void => {
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
if (force || shouldFlushProgressEvent(event) || now - lastAgentRecordPersistedAt >= 500) {
|
|
56
|
+
upsertCrewAgent(manifest, recordFromTask(manifest, task, "live-session"));
|
|
57
|
+
lastAgentRecordPersistedAt = now;
|
|
58
|
+
}
|
|
59
|
+
const summary = progressEventSummary(task, event);
|
|
60
|
+
const decision = shouldAppendProgressEventUpdate({ previous: lastRunProgressSummary, next: summary, nowMs: now, lastAppendMs: lastRunProgressPersistedAt || undefined, minIntervalMs: 1000, force });
|
|
61
|
+
if (decision.shouldAppend) {
|
|
62
|
+
appendEvent(manifest.eventsPath, { type: "task.progress", runId: manifest.runId, taskId: task.id, data: { ...summary, coalesceReason: decision.reason } });
|
|
63
|
+
lastRunProgressSummary = summary;
|
|
64
|
+
lastRunProgressPersistedAt = now;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const attemptStartedAt = new Date();
|
|
68
|
+
const liveResult = await runLiveSessionTask({
|
|
69
|
+
manifest,
|
|
70
|
+
task,
|
|
71
|
+
step,
|
|
72
|
+
agent,
|
|
73
|
+
prompt,
|
|
74
|
+
signal: input.signal,
|
|
75
|
+
transcriptPath,
|
|
76
|
+
runtimeConfig: input.runtimeConfig,
|
|
77
|
+
parentContext: input.parentContext,
|
|
78
|
+
parentModel: input.parentModel,
|
|
79
|
+
modelRegistry: input.modelRegistry,
|
|
80
|
+
onOutput: (text) => appendCrewAgentOutput(manifest, task.id, text),
|
|
81
|
+
onEvent: (event) => {
|
|
82
|
+
appendCrewAgentEvent(manifest, task.id, event);
|
|
83
|
+
task = { ...task, agentProgress: applyAgentProgressEvent(task.agentProgress ?? emptyCrewAgentProgress(), event, task.startedAt) };
|
|
84
|
+
tasks = updateTask(tasks, task);
|
|
85
|
+
persistLiveProgress(event);
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
const 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 });
|
|
89
|
+
const exitCode = liveResult.exitCode;
|
|
90
|
+
const error = liveResult.error || (liveResult.exitCode && liveResult.exitCode !== 0 ? liveResult.stderr || `Live session exited with ${liveResult.exitCode}` : undefined);
|
|
91
|
+
const parsedOutput = { finalText: liveResult.stdout, textEvents: liveResult.stdout ? [liveResult.stdout] : [], jsonEvents: liveResult.jsonEvents, usage: liveResult.usage };
|
|
92
|
+
if (liveResult.usage) task = { ...task, usage: liveResult.usage, agentProgress: applyUsageToProgress(task.agentProgress, liveResult.usage) };
|
|
93
|
+
persistLiveProgress({ type: "attempt_finished" }, true);
|
|
94
|
+
const resultArtifact = writeArtifact(manifest.artifactsRoot, { kind: "result", relativePath: `results/${task.id}.txt`, content: liveResult.stdout || liveResult.stderr || "(no output)", producer: task.id });
|
|
95
|
+
const logArtifact = writeArtifact(manifest.artifactsRoot, { kind: "log", relativePath: `logs/${task.id}.log`, 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"), producer: task.id });
|
|
96
|
+
const transcriptArtifact = fs.existsSync(transcriptPath) ? writeArtifact(manifest.artifactsRoot, { kind: "log", relativePath: `transcripts/${task.id}.jsonl`, content: fs.readFileSync(transcriptPath, "utf-8"), producer: task.id }) : undefined;
|
|
97
|
+
return { task, tasks, startupEvidence, exitCode, error: error || undefined, parsedOutput, resultArtifact, logArtifact, transcriptArtifact };
|
|
98
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { UsageState } from "../../state/types.ts";
|
|
2
|
+
import type { CrewAgentProgress } from "../crew-agent-runtime.ts";
|
|
3
|
+
import { emptyCrewAgentProgress } from "../crew-agent-records.ts";
|
|
4
|
+
import type { ProgressEventSummary } from "../progress-event-coalescer.ts";
|
|
5
|
+
import type { TeamTaskState } from "../../state/types.ts";
|
|
6
|
+
|
|
7
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
8
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function textFromContent(content: unknown): string[] {
|
|
12
|
+
if (typeof content === "string") return [content];
|
|
13
|
+
if (!Array.isArray(content)) return [];
|
|
14
|
+
const text: string[] = [];
|
|
15
|
+
for (const part of content) {
|
|
16
|
+
const obj = asRecord(part);
|
|
17
|
+
if (!obj) continue;
|
|
18
|
+
if (obj.type === "text" && typeof obj.text === "string") text.push(obj.text);
|
|
19
|
+
else if (typeof obj.content === "string") text.push(obj.content);
|
|
20
|
+
}
|
|
21
|
+
return text;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function eventText(event: unknown): string[] {
|
|
25
|
+
const obj = asRecord(event);
|
|
26
|
+
if (!obj) return [];
|
|
27
|
+
const text: string[] = [];
|
|
28
|
+
if (typeof obj.text === "string") text.push(obj.text);
|
|
29
|
+
if (typeof obj.output === "string") text.push(obj.output);
|
|
30
|
+
text.push(...textFromContent(obj.content));
|
|
31
|
+
const message = asRecord(obj.message);
|
|
32
|
+
if (message) text.push(...textFromContent(message.content));
|
|
33
|
+
return text.filter((entry) => entry.trim());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function numberField(obj: Record<string, unknown>, keys: string[]): number | undefined {
|
|
37
|
+
for (const key of keys) {
|
|
38
|
+
const value = obj[key];
|
|
39
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function eventUsage(event: unknown): { input?: number; output?: number; turns?: number } | undefined {
|
|
45
|
+
const obj = asRecord(event);
|
|
46
|
+
if (!obj) return undefined;
|
|
47
|
+
const direct = { input: numberField(obj, ["input", "inputTokens", "input_tokens"]), output: numberField(obj, ["output", "outputTokens", "output_tokens"]), turns: numberField(obj, ["turns", "turnCount", "turn_count"]) };
|
|
48
|
+
if (Object.values(direct).some((value) => value !== undefined)) return direct;
|
|
49
|
+
for (const key of ["usage", "tokenUsage", "tokens", "stats"]) {
|
|
50
|
+
const nested = eventUsage(obj[key]);
|
|
51
|
+
if (nested) return nested;
|
|
52
|
+
}
|
|
53
|
+
const message = asRecord(obj.message);
|
|
54
|
+
return message ? eventUsage(message.usage) : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function previewArgs(args: unknown): string | undefined {
|
|
58
|
+
if (!args) return undefined;
|
|
59
|
+
try {
|
|
60
|
+
const text = typeof args === "string" ? args : JSON.stringify(args);
|
|
61
|
+
return text.length > 240 ? `${text.slice(0, 240)}…` : text;
|
|
62
|
+
} catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function applyUsageToProgress(progress: CrewAgentProgress | undefined, usage: UsageState | undefined): CrewAgentProgress | undefined {
|
|
68
|
+
if (!usage) return progress;
|
|
69
|
+
const base = progress ?? emptyCrewAgentProgress();
|
|
70
|
+
return { ...base, tokens: (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0), turns: usage.turns ?? base.turns };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function shouldFlushProgressEvent(event: unknown): boolean {
|
|
74
|
+
const type = asRecord(event)?.type;
|
|
75
|
+
return type === "tool_execution_start" || type === "tool_execution_end" || type === "message_end" || type === "tool_result_end";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function progressEventSummary(task: TeamTaskState, event: unknown): ProgressEventSummary {
|
|
79
|
+
const type = asRecord(event)?.type;
|
|
80
|
+
return { eventType: typeof type === "string" ? type : "event", currentTool: task.agentProgress?.currentTool, toolCount: task.agentProgress?.toolCount, tokens: task.agentProgress?.tokens, turns: task.agentProgress?.turns, activityState: task.agentProgress?.activityState, lastActivityAt: task.agentProgress?.lastActivityAt };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function applyAgentProgressEvent(progress: CrewAgentProgress, event: unknown, startedAt: string | undefined): CrewAgentProgress {
|
|
84
|
+
const obj = asRecord(event);
|
|
85
|
+
const now = new Date().toISOString();
|
|
86
|
+
const next: CrewAgentProgress = { ...progress, recentTools: [...progress.recentTools], recentOutput: [...progress.recentOutput], lastActivityAt: now, activityState: "active" };
|
|
87
|
+
if (startedAt) next.durationMs = Date.now() - new Date(startedAt).getTime();
|
|
88
|
+
if (obj?.type === "tool_execution_start") {
|
|
89
|
+
next.toolCount += 1;
|
|
90
|
+
next.currentTool = typeof obj.toolName === "string" ? obj.toolName : typeof obj.name === "string" ? obj.name : "tool";
|
|
91
|
+
next.currentToolArgs = previewArgs(obj.args);
|
|
92
|
+
next.currentToolStartedAt = now;
|
|
93
|
+
}
|
|
94
|
+
if (obj?.type === "tool_execution_end") {
|
|
95
|
+
if (next.currentTool) next.recentTools.push({ tool: next.currentTool, args: next.currentToolArgs, endedAt: now });
|
|
96
|
+
next.currentTool = undefined;
|
|
97
|
+
next.currentToolArgs = undefined;
|
|
98
|
+
next.currentToolStartedAt = undefined;
|
|
99
|
+
}
|
|
100
|
+
if ((obj?.type === "tool_execution_error" || obj?.type === "tool_execution_failed") && next.currentTool) next.failedTool = next.currentTool;
|
|
101
|
+
const usage = eventUsage(event);
|
|
102
|
+
if (usage) {
|
|
103
|
+
next.tokens = (usage.input ?? 0) + (usage.output ?? 0);
|
|
104
|
+
next.turns = usage.turns ?? next.turns;
|
|
105
|
+
}
|
|
106
|
+
const text = eventText(event);
|
|
107
|
+
if (text.length > 0) next.recentOutput.push(...text.flatMap((entry) => entry.split(/\r?\n/)).filter(Boolean).slice(-10));
|
|
108
|
+
if (next.recentTools.length > 25) next.recentTools.splice(0, next.recentTools.length - 25);
|
|
109
|
+
if (next.recentOutput.length > 50) next.recentOutput.splice(0, next.recentOutput.length - 50);
|
|
110
|
+
return next;
|
|
111
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { AgentConfig } from "../../agents/agent-config.ts";
|
|
2
|
+
import type { TeamRunManifest, TeamTaskState } from "../../state/types.ts";
|
|
3
|
+
import type { WorkflowStep } from "../../workflows/workflow-config.ts";
|
|
4
|
+
import { buildMemoryBlock } from "../agent-memory.ts";
|
|
5
|
+
import { permissionForRole } from "../role-permission.ts";
|
|
6
|
+
import { renderTaskPacket } from "../task-packet.ts";
|
|
7
|
+
|
|
8
|
+
function readOnlyRoleInstructions(role: string): string {
|
|
9
|
+
if (permissionForRole(role) !== "read_only") return "";
|
|
10
|
+
return [
|
|
11
|
+
"# READ-ONLY ROLE CONTRACT",
|
|
12
|
+
"You are running in READ-ONLY mode for this task.",
|
|
13
|
+
"- Do not create, modify, delete, move, or copy files.",
|
|
14
|
+
"- Do not use shell redirects, heredocs, in-place edits, package installs, git commit/merge/rebase/reset/checkout, or other state-mutating commands.",
|
|
15
|
+
"- If implementation changes are needed, report exact recommendations instead of applying them.",
|
|
16
|
+
"- Prefer read/grep/find/listing tools and read-only git inspection commands.",
|
|
17
|
+
].join("\n");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function coordinationBridgeInstructions(task: TeamTaskState): string {
|
|
21
|
+
return [
|
|
22
|
+
"# Crew Coordination Channel",
|
|
23
|
+
`Mailbox target for this task: ${task.id}`,
|
|
24
|
+
"Use the run mailbox contract for coordination with the leader/orchestrator:",
|
|
25
|
+
"- 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.",
|
|
26
|
+
"- If nudged, answer with current status, blocker, or smallest next step.",
|
|
27
|
+
"- Treat inherited/dependency context as reference-only; do not continue the parent conversation directly.",
|
|
28
|
+
"- Completion handoff should include: DONE/FAILED, summary, changed/read files, verification evidence, and remaining risks.",
|
|
29
|
+
].join("\n");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function inputDependencyContext(task: TeamTaskState): string {
|
|
33
|
+
return (task as TeamTaskState & { dependencyContextText?: string }).dependencyContextText ?? "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function renderTaskPrompt(manifest: TeamRunManifest, step: WorkflowStep, task: TeamTaskState, agent?: AgentConfig): string {
|
|
37
|
+
const memoryBlock = agent?.memory ? buildMemoryBlock(agent.name, agent.memory, task.cwd, Boolean(agent.tools?.some((tool) => tool === "write" || tool === "edit"))) : "";
|
|
38
|
+
return [
|
|
39
|
+
"# pi-crew Worker Runtime Context",
|
|
40
|
+
`Run ID: ${manifest.runId}`,
|
|
41
|
+
`Team: ${manifest.team}`,
|
|
42
|
+
`Workflow: ${manifest.workflow ?? "(none)"}`,
|
|
43
|
+
`State root: ${manifest.stateRoot}`,
|
|
44
|
+
`Artifacts root: ${manifest.artifactsRoot}`,
|
|
45
|
+
`Events path: ${manifest.eventsPath}`,
|
|
46
|
+
`Task ID: ${task.id}`,
|
|
47
|
+
`Task cwd: ${task.cwd}`,
|
|
48
|
+
`Workspace mode: ${manifest.workspaceMode}`,
|
|
49
|
+
"",
|
|
50
|
+
`Goal:\n${manifest.goal}`,
|
|
51
|
+
"",
|
|
52
|
+
`Step: ${step.id}`,
|
|
53
|
+
`Role: ${step.role}`,
|
|
54
|
+
"",
|
|
55
|
+
"Protocol:",
|
|
56
|
+
"- Stay within the task scope unless the prompt explicitly says otherwise.",
|
|
57
|
+
"- Report blockers and verification evidence in the final result.",
|
|
58
|
+
"- Do not claim completion without evidence.",
|
|
59
|
+
"- Follow the Task Packet contract below; escalate if any contract field is impossible to satisfy.",
|
|
60
|
+
"",
|
|
61
|
+
readOnlyRoleInstructions(task.role),
|
|
62
|
+
"",
|
|
63
|
+
coordinationBridgeInstructions(task),
|
|
64
|
+
"",
|
|
65
|
+
task.taskPacket ? renderTaskPacket(task.taskPacket) : "",
|
|
66
|
+
"",
|
|
67
|
+
(inputDependencyContext(task) || ""),
|
|
68
|
+
memoryBlock,
|
|
69
|
+
"Task:",
|
|
70
|
+
step.task.replaceAll("{goal}", manifest.goal),
|
|
71
|
+
].join("\n");
|
|
72
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function cleanResultText(text: string | undefined): string | undefined {
|
|
2
|
+
const trimmed = text?.trim();
|
|
3
|
+
if (!trimmed) return undefined;
|
|
4
|
+
const doneIndex = trimmed.lastIndexOf("\nDONE\n");
|
|
5
|
+
if (doneIndex >= 0) return trimmed.slice(doneIndex + 1).trim();
|
|
6
|
+
if (trimmed === "DONE" || trimmed.startsWith("DONE\n")) return trimmed;
|
|
7
|
+
const fencedPromptIndex = trimmed.lastIndexOf("</file>");
|
|
8
|
+
if (fencedPromptIndex >= 0 && fencedPromptIndex < trimmed.length - 7) return trimmed.slice(fencedPromptIndex + 7).trim() || trimmed;
|
|
9
|
+
return trimmed;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isFinalChildEvent(event: unknown): boolean {
|
|
13
|
+
return Boolean(event && typeof event === "object" && !Array.isArray(event) && (event as Record<string, unknown>).type === "message_end");
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { TaskCheckpointState, TeamRunManifest, TeamTaskState } from "../../state/types.ts";
|
|
2
|
+
import { loadRunManifestById, saveRunTasks } from "../../state/state-store.ts";
|
|
3
|
+
import { recordFromTask, upsertCrewAgent } from "../crew-agent-records.ts";
|
|
4
|
+
|
|
5
|
+
export function updateTask(tasks: TeamTaskState[], updated: TeamTaskState): TeamTaskState[] {
|
|
6
|
+
return tasks.map((task) => task.id === updated.id ? updated : task);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function persistSingleTaskUpdate(manifest: TeamRunManifest, fallbackTasks: TeamTaskState[], updated: TeamTaskState): TeamTaskState[] {
|
|
10
|
+
const latest = loadRunManifestById(manifest.cwd, manifest.runId)?.tasks ?? fallbackTasks;
|
|
11
|
+
const merged = updateTask(latest, updated);
|
|
12
|
+
saveRunTasks(manifest, merged);
|
|
13
|
+
return merged;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function checkpointTask(manifest: TeamRunManifest, tasks: TeamTaskState[], task: TeamTaskState, phase: TaskCheckpointState["phase"], childPid?: number): { task: TeamTaskState; tasks: TeamTaskState[] } {
|
|
17
|
+
const checkpoint: TaskCheckpointState = { phase, updatedAt: new Date().toISOString(), ...(childPid ? { childPid } : task.checkpoint?.childPid ? { childPid: task.checkpoint.childPid } : {}) };
|
|
18
|
+
const nextTask = { ...task, checkpoint };
|
|
19
|
+
const nextTasks = persistSingleTaskUpdate(manifest, updateTask(tasks, nextTask), nextTask);
|
|
20
|
+
upsertCrewAgent(manifest, recordFromTask(manifest, nextTask, "child-process"));
|
|
21
|
+
return { task: nextTask, tasks: nextTasks };
|
|
22
|
+
}
|