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.
Files changed (59) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/NOTICE.md +1 -0
  3. package/docs/architecture.md +164 -92
  4. package/docs/refactor-tasks-phase6.md +662 -0
  5. package/docs/runtime-flow.md +148 -0
  6. package/package.json +1 -1
  7. package/schema.json +1 -0
  8. package/skills/git-master/SKILL.md +19 -0
  9. package/skills/read-only-explorer/SKILL.md +21 -0
  10. package/skills/safe-bash/SKILL.md +16 -0
  11. package/skills/task-packet/SKILL.md +23 -0
  12. package/skills/verify-evidence/SKILL.md +22 -0
  13. package/src/config/config.ts +2 -0
  14. package/src/config/defaults.ts +1 -0
  15. package/src/extension/async-notifier.ts +33 -4
  16. package/src/extension/register.ts +15 -522
  17. package/src/extension/registration/artifact-cleanup.ts +14 -0
  18. package/src/extension/registration/commands.ts +208 -0
  19. package/src/extension/registration/subagent-helpers.ts +1 -1
  20. package/src/extension/registration/subagent-tools.ts +110 -0
  21. package/src/extension/registration/team-tool.ts +44 -0
  22. package/src/extension/team-tool/api.ts +4 -4
  23. package/src/extension/team-tool/cancel.ts +31 -0
  24. package/src/extension/team-tool/inspect.ts +41 -0
  25. package/src/extension/team-tool/lifecycle-actions.ts +79 -0
  26. package/src/extension/team-tool/plan.ts +19 -0
  27. package/src/extension/team-tool/run.ts +41 -3
  28. package/src/extension/team-tool/status.ts +73 -0
  29. package/src/extension/team-tool.ts +57 -224
  30. package/src/runtime/async-marker.ts +26 -0
  31. package/src/runtime/async-runner.ts +44 -9
  32. package/src/runtime/background-runner.ts +2 -0
  33. package/src/runtime/child-pi.ts +5 -1
  34. package/src/runtime/concurrency.ts +9 -3
  35. package/src/runtime/crew-agent-records.ts +1 -0
  36. package/src/runtime/crew-agent-runtime.ts +2 -1
  37. package/src/runtime/model-fallback.ts +21 -4
  38. package/src/runtime/pi-args.ts +2 -0
  39. package/src/runtime/process-status.ts +1 -0
  40. package/src/runtime/role-permission.ts +11 -0
  41. package/src/runtime/task-runner/live-executor.ts +98 -0
  42. package/src/runtime/task-runner/progress.ts +111 -0
  43. package/src/runtime/task-runner/prompt-builder.ts +72 -0
  44. package/src/runtime/task-runner/result-utils.ts +14 -0
  45. package/src/runtime/task-runner/state-helpers.ts +22 -0
  46. package/src/runtime/task-runner.ts +38 -283
  47. package/src/runtime/team-runner.ts +116 -7
  48. package/src/schema/config-schema.ts +1 -0
  49. package/src/state/mailbox.ts +28 -0
  50. package/src/state/types.ts +16 -0
  51. package/src/subagents/async-entry.ts +1 -0
  52. package/src/subagents/index.ts +3 -0
  53. package/src/subagents/live/control.ts +1 -0
  54. package/src/subagents/live/manager.ts +1 -0
  55. package/src/subagents/live/realtime.ts +1 -0
  56. package/src/subagents/live/session-runtime.ts +1 -0
  57. package/src/subagents/manager.ts +1 -0
  58. package/src/subagents/spawn.ts +1 -0
  59. 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: requested,
46
- selectedCount: readyCount === 0 ? 0 : Math.min(readyCount, requested),
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 function buildConfiguredModelCandidates(input: {
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
- }): string[] {
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
- if (availableModels && availableModels.length === 0) return [];
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
- return buildModelCandidates(configuredModels[0], configuredModels.slice(1), availableModels, preferredProvider);
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
  }
@@ -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
  };
@@ -1,5 +1,6 @@
1
1
  import type { CrewAgentRecord } from "./crew-agent-runtime.ts";
2
2
  import type { TeamRunManifest } from "../state/types.ts";
3
+ export { hasAsyncStartMarker } from "./async-marker.ts";
3
4
 
4
5
  export interface ProcessLiveness {
5
6
  pid?: number;
@@ -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
+ }