@tea-agent/loop-agent 0.1.0
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 +121 -0
- package/CHANGELOG.md +37 -0
- package/README.md +144 -0
- package/bin/loop-agent.js +21 -0
- package/dist/adapters/aimax.js +91 -0
- package/dist/adapters/context.js +32 -0
- package/dist/adapters/index.js +28 -0
- package/dist/adapters/loop-agent.js +98 -0
- package/dist/adapters/types.js +1 -0
- package/dist/cli/catalog.js +259 -0
- package/dist/cli/help.js +55 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/program.js +505 -0
- package/dist/cli.js +12 -0
- package/dist/commands/closeout.js +13 -0
- package/dist/commands/coverage-audit.js +14 -0
- package/dist/commands/cursor-prompt.js +222 -0
- package/dist/commands/cursor-worker.js +43 -0
- package/dist/commands/dag-approve.js +102 -0
- package/dist/commands/dag-final-verification.js +76 -0
- package/dist/commands/dag-init-hybrid.js +56 -0
- package/dist/commands/dag-reconcile-tasks.js +51 -0
- package/dist/commands/dag-reject.js +91 -0
- package/dist/commands/dag-report.js +177 -0
- package/dist/commands/dag-resume.js +34 -0
- package/dist/commands/dag-run-task.js +470 -0
- package/dist/commands/dag-validate.js +186 -0
- package/dist/commands/dag-workflow-compile.js +91 -0
- package/dist/commands/dag-workflow-plan.js +130 -0
- package/dist/commands/dag-workflow-validate.js +66 -0
- package/dist/commands/delegate.js +132 -0
- package/dist/commands/docs-archive.js +5 -0
- package/dist/commands/docs-audit.js +5 -0
- package/dist/commands/doctor.js +50 -0
- package/dist/commands/goal.js +92 -0
- package/dist/commands/handoff-check.js +5 -0
- package/dist/commands/harvest.js +44 -0
- package/dist/commands/inspect.js +11 -0
- package/dist/commands/instructions.js +195 -0
- package/dist/commands/knowledge.js +64 -0
- package/dist/commands/loop-benchmark.js +72 -0
- package/dist/commands/loop.js +241 -0
- package/dist/commands/new-task.js +5 -0
- package/dist/commands/pi-prompt.js +181 -0
- package/dist/commands/pi-reuse-benchmark.js +153 -0
- package/dist/commands/plan-list.js +5 -0
- package/dist/commands/promote-run.js +29 -0
- package/dist/commands/reference-index.js +16 -0
- package/dist/commands/run-dag.js +184 -0
- package/dist/commands/spine.js +38 -0
- package/dist/commands/stats.js +84 -0
- package/dist/commands/status.js +56 -0
- package/dist/commands/study-init.js +192 -0
- package/dist/commands/workflow.js +259 -0
- package/dist/commands/worktree-create.js +31 -0
- package/dist/commands/worktree-list.js +5 -0
- package/dist/commands/worktree-remove.js +26 -0
- package/dist/cursor-worker-entry.js +8 -0
- package/dist/executors/config-core.js +55 -0
- package/dist/executors/config.js +2 -0
- package/dist/executors/cursor-artifacts.js +33 -0
- package/dist/executors/cursor-execution-log.js +81 -0
- package/dist/executors/cursor-executor-artifacts.js +135 -0
- package/dist/executors/cursor-executor.js +468 -0
- package/dist/executors/cursor-run.js +115 -0
- package/dist/executors/cursor-tool.js +94 -0
- package/dist/executors/cursor-worker-client.js +213 -0
- package/dist/executors/cursor-worker-protocol.js +18 -0
- package/dist/executors/cursor-worker-server.js +54 -0
- package/dist/executors/cursor-worker.js +3 -0
- package/dist/executors/cursor.js +6 -0
- package/dist/executors/dag-cursor-executor.js +88 -0
- package/dist/executors/dag-pi-executor.js +322 -0
- package/dist/executors/dag-static-executor.js +45 -0
- package/dist/executors/dag.js +4 -0
- package/dist/executors/index.js +8 -0
- package/dist/executors/model-routing.js +60 -0
- package/dist/executors/pi-event-serializer.js +43 -0
- package/dist/executors/pi-executor.js +606 -0
- package/dist/executors/pi-reuse-benchmark.js +316 -0
- package/dist/executors/pi-runtime-reuse.js +29 -0
- package/dist/executors/pi-sdk-executor.js +255 -0
- package/dist/executors/pi-sdk.js +1 -0
- package/dist/executors/pi.js +3 -0
- package/dist/executors/shell-executor.js +300 -0
- package/dist/executors/shell-presets.js +47 -0
- package/dist/executors/shell-verification.js +251 -0
- package/dist/executors/shell-write-guard.js +126 -0
- package/dist/executors/shell.js +3 -0
- package/dist/executors/static.js +1 -0
- package/dist/governance/checks.js +434 -0
- package/dist/governance/harness.js +9 -0
- package/dist/governance/index.js +3 -0
- package/dist/governance/manifest-types.js +128 -0
- package/dist/governance/manifest.js +2 -0
- package/dist/governance/path-guard.js +69 -0
- package/dist/governance/path-guards.js +2 -0
- package/dist/governance/profiles.js +3 -0
- package/dist/governance/requirement-coverage.js +425 -0
- package/dist/governance/skill-safety.js +135 -0
- package/dist/governance/spine-audit.js +152 -0
- package/dist/records/closeout.js +2 -0
- package/dist/records/harvest.js +236 -0
- package/dist/records/index.js +3 -0
- package/dist/records/one-shot-runs.js +421 -0
- package/dist/records/promotion.js +199 -0
- package/dist/shared/artifacts-core.js +88 -0
- package/dist/shared/artifacts.js +2 -0
- package/dist/shared/context-files.js +32 -0
- package/dist/shared/context.js +2 -0
- package/dist/shared/copy-dir.js +17 -0
- package/dist/shared/git-progress.js +165 -0
- package/dist/shared/index.js +5 -0
- package/dist/shared/logger.js +23 -0
- package/dist/shared/one-shot-prompt-args.js +98 -0
- package/dist/shared/path-refs.js +31 -0
- package/dist/shared/prompts.js +26 -0
- package/dist/shared/reference-context.js +238 -0
- package/dist/shared/timeout-policy.js +19 -0
- package/dist/shared/timeout.js +1 -0
- package/dist/shared/types.js +5 -0
- package/dist/task/config-types.js +97 -0
- package/dist/task/config.js +2 -0
- package/dist/task/delegate.js +220 -0
- package/dist/task/goal-audit.js +51 -0
- package/dist/task/goal-policy.js +8 -0
- package/dist/task/goal.js +3 -0
- package/dist/task/ids.js +1 -0
- package/dist/task/index.js +9 -0
- package/dist/task/lifecycle.js +1 -0
- package/dist/task/paths.js +1 -0
- package/dist/task/read-model.js +149 -0
- package/dist/task/runtime.js +699 -0
- package/dist/task/source-state.js +1 -0
- package/dist/task/state.js +55 -0
- package/dist/task/subagent-guidance.js +1 -0
- package/dist/task/workflow-state-types.js +92 -0
- package/dist/task/worktree-cleanup.js +140 -0
- package/dist/task/worktree.js +171 -0
- package/dist/workflows/dag/authoring.js +8 -0
- package/dist/workflows/dag/authority-surface.js +138 -0
- package/dist/workflows/dag/canvas-observer.js +474 -0
- package/dist/workflows/dag/decision-envelope.js +502 -0
- package/dist/workflows/dag/decision-evidence.js +153 -0
- package/dist/workflows/dag/decision-gates.js +1 -0
- package/dist/workflows/dag/executor-registry.js +25 -0
- package/dist/workflows/dag/facts.js +4 -0
- package/dist/workflows/dag/failure-category.js +111 -0
- package/dist/workflows/dag/final-verification.js +180 -0
- package/dist/workflows/dag/governance-constants.js +5 -0
- package/dist/workflows/dag/governance-profile.js +405 -0
- package/dist/workflows/dag/index.js +6 -0
- package/dist/workflows/dag/init-hybrid.js +855 -0
- package/dist/workflows/dag/knowledge-curator.js +162 -0
- package/dist/workflows/dag/lifecycle.js +484 -0
- package/dist/workflows/dag/prompt-source.js +88 -0
- package/dist/workflows/dag/prompt.js +130 -0
- package/dist/workflows/dag/reconcile-tasks.js +404 -0
- package/dist/workflows/dag/recovery-recommendation.js +226 -0
- package/dist/workflows/dag/repair-artifact.js +136 -0
- package/dist/workflows/dag/report.js +1019 -0
- package/dist/workflows/dag/runner.js +1677 -0
- package/dist/workflows/dag/runtime.js +5 -0
- package/dist/workflows/dag/skill-instructions.js +471 -0
- package/dist/workflows/dag/skills.js +41 -0
- package/dist/workflows/dag/spec.js +3 -0
- package/dist/workflows/dag/topo.js +30 -0
- package/dist/workflows/dag/types.js +275 -0
- package/dist/workflows/dag/upstream-artifacts.js +95 -0
- package/dist/workflows/dag/validate.js +527 -0
- package/dist/workflows/dynamic/artifacts.js +65 -0
- package/dist/workflows/dynamic/compile.js +360 -0
- package/dist/workflows/dynamic/compileTypes.js +1 -0
- package/dist/workflows/dynamic/errors.js +5 -0
- package/dist/workflows/dynamic/index.js +7 -0
- package/dist/workflows/dynamic/profiles.js +156 -0
- package/dist/workflows/dynamic/spec.js +114 -0
- package/dist/workflows/dynamic/validate.js +275 -0
- package/dist/workflows/loop/actions.js +1334 -0
- package/dist/workflows/loop/benchmark.js +510 -0
- package/dist/workflows/loop/closeout.js +134 -0
- package/dist/workflows/loop/context.js +48 -0
- package/dist/workflows/loop/events.js +25 -0
- package/dist/workflows/loop/hash.js +32 -0
- package/dist/workflows/loop/index.js +8 -0
- package/dist/workflows/loop/paths.js +17 -0
- package/dist/workflows/loop/rounds.js +81 -0
- package/dist/workflows/loop/signals.js +55 -0
- package/dist/workflows/loop/state.js +116 -0
- package/dist/workflows/loop/templates.js +54 -0
- package/dist/workflows/loop/types.js +28 -0
- package/docs/README.md +62 -0
- package/docs/agent-dag-recovery-playbook.md +158 -0
- package/docs/agent-dag-runner.md +40 -0
- package/docs/cursor-executor-usage.md +25 -0
- package/docs/decisions/README.md +3 -0
- package/docs/design/README.md +36 -0
- package/docs/development-principles.md +71 -0
- package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -0
- package/docs/exec-plans/README.md +6 -0
- package/docs/exec-plans/active/README.md +5 -0
- package/docs/exec-plans/completed/README.md +5 -0
- package/docs/feature-workflow.md +184 -0
- package/docs/harness-methodology-debugging.md +153 -0
- package/docs/harness-methodology-tdd.md +130 -0
- package/docs/harness-methodology-verification.md +27 -0
- package/docs/loop-agent-harness.md +42 -0
- package/docs/progress/README.md +3 -0
- package/docs/reports/README.md +3 -0
- package/docs/templates/adr.md +60 -0
- package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -0
- package/docs/templates/agent-dag-decision-envelope.schema.json +213 -0
- package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -0
- package/docs/templates/agent-dag-decision-gate.prompt.md +246 -0
- package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -0
- package/docs/templates/agent-dag-report.schema.json +423 -0
- package/docs/templates/agent-dag-review-verdict.prompt.md +68 -0
- package/docs/templates/agent-dag.base.json +195 -0
- package/docs/templates/agent-dag.final-verification.json +190 -0
- package/docs/templates/agent-dag.schema.json +316 -0
- package/docs/templates/agent-dag.supervised-implementation.json +500 -0
- package/docs/templates/exec-plan.md +64 -0
- package/docs/templates/feature-spec.md +53 -0
- package/docs/templates/hybrid-dag.json +193 -0
- package/docs/templates/progress-log.md +17 -0
- package/docs/templates/project-start-checklist.md +9 -0
- package/docs/templates/qa-report.md +42 -0
- package/docs/templates/sprint-contract.md +29 -0
- package/docs/verification-matrix.md +30 -0
- package/examples/decision-gate-agent-dag.json +123 -0
- package/examples/example-dag.json +51 -0
- package/examples/hybrid-loop-agent-dag.json +194 -0
- package/harness.json +92 -0
- package/package.json +61 -0
- package/skills/ai-engineering-context/SKILL.md +48 -0
- package/skills/loop-agent/SKILL.md +260 -0
- package/skills/loop-agent/references/README.md +63 -0
- package/skills/loop-agent/references/command-reference.md +315 -0
- package/skills/loop-agent/references/harness-policy.md +258 -0
- package/skills/loop-agent/references/hybrid-dag.md +216 -0
- package/skills/loop-agent/references/learned/README.md +21 -0
- package/skills/loop-agent/references/model-routing.md +36 -0
- package/skills/loop-agent/references/multi-worktree.md +54 -0
- package/skills/loop-agent/references/one-shot-runs.md +85 -0
- package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -0
- package/skills/loop-agent/references/pi-prompt.md +23 -0
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +83 -0
- package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -0
- package/skills/loop-agent/references/task-workflow.md +84 -0
- package/skills/loop-agent/references/verification-and-failure-handling.md +74 -0
- package/skills/requesting-code-review/SKILL.md +101 -0
- package/skills/requesting-code-review/code-reviewer.md +168 -0
- package/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/skills/systematic-debugging/SKILL.md +296 -0
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/skills/systematic-debugging/find-polluter.sh +63 -0
- package/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/skills/systematic-debugging/test-academic.md +14 -0
- package/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/skills/verification-before-completion/SKILL.md +154 -0
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { PI_REUSE_RUNTIME_ENV, isPiReuseRuntimeEffective, resolvePiReuseRuntimeMode as resolveConfiguredPiReuseRuntimeMode, } from './pi-runtime-reuse.js';
|
|
6
|
+
export function extractSubagentStats(stdout) {
|
|
7
|
+
let totalCalls = 0;
|
|
8
|
+
let failedCalls = 0;
|
|
9
|
+
const modes = new Set();
|
|
10
|
+
const agents = new Set();
|
|
11
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
12
|
+
const trimmed = line.trim();
|
|
13
|
+
if (!trimmed)
|
|
14
|
+
continue;
|
|
15
|
+
try {
|
|
16
|
+
const event = JSON.parse(trimmed);
|
|
17
|
+
if (event.type === 'tool_execution_start' && event.toolName === 'subagent') {
|
|
18
|
+
totalCalls += 1;
|
|
19
|
+
if (isRecord(event.input)) {
|
|
20
|
+
if (event.input.agent)
|
|
21
|
+
agents.add(String(event.input.agent));
|
|
22
|
+
if (event.input.tasks)
|
|
23
|
+
modes.add('parallel');
|
|
24
|
+
if (event.input.chain)
|
|
25
|
+
modes.add('chain');
|
|
26
|
+
if (event.input.agent && !event.input.tasks && !event.input.chain)
|
|
27
|
+
modes.add('single');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (event.type === 'tool_execution_end' && event.toolName === 'subagent') {
|
|
31
|
+
if (event.isError === true)
|
|
32
|
+
failedCalls += 1;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// tolerate non-json noise
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (totalCalls === 0)
|
|
40
|
+
return 'calls=0';
|
|
41
|
+
const parts = [`calls=${totalCalls}`];
|
|
42
|
+
if (failedCalls > 0)
|
|
43
|
+
parts.push(`${failedCalls} failed`);
|
|
44
|
+
if (modes.size > 0)
|
|
45
|
+
parts.push(Array.from(modes).sort().join('+'));
|
|
46
|
+
if (agents.size > 0)
|
|
47
|
+
parts.push(`agents=[${Array.from(agents).sort().join(',')}]`);
|
|
48
|
+
return parts.join(' | ');
|
|
49
|
+
}
|
|
50
|
+
export const DEFAULT_TIMEOUT_MS = 1_800_000;
|
|
51
|
+
export const MAX_ALLOWED_TIMEOUT_MS = 1_800_000;
|
|
52
|
+
const PI_BACKEND_ENV = 'CODE_AGENT_PI_BACKEND';
|
|
53
|
+
const PI_CLI_PACKAGE_NAMES = [
|
|
54
|
+
'@earendil-works/pi-coding-agent',
|
|
55
|
+
'@mariozechner/pi-coding-agent',
|
|
56
|
+
];
|
|
57
|
+
export { PI_REUSE_RUNTIME_ENV };
|
|
58
|
+
/** Resolve configured Pi SDK runtime reuse mode. Default off; unknown values fall back to off. */
|
|
59
|
+
export function resolvePiReuseRuntimeMode() {
|
|
60
|
+
return resolveConfiguredPiReuseRuntimeMode();
|
|
61
|
+
}
|
|
62
|
+
/** True only when reuse is opt-in and SDK path is allowed (not cli-only). */
|
|
63
|
+
export function isPiReuseRuntimeEnabled() {
|
|
64
|
+
return isPiReuseRuntimeEffective(resolvePiReuseRuntimeMode(), resolvePiBackend());
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Run `fn` inside an opt-in Pi SDK reuse scope when effective.
|
|
68
|
+
* When reuse is off or backend is cli-only, calls `fn` directly with no scope.
|
|
69
|
+
*/
|
|
70
|
+
export async function withPiReuseRuntimeScope(fn) {
|
|
71
|
+
if (!isPiReuseRuntimeEnabled()) {
|
|
72
|
+
return fn();
|
|
73
|
+
}
|
|
74
|
+
const { beginPiSdkReuseScope, endPiSdkReuseScope } = await import('./pi-sdk-executor.js');
|
|
75
|
+
beginPiSdkReuseScope();
|
|
76
|
+
try {
|
|
77
|
+
return await fn();
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
await endPiSdkReuseScope();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Resolve configured Pi backend mode. Default sdk-first; cli-only preserves pure CLI path. */
|
|
84
|
+
export function resolvePiBackend() {
|
|
85
|
+
const raw = process.env[PI_BACKEND_ENV]?.trim().toLowerCase();
|
|
86
|
+
if (raw === 'cli-only')
|
|
87
|
+
return 'cli-only';
|
|
88
|
+
if (raw === 'sdk-first' || !raw)
|
|
89
|
+
return 'sdk-first';
|
|
90
|
+
return 'sdk-first';
|
|
91
|
+
}
|
|
92
|
+
function piCliPathFromPackageRoot(nodeModulesRoot, packageName) {
|
|
93
|
+
return path.join(nodeModulesRoot, ...packageName.split('/'), 'dist', 'cli.js');
|
|
94
|
+
}
|
|
95
|
+
function findExistingPiCliUnderNodeModules(nodeModulesRoot) {
|
|
96
|
+
for (const packageName of PI_CLI_PACKAGE_NAMES) {
|
|
97
|
+
const candidate = piCliPathFromPackageRoot(nodeModulesRoot, packageName);
|
|
98
|
+
if (existsSync(candidate))
|
|
99
|
+
return candidate;
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
function resolveGlobalNpmPiCliPath() {
|
|
104
|
+
try {
|
|
105
|
+
const npmRoot = spawnSync('npm', ['root', '-g'], { encoding: 'utf-8' });
|
|
106
|
+
if (npmRoot.status === 0 && npmRoot.stdout.trim()) {
|
|
107
|
+
return findExistingPiCliUnderNodeModules(npmRoot.stdout.trim());
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// npm not available
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
function resolvePiCliNearShim(shimPath) {
|
|
116
|
+
if (!existsSync(shimPath))
|
|
117
|
+
return undefined;
|
|
118
|
+
const shimDir = path.dirname(shimPath);
|
|
119
|
+
const nearGlobalShim = findExistingPiCliUnderNodeModules(path.join(shimDir, 'node_modules'));
|
|
120
|
+
if (nearGlobalShim)
|
|
121
|
+
return nearGlobalShim;
|
|
122
|
+
if (shimDir.endsWith(`${path.sep}.bin`)) {
|
|
123
|
+
const nearLocalBin = findExistingPiCliUnderNodeModules(path.join(shimDir, '..'));
|
|
124
|
+
if (nearLocalBin)
|
|
125
|
+
return nearLocalBin;
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
function resolvePiCliFromPathLookup() {
|
|
130
|
+
const lookupCommands = process.platform === 'win32'
|
|
131
|
+
? [{ command: 'where.exe', args: ['pi'] }]
|
|
132
|
+
: [{ command: 'which', args: ['pi'] }];
|
|
133
|
+
for (const lookup of lookupCommands) {
|
|
134
|
+
try {
|
|
135
|
+
const result = spawnSync(lookup.command, lookup.args, { encoding: 'utf-8' });
|
|
136
|
+
if (result.status !== 0 || !result.stdout.trim())
|
|
137
|
+
continue;
|
|
138
|
+
for (const rawPath of result.stdout.split(/\r?\n/)) {
|
|
139
|
+
const lookupPath = rawPath.trim();
|
|
140
|
+
if (!lookupPath)
|
|
141
|
+
continue;
|
|
142
|
+
const nearShim = resolvePiCliNearShim(lookupPath);
|
|
143
|
+
if (nearShim)
|
|
144
|
+
return nearShim;
|
|
145
|
+
if (process.platform !== 'win32') {
|
|
146
|
+
try {
|
|
147
|
+
const realPathResult = spawnSync('readlink', ['-f', lookupPath], {
|
|
148
|
+
encoding: 'utf-8',
|
|
149
|
+
});
|
|
150
|
+
if (realPathResult.status === 0 && realPathResult.stdout.trim()) {
|
|
151
|
+
const resolved = realPathResult.stdout.trim();
|
|
152
|
+
const nearResolvedShim = resolvePiCliNearShim(resolved);
|
|
153
|
+
if (nearResolvedShim)
|
|
154
|
+
return nearResolvedShim;
|
|
155
|
+
if (existsSync(resolved))
|
|
156
|
+
return resolved;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// readlink not available
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (existsSync(lookupPath))
|
|
164
|
+
return lookupPath;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// lookup command not available
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Resolve the pi CLI script path.
|
|
175
|
+
* Uses the same resolution as `which pi` but avoids relying on `#!/usr/bin/env node`
|
|
176
|
+
* in the child process, which can lose provider visibility when spawned from tsx.
|
|
177
|
+
*/
|
|
178
|
+
function resolvePiCliPath() {
|
|
179
|
+
// Check common installation paths
|
|
180
|
+
const home = homedir();
|
|
181
|
+
// Try nvm paths with both "v"-prefixed and non-prefixed version directories
|
|
182
|
+
const nodeVersion = process.versions.node; // e.g. "22.19.0"
|
|
183
|
+
const nvmBase = path.join(home, '.nvm', 'versions', 'node');
|
|
184
|
+
const nvmCandidates = [
|
|
185
|
+
path.join(nvmBase, `v${nodeVersion}`, 'lib', 'node_modules'),
|
|
186
|
+
path.join(nvmBase, nodeVersion, 'lib', 'node_modules'),
|
|
187
|
+
].flatMap((nodeModulesRoot) => PI_CLI_PACKAGE_NAMES.map((packageName) => piCliPathFromPackageRoot(nodeModulesRoot, packageName)));
|
|
188
|
+
const otherCandidates = [
|
|
189
|
+
// Global npm installation (non-nvm)
|
|
190
|
+
...PI_CLI_PACKAGE_NAMES.map((packageName) => piCliPathFromPackageRoot('/usr/local/lib/node_modules', packageName)),
|
|
191
|
+
// Bun installation
|
|
192
|
+
...PI_CLI_PACKAGE_NAMES.map((packageName) => piCliPathFromPackageRoot(path.join(home, '.bun', 'install', 'global', 'node_modules'), packageName)),
|
|
193
|
+
];
|
|
194
|
+
const allCandidates = [...nvmCandidates, ...otherCandidates];
|
|
195
|
+
for (const candidate of allCandidates) {
|
|
196
|
+
if (existsSync(candidate)) {
|
|
197
|
+
return candidate;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const globalNpmPi = resolveGlobalNpmPiCliPath();
|
|
201
|
+
if (globalNpmPi)
|
|
202
|
+
return globalNpmPi;
|
|
203
|
+
const pathLookupPi = resolvePiCliFromPathLookup();
|
|
204
|
+
if (pathLookupPi)
|
|
205
|
+
return pathLookupPi;
|
|
206
|
+
throw new Error('Could not resolve pi CLI path. Ensure pi is installed.');
|
|
207
|
+
}
|
|
208
|
+
let cachedPiCliPath;
|
|
209
|
+
/** Lazily resolve and cache the pi CLI script path (avoids import-time sync I/O). */
|
|
210
|
+
export function getPiCliPath() {
|
|
211
|
+
if (!cachedPiCliPath) {
|
|
212
|
+
cachedPiCliPath = resolvePiCliPath();
|
|
213
|
+
}
|
|
214
|
+
return cachedPiCliPath;
|
|
215
|
+
}
|
|
216
|
+
/** Test-only: reset lazy CLI path cache between isolated contract tests. */
|
|
217
|
+
export function resetPiCliPathCacheForTests() {
|
|
218
|
+
cachedPiCliPath = undefined;
|
|
219
|
+
}
|
|
220
|
+
export function checkPiAvailability() {
|
|
221
|
+
// Use process.execPath to run pi CLI directly, avoiding shebang-based
|
|
222
|
+
// resolution that can drop built-in providers when spawned from tsx.
|
|
223
|
+
const result = spawnSync(process.execPath, [getPiCliPath(), '--help'], { encoding: 'utf-8' });
|
|
224
|
+
if (result.error) {
|
|
225
|
+
return { ok: false, detail: result.error.message };
|
|
226
|
+
}
|
|
227
|
+
if (result.status !== 0) {
|
|
228
|
+
return { ok: false, detail: result.stderr || `exit ${result.status}` };
|
|
229
|
+
}
|
|
230
|
+
return { ok: true, detail: 'pi available' };
|
|
231
|
+
}
|
|
232
|
+
export function extractAssistantTextFromPiJson(stdout) {
|
|
233
|
+
let assistantText = '';
|
|
234
|
+
let parsedEvents = 0;
|
|
235
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
236
|
+
const trimmed = line.trim();
|
|
237
|
+
if (!trimmed)
|
|
238
|
+
continue;
|
|
239
|
+
try {
|
|
240
|
+
const event = JSON.parse(trimmed);
|
|
241
|
+
parsedEvents += 1;
|
|
242
|
+
const candidate = extractAssistantTextFromEvent(event);
|
|
243
|
+
if (candidate)
|
|
244
|
+
assistantText = candidate;
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
// tolerate non-json noise
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return { assistantText: assistantText.trim(), parsedEvents };
|
|
251
|
+
}
|
|
252
|
+
export function extractTokenUsageFromPiJson(stdout) {
|
|
253
|
+
let totalTokens = 0;
|
|
254
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
255
|
+
const trimmed = line.trim();
|
|
256
|
+
if (!trimmed)
|
|
257
|
+
continue;
|
|
258
|
+
try {
|
|
259
|
+
const event = JSON.parse(trimmed);
|
|
260
|
+
const usage = extractUsageFromEvent(event);
|
|
261
|
+
if (usage !== null) {
|
|
262
|
+
totalTokens += usage;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// tolerate non-json noise
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return totalTokens;
|
|
270
|
+
}
|
|
271
|
+
function extractUsageFromEvent(event) {
|
|
272
|
+
const candidates = [
|
|
273
|
+
event.usage,
|
|
274
|
+
isRecord(event.message) ? event.message.usage : undefined,
|
|
275
|
+
event.tokenUsage,
|
|
276
|
+
];
|
|
277
|
+
for (const candidate of candidates) {
|
|
278
|
+
if (!isRecord(candidate))
|
|
279
|
+
continue;
|
|
280
|
+
const input = readNumericUsageField(candidate, ['input_tokens', 'inputTokens', 'prompt_tokens', 'promptTokens']);
|
|
281
|
+
const output = readNumericUsageField(candidate, ['output_tokens', 'outputTokens', 'completion_tokens', 'completionTokens']);
|
|
282
|
+
const total = readNumericUsageField(candidate, ['total_tokens', 'totalTokens']);
|
|
283
|
+
if (input !== null && output !== null)
|
|
284
|
+
return input + output;
|
|
285
|
+
if (total !== null)
|
|
286
|
+
return total;
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
function readNumericUsageField(record, keys) {
|
|
291
|
+
for (const key of keys) {
|
|
292
|
+
const value = record[key];
|
|
293
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
|
|
294
|
+
return Math.trunc(value);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
function extractAssistantTextFromEvent(event) {
|
|
300
|
+
if (event.type === 'turn_end' && isRecord(event.message)) {
|
|
301
|
+
return extractAssistantTextFromMessage(event.message);
|
|
302
|
+
}
|
|
303
|
+
if (event.type === 'message_end' && isRecord(event.message)) {
|
|
304
|
+
return extractAssistantTextFromMessage(event.message);
|
|
305
|
+
}
|
|
306
|
+
if (event.type === 'agent_end' && Array.isArray(event.messages)) {
|
|
307
|
+
for (let i = event.messages.length - 1; i >= 0; i -= 1) {
|
|
308
|
+
const message = event.messages[i];
|
|
309
|
+
if (isRecord(message)) {
|
|
310
|
+
const text = extractAssistantTextFromMessage(message);
|
|
311
|
+
if (text)
|
|
312
|
+
return text;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return '';
|
|
317
|
+
}
|
|
318
|
+
function extractAssistantTextFromMessage(message) {
|
|
319
|
+
if (message.role !== 'assistant' || !Array.isArray(message.content))
|
|
320
|
+
return '';
|
|
321
|
+
const textParts = message.content
|
|
322
|
+
.filter(isRecord)
|
|
323
|
+
.filter((item) => item.type === 'text' && typeof item.text === 'string')
|
|
324
|
+
.map((item) => item.text);
|
|
325
|
+
return textParts.join('\n').trim();
|
|
326
|
+
}
|
|
327
|
+
function isRecord(value) {
|
|
328
|
+
return typeof value === 'object' && value !== null;
|
|
329
|
+
}
|
|
330
|
+
function withSubagentStats(result) {
|
|
331
|
+
return { ...result, subagentStats: extractSubagentStats(result.stdout) };
|
|
332
|
+
}
|
|
333
|
+
function withCliBackend(result) {
|
|
334
|
+
return { ...result, backend: 'cli' };
|
|
335
|
+
}
|
|
336
|
+
/** CLI-only orchestration: primary attempt + optional fallback model retry. */
|
|
337
|
+
async function runCliOrchestration(options, meta) {
|
|
338
|
+
const primary = options.modelConfig ?? {};
|
|
339
|
+
const attemptedModels = [];
|
|
340
|
+
if (meta?.prependAttemptedModel) {
|
|
341
|
+
attemptedModels.push(meta.prependAttemptedModel);
|
|
342
|
+
}
|
|
343
|
+
const primaryResult = await executeSingleCliAttempt(options, primary);
|
|
344
|
+
attemptedModels.push(primaryResult.modelDisplay);
|
|
345
|
+
let primaryIssues = [];
|
|
346
|
+
if (primaryResult.ok && options.validateOutput) {
|
|
347
|
+
primaryIssues = options.validateOutput(primaryResult.assistantText);
|
|
348
|
+
}
|
|
349
|
+
const primaryHardFail = !primaryResult.ok;
|
|
350
|
+
const primaryInvalidOutput = primaryResult.ok && primaryIssues.length > 0;
|
|
351
|
+
const shouldFallback = primary.fallback && ((primaryHardFail && shouldRetryWithFallback(primaryResult.stderr, primaryResult.stdout))
|
|
352
|
+
|| primaryInvalidOutput);
|
|
353
|
+
if (shouldFallback) {
|
|
354
|
+
const fallbackResult = await executeSingleCliAttempt(options, primary.fallback);
|
|
355
|
+
attemptedModels.push(fallbackResult.modelDisplay);
|
|
356
|
+
const combinedStderr = [primaryResult.stderr, fallbackResult.stderr].filter(Boolean).join('\n--- fallback ---\n');
|
|
357
|
+
const combinedStdout = [primaryResult.stdout, fallbackResult.stdout].filter(Boolean).join('\n');
|
|
358
|
+
let fallbackIssues = [];
|
|
359
|
+
if (fallbackResult.ok && options.validateOutput) {
|
|
360
|
+
fallbackIssues = options.validateOutput(fallbackResult.assistantText);
|
|
361
|
+
}
|
|
362
|
+
if (fallbackResult.ok && fallbackIssues.length === 0) {
|
|
363
|
+
return withSubagentStats(withCliBackend({
|
|
364
|
+
...fallbackResult,
|
|
365
|
+
attemptedModels,
|
|
366
|
+
fallbackUsed: true,
|
|
367
|
+
sdkAttempted: meta?.sdkAttempted,
|
|
368
|
+
stderr: combinedStderr,
|
|
369
|
+
stdout: combinedStdout,
|
|
370
|
+
}));
|
|
371
|
+
}
|
|
372
|
+
if (fallbackResult.ok && fallbackIssues.length > 0) {
|
|
373
|
+
return withSubagentStats(withCliBackend({
|
|
374
|
+
...fallbackResult,
|
|
375
|
+
ok: false,
|
|
376
|
+
failureCategory: 'invalid-output',
|
|
377
|
+
attemptedModels,
|
|
378
|
+
fallbackUsed: true,
|
|
379
|
+
sdkAttempted: meta?.sdkAttempted,
|
|
380
|
+
stderr: combinedStderr + `\n[invalid-output] ${fallbackIssues.join('; ')}`,
|
|
381
|
+
stdout: combinedStdout,
|
|
382
|
+
}));
|
|
383
|
+
}
|
|
384
|
+
return withSubagentStats(withCliBackend({
|
|
385
|
+
...fallbackResult,
|
|
386
|
+
attemptedModels,
|
|
387
|
+
fallbackUsed: true,
|
|
388
|
+
sdkAttempted: meta?.sdkAttempted,
|
|
389
|
+
stderr: combinedStderr,
|
|
390
|
+
stdout: combinedStdout,
|
|
391
|
+
}));
|
|
392
|
+
}
|
|
393
|
+
if (primaryInvalidOutput) {
|
|
394
|
+
return withSubagentStats(withCliBackend({
|
|
395
|
+
...primaryResult,
|
|
396
|
+
ok: false,
|
|
397
|
+
failureCategory: 'invalid-output',
|
|
398
|
+
attemptedModels,
|
|
399
|
+
fallbackUsed: false,
|
|
400
|
+
sdkAttempted: meta?.sdkAttempted,
|
|
401
|
+
stderr: primaryResult.stderr + `\n[invalid-output] ${primaryIssues.join('; ')}`,
|
|
402
|
+
}));
|
|
403
|
+
}
|
|
404
|
+
return withSubagentStats(withCliBackend({
|
|
405
|
+
...primaryResult,
|
|
406
|
+
attemptedModels,
|
|
407
|
+
fallbackUsed: false,
|
|
408
|
+
sdkAttempted: meta?.sdkAttempted,
|
|
409
|
+
}));
|
|
410
|
+
}
|
|
411
|
+
function shouldFallbackSdkToCli(sdkResult, validationIssues) {
|
|
412
|
+
if (sdkResult.ok && validationIssues.length > 0)
|
|
413
|
+
return true;
|
|
414
|
+
if (!sdkResult.ok && (sdkResult.failureCategory === 'auth' || sdkResult.failureCategory === 'timeout')) {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
if (!sdkResult.ok && shouldRetryWithFallback(sdkResult.stderr, sdkResult.stdout))
|
|
418
|
+
return true;
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
export async function executePiStep(options) {
|
|
422
|
+
const backendMode = resolvePiBackend();
|
|
423
|
+
if (backendMode === 'cli-only') {
|
|
424
|
+
return runCliOrchestration(options, { sdkAttempted: false });
|
|
425
|
+
}
|
|
426
|
+
const { checkPiSdkAvailability, executeSingleSdkAttempt, getActivePiSdkReuseScope, } = await import('./pi-sdk-executor.js');
|
|
427
|
+
const sdkCheck = await checkPiSdkAvailability(options.repoRoot);
|
|
428
|
+
if (!sdkCheck.ok) {
|
|
429
|
+
return runCliOrchestration(options, { sdkAttempted: false });
|
|
430
|
+
}
|
|
431
|
+
const reuseScope = isPiReuseRuntimeEnabled() ? getActivePiSdkReuseScope() : undefined;
|
|
432
|
+
const reuseRuntimeActive = Boolean(reuseScope);
|
|
433
|
+
const primary = options.modelConfig ?? {};
|
|
434
|
+
const sdkResult = await executeSingleSdkAttempt({
|
|
435
|
+
...options,
|
|
436
|
+
modelConfig: primary,
|
|
437
|
+
reuseScope,
|
|
438
|
+
});
|
|
439
|
+
let validationIssues = [];
|
|
440
|
+
if (sdkResult.ok && options.validateOutput) {
|
|
441
|
+
validationIssues = options.validateOutput(sdkResult.assistantText);
|
|
442
|
+
}
|
|
443
|
+
if (sdkResult.ok && validationIssues.length === 0) {
|
|
444
|
+
return withSubagentStats({
|
|
445
|
+
...sdkResult,
|
|
446
|
+
attemptedModels: [sdkResult.modelDisplay],
|
|
447
|
+
fallbackUsed: false,
|
|
448
|
+
sdkAttempted: true,
|
|
449
|
+
reuseRuntimeActive,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
if (!shouldFallbackSdkToCli(sdkResult, validationIssues)) {
|
|
453
|
+
const failureCategory = validationIssues.length > 0 ? 'invalid-output' : sdkResult.failureCategory;
|
|
454
|
+
const stderr = validationIssues.length > 0
|
|
455
|
+
? sdkResult.stderr + `\n[invalid-output] ${validationIssues.join('; ')}`
|
|
456
|
+
: sdkResult.stderr;
|
|
457
|
+
return withSubagentStats({
|
|
458
|
+
...sdkResult,
|
|
459
|
+
ok: validationIssues.length > 0 ? false : sdkResult.ok,
|
|
460
|
+
failureCategory,
|
|
461
|
+
attemptedModels: [sdkResult.modelDisplay],
|
|
462
|
+
fallbackUsed: false,
|
|
463
|
+
sdkAttempted: true,
|
|
464
|
+
reuseRuntimeActive,
|
|
465
|
+
stderr,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
const cliResult = await runCliOrchestration(options, {
|
|
469
|
+
sdkAttempted: true,
|
|
470
|
+
prependAttemptedModel: sdkResult.modelDisplay,
|
|
471
|
+
});
|
|
472
|
+
return { ...cliResult, reuseRuntimeActive };
|
|
473
|
+
}
|
|
474
|
+
async function executeSingleCliAttempt(options, modelConfig) {
|
|
475
|
+
const modelDisplay = modelConfig.provider && modelConfig.model
|
|
476
|
+
? `${modelConfig.provider}/${modelConfig.model}`
|
|
477
|
+
: modelConfig.model ?? 'default';
|
|
478
|
+
const timeoutMs = options.timeoutMs ?? modelConfig.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
479
|
+
let timedOut = false;
|
|
480
|
+
let timeoutHandle;
|
|
481
|
+
let sigkillHandle;
|
|
482
|
+
// Build pi CLI arguments (same as before).
|
|
483
|
+
// We invoke pi via `node <pi-cli-path>` instead of relying on `#!/usr/bin/env node`
|
|
484
|
+
// to avoid provider resolution issues when spawned from tsx-managed processes.
|
|
485
|
+
const piArgs = ['-p', '--mode', 'json', '--no-session', '--no-context-files', '--no-skills'];
|
|
486
|
+
if (modelConfig.provider) {
|
|
487
|
+
piArgs.push('--provider', modelConfig.provider);
|
|
488
|
+
}
|
|
489
|
+
if (modelConfig.model) {
|
|
490
|
+
piArgs.push('--model', modelConfig.model);
|
|
491
|
+
}
|
|
492
|
+
if (modelConfig.thinking) {
|
|
493
|
+
piArgs.push('--thinking', modelConfig.thinking);
|
|
494
|
+
}
|
|
495
|
+
piArgs.push('--tools', options.toolNames.join(','));
|
|
496
|
+
piArgs.push('--append-system-prompt', options.prompt);
|
|
497
|
+
piArgs.push(...options.attachedFiles.map((file) => `@${file}`));
|
|
498
|
+
piArgs.push(options.userMessage);
|
|
499
|
+
const startedAt = Date.now();
|
|
500
|
+
return await new Promise((resolve, reject) => {
|
|
501
|
+
const child = spawn(process.execPath, [getPiCliPath(), ...piArgs], {
|
|
502
|
+
cwd: options.repoRoot,
|
|
503
|
+
env: {
|
|
504
|
+
...process.env,
|
|
505
|
+
// Explicitly propagate API keys that pi needs to recognise providers.
|
|
506
|
+
// When tsx runs inside a non-login shell, some vars set in ~/.zshrc
|
|
507
|
+
// may not be in process.env; this ensures pi can resolve providers.
|
|
508
|
+
HOME: process.env.HOME,
|
|
509
|
+
DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY ?? '',
|
|
510
|
+
AIMAX_API_KEY: process.env.AIMAX_API_KEY ?? '',
|
|
511
|
+
OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY ?? '',
|
|
512
|
+
NODE_PATH: process.env.NODE_PATH ?? '',
|
|
513
|
+
// Prevent upstream Node loader hooks (tsx/preload/import) from leaking into pi.
|
|
514
|
+
NODE_OPTIONS: '',
|
|
515
|
+
PATH: process.env.PATH ?? '/usr/local/bin:/usr/bin:/bin',
|
|
516
|
+
},
|
|
517
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
518
|
+
});
|
|
519
|
+
let stdout = '';
|
|
520
|
+
let stderr = '';
|
|
521
|
+
child.stdout.setEncoding('utf-8');
|
|
522
|
+
child.stderr.setEncoding('utf-8');
|
|
523
|
+
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
524
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
525
|
+
child.on('error', (err) => {
|
|
526
|
+
if (timeoutHandle)
|
|
527
|
+
clearTimeout(timeoutHandle);
|
|
528
|
+
if (sigkillHandle)
|
|
529
|
+
clearTimeout(sigkillHandle);
|
|
530
|
+
reject(err);
|
|
531
|
+
});
|
|
532
|
+
child.on('close', (code) => {
|
|
533
|
+
if (timeoutHandle)
|
|
534
|
+
clearTimeout(timeoutHandle);
|
|
535
|
+
if (sigkillHandle)
|
|
536
|
+
clearTimeout(sigkillHandle);
|
|
537
|
+
const durationMs = Date.now() - startedAt;
|
|
538
|
+
const { assistantText, parsedEvents } = extractAssistantTextFromPiJson(stdout);
|
|
539
|
+
const tokensUsed = extractTokenUsageFromPiJson(stdout);
|
|
540
|
+
const failureCategory = classifyPiFailure({
|
|
541
|
+
assistantText,
|
|
542
|
+
exitCode: code,
|
|
543
|
+
stderr,
|
|
544
|
+
stdout,
|
|
545
|
+
timedOut,
|
|
546
|
+
});
|
|
547
|
+
resolve({
|
|
548
|
+
assistantText,
|
|
549
|
+
command: ['pi', ...piArgs],
|
|
550
|
+
durationMs,
|
|
551
|
+
exitCode: code,
|
|
552
|
+
failureCategory,
|
|
553
|
+
modelDisplay,
|
|
554
|
+
ok: code === 0 && assistantText.length > 0,
|
|
555
|
+
parsedEvents,
|
|
556
|
+
stderr,
|
|
557
|
+
stdout,
|
|
558
|
+
timedOut,
|
|
559
|
+
tokensUsed,
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
if (timeoutMs > 0) {
|
|
563
|
+
timeoutHandle = setTimeout(() => {
|
|
564
|
+
timedOut = true;
|
|
565
|
+
child.kill('SIGTERM');
|
|
566
|
+
sigkillHandle = setTimeout(() => {
|
|
567
|
+
if (child.exitCode === null) {
|
|
568
|
+
child.kill('SIGKILL');
|
|
569
|
+
}
|
|
570
|
+
}, 5_000);
|
|
571
|
+
}, timeoutMs);
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
export function shouldRetryWithFallback(stderr, stdout) {
|
|
576
|
+
const category = classifyPiFailure({
|
|
577
|
+
assistantText: '',
|
|
578
|
+
exitCode: null,
|
|
579
|
+
stderr,
|
|
580
|
+
stdout,
|
|
581
|
+
timedOut: false,
|
|
582
|
+
});
|
|
583
|
+
return ['quota', 'rate-limit', 'unavailable', 'network'].includes(category);
|
|
584
|
+
}
|
|
585
|
+
export function classifyPiFailure(input) {
|
|
586
|
+
if (input.timedOut)
|
|
587
|
+
return 'timeout';
|
|
588
|
+
if (input.exitCode === 0 && input.assistantText.trim())
|
|
589
|
+
return 'success';
|
|
590
|
+
const combined = `${input.stderr}\n${input.stdout}`.toLowerCase();
|
|
591
|
+
if (/quota|usage limit|reached.*limit|5\s*小时|5小时/.test(combined))
|
|
592
|
+
return 'quota';
|
|
593
|
+
if (/rate.?limit/.test(combined))
|
|
594
|
+
return 'rate-limit';
|
|
595
|
+
if (/unauthorized|forbidden|invalid api key|authentication|auth failed/.test(combined))
|
|
596
|
+
return 'auth';
|
|
597
|
+
if (/unknown provider|unknown model|model.*unavailable|provider.*unavailable|\bunavailable\b|overloaded|capacity|temporarily unavailable/.test(combined))
|
|
598
|
+
return 'unavailable';
|
|
599
|
+
if (/network|econnreset|etimedout|socket hang up|connection reset|dns|fetch failed/.test(combined))
|
|
600
|
+
return 'network';
|
|
601
|
+
if (input.exitCode === 0 && !input.assistantText.trim())
|
|
602
|
+
return 'empty-output';
|
|
603
|
+
if (input.exitCode !== 0)
|
|
604
|
+
return 'nonzero-exit';
|
|
605
|
+
return 'unknown';
|
|
606
|
+
}
|