pi-crew 0.1.7 → 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 +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/register.ts +9 -1
- 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/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/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
|
+
}
|
|
@@ -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,7 @@ 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";
|
|
12
13
|
|
|
13
14
|
function parseRunArgs(args: string): TeamToolParamsValue {
|
|
14
15
|
const tokens = args.match(/"[^"]*"|'[^']*'|\S+/g)?.map((token) => token.replace(/^['"]|['"]$/g, "")) ?? [];
|
|
@@ -64,15 +65,22 @@ function setNestedConfig(config: Record<string, unknown>, key: string, value: un
|
|
|
64
65
|
|
|
65
66
|
export function registerPiTeams(pi: ExtensionAPI): void {
|
|
66
67
|
const notifierState: AsyncNotifierState = { seenFinishedRunIds: new Set() };
|
|
68
|
+
let currentCtx: ExtensionContext | undefined;
|
|
69
|
+
let rpcHandle: PiCrewRpcHandle | undefined;
|
|
67
70
|
registerAutonomousPolicy(pi);
|
|
71
|
+
rpcHandle = registerPiCrewRpc((pi as unknown as { events?: Parameters<typeof registerPiCrewRpc>[0] }).events, () => currentCtx);
|
|
68
72
|
|
|
69
73
|
pi.on("session_start", (_event, ctx) => {
|
|
74
|
+
currentCtx = ctx;
|
|
70
75
|
notifyActiveRuns(ctx);
|
|
71
76
|
const loadedConfig = loadConfig(ctx.cwd);
|
|
72
77
|
startAsyncRunNotifier(ctx, notifierState, loadedConfig.config.notifierIntervalMs ?? 5000);
|
|
73
78
|
});
|
|
74
79
|
pi.on("session_shutdown", () => {
|
|
75
80
|
stopAsyncRunNotifier(notifierState);
|
|
81
|
+
currentCtx = undefined;
|
|
82
|
+
rpcHandle?.unsubscribe();
|
|
83
|
+
rpcHandle = undefined;
|
|
76
84
|
});
|
|
77
85
|
|
|
78
86
|
const tool: ToolDefinition = {
|
|
@@ -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;
|
|
@@ -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
|
+
}
|