pi-crew 0.1.5 → 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.
@@ -0,0 +1,99 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
4
+ import { atomicWriteJson, readJsonFile } from "../state/atomic-write.ts";
5
+ import type { CrewAgentProgress, CrewAgentRecord, CrewRuntimeKind } from "./crew-agent-runtime.ts";
6
+ import { taskStatusToAgentStatus } from "./crew-agent-runtime.ts";
7
+
8
+ export function agentsPath(manifest: TeamRunManifest): string {
9
+ return path.join(manifest.stateRoot, "agents.json");
10
+ }
11
+
12
+ export function agentStateDir(manifest: TeamRunManifest, taskId: string): string {
13
+ return path.join(manifest.stateRoot, "agents", taskId);
14
+ }
15
+
16
+ export function agentStatusPath(manifest: TeamRunManifest, taskId: string): string {
17
+ return path.join(agentStateDir(manifest, taskId), "status.json");
18
+ }
19
+
20
+ export function agentEventsPath(manifest: TeamRunManifest, taskId: string): string {
21
+ return path.join(agentStateDir(manifest, taskId), "events.jsonl");
22
+ }
23
+
24
+ export function agentOutputPath(manifest: TeamRunManifest, taskId: string): string {
25
+ return path.join(agentStateDir(manifest, taskId), "output.log");
26
+ }
27
+
28
+ export function readCrewAgents(manifest: TeamRunManifest): CrewAgentRecord[] {
29
+ return readJsonFile<CrewAgentRecord[]>(agentsPath(manifest)) ?? [];
30
+ }
31
+
32
+ export function saveCrewAgents(manifest: TeamRunManifest, records: CrewAgentRecord[]): void {
33
+ fs.mkdirSync(manifest.stateRoot, { recursive: true });
34
+ atomicWriteJson(agentsPath(manifest), records);
35
+ for (const record of records) writeCrewAgentStatus(manifest, record);
36
+ }
37
+
38
+ export function upsertCrewAgent(manifest: TeamRunManifest, record: CrewAgentRecord): void {
39
+ const records = readCrewAgents(manifest).filter((item) => item.id !== record.id);
40
+ records.push(record);
41
+ saveCrewAgents(manifest, records);
42
+ writeCrewAgentStatus(manifest, record);
43
+ }
44
+
45
+ export function writeCrewAgentStatus(manifest: TeamRunManifest, record: CrewAgentRecord): void {
46
+ fs.mkdirSync(agentStateDir(manifest, record.taskId), { recursive: true });
47
+ atomicWriteJson(agentStatusPath(manifest, record.taskId), record);
48
+ }
49
+
50
+ export function readCrewAgentStatus(manifest: TeamRunManifest, taskOrAgentId: string): CrewAgentRecord | undefined {
51
+ const taskId = taskOrAgentId.includes(":") ? taskOrAgentId.split(":").pop()! : taskOrAgentId;
52
+ return readJsonFile<CrewAgentRecord>(agentStatusPath(manifest, taskId));
53
+ }
54
+
55
+ export function appendCrewAgentEvent(manifest: TeamRunManifest, taskId: string, event: unknown): void {
56
+ fs.mkdirSync(agentStateDir(manifest, taskId), { recursive: true });
57
+ fs.appendFileSync(agentEventsPath(manifest, taskId), `${JSON.stringify({ time: new Date().toISOString(), event })}\n`, "utf-8");
58
+ }
59
+
60
+ export function readCrewAgentEvents(manifest: TeamRunManifest, taskId: string): unknown[] {
61
+ const filePath = agentEventsPath(manifest, taskId);
62
+ if (!fs.existsSync(filePath)) return [];
63
+ return fs.readFileSync(filePath, "utf-8").split(/\r?\n/).filter(Boolean).map((line) => {
64
+ try { return JSON.parse(line) as unknown; } catch { return { raw: line }; }
65
+ });
66
+ }
67
+
68
+ export function appendCrewAgentOutput(manifest: TeamRunManifest, taskId: string, text: string): void {
69
+ if (!text.trim()) return;
70
+ fs.mkdirSync(agentStateDir(manifest, taskId), { recursive: true });
71
+ fs.appendFileSync(agentOutputPath(manifest, taskId), `${text}\n`, "utf-8");
72
+ }
73
+
74
+ export function emptyCrewAgentProgress(): CrewAgentProgress {
75
+ return { recentTools: [], recentOutput: [], toolCount: 0 };
76
+ }
77
+
78
+ export function recordFromTask(manifest: TeamRunManifest, task: TeamTaskState, runtime: CrewRuntimeKind): CrewAgentRecord {
79
+ return {
80
+ id: `${manifest.runId}:${task.id}`,
81
+ runId: manifest.runId,
82
+ taskId: task.id,
83
+ agent: task.agent,
84
+ role: task.role,
85
+ runtime,
86
+ status: taskStatusToAgentStatus(task.status),
87
+ startedAt: task.startedAt ?? new Date().toISOString(),
88
+ completedAt: task.finishedAt,
89
+ resultArtifactPath: task.resultArtifact?.path,
90
+ transcriptPath: task.transcriptArtifact?.path ?? task.logArtifact?.path,
91
+ statusPath: agentStatusPath(manifest, task.id),
92
+ eventsPath: agentEventsPath(manifest, task.id),
93
+ outputPath: agentOutputPath(manifest, task.id),
94
+ toolUses: task.agentProgress?.toolCount,
95
+ jsonEvents: task.jsonEvents,
96
+ progress: task.agentProgress,
97
+ error: task.error,
98
+ };
99
+ }
@@ -0,0 +1,53 @@
1
+ import type { TeamTaskStatus } from "../state/contracts.ts";
2
+
3
+ export type CrewRuntimeKind = "scaffold" | "child-process" | "live-session";
4
+ export type CrewAgentStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "stopped";
5
+
6
+ export interface CrewAgentRecentTool {
7
+ tool: string;
8
+ args?: string;
9
+ endedAt: string;
10
+ }
11
+
12
+ export interface CrewAgentProgress {
13
+ currentTool?: string;
14
+ currentToolArgs?: string;
15
+ currentToolStartedAt?: string;
16
+ recentTools: CrewAgentRecentTool[];
17
+ recentOutput: string[];
18
+ toolCount: number;
19
+ tokens?: number;
20
+ turns?: number;
21
+ durationMs?: number;
22
+ lastActivityAt?: string;
23
+ activityState?: "active" | "needs_attention" | "stale";
24
+ }
25
+
26
+ export interface CrewAgentRecord {
27
+ id: string;
28
+ runId: string;
29
+ taskId: string;
30
+ agent: string;
31
+ role: string;
32
+ runtime: CrewRuntimeKind;
33
+ status: CrewAgentStatus;
34
+ startedAt: string;
35
+ completedAt?: string;
36
+ resultArtifactPath?: string;
37
+ transcriptPath?: string;
38
+ statusPath?: string;
39
+ eventsPath?: string;
40
+ outputPath?: string;
41
+ toolUses?: number;
42
+ jsonEvents?: number;
43
+ progress?: CrewAgentProgress;
44
+ error?: string;
45
+ }
46
+
47
+ export function taskStatusToAgentStatus(status: TeamTaskStatus): CrewAgentStatus {
48
+ if (status === "completed") return "completed";
49
+ if (status === "failed") return "failed";
50
+ if (status === "cancelled") return "cancelled";
51
+ if (status === "running") return "running";
52
+ return "queued";
53
+ }
@@ -0,0 +1,88 @@
1
+ import type { CrewRuntimeConfig } from "../config/config.ts";
2
+ import { writeArtifact } from "../state/artifact-store.ts";
3
+ import { appendEvent } from "../state/event-log.ts";
4
+ import { appendMailboxMessage } from "../state/mailbox.ts";
5
+ import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState } from "../state/types.ts";
6
+ import { aggregateTaskOutputs } from "./task-output-context.ts";
7
+
8
+ export type CrewGroupJoinMode = "off" | "group" | "smart";
9
+
10
+ export interface CrewGroupJoinDelivery {
11
+ batchId: string;
12
+ mode: CrewGroupJoinMode;
13
+ partial: boolean;
14
+ taskIds: string[];
15
+ completed: string[];
16
+ failed: string[];
17
+ skipped: string[];
18
+ remaining: string[];
19
+ artifact?: ArtifactDescriptor;
20
+ messageId?: string;
21
+ }
22
+
23
+ export function resolveGroupJoinMode(runtime?: CrewRuntimeConfig): CrewGroupJoinMode {
24
+ return runtime?.groupJoin ?? "smart";
25
+ }
26
+
27
+ export function shouldGroupJoin(mode: CrewGroupJoinMode, batch: TeamTaskState[]): boolean {
28
+ if (mode === "off") return false;
29
+ if (mode === "group") return batch.length > 0;
30
+ return batch.length > 1;
31
+ }
32
+
33
+ function batchIdFor(runId: string, taskIds: string[]): string {
34
+ return `${runId}_${taskIds.join("+").replace(/[^a-zA-Z0-9_+-]/g, "_")}`;
35
+ }
36
+
37
+ function statusList(tasks: TeamTaskState[], status: TeamTaskState["status"]): string[] {
38
+ return tasks.filter((task) => task.status === status).map((task) => task.id);
39
+ }
40
+
41
+ export function deliverGroupJoin(input: {
42
+ manifest: TeamRunManifest;
43
+ mode: CrewGroupJoinMode;
44
+ batch: TeamTaskState[];
45
+ allTasks: TeamTaskState[];
46
+ partial?: boolean;
47
+ }): CrewGroupJoinDelivery | undefined {
48
+ if (!shouldGroupJoin(input.mode, input.batch)) return undefined;
49
+ const taskIds = input.batch.map((task) => task.id);
50
+ const latest = taskIds.map((id) => input.allTasks.find((task) => task.id === id)).filter((task): task is TeamTaskState => Boolean(task));
51
+ const completed = statusList(latest, "completed");
52
+ const failed = statusList(latest, "failed");
53
+ const skipped = statusList(latest, "skipped");
54
+ const remaining = latest.filter((task) => task.status === "queued" || task.status === "running").map((task) => task.id);
55
+ const partial = input.partial ?? remaining.length > 0;
56
+ const batchId = batchIdFor(input.manifest.runId, taskIds);
57
+ const summary = aggregateTaskOutputs(latest);
58
+ const delivery: CrewGroupJoinDelivery = { batchId, mode: input.mode, partial, taskIds, completed, failed, skipped, remaining };
59
+ const content = `${JSON.stringify({ ...delivery, createdAt: new Date().toISOString() }, null, 2)}\n`;
60
+ const artifact = writeArtifact(input.manifest.artifactsRoot, {
61
+ kind: "metadata",
62
+ relativePath: `metadata/group-joins/${batchId}.json`,
63
+ producer: "group-join",
64
+ content,
65
+ });
66
+ const mailbox = appendMailboxMessage(input.manifest, {
67
+ direction: "outbox",
68
+ from: "group-join",
69
+ to: "leader",
70
+ body: [
71
+ `Group join ${partial ? "partial" : "completed"}: ${taskIds.join(", ")}`,
72
+ `Completed: ${completed.join(", ") || "none"}`,
73
+ `Failed: ${failed.join(", ") || "none"}`,
74
+ `Skipped: ${skipped.join(", ") || "none"}`,
75
+ `Remaining: ${remaining.join(", ") || "none"}`,
76
+ "",
77
+ summary,
78
+ ].join("\n"),
79
+ status: "delivered",
80
+ });
81
+ appendEvent(input.manifest.eventsPath, {
82
+ type: partial ? "agent.group_join.partial" : "agent.group_join.completed",
83
+ runId: input.manifest.runId,
84
+ message: `Group join ${partial ? "partial" : "completed"} for ${taskIds.length} task(s).`,
85
+ data: { ...delivery, artifactPath: artifact.path, messageId: mailbox.id },
86
+ });
87
+ return { ...delivery, artifact, messageId: mailbox.id };
88
+ }
@@ -0,0 +1,33 @@
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 { isLiveSessionRuntimeAvailable } from "./runtime-resolver.ts";
5
+
6
+ export interface LiveSessionSpawnInput {
7
+ manifest: TeamRunManifest;
8
+ task: TeamTaskState;
9
+ step: WorkflowStep;
10
+ agent: AgentConfig;
11
+ prompt: string;
12
+ }
13
+
14
+ export interface LiveSessionUnavailableResult {
15
+ available: false;
16
+ reason: string;
17
+ }
18
+
19
+ export interface LiveSessionPlannedResult {
20
+ available: true;
21
+ reason: string;
22
+ }
23
+
24
+ export async function probeLiveSessionRuntime(): Promise<LiveSessionUnavailableResult | LiveSessionPlannedResult> {
25
+ const availability = await isLiveSessionRuntimeAvailable();
26
+ if (!availability.available) return { available: false, reason: availability.reason ?? "Live-session runtime is unavailable." };
27
+ return { available: true, reason: "Live-session SDK exports are available. Full session execution is intentionally gated behind the runtime adapter implementation." };
28
+ }
29
+
30
+ export async function runLiveSessionTask(_input: LiveSessionSpawnInput): Promise<never> {
31
+ const probe = await probeLiveSessionRuntime();
32
+ throw new Error(probe.available ? "Live-session runtime adapter is not enabled yet; use child-process runtime or scaffold." : probe.reason);
33
+ }
@@ -20,14 +20,37 @@ function decision(action: PolicyDecisionAction, reason: PolicyDecisionReason, me
20
20
  };
21
21
  }
22
22
 
23
+ function taskDepth(task: TeamTaskState, tasksById: Map<string, TeamTaskState>): number {
24
+ let depth = 0;
25
+ let current = task.graph?.parentId;
26
+ const seen = new Set<string>();
27
+ while (current && !seen.has(current)) {
28
+ seen.add(current);
29
+ depth += 1;
30
+ current = tasksById.get(current)?.graph?.parentId;
31
+ }
32
+ return depth;
33
+ }
34
+
23
35
  export function evaluateCrewPolicy(input: PolicyEngineInput): PolicyDecision[] {
24
36
  const decisions: PolicyDecision[] = [];
25
37
  const maxTasksPerRun = input.limits?.maxTasksPerRun;
26
38
  if (maxTasksPerRun !== undefined && input.tasks.length > maxTasksPerRun) {
27
39
  decisions.push(decision("block", "limit_exceeded", `Run has ${input.tasks.length} tasks, exceeding maxTasksPerRun=${maxTasksPerRun}.`));
28
40
  }
41
+ const runningCount = input.tasks.filter((task) => task.status === "running").length;
42
+ if (input.limits?.maxConcurrentWorkers !== undefined && runningCount > input.limits.maxConcurrentWorkers) {
43
+ decisions.push(decision("block", "limit_exceeded", `Run has ${runningCount} running workers, exceeding maxConcurrentWorkers=${input.limits.maxConcurrentWorkers}.`));
44
+ }
45
+ const tasksById = new Map(input.tasks.map((task) => [task.id, task]));
29
46
 
30
47
  for (const task of input.tasks) {
48
+ if (input.limits?.maxChildrenPerTask !== undefined && (task.graph?.children.length ?? 0) > input.limits.maxChildrenPerTask) {
49
+ decisions.push(decision("block", "limit_exceeded", `Task has ${task.graph?.children.length ?? 0} children, exceeding maxChildrenPerTask=${input.limits.maxChildrenPerTask}.`, task.id));
50
+ }
51
+ if (input.limits?.maxTaskDepth !== undefined && taskDepth(task, tasksById) > input.limits.maxTaskDepth) {
52
+ decisions.push(decision("block", "limit_exceeded", `Task graph depth exceeds maxTaskDepth=${input.limits.maxTaskDepth}.`, task.id));
53
+ }
31
54
  if (task.status === "failed") {
32
55
  const retryCount = task.policy?.retryCount ?? 0;
33
56
  const maxRetries = input.limits?.maxRetriesPerTask ?? 0;
@@ -0,0 +1,74 @@
1
+ import type { PolicyDecision, PolicyDecisionReason } from "../state/types.ts";
2
+
3
+ export type FailureScenario = "trust_prompt_unresolved" | "prompt_misdelivery" | "stale_branch" | "compile_red_cross_crate" | "mcp_handshake_failure" | "partial_plugin_startup" | "provider_failure" | "task_failed" | "worker_stale" | "green_unsatisfied";
4
+ export type RecoveryStep = "accept_trust_prompt" | "redirect_prompt_to_agent" | "rebase_branch" | "clean_build" | "retry_mcp_handshake" | "restart_plugin" | "restart_worker" | "rerun_task" | "collect_verification_evidence" | "escalate_to_human";
5
+ export type RecoveryResultState = "planned" | "skipped" | "escalation_required";
6
+
7
+ export interface RecoveryRecipe {
8
+ scenario: FailureScenario;
9
+ steps: RecoveryStep[];
10
+ maxAttempts: number;
11
+ escalationPolicy: "alert_human" | "log_and_continue" | "abort";
12
+ }
13
+
14
+ export interface RecoveryLedgerEntry {
15
+ scenario: FailureScenario;
16
+ taskId?: string;
17
+ decisionReason: PolicyDecisionReason;
18
+ attempt: number;
19
+ state: RecoveryResultState;
20
+ steps: RecoveryStep[];
21
+ message: string;
22
+ createdAt: string;
23
+ }
24
+
25
+ export interface RecoveryLedger {
26
+ entries: RecoveryLedgerEntry[];
27
+ }
28
+
29
+ export function scenarioForPolicyReason(reason: PolicyDecisionReason): FailureScenario {
30
+ switch (reason) {
31
+ case "branch_stale": return "stale_branch";
32
+ case "worker_stale": return "worker_stale";
33
+ case "green_unsatisfied": return "green_unsatisfied";
34
+ case "task_failed": return "task_failed";
35
+ default: return "provider_failure";
36
+ }
37
+ }
38
+
39
+ export function recipeFor(scenario: FailureScenario): RecoveryRecipe {
40
+ switch (scenario) {
41
+ case "trust_prompt_unresolved": return { scenario, steps: ["accept_trust_prompt"], maxAttempts: 1, escalationPolicy: "alert_human" };
42
+ case "prompt_misdelivery": return { scenario, steps: ["redirect_prompt_to_agent"], maxAttempts: 1, escalationPolicy: "alert_human" };
43
+ case "stale_branch": return { scenario, steps: ["rebase_branch", "clean_build"], maxAttempts: 1, escalationPolicy: "alert_human" };
44
+ case "compile_red_cross_crate": return { scenario, steps: ["clean_build"], maxAttempts: 1, escalationPolicy: "alert_human" };
45
+ case "mcp_handshake_failure": return { scenario, steps: ["retry_mcp_handshake"], maxAttempts: 1, escalationPolicy: "abort" };
46
+ case "partial_plugin_startup": return { scenario, steps: ["restart_plugin", "retry_mcp_handshake"], maxAttempts: 1, escalationPolicy: "log_and_continue" };
47
+ case "worker_stale": return { scenario, steps: ["restart_worker"], maxAttempts: 1, escalationPolicy: "alert_human" };
48
+ case "green_unsatisfied": return { scenario, steps: ["collect_verification_evidence"], maxAttempts: 1, escalationPolicy: "alert_human" };
49
+ case "task_failed": return { scenario, steps: ["rerun_task"], maxAttempts: 1, escalationPolicy: "alert_human" };
50
+ case "provider_failure": return { scenario, steps: ["restart_worker"], maxAttempts: 1, escalationPolicy: "alert_human" };
51
+ }
52
+ }
53
+
54
+ export function buildRecoveryLedger(decisions: PolicyDecision[], previous: RecoveryLedger = { entries: [] }): RecoveryLedger {
55
+ const entries = [...previous.entries];
56
+ for (const item of decisions) {
57
+ if (!["retry", "escalate", "block"].includes(item.action)) continue;
58
+ const scenario = scenarioForPolicyReason(item.reason);
59
+ const recipe = recipeFor(scenario);
60
+ const priorAttempts = entries.filter((entry) => entry.scenario === scenario && entry.taskId === item.taskId).length;
61
+ const attempt = priorAttempts + 1;
62
+ entries.push({
63
+ scenario,
64
+ taskId: item.taskId,
65
+ decisionReason: item.reason,
66
+ attempt,
67
+ state: attempt <= recipe.maxAttempts && item.action !== "block" ? "planned" : "escalation_required",
68
+ steps: attempt <= recipe.maxAttempts ? recipe.steps : ["escalate_to_human"],
69
+ message: item.message,
70
+ createdAt: new Date().toISOString(),
71
+ });
72
+ }
73
+ return { entries };
74
+ }
@@ -0,0 +1,28 @@
1
+ export type RolePermissionMode = "read_only" | "workspace_write" | "danger_full_access" | "explicit_confirm";
2
+
3
+ const READ_ONLY_ROLES = new Set(["explorer", "reviewer", "security-reviewer", "verifier", "analyst", "critic", "planner", "writer"]);
4
+ const WRITE_ROLES = new Set(["executor", "test-engineer"]);
5
+ const READ_ONLY_COMMANDS = new Set(["cat", "head", "tail", "less", "more", "wc", "ls", "find", "grep", "rg", "awk", "sed", "echo", "printf", "which", "where", "whoami", "pwd", "env", "printenv", "date", "df", "du", "uname", "file", "stat", "diff", "sort", "uniq", "tr", "cut", "paste", "test", "true", "false", "type", "readlink", "realpath", "basename", "dirname", "sha256sum", "md5sum", "xxd", "hexdump", "od", "strings", "tree", "jq", "git", "gh"]);
6
+
7
+ export interface PermissionCheckResult {
8
+ allowed: boolean;
9
+ mode: RolePermissionMode;
10
+ reason?: string;
11
+ }
12
+
13
+ export function permissionForRole(role: string): RolePermissionMode {
14
+ if (READ_ONLY_ROLES.has(role)) return "read_only";
15
+ if (WRITE_ROLES.has(role)) return "workspace_write";
16
+ return "workspace_write";
17
+ }
18
+
19
+ export function isReadOnlyCommand(command: string): boolean {
20
+ const first = command.trim().split(/\s+/)[0]?.split(/[\\/]/).pop() ?? "";
21
+ return READ_ONLY_COMMANDS.has(first) && !/\s(-i|--in-place)\b|\s>{1,2}\s|\brm\b|\bmv\b|\bcp\b|\bnpm\s+install\b|\bgit\s+(commit|push|merge|rebase|reset|checkout)\b/.test(command);
22
+ }
23
+
24
+ export function checkRolePermission(role: string, command: string): PermissionCheckResult {
25
+ const mode = permissionForRole(role);
26
+ if (mode === "read_only" && !isReadOnlyCommand(command)) return { allowed: false, mode, reason: `Role '${role}' is read-only and command may modify state.` };
27
+ return { allowed: true, mode };
28
+ }
@@ -0,0 +1,75 @@
1
+ import type { PiTeamsConfig } from "../config/config.ts";
2
+ import type { CrewRuntimeKind } from "./crew-agent-runtime.ts";
3
+
4
+ export type CrewRuntimeMode = "auto" | "scaffold" | "child-process" | "live-session";
5
+
6
+ export interface CrewRuntimeCapabilities {
7
+ kind: CrewRuntimeKind;
8
+ requestedMode: CrewRuntimeMode;
9
+ available: boolean;
10
+ fallback?: CrewRuntimeKind;
11
+ steer: boolean;
12
+ resume: boolean;
13
+ liveToolActivity: boolean;
14
+ transcript: boolean;
15
+ reason?: string;
16
+ }
17
+
18
+ export async function isLiveSessionRuntimeAvailable(timeoutMs = 1500, env: NodeJS.ProcessEnv = process.env): Promise<{ available: boolean; reason?: string }> {
19
+ if (env.PI_CREW_ENABLE_EXPERIMENTAL_LIVE_SESSION !== "1") {
20
+ return { available: false, reason: "Live-session runtime adapter is experimental and disabled. Set PI_CREW_ENABLE_EXPERIMENTAL_LIVE_SESSION=1 to probe SDK support." };
21
+ }
22
+ const probe = async (): Promise<{ available: boolean; reason?: string }> => {
23
+ try {
24
+ const mod = await import("@mariozechner/pi-coding-agent");
25
+ const api = mod as Record<string, unknown>;
26
+ const required = ["createAgentSession", "DefaultResourceLoader", "SessionManager", "SettingsManager"];
27
+ const missing = required.filter((name) => typeof api[name] === "undefined");
28
+ if (missing.length) return { available: false, reason: `Pi SDK live-session exports missing: ${missing.join(", ")}.` };
29
+ return { available: true };
30
+ } catch (error) {
31
+ const message = error instanceof Error ? error.message : String(error);
32
+ return { available: false, reason: `Could not load optional Pi SDK live-session runtime: ${message}` };
33
+ }
34
+ };
35
+ let timer: NodeJS.Timeout | undefined;
36
+ try {
37
+ return await Promise.race([
38
+ probe(),
39
+ new Promise<{ available: boolean; reason: string }>((resolve) => {
40
+ timer = setTimeout(() => resolve({ available: false, reason: `Timed out probing optional Pi SDK live-session runtime after ${timeoutMs}ms.` }), timeoutMs);
41
+ timer.unref?.();
42
+ }),
43
+ ]);
44
+ } finally {
45
+ if (timer) clearTimeout(timer);
46
+ }
47
+ }
48
+
49
+ export async function resolveCrewRuntime(config: PiTeamsConfig, env: NodeJS.ProcessEnv = process.env): Promise<CrewRuntimeCapabilities> {
50
+ const requestedMode = config.runtime?.mode ?? "auto";
51
+ const executeWorkers = config.executeWorkers === true || env.PI_TEAMS_EXECUTE_WORKERS === "1";
52
+ if (requestedMode === "scaffold") return scaffoldCaps(requestedMode);
53
+ if (requestedMode === "child-process") return childCaps(requestedMode, executeWorkers ? undefined : "child-process requested but executeWorkers is not enabled; caller should refuse or fall back explicitly.");
54
+ if (requestedMode === "live-session" || (requestedMode === "auto" && config.runtime?.preferLiveSession === true)) {
55
+ const live = await isLiveSessionRuntimeAvailable(1500, env);
56
+ if (live.available) return liveCaps(requestedMode);
57
+ if (requestedMode === "live-session" && config.runtime?.allowChildProcessFallback === false) return { ...scaffoldCaps(requestedMode), available: false, reason: live.reason };
58
+ if (executeWorkers) return { ...childCaps(requestedMode), fallback: "child-process", reason: live.reason };
59
+ return { ...scaffoldCaps(requestedMode), fallback: "scaffold", reason: live.reason };
60
+ }
61
+ if (executeWorkers) return childCaps(requestedMode);
62
+ return scaffoldCaps(requestedMode);
63
+ }
64
+
65
+ function scaffoldCaps(requestedMode: CrewRuntimeMode): CrewRuntimeCapabilities {
66
+ return { kind: "scaffold", requestedMode, available: true, steer: false, resume: false, liveToolActivity: false, transcript: false };
67
+ }
68
+
69
+ function childCaps(requestedMode: CrewRuntimeMode, reason?: string): CrewRuntimeCapabilities {
70
+ return { kind: "child-process", requestedMode, available: true, steer: false, resume: false, liveToolActivity: false, transcript: true, ...(reason ? { reason } : {}) };
71
+ }
72
+
73
+ function liveCaps(requestedMode: CrewRuntimeMode): CrewRuntimeCapabilities {
74
+ return { kind: "live-session", requestedMode, available: true, steer: true, resume: true, liveToolActivity: true, transcript: true };
75
+ }
@@ -0,0 +1,79 @@
1
+ import * as fs from "node:fs";
2
+ import type { UsageState } from "../state/types.ts";
3
+
4
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
5
+ return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
6
+ }
7
+
8
+ function numberField(obj: Record<string, unknown>, keys: string[]): number | undefined {
9
+ for (const key of keys) {
10
+ const value = obj[key];
11
+ if (typeof value === "number" && Number.isFinite(value)) return value;
12
+ }
13
+ return undefined;
14
+ }
15
+
16
+ function usageFromValue(value: unknown): UsageState | undefined {
17
+ const obj = asRecord(value);
18
+ if (!obj) return undefined;
19
+ const direct: UsageState = {
20
+ input: numberField(obj, ["input", "inputTokens", "input_tokens"]),
21
+ output: numberField(obj, ["output", "outputTokens", "output_tokens"]),
22
+ cacheRead: numberField(obj, ["cacheRead", "cache_read", "cacheReadTokens", "cache_read_tokens"]),
23
+ cacheWrite: numberField(obj, ["cacheWrite", "cache_write", "cacheWriteTokens", "cache_write_tokens"]),
24
+ cost: numberField(obj, ["cost", "costUsd", "cost_usd"]),
25
+ turns: numberField(obj, ["turns", "turnCount", "turn_count"]),
26
+ };
27
+ if (Object.values(direct).some((entry) => entry !== undefined)) return direct;
28
+ for (const key of ["usage", "tokenUsage", "tokens", "stats"]) {
29
+ const nested = usageFromValue(obj[key]);
30
+ if (nested) return nested;
31
+ }
32
+ const message = asRecord(obj.message);
33
+ return message ? usageFromValue(message.usage) : undefined;
34
+ }
35
+
36
+ function addUsage(total: UsageState, usage: UsageState): UsageState {
37
+ return {
38
+ input: (total.input ?? 0) + (usage.input ?? 0),
39
+ output: (total.output ?? 0) + (usage.output ?? 0),
40
+ cacheRead: (total.cacheRead ?? 0) + (usage.cacheRead ?? 0),
41
+ cacheWrite: (total.cacheWrite ?? 0) + (usage.cacheWrite ?? 0),
42
+ cost: (total.cost ?? 0) + (usage.cost ?? 0),
43
+ turns: (total.turns ?? 0) + (usage.turns ?? 0),
44
+ };
45
+ }
46
+
47
+ function compactUsage(total: UsageState, foundKeys: Set<keyof UsageState>): UsageState | undefined {
48
+ if (foundKeys.size === 0) return undefined;
49
+ const compact: UsageState = {};
50
+ for (const key of foundKeys) compact[key] = total[key];
51
+ return compact;
52
+ }
53
+
54
+ export function parseSessionUsageFromJsonlText(text: string): UsageState | undefined {
55
+ let total: UsageState = {};
56
+ const foundKeys = new Set<keyof UsageState>();
57
+ for (const line of text.split(/\r?\n/)) {
58
+ const trimmed = line.trim();
59
+ if (!trimmed) continue;
60
+ try {
61
+ const usage = usageFromValue(JSON.parse(trimmed) as unknown);
62
+ if (!usage) continue;
63
+ for (const key of Object.keys(usage) as Array<keyof UsageState>) foundKeys.add(key);
64
+ total = addUsage(total, usage);
65
+ } catch {
66
+ // Session JSONL can contain partial/corrupt lines after interrupted workers.
67
+ }
68
+ }
69
+ return compactUsage(total, foundKeys);
70
+ }
71
+
72
+ export function parseSessionUsage(filePath: string): UsageState | undefined {
73
+ try {
74
+ if (!fs.existsSync(filePath)) return undefined;
75
+ return parseSessionUsageFromJsonlText(fs.readFileSync(filePath, "utf-8"));
76
+ } catch {
77
+ return undefined;
78
+ }
79
+ }