pi-crew 0.1.37 → 0.1.39
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/AGENTS.md +1 -1
- package/CHANGELOG.md +27 -0
- package/README.md +5 -0
- package/agents/analyst.md +11 -11
- package/agents/critic.md +11 -11
- package/agents/executor.md +11 -11
- package/agents/explorer.md +11 -11
- package/agents/planner.md +11 -11
- package/agents/reviewer.md +11 -11
- package/agents/security-reviewer.md +11 -11
- package/agents/test-engineer.md +11 -11
- package/agents/verifier.md +11 -11
- package/agents/writer.md +11 -11
- package/docs/refactor-tasks-phase3.md +394 -394
- package/docs/refactor-tasks-phase4.md +564 -564
- package/docs/refactor-tasks-phase5.md +402 -402
- package/docs/refactor-tasks-phase6.md +662 -662
- package/docs/research-extension-examples.md +297 -297
- package/docs/research-extension-system.md +324 -324
- package/docs/research-optimization-plan.md +548 -548
- package/docs/research-pi-coding-agent.md +357 -357
- package/docs/research-source-pi-crew-reference.md +174 -174
- package/docs/resource-formats.md +10 -8
- package/docs/runtime-flow.md +148 -148
- package/docs/source-runtime-refactor-map.md +83 -83
- package/docs/usage.md +6 -0
- package/index.ts +6 -6
- package/package.json +3 -3
- package/schema.json +2 -2
- package/src/agents/agent-serializer.ts +34 -34
- package/src/config/config.ts +8 -4
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/import-index.ts +18 -2
- package/src/extension/register.ts +11 -1
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-helpers.ts +30 -6
- package/src/extension/registration/subagent-tools.ts +8 -3
- package/src/extension/result-watcher.ts +98 -98
- package/src/extension/run-import.ts +12 -2
- package/src/extension/run-index.ts +12 -2
- package/src/extension/run-maintenance.ts +24 -24
- package/src/extension/team-tool/api.ts +54 -14
- package/src/extension/team-tool/cancel.ts +31 -31
- package/src/extension/team-tool/doctor.ts +179 -179
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +79 -79
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/status.ts +73 -73
- package/src/observability/correlation.ts +35 -35
- package/src/observability/event-to-metric.ts +54 -54
- package/src/observability/exporters/adapter.ts +24 -24
- package/src/observability/exporters/otlp-exporter.ts +65 -65
- package/src/observability/exporters/prometheus-exporter.ts +47 -47
- package/src/observability/metric-registry.ts +72 -72
- package/src/observability/metric-retention.ts +46 -46
- package/src/observability/metric-sink.ts +51 -51
- package/src/observability/metrics-primitives.ts +166 -166
- package/src/prompt/prompt-runtime.ts +68 -68
- package/src/runtime/agent-control.ts +64 -64
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -113
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/background-runner.ts +53 -53
- package/src/runtime/crash-recovery.ts +56 -56
- package/src/runtime/crew-agent-records.ts +54 -9
- package/src/runtime/crew-agent-runtime.ts +58 -58
- package/src/runtime/deadletter.ts +36 -36
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +88 -88
- package/src/runtime/heartbeat-gradient.ts +28 -28
- package/src/runtime/heartbeat-watcher.ts +80 -80
- package/src/runtime/live-agent-control.ts +87 -78
- package/src/runtime/live-agent-manager.ts +85 -85
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +299 -299
- package/src/runtime/manifest-cache.ts +248 -212
- package/src/runtime/model-fallback.ts +261 -261
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +99 -99
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/policy-engine.ts +78 -78
- package/src/runtime/post-exit-stdio-guard.ts +86 -86
- package/src/runtime/process-status.ts +56 -56
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +59 -59
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +28 -28
- package/src/runtime/subagent-manager.ts +80 -12
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-output-context.ts +127 -106
- package/src/runtime/task-runner/live-executor.ts +98 -98
- package/src/runtime/task-runner/progress.ts +111 -111
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/team-runner.ts +1 -1
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/schema/config-schema.ts +21 -21
- package/src/schema/team-tool-schema.ts +100 -100
- package/src/state/artifact-store.ts +122 -108
- package/src/state/contracts.ts +105 -105
- package/src/state/jsonl-writer.ts +77 -77
- package/src/state/mailbox.ts +67 -22
- package/src/state/state-store.ts +36 -5
- package/src/state/task-claims.ts +42 -42
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/discover-teams.ts +27 -5
- package/src/teams/team-serializer.ts +38 -36
- package/src/types/diff.d.ts +18 -18
- package/src/ui/crew-footer.ts +101 -101
- package/src/ui/crew-select-list.ts +111 -111
- package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/loaders.ts +158 -158
- package/src/ui/mascot.ts +441 -441
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/run-dashboard.ts +5 -2
- package/src/ui/run-snapshot-cache.ts +19 -8
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +54 -54
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/ui/transcript-viewer.ts +15 -1
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/file-coalescer.ts +84 -84
- package/src/utils/frontmatter.ts +36 -36
- package/src/utils/fs-watch.ts +31 -31
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +12 -12
- package/src/utils/names.ts +26 -26
- package/src/utils/paths.ts +3 -2
- package/src/utils/safe-paths.ts +34 -0
- package/src/utils/sleep.ts +32 -32
- package/src/utils/timings.ts +31 -31
- package/src/utils/visual.ts +159 -159
- package/src/workflows/discover-workflows.ts +30 -3
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/worktree/branch-freshness.ts +45 -45
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/workflows/default.workflow.md +29 -29
- package/workflows/fast-fix.workflow.md +22 -22
- package/workflows/implementation.workflow.md +38 -38
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
export type RolePermissionMode = "read_only" | "workspace_write" | "danger_full_access" | "explicit_confirm";
|
|
2
|
-
|
|
3
|
-
const READ_ONLY_ROLES = new Set(["explorer", "reviewer", "security-reviewer", "verifier", "analyst", "critic", "planner", "writer"]);
|
|
4
|
-
const WRITE_ROLES = new Set(["executor", "test-engineer"]);
|
|
5
|
-
const READ_ONLY_COMMANDS = new Set(["cat", "head", "tail", "less", "more", "wc", "ls", "find", "grep", "rg", "awk", "sed", "echo", "printf", "which", "where", "whoami", "pwd", "env", "printenv", "date", "df", "du", "uname", "file", "stat", "diff", "sort", "uniq", "tr", "cut", "paste", "test", "true", "false", "type", "readlink", "realpath", "basename", "dirname", "sha256sum", "md5sum", "xxd", "hexdump", "od", "strings", "tree", "jq", "git", "gh"]);
|
|
6
|
-
|
|
7
|
-
export interface PermissionCheckResult {
|
|
8
|
-
allowed: boolean;
|
|
9
|
-
mode: RolePermissionMode;
|
|
10
|
-
reason?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function permissionForRole(role: string): RolePermissionMode {
|
|
14
|
-
if (READ_ONLY_ROLES.has(role)) return "read_only";
|
|
15
|
-
if (WRITE_ROLES.has(role)) return "workspace_write";
|
|
16
|
-
return "workspace_write";
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function isReadOnlyCommand(command: string): boolean {
|
|
20
|
-
const first = command.trim().split(/\s+/)[0]?.split(/[\\/]/).pop() ?? "";
|
|
21
|
-
return READ_ONLY_COMMANDS.has(first) && !/\s(-i|--in-place)\b|\s>{1,2}\s|\brm\b|\bmv\b|\bcp\b|\bnpm\s+install\b|\bgit\s+(commit|push|merge|rebase|reset|checkout)\b/.test(command);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function checkRolePermission(role: string, command: string): PermissionCheckResult {
|
|
25
|
-
const mode = permissionForRole(role);
|
|
26
|
-
if (mode === "read_only" && !isReadOnlyCommand(command)) return { allowed: false, mode, reason: `Role '${role}' is read-only and command may modify state.` };
|
|
27
|
-
return { allowed: true, mode };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function currentCrewRole(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
31
|
-
return env.PI_CREW_ROLE?.trim() || env.PI_TEAMS_ROLE?.trim() || undefined;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function checkSubagentSpawnPermission(role: string | undefined): PermissionCheckResult {
|
|
35
|
-
if (!role) return { allowed: true, mode: "workspace_write" };
|
|
36
|
-
const mode = permissionForRole(role);
|
|
37
|
-
if (mode === "read_only") return { allowed: false, mode, reason: `Role '${role}' is read-only and cannot spawn additional subagents.` };
|
|
38
|
-
return { allowed: true, mode };
|
|
39
|
-
}
|
|
1
|
+
export type RolePermissionMode = "read_only" | "workspace_write" | "danger_full_access" | "explicit_confirm";
|
|
2
|
+
|
|
3
|
+
const READ_ONLY_ROLES = new Set(["explorer", "reviewer", "security-reviewer", "verifier", "analyst", "critic", "planner", "writer"]);
|
|
4
|
+
const WRITE_ROLES = new Set(["executor", "test-engineer"]);
|
|
5
|
+
const READ_ONLY_COMMANDS = new Set(["cat", "head", "tail", "less", "more", "wc", "ls", "find", "grep", "rg", "awk", "sed", "echo", "printf", "which", "where", "whoami", "pwd", "env", "printenv", "date", "df", "du", "uname", "file", "stat", "diff", "sort", "uniq", "tr", "cut", "paste", "test", "true", "false", "type", "readlink", "realpath", "basename", "dirname", "sha256sum", "md5sum", "xxd", "hexdump", "od", "strings", "tree", "jq", "git", "gh"]);
|
|
6
|
+
|
|
7
|
+
export interface PermissionCheckResult {
|
|
8
|
+
allowed: boolean;
|
|
9
|
+
mode: RolePermissionMode;
|
|
10
|
+
reason?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function permissionForRole(role: string): RolePermissionMode {
|
|
14
|
+
if (READ_ONLY_ROLES.has(role)) return "read_only";
|
|
15
|
+
if (WRITE_ROLES.has(role)) return "workspace_write";
|
|
16
|
+
return "workspace_write";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isReadOnlyCommand(command: string): boolean {
|
|
20
|
+
const first = command.trim().split(/\s+/)[0]?.split(/[\\/]/).pop() ?? "";
|
|
21
|
+
return READ_ONLY_COMMANDS.has(first) && !/\s(-i|--in-place)\b|\s>{1,2}\s|\brm\b|\bmv\b|\bcp\b|\bnpm\s+install\b|\bgit\s+(commit|push|merge|rebase|reset|checkout)\b/.test(command);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function checkRolePermission(role: string, command: string): PermissionCheckResult {
|
|
25
|
+
const mode = permissionForRole(role);
|
|
26
|
+
if (mode === "read_only" && !isReadOnlyCommand(command)) return { allowed: false, mode, reason: `Role '${role}' is read-only and command may modify state.` };
|
|
27
|
+
return { allowed: true, mode };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function currentCrewRole(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
31
|
+
return env.PI_CREW_ROLE?.trim() || env.PI_TEAMS_ROLE?.trim() || undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function checkSubagentSpawnPermission(role: string | undefined): PermissionCheckResult {
|
|
35
|
+
if (!role) return { allowed: true, mode: "workspace_write" };
|
|
36
|
+
const mode = permissionForRole(role);
|
|
37
|
+
if (mode === "read_only") return { allowed: false, mode, reason: `Role '${role}' is read-only and cannot spawn additional subagents.` };
|
|
38
|
+
return { allowed: true, mode };
|
|
39
|
+
}
|
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import type { UsageState } from "../state/types.ts";
|
|
3
|
-
|
|
4
|
-
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
5
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function numberField(obj: Record<string, unknown>, keys: string[]): number | undefined {
|
|
9
|
-
for (const key of keys) {
|
|
10
|
-
const value = obj[key];
|
|
11
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
12
|
-
}
|
|
13
|
-
return undefined;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function usageFromValue(value: unknown): UsageState | undefined {
|
|
17
|
-
const obj = asRecord(value);
|
|
18
|
-
if (!obj) return undefined;
|
|
19
|
-
const direct: UsageState = {
|
|
20
|
-
input: numberField(obj, ["input", "inputTokens", "input_tokens"]),
|
|
21
|
-
output: numberField(obj, ["output", "outputTokens", "output_tokens"]),
|
|
22
|
-
cacheRead: numberField(obj, ["cacheRead", "cache_read", "cacheReadTokens", "cache_read_tokens"]),
|
|
23
|
-
cacheWrite: numberField(obj, ["cacheWrite", "cache_write", "cacheWriteTokens", "cache_write_tokens"]),
|
|
24
|
-
cost: numberField(obj, ["cost", "costUsd", "cost_usd"]),
|
|
25
|
-
turns: numberField(obj, ["turns", "turnCount", "turn_count"]),
|
|
26
|
-
};
|
|
27
|
-
if (Object.values(direct).some((entry) => entry !== undefined)) return direct;
|
|
28
|
-
for (const key of ["usage", "tokenUsage", "tokens", "stats"]) {
|
|
29
|
-
const nested = usageFromValue(obj[key]);
|
|
30
|
-
if (nested) return nested;
|
|
31
|
-
}
|
|
32
|
-
const message = asRecord(obj.message);
|
|
33
|
-
return message ? usageFromValue(message.usage) : undefined;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function addUsage(total: UsageState, usage: UsageState): UsageState {
|
|
37
|
-
return {
|
|
38
|
-
input: (total.input ?? 0) + (usage.input ?? 0),
|
|
39
|
-
output: (total.output ?? 0) + (usage.output ?? 0),
|
|
40
|
-
cacheRead: (total.cacheRead ?? 0) + (usage.cacheRead ?? 0),
|
|
41
|
-
cacheWrite: (total.cacheWrite ?? 0) + (usage.cacheWrite ?? 0),
|
|
42
|
-
cost: (total.cost ?? 0) + (usage.cost ?? 0),
|
|
43
|
-
turns: (total.turns ?? 0) + (usage.turns ?? 0),
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function compactUsage(total: UsageState, foundKeys: Set<keyof UsageState>): UsageState | undefined {
|
|
48
|
-
if (foundKeys.size === 0) return undefined;
|
|
49
|
-
const compact: UsageState = {};
|
|
50
|
-
for (const key of foundKeys) compact[key] = total[key];
|
|
51
|
-
return compact;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function parseSessionUsageFromJsonlText(text: string): UsageState | undefined {
|
|
55
|
-
let total: UsageState = {};
|
|
56
|
-
const foundKeys = new Set<keyof UsageState>();
|
|
57
|
-
for (const line of text.split(/\r?\n/)) {
|
|
58
|
-
const trimmed = line.trim();
|
|
59
|
-
if (!trimmed) continue;
|
|
60
|
-
try {
|
|
61
|
-
const usage = usageFromValue(JSON.parse(trimmed) as unknown);
|
|
62
|
-
if (!usage) continue;
|
|
63
|
-
for (const key of Object.keys(usage) as Array<keyof UsageState>) foundKeys.add(key);
|
|
64
|
-
total = addUsage(total, usage);
|
|
65
|
-
} catch {
|
|
66
|
-
// Session JSONL can contain partial/corrupt lines after interrupted workers.
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return compactUsage(total, foundKeys);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function parseSessionUsage(filePath: string): UsageState | undefined {
|
|
73
|
-
try {
|
|
74
|
-
if (!fs.existsSync(filePath)) return undefined;
|
|
75
|
-
return parseSessionUsageFromJsonlText(fs.readFileSync(filePath, "utf-8"));
|
|
76
|
-
} catch {
|
|
77
|
-
return undefined;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import type { UsageState } from "../state/types.ts";
|
|
3
|
+
|
|
4
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
5
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function numberField(obj: Record<string, unknown>, keys: string[]): number | undefined {
|
|
9
|
+
for (const key of keys) {
|
|
10
|
+
const value = obj[key];
|
|
11
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function usageFromValue(value: unknown): UsageState | undefined {
|
|
17
|
+
const obj = asRecord(value);
|
|
18
|
+
if (!obj) return undefined;
|
|
19
|
+
const direct: UsageState = {
|
|
20
|
+
input: numberField(obj, ["input", "inputTokens", "input_tokens"]),
|
|
21
|
+
output: numberField(obj, ["output", "outputTokens", "output_tokens"]),
|
|
22
|
+
cacheRead: numberField(obj, ["cacheRead", "cache_read", "cacheReadTokens", "cache_read_tokens"]),
|
|
23
|
+
cacheWrite: numberField(obj, ["cacheWrite", "cache_write", "cacheWriteTokens", "cache_write_tokens"]),
|
|
24
|
+
cost: numberField(obj, ["cost", "costUsd", "cost_usd"]),
|
|
25
|
+
turns: numberField(obj, ["turns", "turnCount", "turn_count"]),
|
|
26
|
+
};
|
|
27
|
+
if (Object.values(direct).some((entry) => entry !== undefined)) return direct;
|
|
28
|
+
for (const key of ["usage", "tokenUsage", "tokens", "stats"]) {
|
|
29
|
+
const nested = usageFromValue(obj[key]);
|
|
30
|
+
if (nested) return nested;
|
|
31
|
+
}
|
|
32
|
+
const message = asRecord(obj.message);
|
|
33
|
+
return message ? usageFromValue(message.usage) : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function addUsage(total: UsageState, usage: UsageState): UsageState {
|
|
37
|
+
return {
|
|
38
|
+
input: (total.input ?? 0) + (usage.input ?? 0),
|
|
39
|
+
output: (total.output ?? 0) + (usage.output ?? 0),
|
|
40
|
+
cacheRead: (total.cacheRead ?? 0) + (usage.cacheRead ?? 0),
|
|
41
|
+
cacheWrite: (total.cacheWrite ?? 0) + (usage.cacheWrite ?? 0),
|
|
42
|
+
cost: (total.cost ?? 0) + (usage.cost ?? 0),
|
|
43
|
+
turns: (total.turns ?? 0) + (usage.turns ?? 0),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function compactUsage(total: UsageState, foundKeys: Set<keyof UsageState>): UsageState | undefined {
|
|
48
|
+
if (foundKeys.size === 0) return undefined;
|
|
49
|
+
const compact: UsageState = {};
|
|
50
|
+
for (const key of foundKeys) compact[key] = total[key];
|
|
51
|
+
return compact;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function parseSessionUsageFromJsonlText(text: string): UsageState | undefined {
|
|
55
|
+
let total: UsageState = {};
|
|
56
|
+
const foundKeys = new Set<keyof UsageState>();
|
|
57
|
+
for (const line of text.split(/\r?\n/)) {
|
|
58
|
+
const trimmed = line.trim();
|
|
59
|
+
if (!trimmed) continue;
|
|
60
|
+
try {
|
|
61
|
+
const usage = usageFromValue(JSON.parse(trimmed) as unknown);
|
|
62
|
+
if (!usage) continue;
|
|
63
|
+
for (const key of Object.keys(usage) as Array<keyof UsageState>) foundKeys.add(key);
|
|
64
|
+
total = addUsage(total, usage);
|
|
65
|
+
} catch {
|
|
66
|
+
// Session JSONL can contain partial/corrupt lines after interrupted workers.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return compactUsage(total, foundKeys);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function parseSessionUsage(filePath: string): UsageState | undefined {
|
|
73
|
+
try {
|
|
74
|
+
if (!fs.existsSync(filePath)) return undefined;
|
|
75
|
+
return parseSessionUsageFromJsonlText(fs.readFileSync(filePath, "utf-8"));
|
|
76
|
+
} catch {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
|
|
4
|
-
export interface SidechainEntry {
|
|
5
|
-
isSidechain: true;
|
|
6
|
-
agentId: string;
|
|
7
|
-
type: string;
|
|
8
|
-
message: unknown;
|
|
9
|
-
timestamp: string;
|
|
10
|
-
cwd: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function writeSidechainEntry(filePath: string, entry: Omit<SidechainEntry, "isSidechain" | "timestamp">): void {
|
|
14
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
15
|
-
fs.appendFileSync(filePath, `${JSON.stringify({ isSidechain: true, timestamp: new Date().toISOString(), ...entry })}\n`, "utf-8");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function sidechainOutputPath(stateRoot: string, taskId: string): string {
|
|
19
|
-
return path.join(stateRoot, "agents", taskId, "sidechain.output.jsonl");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function eventToSidechainType(event: unknown): string | undefined {
|
|
23
|
-
if (!event || typeof event !== "object" || Array.isArray(event)) return undefined;
|
|
24
|
-
const type = (event as { type?: unknown }).type;
|
|
25
|
-
if (type === "message_start" || type === "message_update" || type === "message_end") return "message";
|
|
26
|
-
if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") return "tool";
|
|
27
|
-
return typeof type === "string" ? type : undefined;
|
|
28
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface SidechainEntry {
|
|
5
|
+
isSidechain: true;
|
|
6
|
+
agentId: string;
|
|
7
|
+
type: string;
|
|
8
|
+
message: unknown;
|
|
9
|
+
timestamp: string;
|
|
10
|
+
cwd: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function writeSidechainEntry(filePath: string, entry: Omit<SidechainEntry, "isSidechain" | "timestamp">): void {
|
|
14
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
15
|
+
fs.appendFileSync(filePath, `${JSON.stringify({ isSidechain: true, timestamp: new Date().toISOString(), ...entry })}\n`, "utf-8");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function sidechainOutputPath(stateRoot: string, taskId: string): string {
|
|
19
|
+
return path.join(stateRoot, "agents", taskId, "sidechain.output.jsonl");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function eventToSidechainType(event: unknown): string | undefined {
|
|
23
|
+
if (!event || typeof event !== "object" || Array.isArray(event)) return undefined;
|
|
24
|
+
const type = (event as { type?: unknown }).type;
|
|
25
|
+
if (type === "message_start" || type === "message_update" || type === "message_end") return "message";
|
|
26
|
+
if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") return "tool";
|
|
27
|
+
return typeof type === "string" ? type : undefined;
|
|
28
|
+
}
|
|
@@ -108,6 +108,9 @@ function totalRunTurns(cwd: string, runId: string | undefined): number | undefin
|
|
|
108
108
|
|
|
109
109
|
export class SubagentManager {
|
|
110
110
|
private readonly records = new Map<string, SubagentRecord>();
|
|
111
|
+
private readonly cwdByRecord = new Map<string, string>();
|
|
112
|
+
private readonly controllers = new Map<string, AbortController>();
|
|
113
|
+
private readonly controllerCleanup = new Map<string, () => void>();
|
|
111
114
|
private queue: QueuedSpawn[] = [];
|
|
112
115
|
private runningBackground = 0;
|
|
113
116
|
private counter = 0;
|
|
@@ -135,6 +138,7 @@ export class SubagentManager {
|
|
|
135
138
|
background: options.background,
|
|
136
139
|
};
|
|
137
140
|
this.records.set(record.id, record);
|
|
141
|
+
this.cwdByRecord.set(record.id, options.cwd);
|
|
138
142
|
savePersistedSubagentRecord(options.cwd, record);
|
|
139
143
|
if (record.status === "queued") {
|
|
140
144
|
this.queue.push({ record, options, runner, signal });
|
|
@@ -157,28 +161,26 @@ export class SubagentManager {
|
|
|
157
161
|
if (!record) return false;
|
|
158
162
|
if (record.status === "queued") {
|
|
159
163
|
this.queue = this.queue.filter((entry) => entry.record.id !== id);
|
|
160
|
-
record
|
|
161
|
-
record.completedAt = Date.now();
|
|
164
|
+
this.markStopped(record);
|
|
162
165
|
return true;
|
|
163
166
|
}
|
|
164
167
|
if (record.status !== "running" && record.status !== "blocked") return false;
|
|
165
|
-
|
|
166
|
-
|
|
168
|
+
this.controllers.get(id)?.abort();
|
|
169
|
+
this.markStopped(record);
|
|
167
170
|
return true;
|
|
168
171
|
}
|
|
169
172
|
|
|
170
173
|
abortAll(): number {
|
|
171
174
|
let count = 0;
|
|
172
175
|
for (const entry of this.queue) {
|
|
173
|
-
entry.record
|
|
174
|
-
entry.record.completedAt = Date.now();
|
|
176
|
+
this.markStopped(entry.record);
|
|
175
177
|
count++;
|
|
176
178
|
}
|
|
177
179
|
this.queue = [];
|
|
178
180
|
for (const record of this.records.values()) {
|
|
179
181
|
if (record.status === "running" || record.status === "blocked") {
|
|
180
|
-
record.
|
|
181
|
-
|
|
182
|
+
this.controllers.get(record.id)?.abort();
|
|
183
|
+
this.markStopped(record);
|
|
182
184
|
count++;
|
|
183
185
|
}
|
|
184
186
|
}
|
|
@@ -188,7 +190,7 @@ export class SubagentManager {
|
|
|
188
190
|
async waitForAll(): Promise<void> {
|
|
189
191
|
while (true) {
|
|
190
192
|
this.drainQueue();
|
|
191
|
-
const pending = this.listAgents().filter((record) => record.status === "running" || record.status === "
|
|
193
|
+
const pending = this.listAgents().filter((record) => record.status === "running" || record.status === "queued").map((record) => record.promise).filter((promise): promise is Promise<void> => Boolean(promise));
|
|
192
194
|
if (!pending.length) break;
|
|
193
195
|
await Promise.allSettled(pending);
|
|
194
196
|
}
|
|
@@ -198,7 +200,7 @@ export class SubagentManager {
|
|
|
198
200
|
while (true) {
|
|
199
201
|
const record = this.records.get(id);
|
|
200
202
|
if (!record) return undefined;
|
|
201
|
-
if (record.status !== "running" && record.status !== "
|
|
203
|
+
if (record.status !== "running" && record.status !== "queued") return record;
|
|
202
204
|
if (record.promise) await record.promise;
|
|
203
205
|
else await new Promise((resolve) => setTimeout(resolve, 100));
|
|
204
206
|
}
|
|
@@ -213,10 +215,13 @@ export class SubagentManager {
|
|
|
213
215
|
if (options.background) this.runningBackground++;
|
|
214
216
|
record.status = "running";
|
|
215
217
|
record.startedAt = Date.now();
|
|
218
|
+
record.completedAt = undefined;
|
|
219
|
+
const runSignal = this.createRunSignal(record.id, signal);
|
|
216
220
|
savePersistedSubagentRecord(options.cwd, record);
|
|
217
221
|
record.promise = (async () => {
|
|
218
222
|
try {
|
|
219
|
-
const result = await runner(options,
|
|
223
|
+
const result = await runner(options, runSignal);
|
|
224
|
+
if (record.status === "stopped") return;
|
|
220
225
|
record.runId = detailsRunId(result);
|
|
221
226
|
record.result = resultText(result);
|
|
222
227
|
savePersistedSubagentRecord(options.cwd, record);
|
|
@@ -228,11 +233,16 @@ export class SubagentManager {
|
|
|
228
233
|
if (record.runId) await this.pollRunToTerminal(options.cwd, record);
|
|
229
234
|
else record.status = "completed";
|
|
230
235
|
} catch (error) {
|
|
236
|
+
if (record.status === "stopped" || runSignal.aborted) {
|
|
237
|
+
record.status = "stopped";
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
231
240
|
record.status = "error";
|
|
232
241
|
record.error = error instanceof Error ? error.message : String(error);
|
|
233
242
|
} finally {
|
|
243
|
+
this.cleanupRunSignal(record.id);
|
|
234
244
|
if (options.background) this.runningBackground = Math.max(0, this.runningBackground - 1);
|
|
235
|
-
record.completedAt = record.completedAt ?? Date.now();
|
|
245
|
+
if (record.status !== "blocked") record.completedAt = record.completedAt ?? Date.now();
|
|
236
246
|
savePersistedSubagentRecord(options.cwd, record);
|
|
237
247
|
if (record.status === "completed" || record.status === "failed" || record.status === "cancelled" || record.status === "error" || record.status === "stopped") {
|
|
238
248
|
// Phase 1.6: Populate telemetry fields
|
|
@@ -246,6 +256,34 @@ export class SubagentManager {
|
|
|
246
256
|
})();
|
|
247
257
|
}
|
|
248
258
|
|
|
259
|
+
private markStopped(record: SubagentRecord): void {
|
|
260
|
+
record.status = "stopped";
|
|
261
|
+
record.completedAt = Date.now();
|
|
262
|
+
const cwd = this.cwdByRecord.get(record.id);
|
|
263
|
+
if (cwd) savePersistedSubagentRecord(cwd, record);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private createRunSignal(id: string, signal?: AbortSignal): AbortSignal {
|
|
267
|
+
const controller = new AbortController();
|
|
268
|
+
this.controllers.set(id, controller);
|
|
269
|
+
if (signal?.aborted) {
|
|
270
|
+
controller.abort();
|
|
271
|
+
return controller.signal;
|
|
272
|
+
}
|
|
273
|
+
if (signal) {
|
|
274
|
+
const abort = (): void => controller.abort();
|
|
275
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
276
|
+
this.controllerCleanup.set(id, () => signal.removeEventListener("abort", abort));
|
|
277
|
+
}
|
|
278
|
+
return controller.signal;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private cleanupRunSignal(id: string): void {
|
|
282
|
+
this.controllerCleanup.get(id)?.();
|
|
283
|
+
this.controllerCleanup.delete(id);
|
|
284
|
+
this.controllers.delete(id);
|
|
285
|
+
}
|
|
286
|
+
|
|
249
287
|
private drainQueue(): void {
|
|
250
288
|
while (this.queue.length > 0 && this.runningBackground < this.maxConcurrent) {
|
|
251
289
|
const next = this.queue.shift();
|
|
@@ -286,6 +324,7 @@ export class SubagentManager {
|
|
|
286
324
|
record.completedAt = undefined;
|
|
287
325
|
this.onComplete?.(record);
|
|
288
326
|
this.scheduleStuckBlockedNotify(cwd, record);
|
|
327
|
+
this.scheduleBlockedTerminalPoll(cwd, record);
|
|
289
328
|
}
|
|
290
329
|
savePersistedSubagentRecord(cwd, record);
|
|
291
330
|
return;
|
|
@@ -294,6 +333,35 @@ export class SubagentManager {
|
|
|
294
333
|
}
|
|
295
334
|
}
|
|
296
335
|
|
|
336
|
+
private scheduleBlockedTerminalPoll(cwd: string, record: SubagentRecord): void {
|
|
337
|
+
const poll = (): void => {
|
|
338
|
+
const current = this.records.get(record.id);
|
|
339
|
+
if (!current || current.status !== "blocked" || !current.runId) return;
|
|
340
|
+
const loaded = loadRunManifestById(cwd, current.runId);
|
|
341
|
+
if (!loaded || loaded.manifest.status === "blocked" || loaded.manifest.status === "running" || loaded.manifest.status === "planning" || loaded.manifest.status === "queued") {
|
|
342
|
+
const timer = setTimeout(poll, this.pollIntervalMs);
|
|
343
|
+
timer.unref?.();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const persisted = readPersistedSubagentRecord(cwd, current.id);
|
|
347
|
+
current.resultConsumed = current.resultConsumed || persisted?.resultConsumed;
|
|
348
|
+
if (loaded.manifest.status === "completed") {
|
|
349
|
+
current.status = "completed";
|
|
350
|
+
current.error = undefined;
|
|
351
|
+
} else if (loaded.manifest.status === "failed" || loaded.manifest.status === "cancelled") {
|
|
352
|
+
current.status = loaded.manifest.status;
|
|
353
|
+
current.error = loaded.manifest.summary;
|
|
354
|
+
} else return;
|
|
355
|
+
current.completedAt = Date.now();
|
|
356
|
+
current.turnCount = current.turnCount ?? totalRunTurns(cwd, current.runId);
|
|
357
|
+
current.durationMs = Math.max(0, current.completedAt - current.startedAt);
|
|
358
|
+
savePersistedSubagentRecord(cwd, current);
|
|
359
|
+
this.onComplete?.(current);
|
|
360
|
+
};
|
|
361
|
+
const timer = setTimeout(poll, this.pollIntervalMs);
|
|
362
|
+
timer.unref?.();
|
|
363
|
+
}
|
|
364
|
+
|
|
297
365
|
private scheduleStuckBlockedNotify(cwd: string, record: SubagentRecord): void {
|
|
298
366
|
const threshold = DEFAULT_SUBAGENT.stuckBlockedNotifyMs;
|
|
299
367
|
const fire = (): void => {
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import type { TeamTaskState } from "../state/types.ts";
|
|
2
|
-
import type { CrewAgentRecord, CrewRuntimeKind } from "./crew-agent-runtime.ts";
|
|
3
|
-
import { recordFromTask } from "./crew-agent-records.ts";
|
|
4
|
-
import type { TeamRunManifest } from "../state/types.ts";
|
|
5
|
-
|
|
6
|
-
export function shouldMaterializeAgent(task: TeamTaskState): boolean {
|
|
7
|
-
return task.status !== "queued" && task.status !== "skipped";
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function recordsForMaterializedTasks(manifest: TeamRunManifest, tasks: TeamTaskState[], runtime: CrewRuntimeKind): CrewAgentRecord[] {
|
|
11
|
-
return tasks.filter(shouldMaterializeAgent).map((task) => recordFromTask(manifest, task, runtime));
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function taskById(tasks: TeamTaskState[]): Map<string, TeamTaskState> {
|
|
15
|
-
const map = new Map<string, TeamTaskState>();
|
|
16
|
-
for (const task of tasks) {
|
|
17
|
-
map.set(task.id, task);
|
|
18
|
-
if (task.stepId) map.set(task.stepId, task);
|
|
19
|
-
}
|
|
20
|
-
return map;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function waitingReason(task: TeamTaskState, tasks: TeamTaskState[]): string | undefined {
|
|
24
|
-
if (task.status !== "queued") return undefined;
|
|
25
|
-
const byId = taskById(tasks);
|
|
26
|
-
const waiting = task.dependsOn.map((id) => byId.get(id)?.id ?? id).filter((id) => byId.get(id)?.status !== "completed");
|
|
27
|
-
if (waiting.length === 0) return "ready";
|
|
28
|
-
return `waiting for ${waiting.join(", ")}`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function formatTaskGraphLines(tasks: TeamTaskState[]): string[] {
|
|
32
|
-
if (tasks.length === 0) return ["- (none)"];
|
|
33
|
-
return tasks.map((task) => {
|
|
34
|
-
const icon = task.status === "completed" ? "✓" : task.status === "running" ? "⠋" : task.status === "failed" ? "✗" : task.status === "cancelled" || task.status === "skipped" ? "■" : "◦";
|
|
35
|
-
const wait = waitingReason(task, tasks);
|
|
36
|
-
return `- ${icon} ${task.id} [${task.status}] ${task.role}->${task.agent}${wait && wait !== "ready" ? ` (${wait})` : ""}`;
|
|
37
|
-
});
|
|
38
|
-
}
|
|
1
|
+
import type { TeamTaskState } from "../state/types.ts";
|
|
2
|
+
import type { CrewAgentRecord, CrewRuntimeKind } from "./crew-agent-runtime.ts";
|
|
3
|
+
import { recordFromTask } from "./crew-agent-records.ts";
|
|
4
|
+
import type { TeamRunManifest } from "../state/types.ts";
|
|
5
|
+
|
|
6
|
+
export function shouldMaterializeAgent(task: TeamTaskState): boolean {
|
|
7
|
+
return task.status !== "queued" && task.status !== "skipped";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function recordsForMaterializedTasks(manifest: TeamRunManifest, tasks: TeamTaskState[], runtime: CrewRuntimeKind): CrewAgentRecord[] {
|
|
11
|
+
return tasks.filter(shouldMaterializeAgent).map((task) => recordFromTask(manifest, task, runtime));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function taskById(tasks: TeamTaskState[]): Map<string, TeamTaskState> {
|
|
15
|
+
const map = new Map<string, TeamTaskState>();
|
|
16
|
+
for (const task of tasks) {
|
|
17
|
+
map.set(task.id, task);
|
|
18
|
+
if (task.stepId) map.set(task.stepId, task);
|
|
19
|
+
}
|
|
20
|
+
return map;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function waitingReason(task: TeamTaskState, tasks: TeamTaskState[]): string | undefined {
|
|
24
|
+
if (task.status !== "queued") return undefined;
|
|
25
|
+
const byId = taskById(tasks);
|
|
26
|
+
const waiting = task.dependsOn.map((id) => byId.get(id)?.id ?? id).filter((id) => byId.get(id)?.status !== "completed");
|
|
27
|
+
if (waiting.length === 0) return "ready";
|
|
28
|
+
return `waiting for ${waiting.join(", ")}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function formatTaskGraphLines(tasks: TeamTaskState[]): string[] {
|
|
32
|
+
if (tasks.length === 0) return ["- (none)"];
|
|
33
|
+
return tasks.map((task) => {
|
|
34
|
+
const icon = task.status === "completed" ? "✓" : task.status === "running" ? "⠋" : task.status === "failed" ? "✗" : task.status === "cancelled" || task.status === "skipped" ? "■" : "◦";
|
|
35
|
+
const wait = waitingReason(task, tasks);
|
|
36
|
+
return `- ${icon} ${task.id} [${task.status}] ${task.role}->${task.agent}${wait && wait !== "ready" ? ` (${wait})` : ""}`;
|
|
37
|
+
});
|
|
38
|
+
}
|