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.
- package/package.json +1 -1
- package/src/agents/agent-config.ts +3 -0
- package/src/agents/discover-agents.ts +34 -5
- package/src/config/config.ts +111 -11
- package/src/extension/async-notifier.ts +3 -1
- package/src/extension/cross-extension-rpc.ts +82 -0
- package/src/extension/register.ts +19 -3
- package/src/extension/result-watcher.ts +89 -0
- package/src/extension/team-tool.ts +145 -19
- package/src/prompt/prompt-runtime.ts +12 -2
- package/src/runtime/agent-memory.ts +72 -0
- package/src/runtime/agent-observability.ts +88 -0
- package/src/runtime/child-pi.ts +60 -4
- package/src/runtime/crew-agent-records.ts +42 -4
- package/src/runtime/crew-agent-runtime.ts +1 -0
- package/src/runtime/foreground-control.ts +82 -0
- package/src/runtime/live-agent-control.ts +78 -0
- package/src/runtime/live-agent-manager.ts +85 -0
- package/src/runtime/live-control-realtime.ts +36 -0
- package/src/runtime/live-session-runtime.ts +271 -5
- package/src/runtime/pi-args.ts +29 -0
- package/src/runtime/runtime-resolver.ts +1 -1
- package/src/runtime/sidechain-output.ts +28 -0
- package/src/runtime/task-runner.ts +83 -12
- package/src/runtime/team-runner.ts +4 -1
- package/src/state/event-log.ts +19 -0
- package/src/ui/run-dashboard.ts +82 -10
- package/src/utils/file-coalescer.ts +33 -0
- package/src/worktree/worktree-manager.ts +73 -2
|
@@ -21,7 +21,7 @@ import { getPiSpawnCommand } from "../runtime/pi-spawn.ts";
|
|
|
21
21
|
import { executeTeamRun } from "../runtime/team-runner.ts";
|
|
22
22
|
import { spawnBackgroundTeamRun } from "../runtime/async-runner.ts";
|
|
23
23
|
import { checkProcessLiveness, isActiveRunStatus } from "../runtime/process-status.ts";
|
|
24
|
-
import { appendEvent, readEvents } from "../state/event-log.ts";
|
|
24
|
+
import { appendEvent, readEvents, readEventsCursor } from "../state/event-log.ts";
|
|
25
25
|
import { cleanupRunWorktrees } from "../worktree/cleanup.ts";
|
|
26
26
|
import { piTeamsHelp } from "./help.ts";
|
|
27
27
|
import { initializeProject } from "./project-init.ts";
|
|
@@ -35,10 +35,15 @@ import { formatValidationReport, validateResources } from "./validate-resources.
|
|
|
35
35
|
import { formatRecommendation, recommendTeam } from "./team-recommendation.ts";
|
|
36
36
|
import { toolResult, type PiTeamsToolResult } from "./tool-result.ts";
|
|
37
37
|
import { touchWorkerHeartbeat } from "../runtime/worker-heartbeat.ts";
|
|
38
|
-
import { agentEventsPath, agentOutputPath, readCrewAgentEvents, readCrewAgentStatus, readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
38
|
+
import { agentEventsPath, agentOutputPath, readCrewAgentEvents, readCrewAgentEventsCursor, readCrewAgentStatus, readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
39
39
|
import { resolveCrewRuntime } from "../runtime/runtime-resolver.ts";
|
|
40
40
|
import { probeLiveSessionRuntime } from "../runtime/live-session-runtime.ts";
|
|
41
41
|
import { applyAttentionState, formatActivityAge, resolveCrewControlConfig } from "../runtime/agent-control.ts";
|
|
42
|
+
import { buildAgentDashboard, readAgentOutput } from "../runtime/agent-observability.ts";
|
|
43
|
+
import { readForegroundControlStatus, writeForegroundInterruptRequest } from "../runtime/foreground-control.ts";
|
|
44
|
+
import { listLiveAgents, resumeLiveAgent, steerLiveAgent, stopLiveAgent } from "../runtime/live-agent-manager.ts";
|
|
45
|
+
import { appendLiveAgentControlRequest } from "../runtime/live-agent-control.ts";
|
|
46
|
+
import { liveControlRealtimeMessage, publishLiveControlRealtime } from "../runtime/live-control-realtime.ts";
|
|
42
47
|
|
|
43
48
|
export interface TeamToolDetails {
|
|
44
49
|
action: string;
|
|
@@ -47,7 +52,11 @@ export interface TeamToolDetails {
|
|
|
47
52
|
artifactsRoot?: string;
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
type TeamContext = Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext, "model"
|
|
55
|
+
type TeamContext = Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext, "model">> & {
|
|
56
|
+
modelRegistry?: unknown;
|
|
57
|
+
sessionManager?: { getBranch?: () => unknown[] };
|
|
58
|
+
events?: { emit?: (event: string, data: unknown) => void };
|
|
59
|
+
};
|
|
51
60
|
|
|
52
61
|
function result(text: string, details: TeamToolDetails, isError = false): PiTeamsToolResult {
|
|
53
62
|
return toolResult(text, details, isError);
|
|
@@ -57,6 +66,29 @@ function formatScoped(name: string, source: string, description: string): string
|
|
|
57
66
|
return `- ${name} (${source}): ${description}`;
|
|
58
67
|
}
|
|
59
68
|
|
|
69
|
+
function extractTextContent(content: unknown): string {
|
|
70
|
+
if (typeof content === "string") return content;
|
|
71
|
+
if (!Array.isArray(content)) return "";
|
|
72
|
+
return content.map((part) => part && typeof part === "object" && !Array.isArray(part) && typeof (part as { text?: unknown }).text === "string" ? (part as { text: string }).text : "").filter(Boolean).join("\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildParentContext(ctx: TeamContext): string | undefined {
|
|
76
|
+
const branch = ctx.sessionManager?.getBranch?.();
|
|
77
|
+
if (!Array.isArray(branch) || branch.length === 0) return undefined;
|
|
78
|
+
const parts: string[] = [];
|
|
79
|
+
for (const entry of branch.slice(-20)) {
|
|
80
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
81
|
+
const record = entry as { type?: unknown; message?: unknown; summary?: unknown };
|
|
82
|
+
if (record.type === "compaction" && typeof record.summary === "string") parts.push(`[Summary]: ${record.summary}`);
|
|
83
|
+
const message = record.message && typeof record.message === "object" && !Array.isArray(record.message) ? record.message as { role?: unknown; content?: unknown } : undefined;
|
|
84
|
+
if (!message || (message.role !== "user" && message.role !== "assistant")) continue;
|
|
85
|
+
const text = extractTextContent(message.content).trim();
|
|
86
|
+
if (text) parts.push(`[${message.role === "user" ? "User" : "Assistant"}]: ${text}`);
|
|
87
|
+
}
|
|
88
|
+
if (!parts.length) return undefined;
|
|
89
|
+
return [`# Parent Conversation Context`, "The following context was inherited from the parent Pi session. Treat it as reference-only.", "", parts.join("\n\n")].join("\n");
|
|
90
|
+
}
|
|
91
|
+
|
|
60
92
|
export function handleList(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
61
93
|
const resource = params.resource;
|
|
62
94
|
const blocks: string[] = [];
|
|
@@ -134,6 +166,18 @@ function commandExists(command: string, args: string[]): { ok: boolean; detail:
|
|
|
134
166
|
return { ok: false, detail: output.error?.message ?? firstOutputLine(output.stdout, output.stderr) };
|
|
135
167
|
}
|
|
136
168
|
|
|
169
|
+
function effectiveRunConfig(base: PiTeamsConfig, rawOverride: unknown): PiTeamsConfig {
|
|
170
|
+
const patch = configPatchFromConfig(rawOverride);
|
|
171
|
+
return {
|
|
172
|
+
...base,
|
|
173
|
+
...patch,
|
|
174
|
+
limits: patch.limits ? { ...(base.limits ?? {}), ...patch.limits } : base.limits,
|
|
175
|
+
runtime: patch.runtime ? { ...(base.runtime ?? {}), ...patch.runtime } : base.runtime,
|
|
176
|
+
control: patch.control ? { ...(base.control ?? {}), ...patch.control } : base.control,
|
|
177
|
+
worktree: patch.worktree ? { ...(base.worktree ?? {}), ...patch.worktree } : base.worktree,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
137
181
|
function piCommandExists(): { ok: boolean; detail: string } {
|
|
138
182
|
const spec = getPiSpawnCommand(["--version"]);
|
|
139
183
|
const output = spawnSync(spec.command, spec.args, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -259,9 +303,10 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
259
303
|
return result(text, { action: "run", status: "ok", runId: updatedManifest.runId, artifactsRoot: updatedManifest.artifactsRoot });
|
|
260
304
|
}
|
|
261
305
|
|
|
262
|
-
const runtime = await resolveCrewRuntime(loadedConfig.config);
|
|
306
|
+
const runtime = await resolveCrewRuntime(effectiveRunConfig(loadedConfig.config, params.config));
|
|
263
307
|
const executeWorkers = runtime.kind === "child-process";
|
|
264
|
-
const
|
|
308
|
+
const executedConfig = effectiveRunConfig(loadedConfig.config, params.config);
|
|
309
|
+
const executed = await executeTeamRun({ manifest: updatedManifest, tasks, team, workflow, agents, executeWorkers, limits: executedConfig.limits, runtime, runtimeConfig: executedConfig.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry });
|
|
265
310
|
const text = [
|
|
266
311
|
`Created pi-crew run ${executed.manifest.runId}.`,
|
|
267
312
|
`Team: ${team.name}`,
|
|
@@ -272,9 +317,11 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
272
317
|
`Artifacts: ${executed.manifest.artifactsRoot}`,
|
|
273
318
|
"",
|
|
274
319
|
`Runtime: ${runtime.kind}${runtime.fallback ? ` (fallback from ${runtime.requestedMode})` : ""}${runtime.reason ? ` - ${runtime.reason}` : ""}`,
|
|
275
|
-
|
|
320
|
+
runtime.kind === "child-process"
|
|
276
321
|
? "Child Pi worker execution was enabled."
|
|
277
|
-
:
|
|
322
|
+
: runtime.kind === "live-session"
|
|
323
|
+
? "Experimental live-session worker execution was enabled."
|
|
324
|
+
: "Safe scaffold mode: child Pi workers were not launched. Set PI_CREW_EXECUTE_WORKERS=1, PI_TEAMS_EXECUTE_WORKERS=1, or runtime.mode=child-process to enable real worker execution.",
|
|
278
325
|
].join("\n");
|
|
279
326
|
return result(text, { action: "run", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot }, executed.manifest.status === "failed");
|
|
280
327
|
}
|
|
@@ -382,7 +429,7 @@ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
382
429
|
const loadedConfig = loadConfig(ctx.cwd);
|
|
383
430
|
const runtime = await resolveCrewRuntime(loadedConfig.config);
|
|
384
431
|
const executeWorkers = runtime.kind === "child-process";
|
|
385
|
-
const executed = await executeTeamRun({ manifest: loaded.manifest, tasks: resetTasks, team, workflow, agents: allAgents(discoverAgents(ctx.cwd)), executeWorkers, limits: loadedConfig.config.limits, runtime, runtimeConfig: loadedConfig.config.runtime });
|
|
432
|
+
const executed = await executeTeamRun({ manifest: loaded.manifest, tasks: resetTasks, team, workflow, agents: allAgents(discoverAgents(ctx.cwd)), executeWorkers, limits: loadedConfig.config.limits, runtime, runtimeConfig: loadedConfig.config.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry });
|
|
386
433
|
return result([`Resumed run ${executed.manifest.runId}.`, `Status: ${executed.manifest.status}`, `Tasks: ${executed.tasks.length}`, `Artifacts: ${executed.manifest.artifactsRoot}`].join("\n"), { action: "resume", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot }, executed.manifest.status === "failed");
|
|
387
434
|
});
|
|
388
435
|
}
|
|
@@ -458,12 +505,39 @@ function autonomousPatchFromConfig(config: unknown): PiTeamsAutonomousConfig {
|
|
|
458
505
|
function configPatchFromConfig(config: unknown): PiTeamsConfig {
|
|
459
506
|
const cfg = configRecord(config);
|
|
460
507
|
const control = configRecord(cfg.control);
|
|
508
|
+
const runtime = configRecord(cfg.runtime);
|
|
509
|
+
const limits = configRecord(cfg.limits);
|
|
510
|
+
const worktree = configRecord(cfg.worktree);
|
|
461
511
|
return {
|
|
462
512
|
asyncByDefault: typeof cfg.asyncByDefault === "boolean" ? cfg.asyncByDefault : undefined,
|
|
463
513
|
executeWorkers: typeof cfg.executeWorkers === "boolean" ? cfg.executeWorkers : undefined,
|
|
464
514
|
notifierIntervalMs: typeof cfg.notifierIntervalMs === "number" && Number.isFinite(cfg.notifierIntervalMs) ? cfg.notifierIntervalMs : undefined,
|
|
465
515
|
requireCleanWorktreeLeader: typeof cfg.requireCleanWorktreeLeader === "boolean" ? cfg.requireCleanWorktreeLeader : undefined,
|
|
466
516
|
autonomous: typeof cfg.autonomous === "object" && cfg.autonomous !== null && !Array.isArray(cfg.autonomous) ? autonomousPatchFromConfig(cfg.autonomous) : undefined,
|
|
517
|
+
limits: Object.keys(limits).length > 0 ? {
|
|
518
|
+
maxConcurrentWorkers: typeof limits.maxConcurrentWorkers === "number" && Number.isInteger(limits.maxConcurrentWorkers) && limits.maxConcurrentWorkers > 0 ? limits.maxConcurrentWorkers : undefined,
|
|
519
|
+
maxTaskDepth: typeof limits.maxTaskDepth === "number" && Number.isInteger(limits.maxTaskDepth) && limits.maxTaskDepth > 0 ? limits.maxTaskDepth : undefined,
|
|
520
|
+
maxChildrenPerTask: typeof limits.maxChildrenPerTask === "number" && Number.isInteger(limits.maxChildrenPerTask) && limits.maxChildrenPerTask > 0 ? limits.maxChildrenPerTask : undefined,
|
|
521
|
+
maxRunMinutes: typeof limits.maxRunMinutes === "number" && Number.isInteger(limits.maxRunMinutes) && limits.maxRunMinutes > 0 ? limits.maxRunMinutes : undefined,
|
|
522
|
+
maxRetriesPerTask: typeof limits.maxRetriesPerTask === "number" && Number.isInteger(limits.maxRetriesPerTask) && limits.maxRetriesPerTask > 0 ? limits.maxRetriesPerTask : undefined,
|
|
523
|
+
maxTasksPerRun: typeof limits.maxTasksPerRun === "number" && Number.isInteger(limits.maxTasksPerRun) && limits.maxTasksPerRun > 0 ? limits.maxTasksPerRun : undefined,
|
|
524
|
+
heartbeatStaleMs: typeof limits.heartbeatStaleMs === "number" && Number.isInteger(limits.heartbeatStaleMs) && limits.heartbeatStaleMs > 0 ? limits.heartbeatStaleMs : undefined,
|
|
525
|
+
} : undefined,
|
|
526
|
+
runtime: Object.keys(runtime).length > 0 ? {
|
|
527
|
+
mode: runtime.mode === "auto" || runtime.mode === "scaffold" || runtime.mode === "child-process" || runtime.mode === "live-session" ? runtime.mode : undefined,
|
|
528
|
+
preferLiveSession: typeof runtime.preferLiveSession === "boolean" ? runtime.preferLiveSession : undefined,
|
|
529
|
+
allowChildProcessFallback: typeof runtime.allowChildProcessFallback === "boolean" ? runtime.allowChildProcessFallback : undefined,
|
|
530
|
+
maxTurns: typeof runtime.maxTurns === "number" && Number.isInteger(runtime.maxTurns) && runtime.maxTurns > 0 ? runtime.maxTurns : undefined,
|
|
531
|
+
graceTurns: typeof runtime.graceTurns === "number" && Number.isInteger(runtime.graceTurns) && runtime.graceTurns > 0 ? runtime.graceTurns : undefined,
|
|
532
|
+
inheritContext: typeof runtime.inheritContext === "boolean" ? runtime.inheritContext : undefined,
|
|
533
|
+
promptMode: runtime.promptMode === "replace" || runtime.promptMode === "append" ? runtime.promptMode : undefined,
|
|
534
|
+
groupJoin: runtime.groupJoin === "off" || runtime.groupJoin === "group" || runtime.groupJoin === "smart" ? runtime.groupJoin : undefined,
|
|
535
|
+
} : undefined,
|
|
536
|
+
worktree: Object.keys(worktree).length > 0 ? {
|
|
537
|
+
setupHook: typeof worktree.setupHook === "string" && worktree.setupHook.trim() ? worktree.setupHook.trim() : undefined,
|
|
538
|
+
setupHookTimeoutMs: typeof worktree.setupHookTimeoutMs === "number" && Number.isInteger(worktree.setupHookTimeoutMs) && worktree.setupHookTimeoutMs > 0 ? worktree.setupHookTimeoutMs : undefined,
|
|
539
|
+
linkNodeModules: typeof worktree.linkNodeModules === "boolean" ? worktree.linkNodeModules : undefined,
|
|
540
|
+
} : undefined,
|
|
467
541
|
control: Object.keys(control).length > 0 ? {
|
|
468
542
|
enabled: typeof control.enabled === "boolean" ? control.enabled : undefined,
|
|
469
543
|
needsAttentionAfterMs: typeof control.needsAttentionAfterMs === "number" && Number.isInteger(control.needsAttentionAfterMs) && control.needsAttentionAfterMs > 0 ? control.needsAttentionAfterMs : undefined,
|
|
@@ -575,7 +649,12 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
575
649
|
return result(JSON.stringify(task, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
576
650
|
}
|
|
577
651
|
if (operation === "read-events") {
|
|
578
|
-
|
|
652
|
+
const sinceSeq = typeof cfg.sinceSeq === "number" ? cfg.sinceSeq : undefined;
|
|
653
|
+
const limit = typeof cfg.limit === "number" ? cfg.limit : undefined;
|
|
654
|
+
const payload = sinceSeq !== undefined || limit !== undefined
|
|
655
|
+
? readEventsCursor(loaded.manifest.eventsPath, { sinceSeq, limit })
|
|
656
|
+
: { events: readEvents(loaded.manifest.eventsPath), nextSeq: undefined, total: undefined };
|
|
657
|
+
return result(JSON.stringify(payload, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
579
658
|
}
|
|
580
659
|
if (operation === "runtime-capabilities") {
|
|
581
660
|
const loadedConfig = loadConfig(ctx.cwd);
|
|
@@ -604,18 +683,43 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
604
683
|
}
|
|
605
684
|
if (operation === "read-agent-events") {
|
|
606
685
|
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
return result(
|
|
686
|
+
const agents = readCrewAgents(loaded.manifest);
|
|
687
|
+
const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
|
|
688
|
+
if (!agent) return result("API read-agent-events requires config.agentId matching an agent id or task id, or at least one agent in the run.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
689
|
+
const sinceSeq = typeof cfg.sinceSeq === "number" ? cfg.sinceSeq : undefined;
|
|
690
|
+
const limit = typeof cfg.limit === "number" ? cfg.limit : undefined;
|
|
691
|
+
const payload = sinceSeq !== undefined || limit !== undefined
|
|
692
|
+
? readCrewAgentEventsCursor(loaded.manifest, agent.taskId, { sinceSeq, limit })
|
|
693
|
+
: { path: agentEventsPath(loaded.manifest, agent.taskId), events: readCrewAgentEvents(loaded.manifest, agent.taskId) };
|
|
694
|
+
return result(JSON.stringify(payload, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
610
695
|
}
|
|
611
696
|
if (operation === "read-agent-transcript") {
|
|
612
697
|
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
613
|
-
const
|
|
614
|
-
|
|
698
|
+
const agents = readCrewAgents(loaded.manifest);
|
|
699
|
+
const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
|
|
700
|
+
if (!agent) return result("API read-agent-transcript requires config.agentId matching an agent id or task id, or at least one agent in the run.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
615
701
|
const transcriptPath = agent.transcriptPath && fs.existsSync(agent.transcriptPath) ? agent.transcriptPath : agentOutputPath(loaded.manifest, agent.taskId);
|
|
616
702
|
const text = fs.existsSync(transcriptPath) ? fs.readFileSync(transcriptPath, "utf-8") : "";
|
|
617
703
|
return result(text || `(no transcript at ${transcriptPath})`, { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
618
704
|
}
|
|
705
|
+
if (operation === "read-agent-output") {
|
|
706
|
+
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
707
|
+
const agents = readCrewAgents(loaded.manifest);
|
|
708
|
+
const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
|
|
709
|
+
if (!agent) return result("API read-agent-output requires config.agentId matching an agent id or task id, or at least one agent in the run.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
710
|
+
const maxBytes = typeof cfg.maxBytes === "number" ? cfg.maxBytes : undefined;
|
|
711
|
+
return result(JSON.stringify(readAgentOutput(loaded.manifest, agent.taskId, maxBytes), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
712
|
+
}
|
|
713
|
+
if (operation === "agent-dashboard") {
|
|
714
|
+
return result(buildAgentDashboard(loaded.manifest).text, { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
715
|
+
}
|
|
716
|
+
if (operation === "foreground-status") {
|
|
717
|
+
return result(JSON.stringify(readForegroundControlStatus(loaded.manifest, loaded.tasks), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
718
|
+
}
|
|
719
|
+
if (operation === "foreground-interrupt") {
|
|
720
|
+
const reason = typeof cfg.reason === "string" && cfg.reason.trim() ? cfg.reason.trim() : undefined;
|
|
721
|
+
return result(JSON.stringify(writeForegroundInterruptRequest(loaded.manifest, reason), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
722
|
+
}
|
|
619
723
|
if (operation === "nudge-agent") {
|
|
620
724
|
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
621
725
|
const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
|
|
@@ -625,12 +729,34 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
625
729
|
appendEvent(loaded.manifest.eventsPath, { type: "agent.nudged", runId: loaded.manifest.runId, taskId: agent.taskId, message: messageText, data: { agentId: agent.id, mailboxMessageId: message.id } });
|
|
626
730
|
return result(JSON.stringify({ agentId: agent.id, mailboxMessage: message }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
627
731
|
}
|
|
732
|
+
if (operation === "list-live-agents") {
|
|
733
|
+
return result(JSON.stringify(listLiveAgents().filter((agent) => agent.runId === loaded.manifest.runId), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
734
|
+
}
|
|
628
735
|
if (operation === "steer-agent" || operation === "stop-agent" || operation === "resume-agent" || operation === "interrupt-agent") {
|
|
629
|
-
const
|
|
630
|
-
if (!
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
736
|
+
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
737
|
+
if (!agentId) return result(`API ${operation} requires config.agentId.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
738
|
+
const message = typeof cfg.message === "string" && cfg.message.trim() ? cfg.message.trim() : undefined;
|
|
739
|
+
const prompt = typeof cfg.prompt === "string" && cfg.prompt.trim() ? cfg.prompt.trim() : message;
|
|
740
|
+
try {
|
|
741
|
+
if (operation === "steer-agent") return result(JSON.stringify(await steerLiveAgent(agentId, message ?? "Please report current status and wrap up if possible."), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
742
|
+
if (operation === "resume-agent") {
|
|
743
|
+
if (!prompt) return result("API resume-agent requires config.prompt or config.message.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
744
|
+
return result(JSON.stringify(await resumeLiveAgent(agentId, prompt), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
745
|
+
}
|
|
746
|
+
return result(JSON.stringify(await stopLiveAgent(agentId), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
747
|
+
} catch (error) {
|
|
748
|
+
const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
|
|
749
|
+
if (!agent) {
|
|
750
|
+
const err = error instanceof Error ? error.message : String(error);
|
|
751
|
+
return result(err, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
752
|
+
}
|
|
753
|
+
if (operation === "resume-agent" && !prompt) return result("API resume-agent requires config.prompt or config.message.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
754
|
+
const request = appendLiveAgentControlRequest(loaded.manifest, { taskId: agent.taskId, agentId: agent.id, operation: operation === "resume-agent" ? "resume" : operation === "steer-agent" ? "steer" : "stop", message: operation === "resume-agent" ? prompt : message });
|
|
755
|
+
publishLiveControlRealtime(request);
|
|
756
|
+
ctx.events?.emit?.("pi-crew:live-control", liveControlRealtimeMessage(request));
|
|
757
|
+
appendEvent(loaded.manifest.eventsPath, { type: "agent.control.queued", runId: loaded.manifest.runId, taskId: agent.taskId, message: `Queued ${request.operation} control request for live agent.`, data: { request, realtime: true } });
|
|
758
|
+
return result(JSON.stringify({ queued: true, request }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
759
|
+
}
|
|
634
760
|
}
|
|
635
761
|
if (operation === "read-mailbox") {
|
|
636
762
|
const direction = cfg.direction === "inbox" || cfg.direction === "outbox" ? cfg.direction as MailboxDirection : undefined;
|
|
@@ -2,6 +2,8 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
|
2
2
|
|
|
3
3
|
export const PI_TEAMS_INHERIT_PROJECT_CONTEXT_ENV = "PI_TEAMS_INHERIT_PROJECT_CONTEXT";
|
|
4
4
|
export const PI_TEAMS_INHERIT_SKILLS_ENV = "PI_TEAMS_INHERIT_SKILLS";
|
|
5
|
+
export const PI_CREW_INHERIT_PROJECT_CONTEXT_ENV = "PI_CREW_INHERIT_PROJECT_CONTEXT";
|
|
6
|
+
export const PI_CREW_INHERIT_SKILLS_ENV = "PI_CREW_INHERIT_SKILLS";
|
|
5
7
|
|
|
6
8
|
const PROJECT_CONTEXT_HEADER = "\n\n# Project Context\n\nProject-specific instructions and guidelines:\n\n";
|
|
7
9
|
const SKILLS_HEADER = "\n\nThe following skills provide specialized instructions for specific tasks.";
|
|
@@ -13,6 +15,14 @@ function readBooleanEnv(name: string): boolean | undefined {
|
|
|
13
15
|
return value !== "0";
|
|
14
16
|
}
|
|
15
17
|
|
|
18
|
+
function readBooleanEnvAny(...names: string[]): boolean | undefined {
|
|
19
|
+
for (const name of names) {
|
|
20
|
+
const value = readBooleanEnv(name);
|
|
21
|
+
if (value !== undefined) return value;
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
function findSectionEnd(prompt: string, startIndex: number, nextHeaders: string[]): number {
|
|
17
27
|
let endIndex = prompt.length;
|
|
18
28
|
for (const header of nextHeaders) {
|
|
@@ -45,8 +55,8 @@ export function rewriteTeamWorkerPrompt(prompt: string, options: { inheritProjec
|
|
|
45
55
|
|
|
46
56
|
export default function registerPiTeamsPromptRuntime(pi: ExtensionAPI): void {
|
|
47
57
|
pi.on("before_agent_start", (event) => {
|
|
48
|
-
const inheritProjectContext =
|
|
49
|
-
const inheritSkills =
|
|
58
|
+
const inheritProjectContext = readBooleanEnvAny(PI_CREW_INHERIT_PROJECT_CONTEXT_ENV, PI_TEAMS_INHERIT_PROJECT_CONTEXT_ENV);
|
|
59
|
+
const inheritSkills = readBooleanEnvAny(PI_CREW_INHERIT_SKILLS_ENV, PI_TEAMS_INHERIT_SKILLS_ENV);
|
|
50
60
|
if (inheritProjectContext === undefined && inheritSkills === undefined) return;
|
|
51
61
|
const rewritten = rewriteTeamWorkerPrompt(event.systemPrompt, {
|
|
52
62
|
inheritProjectContext: inheritProjectContext ?? true,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
export type AgentMemoryScope = "user" | "project" | "local";
|
|
6
|
+
const MAX_MEMORY_LINES = 200;
|
|
7
|
+
|
|
8
|
+
export function isUnsafeMemoryName(name: string): boolean {
|
|
9
|
+
return !name || name.length > 128 || !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isSymlink(filePath: string): boolean {
|
|
13
|
+
try {
|
|
14
|
+
return fs.lstatSync(filePath).isSymbolicLink();
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function safeReadMemoryFile(filePath: string): string | undefined {
|
|
21
|
+
if (!fs.existsSync(filePath) || isSymlink(filePath)) return undefined;
|
|
22
|
+
try {
|
|
23
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
24
|
+
} catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveMemoryDir(agentName: string, scope: AgentMemoryScope, cwd: string): string {
|
|
30
|
+
if (isUnsafeMemoryName(agentName)) throw new Error(`Unsafe agent name for memory directory: ${agentName}`);
|
|
31
|
+
if (scope === "user") return path.join(os.homedir(), ".pi", "agent-memory", agentName);
|
|
32
|
+
if (scope === "project") return path.join(cwd, ".pi", "agent-memory", agentName);
|
|
33
|
+
return path.join(cwd, ".pi", "agent-memory-local", agentName);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function ensureMemoryDir(memoryDir: string): void {
|
|
37
|
+
if (fs.existsSync(memoryDir)) {
|
|
38
|
+
if (isSymlink(memoryDir)) throw new Error(`Refusing to use symlinked memory directory: ${memoryDir}`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
fs.mkdirSync(memoryDir, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function readMemoryIndex(memoryDir: string): string | undefined {
|
|
45
|
+
if (isSymlink(memoryDir)) return undefined;
|
|
46
|
+
const content = safeReadMemoryFile(path.join(memoryDir, "MEMORY.md"));
|
|
47
|
+
if (content === undefined) return undefined;
|
|
48
|
+
const lines = content.split(/\r?\n/);
|
|
49
|
+
return lines.length > MAX_MEMORY_LINES ? `${lines.slice(0, MAX_MEMORY_LINES).join("\n")}\n... (truncated at 200 lines)` : content;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildMemoryBlock(agentName: string, scope: AgentMemoryScope, cwd: string, writable: boolean): string {
|
|
53
|
+
const memoryDir = resolveMemoryDir(agentName, scope, cwd);
|
|
54
|
+
if (writable) ensureMemoryDir(memoryDir);
|
|
55
|
+
const existing = readMemoryIndex(memoryDir);
|
|
56
|
+
const mode = writable ? "read-write" : "read-only";
|
|
57
|
+
return [
|
|
58
|
+
`# Agent Memory (${mode})`,
|
|
59
|
+
`Memory scope: ${scope}`,
|
|
60
|
+
`Memory directory: ${memoryDir}`,
|
|
61
|
+
writable ? "Use this persistent directory to maintain useful long-term notes for this agent." : "You may reference existing memory, but do not create or modify memory files.",
|
|
62
|
+
"",
|
|
63
|
+
existing ? `## Current MEMORY.md\n${existing}` : "No MEMORY.md exists yet.",
|
|
64
|
+
writable ? [
|
|
65
|
+
"",
|
|
66
|
+
"## Memory Instructions",
|
|
67
|
+
"- Keep MEMORY.md concise (under 200 lines); store details in separate linked files.",
|
|
68
|
+
"- Reject stale memories; update or remove outdated notes.",
|
|
69
|
+
"- Use safe relative filenames inside the memory directory only.",
|
|
70
|
+
].join("\n") : "",
|
|
71
|
+
].filter(Boolean).join("\n");
|
|
72
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import type { TeamRunManifest } from "../state/types.ts";
|
|
3
|
+
import { agentOutputPath, readCrewAgents } from "./crew-agent-records.ts";
|
|
4
|
+
import type { CrewAgentRecord } from "./crew-agent-runtime.ts";
|
|
5
|
+
|
|
6
|
+
export interface TextTailResult {
|
|
7
|
+
path: string;
|
|
8
|
+
text: string;
|
|
9
|
+
bytes: number;
|
|
10
|
+
truncated: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function readTextTail(filePath: string, maxBytes = 64_000): TextTailResult {
|
|
14
|
+
if (!fs.existsSync(filePath)) return { path: filePath, text: "", bytes: 0, truncated: false };
|
|
15
|
+
const stat = fs.statSync(filePath);
|
|
16
|
+
const bytesToRead = Math.min(stat.size, Math.max(0, maxBytes));
|
|
17
|
+
const fd = fs.openSync(filePath, "r");
|
|
18
|
+
try {
|
|
19
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
20
|
+
fs.readSync(fd, buffer, 0, bytesToRead, stat.size - bytesToRead);
|
|
21
|
+
return { path: filePath, text: buffer.toString("utf-8"), bytes: stat.size, truncated: stat.size > bytesToRead };
|
|
22
|
+
} finally {
|
|
23
|
+
fs.closeSync(fd);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function activityText(agent: CrewAgentRecord): string {
|
|
28
|
+
const parts: string[] = [];
|
|
29
|
+
if (agent.progress?.activityState) parts.push(agent.progress.activityState);
|
|
30
|
+
if (agent.progress?.currentTool) parts.push(`tool=${agent.progress.currentTool}`);
|
|
31
|
+
if (agent.toolUses !== undefined) parts.push(`tools=${agent.toolUses}`);
|
|
32
|
+
if (agent.progress?.tokens !== undefined) parts.push(`tokens=${agent.progress.tokens}`);
|
|
33
|
+
if (agent.progress?.turns !== undefined) parts.push(`turns=${agent.progress.turns}`);
|
|
34
|
+
if (agent.progress?.durationMs !== undefined) parts.push(`durationMs=${agent.progress.durationMs}`);
|
|
35
|
+
if (agent.progress?.failedTool) parts.push(`failedTool=${agent.progress.failedTool}`);
|
|
36
|
+
if (agent.progress?.recentOutput?.length) parts.push(`last=${agent.progress.recentOutput.at(-1)}`);
|
|
37
|
+
return parts.join(" ") || "idle";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function statusGlyph(status: CrewAgentRecord["status"]): string {
|
|
41
|
+
if (status === "completed") return "✓";
|
|
42
|
+
if (status === "failed") return "✗";
|
|
43
|
+
if (status === "running") return "▶";
|
|
44
|
+
if (status === "cancelled" || status === "stopped") return "■";
|
|
45
|
+
return "·";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function outputWarning(agent: CrewAgentRecord): string {
|
|
49
|
+
if (agent.status !== "completed") return "";
|
|
50
|
+
if (!agent.outputPath || !fs.existsSync(agent.outputPath)) return " no-output";
|
|
51
|
+
try {
|
|
52
|
+
return fs.statSync(agent.outputPath).size === 0 ? " no-output" : "";
|
|
53
|
+
} catch {
|
|
54
|
+
return " no-output";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function agentLine(agent: CrewAgentRecord): string {
|
|
59
|
+
return `- ${statusGlyph(agent.status)} ${agent.taskId} [${agent.status}] ${agent.role}->${agent.agent} runtime=${agent.runtime} ${activityText(agent)}${outputWarning(agent)}${agent.error ? ` error=${agent.error}` : ""}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function buildAgentDashboard(manifest: TeamRunManifest): { text: string; groups: Record<string, CrewAgentRecord[]> } {
|
|
63
|
+
const agents = readCrewAgents(manifest);
|
|
64
|
+
const groups: Record<string, CrewAgentRecord[]> = {
|
|
65
|
+
running: agents.filter((agent) => agent.status === "running"),
|
|
66
|
+
queued: agents.filter((agent) => agent.status === "queued"),
|
|
67
|
+
recent: agents.filter((agent) => agent.status !== "running" && agent.status !== "queued"),
|
|
68
|
+
};
|
|
69
|
+
const lines = [
|
|
70
|
+
`Crew agents for ${manifest.runId}`,
|
|
71
|
+
`Run status: ${manifest.status}`,
|
|
72
|
+
`Counts: running=${groups.running.length}, queued=${groups.queued.length}, recent=${groups.recent.length}`,
|
|
73
|
+
"",
|
|
74
|
+
"## Running",
|
|
75
|
+
...(groups.running.length ? groups.running.map(agentLine) : ["- (none)"]),
|
|
76
|
+
"",
|
|
77
|
+
"## Queued",
|
|
78
|
+
...(groups.queued.length ? groups.queued.map(agentLine) : ["- (none)"]),
|
|
79
|
+
"",
|
|
80
|
+
"## Recent",
|
|
81
|
+
...(groups.recent.length ? groups.recent.slice(-10).map(agentLine) : ["- (none)"]),
|
|
82
|
+
];
|
|
83
|
+
return { text: lines.join("\n"), groups };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function readAgentOutput(manifest: TeamRunManifest, taskId: string, maxBytes?: number): TextTailResult {
|
|
87
|
+
return readTextTail(agentOutputPath(manifest, taskId), maxBytes);
|
|
88
|
+
}
|
package/src/runtime/child-pi.ts
CHANGED
|
@@ -2,10 +2,12 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
5
|
-
import { buildPiWorkerArgs, cleanupTempDir } from "./pi-args.ts";
|
|
5
|
+
import { buildPiWorkerArgs, checkCrewDepth, cleanupTempDir } from "./pi-args.ts";
|
|
6
6
|
import { getPiSpawnCommand } from "./pi-spawn.ts";
|
|
7
7
|
|
|
8
8
|
const POST_EXIT_STDIO_GUARD_MS = 3000;
|
|
9
|
+
const FINAL_DRAIN_MS = 5000;
|
|
10
|
+
const HARD_KILL_MS = 3000;
|
|
9
11
|
|
|
10
12
|
export interface ChildPiRunInput {
|
|
11
13
|
cwd: string;
|
|
@@ -16,6 +18,9 @@ export interface ChildPiRunInput {
|
|
|
16
18
|
transcriptPath?: string;
|
|
17
19
|
onStdoutLine?: (line: string) => void;
|
|
18
20
|
onJsonEvent?: (event: unknown) => void;
|
|
21
|
+
maxDepth?: number;
|
|
22
|
+
finalDrainMs?: number;
|
|
23
|
+
hardKillMs?: number;
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
export interface ChildPiRunResult {
|
|
@@ -71,7 +76,25 @@ function observeStdoutChunk(input: ChildPiRunInput, text: string): void {
|
|
|
71
76
|
observer.flush();
|
|
72
77
|
}
|
|
73
78
|
|
|
79
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
80
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isFinalAssistantEvent(event: unknown): boolean {
|
|
84
|
+
const obj = asRecord(event);
|
|
85
|
+
if (!obj || obj.type !== "message_end") return false;
|
|
86
|
+
const message = asRecord(obj.message);
|
|
87
|
+
const role = message?.role;
|
|
88
|
+
if (role !== undefined && role !== "assistant") return false;
|
|
89
|
+
const stopReason = typeof message?.stopReason === "string" ? message.stopReason : typeof obj.stopReason === "string" ? obj.stopReason : undefined;
|
|
90
|
+
if (stopReason !== undefined && stopReason !== "stop") return false;
|
|
91
|
+
const content = Array.isArray(message?.content) ? message.content : [];
|
|
92
|
+
return !content.some((part) => asRecord(part)?.type === "toolCall");
|
|
93
|
+
}
|
|
94
|
+
|
|
74
95
|
export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResult> {
|
|
96
|
+
const depth = checkCrewDepth(input.maxDepth);
|
|
97
|
+
if (depth.blocked) return { exitCode: 1, stdout: "", stderr: `pi-crew depth guard blocked child worker: depth ${depth.depth} >= max ${depth.maxDepth}` };
|
|
75
98
|
const mock = process.env.PI_TEAMS_MOCK_CHILD_PI;
|
|
76
99
|
if (mock) {
|
|
77
100
|
if (mock === "success") {
|
|
@@ -87,7 +110,7 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
87
110
|
if (mock === "retryable-failure") return { exitCode: 1, stdout: "", stderr: "rate limit: mock failure" };
|
|
88
111
|
return { exitCode: 1, stdout: "", stderr: `mock failure: ${mock}` };
|
|
89
112
|
}
|
|
90
|
-
const built = buildPiWorkerArgs({ task: input.task, agent: input.agent, model: input.model, sessionEnabled: false });
|
|
113
|
+
const built = buildPiWorkerArgs({ task: input.task, agent: input.agent, model: input.model, sessionEnabled: false, maxDepth: input.maxDepth });
|
|
91
114
|
const spawnSpec = getPiSpawnCommand(built.args);
|
|
92
115
|
try {
|
|
93
116
|
return await new Promise<ChildPiRunResult>((resolve) => {
|
|
@@ -99,13 +122,44 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
99
122
|
let stdout = "";
|
|
100
123
|
let stderr = "";
|
|
101
124
|
let settled = false;
|
|
125
|
+
let childExited = false;
|
|
102
126
|
let postExitGuard: NodeJS.Timeout | undefined;
|
|
103
|
-
|
|
127
|
+
let finalDrainTimer: NodeJS.Timeout | undefined;
|
|
128
|
+
let hardKillTimer: NodeJS.Timeout | undefined;
|
|
129
|
+
const finalDrainMs = input.finalDrainMs ?? FINAL_DRAIN_MS;
|
|
130
|
+
const hardKillMs = input.hardKillMs ?? HARD_KILL_MS;
|
|
131
|
+
let forcedFinalDrain = false;
|
|
132
|
+
const lineObserver = new ChildPiLineObserver({
|
|
133
|
+
...input,
|
|
134
|
+
onJsonEvent: (event) => {
|
|
135
|
+
input.onJsonEvent?.(event);
|
|
136
|
+
if (!isFinalAssistantEvent(event) || childExited || settled || finalDrainTimer) return;
|
|
137
|
+
finalDrainTimer = setTimeout(() => {
|
|
138
|
+
if (settled || childExited) return;
|
|
139
|
+
forcedFinalDrain = true;
|
|
140
|
+
try { child.kill(process.platform === "win32" ? undefined : "SIGTERM"); } catch {}
|
|
141
|
+
hardKillTimer = setTimeout(() => {
|
|
142
|
+
if (settled || childExited) return;
|
|
143
|
+
try { child.kill(process.platform === "win32" ? undefined : "SIGKILL"); } catch {}
|
|
144
|
+
}, hardKillMs);
|
|
145
|
+
hardKillTimer.unref?.();
|
|
146
|
+
}, finalDrainMs);
|
|
147
|
+
finalDrainTimer.unref?.();
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const clearFinalDrainTimers = (): void => {
|
|
152
|
+
if (finalDrainTimer) clearTimeout(finalDrainTimer);
|
|
153
|
+
if (hardKillTimer) clearTimeout(hardKillTimer);
|
|
154
|
+
finalDrainTimer = undefined;
|
|
155
|
+
hardKillTimer = undefined;
|
|
156
|
+
};
|
|
104
157
|
|
|
105
158
|
const settle = (result: ChildPiRunResult): void => {
|
|
106
159
|
if (settled) return;
|
|
107
160
|
settled = true;
|
|
108
161
|
if (postExitGuard) clearTimeout(postExitGuard);
|
|
162
|
+
clearFinalDrainTimers();
|
|
109
163
|
lineObserver.flush();
|
|
110
164
|
input.signal?.removeEventListener("abort", abort);
|
|
111
165
|
cleanupTempDir(built.tempDir);
|
|
@@ -133,6 +187,8 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
133
187
|
settle({ exitCode: null, stdout, stderr, error: error.message });
|
|
134
188
|
});
|
|
135
189
|
child.on("exit", () => {
|
|
190
|
+
childExited = true;
|
|
191
|
+
clearFinalDrainTimers();
|
|
136
192
|
postExitGuard = setTimeout(() => {
|
|
137
193
|
child.stdout?.destroy();
|
|
138
194
|
child.stderr?.destroy();
|
|
@@ -140,7 +196,7 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
140
196
|
postExitGuard.unref?.();
|
|
141
197
|
});
|
|
142
198
|
child.on("close", (exitCode) => {
|
|
143
|
-
settle({ exitCode, stdout, stderr });
|
|
199
|
+
settle({ exitCode, stdout, stderr, ...(forcedFinalDrain && !stderr.trim() ? { error: `Child Pi did not exit within ${finalDrainMs}ms after final assistant message; termination was requested.` } : {}) });
|
|
144
200
|
});
|
|
145
201
|
});
|
|
146
202
|
} finally {
|