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,99 +1,99 @@
|
|
|
1
|
-
export interface RunnerSubagentStep {
|
|
2
|
-
agent: string;
|
|
3
|
-
task: string;
|
|
4
|
-
cwd?: string;
|
|
5
|
-
model?: string;
|
|
6
|
-
modelCandidates?: string[];
|
|
7
|
-
tools?: string[];
|
|
8
|
-
extensions?: string[];
|
|
9
|
-
mcpDirectTools?: string[];
|
|
10
|
-
systemPrompt?: string | null;
|
|
11
|
-
systemPromptMode?: "append" | "replace";
|
|
12
|
-
inheritProjectContext: boolean;
|
|
13
|
-
inheritSkills: boolean;
|
|
14
|
-
skills?: string[];
|
|
15
|
-
outputPath?: string;
|
|
16
|
-
sessionFile?: string;
|
|
17
|
-
maxSubagentDepth?: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface ParallelStepGroup {
|
|
21
|
-
parallel: RunnerSubagentStep[];
|
|
22
|
-
concurrency?: number;
|
|
23
|
-
failFast?: boolean;
|
|
24
|
-
worktree?: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export type RunnerStep = RunnerSubagentStep | ParallelStepGroup;
|
|
28
|
-
|
|
29
|
-
export function isParallelGroup(step: RunnerStep): step is ParallelStepGroup {
|
|
30
|
-
return "parallel" in step && Array.isArray(step.parallel);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function flattenSteps(steps: RunnerStep[]): RunnerSubagentStep[] {
|
|
34
|
-
const flat: RunnerSubagentStep[] = [];
|
|
35
|
-
for (const step of steps) {
|
|
36
|
-
if (isParallelGroup(step)) {
|
|
37
|
-
for (const task of step.parallel) flat.push(task);
|
|
38
|
-
} else {
|
|
39
|
-
flat.push(step);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return flat;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export async function mapConcurrent<T, R>(items: T[], limit: number, fn: (item: T, i: number) => Promise<R>): Promise<R[]> {
|
|
46
|
-
const safeLimit = Math.max(1, Math.floor(limit) || 1);
|
|
47
|
-
const results: R[] = new Array(items.length);
|
|
48
|
-
let next = 0;
|
|
49
|
-
|
|
50
|
-
const worker = async (_workerIndex: number): Promise<void> => {
|
|
51
|
-
while (next < items.length) {
|
|
52
|
-
const i = next++;
|
|
53
|
-
results[i] = await fn(items[i], i);
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
await Promise.all(Array.from({ length: Math.min(safeLimit, items.length) }, (_, workerIndex) => worker(workerIndex)));
|
|
58
|
-
return results;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface ParallelTaskResult {
|
|
62
|
-
agent: string;
|
|
63
|
-
taskIndex?: number;
|
|
64
|
-
output: string;
|
|
65
|
-
exitCode: number | null;
|
|
66
|
-
error?: string;
|
|
67
|
-
model?: string;
|
|
68
|
-
attemptedModels?: string[];
|
|
69
|
-
outputTargetPath?: string;
|
|
70
|
-
outputTargetExists?: boolean;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function aggregateParallelOutputs(
|
|
74
|
-
results: ParallelTaskResult[],
|
|
75
|
-
headerFormat: (index: number, agent: string) => string = (i, agent) => `=== Parallel Task ${i + 1} (${agent}) ===`,
|
|
76
|
-
): string {
|
|
77
|
-
return results
|
|
78
|
-
.map((r, i) => {
|
|
79
|
-
const header = headerFormat(r.taskIndex ?? i, r.agent);
|
|
80
|
-
const hasOutput = Boolean(r.output?.trim());
|
|
81
|
-
const status =
|
|
82
|
-
r.exitCode === -1
|
|
83
|
-
? "SKIPPED"
|
|
84
|
-
: r.exitCode !== 0 && r.exitCode !== null
|
|
85
|
-
? `FAILED (exit code ${r.exitCode})${r.error ? `: ${r.error}` : ""}`
|
|
86
|
-
: r.error
|
|
87
|
-
? `WARNING: ${r.error}`
|
|
88
|
-
: !hasOutput && r.outputTargetPath && r.outputTargetExists === false
|
|
89
|
-
? `EMPTY OUTPUT (expected output file missing: ${r.outputTargetPath})`
|
|
90
|
-
: !hasOutput && !r.outputTargetPath
|
|
91
|
-
? "EMPTY OUTPUT (no textual response returned)"
|
|
92
|
-
: "";
|
|
93
|
-
const body = status ? (hasOutput ? `${status}\n${r.output}` : status) : r.output;
|
|
94
|
-
return `${header}\n${body}`;
|
|
95
|
-
})
|
|
96
|
-
.join("\n\n");
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export const MAX_PARALLEL_CONCURRENCY = 4;
|
|
1
|
+
export interface RunnerSubagentStep {
|
|
2
|
+
agent: string;
|
|
3
|
+
task: string;
|
|
4
|
+
cwd?: string;
|
|
5
|
+
model?: string;
|
|
6
|
+
modelCandidates?: string[];
|
|
7
|
+
tools?: string[];
|
|
8
|
+
extensions?: string[];
|
|
9
|
+
mcpDirectTools?: string[];
|
|
10
|
+
systemPrompt?: string | null;
|
|
11
|
+
systemPromptMode?: "append" | "replace";
|
|
12
|
+
inheritProjectContext: boolean;
|
|
13
|
+
inheritSkills: boolean;
|
|
14
|
+
skills?: string[];
|
|
15
|
+
outputPath?: string;
|
|
16
|
+
sessionFile?: string;
|
|
17
|
+
maxSubagentDepth?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ParallelStepGroup {
|
|
21
|
+
parallel: RunnerSubagentStep[];
|
|
22
|
+
concurrency?: number;
|
|
23
|
+
failFast?: boolean;
|
|
24
|
+
worktree?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type RunnerStep = RunnerSubagentStep | ParallelStepGroup;
|
|
28
|
+
|
|
29
|
+
export function isParallelGroup(step: RunnerStep): step is ParallelStepGroup {
|
|
30
|
+
return "parallel" in step && Array.isArray(step.parallel);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function flattenSteps(steps: RunnerStep[]): RunnerSubagentStep[] {
|
|
34
|
+
const flat: RunnerSubagentStep[] = [];
|
|
35
|
+
for (const step of steps) {
|
|
36
|
+
if (isParallelGroup(step)) {
|
|
37
|
+
for (const task of step.parallel) flat.push(task);
|
|
38
|
+
} else {
|
|
39
|
+
flat.push(step);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return flat;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function mapConcurrent<T, R>(items: T[], limit: number, fn: (item: T, i: number) => Promise<R>): Promise<R[]> {
|
|
46
|
+
const safeLimit = Math.max(1, Math.floor(limit) || 1);
|
|
47
|
+
const results: R[] = new Array(items.length);
|
|
48
|
+
let next = 0;
|
|
49
|
+
|
|
50
|
+
const worker = async (_workerIndex: number): Promise<void> => {
|
|
51
|
+
while (next < items.length) {
|
|
52
|
+
const i = next++;
|
|
53
|
+
results[i] = await fn(items[i], i);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
await Promise.all(Array.from({ length: Math.min(safeLimit, items.length) }, (_, workerIndex) => worker(workerIndex)));
|
|
58
|
+
return results;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ParallelTaskResult {
|
|
62
|
+
agent: string;
|
|
63
|
+
taskIndex?: number;
|
|
64
|
+
output: string;
|
|
65
|
+
exitCode: number | null;
|
|
66
|
+
error?: string;
|
|
67
|
+
model?: string;
|
|
68
|
+
attemptedModels?: string[];
|
|
69
|
+
outputTargetPath?: string;
|
|
70
|
+
outputTargetExists?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function aggregateParallelOutputs(
|
|
74
|
+
results: ParallelTaskResult[],
|
|
75
|
+
headerFormat: (index: number, agent: string) => string = (i, agent) => `=== Parallel Task ${i + 1} (${agent}) ===`,
|
|
76
|
+
): string {
|
|
77
|
+
return results
|
|
78
|
+
.map((r, i) => {
|
|
79
|
+
const header = headerFormat(r.taskIndex ?? i, r.agent);
|
|
80
|
+
const hasOutput = Boolean(r.output?.trim());
|
|
81
|
+
const status =
|
|
82
|
+
r.exitCode === -1
|
|
83
|
+
? "SKIPPED"
|
|
84
|
+
: r.exitCode !== 0 && r.exitCode !== null
|
|
85
|
+
? `FAILED (exit code ${r.exitCode})${r.error ? `: ${r.error}` : ""}`
|
|
86
|
+
: r.error
|
|
87
|
+
? `WARNING: ${r.error}`
|
|
88
|
+
: !hasOutput && r.outputTargetPath && r.outputTargetExists === false
|
|
89
|
+
? `EMPTY OUTPUT (expected output file missing: ${r.outputTargetPath})`
|
|
90
|
+
: !hasOutput && !r.outputTargetPath
|
|
91
|
+
? "EMPTY OUTPUT (no textual response returned)"
|
|
92
|
+
: "";
|
|
93
|
+
const body = status ? (hasOutput ? `${status}\n${r.output}` : status) : r.output;
|
|
94
|
+
return `${header}\n${body}`;
|
|
95
|
+
})
|
|
96
|
+
.join("\n\n");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const MAX_PARALLEL_CONCURRENCY = 4;
|
|
@@ -1,111 +1,111 @@
|
|
|
1
|
-
export interface ParsedPiUsage {
|
|
2
|
-
input?: number;
|
|
3
|
-
output?: number;
|
|
4
|
-
cacheRead?: number;
|
|
5
|
-
cacheWrite?: number;
|
|
6
|
-
cost?: number;
|
|
7
|
-
turns?: number;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface ParsedPiJsonOutput {
|
|
11
|
-
jsonEvents: number;
|
|
12
|
-
textEvents: string[];
|
|
13
|
-
finalText?: string;
|
|
14
|
-
usage?: ParsedPiUsage;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
18
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function numberField(obj: Record<string, unknown>, keys: string[]): number | undefined {
|
|
22
|
-
for (const key of keys) {
|
|
23
|
-
const value = obj[key];
|
|
24
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
25
|
-
}
|
|
26
|
-
return undefined;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function mergeUsage(target: ParsedPiUsage, source: ParsedPiUsage): ParsedPiUsage {
|
|
30
|
-
return {
|
|
31
|
-
input: source.input ?? target.input,
|
|
32
|
-
output: source.output ?? target.output,
|
|
33
|
-
cacheRead: source.cacheRead ?? target.cacheRead,
|
|
34
|
-
cacheWrite: source.cacheWrite ?? target.cacheWrite,
|
|
35
|
-
cost: source.cost ?? target.cost,
|
|
36
|
-
turns: source.turns ?? target.turns,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function extractUsage(value: unknown): ParsedPiUsage | undefined {
|
|
41
|
-
const obj = asRecord(value);
|
|
42
|
-
if (!obj) return undefined;
|
|
43
|
-
const direct: ParsedPiUsage = {
|
|
44
|
-
input: numberField(obj, ["input", "inputTokens", "input_tokens"]),
|
|
45
|
-
output: numberField(obj, ["output", "outputTokens", "output_tokens"]),
|
|
46
|
-
cacheRead: numberField(obj, ["cacheRead", "cache_read", "cacheReadTokens", "cache_read_tokens"]),
|
|
47
|
-
cacheWrite: numberField(obj, ["cacheWrite", "cache_write", "cacheWriteTokens", "cache_write_tokens"]),
|
|
48
|
-
cost: numberField(obj, ["cost", "costUsd", "cost_usd"]),
|
|
49
|
-
turns: numberField(obj, ["turns", "turnCount", "turn_count"]),
|
|
50
|
-
};
|
|
51
|
-
if (Object.values(direct).some((entry) => entry !== undefined)) return direct;
|
|
52
|
-
for (const key of ["usage", "tokenUsage", "tokens", "stats"]) {
|
|
53
|
-
const nested = extractUsage(obj[key]);
|
|
54
|
-
if (nested) return nested;
|
|
55
|
-
}
|
|
56
|
-
return undefined;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function textFromContent(content: unknown): string[] {
|
|
60
|
-
if (typeof content === "string") return [content];
|
|
61
|
-
if (!Array.isArray(content)) return [];
|
|
62
|
-
const text: string[] = [];
|
|
63
|
-
for (const part of content) {
|
|
64
|
-
const obj = asRecord(part);
|
|
65
|
-
if (!obj) continue;
|
|
66
|
-
if (obj.type === "text" && typeof obj.text === "string") text.push(obj.text);
|
|
67
|
-
else if (typeof obj.content === "string") text.push(obj.content);
|
|
68
|
-
}
|
|
69
|
-
return text;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function extractText(value: unknown): string[] {
|
|
73
|
-
const obj = asRecord(value);
|
|
74
|
-
if (!obj) return [];
|
|
75
|
-
const message = asRecord(obj.message);
|
|
76
|
-
if (message?.role !== undefined && message.role !== "assistant") return [];
|
|
77
|
-
const text: string[] = [];
|
|
78
|
-
if (typeof obj.text === "string") text.push(obj.text);
|
|
79
|
-
if (typeof obj.output === "string") text.push(obj.output);
|
|
80
|
-
if (typeof obj.finalOutput === "string") text.push(obj.finalOutput);
|
|
81
|
-
if (typeof obj.final_output === "string") text.push(obj.final_output);
|
|
82
|
-
if (!message) text.push(...textFromContent(obj.content));
|
|
83
|
-
if (message) text.push(...textFromContent(message.content));
|
|
84
|
-
return text.filter((entry) => entry.trim().length > 0);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function parsePiJsonOutput(stdout: string): ParsedPiJsonOutput {
|
|
88
|
-
let jsonEvents = 0;
|
|
89
|
-
const textEvents: string[] = [];
|
|
90
|
-
let usage: ParsedPiUsage | undefined;
|
|
91
|
-
for (const line of stdout.split("\n")) {
|
|
92
|
-
const trimmed = line.trim();
|
|
93
|
-
if (!trimmed) continue;
|
|
94
|
-
let event: unknown;
|
|
95
|
-
try {
|
|
96
|
-
event = JSON.parse(trimmed) as unknown;
|
|
97
|
-
} catch {
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
jsonEvents++;
|
|
101
|
-
textEvents.push(...extractText(event));
|
|
102
|
-
const eventUsage = extractUsage(event);
|
|
103
|
-
if (eventUsage) usage = mergeUsage(usage ?? {}, eventUsage);
|
|
104
|
-
}
|
|
105
|
-
return {
|
|
106
|
-
jsonEvents,
|
|
107
|
-
textEvents,
|
|
108
|
-
finalText: textEvents.length > 0 ? textEvents[textEvents.length - 1] : undefined,
|
|
109
|
-
usage,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
1
|
+
export interface ParsedPiUsage {
|
|
2
|
+
input?: number;
|
|
3
|
+
output?: number;
|
|
4
|
+
cacheRead?: number;
|
|
5
|
+
cacheWrite?: number;
|
|
6
|
+
cost?: number;
|
|
7
|
+
turns?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ParsedPiJsonOutput {
|
|
11
|
+
jsonEvents: number;
|
|
12
|
+
textEvents: string[];
|
|
13
|
+
finalText?: string;
|
|
14
|
+
usage?: ParsedPiUsage;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
18
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function numberField(obj: Record<string, unknown>, keys: string[]): number | undefined {
|
|
22
|
+
for (const key of keys) {
|
|
23
|
+
const value = obj[key];
|
|
24
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function mergeUsage(target: ParsedPiUsage, source: ParsedPiUsage): ParsedPiUsage {
|
|
30
|
+
return {
|
|
31
|
+
input: source.input ?? target.input,
|
|
32
|
+
output: source.output ?? target.output,
|
|
33
|
+
cacheRead: source.cacheRead ?? target.cacheRead,
|
|
34
|
+
cacheWrite: source.cacheWrite ?? target.cacheWrite,
|
|
35
|
+
cost: source.cost ?? target.cost,
|
|
36
|
+
turns: source.turns ?? target.turns,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function extractUsage(value: unknown): ParsedPiUsage | undefined {
|
|
41
|
+
const obj = asRecord(value);
|
|
42
|
+
if (!obj) return undefined;
|
|
43
|
+
const direct: ParsedPiUsage = {
|
|
44
|
+
input: numberField(obj, ["input", "inputTokens", "input_tokens"]),
|
|
45
|
+
output: numberField(obj, ["output", "outputTokens", "output_tokens"]),
|
|
46
|
+
cacheRead: numberField(obj, ["cacheRead", "cache_read", "cacheReadTokens", "cache_read_tokens"]),
|
|
47
|
+
cacheWrite: numberField(obj, ["cacheWrite", "cache_write", "cacheWriteTokens", "cache_write_tokens"]),
|
|
48
|
+
cost: numberField(obj, ["cost", "costUsd", "cost_usd"]),
|
|
49
|
+
turns: numberField(obj, ["turns", "turnCount", "turn_count"]),
|
|
50
|
+
};
|
|
51
|
+
if (Object.values(direct).some((entry) => entry !== undefined)) return direct;
|
|
52
|
+
for (const key of ["usage", "tokenUsage", "tokens", "stats"]) {
|
|
53
|
+
const nested = extractUsage(obj[key]);
|
|
54
|
+
if (nested) return nested;
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function textFromContent(content: unknown): string[] {
|
|
60
|
+
if (typeof content === "string") return [content];
|
|
61
|
+
if (!Array.isArray(content)) return [];
|
|
62
|
+
const text: string[] = [];
|
|
63
|
+
for (const part of content) {
|
|
64
|
+
const obj = asRecord(part);
|
|
65
|
+
if (!obj) continue;
|
|
66
|
+
if (obj.type === "text" && typeof obj.text === "string") text.push(obj.text);
|
|
67
|
+
else if (typeof obj.content === "string") text.push(obj.content);
|
|
68
|
+
}
|
|
69
|
+
return text;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function extractText(value: unknown): string[] {
|
|
73
|
+
const obj = asRecord(value);
|
|
74
|
+
if (!obj) return [];
|
|
75
|
+
const message = asRecord(obj.message);
|
|
76
|
+
if (message?.role !== undefined && message.role !== "assistant") return [];
|
|
77
|
+
const text: string[] = [];
|
|
78
|
+
if (typeof obj.text === "string") text.push(obj.text);
|
|
79
|
+
if (typeof obj.output === "string") text.push(obj.output);
|
|
80
|
+
if (typeof obj.finalOutput === "string") text.push(obj.finalOutput);
|
|
81
|
+
if (typeof obj.final_output === "string") text.push(obj.final_output);
|
|
82
|
+
if (!message) text.push(...textFromContent(obj.content));
|
|
83
|
+
if (message) text.push(...textFromContent(message.content));
|
|
84
|
+
return text.filter((entry) => entry.trim().length > 0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function parsePiJsonOutput(stdout: string): ParsedPiJsonOutput {
|
|
88
|
+
let jsonEvents = 0;
|
|
89
|
+
const textEvents: string[] = [];
|
|
90
|
+
let usage: ParsedPiUsage | undefined;
|
|
91
|
+
for (const line of stdout.split("\n")) {
|
|
92
|
+
const trimmed = line.trim();
|
|
93
|
+
if (!trimmed) continue;
|
|
94
|
+
let event: unknown;
|
|
95
|
+
try {
|
|
96
|
+
event = JSON.parse(trimmed) as unknown;
|
|
97
|
+
} catch {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
jsonEvents++;
|
|
101
|
+
textEvents.push(...extractText(event));
|
|
102
|
+
const eventUsage = extractUsage(event);
|
|
103
|
+
if (eventUsage) usage = mergeUsage(usage ?? {}, eventUsage);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
jsonEvents,
|
|
107
|
+
textEvents,
|
|
108
|
+
finalText: textEvents.length > 0 ? textEvents[textEvents.length - 1] : undefined,
|
|
109
|
+
usage,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
import type { CrewLimitsConfig } from "../config/config.ts";
|
|
2
|
-
import type { PolicyDecision, PolicyDecisionAction, PolicyDecisionReason, TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
3
|
-
import { evaluateGreenContract } from "./green-contract.ts";
|
|
4
|
-
import { isWorkerHeartbeatStale } from "./worker-heartbeat.ts";
|
|
5
|
-
|
|
6
|
-
export interface PolicyEngineInput {
|
|
7
|
-
manifest: TeamRunManifest;
|
|
8
|
-
tasks: TeamTaskState[];
|
|
9
|
-
limits?: CrewLimitsConfig;
|
|
10
|
-
now?: Date;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function decision(action: PolicyDecisionAction, reason: PolicyDecisionReason, message: string, taskId?: string): PolicyDecision {
|
|
14
|
-
return {
|
|
15
|
-
action,
|
|
16
|
-
reason,
|
|
17
|
-
message,
|
|
18
|
-
taskId,
|
|
19
|
-
createdAt: new Date().toISOString(),
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function taskDepth(task: TeamTaskState, tasksById: Map<string, TeamTaskState>): number {
|
|
24
|
-
let depth = 0;
|
|
25
|
-
let current = task.graph?.parentId;
|
|
26
|
-
const seen = new Set<string>();
|
|
27
|
-
while (current && !seen.has(current)) {
|
|
28
|
-
seen.add(current);
|
|
29
|
-
depth += 1;
|
|
30
|
-
current = tasksById.get(current)?.graph?.parentId;
|
|
31
|
-
}
|
|
32
|
-
return depth;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function evaluateCrewPolicy(input: PolicyEngineInput): PolicyDecision[] {
|
|
36
|
-
const decisions: PolicyDecision[] = [];
|
|
37
|
-
const maxTasksPerRun = input.limits?.maxTasksPerRun;
|
|
38
|
-
if (maxTasksPerRun !== undefined && input.tasks.length > maxTasksPerRun) {
|
|
39
|
-
decisions.push(decision("block", "limit_exceeded", `Run has ${input.tasks.length} tasks, exceeding maxTasksPerRun=${maxTasksPerRun}.`));
|
|
40
|
-
}
|
|
41
|
-
const runningCount = input.tasks.filter((task) => task.status === "running").length;
|
|
42
|
-
if (input.limits?.maxConcurrentWorkers !== undefined && runningCount > input.limits.maxConcurrentWorkers) {
|
|
43
|
-
decisions.push(decision("block", "limit_exceeded", `Run has ${runningCount} running workers, exceeding maxConcurrentWorkers=${input.limits.maxConcurrentWorkers}.`));
|
|
44
|
-
}
|
|
45
|
-
const tasksById = new Map(input.tasks.map((task) => [task.id, task]));
|
|
46
|
-
|
|
47
|
-
for (const task of input.tasks) {
|
|
48
|
-
if (input.limits?.maxChildrenPerTask !== undefined && (task.graph?.children.length ?? 0) > input.limits.maxChildrenPerTask) {
|
|
49
|
-
decisions.push(decision("block", "limit_exceeded", `Task has ${task.graph?.children.length ?? 0} children, exceeding maxChildrenPerTask=${input.limits.maxChildrenPerTask}.`, task.id));
|
|
50
|
-
}
|
|
51
|
-
if (input.limits?.maxTaskDepth !== undefined && taskDepth(task, tasksById) > input.limits.maxTaskDepth) {
|
|
52
|
-
decisions.push(decision("block", "limit_exceeded", `Task graph depth exceeds maxTaskDepth=${input.limits.maxTaskDepth}.`, task.id));
|
|
53
|
-
}
|
|
54
|
-
if (task.status === "failed") {
|
|
55
|
-
const retryCount = task.policy?.retryCount ?? 0;
|
|
56
|
-
const maxRetries = input.limits?.maxRetriesPerTask ?? 0;
|
|
57
|
-
decisions.push(decision(retryCount < maxRetries ? "retry" : "escalate", "task_failed", task.error ? `Task failed: ${task.error}` : "Task failed.", task.id));
|
|
58
|
-
}
|
|
59
|
-
if ((task.status === "running" || task.status === "queued") && task.heartbeat && task.heartbeat.alive !== false && isWorkerHeartbeatStale(task.heartbeat, input.limits?.heartbeatStaleMs ?? 60_000, input.now)) {
|
|
60
|
-
decisions.push(decision("escalate", "worker_stale", "Worker heartbeat is stale.", task.id));
|
|
61
|
-
}
|
|
62
|
-
if (task.taskPacket?.verification) {
|
|
63
|
-
const outcome = evaluateGreenContract(task.taskPacket.verification, task.verification);
|
|
64
|
-
if (!outcome.satisfied && task.status === "completed") {
|
|
65
|
-
decisions.push(decision("block", "green_unsatisfied", `Green contract unsatisfied: required=${outcome.requiredGreenLevel}, observed=${outcome.observedGreenLevel}.`, task.id));
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (decisions.length === 0 && input.tasks.length > 0 && input.tasks.every((task) => task.status === "completed")) {
|
|
71
|
-
decisions.push(decision("closeout", "run_complete", "All tasks completed and no policy blockers were found."));
|
|
72
|
-
}
|
|
73
|
-
return decisions;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function summarizePolicyDecisions(decisions: PolicyDecision[]): string[] {
|
|
77
|
-
return decisions.map((item) => `- ${item.action} (${item.reason})${item.taskId ? ` ${item.taskId}` : ""}: ${item.message}`);
|
|
78
|
-
}
|
|
1
|
+
import type { CrewLimitsConfig } from "../config/config.ts";
|
|
2
|
+
import type { PolicyDecision, PolicyDecisionAction, PolicyDecisionReason, TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
3
|
+
import { evaluateGreenContract } from "./green-contract.ts";
|
|
4
|
+
import { isWorkerHeartbeatStale } from "./worker-heartbeat.ts";
|
|
5
|
+
|
|
6
|
+
export interface PolicyEngineInput {
|
|
7
|
+
manifest: TeamRunManifest;
|
|
8
|
+
tasks: TeamTaskState[];
|
|
9
|
+
limits?: CrewLimitsConfig;
|
|
10
|
+
now?: Date;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function decision(action: PolicyDecisionAction, reason: PolicyDecisionReason, message: string, taskId?: string): PolicyDecision {
|
|
14
|
+
return {
|
|
15
|
+
action,
|
|
16
|
+
reason,
|
|
17
|
+
message,
|
|
18
|
+
taskId,
|
|
19
|
+
createdAt: new Date().toISOString(),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function taskDepth(task: TeamTaskState, tasksById: Map<string, TeamTaskState>): number {
|
|
24
|
+
let depth = 0;
|
|
25
|
+
let current = task.graph?.parentId;
|
|
26
|
+
const seen = new Set<string>();
|
|
27
|
+
while (current && !seen.has(current)) {
|
|
28
|
+
seen.add(current);
|
|
29
|
+
depth += 1;
|
|
30
|
+
current = tasksById.get(current)?.graph?.parentId;
|
|
31
|
+
}
|
|
32
|
+
return depth;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function evaluateCrewPolicy(input: PolicyEngineInput): PolicyDecision[] {
|
|
36
|
+
const decisions: PolicyDecision[] = [];
|
|
37
|
+
const maxTasksPerRun = input.limits?.maxTasksPerRun;
|
|
38
|
+
if (maxTasksPerRun !== undefined && input.tasks.length > maxTasksPerRun) {
|
|
39
|
+
decisions.push(decision("block", "limit_exceeded", `Run has ${input.tasks.length} tasks, exceeding maxTasksPerRun=${maxTasksPerRun}.`));
|
|
40
|
+
}
|
|
41
|
+
const runningCount = input.tasks.filter((task) => task.status === "running").length;
|
|
42
|
+
if (input.limits?.maxConcurrentWorkers !== undefined && runningCount > input.limits.maxConcurrentWorkers) {
|
|
43
|
+
decisions.push(decision("block", "limit_exceeded", `Run has ${runningCount} running workers, exceeding maxConcurrentWorkers=${input.limits.maxConcurrentWorkers}.`));
|
|
44
|
+
}
|
|
45
|
+
const tasksById = new Map(input.tasks.map((task) => [task.id, task]));
|
|
46
|
+
|
|
47
|
+
for (const task of input.tasks) {
|
|
48
|
+
if (input.limits?.maxChildrenPerTask !== undefined && (task.graph?.children.length ?? 0) > input.limits.maxChildrenPerTask) {
|
|
49
|
+
decisions.push(decision("block", "limit_exceeded", `Task has ${task.graph?.children.length ?? 0} children, exceeding maxChildrenPerTask=${input.limits.maxChildrenPerTask}.`, task.id));
|
|
50
|
+
}
|
|
51
|
+
if (input.limits?.maxTaskDepth !== undefined && taskDepth(task, tasksById) > input.limits.maxTaskDepth) {
|
|
52
|
+
decisions.push(decision("block", "limit_exceeded", `Task graph depth exceeds maxTaskDepth=${input.limits.maxTaskDepth}.`, task.id));
|
|
53
|
+
}
|
|
54
|
+
if (task.status === "failed") {
|
|
55
|
+
const retryCount = task.policy?.retryCount ?? 0;
|
|
56
|
+
const maxRetries = input.limits?.maxRetriesPerTask ?? 0;
|
|
57
|
+
decisions.push(decision(retryCount < maxRetries ? "retry" : "escalate", "task_failed", task.error ? `Task failed: ${task.error}` : "Task failed.", task.id));
|
|
58
|
+
}
|
|
59
|
+
if ((task.status === "running" || task.status === "queued") && task.heartbeat && task.heartbeat.alive !== false && isWorkerHeartbeatStale(task.heartbeat, input.limits?.heartbeatStaleMs ?? 60_000, input.now)) {
|
|
60
|
+
decisions.push(decision("escalate", "worker_stale", "Worker heartbeat is stale.", task.id));
|
|
61
|
+
}
|
|
62
|
+
if (task.taskPacket?.verification) {
|
|
63
|
+
const outcome = evaluateGreenContract(task.taskPacket.verification, task.verification);
|
|
64
|
+
if (!outcome.satisfied && task.status === "completed") {
|
|
65
|
+
decisions.push(decision("block", "green_unsatisfied", `Green contract unsatisfied: required=${outcome.requiredGreenLevel}, observed=${outcome.observedGreenLevel}.`, task.id));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (decisions.length === 0 && input.tasks.length > 0 && input.tasks.every((task) => task.status === "completed")) {
|
|
71
|
+
decisions.push(decision("closeout", "run_complete", "All tasks completed and no policy blockers were found."));
|
|
72
|
+
}
|
|
73
|
+
return decisions;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function summarizePolicyDecisions(decisions: PolicyDecision[]): string[] {
|
|
77
|
+
return decisions.map((item) => `- ${item.action} (${item.reason})${item.taskId ? ` ${item.taskId}` : ""}: ${item.message}`);
|
|
78
|
+
}
|