@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +116 -4
  2. package/README.md +16 -11
  3. package/dist/constants.d.ts +4 -0
  4. package/dist/constants.js +4 -0
  5. package/dist/conversation.d.ts +76 -21
  6. package/dist/conversation.js +280 -70
  7. package/dist/helpers.d.ts +8 -8
  8. package/dist/helpers.js +34 -15
  9. package/dist/index.d.ts +6 -23
  10. package/dist/index.js +233 -634
  11. package/dist/lifecycle/completion.d.ts +3 -0
  12. package/dist/lifecycle/completion.js +50 -0
  13. package/dist/lifecycle/index.d.ts +5 -0
  14. package/dist/lifecycle/index.js +5 -0
  15. package/dist/lifecycle/polling.d.ts +16 -0
  16. package/dist/lifecycle/polling.js +61 -0
  17. package/dist/lifecycle/restore.d.ts +2 -0
  18. package/dist/lifecycle/restore.js +34 -0
  19. package/dist/lifecycle/toolStats.d.ts +2 -0
  20. package/dist/lifecycle/toolStats.js +17 -0
  21. package/dist/lifecycle/widget.d.ts +8 -0
  22. package/dist/lifecycle/widget.js +75 -0
  23. package/dist/session-text.d.ts +11 -2
  24. package/dist/session-text.js +78 -2
  25. package/dist/subagent/buildArgv.d.ts +1 -0
  26. package/dist/subagent/buildArgv.js +1 -1
  27. package/dist/subagent/runSdk.js +50 -26
  28. package/dist/subagent/tmux.d.ts +12 -9
  29. package/dist/subagent/tmux.js +107 -44
  30. package/dist/subagent/waitCompletion.d.ts +5 -5
  31. package/dist/subagent/waitCompletion.js +32 -41
  32. package/dist/task-widget.d.ts +21 -0
  33. package/dist/task-widget.js +122 -0
  34. package/dist/tool/index.d.ts +5 -0
  35. package/dist/tool/index.js +5 -0
  36. package/dist/tool/prompt.d.ts +8 -0
  37. package/dist/tool/prompt.js +17 -0
  38. package/dist/tool/renderCall.d.ts +3 -0
  39. package/dist/tool/renderCall.js +12 -0
  40. package/dist/tool/renderResult.d.ts +8 -0
  41. package/dist/tool/renderResult.js +51 -0
  42. package/dist/tool/schema.d.ts +8 -0
  43. package/dist/tool/schema.js +24 -0
  44. package/dist/tool/taskComplete.d.ts +8 -0
  45. package/dist/tool/taskComplete.js +65 -0
  46. package/dist/types.d.ts +54 -0
  47. package/dist/types.js +1 -0
  48. package/package.json +1 -1
@@ -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
- export const CONVERSATION_REGISTRY_FILE = "task-conversations.json";
4
- export function getArtifactsDir(piDir) {
5
- return join(piDir, "artifacts");
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 getConversationRegistryPath(piDir) {
8
- return join(getArtifactsDir(piDir), CONVERSATION_REGISTRY_FILE);
24
+ export function getTaskSessionsRegistryPath(piDir) {
25
+ return join(piDir, "artifacts", TASK_SESSIONS_REGISTRY_FILE);
9
26
  }
10
- export function taskArtifactName(taskId) {
11
- return taskId.startsWith("task-") ? taskId : `task-${taskId}`;
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 taskIdFromArtifactName(artifactName) {
14
- return artifactName.startsWith("task-")
15
- ? artifactName.slice("task-".length)
16
- : artifactName;
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 readConversationRegistry(piDir) {
133
+ export function readTaskSessionsRegistry(piDir) {
30
134
  try {
31
- const parsed = JSON.parse(readFileSync(getConversationRegistryPath(piDir), "utf-8"));
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 (typeof value === "string")
38
- registry[key] = value;
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 writeConversationRegistry(piDir, registry) {
47
- const artifactsDir = getArtifactsDir(piDir);
48
- mkdirSync(artifactsDir, { recursive: true });
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
- export function readConversationMetadata(metadataPath) {
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
- const parsed = JSON.parse(readFileSync(metadataPath, "utf-8"));
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 buildSessionCard(metadata) {
63
- return [
64
- `# ${metadata.conversation_id}`,
65
- "",
66
- `Agent: ${metadata.agent_type}`,
67
- `Task: ${taskArtifactName(metadata.task_id)}`,
68
- `Last used: ${metadata.last_used_at}`,
69
- `Session dir: ${metadata.session_dir}`,
70
- "",
71
- "## Resume",
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
- "## Last prompt",
298
+ "#### Result",
82
299
  "",
83
- metadata.last_prompt ?? "",
300
+ input.result.trim(),
84
301
  "",
85
302
  ].join("\n");
86
- }
87
- export function writeConversationArtifacts(options) {
88
- const now = new Date().toISOString();
89
- const metadataPath = join(options.taskDir, "metadata.json");
90
- const existing = readConversationMetadata(metadataPath);
91
- const metadata = {
92
- conversation_id: options.conversationId,
93
- task_id: options.taskId,
94
- artifact: taskArtifactName(options.taskId),
95
- agent_type: options.agentType,
96
- session_dir: options.sessionDir,
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
- mkdirSync(options.taskDir, { recursive: true });
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 registry = readConversationRegistry(piDir);
109
- const entries = Object.entries(registry).sort(([left], [right]) => left.localeCompare(right));
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 [conversationId, artifactName] of entries) {
115
- const taskId = taskIdFromArtifactName(artifactName);
116
- const metadata = readConversationMetadata(join(getArtifactsDir(piDir), taskArtifactName(taskId), "metadata.json"));
117
- const suffix = metadata
118
- ? ` ${metadata.agent_type}, last used ${metadata.last_used_at}`
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 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.";
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
- * - Resume: pass `resume=true` `--session <name>` is included so pi
76
- * continues the existing session file in --session-dir.
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 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>
35
+ export const TASK_PROMPT_INSTRUCTIONS = `Your final assistant message IS the result the parent agent will read.
41
36
 
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;
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.slice(0, 500),
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
- * - Resume: pass `resume=true` `--session <name>` is included so pi
260
- * continues the existing session file in --session-dir.
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 (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.
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, inline subprocess), pane death
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;