@heyhuynhgiabuu/pi-task 0.1.5 → 0.2.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 +116 -4
- package/README.md +16 -11
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +4 -0
- package/dist/conversation.d.ts +76 -21
- package/dist/conversation.js +280 -70
- package/dist/helpers.d.ts +8 -8
- package/dist/helpers.js +34 -15
- package/dist/index.d.ts +6 -23
- package/dist/index.js +233 -634
- package/dist/lifecycle/completion.d.ts +3 -0
- package/dist/lifecycle/completion.js +50 -0
- package/dist/lifecycle/index.d.ts +5 -0
- package/dist/lifecycle/index.js +5 -0
- package/dist/lifecycle/polling.d.ts +16 -0
- package/dist/lifecycle/polling.js +61 -0
- package/dist/lifecycle/restore.d.ts +2 -0
- package/dist/lifecycle/restore.js +34 -0
- package/dist/lifecycle/toolStats.d.ts +2 -0
- package/dist/lifecycle/toolStats.js +17 -0
- package/dist/lifecycle/widget.d.ts +8 -0
- package/dist/lifecycle/widget.js +75 -0
- package/dist/session-text.d.ts +11 -2
- package/dist/session-text.js +78 -2
- package/dist/subagent/buildArgv.d.ts +1 -0
- package/dist/subagent/buildArgv.js +1 -1
- package/dist/subagent/runSdk.js +50 -26
- package/dist/subagent/tmux.d.ts +12 -9
- package/dist/subagent/tmux.js +107 -44
- package/dist/subagent/waitCompletion.d.ts +5 -5
- package/dist/subagent/waitCompletion.js +32 -41
- package/dist/task-widget.d.ts +21 -0
- package/dist/task-widget.js +122 -0
- package/dist/tool/index.d.ts +5 -0
- package/dist/tool/index.js +5 -0
- package/dist/tool/prompt.d.ts +8 -0
- package/dist/tool/prompt.js +17 -0
- package/dist/tool/renderCall.d.ts +3 -0
- package/dist/tool/renderCall.js +12 -0
- package/dist/tool/renderResult.d.ts +8 -0
- package/dist/tool/renderResult.js +51 -0
- package/dist/tool/schema.d.ts +8 -0
- package/dist/tool/schema.js +24 -0
- package/dist/tool/taskComplete.d.ts +8 -0
- package/dist/tool/taskComplete.js +65 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.js +1 -0
- package/package.json +1 -1
package/dist/conversation.js
CHANGED
|
@@ -1,19 +1,123 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Conversational subagent helpers.
|
|
5
|
+
*
|
|
6
|
+
* Per-task data lives in `.pi/artifacts/TASKS.md` as `### <task-id>` blocks.
|
|
7
|
+
* A small `task-sessions.json` registry in the same directory maps
|
|
8
|
+
* `conversation_id` to the auto-saved session file path so the
|
|
9
|
+
* subagent can be resumed later.
|
|
10
|
+
*
|
|
11
|
+
* The subagent's session is auto-saved by pi at
|
|
12
|
+
* `~/.pi/agent/sessions/<cwd>/<session-id>.jsonl`. pi-task does not
|
|
13
|
+
* maintain its own session storage.
|
|
14
|
+
*
|
|
15
|
+
* All artifacts live flat at the top of `.pi/artifacts/`, alongside the
|
|
16
|
+
* pikit canonical files (TODO.md, PLAN.md, PROGRESS.md, DECISIONS.md).
|
|
17
|
+
* No subdirs. No per-task paths.
|
|
18
|
+
*/
|
|
19
|
+
export const TASKS_FILE = "TASKS.md";
|
|
20
|
+
export const TASK_SESSIONS_REGISTRY_FILE = "task-sessions.json";
|
|
21
|
+
export function getTasksFilePath(piDir) {
|
|
22
|
+
return join(piDir, "artifacts", TASKS_FILE);
|
|
6
23
|
}
|
|
7
|
-
export function
|
|
8
|
-
return join(
|
|
24
|
+
export function getTaskSessionsRegistryPath(piDir) {
|
|
25
|
+
return join(piDir, "artifacts", TASK_SESSIONS_REGISTRY_FILE);
|
|
9
26
|
}
|
|
10
|
-
export function
|
|
11
|
-
|
|
27
|
+
export function readRegistry(piDir) {
|
|
28
|
+
const path = join(piDir, "task-registry.json");
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function writeRegistry(piDir, entries) {
|
|
37
|
+
const path = join(piDir, "task-registry.json");
|
|
38
|
+
writeFileSync(path, JSON.stringify(entries, null, 2), "utf-8");
|
|
39
|
+
}
|
|
40
|
+
export function readTaskSessionHistory(piDir) {
|
|
41
|
+
const path = join(piDir, "task-session-history.json");
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function writeTaskSessionHistory(piDir, entries) {
|
|
50
|
+
const path = join(piDir, "task-session-history.json");
|
|
51
|
+
writeFileSync(path, JSON.stringify(entries, null, 2), "utf-8");
|
|
52
|
+
}
|
|
53
|
+
export function upsertTaskSessionHistory(piDir, entry) {
|
|
54
|
+
const entries = readTaskSessionHistory(piDir);
|
|
55
|
+
const index = entries.findIndex((existing) => existing.id === entry.id);
|
|
56
|
+
if (index >= 0) {
|
|
57
|
+
entries[index] = { ...entries[index], ...entry };
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
entries.push(entry);
|
|
61
|
+
}
|
|
62
|
+
writeTaskSessionHistory(piDir, entries);
|
|
12
63
|
}
|
|
13
|
-
export function
|
|
14
|
-
return
|
|
15
|
-
|
|
16
|
-
|
|
64
|
+
export function findTaskSessionHistory(piDir, idOrSessionName) {
|
|
65
|
+
return readTaskSessionHistory(piDir).find((entry) => entry.id === idOrSessionName || entry.sessionName === idOrSessionName);
|
|
66
|
+
}
|
|
67
|
+
export function findJsonlSessionByName(piDir, sessionName, agentType) {
|
|
68
|
+
const artifactsDir = join(piDir, "artifacts");
|
|
69
|
+
const sessionDir = join(artifactsDir, "sessions");
|
|
70
|
+
try {
|
|
71
|
+
if (!existsSync(sessionDir))
|
|
72
|
+
return undefined;
|
|
73
|
+
const files = readdirSync(sessionDir)
|
|
74
|
+
.filter((file) => file.endsWith(".jsonl"))
|
|
75
|
+
.sort()
|
|
76
|
+
.reverse();
|
|
77
|
+
for (const file of files) {
|
|
78
|
+
const content = readFileSync(join(sessionDir, file), "utf-8");
|
|
79
|
+
let startedAt = Date.now();
|
|
80
|
+
for (const rawLine of content.split("\n")) {
|
|
81
|
+
const line = rawLine.trim();
|
|
82
|
+
if (!line)
|
|
83
|
+
continue;
|
|
84
|
+
try {
|
|
85
|
+
const entry = JSON.parse(line);
|
|
86
|
+
if (entry.type === "session" && entry.timestamp) {
|
|
87
|
+
const parsed = Date.parse(entry.timestamp);
|
|
88
|
+
if (Number.isFinite(parsed))
|
|
89
|
+
startedAt = parsed;
|
|
90
|
+
}
|
|
91
|
+
if (entry.type === "session_info") {
|
|
92
|
+
const name = entry.name ?? entry.session_info?.name;
|
|
93
|
+
if (name === sessionName) {
|
|
94
|
+
return {
|
|
95
|
+
id: sessionName,
|
|
96
|
+
agentType,
|
|
97
|
+
description: `Resumed session ${sessionName}`,
|
|
98
|
+
sessionName,
|
|
99
|
+
sessionRef: join(sessionDir, file),
|
|
100
|
+
startedAt,
|
|
101
|
+
piDir,
|
|
102
|
+
dir: artifactsDir,
|
|
103
|
+
conversationId: sessionName,
|
|
104
|
+
status: "done",
|
|
105
|
+
background: false,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Skip malformed lines.
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
17
121
|
}
|
|
18
122
|
export function normalizeConversationId(value) {
|
|
19
123
|
if (typeof value !== "string")
|
|
@@ -26,16 +130,21 @@ export function normalizeConversationId(value) {
|
|
|
26
130
|
}
|
|
27
131
|
return conversationId;
|
|
28
132
|
}
|
|
29
|
-
export function
|
|
133
|
+
export function readTaskSessionsRegistry(piDir) {
|
|
30
134
|
try {
|
|
31
|
-
const parsed = JSON.parse(readFileSync(
|
|
135
|
+
const parsed = JSON.parse(readFileSync(getTaskSessionsRegistryPath(piDir), "utf-8"));
|
|
32
136
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
33
137
|
return {};
|
|
34
138
|
}
|
|
35
139
|
const registry = {};
|
|
36
140
|
for (const [key, value] of Object.entries(parsed)) {
|
|
37
|
-
if (
|
|
38
|
-
|
|
141
|
+
if (value &&
|
|
142
|
+
typeof value === "object" &&
|
|
143
|
+
typeof value.task_id === "string" &&
|
|
144
|
+
typeof value.session_file === "string") {
|
|
145
|
+
const v = value;
|
|
146
|
+
registry[key] = { task_id: v.task_id, session_file: v.session_file };
|
|
147
|
+
}
|
|
39
148
|
}
|
|
40
149
|
return registry;
|
|
41
150
|
}
|
|
@@ -43,81 +152,182 @@ export function readConversationRegistry(piDir) {
|
|
|
43
152
|
return {};
|
|
44
153
|
}
|
|
45
154
|
}
|
|
46
|
-
export function
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
writeFileSync(getConversationRegistryPath(piDir), `${JSON.stringify(registry, null, 2)}\n`, "utf-8");
|
|
155
|
+
export function writeTaskSessionsRegistry(piDir, registry) {
|
|
156
|
+
mkdirSync(join(piDir, "artifacts"), { recursive: true });
|
|
157
|
+
writeFileSync(getTaskSessionsRegistryPath(piDir), `${JSON.stringify(registry, null, 2)}\n`, "utf-8");
|
|
50
158
|
}
|
|
51
|
-
|
|
159
|
+
/**
|
|
160
|
+
* Find a `### <task-id>` block in TASKS.md. Returns the block content
|
|
161
|
+
* (everything between the heading and the next H3 or EOF) plus the
|
|
162
|
+
* status line if present. Returns undefined if no block exists.
|
|
163
|
+
*/
|
|
164
|
+
export function readTaskBlock(piDir, taskId) {
|
|
165
|
+
let content;
|
|
52
166
|
try {
|
|
53
|
-
|
|
54
|
-
if (!parsed.conversation_id || !parsed.task_id)
|
|
55
|
-
return undefined;
|
|
56
|
-
return parsed;
|
|
167
|
+
content = readFileSync(getTasksFilePath(piDir), "utf-8");
|
|
57
168
|
}
|
|
58
169
|
catch {
|
|
59
170
|
return undefined;
|
|
60
171
|
}
|
|
172
|
+
return parseTaskBlocks(content).get(taskId);
|
|
61
173
|
}
|
|
62
|
-
export function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
""
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
174
|
+
export function listTaskBlocks(piDir) {
|
|
175
|
+
let content;
|
|
176
|
+
try {
|
|
177
|
+
content = readFileSync(getTasksFilePath(piDir), "utf-8");
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return new Map();
|
|
181
|
+
}
|
|
182
|
+
return parseTaskBlocks(content);
|
|
183
|
+
}
|
|
184
|
+
function parseTaskBlocks(content) {
|
|
185
|
+
const blocks = new Map();
|
|
186
|
+
const lines = content.split("\n");
|
|
187
|
+
let currentTaskId = null;
|
|
188
|
+
let currentStatus = null;
|
|
189
|
+
let currentBody = [];
|
|
190
|
+
const flush = () => {
|
|
191
|
+
if (currentTaskId !== null) {
|
|
192
|
+
blocks.set(currentTaskId, {
|
|
193
|
+
status: currentStatus,
|
|
194
|
+
body: currentBody.join("\n"),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
currentTaskId = null;
|
|
198
|
+
currentStatus = null;
|
|
199
|
+
currentBody = [];
|
|
200
|
+
};
|
|
201
|
+
for (const line of lines) {
|
|
202
|
+
const heading = line.match(/^###\s+(\S+)\s*$/);
|
|
203
|
+
if (heading) {
|
|
204
|
+
flush();
|
|
205
|
+
currentTaskId = heading[1];
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (currentTaskId === null)
|
|
209
|
+
continue;
|
|
210
|
+
const statusMatch = line.match(/^status:\s*(\S+)/);
|
|
211
|
+
if (statusMatch) {
|
|
212
|
+
currentStatus = statusMatch[1].toLowerCase();
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
currentBody.push(line);
|
|
216
|
+
}
|
|
217
|
+
flush();
|
|
218
|
+
return blocks;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Append or update a `### <task-id>` block in TASKS.md. If the block
|
|
222
|
+
* already exists, its body is replaced. Otherwise, the block is
|
|
223
|
+
* appended at the end of the file.
|
|
224
|
+
*/
|
|
225
|
+
export function writeTaskBlock(options) {
|
|
226
|
+
const path = getTasksFilePath(options.piDir);
|
|
227
|
+
let content = "";
|
|
228
|
+
try {
|
|
229
|
+
content = readFileSync(path, "utf-8");
|
|
230
|
+
if (!content.endsWith("\n"))
|
|
231
|
+
content += "\n";
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
content = "";
|
|
235
|
+
}
|
|
236
|
+
const heading = `### ${options.taskId}`;
|
|
237
|
+
const statusLine = `status: ${options.status} | updated: ${options.updated}`;
|
|
238
|
+
const block = `${heading}\n${statusLine}\n\n${options.body}\n`;
|
|
239
|
+
const headingRe = new RegExp(`^### ${escapeRegExp(options.taskId)}\\s*$`, "m");
|
|
240
|
+
const match = content.match(headingRe);
|
|
241
|
+
if (match && match.index !== undefined) {
|
|
242
|
+
const start = match.index;
|
|
243
|
+
const after = content.slice(start);
|
|
244
|
+
const nextHeading = after.search(/^###\s+\S+/m);
|
|
245
|
+
const end = nextHeading > 0 ? start + nextHeading : content.length;
|
|
246
|
+
content = content.slice(0, start) + block + content.slice(end);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
if (content.length > 0 && !content.endsWith("\n\n")) {
|
|
250
|
+
content += "\n";
|
|
251
|
+
}
|
|
252
|
+
content += block;
|
|
253
|
+
}
|
|
254
|
+
mkdirSync(join(options.piDir, "artifacts"), { recursive: true });
|
|
255
|
+
writeFileSync(path, content, "utf-8");
|
|
256
|
+
}
|
|
257
|
+
function escapeRegExp(value) {
|
|
258
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
259
|
+
}
|
|
260
|
+
export function parseMetadataFromBody(body) {
|
|
261
|
+
if (!body)
|
|
262
|
+
return undefined;
|
|
263
|
+
const match = body.match(/```json\n([\s\S]*?)\n```/);
|
|
264
|
+
if (!match)
|
|
265
|
+
return undefined;
|
|
266
|
+
try {
|
|
267
|
+
return JSON.parse(match[1]);
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Persist a completed task: write (or update) the `### <task-id>` block
|
|
275
|
+
* in TASKS.md with metadata and result as H4 subsections. Also updates
|
|
276
|
+
* the task-sessions registry.
|
|
277
|
+
*/
|
|
278
|
+
export function writeConversationArtifacts(input) {
|
|
279
|
+
const now = new Date().toISOString();
|
|
280
|
+
const existing = readTaskBlock(input.piDir, input.taskId);
|
|
281
|
+
const previous = parseMetadataFromBody(existing?.body);
|
|
282
|
+
const metadata = {
|
|
283
|
+
conversation_id: input.conversationId,
|
|
284
|
+
task_id: input.taskId,
|
|
285
|
+
agent_type: input.agentType,
|
|
286
|
+
session_file: input.sessionFile,
|
|
287
|
+
created_at: previous?.created_at ?? now,
|
|
288
|
+
last_used_at: now,
|
|
289
|
+
last_prompt: input.prompt,
|
|
290
|
+
};
|
|
291
|
+
const body = [
|
|
292
|
+
"#### Metadata",
|
|
72
293
|
"",
|
|
73
294
|
"```json",
|
|
74
|
-
JSON.stringify(
|
|
75
|
-
agent_type: metadata.agent_type,
|
|
76
|
-
conversation_id: metadata.conversation_id,
|
|
77
|
-
prompt: "Continue from the prior specialist conversation.",
|
|
78
|
-
}, null, 2),
|
|
295
|
+
JSON.stringify(metadata, null, 2),
|
|
79
296
|
"```",
|
|
80
297
|
"",
|
|
81
|
-
"
|
|
298
|
+
"#### Result",
|
|
82
299
|
"",
|
|
83
|
-
|
|
300
|
+
input.result.trim(),
|
|
84
301
|
"",
|
|
85
302
|
].join("\n");
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
session_name: options.sessionName,
|
|
98
|
-
created_at: existing?.created_at ?? now,
|
|
99
|
-
last_used_at: now,
|
|
100
|
-
last_prompt: options.prompt,
|
|
303
|
+
writeTaskBlock({
|
|
304
|
+
piDir: input.piDir,
|
|
305
|
+
taskId: input.taskId,
|
|
306
|
+
status: "done",
|
|
307
|
+
updated: now,
|
|
308
|
+
body,
|
|
309
|
+
});
|
|
310
|
+
const registry = readTaskSessionsRegistry(input.piDir);
|
|
311
|
+
registry[input.conversationId] = {
|
|
312
|
+
task_id: input.taskId,
|
|
313
|
+
session_file: input.sessionFile,
|
|
101
314
|
};
|
|
102
|
-
|
|
103
|
-
writeFileSync(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf-8");
|
|
104
|
-
writeFileSync(join(options.taskDir, "SESSION.md"), buildSessionCard(metadata), "utf-8");
|
|
315
|
+
writeTaskSessionsRegistry(input.piDir, registry);
|
|
105
316
|
return metadata;
|
|
106
317
|
}
|
|
107
318
|
export function renderConversationSessions(piDir) {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
if (entries.length === 0) {
|
|
319
|
+
const blocks = listTaskBlocks(piDir);
|
|
320
|
+
if (blocks.size === 0) {
|
|
111
321
|
return 'No durable task conversations found. Start one with task({ conversation_id: "research-ai", ... }).';
|
|
112
322
|
}
|
|
323
|
+
const entries = Array.from(blocks.entries()).sort(([a], [b]) => a.localeCompare(b));
|
|
113
324
|
const lines = ["Durable task conversations:"];
|
|
114
|
-
for (const [
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
lines.push(`${conversationId} -> ${taskArtifactName(taskId)}${suffix}`);
|
|
325
|
+
for (const [taskId, block] of entries) {
|
|
326
|
+
const metadata = parseMetadataFromBody(block.body);
|
|
327
|
+
const agent = metadata?.agent_type ?? "unknown";
|
|
328
|
+
const last = metadata?.last_used_at ?? "unknown";
|
|
329
|
+
const conv = metadata?.conversation_id ?? "(no conversation_id)";
|
|
330
|
+
lines.push(`${conv} -> ${taskId} — ${agent}, last used ${last}`);
|
|
121
331
|
}
|
|
122
332
|
return lines.join("\n");
|
|
123
333
|
}
|
package/dist/helpers.d.ts
CHANGED
|
@@ -40,8 +40,8 @@ export interface ToolCallRecord {
|
|
|
40
40
|
id: string;
|
|
41
41
|
}
|
|
42
42
|
export declare const TASK_BACKGROUND_DEFAULT = true;
|
|
43
|
-
export declare const
|
|
44
|
-
export declare const OUTPUT_FORMAT_GUIDE = "
|
|
43
|
+
export declare const TASK_PROMPT_INSTRUCTIONS = "Your final assistant message IS the result the parent agent will read.\n\nWhen you are done, end your final assistant message with a clear, self-contained summary in plain text. Do not wrap it in XML tags. Do not write a RESULT.md file \u2014 the parent agent reads your final assistant message from the session JSONL, not from any file.";
|
|
44
|
+
export declare const OUTPUT_FORMAT_GUIDE = "Your final assistant message IS the result the parent agent will read.\n\nWhen you are done, end your final assistant message with a clear, self-contained summary in plain text. Do not wrap it in XML tags. Do not write a RESULT.md file \u2014 the parent agent reads your final assistant message from the session JSONL, not from any file.";
|
|
45
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
46
|
/** @deprecated Import from ./agent-tools.js */
|
|
47
47
|
export { ALL_TOOL_NAMES, BUILTIN_TOOL_NAMES } from "./agent-tools.js";
|
|
@@ -72,12 +72,12 @@ export declare function formatAgentList(agents: AgentConfig[]): string;
|
|
|
72
72
|
* Build pi CLI arguments for spawning or resuming a sub-agent session.
|
|
73
73
|
*
|
|
74
74
|
* - Fresh spawn: omit `resume` or pass falsy — `--session` is not included.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
export declare function buildPiArgs(agent: AgentConfig, sessionName: string, sessionDir: string, promptContent: string, resume?: boolean, parentToolNames?: string[]): string[];
|
|
75
|
+
* - Resume: pass `resume=true` and optionally `resumeSessionRef` —
|
|
76
|
+
* `--session <ref>` is included so pi continues an existing session.
|
|
77
|
+
*/
|
|
78
|
+
export declare function buildPiArgs(agent: AgentConfig, sessionName: string, sessionDir: string, promptContent: string, resume?: boolean, parentToolNames?: string[], resumeSessionRef?: string): string[];
|
|
79
79
|
/** Count tool uses and turns from pi JSONL session files. */
|
|
80
|
-
export declare function countToolUses(sessionDir: string): {
|
|
80
|
+
export declare function countToolUses(sessionDir: string, sessionName?: string): {
|
|
81
81
|
toolUses: number;
|
|
82
82
|
turns: number;
|
|
83
83
|
};
|
|
@@ -94,7 +94,7 @@ export declare function summarizeArgs(toolName: string, args: unknown): string;
|
|
|
94
94
|
* Returns total counts plus the last `limit` records in chronological order.
|
|
95
95
|
* Safe against malformed lines and missing fields.
|
|
96
96
|
*/
|
|
97
|
-
export declare function readRecentToolCalls(sessionDir: string, limit?: number): {
|
|
97
|
+
export declare function readRecentToolCalls(sessionDir: string, limit?: number, sessionName?: string): {
|
|
98
98
|
toolUses: number;
|
|
99
99
|
turns: number;
|
|
100
100
|
recent: ToolCallRecord[];
|
package/dist/helpers.js
CHANGED
|
@@ -32,15 +32,10 @@ export function parseMarkdownFrontmatter(content) {
|
|
|
32
32
|
}
|
|
33
33
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
34
34
|
export const TASK_BACKGROUND_DEFAULT = true;
|
|
35
|
-
export const
|
|
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>
|
|
35
|
+
export const TASK_PROMPT_INSTRUCTIONS = `Your final assistant message IS the result the parent agent will read.
|
|
41
36
|
|
|
42
|
-
|
|
43
|
-
export const OUTPUT_FORMAT_GUIDE =
|
|
37
|
+
When you are done, end your final assistant message with a clear, self-contained summary in plain text. Do not wrap it in XML tags. Do not write a RESULT.md file — the parent agent reads your final assistant message from the session JSONL, not from any file.`;
|
|
38
|
+
export const OUTPUT_FORMAT_GUIDE = TASK_PROMPT_INSTRUCTIONS;
|
|
44
39
|
export const TASK_TOOL_DESCRIPTION = `Launch a new agent to handle complex, multistep tasks autonomously.
|
|
45
40
|
|
|
46
41
|
Include relevant context from your current work in the prompt parameter —
|
|
@@ -89,7 +84,7 @@ export function parseResultXml(raw) {
|
|
|
89
84
|
!extractTag(raw, EVIDENCE_RE)) {
|
|
90
85
|
return {
|
|
91
86
|
status: "unknown",
|
|
92
|
-
summary: raw.
|
|
87
|
+
summary: raw.trim(),
|
|
93
88
|
findings: "",
|
|
94
89
|
evidence: "",
|
|
95
90
|
confidence: "",
|
|
@@ -256,22 +251,42 @@ export function formatAgentList(agents) {
|
|
|
256
251
|
* Build pi CLI arguments for spawning or resuming a sub-agent session.
|
|
257
252
|
*
|
|
258
253
|
* - Fresh spawn: omit `resume` or pass falsy — `--session` is not included.
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
export function buildPiArgs(agent, sessionName, sessionDir, promptContent, resume, parentToolNames) {
|
|
254
|
+
* - Resume: pass `resume=true` and optionally `resumeSessionRef` —
|
|
255
|
+
* `--session <ref>` is included so pi continues an existing session.
|
|
256
|
+
*/
|
|
257
|
+
export function buildPiArgs(agent, sessionName, sessionDir, promptContent, resume, parentToolNames, resumeSessionRef) {
|
|
263
258
|
return buildPiArgv({
|
|
264
259
|
agent,
|
|
265
260
|
sessionName,
|
|
266
261
|
sessionDir,
|
|
267
262
|
promptContent,
|
|
268
263
|
resume,
|
|
264
|
+
resumeSessionRef,
|
|
269
265
|
parentToolNames,
|
|
270
266
|
});
|
|
271
267
|
}
|
|
272
268
|
// ─── JSONL Session Helpers ───────────────────────────────────────────────────
|
|
269
|
+
function matchesJsonlSessionName(content, sessionName) {
|
|
270
|
+
if (!sessionName)
|
|
271
|
+
return true;
|
|
272
|
+
for (const rawLine of content.split("\n")) {
|
|
273
|
+
const line = rawLine.trim();
|
|
274
|
+
if (!line)
|
|
275
|
+
continue;
|
|
276
|
+
try {
|
|
277
|
+
const entry = JSON.parse(line);
|
|
278
|
+
if (entry.type === "session_info") {
|
|
279
|
+
return (entry.name ?? entry.session_info?.name) === sessionName;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
// Skip malformed lines
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
273
288
|
/** Count tool uses and turns from pi JSONL session files. */
|
|
274
|
-
export function countToolUses(sessionDir) {
|
|
289
|
+
export function countToolUses(sessionDir, sessionName) {
|
|
275
290
|
let toolUses = 0;
|
|
276
291
|
let turns = 0;
|
|
277
292
|
try {
|
|
@@ -280,6 +295,8 @@ export function countToolUses(sessionDir) {
|
|
|
280
295
|
const files = readdirSync(sessionDir).filter((f) => f.endsWith(".jsonl"));
|
|
281
296
|
for (const file of files) {
|
|
282
297
|
const content = readFileSync(join(sessionDir, file), "utf-8");
|
|
298
|
+
if (!matchesJsonlSessionName(content, sessionName))
|
|
299
|
+
continue;
|
|
283
300
|
for (const rawLine of content.split("\n")) {
|
|
284
301
|
const line = rawLine.trim();
|
|
285
302
|
if (!line)
|
|
@@ -368,7 +385,7 @@ export function summarizeArgs(toolName, args) {
|
|
|
368
385
|
* Returns total counts plus the last `limit` records in chronological order.
|
|
369
386
|
* Safe against malformed lines and missing fields.
|
|
370
387
|
*/
|
|
371
|
-
export function readRecentToolCalls(sessionDir, limit = 12) {
|
|
388
|
+
export function readRecentToolCalls(sessionDir, limit = 12, sessionName) {
|
|
372
389
|
let toolUses = 0;
|
|
373
390
|
let turns = 0;
|
|
374
391
|
const calls = [];
|
|
@@ -379,6 +396,8 @@ export function readRecentToolCalls(sessionDir, limit = 12) {
|
|
|
379
396
|
const files = readdirSync(sessionDir).filter((f) => f.endsWith(".jsonl"));
|
|
380
397
|
for (const file of files) {
|
|
381
398
|
const content = readFileSync(join(sessionDir, file), "utf-8");
|
|
399
|
+
if (!matchesJsonlSessionName(content, sessionName))
|
|
400
|
+
continue;
|
|
382
401
|
for (const rawLine of content.split("\n")) {
|
|
383
402
|
const line = rawLine.trim();
|
|
384
403
|
if (!line)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,35 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Task Tool — Delegate complex work to specialist agents.
|
|
3
3
|
*
|
|
4
|
-
* Spawns pi CLI in a tmux split pane (
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Spawns pi CLI in a tmux split pane (foreground) or background.
|
|
5
|
+
* Completion is detected from the subagent's final assistant message
|
|
6
|
+
* in the persistent session JSONL (stopReason gating). The final message
|
|
7
|
+
* is the authoritative result; no RESULT.md is used.
|
|
7
8
|
*
|
|
8
9
|
* Three agent sources:
|
|
9
10
|
* - .pi/agents/*.md project-local agents
|
|
10
11
|
* - ~/.pi/agent/agents/*.md user-global agents (fallback)
|
|
11
12
|
*
|
|
12
13
|
* P0: Persistent task registry (appendEntry + JSON), --session resume,
|
|
13
|
-
* sendMessage completion notification.
|
|
14
|
-
* P1: Foreground mode (background:false
|
|
15
|
-
* detection, 30-minute timeout.
|
|
14
|
+
* sendMessage completion notification, Ctrl+O expand/collapse.
|
|
15
|
+
* P1: Foreground mode (background:false), pane death detection, timeout.
|
|
16
16
|
*/
|
|
17
17
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
|
-
export /** Details attached to tool result for rendering. */ interface TaskDetails {
|
|
19
|
-
task_id: string;
|
|
20
|
-
agent_type: string;
|
|
21
|
-
description: string;
|
|
22
|
-
conversation_id?: string;
|
|
23
|
-
phase: "done" | "timeout" | "aborted" | "failed";
|
|
24
|
-
status?: string;
|
|
25
|
-
summary?: string;
|
|
26
|
-
findings?: string;
|
|
27
|
-
evidence?: string;
|
|
28
|
-
confidence?: string;
|
|
29
|
-
duration_ms?: number;
|
|
30
|
-
turn_count?: number;
|
|
31
|
-
tool_uses?: number;
|
|
32
|
-
background?: boolean;
|
|
33
|
-
tmux_session?: string;
|
|
34
|
-
}
|
|
35
18
|
export default function (pi: ExtensionAPI): void;
|