@heyhuynhgiabuu/pi-task 0.1.4 → 0.1.6
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 +52 -4
- package/README.md +33 -1
- package/dist/conversation.d.ts +86 -0
- package/dist/conversation.js +238 -0
- package/dist/helpers.d.ts +6 -6
- package/dist/helpers.js +30 -6
- package/dist/index.d.ts +17 -0
- package/dist/index.js +427 -175
- package/dist/session-text.d.ts +2 -2
- package/dist/session-text.js +28 -2
- package/dist/subagent/buildArgv.d.ts +1 -0
- package/dist/subagent/buildArgv.js +1 -1
- package/dist/subagent/waitCompletion.d.ts +1 -0
- package/dist/subagent/waitCompletion.js +27 -20
- package/dist/task-widget.d.ts +21 -0
- package/dist/task-widget.js +122 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14,24 +14,26 @@
|
|
|
14
14
|
* P1: Foreground mode (background:false, inline subprocess), pane death
|
|
15
15
|
* detection, 30-minute timeout.
|
|
16
16
|
*/
|
|
17
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
18
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
17
|
import { execFileSync } from "node:child_process";
|
|
20
18
|
import { randomUUID } from "node:crypto";
|
|
19
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { mkdir } from "node:fs/promises";
|
|
21
21
|
import { dirname, join } from "node:path";
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
23
23
|
import { Type } from "@sinclair/typebox";
|
|
24
24
|
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
25
|
+
import { buildAgentToolSelection } from "./agent-tools.js";
|
|
26
|
+
import { normalizeConversationId, parseMetadataFromBody, readTaskBlock, readTaskSessionsRegistry, renderConversationSessions, writeConversationArtifacts, writeTaskSessionsRegistry, } from "./conversation.js";
|
|
25
27
|
import { TASK_BACKGROUND_DEFAULT, TASK_RESULT_XML_INSTRUCTIONS, TASK_TOOL_DESCRIPTION, buildTmuxSplitWindowArgs, chooseTmuxSplitDirection, formatBackgroundReceipt, buildPiArgs, parseResultXml, formatMs, shellQuote, discoverAgents, formatAgentList, countToolUses, readRecentToolCalls, } from "./helpers.js";
|
|
26
28
|
import { runSdkSubagent } from "./subagent/runSdk.js";
|
|
27
29
|
import { checkTaskCompletion, waitForTaskCompletion as waitForSessionTaskCompletion, } from "./subagent/waitCompletion.js";
|
|
28
|
-
import {
|
|
30
|
+
import { renderTaskWidget, TASK_WIDGET_RENDER_MS, } from "./task-widget.js";
|
|
29
31
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
30
32
|
const BUNDLED_AGENT_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "agents");
|
|
31
33
|
const BACKGROUND_CHECK_MS = 10_000; // poll every 10 sec
|
|
32
34
|
const COUNT_POLL_MS = 3_000; // update toolcall counts every 3 sec
|
|
33
35
|
const TASK_TIMEOUT_MS = 30 * 60 * 1_000; // 30 minutes
|
|
34
|
-
//
|
|
36
|
+
// Conversation helpers live in ./conversation.js.
|
|
35
37
|
function readRegistry(piDir) {
|
|
36
38
|
const path = join(piDir, "task-registry.json");
|
|
37
39
|
try {
|
|
@@ -45,6 +47,87 @@ function writeRegistry(piDir, entries) {
|
|
|
45
47
|
const path = join(piDir, "task-registry.json");
|
|
46
48
|
writeFileSync(path, JSON.stringify(entries, null, 2), "utf-8");
|
|
47
49
|
}
|
|
50
|
+
function readTaskSessionHistory(piDir) {
|
|
51
|
+
const path = join(piDir, "task-session-history.json");
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function writeTaskSessionHistory(piDir, entries) {
|
|
60
|
+
const path = join(piDir, "task-session-history.json");
|
|
61
|
+
writeFileSync(path, JSON.stringify(entries, null, 2), "utf-8");
|
|
62
|
+
}
|
|
63
|
+
function upsertTaskSessionHistory(piDir, entry) {
|
|
64
|
+
const entries = readTaskSessionHistory(piDir);
|
|
65
|
+
const index = entries.findIndex((existing) => existing.id === entry.id);
|
|
66
|
+
if (index >= 0) {
|
|
67
|
+
entries[index] = { ...entries[index], ...entry };
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
entries.push(entry);
|
|
71
|
+
}
|
|
72
|
+
writeTaskSessionHistory(piDir, entries);
|
|
73
|
+
}
|
|
74
|
+
function findTaskSessionHistory(piDir, idOrSessionName) {
|
|
75
|
+
return readTaskSessionHistory(piDir).find((entry) => entry.id === idOrSessionName || entry.sessionName === idOrSessionName);
|
|
76
|
+
}
|
|
77
|
+
function findJsonlSessionByName(piDir, sessionName, agentType) {
|
|
78
|
+
const artifactsDir = join(piDir, "artifacts");
|
|
79
|
+
const sessionDir = join(artifactsDir, "sessions");
|
|
80
|
+
try {
|
|
81
|
+
if (!existsSync(sessionDir))
|
|
82
|
+
return undefined;
|
|
83
|
+
const files = readdirSync(sessionDir)
|
|
84
|
+
.filter((file) => file.endsWith(".jsonl"))
|
|
85
|
+
.sort()
|
|
86
|
+
.reverse();
|
|
87
|
+
for (const file of files) {
|
|
88
|
+
const content = readFileSync(join(sessionDir, file), "utf-8");
|
|
89
|
+
let startedAt = Date.now();
|
|
90
|
+
for (const rawLine of content.split("\n")) {
|
|
91
|
+
const line = rawLine.trim();
|
|
92
|
+
if (!line)
|
|
93
|
+
continue;
|
|
94
|
+
try {
|
|
95
|
+
const entry = JSON.parse(line);
|
|
96
|
+
if (entry.type === "session" && entry.timestamp) {
|
|
97
|
+
const parsed = Date.parse(entry.timestamp);
|
|
98
|
+
if (Number.isFinite(parsed))
|
|
99
|
+
startedAt = parsed;
|
|
100
|
+
}
|
|
101
|
+
if (entry.type === "session_info") {
|
|
102
|
+
const name = entry.name ?? entry.session_info?.name;
|
|
103
|
+
if (name === sessionName) {
|
|
104
|
+
return {
|
|
105
|
+
id: sessionName,
|
|
106
|
+
agentType,
|
|
107
|
+
description: `Resumed session ${sessionName}`,
|
|
108
|
+
sessionName,
|
|
109
|
+
sessionRef: join(sessionDir, file),
|
|
110
|
+
startedAt,
|
|
111
|
+
piDir,
|
|
112
|
+
dir: artifactsDir,
|
|
113
|
+
status: "done",
|
|
114
|
+
background: false,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Skip malformed lines
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
48
131
|
// ─── Tmux Helpers ────────────────────────────────────────────────────────────
|
|
49
132
|
function tmuxCmd(args) {
|
|
50
133
|
return execFileSync("tmux", args, {
|
|
@@ -128,6 +211,22 @@ function completeTask(pi, id, task, content, phase, piDir) {
|
|
|
128
211
|
killAgentPane(task.paneId, task.originalPane);
|
|
129
212
|
const parsed = parseResultXml(content);
|
|
130
213
|
const durationMs = Date.now() - task.startedAt;
|
|
214
|
+
const completedSessionRef = findJsonlSessionByName(piDir, task.sessionName, task.agentType)?.sessionRef;
|
|
215
|
+
upsertTaskSessionHistory(piDir, {
|
|
216
|
+
id,
|
|
217
|
+
agentType: task.agentType,
|
|
218
|
+
description: task.description,
|
|
219
|
+
sessionName: task.sessionName,
|
|
220
|
+
startedAt: task.startedAt,
|
|
221
|
+
paneId: task.paneId,
|
|
222
|
+
piDir,
|
|
223
|
+
dir: task.dir,
|
|
224
|
+
conversationId: task.conversationId,
|
|
225
|
+
sessionRef: completedSessionRef,
|
|
226
|
+
status: phase,
|
|
227
|
+
completedAt: Date.now(),
|
|
228
|
+
background: true,
|
|
229
|
+
});
|
|
131
230
|
// Send completion notification
|
|
132
231
|
pi.sendMessage({
|
|
133
232
|
customType: "task-complete",
|
|
@@ -190,6 +289,7 @@ export default function (pi) {
|
|
|
190
289
|
startedAt: entry.startedAt,
|
|
191
290
|
toolUses: 0,
|
|
192
291
|
turns: 0,
|
|
292
|
+
conversationId: entry.conversationId,
|
|
193
293
|
recentCalls: [],
|
|
194
294
|
};
|
|
195
295
|
backgroundTasks.set(entry.id, bgtask);
|
|
@@ -212,62 +312,27 @@ export default function (pi) {
|
|
|
212
312
|
]) {
|
|
213
313
|
const sessionDir = join(task.dir, "sessions");
|
|
214
314
|
// Single walk: counts + recent tool-call history with status
|
|
215
|
-
const { toolUses, turns, recent } = readRecentToolCalls(sessionDir, 12);
|
|
315
|
+
const { toolUses, turns, recent } = readRecentToolCalls(sessionDir, 12, task.sessionName);
|
|
216
316
|
task.toolUses = toolUses;
|
|
217
317
|
task.turns = turns;
|
|
218
318
|
task.recentCalls = recent;
|
|
219
319
|
}
|
|
220
320
|
}, COUNT_POLL_MS);
|
|
221
|
-
/**
|
|
222
|
-
* Render a streaming view of one active subagent. Layout per task:
|
|
223
|
-
*
|
|
224
|
-
* ⠋ Scout — SDK docs • 1m 0s 11 toolcalls (themed: accent + dim)
|
|
225
|
-
* ├─ ✓ websearch Model Context Protocol 2026 (green/success)
|
|
226
|
-
* ├─ ✓ codesearch MCP reference server typescript
|
|
227
|
-
* ├─ ✗ bash curl -sL "https://api.github.com..." (red/error)
|
|
228
|
-
* └─ ⠹ read /Users/.../scout.md (yellow/warning, animates)
|
|
229
|
-
*
|
|
230
|
-
* The header caret and in-progress tool marks share the same spinner
|
|
231
|
-
* frame set (rotates every WIDGET_RENDER_MS based on wall-clock time,
|
|
232
|
-
* so the animation cadence is stable regardless of TUI render rate).
|
|
233
|
-
*/
|
|
234
321
|
// Theme reference is captured at setWidget time so renderWidget can use it.
|
|
235
|
-
// We don't import the Theme type because it's not exported; structural typing
|
|
236
|
-
// via `any` here is safe — the c() helper only calls `theme(color, text)`.
|
|
237
322
|
let widgetTheme = null;
|
|
238
|
-
// 8-frame Braille spinner. 80ms cadence = 12.5 FPS, which is the human
|
|
239
|
-
// perception threshold for "smooth motion" (below ~10 FPS the brain
|
|
240
|
-
// sees discrete steps; above ~12 FPS it reads as continuous rotation).
|
|
241
|
-
// Full rotation: 8 × 80ms = 640ms. Used for both per-tool in-progress
|
|
242
|
-
// marks AND the header caret (the "agent is active" indicator).
|
|
243
|
-
const WIDGET_SPINNER_FRAMES = [
|
|
244
|
-
"\u280B",
|
|
245
|
-
"\u2819",
|
|
246
|
-
"\u2838",
|
|
247
|
-
"\u2834",
|
|
248
|
-
"\u2826",
|
|
249
|
-
"\u2827",
|
|
250
|
-
"\u2807",
|
|
251
|
-
"\u280F",
|
|
252
|
-
];
|
|
253
|
-
const WIDGET_CARET_FRAMES = WIDGET_SPINNER_FRAMES;
|
|
254
|
-
const WIDGET_RENDER_MS = 80;
|
|
255
|
-
const WIDGET_MAX_TOOL_LINES = 12;
|
|
256
|
-
const WIDGET_MAX_WIDTH = 120;
|
|
257
|
-
const TREE_MIDDLE = "\u251C\u2500"; // ├─
|
|
258
|
-
const TREE_LAST = "\u2514\u2500"; // └─
|
|
259
|
-
function c(color, text) {
|
|
260
|
-
// widgetTheme is a Theme object with a .fg(color, text) method,
|
|
261
|
-
// not a callable. Calling it as a function throws "widgetTheme is not
|
|
262
|
-
// a function" which the outer try/catch in renderWidget swallows.
|
|
263
|
-
return widgetTheme ? widgetTheme.fg(color, text) : text;
|
|
264
|
-
}
|
|
265
323
|
function renderWidget(width) {
|
|
266
324
|
// Defensive: never let a render exception kill the TUI. If anything
|
|
267
325
|
// throws (theme lookup miss, malformed session JSONL, etc.), fall
|
|
268
326
|
// back to a minimal single-line summary so the TUI stays alive.
|
|
269
327
|
try {
|
|
270
|
-
return
|
|
328
|
+
return renderTaskWidget({
|
|
329
|
+
foregroundTasks: foregroundTasks.entries(),
|
|
330
|
+
backgroundTasks: backgroundTasks.entries(),
|
|
331
|
+
foregroundCount: foregroundTasks.size,
|
|
332
|
+
backgroundCount: backgroundTasks.size,
|
|
333
|
+
width,
|
|
334
|
+
theme: widgetTheme,
|
|
335
|
+
});
|
|
271
336
|
}
|
|
272
337
|
catch (err) {
|
|
273
338
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -279,7 +344,7 @@ export default function (pi) {
|
|
|
279
344
|
return [];
|
|
280
345
|
const [, task] = active[0];
|
|
281
346
|
return [
|
|
282
|
-
truncateToWidth(`${task.agentType} \u2022 ${formatMs(Date.now() - task.startedAt)} (render error: ${msg})`, Math.min(width,
|
|
347
|
+
truncateToWidth(`${task.agentType} \u2022 ${formatMs(Date.now() - task.startedAt)} (render error: ${msg})`, Math.min(width, 120)),
|
|
283
348
|
];
|
|
284
349
|
}
|
|
285
350
|
}
|
|
@@ -289,7 +354,7 @@ export default function (pi) {
|
|
|
289
354
|
widgetCtx = targetCtx;
|
|
290
355
|
targetCtx.ui.setWidget("task", (tui, theme) => {
|
|
291
356
|
widgetTheme = theme ?? null;
|
|
292
|
-
widgetTimer = setInterval(() => tui.requestRender(),
|
|
357
|
+
widgetTimer = setInterval(() => tui.requestRender(), TASK_WIDGET_RENDER_MS);
|
|
293
358
|
// Don't keep the process alive just for the widget refresh.
|
|
294
359
|
widgetTimer.unref?.();
|
|
295
360
|
return {
|
|
@@ -311,62 +376,6 @@ export default function (pi) {
|
|
|
311
376
|
widgetCtx = null;
|
|
312
377
|
}
|
|
313
378
|
}
|
|
314
|
-
function renderWidgetInner(width) {
|
|
315
|
-
const active = [
|
|
316
|
-
...Array.from(foregroundTasks.entries()),
|
|
317
|
-
...Array.from(backgroundTasks.entries()),
|
|
318
|
-
];
|
|
319
|
-
if (active.length === 0)
|
|
320
|
-
return [];
|
|
321
|
-
const now = Date.now();
|
|
322
|
-
const maxWidth = Math.min(width, WIDGET_MAX_WIDTH);
|
|
323
|
-
const tick = Math.floor(now / WIDGET_RENDER_MS);
|
|
324
|
-
const spinner = WIDGET_SPINNER_FRAMES[tick % WIDGET_SPINNER_FRAMES.length];
|
|
325
|
-
const caret = WIDGET_CARET_FRAMES[tick % WIDGET_CARET_FRAMES.length];
|
|
326
|
-
const lines = [];
|
|
327
|
-
for (const [, task] of active) {
|
|
328
|
-
const agentName = task.agentType.charAt(0).toUpperCase() + task.agentType.slice(1);
|
|
329
|
-
const elapsed = formatMs(now - task.startedAt);
|
|
330
|
-
const total = task.toolUses > 0 ? ` ${task.turns || task.toolUses} toolcalls` : "";
|
|
331
|
-
const description = task.description ? ` — ${task.description}` : "";
|
|
332
|
-
// Header: ▼ <Agent> — <description> • 1m 0s 11 toolcalls
|
|
333
|
-
const header = c("accent", caret) +
|
|
334
|
-
" " +
|
|
335
|
-
c("toolTitle", agentName) +
|
|
336
|
-
c("dim", `${description} \u2022 ${elapsed}${total}`);
|
|
337
|
-
lines.push(truncateToWidth(header, maxWidth));
|
|
338
|
-
const recent = task.recentCalls ?? [];
|
|
339
|
-
if (recent.length > 0) {
|
|
340
|
-
const slice = recent.slice(-WIDGET_MAX_TOOL_LINES);
|
|
341
|
-
slice.forEach((tc, idx) => {
|
|
342
|
-
const isLast = idx === slice.length - 1;
|
|
343
|
-
const connector = isLast ? TREE_LAST : TREE_MIDDLE;
|
|
344
|
-
const isInProgress = tc.status === "in_progress";
|
|
345
|
-
const markChar = isInProgress
|
|
346
|
-
? spinner
|
|
347
|
-
: tc.status === "error"
|
|
348
|
-
? "\u2717"
|
|
349
|
-
: "\u2713";
|
|
350
|
-
const markColor = isInProgress
|
|
351
|
-
? "warning"
|
|
352
|
-
: tc.status === "error"
|
|
353
|
-
? "error"
|
|
354
|
-
: "success";
|
|
355
|
-
const detailStr = tc.detail ? ` ${tc.detail}` : "";
|
|
356
|
-
const line = " " +
|
|
357
|
-
c("dim", connector) +
|
|
358
|
-
" " +
|
|
359
|
-
c(markColor, markChar) +
|
|
360
|
-
" " +
|
|
361
|
-
c("text", tc.name) +
|
|
362
|
-
c("dim", detailStr);
|
|
363
|
-
lines.push(truncateToWidth(line, maxWidth));
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
lines.push("");
|
|
367
|
-
}
|
|
368
|
-
return lines;
|
|
369
|
-
}
|
|
370
379
|
// ── Polling loop (background task completion, pane death, timeout) ──────
|
|
371
380
|
const checkInterval = setInterval(async () => {
|
|
372
381
|
if (backgroundTasks.size === 0) {
|
|
@@ -379,24 +388,27 @@ export default function (pi) {
|
|
|
379
388
|
const task = backgroundTasks.get(id);
|
|
380
389
|
if (!task)
|
|
381
390
|
continue;
|
|
382
|
-
backgroundTasks.delete(id); // Remove atomically
|
|
383
391
|
// ── Check timeout ────────────────────────────────────────────
|
|
384
392
|
if (now - task.startedAt > TASK_TIMEOUT_MS) {
|
|
385
393
|
killAgentPane(task.paneId, task.originalPane);
|
|
394
|
+
backgroundTasks.delete(id);
|
|
395
|
+
clearTaskWidgetIfIdle();
|
|
386
396
|
completeTask(pi, id, task, "Task timed out after 30 minutes", "timeout", piDir);
|
|
387
397
|
continue;
|
|
388
398
|
}
|
|
389
399
|
const snapshot = await checkTaskCompletion({
|
|
390
400
|
resultPath: join(task.dir, "RESULT.md"),
|
|
391
|
-
sessionDir: task.dir,
|
|
401
|
+
sessionDir: join(task.dir, "sessions"),
|
|
392
402
|
sessionName: task.sessionName,
|
|
393
403
|
paneId: task.paneId,
|
|
404
|
+
sinceMs: task.startedAt,
|
|
394
405
|
});
|
|
395
406
|
if (snapshot.status === "running") {
|
|
396
|
-
backgroundTasks.set(id, task);
|
|
397
407
|
continue;
|
|
398
408
|
}
|
|
399
409
|
const phase = snapshot.status === "completed" ? "done" : "failed";
|
|
410
|
+
backgroundTasks.delete(id);
|
|
411
|
+
clearTaskWidgetIfIdle();
|
|
400
412
|
completeTask(pi, id, task, snapshot.content, phase, piDir);
|
|
401
413
|
}
|
|
402
414
|
}, BACKGROUND_CHECK_MS);
|
|
@@ -422,34 +434,36 @@ export default function (pi) {
|
|
|
422
434
|
const confidence = d.confidence || "";
|
|
423
435
|
const durationMs = d.duration_ms || 0;
|
|
424
436
|
const toolUses = d.tool_uses || 0;
|
|
425
|
-
|
|
426
|
-
let line = theme.fg("accent", agentType);
|
|
437
|
+
let line = " " + theme.fg("accent", agentType);
|
|
427
438
|
if (desc)
|
|
428
439
|
line += theme.fg("dim", ` - ${desc}`);
|
|
429
|
-
const useStr = toolUses > 0 ? `${
|
|
440
|
+
const useStr = toolUses > 0 ? `${toolUses} toolcalls` : "";
|
|
430
441
|
const durStr = durationMs >= 1000 ? formatMs(durationMs) : "";
|
|
431
442
|
const statsParts = [useStr, durStr].filter(Boolean);
|
|
432
|
-
|
|
433
|
-
line += "\n" + theme.fg("dim", statsParts.join(" • "));
|
|
434
|
-
}
|
|
443
|
+
const statsText = statsParts.join(" • ");
|
|
435
444
|
const confStr = confidence ? confidence.toUpperCase() : "";
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
line += "\n"
|
|
445
|
+
const confColor = confidence === "high"
|
|
446
|
+
? "success"
|
|
447
|
+
: confidence === "low"
|
|
448
|
+
? "error"
|
|
449
|
+
: "accent";
|
|
450
|
+
if (statsText || confStr) {
|
|
451
|
+
line += "\n ";
|
|
452
|
+
if (confStr)
|
|
453
|
+
line += theme.fg(confColor, `[${confStr}]`);
|
|
454
|
+
if (statsText)
|
|
455
|
+
line += (confStr ? " " : "") + theme.fg("dim", statsText);
|
|
443
456
|
}
|
|
444
457
|
if (expanded) {
|
|
445
458
|
if (summary)
|
|
446
|
-
line += "\n" + theme.fg("muted", summary);
|
|
459
|
+
line += "\n " + theme.fg("muted", summary);
|
|
447
460
|
if (findings)
|
|
448
|
-
line += "\n" + theme.fg("dim", findings);
|
|
461
|
+
line += "\n " + theme.fg("dim", findings);
|
|
449
462
|
}
|
|
450
463
|
if (!line.trim())
|
|
451
464
|
return undefined;
|
|
452
|
-
|
|
465
|
+
const subtleBg = (text) => `\x1b[48;2;30;28;44m${text}\x1b[0m`;
|
|
466
|
+
return new Text(line, 0, 1, subtleBg);
|
|
453
467
|
});
|
|
454
468
|
// ── Tool Registration ──────────────────────────────────────────────────
|
|
455
469
|
pi.registerTool({
|
|
@@ -478,7 +492,10 @@ export default function (pi) {
|
|
|
478
492
|
description: "A short (3-5 word) summary of the task",
|
|
479
493
|
}),
|
|
480
494
|
task_id: Type.Optional(Type.String({
|
|
481
|
-
description: "Resume
|
|
495
|
+
description: "Resume an existing background task by id instead of starting a new task.",
|
|
496
|
+
})),
|
|
497
|
+
conversation_id: Type.Optional(Type.String({
|
|
498
|
+
description: "Durable specialist conversation id. Reuses .pi/artifacts/task-<id>/sessions when called again.",
|
|
482
499
|
})),
|
|
483
500
|
background: Type.Optional(Type.Boolean({
|
|
484
501
|
description: "Run in background (async). You will be notified when it completes. DO NOT sleep, poll, ask the task for status, or duplicate its work while it runs in background.",
|
|
@@ -508,22 +525,139 @@ export default function (pi) {
|
|
|
508
525
|
isError: true,
|
|
509
526
|
};
|
|
510
527
|
}
|
|
511
|
-
// ── Resolve task identity: new or resume
|
|
528
|
+
// ── Resolve task identity: new, task resume, or conversation resume ──
|
|
529
|
+
const conversationId = normalizeConversationId(params.conversation_id);
|
|
530
|
+
const taskSessionsRegistry = conversationId
|
|
531
|
+
? readTaskSessionsRegistry(piDir)
|
|
532
|
+
: {};
|
|
533
|
+
const registeredTaskId = conversationId
|
|
534
|
+
? taskSessionsRegistry[conversationId]?.task_id
|
|
535
|
+
: undefined;
|
|
536
|
+
if (params.task_id &&
|
|
537
|
+
registeredTaskId &&
|
|
538
|
+
params.task_id !== registeredTaskId) {
|
|
539
|
+
return {
|
|
540
|
+
content: [
|
|
541
|
+
{
|
|
542
|
+
type: "text",
|
|
543
|
+
text: `conversation_id "${conversationId}" maps to ${registeredTaskId}, not ${params.task_id}. Omit task_id or use the mapped task id.`,
|
|
544
|
+
},
|
|
545
|
+
],
|
|
546
|
+
details: {
|
|
547
|
+
phase: "failed",
|
|
548
|
+
error: "conversation_id/task_id mismatch",
|
|
549
|
+
},
|
|
550
|
+
isError: true,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
512
553
|
let id;
|
|
513
554
|
let sessionName;
|
|
514
|
-
let artifactDir;
|
|
515
555
|
let resultPath;
|
|
516
556
|
let resume = false;
|
|
517
|
-
|
|
518
|
-
|
|
557
|
+
let resumeSessionRef;
|
|
558
|
+
const artifactsDir = join(piDir, "artifacts");
|
|
559
|
+
if (registeredTaskId) {
|
|
560
|
+
id = registeredTaskId;
|
|
561
|
+
sessionName = conversationId ?? `task-${id}`;
|
|
562
|
+
resultPath = join(artifactsDir, `RESULT-${id}.md`);
|
|
563
|
+
if (!existsSync(resultPath)) {
|
|
564
|
+
return {
|
|
565
|
+
content: [
|
|
566
|
+
{
|
|
567
|
+
type: "text",
|
|
568
|
+
text: `conversation_id "${conversationId}" has no prior result file at ${resultPath}. Cannot resume.`,
|
|
569
|
+
},
|
|
570
|
+
],
|
|
571
|
+
details: {
|
|
572
|
+
phase: "failed",
|
|
573
|
+
error: "Conversation result missing",
|
|
574
|
+
conversation_id: conversationId,
|
|
575
|
+
},
|
|
576
|
+
isError: true,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
const block = readTaskBlock(piDir, id);
|
|
580
|
+
const previousMetadata = parseMetadataFromBody(block?.body);
|
|
581
|
+
const metadataAgent = previousMetadata?.agent_type;
|
|
582
|
+
if (metadataAgent && metadataAgent !== agent.name) {
|
|
583
|
+
return {
|
|
584
|
+
content: [
|
|
585
|
+
{
|
|
586
|
+
type: "text",
|
|
587
|
+
text: `conversation_id "${conversationId}" belongs to agent "${metadataAgent}", not "${agent.name}". Use the original agent_type or start a different conversation_id.`,
|
|
588
|
+
},
|
|
589
|
+
],
|
|
590
|
+
details: {
|
|
591
|
+
phase: "failed",
|
|
592
|
+
error: "conversation_id agent_type mismatch",
|
|
593
|
+
conversation_id: conversationId,
|
|
594
|
+
},
|
|
595
|
+
isError: true,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
resume = true;
|
|
599
|
+
const entry = readRegistry(piDir).find((candidate) => candidate.id === id);
|
|
600
|
+
if (params.background !== false &&
|
|
601
|
+
entry?.paneId &&
|
|
602
|
+
paneExists(entry.paneId)) {
|
|
603
|
+
const bgtask = {
|
|
604
|
+
dir: artifactsDir,
|
|
605
|
+
agentType: entry.agentType,
|
|
606
|
+
sessionName,
|
|
607
|
+
paneId: entry.paneId,
|
|
608
|
+
originalPane: null,
|
|
609
|
+
description: params.description || entry.description,
|
|
610
|
+
startedAt: entry.startedAt,
|
|
611
|
+
toolUses: 0,
|
|
612
|
+
turns: 0,
|
|
613
|
+
conversationId,
|
|
614
|
+
recentCalls: [],
|
|
615
|
+
};
|
|
616
|
+
backgroundTasks.set(id, bgtask);
|
|
617
|
+
return {
|
|
618
|
+
content: [
|
|
619
|
+
{
|
|
620
|
+
type: "text",
|
|
621
|
+
text: `Resumed conversation "${conversationId}" via ${sessionName}. The subagent is running in background and will notify on completion.`,
|
|
622
|
+
},
|
|
623
|
+
],
|
|
624
|
+
details: {
|
|
625
|
+
task_id: id,
|
|
626
|
+
agent_type: agent.name,
|
|
627
|
+
description: params.description,
|
|
628
|
+
conversation_id: conversationId,
|
|
629
|
+
tmux_session: sessionName,
|
|
630
|
+
background: true,
|
|
631
|
+
},
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
else if (params.task_id) {
|
|
636
|
+
// Look up active tasks first, then durable completed-session history.
|
|
519
637
|
const entries = readRegistry(piDir);
|
|
520
|
-
|
|
638
|
+
let entry = entries.find((e) => e.id === params.task_id || e.sessionName === params.task_id) ??
|
|
639
|
+
findTaskSessionHistory(piDir, params.task_id) ??
|
|
640
|
+
findJsonlSessionByName(piDir, params.task_id, agent.name);
|
|
641
|
+
// Older history entries were written before we stored the
|
|
642
|
+
// actual JSONL path needed by `pi --session`. Repair them by
|
|
643
|
+
// resolving the display session name to a session file.
|
|
644
|
+
if (entry && !entry.sessionRef) {
|
|
645
|
+
const discovered = findJsonlSessionByName(piDir, entry.sessionName, entry.agentType);
|
|
646
|
+
if (discovered?.sessionRef) {
|
|
647
|
+
entry = { ...entry, sessionRef: discovered.sessionRef };
|
|
648
|
+
upsertTaskSessionHistory(piDir, {
|
|
649
|
+
...entry,
|
|
650
|
+
status: "done",
|
|
651
|
+
background: false,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
}
|
|
521
655
|
if (!entry) {
|
|
522
656
|
return {
|
|
523
657
|
content: [
|
|
524
658
|
{
|
|
525
659
|
type: "text",
|
|
526
|
-
text: `Unknown task_id: "${params.task_id}". No task with that ID
|
|
660
|
+
text: `Unknown task_id: "${params.task_id}". No active or completed task session with that ID/session name was found.`,
|
|
527
661
|
},
|
|
528
662
|
],
|
|
529
663
|
details: {
|
|
@@ -548,26 +682,28 @@ export default function (pi) {
|
|
|
548
682
|
isError: true,
|
|
549
683
|
};
|
|
550
684
|
}
|
|
551
|
-
// Resume: reuse existing
|
|
685
|
+
// Resume: reuse the existing session name; runtime files are
|
|
686
|
+
// flat in artifactsDir, no per-task subdir.
|
|
552
687
|
id = entry.id;
|
|
553
688
|
sessionName = entry.sessionName;
|
|
554
|
-
|
|
555
|
-
resultPath = join(artifactDir, "RESULT.md");
|
|
689
|
+
resultPath = join(artifactsDir, `RESULT-${id}.md`);
|
|
556
690
|
resume = true;
|
|
691
|
+
resumeSessionRef = entry.sessionRef;
|
|
557
692
|
// If background and pane still alive, reattach to tracker
|
|
558
693
|
if (params.background !== false &&
|
|
559
694
|
entry.paneId &&
|
|
560
695
|
paneExists(entry.paneId)) {
|
|
561
696
|
const bgtask = {
|
|
562
|
-
dir:
|
|
563
|
-
agentType:
|
|
697
|
+
dir: artifactsDir,
|
|
698
|
+
agentType: entry.agentType,
|
|
564
699
|
sessionName,
|
|
565
700
|
paneId: entry.paneId,
|
|
566
701
|
originalPane: null,
|
|
567
|
-
description: params.description || entry.
|
|
702
|
+
description: params.description || entry.description,
|
|
568
703
|
startedAt: entry.startedAt,
|
|
569
704
|
toolUses: 0,
|
|
570
705
|
turns: 0,
|
|
706
|
+
conversationId: entry.conversationId,
|
|
571
707
|
recentCalls: [],
|
|
572
708
|
};
|
|
573
709
|
backgroundTasks.set(id, bgtask);
|
|
@@ -580,27 +716,65 @@ export default function (pi) {
|
|
|
580
716
|
],
|
|
581
717
|
details: {
|
|
582
718
|
task_id: id,
|
|
583
|
-
agent_type:
|
|
584
|
-
description: params.description,
|
|
719
|
+
agent_type: entry.agentType,
|
|
720
|
+
description: params.description || entry.description,
|
|
721
|
+
conversation_id: entry.conversationId ?? conversationId,
|
|
585
722
|
tmux_session: sessionName,
|
|
586
723
|
background: true,
|
|
587
724
|
},
|
|
588
725
|
};
|
|
589
726
|
}
|
|
727
|
+
if (!resumeSessionRef) {
|
|
728
|
+
return {
|
|
729
|
+
content: [
|
|
730
|
+
{
|
|
731
|
+
type: "text",
|
|
732
|
+
text: `Task "${params.task_id}" was found, but its session JSONL file could not be resolved. Cannot resume without a --session file path.`,
|
|
733
|
+
},
|
|
734
|
+
],
|
|
735
|
+
details: {
|
|
736
|
+
phase: "failed",
|
|
737
|
+
error: "Task session file missing",
|
|
738
|
+
},
|
|
739
|
+
isError: true,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
590
742
|
}
|
|
591
743
|
else {
|
|
592
744
|
id = `${Date.now().toString(36)}-${randomUUID().slice(0, 4)}`;
|
|
593
|
-
sessionName = `task-${id}`;
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
745
|
+
sessionName = conversationId ?? `task-${id}`;
|
|
746
|
+
resultPath = join(artifactsDir, `RESULT-${id}.md`);
|
|
747
|
+
}
|
|
748
|
+
if (conversationId && !hasTmux()) {
|
|
749
|
+
return {
|
|
750
|
+
content: [
|
|
751
|
+
{
|
|
752
|
+
type: "text",
|
|
753
|
+
text: "Durable conversations require the tmux/CLI backend so Pi can save and reopen the subagent session. Install/start tmux or omit conversation_id for a one-shot SDK task.",
|
|
754
|
+
},
|
|
755
|
+
],
|
|
756
|
+
details: {
|
|
757
|
+
phase: "failed",
|
|
758
|
+
error: "tmux required for durable conversation",
|
|
759
|
+
conversation_id: conversationId,
|
|
760
|
+
},
|
|
761
|
+
isError: true,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
if (conversationId) {
|
|
765
|
+
await mkdir(artifactsDir, { recursive: true });
|
|
766
|
+
const taskSessionsRegistry = readTaskSessionsRegistry(piDir);
|
|
767
|
+
taskSessionsRegistry[conversationId] = {
|
|
768
|
+
task_id: id,
|
|
769
|
+
session_file: `${artifactsDir}/${id}`,
|
|
770
|
+
};
|
|
771
|
+
writeTaskSessionsRegistry(piDir, taskSessionsRegistry);
|
|
597
772
|
}
|
|
598
773
|
const descText = params.description || "";
|
|
599
774
|
const isBackground = params.background ?? TASK_BACKGROUND_DEFAULT;
|
|
600
775
|
// default true
|
|
601
|
-
// ──
|
|
602
|
-
const
|
|
603
|
-
const contextContent = [
|
|
776
|
+
// ── Build the prompt (instructions are inlined; no CONTEXT.md file) ─
|
|
777
|
+
const promptContent = [
|
|
604
778
|
`# Task: ${descText}`,
|
|
605
779
|
"",
|
|
606
780
|
`## Agent`,
|
|
@@ -613,26 +787,18 @@ export default function (pi) {
|
|
|
613
787
|
ctx.cwd,
|
|
614
788
|
"",
|
|
615
789
|
`## Output`,
|
|
616
|
-
|
|
790
|
+
"Your final assistant message is the result. End with a clear summary of what you did and any findings. No file write is required.",
|
|
617
791
|
"",
|
|
618
|
-
"Use this format:",
|
|
792
|
+
"Use this format for the summary:",
|
|
619
793
|
"",
|
|
620
794
|
"```",
|
|
621
795
|
TASK_RESULT_XML_INSTRUCTIONS,
|
|
622
796
|
"```",
|
|
623
797
|
].join("\n");
|
|
624
|
-
|
|
625
|
-
const promptContent = [
|
|
626
|
-
`Read ${contextPath} for your task.`,
|
|
627
|
-
`Write your findings/output to ${resultPath}`,
|
|
628
|
-
"",
|
|
629
|
-
"Format:",
|
|
630
|
-
TASK_RESULT_XML_INSTRUCTIONS,
|
|
631
|
-
].join("\n");
|
|
632
|
-
const sessionDir = join(artifactDir, "sessions");
|
|
798
|
+
const sessionDir = join(artifactsDir, "sessions");
|
|
633
799
|
await mkdir(sessionDir, { recursive: true });
|
|
634
800
|
// ─── Build and run the sub-agent pi process ──────────────────────────
|
|
635
|
-
const piArgs = buildPiArgs(agent, sessionName, sessionDir, promptContent, resume, parentToolNames);
|
|
801
|
+
const piArgs = buildPiArgs(agent, sessionName, sessionDir, promptContent, resume, parentToolNames, resumeSessionRef);
|
|
636
802
|
const envPrefix = `PI_TASK_TOOL_DISABLED=1`;
|
|
637
803
|
const toolSelection = buildAgentToolSelection({
|
|
638
804
|
tools: agent.tools,
|
|
@@ -653,7 +819,7 @@ export default function (pi) {
|
|
|
653
819
|
const foregroundTask = isBackground
|
|
654
820
|
? undefined
|
|
655
821
|
: {
|
|
656
|
-
dir:
|
|
822
|
+
dir: artifactsDir,
|
|
657
823
|
agentType: agent.name,
|
|
658
824
|
sessionName,
|
|
659
825
|
originalPane: null,
|
|
@@ -661,6 +827,7 @@ export default function (pi) {
|
|
|
661
827
|
startedAt: Date.now(),
|
|
662
828
|
toolUses: 0,
|
|
663
829
|
turns: 0,
|
|
830
|
+
conversationId,
|
|
664
831
|
recentCalls: [],
|
|
665
832
|
};
|
|
666
833
|
if (foregroundTask) {
|
|
@@ -671,7 +838,7 @@ export default function (pi) {
|
|
|
671
838
|
if (!hasTmux()) {
|
|
672
839
|
if (isBackground) {
|
|
673
840
|
const bgtask = {
|
|
674
|
-
dir:
|
|
841
|
+
dir: artifactsDir,
|
|
675
842
|
agentType: agent.name,
|
|
676
843
|
sessionName,
|
|
677
844
|
originalPane: null,
|
|
@@ -679,6 +846,7 @@ export default function (pi) {
|
|
|
679
846
|
startedAt: Date.now(),
|
|
680
847
|
toolUses: 0,
|
|
681
848
|
turns: 0,
|
|
849
|
+
conversationId,
|
|
682
850
|
recentCalls: [],
|
|
683
851
|
};
|
|
684
852
|
backgroundTasks.set(id, bgtask);
|
|
@@ -689,17 +857,22 @@ export default function (pi) {
|
|
|
689
857
|
sessionName,
|
|
690
858
|
startedAt: bgtask.startedAt,
|
|
691
859
|
piDir,
|
|
692
|
-
dir:
|
|
860
|
+
dir: artifactsDir,
|
|
861
|
+
conversationId,
|
|
693
862
|
};
|
|
694
863
|
const entries = readRegistry(piDir);
|
|
695
864
|
entries.push(entry);
|
|
696
865
|
writeRegistry(piDir, entries);
|
|
866
|
+
upsertTaskSessionHistory(piDir, {
|
|
867
|
+
...entry,
|
|
868
|
+
status: "running",
|
|
869
|
+
background: true,
|
|
870
|
+
});
|
|
697
871
|
pi.appendEntry("task-registry", entry);
|
|
698
872
|
ensureTaskWidget(ctx);
|
|
699
873
|
void runSdkFallback()
|
|
700
874
|
.then(async ({ output }) => {
|
|
701
875
|
const finalOutput = output || "SDK subagent completed without assistant text.";
|
|
702
|
-
await writeFile(resultPath, finalOutput, "utf-8");
|
|
703
876
|
backgroundTasks.delete(id);
|
|
704
877
|
clearTaskWidgetIfIdle();
|
|
705
878
|
completeTask(pi, id, bgtask, finalOutput, "done", piDir);
|
|
@@ -722,13 +895,24 @@ export default function (pi) {
|
|
|
722
895
|
background: true,
|
|
723
896
|
backend: "sdk",
|
|
724
897
|
result_path: resultPath,
|
|
898
|
+
conversation_id: conversationId,
|
|
725
899
|
},
|
|
726
900
|
};
|
|
727
901
|
}
|
|
728
902
|
try {
|
|
729
903
|
const { output, sessionPath } = await runSdkFallback();
|
|
730
904
|
const finalOutput = output || "SDK subagent completed without assistant text.";
|
|
731
|
-
|
|
905
|
+
if (conversationId) {
|
|
906
|
+
writeConversationArtifacts({
|
|
907
|
+
piDir,
|
|
908
|
+
taskId: id,
|
|
909
|
+
conversationId,
|
|
910
|
+
agentType: agent.name,
|
|
911
|
+
sessionFile: sessionPath ?? "unknown",
|
|
912
|
+
prompt: params.prompt,
|
|
913
|
+
result: finalOutput,
|
|
914
|
+
});
|
|
915
|
+
}
|
|
732
916
|
return {
|
|
733
917
|
content: [{ type: "text", text: finalOutput }],
|
|
734
918
|
details: {
|
|
@@ -736,6 +920,7 @@ export default function (pi) {
|
|
|
736
920
|
backend: "sdk",
|
|
737
921
|
session_path: sessionPath,
|
|
738
922
|
result_path: resultPath,
|
|
923
|
+
conversation_id: conversationId,
|
|
739
924
|
},
|
|
740
925
|
};
|
|
741
926
|
}
|
|
@@ -786,14 +971,29 @@ export default function (pi) {
|
|
|
786
971
|
}
|
|
787
972
|
// ── FOREGROUND MODE: block until result, return directly ────────────
|
|
788
973
|
if (!isBackground) {
|
|
789
|
-
const startedAt = Date.now();
|
|
974
|
+
const startedAt = foregroundTask?.startedAt ?? Date.now();
|
|
975
|
+
upsertTaskSessionHistory(piDir, {
|
|
976
|
+
id,
|
|
977
|
+
agentType: agent.name,
|
|
978
|
+
description: descText,
|
|
979
|
+
sessionName,
|
|
980
|
+
startedAt,
|
|
981
|
+
paneId,
|
|
982
|
+
piDir,
|
|
983
|
+
dir: artifactsDir,
|
|
984
|
+
conversationId,
|
|
985
|
+
status: "running",
|
|
986
|
+
background: false,
|
|
987
|
+
});
|
|
790
988
|
const completion = await waitForSessionTaskCompletion({
|
|
791
989
|
resultPath,
|
|
792
990
|
sessionDir,
|
|
793
991
|
sessionName,
|
|
794
992
|
paneId,
|
|
795
993
|
signal,
|
|
796
|
-
timeoutMs:
|
|
994
|
+
timeoutMs: TASK_TIMEOUT_MS,
|
|
995
|
+
pollMs: 1000,
|
|
996
|
+
sinceMs: startedAt,
|
|
797
997
|
});
|
|
798
998
|
const content = completion.content;
|
|
799
999
|
const phase = completion.status === "completed"
|
|
@@ -801,19 +1001,46 @@ export default function (pi) {
|
|
|
801
1001
|
: completion.status === "cancelled"
|
|
802
1002
|
? "cancelled"
|
|
803
1003
|
: "failed";
|
|
1004
|
+
const completedSessionRef = findJsonlSessionByName(piDir, sessionName, agent.name)?.sessionRef;
|
|
1005
|
+
upsertTaskSessionHistory(piDir, {
|
|
1006
|
+
id,
|
|
1007
|
+
agentType: agent.name,
|
|
1008
|
+
description: descText,
|
|
1009
|
+
sessionName,
|
|
1010
|
+
startedAt,
|
|
1011
|
+
paneId,
|
|
1012
|
+
piDir,
|
|
1013
|
+
dir: artifactsDir,
|
|
1014
|
+
conversationId,
|
|
1015
|
+
sessionRef: completedSessionRef,
|
|
1016
|
+
status: phase,
|
|
1017
|
+
completedAt: Date.now(),
|
|
1018
|
+
background: false,
|
|
1019
|
+
});
|
|
804
1020
|
killAgentPane(paneId, originalPane);
|
|
805
1021
|
foregroundTasks.delete(id);
|
|
806
1022
|
clearTaskWidgetIfIdle();
|
|
1023
|
+
if (conversationId) {
|
|
1024
|
+
writeConversationArtifacts({
|
|
1025
|
+
piDir,
|
|
1026
|
+
taskId: id,
|
|
1027
|
+
conversationId,
|
|
1028
|
+
agentType: agent.name,
|
|
1029
|
+
sessionFile: `${sessionDir}/${sessionName}`,
|
|
1030
|
+
prompt: params.prompt,
|
|
1031
|
+
result: content,
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
807
1034
|
const parsed = parseResultXml(content);
|
|
808
1035
|
const durationMs = Date.now() - startedAt;
|
|
809
|
-
const { toolUses, turns } = countToolUses(sessionDir);
|
|
1036
|
+
const { toolUses, turns } = countToolUses(sessionDir, sessionName);
|
|
810
1037
|
return {
|
|
811
1038
|
content: [
|
|
812
1039
|
{
|
|
813
1040
|
type: "text",
|
|
814
1041
|
text: [
|
|
815
1042
|
`${parsed.status || "done"}: ${parsed.summary || content.slice(0, 300)}`,
|
|
816
|
-
toolUses > 0 ? `\n${
|
|
1043
|
+
toolUses > 0 ? `\n${toolUses} toolcalls` : "",
|
|
817
1044
|
durationMs >= 1000 ? `\n${formatMs(durationMs)}` : "",
|
|
818
1045
|
]
|
|
819
1046
|
.filter(Boolean)
|
|
@@ -834,12 +1061,13 @@ export default function (pi) {
|
|
|
834
1061
|
tool_uses: toolUses,
|
|
835
1062
|
turn_count: turns,
|
|
836
1063
|
background: false,
|
|
1064
|
+
conversation_id: conversationId,
|
|
837
1065
|
},
|
|
838
1066
|
};
|
|
839
1067
|
}
|
|
840
1068
|
// ── BACKGROUND MODE (default): add to tracker, return immediately ─────
|
|
841
1069
|
const bgtask = {
|
|
842
|
-
dir:
|
|
1070
|
+
dir: artifactsDir,
|
|
843
1071
|
agentType: agent.name,
|
|
844
1072
|
sessionName,
|
|
845
1073
|
paneId,
|
|
@@ -848,6 +1076,7 @@ export default function (pi) {
|
|
|
848
1076
|
startedAt: Date.now(),
|
|
849
1077
|
toolUses: 0,
|
|
850
1078
|
turns: 0,
|
|
1079
|
+
conversationId,
|
|
851
1080
|
recentCalls: [],
|
|
852
1081
|
};
|
|
853
1082
|
backgroundTasks.set(id, bgtask);
|
|
@@ -857,15 +1086,21 @@ export default function (pi) {
|
|
|
857
1086
|
agentType: agent.name,
|
|
858
1087
|
description: descText,
|
|
859
1088
|
sessionName,
|
|
860
|
-
startedAt:
|
|
1089
|
+
startedAt: bgtask.startedAt,
|
|
861
1090
|
paneId,
|
|
862
1091
|
piDir,
|
|
863
|
-
dir:
|
|
1092
|
+
dir: artifactsDir,
|
|
1093
|
+
conversationId,
|
|
864
1094
|
};
|
|
865
1095
|
// Write to JSON registry for on-load restore
|
|
866
1096
|
const entries = readRegistry(piDir);
|
|
867
1097
|
entries.push(entry);
|
|
868
1098
|
writeRegistry(piDir, entries);
|
|
1099
|
+
upsertTaskSessionHistory(piDir, {
|
|
1100
|
+
...entry,
|
|
1101
|
+
status: "running",
|
|
1102
|
+
background: true,
|
|
1103
|
+
});
|
|
869
1104
|
// Also persist to session store via appendEntry (audit trail)
|
|
870
1105
|
pi.appendEntry("task-registry", entry);
|
|
871
1106
|
// ── Abort signal handling ──────────────────────────────────────────
|
|
@@ -896,7 +1131,7 @@ export default function (pi) {
|
|
|
896
1131
|
taskId: id,
|
|
897
1132
|
agentType: agent.name,
|
|
898
1133
|
tmuxSession: sessionName,
|
|
899
|
-
artifactDir,
|
|
1134
|
+
artifactDir: artifactsDir,
|
|
900
1135
|
}),
|
|
901
1136
|
},
|
|
902
1137
|
],
|
|
@@ -923,7 +1158,17 @@ export default function (pi) {
|
|
|
923
1158
|
if (!d)
|
|
924
1159
|
return new Text("", 0, 0);
|
|
925
1160
|
if (d.background) {
|
|
926
|
-
|
|
1161
|
+
const toolUses = d.tool_uses || 0;
|
|
1162
|
+
const durationMs = d.duration_ms || 0;
|
|
1163
|
+
const confidence = d.confidence || "";
|
|
1164
|
+
const useStr = toolUses > 0 ? `${toolUses} toolcalls` : "";
|
|
1165
|
+
const durStr = durationMs >= 1000 ? formatMs(durationMs) : "";
|
|
1166
|
+
const statsParts = [useStr, durStr].filter(Boolean);
|
|
1167
|
+
const statsText = statsParts.join(" \u2022 ");
|
|
1168
|
+
const confStr = confidence ? `[${confidence.toUpperCase()}]` : "";
|
|
1169
|
+
const statsLine = [confStr, statsText].filter(Boolean).join(" ");
|
|
1170
|
+
const subtleBg = (text) => `\x1b[48;2;30;28;44m${text}\x1b[0m`;
|
|
1171
|
+
return new Text(statsLine ? " " + theme.fg("dim", statsLine.trim()) : "", 0, 0, subtleBg);
|
|
927
1172
|
}
|
|
928
1173
|
if (d.phase === "timeout" ||
|
|
929
1174
|
d.phase === "aborted" ||
|
|
@@ -938,8 +1183,7 @@ export default function (pi) {
|
|
|
938
1183
|
d.status === "failed";
|
|
939
1184
|
const durationMs = d.duration_ms || 0;
|
|
940
1185
|
const toolUses = d.tool_uses || 0;
|
|
941
|
-
const
|
|
942
|
-
const useStr = toolUses > 0 ? `${turns || toolUses} toolcalls` : "";
|
|
1186
|
+
const useStr = toolUses > 0 ? `${toolUses} toolcalls` : "";
|
|
943
1187
|
const durStr = durationMs >= 1000 ? formatMs(durationMs) : "";
|
|
944
1188
|
const statsParts = [useStr, durStr].filter(Boolean);
|
|
945
1189
|
const statsStr = statsParts.length
|
|
@@ -974,4 +1218,12 @@ export default function (pi) {
|
|
|
974
1218
|
return new Text(line, 0, 0);
|
|
975
1219
|
},
|
|
976
1220
|
});
|
|
1221
|
+
pi.registerCommand("task-sessions", {
|
|
1222
|
+
description: "List durable pi-task conversations",
|
|
1223
|
+
handler: async (_args, ctx) => {
|
|
1224
|
+
const cwd = ctx.sessionManager?.getCwd?.() ?? process.cwd();
|
|
1225
|
+
const { piDir } = discoverAgents(cwd);
|
|
1226
|
+
ctx.ui.notify(renderConversationSessions(piDir), "info");
|
|
1227
|
+
},
|
|
1228
|
+
});
|
|
977
1229
|
}
|