@heyhuynhgiabuu/pi-task 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/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/agents/explore.md +61 -0
- package/agents/planner.md +65 -0
- package/agents/reviewer.md +69 -0
- package/agents/scout.md +67 -0
- package/agents/vision.md +65 -0
- package/agents/worker.md +60 -0
- package/dist/agent-tools.d.ts +38 -0
- package/dist/agent-tools.js +91 -0
- package/dist/helpers.d.ts +99 -0
- package/dist/helpers.js +433 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +969 -0
- package/dist/policy.d.ts +3 -0
- package/dist/policy.js +35 -0
- package/dist/session-text.d.ts +7 -0
- package/dist/session-text.js +50 -0
- package/dist/subagent/buildArgv.d.ts +13 -0
- package/dist/subagent/buildArgv.js +25 -0
- package/dist/subagent/runSdk.d.ts +17 -0
- package/dist/subagent/runSdk.js +63 -0
- package/dist/subagent/tmux.d.ts +14 -0
- package/dist/subagent/tmux.js +72 -0
- package/dist/subagent/waitCompletion.d.ts +17 -0
- package/dist/subagent/waitCompletion.js +66 -0
- package/media/demo.png +0 -0
- package/package.json +63 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Extension — Pure helper functions.
|
|
3
|
+
*
|
|
4
|
+
* No side effects, no ExtensionAPI dependency. All functions here are
|
|
5
|
+
* unit-testable with node:assert/strict.
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseMarkdownFrontmatter(content: string): {
|
|
8
|
+
frontmatter: Record<string, string>;
|
|
9
|
+
body: string;
|
|
10
|
+
};
|
|
11
|
+
export interface AgentConfig {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
model?: string;
|
|
15
|
+
thinking?: string;
|
|
16
|
+
/** Explicit allowlist from frontmatter `tools:` */
|
|
17
|
+
tools?: string | string[];
|
|
18
|
+
disallowedTools?: string[];
|
|
19
|
+
body: string;
|
|
20
|
+
source: "project" | "user" | "bundled";
|
|
21
|
+
path: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ParsedResult {
|
|
24
|
+
status: string;
|
|
25
|
+
summary: string;
|
|
26
|
+
findings: string;
|
|
27
|
+
evidence: string;
|
|
28
|
+
confidence: string;
|
|
29
|
+
raw: string;
|
|
30
|
+
}
|
|
31
|
+
/** A single tool call extracted from a subagent session JSONL. */
|
|
32
|
+
export interface ToolCallRecord {
|
|
33
|
+
/** Tool name (e.g. "websearch", "read", "bash") */
|
|
34
|
+
name: string;
|
|
35
|
+
/** Short, human-readable summary of the call's primary argument */
|
|
36
|
+
detail: string;
|
|
37
|
+
/** "done" if a matching toolResult was seen, "error" if isError, "in_progress" otherwise */
|
|
38
|
+
status: "done" | "error" | "in_progress";
|
|
39
|
+
/** Entry id of the toolCall block (used for stable sorting/debug) */
|
|
40
|
+
id: string;
|
|
41
|
+
}
|
|
42
|
+
export declare const TASK_BACKGROUND_DEFAULT = true;
|
|
43
|
+
export declare const TASK_RESULT_XML_INSTRUCTIONS = "<status>success|failure|blocked|partial</status>\n<summary>One sentence: what was accomplished</summary>\n<findings>Key findings with file:line references</findings>\n<evidence>Verification evidence, commands run, output snippets</evidence>\n<confidence>high|medium|low (optional \u2014 how certain the findings are)</confidence>\n<files>Comma-separated absolute paths of files read/created (optional)</files>\n\nPrefer writing this block to RESULT.md when done. If you cannot write the file, your final assistant message MUST include the same XML block.";
|
|
44
|
+
export declare const OUTPUT_FORMAT_GUIDE = "<status>success|failure|blocked|partial</status>\n<summary>One sentence: what was accomplished</summary>\n<findings>Key findings with file:line references</findings>\n<evidence>Verification evidence, commands run, output snippets</evidence>\n<confidence>high|medium|low (optional \u2014 how certain the findings are)</confidence>\n<files>Comma-separated absolute paths of files read/created (optional)</files>\n\nPrefer writing this block to RESULT.md when done. If you cannot write the file, your final assistant message MUST include the same XML block.";
|
|
45
|
+
export declare const TASK_TOOL_DESCRIPTION = "Launch a new agent to handle complex, multistep tasks autonomously.\n\nInclude relevant context from your current work in the prompt parameter \u2014\nthis becomes the subagent's instructions. The subagent knows nothing about what you've been doing except what you put in the prompt.\n\nWhen NOT to use:\n- To read a specific file path, use Read or Grep instead\n- To search for a class definition like 'class Foo', use Grep instead\n- To search code within 2-3 files, use Read instead\n- If no available agent fits the task, use other tools directly\n\nUsage notes:\n1. Provide complete context in the prompt \u2014 the subagent starts with a fresh context\n2. Launch multiple agents concurrently when possible (use a single message with multiple tool calls)\n3. Once you delegate work, do NOT duplicate it. Continue with non-overlapping tasks, or wait for the result\n4. Background is the default. Use background:false only when you need the caller to wait inline for the tmux task result\n5. Do not trust delegated output blindly. Read changed files, review the diff, verify scope, and run the relevant checks before claiming completion\n6. Clearly tell the agent whether to write code or just research, since it doesn't know the user's intent\n7. The result returned by the agent is not visible to the user. Send a concise summary back to the user\n8. Pass task_id to resume a previous subagent session (continues with its prior context)\n\nBackground mode (background: true):\n- Launches the subagent asynchronously and returns immediately\n- You will be notified automatically when it finishes\n- DO NOT sleep, poll, ask the task for status, or duplicate its work while it runs in background\n- Avoid working with the same files or topics the background task is using\n- Work on non-overlapping tasks, or briefly tell the user what you launched and end your response";
|
|
46
|
+
/** @deprecated Import from ./agent-tools.js */
|
|
47
|
+
export { ALL_TOOL_NAMES, BUILTIN_TOOL_NAMES } from "./agent-tools.js";
|
|
48
|
+
export declare function extractTag(raw: string, re: RegExp): string;
|
|
49
|
+
export declare function parseResultXml(raw: string): ParsedResult;
|
|
50
|
+
export declare function formatMs(ms: number): string;
|
|
51
|
+
export declare function parseIdTimestamp(id: string): number;
|
|
52
|
+
export declare function shellQuote(value: string): string;
|
|
53
|
+
export declare function buildTmuxSendKeysArgs(paneId: string, command: string): string[];
|
|
54
|
+
export interface BackgroundReceiptInput {
|
|
55
|
+
taskId: string;
|
|
56
|
+
agentType: string;
|
|
57
|
+
tmuxSession: string;
|
|
58
|
+
artifactDir: string;
|
|
59
|
+
}
|
|
60
|
+
export declare function formatBackgroundReceipt(input: BackgroundReceiptInput): string;
|
|
61
|
+
export declare function findPiDir(cwd: string): string | null;
|
|
62
|
+
export declare function getGlobalAgentDir(): string;
|
|
63
|
+
export declare function loadAgentsFromDir(dir: string, source: "project" | "user" | "bundled"): AgentConfig[];
|
|
64
|
+
export declare function discoverAgents(cwd: string, bundledAgentDir?: string): {
|
|
65
|
+
agents: AgentConfig[];
|
|
66
|
+
piDir: string;
|
|
67
|
+
};
|
|
68
|
+
export declare function formatAgentList(agents: AgentConfig[]): string;
|
|
69
|
+
/**
|
|
70
|
+
* Build pi CLI arguments for spawning or resuming a sub-agent session.
|
|
71
|
+
*
|
|
72
|
+
* - Fresh spawn: omit `resume` or pass falsy — `--session` is not included.
|
|
73
|
+
* - Resume: pass `resume=true` — `--session <name>` is included so pi
|
|
74
|
+
* continues the existing session file in --session-dir.
|
|
75
|
+
*/
|
|
76
|
+
export declare function buildPiArgs(agent: AgentConfig, sessionName: string, sessionDir: string, promptContent: string, resume?: boolean, parentToolNames?: string[]): string[];
|
|
77
|
+
/** Count tool uses and turns from pi JSONL session files. */
|
|
78
|
+
export declare function countToolUses(sessionDir: string): {
|
|
79
|
+
toolUses: number;
|
|
80
|
+
turns: number;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Extract a short, human-readable summary of a tool call's primary argument.
|
|
84
|
+
* Falls back to the first string-valued property for unknown tools.
|
|
85
|
+
*/
|
|
86
|
+
export declare function summarizeArgs(toolName: string, args: unknown): string;
|
|
87
|
+
/**
|
|
88
|
+
* Read the most recent tool calls from a pi JSONL session directory,
|
|
89
|
+
* with each call's status (done / error / in_progress) determined by
|
|
90
|
+
* whether a matching toolResult has been written.
|
|
91
|
+
*
|
|
92
|
+
* Returns total counts plus the last `limit` records in chronological order.
|
|
93
|
+
* Safe against malformed lines and missing fields.
|
|
94
|
+
*/
|
|
95
|
+
export declare function readRecentToolCalls(sessionDir: string, limit?: number): {
|
|
96
|
+
toolUses: number;
|
|
97
|
+
turns: number;
|
|
98
|
+
recent: ToolCallRecord[];
|
|
99
|
+
};
|
package/dist/helpers.js
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Extension — Pure helper functions.
|
|
3
|
+
*
|
|
4
|
+
* No side effects, no ExtensionAPI dependency. All functions here are
|
|
5
|
+
* unit-testable with node:assert/strict.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
8
|
+
import { join, dirname, basename, resolve } from "node:path";
|
|
9
|
+
import { parseToolList } from "./agent-tools.js";
|
|
10
|
+
import { parseMergedDisallowedTools } from "./policy.js";
|
|
11
|
+
import { buildPiArgv } from "./subagent/buildArgv.js";
|
|
12
|
+
export function parseMarkdownFrontmatter(content) {
|
|
13
|
+
if (!content.startsWith("---\n")) {
|
|
14
|
+
return { frontmatter: {}, body: content };
|
|
15
|
+
}
|
|
16
|
+
const end = content.indexOf("\n---", 4);
|
|
17
|
+
if (end === -1)
|
|
18
|
+
return { frontmatter: {}, body: content };
|
|
19
|
+
const raw = content.slice(4, end).trim();
|
|
20
|
+
const body = content.slice(end + "\n---".length).replace(/^\n/, "");
|
|
21
|
+
const frontmatter = {};
|
|
22
|
+
for (const line of raw.split("\n")) {
|
|
23
|
+
const idx = line.indexOf(":");
|
|
24
|
+
if (idx === -1)
|
|
25
|
+
continue;
|
|
26
|
+
const key = line.slice(0, idx).trim();
|
|
27
|
+
const value = line.slice(idx + 1).trim();
|
|
28
|
+
if (key)
|
|
29
|
+
frontmatter[key] = value;
|
|
30
|
+
}
|
|
31
|
+
return { frontmatter, body };
|
|
32
|
+
}
|
|
33
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
34
|
+
export const TASK_BACKGROUND_DEFAULT = true;
|
|
35
|
+
export const TASK_RESULT_XML_INSTRUCTIONS = `<status>success|failure|blocked|partial</status>
|
|
36
|
+
<summary>One sentence: what was accomplished</summary>
|
|
37
|
+
<findings>Key findings with file:line references</findings>
|
|
38
|
+
<evidence>Verification evidence, commands run, output snippets</evidence>
|
|
39
|
+
<confidence>high|medium|low (optional — how certain the findings are)</confidence>
|
|
40
|
+
<files>Comma-separated absolute paths of files read/created (optional)</files>
|
|
41
|
+
|
|
42
|
+
Prefer writing this block to RESULT.md when done. If you cannot write the file, your final assistant message MUST include the same XML block.`;
|
|
43
|
+
export const OUTPUT_FORMAT_GUIDE = TASK_RESULT_XML_INSTRUCTIONS;
|
|
44
|
+
export const TASK_TOOL_DESCRIPTION = `Launch a new agent to handle complex, multistep tasks autonomously.
|
|
45
|
+
|
|
46
|
+
Include relevant context from your current work in the prompt parameter —
|
|
47
|
+
this becomes the subagent's instructions. The subagent knows nothing about what you've been doing except what you put in the prompt.
|
|
48
|
+
|
|
49
|
+
When NOT to use:
|
|
50
|
+
- To read a specific file path, use Read or Grep instead
|
|
51
|
+
- To search for a class definition like 'class Foo', use Grep instead
|
|
52
|
+
- To search code within 2-3 files, use Read instead
|
|
53
|
+
- If no available agent fits the task, use other tools directly
|
|
54
|
+
|
|
55
|
+
Usage notes:
|
|
56
|
+
1. Provide complete context in the prompt — the subagent starts with a fresh context
|
|
57
|
+
2. Launch multiple agents concurrently when possible (use a single message with multiple tool calls)
|
|
58
|
+
3. Once you delegate work, do NOT duplicate it. Continue with non-overlapping tasks, or wait for the result
|
|
59
|
+
4. Background is the default. Use background:false only when you need the caller to wait inline for the tmux task result
|
|
60
|
+
5. Do not trust delegated output blindly. Read changed files, review the diff, verify scope, and run the relevant checks before claiming completion
|
|
61
|
+
6. Clearly tell the agent whether to write code or just research, since it doesn't know the user's intent
|
|
62
|
+
7. The result returned by the agent is not visible to the user. Send a concise summary back to the user
|
|
63
|
+
8. Pass task_id to resume a previous subagent session (continues with its prior context)
|
|
64
|
+
|
|
65
|
+
Background mode (background: true):
|
|
66
|
+
- Launches the subagent asynchronously and returns immediately
|
|
67
|
+
- You will be notified automatically when it finishes
|
|
68
|
+
- DO NOT sleep, poll, ask the task for status, or duplicate its work while it runs in background
|
|
69
|
+
- Avoid working with the same files or topics the background task is using
|
|
70
|
+
- Work on non-overlapping tasks, or briefly tell the user what you launched and end your response`;
|
|
71
|
+
/** @deprecated Import from ./agent-tools.js */
|
|
72
|
+
export { ALL_TOOL_NAMES, BUILTIN_TOOL_NAMES } from "./agent-tools.js";
|
|
73
|
+
// Cached regex patterns for XML result parsing
|
|
74
|
+
const STATUS_RE = /<status>([\s\S]*?)<\/status>/i;
|
|
75
|
+
const SUMMARY_RE = /<summary>([\s\S]*?)<\/summary>/i;
|
|
76
|
+
const FINDINGS_RE = /<findings>([\s\S]*?)<\/findings>/i;
|
|
77
|
+
const EVIDENCE_RE = /<evidence>([\s\S]*?)<\/evidence>/i;
|
|
78
|
+
const CONFIDENCE_RE = /<confidence>([\s\S]*?)<\/confidence>/i;
|
|
79
|
+
// ─── Result Parsing ──────────────────────────────────────────────────────────
|
|
80
|
+
export function extractTag(raw, re) {
|
|
81
|
+
const m = raw.match(re);
|
|
82
|
+
return m ? m[1].trim() : "";
|
|
83
|
+
}
|
|
84
|
+
export function parseResultXml(raw) {
|
|
85
|
+
const status = extractTag(raw, STATUS_RE);
|
|
86
|
+
if (!status &&
|
|
87
|
+
!extractTag(raw, SUMMARY_RE) &&
|
|
88
|
+
!extractTag(raw, FINDINGS_RE) &&
|
|
89
|
+
!extractTag(raw, EVIDENCE_RE)) {
|
|
90
|
+
return {
|
|
91
|
+
status: "unknown",
|
|
92
|
+
summary: raw.slice(0, 500),
|
|
93
|
+
findings: "",
|
|
94
|
+
evidence: "",
|
|
95
|
+
confidence: "",
|
|
96
|
+
raw,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const confidence = extractTag(raw, CONFIDENCE_RE);
|
|
100
|
+
return {
|
|
101
|
+
status: status || "unknown",
|
|
102
|
+
summary: extractTag(raw, SUMMARY_RE) || "",
|
|
103
|
+
findings: extractTag(raw, FINDINGS_RE) || "",
|
|
104
|
+
evidence: extractTag(raw, EVIDENCE_RE) || "",
|
|
105
|
+
confidence: confidence || "",
|
|
106
|
+
raw,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// ─── Formatting ──────────────────────────────────────────────────────────────
|
|
110
|
+
export function formatMs(ms) {
|
|
111
|
+
if (ms >= 60_000)
|
|
112
|
+
return `${Math.floor(ms / 60_000)}m ${Math.floor((ms % 60_000) / 1_000)}s`;
|
|
113
|
+
if (ms >= 1_000)
|
|
114
|
+
return `${(ms / 1_000).toFixed(1)}s`;
|
|
115
|
+
return `${ms}ms`;
|
|
116
|
+
}
|
|
117
|
+
export function parseIdTimestamp(id) {
|
|
118
|
+
try {
|
|
119
|
+
const ts36 = id.split("-")[0];
|
|
120
|
+
if (ts36)
|
|
121
|
+
return parseInt(ts36, 36);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
/* fall through */
|
|
125
|
+
}
|
|
126
|
+
return Date.now();
|
|
127
|
+
}
|
|
128
|
+
export function shellQuote(value) {
|
|
129
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
130
|
+
}
|
|
131
|
+
export function buildTmuxSendKeysArgs(paneId, command) {
|
|
132
|
+
return ["send-keys", "-t", paneId, command, "Enter"];
|
|
133
|
+
}
|
|
134
|
+
export function formatBackgroundReceipt(input) {
|
|
135
|
+
return [
|
|
136
|
+
`Started task ${input.taskId} with ${input.agentType}.`,
|
|
137
|
+
`Tmux session: ${input.tmuxSession}.`,
|
|
138
|
+
`Artifact directory: ${input.artifactDir}.`,
|
|
139
|
+
"A completion notification will arrive automatically; do not poll or duplicate this work.",
|
|
140
|
+
].join("\n");
|
|
141
|
+
}
|
|
142
|
+
// ─── Agent Discovery ─────────────────────────────────────────────────────────
|
|
143
|
+
export function findPiDir(cwd) {
|
|
144
|
+
let current = resolve(cwd);
|
|
145
|
+
while (true) {
|
|
146
|
+
if (basename(current) === ".pi") {
|
|
147
|
+
const parent = dirname(current);
|
|
148
|
+
if (parent === current)
|
|
149
|
+
return current;
|
|
150
|
+
current = parent;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (existsSync(join(current, ".pi")))
|
|
154
|
+
return join(current, ".pi");
|
|
155
|
+
const parent = dirname(current);
|
|
156
|
+
if (parent === current)
|
|
157
|
+
return null;
|
|
158
|
+
current = parent;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
export function getGlobalAgentDir() {
|
|
162
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
163
|
+
return join(home, ".pi", "agent", "agents");
|
|
164
|
+
}
|
|
165
|
+
export function loadAgentsFromDir(dir, source) {
|
|
166
|
+
const agents = [];
|
|
167
|
+
if (!existsSync(dir))
|
|
168
|
+
return agents;
|
|
169
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
170
|
+
if (!entry.name.endsWith(".md"))
|
|
171
|
+
continue;
|
|
172
|
+
if (!entry.isFile() && !entry.isSymbolicLink())
|
|
173
|
+
continue;
|
|
174
|
+
const filePath = join(dir, entry.name);
|
|
175
|
+
let content;
|
|
176
|
+
try {
|
|
177
|
+
content = readFileSync(filePath, "utf-8");
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const { frontmatter, body } = parseMarkdownFrontmatter(content);
|
|
183
|
+
if (!frontmatter.description)
|
|
184
|
+
continue;
|
|
185
|
+
const name = basename(entry.name, ".md");
|
|
186
|
+
const disallowedRaw = frontmatter.disallowed_tools;
|
|
187
|
+
const merged = parseMergedDisallowedTools(parseToolList(disallowedRaw).join(","));
|
|
188
|
+
const disallowedTools = merged.length > 0 ? merged : undefined;
|
|
189
|
+
const tools = parseToolList(frontmatter.tools);
|
|
190
|
+
agents.push({
|
|
191
|
+
name,
|
|
192
|
+
description: frontmatter.description,
|
|
193
|
+
model: frontmatter.model,
|
|
194
|
+
thinking: frontmatter.thinking,
|
|
195
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
196
|
+
disallowedTools,
|
|
197
|
+
body,
|
|
198
|
+
source,
|
|
199
|
+
path: filePath,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return agents;
|
|
203
|
+
}
|
|
204
|
+
export function discoverAgents(cwd, bundledAgentDir) {
|
|
205
|
+
const piDir = findPiDir(cwd) || join(cwd, ".pi");
|
|
206
|
+
const projectDir = join(piDir, "agents");
|
|
207
|
+
const userDir = getGlobalAgentDir();
|
|
208
|
+
const bundledAgents = bundledAgentDir
|
|
209
|
+
? loadAgentsFromDir(bundledAgentDir, "bundled")
|
|
210
|
+
: [];
|
|
211
|
+
const userAgents = loadAgentsFromDir(userDir, "user");
|
|
212
|
+
const projectAgents = loadAgentsFromDir(projectDir, "project");
|
|
213
|
+
// Override order: bundled < user < project.
|
|
214
|
+
const agentMap = new Map();
|
|
215
|
+
for (const a of bundledAgents)
|
|
216
|
+
agentMap.set(a.name, a);
|
|
217
|
+
for (const a of userAgents)
|
|
218
|
+
agentMap.set(a.name, a);
|
|
219
|
+
for (const a of projectAgents)
|
|
220
|
+
agentMap.set(a.name, a);
|
|
221
|
+
return {
|
|
222
|
+
agents: Array.from(agentMap.values()).sort((a, b) => a.name.localeCompare(b.name)),
|
|
223
|
+
piDir,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
export function formatAgentList(agents) {
|
|
227
|
+
if (agents.length === 0)
|
|
228
|
+
return "none available";
|
|
229
|
+
return agents
|
|
230
|
+
.map((a) => `${a.name} (${a.source}): ${a.description}`)
|
|
231
|
+
.join("\n");
|
|
232
|
+
}
|
|
233
|
+
// ─── Sub-agent CLI args ─────────────────────────────────────────────────────
|
|
234
|
+
/**
|
|
235
|
+
* Build pi CLI arguments for spawning or resuming a sub-agent session.
|
|
236
|
+
*
|
|
237
|
+
* - Fresh spawn: omit `resume` or pass falsy — `--session` is not included.
|
|
238
|
+
* - Resume: pass `resume=true` — `--session <name>` is included so pi
|
|
239
|
+
* continues the existing session file in --session-dir.
|
|
240
|
+
*/
|
|
241
|
+
export function buildPiArgs(agent, sessionName, sessionDir, promptContent, resume, parentToolNames) {
|
|
242
|
+
return buildPiArgv({
|
|
243
|
+
agent,
|
|
244
|
+
sessionName,
|
|
245
|
+
sessionDir,
|
|
246
|
+
promptContent,
|
|
247
|
+
resume,
|
|
248
|
+
parentToolNames,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
// ─── JSONL Session Helpers ───────────────────────────────────────────────────
|
|
252
|
+
/** Count tool uses and turns from pi JSONL session files. */
|
|
253
|
+
export function countToolUses(sessionDir) {
|
|
254
|
+
let toolUses = 0;
|
|
255
|
+
let turns = 0;
|
|
256
|
+
try {
|
|
257
|
+
if (!existsSync(sessionDir))
|
|
258
|
+
return { toolUses, turns };
|
|
259
|
+
const files = readdirSync(sessionDir).filter((f) => f.endsWith(".jsonl"));
|
|
260
|
+
for (const file of files) {
|
|
261
|
+
const content = readFileSync(join(sessionDir, file), "utf-8");
|
|
262
|
+
for (const rawLine of content.split("\n")) {
|
|
263
|
+
const line = rawLine.trim();
|
|
264
|
+
if (!line)
|
|
265
|
+
continue;
|
|
266
|
+
try {
|
|
267
|
+
const entry = JSON.parse(line);
|
|
268
|
+
if (entry.type === "message" &&
|
|
269
|
+
entry.message?.role === "assistant" &&
|
|
270
|
+
Array.isArray(entry.message.content)) {
|
|
271
|
+
turns++;
|
|
272
|
+
for (const block of entry.message.content) {
|
|
273
|
+
if (block.type === "toolCall")
|
|
274
|
+
toolUses++;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// Skip malformed lines
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
// Session dir might not exist or be inaccessible
|
|
286
|
+
}
|
|
287
|
+
return { toolUses, turns };
|
|
288
|
+
}
|
|
289
|
+
// ─── JSONL Session Helpers — streaming ───────────────────────────────────────
|
|
290
|
+
/**
|
|
291
|
+
* Extract a short, human-readable summary of a tool call's primary argument.
|
|
292
|
+
* Falls back to the first string-valued property for unknown tools.
|
|
293
|
+
*/
|
|
294
|
+
export function summarizeArgs(toolName, args) {
|
|
295
|
+
if (!args || typeof args !== "object")
|
|
296
|
+
return "";
|
|
297
|
+
const a = args;
|
|
298
|
+
const pick = (...keys) => {
|
|
299
|
+
for (const k of keys) {
|
|
300
|
+
const v = a[k];
|
|
301
|
+
if (typeof v === "string" && v.length > 0)
|
|
302
|
+
return v;
|
|
303
|
+
}
|
|
304
|
+
return "";
|
|
305
|
+
};
|
|
306
|
+
switch (toolName) {
|
|
307
|
+
case "read":
|
|
308
|
+
case "write":
|
|
309
|
+
case "edit":
|
|
310
|
+
case "ls":
|
|
311
|
+
return pick("path", "file_path");
|
|
312
|
+
case "bash":
|
|
313
|
+
return pick("command", "cmd");
|
|
314
|
+
case "grep":
|
|
315
|
+
case "codesearch":
|
|
316
|
+
case "websearch":
|
|
317
|
+
return pick("query", "pattern", "search_term", "glob");
|
|
318
|
+
case "web_fetch":
|
|
319
|
+
case "webclaw_scrape":
|
|
320
|
+
case "lightpanda_markdown":
|
|
321
|
+
case "lightpanda_links":
|
|
322
|
+
case "lightpanda_structuredData":
|
|
323
|
+
return pick("url");
|
|
324
|
+
case "webclaw_batch":
|
|
325
|
+
return Array.isArray(a.urls) ? `${a.urls.length} urls` : pick("urls");
|
|
326
|
+
case "context7":
|
|
327
|
+
return pick("libraryId", "topic", "libraryName");
|
|
328
|
+
case "deepwiki":
|
|
329
|
+
return pick("question", "repo");
|
|
330
|
+
case "find":
|
|
331
|
+
return pick("pattern", "glob");
|
|
332
|
+
default: {
|
|
333
|
+
// Fallback: first non-empty string property
|
|
334
|
+
for (const v of Object.values(a)) {
|
|
335
|
+
if (typeof v === "string" && v.length > 0)
|
|
336
|
+
return v;
|
|
337
|
+
}
|
|
338
|
+
return "";
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Read the most recent tool calls from a pi JSONL session directory,
|
|
344
|
+
* with each call's status (done / error / in_progress) determined by
|
|
345
|
+
* whether a matching toolResult has been written.
|
|
346
|
+
*
|
|
347
|
+
* Returns total counts plus the last `limit` records in chronological order.
|
|
348
|
+
* Safe against malformed lines and missing fields.
|
|
349
|
+
*/
|
|
350
|
+
export function readRecentToolCalls(sessionDir, limit = 12) {
|
|
351
|
+
let toolUses = 0;
|
|
352
|
+
let turns = 0;
|
|
353
|
+
const calls = [];
|
|
354
|
+
const resultsById = new Map();
|
|
355
|
+
try {
|
|
356
|
+
if (!existsSync(sessionDir))
|
|
357
|
+
return { toolUses, turns, recent: [] };
|
|
358
|
+
const files = readdirSync(sessionDir).filter((f) => f.endsWith(".jsonl"));
|
|
359
|
+
for (const file of files) {
|
|
360
|
+
const content = readFileSync(join(sessionDir, file), "utf-8");
|
|
361
|
+
for (const rawLine of content.split("\n")) {
|
|
362
|
+
const line = rawLine.trim();
|
|
363
|
+
if (!line)
|
|
364
|
+
continue;
|
|
365
|
+
let entry;
|
|
366
|
+
try {
|
|
367
|
+
entry = JSON.parse(line);
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const msg = entry?.message;
|
|
373
|
+
if (!msg || typeof msg !== "object")
|
|
374
|
+
continue;
|
|
375
|
+
// Collect tool results first so we can match them to tool calls
|
|
376
|
+
if (msg.role === "toolResult") {
|
|
377
|
+
const ts = typeof msg.timestamp === "number"
|
|
378
|
+
? msg.timestamp
|
|
379
|
+
: Date.parse(entry?.timestamp ?? "") || 0;
|
|
380
|
+
if (typeof msg.toolCallId === "string") {
|
|
381
|
+
resultsById.set(msg.toolCallId, {
|
|
382
|
+
isError: Boolean(msg.isError),
|
|
383
|
+
ts,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (msg.role !== "assistant" || !Array.isArray(msg.content))
|
|
389
|
+
continue;
|
|
390
|
+
turns++;
|
|
391
|
+
for (const block of msg.content) {
|
|
392
|
+
if (!block || block.type !== "toolCall")
|
|
393
|
+
continue;
|
|
394
|
+
toolUses++;
|
|
395
|
+
const id = typeof block.id === "string" ? block.id : "";
|
|
396
|
+
if (!id)
|
|
397
|
+
continue; // can't match results without an id
|
|
398
|
+
calls.push({
|
|
399
|
+
name: typeof block.name === "string" ? block.name : "tool",
|
|
400
|
+
detail: summarizeArgs(typeof block.name === "string" ? block.name : "", block.arguments),
|
|
401
|
+
id,
|
|
402
|
+
ts: typeof msg.timestamp === "number"
|
|
403
|
+
? msg.timestamp
|
|
404
|
+
: Date.parse(entry?.timestamp ?? "") || 0,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
return { toolUses, turns, recent: [] };
|
|
412
|
+
}
|
|
413
|
+
// Determine status for each call, then take the last `limit` in order
|
|
414
|
+
const ordered = calls.slice().sort((a, b) => a.ts - b.ts);
|
|
415
|
+
const all = ordered.map((c) => {
|
|
416
|
+
const r = resultsById.get(c.id);
|
|
417
|
+
if (!r)
|
|
418
|
+
return {
|
|
419
|
+
name: c.name,
|
|
420
|
+
detail: c.detail,
|
|
421
|
+
id: c.id,
|
|
422
|
+
status: "in_progress",
|
|
423
|
+
};
|
|
424
|
+
return {
|
|
425
|
+
name: c.name,
|
|
426
|
+
detail: c.detail,
|
|
427
|
+
id: c.id,
|
|
428
|
+
status: r.isError ? "error" : "done",
|
|
429
|
+
};
|
|
430
|
+
});
|
|
431
|
+
const recent = all.slice(Math.max(0, all.length - limit));
|
|
432
|
+
return { toolUses, turns, recent };
|
|
433
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Tool — Delegate complex work to specialist agents.
|
|
3
|
+
*
|
|
4
|
+
* Spawns pi CLI in a tmux split pane (so you can watch it live) and
|
|
5
|
+
* detects completion via RESULT.md polling. On completion, tool call
|
|
6
|
+
* count and duration are reported as a notification.
|
|
7
|
+
*
|
|
8
|
+
* Three agent sources:
|
|
9
|
+
* - .pi/agents/*.md project-local agents
|
|
10
|
+
* - ~/.pi/agent/agents/*.md user-global agents (fallback)
|
|
11
|
+
*
|
|
12
|
+
* P0: Persistent task registry (appendEntry + JSON), --session resume,
|
|
13
|
+
* sendMessage completion notification.
|
|
14
|
+
* P1: Foreground mode (background:false, inline subprocess), pane death
|
|
15
|
+
* detection, 30-minute timeout.
|
|
16
|
+
*/
|
|
17
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
export default function (pi: ExtensionAPI): void;
|