pi-crew 0.1.7 → 0.1.9
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 +1 -0
- package/src/agents/discover-agents.ts +5 -0
- package/src/config/config.ts +25 -0
- package/src/extension/async-notifier.ts +3 -1
- package/src/extension/cross-extension-rpc.ts +82 -0
- package/src/extension/help.ts +2 -0
- package/src/extension/register.ts +79 -2
- package/src/extension/result-watcher.ts +89 -0
- package/src/extension/team-tool.ts +105 -11
- package/src/runtime/agent-memory.ts +72 -0
- package/src/runtime/agent-observability.ts +29 -4
- 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/runtime-resolver.ts +1 -1
- package/src/runtime/sidechain-output.ts +28 -0
- package/src/runtime/task-runner.ts +77 -12
- package/src/runtime/team-runner.ts +4 -1
- package/src/ui/crew-widget.ts +113 -0
- package/src/ui/run-dashboard.ts +56 -16
- package/src/ui/transcript-viewer.ts +204 -0
- package/src/utils/file-coalescer.ts +33 -0
- package/src/worktree/worktree-manager.ts +73 -2
package/package.json
CHANGED
|
@@ -15,6 +15,10 @@ function parseCost(value: string | undefined): "free" | "cheap" | "expensive" |
|
|
|
15
15
|
return value === "free" || value === "cheap" || value === "expensive" ? value : undefined;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function parseMemory(value: string | undefined): "user" | "project" | "local" | undefined {
|
|
19
|
+
return value === "user" || value === "project" || value === "local" ? value : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
18
22
|
function parseAgentFile(filePath: string, source: ResourceSource): AgentConfig | undefined {
|
|
19
23
|
try {
|
|
20
24
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -41,6 +45,7 @@ function parseAgentFile(filePath: string, source: ResourceSource): AgentConfig |
|
|
|
41
45
|
systemPromptMode: frontmatter.systemPromptMode === "append" ? "append" : "replace",
|
|
42
46
|
inheritProjectContext: frontmatter.inheritProjectContext === "true",
|
|
43
47
|
inheritSkills: frontmatter.inheritSkills === "true",
|
|
48
|
+
memory: parseMemory(frontmatter.memory),
|
|
44
49
|
disabled: frontmatter.disabled === "true" || frontmatter.enabled === "false",
|
|
45
50
|
routing: triggers || useWhen || avoidWhen || cost || category ? { triggers, useWhen, avoidWhen, cost, category } : undefined,
|
|
46
51
|
};
|
package/src/config/config.ts
CHANGED
|
@@ -41,6 +41,12 @@ export interface CrewControlConfig {
|
|
|
41
41
|
needsAttentionAfterMs?: number;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
export interface CrewWorktreeConfig {
|
|
45
|
+
setupHook?: string;
|
|
46
|
+
setupHookTimeoutMs?: number;
|
|
47
|
+
linkNodeModules?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
44
50
|
export interface AgentOverrideConfig {
|
|
45
51
|
disabled?: boolean;
|
|
46
52
|
model?: string | false;
|
|
@@ -63,6 +69,7 @@ export interface PiTeamsConfig {
|
|
|
63
69
|
limits?: CrewLimitsConfig;
|
|
64
70
|
runtime?: CrewRuntimeConfig;
|
|
65
71
|
control?: CrewControlConfig;
|
|
72
|
+
worktree?: CrewWorktreeConfig;
|
|
66
73
|
agents?: CrewAgentsConfig;
|
|
67
74
|
}
|
|
68
75
|
|
|
@@ -123,6 +130,12 @@ function mergeConfig(base: PiTeamsConfig, override: PiTeamsConfig): PiTeamsConfi
|
|
|
123
130
|
...withoutUndefined((override.control ?? {}) as Record<string, unknown>),
|
|
124
131
|
};
|
|
125
132
|
}
|
|
133
|
+
if (base.worktree || override.worktree) {
|
|
134
|
+
merged.worktree = {
|
|
135
|
+
...(base.worktree ?? {}),
|
|
136
|
+
...withoutUndefined((override.worktree ?? {}) as Record<string, unknown>),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
126
139
|
if (base.agents || override.agents) {
|
|
127
140
|
merged.agents = {
|
|
128
141
|
...(base.agents ?? {}),
|
|
@@ -245,6 +258,17 @@ function parseControlConfig(value: unknown): CrewControlConfig | undefined {
|
|
|
245
258
|
return Object.values(control).some((entry) => entry !== undefined) ? control : undefined;
|
|
246
259
|
}
|
|
247
260
|
|
|
261
|
+
function parseWorktreeConfig(value: unknown): CrewWorktreeConfig | undefined {
|
|
262
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
263
|
+
const obj = value as Record<string, unknown>;
|
|
264
|
+
const worktree: CrewWorktreeConfig = {
|
|
265
|
+
setupHook: typeof obj.setupHook === "string" && obj.setupHook.trim() ? obj.setupHook.trim() : undefined,
|
|
266
|
+
setupHookTimeoutMs: parsePositiveInteger(obj.setupHookTimeoutMs, 300_000),
|
|
267
|
+
linkNodeModules: typeof obj.linkNodeModules === "boolean" ? obj.linkNodeModules : undefined,
|
|
268
|
+
};
|
|
269
|
+
return Object.values(worktree).some((entry) => entry !== undefined) ? worktree : undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
248
272
|
function parseStringArrayOrFalse(value: unknown): string[] | false | undefined {
|
|
249
273
|
if (value === false) return false;
|
|
250
274
|
if (typeof value === "string") return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
@@ -294,6 +318,7 @@ function parseConfig(raw: unknown): PiTeamsConfig {
|
|
|
294
318
|
limits: parseLimitsConfig(obj.limits),
|
|
295
319
|
runtime: parseRuntimeConfig(obj.runtime),
|
|
296
320
|
control: parseControlConfig(obj.control),
|
|
321
|
+
worktree: parseWorktreeConfig(obj.worktree),
|
|
297
322
|
agents: parseAgentsConfig(obj.agents),
|
|
298
323
|
};
|
|
299
324
|
}
|
|
@@ -13,7 +13,9 @@ function isFinished(status: string): boolean {
|
|
|
13
13
|
export function startAsyncRunNotifier(ctx: ExtensionContext, state: AsyncNotifierState, intervalMs = 5000): void {
|
|
14
14
|
if (state.interval) clearInterval(state.interval);
|
|
15
15
|
for (const run of listRuns(ctx.cwd)) {
|
|
16
|
-
|
|
16
|
+
// Treat all pre-existing runs as seen. This avoids noisy error toasts when
|
|
17
|
+
// an old active/stale run is later inspected and transitions to failed.
|
|
18
|
+
state.seenFinishedRunIds.add(run.runId);
|
|
17
19
|
}
|
|
18
20
|
state.interval = setInterval(() => {
|
|
19
21
|
try {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
|
|
3
|
+
import { handleTeamTool } from "./team-tool.ts";
|
|
4
|
+
import { parseLiveControlRealtimeMessage, publishLiveControlRealtime } from "../runtime/live-control-realtime.ts";
|
|
5
|
+
|
|
6
|
+
export interface EventBusLike {
|
|
7
|
+
on(event: string, handler: (data: unknown) => void): (() => void) | void;
|
|
8
|
+
emit(event: string, data: unknown): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type RpcReply<T = unknown> = { success: true; data?: T } | { success: false; error: string };
|
|
12
|
+
export const PI_CREW_RPC_VERSION = 1;
|
|
13
|
+
|
|
14
|
+
export interface PiCrewRpcHandle {
|
|
15
|
+
unsubscribe(): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function requestId(raw: unknown): string | undefined {
|
|
19
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) && typeof (raw as { requestId?: unknown }).requestId === "string" ? (raw as { requestId: string }).requestId : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function reply(events: EventBusLike, channel: string, id: string | undefined, payload: RpcReply): void {
|
|
23
|
+
if (!id) return;
|
|
24
|
+
events.emit(`${channel}:reply:${id}`, payload);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function textOf(result: Awaited<ReturnType<typeof handleTeamTool>>): string {
|
|
28
|
+
return result.content?.map((item) => item.type === "text" ? item.text : "").join("\n") ?? "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function on(events: EventBusLike, channel: string, handler: (raw: unknown) => void): () => void {
|
|
32
|
+
const unsub = events.on(channel, handler);
|
|
33
|
+
return typeof unsub === "function" ? unsub : () => {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function registerPiCrewRpc(events: EventBusLike | undefined, getCtx: () => ExtensionContext | undefined): PiCrewRpcHandle | undefined {
|
|
37
|
+
if (!events) return undefined;
|
|
38
|
+
const unsubs = [
|
|
39
|
+
on(events, "pi-crew:rpc:ping", (raw) => reply(events, "pi-crew:rpc:ping", requestId(raw), { success: true, data: { version: PI_CREW_RPC_VERSION } })),
|
|
40
|
+
on(events, "pi-crew:rpc:run", async (raw) => {
|
|
41
|
+
const id = requestId(raw);
|
|
42
|
+
try {
|
|
43
|
+
const ctx = getCtx();
|
|
44
|
+
if (!ctx) throw new Error("No active pi-crew session context.");
|
|
45
|
+
const params: TeamToolParamsValue = raw && typeof raw === "object" && !Array.isArray(raw) ? { ...(raw as object), action: "run" } as TeamToolParamsValue : { action: "run" };
|
|
46
|
+
const result = await handleTeamTool(params, ctx);
|
|
47
|
+
reply(events, "pi-crew:rpc:run", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: result.details });
|
|
48
|
+
} catch (error) {
|
|
49
|
+
reply(events, "pi-crew:rpc:run", id, { success: false, error: error instanceof Error ? error.message : String(error) });
|
|
50
|
+
}
|
|
51
|
+
}),
|
|
52
|
+
on(events, "pi-crew:rpc:status", async (raw) => {
|
|
53
|
+
const id = requestId(raw);
|
|
54
|
+
try {
|
|
55
|
+
const ctx = getCtx();
|
|
56
|
+
if (!ctx) throw new Error("No active pi-crew session context.");
|
|
57
|
+
const runId = raw && typeof raw === "object" && !Array.isArray(raw) ? (raw as { runId?: string }).runId : undefined;
|
|
58
|
+
const result = await handleTeamTool({ action: "status", runId }, ctx);
|
|
59
|
+
reply(events, "pi-crew:rpc:status", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: { text: textOf(result), details: result.details } });
|
|
60
|
+
} catch (error) {
|
|
61
|
+
reply(events, "pi-crew:rpc:status", id, { success: false, error: error instanceof Error ? error.message : String(error) });
|
|
62
|
+
}
|
|
63
|
+
}),
|
|
64
|
+
on(events, "pi-crew:live-control", (raw) => {
|
|
65
|
+
const request = parseLiveControlRealtimeMessage(raw);
|
|
66
|
+
if (request) publishLiveControlRealtime(request);
|
|
67
|
+
}),
|
|
68
|
+
on(events, "pi-crew:rpc:live-control", async (raw) => {
|
|
69
|
+
const id = requestId(raw);
|
|
70
|
+
try {
|
|
71
|
+
const ctx = getCtx();
|
|
72
|
+
if (!ctx) throw new Error("No active pi-crew session context.");
|
|
73
|
+
const obj = raw && typeof raw === "object" && !Array.isArray(raw) ? raw as Record<string, unknown> : {};
|
|
74
|
+
const result = await handleTeamTool({ action: "api", runId: typeof obj.runId === "string" ? obj.runId : undefined, config: { operation: typeof obj.operation === "string" ? obj.operation : "steer-agent", agentId: obj.agentId, message: obj.message, prompt: obj.prompt } }, ctx);
|
|
75
|
+
reply(events, "pi-crew:rpc:live-control", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: { text: textOf(result), details: result.details } });
|
|
76
|
+
} catch (error) {
|
|
77
|
+
reply(events, "pi-crew:rpc:live-control", id, { success: false, error: error instanceof Error ? error.message : String(error) });
|
|
78
|
+
}
|
|
79
|
+
}),
|
|
80
|
+
];
|
|
81
|
+
return { unsubscribe: () => unsubs.forEach((unsub) => unsub()) };
|
|
82
|
+
}
|
package/src/extension/help.ts
CHANGED
|
@@ -18,6 +18,8 @@ export function piTeamsHelp(): string {
|
|
|
18
18
|
"- /team-worktrees <runId>",
|
|
19
19
|
"- /team-api <runId> <operation> [taskId=<taskId>] [body=<message>]",
|
|
20
20
|
"- /team-dashboard",
|
|
21
|
+
"- /team-transcript <runId> [taskId]",
|
|
22
|
+
"- /team-result <runId> [taskId]",
|
|
21
23
|
"- /team-manager",
|
|
22
24
|
"",
|
|
23
25
|
"Maintenance:",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionCommandContext, ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { loadConfig } from "../config/config.ts";
|
|
3
3
|
import { registerAutonomousPolicy } from "./autonomous-policy.ts";
|
|
4
4
|
import { TeamToolParams, type TeamToolParamsValue } from "../schema/team-tool-schema.ts";
|
|
@@ -9,6 +9,11 @@ import { handleTeamManagerCommand } from "./team-manager-command.ts";
|
|
|
9
9
|
import { handleTeamTool, type TeamToolDetails } from "./team-tool.ts";
|
|
10
10
|
import { listRuns } from "./run-index.ts";
|
|
11
11
|
import { RunDashboard, type RunDashboardSelection } from "../ui/run-dashboard.ts";
|
|
12
|
+
import { registerPiCrewRpc, type PiCrewRpcHandle } from "./cross-extension-rpc.ts";
|
|
13
|
+
import { stopCrewWidget, updateCrewWidget, type CrewWidgetState } from "../ui/crew-widget.ts";
|
|
14
|
+
import { DurableTextViewer, DurableTranscriptViewer } from "../ui/transcript-viewer.ts";
|
|
15
|
+
import { loadRunManifestById } from "../state/state-store.ts";
|
|
16
|
+
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
12
17
|
|
|
13
18
|
function parseRunArgs(args: string): TeamToolParamsValue {
|
|
14
19
|
const tokens = args.match(/"[^"]*"|'[^']*'|\S+/g)?.map((token) => token.replace(/^['"]|['"]$/g, "")) ?? [];
|
|
@@ -44,6 +49,36 @@ function parseScalar(raw: string): unknown {
|
|
|
44
49
|
return raw;
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
async function selectAgentTask(ctx: ExtensionCommandContext, runId: string | undefined, taskId?: string): Promise<{ runId: string; taskId?: string } | undefined> {
|
|
53
|
+
if (!runId) return undefined;
|
|
54
|
+
if (taskId) return { runId, taskId };
|
|
55
|
+
const loaded = loadRunManifestById(ctx.cwd, runId);
|
|
56
|
+
if (!loaded) return { runId };
|
|
57
|
+
const agents = readCrewAgents(loaded.manifest);
|
|
58
|
+
if (ctx.hasUI && agents.length > 1) {
|
|
59
|
+
const choice = await ctx.ui.select("Select pi-crew agent", agents.map((agent) => `${agent.taskId} ${agent.role}→${agent.agent} [${agent.status}]`));
|
|
60
|
+
return { runId, taskId: choice?.split(" ")[0] };
|
|
61
|
+
}
|
|
62
|
+
return { runId, taskId: agents[0]?.taskId };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function openTranscriptViewer(ctx: ExtensionCommandContext, runId: string | undefined, taskId?: string): Promise<boolean> {
|
|
66
|
+
const selected = await selectAgentTask(ctx, runId, taskId);
|
|
67
|
+
if (!selected) return false;
|
|
68
|
+
// eslint-disable-next-line no-param-reassign
|
|
69
|
+
runId = selected.runId;
|
|
70
|
+
// eslint-disable-next-line no-param-reassign
|
|
71
|
+
taskId = selected.taskId;
|
|
72
|
+
if (!runId || !ctx.hasUI) return false;
|
|
73
|
+
const loaded = loadRunManifestById(ctx.cwd, runId);
|
|
74
|
+
if (!loaded) return false;
|
|
75
|
+
await ctx.ui.custom<undefined>((_tui, theme, _keybindings, done) => new DurableTranscriptViewer(loaded.manifest, theme, done, taskId), {
|
|
76
|
+
overlay: true,
|
|
77
|
+
overlayOptions: { width: "90%", maxHeight: "85%", anchor: "center" },
|
|
78
|
+
});
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
47
82
|
function pushUnset(config: Record<string, unknown>, key: string): void {
|
|
48
83
|
const current = Array.isArray(config.unset) ? config.unset : [];
|
|
49
84
|
current.push(key);
|
|
@@ -64,15 +99,27 @@ function setNestedConfig(config: Record<string, unknown>, key: string, value: un
|
|
|
64
99
|
|
|
65
100
|
export function registerPiTeams(pi: ExtensionAPI): void {
|
|
66
101
|
const notifierState: AsyncNotifierState = { seenFinishedRunIds: new Set() };
|
|
102
|
+
let currentCtx: ExtensionContext | undefined;
|
|
103
|
+
let rpcHandle: PiCrewRpcHandle | undefined;
|
|
104
|
+
const widgetState: CrewWidgetState = { frame: 0 };
|
|
67
105
|
registerAutonomousPolicy(pi);
|
|
106
|
+
rpcHandle = registerPiCrewRpc((pi as unknown as { events?: Parameters<typeof registerPiCrewRpc>[0] }).events, () => currentCtx);
|
|
68
107
|
|
|
69
108
|
pi.on("session_start", (_event, ctx) => {
|
|
109
|
+
currentCtx = ctx;
|
|
70
110
|
notifyActiveRuns(ctx);
|
|
71
111
|
const loadedConfig = loadConfig(ctx.cwd);
|
|
72
112
|
startAsyncRunNotifier(ctx, notifierState, loadedConfig.config.notifierIntervalMs ?? 5000);
|
|
113
|
+
updateCrewWidget(ctx, widgetState);
|
|
114
|
+
widgetState.interval = setInterval(() => { if (currentCtx) updateCrewWidget(currentCtx, widgetState); }, 1000);
|
|
115
|
+
widgetState.interval.unref?.();
|
|
73
116
|
});
|
|
74
117
|
pi.on("session_shutdown", () => {
|
|
75
118
|
stopAsyncRunNotifier(notifierState);
|
|
119
|
+
stopCrewWidget(currentCtx, widgetState);
|
|
120
|
+
currentCtx = undefined;
|
|
121
|
+
rpcHandle?.unsubscribe();
|
|
122
|
+
rpcHandle = undefined;
|
|
76
123
|
});
|
|
77
124
|
|
|
78
125
|
const tool: ToolDefinition = {
|
|
@@ -82,7 +129,9 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
82
129
|
promptSnippet: "Use the team tool proactively for coordinated multi-agent work. If unsure, call { action: 'recommend', goal } first, then run or plan with the suggested team/workflow.",
|
|
83
130
|
parameters: TeamToolParams as never,
|
|
84
131
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
85
|
-
|
|
132
|
+
const output = await handleTeamTool(params as TeamToolParamsValue, ctx);
|
|
133
|
+
updateCrewWidget(ctx, widgetState);
|
|
134
|
+
return output;
|
|
86
135
|
},
|
|
87
136
|
};
|
|
88
137
|
|
|
@@ -242,6 +291,33 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
242
291
|
handler: handleTeamManagerCommand,
|
|
243
292
|
});
|
|
244
293
|
|
|
294
|
+
pi.registerCommand("team-result", {
|
|
295
|
+
description: "Open a pi-crew agent result viewer: <runId> [taskId]",
|
|
296
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
297
|
+
const [runId, rawTaskId] = args.trim().split(/\s+/).filter(Boolean);
|
|
298
|
+
const selected = await selectAgentTask(ctx, runId, rawTaskId);
|
|
299
|
+
const loaded = selected ? loadRunManifestById(ctx.cwd, selected.runId) : undefined;
|
|
300
|
+
if (ctx.hasUI && loaded) {
|
|
301
|
+
const agent = readCrewAgents(loaded.manifest).find((item) => item.taskId === selected?.taskId || item.id === selected?.taskId) ?? readCrewAgents(loaded.manifest)[0];
|
|
302
|
+
const text = agent?.resultArtifactPath ? commandText(await handleTeamTool({ action: "api", runId: selected!.runId, config: { operation: "read-agent-output", agentId: agent.taskId, maxBytes: 64_000 } }, ctx)) : "(no result)";
|
|
303
|
+
await ctx.ui.custom<undefined>((_tui, theme, _keybindings, done) => new DurableTextViewer("pi-crew result", `${selected!.runId}:${agent?.taskId ?? "unknown"}`, text.split(/\r?\n/), theme, done), { overlay: true, overlayOptions: { width: "90%", maxHeight: "85%", anchor: "center" } });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const result = await handleTeamTool({ action: "api", runId, config: { operation: "read-agent-output", agentId: rawTaskId, maxBytes: 64_000 } }, ctx);
|
|
307
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
pi.registerCommand("team-transcript", {
|
|
312
|
+
description: "Open a pi-crew transcript viewer: <runId> [taskId]",
|
|
313
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
314
|
+
const [runId, taskId] = args.trim().split(/\s+/).filter(Boolean);
|
|
315
|
+
if (await openTranscriptViewer(ctx, runId, taskId)) return;
|
|
316
|
+
const result = await handleTeamTool({ action: "api", runId, config: { operation: "read-agent-transcript", agentId: taskId } }, ctx);
|
|
317
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
|
|
245
321
|
pi.registerCommand("team-dashboard", {
|
|
246
322
|
description: "Open a pi-crew run dashboard overlay",
|
|
247
323
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
@@ -253,6 +329,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
253
329
|
});
|
|
254
330
|
if (!selection) return;
|
|
255
331
|
if (selection.action === "reload") continue;
|
|
332
|
+
if (selection.action === "agent-transcript" && await openTranscriptViewer(ctx, selection.runId)) continue;
|
|
256
333
|
const result = selection.action === "api"
|
|
257
334
|
? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-manifest" } }, ctx)
|
|
258
335
|
: selection.action === "agents"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { createFileCoalescer } from "../utils/file-coalescer.ts";
|
|
4
|
+
|
|
5
|
+
export interface ResultWatcherEvents {
|
|
6
|
+
emit(event: string, data: unknown): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ResultWatcherHandle {
|
|
10
|
+
start(): void;
|
|
11
|
+
prime(): void;
|
|
12
|
+
stop(): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ResultWatcherOptions {
|
|
16
|
+
eventName?: string;
|
|
17
|
+
completionTtlMs?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readJson(filePath: string): unknown | undefined {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8")) as unknown;
|
|
23
|
+
} catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function completionKey(payload: unknown, file: string): string {
|
|
29
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return `file:${file}`;
|
|
30
|
+
const obj = payload as Record<string, unknown>;
|
|
31
|
+
const id = [obj.runId, obj.sessionId, obj.id, obj.status].filter((entry): entry is string => typeof entry === "string" && entry.length > 0).join(":");
|
|
32
|
+
return id || `file:${file}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createResultWatcher(events: ResultWatcherEvents, resultsDir: string, eventNameOrOptions: string | ResultWatcherOptions = "pi-crew:run-result"): ResultWatcherHandle {
|
|
36
|
+
const options = typeof eventNameOrOptions === "string" ? { eventName: eventNameOrOptions } : eventNameOrOptions;
|
|
37
|
+
const eventName = options.eventName ?? "pi-crew:run-result";
|
|
38
|
+
const completionTtlMs = options.completionTtlMs ?? 5 * 60_000;
|
|
39
|
+
const seen = new Map<string, number>();
|
|
40
|
+
let watcher: fs.FSWatcher | undefined;
|
|
41
|
+
let restartTimer: ReturnType<typeof setTimeout> | undefined;
|
|
42
|
+
const coalescer = createFileCoalescer((file) => {
|
|
43
|
+
const filePath = path.join(resultsDir, file);
|
|
44
|
+
if (!file.endsWith(".json") || !fs.existsSync(filePath)) return;
|
|
45
|
+
const payload = readJson(filePath);
|
|
46
|
+
if (payload !== undefined) {
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
for (const [key, expiresAt] of seen) if (expiresAt <= now) seen.delete(key);
|
|
49
|
+
const key = completionKey(payload, file);
|
|
50
|
+
if (!seen.has(key)) {
|
|
51
|
+
seen.set(key, now + completionTtlMs);
|
|
52
|
+
events.emit(eventName, payload);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
try { fs.unlinkSync(filePath); } catch {}
|
|
56
|
+
}, 50);
|
|
57
|
+
const scheduleRestart = () => {
|
|
58
|
+
if (restartTimer) clearTimeout(restartTimer);
|
|
59
|
+
restartTimer = setTimeout(() => {
|
|
60
|
+
restartTimer = undefined;
|
|
61
|
+
try { handle.start(); } catch {}
|
|
62
|
+
}, 3000);
|
|
63
|
+
restartTimer.unref?.();
|
|
64
|
+
};
|
|
65
|
+
const handle: ResultWatcherHandle = {
|
|
66
|
+
start() {
|
|
67
|
+
fs.mkdirSync(resultsDir, { recursive: true });
|
|
68
|
+
watcher?.close();
|
|
69
|
+
watcher = fs.watch(resultsDir, (event, file) => {
|
|
70
|
+
if (event !== "rename" || !file) return;
|
|
71
|
+
coalescer.schedule(file.toString());
|
|
72
|
+
});
|
|
73
|
+
watcher.on("error", scheduleRestart);
|
|
74
|
+
watcher.unref?.();
|
|
75
|
+
},
|
|
76
|
+
prime() {
|
|
77
|
+
if (!fs.existsSync(resultsDir)) return;
|
|
78
|
+
for (const file of fs.readdirSync(resultsDir).filter((entry) => entry.endsWith(".json"))) coalescer.schedule(file, 0);
|
|
79
|
+
},
|
|
80
|
+
stop() {
|
|
81
|
+
watcher?.close();
|
|
82
|
+
watcher = undefined;
|
|
83
|
+
if (restartTimer) clearTimeout(restartTimer);
|
|
84
|
+
restartTimer = undefined;
|
|
85
|
+
coalescer.clear();
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
return handle;
|
|
89
|
+
}
|
|
@@ -41,6 +41,9 @@ import { probeLiveSessionRuntime } from "../runtime/live-session-runtime.ts";
|
|
|
41
41
|
import { applyAttentionState, formatActivityAge, resolveCrewControlConfig } from "../runtime/agent-control.ts";
|
|
42
42
|
import { buildAgentDashboard, readAgentOutput } from "../runtime/agent-observability.ts";
|
|
43
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";
|
|
44
47
|
|
|
45
48
|
export interface TeamToolDetails {
|
|
46
49
|
action: string;
|
|
@@ -49,7 +52,11 @@ export interface TeamToolDetails {
|
|
|
49
52
|
artifactsRoot?: string;
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
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
|
+
};
|
|
53
60
|
|
|
54
61
|
function result(text: string, details: TeamToolDetails, isError = false): PiTeamsToolResult {
|
|
55
62
|
return toolResult(text, details, isError);
|
|
@@ -59,6 +66,29 @@ function formatScoped(name: string, source: string, description: string): string
|
|
|
59
66
|
return `- ${name} (${source}): ${description}`;
|
|
60
67
|
}
|
|
61
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
|
+
|
|
62
92
|
export function handleList(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
63
93
|
const resource = params.resource;
|
|
64
94
|
const blocks: string[] = [];
|
|
@@ -136,6 +166,18 @@ function commandExists(command: string, args: string[]): { ok: boolean; detail:
|
|
|
136
166
|
return { ok: false, detail: output.error?.message ?? firstOutputLine(output.stdout, output.stderr) };
|
|
137
167
|
}
|
|
138
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
|
+
|
|
139
181
|
function piCommandExists(): { ok: boolean; detail: string } {
|
|
140
182
|
const spec = getPiSpawnCommand(["--version"]);
|
|
141
183
|
const output = spawnSync(spec.command, spec.args, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -261,9 +303,10 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
261
303
|
return result(text, { action: "run", status: "ok", runId: updatedManifest.runId, artifactsRoot: updatedManifest.artifactsRoot });
|
|
262
304
|
}
|
|
263
305
|
|
|
264
|
-
const runtime = await resolveCrewRuntime(loadedConfig.config);
|
|
306
|
+
const runtime = await resolveCrewRuntime(effectiveRunConfig(loadedConfig.config, params.config));
|
|
265
307
|
const executeWorkers = runtime.kind === "child-process";
|
|
266
|
-
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 });
|
|
267
310
|
const text = [
|
|
268
311
|
`Created pi-crew run ${executed.manifest.runId}.`,
|
|
269
312
|
`Team: ${team.name}`,
|
|
@@ -274,9 +317,11 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
274
317
|
`Artifacts: ${executed.manifest.artifactsRoot}`,
|
|
275
318
|
"",
|
|
276
319
|
`Runtime: ${runtime.kind}${runtime.fallback ? ` (fallback from ${runtime.requestedMode})` : ""}${runtime.reason ? ` - ${runtime.reason}` : ""}`,
|
|
277
|
-
|
|
320
|
+
runtime.kind === "child-process"
|
|
278
321
|
? "Child Pi worker execution was enabled."
|
|
279
|
-
:
|
|
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.",
|
|
280
325
|
].join("\n");
|
|
281
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");
|
|
282
327
|
}
|
|
@@ -384,7 +429,7 @@ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
384
429
|
const loadedConfig = loadConfig(ctx.cwd);
|
|
385
430
|
const runtime = await resolveCrewRuntime(loadedConfig.config);
|
|
386
431
|
const executeWorkers = runtime.kind === "child-process";
|
|
387
|
-
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 });
|
|
388
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");
|
|
389
434
|
});
|
|
390
435
|
}
|
|
@@ -460,12 +505,39 @@ function autonomousPatchFromConfig(config: unknown): PiTeamsAutonomousConfig {
|
|
|
460
505
|
function configPatchFromConfig(config: unknown): PiTeamsConfig {
|
|
461
506
|
const cfg = configRecord(config);
|
|
462
507
|
const control = configRecord(cfg.control);
|
|
508
|
+
const runtime = configRecord(cfg.runtime);
|
|
509
|
+
const limits = configRecord(cfg.limits);
|
|
510
|
+
const worktree = configRecord(cfg.worktree);
|
|
463
511
|
return {
|
|
464
512
|
asyncByDefault: typeof cfg.asyncByDefault === "boolean" ? cfg.asyncByDefault : undefined,
|
|
465
513
|
executeWorkers: typeof cfg.executeWorkers === "boolean" ? cfg.executeWorkers : undefined,
|
|
466
514
|
notifierIntervalMs: typeof cfg.notifierIntervalMs === "number" && Number.isFinite(cfg.notifierIntervalMs) ? cfg.notifierIntervalMs : undefined,
|
|
467
515
|
requireCleanWorktreeLeader: typeof cfg.requireCleanWorktreeLeader === "boolean" ? cfg.requireCleanWorktreeLeader : undefined,
|
|
468
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,
|
|
469
541
|
control: Object.keys(control).length > 0 ? {
|
|
470
542
|
enabled: typeof control.enabled === "boolean" ? control.enabled : undefined,
|
|
471
543
|
needsAttentionAfterMs: typeof control.needsAttentionAfterMs === "number" && Number.isInteger(control.needsAttentionAfterMs) && control.needsAttentionAfterMs > 0 ? control.needsAttentionAfterMs : undefined,
|
|
@@ -657,12 +729,34 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
657
729
|
appendEvent(loaded.manifest.eventsPath, { type: "agent.nudged", runId: loaded.manifest.runId, taskId: agent.taskId, message: messageText, data: { agentId: agent.id, mailboxMessageId: message.id } });
|
|
658
730
|
return result(JSON.stringify({ agentId: agent.id, mailboxMessage: message }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
659
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
|
+
}
|
|
660
735
|
if (operation === "steer-agent" || operation === "stop-agent" || operation === "resume-agent" || operation === "interrupt-agent") {
|
|
661
|
-
const
|
|
662
|
-
if (!
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
+
}
|
|
666
760
|
}
|
|
667
761
|
if (operation === "read-mailbox") {
|
|
668
762
|
const direction = cfg.direction === "inbox" || cfg.direction === "outbox" ? cfg.direction as MailboxDirection : undefined;
|