@bermudi/pi-delegate 0.1.13 → 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/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(`… ${hidden} lines hidden · Ctrl+O expands`, getTermWidth()),
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
- return id ? ` #${id}` : "";
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.trim();
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, human-readable summary of a tool call. */
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
- if (!args || typeof args !== "object") return name;
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(firstArg(args, "path", ["file_path"]) ?? "...");
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(firstArg(args, "path", ["file_path"]) ?? "...");
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(firstArg(args, "path", ["file_path"]) ?? "...");
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 preview = val.length > 50 ? val.slice(0, 50) + "…" : val;
292
- return `${name} ${preview}`;
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 preview = JSON.stringify(args).slice(0, 50);
297
- return `${name} ${preview}${preview.length >= 50 ? "…" : ""}`;
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 name;
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
- const isAbort = r.error === "Aborted";
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)} | ${fmtTokens(result.tokens)} tokens`,
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 = formatToolCallShort(activity.name, activity.args);
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 = formatToolCallShort(activity.name, activity.args);
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), `${fmtTokens(r.tokens)} tokens`];
596
+ return [fmtDuration(r.durationMs), taskTokenLabel(r)];
538
597
  }
539
598
 
540
599
  /** Pending-task waiting label: "queued (N running)" at the concurrency cap,