@bermudi/pi-delegate 0.1.13 → 0.1.15
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/README.md +14 -17
- package/delegate.ts +0 -8
- package/dispatch.ts +25 -2
- package/extension.ts +14 -6
- package/file-tracking.ts +286 -15
- package/format.ts +107 -21
- package/isolated-workspace.ts +550 -62
- package/key-hints.ts +38 -0
- package/lifecycle.ts +559 -80
- package/manual.ts +1 -1
- package/model.ts +6 -1
- package/package.json +13 -9
- package/pool.ts +37 -6
- package/quiescence.ts +19 -6
- package/render-branches.ts +243 -85
- package/render-result.ts +116 -16
- package/runner.ts +361 -82
- package/session-quarantine.ts +266 -0
- package/spill.ts +93 -22
- package/task-resolution.ts +63 -50
- package/telemetry.ts +1 -1
- package/ticket-format.ts +15 -9
- package/tickets.ts +36 -14
- package/tools.ts +5 -4
- package/types.ts +92 -8
- package/utils.ts +123 -2
- package/workspace.ts +133 -0
- package/settings.ts +0 -418
package/format.ts
CHANGED
|
@@ -3,6 +3,8 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { renderOutputForLLM } from "./spill.ts";
|
|
5
5
|
import { getOutputSpillThreshold, getOutputSpillTail } from "./config.ts";
|
|
6
|
+
import { toolExpandHint } from "./key-hints.ts";
|
|
7
|
+
import { sanitizeTerminalLine } from "./utils.ts";
|
|
6
8
|
import type {
|
|
7
9
|
ResolvedTask,
|
|
8
10
|
TaskProgress,
|
|
@@ -136,9 +138,13 @@ export function applyLineBudget(lines: string[], expanded: boolean): string[] {
|
|
|
136
138
|
const budget = Math.max(10, Math.min(18, Math.floor(rows * 0.4)));
|
|
137
139
|
if (lines.length <= budget) return [...lines];
|
|
138
140
|
const hidden = lines.length - budget + 1;
|
|
141
|
+
const expandHint = toolExpandHint("expands");
|
|
139
142
|
return [
|
|
140
143
|
...lines.slice(0, budget - 1),
|
|
141
|
-
truncLine(
|
|
144
|
+
truncLine(
|
|
145
|
+
`… ${hidden} lines hidden${expandHint ? ` · ${expandHint}` : ""}`,
|
|
146
|
+
getTermWidth(),
|
|
147
|
+
),
|
|
142
148
|
];
|
|
143
149
|
}
|
|
144
150
|
|
|
@@ -189,7 +195,36 @@ export function trunc(s: string, n: number): string {
|
|
|
189
195
|
|
|
190
196
|
/** Render an optional task `id` in a compact, visually distinct form. */
|
|
191
197
|
export function formatTaskId(id: string | undefined): string {
|
|
192
|
-
|
|
198
|
+
if (!id) return "";
|
|
199
|
+
const safeId = sanitizeTerminalLine(id);
|
|
200
|
+
return safeId ? ` #${safeId}` : "";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Short identity tag for a resumed transcript, derived from its session file
|
|
205
|
+
* path (pi names sessions `<timestamp>_<uuid>.jsonl`, so the UUID prefix is
|
|
206
|
+
* the stable part). Best-effort display identity only — never parsed back.
|
|
207
|
+
*/
|
|
208
|
+
export function formatResumeTag(resumeFrom: string): string {
|
|
209
|
+
const stem = sanitizeTerminalLine(resumeFrom).replace(/\.jsonl$/i, "");
|
|
210
|
+
const base = stem.split("/").pop() ?? stem;
|
|
211
|
+
const unique = base.includes("_") ? (base.split("_").pop() ?? base) : base;
|
|
212
|
+
return (unique.slice(0, 8) || "resumed").trim();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Plain-text revival marker for a progress row that continued an earlier
|
|
217
|
+
* transcript. Empty when the row is not a resume, or when the agent name
|
|
218
|
+
* already carries the resume identity (`resume:<tag>`, set at task resolution
|
|
219
|
+
* for omitted-agent resumes) — the marker must not duplicate it.
|
|
220
|
+
*/
|
|
221
|
+
export function resumeMarker(p: {
|
|
222
|
+
agent: string;
|
|
223
|
+
resumedFrom?: string;
|
|
224
|
+
}): string {
|
|
225
|
+
return p.resumedFrom && p.agent !== `resume:${p.resumedFrom}`
|
|
226
|
+
? ` ↻${p.resumedFrom}`
|
|
227
|
+
: "";
|
|
193
228
|
}
|
|
194
229
|
|
|
195
230
|
/**
|
|
@@ -206,7 +241,7 @@ export function previewOutputLine(output: string, maxWidth: number): string {
|
|
|
206
241
|
const clean = output.trim();
|
|
207
242
|
if (!clean || clean === "(no output)") return "";
|
|
208
243
|
for (const raw of clean.split("\n")) {
|
|
209
|
-
const line = raw
|
|
244
|
+
const line = sanitizeTerminalLine(raw);
|
|
210
245
|
if (!line) continue;
|
|
211
246
|
// Strip leading markdown noise so the preview reads as content, not markup.
|
|
212
247
|
const stripped = line
|
|
@@ -241,20 +276,23 @@ function firstArg(
|
|
|
241
276
|
return undefined;
|
|
242
277
|
}
|
|
243
278
|
|
|
244
|
-
/** Render a compact,
|
|
279
|
+
/** Render a compact, terminal-safe, single-line summary of a tool call. */
|
|
245
280
|
export function formatToolCallShort(
|
|
246
281
|
name: string,
|
|
247
282
|
args: Record<string, unknown>,
|
|
248
283
|
): string {
|
|
249
|
-
|
|
284
|
+
const safeName = sanitizeTerminalLine(name);
|
|
285
|
+
if (!args || typeof args !== "object") return safeName;
|
|
250
286
|
switch (name) {
|
|
251
287
|
case "bash": {
|
|
252
|
-
const cmd = firstArg(args, "command") ?? "...";
|
|
288
|
+
const cmd = sanitizeTerminalLine(firstArg(args, "command") ?? "...");
|
|
253
289
|
const maxLen = 80;
|
|
254
290
|
return `$ ${cmd.length > maxLen ? cmd.slice(0, maxLen) + "…" : cmd}`;
|
|
255
291
|
}
|
|
256
292
|
case "read": {
|
|
257
|
-
const p = shortenPath(
|
|
293
|
+
const p = shortenPath(
|
|
294
|
+
sanitizeTerminalLine(firstArg(args, "path", ["file_path"]) ?? "..."),
|
|
295
|
+
);
|
|
258
296
|
const offset = typeof args.offset === "number" ? args.offset : undefined;
|
|
259
297
|
const limit = typeof args.limit === "number" ? args.limit : undefined;
|
|
260
298
|
let line = `read ${p}`;
|
|
@@ -266,16 +304,20 @@ export function formatToolCallShort(
|
|
|
266
304
|
return line;
|
|
267
305
|
}
|
|
268
306
|
case "write": {
|
|
269
|
-
const p = shortenPath(
|
|
307
|
+
const p = shortenPath(
|
|
308
|
+
sanitizeTerminalLine(firstArg(args, "path", ["file_path"]) ?? "..."),
|
|
309
|
+
);
|
|
270
310
|
const lines = String(args.content ?? "").split("\n").length;
|
|
271
311
|
return `write ${p}${lines > 1 ? ` (${lines} lines)` : ""}`;
|
|
272
312
|
}
|
|
273
313
|
case "edit": {
|
|
274
|
-
const p = shortenPath(
|
|
314
|
+
const p = shortenPath(
|
|
315
|
+
sanitizeTerminalLine(firstArg(args, "path", ["file_path"]) ?? "..."),
|
|
316
|
+
);
|
|
275
317
|
return `edit ${p}`;
|
|
276
318
|
}
|
|
277
319
|
default: {
|
|
278
|
-
// Try to pick a meaningful first arg before falling back to JSON
|
|
320
|
+
// Try to pick a meaningful first arg before falling back to JSON.
|
|
279
321
|
for (const key of [
|
|
280
322
|
"command",
|
|
281
323
|
"path",
|
|
@@ -288,15 +330,20 @@ export function formatToolCallShort(
|
|
|
288
330
|
]) {
|
|
289
331
|
const val = args[key];
|
|
290
332
|
if (typeof val === "string" && val.trim()) {
|
|
291
|
-
const
|
|
292
|
-
|
|
333
|
+
const safeValue = sanitizeTerminalLine(val);
|
|
334
|
+
const preview =
|
|
335
|
+
safeValue.length > 50 ? safeValue.slice(0, 50) + "…" : safeValue;
|
|
336
|
+
return `${safeName} ${preview}`;
|
|
293
337
|
}
|
|
294
338
|
}
|
|
295
339
|
try {
|
|
296
|
-
const
|
|
297
|
-
|
|
340
|
+
const json = JSON.stringify(args);
|
|
341
|
+
if (json === undefined) return safeName;
|
|
342
|
+
const serialized = sanitizeTerminalLine(json);
|
|
343
|
+
const preview = serialized.slice(0, 50);
|
|
344
|
+
return `${safeName} ${preview}${serialized.length > 50 ? "…" : ""}`;
|
|
298
345
|
} catch {
|
|
299
|
-
return
|
|
346
|
+
return safeName;
|
|
300
347
|
}
|
|
301
348
|
}
|
|
302
349
|
}
|
|
@@ -338,6 +385,10 @@ function isResumableSessionFile(sessionFile: string): boolean {
|
|
|
338
385
|
return false;
|
|
339
386
|
}
|
|
340
387
|
|
|
388
|
+
/** Shared LLM-facing warning for results returned before proven quiescence. */
|
|
389
|
+
export const INCOMPLETE_QUIESCENCE_WARNING =
|
|
390
|
+
"[INCOMPLETE: quiescence was abandoned; output, file evidence, token usage, and cost are lower bounds while the quarantined session settles.]";
|
|
391
|
+
|
|
341
392
|
/**
|
|
342
393
|
* Render a failed or aborted task's result lines for LLM consumption.
|
|
343
394
|
*
|
|
@@ -364,7 +415,10 @@ export function formatFailedTask(
|
|
|
364
415
|
config?: import("./config.ts").DelegateConfig,
|
|
365
416
|
): string[] {
|
|
366
417
|
const parts: string[] = [];
|
|
367
|
-
|
|
418
|
+
if (r.incomplete === "quiescence_abandoned") {
|
|
419
|
+
parts.push(INCOMPLETE_QUIESCENCE_WARNING);
|
|
420
|
+
}
|
|
421
|
+
const isAbort = r.failureKind === "cancelled";
|
|
368
422
|
// Empty string is falsy but not nullish — `||` covers both undefined and "".
|
|
369
423
|
const failParts = [r.error || "unknown error"];
|
|
370
424
|
if (r.sessionFile) failParts.push(`session: ${shortenPath(r.sessionFile)}`);
|
|
@@ -436,7 +490,7 @@ export function formatCompletedTask(
|
|
|
436
490
|
// `|| task.sessionAction` covers action-only tasks (close/list/...) where prompt is
|
|
437
491
|
// empty. Async prompt tasks always set prompt, so this is a no-op there.
|
|
438
492
|
parts.push(
|
|
439
|
-
`=== ${result.agent}${formatTaskId(result.id ?? task.id)}: ${trunc(task.prompt || task.sessionAction || "", 80)} ===`,
|
|
493
|
+
`=== ${result.agent}${resumeMarker(result)}${formatTaskId(result.id ?? task.id)}: ${trunc(task.prompt || task.sessionAction || "", 80)} ===`,
|
|
440
494
|
);
|
|
441
495
|
if (task.warnings?.length) {
|
|
442
496
|
for (const w of task.warnings) parts.push(`[WARNING: ${w}]`);
|
|
@@ -444,8 +498,11 @@ export function formatCompletedTask(
|
|
|
444
498
|
if (result.error) {
|
|
445
499
|
parts.push(...formatFailedTask(result, task.cwd, config));
|
|
446
500
|
} else {
|
|
501
|
+
if (result.incomplete === "quiescence_abandoned") {
|
|
502
|
+
parts.push(INCOMPLETE_QUIESCENCE_WARNING);
|
|
503
|
+
}
|
|
447
504
|
const meta = [
|
|
448
|
-
`OK | ${fmtDuration(result.durationMs)} | ${
|
|
505
|
+
`OK | ${fmtDuration(result.durationMs)} | ${taskTokenLabel(result)}`,
|
|
449
506
|
];
|
|
450
507
|
if (result.sessionFile) meta.push(shortenPath(result.sessionFile));
|
|
451
508
|
const touched = relativeTouchedSummary(result.touchedFiles, task.cwd);
|
|
@@ -477,6 +534,23 @@ export function formatCompletedTask(
|
|
|
477
534
|
for (const conflict of integration.conflicts ?? []) {
|
|
478
535
|
parts.push(`conflict: ${conflict.path}: ${conflict.reason}`);
|
|
479
536
|
}
|
|
537
|
+
for (const issue of "classificationIssues" in integration
|
|
538
|
+
? (integration.classificationIssues ?? [])
|
|
539
|
+
: []) {
|
|
540
|
+
parts.push(
|
|
541
|
+
`attribution classification issue: ${issue.path}: ${issue.reason}`,
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
if (integration.cleanupIssue) {
|
|
545
|
+
parts.push(
|
|
546
|
+
`cleanup ${integration.cleanupIssue.status}: ${integration.cleanupIssue.reason}`,
|
|
547
|
+
);
|
|
548
|
+
if (integration.cleanupIssue.recoveryPath) {
|
|
549
|
+
parts.push(
|
|
550
|
+
`cleanup recovery path: ${integration.cleanupIssue.recoveryPath}`,
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
480
554
|
if (integration.status === "applied_unverified") {
|
|
481
555
|
parts.push(
|
|
482
556
|
'Changes were applied but not verified. Suggested next call: delegate({ tasks: [{ agent: "reviewer", workspace: "scratch", prompt: "Review the applied isolated changes and run the relevant tests." }] })',
|
|
@@ -511,7 +585,9 @@ export function latestActivity(p: TaskProgress): ToolActivity | null {
|
|
|
511
585
|
export function formatActivityLabel(p: TaskProgress): string {
|
|
512
586
|
const activity = inFlightActivity(p) ?? latestActivity(p);
|
|
513
587
|
if (!activity) return "thinking";
|
|
514
|
-
const call =
|
|
588
|
+
const call = sanitizeTerminalLine(
|
|
589
|
+
formatToolCallShort(activity.name, activity.args),
|
|
590
|
+
);
|
|
515
591
|
if (!activity.result) return call;
|
|
516
592
|
return `last: ${call}`;
|
|
517
593
|
}
|
|
@@ -522,7 +598,9 @@ export function formatActivityLabel(p: TaskProgress): string {
|
|
|
522
598
|
export function compactActivity(p: TaskProgress): string {
|
|
523
599
|
const activity = inFlightActivity(p) ?? latestActivity(p);
|
|
524
600
|
if (!activity) return "thinking…";
|
|
525
|
-
const call =
|
|
601
|
+
const call = sanitizeTerminalLine(
|
|
602
|
+
formatToolCallShort(activity.name, activity.args),
|
|
603
|
+
);
|
|
526
604
|
if (!activity.result) {
|
|
527
605
|
const toolAge = fmtDuration(Date.now() - activity.startTime);
|
|
528
606
|
return `${call} | ${toolAge}`;
|
|
@@ -533,8 +611,16 @@ export function compactActivity(p: TaskProgress): string {
|
|
|
533
611
|
|
|
534
612
|
/** Per-task stats core: [duration, tokens]. Callers append medium-specific
|
|
535
613
|
* extras (touched files; themed join). */
|
|
614
|
+
export function taskTokenLabel(
|
|
615
|
+
result: Pick<TaskResult, "tokens" | "incomplete">,
|
|
616
|
+
): string {
|
|
617
|
+
return result.incomplete === "quiescence_abandoned"
|
|
618
|
+
? `≥${fmtTokens(result.tokens)} tokens (incomplete)`
|
|
619
|
+
: `${fmtTokens(result.tokens)} tokens`;
|
|
620
|
+
}
|
|
621
|
+
|
|
536
622
|
export function taskMetaBase(r: TaskResult): string[] {
|
|
537
|
-
return [fmtDuration(r.durationMs),
|
|
623
|
+
return [fmtDuration(r.durationMs), taskTokenLabel(r)];
|
|
538
624
|
}
|
|
539
625
|
|
|
540
626
|
/** Pending-task waiting label: "queued (N running)" at the concurrency cap,
|