pi-crew 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 +32 -0
- package/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/NOTICE.md +15 -0
- package/README.md +703 -0
- package/agents/analyst.md +11 -0
- package/agents/critic.md +11 -0
- package/agents/executor.md +11 -0
- package/agents/explorer.md +11 -0
- package/agents/planner.md +11 -0
- package/agents/reviewer.md +11 -0
- package/agents/security-reviewer.md +11 -0
- package/agents/test-engineer.md +11 -0
- package/agents/verifier.md +11 -0
- package/agents/writer.md +11 -0
- package/docs/architecture.md +92 -0
- package/docs/live-mailbox-runtime.md +36 -0
- package/docs/publishing.md +65 -0
- package/docs/resource-formats.md +131 -0
- package/docs/usage.md +203 -0
- package/index.ts +6 -0
- package/install.mjs +19 -0
- package/package.json +79 -0
- package/schema.json +45 -0
- package/skills/.gitkeep +0 -0
- package/src/agents/agent-config.ts +27 -0
- package/src/agents/agent-serializer.ts +34 -0
- package/src/agents/discover-agents.ts +73 -0
- package/src/config/config.ts +193 -0
- package/src/extension/async-notifier.ts +36 -0
- package/src/extension/autonomous-policy.ts +122 -0
- package/src/extension/help.ts +43 -0
- package/src/extension/import-index.ts +52 -0
- package/src/extension/management.ts +335 -0
- package/src/extension/project-init.ts +74 -0
- package/src/extension/register.ts +349 -0
- package/src/extension/run-bundle-schema.ts +85 -0
- package/src/extension/run-export.ts +59 -0
- package/src/extension/run-import.ts +46 -0
- package/src/extension/run-index.ts +28 -0
- package/src/extension/run-maintenance.ts +24 -0
- package/src/extension/session-summary.ts +8 -0
- package/src/extension/team-manager-command.ts +86 -0
- package/src/extension/team-recommendation.ts +174 -0
- package/src/extension/team-tool.ts +783 -0
- package/src/extension/tool-result.ts +16 -0
- package/src/extension/validate-resources.ts +77 -0
- package/src/prompt/prompt-runtime.ts +58 -0
- package/src/runtime/async-runner.ts +26 -0
- package/src/runtime/background-runner.ts +43 -0
- package/src/runtime/child-pi.ts +75 -0
- package/src/runtime/model-fallback.ts +101 -0
- package/src/runtime/pi-args.ts +81 -0
- package/src/runtime/pi-json-output.ts +110 -0
- package/src/runtime/pi-spawn.ts +96 -0
- package/src/runtime/process-status.ts +25 -0
- package/src/runtime/task-runner.ts +164 -0
- package/src/runtime/team-runner.ts +135 -0
- package/src/runtime/worker-heartbeat.ts +21 -0
- package/src/schema/team-tool-schema.ts +100 -0
- package/src/state/artifact-store.ts +36 -0
- package/src/state/atomic-write.ts +18 -0
- package/src/state/contracts.ts +88 -0
- package/src/state/event-log.ts +27 -0
- package/src/state/locks.ts +40 -0
- package/src/state/mailbox.ts +188 -0
- package/src/state/state-store.ts +119 -0
- package/src/state/task-claims.ts +42 -0
- package/src/state/types.ts +88 -0
- package/src/state/usage.ts +29 -0
- package/src/teams/discover-teams.ts +84 -0
- package/src/teams/team-config.ts +22 -0
- package/src/teams/team-serializer.ts +36 -0
- package/src/ui/run-dashboard.ts +138 -0
- package/src/utils/frontmatter.ts +36 -0
- package/src/utils/ids.ts +12 -0
- package/src/utils/names.ts +26 -0
- package/src/utils/paths.ts +15 -0
- package/src/workflows/discover-workflows.ts +101 -0
- package/src/workflows/validate-workflow.ts +40 -0
- package/src/workflows/workflow-config.ts +24 -0
- package/src/workflows/workflow-serializer.ts +31 -0
- package/src/worktree/cleanup.ts +69 -0
- package/src/worktree/worktree-manager.ts +60 -0
- package/teams/default.team.md +12 -0
- package/teams/fast-fix.team.md +11 -0
- package/teams/implementation.team.md +15 -0
- package/teams/research.team.md +11 -0
- package/teams/review.team.md +12 -0
- package/tsconfig.json +19 -0
- package/workflows/default.workflow.md +29 -0
- package/workflows/fast-fix.workflow.md +22 -0
- package/workflows/implementation.workflow.md +47 -0
- package/workflows/research.workflow.md +22 -0
- package/workflows/review.workflow.md +30 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
2
|
+
import type { TeamToolDetails } from "./team-tool.ts";
|
|
3
|
+
|
|
4
|
+
export type PiTeamsToolResult<TDetails = TeamToolDetails> = AgentToolResult<TDetails> & { isError?: boolean };
|
|
5
|
+
|
|
6
|
+
export function toolResult<TDetails>(text: string, details: TDetails, isError = false): PiTeamsToolResult<TDetails> {
|
|
7
|
+
return { content: [{ type: "text", text }], details, isError };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isToolError(result: { isError?: boolean }): boolean {
|
|
11
|
+
return result.isError === true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function textFromToolResult(result: { content?: Array<{ type: string; text?: string }> }): string {
|
|
15
|
+
return result.content?.map((item) => item.text ?? "").join("\n") ?? "";
|
|
16
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { allAgents, discoverAgents } from "../agents/discover-agents.ts";
|
|
2
|
+
import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
|
|
3
|
+
import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows.ts";
|
|
4
|
+
import { validateWorkflowForTeam } from "../workflows/validate-workflow.ts";
|
|
5
|
+
|
|
6
|
+
export interface ValidationIssue {
|
|
7
|
+
level: "error" | "warning";
|
|
8
|
+
resource: string;
|
|
9
|
+
message: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ValidationReport {
|
|
13
|
+
issues: ValidationIssue[];
|
|
14
|
+
agents: number;
|
|
15
|
+
teams: number;
|
|
16
|
+
workflows: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function validateResources(cwd: string): ValidationReport {
|
|
20
|
+
const agents = allAgents(discoverAgents(cwd));
|
|
21
|
+
const teams = allTeams(discoverTeams(cwd));
|
|
22
|
+
const workflows = allWorkflows(discoverWorkflows(cwd));
|
|
23
|
+
const agentNames = new Set(agents.map((agent) => agent.name));
|
|
24
|
+
const workflowNames = new Set(workflows.map((workflow) => workflow.name));
|
|
25
|
+
const issues: ValidationIssue[] = [];
|
|
26
|
+
|
|
27
|
+
for (const agent of agents) {
|
|
28
|
+
const modelValues = [agent.model, ...(agent.fallbackModels ?? [])].filter((value): value is string => typeof value === "string" && value.length > 0);
|
|
29
|
+
for (const model of modelValues) {
|
|
30
|
+
if (/\s/.test(model)) {
|
|
31
|
+
issues.push({ level: "warning", resource: `agent:${agent.name}`, message: `Model reference '${model}' contains whitespace.` });
|
|
32
|
+
}
|
|
33
|
+
if (model.includes("/") && model.split("/").some((part) => part.trim() === "")) {
|
|
34
|
+
issues.push({ level: "warning", resource: `agent:${agent.name}`, message: `Model reference '${model}' has an empty provider/model segment.` });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const team of teams) {
|
|
40
|
+
for (const role of team.roles) {
|
|
41
|
+
if (!agentNames.has(role.agent)) {
|
|
42
|
+
issues.push({ level: "error", resource: `team:${team.name}`, message: `Role '${role.name}' references unknown agent '${role.agent}'.` });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (team.defaultWorkflow && !workflowNames.has(team.defaultWorkflow)) {
|
|
46
|
+
issues.push({ level: "error", resource: `team:${team.name}`, message: `defaultWorkflow references unknown workflow '${team.defaultWorkflow}'.` });
|
|
47
|
+
}
|
|
48
|
+
const workflow = workflows.find((candidate) => candidate.name === team.defaultWorkflow);
|
|
49
|
+
if (workflow) {
|
|
50
|
+
for (const error of validateWorkflowForTeam(workflow, team)) {
|
|
51
|
+
issues.push({ level: "error", resource: `workflow:${workflow.name}`, message: `Team '${team.name}': ${error}` });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const workflow of workflows) {
|
|
57
|
+
if (workflow.steps.length === 0) {
|
|
58
|
+
issues.push({ level: "warning", resource: `workflow:${workflow.name}`, message: "Workflow has no steps." });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { issues, agents: agents.length, teams: teams.length, workflows: workflows.length };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function formatValidationReport(report: ValidationReport): string {
|
|
66
|
+
const lines = [
|
|
67
|
+
"pi-crew resource validation:",
|
|
68
|
+
`Agents: ${report.agents}`,
|
|
69
|
+
`Teams: ${report.teams}`,
|
|
70
|
+
`Workflows: ${report.workflows}`,
|
|
71
|
+
`Issues: ${report.issues.length}`,
|
|
72
|
+
];
|
|
73
|
+
if (report.issues.length > 0) {
|
|
74
|
+
lines.push("", ...report.issues.map((issue) => `- ${issue.level.toUpperCase()} ${issue.resource}: ${issue.message}`));
|
|
75
|
+
}
|
|
76
|
+
return lines.join("\n");
|
|
77
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export const PI_TEAMS_INHERIT_PROJECT_CONTEXT_ENV = "PI_TEAMS_INHERIT_PROJECT_CONTEXT";
|
|
4
|
+
export const PI_TEAMS_INHERIT_SKILLS_ENV = "PI_TEAMS_INHERIT_SKILLS";
|
|
5
|
+
|
|
6
|
+
const PROJECT_CONTEXT_HEADER = "\n\n# Project Context\n\nProject-specific instructions and guidelines:\n\n";
|
|
7
|
+
const SKILLS_HEADER = "\n\nThe following skills provide specialized instructions for specific tasks.";
|
|
8
|
+
const DATE_HEADER = "\nCurrent date:";
|
|
9
|
+
|
|
10
|
+
function readBooleanEnv(name: string): boolean | undefined {
|
|
11
|
+
const value = process.env[name];
|
|
12
|
+
if (value === undefined) return undefined;
|
|
13
|
+
return value !== "0";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function findSectionEnd(prompt: string, startIndex: number, nextHeaders: string[]): number {
|
|
17
|
+
let endIndex = prompt.length;
|
|
18
|
+
for (const header of nextHeaders) {
|
|
19
|
+
const index = prompt.indexOf(header, startIndex);
|
|
20
|
+
if (index !== -1 && index < endIndex) endIndex = index;
|
|
21
|
+
}
|
|
22
|
+
return endIndex;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function stripProjectContext(prompt: string): string {
|
|
26
|
+
const startIndex = prompt.indexOf(PROJECT_CONTEXT_HEADER);
|
|
27
|
+
if (startIndex === -1) return prompt;
|
|
28
|
+
const endIndex = findSectionEnd(prompt, startIndex + PROJECT_CONTEXT_HEADER.length, [SKILLS_HEADER, DATE_HEADER]);
|
|
29
|
+
return `${prompt.slice(0, startIndex)}${prompt.slice(endIndex)}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function stripInheritedSkills(prompt: string): string {
|
|
33
|
+
const startIndex = prompt.indexOf(SKILLS_HEADER);
|
|
34
|
+
if (startIndex === -1) return prompt;
|
|
35
|
+
const endIndex = findSectionEnd(prompt, startIndex + SKILLS_HEADER.length, [DATE_HEADER]);
|
|
36
|
+
return `${prompt.slice(0, startIndex)}${prompt.slice(endIndex)}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function rewriteTeamWorkerPrompt(prompt: string, options: { inheritProjectContext: boolean; inheritSkills: boolean }): string {
|
|
40
|
+
let rewritten = prompt;
|
|
41
|
+
if (!options.inheritProjectContext) rewritten = stripProjectContext(rewritten);
|
|
42
|
+
if (!options.inheritSkills) rewritten = stripInheritedSkills(rewritten);
|
|
43
|
+
return rewritten;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default function registerPiTeamsPromptRuntime(pi: ExtensionAPI): void {
|
|
47
|
+
pi.on("before_agent_start", (event) => {
|
|
48
|
+
const inheritProjectContext = readBooleanEnv(PI_TEAMS_INHERIT_PROJECT_CONTEXT_ENV);
|
|
49
|
+
const inheritSkills = readBooleanEnv(PI_TEAMS_INHERIT_SKILLS_ENV);
|
|
50
|
+
if (inheritProjectContext === undefined && inheritSkills === undefined) return;
|
|
51
|
+
const rewritten = rewriteTeamWorkerPrompt(event.systemPrompt, {
|
|
52
|
+
inheritProjectContext: inheritProjectContext ?? true,
|
|
53
|
+
inheritSkills: inheritSkills ?? true,
|
|
54
|
+
});
|
|
55
|
+
if (rewritten === event.systemPrompt) return;
|
|
56
|
+
return { systemPrompt: rewritten };
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import type { TeamRunManifest } from "../state/types.ts";
|
|
6
|
+
|
|
7
|
+
export interface SpawnBackgroundTeamRunResult {
|
|
8
|
+
pid?: number;
|
|
9
|
+
logPath: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function spawnBackgroundTeamRun(manifest: TeamRunManifest): SpawnBackgroundTeamRunResult {
|
|
13
|
+
const runnerPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "background-runner.ts");
|
|
14
|
+
const logPath = path.join(manifest.stateRoot, "background.log");
|
|
15
|
+
fs.mkdirSync(manifest.stateRoot, { recursive: true });
|
|
16
|
+
const logFd = fs.openSync(logPath, "a");
|
|
17
|
+
const child = spawn(process.execPath, ["--experimental-strip-types", runnerPath, "--cwd", manifest.cwd, "--run-id", manifest.runId], {
|
|
18
|
+
cwd: manifest.cwd,
|
|
19
|
+
detached: true,
|
|
20
|
+
stdio: ["ignore", logFd, logFd],
|
|
21
|
+
env: { ...process.env },
|
|
22
|
+
});
|
|
23
|
+
child.unref();
|
|
24
|
+
fs.closeSync(logFd);
|
|
25
|
+
return { pid: child.pid, logPath };
|
|
26
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { allAgents, discoverAgents } from "../agents/discover-agents.ts";
|
|
2
|
+
import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
|
|
3
|
+
import { appendEvent } from "../state/event-log.ts";
|
|
4
|
+
import { loadRunManifestById, updateRunStatus } from "../state/state-store.ts";
|
|
5
|
+
import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows.ts";
|
|
6
|
+
import { executeTeamRun } from "./team-runner.ts";
|
|
7
|
+
|
|
8
|
+
function argValue(name: string): string | undefined {
|
|
9
|
+
const index = process.argv.indexOf(name);
|
|
10
|
+
if (index === -1) return undefined;
|
|
11
|
+
return process.argv[index + 1];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function main(): Promise<void> {
|
|
15
|
+
const cwd = argValue("--cwd");
|
|
16
|
+
const runId = argValue("--run-id");
|
|
17
|
+
if (!cwd || !runId) throw new Error("Usage: background-runner.ts --cwd <cwd> --run-id <runId>");
|
|
18
|
+
|
|
19
|
+
const loaded = loadRunManifestById(cwd, runId);
|
|
20
|
+
if (!loaded) throw new Error(`Run '${runId}' not found.`);
|
|
21
|
+
let { manifest, tasks } = loaded;
|
|
22
|
+
appendEvent(manifest.eventsPath, { type: "async.started", runId: manifest.runId, data: { pid: process.pid } });
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const team = allTeams(discoverTeams(cwd)).find((candidate) => candidate.name === manifest.team);
|
|
26
|
+
if (!team) throw new Error(`Team '${manifest.team}' not found.`);
|
|
27
|
+
const workflow = allWorkflows(discoverWorkflows(cwd)).find((candidate) => candidate.name === manifest.workflow);
|
|
28
|
+
if (!workflow) throw new Error(`Workflow '${manifest.workflow ?? ""}' not found.`);
|
|
29
|
+
const agents = allAgents(discoverAgents(cwd));
|
|
30
|
+
const executeWorkers = process.env.PI_TEAMS_EXECUTE_WORKERS === "1";
|
|
31
|
+
const result = await executeTeamRun({ manifest, tasks, team, workflow, agents, executeWorkers });
|
|
32
|
+
manifest = result.manifest;
|
|
33
|
+
tasks = result.tasks;
|
|
34
|
+
appendEvent(manifest.eventsPath, { type: "async.completed", runId: manifest.runId, data: { status: manifest.status, tasks: tasks.length } });
|
|
35
|
+
} catch (error) {
|
|
36
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
37
|
+
manifest = updateRunStatus(manifest, "failed", message);
|
|
38
|
+
appendEvent(manifest.eventsPath, { type: "async.failed", runId: manifest.runId, message });
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await main();
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
3
|
+
import { buildPiWorkerArgs, cleanupTempDir } from "./pi-args.ts";
|
|
4
|
+
import { getPiSpawnCommand } from "./pi-spawn.ts";
|
|
5
|
+
|
|
6
|
+
export interface ChildPiRunInput {
|
|
7
|
+
cwd: string;
|
|
8
|
+
task: string;
|
|
9
|
+
agent: AgentConfig;
|
|
10
|
+
model?: string;
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ChildPiRunResult {
|
|
15
|
+
exitCode: number | null;
|
|
16
|
+
stdout: string;
|
|
17
|
+
stderr: string;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResult> {
|
|
22
|
+
const mock = process.env.PI_TEAMS_MOCK_CHILD_PI;
|
|
23
|
+
if (mock) {
|
|
24
|
+
if (mock === "success") return { exitCode: 0, stdout: `Mock child Pi success for ${input.agent.name}\n`, stderr: "" };
|
|
25
|
+
if (mock === "json-success") return { exitCode: 0, stdout: `${JSON.stringify({ type: "message", message: { role: "assistant", content: [{ type: "text", text: `Mock JSON success for ${input.agent.name}` }] } })}\n${JSON.stringify({ type: "message_end", usage: { input: 10, output: 5, cost: 0.001, turns: 1 } })}\n`, stderr: "" };
|
|
26
|
+
if (mock === "retryable-failure") return { exitCode: 1, stdout: "", stderr: "rate limit: mock failure" };
|
|
27
|
+
return { exitCode: 1, stdout: "", stderr: `mock failure: ${mock}` };
|
|
28
|
+
}
|
|
29
|
+
const built = buildPiWorkerArgs({ task: input.task, agent: input.agent, model: input.model, sessionEnabled: false });
|
|
30
|
+
const spawnSpec = getPiSpawnCommand(built.args);
|
|
31
|
+
try {
|
|
32
|
+
return await new Promise<ChildPiRunResult>((resolve) => {
|
|
33
|
+
const child = spawn(spawnSpec.command, spawnSpec.args, {
|
|
34
|
+
cwd: input.cwd,
|
|
35
|
+
env: { ...process.env, ...built.env },
|
|
36
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
37
|
+
});
|
|
38
|
+
let stdout = "";
|
|
39
|
+
let stderr = "";
|
|
40
|
+
let settled = false;
|
|
41
|
+
|
|
42
|
+
const settle = (result: ChildPiRunResult): void => {
|
|
43
|
+
if (settled) return;
|
|
44
|
+
settled = true;
|
|
45
|
+
cleanupTempDir(built.tempDir);
|
|
46
|
+
resolve(result);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const abort = (): void => {
|
|
50
|
+
try {
|
|
51
|
+
child.kill(process.platform === "win32" ? undefined : "SIGTERM");
|
|
52
|
+
} catch {
|
|
53
|
+
// Ignore kill races.
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
input.signal?.addEventListener("abort", abort, { once: true });
|
|
58
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
59
|
+
stdout += chunk.toString("utf-8");
|
|
60
|
+
});
|
|
61
|
+
child.stderr?.on("data", (chunk: Buffer) => {
|
|
62
|
+
stderr += chunk.toString("utf-8");
|
|
63
|
+
});
|
|
64
|
+
child.on("error", (error) => {
|
|
65
|
+
settle({ exitCode: null, stdout, stderr, error: error.message });
|
|
66
|
+
});
|
|
67
|
+
child.on("close", (exitCode) => {
|
|
68
|
+
input.signal?.removeEventListener("abort", abort);
|
|
69
|
+
settle({ exitCode, stdout, stderr });
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
} finally {
|
|
73
|
+
cleanupTempDir(built.tempDir);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export interface AvailableModelInfo {
|
|
2
|
+
provider: string;
|
|
3
|
+
id: string;
|
|
4
|
+
fullId: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ModelAttemptSummary {
|
|
8
|
+
model: string;
|
|
9
|
+
success: boolean;
|
|
10
|
+
exitCode?: number | null;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function splitThinkingSuffix(model: string): { baseModel: string; thinkingSuffix: string } {
|
|
15
|
+
const colonIdx = model.lastIndexOf(":");
|
|
16
|
+
if (colonIdx === -1) return { baseModel: model, thinkingSuffix: "" };
|
|
17
|
+
return {
|
|
18
|
+
baseModel: model.substring(0, colonIdx),
|
|
19
|
+
thinkingSuffix: model.substring(colonIdx),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function resolveModelCandidate(
|
|
24
|
+
model: string | undefined,
|
|
25
|
+
availableModels: AvailableModelInfo[] | undefined,
|
|
26
|
+
preferredProvider?: string,
|
|
27
|
+
): string | undefined {
|
|
28
|
+
if (!model) return undefined;
|
|
29
|
+
if (model.includes("/")) return model;
|
|
30
|
+
if (!availableModels || availableModels.length === 0) return model;
|
|
31
|
+
|
|
32
|
+
const { baseModel, thinkingSuffix } = splitThinkingSuffix(model);
|
|
33
|
+
const matches = availableModels.filter((entry) => entry.id === baseModel);
|
|
34
|
+
if (preferredProvider) {
|
|
35
|
+
const preferredMatch = matches.find((entry) => entry.provider === preferredProvider);
|
|
36
|
+
if (preferredMatch) return `${preferredMatch.fullId}${thinkingSuffix}`;
|
|
37
|
+
}
|
|
38
|
+
if (matches.length !== 1) return model;
|
|
39
|
+
return `${matches[0]!.fullId}${thinkingSuffix}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const RETRYABLE_MODEL_FAILURE_PATTERNS = [
|
|
43
|
+
/rate\s*limit/i,
|
|
44
|
+
/too many requests/i,
|
|
45
|
+
/\b429\b/,
|
|
46
|
+
/quota/i,
|
|
47
|
+
/billing/i,
|
|
48
|
+
/credit/i,
|
|
49
|
+
/auth(?:entication)?/i,
|
|
50
|
+
/unauthori[sz]ed/i,
|
|
51
|
+
/forbidden/i,
|
|
52
|
+
/api key/i,
|
|
53
|
+
/token expired/i,
|
|
54
|
+
/invalid key/i,
|
|
55
|
+
/provider.*unavailable/i,
|
|
56
|
+
/model.*unavailable/i,
|
|
57
|
+
/model.*disabled/i,
|
|
58
|
+
/model.*not found/i,
|
|
59
|
+
/unknown model/i,
|
|
60
|
+
/overloaded/i,
|
|
61
|
+
/service unavailable/i,
|
|
62
|
+
/temporar(?:ily)? unavailable/i,
|
|
63
|
+
/connection refused/i,
|
|
64
|
+
/fetch failed/i,
|
|
65
|
+
/network error/i,
|
|
66
|
+
/socket hang up/i,
|
|
67
|
+
/upstream/i,
|
|
68
|
+
/timed? out/i,
|
|
69
|
+
/timeout/i,
|
|
70
|
+
/\b502\b/,
|
|
71
|
+
/\b503\b/,
|
|
72
|
+
/\b504\b/,
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
export function isRetryableModelFailure(error: string | undefined): boolean {
|
|
76
|
+
if (!error) return false;
|
|
77
|
+
return RETRYABLE_MODEL_FAILURE_PATTERNS.some((pattern) => pattern.test(error));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function formatModelAttemptNote(attempt: ModelAttemptSummary, nextModel?: string): string {
|
|
81
|
+
const failure = attempt.error?.trim() || `exit ${attempt.exitCode ?? 1}`;
|
|
82
|
+
return nextModel ? `[fallback] ${attempt.model} failed: ${failure}. Retrying with ${nextModel}.` : `[fallback] ${attempt.model} failed: ${failure}.`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function buildModelCandidates(
|
|
86
|
+
primaryModel: string | undefined,
|
|
87
|
+
fallbackModels: string[] | undefined,
|
|
88
|
+
availableModels: AvailableModelInfo[] | undefined,
|
|
89
|
+
preferredProvider?: string,
|
|
90
|
+
): string[] {
|
|
91
|
+
const seen = new Set<string>();
|
|
92
|
+
const candidates: string[] = [];
|
|
93
|
+
for (const raw of [primaryModel, ...(fallbackModels ?? [])]) {
|
|
94
|
+
if (!raw) continue;
|
|
95
|
+
const normalized = resolveModelCandidate(raw.trim(), availableModels, preferredProvider);
|
|
96
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
97
|
+
seen.add(normalized);
|
|
98
|
+
candidates.push(normalized);
|
|
99
|
+
}
|
|
100
|
+
return candidates;
|
|
101
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
6
|
+
|
|
7
|
+
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
8
|
+
const PROMPT_RUNTIME_EXTENSION_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "prompt", "prompt-runtime.ts");
|
|
9
|
+
const TASK_ARG_LIMIT = 8000;
|
|
10
|
+
|
|
11
|
+
export interface BuildPiWorkerArgsInput {
|
|
12
|
+
task: string;
|
|
13
|
+
agent: AgentConfig;
|
|
14
|
+
model?: string;
|
|
15
|
+
sessionEnabled?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface BuildPiWorkerArgsResult {
|
|
19
|
+
args: string[];
|
|
20
|
+
env: Record<string, string | undefined>;
|
|
21
|
+
tempDir?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function applyThinkingSuffix(model: string | undefined, thinking: string | undefined): string | undefined {
|
|
25
|
+
if (!model || !thinking || thinking === "off") return model;
|
|
26
|
+
const colonIdx = model.lastIndexOf(":");
|
|
27
|
+
if (colonIdx !== -1 && THINKING_LEVELS.includes(model.substring(colonIdx + 1))) return model;
|
|
28
|
+
return `${model}:${thinking}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildPiWorkerArgs(input: BuildPiWorkerArgsInput): BuildPiWorkerArgsResult {
|
|
32
|
+
const args = ["--mode", "json", "-p"];
|
|
33
|
+
if (input.sessionEnabled === false) args.push("--no-session");
|
|
34
|
+
|
|
35
|
+
const model = applyThinkingSuffix(input.model ?? input.agent.model, input.agent.thinking);
|
|
36
|
+
if (model) args.push("--model", model);
|
|
37
|
+
|
|
38
|
+
if (input.agent.tools?.length) args.push("--tools", input.agent.tools.join(","));
|
|
39
|
+
if (input.agent.extensions !== undefined) {
|
|
40
|
+
args.push("--no-extensions");
|
|
41
|
+
for (const extension of [PROMPT_RUNTIME_EXTENSION_PATH, ...input.agent.extensions]) args.push("--extension", extension);
|
|
42
|
+
} else {
|
|
43
|
+
args.push("--extension", PROMPT_RUNTIME_EXTENSION_PATH);
|
|
44
|
+
}
|
|
45
|
+
if (!input.agent.inheritSkills) args.push("--no-skills");
|
|
46
|
+
|
|
47
|
+
let tempDir: string | undefined;
|
|
48
|
+
if (input.agent.systemPrompt) {
|
|
49
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-crew-"));
|
|
50
|
+
const promptPath = path.join(tempDir, `${input.agent.name.replace(/[^\w.-]/g, "_")}.md`);
|
|
51
|
+
fs.writeFileSync(promptPath, input.agent.systemPrompt, { mode: 0o600 });
|
|
52
|
+
args.push(input.agent.systemPromptMode === "append" ? "--append-system-prompt" : "--system-prompt", promptPath);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (input.task.length > TASK_ARG_LIMIT) {
|
|
56
|
+
if (!tempDir) tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-crew-"));
|
|
57
|
+
const taskPath = path.join(tempDir, "task.md");
|
|
58
|
+
fs.writeFileSync(taskPath, input.task, { mode: 0o600 });
|
|
59
|
+
args.push(`@${taskPath}`);
|
|
60
|
+
} else {
|
|
61
|
+
args.push(`Task: ${input.task}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
args,
|
|
66
|
+
env: {
|
|
67
|
+
PI_TEAMS_INHERIT_PROJECT_CONTEXT: input.agent.inheritProjectContext ? "1" : "0",
|
|
68
|
+
PI_TEAMS_INHERIT_SKILLS: input.agent.inheritSkills ? "1" : "0",
|
|
69
|
+
},
|
|
70
|
+
tempDir,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function cleanupTempDir(tempDir: string | undefined): void {
|
|
75
|
+
if (!tempDir) return;
|
|
76
|
+
try {
|
|
77
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
78
|
+
} catch {
|
|
79
|
+
// Best effort.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
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 text: string[] = [];
|
|
76
|
+
if (typeof obj.text === "string") text.push(obj.text);
|
|
77
|
+
if (typeof obj.output === "string") text.push(obj.output);
|
|
78
|
+
if (typeof obj.finalOutput === "string") text.push(obj.finalOutput);
|
|
79
|
+
if (typeof obj.final_output === "string") text.push(obj.final_output);
|
|
80
|
+
text.push(...textFromContent(obj.content));
|
|
81
|
+
const message = asRecord(obj.message);
|
|
82
|
+
if (message) text.push(...textFromContent(message.content));
|
|
83
|
+
return text.filter((entry) => entry.trim().length > 0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function parsePiJsonOutput(stdout: string): ParsedPiJsonOutput {
|
|
87
|
+
let jsonEvents = 0;
|
|
88
|
+
const textEvents: string[] = [];
|
|
89
|
+
let usage: ParsedPiUsage | undefined;
|
|
90
|
+
for (const line of stdout.split("\n")) {
|
|
91
|
+
const trimmed = line.trim();
|
|
92
|
+
if (!trimmed) continue;
|
|
93
|
+
let event: unknown;
|
|
94
|
+
try {
|
|
95
|
+
event = JSON.parse(trimmed) as unknown;
|
|
96
|
+
} catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
jsonEvents++;
|
|
100
|
+
textEvents.push(...extractText(event));
|
|
101
|
+
const eventUsage = extractUsage(event);
|
|
102
|
+
if (eventUsage) usage = mergeUsage(usage ?? {}, eventUsage);
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
jsonEvents,
|
|
106
|
+
textEvents,
|
|
107
|
+
finalText: textEvents.length > 0 ? textEvents[textEvents.length - 1] : undefined,
|
|
108
|
+
usage,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
export interface PiSpawnCommand {
|
|
6
|
+
command: string;
|
|
7
|
+
args: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isRunnableNodeScript(filePath: string): boolean {
|
|
11
|
+
return fs.existsSync(filePath) && /\.(?:mjs|cjs|js)$/i.test(filePath);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolvePiPackageRoot(): string | undefined {
|
|
15
|
+
try {
|
|
16
|
+
const entry = process.argv[1];
|
|
17
|
+
if (!entry) return undefined;
|
|
18
|
+
let dir = path.dirname(fs.realpathSync(entry));
|
|
19
|
+
while (dir !== path.dirname(dir)) {
|
|
20
|
+
try {
|
|
21
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf-8")) as { name?: string };
|
|
22
|
+
if (pkg.name === "@mariozechner/pi-coding-agent") return dir;
|
|
23
|
+
} catch {
|
|
24
|
+
// Continue walking upward.
|
|
25
|
+
}
|
|
26
|
+
dir = path.dirname(dir);
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function packageBinScript(packageJsonPath: string): string | undefined {
|
|
35
|
+
try {
|
|
36
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as { bin?: string | Record<string, string> };
|
|
37
|
+
const binPath = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.pi ?? Object.values(pkg.bin ?? {})[0];
|
|
38
|
+
if (!binPath) return undefined;
|
|
39
|
+
const candidate = path.resolve(path.dirname(packageJsonPath), binPath);
|
|
40
|
+
return isRunnableNodeScript(candidate) ? candidate : undefined;
|
|
41
|
+
} catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function findPiPackageJsonFrom(startDir: string): string | undefined {
|
|
47
|
+
let dir = startDir;
|
|
48
|
+
while (dir !== path.dirname(dir)) {
|
|
49
|
+
const direct = path.join(dir, "package.json");
|
|
50
|
+
try {
|
|
51
|
+
const pkg = JSON.parse(fs.readFileSync(direct, "utf-8")) as { name?: string };
|
|
52
|
+
if (pkg.name === "@mariozechner/pi-coding-agent") return direct;
|
|
53
|
+
} catch {
|
|
54
|
+
// Continue searching upward and in node_modules.
|
|
55
|
+
}
|
|
56
|
+
const dependency = path.join(dir, "node_modules", "@mariozechner", "pi-coding-agent", "package.json");
|
|
57
|
+
if (fs.existsSync(dependency)) return dependency;
|
|
58
|
+
dir = path.dirname(dir);
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolvePiCliScript(): string | undefined {
|
|
64
|
+
const explicit = process.env.PI_TEAMS_PI_BIN?.trim();
|
|
65
|
+
if (explicit && isRunnableNodeScript(explicit)) return explicit;
|
|
66
|
+
|
|
67
|
+
const argv1 = process.argv[1];
|
|
68
|
+
if (argv1) {
|
|
69
|
+
const argvPath = path.isAbsolute(argv1) ? argv1 : path.resolve(argv1);
|
|
70
|
+
if (isRunnableNodeScript(argvPath)) return argvPath;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const roots = [
|
|
74
|
+
resolvePiPackageRoot(),
|
|
75
|
+
process.env.APPDATA ? path.join(process.env.APPDATA, "npm", "node_modules", "@mariozechner", "pi-coding-agent") : undefined,
|
|
76
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
77
|
+
process.cwd(),
|
|
78
|
+
].filter((entry): entry is string => Boolean(entry));
|
|
79
|
+
|
|
80
|
+
for (const root of roots) {
|
|
81
|
+
const packageJsonPath = root.endsWith("package.json") ? root : findPiPackageJsonFrom(root) ?? path.join(root, "package.json");
|
|
82
|
+
const script = packageBinScript(packageJsonPath);
|
|
83
|
+
if (script) return script;
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getPiSpawnCommand(args: string[]): PiSpawnCommand {
|
|
89
|
+
const explicit = process.env.PI_TEAMS_PI_BIN?.trim();
|
|
90
|
+
if (explicit && fs.existsSync(explicit) && !isRunnableNodeScript(explicit)) return { command: explicit, args };
|
|
91
|
+
if (process.platform === "win32") {
|
|
92
|
+
const script = resolvePiCliScript();
|
|
93
|
+
if (script) return { command: process.execPath, args: [script, ...args] };
|
|
94
|
+
}
|
|
95
|
+
return { command: "pi", args };
|
|
96
|
+
}
|