@pi-unipi/background-tasks 2.16.1 → 2.17.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.
Files changed (46) hide show
  1. package/README.md +21 -27
  2. package/package.json +3 -4
  3. package/src/cards.ts +76 -0
  4. package/src/child-process.ts +1 -1
  5. package/src/config.ts +0 -42
  6. package/src/context-visible-conversation-v2.ts +1 -1
  7. package/src/delegate/artifacts.ts +1 -1
  8. package/src/delegate/launch.ts +17 -30
  9. package/src/delegate/result-package.ts +1 -1
  10. package/src/delegate/runner.ts +1 -20
  11. package/src/delegate/seed.ts +1 -1
  12. package/src/delegate-extension.ts +16 -168
  13. package/src/index.ts +53 -25
  14. package/src/json-utils.ts +56 -0
  15. package/src/package-assets.ts +51 -0
  16. package/src/registry.ts +8 -459
  17. package/src/task-manager.ts +13 -2
  18. package/src/tools.ts +4 -189
  19. package/src/types.ts +17 -70
  20. package/extensions/anthropic-attribution.ts +0 -1
  21. package/extensions/fusion-child.ts +0 -1
  22. package/src/anthropic-attribution-path.ts +0 -21
  23. package/src/anthropic-attribution.ts +0 -1983
  24. package/src/attested-pi-run.ts +0 -612
  25. package/src/fixtures/fusion-golden-bytes.json +0 -310
  26. package/src/fixtures/fusion-validate-golden-bytes.json +0 -282
  27. package/src/fusion/artifacts.ts +0 -967
  28. package/src/fusion/budget.ts +0 -1162
  29. package/src/fusion/child-protocol.ts +0 -305
  30. package/src/fusion/claude-cache.ts +0 -207
  31. package/src/fusion/clean-context.ts +0 -91
  32. package/src/fusion/config.ts +0 -449
  33. package/src/fusion/context.ts +0 -265
  34. package/src/fusion/evaluation.ts +0 -800
  35. package/src/fusion/orchestrator.ts +0 -1288
  36. package/src/fusion/output-contract.ts +0 -34
  37. package/src/fusion/pi-child.ts +0 -2373
  38. package/src/fusion/prompts.ts +0 -345
  39. package/src/fusion/result-package.ts +0 -959
  40. package/src/fusion/source-policy.ts +0 -257
  41. package/src/fusion/types.ts +0 -1139
  42. package/src/fusion/web-fetch.ts +0 -1060
  43. package/src/fusion/workflows.ts +0 -184
  44. package/src/fusion-child-extension.ts +0 -1052
  45. package/src/fusion-extension.ts +0 -1293
  46. package/src/ui/fusion-model-selector.ts +0 -322
package/src/tools.ts CHANGED
@@ -6,9 +6,7 @@
6
6
  */
7
7
 
8
8
  import { Text } from "@earendil-works/pi-tui";
9
- import { getInstalledPackageVersion } from "@pi-unipi/core";
10
- import { dirname } from "node:path";
11
- import { fileURLToPath } from "node:url";
9
+ import { renderCompletionCard, renderLaunchCard } from "./cards.js";
12
10
  import { Type, type Static } from "typebox";
13
11
  import {
14
12
  COMMAND_PREVIEW_CHARS,
@@ -27,7 +25,6 @@ import {
27
25
  type BgRunDetails,
28
26
  type BgStatusDetails,
29
27
  type BgTaskSnapshot,
30
- type StartAttestedPiTaskOptions,
31
28
  type StartTaskOptions,
32
29
  } from "./types.js";
33
30
  import type { BackgroundTaskRegistry } from "./registry.js";
@@ -88,31 +85,12 @@ export const BgKillParams = Type.Object({
88
85
  taskId: Type.String({ description: "Task ID or unambiguous prefix of a running task" }),
89
86
  });
90
87
 
91
- export const BgPiAttestedParams = Type.Object({
92
- name: Type.String({ description: "Short human-readable task name" }),
93
- provider: Type.String({ description: "Pi provider id (e.g. anthropic)" }),
94
- model: Type.String({ description: "Model id under the provider" }),
95
- prompt: Type.String({ description: "Final user prompt for the child Pi run" }),
96
- reportPath: Type.String({
97
- description: "Relative path (inside cwd) where the child must write its report before exit",
98
- }),
99
- extraPiArgs: Type.Optional(
100
- Type.Array(Type.String(), { description: "Extra literal pi CLI args (restricted)" }),
101
- ),
102
- thinking: Type.Optional(Type.String({ description: "Thinking level passed as --thinking" })),
103
- timeoutSeconds: Type.Optional(Type.Number({ description: "Kill after this many seconds" })),
104
- });
105
-
106
88
  // ── Registration ────────────────────────────────────────────────────────────
107
89
 
108
90
  export interface RegisterSurfaceOptions {
109
91
  pi: import("@earendil-works/pi-coding-agent").ExtensionAPI;
110
92
  registry: BackgroundTaskRegistry;
111
93
  startTask: (ctx: any, command: string, options?: StartTaskOptions) => Promise<any>;
112
- startAttestedPiTask: (
113
- ctx: any,
114
- options: StartAttestedPiTaskOptions,
115
- ) => Promise<any>;
116
94
  openTaskManager: (ctx: any, initialTaskId?: string) => Promise<void>;
117
95
  clearFinishedNotices: (ctx: any) => number;
118
96
  openSettings: (ctx: any) => Promise<void>;
@@ -135,25 +113,6 @@ export function registerToolsAndCommands(options: RegisterSurfaceOptions): void
135
113
 
136
114
  // ── Commands (/unipi:* namespace — ours) ──────────────────────────────────
137
115
 
138
- // Update info lives with OUR updater module; this only reports versions.
139
- pi.registerCommand("unipi:bg-update", {
140
- description: "Show the installed background-tasks version and how to update",
141
- handler: (_args, ctx) => {
142
- const here = dirname(fileURLToPath(import.meta.url));
143
- const current = getInstalledPackageVersion(here, "@pi-unipi/background-tasks");
144
- const lines = [
145
- `@pi-unipi/background-tasks ${current} is installed.`,
146
- "Background tasks ship inside the @pi-unipi/unipi umbrella package.",
147
- "Update from npm:",
148
- " pi install npm:@pi-unipi/unipi@latest",
149
- "Or use /unipi:updater-settings to check for updates.",
150
- "This command only prints update instructions; it does not install or self-update.",
151
- ];
152
- ctx.ui.notify(lines.join("\n"), "info");
153
- return Promise.resolve();
154
- },
155
- });
156
-
157
116
  pi.registerCommand("unipi:bg", {
158
117
  description:
159
118
  'Start a shell command as a tracked background task: /unipi:bg [--agent] [--name "Task name"] <command>',
@@ -180,14 +139,6 @@ export function registerToolsAndCommands(options: RegisterSurfaceOptions): void
180
139
  },
181
140
  });
182
141
 
183
- pi.registerCommand("unipi:tasks", {
184
- description: "Open the background task manager UI",
185
- handler: async (args, ctx) => {
186
- const taskId = typeof args === "string" ? args.trim() : "";
187
- await options.openTaskManager(ctx, taskId || undefined);
188
- },
189
- });
190
-
191
142
  pi.registerCommand("unipi:bg-tasks", {
192
143
  description: "Open the background task manager UI",
193
144
  handler: async (args, ctx) => {
@@ -196,86 +147,6 @@ export function registerToolsAndCommands(options: RegisterSurfaceOptions): void
196
147
  },
197
148
  });
198
149
 
199
- pi.registerCommand("unipi:bg-clear", {
200
- description: "Clear finished background task footer notices",
201
- handler: (_args, ctx) => {
202
- options.clearFinishedNotices(ctx);
203
- return Promise.resolve();
204
- },
205
- });
206
-
207
- pi.registerCommand("unipi:jobs", {
208
- description: "List running and recent background tasks",
209
- handler: (_args, ctx) => {
210
- ctx.ui.notify(
211
- formatSnapshotList(registry.allTasks().map((task) => registry.snapshot(task))),
212
- "info",
213
- );
214
- return Promise.resolve();
215
- },
216
- });
217
-
218
- pi.registerCommand("unipi:logs", {
219
- description: "Show bounded output from a background task: /unipi:logs <id> [maxBytes]",
220
- getArgumentCompletions: (prefix: string) => {
221
- const matches = registry
222
- .allTasks()
223
- .filter((task) => task.id.startsWith(prefix.trim()))
224
- .slice(0, 20)
225
- .map((task) => ({
226
- value: task.id,
227
- label: `${task.id} ${taskDisplayName(task)}`,
228
- description: `${task.status} — ${truncateChars(task.command, 60)}`,
229
- }));
230
- return matches.length > 0 ? matches : null;
231
- },
232
- handler: async (args, ctx) => {
233
- try {
234
- const [id, bytes] = args.trim().split(/\s+/, 2);
235
- const task = registry.resolveTask(id ?? "");
236
- const maxBytes = normalizeMaxBytes(Number(bytes), DEFAULT_LOG_BYTES);
237
- const logs = await registry.getTaskLogs(task, maxBytes, true);
238
- ctx.ui.notify(logs.text, "info");
239
- } catch (error) {
240
- ctx.ui.notify(
241
- `Background logs error: ${error instanceof Error ? error.message : String(error)}`,
242
- "error",
243
- );
244
- }
245
- },
246
- });
247
-
248
- pi.registerCommand("unipi:kill", {
249
- description: "Stop a running background task: /unipi:kill <id>",
250
- getArgumentCompletions: (prefix: string) => {
251
- const matches = registry
252
- .allTasks()
253
- .filter((task) => task.status === "running" && task.id.startsWith(prefix.trim()))
254
- .slice(0, 20)
255
- .map((task) => ({
256
- value: task.id,
257
- label: `${task.id} ${taskDisplayName(task)}`,
258
- description: truncateChars(task.command, 70),
259
- }));
260
- return matches.length > 0 ? matches : null;
261
- },
262
- handler: async (args, ctx) => {
263
- try {
264
- const task = registry.resolveTask(args.trim());
265
- await registry.stopTask(task, "user");
266
- ctx.ui.notify(
267
- `Killed ${taskDisplayName(task)} (${task.id}). Output: ${task.outputPath}`,
268
- "info",
269
- );
270
- } catch (error) {
271
- ctx.ui.notify(
272
- `Background kill error: ${error instanceof Error ? error.message : String(error)}`,
273
- "error",
274
- );
275
- }
276
- },
277
- });
278
-
279
150
  // Shortcuts (same keys as reference; documented in our README)
280
151
  pi.registerCommand("unipi:bg-settings", {
281
152
  description: "Open background-tasks settings (master toggle, defaults)",
@@ -301,21 +172,8 @@ export function registerToolsAndCommands(options: RegisterSurfaceOptions): void
301
172
 
302
173
  pi.registerMessageRenderer<BgTaskSnapshot>(
303
174
  "background-task-notification",
304
- (message: { details?: BgTaskSnapshot }, _options: unknown, theme: any) => {
305
- const task = message.details;
306
- const status = task?.status ?? "completed";
307
- const color: string =
308
- status === "completed" ? "success" : status === "failed" ? "error" : status === "killed" ? "warning" : "accent";
309
- const id = task?.id ?? "background task";
310
- const name = task ? taskDisplayName(task) : "Background task";
311
- const output = task?.outputPath ? `\n${theme.fg("dim", `Output: ${task.outputPath}`)}` : "";
312
- const error = task?.error ? `\n${theme.fg("error", task.error)}` : "";
313
- return new Text(
314
- `${theme.fg(color, `[bg ${status}]`)} ${theme.fg("accent", name)} ${theme.fg("dim", `(${id})`)}${output}${error}`,
315
- 0,
316
- 0,
317
- );
318
- },
175
+ (message: { details?: BgTaskSnapshot }, _options: unknown, theme: any) =>
176
+ renderCompletionCard(theme, message.details),
319
177
  );
320
178
 
321
179
  // ── Tools (reference names kept) ─────────────────────────────────────────
@@ -371,12 +229,7 @@ export function registerToolsAndCommands(options: RegisterSurfaceOptions): void
371
229
  );
372
230
  },
373
231
  renderResult(result: { details: BgRunDetails }, _options: unknown, theme: any) {
374
- const { task } = result.details;
375
- return new Text(
376
- `${theme.fg("success", "✓ started")} ${theme.fg("accent", taskDisplayName(task))} ${theme.fg("dim", `(${task.id})`)}\n${theme.fg("dim", `Output: ${task.outputPath}`)}`,
377
- 0,
378
- 0,
379
- );
232
+ return renderLaunchCard(theme, result.details.task);
380
233
  },
381
234
  });
382
235
 
@@ -491,42 +344,4 @@ export function registerToolsAndCommands(options: RegisterSurfaceOptions): void
491
344
  );
492
345
  },
493
346
  });
494
-
495
- pi.registerTool<typeof BgPiAttestedParams, BgRunDetails>({
496
- name: "bg_run_pi_attested",
497
- label: "Attested Pi Run",
498
- description:
499
- "Opt-in evidence-oriented direct Pi spawn. Launches exactly one `pi --mode json` child, records raw events/stderr, hashes prompt/report/output, observes OAuth through ModelRegistry, and emits a strict attestation sidecar only after successful completion.",
500
- promptSnippet: "Start an attested direct Pi agent task and return its task ID plus output path",
501
- promptGuidelines: [
502
- "Use only when the user explicitly asks for an attested Pi evidence-producing task; ordinary background work should use bg_run unchanged.",
503
- "Provide provider/model as structured fields and a relative reportPath that the child Pi prompt will write before exit.",
504
- "Do not provide channel, auth, route, or hash claims; the producer observes those facts itself and fails loudly if it cannot attest them.",
505
- ],
506
- parameters: BgPiAttestedParams,
507
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
508
- const task = await options.startAttestedPiTask(ctx, params);
509
- return {
510
- content: textContent(
511
- `Started attested Pi task ${taskDisplayName(task)} (${task.id})\nStatus: ${task.status}\nPID: ${String(task.pid ?? "unknown")}\nOutput: ${task.outputPath}\nAttestation: ${task.attestationPath ?? "pending until completion"}`,
512
- ),
513
- details: { task: registry.snapshot(task) },
514
- };
515
- },
516
- renderCall(args: Static<typeof BgPiAttestedParams>, theme: any) {
517
- return new Text(
518
- `${theme.fg("toolTitle", theme.bold("bg_run_pi_attested "))}${theme.fg("muted", truncateChars(args.name, COMMAND_PREVIEW_CHARS))}`,
519
- 0,
520
- 0,
521
- );
522
- },
523
- renderResult(result: { details: BgRunDetails }, _options: unknown, theme: any) {
524
- const { task } = result.details;
525
- return new Text(
526
- `${theme.fg("success", "✓ started")} ${theme.fg("accent", taskDisplayName(task))} ${theme.fg("dim", `(${task.id})`)}\n${theme.fg("dim", `Output: ${task.outputPath}`)}\n${theme.fg("dim", `Attestation: ${task.attestationPath ?? "pending"}`)}`,
527
- 0,
528
- 0,
529
- );
530
- },
531
- });
532
347
  }
package/src/types.ts CHANGED
@@ -12,7 +12,6 @@
12
12
  import { statSync, type WriteStream } from "node:fs";
13
13
  import { join } from "node:path";
14
14
  import type { BackgroundTaskChildProcess } from "./child-process.js";
15
- import type { FusionResultDetails, FusionUsage, FusionWorkflowId } from "./fusion/types.js";
16
15
 
17
16
  export const TASK_STATUS_VALUES = ["running", "completed", "failed", "killed"] as const;
18
17
  export const TERMINAL_TASK_STATUS_VALUES = ["completed", "failed", "killed"] as const;
@@ -69,20 +68,9 @@ export interface BgTaskSnapshot {
69
68
  toolUsage?: TaskToolUsage | undefined;
70
69
  model?: string | undefined;
71
70
  telemetryUnavailableReason?: string | undefined;
72
- attestationPath?: string | undefined;
73
71
  delegate?: DelegateTaskFacts | undefined;
74
- fusion?: FusionTaskFacts | undefined;
75
- }
76
-
77
- export interface AttestedPiTaskFiles {
78
- eventsPath: string;
79
- stderrPath: string;
80
- wrapperPath: string;
81
- attestationPath: string;
82
- }
83
-
84
- export interface AttestedPiTaskSnapshot extends BgTaskSnapshot {
85
- attestedPi?: AttestedPiTaskFiles | undefined;
72
+ /** Last few non-empty output lines (bounded), for compact chat cards. */
73
+ outputTail?: string[] | undefined;
86
74
  }
87
75
 
88
76
  /** Delegate-specific task facts surfaced through snapshots and `bg_result`. */
@@ -114,25 +102,6 @@ export interface DelegateTaskOutcome {
114
102
  export type DelegateBudgetRouteSource = import("./delegate/types.js").DelegateBudgetRouteSource;
115
103
  export type DelegateExtensionMode = import("./delegate/types.js").DelegateExtensionMode;
116
104
 
117
- /** Fusion-specific task facts surfaced through snapshots and `bg_result`. */
118
- export interface FusionTaskFacts {
119
- runId: string;
120
- workflow: FusionWorkflowId;
121
- artifactDir: string;
122
- artifactDirAbs: string;
123
- state: string;
124
- outcome?: FusionTaskOutcome | undefined;
125
- /** Durable once-only accounting claim made by the first successful bg_result retrieval. */
126
- usageDelivered: boolean;
127
- }
128
-
129
- export interface FusionTaskOutcome {
130
- status: "committed" | "failed" | "cancelled";
131
- resultDetails?: FusionResultDetails | undefined;
132
- usage?: FusionUsage | undefined;
133
- error?: string | undefined;
134
- }
135
-
136
105
  export interface BgTask extends Omit<BgTaskSnapshot, "name"> {
137
106
  name: string;
138
107
  outputAbsPath: string;
@@ -160,14 +129,8 @@ export interface BgTask extends Omit<BgTaskSnapshot, "name"> {
160
129
  /** Partial trailing stdout line held between chunks while reconstructing wrapped-agent control lines. */
161
130
  agentStdoutBuffer?: string | undefined;
162
131
  telemetryUnavailableReason?: string | undefined;
163
- attestationPath?: string | undefined;
164
- attestedPi?: AttestedPiTaskFiles | undefined;
165
132
  delegate?: DelegateTaskFacts | undefined;
166
- fusion?: FusionTaskFacts | undefined;
167
- /** Cancellation hook for an in-process managed task such as Fusion. */
168
- managedCancel?: (() => void) | undefined;
169
- managedCancelRequested?: boolean | undefined;
170
- managedStopWaitMs?: number | undefined;
133
+ outputTail?: string[] | undefined;
171
134
  metadataWriteChain?: Promise<void> | undefined;
172
135
  waiters: Array<() => void>;
173
136
  }
@@ -264,23 +227,6 @@ export interface StartTaskOptions {
264
227
  terminalPublicationGate?: Promise<void> | undefined;
265
228
  }
266
229
 
267
- /** Prepared managed launch handed to the registry after preflight has succeeded. */
268
- export interface StartManagedTaskOptions {
269
- id: string;
270
- name: string;
271
- command: string;
272
- description?: string | undefined;
273
- isAgent: boolean;
274
- completion: Promise<void>;
275
- cancel: () => void;
276
- notifyOnCompletion: boolean;
277
- triggerOnCompletion: boolean;
278
- fusion: FusionTaskFacts;
279
- stopWaitMs?: number | undefined;
280
- /** Prevent terminal publication until the launch receipt handoff is observable. */
281
- terminalPublicationGate?: Promise<void> | undefined;
282
- }
283
-
284
230
  export interface StartDelegateTaskOptions {
285
231
  name: string;
286
232
  argv: readonly string[];
@@ -293,17 +239,6 @@ export interface StartDelegateTaskOptions {
293
239
  timeoutSeconds?: number | undefined;
294
240
  }
295
241
 
296
- export interface StartAttestedPiTaskOptions {
297
- name: string;
298
- provider: string;
299
- model: string;
300
- prompt: string;
301
- reportPath: string;
302
- extraPiArgs?: string[] | undefined;
303
- thinking?: string | undefined;
304
- timeoutSeconds?: number | undefined;
305
- }
306
-
307
242
  /** Default tool-output byte cap (mirrors pi's DEFAULT_MAX_BYTES). */
308
243
  export const DEFAULT_MAX_BYTES = 30 * 1024;
309
244
 
@@ -830,12 +765,24 @@ export function snapshot(task: BgTask): BgTaskSnapshot {
830
765
  toolUsage: task.toolUsage,
831
766
  model: task.model,
832
767
  telemetryUnavailableReason: task.telemetryUnavailableReason,
833
- attestationPath: task.attestationPath,
834
768
  delegate: task.delegate,
835
- fusion: task.fusion,
769
+ outputTail: task.outputTail === undefined ? undefined : [...task.outputTail],
836
770
  };
837
771
  }
838
772
 
773
+ export const OUTPUT_TAIL_LINES = 3;
774
+ const OUTPUT_TAIL_LINE_CHARS = 200;
775
+
776
+ /** Fold a raw output chunk into the task's bounded tail ring (non-empty lines only). */
777
+ export function appendOutputTail(task: { outputTail?: string[] | undefined }, chunk: string): void {
778
+ if (chunk.length === 0) return;
779
+ const lines = chunk.split(/\r?\n/u).map((line) => line.replace(/\x1b\[[0-9;]*[A-Za-z]/gu, "").trimEnd()).filter((line) => line.trim().length > 0);
780
+ if (lines.length === 0) return;
781
+ const tail = task.outputTail ?? [];
782
+ for (const line of lines) tail.push(line.length > OUTPUT_TAIL_LINE_CHARS ? `${line.slice(0, OUTPUT_TAIL_LINE_CHARS)}…` : line);
783
+ task.outputTail = tail.slice(-OUTPUT_TAIL_LINES);
784
+ }
785
+
839
786
  export async function boundedRead(
840
787
  filePath: string,
841
788
  maxBytes: number,
@@ -1 +0,0 @@
1
- export { default } from '../src/anthropic-attribution.js';
@@ -1 +0,0 @@
1
- export { default } from '../src/fusion-child-extension.js';
@@ -1,21 +0,0 @@
1
- /**
2
- * @pi-unipi/background-tasks — Anthropic attribution extension path
3
- *
4
- * Resolves this package's own attribution extension entry so isolated child Pi
5
- * processes can load it explicitly. Mirrors the reference layout:
6
- * extensions/anthropic-attribution.ts re-exports src/anthropic-attribution.ts.
7
- */
8
-
9
- import { dirname, resolve } from "node:path";
10
- import { fileURLToPath } from "node:url";
11
- import { existsSync } from "node:fs";
12
-
13
- export function resolveAnthropicAttributionExtensionPath(): string {
14
- // src/anthropic-attribution-path.ts -> packages/background-tasks/extensions/
15
- const here = dirname(fileURLToPath(import.meta.url));
16
- const candidate = resolve(here, "..", "extensions", "anthropic-attribution.ts");
17
- if (!existsSync(candidate)) {
18
- throw new Error(`Anthropic attribution extension is missing: ${candidate}`);
19
- }
20
- return candidate;
21
- }