pi-crew 0.1.6 → 0.1.7
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 +2 -0
- package/src/agents/discover-agents.ts +29 -5
- package/src/config/config.ts +86 -11
- package/src/extension/register.ts +10 -2
- package/src/extension/team-tool.ts +40 -8
- package/src/prompt/prompt-runtime.ts +12 -2
- 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/pi-args.ts +29 -0
- package/src/runtime/task-runner.ts +7 -1
- package/src/runtime/team-runner.ts +1 -1
- package/src/state/event-log.ts +19 -0
- package/src/ui/run-dashboard.ts +82 -10
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
|
|
|
@@ -40,6 +41,7 @@ function parseAgentFile(filePath: string, source: ResourceSource): AgentConfig |
|
|
|
40
41
|
systemPromptMode: frontmatter.systemPromptMode === "append" ? "append" : "replace",
|
|
41
42
|
inheritProjectContext: frontmatter.inheritProjectContext === "true",
|
|
42
43
|
inheritSkills: frontmatter.inheritSkills === "true",
|
|
44
|
+
disabled: frontmatter.disabled === "true" || frontmatter.enabled === "false",
|
|
43
45
|
routing: triggers || useWhen || avoidWhen || cost || category ? { triggers, useWhen, avoidWhen, cost, category } : undefined,
|
|
44
46
|
};
|
|
45
47
|
} catch {
|
|
@@ -56,18 +58,40 @@ function readAgentDir(dir: string, source: ResourceSource): AgentConfig[] {
|
|
|
56
58
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
function applyAgentOverrides(agents: AgentConfig[], cwd: string): AgentConfig[] {
|
|
62
|
+
const loaded = loadConfig(cwd);
|
|
63
|
+
const config = loaded.config.agents;
|
|
64
|
+
const overrides = config?.overrides ?? {};
|
|
65
|
+
return agents
|
|
66
|
+
.filter((agent) => !(config?.disableBuiltins && agent.source === "builtin"))
|
|
67
|
+
.map((agent) => {
|
|
68
|
+
const overrideEntry = Object.entries(overrides).find(([name]) => name.toLowerCase() === agent.name.toLowerCase());
|
|
69
|
+
if (!overrideEntry) return agent;
|
|
70
|
+
const [, override] = overrideEntry;
|
|
71
|
+
return {
|
|
72
|
+
...agent,
|
|
73
|
+
disabled: override.disabled ?? agent.disabled,
|
|
74
|
+
model: override.model === false ? undefined : override.model ?? agent.model,
|
|
75
|
+
fallbackModels: override.fallbackModels === false ? undefined : override.fallbackModels ?? agent.fallbackModels,
|
|
76
|
+
thinking: override.thinking === false ? undefined : override.thinking ?? agent.thinking,
|
|
77
|
+
tools: override.tools === false ? undefined : override.tools ?? agent.tools,
|
|
78
|
+
override: { source: "config", path: loaded.path },
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
59
83
|
export function discoverAgents(cwd: string): AgentDiscoveryResult {
|
|
60
84
|
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"),
|
|
85
|
+
builtin: applyAgentOverrides(readAgentDir(path.join(packageRoot(), "agents"), "builtin"), cwd),
|
|
86
|
+
user: applyAgentOverrides(readAgentDir(path.join(userPiRoot(), "agents"), "user"), cwd),
|
|
87
|
+
project: applyAgentOverrides(readAgentDir(path.join(projectPiRoot(cwd), "agents"), "project"), cwd),
|
|
64
88
|
};
|
|
65
89
|
}
|
|
66
90
|
|
|
67
91
|
export function allAgents(discovery: AgentDiscoveryResult): AgentConfig[] {
|
|
68
92
|
const byName = new Map<string, AgentConfig>();
|
|
69
93
|
for (const agent of [...discovery.builtin, ...discovery.user, ...discovery.project]) {
|
|
70
|
-
byName.set(agent.name, agent);
|
|
94
|
+
byName.set(agent.name.toLowerCase(), agent);
|
|
71
95
|
}
|
|
72
|
-
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
96
|
+
return [...byName.values()].filter((agent) => !agent.disabled).sort((a, b) => a.name.localeCompare(b.name));
|
|
73
97
|
}
|
package/src/config/config.ts
CHANGED
|
@@ -41,6 +41,19 @@ export interface CrewControlConfig {
|
|
|
41
41
|
needsAttentionAfterMs?: number;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
export interface AgentOverrideConfig {
|
|
45
|
+
disabled?: boolean;
|
|
46
|
+
model?: string | false;
|
|
47
|
+
fallbackModels?: string[] | false;
|
|
48
|
+
thinking?: string | false;
|
|
49
|
+
tools?: string[] | false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface CrewAgentsConfig {
|
|
53
|
+
disableBuiltins?: boolean;
|
|
54
|
+
overrides?: Record<string, AgentOverrideConfig>;
|
|
55
|
+
}
|
|
56
|
+
|
|
44
57
|
export interface PiTeamsConfig {
|
|
45
58
|
asyncByDefault?: boolean;
|
|
46
59
|
executeWorkers?: boolean;
|
|
@@ -50,6 +63,7 @@ export interface PiTeamsConfig {
|
|
|
50
63
|
limits?: CrewLimitsConfig;
|
|
51
64
|
runtime?: CrewRuntimeConfig;
|
|
52
65
|
control?: CrewControlConfig;
|
|
66
|
+
agents?: CrewAgentsConfig;
|
|
53
67
|
}
|
|
54
68
|
|
|
55
69
|
export interface LoadedPiTeamsConfig {
|
|
@@ -109,6 +123,17 @@ function mergeConfig(base: PiTeamsConfig, override: PiTeamsConfig): PiTeamsConfi
|
|
|
109
123
|
...withoutUndefined((override.control ?? {}) as Record<string, unknown>),
|
|
110
124
|
};
|
|
111
125
|
}
|
|
126
|
+
if (base.agents || override.agents) {
|
|
127
|
+
merged.agents = {
|
|
128
|
+
...(base.agents ?? {}),
|
|
129
|
+
...withoutUndefined((override.agents ?? {}) as Record<string, unknown>),
|
|
130
|
+
overrides: {
|
|
131
|
+
...(base.agents?.overrides ?? {}),
|
|
132
|
+
...(override.agents?.overrides ?? {}),
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (merged.agents?.overrides && Object.keys(merged.agents.overrides).length === 0) delete merged.agents.overrides;
|
|
112
137
|
return merged;
|
|
113
138
|
}
|
|
114
139
|
|
|
@@ -159,21 +184,33 @@ function parseAutonomousConfig(value: unknown): PiTeamsAutonomousConfig | undefi
|
|
|
159
184
|
};
|
|
160
185
|
}
|
|
161
186
|
|
|
162
|
-
|
|
163
|
-
|
|
187
|
+
const LIMIT_CEILINGS = {
|
|
188
|
+
maxConcurrentWorkers: 1024,
|
|
189
|
+
maxTaskDepth: 100,
|
|
190
|
+
maxChildrenPerTask: 1000,
|
|
191
|
+
maxRunMinutes: 1440,
|
|
192
|
+
maxRetriesPerTask: 100,
|
|
193
|
+
maxTasksPerRun: 10_000,
|
|
194
|
+
heartbeatStaleMs: 24 * 60 * 60 * 1000,
|
|
195
|
+
runtimeMaxTurns: 10_000,
|
|
196
|
+
runtimeGraceTurns: 1_000,
|
|
197
|
+
} as const;
|
|
198
|
+
|
|
199
|
+
function parsePositiveInteger(value: unknown, max = Number.MAX_SAFE_INTEGER): number | undefined {
|
|
200
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= max ? value : undefined;
|
|
164
201
|
}
|
|
165
202
|
|
|
166
203
|
function parseLimitsConfig(value: unknown): CrewLimitsConfig | undefined {
|
|
167
204
|
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
168
205
|
const obj = value as Record<string, unknown>;
|
|
169
206
|
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),
|
|
207
|
+
maxConcurrentWorkers: parsePositiveInteger(obj.maxConcurrentWorkers, LIMIT_CEILINGS.maxConcurrentWorkers),
|
|
208
|
+
maxTaskDepth: parsePositiveInteger(obj.maxTaskDepth, LIMIT_CEILINGS.maxTaskDepth),
|
|
209
|
+
maxChildrenPerTask: parsePositiveInteger(obj.maxChildrenPerTask, LIMIT_CEILINGS.maxChildrenPerTask),
|
|
210
|
+
maxRunMinutes: parsePositiveInteger(obj.maxRunMinutes, LIMIT_CEILINGS.maxRunMinutes),
|
|
211
|
+
maxRetriesPerTask: parsePositiveInteger(obj.maxRetriesPerTask, LIMIT_CEILINGS.maxRetriesPerTask),
|
|
212
|
+
maxTasksPerRun: parsePositiveInteger(obj.maxTasksPerRun, LIMIT_CEILINGS.maxTasksPerRun),
|
|
213
|
+
heartbeatStaleMs: parsePositiveInteger(obj.heartbeatStaleMs, LIMIT_CEILINGS.heartbeatStaleMs),
|
|
177
214
|
};
|
|
178
215
|
return Object.values(limits).some((entry) => entry !== undefined) ? limits : undefined;
|
|
179
216
|
}
|
|
@@ -189,8 +226,8 @@ function parseRuntimeConfig(value: unknown): CrewRuntimeConfig | undefined {
|
|
|
189
226
|
mode: parseRuntimeMode(obj.mode),
|
|
190
227
|
preferLiveSession: typeof obj.preferLiveSession === "boolean" ? obj.preferLiveSession : undefined,
|
|
191
228
|
allowChildProcessFallback: typeof obj.allowChildProcessFallback === "boolean" ? obj.allowChildProcessFallback : undefined,
|
|
192
|
-
maxTurns: parsePositiveInteger(obj.maxTurns),
|
|
193
|
-
graceTurns: parsePositiveInteger(obj.graceTurns),
|
|
229
|
+
maxTurns: parsePositiveInteger(obj.maxTurns, LIMIT_CEILINGS.runtimeMaxTurns),
|
|
230
|
+
graceTurns: parsePositiveInteger(obj.graceTurns, LIMIT_CEILINGS.runtimeGraceTurns),
|
|
194
231
|
inheritContext: typeof obj.inheritContext === "boolean" ? obj.inheritContext : undefined,
|
|
195
232
|
promptMode: obj.promptMode === "replace" || obj.promptMode === "append" ? obj.promptMode : undefined,
|
|
196
233
|
groupJoin: obj.groupJoin === "off" || obj.groupJoin === "group" || obj.groupJoin === "smart" ? obj.groupJoin : undefined,
|
|
@@ -208,6 +245,43 @@ function parseControlConfig(value: unknown): CrewControlConfig | undefined {
|
|
|
208
245
|
return Object.values(control).some((entry) => entry !== undefined) ? control : undefined;
|
|
209
246
|
}
|
|
210
247
|
|
|
248
|
+
function parseStringArrayOrFalse(value: unknown): string[] | false | undefined {
|
|
249
|
+
if (value === false) return false;
|
|
250
|
+
if (typeof value === "string") return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
251
|
+
if (Array.isArray(value)) return value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim());
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function parseAgentOverride(value: unknown): AgentOverrideConfig | undefined {
|
|
256
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
257
|
+
const obj = value as Record<string, unknown>;
|
|
258
|
+
const override: AgentOverrideConfig = {
|
|
259
|
+
disabled: typeof obj.disabled === "boolean" ? obj.disabled : undefined,
|
|
260
|
+
model: typeof obj.model === "string" || obj.model === false ? obj.model : undefined,
|
|
261
|
+
fallbackModels: parseStringArrayOrFalse(obj.fallbackModels),
|
|
262
|
+
thinking: typeof obj.thinking === "string" || obj.thinking === false ? obj.thinking : undefined,
|
|
263
|
+
tools: parseStringArrayOrFalse(obj.tools),
|
|
264
|
+
};
|
|
265
|
+
return Object.values(override).some((entry) => entry !== undefined) ? override : undefined;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function parseAgentsConfig(value: unknown): CrewAgentsConfig | undefined {
|
|
269
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
270
|
+
const obj = value as Record<string, unknown>;
|
|
271
|
+
const overrides: Record<string, AgentOverrideConfig> = {};
|
|
272
|
+
if (obj.overrides && typeof obj.overrides === "object" && !Array.isArray(obj.overrides)) {
|
|
273
|
+
for (const [name, rawOverride] of Object.entries(obj.overrides)) {
|
|
274
|
+
const parsed = parseAgentOverride(rawOverride);
|
|
275
|
+
if (parsed) overrides[name] = parsed;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const agents: CrewAgentsConfig = {
|
|
279
|
+
disableBuiltins: typeof obj.disableBuiltins === "boolean" ? obj.disableBuiltins : undefined,
|
|
280
|
+
overrides: Object.keys(overrides).length > 0 ? overrides : undefined,
|
|
281
|
+
};
|
|
282
|
+
return Object.values(agents).some((entry) => entry !== undefined) ? agents : undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
211
285
|
function parseConfig(raw: unknown): PiTeamsConfig {
|
|
212
286
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
213
287
|
const obj = raw as Record<string, unknown>;
|
|
@@ -220,6 +294,7 @@ function parseConfig(raw: unknown): PiTeamsConfig {
|
|
|
220
294
|
limits: parseLimitsConfig(obj.limits),
|
|
221
295
|
runtime: parseRuntimeConfig(obj.runtime),
|
|
222
296
|
control: parseControlConfig(obj.control),
|
|
297
|
+
agents: parseAgentsConfig(obj.agents),
|
|
223
298
|
};
|
|
224
299
|
}
|
|
225
300
|
|
|
@@ -167,7 +167,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
167
167
|
const config: Record<string, unknown> = { operation };
|
|
168
168
|
for (const token of tokens.filter((item) => item.includes("="))) {
|
|
169
169
|
const [key, ...rest] = token.split("=");
|
|
170
|
-
if (key) config[key] = rest.join("=");
|
|
170
|
+
if (key) config[key] = parseScalar(rest.join("="));
|
|
171
171
|
}
|
|
172
172
|
const result = await handleTeamTool({ action: "api", runId, config }, ctx);
|
|
173
173
|
await notifyCommandResult(ctx, commandText(result));
|
|
@@ -255,7 +255,15 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
255
255
|
if (selection.action === "reload") continue;
|
|
256
256
|
const result = selection.action === "api"
|
|
257
257
|
? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-manifest" } }, ctx)
|
|
258
|
-
:
|
|
258
|
+
: selection.action === "agents"
|
|
259
|
+
? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "agent-dashboard" } }, ctx)
|
|
260
|
+
: selection.action === "agent-events"
|
|
261
|
+
? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-events", limit: 50 } }, ctx)
|
|
262
|
+
: selection.action === "agent-output"
|
|
263
|
+
? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-output", maxBytes: 32_000 } }, ctx)
|
|
264
|
+
: selection.action === "agent-transcript"
|
|
265
|
+
? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-transcript" } }, ctx)
|
|
266
|
+
: await handleTeamTool({ action: selection.action, runId: selection.runId }, ctx);
|
|
259
267
|
await notifyCommandResult(ctx, commandText(result));
|
|
260
268
|
return;
|
|
261
269
|
}
|
|
@@ -21,7 +21,7 @@ import { getPiSpawnCommand } from "../runtime/pi-spawn.ts";
|
|
|
21
21
|
import { executeTeamRun } from "../runtime/team-runner.ts";
|
|
22
22
|
import { spawnBackgroundTeamRun } from "../runtime/async-runner.ts";
|
|
23
23
|
import { checkProcessLiveness, isActiveRunStatus } from "../runtime/process-status.ts";
|
|
24
|
-
import { appendEvent, readEvents } from "../state/event-log.ts";
|
|
24
|
+
import { appendEvent, readEvents, readEventsCursor } from "../state/event-log.ts";
|
|
25
25
|
import { cleanupRunWorktrees } from "../worktree/cleanup.ts";
|
|
26
26
|
import { piTeamsHelp } from "./help.ts";
|
|
27
27
|
import { initializeProject } from "./project-init.ts";
|
|
@@ -35,10 +35,12 @@ import { formatValidationReport, validateResources } from "./validate-resources.
|
|
|
35
35
|
import { formatRecommendation, recommendTeam } from "./team-recommendation.ts";
|
|
36
36
|
import { toolResult, type PiTeamsToolResult } from "./tool-result.ts";
|
|
37
37
|
import { touchWorkerHeartbeat } from "../runtime/worker-heartbeat.ts";
|
|
38
|
-
import { agentEventsPath, agentOutputPath, readCrewAgentEvents, readCrewAgentStatus, readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
38
|
+
import { agentEventsPath, agentOutputPath, readCrewAgentEvents, readCrewAgentEventsCursor, readCrewAgentStatus, readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
39
39
|
import { resolveCrewRuntime } from "../runtime/runtime-resolver.ts";
|
|
40
40
|
import { probeLiveSessionRuntime } from "../runtime/live-session-runtime.ts";
|
|
41
41
|
import { applyAttentionState, formatActivityAge, resolveCrewControlConfig } from "../runtime/agent-control.ts";
|
|
42
|
+
import { buildAgentDashboard, readAgentOutput } from "../runtime/agent-observability.ts";
|
|
43
|
+
import { readForegroundControlStatus, writeForegroundInterruptRequest } from "../runtime/foreground-control.ts";
|
|
42
44
|
|
|
43
45
|
export interface TeamToolDetails {
|
|
44
46
|
action: string;
|
|
@@ -575,7 +577,12 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
575
577
|
return result(JSON.stringify(task, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
576
578
|
}
|
|
577
579
|
if (operation === "read-events") {
|
|
578
|
-
|
|
580
|
+
const sinceSeq = typeof cfg.sinceSeq === "number" ? cfg.sinceSeq : undefined;
|
|
581
|
+
const limit = typeof cfg.limit === "number" ? cfg.limit : undefined;
|
|
582
|
+
const payload = sinceSeq !== undefined || limit !== undefined
|
|
583
|
+
? readEventsCursor(loaded.manifest.eventsPath, { sinceSeq, limit })
|
|
584
|
+
: { events: readEvents(loaded.manifest.eventsPath), nextSeq: undefined, total: undefined };
|
|
585
|
+
return result(JSON.stringify(payload, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
579
586
|
}
|
|
580
587
|
if (operation === "runtime-capabilities") {
|
|
581
588
|
const loadedConfig = loadConfig(ctx.cwd);
|
|
@@ -604,18 +611,43 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
604
611
|
}
|
|
605
612
|
if (operation === "read-agent-events") {
|
|
606
613
|
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
return result(
|
|
614
|
+
const agents = readCrewAgents(loaded.manifest);
|
|
615
|
+
const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
|
|
616
|
+
if (!agent) return result("API read-agent-events requires config.agentId matching an agent id or task id, or at least one agent in the run.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
617
|
+
const sinceSeq = typeof cfg.sinceSeq === "number" ? cfg.sinceSeq : undefined;
|
|
618
|
+
const limit = typeof cfg.limit === "number" ? cfg.limit : undefined;
|
|
619
|
+
const payload = sinceSeq !== undefined || limit !== undefined
|
|
620
|
+
? readCrewAgentEventsCursor(loaded.manifest, agent.taskId, { sinceSeq, limit })
|
|
621
|
+
: { path: agentEventsPath(loaded.manifest, agent.taskId), events: readCrewAgentEvents(loaded.manifest, agent.taskId) };
|
|
622
|
+
return result(JSON.stringify(payload, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
610
623
|
}
|
|
611
624
|
if (operation === "read-agent-transcript") {
|
|
612
625
|
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
613
|
-
const
|
|
614
|
-
|
|
626
|
+
const agents = readCrewAgents(loaded.manifest);
|
|
627
|
+
const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
|
|
628
|
+
if (!agent) return result("API read-agent-transcript requires config.agentId matching an agent id or task id, or at least one agent in the run.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
615
629
|
const transcriptPath = agent.transcriptPath && fs.existsSync(agent.transcriptPath) ? agent.transcriptPath : agentOutputPath(loaded.manifest, agent.taskId);
|
|
616
630
|
const text = fs.existsSync(transcriptPath) ? fs.readFileSync(transcriptPath, "utf-8") : "";
|
|
617
631
|
return result(text || `(no transcript at ${transcriptPath})`, { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
618
632
|
}
|
|
633
|
+
if (operation === "read-agent-output") {
|
|
634
|
+
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
635
|
+
const agents = readCrewAgents(loaded.manifest);
|
|
636
|
+
const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
|
|
637
|
+
if (!agent) return result("API read-agent-output requires config.agentId matching an agent id or task id, or at least one agent in the run.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
638
|
+
const maxBytes = typeof cfg.maxBytes === "number" ? cfg.maxBytes : undefined;
|
|
639
|
+
return result(JSON.stringify(readAgentOutput(loaded.manifest, agent.taskId, maxBytes), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
640
|
+
}
|
|
641
|
+
if (operation === "agent-dashboard") {
|
|
642
|
+
return result(buildAgentDashboard(loaded.manifest).text, { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
643
|
+
}
|
|
644
|
+
if (operation === "foreground-status") {
|
|
645
|
+
return result(JSON.stringify(readForegroundControlStatus(loaded.manifest, loaded.tasks), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
646
|
+
}
|
|
647
|
+
if (operation === "foreground-interrupt") {
|
|
648
|
+
const reason = typeof cfg.reason === "string" && cfg.reason.trim() ? cfg.reason.trim() : undefined;
|
|
649
|
+
return result(JSON.stringify(writeForegroundInterruptRequest(loaded.manifest, reason), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
650
|
+
}
|
|
619
651
|
if (operation === "nudge-agent") {
|
|
620
652
|
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
621
653
|
const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
|
|
@@ -2,6 +2,8 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
|
2
2
|
|
|
3
3
|
export const PI_TEAMS_INHERIT_PROJECT_CONTEXT_ENV = "PI_TEAMS_INHERIT_PROJECT_CONTEXT";
|
|
4
4
|
export const PI_TEAMS_INHERIT_SKILLS_ENV = "PI_TEAMS_INHERIT_SKILLS";
|
|
5
|
+
export const PI_CREW_INHERIT_PROJECT_CONTEXT_ENV = "PI_CREW_INHERIT_PROJECT_CONTEXT";
|
|
6
|
+
export const PI_CREW_INHERIT_SKILLS_ENV = "PI_CREW_INHERIT_SKILLS";
|
|
5
7
|
|
|
6
8
|
const PROJECT_CONTEXT_HEADER = "\n\n# Project Context\n\nProject-specific instructions and guidelines:\n\n";
|
|
7
9
|
const SKILLS_HEADER = "\n\nThe following skills provide specialized instructions for specific tasks.";
|
|
@@ -13,6 +15,14 @@ function readBooleanEnv(name: string): boolean | undefined {
|
|
|
13
15
|
return value !== "0";
|
|
14
16
|
}
|
|
15
17
|
|
|
18
|
+
function readBooleanEnvAny(...names: string[]): boolean | undefined {
|
|
19
|
+
for (const name of names) {
|
|
20
|
+
const value = readBooleanEnv(name);
|
|
21
|
+
if (value !== undefined) return value;
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
function findSectionEnd(prompt: string, startIndex: number, nextHeaders: string[]): number {
|
|
17
27
|
let endIndex = prompt.length;
|
|
18
28
|
for (const header of nextHeaders) {
|
|
@@ -45,8 +55,8 @@ export function rewriteTeamWorkerPrompt(prompt: string, options: { inheritProjec
|
|
|
45
55
|
|
|
46
56
|
export default function registerPiTeamsPromptRuntime(pi: ExtensionAPI): void {
|
|
47
57
|
pi.on("before_agent_start", (event) => {
|
|
48
|
-
const inheritProjectContext =
|
|
49
|
-
const inheritSkills =
|
|
58
|
+
const inheritProjectContext = readBooleanEnvAny(PI_CREW_INHERIT_PROJECT_CONTEXT_ENV, PI_TEAMS_INHERIT_PROJECT_CONTEXT_ENV);
|
|
59
|
+
const inheritSkills = readBooleanEnvAny(PI_CREW_INHERIT_SKILLS_ENV, PI_TEAMS_INHERIT_SKILLS_ENV);
|
|
50
60
|
if (inheritProjectContext === undefined && inheritSkills === undefined) return;
|
|
51
61
|
const rewritten = rewriteTeamWorkerPrompt(event.systemPrompt, {
|
|
52
62
|
inheritProjectContext: inheritProjectContext ?? true,
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import type { TeamRunManifest } from "../state/types.ts";
|
|
3
|
+
import { agentOutputPath, readCrewAgents } from "./crew-agent-records.ts";
|
|
4
|
+
import type { CrewAgentRecord } from "./crew-agent-runtime.ts";
|
|
5
|
+
|
|
6
|
+
export interface TextTailResult {
|
|
7
|
+
path: string;
|
|
8
|
+
text: string;
|
|
9
|
+
bytes: number;
|
|
10
|
+
truncated: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function readTextTail(filePath: string, maxBytes = 64_000): TextTailResult {
|
|
14
|
+
if (!fs.existsSync(filePath)) return { path: filePath, text: "", bytes: 0, truncated: false };
|
|
15
|
+
const stat = fs.statSync(filePath);
|
|
16
|
+
const bytesToRead = Math.min(stat.size, Math.max(0, maxBytes));
|
|
17
|
+
const fd = fs.openSync(filePath, "r");
|
|
18
|
+
try {
|
|
19
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
20
|
+
fs.readSync(fd, buffer, 0, bytesToRead, stat.size - bytesToRead);
|
|
21
|
+
return { path: filePath, text: buffer.toString("utf-8"), bytes: stat.size, truncated: stat.size > bytesToRead };
|
|
22
|
+
} finally {
|
|
23
|
+
fs.closeSync(fd);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function activityText(agent: CrewAgentRecord): string {
|
|
28
|
+
const parts: string[] = [];
|
|
29
|
+
if (agent.progress?.activityState) parts.push(agent.progress.activityState);
|
|
30
|
+
if (agent.progress?.currentTool) parts.push(`tool=${agent.progress.currentTool}`);
|
|
31
|
+
if (agent.toolUses !== undefined) parts.push(`tools=${agent.toolUses}`);
|
|
32
|
+
if (agent.progress?.tokens !== undefined) parts.push(`tokens=${agent.progress.tokens}`);
|
|
33
|
+
if (agent.progress?.turns !== undefined) parts.push(`turns=${agent.progress.turns}`);
|
|
34
|
+
if (agent.progress?.durationMs !== undefined) parts.push(`durationMs=${agent.progress.durationMs}`);
|
|
35
|
+
if (agent.progress?.failedTool) parts.push(`failedTool=${agent.progress.failedTool}`);
|
|
36
|
+
if (agent.progress?.recentOutput?.length) parts.push(`last=${agent.progress.recentOutput.at(-1)}`);
|
|
37
|
+
return parts.join(" ") || "idle";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function statusGlyph(status: CrewAgentRecord["status"]): string {
|
|
41
|
+
if (status === "completed") return "✓";
|
|
42
|
+
if (status === "failed") return "✗";
|
|
43
|
+
if (status === "running") return "▶";
|
|
44
|
+
if (status === "cancelled" || status === "stopped") return "■";
|
|
45
|
+
return "·";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function outputWarning(agent: CrewAgentRecord): string {
|
|
49
|
+
if (agent.status !== "completed") return "";
|
|
50
|
+
if (!agent.outputPath || !fs.existsSync(agent.outputPath)) return " no-output";
|
|
51
|
+
try {
|
|
52
|
+
return fs.statSync(agent.outputPath).size === 0 ? " no-output" : "";
|
|
53
|
+
} catch {
|
|
54
|
+
return " no-output";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function agentLine(agent: CrewAgentRecord): string {
|
|
59
|
+
return `- ${statusGlyph(agent.status)} ${agent.taskId} [${agent.status}] ${agent.role}->${agent.agent} runtime=${agent.runtime} ${activityText(agent)}${outputWarning(agent)}${agent.error ? ` error=${agent.error}` : ""}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function buildAgentDashboard(manifest: TeamRunManifest): { text: string; groups: Record<string, CrewAgentRecord[]> } {
|
|
63
|
+
const agents = readCrewAgents(manifest);
|
|
64
|
+
const groups: Record<string, CrewAgentRecord[]> = {
|
|
65
|
+
running: agents.filter((agent) => agent.status === "running"),
|
|
66
|
+
queued: agents.filter((agent) => agent.status === "queued"),
|
|
67
|
+
recent: agents.filter((agent) => agent.status !== "running" && agent.status !== "queued"),
|
|
68
|
+
};
|
|
69
|
+
const lines = [
|
|
70
|
+
`Crew agents for ${manifest.runId}`,
|
|
71
|
+
`Run status: ${manifest.status}`,
|
|
72
|
+
`Counts: running=${groups.running.length}, queued=${groups.queued.length}, recent=${groups.recent.length}`,
|
|
73
|
+
"",
|
|
74
|
+
"## Running",
|
|
75
|
+
...(groups.running.length ? groups.running.map(agentLine) : ["- (none)"]),
|
|
76
|
+
"",
|
|
77
|
+
"## Queued",
|
|
78
|
+
...(groups.queued.length ? groups.queued.map(agentLine) : ["- (none)"]),
|
|
79
|
+
"",
|
|
80
|
+
"## Recent",
|
|
81
|
+
...(groups.recent.length ? groups.recent.slice(-10).map(agentLine) : ["- (none)"]),
|
|
82
|
+
];
|
|
83
|
+
return { text: lines.join("\n"), groups };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function readAgentOutput(manifest: TeamRunManifest, taskId: string, maxBytes?: number): TextTailResult {
|
|
87
|
+
return readTextTail(agentOutputPath(manifest, taskId), maxBytes);
|
|
88
|
+
}
|
package/src/runtime/child-pi.ts
CHANGED
|
@@ -2,10 +2,12 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
5
|
-
import { buildPiWorkerArgs, cleanupTempDir } from "./pi-args.ts";
|
|
5
|
+
import { buildPiWorkerArgs, checkCrewDepth, cleanupTempDir } from "./pi-args.ts";
|
|
6
6
|
import { getPiSpawnCommand } from "./pi-spawn.ts";
|
|
7
7
|
|
|
8
8
|
const POST_EXIT_STDIO_GUARD_MS = 3000;
|
|
9
|
+
const FINAL_DRAIN_MS = 5000;
|
|
10
|
+
const HARD_KILL_MS = 3000;
|
|
9
11
|
|
|
10
12
|
export interface ChildPiRunInput {
|
|
11
13
|
cwd: string;
|
|
@@ -16,6 +18,9 @@ export interface ChildPiRunInput {
|
|
|
16
18
|
transcriptPath?: string;
|
|
17
19
|
onStdoutLine?: (line: string) => void;
|
|
18
20
|
onJsonEvent?: (event: unknown) => void;
|
|
21
|
+
maxDepth?: number;
|
|
22
|
+
finalDrainMs?: number;
|
|
23
|
+
hardKillMs?: number;
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
export interface ChildPiRunResult {
|
|
@@ -71,7 +76,25 @@ function observeStdoutChunk(input: ChildPiRunInput, text: string): void {
|
|
|
71
76
|
observer.flush();
|
|
72
77
|
}
|
|
73
78
|
|
|
79
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
80
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isFinalAssistantEvent(event: unknown): boolean {
|
|
84
|
+
const obj = asRecord(event);
|
|
85
|
+
if (!obj || obj.type !== "message_end") return false;
|
|
86
|
+
const message = asRecord(obj.message);
|
|
87
|
+
const role = message?.role;
|
|
88
|
+
if (role !== undefined && role !== "assistant") return false;
|
|
89
|
+
const stopReason = typeof message?.stopReason === "string" ? message.stopReason : typeof obj.stopReason === "string" ? obj.stopReason : undefined;
|
|
90
|
+
if (stopReason !== undefined && stopReason !== "stop") return false;
|
|
91
|
+
const content = Array.isArray(message?.content) ? message.content : [];
|
|
92
|
+
return !content.some((part) => asRecord(part)?.type === "toolCall");
|
|
93
|
+
}
|
|
94
|
+
|
|
74
95
|
export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResult> {
|
|
96
|
+
const depth = checkCrewDepth(input.maxDepth);
|
|
97
|
+
if (depth.blocked) return { exitCode: 1, stdout: "", stderr: `pi-crew depth guard blocked child worker: depth ${depth.depth} >= max ${depth.maxDepth}` };
|
|
75
98
|
const mock = process.env.PI_TEAMS_MOCK_CHILD_PI;
|
|
76
99
|
if (mock) {
|
|
77
100
|
if (mock === "success") {
|
|
@@ -87,7 +110,7 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
87
110
|
if (mock === "retryable-failure") return { exitCode: 1, stdout: "", stderr: "rate limit: mock failure" };
|
|
88
111
|
return { exitCode: 1, stdout: "", stderr: `mock failure: ${mock}` };
|
|
89
112
|
}
|
|
90
|
-
const built = buildPiWorkerArgs({ task: input.task, agent: input.agent, model: input.model, sessionEnabled: false });
|
|
113
|
+
const built = buildPiWorkerArgs({ task: input.task, agent: input.agent, model: input.model, sessionEnabled: false, maxDepth: input.maxDepth });
|
|
91
114
|
const spawnSpec = getPiSpawnCommand(built.args);
|
|
92
115
|
try {
|
|
93
116
|
return await new Promise<ChildPiRunResult>((resolve) => {
|
|
@@ -99,13 +122,44 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
99
122
|
let stdout = "";
|
|
100
123
|
let stderr = "";
|
|
101
124
|
let settled = false;
|
|
125
|
+
let childExited = false;
|
|
102
126
|
let postExitGuard: NodeJS.Timeout | undefined;
|
|
103
|
-
|
|
127
|
+
let finalDrainTimer: NodeJS.Timeout | undefined;
|
|
128
|
+
let hardKillTimer: NodeJS.Timeout | undefined;
|
|
129
|
+
const finalDrainMs = input.finalDrainMs ?? FINAL_DRAIN_MS;
|
|
130
|
+
const hardKillMs = input.hardKillMs ?? HARD_KILL_MS;
|
|
131
|
+
let forcedFinalDrain = false;
|
|
132
|
+
const lineObserver = new ChildPiLineObserver({
|
|
133
|
+
...input,
|
|
134
|
+
onJsonEvent: (event) => {
|
|
135
|
+
input.onJsonEvent?.(event);
|
|
136
|
+
if (!isFinalAssistantEvent(event) || childExited || settled || finalDrainTimer) return;
|
|
137
|
+
finalDrainTimer = setTimeout(() => {
|
|
138
|
+
if (settled || childExited) return;
|
|
139
|
+
forcedFinalDrain = true;
|
|
140
|
+
try { child.kill(process.platform === "win32" ? undefined : "SIGTERM"); } catch {}
|
|
141
|
+
hardKillTimer = setTimeout(() => {
|
|
142
|
+
if (settled || childExited) return;
|
|
143
|
+
try { child.kill(process.platform === "win32" ? undefined : "SIGKILL"); } catch {}
|
|
144
|
+
}, hardKillMs);
|
|
145
|
+
hardKillTimer.unref?.();
|
|
146
|
+
}, finalDrainMs);
|
|
147
|
+
finalDrainTimer.unref?.();
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const clearFinalDrainTimers = (): void => {
|
|
152
|
+
if (finalDrainTimer) clearTimeout(finalDrainTimer);
|
|
153
|
+
if (hardKillTimer) clearTimeout(hardKillTimer);
|
|
154
|
+
finalDrainTimer = undefined;
|
|
155
|
+
hardKillTimer = undefined;
|
|
156
|
+
};
|
|
104
157
|
|
|
105
158
|
const settle = (result: ChildPiRunResult): void => {
|
|
106
159
|
if (settled) return;
|
|
107
160
|
settled = true;
|
|
108
161
|
if (postExitGuard) clearTimeout(postExitGuard);
|
|
162
|
+
clearFinalDrainTimers();
|
|
109
163
|
lineObserver.flush();
|
|
110
164
|
input.signal?.removeEventListener("abort", abort);
|
|
111
165
|
cleanupTempDir(built.tempDir);
|
|
@@ -133,6 +187,8 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
133
187
|
settle({ exitCode: null, stdout, stderr, error: error.message });
|
|
134
188
|
});
|
|
135
189
|
child.on("exit", () => {
|
|
190
|
+
childExited = true;
|
|
191
|
+
clearFinalDrainTimers();
|
|
136
192
|
postExitGuard = setTimeout(() => {
|
|
137
193
|
child.stdout?.destroy();
|
|
138
194
|
child.stderr?.destroy();
|
|
@@ -140,7 +196,7 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
140
196
|
postExitGuard.unref?.();
|
|
141
197
|
});
|
|
142
198
|
child.on("close", (exitCode) => {
|
|
143
|
-
settle({ exitCode, stdout, stderr });
|
|
199
|
+
settle({ exitCode, stdout, stderr, ...(forcedFinalDrain && !stderr.trim() ? { error: `Child Pi did not exit within ${finalDrainMs}ms after final assistant message; termination was requested.` } : {}) });
|
|
144
200
|
});
|
|
145
201
|
});
|
|
146
202
|
} finally {
|
|
@@ -52,17 +52,55 @@ export function readCrewAgentStatus(manifest: TeamRunManifest, taskOrAgentId: st
|
|
|
52
52
|
return readJsonFile<CrewAgentRecord>(agentStatusPath(manifest, taskId));
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
function nextAgentEventSeq(filePath: string): number {
|
|
56
|
+
if (!fs.existsSync(filePath)) return 1;
|
|
57
|
+
let max = 0;
|
|
58
|
+
for (const line of fs.readFileSync(filePath, "utf-8").split(/\r?\n/)) {
|
|
59
|
+
if (!line.trim()) continue;
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(line) as { seq?: unknown };
|
|
62
|
+
if (typeof parsed.seq === "number" && Number.isFinite(parsed.seq)) max = Math.max(max, parsed.seq);
|
|
63
|
+
else max += 1;
|
|
64
|
+
} catch {
|
|
65
|
+
max += 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return max + 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
55
71
|
export function appendCrewAgentEvent(manifest: TeamRunManifest, taskId: string, event: unknown): void {
|
|
56
72
|
fs.mkdirSync(agentStateDir(manifest, taskId), { recursive: true });
|
|
57
|
-
|
|
73
|
+
const filePath = agentEventsPath(manifest, taskId);
|
|
74
|
+
fs.appendFileSync(filePath, `${JSON.stringify({ seq: nextAgentEventSeq(filePath), time: new Date().toISOString(), event })}\n`, "utf-8");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface CrewAgentEventCursorOptions {
|
|
78
|
+
sinceSeq?: number;
|
|
79
|
+
limit?: number;
|
|
58
80
|
}
|
|
59
81
|
|
|
60
82
|
export function readCrewAgentEvents(manifest: TeamRunManifest, taskId: string): unknown[] {
|
|
83
|
+
return readCrewAgentEventsCursor(manifest, taskId).events;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function readCrewAgentEventsCursor(manifest: TeamRunManifest, taskId: string, options: CrewAgentEventCursorOptions = {}): { path: string; events: unknown[]; nextSeq: number; total: number } {
|
|
61
87
|
const filePath = agentEventsPath(manifest, taskId);
|
|
62
|
-
if (!fs.existsSync(filePath)) return [];
|
|
63
|
-
|
|
64
|
-
|
|
88
|
+
if (!fs.existsSync(filePath)) return { path: filePath, events: [], nextSeq: options.sinceSeq ?? 0, total: 0 };
|
|
89
|
+
const sinceSeq = typeof options.sinceSeq === "number" && Number.isInteger(options.sinceSeq) && options.sinceSeq >= 0 ? options.sinceSeq : 0;
|
|
90
|
+
const limit = typeof options.limit === "number" && Number.isInteger(options.limit) && options.limit >= 0 ? options.limit : undefined;
|
|
91
|
+
const parsed = fs.readFileSync(filePath, "utf-8").split(/\r?\n/).filter(Boolean).map((line, index) => {
|
|
92
|
+
try {
|
|
93
|
+
const event = JSON.parse(line) as Record<string, unknown>;
|
|
94
|
+
if (typeof event.seq !== "number") event.seq = index + 1;
|
|
95
|
+
return event;
|
|
96
|
+
} catch {
|
|
97
|
+
return { seq: index + 1, raw: line };
|
|
98
|
+
}
|
|
65
99
|
});
|
|
100
|
+
const filtered = parsed.filter((event) => typeof event.seq === "number" && event.seq > sinceSeq);
|
|
101
|
+
const events = limit !== undefined ? filtered.slice(0, limit) : filtered;
|
|
102
|
+
const returnedMaxSeq = events.reduce((max, event) => typeof event.seq === "number" ? Math.max(max, event.seq) : max, sinceSeq);
|
|
103
|
+
return { path: filePath, events, nextSeq: returnedMaxSeq, total: filtered.length };
|
|
66
104
|
}
|
|
67
105
|
|
|
68
106
|
export function appendCrewAgentOutput(manifest: TeamRunManifest, taskId: string, text: string): void {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { appendEvent } from "../state/event-log.ts";
|
|
4
|
+
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
5
|
+
import { checkProcessLiveness, isActiveRunStatus } from "./process-status.ts";
|
|
6
|
+
import { readCrewAgents } from "./crew-agent-records.ts";
|
|
7
|
+
|
|
8
|
+
export type ForegroundControlRequestType = "interrupt" | "status";
|
|
9
|
+
|
|
10
|
+
export interface ForegroundControlStatus {
|
|
11
|
+
runId: string;
|
|
12
|
+
status: TeamRunManifest["status"];
|
|
13
|
+
active: boolean;
|
|
14
|
+
asyncPid?: number;
|
|
15
|
+
asyncAlive?: boolean;
|
|
16
|
+
runningTasks: string[];
|
|
17
|
+
runningAgents: string[];
|
|
18
|
+
controlPath: string;
|
|
19
|
+
lastRequest?: ForegroundControlRequest;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ForegroundControlRequest {
|
|
23
|
+
id: string;
|
|
24
|
+
type: ForegroundControlRequestType;
|
|
25
|
+
createdAt: string;
|
|
26
|
+
reason: string;
|
|
27
|
+
acknowledged: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function foregroundControlPath(manifest: TeamRunManifest): string {
|
|
31
|
+
return path.join(manifest.stateRoot, "foreground-control.json");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readLastRequest(controlPath: string): ForegroundControlRequest | undefined {
|
|
35
|
+
if (!fs.existsSync(controlPath)) return undefined;
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(fs.readFileSync(controlPath, "utf-8")) as { requests?: ForegroundControlRequest[] };
|
|
38
|
+
return parsed.requests?.at(-1);
|
|
39
|
+
} catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function readForegroundControlStatus(manifest: TeamRunManifest, tasks: TeamTaskState[]): ForegroundControlStatus {
|
|
45
|
+
const controlPath = foregroundControlPath(manifest);
|
|
46
|
+
const asyncAlive = manifest.async?.pid !== undefined ? checkProcessLiveness(manifest.async.pid).alive : undefined;
|
|
47
|
+
return {
|
|
48
|
+
runId: manifest.runId,
|
|
49
|
+
status: manifest.status,
|
|
50
|
+
active: isActiveRunStatus(manifest.status),
|
|
51
|
+
asyncPid: manifest.async?.pid,
|
|
52
|
+
asyncAlive,
|
|
53
|
+
runningTasks: tasks.filter((task) => task.status === "running").map((task) => task.id),
|
|
54
|
+
runningAgents: readCrewAgents(manifest).filter((agent) => agent.status === "running").map((agent) => agent.id),
|
|
55
|
+
controlPath,
|
|
56
|
+
lastRequest: readLastRequest(controlPath),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function writeForegroundInterruptRequest(manifest: TeamRunManifest, reason = "User requested foreground interrupt."): ForegroundControlRequest {
|
|
61
|
+
const controlPath = foregroundControlPath(manifest);
|
|
62
|
+
let requests: ForegroundControlRequest[] = [];
|
|
63
|
+
if (fs.existsSync(controlPath)) {
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(fs.readFileSync(controlPath, "utf-8")) as { requests?: ForegroundControlRequest[] };
|
|
66
|
+
requests = Array.isArray(parsed.requests) ? parsed.requests : [];
|
|
67
|
+
} catch {
|
|
68
|
+
requests = [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const request: ForegroundControlRequest = {
|
|
72
|
+
id: `fg_${Date.now().toString(36)}_${Math.random().toString(16).slice(2, 10)}`,
|
|
73
|
+
type: "interrupt",
|
|
74
|
+
createdAt: new Date().toISOString(),
|
|
75
|
+
reason,
|
|
76
|
+
acknowledged: false,
|
|
77
|
+
};
|
|
78
|
+
fs.mkdirSync(path.dirname(controlPath), { recursive: true });
|
|
79
|
+
fs.writeFileSync(controlPath, `${JSON.stringify({ requests: [...requests, request] }, null, 2)}\n`, "utf-8");
|
|
80
|
+
appendEvent(manifest.eventsPath, { type: "foreground.interrupt_requested", runId: manifest.runId, message: reason, data: { requestId: request.id, controlPath } });
|
|
81
|
+
return request;
|
|
82
|
+
}
|
package/src/runtime/pi-args.ts
CHANGED
|
@@ -7,12 +7,14 @@ import type { AgentConfig } from "../agents/agent-config.ts";
|
|
|
7
7
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
8
8
|
const PROMPT_RUNTIME_EXTENSION_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "prompt", "prompt-runtime.ts");
|
|
9
9
|
const TASK_ARG_LIMIT = 8000;
|
|
10
|
+
const DEFAULT_MAX_CREW_DEPTH = 2;
|
|
10
11
|
|
|
11
12
|
export interface BuildPiWorkerArgsInput {
|
|
12
13
|
task: string;
|
|
13
14
|
agent: AgentConfig;
|
|
14
15
|
model?: string;
|
|
15
16
|
sessionEnabled?: boolean;
|
|
17
|
+
maxDepth?: number;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export interface BuildPiWorkerArgsResult {
|
|
@@ -28,6 +30,25 @@ export function applyThinkingSuffix(model: string | undefined, thinking: string
|
|
|
28
30
|
return `${model}:${thinking}`;
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
export function currentCrewDepth(env: NodeJS.ProcessEnv = process.env): number {
|
|
34
|
+
const raw = env.PI_CREW_DEPTH ?? env.PI_TEAMS_DEPTH ?? "0";
|
|
35
|
+
const parsed = Number(raw);
|
|
36
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function resolveCrewMaxDepth(inputMaxDepth?: number, env: NodeJS.ProcessEnv = process.env): number {
|
|
40
|
+
const raw = env.PI_CREW_MAX_DEPTH ?? env.PI_TEAMS_MAX_DEPTH;
|
|
41
|
+
const envDepth = raw !== undefined ? Number(raw) : NaN;
|
|
42
|
+
if (Number.isInteger(envDepth) && envDepth >= 0) return envDepth;
|
|
43
|
+
return Number.isInteger(inputMaxDepth) && inputMaxDepth !== undefined && inputMaxDepth >= 0 ? inputMaxDepth : DEFAULT_MAX_CREW_DEPTH;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function checkCrewDepth(inputMaxDepth?: number, env: NodeJS.ProcessEnv = process.env): { blocked: boolean; depth: number; maxDepth: number } {
|
|
47
|
+
const depth = currentCrewDepth(env);
|
|
48
|
+
const maxDepth = resolveCrewMaxDepth(inputMaxDepth, env);
|
|
49
|
+
return { depth, maxDepth, blocked: depth >= maxDepth };
|
|
50
|
+
}
|
|
51
|
+
|
|
31
52
|
export function buildPiWorkerArgs(input: BuildPiWorkerArgsInput): BuildPiWorkerArgsResult {
|
|
32
53
|
const args = ["--mode", "json", "-p"];
|
|
33
54
|
if (input.sessionEnabled === false) args.push("--no-session");
|
|
@@ -61,11 +82,19 @@ export function buildPiWorkerArgs(input: BuildPiWorkerArgsInput): BuildPiWorkerA
|
|
|
61
82
|
args.push(`Task: ${input.task}`);
|
|
62
83
|
}
|
|
63
84
|
|
|
85
|
+
const parentDepth = currentCrewDepth();
|
|
86
|
+
const maxDepth = resolveCrewMaxDepth(input.maxDepth);
|
|
64
87
|
return {
|
|
65
88
|
args,
|
|
66
89
|
env: {
|
|
90
|
+
PI_CREW_INHERIT_PROJECT_CONTEXT: input.agent.inheritProjectContext ? "1" : "0",
|
|
91
|
+
PI_CREW_INHERIT_SKILLS: input.agent.inheritSkills ? "1" : "0",
|
|
92
|
+
PI_CREW_DEPTH: String(parentDepth + 1),
|
|
93
|
+
PI_CREW_MAX_DEPTH: String(maxDepth),
|
|
67
94
|
PI_TEAMS_INHERIT_PROJECT_CONTEXT: input.agent.inheritProjectContext ? "1" : "0",
|
|
68
95
|
PI_TEAMS_INHERIT_SKILLS: input.agent.inheritSkills ? "1" : "0",
|
|
96
|
+
PI_TEAMS_DEPTH: String(parentDepth + 1),
|
|
97
|
+
PI_TEAMS_MAX_DEPTH: String(maxDepth),
|
|
69
98
|
},
|
|
70
99
|
tempDir,
|
|
71
100
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
3
|
+
import type { CrewLimitsConfig } from "../config/config.ts";
|
|
3
4
|
import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
|
|
4
5
|
import { writeArtifact } from "../state/artifact-store.ts";
|
|
5
6
|
import { appendEvent } from "../state/event-log.ts";
|
|
@@ -28,6 +29,7 @@ export interface TaskRunnerInput {
|
|
|
28
29
|
agent: AgentConfig;
|
|
29
30
|
signal?: AbortSignal;
|
|
30
31
|
executeWorkers: boolean;
|
|
32
|
+
limits?: CrewLimitsConfig;
|
|
31
33
|
dependencyContextText?: string;
|
|
32
34
|
}
|
|
33
35
|
|
|
@@ -190,6 +192,9 @@ function applyAgentProgressEvent(progress: CrewAgentProgress, event: unknown, st
|
|
|
190
192
|
next.currentToolArgs = undefined;
|
|
191
193
|
next.currentToolStartedAt = undefined;
|
|
192
194
|
}
|
|
195
|
+
if ((obj?.type === "tool_execution_error" || obj?.type === "tool_execution_failed") && next.currentTool) {
|
|
196
|
+
next.failedTool = next.currentTool;
|
|
197
|
+
}
|
|
193
198
|
const usage = eventUsage(event);
|
|
194
199
|
if (usage) {
|
|
195
200
|
next.tokens = (usage.input ?? 0) + (usage.output ?? 0);
|
|
@@ -269,6 +274,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
269
274
|
model,
|
|
270
275
|
signal: input.signal,
|
|
271
276
|
transcriptPath,
|
|
277
|
+
maxDepth: input.limits?.maxTaskDepth,
|
|
272
278
|
onStdoutLine: (line) => appendCrewAgentOutput(manifest, task.id, line),
|
|
273
279
|
onJsonEvent: (event) => {
|
|
274
280
|
appendCrewAgentEvent(manifest, task.id, event);
|
|
@@ -349,7 +355,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
349
355
|
modelAttempts,
|
|
350
356
|
usage: parsedOutput?.usage,
|
|
351
357
|
jsonEvents: parsedOutput?.jsonEvents,
|
|
352
|
-
agentProgress: task.agentProgress,
|
|
358
|
+
agentProgress: error && task.agentProgress?.currentTool ? { ...task.agentProgress, failedTool: task.agentProgress.currentTool } : task.agentProgress,
|
|
353
359
|
error,
|
|
354
360
|
verification: createVerificationEvidence(taskPacket.verification, !error, error ? `Task failed: ${error}` : input.executeWorkers ? "Worker finished without reporting a verification failure." : "Safe scaffold mode; verification commands were not executed."),
|
|
355
361
|
promptArtifact,
|
|
@@ -174,7 +174,7 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
|
|
|
174
174
|
const results = await Promise.all(readyBatch.map((task) => {
|
|
175
175
|
const step = findStep(input.workflow, task);
|
|
176
176
|
const agent = findAgent(input.agents, task);
|
|
177
|
-
return runTeamTask({ manifest, tasks, task, step, agent, signal: input.signal, executeWorkers: input.executeWorkers });
|
|
177
|
+
return runTeamTask({ manifest, tasks, task, step, agent, signal: input.signal, executeWorkers: input.executeWorkers, limits: input.limits });
|
|
178
178
|
}));
|
|
179
179
|
manifest = { ...results.at(-1)!.manifest, artifacts: mergeArtifacts([manifest.artifacts, ...results.map((item) => item.manifest.artifacts)].flat()) };
|
|
180
180
|
tasks = mergeTaskUpdates(tasks, results);
|
package/src/state/event-log.ts
CHANGED
|
@@ -94,6 +94,25 @@ export function readEvents(eventsPath: string): TeamEvent[] {
|
|
|
94
94
|
.map((line) => JSON.parse(line) as TeamEvent);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
export interface EventCursorOptions {
|
|
98
|
+
sinceSeq?: number;
|
|
99
|
+
limit?: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function positiveInteger(value: number | undefined): number | undefined {
|
|
103
|
+
return value !== undefined && Number.isInteger(value) && value >= 0 ? value : undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function readEventsCursor(eventsPath: string, options: EventCursorOptions = {}): { events: TeamEvent[]; nextSeq: number; total: number } {
|
|
107
|
+
const sinceSeq = positiveInteger(options.sinceSeq) ?? 0;
|
|
108
|
+
const limit = positiveInteger(options.limit);
|
|
109
|
+
const all = readEvents(eventsPath);
|
|
110
|
+
const filtered = all.filter((event) => (event.metadata?.seq ?? 0) > sinceSeq);
|
|
111
|
+
const events = limit !== undefined ? filtered.slice(0, limit) : filtered;
|
|
112
|
+
const returnedMaxSeq = events.reduce((max, event) => Math.max(max, event.metadata?.seq ?? 0), sinceSeq);
|
|
113
|
+
return { events, nextSeq: returnedMaxSeq, total: filtered.length };
|
|
114
|
+
}
|
|
115
|
+
|
|
97
116
|
export function dedupeTerminalEvents(events: TeamEvent[]): TeamEvent[] {
|
|
98
117
|
const seen = new Set<string>();
|
|
99
118
|
const output: TeamEvent[] = [];
|
package/src/ui/run-dashboard.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import type { TeamRunManifest } from "../state/types.ts";
|
|
3
|
+
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
4
|
+
import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
|
|
3
5
|
|
|
4
6
|
interface DashboardComponent {
|
|
5
7
|
invalidate(): void;
|
|
@@ -7,17 +9,43 @@ interface DashboardComponent {
|
|
|
7
9
|
handleInput(data: string): void;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
|
-
export type RunDashboardAction = "status" | "summary" | "artifacts" | "api" | "reload";
|
|
12
|
+
export type RunDashboardAction = "status" | "summary" | "artifacts" | "api" | "events" | "agents" | "agent-events" | "agent-output" | "agent-transcript" | "reload";
|
|
11
13
|
export interface RunDashboardSelection {
|
|
12
14
|
runId: string;
|
|
13
15
|
action: RunDashboardAction;
|
|
14
16
|
}
|
|
15
17
|
|
|
18
|
+
const ANSI_PATTERN = /\u001b\[[0-?]*[ -/]*[@-~]/g;
|
|
19
|
+
|
|
20
|
+
function visibleLength(value: string): number {
|
|
21
|
+
return value.replace(ANSI_PATTERN, "").length;
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
function truncate(value: string, width: number): string {
|
|
17
25
|
if (width <= 0) return "";
|
|
18
|
-
if (value
|
|
26
|
+
if (visibleLength(value) <= width) return value;
|
|
19
27
|
if (width <= 1) return "…";
|
|
20
|
-
|
|
28
|
+
let output = "";
|
|
29
|
+
let visible = 0;
|
|
30
|
+
for (let index = 0; index < value.length;) {
|
|
31
|
+
const slice = value.slice(index);
|
|
32
|
+
const ansi = slice.match(/^\u001b\[[0-?]*[ -/]*[@-~]/);
|
|
33
|
+
if (ansi?.[0]) {
|
|
34
|
+
output += ansi[0];
|
|
35
|
+
index += ansi[0].length;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const char = value[index]!;
|
|
39
|
+
if (visible >= width - 1) break;
|
|
40
|
+
output += char;
|
|
41
|
+
visible += 1;
|
|
42
|
+
index += char.length;
|
|
43
|
+
}
|
|
44
|
+
return `${output}\u001b[0m…`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function padVisible(value: string, width: number): string {
|
|
48
|
+
return `${value}${" ".repeat(Math.max(0, width - visibleLength(value)))}`;
|
|
21
49
|
}
|
|
22
50
|
|
|
23
51
|
function statusIcon(status: string): string {
|
|
@@ -40,6 +68,30 @@ function readProgressPreview(run: TeamRunManifest, maxLines = 5): string[] {
|
|
|
40
68
|
}
|
|
41
69
|
}
|
|
42
70
|
|
|
71
|
+
function agentPreviewLine(agent: CrewAgentRecord): string {
|
|
72
|
+
const stats = [
|
|
73
|
+
agent.progress?.activityState,
|
|
74
|
+
agent.progress?.currentTool ? `tool=${agent.progress.currentTool}` : undefined,
|
|
75
|
+
agent.toolUses !== undefined ? `${agent.toolUses} tools` : undefined,
|
|
76
|
+
agent.progress?.tokens !== undefined ? `${agent.progress.tokens} tok` : undefined,
|
|
77
|
+
agent.progress?.turns !== undefined ? `${agent.progress.turns} turns` : undefined,
|
|
78
|
+
agent.progress?.failedTool ? `failedTool=${agent.progress.failedTool}` : undefined,
|
|
79
|
+
].filter((part): part is string => Boolean(part));
|
|
80
|
+
const recent = agent.progress?.recentOutput?.at(-1);
|
|
81
|
+
return `Agent: ${statusIcon(agent.status)} ${agent.taskId} ${agent.role}->${agent.agent}${stats.length ? ` · ${stats.join(" · ")}` : ""}${recent ? ` ⎿ ${recent}` : ""}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readAgentPreview(run: TeamRunManifest, maxLines = 5): string[] {
|
|
85
|
+
try {
|
|
86
|
+
const agents = readCrewAgents(run);
|
|
87
|
+
if (!agents.length) return ["Agents: (none)"];
|
|
88
|
+
return ["Agents:", ...agents.slice(0, maxLines).map(agentPreviewLine), ...(agents.length > maxLines ? [`Agents: +${agents.length - maxLines} more`] : [])];
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
91
|
+
return [`Agents: failed to read (${message})`];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
43
95
|
function countByStatus(runs: TeamRunManifest[]): string {
|
|
44
96
|
const counts = new Map<string, number>();
|
|
45
97
|
for (const run of runs) counts.set(run.status, (counts.get(run.status) ?? 0) + 1);
|
|
@@ -64,19 +116,19 @@ export class RunDashboard implements DashboardComponent {
|
|
|
64
116
|
const borderWidth = Math.min(innerWidth, Math.max(0, width - 2));
|
|
65
117
|
const lines = [
|
|
66
118
|
`╭${"─".repeat(borderWidth)}╮`,
|
|
67
|
-
`│ ${truncate("pi-crew dashboard", innerWidth - 1)
|
|
68
|
-
`│ ${truncate("↑/↓/j/k select • r reload • p progress • s/u/a/i actions • q close", innerWidth - 1)
|
|
69
|
-
`│ ${truncate(`Runs: ${this.runs.length} • ${countByStatus(this.runs)}`, innerWidth - 1)
|
|
119
|
+
`│ ${padVisible(truncate("pi-crew dashboard", innerWidth - 1), innerWidth - 1)}│`,
|
|
120
|
+
`│ ${padVisible(truncate("↑/↓/j/k select • r reload • p progress • s/u/a/i actions • d agents • e/v/o viewers • q close", innerWidth - 1), innerWidth - 1)}│`,
|
|
121
|
+
`│ ${padVisible(truncate(`Runs: ${this.runs.length} • ${countByStatus(this.runs)}`, innerWidth - 1), innerWidth - 1)}│`,
|
|
70
122
|
`├${"─".repeat(borderWidth)}┤`,
|
|
71
123
|
];
|
|
72
124
|
if (this.runs.length === 0) {
|
|
73
|
-
lines.push(`│ ${truncate("No runs found.", innerWidth - 1)
|
|
125
|
+
lines.push(`│ ${padVisible(truncate("No runs found.", innerWidth - 1), innerWidth - 1)}│`);
|
|
74
126
|
} else {
|
|
75
127
|
for (let i = 0; i < Math.min(this.runs.length, 10); i++) {
|
|
76
128
|
const run = this.runs[i]!;
|
|
77
129
|
const marker = i === this.selected ? "›" : " ";
|
|
78
130
|
const text = `${marker} ${statusIcon(run.status)} ${run.runId} ${run.status} ${run.team}/${run.workflow ?? "none"} ${run.goal}`;
|
|
79
|
-
lines.push(`│ ${truncate(text, innerWidth - 1)
|
|
131
|
+
lines.push(`│ ${padVisible(truncate(text, innerWidth - 1), innerWidth - 1)}│`);
|
|
80
132
|
}
|
|
81
133
|
const selectedRun = this.runs[this.selected];
|
|
82
134
|
if (selectedRun) {
|
|
@@ -90,8 +142,8 @@ export class RunDashboard implements DashboardComponent {
|
|
|
90
142
|
selectedRun.async ? `Async: pid=${selectedRun.async.pid ?? "unknown"} log=${selectedRun.async.logPath}` : "Async: no",
|
|
91
143
|
`Goal: ${selectedRun.goal}`,
|
|
92
144
|
];
|
|
93
|
-
for (const detail of [...details, ...readProgressPreview(selectedRun, this.showFullProgress ? 20 : 5)]) {
|
|
94
|
-
lines.push(`│ ${truncate(detail, innerWidth - 1)
|
|
145
|
+
for (const detail of [...details, ...readAgentPreview(selectedRun), ...readProgressPreview(selectedRun, this.showFullProgress ? 20 : 5)]) {
|
|
146
|
+
lines.push(`│ ${padVisible(truncate(detail, innerWidth - 1), innerWidth - 1)}│`);
|
|
95
147
|
}
|
|
96
148
|
}
|
|
97
149
|
}
|
|
@@ -124,6 +176,26 @@ export class RunDashboard implements DashboardComponent {
|
|
|
124
176
|
this.done(runId ? { runId, action: "api" } : undefined);
|
|
125
177
|
return;
|
|
126
178
|
}
|
|
179
|
+
if (data === "d") {
|
|
180
|
+
const runId = this.runs[this.selected]?.runId;
|
|
181
|
+
this.done(runId ? { runId, action: "agents" } : undefined);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (data === "e") {
|
|
185
|
+
const runId = this.runs[this.selected]?.runId;
|
|
186
|
+
this.done(runId ? { runId, action: "agent-events" } : undefined);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (data === "o") {
|
|
190
|
+
const runId = this.runs[this.selected]?.runId;
|
|
191
|
+
this.done(runId ? { runId, action: "agent-output" } : undefined);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (data === "v") {
|
|
195
|
+
const runId = this.runs[this.selected]?.runId;
|
|
196
|
+
this.done(runId ? { runId, action: "agent-transcript" } : undefined);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
127
199
|
if (data === "r") {
|
|
128
200
|
this.done({ runId: this.runs[this.selected]?.runId ?? "", action: "reload" });
|
|
129
201
|
return;
|