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.
- package/package.json +2 -1
- package/schema.json +24 -0
- package/src/config/config.ts +64 -0
- package/src/extension/team-tool.ts +79 -8
- package/src/runtime/agent-control.ts +64 -0
- package/src/runtime/async-runner.ts +30 -1
- package/src/runtime/background-runner.ts +4 -2
- package/src/runtime/child-pi.ts +78 -4
- package/src/runtime/crew-agent-records.ts +99 -0
- package/src/runtime/crew-agent-runtime.ts +53 -0
- package/src/runtime/group-join.ts +88 -0
- package/src/runtime/live-session-runtime.ts +33 -0
- package/src/runtime/policy-engine.ts +23 -0
- package/src/runtime/recovery-recipes.ts +74 -0
- package/src/runtime/role-permission.ts +28 -0
- package/src/runtime/runtime-resolver.ts +75 -0
- package/src/runtime/session-usage.ts +79 -0
- package/src/runtime/task-graph-scheduler.ts +107 -0
- package/src/runtime/task-output-context.ts +106 -0
- package/src/runtime/task-runner.ts +214 -4
- package/src/runtime/team-runner.ts +86 -14
- package/src/runtime/worker-startup.ts +57 -0
- package/src/state/contracts.ts +7 -0
- package/src/state/event-log.ts +84 -2
- package/src/state/state-store.ts +23 -2
- package/src/state/types.ts +3 -0
- package/src/worktree/branch-freshness.ts +45 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-crew",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
|
|
5
5
|
"author": "baphuongna",
|
|
6
6
|
"license": "MIT",
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"@mariozechner/pi-tui": "*"
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
|
+
"jiti": "^2.6.1",
|
|
71
72
|
"typebox": "^1.1.24"
|
|
72
73
|
},
|
|
73
74
|
"devDependencies": {
|
package/schema.json
CHANGED
|
@@ -54,6 +54,30 @@
|
|
|
54
54
|
"maxTasksPerRun": { "type": "integer", "minimum": 1 },
|
|
55
55
|
"heartbeatStaleMs": { "type": "integer", "minimum": 1 }
|
|
56
56
|
}
|
|
57
|
+
},
|
|
58
|
+
"runtime": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"additionalProperties": false,
|
|
61
|
+
"description": "Crew runtime selection and live-agent behavior knobs.",
|
|
62
|
+
"properties": {
|
|
63
|
+
"mode": { "type": "string", "enum": ["auto", "scaffold", "child-process", "live-session"] },
|
|
64
|
+
"preferLiveSession": { "type": "boolean" },
|
|
65
|
+
"allowChildProcessFallback": { "type": "boolean" },
|
|
66
|
+
"maxTurns": { "type": "integer", "minimum": 1 },
|
|
67
|
+
"graceTurns": { "type": "integer", "minimum": 1 },
|
|
68
|
+
"inheritContext": { "type": "boolean" },
|
|
69
|
+
"promptMode": { "type": "string", "enum": ["replace", "append"] },
|
|
70
|
+
"groupJoin": { "type": "string", "enum": ["off", "group", "smart"] }
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"control": {
|
|
74
|
+
"type": "object",
|
|
75
|
+
"additionalProperties": false,
|
|
76
|
+
"description": "Agent control-plane settings for attention/stale activity detection.",
|
|
77
|
+
"properties": {
|
|
78
|
+
"enabled": { "type": "boolean" },
|
|
79
|
+
"needsAttentionAfterMs": { "type": "integer", "minimum": 1 }
|
|
80
|
+
}
|
|
57
81
|
}
|
|
58
82
|
}
|
|
59
83
|
}
|
package/src/config/config.ts
CHANGED
|
@@ -23,6 +23,24 @@ export interface CrewLimitsConfig {
|
|
|
23
23
|
heartbeatStaleMs?: number;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export type CrewRuntimeMode = "auto" | "scaffold" | "child-process" | "live-session";
|
|
27
|
+
|
|
28
|
+
export interface CrewRuntimeConfig {
|
|
29
|
+
mode?: CrewRuntimeMode;
|
|
30
|
+
preferLiveSession?: boolean;
|
|
31
|
+
allowChildProcessFallback?: boolean;
|
|
32
|
+
maxTurns?: number;
|
|
33
|
+
graceTurns?: number;
|
|
34
|
+
inheritContext?: boolean;
|
|
35
|
+
promptMode?: "replace" | "append";
|
|
36
|
+
groupJoin?: "off" | "group" | "smart";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CrewControlConfig {
|
|
40
|
+
enabled?: boolean;
|
|
41
|
+
needsAttentionAfterMs?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
export interface PiTeamsConfig {
|
|
27
45
|
asyncByDefault?: boolean;
|
|
28
46
|
executeWorkers?: boolean;
|
|
@@ -30,6 +48,8 @@ export interface PiTeamsConfig {
|
|
|
30
48
|
requireCleanWorktreeLeader?: boolean;
|
|
31
49
|
autonomous?: PiTeamsAutonomousConfig;
|
|
32
50
|
limits?: CrewLimitsConfig;
|
|
51
|
+
runtime?: CrewRuntimeConfig;
|
|
52
|
+
control?: CrewControlConfig;
|
|
33
53
|
}
|
|
34
54
|
|
|
35
55
|
export interface LoadedPiTeamsConfig {
|
|
@@ -77,6 +97,18 @@ function mergeConfig(base: PiTeamsConfig, override: PiTeamsConfig): PiTeamsConfi
|
|
|
77
97
|
...withoutUndefined((override.limits ?? {}) as Record<string, unknown>),
|
|
78
98
|
};
|
|
79
99
|
}
|
|
100
|
+
if (base.runtime || override.runtime) {
|
|
101
|
+
merged.runtime = {
|
|
102
|
+
...(base.runtime ?? {}),
|
|
103
|
+
...withoutUndefined((override.runtime ?? {}) as Record<string, unknown>),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (base.control || override.control) {
|
|
107
|
+
merged.control = {
|
|
108
|
+
...(base.control ?? {}),
|
|
109
|
+
...withoutUndefined((override.control ?? {}) as Record<string, unknown>),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
80
112
|
return merged;
|
|
81
113
|
}
|
|
82
114
|
|
|
@@ -146,6 +178,36 @@ function parseLimitsConfig(value: unknown): CrewLimitsConfig | undefined {
|
|
|
146
178
|
return Object.values(limits).some((entry) => entry !== undefined) ? limits : undefined;
|
|
147
179
|
}
|
|
148
180
|
|
|
181
|
+
function parseRuntimeMode(value: unknown): CrewRuntimeMode | undefined {
|
|
182
|
+
return value === "auto" || value === "scaffold" || value === "child-process" || value === "live-session" ? value : undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function parseRuntimeConfig(value: unknown): CrewRuntimeConfig | undefined {
|
|
186
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
187
|
+
const obj = value as Record<string, unknown>;
|
|
188
|
+
const runtime: CrewRuntimeConfig = {
|
|
189
|
+
mode: parseRuntimeMode(obj.mode),
|
|
190
|
+
preferLiveSession: typeof obj.preferLiveSession === "boolean" ? obj.preferLiveSession : undefined,
|
|
191
|
+
allowChildProcessFallback: typeof obj.allowChildProcessFallback === "boolean" ? obj.allowChildProcessFallback : undefined,
|
|
192
|
+
maxTurns: parsePositiveInteger(obj.maxTurns),
|
|
193
|
+
graceTurns: parsePositiveInteger(obj.graceTurns),
|
|
194
|
+
inheritContext: typeof obj.inheritContext === "boolean" ? obj.inheritContext : undefined,
|
|
195
|
+
promptMode: obj.promptMode === "replace" || obj.promptMode === "append" ? obj.promptMode : undefined,
|
|
196
|
+
groupJoin: obj.groupJoin === "off" || obj.groupJoin === "group" || obj.groupJoin === "smart" ? obj.groupJoin : undefined,
|
|
197
|
+
};
|
|
198
|
+
return Object.values(runtime).some((entry) => entry !== undefined) ? runtime : undefined;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function parseControlConfig(value: unknown): CrewControlConfig | undefined {
|
|
202
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
203
|
+
const obj = value as Record<string, unknown>;
|
|
204
|
+
const control: CrewControlConfig = {
|
|
205
|
+
enabled: typeof obj.enabled === "boolean" ? obj.enabled : undefined,
|
|
206
|
+
needsAttentionAfterMs: parsePositiveInteger(obj.needsAttentionAfterMs),
|
|
207
|
+
};
|
|
208
|
+
return Object.values(control).some((entry) => entry !== undefined) ? control : undefined;
|
|
209
|
+
}
|
|
210
|
+
|
|
149
211
|
function parseConfig(raw: unknown): PiTeamsConfig {
|
|
150
212
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
151
213
|
const obj = raw as Record<string, unknown>;
|
|
@@ -156,6 +218,8 @@ function parseConfig(raw: unknown): PiTeamsConfig {
|
|
|
156
218
|
requireCleanWorktreeLeader: typeof obj.requireCleanWorktreeLeader === "boolean" ? obj.requireCleanWorktreeLeader : undefined,
|
|
157
219
|
autonomous: parseAutonomousConfig(obj.autonomous),
|
|
158
220
|
limits: parseLimitsConfig(obj.limits),
|
|
221
|
+
runtime: parseRuntimeConfig(obj.runtime),
|
|
222
|
+
control: parseControlConfig(obj.control),
|
|
159
223
|
};
|
|
160
224
|
}
|
|
161
225
|
|
|
@@ -35,6 +35,10 @@ 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";
|
|
39
|
+
import { resolveCrewRuntime } from "../runtime/runtime-resolver.ts";
|
|
40
|
+
import { probeLiveSessionRuntime } from "../runtime/live-session-runtime.ts";
|
|
41
|
+
import { applyAttentionState, formatActivityAge, resolveCrewControlConfig } from "../runtime/agent-control.ts";
|
|
38
42
|
|
|
39
43
|
export interface TeamToolDetails {
|
|
40
44
|
action: string;
|
|
@@ -255,8 +259,9 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
255
259
|
return result(text, { action: "run", status: "ok", runId: updatedManifest.runId, artifactsRoot: updatedManifest.artifactsRoot });
|
|
256
260
|
}
|
|
257
261
|
|
|
258
|
-
const
|
|
259
|
-
const
|
|
262
|
+
const runtime = await resolveCrewRuntime(loadedConfig.config);
|
|
263
|
+
const executeWorkers = runtime.kind === "child-process";
|
|
264
|
+
const executed = await executeTeamRun({ manifest: updatedManifest, tasks, team, workflow, agents, executeWorkers, limits: loadedConfig.config.limits, runtime, runtimeConfig: loadedConfig.config.runtime });
|
|
260
265
|
const text = [
|
|
261
266
|
`Created pi-crew run ${executed.manifest.runId}.`,
|
|
262
267
|
`Team: ${team.name}`,
|
|
@@ -266,9 +271,10 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
266
271
|
`State: ${executed.manifest.stateRoot}`,
|
|
267
272
|
`Artifacts: ${executed.manifest.artifactsRoot}`,
|
|
268
273
|
"",
|
|
274
|
+
`Runtime: ${runtime.kind}${runtime.fallback ? ` (fallback from ${runtime.requestedMode})` : ""}${runtime.reason ? ` - ${runtime.reason}` : ""}`,
|
|
269
275
|
executeWorkers
|
|
270
|
-
? "Child Pi worker execution was enabled
|
|
271
|
-
: "Safe scaffold mode: child Pi workers were not launched. Set PI_TEAMS_EXECUTE_WORKERS=1 to enable real worker execution.",
|
|
276
|
+
? "Child Pi worker execution was enabled."
|
|
277
|
+
: "Safe scaffold mode: child Pi workers were not launched. Set PI_TEAMS_EXECUTE_WORKERS=1 or runtime.mode=child-process to enable real worker execution.",
|
|
272
278
|
].join("\n");
|
|
273
279
|
return result(text, { action: "run", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot }, executed.manifest.status === "failed");
|
|
274
280
|
}
|
|
@@ -291,6 +297,8 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
291
297
|
const counts = new Map<string, number>();
|
|
292
298
|
for (const task of tasks) counts.set(task.status, (counts.get(task.status) ?? 0) + 1);
|
|
293
299
|
const events = readEvents(manifest.eventsPath).slice(-8);
|
|
300
|
+
const controlConfig = resolveCrewControlConfig(loadConfig(ctx.cwd).config);
|
|
301
|
+
const crewAgents = readCrewAgents(manifest).map((agent) => applyAttentionState(manifest, agent, controlConfig));
|
|
294
302
|
const artifactLines = manifest.artifacts.slice(-10).map((artifact) => `- ${artifact.kind}: ${artifact.path}${artifact.sizeBytes !== undefined ? ` (${artifact.sizeBytes} bytes)` : ""}`);
|
|
295
303
|
const totalUsage = aggregateUsage(tasks);
|
|
296
304
|
const lines = [
|
|
@@ -308,6 +316,8 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
308
316
|
"Tasks:",
|
|
309
317
|
...(tasks.length ? tasks.map((task) => `- ${task.id} [${task.status}] ${task.role} -> ${task.agent}${task.taskPacket ? ` scope=${task.taskPacket.scope}` : ""}${task.verification ? ` green=${task.verification.observedGreenLevel}/${task.verification.requiredGreenLevel}` : ""}${task.modelAttempts?.length ? ` attempts=${task.modelAttempts.length}` : ""}${task.jsonEvents !== undefined ? ` jsonEvents=${task.jsonEvents}` : ""}${task.usage ? ` usage=${JSON.stringify(task.usage)}` : ""}${task.worktree ? ` worktree=${task.worktree.path}` : ""}${task.error ? ` error=${task.error}` : ""}`) : ["- (none)"]),
|
|
310
318
|
`Task counts: ${[...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none"}`,
|
|
319
|
+
"Agents:",
|
|
320
|
+
...(crewAgents.length ? crewAgents.map((agent) => `- ${agent.id} [${agent.status}] ${agent.role} -> ${agent.agent} runtime=${agent.runtime}${agent.progress?.activityState === "needs_attention" ? " needs_attention" : ""}${formatActivityAge(agent) ? ` activity=${formatActivityAge(agent)}` : ""}${agent.progress?.currentTool ? ` tool=${agent.progress.currentTool}` : ""}${agent.toolUses ? ` tools=${agent.toolUses}` : ""}${agent.progress?.tokens ? ` tokens=${agent.progress.tokens}` : ""}${agent.progress?.turns ? ` turns=${agent.progress.turns}` : ""}${agent.jsonEvents !== undefined ? ` jsonEvents=${agent.jsonEvents}` : ""}${agent.statusPath ? ` status=${agent.statusPath}` : ""}${agent.error ? ` error=${agent.error}` : ""}`) : ["- (none)"]),
|
|
311
321
|
"Policy decisions:",
|
|
312
322
|
...(manifest.policyDecisions?.length ? manifest.policyDecisions.map((item) => `- ${item.action} (${item.reason})${item.taskId ? ` ${item.taskId}` : ""}: ${item.message}`) : ["- (none)"]),
|
|
313
323
|
`Total usage: ${formatUsage(totalUsage)}`,
|
|
@@ -370,8 +380,9 @@ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
370
380
|
saveRunTasks(loaded.manifest, resetTasks);
|
|
371
381
|
appendEvent(loaded.manifest.eventsPath, { type: "run.resume_requested", runId: loaded.manifest.runId });
|
|
372
382
|
const loadedConfig = loadConfig(ctx.cwd);
|
|
373
|
-
const
|
|
374
|
-
const
|
|
383
|
+
const runtime = await resolveCrewRuntime(loadedConfig.config);
|
|
384
|
+
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 });
|
|
375
386
|
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");
|
|
376
387
|
});
|
|
377
388
|
}
|
|
@@ -446,12 +457,17 @@ function autonomousPatchFromConfig(config: unknown): PiTeamsAutonomousConfig {
|
|
|
446
457
|
|
|
447
458
|
function configPatchFromConfig(config: unknown): PiTeamsConfig {
|
|
448
459
|
const cfg = configRecord(config);
|
|
460
|
+
const control = configRecord(cfg.control);
|
|
449
461
|
return {
|
|
450
462
|
asyncByDefault: typeof cfg.asyncByDefault === "boolean" ? cfg.asyncByDefault : undefined,
|
|
451
463
|
executeWorkers: typeof cfg.executeWorkers === "boolean" ? cfg.executeWorkers : undefined,
|
|
452
464
|
notifierIntervalMs: typeof cfg.notifierIntervalMs === "number" && Number.isFinite(cfg.notifierIntervalMs) ? cfg.notifierIntervalMs : undefined,
|
|
453
465
|
requireCleanWorktreeLeader: typeof cfg.requireCleanWorktreeLeader === "boolean" ? cfg.requireCleanWorktreeLeader : undefined,
|
|
454
466
|
autonomous: typeof cfg.autonomous === "object" && cfg.autonomous !== null && !Array.isArray(cfg.autonomous) ? autonomousPatchFromConfig(cfg.autonomous) : undefined,
|
|
467
|
+
control: Object.keys(control).length > 0 ? {
|
|
468
|
+
enabled: typeof control.enabled === "boolean" ? control.enabled : undefined,
|
|
469
|
+
needsAttentionAfterMs: typeof control.needsAttentionAfterMs === "number" && Number.isInteger(control.needsAttentionAfterMs) && control.needsAttentionAfterMs > 0 ? control.needsAttentionAfterMs : undefined,
|
|
470
|
+
} : undefined,
|
|
455
471
|
};
|
|
456
472
|
}
|
|
457
473
|
|
|
@@ -540,7 +556,7 @@ export function handleCleanup(params: TeamToolParamsValue, ctx: TeamContext): Pi
|
|
|
540
556
|
return result(lines.join("\n"), { action: "cleanup", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
541
557
|
}
|
|
542
558
|
|
|
543
|
-
export function handleApi(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
559
|
+
export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
|
|
544
560
|
if (!params.runId) return result("API requires runId.", { action: "api", status: "error" }, true);
|
|
545
561
|
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
546
562
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "api", status: "error" }, true);
|
|
@@ -561,6 +577,61 @@ export function handleApi(params: TeamToolParamsValue, ctx: TeamContext): PiTeam
|
|
|
561
577
|
if (operation === "read-events") {
|
|
562
578
|
return result(JSON.stringify(readEvents(loaded.manifest.eventsPath), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
563
579
|
}
|
|
580
|
+
if (operation === "runtime-capabilities") {
|
|
581
|
+
const loadedConfig = loadConfig(ctx.cwd);
|
|
582
|
+
return result(JSON.stringify(await resolveCrewRuntime(loadedConfig.config), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
583
|
+
}
|
|
584
|
+
if (operation === "probe-live-session") {
|
|
585
|
+
return result(JSON.stringify(await probeLiveSessionRuntime(), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
586
|
+
}
|
|
587
|
+
if (operation === "list-agents") {
|
|
588
|
+
return result(JSON.stringify(readCrewAgents(loaded.manifest), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
589
|
+
}
|
|
590
|
+
if (operation === "get-agent-result") {
|
|
591
|
+
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
592
|
+
const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
|
|
593
|
+
if (!agent) return result("API get-agent-result requires config.agentId matching an agent id or task id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
594
|
+
const task = loaded.tasks.find((item) => item.id === agent.taskId);
|
|
595
|
+
const text = task?.resultArtifact && fs.existsSync(task.resultArtifact.path) ? fs.readFileSync(task.resultArtifact.path, "utf-8") : JSON.stringify(agent, null, 2);
|
|
596
|
+
return result(text, { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
597
|
+
}
|
|
598
|
+
if (operation === "read-agent-status") {
|
|
599
|
+
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
600
|
+
const agent = agentId ? readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId) : undefined;
|
|
601
|
+
const status = agent ? readCrewAgentStatus(loaded.manifest, agent.taskId) ?? agent : undefined;
|
|
602
|
+
if (!status) return result("API read-agent-status requires config.agentId matching an agent id or task id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
603
|
+
return result(JSON.stringify(status, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
604
|
+
}
|
|
605
|
+
if (operation === "read-agent-events") {
|
|
606
|
+
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
607
|
+
const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
|
|
608
|
+
if (!agent) return result("API read-agent-events requires config.agentId matching an agent id or task id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
609
|
+
return result(JSON.stringify({ path: agentEventsPath(loaded.manifest, agent.taskId), events: readCrewAgentEvents(loaded.manifest, agent.taskId) }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
610
|
+
}
|
|
611
|
+
if (operation === "read-agent-transcript") {
|
|
612
|
+
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
613
|
+
const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
|
|
614
|
+
if (!agent) return result("API read-agent-transcript requires config.agentId matching an agent id or task id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
615
|
+
const transcriptPath = agent.transcriptPath && fs.existsSync(agent.transcriptPath) ? agent.transcriptPath : agentOutputPath(loaded.manifest, agent.taskId);
|
|
616
|
+
const text = fs.existsSync(transcriptPath) ? fs.readFileSync(transcriptPath, "utf-8") : "";
|
|
617
|
+
return result(text || `(no transcript at ${transcriptPath})`, { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
618
|
+
}
|
|
619
|
+
if (operation === "nudge-agent") {
|
|
620
|
+
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
621
|
+
const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
|
|
622
|
+
if (!agent) return result("API nudge-agent requires config.agentId matching an agent id or task id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
623
|
+
const messageText = typeof cfg.message === "string" && cfg.message.trim() ? cfg.message.trim() : "Please report your current status, blocker, or smallest next step.";
|
|
624
|
+
const message = appendMailboxMessage(loaded.manifest, { direction: "inbox", from: "leader", to: agent.taskId, taskId: agent.taskId, body: messageText });
|
|
625
|
+
appendEvent(loaded.manifest.eventsPath, { type: "agent.nudged", runId: loaded.manifest.runId, taskId: agent.taskId, message: messageText, data: { agentId: agent.id, mailboxMessageId: message.id } });
|
|
626
|
+
return result(JSON.stringify({ agentId: agent.id, mailboxMessage: message }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
627
|
+
}
|
|
628
|
+
if (operation === "steer-agent" || operation === "stop-agent" || operation === "resume-agent" || operation === "interrupt-agent") {
|
|
629
|
+
const runtime = await resolveCrewRuntime(loadConfig(ctx.cwd).config);
|
|
630
|
+
if (!runtime.steer && operation === "steer-agent") return result(`Runtime '${runtime.kind}' does not support live steering. Use nudge-agent for mailbox-based child-process coordination.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
631
|
+
if (!runtime.resume && operation === "resume-agent") return result(`Runtime '${runtime.kind}' does not support live resume.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
632
|
+
if (operation === "interrupt-agent" && runtime.kind !== "live-session") return result(`Runtime '${runtime.kind}' does not expose per-agent interrupt yet. Use nudge-agent or cancel the run.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
633
|
+
return result(`Operation '${operation}' is reserved for live-session runtime and is not active for this run yet.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
634
|
+
}
|
|
564
635
|
if (operation === "read-mailbox") {
|
|
565
636
|
const direction = cfg.direction === "inbox" || cfg.direction === "outbox" ? cfg.direction as MailboxDirection : undefined;
|
|
566
637
|
const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
|
|
@@ -762,7 +833,7 @@ export async function handleTeamTool(params: TeamToolParamsValue, ctx: TeamConte
|
|
|
762
833
|
}
|
|
763
834
|
case "doctor": return handleDoctor(ctx, params);
|
|
764
835
|
case "cleanup": return handleCleanup(params, ctx);
|
|
765
|
-
case "api": return handleApi(params, ctx);
|
|
836
|
+
case "api": return await handleApi(params, ctx);
|
|
766
837
|
case "events": return handleEvents(params, ctx);
|
|
767
838
|
case "artifacts": return handleArtifacts(params, ctx);
|
|
768
839
|
case "worktrees": return handleWorktrees(params, ctx);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { PiTeamsConfig } from "../config/config.ts";
|
|
2
|
+
import type { TeamRunManifest } from "../state/types.ts";
|
|
3
|
+
import { appendEvent } from "../state/event-log.ts";
|
|
4
|
+
import type { CrewAgentRecord } from "./crew-agent-runtime.ts";
|
|
5
|
+
import { upsertCrewAgent } from "./crew-agent-records.ts";
|
|
6
|
+
|
|
7
|
+
export interface CrewControlConfig {
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
needsAttentionAfterMs: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const DEFAULT_NEEDS_ATTENTION_MS = 60_000;
|
|
13
|
+
|
|
14
|
+
function positiveInt(value: unknown): number | undefined {
|
|
15
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveCrewControlConfig(config: PiTeamsConfig | undefined): CrewControlConfig {
|
|
19
|
+
const raw = config as PiTeamsConfig & { control?: { enabled?: unknown; needsAttentionAfterMs?: unknown } } | undefined;
|
|
20
|
+
return {
|
|
21
|
+
enabled: raw?.control?.enabled === false ? false : true,
|
|
22
|
+
needsAttentionAfterMs: positiveInt(raw?.control?.needsAttentionAfterMs) ?? DEFAULT_NEEDS_ATTENTION_MS,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function activityAgeMs(agent: CrewAgentRecord, now = Date.now()): number | undefined {
|
|
27
|
+
const timestamp = agent.progress?.lastActivityAt ?? agent.startedAt;
|
|
28
|
+
if (!timestamp) return undefined;
|
|
29
|
+
const ms = now - new Date(timestamp).getTime();
|
|
30
|
+
return Number.isFinite(ms) ? Math.max(0, ms) : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function formatActivityAge(agent: CrewAgentRecord, now = Date.now()): string | undefined {
|
|
34
|
+
const age = activityAgeMs(agent, now);
|
|
35
|
+
if (age === undefined) return undefined;
|
|
36
|
+
if (age < 1000) return "active now";
|
|
37
|
+
const seconds = Math.floor(age / 1000);
|
|
38
|
+
if (seconds < 60) return agent.progress?.activityState === "needs_attention" ? `no activity for ${seconds}s` : `active ${seconds}s ago`;
|
|
39
|
+
const minutes = Math.floor(seconds / 60);
|
|
40
|
+
return agent.progress?.activityState === "needs_attention" ? `no activity for ${minutes}m` : `active ${minutes}m ago`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function applyAttentionState(manifest: TeamRunManifest, agent: CrewAgentRecord, config: CrewControlConfig, now = Date.now()): CrewAgentRecord {
|
|
44
|
+
if (!config.enabled || agent.status !== "running") return agent;
|
|
45
|
+
const age = activityAgeMs(agent, now);
|
|
46
|
+
if (age === undefined || age <= config.needsAttentionAfterMs) return agent;
|
|
47
|
+
if (agent.progress?.activityState === "needs_attention") return agent;
|
|
48
|
+
const updated: CrewAgentRecord = {
|
|
49
|
+
...agent,
|
|
50
|
+
progress: {
|
|
51
|
+
...(agent.progress ?? { recentTools: [], recentOutput: [], toolCount: agent.toolUses ?? 0 }),
|
|
52
|
+
activityState: "needs_attention",
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
upsertCrewAgent(manifest, updated);
|
|
56
|
+
appendEvent(manifest.eventsPath, {
|
|
57
|
+
type: "agent.needs_attention",
|
|
58
|
+
runId: manifest.runId,
|
|
59
|
+
taskId: agent.taskId,
|
|
60
|
+
message: `${agent.agent} needs attention (no observed activity for ${Math.floor(age / 1000)}s).`,
|
|
61
|
+
data: { agentId: agent.id, ageMs: age, needsAttentionAfterMs: config.needsAttentionAfterMs },
|
|
62
|
+
});
|
|
63
|
+
return updated;
|
|
64
|
+
}
|
|
@@ -1,9 +1,36 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import * as fs from "node:fs";
|
|
3
4
|
import * as path from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import type { TeamRunManifest } from "../state/types.ts";
|
|
6
7
|
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
|
|
10
|
+
function resolveJitiCliPath(): string | undefined {
|
|
11
|
+
const candidates: Array<() => string> = [
|
|
12
|
+
() => path.join(path.dirname(require.resolve("jiti/package.json")), "lib/jiti-cli.mjs"),
|
|
13
|
+
() => path.join(path.dirname(require.resolve("@mariozechner/jiti/package.json")), "lib/jiti-cli.mjs"),
|
|
14
|
+
];
|
|
15
|
+
for (const candidate of candidates) {
|
|
16
|
+
try {
|
|
17
|
+
const filePath = candidate();
|
|
18
|
+
if (fs.existsSync(filePath)) return filePath;
|
|
19
|
+
} catch {
|
|
20
|
+
// Try the next possible runtime dependency location.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getBackgroundRunnerCommand(runnerPath: string, cwd: string, runId: string): { args: string[]; loader: "jiti" | "strip-types" } {
|
|
27
|
+
const jitiCliPath = resolveJitiCliPath();
|
|
28
|
+
const runnerArgs = [runnerPath, "--cwd", cwd, "--run-id", runId];
|
|
29
|
+
return jitiCliPath
|
|
30
|
+
? { args: [jitiCliPath, ...runnerArgs], loader: "jiti" }
|
|
31
|
+
: { args: ["--experimental-strip-types", ...runnerArgs], loader: "strip-types" };
|
|
32
|
+
}
|
|
33
|
+
|
|
7
34
|
export interface SpawnBackgroundTeamRunResult {
|
|
8
35
|
pid?: number;
|
|
9
36
|
logPath: string;
|
|
@@ -14,7 +41,9 @@ export function spawnBackgroundTeamRun(manifest: TeamRunManifest): SpawnBackgrou
|
|
|
14
41
|
const logPath = path.join(manifest.stateRoot, "background.log");
|
|
15
42
|
fs.mkdirSync(manifest.stateRoot, { recursive: true });
|
|
16
43
|
const logFd = fs.openSync(logPath, "a");
|
|
17
|
-
const
|
|
44
|
+
const command = getBackgroundRunnerCommand(runnerPath, manifest.cwd, manifest.runId);
|
|
45
|
+
fs.appendFileSync(logPath, `[pi-crew] background loader=${command.loader}\n`, "utf-8");
|
|
46
|
+
const child = spawn(process.execPath, command.args, {
|
|
18
47
|
cwd: manifest.cwd,
|
|
19
48
|
detached: true,
|
|
20
49
|
stdio: ["ignore", logFd, logFd],
|
|
@@ -5,6 +5,7 @@ import { loadRunManifestById, updateRunStatus } from "../state/state-store.ts";
|
|
|
5
5
|
import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows.ts";
|
|
6
6
|
import { loadConfig } from "../config/config.ts";
|
|
7
7
|
import { executeTeamRun } from "./team-runner.ts";
|
|
8
|
+
import { resolveCrewRuntime } from "./runtime-resolver.ts";
|
|
8
9
|
|
|
9
10
|
function argValue(name: string): string | undefined {
|
|
10
11
|
const index = process.argv.indexOf(name);
|
|
@@ -29,8 +30,9 @@ async function main(): Promise<void> {
|
|
|
29
30
|
if (!workflow) throw new Error(`Workflow '${manifest.workflow ?? ""}' not found.`);
|
|
30
31
|
const agents = allAgents(discoverAgents(cwd));
|
|
31
32
|
const loadedConfig = loadConfig(cwd);
|
|
32
|
-
const
|
|
33
|
-
const
|
|
33
|
+
const runtime = await resolveCrewRuntime(loadedConfig.config);
|
|
34
|
+
const executeWorkers = runtime.kind === "child-process";
|
|
35
|
+
const result = await executeTeamRun({ manifest, tasks, team, workflow, agents, executeWorkers, limits: loadedConfig.config.limits, runtime, runtimeConfig: loadedConfig.config.runtime });
|
|
34
36
|
manifest = result.manifest;
|
|
35
37
|
tasks = result.tasks;
|
|
36
38
|
appendEvent(manifest.eventsPath, { type: "async.completed", runId: manifest.runId, data: { status: manifest.status, tasks: tasks.length } });
|
package/src/runtime/child-pi.ts
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
2
4
|
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
3
5
|
import { buildPiWorkerArgs, cleanupTempDir } from "./pi-args.ts";
|
|
4
6
|
import { getPiSpawnCommand } from "./pi-spawn.ts";
|
|
5
7
|
|
|
8
|
+
const POST_EXIT_STDIO_GUARD_MS = 3000;
|
|
9
|
+
|
|
6
10
|
export interface ChildPiRunInput {
|
|
7
11
|
cwd: string;
|
|
8
12
|
task: string;
|
|
9
13
|
agent: AgentConfig;
|
|
10
14
|
model?: string;
|
|
11
15
|
signal?: AbortSignal;
|
|
16
|
+
transcriptPath?: string;
|
|
17
|
+
onStdoutLine?: (line: string) => void;
|
|
18
|
+
onJsonEvent?: (event: unknown) => void;
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
export interface ChildPiRunResult {
|
|
@@ -18,11 +25,65 @@ export interface ChildPiRunResult {
|
|
|
18
25
|
error?: string;
|
|
19
26
|
}
|
|
20
27
|
|
|
28
|
+
function appendTranscript(input: ChildPiRunInput, line: string): void {
|
|
29
|
+
if (!input.transcriptPath) return;
|
|
30
|
+
fs.mkdirSync(path.dirname(input.transcriptPath), { recursive: true });
|
|
31
|
+
fs.appendFileSync(input.transcriptPath, `${line}\n`, "utf-8");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class ChildPiLineObserver {
|
|
35
|
+
private buffer = "";
|
|
36
|
+
private readonly input: ChildPiRunInput;
|
|
37
|
+
|
|
38
|
+
constructor(input: ChildPiRunInput) {
|
|
39
|
+
this.input = input;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
observe(text: string): void {
|
|
43
|
+
this.buffer += text;
|
|
44
|
+
const lines = this.buffer.split(/\r?\n/);
|
|
45
|
+
this.buffer = lines.pop() ?? "";
|
|
46
|
+
for (const line of lines) this.emitLine(line);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
flush(): void {
|
|
50
|
+
if (!this.buffer) return;
|
|
51
|
+
const line = this.buffer;
|
|
52
|
+
this.buffer = "";
|
|
53
|
+
this.emitLine(line);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private emitLine(line: string): void {
|
|
57
|
+
if (!line.trim()) return;
|
|
58
|
+
appendTranscript(this.input, line);
|
|
59
|
+
this.input.onStdoutLine?.(line);
|
|
60
|
+
try {
|
|
61
|
+
this.input.onJsonEvent?.(JSON.parse(line));
|
|
62
|
+
} catch {
|
|
63
|
+
// Raw stdout is allowed.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function observeStdoutChunk(input: ChildPiRunInput, text: string): void {
|
|
69
|
+
const observer = new ChildPiLineObserver(input);
|
|
70
|
+
observer.observe(text);
|
|
71
|
+
observer.flush();
|
|
72
|
+
}
|
|
73
|
+
|
|
21
74
|
export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResult> {
|
|
22
75
|
const mock = process.env.PI_TEAMS_MOCK_CHILD_PI;
|
|
23
76
|
if (mock) {
|
|
24
|
-
if (mock === "success")
|
|
25
|
-
|
|
77
|
+
if (mock === "success") {
|
|
78
|
+
const stdout = `Mock child Pi success for ${input.agent.name}\n`;
|
|
79
|
+
observeStdoutChunk(input, stdout);
|
|
80
|
+
return { exitCode: 0, stdout, stderr: "" };
|
|
81
|
+
}
|
|
82
|
+
if (mock === "json-success") {
|
|
83
|
+
const stdout = `${JSON.stringify({ type: "message", message: { role: "assistant", content: [{ type: "text", text: `Mock JSON success for ${input.agent.name}` }] } })}\n${JSON.stringify({ type: "message_end", usage: { input: 10, output: 5, cost: 0.001, turns: 1 } })}\n`;
|
|
84
|
+
observeStdoutChunk(input, stdout);
|
|
85
|
+
return { exitCode: 0, stdout, stderr: "" };
|
|
86
|
+
}
|
|
26
87
|
if (mock === "retryable-failure") return { exitCode: 1, stdout: "", stderr: "rate limit: mock failure" };
|
|
27
88
|
return { exitCode: 1, stdout: "", stderr: `mock failure: ${mock}` };
|
|
28
89
|
}
|
|
@@ -38,10 +99,15 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
38
99
|
let stdout = "";
|
|
39
100
|
let stderr = "";
|
|
40
101
|
let settled = false;
|
|
102
|
+
let postExitGuard: NodeJS.Timeout | undefined;
|
|
103
|
+
const lineObserver = new ChildPiLineObserver(input);
|
|
41
104
|
|
|
42
105
|
const settle = (result: ChildPiRunResult): void => {
|
|
43
106
|
if (settled) return;
|
|
44
107
|
settled = true;
|
|
108
|
+
if (postExitGuard) clearTimeout(postExitGuard);
|
|
109
|
+
lineObserver.flush();
|
|
110
|
+
input.signal?.removeEventListener("abort", abort);
|
|
45
111
|
cleanupTempDir(built.tempDir);
|
|
46
112
|
resolve(result);
|
|
47
113
|
};
|
|
@@ -56,7 +122,9 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
56
122
|
|
|
57
123
|
input.signal?.addEventListener("abort", abort, { once: true });
|
|
58
124
|
child.stdout?.on("data", (chunk: Buffer) => {
|
|
59
|
-
|
|
125
|
+
const text = chunk.toString("utf-8");
|
|
126
|
+
stdout += text;
|
|
127
|
+
lineObserver.observe(text);
|
|
60
128
|
});
|
|
61
129
|
child.stderr?.on("data", (chunk: Buffer) => {
|
|
62
130
|
stderr += chunk.toString("utf-8");
|
|
@@ -64,8 +132,14 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
64
132
|
child.on("error", (error) => {
|
|
65
133
|
settle({ exitCode: null, stdout, stderr, error: error.message });
|
|
66
134
|
});
|
|
135
|
+
child.on("exit", () => {
|
|
136
|
+
postExitGuard = setTimeout(() => {
|
|
137
|
+
child.stdout?.destroy();
|
|
138
|
+
child.stderr?.destroy();
|
|
139
|
+
}, POST_EXIT_STDIO_GUARD_MS);
|
|
140
|
+
postExitGuard.unref?.();
|
|
141
|
+
});
|
|
67
142
|
child.on("close", (exitCode) => {
|
|
68
|
-
input.signal?.removeEventListener("abort", abort);
|
|
69
143
|
settle({ exitCode, stdout, stderr });
|
|
70
144
|
});
|
|
71
145
|
});
|