@oh-my-pi/pi-coding-agent 1.337.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/CHANGELOG.md +1228 -0
- package/README.md +1041 -0
- package/docs/compaction.md +403 -0
- package/docs/custom-tools.md +541 -0
- package/docs/extension-loading.md +1004 -0
- package/docs/hooks.md +867 -0
- package/docs/rpc.md +1040 -0
- package/docs/sdk.md +994 -0
- package/docs/session-tree-plan.md +441 -0
- package/docs/session.md +240 -0
- package/docs/skills.md +290 -0
- package/docs/theme.md +637 -0
- package/docs/tree.md +197 -0
- package/docs/tui.md +341 -0
- package/examples/README.md +21 -0
- package/examples/custom-tools/README.md +124 -0
- package/examples/custom-tools/hello/index.ts +20 -0
- package/examples/custom-tools/question/index.ts +84 -0
- package/examples/custom-tools/subagent/README.md +172 -0
- package/examples/custom-tools/subagent/agents/planner.md +37 -0
- package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
- package/examples/custom-tools/subagent/agents/scout.md +50 -0
- package/examples/custom-tools/subagent/agents/worker.md +24 -0
- package/examples/custom-tools/subagent/agents.ts +156 -0
- package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
- package/examples/custom-tools/subagent/commands/implement.md +10 -0
- package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
- package/examples/custom-tools/subagent/index.ts +1002 -0
- package/examples/custom-tools/todo/index.ts +212 -0
- package/examples/hooks/README.md +56 -0
- package/examples/hooks/auto-commit-on-exit.ts +49 -0
- package/examples/hooks/confirm-destructive.ts +59 -0
- package/examples/hooks/custom-compaction.ts +116 -0
- package/examples/hooks/dirty-repo-guard.ts +52 -0
- package/examples/hooks/file-trigger.ts +41 -0
- package/examples/hooks/git-checkpoint.ts +53 -0
- package/examples/hooks/handoff.ts +150 -0
- package/examples/hooks/permission-gate.ts +34 -0
- package/examples/hooks/protected-paths.ts +30 -0
- package/examples/hooks/qna.ts +119 -0
- package/examples/hooks/snake.ts +343 -0
- package/examples/hooks/status-line.ts +40 -0
- package/examples/sdk/01-minimal.ts +22 -0
- package/examples/sdk/02-custom-model.ts +49 -0
- package/examples/sdk/03-custom-prompt.ts +44 -0
- package/examples/sdk/04-skills.ts +44 -0
- package/examples/sdk/05-tools.ts +90 -0
- package/examples/sdk/06-hooks.ts +61 -0
- package/examples/sdk/07-context-files.ts +36 -0
- package/examples/sdk/08-slash-commands.ts +42 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
- package/examples/sdk/10-settings.ts +38 -0
- package/examples/sdk/11-sessions.ts +48 -0
- package/examples/sdk/12-full-control.ts +95 -0
- package/examples/sdk/README.md +154 -0
- package/package.json +81 -0
- package/src/cli/args.ts +246 -0
- package/src/cli/file-processor.ts +72 -0
- package/src/cli/list-models.ts +104 -0
- package/src/cli/plugin-cli.ts +650 -0
- package/src/cli/session-picker.ts +41 -0
- package/src/cli.ts +10 -0
- package/src/commands/init.md +20 -0
- package/src/config.ts +159 -0
- package/src/core/agent-session.ts +1900 -0
- package/src/core/auth-storage.ts +236 -0
- package/src/core/bash-executor.ts +196 -0
- package/src/core/compaction/branch-summarization.ts +343 -0
- package/src/core/compaction/compaction.ts +742 -0
- package/src/core/compaction/index.ts +7 -0
- package/src/core/compaction/utils.ts +154 -0
- package/src/core/custom-tools/index.ts +21 -0
- package/src/core/custom-tools/loader.ts +248 -0
- package/src/core/custom-tools/types.ts +169 -0
- package/src/core/custom-tools/wrapper.ts +28 -0
- package/src/core/exec.ts +129 -0
- package/src/core/export-html/index.ts +211 -0
- package/src/core/export-html/template.css +781 -0
- package/src/core/export-html/template.html +54 -0
- package/src/core/export-html/template.js +1185 -0
- package/src/core/export-html/vendor/highlight.min.js +1213 -0
- package/src/core/export-html/vendor/marked.min.js +6 -0
- package/src/core/hooks/index.ts +16 -0
- package/src/core/hooks/loader.ts +312 -0
- package/src/core/hooks/runner.ts +434 -0
- package/src/core/hooks/tool-wrapper.ts +99 -0
- package/src/core/hooks/types.ts +773 -0
- package/src/core/index.ts +52 -0
- package/src/core/mcp/client.ts +158 -0
- package/src/core/mcp/config.ts +154 -0
- package/src/core/mcp/index.ts +45 -0
- package/src/core/mcp/loader.ts +68 -0
- package/src/core/mcp/manager.ts +181 -0
- package/src/core/mcp/tool-bridge.ts +148 -0
- package/src/core/mcp/transports/http.ts +316 -0
- package/src/core/mcp/transports/index.ts +6 -0
- package/src/core/mcp/transports/stdio.ts +252 -0
- package/src/core/mcp/types.ts +220 -0
- package/src/core/messages.ts +189 -0
- package/src/core/model-registry.ts +317 -0
- package/src/core/model-resolver.ts +393 -0
- package/src/core/plugins/doctor.ts +59 -0
- package/src/core/plugins/index.ts +38 -0
- package/src/core/plugins/installer.ts +189 -0
- package/src/core/plugins/loader.ts +338 -0
- package/src/core/plugins/manager.ts +672 -0
- package/src/core/plugins/parser.ts +105 -0
- package/src/core/plugins/paths.ts +32 -0
- package/src/core/plugins/types.ts +190 -0
- package/src/core/sdk.ts +760 -0
- package/src/core/session-manager.ts +1128 -0
- package/src/core/settings-manager.ts +443 -0
- package/src/core/skills.ts +437 -0
- package/src/core/slash-commands.ts +248 -0
- package/src/core/system-prompt.ts +439 -0
- package/src/core/timings.ts +25 -0
- package/src/core/tools/ask.ts +211 -0
- package/src/core/tools/bash-interceptor.ts +120 -0
- package/src/core/tools/bash.ts +250 -0
- package/src/core/tools/context.ts +32 -0
- package/src/core/tools/edit-diff.ts +475 -0
- package/src/core/tools/edit.ts +208 -0
- package/src/core/tools/exa/company.ts +59 -0
- package/src/core/tools/exa/index.ts +64 -0
- package/src/core/tools/exa/linkedin.ts +59 -0
- package/src/core/tools/exa/logger.ts +56 -0
- package/src/core/tools/exa/mcp-client.ts +368 -0
- package/src/core/tools/exa/render.ts +196 -0
- package/src/core/tools/exa/researcher.ts +90 -0
- package/src/core/tools/exa/search.ts +337 -0
- package/src/core/tools/exa/types.ts +168 -0
- package/src/core/tools/exa/websets.ts +248 -0
- package/src/core/tools/find.ts +261 -0
- package/src/core/tools/grep.ts +555 -0
- package/src/core/tools/index.ts +202 -0
- package/src/core/tools/ls.ts +140 -0
- package/src/core/tools/lsp/client.ts +605 -0
- package/src/core/tools/lsp/config.ts +147 -0
- package/src/core/tools/lsp/edits.ts +101 -0
- package/src/core/tools/lsp/index.ts +804 -0
- package/src/core/tools/lsp/render.ts +447 -0
- package/src/core/tools/lsp/rust-analyzer.ts +145 -0
- package/src/core/tools/lsp/types.ts +463 -0
- package/src/core/tools/lsp/utils.ts +486 -0
- package/src/core/tools/notebook.ts +229 -0
- package/src/core/tools/path-utils.ts +61 -0
- package/src/core/tools/read.ts +240 -0
- package/src/core/tools/renderers.ts +540 -0
- package/src/core/tools/task/agents.ts +153 -0
- package/src/core/tools/task/artifacts.ts +114 -0
- package/src/core/tools/task/bundled-agents/browser.md +71 -0
- package/src/core/tools/task/bundled-agents/explore.md +82 -0
- package/src/core/tools/task/bundled-agents/plan.md +54 -0
- package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
- package/src/core/tools/task/bundled-agents/task.md +53 -0
- package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
- package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
- package/src/core/tools/task/bundled-commands/implement.md +11 -0
- package/src/core/tools/task/commands.ts +213 -0
- package/src/core/tools/task/discovery.ts +208 -0
- package/src/core/tools/task/executor.ts +367 -0
- package/src/core/tools/task/index.ts +388 -0
- package/src/core/tools/task/model-resolver.ts +115 -0
- package/src/core/tools/task/parallel.ts +38 -0
- package/src/core/tools/task/render.ts +232 -0
- package/src/core/tools/task/types.ts +99 -0
- package/src/core/tools/truncate.ts +265 -0
- package/src/core/tools/web-fetch.ts +2370 -0
- package/src/core/tools/web-search/auth.ts +193 -0
- package/src/core/tools/web-search/index.ts +537 -0
- package/src/core/tools/web-search/providers/anthropic.ts +198 -0
- package/src/core/tools/web-search/providers/exa.ts +302 -0
- package/src/core/tools/web-search/providers/perplexity.ts +195 -0
- package/src/core/tools/web-search/render.ts +182 -0
- package/src/core/tools/web-search/types.ts +180 -0
- package/src/core/tools/write.ts +99 -0
- package/src/index.ts +176 -0
- package/src/main.ts +464 -0
- package/src/migrations.ts +135 -0
- package/src/modes/index.ts +43 -0
- package/src/modes/interactive/components/armin.ts +382 -0
- package/src/modes/interactive/components/assistant-message.ts +86 -0
- package/src/modes/interactive/components/bash-execution.ts +196 -0
- package/src/modes/interactive/components/bordered-loader.ts +41 -0
- package/src/modes/interactive/components/branch-summary-message.ts +42 -0
- package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
- package/src/modes/interactive/components/custom-editor.ts +122 -0
- package/src/modes/interactive/components/diff.ts +147 -0
- package/src/modes/interactive/components/dynamic-border.ts +25 -0
- package/src/modes/interactive/components/footer.ts +381 -0
- package/src/modes/interactive/components/hook-editor.ts +117 -0
- package/src/modes/interactive/components/hook-input.ts +64 -0
- package/src/modes/interactive/components/hook-message.ts +96 -0
- package/src/modes/interactive/components/hook-selector.ts +91 -0
- package/src/modes/interactive/components/model-selector.ts +247 -0
- package/src/modes/interactive/components/oauth-selector.ts +120 -0
- package/src/modes/interactive/components/plugin-settings.ts +479 -0
- package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
- package/src/modes/interactive/components/session-selector.ts +204 -0
- package/src/modes/interactive/components/settings-selector.ts +453 -0
- package/src/modes/interactive/components/show-images-selector.ts +45 -0
- package/src/modes/interactive/components/theme-selector.ts +62 -0
- package/src/modes/interactive/components/thinking-selector.ts +64 -0
- package/src/modes/interactive/components/tool-execution.ts +675 -0
- package/src/modes/interactive/components/tree-selector.ts +866 -0
- package/src/modes/interactive/components/user-message-selector.ts +159 -0
- package/src/modes/interactive/components/user-message.ts +18 -0
- package/src/modes/interactive/components/visual-truncate.ts +50 -0
- package/src/modes/interactive/components/welcome.ts +183 -0
- package/src/modes/interactive/interactive-mode.ts +2516 -0
- package/src/modes/interactive/theme/dark.json +101 -0
- package/src/modes/interactive/theme/light.json +98 -0
- package/src/modes/interactive/theme/theme-schema.json +308 -0
- package/src/modes/interactive/theme/theme.ts +998 -0
- package/src/modes/print-mode.ts +128 -0
- package/src/modes/rpc/rpc-client.ts +527 -0
- package/src/modes/rpc/rpc-mode.ts +483 -0
- package/src/modes/rpc/rpc-types.ts +203 -0
- package/src/utils/changelog.ts +99 -0
- package/src/utils/clipboard.ts +265 -0
- package/src/utils/fuzzy.ts +108 -0
- package/src/utils/mime.ts +30 -0
- package/src/utils/shell.ts +276 -0
- package/src/utils/tools-manager.ts +274 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent discovery from filesystem.
|
|
3
|
+
*
|
|
4
|
+
* Discovers agent definitions from:
|
|
5
|
+
* - ~/.pi/agent/agents/*.md (user-level, primary)
|
|
6
|
+
* - ~/.claude/agents/*.md (user-level, fallback)
|
|
7
|
+
* - .pi/agents/*.md (project-level, primary)
|
|
8
|
+
* - .claude/agents/*.md (project-level, fallback)
|
|
9
|
+
*
|
|
10
|
+
* Agent files use markdown with YAML frontmatter.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as os from "node:os";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
import { loadBundledAgents } from "./agents.js";
|
|
17
|
+
import type { AgentDefinition, AgentSource } from "./types.js";
|
|
18
|
+
|
|
19
|
+
/** Result of agent discovery */
|
|
20
|
+
export interface DiscoveryResult {
|
|
21
|
+
agents: AgentDefinition[];
|
|
22
|
+
projectAgentsDir: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse YAML frontmatter from markdown content.
|
|
27
|
+
*/
|
|
28
|
+
function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
|
|
29
|
+
const frontmatter: Record<string, string> = {};
|
|
30
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
31
|
+
|
|
32
|
+
if (!normalized.startsWith("---")) {
|
|
33
|
+
return { frontmatter, body: normalized };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const endIndex = normalized.indexOf("\n---", 3);
|
|
37
|
+
if (endIndex === -1) {
|
|
38
|
+
return { frontmatter, body: normalized };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const frontmatterBlock = normalized.slice(4, endIndex);
|
|
42
|
+
const body = normalized.slice(endIndex + 4).trim();
|
|
43
|
+
|
|
44
|
+
for (const line of frontmatterBlock.split("\n")) {
|
|
45
|
+
const match = line.match(/^([\w-]+):\s*(.*)$/);
|
|
46
|
+
if (match) {
|
|
47
|
+
let value = match[2].trim();
|
|
48
|
+
// Strip quotes
|
|
49
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
50
|
+
value = value.slice(1, -1);
|
|
51
|
+
}
|
|
52
|
+
frontmatter[match[1]] = value;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { frontmatter, body };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Load agents from a directory.
|
|
61
|
+
*/
|
|
62
|
+
function loadAgentsFromDir(dir: string, source: AgentSource): AgentDefinition[] {
|
|
63
|
+
const agents: AgentDefinition[] = [];
|
|
64
|
+
|
|
65
|
+
if (!fs.existsSync(dir)) {
|
|
66
|
+
return agents;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let entries: fs.Dirent[];
|
|
70
|
+
try {
|
|
71
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
72
|
+
} catch {
|
|
73
|
+
return agents;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
78
|
+
|
|
79
|
+
const filePath = path.join(dir, entry.name);
|
|
80
|
+
|
|
81
|
+
// Handle both regular files and symlinks
|
|
82
|
+
try {
|
|
83
|
+
if (!fs.statSync(filePath).isFile()) continue;
|
|
84
|
+
} catch {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let content: string;
|
|
89
|
+
try {
|
|
90
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
91
|
+
} catch {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
96
|
+
|
|
97
|
+
// Require name and description
|
|
98
|
+
if (!frontmatter.name || !frontmatter.description) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const tools = frontmatter.tools
|
|
103
|
+
?.split(",")
|
|
104
|
+
.map((t) => t.trim())
|
|
105
|
+
.filter(Boolean);
|
|
106
|
+
|
|
107
|
+
const recursive =
|
|
108
|
+
frontmatter.recursive === undefined
|
|
109
|
+
? undefined
|
|
110
|
+
: frontmatter.recursive === "true" || frontmatter.recursive === "1";
|
|
111
|
+
|
|
112
|
+
agents.push({
|
|
113
|
+
name: frontmatter.name,
|
|
114
|
+
description: frontmatter.description,
|
|
115
|
+
tools: tools && tools.length > 0 ? tools : undefined,
|
|
116
|
+
model: frontmatter.model,
|
|
117
|
+
recursive,
|
|
118
|
+
systemPrompt: body,
|
|
119
|
+
source,
|
|
120
|
+
filePath,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return agents;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check if path is a directory.
|
|
129
|
+
*/
|
|
130
|
+
function isDirectory(p: string): boolean {
|
|
131
|
+
try {
|
|
132
|
+
return fs.statSync(p).isDirectory();
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Find nearest directory by walking up from cwd.
|
|
140
|
+
*/
|
|
141
|
+
function findNearestDir(cwd: string, relPath: string): string | null {
|
|
142
|
+
let currentDir = cwd;
|
|
143
|
+
while (true) {
|
|
144
|
+
const candidate = path.join(currentDir, relPath);
|
|
145
|
+
if (isDirectory(candidate)) return candidate;
|
|
146
|
+
|
|
147
|
+
const parentDir = path.dirname(currentDir);
|
|
148
|
+
if (parentDir === currentDir) return null;
|
|
149
|
+
currentDir = parentDir;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Discover agents from filesystem and merge with bundled agents.
|
|
155
|
+
*
|
|
156
|
+
* Precedence (highest wins): project > user > bundled
|
|
157
|
+
* Within each level: .pi > .claude
|
|
158
|
+
*
|
|
159
|
+
* @param cwd - Current working directory for project agent discovery
|
|
160
|
+
*/
|
|
161
|
+
export function discoverAgents(cwd: string): DiscoveryResult {
|
|
162
|
+
// Primary directories (.pi)
|
|
163
|
+
const userPiDir = path.join(os.homedir(), ".pi", "agent", "agents");
|
|
164
|
+
const projectPiDir = findNearestDir(cwd, ".pi/agents");
|
|
165
|
+
|
|
166
|
+
// Fallback directories (.claude)
|
|
167
|
+
const userClaudeDir = path.join(os.homedir(), ".claude", "agents");
|
|
168
|
+
const projectClaudeDir = findNearestDir(cwd, ".claude/agents");
|
|
169
|
+
|
|
170
|
+
const agentMap = new Map<string, AgentDefinition>();
|
|
171
|
+
|
|
172
|
+
// 1. Bundled agents (lowest priority)
|
|
173
|
+
for (const agent of loadBundledAgents()) {
|
|
174
|
+
agentMap.set(agent.name, agent);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 2. User agents (.claude then .pi - .pi overrides .claude)
|
|
178
|
+
for (const agent of loadAgentsFromDir(userClaudeDir, "user")) {
|
|
179
|
+
agentMap.set(agent.name, agent);
|
|
180
|
+
}
|
|
181
|
+
for (const agent of loadAgentsFromDir(userPiDir, "user")) {
|
|
182
|
+
agentMap.set(agent.name, agent);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 3. Project agents (highest priority - .claude then .pi)
|
|
186
|
+
if (projectClaudeDir) {
|
|
187
|
+
for (const agent of loadAgentsFromDir(projectClaudeDir, "project")) {
|
|
188
|
+
agentMap.set(agent.name, agent);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (projectPiDir) {
|
|
192
|
+
for (const agent of loadAgentsFromDir(projectPiDir, "project")) {
|
|
193
|
+
agentMap.set(agent.name, agent);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
agents: Array.from(agentMap.values()),
|
|
199
|
+
projectAgentsDir: projectPiDir,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get an agent by name from discovered agents.
|
|
205
|
+
*/
|
|
206
|
+
export function getAgent(agents: AgentDefinition[], name: string): AgentDefinition | undefined {
|
|
207
|
+
return agents.find((a) => a.name === name);
|
|
208
|
+
}
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subprocess execution for subagents.
|
|
3
|
+
*
|
|
4
|
+
* Spawns `pi` in JSON mode to execute tasks with isolated context.
|
|
5
|
+
* Parses JSON events for progress tracking.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as os from "node:os";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import * as readline from "node:readline";
|
|
13
|
+
import { resolveModelPattern } from "./model-resolver.js";
|
|
14
|
+
import {
|
|
15
|
+
type AgentDefinition,
|
|
16
|
+
type AgentProgress,
|
|
17
|
+
MAX_OUTPUT_BYTES,
|
|
18
|
+
MAX_OUTPUT_LINES,
|
|
19
|
+
PI_NO_SUBAGENTS_ENV,
|
|
20
|
+
type SingleResult,
|
|
21
|
+
} from "./types.js";
|
|
22
|
+
|
|
23
|
+
/** pi command: 'pi.cmd' on Windows, 'pi' elsewhere */
|
|
24
|
+
const PI_CMD = process.platform === "win32" ? "pi.cmd" : "pi";
|
|
25
|
+
|
|
26
|
+
/** Windows shell option for spawn */
|
|
27
|
+
const PI_SHELL_OPT = process.platform === "win32";
|
|
28
|
+
|
|
29
|
+
/** Options for subprocess execution */
|
|
30
|
+
export interface ExecutorOptions {
|
|
31
|
+
cwd: string;
|
|
32
|
+
agent: AgentDefinition;
|
|
33
|
+
task: string;
|
|
34
|
+
index: number;
|
|
35
|
+
context?: string;
|
|
36
|
+
modelOverride?: string;
|
|
37
|
+
signal?: AbortSignal;
|
|
38
|
+
onProgress?: (progress: AgentProgress) => void;
|
|
39
|
+
sessionFile?: string | null;
|
|
40
|
+
persistArtifacts?: boolean;
|
|
41
|
+
artifactsDir?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Truncate output to byte and line limits.
|
|
46
|
+
*/
|
|
47
|
+
function truncateOutput(output: string): { text: string; truncated: boolean } {
|
|
48
|
+
let truncated = false;
|
|
49
|
+
let byteBudget = MAX_OUTPUT_BYTES;
|
|
50
|
+
let lineBudget = MAX_OUTPUT_LINES;
|
|
51
|
+
|
|
52
|
+
let i = 0;
|
|
53
|
+
let lastNewlineIndex = -1;
|
|
54
|
+
while (i < output.length && byteBudget > 0) {
|
|
55
|
+
const ch = output.charCodeAt(i);
|
|
56
|
+
byteBudget--;
|
|
57
|
+
|
|
58
|
+
if (ch === 10 /* \n */) {
|
|
59
|
+
lineBudget--;
|
|
60
|
+
lastNewlineIndex = i;
|
|
61
|
+
if (lineBudget <= 0) {
|
|
62
|
+
truncated = true;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
i++;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (i < output.length) {
|
|
71
|
+
truncated = true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (truncated && lineBudget <= 0 && lastNewlineIndex >= 0) {
|
|
75
|
+
output = output.slice(0, lastNewlineIndex);
|
|
76
|
+
} else {
|
|
77
|
+
output = output.slice(0, i);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { text: output, truncated };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Extract a short preview from tool args for display.
|
|
85
|
+
*/
|
|
86
|
+
function extractToolArgsPreview(args: Record<string, unknown>): string {
|
|
87
|
+
// Priority order for preview
|
|
88
|
+
const previewKeys = ["command", "file_path", "path", "pattern", "query", "url", "task", "prompt"];
|
|
89
|
+
|
|
90
|
+
for (const key of previewKeys) {
|
|
91
|
+
if (args[key] && typeof args[key] === "string") {
|
|
92
|
+
const value = args[key] as string;
|
|
93
|
+
return value.length > 60 ? `${value.slice(0, 57)}...` : value;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Run a single agent as a subprocess.
|
|
102
|
+
*/
|
|
103
|
+
export async function runSubprocess(options: ExecutorOptions): Promise<SingleResult> {
|
|
104
|
+
const { cwd, agent, task, index, context, modelOverride, signal, onProgress } = options;
|
|
105
|
+
const startTime = Date.now();
|
|
106
|
+
|
|
107
|
+
// Initialize progress
|
|
108
|
+
const progress: AgentProgress = {
|
|
109
|
+
index,
|
|
110
|
+
agent: agent.name,
|
|
111
|
+
agentSource: agent.source,
|
|
112
|
+
status: "running",
|
|
113
|
+
task,
|
|
114
|
+
recentTools: [],
|
|
115
|
+
recentOutput: [],
|
|
116
|
+
toolCount: 0,
|
|
117
|
+
tokens: 0,
|
|
118
|
+
durationMs: 0,
|
|
119
|
+
modelOverride,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Check if already aborted
|
|
123
|
+
if (signal?.aborted) {
|
|
124
|
+
return {
|
|
125
|
+
index,
|
|
126
|
+
agent: agent.name,
|
|
127
|
+
agentSource: agent.source,
|
|
128
|
+
task,
|
|
129
|
+
exitCode: 1,
|
|
130
|
+
output: "",
|
|
131
|
+
stderr: "Aborted before start",
|
|
132
|
+
truncated: false,
|
|
133
|
+
durationMs: 0,
|
|
134
|
+
tokens: 0,
|
|
135
|
+
modelOverride,
|
|
136
|
+
error: "Aborted",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Write system prompt to temp file
|
|
141
|
+
const tempDir = os.tmpdir();
|
|
142
|
+
const promptFile = path.join(
|
|
143
|
+
tempDir,
|
|
144
|
+
`pi-agent-${agent.name}-${Date.now()}-${Math.random().toString(36).slice(2)}.md`,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
fs.writeFileSync(promptFile, agent.systemPrompt, "utf-8");
|
|
149
|
+
} catch (err) {
|
|
150
|
+
return {
|
|
151
|
+
index,
|
|
152
|
+
agent: agent.name,
|
|
153
|
+
agentSource: agent.source,
|
|
154
|
+
task,
|
|
155
|
+
exitCode: 1,
|
|
156
|
+
output: "",
|
|
157
|
+
stderr: `Failed to write prompt file: ${err}`,
|
|
158
|
+
truncated: false,
|
|
159
|
+
durationMs: Date.now() - startTime,
|
|
160
|
+
tokens: 0,
|
|
161
|
+
modelOverride,
|
|
162
|
+
error: `Failed to write prompt file: ${err}`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Build full task with context
|
|
167
|
+
const fullTask = context ? `${context}\n\n${task}` : task;
|
|
168
|
+
|
|
169
|
+
// Build args
|
|
170
|
+
const args: string[] = ["--mode", "json", "--non-interactive"];
|
|
171
|
+
|
|
172
|
+
// Add system prompt
|
|
173
|
+
args.push("--append-system-prompt", promptFile);
|
|
174
|
+
|
|
175
|
+
// Add tools if specified
|
|
176
|
+
if (agent.tools && agent.tools.length > 0) {
|
|
177
|
+
args.push("--tools", agent.tools.join(","));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Resolve and add model
|
|
181
|
+
const resolvedModel = resolveModelPattern(modelOverride || agent.model);
|
|
182
|
+
if (resolvedModel) {
|
|
183
|
+
args.push("--model", resolvedModel);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Add session options
|
|
187
|
+
if (options.sessionFile) {
|
|
188
|
+
args.push("--session", options.sessionFile);
|
|
189
|
+
} else {
|
|
190
|
+
args.push("--no-session");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Add task as prompt
|
|
194
|
+
args.push("--prompt", fullTask);
|
|
195
|
+
|
|
196
|
+
// Set up environment
|
|
197
|
+
const env = { ...process.env };
|
|
198
|
+
if (!agent.recursive) {
|
|
199
|
+
env[PI_NO_SUBAGENTS_ENV] = "1";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Spawn subprocess
|
|
203
|
+
const proc = spawn(PI_CMD, args, {
|
|
204
|
+
cwd,
|
|
205
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
206
|
+
shell: PI_SHELL_OPT,
|
|
207
|
+
env,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
let output = "";
|
|
211
|
+
let stderr = "";
|
|
212
|
+
let finalOutput = "";
|
|
213
|
+
let resolved = false;
|
|
214
|
+
const jsonlEvents: string[] = [];
|
|
215
|
+
|
|
216
|
+
// Handle abort signal
|
|
217
|
+
const onAbort = () => {
|
|
218
|
+
if (!resolved) {
|
|
219
|
+
proc.kill("SIGTERM");
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
if (signal) {
|
|
223
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Parse JSON events from stdout
|
|
227
|
+
const rl = readline.createInterface({ input: proc.stdout! });
|
|
228
|
+
|
|
229
|
+
rl.on("line", (line) => {
|
|
230
|
+
if (resolved) return;
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const event = JSON.parse(line);
|
|
234
|
+
jsonlEvents.push(line);
|
|
235
|
+
const now = Date.now();
|
|
236
|
+
|
|
237
|
+
switch (event.type) {
|
|
238
|
+
case "tool_execution_start":
|
|
239
|
+
progress.toolCount++;
|
|
240
|
+
progress.currentTool = event.toolName;
|
|
241
|
+
progress.currentToolArgs = extractToolArgsPreview(event.toolArgs || event.args || {});
|
|
242
|
+
progress.currentToolStartMs = now;
|
|
243
|
+
break;
|
|
244
|
+
|
|
245
|
+
case "tool_execution_end":
|
|
246
|
+
if (progress.currentTool) {
|
|
247
|
+
progress.recentTools.unshift({
|
|
248
|
+
tool: progress.currentTool,
|
|
249
|
+
args: progress.currentToolArgs || "",
|
|
250
|
+
endMs: now,
|
|
251
|
+
});
|
|
252
|
+
// Keep only last 5
|
|
253
|
+
if (progress.recentTools.length > 5) {
|
|
254
|
+
progress.recentTools.pop();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
progress.currentTool = undefined;
|
|
258
|
+
progress.currentToolArgs = undefined;
|
|
259
|
+
progress.currentToolStartMs = undefined;
|
|
260
|
+
break;
|
|
261
|
+
|
|
262
|
+
case "message_update":
|
|
263
|
+
case "message_end": {
|
|
264
|
+
// Extract text content for recent output (prefer message.content, fallback to event.content)
|
|
265
|
+
const messageContent = event.message?.content || event.content;
|
|
266
|
+
if (messageContent && Array.isArray(messageContent)) {
|
|
267
|
+
for (const block of messageContent) {
|
|
268
|
+
if (block.type === "text" && block.text) {
|
|
269
|
+
const lines = block.text.split("\n").filter((l: string) => l.trim());
|
|
270
|
+
for (const l of lines) {
|
|
271
|
+
if (!progress.recentOutput.includes(l)) {
|
|
272
|
+
progress.recentOutput.unshift(l);
|
|
273
|
+
if (progress.recentOutput.length > 8) {
|
|
274
|
+
progress.recentOutput.pop();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
output += block.text;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// Extract usage (prefer message.usage, fallback to event.usage)
|
|
283
|
+
const messageUsage = event.message?.usage || event.usage;
|
|
284
|
+
if (messageUsage) {
|
|
285
|
+
progress.tokens = (messageUsage.input_tokens || 0) + (messageUsage.output_tokens || 0);
|
|
286
|
+
}
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
case "agent_end":
|
|
291
|
+
// Extract final content from messages array
|
|
292
|
+
if (event.messages && Array.isArray(event.messages)) {
|
|
293
|
+
for (const msg of event.messages) {
|
|
294
|
+
if (msg.content && Array.isArray(msg.content)) {
|
|
295
|
+
for (const block of msg.content) {
|
|
296
|
+
if (block.type === "text" && block.text) {
|
|
297
|
+
finalOutput += block.text;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
progress.durationMs = now - startTime;
|
|
307
|
+
onProgress?.(progress);
|
|
308
|
+
} catch {
|
|
309
|
+
// Ignore non-JSON lines
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Capture stderr
|
|
314
|
+
const stderrDecoder = new TextDecoder();
|
|
315
|
+
proc.stderr?.on("data", (chunk: Buffer) => {
|
|
316
|
+
stderr += stderrDecoder.decode(chunk, { stream: true });
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Wait for process to exit
|
|
320
|
+
const exitCode = await new Promise<number>((resolve) => {
|
|
321
|
+
proc.on("close", (code) => {
|
|
322
|
+
resolved = true;
|
|
323
|
+
resolve(code ?? 1);
|
|
324
|
+
});
|
|
325
|
+
proc.on("error", (err) => {
|
|
326
|
+
resolved = true;
|
|
327
|
+
stderr += `\nProcess error: ${err.message}`;
|
|
328
|
+
resolve(1);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Cleanup
|
|
333
|
+
if (signal) {
|
|
334
|
+
signal.removeEventListener("abort", onAbort);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
fs.unlinkSync(promptFile);
|
|
339
|
+
} catch {
|
|
340
|
+
// Ignore cleanup errors
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Use final output if available, otherwise accumulated output
|
|
344
|
+
const rawOutput = finalOutput || output;
|
|
345
|
+
const { text: truncatedOutput, truncated } = truncateOutput(rawOutput);
|
|
346
|
+
|
|
347
|
+
// Update final progress
|
|
348
|
+
progress.status = exitCode === 0 ? "completed" : "failed";
|
|
349
|
+
progress.durationMs = Date.now() - startTime;
|
|
350
|
+
onProgress?.(progress);
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
index,
|
|
354
|
+
agent: agent.name,
|
|
355
|
+
agentSource: agent.source,
|
|
356
|
+
task,
|
|
357
|
+
exitCode,
|
|
358
|
+
output: truncatedOutput,
|
|
359
|
+
stderr,
|
|
360
|
+
truncated,
|
|
361
|
+
durationMs: Date.now() - startTime,
|
|
362
|
+
tokens: progress.tokens,
|
|
363
|
+
modelOverride,
|
|
364
|
+
error: exitCode !== 0 && stderr ? stderr : undefined,
|
|
365
|
+
jsonlEvents,
|
|
366
|
+
};
|
|
367
|
+
}
|