@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/index.js
CHANGED
|
@@ -1,458 +1,74 @@
|
|
|
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
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
18
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
|
-
import { execFileSync } from "node:child_process";
|
|
20
17
|
import { randomUUID } from "node:crypto";
|
|
18
|
+
import { existsSync } from "node:fs";
|
|
19
|
+
import { mkdir } from "node:fs/promises";
|
|
21
20
|
import { dirname, join } from "node:path";
|
|
22
21
|
import { fileURLToPath } from "node:url";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import { TASK_BACKGROUND_DEFAULT,
|
|
22
|
+
import { buildAgentToolSelection } from "./agent-tools.js";
|
|
23
|
+
import { BACKGROUND_CHECK_MS, COUNT_POLL_MS, MAX_POLL_ERRORS, TASK_TIMEOUT_MS, } from "./constants.js";
|
|
24
|
+
import { findJsonlSessionByName, normalizeConversationId, parseMetadataFromBody, readTaskBlock, findTaskSessionHistory, readRegistry, readTaskSessionsRegistry, renderConversationSessions, upsertTaskSessionHistory, writeConversationArtifacts, writeRegistry, writeTaskSessionsRegistry, } from "./conversation.js";
|
|
25
|
+
import { TASK_BACKGROUND_DEFAULT, TASK_TOOL_DESCRIPTION, buildPiArgs, countToolUses, discoverAgents, formatAgentList, formatBackgroundReceipt, parseResultXml, shellQuote, } from "./helpers.js";
|
|
26
|
+
import { completeTask, createTaskWidgetController, restoreActiveBackgroundTasks, startBackgroundPolling, startToolStatsPolling, } from "./lifecycle/index.js";
|
|
27
27
|
import { runSdkSubagent } from "./subagent/runSdk.js";
|
|
28
28
|
import { checkTaskCompletion, waitForTaskCompletion as waitForSessionTaskCompletion, } from "./subagent/waitCompletion.js";
|
|
29
|
-
import {
|
|
29
|
+
import { hasTmux, killAgentPane, paneExists, setPaneRemainOnExit, splitWindowPane, wrapWithPaneExitWatcher, } from "./subagent/tmux.js";
|
|
30
|
+
import { buildTaskPrompt, createTaskCompleteRenderer, renderCall, renderResult, taskParametersSchema, } from "./tool/index.js";
|
|
30
31
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
31
32
|
const BUNDLED_AGENT_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "agents");
|
|
32
|
-
const BACKGROUND_CHECK_MS = 10_000; // poll every 10 sec
|
|
33
|
-
const COUNT_POLL_MS = 3_000; // update toolcall counts every 3 sec
|
|
34
|
-
const TASK_TIMEOUT_MS = 30 * 60 * 1_000; // 30 minutes
|
|
35
33
|
// Conversation helpers live in ./conversation.js.
|
|
36
|
-
function readRegistry(piDir) {
|
|
37
|
-
const path = join(piDir, "task-registry.json");
|
|
38
|
-
try {
|
|
39
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
return [];
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
function writeRegistry(piDir, entries) {
|
|
46
|
-
const path = join(piDir, "task-registry.json");
|
|
47
|
-
writeFileSync(path, JSON.stringify(entries, null, 2), "utf-8");
|
|
48
|
-
}
|
|
49
|
-
// ─── Tmux Helpers ────────────────────────────────────────────────────────────
|
|
50
|
-
function tmuxCmd(args) {
|
|
51
|
-
return execFileSync("tmux", args, {
|
|
52
|
-
encoding: "utf-8",
|
|
53
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
54
|
-
}).trim();
|
|
55
|
-
}
|
|
56
|
-
function hasTmux() {
|
|
57
|
-
try {
|
|
58
|
-
execFileSync("tmux", ["-V"], { stdio: "ignore" });
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
function paneExists(paneId) {
|
|
66
|
-
try {
|
|
67
|
-
return tmuxCmd(["list-panes", "-a", "-F", "#{pane_id}"])
|
|
68
|
-
.split("\n")
|
|
69
|
-
.includes(paneId);
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
function getCurrentPaneId() {
|
|
76
|
-
try {
|
|
77
|
-
return tmuxCmd(["display-message", "-p", "#{pane_id}"]);
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
function getCurrentPaneSize(targetPane) {
|
|
84
|
-
try {
|
|
85
|
-
const args = ["display-message", "-p", "#{pane_width} #{pane_height}"];
|
|
86
|
-
if (targetPane)
|
|
87
|
-
args.splice(1, 0, "-t", targetPane);
|
|
88
|
-
const raw = tmuxCmd(args);
|
|
89
|
-
const [widthRaw, heightRaw] = raw.trim().split(/\s+/, 2);
|
|
90
|
-
const width = Number(widthRaw);
|
|
91
|
-
const height = Number(heightRaw);
|
|
92
|
-
if (!Number.isFinite(width) || !Number.isFinite(height))
|
|
93
|
-
return null;
|
|
94
|
-
return { width, height };
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
function splitWindowPane(cwd, command) {
|
|
101
|
-
const originalPane = getCurrentPaneId();
|
|
102
|
-
const paneSize = getCurrentPaneSize(originalPane);
|
|
103
|
-
const direction = chooseTmuxSplitDirection(paneSize?.width ?? 0, paneSize?.height ?? 0);
|
|
104
|
-
const paneId = tmuxCmd(buildTmuxSplitWindowArgs(cwd, command, direction, originalPane));
|
|
105
|
-
return { paneId, originalPane };
|
|
106
|
-
}
|
|
107
|
-
function killAgentPane(paneId, originalPane) {
|
|
108
|
-
if (paneId) {
|
|
109
|
-
try {
|
|
110
|
-
if (paneExists(paneId))
|
|
111
|
-
tmuxCmd(["kill-pane", "-t", paneId]);
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
/* ignore */
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
if (originalPane) {
|
|
118
|
-
try {
|
|
119
|
-
tmuxCmd(["select-pane", "-t", originalPane]);
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
/* ignore */
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
// ─── Process a completed task (sendMessage + registry cleanup) ──────────────
|
|
127
|
-
function completeTask(pi, id, task, content, phase, piDir) {
|
|
128
|
-
// Kill the tmux pane if still alive
|
|
129
|
-
killAgentPane(task.paneId, task.originalPane);
|
|
130
|
-
const parsed = parseResultXml(content);
|
|
131
|
-
const durationMs = Date.now() - task.startedAt;
|
|
132
|
-
// Send completion notification
|
|
133
|
-
pi.sendMessage({
|
|
134
|
-
customType: "task-complete",
|
|
135
|
-
content: `Background task ${id} (${task.agentType}) ${phase}.\n\nResult:\n${content}`,
|
|
136
|
-
display: true,
|
|
137
|
-
details: {
|
|
138
|
-
task_id: id,
|
|
139
|
-
agent_type: task.agentType,
|
|
140
|
-
description: task.description,
|
|
141
|
-
phase,
|
|
142
|
-
status: phase,
|
|
143
|
-
result: content,
|
|
144
|
-
summary: parsed.summary,
|
|
145
|
-
findings: parsed.findings,
|
|
146
|
-
confidence: parsed.confidence,
|
|
147
|
-
duration_ms: durationMs,
|
|
148
|
-
tool_uses: task.toolUses,
|
|
149
|
-
turn_count: task.turns,
|
|
150
|
-
},
|
|
151
|
-
}, {
|
|
152
|
-
triggerTurn: true,
|
|
153
|
-
deliverAs: "followUp",
|
|
154
|
-
});
|
|
155
|
-
// Remove from registry
|
|
156
|
-
const entries = readRegistry(piDir).filter((e) => e.id !== id);
|
|
157
|
-
writeRegistry(piDir, entries);
|
|
158
|
-
}
|
|
159
34
|
// ─── Extension Entry Point ──────────────────────────────────────────────────
|
|
160
35
|
export default function (pi) {
|
|
161
36
|
// Prevent recursive loading
|
|
162
37
|
if (process.env.PI_TASK_TOOL_DISABLED === "1")
|
|
163
38
|
return;
|
|
164
39
|
// ── Background task tracker ────────────────────────────────────────────
|
|
40
|
+
const { piDir } = discoverAgents(process.cwd(), BUNDLED_AGENT_DIR);
|
|
165
41
|
const backgroundTasks = new Map();
|
|
166
42
|
const foregroundTasks = new Map();
|
|
167
|
-
|
|
43
|
+
const taskWidget = createTaskWidgetController(foregroundTasks, backgroundTasks);
|
|
44
|
+
const { ensureTaskWidget, clearTaskWidgetIfIdle } = taskWidget;
|
|
168
45
|
// ── Restore active tasks from registry on load ──────────────────────────
|
|
169
|
-
|
|
170
|
-
const registry = readRegistry(piDir);
|
|
171
|
-
const staleIds = [];
|
|
172
|
-
for (const entry of registry) {
|
|
173
|
-
// Only restore if artifact dir still exists
|
|
174
|
-
if (!existsSync(entry.dir)) {
|
|
175
|
-
staleIds.push(entry.id);
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
// Check if tmux pane is still alive
|
|
179
|
-
const paneAlive = entry.paneId ? paneExists(entry.paneId) : false;
|
|
180
|
-
if (!paneAlive) {
|
|
181
|
-
staleIds.push(entry.id);
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
const bgtask = {
|
|
185
|
-
dir: entry.dir,
|
|
186
|
-
agentType: entry.agentType,
|
|
187
|
-
sessionName: entry.sessionName,
|
|
188
|
-
paneId: entry.paneId,
|
|
189
|
-
originalPane: null,
|
|
190
|
-
description: entry.description,
|
|
191
|
-
startedAt: entry.startedAt,
|
|
192
|
-
toolUses: 0,
|
|
193
|
-
turns: 0,
|
|
194
|
-
conversationId: entry.conversationId,
|
|
195
|
-
recentCalls: [],
|
|
196
|
-
};
|
|
197
|
-
backgroundTasks.set(entry.id, bgtask);
|
|
198
|
-
}
|
|
199
|
-
if (staleIds.length) {
|
|
200
|
-
writeRegistry(piDir, registry.filter((e) => !staleIds.includes(e.id)));
|
|
201
|
-
}
|
|
46
|
+
restoreActiveBackgroundTasks(piDir, backgroundTasks);
|
|
202
47
|
// ── Widget / timer setup ───────────────────────────────────────────────
|
|
203
|
-
|
|
204
|
-
function stopWidget() {
|
|
205
|
-
if (widgetTimer) {
|
|
206
|
-
clearInterval(widgetTimer);
|
|
207
|
-
widgetTimer = null;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
const countInterval = setInterval(() => {
|
|
211
|
-
for (const task of [
|
|
212
|
-
...foregroundTasks.values(),
|
|
213
|
-
...backgroundTasks.values(),
|
|
214
|
-
]) {
|
|
215
|
-
const sessionDir = join(task.dir, "sessions");
|
|
216
|
-
// Single walk: counts + recent tool-call history with status
|
|
217
|
-
const { toolUses, turns, recent } = readRecentToolCalls(sessionDir, 12);
|
|
218
|
-
task.toolUses = toolUses;
|
|
219
|
-
task.turns = turns;
|
|
220
|
-
task.recentCalls = recent;
|
|
221
|
-
}
|
|
222
|
-
}, COUNT_POLL_MS);
|
|
223
|
-
/**
|
|
224
|
-
* Render a streaming view of one active subagent. Layout per task:
|
|
225
|
-
*
|
|
226
|
-
* ⠋ Scout — SDK docs • 1m 0s 11 toolcalls (themed: accent + dim)
|
|
227
|
-
* ├─ ✓ websearch Model Context Protocol 2026 (green/success)
|
|
228
|
-
* ├─ ✓ codesearch MCP reference server typescript
|
|
229
|
-
* ├─ ✗ bash curl -sL "https://api.github.com..." (red/error)
|
|
230
|
-
* └─ ⠹ read /Users/.../scout.md (yellow/warning, animates)
|
|
231
|
-
*
|
|
232
|
-
* The header caret and in-progress tool marks share the same spinner
|
|
233
|
-
* frame set (rotates every WIDGET_RENDER_MS based on wall-clock time,
|
|
234
|
-
* so the animation cadence is stable regardless of TUI render rate).
|
|
235
|
-
*/
|
|
236
|
-
// Theme reference is captured at setWidget time so renderWidget can use it.
|
|
237
|
-
// We don't import the Theme type because it's not exported; structural typing
|
|
238
|
-
// via `any` here is safe — the c() helper only calls `theme(color, text)`.
|
|
239
|
-
let widgetTheme = null;
|
|
240
|
-
// 8-frame Braille spinner. 80ms cadence = 12.5 FPS, which is the human
|
|
241
|
-
// perception threshold for "smooth motion" (below ~10 FPS the brain
|
|
242
|
-
// sees discrete steps; above ~12 FPS it reads as continuous rotation).
|
|
243
|
-
// Full rotation: 8 × 80ms = 640ms. Used for both per-tool in-progress
|
|
244
|
-
// marks AND the header caret (the "agent is active" indicator).
|
|
245
|
-
const WIDGET_SPINNER_FRAMES = [
|
|
246
|
-
"\u280B",
|
|
247
|
-
"\u2819",
|
|
248
|
-
"\u2838",
|
|
249
|
-
"\u2834",
|
|
250
|
-
"\u2826",
|
|
251
|
-
"\u2827",
|
|
252
|
-
"\u2807",
|
|
253
|
-
"\u280F",
|
|
254
|
-
];
|
|
255
|
-
const WIDGET_CARET_FRAMES = WIDGET_SPINNER_FRAMES;
|
|
256
|
-
const WIDGET_RENDER_MS = 80;
|
|
257
|
-
const WIDGET_MAX_TOOL_LINES = 12;
|
|
258
|
-
const WIDGET_MAX_WIDTH = 120;
|
|
259
|
-
const TREE_MIDDLE = "\u251C\u2500"; // ├─
|
|
260
|
-
const TREE_LAST = "\u2514\u2500"; // └─
|
|
261
|
-
function c(color, text) {
|
|
262
|
-
// widgetTheme is a Theme object with a .fg(color, text) method,
|
|
263
|
-
// not a callable. Calling it as a function throws "widgetTheme is not
|
|
264
|
-
// a function" which the outer try/catch in renderWidget swallows.
|
|
265
|
-
return widgetTheme ? widgetTheme.fg(color, text) : text;
|
|
266
|
-
}
|
|
267
|
-
function renderWidget(width) {
|
|
268
|
-
// Defensive: never let a render exception kill the TUI. If anything
|
|
269
|
-
// throws (theme lookup miss, malformed session JSONL, etc.), fall
|
|
270
|
-
// back to a minimal single-line summary so the TUI stays alive.
|
|
271
|
-
try {
|
|
272
|
-
return renderWidgetInner(width);
|
|
273
|
-
}
|
|
274
|
-
catch (err) {
|
|
275
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
276
|
-
const active = [
|
|
277
|
-
...Array.from(foregroundTasks.entries()),
|
|
278
|
-
...Array.from(backgroundTasks.entries()),
|
|
279
|
-
];
|
|
280
|
-
if (active.length === 0)
|
|
281
|
-
return [];
|
|
282
|
-
const [, task] = active[0];
|
|
283
|
-
return [
|
|
284
|
-
truncateToWidth(`${task.agentType} \u2022 ${formatMs(Date.now() - task.startedAt)} (render error: ${msg})`, Math.min(width, WIDGET_MAX_WIDTH)),
|
|
285
|
-
];
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
function ensureTaskWidget(targetCtx) {
|
|
289
|
-
if (widgetCtx || targetCtx.mode !== "tui")
|
|
290
|
-
return;
|
|
291
|
-
widgetCtx = targetCtx;
|
|
292
|
-
targetCtx.ui.setWidget("task", (tui, theme) => {
|
|
293
|
-
widgetTheme = theme ?? null;
|
|
294
|
-
widgetTimer = setInterval(() => tui.requestRender(), WIDGET_RENDER_MS);
|
|
295
|
-
// Don't keep the process alive just for the widget refresh.
|
|
296
|
-
widgetTimer.unref?.();
|
|
297
|
-
return {
|
|
298
|
-
render: (width) => renderWidget(width),
|
|
299
|
-
invalidate: () => { },
|
|
300
|
-
dispose: () => {
|
|
301
|
-
widgetTheme = null;
|
|
302
|
-
stopWidget();
|
|
303
|
-
},
|
|
304
|
-
};
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
function clearTaskWidgetIfIdle() {
|
|
308
|
-
if (foregroundTasks.size > 0 || backgroundTasks.size > 0)
|
|
309
|
-
return;
|
|
310
|
-
stopWidget();
|
|
311
|
-
if (widgetCtx) {
|
|
312
|
-
widgetCtx.ui.setWidget("task", undefined);
|
|
313
|
-
widgetCtx = null;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
function renderWidgetInner(width) {
|
|
317
|
-
const active = [
|
|
318
|
-
...Array.from(foregroundTasks.entries()),
|
|
319
|
-
...Array.from(backgroundTasks.entries()),
|
|
320
|
-
];
|
|
321
|
-
if (active.length === 0)
|
|
322
|
-
return [];
|
|
323
|
-
const now = Date.now();
|
|
324
|
-
const maxWidth = Math.min(width, WIDGET_MAX_WIDTH);
|
|
325
|
-
const tick = Math.floor(now / WIDGET_RENDER_MS);
|
|
326
|
-
const spinner = WIDGET_SPINNER_FRAMES[tick % WIDGET_SPINNER_FRAMES.length];
|
|
327
|
-
const caret = WIDGET_CARET_FRAMES[tick % WIDGET_CARET_FRAMES.length];
|
|
328
|
-
const lines = [];
|
|
329
|
-
for (const [, task] of active) {
|
|
330
|
-
const agentName = task.agentType.charAt(0).toUpperCase() + task.agentType.slice(1);
|
|
331
|
-
const elapsed = formatMs(now - task.startedAt);
|
|
332
|
-
const total = task.toolUses > 0 ? ` ${task.turns || task.toolUses} toolcalls` : "";
|
|
333
|
-
const description = task.description ? ` — ${task.description}` : "";
|
|
334
|
-
// Header: ▼ <Agent> — <description> • 1m 0s 11 toolcalls
|
|
335
|
-
const header = c("accent", caret) +
|
|
336
|
-
" " +
|
|
337
|
-
c("toolTitle", agentName) +
|
|
338
|
-
c("dim", `${description} \u2022 ${elapsed}${total}`);
|
|
339
|
-
lines.push(truncateToWidth(header, maxWidth));
|
|
340
|
-
const recent = task.recentCalls ?? [];
|
|
341
|
-
if (recent.length > 0) {
|
|
342
|
-
const slice = recent.slice(-WIDGET_MAX_TOOL_LINES);
|
|
343
|
-
slice.forEach((tc, idx) => {
|
|
344
|
-
const isLast = idx === slice.length - 1;
|
|
345
|
-
const connector = isLast ? TREE_LAST : TREE_MIDDLE;
|
|
346
|
-
const isInProgress = tc.status === "in_progress";
|
|
347
|
-
const markChar = isInProgress
|
|
348
|
-
? spinner
|
|
349
|
-
: tc.status === "error"
|
|
350
|
-
? "\u2717"
|
|
351
|
-
: "\u2713";
|
|
352
|
-
const markColor = isInProgress
|
|
353
|
-
? "warning"
|
|
354
|
-
: tc.status === "error"
|
|
355
|
-
? "error"
|
|
356
|
-
: "success";
|
|
357
|
-
const detailStr = tc.detail ? ` ${tc.detail}` : "";
|
|
358
|
-
const line = " " +
|
|
359
|
-
c("dim", connector) +
|
|
360
|
-
" " +
|
|
361
|
-
c(markColor, markChar) +
|
|
362
|
-
" " +
|
|
363
|
-
c("text", tc.name) +
|
|
364
|
-
c("dim", detailStr);
|
|
365
|
-
lines.push(truncateToWidth(line, maxWidth));
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
lines.push("");
|
|
369
|
-
}
|
|
370
|
-
return lines;
|
|
371
|
-
}
|
|
48
|
+
const countInterval = startToolStatsPolling(foregroundTasks, backgroundTasks, COUNT_POLL_MS);
|
|
372
49
|
// ── Polling loop (background task completion, pane death, timeout) ──────
|
|
373
|
-
const checkInterval =
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (now - task.startedAt > TASK_TIMEOUT_MS) {
|
|
387
|
-
killAgentPane(task.paneId, task.originalPane);
|
|
388
|
-
completeTask(pi, id, task, "Task timed out after 30 minutes", "timeout", piDir);
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
const snapshot = await checkTaskCompletion({
|
|
392
|
-
resultPath: join(task.dir, "RESULT.md"),
|
|
393
|
-
sessionDir: task.dir,
|
|
394
|
-
sessionName: task.sessionName,
|
|
395
|
-
paneId: task.paneId,
|
|
396
|
-
});
|
|
397
|
-
if (snapshot.status === "running") {
|
|
398
|
-
backgroundTasks.set(id, task);
|
|
399
|
-
continue;
|
|
400
|
-
}
|
|
401
|
-
const phase = snapshot.status === "completed" ? "done" : "failed";
|
|
402
|
-
completeTask(pi, id, task, snapshot.content, phase, piDir);
|
|
403
|
-
}
|
|
50
|
+
const checkInterval = startBackgroundPolling({
|
|
51
|
+
backgroundTasks,
|
|
52
|
+
checkTaskCompletion,
|
|
53
|
+
killAgentPane: (paneId, originalPane) => {
|
|
54
|
+
if (paneId)
|
|
55
|
+
killAgentPane(paneId, originalPane);
|
|
56
|
+
},
|
|
57
|
+
clearTaskWidgetIfIdle,
|
|
58
|
+
completeTask,
|
|
59
|
+
TASK_TIMEOUT_MS,
|
|
60
|
+
MAX_POLL_ERRORS,
|
|
61
|
+
piDir,
|
|
62
|
+
pi,
|
|
404
63
|
}, BACKGROUND_CHECK_MS);
|
|
405
64
|
// ── Cleanup on shutdown ────────────────────────────────────────────────
|
|
406
65
|
pi.on("session_shutdown", () => {
|
|
407
66
|
clearInterval(checkInterval);
|
|
408
67
|
clearInterval(countInterval);
|
|
409
|
-
|
|
410
|
-
if (widgetCtx) {
|
|
411
|
-
widgetCtx.ui.setWidget("task", undefined);
|
|
412
|
-
widgetCtx = null;
|
|
413
|
-
}
|
|
68
|
+
taskWidget.dispose();
|
|
414
69
|
});
|
|
415
70
|
// ── Custom notification renderer ───────────────────────────────────────
|
|
416
|
-
pi.registerMessageRenderer?.("task-complete", (
|
|
417
|
-
const d = message.details;
|
|
418
|
-
if (!d)
|
|
419
|
-
return undefined;
|
|
420
|
-
const agentType = d.agent_type || "";
|
|
421
|
-
const desc = d.description || "";
|
|
422
|
-
const summary = d.summary || "";
|
|
423
|
-
const findings = d.findings || "";
|
|
424
|
-
const confidence = d.confidence || "";
|
|
425
|
-
const durationMs = d.duration_ms || 0;
|
|
426
|
-
const toolUses = d.tool_uses || 0;
|
|
427
|
-
const turns = d.turn_count || 0;
|
|
428
|
-
let line = theme.fg("accent", agentType);
|
|
429
|
-
if (desc)
|
|
430
|
-
line += theme.fg("dim", ` - ${desc}`);
|
|
431
|
-
const useStr = toolUses > 0 ? `${turns || toolUses} toolcalls` : "";
|
|
432
|
-
const durStr = durationMs >= 1000 ? formatMs(durationMs) : "";
|
|
433
|
-
const statsParts = [useStr, durStr].filter(Boolean);
|
|
434
|
-
if (statsParts.length) {
|
|
435
|
-
line += "\n" + theme.fg("dim", statsParts.join(" • "));
|
|
436
|
-
}
|
|
437
|
-
const confStr = confidence ? confidence.toUpperCase() : "";
|
|
438
|
-
if (confStr && (statsParts.length || expanded)) {
|
|
439
|
-
const confColor = confidence === "high"
|
|
440
|
-
? "success"
|
|
441
|
-
: confidence === "low"
|
|
442
|
-
? "error"
|
|
443
|
-
: "accent";
|
|
444
|
-
line += "\n" + theme.fg(confColor, `[${confStr}]`);
|
|
445
|
-
}
|
|
446
|
-
if (expanded) {
|
|
447
|
-
if (summary)
|
|
448
|
-
line += "\n" + theme.fg("muted", summary);
|
|
449
|
-
if (findings)
|
|
450
|
-
line += "\n" + theme.fg("dim", findings);
|
|
451
|
-
}
|
|
452
|
-
if (!line.trim())
|
|
453
|
-
return undefined;
|
|
454
|
-
return new Text(line, 0, 0);
|
|
455
|
-
});
|
|
71
|
+
pi.registerMessageRenderer?.("task-complete", createTaskCompleteRenderer());
|
|
456
72
|
// ── Tool Registration ──────────────────────────────────────────────────
|
|
457
73
|
pi.registerTool({
|
|
458
74
|
name: "task",
|
|
@@ -469,27 +85,7 @@ export default function (pi) {
|
|
|
469
85
|
"After delegated work completes, read changed files, review diff, verify scope, and run relevant checks",
|
|
470
86
|
"Send the user a concise summary of the result since the agent's output is not user-visible",
|
|
471
87
|
],
|
|
472
|
-
parameters:
|
|
473
|
-
agent_type: Type.String({
|
|
474
|
-
description: "The type of specialist agent to use for this task",
|
|
475
|
-
}),
|
|
476
|
-
prompt: Type.String({
|
|
477
|
-
description: "The complete task for the agent to perform. Be detailed and self-contained.",
|
|
478
|
-
}),
|
|
479
|
-
description: Type.String({
|
|
480
|
-
description: "A short (3-5 word) summary of the task",
|
|
481
|
-
}),
|
|
482
|
-
task_id: Type.Optional(Type.String({
|
|
483
|
-
description: "Resume an existing background task by id instead of starting a new task.",
|
|
484
|
-
})),
|
|
485
|
-
conversation_id: Type.Optional(Type.String({
|
|
486
|
-
description: "Durable specialist conversation id. Reuses .pi/artifacts/task-<id>/sessions when called again.",
|
|
487
|
-
})),
|
|
488
|
-
background: Type.Optional(Type.Boolean({
|
|
489
|
-
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.",
|
|
490
|
-
default: true,
|
|
491
|
-
})),
|
|
492
|
-
}),
|
|
88
|
+
parameters: taskParametersSchema(),
|
|
493
89
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
494
90
|
const { agents, piDir } = discoverAgents(ctx.cwd, BUNDLED_AGENT_DIR);
|
|
495
91
|
const parentToolNames = pi
|
|
@@ -515,14 +111,11 @@ export default function (pi) {
|
|
|
515
111
|
}
|
|
516
112
|
// ── Resolve task identity: new, task resume, or conversation resume ──
|
|
517
113
|
const conversationId = normalizeConversationId(params.conversation_id);
|
|
518
|
-
const
|
|
519
|
-
?
|
|
114
|
+
const taskSessionsRegistry = conversationId
|
|
115
|
+
? readTaskSessionsRegistry(piDir)
|
|
520
116
|
: {};
|
|
521
|
-
const
|
|
522
|
-
?
|
|
523
|
-
: undefined;
|
|
524
|
-
const registeredTaskId = registeredArtifact
|
|
525
|
-
? taskIdFromArtifactName(registeredArtifact)
|
|
117
|
+
const registeredTaskId = conversationId
|
|
118
|
+
? taskSessionsRegistry[conversationId]?.task_id
|
|
526
119
|
: undefined;
|
|
527
120
|
if (params.task_id &&
|
|
528
121
|
registeredTaskId &&
|
|
@@ -531,7 +124,7 @@ export default function (pi) {
|
|
|
531
124
|
content: [
|
|
532
125
|
{
|
|
533
126
|
type: "text",
|
|
534
|
-
text: `conversation_id "${conversationId}" maps to ${
|
|
127
|
+
text: `conversation_id "${conversationId}" maps to ${registeredTaskId}, not ${params.task_id}. Omit task_id or use the mapped task id.`,
|
|
535
128
|
},
|
|
536
129
|
],
|
|
537
130
|
details: {
|
|
@@ -543,37 +136,21 @@ export default function (pi) {
|
|
|
543
136
|
}
|
|
544
137
|
let id;
|
|
545
138
|
let sessionName;
|
|
546
|
-
let artifactDir;
|
|
547
|
-
let resultPath;
|
|
548
139
|
let resume = false;
|
|
140
|
+
let resumeSessionRef;
|
|
141
|
+
const artifactsDir = join(piDir, "artifacts");
|
|
549
142
|
if (registeredTaskId) {
|
|
550
143
|
id = registeredTaskId;
|
|
551
|
-
sessionName =
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
content: [
|
|
557
|
-
{
|
|
558
|
-
type: "text",
|
|
559
|
-
text: `conversation_id "${conversationId}" points to missing artifact directory: ${artifactDir}`,
|
|
560
|
-
},
|
|
561
|
-
],
|
|
562
|
-
details: {
|
|
563
|
-
phase: "failed",
|
|
564
|
-
error: "Conversation artifact dir missing",
|
|
565
|
-
conversation_id: conversationId,
|
|
566
|
-
},
|
|
567
|
-
isError: true,
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
const metadata = readConversationMetadata(join(artifactDir, "metadata.json"));
|
|
571
|
-
if (metadata?.agent_type && metadata.agent_type !== agent.name) {
|
|
144
|
+
sessionName = conversationId ?? `task-${id}`;
|
|
145
|
+
const block = readTaskBlock(piDir, id);
|
|
146
|
+
const previousMetadata = parseMetadataFromBody(block?.body);
|
|
147
|
+
const metadataAgent = previousMetadata?.agent_type;
|
|
148
|
+
if (metadataAgent && metadataAgent !== agent.name) {
|
|
572
149
|
return {
|
|
573
150
|
content: [
|
|
574
151
|
{
|
|
575
152
|
type: "text",
|
|
576
|
-
text: `conversation_id "${conversationId}" belongs to agent "${
|
|
153
|
+
text: `conversation_id "${conversationId}" belongs to agent "${metadataAgent}", not "${agent.name}". Use the original agent_type or start a different conversation_id.`,
|
|
577
154
|
},
|
|
578
155
|
],
|
|
579
156
|
details: {
|
|
@@ -590,7 +167,7 @@ export default function (pi) {
|
|
|
590
167
|
entry?.paneId &&
|
|
591
168
|
paneExists(entry.paneId)) {
|
|
592
169
|
const bgtask = {
|
|
593
|
-
dir:
|
|
170
|
+
dir: artifactsDir,
|
|
594
171
|
agentType: entry.agentType,
|
|
595
172
|
sessionName,
|
|
596
173
|
paneId: entry.paneId,
|
|
@@ -607,7 +184,7 @@ export default function (pi) {
|
|
|
607
184
|
content: [
|
|
608
185
|
{
|
|
609
186
|
type: "text",
|
|
610
|
-
text: `Resumed conversation "${conversationId}" via ${
|
|
187
|
+
text: `Resumed conversation "${conversationId}" via ${sessionName}. The subagent is running in background and will notify on completion.`,
|
|
611
188
|
},
|
|
612
189
|
],
|
|
613
190
|
details: {
|
|
@@ -622,15 +199,31 @@ export default function (pi) {
|
|
|
622
199
|
}
|
|
623
200
|
}
|
|
624
201
|
else if (params.task_id) {
|
|
625
|
-
// Look up
|
|
202
|
+
// Look up active tasks first, then durable completed-session history.
|
|
626
203
|
const entries = readRegistry(piDir);
|
|
627
|
-
|
|
204
|
+
let entry = entries.find((e) => e.id === params.task_id || e.sessionName === params.task_id) ??
|
|
205
|
+
findTaskSessionHistory(piDir, params.task_id) ??
|
|
206
|
+
findJsonlSessionByName(piDir, params.task_id, agent.name);
|
|
207
|
+
// Older history entries were written before we stored the
|
|
208
|
+
// actual JSONL path needed by `pi --session`. Repair them by
|
|
209
|
+
// resolving the display session name to a session file.
|
|
210
|
+
if (entry && !entry.sessionRef) {
|
|
211
|
+
const discovered = findJsonlSessionByName(piDir, entry.sessionName, entry.agentType);
|
|
212
|
+
if (discovered?.sessionRef) {
|
|
213
|
+
entry = { ...entry, sessionRef: discovered.sessionRef };
|
|
214
|
+
upsertTaskSessionHistory(piDir, {
|
|
215
|
+
...entry,
|
|
216
|
+
status: "done",
|
|
217
|
+
background: false,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
628
221
|
if (!entry) {
|
|
629
222
|
return {
|
|
630
223
|
content: [
|
|
631
224
|
{
|
|
632
225
|
type: "text",
|
|
633
|
-
text: `Unknown task_id: "${params.task_id}". No task with that ID
|
|
226
|
+
text: `Unknown task_id: "${params.task_id}". No active or completed task session with that ID/session name was found.`,
|
|
634
227
|
},
|
|
635
228
|
],
|
|
636
229
|
details: {
|
|
@@ -655,23 +248,23 @@ export default function (pi) {
|
|
|
655
248
|
isError: true,
|
|
656
249
|
};
|
|
657
250
|
}
|
|
658
|
-
// Resume: reuse existing
|
|
251
|
+
// Resume: reuse the existing session name; runtime files are
|
|
252
|
+
// flat in artifactsDir, no per-task subdir.
|
|
659
253
|
id = entry.id;
|
|
660
254
|
sessionName = entry.sessionName;
|
|
661
|
-
artifactDir = entry.dir;
|
|
662
|
-
resultPath = join(artifactDir, "RESULT.md");
|
|
663
255
|
resume = true;
|
|
256
|
+
resumeSessionRef = entry.sessionRef;
|
|
664
257
|
// If background and pane still alive, reattach to tracker
|
|
665
258
|
if (params.background !== false &&
|
|
666
259
|
entry.paneId &&
|
|
667
260
|
paneExists(entry.paneId)) {
|
|
668
261
|
const bgtask = {
|
|
669
|
-
dir:
|
|
670
|
-
agentType:
|
|
262
|
+
dir: artifactsDir,
|
|
263
|
+
agentType: entry.agentType,
|
|
671
264
|
sessionName,
|
|
672
265
|
paneId: entry.paneId,
|
|
673
266
|
originalPane: null,
|
|
674
|
-
description: params.description || entry.
|
|
267
|
+
description: params.description || entry.description,
|
|
675
268
|
startedAt: entry.startedAt,
|
|
676
269
|
toolUses: 0,
|
|
677
270
|
turns: 0,
|
|
@@ -688,21 +281,33 @@ export default function (pi) {
|
|
|
688
281
|
],
|
|
689
282
|
details: {
|
|
690
283
|
task_id: id,
|
|
691
|
-
agent_type:
|
|
692
|
-
description: params.description,
|
|
284
|
+
agent_type: entry.agentType,
|
|
285
|
+
description: params.description || entry.description,
|
|
693
286
|
conversation_id: entry.conversationId ?? conversationId,
|
|
694
287
|
tmux_session: sessionName,
|
|
695
288
|
background: true,
|
|
696
289
|
},
|
|
697
290
|
};
|
|
698
291
|
}
|
|
292
|
+
if (!resumeSessionRef) {
|
|
293
|
+
return {
|
|
294
|
+
content: [
|
|
295
|
+
{
|
|
296
|
+
type: "text",
|
|
297
|
+
text: `Task "${params.task_id}" was found, but its session JSONL file could not be resolved. Cannot resume without a --session file path.`,
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
details: {
|
|
301
|
+
phase: "failed",
|
|
302
|
+
error: "Task session file missing",
|
|
303
|
+
},
|
|
304
|
+
isError: true,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
699
307
|
}
|
|
700
308
|
else {
|
|
701
309
|
id = `${Date.now().toString(36)}-${randomUUID().slice(0, 4)}`;
|
|
702
|
-
sessionName =
|
|
703
|
-
artifactDir = join(getArtifactsDir(piDir), sessionName);
|
|
704
|
-
await mkdir(artifactDir, { recursive: true });
|
|
705
|
-
resultPath = join(artifactDir, "RESULT.md");
|
|
310
|
+
sessionName = conversationId ?? `task-${id}`;
|
|
706
311
|
}
|
|
707
312
|
if (conversationId && !hasTmux()) {
|
|
708
313
|
return {
|
|
@@ -721,58 +326,36 @@ export default function (pi) {
|
|
|
721
326
|
};
|
|
722
327
|
}
|
|
723
328
|
if (conversationId) {
|
|
724
|
-
await mkdir(
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
agentType: agent.name,
|
|
732
|
-
sessionDir: join(artifactDir, "sessions"),
|
|
733
|
-
sessionName,
|
|
734
|
-
prompt: params.prompt,
|
|
735
|
-
});
|
|
329
|
+
await mkdir(artifactsDir, { recursive: true });
|
|
330
|
+
const taskSessionsRegistry = readTaskSessionsRegistry(piDir);
|
|
331
|
+
taskSessionsRegistry[conversationId] = {
|
|
332
|
+
task_id: id,
|
|
333
|
+
session_file: `${artifactsDir}/${id}`,
|
|
334
|
+
};
|
|
335
|
+
writeTaskSessionsRegistry(piDir, taskSessionsRegistry);
|
|
736
336
|
}
|
|
737
337
|
const descText = params.description || "";
|
|
738
338
|
const isBackground = params.background ?? TASK_BACKGROUND_DEFAULT;
|
|
739
339
|
// default true
|
|
740
|
-
// ──
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
params.prompt,
|
|
750
|
-
"",
|
|
751
|
-
`## Working Directory`,
|
|
752
|
-
ctx.cwd,
|
|
753
|
-
"",
|
|
754
|
-
`## Output`,
|
|
755
|
-
`Write your result to ${resultPath}`,
|
|
756
|
-
"",
|
|
757
|
-
"Use this format:",
|
|
758
|
-
"",
|
|
759
|
-
"```",
|
|
760
|
-
TASK_RESULT_XML_INSTRUCTIONS,
|
|
761
|
-
"```",
|
|
762
|
-
].join("\n");
|
|
763
|
-
await writeFile(contextPath, contextContent, "utf-8");
|
|
764
|
-
const promptContent = [
|
|
765
|
-
`Read ${contextPath} for your task.`,
|
|
766
|
-
`Write your findings/output to ${resultPath}`,
|
|
767
|
-
"",
|
|
768
|
-
"Format:",
|
|
769
|
-
TASK_RESULT_XML_INSTRUCTIONS,
|
|
770
|
-
].join("\n");
|
|
771
|
-
const sessionDir = join(artifactDir, "sessions");
|
|
340
|
+
// ── Build the prompt (instructions are inlined; no CONTEXT.md file) ─
|
|
341
|
+
const promptContent = buildTaskPrompt({
|
|
342
|
+
description: descText,
|
|
343
|
+
agentName: agent.name,
|
|
344
|
+
agentSource: agent.source,
|
|
345
|
+
prompt: params.prompt,
|
|
346
|
+
cwd: ctx.cwd,
|
|
347
|
+
});
|
|
348
|
+
const sessionDir = join(artifactsDir, "sessions", id);
|
|
772
349
|
await mkdir(sessionDir, { recursive: true });
|
|
773
350
|
// ─── Build and run the sub-agent pi process ──────────────────────────
|
|
774
|
-
const piArgs = buildPiArgs(agent, sessionName, sessionDir, promptContent, resume, parentToolNames);
|
|
351
|
+
const piArgs = buildPiArgs(agent, sessionName, sessionDir, promptContent, resume, parentToolNames, resumeSessionRef);
|
|
775
352
|
const envPrefix = `PI_TASK_TOOL_DISABLED=1`;
|
|
353
|
+
const forceTmuxBackend = process.env.PI_TASK_BACKEND === "tmux" ||
|
|
354
|
+
process.env.PI_TASK_USE_TMUX_BACKEND === "1";
|
|
355
|
+
const forceSdkBackend = process.env.PI_TASK_BACKEND === "sdk" ||
|
|
356
|
+
process.env.PI_TASK_USE_SDK_BACKEND === "1";
|
|
357
|
+
const tmuxAvailable = hasTmux();
|
|
358
|
+
const useSdkBackend = forceSdkBackend || (!forceTmuxBackend && !tmuxAvailable);
|
|
776
359
|
const toolSelection = buildAgentToolSelection({
|
|
777
360
|
tools: agent.tools,
|
|
778
361
|
disallowedTools: agent.disallowedTools,
|
|
@@ -792,7 +375,7 @@ export default function (pi) {
|
|
|
792
375
|
const foregroundTask = isBackground
|
|
793
376
|
? undefined
|
|
794
377
|
: {
|
|
795
|
-
dir:
|
|
378
|
+
dir: artifactsDir,
|
|
796
379
|
agentType: agent.name,
|
|
797
380
|
sessionName,
|
|
798
381
|
originalPane: null,
|
|
@@ -807,11 +390,13 @@ export default function (pi) {
|
|
|
807
390
|
foregroundTasks.set(id, foregroundTask);
|
|
808
391
|
ensureTaskWidget(ctx);
|
|
809
392
|
}
|
|
810
|
-
// Prefer tmux
|
|
811
|
-
|
|
393
|
+
// Prefer tmux when the parent Pi is running inside tmux so users can watch
|
|
394
|
+
// the subagent's interactive Pi TUI. Fall back to the SDK only when tmux is
|
|
395
|
+
// unavailable, or when explicitly forced with PI_TASK_BACKEND=sdk.
|
|
396
|
+
if (useSdkBackend) {
|
|
812
397
|
if (isBackground) {
|
|
813
398
|
const bgtask = {
|
|
814
|
-
dir:
|
|
399
|
+
dir: artifactsDir,
|
|
815
400
|
agentType: agent.name,
|
|
816
401
|
sessionName,
|
|
817
402
|
originalPane: null,
|
|
@@ -830,18 +415,22 @@ export default function (pi) {
|
|
|
830
415
|
sessionName,
|
|
831
416
|
startedAt: bgtask.startedAt,
|
|
832
417
|
piDir,
|
|
833
|
-
dir:
|
|
418
|
+
dir: artifactsDir,
|
|
834
419
|
conversationId,
|
|
835
420
|
};
|
|
836
421
|
const entries = readRegistry(piDir);
|
|
837
422
|
entries.push(entry);
|
|
838
423
|
writeRegistry(piDir, entries);
|
|
424
|
+
upsertTaskSessionHistory(piDir, {
|
|
425
|
+
...entry,
|
|
426
|
+
status: "running",
|
|
427
|
+
background: true,
|
|
428
|
+
});
|
|
839
429
|
pi.appendEntry("task-registry", entry);
|
|
840
430
|
ensureTaskWidget(ctx);
|
|
841
431
|
void runSdkFallback()
|
|
842
432
|
.then(async ({ output }) => {
|
|
843
433
|
const finalOutput = output || "SDK subagent completed without assistant text.";
|
|
844
|
-
await writeFile(resultPath, finalOutput, "utf-8");
|
|
845
434
|
backgroundTasks.delete(id);
|
|
846
435
|
clearTaskWidgetIfIdle();
|
|
847
436
|
completeTask(pi, id, bgtask, finalOutput, "done", piDir);
|
|
@@ -856,14 +445,12 @@ export default function (pi) {
|
|
|
856
445
|
content: [
|
|
857
446
|
{
|
|
858
447
|
type: "text",
|
|
859
|
-
text: `Task ${id} started with SDK backend
|
|
448
|
+
text: `Task ${id} started with SDK backend.`,
|
|
860
449
|
},
|
|
861
450
|
],
|
|
862
451
|
details: {
|
|
863
452
|
task_id: id,
|
|
864
453
|
background: true,
|
|
865
|
-
backend: "sdk",
|
|
866
|
-
result_path: resultPath,
|
|
867
454
|
conversation_id: conversationId,
|
|
868
455
|
},
|
|
869
456
|
};
|
|
@@ -871,14 +458,23 @@ export default function (pi) {
|
|
|
871
458
|
try {
|
|
872
459
|
const { output, sessionPath } = await runSdkFallback();
|
|
873
460
|
const finalOutput = output || "SDK subagent completed without assistant text.";
|
|
874
|
-
|
|
461
|
+
if (conversationId) {
|
|
462
|
+
writeConversationArtifacts({
|
|
463
|
+
piDir,
|
|
464
|
+
taskId: id,
|
|
465
|
+
conversationId,
|
|
466
|
+
agentType: agent.name,
|
|
467
|
+
sessionFile: sessionPath ?? "unknown",
|
|
468
|
+
prompt: params.prompt,
|
|
469
|
+
result: finalOutput,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
875
472
|
return {
|
|
876
473
|
content: [{ type: "text", text: finalOutput }],
|
|
877
474
|
details: {
|
|
878
475
|
phase: "done",
|
|
879
476
|
backend: "sdk",
|
|
880
477
|
session_path: sessionPath,
|
|
881
|
-
result_path: resultPath,
|
|
882
478
|
conversation_id: conversationId,
|
|
883
479
|
},
|
|
884
480
|
};
|
|
@@ -903,12 +499,15 @@ export default function (pi) {
|
|
|
903
499
|
}
|
|
904
500
|
}
|
|
905
501
|
const shellCommand = `${envPrefix} pi ${piArgs.map((a) => shellQuote(a)).join(" ")}`;
|
|
502
|
+
const sessionFile = join(sessionDir, sessionName + ".jsonl");
|
|
503
|
+
const tmuxCommand = wrapWithPaneExitWatcher(sessionFile, `cd ${shellQuote(ctx.cwd)} && ${shellCommand}`);
|
|
906
504
|
let paneId;
|
|
907
505
|
let originalPane;
|
|
908
506
|
try {
|
|
909
|
-
const splitResult = splitWindowPane(ctx.cwd,
|
|
507
|
+
const splitResult = splitWindowPane(ctx.cwd, tmuxCommand);
|
|
910
508
|
paneId = splitResult.paneId;
|
|
911
509
|
originalPane = splitResult.originalPane;
|
|
510
|
+
setPaneRemainOnExit(paneId, true);
|
|
912
511
|
if (foregroundTask) {
|
|
913
512
|
foregroundTask.paneId = paneId;
|
|
914
513
|
foregroundTask.originalPane = originalPane;
|
|
@@ -930,38 +529,101 @@ export default function (pi) {
|
|
|
930
529
|
}
|
|
931
530
|
// ── FOREGROUND MODE: block until result, return directly ────────────
|
|
932
531
|
if (!isBackground) {
|
|
933
|
-
const startedAt = Date.now();
|
|
532
|
+
const startedAt = foregroundTask?.startedAt ?? Date.now();
|
|
533
|
+
upsertTaskSessionHistory(piDir, {
|
|
534
|
+
id,
|
|
535
|
+
agentType: agent.name,
|
|
536
|
+
description: descText,
|
|
537
|
+
sessionName,
|
|
538
|
+
startedAt,
|
|
539
|
+
paneId,
|
|
540
|
+
piDir,
|
|
541
|
+
dir: artifactsDir,
|
|
542
|
+
conversationId,
|
|
543
|
+
status: "running",
|
|
544
|
+
background: false,
|
|
545
|
+
});
|
|
546
|
+
// Poll tool-call progress while waiting for completion
|
|
547
|
+
let lastToolCalls = -1;
|
|
548
|
+
const onAbort = () => clearInterval(toolProgressInterval);
|
|
549
|
+
const toolProgressInterval = setInterval(() => {
|
|
550
|
+
try {
|
|
551
|
+
const stats = countToolUses(sessionDir, sessionName);
|
|
552
|
+
if (stats.toolUses > 0 && stats.toolUses !== lastToolCalls) {
|
|
553
|
+
lastToolCalls = stats.toolUses;
|
|
554
|
+
_onUpdate?.({
|
|
555
|
+
content: [
|
|
556
|
+
{
|
|
557
|
+
type: "text",
|
|
558
|
+
text: `${stats.toolUses} tool call${stats.toolUses !== 1 ? "s" : ""}`,
|
|
559
|
+
},
|
|
560
|
+
],
|
|
561
|
+
details: { toolCalls: stats.toolUses },
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
// session file may not exist yet
|
|
567
|
+
}
|
|
568
|
+
}, COUNT_POLL_MS);
|
|
569
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
934
570
|
const completion = await waitForSessionTaskCompletion({
|
|
935
|
-
resultPath,
|
|
936
571
|
sessionDir,
|
|
937
572
|
sessionName,
|
|
938
573
|
paneId,
|
|
939
574
|
signal,
|
|
940
|
-
timeoutMs:
|
|
575
|
+
timeoutMs: TASK_TIMEOUT_MS,
|
|
576
|
+
pollMs: 1000,
|
|
577
|
+
sinceMs: startedAt,
|
|
941
578
|
});
|
|
579
|
+
clearInterval(toolProgressInterval);
|
|
580
|
+
signal?.removeEventListener("abort", onAbort);
|
|
942
581
|
const content = completion.content;
|
|
943
582
|
const phase = completion.status === "completed"
|
|
944
583
|
? "done"
|
|
945
584
|
: completion.status === "cancelled"
|
|
946
585
|
? "cancelled"
|
|
947
586
|
: "failed";
|
|
948
|
-
|
|
587
|
+
const completedSessionRef = findJsonlSessionByName(piDir, sessionName, agent.name)?.sessionRef;
|
|
588
|
+
upsertTaskSessionHistory(piDir, {
|
|
589
|
+
id,
|
|
590
|
+
agentType: agent.name,
|
|
591
|
+
description: descText,
|
|
592
|
+
sessionName,
|
|
593
|
+
startedAt,
|
|
594
|
+
paneId,
|
|
595
|
+
piDir,
|
|
596
|
+
dir: artifactsDir,
|
|
597
|
+
conversationId,
|
|
598
|
+
sessionRef: completedSessionRef,
|
|
599
|
+
status: phase,
|
|
600
|
+
completedAt: Date.now(),
|
|
601
|
+
background: false,
|
|
602
|
+
});
|
|
603
|
+
if (phase === "done") {
|
|
604
|
+
killAgentPane(paneId, originalPane);
|
|
605
|
+
}
|
|
949
606
|
foregroundTasks.delete(id);
|
|
950
607
|
clearTaskWidgetIfIdle();
|
|
608
|
+
if (conversationId) {
|
|
609
|
+
writeConversationArtifacts({
|
|
610
|
+
piDir,
|
|
611
|
+
taskId: id,
|
|
612
|
+
conversationId,
|
|
613
|
+
agentType: agent.name,
|
|
614
|
+
sessionFile: `${sessionDir}/${sessionName}`,
|
|
615
|
+
prompt: params.prompt,
|
|
616
|
+
result: content,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
951
619
|
const parsed = parseResultXml(content);
|
|
952
620
|
const durationMs = Date.now() - startedAt;
|
|
953
|
-
const { toolUses, turns } = countToolUses(sessionDir);
|
|
621
|
+
const { toolUses, turns } = countToolUses(sessionDir, sessionName);
|
|
954
622
|
return {
|
|
955
623
|
content: [
|
|
956
624
|
{
|
|
957
625
|
type: "text",
|
|
958
|
-
text:
|
|
959
|
-
`${parsed.status || "done"}: ${parsed.summary || content.slice(0, 300)}`,
|
|
960
|
-
toolUses > 0 ? `\n${turns || toolUses} toolcalls` : "",
|
|
961
|
-
durationMs >= 1000 ? `\n${formatMs(durationMs)}` : "",
|
|
962
|
-
]
|
|
963
|
-
.filter(Boolean)
|
|
964
|
-
.join(""),
|
|
626
|
+
text: parsed.summary || content.trim(),
|
|
965
627
|
},
|
|
966
628
|
],
|
|
967
629
|
details: {
|
|
@@ -969,7 +631,7 @@ export default function (pi) {
|
|
|
969
631
|
agent_type: agent.name,
|
|
970
632
|
description: descText,
|
|
971
633
|
phase,
|
|
972
|
-
status:
|
|
634
|
+
status: "done",
|
|
973
635
|
summary: parsed.summary || "",
|
|
974
636
|
findings: parsed.findings || "",
|
|
975
637
|
evidence: parsed.evidence || "",
|
|
@@ -984,7 +646,7 @@ export default function (pi) {
|
|
|
984
646
|
}
|
|
985
647
|
// ── BACKGROUND MODE (default): add to tracker, return immediately ─────
|
|
986
648
|
const bgtask = {
|
|
987
|
-
dir:
|
|
649
|
+
dir: artifactsDir,
|
|
988
650
|
agentType: agent.name,
|
|
989
651
|
sessionName,
|
|
990
652
|
paneId,
|
|
@@ -1003,16 +665,21 @@ export default function (pi) {
|
|
|
1003
665
|
agentType: agent.name,
|
|
1004
666
|
description: descText,
|
|
1005
667
|
sessionName,
|
|
1006
|
-
startedAt:
|
|
668
|
+
startedAt: bgtask.startedAt,
|
|
1007
669
|
paneId,
|
|
1008
670
|
piDir,
|
|
1009
|
-
dir:
|
|
671
|
+
dir: artifactsDir,
|
|
1010
672
|
conversationId,
|
|
1011
673
|
};
|
|
1012
674
|
// Write to JSON registry for on-load restore
|
|
1013
675
|
const entries = readRegistry(piDir);
|
|
1014
676
|
entries.push(entry);
|
|
1015
677
|
writeRegistry(piDir, entries);
|
|
678
|
+
upsertTaskSessionHistory(piDir, {
|
|
679
|
+
...entry,
|
|
680
|
+
status: "running",
|
|
681
|
+
background: true,
|
|
682
|
+
});
|
|
1016
683
|
// Also persist to session store via appendEntry (audit trail)
|
|
1017
684
|
pi.appendEntry("task-registry", entry);
|
|
1018
685
|
// ── Abort signal handling ──────────────────────────────────────────
|
|
@@ -1024,13 +691,7 @@ export default function (pi) {
|
|
|
1024
691
|
// Clean registry
|
|
1025
692
|
const remaining = readRegistry(piDir).filter((e) => e.id !== id);
|
|
1026
693
|
writeRegistry(piDir, remaining);
|
|
1027
|
-
|
|
1028
|
-
stopWidget();
|
|
1029
|
-
if (widgetCtx) {
|
|
1030
|
-
widgetCtx.ui.setWidget("task", undefined);
|
|
1031
|
-
widgetCtx = null;
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
694
|
+
clearTaskWidgetIfIdle();
|
|
1034
695
|
}, { once: true });
|
|
1035
696
|
}
|
|
1036
697
|
// ── Sticky widget ──────────────────────────────────────────────────
|
|
@@ -1043,7 +704,7 @@ export default function (pi) {
|
|
|
1043
704
|
taskId: id,
|
|
1044
705
|
agentType: agent.name,
|
|
1045
706
|
tmuxSession: sessionName,
|
|
1046
|
-
artifactDir,
|
|
707
|
+
artifactDir: artifactsDir,
|
|
1047
708
|
}),
|
|
1048
709
|
},
|
|
1049
710
|
],
|
|
@@ -1056,70 +717,8 @@ export default function (pi) {
|
|
|
1056
717
|
},
|
|
1057
718
|
};
|
|
1058
719
|
},
|
|
1059
|
-
renderCall
|
|
1060
|
-
|
|
1061
|
-
const desc = args.description || "";
|
|
1062
|
-
let text = theme.fg("toolTitle", "");
|
|
1063
|
-
text += theme.fg("accent", agentName);
|
|
1064
|
-
if (desc)
|
|
1065
|
-
text += theme.fg("dim", ` - ${desc}`);
|
|
1066
|
-
return new Text(text, 0, 0);
|
|
1067
|
-
},
|
|
1068
|
-
renderResult(result, { expanded }, theme, _context) {
|
|
1069
|
-
const d = result.details;
|
|
1070
|
-
if (!d)
|
|
1071
|
-
return new Text("", 0, 0);
|
|
1072
|
-
if (d.background) {
|
|
1073
|
-
return new Text("", 0, 0);
|
|
1074
|
-
}
|
|
1075
|
-
if (d.phase === "timeout" ||
|
|
1076
|
-
d.phase === "aborted" ||
|
|
1077
|
-
d.phase === "failed") {
|
|
1078
|
-
const line = theme.fg("error", "x") + " " + theme.fg("dim", `[${d.phase}]`);
|
|
1079
|
-
return new Text(line, 0, 0);
|
|
1080
|
-
}
|
|
1081
|
-
const isError = d.status === "failure" ||
|
|
1082
|
-
d.status === "blocked" ||
|
|
1083
|
-
d.status === "unknown" ||
|
|
1084
|
-
d.status === "timeout" ||
|
|
1085
|
-
d.status === "failed";
|
|
1086
|
-
const durationMs = d.duration_ms || 0;
|
|
1087
|
-
const toolUses = d.tool_uses || 0;
|
|
1088
|
-
const turns = d.turn_count || 0;
|
|
1089
|
-
const useStr = toolUses > 0 ? `${turns || toolUses} toolcalls` : "";
|
|
1090
|
-
const durStr = durationMs >= 1000 ? formatMs(durationMs) : "";
|
|
1091
|
-
const statsParts = [useStr, durStr].filter(Boolean);
|
|
1092
|
-
const statsStr = statsParts.length
|
|
1093
|
-
? " " + theme.fg("dim", statsParts.join(" • "))
|
|
1094
|
-
: "";
|
|
1095
|
-
const icon = isError ? theme.fg("error", "x") : theme.fg("success", "✓");
|
|
1096
|
-
const statusLabel = d.status && d.status !== "done" ? d.status : "done";
|
|
1097
|
-
let line = icon +
|
|
1098
|
-
" " +
|
|
1099
|
-
theme.fg(isError ? "error" : "success", statusLabel) +
|
|
1100
|
-
statsStr;
|
|
1101
|
-
if (expanded) {
|
|
1102
|
-
const s = d.summary || "";
|
|
1103
|
-
const f = d.findings || "";
|
|
1104
|
-
const e = d.evidence || "";
|
|
1105
|
-
if (s)
|
|
1106
|
-
line += "\n" + theme.fg("muted", s);
|
|
1107
|
-
if (f)
|
|
1108
|
-
line += "\n" + theme.fg("dim", f);
|
|
1109
|
-
if (e)
|
|
1110
|
-
line += "\n" + theme.fg("muted", "Evidence: ") + theme.fg("dim", e);
|
|
1111
|
-
}
|
|
1112
|
-
else {
|
|
1113
|
-
const preview = (d.summary || "").slice(0, 80);
|
|
1114
|
-
if (preview)
|
|
1115
|
-
line += "\n" + theme.fg("dim", ` ⎿ ${preview}`);
|
|
1116
|
-
else
|
|
1117
|
-
line +=
|
|
1118
|
-
"\n" +
|
|
1119
|
-
theme.fg("dim", ` ⎿ ${isError ? d.status || "error" : "Done"}`);
|
|
1120
|
-
}
|
|
1121
|
-
return new Text(line, 0, 0);
|
|
1122
|
-
},
|
|
720
|
+
renderCall,
|
|
721
|
+
renderResult,
|
|
1123
722
|
});
|
|
1124
723
|
pi.registerCommand("task-sessions", {
|
|
1125
724
|
description: "List durable pi-task conversations",
|