patch pi codex websocket usage-limit error mapping
remove deprecated subagent extension (to be remade later)
This commit is contained in:
parent
6db85eef8b
commit
7879b5aada
17 changed files with 154 additions and 1599 deletions
24
flake.lock
generated
24
flake.lock
generated
|
|
@ -67,11 +67,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1778446047,
|
"lastModified": 1782772816,
|
||||||
"narHash": "sha256-oQvcadh2BCkrog+SGrG6YffKJrveYpjj3TdQJWaKhaM=",
|
"narHash": "sha256-s9BuFv0mRuZx9C1MF8qPHRdcAK14ONi0A5m6E2wqOoM=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "bun2nix",
|
"repo": "bun2nix",
|
||||||
"rev": "f2bc12af1a6369648aac41041ceeaa0b866599c6",
|
"rev": "5a39d717029e94163ac223aee8d5c9946cafed1c",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -131,11 +131,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1778716662,
|
"lastModified": 1782949081,
|
||||||
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
|
"narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=",
|
||||||
"owner": "hercules-ci",
|
"owner": "hercules-ci",
|
||||||
"repo": "flake-parts",
|
"repo": "flake-parts",
|
||||||
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
|
"rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -249,11 +249,11 @@
|
||||||
"treefmt-nix": "treefmt-nix"
|
"treefmt-nix": "treefmt-nix"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1781849779,
|
"lastModified": 1783669679,
|
||||||
"narHash": "sha256-Ihdla0v+oCO6dNN6iExgdGc9+JmKLmAnbiTneiJgZbE=",
|
"narHash": "sha256-WDGDMsPXxceC0k1ktqcobmwE4GCTrzzULUKlp1CzSKQ=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "llm-agents.nix",
|
"repo": "llm-agents.nix",
|
||||||
"rev": "a1e67cc315b6bd924614d4232e09bab54df3ae39",
|
"rev": "6e9f6664e72433966557ab83a7c3e8c0bbf64a44",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -355,11 +355,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1781607440,
|
"lastModified": 1783279667,
|
||||||
"narHash": "sha256-rxO+uc/KFbSJp+pgyXRuAX6QlG9hJdnt0BXpEQRXY+U=",
|
"narHash": "sha256-/NAkDSsve+GNM0Bt6tleJdCGfsTlK89nPjkVOzZMo0s=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "3e41b24abd260e8f71dbe2f5737d24122f972158",
|
"rev": "f205b5574fd0cb7da5b702a2da51507b7f4fdd1b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,18 @@ let
|
||||||
# Pinned llm-agents for Claude Code 2.0.64
|
# Pinned llm-agents for Claude Code 2.0.64
|
||||||
llmPkgsPinned = inputs.llm-agents-pinned.packages.${system};
|
llmPkgsPinned = inputs.llm-agents-pinned.packages.${system};
|
||||||
|
|
||||||
|
# pi with a local patch applied: maps Codex usage-limit errors that arrive
|
||||||
|
# over the (default) WebSocket transport to the same friendly
|
||||||
|
# "You have hit your ChatGPT usage limit" message the SSE path already emits.
|
||||||
|
# Without it, a normal ChatGPT-subscription quota trip surfaces as the raw,
|
||||||
|
# misleading "exceeded your current quota / check your billing" API text.
|
||||||
|
# See patches/pi/fix-codex-ws-usage-limit.mjs.
|
||||||
|
piPatched = llmPkgs.pi.overrideAttrs (o: {
|
||||||
|
postInstall = (o.postInstall or "") + ''
|
||||||
|
${pkgs.nodejs}/bin/node ${./../../../patches/pi/fix-codex-ws-usage-limit.mjs} $out/lib/node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js
|
||||||
|
'';
|
||||||
|
});
|
||||||
|
|
||||||
# Z.AI Gateway wrapper for Claude Code
|
# Z.AI Gateway wrapper for Claude Code
|
||||||
zaiWrapperPackages =
|
zaiWrapperPackages =
|
||||||
let
|
let
|
||||||
|
|
@ -275,18 +287,19 @@ in
|
||||||
home.packages = [
|
home.packages = [
|
||||||
llmPkgs.claude-code # latest
|
llmPkgs.claude-code # latest
|
||||||
llmPkgs.opencode # latest
|
llmPkgs.opencode # latest
|
||||||
llmPkgs.pi
|
piPatched
|
||||||
llmPkgs.ccstatusline # latest
|
llmPkgs.ccstatusline # latest
|
||||||
llmPkgs.ccusage # latest
|
llmPkgs.ccusage # latest
|
||||||
llmPkgs.codex
|
llmPkgs.codex
|
||||||
llmPkgs.rtk
|
llmPkgs.rtk
|
||||||
|
llmPkgs.cursor-agent
|
||||||
]
|
]
|
||||||
++ zaiWrapperPackages
|
++ zaiWrapperPackages
|
||||||
++ kimiWrapperPackages
|
++ kimiWrapperPackages
|
||||||
++ cfg.extraPackages;
|
++ cfg.extraPackages;
|
||||||
|
|
||||||
home.activation.installPiFff = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
home.activation.installPiFff = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
||||||
if [ -x ${lib.getExe llmPkgs.pi} ]; then
|
if [ -x ${lib.getExe piPatched} ]; then
|
||||||
export PI_SKIP_VERSION_CHECK=1
|
export PI_SKIP_VERSION_CHECK=1
|
||||||
export PI_TELEMETRY=0
|
export PI_TELEMETRY=0
|
||||||
export PATH=${pkgs.nodejs}/bin:${pkgs.git}/bin:$PATH
|
export PATH=${pkgs.nodejs}/bin:${pkgs.git}/bin:$PATH
|
||||||
|
|
@ -295,10 +308,10 @@ in
|
||||||
mkdir -p "$(dirname "$settings_file")"
|
mkdir -p "$(dirname "$settings_file")"
|
||||||
${pkgs.nodejs}/bin/node -e "const fs=require('fs'); const path=process.env.HOME+'/.pi/agent/settings.json'; let settings={}; if (fs.existsSync(path)) settings=JSON.parse(fs.readFileSync(path,'utf8')); settings.theme='dark'; fs.writeFileSync(path, JSON.stringify(settings, null, 2)+String.fromCharCode(10));"
|
${pkgs.nodejs}/bin/node -e "const fs=require('fs'); const path=process.env.HOME+'/.pi/agent/settings.json'; let settings={}; if (fs.existsSync(path)) settings=JSON.parse(fs.readFileSync(path,'utf8')); settings.theme='dark'; fs.writeFileSync(path, JSON.stringify(settings, null, 2)+String.fromCharCode(10));"
|
||||||
|
|
||||||
${lib.getExe llmPkgs.pi} list | ${pkgs.gnugrep}/bin/grep -q '@ff-labs/pi-fff' || \
|
${lib.getExe piPatched} list | ${pkgs.gnugrep}/bin/grep -q '@ff-labs/pi-fff' || \
|
||||||
${lib.getExe llmPkgs.pi} install npm:@ff-labs/pi-fff@0.9.4
|
${lib.getExe piPatched} install npm:@ff-labs/pi-fff@0.9.4
|
||||||
${lib.getExe llmPkgs.pi} list | ${pkgs.gnugrep}/bin/grep -q '@juicesharp/rpiv-ask-user-question' || \
|
${lib.getExe piPatched} list | ${pkgs.gnugrep}/bin/grep -q '@juicesharp/rpiv-ask-user-question' || \
|
||||||
${lib.getExe llmPkgs.pi} install npm:@juicesharp/rpiv-ask-user-question@1.20.0
|
${lib.getExe piPatched} install npm:@juicesharp/rpiv-ask-user-question@1.20.0
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
|
|
||||||
123
patches/pi/fix-codex-ws-usage-limit.mjs
Normal file
123
patches/pi/fix-codex-ws-usage-limit.mjs
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
// Patches @earendil-works/pi-ai's openai-codex-responses provider so that
|
||||||
|
// usage-limit errors arriving over the WebSocket transport are mapped to the
|
||||||
|
// same friendly "You have hit your ChatGPT usage limit" message that the
|
||||||
|
// SSE/fetch path already produces via parseErrorResponse().
|
||||||
|
//
|
||||||
|
// Without this, the default `transport: auto` (WebSocket-first) path throws
|
||||||
|
// the raw upstream string verbatim, e.g.:
|
||||||
|
// "You exceeded your current quota, please check your plan and billing
|
||||||
|
// details. For more information on this error, read the docs:
|
||||||
|
// https://platform.openai.com/docs/guides/error-codes/api-errors."
|
||||||
|
// which is misleading: the account is a ChatGPT subscription (prolite plan),
|
||||||
|
// not API billing, and the real limit is a per-window ChatGPT/Codex quota.
|
||||||
|
//
|
||||||
|
// Usage: node fix-codex-ws-usage-limit.mjs <path-to-openai-codex-responses.js>
|
||||||
|
import { readFileSync, writeFileSync } from "node:fs";
|
||||||
|
|
||||||
|
const file = process.argv[2];
|
||||||
|
if (!file) {
|
||||||
|
console.error("fix-codex-ws-usage-limit: missing target file argument");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let src = readFileSync(file, "utf8");
|
||||||
|
|
||||||
|
// 1. Insert a friendly-message helper just before mapCodexEvents.
|
||||||
|
const helperAnchor = "async function* mapCodexEvents(events) {";
|
||||||
|
if (!src.includes(helperAnchor)) {
|
||||||
|
console.error(
|
||||||
|
"fix-codex-ws-usage-limit: anchor 'async function* mapCodexEvents(events) {' not found; refusing to patch an unfamiliar file.",
|
||||||
|
);
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const helper = `function friendlyCodexUsageMessage(code, message, payload) {
|
||||||
|
const codeStr = typeof code === "string" ? code : "";
|
||||||
|
const msgStr = typeof message === "string" ? message : "";
|
||||||
|
const isUsageLimit =
|
||||||
|
/usage_limit_reached|usage_not_included|rate_limit_exceeded/i.test(codeStr) ||
|
||||||
|
/insufficient_quota|exceeded your current quota|quota exceeded|out of budget|usage limit|billing/i.test(msgStr);
|
||||||
|
if (!isUsageLimit)
|
||||||
|
return undefined;
|
||||||
|
const src = (payload && typeof payload === "object")
|
||||||
|
? (payload.response?.error ?? payload.error ?? payload)
|
||||||
|
: {};
|
||||||
|
const plan = src.plan_type ? \` (\${String(src.plan_type).toLowerCase()} plan)\` : "";
|
||||||
|
const resetsAt = src.resets_at ?? payload?.resets_at;
|
||||||
|
const mins = typeof resetsAt === "number"
|
||||||
|
? Math.max(0, Math.round((resetsAt * 1000 - Date.now()) / 60000))
|
||||||
|
: undefined;
|
||||||
|
const when = mins !== undefined ? \` Try again in ~\${mins} min.\` : "";
|
||||||
|
return \`You have hit your ChatGPT usage limit\${plan}.\${when}\`.trim();
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (!src.includes("function friendlyCodexUsageMessage(")) {
|
||||||
|
src = src.replace(helperAnchor, helper + helperAnchor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Map usage-limit errors in the `error` event branch.
|
||||||
|
const errorBranchOld = ` if (type === "error") {
|
||||||
|
const code = event.code || "";
|
||||||
|
const message = event.message || "";
|
||||||
|
throw new CodexApiError(\`Codex error: \${message || code || JSON.stringify(event)}\`, {
|
||||||
|
code: code || undefined,
|
||||||
|
payload: event,
|
||||||
|
});
|
||||||
|
}`;
|
||||||
|
const errorBranchWithExtractorOld = ` if (type === "error") {
|
||||||
|
const { code, message } = extractCodexEventError(event);
|
||||||
|
throw new CodexApiError(\`Codex error: \${message || code || JSON.stringify(event)}\`, {
|
||||||
|
code,
|
||||||
|
payload: event,
|
||||||
|
});
|
||||||
|
}`;
|
||||||
|
const errorBranchNew = ` if (type === "error") {
|
||||||
|
const code = event.code || "";
|
||||||
|
const message = event.message || "";
|
||||||
|
const friendly = friendlyCodexUsageMessage(code, message, event);
|
||||||
|
throw new CodexApiError(friendly || \`Codex error: \${message || code || JSON.stringify(event)}\`, {
|
||||||
|
code: code || undefined,
|
||||||
|
payload: event,
|
||||||
|
});
|
||||||
|
}`;
|
||||||
|
const errorBranchWithExtractorNew = ` if (type === "error") {
|
||||||
|
const { code, message } = extractCodexEventError(event);
|
||||||
|
const friendly = friendlyCodexUsageMessage(code, message, event);
|
||||||
|
throw new CodexApiError(friendly || \`Codex error: \${message || code || JSON.stringify(event)}\`, {
|
||||||
|
code,
|
||||||
|
payload: event,
|
||||||
|
});
|
||||||
|
}`;
|
||||||
|
if (src.includes(errorBranchOld)) {
|
||||||
|
src = src.replace(errorBranchOld, errorBranchNew);
|
||||||
|
} else if (src.includes(errorBranchWithExtractorOld)) {
|
||||||
|
src = src.replace(errorBranchWithExtractorOld, errorBranchWithExtractorNew);
|
||||||
|
} else if (!src.includes("const friendly = friendlyCodexUsageMessage(code, message, event);")) {
|
||||||
|
console.error("fix-codex-ws-usage-limit: could not locate the 'error' event throw branch; refusing to patch.");
|
||||||
|
process.exit(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Map usage-limit errors in the `response.failed` event branch.
|
||||||
|
const failedBranchOld = ` if (type === "response.failed") {
|
||||||
|
const response = event.response;
|
||||||
|
const code = response?.error?.code;
|
||||||
|
const message = response?.error?.message;
|
||||||
|
throw new CodexApiError(message || "Codex response failed", { code, payload: event });
|
||||||
|
}`;
|
||||||
|
const failedBranchNew = ` if (type === "response.failed") {
|
||||||
|
const response = event.response;
|
||||||
|
const code = response?.error?.code;
|
||||||
|
const message = response?.error?.message;
|
||||||
|
const friendly = friendlyCodexUsageMessage(code || "", message || "", event);
|
||||||
|
throw new CodexApiError(friendly || message || "Codex response failed", { code, payload: event });
|
||||||
|
}`;
|
||||||
|
if (src.includes(failedBranchOld)) {
|
||||||
|
src = src.replace(failedBranchOld, failedBranchNew);
|
||||||
|
} else if (!src.includes('const friendly = friendlyCodexUsageMessage(code || "", message || "", event);')) {
|
||||||
|
console.error("fix-codex-ws-usage-limit: could not locate the 'response.failed' throw branch; refusing to patch.");
|
||||||
|
process.exit(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(file, src);
|
||||||
|
console.error("fix-codex-ws-usage-limit: patched " + file);
|
||||||
|
|
@ -50,6 +50,3 @@ Coding context:
|
||||||
Use exa_code to find examples of React hook state management patterns
|
Use exa_code to find examples of React hook state management patterns
|
||||||
```
|
```
|
||||||
|
|
||||||
Subagent usage:
|
|
||||||
|
|
||||||
The `librarian` subagent can use these tools once the extension is globally installed.
|
|
||||||
|
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
# pi subagent scaffold
|
|
||||||
|
|
||||||
Foundation for a global pi subagent extension with parallel and chained delegation.
|
|
||||||
|
|
||||||
## What this includes
|
|
||||||
|
|
||||||
- `index.ts` — main `subagent` tool and `/subagents` command
|
|
||||||
- `agents.ts` — agent discovery and manifest parsing
|
|
||||||
- `agents/` — starter bundled agents:
|
|
||||||
- `scout` → `zai/glm-5.1`
|
|
||||||
- `planner` → `openai-codex/gpt-5.4:xhigh`
|
|
||||||
- `implementer` → `openai-codex/gpt-5.3-codex`
|
|
||||||
- `reviewer` → `openai-codex/gpt-5.4:xhigh`
|
|
||||||
- `librarian` → `zai/glm-5.1` + `exa_search` / `exa_code`
|
|
||||||
- `prompts/` — starter prompt templates:
|
|
||||||
- `/implement`
|
|
||||||
- `/scout-and-plan`
|
|
||||||
- `/implement-and-review`
|
|
||||||
- `/parallel-scout`
|
|
||||||
- `/research`
|
|
||||||
|
|
||||||
## Discovery model
|
|
||||||
|
|
||||||
This extension supports three agent sources:
|
|
||||||
|
|
||||||
- **bundled** — agents shipped in this directory (`./agents`)
|
|
||||||
- **user** — `~/.pi/agent/agents`
|
|
||||||
- **project** — nearest `.pi/agents` from the current working directory upward
|
|
||||||
|
|
||||||
Scope values:
|
|
||||||
|
|
||||||
- `global` — bundled + user
|
|
||||||
- `project` — project only
|
|
||||||
- `both` — bundled + user + project
|
|
||||||
|
|
||||||
Precedence when names collide:
|
|
||||||
|
|
||||||
1. bundled
|
|
||||||
2. user
|
|
||||||
3. project
|
|
||||||
|
|
||||||
So project-local agents override user/global ones, and user/global agents override bundled defaults.
|
|
||||||
|
|
||||||
## Parallel safety
|
|
||||||
|
|
||||||
Parallel mode is intentionally conservative.
|
|
||||||
|
|
||||||
- agents with `parallelSafe: true` can run in parallel
|
|
||||||
- agents with `parallelSafe: false` are blocked in parallel mode
|
|
||||||
|
|
||||||
The bundled `implementer` is marked serial-only.
|
|
||||||
|
|
||||||
## Agent manifest format
|
|
||||||
|
|
||||||
Each agent is a markdown file with YAML frontmatter:
|
|
||||||
|
|
||||||
```md
|
|
||||||
---
|
|
||||||
name: planner
|
|
||||||
description: Creates implementation plans
|
|
||||||
model: anthropic/claude-sonnet-4-5
|
|
||||||
tools: read, grep, find, ls
|
|
||||||
parallelSafe: true
|
|
||||||
role: planner
|
|
||||||
tags: planning, design
|
|
||||||
---
|
|
||||||
|
|
||||||
System prompt goes here.
|
|
||||||
```
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
|
|
||||||
- `model` accepts normal pi `--model` values, including `provider/id` and optional `:thinking`
|
|
||||||
- the current scaffold intentionally uses built-in pi providers (`openai-codex`, `zai`), so no custom provider extension is required yet
|
|
||||||
- `tools` should usually be explicit to avoid recursive delegation
|
|
||||||
- `parallelSafe` defaults to `false` if omitted
|
|
||||||
|
|
||||||
## Install globally from this repo
|
|
||||||
|
|
||||||
Recommended runtime target:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p ~/.pi/agent/extensions
|
|
||||||
ln -sfn /Users/rasyidanakbar/Development/dotfiles/pi/extensions/subagent ~/.pi/agent/extensions/subagent
|
|
||||||
```
|
|
||||||
|
|
||||||
Then start pi and run:
|
|
||||||
|
|
||||||
```text
|
|
||||||
/reload
|
|
||||||
```
|
|
||||||
|
|
||||||
Because prompts and bundled agents live inside the extension directory, a single directory symlink is enough.
|
|
||||||
|
|
||||||
## Usage examples
|
|
||||||
|
|
||||||
Single agent:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Use subagent planner to propose a plan for refactoring auth
|
|
||||||
```
|
|
||||||
|
|
||||||
Parallel:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Use subagent with two tasks in parallel: scout auth flow, librarian find similar patterns
|
|
||||||
```
|
|
||||||
|
|
||||||
Chain:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Use subagent chain: scout -> planner -> implementer
|
|
||||||
```
|
|
||||||
|
|
||||||
Prompt templates:
|
|
||||||
|
|
||||||
```text
|
|
||||||
/implement add request validation to the API
|
|
||||||
/scout-and-plan refactor auth to support oauth
|
|
||||||
/implement-and-review add pagination to the endpoint
|
|
||||||
/parallel-scout investigate session handling
|
|
||||||
/research evaluate the best pattern for background job retries in this codebase
|
|
||||||
```
|
|
||||||
|
|
||||||
## Good next steps
|
|
||||||
|
|
||||||
1. add `parallel_then_reduce`
|
|
||||||
2. add budget / timeout controls
|
|
||||||
3. add a custom `index_search` tool for librarian workflows
|
|
||||||
4. wire the whole `pi/extensions/` directory into Home Manager so `~/.pi/agent/extensions/` is managed automatically
|
|
||||||
5. pair librarian with the separate `pi/extensions/exa-tools` extension for web/docs/code retrieval
|
|
||||||
|
|
@ -1,186 +0,0 @@
|
||||||
import * as fs from "node:fs";
|
|
||||||
import * as path from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import { getAgentDir, parseFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
||||||
|
|
||||||
export type AgentScope = "global" | "project" | "both";
|
|
||||||
export type AgentSource = "bundled" | "user" | "project" | "unknown";
|
|
||||||
|
|
||||||
export interface AgentConfig {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
tools?: string[];
|
|
||||||
model?: string;
|
|
||||||
systemPrompt: string;
|
|
||||||
source: AgentSource;
|
|
||||||
filePath: string;
|
|
||||||
parallelSafe: boolean;
|
|
||||||
role?: string;
|
|
||||||
tags?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AgentDiscoveryResult {
|
|
||||||
agents: AgentConfig[];
|
|
||||||
bundledAgentsDir: string;
|
|
||||||
userAgentsDir: string;
|
|
||||||
projectAgentsDir: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readString(value: unknown): string | undefined {
|
|
||||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readBoolean(value: unknown): boolean | undefined {
|
|
||||||
if (typeof value === "boolean") return value;
|
|
||||||
if (typeof value === "number") return value !== 0;
|
|
||||||
if (typeof value !== "string") return undefined;
|
|
||||||
|
|
||||||
const normalized = value.trim().toLowerCase();
|
|
||||||
if (["true", "yes", "on", "1"].includes(normalized)) return true;
|
|
||||||
if (["false", "no", "off", "0"].includes(normalized)) return false;
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readStringList(value: unknown): string[] | undefined {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
const items = value
|
|
||||||
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
|
||||||
.filter(Boolean);
|
|
||||||
return items.length > 0 ? items : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === "string") {
|
|
||||||
const items = value
|
|
||||||
.split(",")
|
|
||||||
.map((item) => item.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
return items.length > 0 ? items : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isDirectory(pathname: string): boolean {
|
|
||||||
try {
|
|
||||||
return fs.statSync(pathname).isDirectory();
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBundledAgentsDir(): string {
|
|
||||||
return fileURLToPath(new URL("./agents", import.meta.url));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUserAgentsDir(): string {
|
|
||||||
return path.join(getAgentDir(), "agents");
|
|
||||||
}
|
|
||||||
|
|
||||||
function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
||||||
let currentDir = cwd;
|
|
||||||
while (true) {
|
|
||||||
const candidate = path.join(currentDir, ".pi", "agents");
|
|
||||||
if (isDirectory(candidate)) return candidate;
|
|
||||||
|
|
||||||
const parentDir = path.dirname(currentDir);
|
|
||||||
if (parentDir === currentDir) return null;
|
|
||||||
currentDir = parentDir;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadAgentsFromDir(dir: string, source: Exclude<AgentSource, "unknown">): AgentConfig[] {
|
|
||||||
const agents: AgentConfig[] = [];
|
|
||||||
|
|
||||||
if (!isDirectory(dir)) return agents;
|
|
||||||
|
|
||||||
let entries: fs.Dirent[];
|
|
||||||
try {
|
|
||||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
||||||
} catch {
|
|
||||||
return agents;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (!entry.name.endsWith(".md")) continue;
|
|
||||||
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
||||||
|
|
||||||
const filePath = path.join(dir, entry.name);
|
|
||||||
let content: string;
|
|
||||||
try {
|
|
||||||
content = fs.readFileSync(filePath, "utf-8");
|
|
||||||
} catch {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { frontmatter, body } = parseFrontmatter<Record<string, unknown>>(content);
|
|
||||||
const name = readString(frontmatter.name);
|
|
||||||
const description = readString(frontmatter.description);
|
|
||||||
|
|
||||||
if (!name || !description) continue;
|
|
||||||
|
|
||||||
agents.push({
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
tools: readStringList(frontmatter.tools),
|
|
||||||
model: readString(frontmatter.model),
|
|
||||||
systemPrompt: body.trim(),
|
|
||||||
source,
|
|
||||||
filePath,
|
|
||||||
parallelSafe: readBoolean(frontmatter.parallelSafe) ?? false,
|
|
||||||
role: readString(frontmatter.role),
|
|
||||||
tags: readStringList(frontmatter.tags),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return agents;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
||||||
const bundledAgentsDir = getBundledAgentsDir();
|
|
||||||
const userAgentsDir = getUserAgentsDir();
|
|
||||||
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
||||||
|
|
||||||
const bundledAgents = loadAgentsFromDir(bundledAgentsDir, "bundled");
|
|
||||||
const userAgents = scope === "project" ? [] : loadAgentsFromDir(userAgentsDir, "user");
|
|
||||||
const projectAgents = scope === "global" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
|
|
||||||
|
|
||||||
const agentMap = new Map<string, AgentConfig>();
|
|
||||||
|
|
||||||
if (scope === "global") {
|
|
||||||
for (const agent of bundledAgents) agentMap.set(agent.name, agent);
|
|
||||||
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scope === "project") {
|
|
||||||
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scope === "both") {
|
|
||||||
for (const agent of bundledAgents) agentMap.set(agent.name, agent);
|
|
||||||
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
||||||
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
agents: Array.from(agentMap.values()),
|
|
||||||
bundledAgentsDir,
|
|
||||||
userAgentsDir,
|
|
||||||
projectAgentsDir,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatAgentList(agents: AgentConfig[], maxItems: number): { text: string; remaining: number } {
|
|
||||||
if (agents.length === 0) return { text: "none", remaining: 0 };
|
|
||||||
|
|
||||||
const listed = agents.slice(0, maxItems);
|
|
||||||
const remaining = Math.max(0, agents.length - listed.length);
|
|
||||||
const text = listed
|
|
||||||
.map((agent) => {
|
|
||||||
const mode = agent.parallelSafe ? "parallel" : "serial";
|
|
||||||
const role = agent.role ? ` ${agent.role}` : "";
|
|
||||||
return `${agent.name} (${agent.source}, ${mode}${role ? `, ${role}` : ""})`;
|
|
||||||
})
|
|
||||||
.join(", ");
|
|
||||||
|
|
||||||
return { text, remaining };
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
---
|
|
||||||
name: implementer
|
|
||||||
description: Executes approved plans and makes code changes carefully
|
|
||||||
tools: read, grep, find, ls, bash, edit, write
|
|
||||||
model: openai-codex/gpt-5.3-codex
|
|
||||||
parallelSafe: false
|
|
||||||
role: implementer
|
|
||||||
tags: implementation, coding, execution
|
|
||||||
---
|
|
||||||
|
|
||||||
You are an implementation specialist operating in an isolated subagent context.
|
|
||||||
|
|
||||||
Your job is to make the requested change safely and completely.
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
- Follow the provided plan if one exists.
|
|
||||||
- Prefer minimal, targeted edits.
|
|
||||||
- Do not make unrelated refactors.
|
|
||||||
- When using bash, keep commands focused on verification and repository-aware workflows.
|
|
||||||
- If the task is ambiguous, make the smallest reasonable assumption and document it.
|
|
||||||
|
|
||||||
Output format:
|
|
||||||
|
|
||||||
## Completed
|
|
||||||
What you changed.
|
|
||||||
|
|
||||||
## Files Changed
|
|
||||||
- `path/to/file.ts` - summary of change
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
Commands run, checks performed, or why validation was not run.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
Any assumptions, follow-up items, or risks for a reviewer.
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
---
|
|
||||||
name: librarian
|
|
||||||
description: Retrieves references, patterns, prior art, and supporting context
|
|
||||||
tools: read, grep, find, ls, exa_search, exa_code
|
|
||||||
model: zai/glm-5.1
|
|
||||||
parallelSafe: true
|
|
||||||
role: librarian
|
|
||||||
tags: search, references, indexing, docs
|
|
||||||
---
|
|
||||||
|
|
||||||
You are a librarian.
|
|
||||||
|
|
||||||
Your job is to gather supporting references from the repository so other agents can reason faster.
|
|
||||||
|
|
||||||
Focus on:
|
|
||||||
- similar implementations elsewhere in the repo
|
|
||||||
- naming conventions and file patterns
|
|
||||||
- related tests, docs, or configs
|
|
||||||
- existing abstractions worth reusing
|
|
||||||
- official docs, external references, and code examples when local context is not enough
|
|
||||||
|
|
||||||
Tool preference:
|
|
||||||
- use `exa_search` for official docs, web references, release notes, or broader external research
|
|
||||||
- use `exa_code` for library usage patterns, OSS examples, and coding-specific prior art
|
|
||||||
- use local repo tools first when the answer is already likely in the current codebase
|
|
||||||
|
|
||||||
Output format:
|
|
||||||
|
|
||||||
## Relevant References
|
|
||||||
- `path/to/file.ts` - why it is relevant
|
|
||||||
|
|
||||||
## Reusable Patterns
|
|
||||||
Summarize useful conventions or implementation patterns.
|
|
||||||
|
|
||||||
## Related Tests / Docs
|
|
||||||
Point to tests, fixtures, docs, or configs that should be consulted.
|
|
||||||
|
|
||||||
## Recommendation
|
|
||||||
What other agent should do with these references.
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
---
|
|
||||||
name: planner
|
|
||||||
description: Turns requirements and findings into a concrete execution plan
|
|
||||||
tools: read, grep, find, ls
|
|
||||||
model: openai-codex/gpt-5.4:xhigh
|
|
||||||
parallelSafe: true
|
|
||||||
role: planner
|
|
||||||
tags: planning, design, execution
|
|
||||||
---
|
|
||||||
|
|
||||||
You are a planning specialist.
|
|
||||||
|
|
||||||
You receive requirements, codebase findings, or both. Produce a concrete implementation plan that a separate implementation agent can follow.
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
- Do not modify code.
|
|
||||||
- Do not invent files or architecture without stating that they are proposals.
|
|
||||||
- Keep steps small, ordered, and testable.
|
|
||||||
- Prefer minimal, low-risk changes over broad rewrites.
|
|
||||||
|
|
||||||
Output format:
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
One concise sentence.
|
|
||||||
|
|
||||||
## Plan
|
|
||||||
Numbered, execution-ready steps.
|
|
||||||
|
|
||||||
## Files to Touch
|
|
||||||
List likely files and the intended change in each.
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
List the checks, commands, or behavioral verification the implementer should run.
|
|
||||||
|
|
||||||
## Risks
|
|
||||||
Call out breakage risks, hidden dependencies, or edge cases.
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
---
|
|
||||||
name: reviewer
|
|
||||||
description: Reviews code and plans for correctness, regressions, and maintainability
|
|
||||||
tools: read, grep, find, ls
|
|
||||||
model: openai-codex/gpt-5.4:xhigh
|
|
||||||
parallelSafe: true
|
|
||||||
role: reviewer
|
|
||||||
tags: review, quality, safety
|
|
||||||
---
|
|
||||||
|
|
||||||
You are a reviewer.
|
|
||||||
|
|
||||||
Your job is to evaluate the delegated work for correctness, regression risk, maintainability, and missing validation.
|
|
||||||
|
|
||||||
Focus on:
|
|
||||||
- logic bugs
|
|
||||||
- edge cases
|
|
||||||
- mismatches between plan and implementation
|
|
||||||
- missing tests or validation
|
|
||||||
- unnecessary complexity
|
|
||||||
|
|
||||||
Output format:
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
Two or three sentences on overall quality.
|
|
||||||
|
|
||||||
## Must Fix
|
|
||||||
Critical issues that block merging.
|
|
||||||
|
|
||||||
## Should Fix
|
|
||||||
Important but non-blocking issues.
|
|
||||||
|
|
||||||
## Nice to Improve
|
|
||||||
Optional cleanups or simplifications.
|
|
||||||
|
|
||||||
## Validation Gaps
|
|
||||||
What should still be checked.
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
---
|
|
||||||
name: scout
|
|
||||||
description: Fast codebase recon for locating relevant files, symbols, and flows
|
|
||||||
tools: read, grep, find, ls
|
|
||||||
model: zai/glm-5.1
|
|
||||||
parallelSafe: true
|
|
||||||
role: scout
|
|
||||||
tags: search, recon, context
|
|
||||||
---
|
|
||||||
|
|
||||||
You are a scout.
|
|
||||||
|
|
||||||
Your job is to quickly investigate a codebase and return compressed, high-signal findings that another agent can use without re-reading everything from scratch.
|
|
||||||
|
|
||||||
You are optimized for:
|
|
||||||
- locating the right files
|
|
||||||
- tracing imports and call paths
|
|
||||||
- identifying key types, interfaces, and entrypoints
|
|
||||||
- narrowing the search space for planner/reviewer/implementer agents
|
|
||||||
|
|
||||||
Do not propose broad speculative rewrites. Stay concrete.
|
|
||||||
|
|
||||||
Output format:
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
Restate the delegated task in one or two lines.
|
|
||||||
|
|
||||||
## Key Files
|
|
||||||
List exact files and why they matter.
|
|
||||||
- `path/to/file.ts` - purpose
|
|
||||||
- `path/to/other.ts` - purpose
|
|
||||||
|
|
||||||
## Important Findings
|
|
||||||
Bullet the most relevant facts, APIs, constraints, and relationships.
|
|
||||||
|
|
||||||
## Suggested Next Read
|
|
||||||
Name the 1-3 files another agent should inspect first, and why.
|
|
||||||
|
|
||||||
## Open Questions
|
|
||||||
Any ambiguity or missing context that another agent should verify.
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,10 +0,0 @@
|
||||||
---
|
|
||||||
description: Implementer makes the change, reviewer critiques it, implementer applies the review
|
|
||||||
---
|
|
||||||
Use the `subagent` tool with `chain` mode for this workflow:
|
|
||||||
|
|
||||||
1. Run `implementer` to implement: $@
|
|
||||||
2. Run `reviewer` to review the implementation output using `{previous}`
|
|
||||||
3. Run `implementer` again to apply the review feedback using `{previous}`
|
|
||||||
|
|
||||||
Use a chain so each step receives the prior step's output via `{previous}`.
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
---
|
|
||||||
description: Scout gathers context, planner creates a plan, implementer applies it
|
|
||||||
---
|
|
||||||
Use the `subagent` tool with `chain` mode for this workflow:
|
|
||||||
|
|
||||||
1. Run `scout` to gather the most relevant code paths for: $@
|
|
||||||
2. Run `planner` to turn the findings into an implementation plan for: $@
|
|
||||||
3. Run `implementer` to execute the plan from the previous step using `{previous}`
|
|
||||||
|
|
||||||
Use a chain so each step receives the prior step's output via `{previous}`.
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
---
|
|
||||||
description: Run scout and librarian in parallel, then summarize their findings
|
|
||||||
---
|
|
||||||
Use the `subagent` tool with `tasks` mode to run these in parallel for: $@
|
|
||||||
|
|
||||||
- `scout`: identify the most relevant files, flows, and entrypoints
|
|
||||||
- `librarian`: find related patterns, prior art, tests, or docs
|
|
||||||
|
|
||||||
After both results return, synthesize them into one concise summary with recommended next steps.
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
---
|
|
||||||
description: Run local scout and external librarian research in parallel, then synthesize grounded guidance
|
|
||||||
---
|
|
||||||
Use the `subagent` tool with `tasks` mode to run these in parallel for: $@
|
|
||||||
|
|
||||||
- `scout`: inspect the current repository and identify the most relevant local files, entrypoints, constraints, and likely change surface
|
|
||||||
- `librarian`: gather external references and coding prior art, using `exa_search` for docs/web research and `exa_code` for implementation examples when useful
|
|
||||||
|
|
||||||
After both results return:
|
|
||||||
|
|
||||||
1. Synthesize them into one grounded response.
|
|
||||||
2. Clearly separate:
|
|
||||||
- local repository findings
|
|
||||||
- external references and docs
|
|
||||||
- recommended implementation patterns
|
|
||||||
3. Prefer official docs and high-signal sources over generic summaries.
|
|
||||||
4. Call out where external advice may not fit this repository's existing architecture.
|
|
||||||
5. Do **not** implement changes unless the user explicitly asks.
|
|
||||||
|
|
||||||
Return the final answer in this structure:
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
## Local Findings
|
|
||||||
## External References
|
|
||||||
## Recommended Pattern
|
|
||||||
## Risks / Caveats
|
|
||||||
## Next Steps
|
|
||||||
## Sources
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
---
|
|
||||||
description: Scout gathers context, planner turns it into a concrete plan without implementation
|
|
||||||
---
|
|
||||||
Use the `subagent` tool with `chain` mode for this workflow:
|
|
||||||
|
|
||||||
1. Run `scout` to gather the most relevant code paths for: $@
|
|
||||||
2. Run `planner` to create a concrete implementation plan for: $@ using `{previous}` as context
|
|
||||||
|
|
||||||
Return the plan only. Do not implement changes.
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue