@bermudi/pi-delegate 0.1.12 → 0.1.14
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 +24 -2
- package/extension.ts +14 -6
- package/file-tracking.ts +286 -15
- package/format.ts +79 -20
- package/isolated-workspace.ts +550 -62
- package/key-hints.ts +38 -0
- package/lifecycle.ts +555 -80
- package/manual.ts +10 -7
- package/model.ts +6 -1
- package/package.json +13 -9
- package/pool.ts +37 -6
- package/quiescence.ts +19 -6
- package/render-branches.ts +232 -85
- package/render-result.ts +116 -16
- package/runner.ts +361 -82
- package/schema.ts +2 -2
- package/session-quarantine.ts +266 -0
- package/spill.ts +93 -22
- package/task-resolution.ts +47 -48
- package/telemetry.ts +1 -1
- package/ticket-format.ts +8 -3
- package/tickets.ts +33 -13
- package/tools.ts +5 -4
- package/types.ts +77 -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,9 @@ 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}` : "";
|
|
193
201
|
}
|
|
194
202
|
|
|
195
203
|
/**
|
|
@@ -206,7 +214,7 @@ export function previewOutputLine(output: string, maxWidth: number): string {
|
|
|
206
214
|
const clean = output.trim();
|
|
207
215
|
if (!clean || clean === "(no output)") return "";
|
|
208
216
|
for (const raw of clean.split("\n")) {
|
|
209
|
-
const line = raw
|
|
217
|
+
const line = sanitizeTerminalLine(raw);
|
|
210
218
|
if (!line) continue;
|
|
211
219
|
// Strip leading markdown noise so the preview reads as content, not markup.
|
|
212
220
|
const stripped = line
|
|
@@ -241,20 +249,23 @@ function firstArg(
|
|
|
241
249
|
return undefined;
|
|
242
250
|
}
|
|
243
251
|
|
|
244
|
-
/** Render a compact,
|
|
252
|
+
/** Render a compact, terminal-safe, single-line summary of a tool call. */
|
|
245
253
|
export function formatToolCallShort(
|
|
246
254
|
name: string,
|
|
247
255
|
args: Record<string, unknown>,
|
|
248
256
|
): string {
|
|
249
|
-
|
|
257
|
+
const safeName = sanitizeTerminalLine(name);
|
|
258
|
+
if (!args || typeof args !== "object") return safeName;
|
|
250
259
|
switch (name) {
|
|
251
260
|
case "bash": {
|
|
252
|
-
const cmd = firstArg(args, "command") ?? "...";
|
|
261
|
+
const cmd = sanitizeTerminalLine(firstArg(args, "command") ?? "...");
|
|
253
262
|
const maxLen = 80;
|
|
254
263
|
return `$ ${cmd.length > maxLen ? cmd.slice(0, maxLen) + "…" : cmd}`;
|
|
255
264
|
}
|
|
256
265
|
case "read": {
|
|
257
|
-
const p = shortenPath(
|
|
266
|
+
const p = shortenPath(
|
|
267
|
+
sanitizeTerminalLine(firstArg(args, "path", ["file_path"]) ?? "..."),
|
|
268
|
+
);
|
|
258
269
|
const offset = typeof args.offset === "number" ? args.offset : undefined;
|
|
259
270
|
const limit = typeof args.limit === "number" ? args.limit : undefined;
|
|
260
271
|
let line = `read ${p}`;
|
|
@@ -266,16 +277,20 @@ export function formatToolCallShort(
|
|
|
266
277
|
return line;
|
|
267
278
|
}
|
|
268
279
|
case "write": {
|
|
269
|
-
const p = shortenPath(
|
|
280
|
+
const p = shortenPath(
|
|
281
|
+
sanitizeTerminalLine(firstArg(args, "path", ["file_path"]) ?? "..."),
|
|
282
|
+
);
|
|
270
283
|
const lines = String(args.content ?? "").split("\n").length;
|
|
271
284
|
return `write ${p}${lines > 1 ? ` (${lines} lines)` : ""}`;
|
|
272
285
|
}
|
|
273
286
|
case "edit": {
|
|
274
|
-
const p = shortenPath(
|
|
287
|
+
const p = shortenPath(
|
|
288
|
+
sanitizeTerminalLine(firstArg(args, "path", ["file_path"]) ?? "..."),
|
|
289
|
+
);
|
|
275
290
|
return `edit ${p}`;
|
|
276
291
|
}
|
|
277
292
|
default: {
|
|
278
|
-
// Try to pick a meaningful first arg before falling back to JSON
|
|
293
|
+
// Try to pick a meaningful first arg before falling back to JSON.
|
|
279
294
|
for (const key of [
|
|
280
295
|
"command",
|
|
281
296
|
"path",
|
|
@@ -288,15 +303,20 @@ export function formatToolCallShort(
|
|
|
288
303
|
]) {
|
|
289
304
|
const val = args[key];
|
|
290
305
|
if (typeof val === "string" && val.trim()) {
|
|
291
|
-
const
|
|
292
|
-
|
|
306
|
+
const safeValue = sanitizeTerminalLine(val);
|
|
307
|
+
const preview =
|
|
308
|
+
safeValue.length > 50 ? safeValue.slice(0, 50) + "…" : safeValue;
|
|
309
|
+
return `${safeName} ${preview}`;
|
|
293
310
|
}
|
|
294
311
|
}
|
|
295
312
|
try {
|
|
296
|
-
const
|
|
297
|
-
|
|
313
|
+
const json = JSON.stringify(args);
|
|
314
|
+
if (json === undefined) return safeName;
|
|
315
|
+
const serialized = sanitizeTerminalLine(json);
|
|
316
|
+
const preview = serialized.slice(0, 50);
|
|
317
|
+
return `${safeName} ${preview}${serialized.length > 50 ? "…" : ""}`;
|
|
298
318
|
} catch {
|
|
299
|
-
return
|
|
319
|
+
return safeName;
|
|
300
320
|
}
|
|
301
321
|
}
|
|
302
322
|
}
|
|
@@ -338,6 +358,10 @@ function isResumableSessionFile(sessionFile: string): boolean {
|
|
|
338
358
|
return false;
|
|
339
359
|
}
|
|
340
360
|
|
|
361
|
+
/** Shared LLM-facing warning for results returned before proven quiescence. */
|
|
362
|
+
export const INCOMPLETE_QUIESCENCE_WARNING =
|
|
363
|
+
"[INCOMPLETE: quiescence was abandoned; output, file evidence, token usage, and cost are lower bounds while the quarantined session settles.]";
|
|
364
|
+
|
|
341
365
|
/**
|
|
342
366
|
* Render a failed or aborted task's result lines for LLM consumption.
|
|
343
367
|
*
|
|
@@ -364,7 +388,10 @@ export function formatFailedTask(
|
|
|
364
388
|
config?: import("./config.ts").DelegateConfig,
|
|
365
389
|
): string[] {
|
|
366
390
|
const parts: string[] = [];
|
|
367
|
-
|
|
391
|
+
if (r.incomplete === "quiescence_abandoned") {
|
|
392
|
+
parts.push(INCOMPLETE_QUIESCENCE_WARNING);
|
|
393
|
+
}
|
|
394
|
+
const isAbort = r.failureKind === "cancelled";
|
|
368
395
|
// Empty string is falsy but not nullish — `||` covers both undefined and "".
|
|
369
396
|
const failParts = [r.error || "unknown error"];
|
|
370
397
|
if (r.sessionFile) failParts.push(`session: ${shortenPath(r.sessionFile)}`);
|
|
@@ -444,8 +471,11 @@ export function formatCompletedTask(
|
|
|
444
471
|
if (result.error) {
|
|
445
472
|
parts.push(...formatFailedTask(result, task.cwd, config));
|
|
446
473
|
} else {
|
|
474
|
+
if (result.incomplete === "quiescence_abandoned") {
|
|
475
|
+
parts.push(INCOMPLETE_QUIESCENCE_WARNING);
|
|
476
|
+
}
|
|
447
477
|
const meta = [
|
|
448
|
-
`OK | ${fmtDuration(result.durationMs)} | ${
|
|
478
|
+
`OK | ${fmtDuration(result.durationMs)} | ${taskTokenLabel(result)}`,
|
|
449
479
|
];
|
|
450
480
|
if (result.sessionFile) meta.push(shortenPath(result.sessionFile));
|
|
451
481
|
const touched = relativeTouchedSummary(result.touchedFiles, task.cwd);
|
|
@@ -477,6 +507,23 @@ export function formatCompletedTask(
|
|
|
477
507
|
for (const conflict of integration.conflicts ?? []) {
|
|
478
508
|
parts.push(`conflict: ${conflict.path}: ${conflict.reason}`);
|
|
479
509
|
}
|
|
510
|
+
for (const issue of "classificationIssues" in integration
|
|
511
|
+
? (integration.classificationIssues ?? [])
|
|
512
|
+
: []) {
|
|
513
|
+
parts.push(
|
|
514
|
+
`attribution classification issue: ${issue.path}: ${issue.reason}`,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
if (integration.cleanupIssue) {
|
|
518
|
+
parts.push(
|
|
519
|
+
`cleanup ${integration.cleanupIssue.status}: ${integration.cleanupIssue.reason}`,
|
|
520
|
+
);
|
|
521
|
+
if (integration.cleanupIssue.recoveryPath) {
|
|
522
|
+
parts.push(
|
|
523
|
+
`cleanup recovery path: ${integration.cleanupIssue.recoveryPath}`,
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
480
527
|
if (integration.status === "applied_unverified") {
|
|
481
528
|
parts.push(
|
|
482
529
|
'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 +558,9 @@ export function latestActivity(p: TaskProgress): ToolActivity | null {
|
|
|
511
558
|
export function formatActivityLabel(p: TaskProgress): string {
|
|
512
559
|
const activity = inFlightActivity(p) ?? latestActivity(p);
|
|
513
560
|
if (!activity) return "thinking";
|
|
514
|
-
const call =
|
|
561
|
+
const call = sanitizeTerminalLine(
|
|
562
|
+
formatToolCallShort(activity.name, activity.args),
|
|
563
|
+
);
|
|
515
564
|
if (!activity.result) return call;
|
|
516
565
|
return `last: ${call}`;
|
|
517
566
|
}
|
|
@@ -522,7 +571,9 @@ export function formatActivityLabel(p: TaskProgress): string {
|
|
|
522
571
|
export function compactActivity(p: TaskProgress): string {
|
|
523
572
|
const activity = inFlightActivity(p) ?? latestActivity(p);
|
|
524
573
|
if (!activity) return "thinking…";
|
|
525
|
-
const call =
|
|
574
|
+
const call = sanitizeTerminalLine(
|
|
575
|
+
formatToolCallShort(activity.name, activity.args),
|
|
576
|
+
);
|
|
526
577
|
if (!activity.result) {
|
|
527
578
|
const toolAge = fmtDuration(Date.now() - activity.startTime);
|
|
528
579
|
return `${call} | ${toolAge}`;
|
|
@@ -533,8 +584,16 @@ export function compactActivity(p: TaskProgress): string {
|
|
|
533
584
|
|
|
534
585
|
/** Per-task stats core: [duration, tokens]. Callers append medium-specific
|
|
535
586
|
* extras (touched files; themed join). */
|
|
587
|
+
export function taskTokenLabel(
|
|
588
|
+
result: Pick<TaskResult, "tokens" | "incomplete">,
|
|
589
|
+
): string {
|
|
590
|
+
return result.incomplete === "quiescence_abandoned"
|
|
591
|
+
? `≥${fmtTokens(result.tokens)} tokens (incomplete)`
|
|
592
|
+
: `${fmtTokens(result.tokens)} tokens`;
|
|
593
|
+
}
|
|
594
|
+
|
|
536
595
|
export function taskMetaBase(r: TaskResult): string[] {
|
|
537
|
-
return [fmtDuration(r.durationMs),
|
|
596
|
+
return [fmtDuration(r.durationMs), taskTokenLabel(r)];
|
|
538
597
|
}
|
|
539
598
|
|
|
540
599
|
/** Pending-task waiting label: "queued (N running)" at the concurrency cap,
|