pi-crew 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/agents/agent-config.ts +3 -0
- package/src/agents/discover-agents.ts +34 -5
- package/src/config/config.ts +111 -11
- package/src/extension/async-notifier.ts +3 -1
- package/src/extension/cross-extension-rpc.ts +82 -0
- package/src/extension/register.ts +19 -3
- package/src/extension/result-watcher.ts +89 -0
- package/src/extension/team-tool.ts +145 -19
- package/src/prompt/prompt-runtime.ts +12 -2
- package/src/runtime/agent-memory.ts +72 -0
- package/src/runtime/agent-observability.ts +88 -0
- package/src/runtime/child-pi.ts +60 -4
- package/src/runtime/crew-agent-records.ts +42 -4
- package/src/runtime/crew-agent-runtime.ts +1 -0
- package/src/runtime/foreground-control.ts +82 -0
- package/src/runtime/live-agent-control.ts +78 -0
- package/src/runtime/live-agent-manager.ts +85 -0
- package/src/runtime/live-control-realtime.ts +36 -0
- package/src/runtime/live-session-runtime.ts +271 -5
- package/src/runtime/pi-args.ts +29 -0
- package/src/runtime/runtime-resolver.ts +1 -1
- package/src/runtime/sidechain-output.ts +28 -0
- package/src/runtime/task-runner.ts +83 -12
- package/src/runtime/team-runner.ts +4 -1
- package/src/state/event-log.ts +19 -0
- package/src/ui/run-dashboard.ts +82 -10
- package/src/utils/file-coalescer.ts +33 -0
- package/src/worktree/worktree-manager.ts +73 -2
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { AgentConfig, ResourceSource } from "./agent-config.ts";
|
|
4
|
+
import { loadConfig } from "../config/config.ts";
|
|
4
5
|
import { parseCsv, parseFrontmatter } from "../utils/frontmatter.ts";
|
|
5
6
|
import { packageRoot, projectPiRoot, userPiRoot } from "../utils/paths.ts";
|
|
6
7
|
|
|
@@ -14,6 +15,10 @@ function parseCost(value: string | undefined): "free" | "cheap" | "expensive" |
|
|
|
14
15
|
return value === "free" || value === "cheap" || value === "expensive" ? value : undefined;
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
function parseMemory(value: string | undefined): "user" | "project" | "local" | undefined {
|
|
19
|
+
return value === "user" || value === "project" || value === "local" ? value : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
function parseAgentFile(filePath: string, source: ResourceSource): AgentConfig | undefined {
|
|
18
23
|
try {
|
|
19
24
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -40,6 +45,8 @@ function parseAgentFile(filePath: string, source: ResourceSource): AgentConfig |
|
|
|
40
45
|
systemPromptMode: frontmatter.systemPromptMode === "append" ? "append" : "replace",
|
|
41
46
|
inheritProjectContext: frontmatter.inheritProjectContext === "true",
|
|
42
47
|
inheritSkills: frontmatter.inheritSkills === "true",
|
|
48
|
+
memory: parseMemory(frontmatter.memory),
|
|
49
|
+
disabled: frontmatter.disabled === "true" || frontmatter.enabled === "false",
|
|
43
50
|
routing: triggers || useWhen || avoidWhen || cost || category ? { triggers, useWhen, avoidWhen, cost, category } : undefined,
|
|
44
51
|
};
|
|
45
52
|
} catch {
|
|
@@ -56,18 +63,40 @@ function readAgentDir(dir: string, source: ResourceSource): AgentConfig[] {
|
|
|
56
63
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
57
64
|
}
|
|
58
65
|
|
|
66
|
+
function applyAgentOverrides(agents: AgentConfig[], cwd: string): AgentConfig[] {
|
|
67
|
+
const loaded = loadConfig(cwd);
|
|
68
|
+
const config = loaded.config.agents;
|
|
69
|
+
const overrides = config?.overrides ?? {};
|
|
70
|
+
return agents
|
|
71
|
+
.filter((agent) => !(config?.disableBuiltins && agent.source === "builtin"))
|
|
72
|
+
.map((agent) => {
|
|
73
|
+
const overrideEntry = Object.entries(overrides).find(([name]) => name.toLowerCase() === agent.name.toLowerCase());
|
|
74
|
+
if (!overrideEntry) return agent;
|
|
75
|
+
const [, override] = overrideEntry;
|
|
76
|
+
return {
|
|
77
|
+
...agent,
|
|
78
|
+
disabled: override.disabled ?? agent.disabled,
|
|
79
|
+
model: override.model === false ? undefined : override.model ?? agent.model,
|
|
80
|
+
fallbackModels: override.fallbackModels === false ? undefined : override.fallbackModels ?? agent.fallbackModels,
|
|
81
|
+
thinking: override.thinking === false ? undefined : override.thinking ?? agent.thinking,
|
|
82
|
+
tools: override.tools === false ? undefined : override.tools ?? agent.tools,
|
|
83
|
+
override: { source: "config", path: loaded.path },
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
59
88
|
export function discoverAgents(cwd: string): AgentDiscoveryResult {
|
|
60
89
|
return {
|
|
61
|
-
builtin: readAgentDir(path.join(packageRoot(), "agents"), "builtin"),
|
|
62
|
-
user: readAgentDir(path.join(userPiRoot(), "agents"), "user"),
|
|
63
|
-
project: readAgentDir(path.join(projectPiRoot(cwd), "agents"), "project"),
|
|
90
|
+
builtin: applyAgentOverrides(readAgentDir(path.join(packageRoot(), "agents"), "builtin"), cwd),
|
|
91
|
+
user: applyAgentOverrides(readAgentDir(path.join(userPiRoot(), "agents"), "user"), cwd),
|
|
92
|
+
project: applyAgentOverrides(readAgentDir(path.join(projectPiRoot(cwd), "agents"), "project"), cwd),
|
|
64
93
|
};
|
|
65
94
|
}
|
|
66
95
|
|
|
67
96
|
export function allAgents(discovery: AgentDiscoveryResult): AgentConfig[] {
|
|
68
97
|
const byName = new Map<string, AgentConfig>();
|
|
69
98
|
for (const agent of [...discovery.builtin, ...discovery.user, ...discovery.project]) {
|
|
70
|
-
byName.set(agent.name, agent);
|
|
99
|
+
byName.set(agent.name.toLowerCase(), agent);
|
|
71
100
|
}
|
|
72
|
-
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
101
|
+
return [...byName.values()].filter((agent) => !agent.disabled).sort((a, b) => a.name.localeCompare(b.name));
|
|
73
102
|
}
|
package/src/config/config.ts
CHANGED
|
@@ -41,6 +41,25 @@ 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
|
+
|
|
50
|
+
export interface AgentOverrideConfig {
|
|
51
|
+
disabled?: boolean;
|
|
52
|
+
model?: string | false;
|
|
53
|
+
fallbackModels?: string[] | false;
|
|
54
|
+
thinking?: string | false;
|
|
55
|
+
tools?: string[] | false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface CrewAgentsConfig {
|
|
59
|
+
disableBuiltins?: boolean;
|
|
60
|
+
overrides?: Record<string, AgentOverrideConfig>;
|
|
61
|
+
}
|
|
62
|
+
|
|
44
63
|
export interface PiTeamsConfig {
|
|
45
64
|
asyncByDefault?: boolean;
|
|
46
65
|
executeWorkers?: boolean;
|
|
@@ -50,6 +69,8 @@ export interface PiTeamsConfig {
|
|
|
50
69
|
limits?: CrewLimitsConfig;
|
|
51
70
|
runtime?: CrewRuntimeConfig;
|
|
52
71
|
control?: CrewControlConfig;
|
|
72
|
+
worktree?: CrewWorktreeConfig;
|
|
73
|
+
agents?: CrewAgentsConfig;
|
|
53
74
|
}
|
|
54
75
|
|
|
55
76
|
export interface LoadedPiTeamsConfig {
|
|
@@ -109,6 +130,23 @@ function mergeConfig(base: PiTeamsConfig, override: PiTeamsConfig): PiTeamsConfi
|
|
|
109
130
|
...withoutUndefined((override.control ?? {}) as Record<string, unknown>),
|
|
110
131
|
};
|
|
111
132
|
}
|
|
133
|
+
if (base.worktree || override.worktree) {
|
|
134
|
+
merged.worktree = {
|
|
135
|
+
...(base.worktree ?? {}),
|
|
136
|
+
...withoutUndefined((override.worktree ?? {}) as Record<string, unknown>),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (base.agents || override.agents) {
|
|
140
|
+
merged.agents = {
|
|
141
|
+
...(base.agents ?? {}),
|
|
142
|
+
...withoutUndefined((override.agents ?? {}) as Record<string, unknown>),
|
|
143
|
+
overrides: {
|
|
144
|
+
...(base.agents?.overrides ?? {}),
|
|
145
|
+
...(override.agents?.overrides ?? {}),
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (merged.agents?.overrides && Object.keys(merged.agents.overrides).length === 0) delete merged.agents.overrides;
|
|
112
150
|
return merged;
|
|
113
151
|
}
|
|
114
152
|
|
|
@@ -159,21 +197,33 @@ function parseAutonomousConfig(value: unknown): PiTeamsAutonomousConfig | undefi
|
|
|
159
197
|
};
|
|
160
198
|
}
|
|
161
199
|
|
|
162
|
-
|
|
163
|
-
|
|
200
|
+
const LIMIT_CEILINGS = {
|
|
201
|
+
maxConcurrentWorkers: 1024,
|
|
202
|
+
maxTaskDepth: 100,
|
|
203
|
+
maxChildrenPerTask: 1000,
|
|
204
|
+
maxRunMinutes: 1440,
|
|
205
|
+
maxRetriesPerTask: 100,
|
|
206
|
+
maxTasksPerRun: 10_000,
|
|
207
|
+
heartbeatStaleMs: 24 * 60 * 60 * 1000,
|
|
208
|
+
runtimeMaxTurns: 10_000,
|
|
209
|
+
runtimeGraceTurns: 1_000,
|
|
210
|
+
} as const;
|
|
211
|
+
|
|
212
|
+
function parsePositiveInteger(value: unknown, max = Number.MAX_SAFE_INTEGER): number | undefined {
|
|
213
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= max ? value : undefined;
|
|
164
214
|
}
|
|
165
215
|
|
|
166
216
|
function parseLimitsConfig(value: unknown): CrewLimitsConfig | undefined {
|
|
167
217
|
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
168
218
|
const obj = value as Record<string, unknown>;
|
|
169
219
|
const limits: CrewLimitsConfig = {
|
|
170
|
-
maxConcurrentWorkers: parsePositiveInteger(obj.maxConcurrentWorkers),
|
|
171
|
-
maxTaskDepth: parsePositiveInteger(obj.maxTaskDepth),
|
|
172
|
-
maxChildrenPerTask: parsePositiveInteger(obj.maxChildrenPerTask),
|
|
173
|
-
maxRunMinutes: parsePositiveInteger(obj.maxRunMinutes),
|
|
174
|
-
maxRetriesPerTask: parsePositiveInteger(obj.maxRetriesPerTask),
|
|
175
|
-
maxTasksPerRun: parsePositiveInteger(obj.maxTasksPerRun),
|
|
176
|
-
heartbeatStaleMs: parsePositiveInteger(obj.heartbeatStaleMs),
|
|
220
|
+
maxConcurrentWorkers: parsePositiveInteger(obj.maxConcurrentWorkers, LIMIT_CEILINGS.maxConcurrentWorkers),
|
|
221
|
+
maxTaskDepth: parsePositiveInteger(obj.maxTaskDepth, LIMIT_CEILINGS.maxTaskDepth),
|
|
222
|
+
maxChildrenPerTask: parsePositiveInteger(obj.maxChildrenPerTask, LIMIT_CEILINGS.maxChildrenPerTask),
|
|
223
|
+
maxRunMinutes: parsePositiveInteger(obj.maxRunMinutes, LIMIT_CEILINGS.maxRunMinutes),
|
|
224
|
+
maxRetriesPerTask: parsePositiveInteger(obj.maxRetriesPerTask, LIMIT_CEILINGS.maxRetriesPerTask),
|
|
225
|
+
maxTasksPerRun: parsePositiveInteger(obj.maxTasksPerRun, LIMIT_CEILINGS.maxTasksPerRun),
|
|
226
|
+
heartbeatStaleMs: parsePositiveInteger(obj.heartbeatStaleMs, LIMIT_CEILINGS.heartbeatStaleMs),
|
|
177
227
|
};
|
|
178
228
|
return Object.values(limits).some((entry) => entry !== undefined) ? limits : undefined;
|
|
179
229
|
}
|
|
@@ -189,8 +239,8 @@ function parseRuntimeConfig(value: unknown): CrewRuntimeConfig | undefined {
|
|
|
189
239
|
mode: parseRuntimeMode(obj.mode),
|
|
190
240
|
preferLiveSession: typeof obj.preferLiveSession === "boolean" ? obj.preferLiveSession : undefined,
|
|
191
241
|
allowChildProcessFallback: typeof obj.allowChildProcessFallback === "boolean" ? obj.allowChildProcessFallback : undefined,
|
|
192
|
-
maxTurns: parsePositiveInteger(obj.maxTurns),
|
|
193
|
-
graceTurns: parsePositiveInteger(obj.graceTurns),
|
|
242
|
+
maxTurns: parsePositiveInteger(obj.maxTurns, LIMIT_CEILINGS.runtimeMaxTurns),
|
|
243
|
+
graceTurns: parsePositiveInteger(obj.graceTurns, LIMIT_CEILINGS.runtimeGraceTurns),
|
|
194
244
|
inheritContext: typeof obj.inheritContext === "boolean" ? obj.inheritContext : undefined,
|
|
195
245
|
promptMode: obj.promptMode === "replace" || obj.promptMode === "append" ? obj.promptMode : undefined,
|
|
196
246
|
groupJoin: obj.groupJoin === "off" || obj.groupJoin === "group" || obj.groupJoin === "smart" ? obj.groupJoin : undefined,
|
|
@@ -208,6 +258,54 @@ function parseControlConfig(value: unknown): CrewControlConfig | undefined {
|
|
|
208
258
|
return Object.values(control).some((entry) => entry !== undefined) ? control : undefined;
|
|
209
259
|
}
|
|
210
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
|
+
|
|
272
|
+
function parseStringArrayOrFalse(value: unknown): string[] | false | undefined {
|
|
273
|
+
if (value === false) return false;
|
|
274
|
+
if (typeof value === "string") return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
275
|
+
if (Array.isArray(value)) return value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim());
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function parseAgentOverride(value: unknown): AgentOverrideConfig | undefined {
|
|
280
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
281
|
+
const obj = value as Record<string, unknown>;
|
|
282
|
+
const override: AgentOverrideConfig = {
|
|
283
|
+
disabled: typeof obj.disabled === "boolean" ? obj.disabled : undefined,
|
|
284
|
+
model: typeof obj.model === "string" || obj.model === false ? obj.model : undefined,
|
|
285
|
+
fallbackModels: parseStringArrayOrFalse(obj.fallbackModels),
|
|
286
|
+
thinking: typeof obj.thinking === "string" || obj.thinking === false ? obj.thinking : undefined,
|
|
287
|
+
tools: parseStringArrayOrFalse(obj.tools),
|
|
288
|
+
};
|
|
289
|
+
return Object.values(override).some((entry) => entry !== undefined) ? override : undefined;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function parseAgentsConfig(value: unknown): CrewAgentsConfig | undefined {
|
|
293
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
294
|
+
const obj = value as Record<string, unknown>;
|
|
295
|
+
const overrides: Record<string, AgentOverrideConfig> = {};
|
|
296
|
+
if (obj.overrides && typeof obj.overrides === "object" && !Array.isArray(obj.overrides)) {
|
|
297
|
+
for (const [name, rawOverride] of Object.entries(obj.overrides)) {
|
|
298
|
+
const parsed = parseAgentOverride(rawOverride);
|
|
299
|
+
if (parsed) overrides[name] = parsed;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const agents: CrewAgentsConfig = {
|
|
303
|
+
disableBuiltins: typeof obj.disableBuiltins === "boolean" ? obj.disableBuiltins : undefined,
|
|
304
|
+
overrides: Object.keys(overrides).length > 0 ? overrides : undefined,
|
|
305
|
+
};
|
|
306
|
+
return Object.values(agents).some((entry) => entry !== undefined) ? agents : undefined;
|
|
307
|
+
}
|
|
308
|
+
|
|
211
309
|
function parseConfig(raw: unknown): PiTeamsConfig {
|
|
212
310
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
213
311
|
const obj = raw as Record<string, unknown>;
|
|
@@ -220,6 +318,8 @@ function parseConfig(raw: unknown): PiTeamsConfig {
|
|
|
220
318
|
limits: parseLimitsConfig(obj.limits),
|
|
221
319
|
runtime: parseRuntimeConfig(obj.runtime),
|
|
222
320
|
control: parseControlConfig(obj.control),
|
|
321
|
+
worktree: parseWorktreeConfig(obj.worktree),
|
|
322
|
+
agents: parseAgentsConfig(obj.agents),
|
|
223
323
|
};
|
|
224
324
|
}
|
|
225
325
|
|
|
@@ -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 = {
|
|
@@ -167,7 +175,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
167
175
|
const config: Record<string, unknown> = { operation };
|
|
168
176
|
for (const token of tokens.filter((item) => item.includes("="))) {
|
|
169
177
|
const [key, ...rest] = token.split("=");
|
|
170
|
-
if (key) config[key] = rest.join("=");
|
|
178
|
+
if (key) config[key] = parseScalar(rest.join("="));
|
|
171
179
|
}
|
|
172
180
|
const result = await handleTeamTool({ action: "api", runId, config }, ctx);
|
|
173
181
|
await notifyCommandResult(ctx, commandText(result));
|
|
@@ -255,7 +263,15 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
255
263
|
if (selection.action === "reload") continue;
|
|
256
264
|
const result = selection.action === "api"
|
|
257
265
|
? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-manifest" } }, ctx)
|
|
258
|
-
:
|
|
266
|
+
: selection.action === "agents"
|
|
267
|
+
? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "agent-dashboard" } }, ctx)
|
|
268
|
+
: selection.action === "agent-events"
|
|
269
|
+
? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-events", limit: 50 } }, ctx)
|
|
270
|
+
: selection.action === "agent-output"
|
|
271
|
+
? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-output", maxBytes: 32_000 } }, ctx)
|
|
272
|
+
: selection.action === "agent-transcript"
|
|
273
|
+
? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-transcript" } }, ctx)
|
|
274
|
+
: await handleTeamTool({ action: selection.action, runId: selection.runId }, ctx);
|
|
259
275
|
await notifyCommandResult(ctx, commandText(result));
|
|
260
276
|
return;
|
|
261
277
|
}
|
|
@@ -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
|
+
}
|