pi-crew 0.1.6 → 0.1.8

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.
@@ -52,17 +52,55 @@ export function readCrewAgentStatus(manifest: TeamRunManifest, taskOrAgentId: st
52
52
  return readJsonFile<CrewAgentRecord>(agentStatusPath(manifest, taskId));
53
53
  }
54
54
 
55
+ function nextAgentEventSeq(filePath: string): number {
56
+ if (!fs.existsSync(filePath)) return 1;
57
+ let max = 0;
58
+ for (const line of fs.readFileSync(filePath, "utf-8").split(/\r?\n/)) {
59
+ if (!line.trim()) continue;
60
+ try {
61
+ const parsed = JSON.parse(line) as { seq?: unknown };
62
+ if (typeof parsed.seq === "number" && Number.isFinite(parsed.seq)) max = Math.max(max, parsed.seq);
63
+ else max += 1;
64
+ } catch {
65
+ max += 1;
66
+ }
67
+ }
68
+ return max + 1;
69
+ }
70
+
55
71
  export function appendCrewAgentEvent(manifest: TeamRunManifest, taskId: string, event: unknown): void {
56
72
  fs.mkdirSync(agentStateDir(manifest, taskId), { recursive: true });
57
- fs.appendFileSync(agentEventsPath(manifest, taskId), `${JSON.stringify({ time: new Date().toISOString(), event })}\n`, "utf-8");
73
+ const filePath = agentEventsPath(manifest, taskId);
74
+ fs.appendFileSync(filePath, `${JSON.stringify({ seq: nextAgentEventSeq(filePath), time: new Date().toISOString(), event })}\n`, "utf-8");
75
+ }
76
+
77
+ export interface CrewAgentEventCursorOptions {
78
+ sinceSeq?: number;
79
+ limit?: number;
58
80
  }
59
81
 
60
82
  export function readCrewAgentEvents(manifest: TeamRunManifest, taskId: string): unknown[] {
83
+ return readCrewAgentEventsCursor(manifest, taskId).events;
84
+ }
85
+
86
+ export function readCrewAgentEventsCursor(manifest: TeamRunManifest, taskId: string, options: CrewAgentEventCursorOptions = {}): { path: string; events: unknown[]; nextSeq: number; total: number } {
61
87
  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 }; }
88
+ if (!fs.existsSync(filePath)) return { path: filePath, events: [], nextSeq: options.sinceSeq ?? 0, total: 0 };
89
+ const sinceSeq = typeof options.sinceSeq === "number" && Number.isInteger(options.sinceSeq) && options.sinceSeq >= 0 ? options.sinceSeq : 0;
90
+ const limit = typeof options.limit === "number" && Number.isInteger(options.limit) && options.limit >= 0 ? options.limit : undefined;
91
+ const parsed = fs.readFileSync(filePath, "utf-8").split(/\r?\n/).filter(Boolean).map((line, index) => {
92
+ try {
93
+ const event = JSON.parse(line) as Record<string, unknown>;
94
+ if (typeof event.seq !== "number") event.seq = index + 1;
95
+ return event;
96
+ } catch {
97
+ return { seq: index + 1, raw: line };
98
+ }
65
99
  });
100
+ const filtered = parsed.filter((event) => typeof event.seq === "number" && event.seq > sinceSeq);
101
+ const events = limit !== undefined ? filtered.slice(0, limit) : filtered;
102
+ const returnedMaxSeq = events.reduce((max, event) => typeof event.seq === "number" ? Math.max(max, event.seq) : max, sinceSeq);
103
+ return { path: filePath, events, nextSeq: returnedMaxSeq, total: filtered.length };
66
104
  }
67
105
 
68
106
  export function appendCrewAgentOutput(manifest: TeamRunManifest, taskId: string, text: string): void {
@@ -21,6 +21,7 @@ export interface CrewAgentProgress {
21
21
  durationMs?: number;
22
22
  lastActivityAt?: string;
23
23
  activityState?: "active" | "needs_attention" | "stale";
24
+ failedTool?: string;
24
25
  }
25
26
 
26
27
  export interface CrewAgentRecord {
@@ -0,0 +1,82 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { appendEvent } from "../state/event-log.ts";
4
+ import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
5
+ import { checkProcessLiveness, isActiveRunStatus } from "./process-status.ts";
6
+ import { readCrewAgents } from "./crew-agent-records.ts";
7
+
8
+ export type ForegroundControlRequestType = "interrupt" | "status";
9
+
10
+ export interface ForegroundControlStatus {
11
+ runId: string;
12
+ status: TeamRunManifest["status"];
13
+ active: boolean;
14
+ asyncPid?: number;
15
+ asyncAlive?: boolean;
16
+ runningTasks: string[];
17
+ runningAgents: string[];
18
+ controlPath: string;
19
+ lastRequest?: ForegroundControlRequest;
20
+ }
21
+
22
+ export interface ForegroundControlRequest {
23
+ id: string;
24
+ type: ForegroundControlRequestType;
25
+ createdAt: string;
26
+ reason: string;
27
+ acknowledged: boolean;
28
+ }
29
+
30
+ export function foregroundControlPath(manifest: TeamRunManifest): string {
31
+ return path.join(manifest.stateRoot, "foreground-control.json");
32
+ }
33
+
34
+ function readLastRequest(controlPath: string): ForegroundControlRequest | undefined {
35
+ if (!fs.existsSync(controlPath)) return undefined;
36
+ try {
37
+ const parsed = JSON.parse(fs.readFileSync(controlPath, "utf-8")) as { requests?: ForegroundControlRequest[] };
38
+ return parsed.requests?.at(-1);
39
+ } catch {
40
+ return undefined;
41
+ }
42
+ }
43
+
44
+ export function readForegroundControlStatus(manifest: TeamRunManifest, tasks: TeamTaskState[]): ForegroundControlStatus {
45
+ const controlPath = foregroundControlPath(manifest);
46
+ const asyncAlive = manifest.async?.pid !== undefined ? checkProcessLiveness(manifest.async.pid).alive : undefined;
47
+ return {
48
+ runId: manifest.runId,
49
+ status: manifest.status,
50
+ active: isActiveRunStatus(manifest.status),
51
+ asyncPid: manifest.async?.pid,
52
+ asyncAlive,
53
+ runningTasks: tasks.filter((task) => task.status === "running").map((task) => task.id),
54
+ runningAgents: readCrewAgents(manifest).filter((agent) => agent.status === "running").map((agent) => agent.id),
55
+ controlPath,
56
+ lastRequest: readLastRequest(controlPath),
57
+ };
58
+ }
59
+
60
+ export function writeForegroundInterruptRequest(manifest: TeamRunManifest, reason = "User requested foreground interrupt."): ForegroundControlRequest {
61
+ const controlPath = foregroundControlPath(manifest);
62
+ let requests: ForegroundControlRequest[] = [];
63
+ if (fs.existsSync(controlPath)) {
64
+ try {
65
+ const parsed = JSON.parse(fs.readFileSync(controlPath, "utf-8")) as { requests?: ForegroundControlRequest[] };
66
+ requests = Array.isArray(parsed.requests) ? parsed.requests : [];
67
+ } catch {
68
+ requests = [];
69
+ }
70
+ }
71
+ const request: ForegroundControlRequest = {
72
+ id: `fg_${Date.now().toString(36)}_${Math.random().toString(16).slice(2, 10)}`,
73
+ type: "interrupt",
74
+ createdAt: new Date().toISOString(),
75
+ reason,
76
+ acknowledged: false,
77
+ };
78
+ fs.mkdirSync(path.dirname(controlPath), { recursive: true });
79
+ fs.writeFileSync(controlPath, `${JSON.stringify({ requests: [...requests, request] }, null, 2)}\n`, "utf-8");
80
+ appendEvent(manifest.eventsPath, { type: "foreground.interrupt_requested", runId: manifest.runId, message: reason, data: { requestId: request.id, controlPath } });
81
+ return request;
82
+ }
@@ -0,0 +1,78 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { TeamRunManifest } from "../state/types.ts";
4
+
5
+ export type LiveAgentControlOperation = "steer" | "stop" | "resume";
6
+
7
+ export interface LiveAgentControlRequest {
8
+ id: string;
9
+ runId: string;
10
+ taskId: string;
11
+ agentId?: string;
12
+ operation: LiveAgentControlOperation;
13
+ message?: string;
14
+ createdAt: string;
15
+ processedAt?: string;
16
+ error?: string;
17
+ }
18
+
19
+ export interface LiveAgentControlCursor {
20
+ offset: number;
21
+ }
22
+
23
+ export function liveAgentControlPath(manifest: TeamRunManifest, taskId: string): string {
24
+ return path.join(manifest.stateRoot, "agents", taskId, "live-control.jsonl");
25
+ }
26
+
27
+ function requestId(): string {
28
+ return `ctrl_${Date.now().toString(36)}_${Math.random().toString(16).slice(2, 10)}`;
29
+ }
30
+
31
+ export function appendLiveAgentControlRequest(manifest: TeamRunManifest, input: { taskId: string; agentId?: string; operation: LiveAgentControlOperation; message?: string }): LiveAgentControlRequest {
32
+ const request: LiveAgentControlRequest = {
33
+ id: requestId(),
34
+ runId: manifest.runId,
35
+ taskId: input.taskId,
36
+ agentId: input.agentId,
37
+ operation: input.operation,
38
+ message: input.message,
39
+ createdAt: new Date().toISOString(),
40
+ };
41
+ const filePath = liveAgentControlPath(manifest, input.taskId);
42
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
43
+ fs.appendFileSync(filePath, `${JSON.stringify(request)}\n`, "utf-8");
44
+ return request;
45
+ }
46
+
47
+ export function readLiveAgentControlRequests(manifest: TeamRunManifest, taskId: string, cursor: LiveAgentControlCursor = { offset: 0 }): { requests: LiveAgentControlRequest[]; cursor: LiveAgentControlCursor } {
48
+ const filePath = liveAgentControlPath(manifest, taskId);
49
+ if (!fs.existsSync(filePath)) return { requests: [], cursor };
50
+ const text = fs.readFileSync(filePath, "utf-8");
51
+ const lines = text.split(/\r?\n/).filter(Boolean);
52
+ const requests = lines.slice(cursor.offset).flatMap((line) => {
53
+ try {
54
+ const parsed = JSON.parse(line) as LiveAgentControlRequest;
55
+ return parsed && parsed.runId === manifest.runId && parsed.taskId === taskId ? [parsed] : [];
56
+ } catch {
57
+ return [];
58
+ }
59
+ });
60
+ return { requests, cursor: { offset: lines.length } };
61
+ }
62
+
63
+ export async function applyLiveAgentControlRequest(input: { request: LiveAgentControlRequest; taskId: string; agentId: string; session: { steer?: (text: string) => Promise<void>; prompt?: (text: string, options?: Record<string, unknown>) => Promise<void>; abort?: () => Promise<void> | void }; seenRequestIds?: Set<string> }): Promise<boolean> {
64
+ const { request, taskId, agentId, session, seenRequestIds } = input;
65
+ if (seenRequestIds?.has(request.id)) return false;
66
+ if (request.agentId && request.agentId !== agentId && request.agentId !== taskId) return false;
67
+ seenRequestIds?.add(request.id);
68
+ if (request.operation === "steer") await session.steer?.(request.message ?? "Please report current status and wrap up if possible.");
69
+ else if (request.operation === "resume") await session.prompt?.(request.message ?? "Please resume and report final status.", { source: "api", expandPromptTemplates: false });
70
+ else if (request.operation === "stop") await session.abort?.();
71
+ return true;
72
+ }
73
+
74
+ export async function applyLiveAgentControlRequests(input: { manifest: TeamRunManifest; taskId: string; agentId: string; session: { steer?: (text: string) => Promise<void>; prompt?: (text: string, options?: Record<string, unknown>) => Promise<void>; abort?: () => Promise<void> | void }; cursor: LiveAgentControlCursor; seenRequestIds?: Set<string> }): Promise<LiveAgentControlCursor> {
75
+ const batch = readLiveAgentControlRequests(input.manifest, input.taskId, input.cursor);
76
+ for (const request of batch.requests) await applyLiveAgentControlRequest({ request, taskId: input.taskId, agentId: input.agentId, session: input.session, seenRequestIds: input.seenRequestIds });
77
+ return batch.cursor;
78
+ }
@@ -0,0 +1,85 @@
1
+ import type { CrewAgentRecord } from "./crew-agent-runtime.ts";
2
+
3
+ type LiveSessionHandle = {
4
+ steer?: (text: string) => Promise<void>;
5
+ prompt?: (text: string, options?: Record<string, unknown>) => Promise<void>;
6
+ abort?: () => Promise<void> | void;
7
+ };
8
+
9
+ export interface LiveAgentHandle {
10
+ agentId: string;
11
+ taskId: string;
12
+ runId: string;
13
+ session: LiveSessionHandle;
14
+ createdAt: string;
15
+ updatedAt: string;
16
+ status: CrewAgentRecord["status"];
17
+ pendingSteers: string[];
18
+ }
19
+
20
+ const liveAgents = new Map<string, LiveAgentHandle>();
21
+
22
+ export function registerLiveAgent(input: Omit<LiveAgentHandle, "createdAt" | "updatedAt" | "pendingSteers">): LiveAgentHandle {
23
+ const now = new Date().toISOString();
24
+ const existing = liveAgents.get(input.agentId);
25
+ const handle: LiveAgentHandle = { ...input, createdAt: existing?.createdAt ?? now, updatedAt: now, pendingSteers: existing?.pendingSteers ?? [] };
26
+ liveAgents.set(input.agentId, handle);
27
+ if (handle.pendingSteers.length && typeof handle.session.steer === "function") {
28
+ const pending = [...handle.pendingSteers];
29
+ handle.pendingSteers.length = 0;
30
+ for (const message of pending) void handle.session.steer(message).catch(() => {});
31
+ }
32
+ return handle;
33
+ }
34
+
35
+ export function updateLiveAgentStatus(agentId: string, status: CrewAgentRecord["status"]): void {
36
+ const handle = liveAgents.get(agentId);
37
+ if (!handle) return;
38
+ handle.status = status;
39
+ handle.updatedAt = new Date().toISOString();
40
+ }
41
+
42
+ export function getLiveAgent(agentIdOrTaskId: string): LiveAgentHandle | undefined {
43
+ return liveAgents.get(agentIdOrTaskId) ?? [...liveAgents.values()].find((entry) => entry.taskId === agentIdOrTaskId);
44
+ }
45
+
46
+ export function listLiveAgents(): LiveAgentHandle[] {
47
+ return [...liveAgents.values()].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
48
+ }
49
+
50
+ export async function steerLiveAgent(agentIdOrTaskId: string, message: string): Promise<LiveAgentHandle> {
51
+ const handle = getLiveAgent(agentIdOrTaskId);
52
+ if (!handle) throw new Error(`Live agent '${agentIdOrTaskId}' is not registered in this process.`);
53
+ if (typeof handle.session.steer !== "function") {
54
+ handle.pendingSteers.push(message);
55
+ return handle;
56
+ }
57
+ await handle.session.steer(message);
58
+ handle.updatedAt = new Date().toISOString();
59
+ return handle;
60
+ }
61
+
62
+ export async function stopLiveAgent(agentIdOrTaskId: string): Promise<LiveAgentHandle> {
63
+ const handle = getLiveAgent(agentIdOrTaskId);
64
+ if (!handle) throw new Error(`Live agent '${agentIdOrTaskId}' is not registered in this process.`);
65
+ if (typeof handle.session.abort !== "function") throw new Error(`Live agent '${agentIdOrTaskId}' does not expose abort().`);
66
+ await handle.session.abort();
67
+ handle.status = "stopped";
68
+ handle.updatedAt = new Date().toISOString();
69
+ return handle;
70
+ }
71
+
72
+ export async function resumeLiveAgent(agentIdOrTaskId: string, prompt: string): Promise<LiveAgentHandle> {
73
+ const handle = getLiveAgent(agentIdOrTaskId);
74
+ if (!handle) throw new Error(`Live agent '${agentIdOrTaskId}' is not registered in this process.`);
75
+ if (typeof handle.session.prompt !== "function") throw new Error(`Live agent '${agentIdOrTaskId}' does not expose prompt().`);
76
+ handle.status = "running";
77
+ await handle.session.prompt(prompt, { source: "api", expandPromptTemplates: false });
78
+ handle.status = "completed";
79
+ handle.updatedAt = new Date().toISOString();
80
+ return handle;
81
+ }
82
+
83
+ export function clearLiveAgentsForTest(): void {
84
+ liveAgents.clear();
85
+ }
@@ -0,0 +1,36 @@
1
+ import type { LiveAgentControlRequest } from "./live-agent-control.ts";
2
+
3
+ export interface LiveControlRealtimeMessage {
4
+ type: "live-control";
5
+ version: 1;
6
+ request: LiveAgentControlRequest;
7
+ }
8
+
9
+ type Listener = (request: LiveAgentControlRequest) => void | Promise<void>;
10
+
11
+ const listeners = new Set<Listener>();
12
+
13
+ export function publishLiveControlRealtime(request: LiveAgentControlRequest): void {
14
+ for (const listener of [...listeners]) void listener(request);
15
+ }
16
+
17
+ export function subscribeLiveControlRealtime(listener: Listener): () => void {
18
+ listeners.add(listener);
19
+ return () => listeners.delete(listener);
20
+ }
21
+
22
+ export function liveControlRealtimeMessage(request: LiveAgentControlRequest): LiveControlRealtimeMessage {
23
+ return { type: "live-control", version: 1, request };
24
+ }
25
+
26
+ export function parseLiveControlRealtimeMessage(raw: unknown): LiveAgentControlRequest | undefined {
27
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
28
+ const message = raw as { type?: unknown; version?: unknown; request?: unknown };
29
+ if (message.type !== "live-control" || message.version !== 1 || !message.request || typeof message.request !== "object" || Array.isArray(message.request)) return undefined;
30
+ const request = message.request as Partial<LiveAgentControlRequest>;
31
+ return typeof request.id === "string" && typeof request.runId === "string" && typeof request.taskId === "string" && (request.operation === "steer" || request.operation === "stop" || request.operation === "resume") && typeof request.createdAt === "string" ? request as LiveAgentControlRequest : undefined;
32
+ }
33
+
34
+ export function clearLiveControlRealtimeForTest(): void {
35
+ listeners.clear();
36
+ }
@@ -1,5 +1,13 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
1
3
  import type { AgentConfig } from "../agents/agent-config.ts";
2
- import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
4
+ import type { CrewRuntimeConfig } from "../config/config.ts";
5
+ import type { TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
6
+ import { buildMemoryBlock } from "./agent-memory.ts";
7
+ import { registerLiveAgent, updateLiveAgentStatus } from "./live-agent-manager.ts";
8
+ import { applyLiveAgentControlRequest, applyLiveAgentControlRequests, type LiveAgentControlCursor } from "./live-agent-control.ts";
9
+ import { subscribeLiveControlRealtime } from "./live-control-realtime.ts";
10
+ import { eventToSidechainType, sidechainOutputPath, writeSidechainEntry } from "./sidechain-output.ts";
3
11
  import type { WorkflowStep } from "../workflows/workflow-config.ts";
4
12
  import { isLiveSessionRuntimeAvailable } from "./runtime-resolver.ts";
5
13
 
@@ -9,6 +17,24 @@ export interface LiveSessionSpawnInput {
9
17
  step: WorkflowStep;
10
18
  agent: AgentConfig;
11
19
  prompt: string;
20
+ signal?: AbortSignal;
21
+ transcriptPath?: string;
22
+ onEvent?: (event: unknown) => void;
23
+ onOutput?: (text: string) => void;
24
+ runtimeConfig?: CrewRuntimeConfig;
25
+ parentContext?: string;
26
+ parentModel?: unknown;
27
+ modelRegistry?: unknown;
28
+ }
29
+
30
+ export interface LiveSessionRunResult {
31
+ available: true;
32
+ exitCode: number | null;
33
+ stdout: string;
34
+ stderr: string;
35
+ jsonEvents: number;
36
+ usage?: UsageState;
37
+ error?: string;
12
38
  }
13
39
 
14
40
  export interface LiveSessionUnavailableResult {
@@ -21,13 +47,253 @@ export interface LiveSessionPlannedResult {
21
47
  reason: string;
22
48
  }
23
49
 
50
+ type LiveSessionModule = Record<string, unknown> & {
51
+ createAgentSession?: (options?: Record<string, unknown>) => Promise<{ session: LiveSessionLike; modelFallbackMessage?: string }>;
52
+ DefaultResourceLoader?: new (options: Record<string, unknown>) => { reload?: () => Promise<void> };
53
+ SessionManager?: { inMemory?: (cwd?: string) => unknown; create?: (cwd?: string, sessionDir?: string) => unknown };
54
+ SettingsManager?: { create?: (cwd?: string, agentDir?: string) => unknown };
55
+ getAgentDir?: () => string;
56
+ };
57
+
58
+ type LiveSessionLike = {
59
+ subscribe?: (listener: (event: unknown) => void) => (() => void);
60
+ prompt?: (text: string, options?: Record<string, unknown>) => Promise<void>;
61
+ steer?: (text: string) => Promise<void>;
62
+ abort?: () => Promise<void> | void;
63
+ getStats?: () => unknown;
64
+ stats?: unknown;
65
+ bindExtensions?: (bindings?: Record<string, unknown>) => Promise<void>;
66
+ getActiveToolNames?: () => string[];
67
+ setActiveToolsByName?: (names: string[]) => void;
68
+ };
69
+
70
+ function appendTranscript(filePath: string | undefined, event: unknown): void {
71
+ if (!filePath) return;
72
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
73
+ fs.appendFileSync(filePath, `${JSON.stringify(event)}\n`, "utf-8");
74
+ }
75
+
76
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
77
+ return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
78
+ }
79
+
80
+ function textFromContent(content: unknown): string[] {
81
+ if (typeof content === "string") return [content];
82
+ if (!Array.isArray(content)) return [];
83
+ return content.flatMap((part) => {
84
+ const obj = asRecord(part);
85
+ if (!obj) return [];
86
+ if (obj.type === "text" && typeof obj.text === "string") return [obj.text];
87
+ if (typeof obj.content === "string") return [obj.content];
88
+ return [];
89
+ });
90
+ }
91
+
92
+ function eventText(event: unknown): string[] {
93
+ const obj = asRecord(event);
94
+ if (!obj) return [];
95
+ const text: string[] = [];
96
+ if (typeof obj.text === "string") text.push(obj.text);
97
+ text.push(...textFromContent(obj.content));
98
+ const message = asRecord(obj.message);
99
+ if (message) text.push(...textFromContent(message.content));
100
+ return text.filter((entry) => entry.trim());
101
+ }
102
+
103
+ function finalAssistantText(event: unknown): string[] {
104
+ const obj = asRecord(event);
105
+ if (!obj || obj.type !== "message_end") return [];
106
+ const message = asRecord(obj.message);
107
+ if (message?.role !== "assistant") return [];
108
+ return textFromContent(message.content);
109
+ }
110
+
111
+ function numberField(obj: Record<string, unknown> | undefined, keys: string[]): number | undefined {
112
+ if (!obj) return undefined;
113
+ for (const key of keys) {
114
+ const value = obj[key];
115
+ if (typeof value === "number" && Number.isFinite(value)) return value;
116
+ }
117
+ return undefined;
118
+ }
119
+
120
+ function modelFromRegistry(modelRegistry: unknown, modelId: string | undefined): unknown {
121
+ if (!modelId || !modelId.includes("/")) return undefined;
122
+ const registry = asRecord(modelRegistry);
123
+ const find = registry?.find;
124
+ if (typeof find !== "function") return undefined;
125
+ const [provider, ...modelParts] = modelId.split("/");
126
+ const id = modelParts.join("/");
127
+ try {
128
+ return find.call(modelRegistry, provider, id);
129
+ } catch {
130
+ return undefined;
131
+ }
132
+ }
133
+
134
+ function liveSystemPrompt(input: LiveSessionSpawnInput): string {
135
+ const memory = input.agent.memory ? buildMemoryBlock(input.agent.name, input.agent.memory, input.task.cwd, Boolean(input.agent.tools?.some((tool) => tool === "write" || tool === "edit"))) : "";
136
+ return [
137
+ "# pi-crew Live Subagent",
138
+ `Run ID: ${input.manifest.runId}`,
139
+ `Task ID: ${input.task.id}`,
140
+ `Role: ${input.task.role}`,
141
+ `Agent: ${input.agent.name}`,
142
+ `Working directory: ${input.task.cwd}`,
143
+ "",
144
+ input.agent.systemPrompt || "Follow the user task exactly and report verification evidence.",
145
+ memory ? `\n${memory}` : "",
146
+ ].filter(Boolean).join("\n");
147
+ }
148
+
149
+ function filterActiveTools(session: LiveSessionLike, agent: AgentConfig): void {
150
+ if (typeof session.getActiveToolNames !== "function" || typeof session.setActiveToolsByName !== "function") return;
151
+ const recursiveTools = new Set(["team", "Team", "Agent", "get_subagent_result", "steer_subagent"]);
152
+ const allowed = agent.tools?.length ? new Set(agent.tools) : undefined;
153
+ const active = session.getActiveToolNames().filter((name) => !recursiveTools.has(name) && (!allowed || allowed.has(name)));
154
+ session.setActiveToolsByName(active);
155
+ }
156
+
157
+ function usageFromStats(stats: unknown): UsageState | undefined {
158
+ const obj = asRecord(stats);
159
+ if (!obj) return undefined;
160
+ const input = numberField(obj, ["input", "inputTokens", "input_tokens"]);
161
+ const output = numberField(obj, ["output", "outputTokens", "output_tokens"]);
162
+ const cacheRead = numberField(obj, ["cacheRead", "cache_read"]);
163
+ const cacheWrite = numberField(obj, ["cacheWrite", "cache_write"]);
164
+ const cost = numberField(obj, ["cost"]);
165
+ const turns = numberField(obj, ["turns", "turnCount", "turn_count"]);
166
+ return [input, output, cacheRead, cacheWrite, cost, turns].some((value) => value !== undefined) ? { input, output, cacheRead, cacheWrite, cost, turns } : undefined;
167
+ }
168
+
24
169
  export async function probeLiveSessionRuntime(): Promise<LiveSessionUnavailableResult | LiveSessionPlannedResult> {
25
170
  const availability = await isLiveSessionRuntimeAvailable();
26
171
  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." };
172
+ return { available: true, reason: "Live-session SDK exports are available and pi-crew can run experimental in-process live agents when runtime.mode=live-session." };
28
173
  }
29
174
 
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);
175
+ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<LiveSessionRunResult> {
176
+ if (process.env.PI_CREW_MOCK_LIVE_SESSION === "success") {
177
+ const agentId = `${input.manifest.runId}:${input.task.id}`;
178
+ const inherited = input.runtimeConfig?.inheritContext === true && input.parentContext ? ` with inherited context: ${input.parentContext}` : "";
179
+ const event = { type: "message_end", message: { role: "assistant", content: [{ type: "text", text: `Mock live-session success for ${input.agent.name}${inherited}` }] } };
180
+ const mockSession = { steer: async () => {}, prompt: async () => {}, abort: async () => {} };
181
+ registerLiveAgent({ agentId, runId: input.manifest.runId, taskId: input.task.id, session: mockSession, status: "running" });
182
+ appendTranscript(input.transcriptPath, event);
183
+ const sidechainPath = sidechainOutputPath(input.manifest.stateRoot, input.task.id);
184
+ writeSidechainEntry(sidechainPath, { agentId, type: "user", message: { role: "user", content: input.prompt }, cwd: input.task.cwd });
185
+ writeSidechainEntry(sidechainPath, { agentId, type: "message", message: event, cwd: input.task.cwd });
186
+ input.onEvent?.(event);
187
+ const stdout = `Mock live-session success for ${input.agent.name}${inherited}`;
188
+ input.onOutput?.(stdout);
189
+ updateLiveAgentStatus(agentId, "completed");
190
+ return { available: true, exitCode: 0, stdout, stderr: "", jsonEvents: 1 };
191
+ }
192
+ const availability = await isLiveSessionRuntimeAvailable();
193
+ if (!availability.available) return { available: true, exitCode: 1, stdout: "", stderr: availability.reason ?? "Live-session runtime unavailable.", jsonEvents: 0, error: availability.reason };
194
+ const mod = await import("@mariozechner/pi-coding-agent") as LiveSessionModule;
195
+ if (typeof mod.createAgentSession !== "function") return { available: true, exitCode: 1, stdout: "", stderr: "createAgentSession export is unavailable.", jsonEvents: 0, error: "createAgentSession export is unavailable." };
196
+ let session: LiveSessionLike | undefined;
197
+ let unsubscribe: (() => void) | undefined;
198
+ let unsubscribeControlRealtime: (() => void) | undefined;
199
+ let controlTimer: ReturnType<typeof setInterval> | undefined;
200
+ let stdout = "";
201
+ let jsonEvents = 0;
202
+ try {
203
+ const agentDir = typeof mod.getAgentDir === "function" ? mod.getAgentDir() : undefined;
204
+ let resourceLoader: unknown;
205
+ if (mod.DefaultResourceLoader && agentDir) {
206
+ resourceLoader = new mod.DefaultResourceLoader({
207
+ cwd: input.task.cwd,
208
+ agentDir,
209
+ noPromptTemplates: true,
210
+ noThemes: true,
211
+ noContextFiles: input.runtimeConfig?.inheritContext !== true,
212
+ systemPromptOverride: () => liveSystemPrompt(input),
213
+ appendSystemPromptOverride: () => [],
214
+ });
215
+ await (resourceLoader as { reload?: () => Promise<void> }).reload?.();
216
+ }
217
+ const resolvedModel = modelFromRegistry(input.modelRegistry, input.agent.model) ?? input.parentModel;
218
+ const created = await mod.createAgentSession({
219
+ cwd: input.task.cwd,
220
+ ...(agentDir ? { agentDir } : {}),
221
+ ...(resourceLoader ? { resourceLoader } : {}),
222
+ ...(mod.SessionManager?.inMemory ? { sessionManager: mod.SessionManager.inMemory(input.task.cwd) } : {}),
223
+ ...(mod.SettingsManager?.create && agentDir ? { settingsManager: mod.SettingsManager.create(input.task.cwd, agentDir) } : {}),
224
+ ...(input.modelRegistry ? { modelRegistry: input.modelRegistry } : {}),
225
+ ...(resolvedModel ? { model: resolvedModel } : {}),
226
+ ...(input.agent.thinking ? { thinkingLevel: input.agent.thinking } : {}),
227
+ });
228
+ session = created.session;
229
+ filterActiveTools(session, input.agent);
230
+ await session.bindExtensions?.({});
231
+ const agentId = `${input.manifest.runId}:${input.task.id}`;
232
+ registerLiveAgent({ agentId, runId: input.manifest.runId, taskId: input.task.id, session, status: "running" });
233
+ let controlCursor: LiveAgentControlCursor = { offset: 0 };
234
+ const seenControlRequestIds = new Set<string>();
235
+ let controlBusy = false;
236
+ const pollControl = async () => {
237
+ if (controlBusy || !session) return;
238
+ controlBusy = true;
239
+ try {
240
+ controlCursor = await applyLiveAgentControlRequests({ manifest: input.manifest, taskId: input.task.id, agentId, session, cursor: controlCursor, seenRequestIds: seenControlRequestIds });
241
+ } finally {
242
+ controlBusy = false;
243
+ }
244
+ };
245
+ unsubscribeControlRealtime = subscribeLiveControlRealtime((request) => {
246
+ if (request.runId !== input.manifest.runId || request.taskId !== input.task.id || !session) return;
247
+ void applyLiveAgentControlRequest({ request, taskId: input.task.id, agentId, session, seenRequestIds: seenControlRequestIds });
248
+ });
249
+ await pollControl();
250
+ controlTimer = setInterval(() => { void pollControl(); }, 500);
251
+ let turnCount = 0;
252
+ let softLimitReached = false;
253
+ const maxTurns = input.runtimeConfig?.maxTurns;
254
+ const graceTurns = input.runtimeConfig?.graceTurns ?? 5;
255
+ const sidechainPath = sidechainOutputPath(input.manifest.stateRoot, input.task.id);
256
+ writeSidechainEntry(sidechainPath, { agentId, type: "user", message: { role: "user", content: input.prompt }, cwd: input.task.cwd });
257
+ if (typeof session.subscribe === "function") {
258
+ unsubscribe = session.subscribe((event) => {
259
+ jsonEvents += 1;
260
+ appendTranscript(input.transcriptPath, event);
261
+ const sidechainType = eventToSidechainType(event);
262
+ if (sidechainType) writeSidechainEntry(sidechainPath, { agentId, type: sidechainType, message: event, cwd: input.task.cwd });
263
+ const obj = asRecord(event);
264
+ if (obj?.type === "turn_end") {
265
+ turnCount += 1;
266
+ if (maxTurns !== undefined && !softLimitReached && turnCount >= maxTurns) {
267
+ softLimitReached = true;
268
+ void session?.steer?.("You have reached your turn limit. Wrap up immediately — provide your final answer now.");
269
+ } else if (maxTurns !== undefined && softLimitReached && turnCount >= maxTurns + graceTurns) {
270
+ void session?.abort?.();
271
+ }
272
+ }
273
+ input.onEvent?.(event);
274
+ const text = [...eventText(event), ...finalAssistantText(event)].join("\n");
275
+ if (text.trim()) {
276
+ stdout += `${text}\n`;
277
+ input.onOutput?.(text);
278
+ }
279
+ });
280
+ }
281
+ if (input.signal) {
282
+ if (input.signal.aborted) await session.abort?.();
283
+ else input.signal.addEventListener("abort", () => { void session?.abort?.(); }, { once: true });
284
+ }
285
+ const effectivePrompt = input.runtimeConfig?.inheritContext === true && input.parentContext ? `${input.parentContext}\n\n---\n# Live Subagent Task\n${input.prompt}` : input.prompt;
286
+ await session.prompt?.(effectivePrompt, { source: "api", expandPromptTemplates: false });
287
+ const usage = usageFromStats(typeof session.getStats === "function" ? session.getStats() : session.stats);
288
+ updateLiveAgentStatus(agentId, "completed");
289
+ return { available: true, exitCode: 0, stdout: stdout.trim(), stderr: created.modelFallbackMessage ?? "", jsonEvents, usage };
290
+ } catch (error) {
291
+ const message = error instanceof Error ? error.message : String(error);
292
+ updateLiveAgentStatus(`${input.manifest.runId}:${input.task.id}`, "failed");
293
+ return { available: true, exitCode: 1, stdout: stdout.trim(), stderr: message, jsonEvents, error: message };
294
+ } finally {
295
+ if (controlTimer) clearInterval(controlTimer);
296
+ unsubscribeControlRealtime?.();
297
+ unsubscribe?.();
298
+ }
33
299
  }