pi-background-tasks 1.0.7 → 2.0.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 (45) hide show
  1. package/README.md +7 -7
  2. package/TESTING.md +3 -3
  3. package/TEST_PLAN.md +2 -2
  4. package/docs/INDEX.md +25 -25
  5. package/docs/choose-a-workflow.md +4 -4
  6. package/docs/commands/bg-clear.md +1 -1
  7. package/docs/commands/bg-update.md +1 -1
  8. package/docs/commands/bg.md +1 -1
  9. package/docs/commands/fusion-models.md +1 -1
  10. package/docs/commands/fusion.md +5 -8
  11. package/docs/commands/jobs.md +1 -1
  12. package/docs/commands/kill.md +1 -1
  13. package/docs/commands/logs.md +1 -1
  14. package/docs/commands/task-manager.md +2 -2
  15. package/docs/concepts/completion-delivery.md +1 -0
  16. package/docs/getting-started.md +1 -1
  17. package/docs/manifest.json +59 -50
  18. package/docs/read-before-edit.md +1 -0
  19. package/docs/reference/runtime-contracts.md +48 -45
  20. package/docs/reference/shortcuts-and-dock.md +2 -2
  21. package/docs/subsystems/background-task-runtime.md +7 -1
  22. package/docs/subsystems/docs-freshness-gate.md +4 -4
  23. package/docs/subsystems/fusion.md +13 -9
  24. package/docs/subsystems/host-ui-and-telemetry.md +1 -1
  25. package/docs/tools/bg_delegate.md +1 -1
  26. package/docs/tools/bg_kill.md +1 -1
  27. package/docs/tools/bg_logs.md +1 -1
  28. package/docs/tools/bg_result.md +14 -10
  29. package/docs/tools/bg_run.md +1 -1
  30. package/docs/tools/bg_run_pi_attested.md +1 -1
  31. package/docs/tools/bg_status.md +1 -1
  32. package/docs/tools/fusion_investigate.md +6 -4
  33. package/docs/tools/fusion_reason.md +5 -5
  34. package/docs/tools/fusion_research.md +6 -2
  35. package/docs/tools/fusion_validate.md +5 -3
  36. package/package.json +1 -1
  37. package/src/core/common.ts +50 -2
  38. package/src/core/fusion/artifacts.ts +72 -20
  39. package/src/core/fusion/orchestrator.ts +154 -68
  40. package/src/core/fusion/result-package.ts +385 -0
  41. package/src/core/fusion/types.ts +9 -0
  42. package/src/core/registry.ts +187 -20
  43. package/src/delegate-extension.ts +130 -24
  44. package/src/extension.ts +17 -6
  45. package/src/fusion-extension.ts +308 -154
@@ -10,6 +10,9 @@ import { Text } from '@earendil-works/pi-tui';
10
10
  import { Type, type Static } from 'typebox';
11
11
  import type { BgTask, BgTaskSnapshot, StartDelegateTaskOptions } from './core/common.js';
12
12
  import { truncateChars } from './core/common.js';
13
+ import { sha256Buffer } from './core/attested-pi-run.js';
14
+ import { readFusionCommittedResult } from './core/fusion/result-package.js';
15
+ import { cloneFusionUsage, type FusionUsage, type FusionWorkflowId } from './core/fusion/types.js';
13
16
  import {
14
17
  DELEGATE_AUTO_DELIVER_MODES,
15
18
  DELEGATE_CAPABILITIES,
@@ -74,7 +77,10 @@ const DelegateParams = Type.Object(
74
77
  provider: Type.String({ description: 'Exact provider name to pin.' }),
75
78
  model: Type.String({ description: 'Exact provider-local model id to pin.' }),
76
79
  },
77
- { additionalProperties: false, description: 'Explicit route. Defaults to the current model.' },
80
+ {
81
+ additionalProperties: false,
82
+ description: 'Explicit route. Defaults to the current model.',
83
+ },
78
84
  ),
79
85
  ),
80
86
  capability: Type.Optional(
@@ -83,7 +89,9 @@ const DelegateParams = Type.Object(
83
89
  }),
84
90
  ),
85
91
  maxTurns: Type.Optional(
86
- Type.Number({ description: `Maximum agent turns. Default ${String(DELEGATE_DEFAULT_MAX_TURNS)}.` }),
92
+ Type.Number({
93
+ description: `Maximum agent turns. Default ${String(DELEGATE_DEFAULT_MAX_TURNS)}.`,
94
+ }),
87
95
  ),
88
96
  maxToolCalls: Type.Optional(
89
97
  Type.Number({
@@ -113,7 +121,9 @@ const DelegateParams = Type.Object(
113
121
 
114
122
  const ResultParams = Type.Object(
115
123
  {
116
- taskId: Type.String({ description: 'Delegate task id returned by bg_delegate.' }),
124
+ taskId: Type.String({
125
+ description: 'Background delegate or Fusion task id returned by its launch tool.',
126
+ }),
117
127
  delivery: Type.Optional(
118
128
  Type.String({
119
129
  description:
@@ -141,6 +151,20 @@ export interface DelegateLaunchDetails {
141
151
  trigger_on_completion: boolean;
142
152
  }
143
153
 
154
+ export interface FusionBackgroundResultDetails {
155
+ schema_version: 'pi-background-tasks.fusion-result-view.v1';
156
+ task_id: string;
157
+ state: 'running' | 'committed' | 'failed' | 'cancelled';
158
+ delivery: DelegateDeliveryMode | 'none';
159
+ workflow: FusionWorkflowId;
160
+ artifact_dir: string;
161
+ answer_bytes?: number | undefined;
162
+ answer_sha256?: string | undefined;
163
+ usage_delivered?: boolean | undefined;
164
+ }
165
+
166
+ export type BackgroundResultDetails = DelegateResultDetails | FusionBackgroundResultDetails;
167
+
144
168
  export interface DelegateResultDetails {
145
169
  schema_version: 'pi-background-tasks.delegate-result-view.v1';
146
170
  task_id: string;
@@ -217,7 +241,12 @@ function requireRoute(value: unknown): DelegateRoute | undefined {
217
241
 
218
242
  function optionalPositiveInteger(value: unknown, label: string): number | undefined {
219
243
  if (value === undefined) return undefined;
220
- if (typeof value !== 'number' || !Number.isFinite(value) || !Number.isInteger(value) || value <= 0)
244
+ if (
245
+ typeof value !== 'number' ||
246
+ !Number.isFinite(value) ||
247
+ !Number.isInteger(value) ||
248
+ value <= 0
249
+ )
221
250
  throw new DelegateError(`bg_delegate ${label} must be a positive integer`, {
222
251
  code: 'invalid_arguments',
223
252
  childCreated: false,
@@ -226,12 +255,10 @@ function optionalPositiveInteger(value: unknown, label: string): number | undefi
226
255
  }
227
256
 
228
257
  export interface DelegateExtensionDependencies {
229
- startDelegateTask: (
230
- ctx: ExtensionContext,
231
- options: StartDelegateTaskOptions,
232
- ) => Promise<BgTask>;
258
+ startDelegateTask: (ctx: ExtensionContext, options: StartDelegateTaskOptions) => Promise<BgTask>;
233
259
  snapshot: (task: BgTask) => BgTaskSnapshot;
234
260
  resolveTask: (idOrPrefix: string) => BgTask;
261
+ claimFusionUsage: (task: BgTask) => Promise<boolean>;
235
262
  /** Overridable so tests can supply observed evidence without touching disk. */
236
263
  loadHookEvidence?: (() => Promise<DelegateHookContractEvidence>) | undefined;
237
264
  }
@@ -326,13 +353,11 @@ export function registerDelegateExtension(
326
353
  id: ctx.model.id,
327
354
  contextWindow: ctx.model.contextWindow,
328
355
  },
329
- availableModels: ctx.modelRegistry
330
- .getAll()
331
- .map((model) => ({
332
- provider: model.provider,
333
- id: model.id,
334
- contextWindow: model.contextWindow,
335
- })),
356
+ availableModels: ctx.modelRegistry.getAll().map((model) => ({
357
+ provider: model.provider,
358
+ id: model.id,
359
+ contextWindow: model.contextWindow,
360
+ })),
336
361
  thinkingLevel: pi.getThinkingLevel(),
337
362
  });
338
363
 
@@ -425,15 +450,15 @@ export function registerDelegateExtension(
425
450
  },
426
451
  });
427
452
 
428
- pi.registerTool<typeof ResultParams, DelegateResultDetails>({
453
+ pi.registerTool<typeof ResultParams, BackgroundResultDetails>({
429
454
  name: DELEGATE_RESULT_TOOL_NAME,
430
- label: 'Delegate Result',
455
+ label: 'Background Result',
431
456
  description:
432
- 'Retrieve the hash-verified answer from a bg_delegate task. Never blocks: a running task returns a typed not-ready result. A completed answer is verified against its recorded SHA-256 before it is returned, and an oversized answer is never truncated.',
433
- promptSnippet: 'Retrieve the verified answer from a completed bg_delegate task',
457
+ 'Retrieve a hash-verified result from a bg_delegate or background Fusion task. Never blocks: a running task returns a typed not-ready result. Oversized answers are never truncated.',
458
+ promptSnippet: 'Retrieve the verified answer from a completed delegate or Fusion task',
434
459
  promptGuidelines: [
435
- 'Call bg_result once the delegate terminal notification has arrived. It never blocks and must not be polled.',
436
- 'A not-ready result means the delegate is still running; end the turn and wait for the notification.',
460
+ 'Call bg_result once the delegate or Fusion terminal notification has arrived. It never blocks and must not be polled.',
461
+ 'A not-ready result means the task is still running; end the turn and wait for the notification.',
437
462
  ],
438
463
  parameters: ResultParams,
439
464
  prepareArguments(args): ResultParamsValue {
@@ -463,10 +488,86 @@ export function registerDelegateExtension(
463
488
  { code: 'task_unknown', childCreated: false },
464
489
  );
465
490
  }
491
+ const fusion = task.fusion;
492
+ if (fusion !== undefined) {
493
+ const requestedDelivery = requireDelivery(params.delivery);
494
+ if (task.status === 'running') {
495
+ const details: FusionBackgroundResultDetails = {
496
+ schema_version: 'pi-background-tasks.fusion-result-view.v1',
497
+ task_id: task.id,
498
+ state: 'running',
499
+ delivery: 'none',
500
+ workflow: fusion.workflow,
501
+ artifact_dir: fusion.artifactDir,
502
+ };
503
+ return {
504
+ content: textContent(
505
+ `Fusion ${task.id} is still running. bg_result never blocks. End this turn; the terminal notification will wake you, then call bg_result again.`,
506
+ ),
507
+ details,
508
+ };
509
+ }
510
+ if (task.status !== 'completed' || fusion.outcome?.status !== 'committed') {
511
+ throw new Error(
512
+ `Fusion ${task.id} did not commit a result (${task.status}): ${fusion.outcome?.error ?? task.error ?? 'no terminal detail'}`,
513
+ );
514
+ }
515
+ const verified = await readFusionCommittedResult({
516
+ artifactDirAbs: fusion.artifactDirAbs,
517
+ artifactDir: fusion.artifactDir,
518
+ runId: fusion.runId,
519
+ workflow: fusion.workflow,
520
+ });
521
+ const answerBytes = Buffer.byteLength(verified.mergedText, 'utf8');
522
+ const answerSha256 = sha256Buffer(Buffer.from(verified.mergedText, 'utf8'));
523
+ const useArtifact =
524
+ requestedDelivery === 'artifact' ||
525
+ (requestedDelivery === undefined && answerBytes > DELEGATE_INLINE_ANSWER_BYTES);
526
+ if (requestedDelivery === 'inline' && answerBytes > DELEGATE_INLINE_ANSWER_BYTES) {
527
+ throw new Error(
528
+ `Fusion result ${task.id} is ${String(answerBytes)} bytes, above the ${String(DELEGATE_INLINE_ANSWER_BYTES)}-byte inline limit. Use delivery:"artifact"; nothing was truncated.`,
529
+ );
530
+ }
531
+ const usageDelivered = await deps.claimFusionUsage(task);
532
+ const details: FusionBackgroundResultDetails = {
533
+ schema_version: 'pi-background-tasks.fusion-result-view.v1',
534
+ task_id: task.id,
535
+ state: 'committed',
536
+ delivery: useArtifact ? 'artifact' : 'inline',
537
+ workflow: fusion.workflow,
538
+ artifact_dir: fusion.artifactDir,
539
+ answer_bytes: answerBytes,
540
+ answer_sha256: answerSha256,
541
+ usage_delivered: usageDelivered,
542
+ };
543
+ const header = [
544
+ `Fusion ${task.id} completed (${fusion.workflow}).`,
545
+ `Answer: ${String(answerBytes)} bytes, ${answerSha256} (verified).`,
546
+ `Artifacts: ${fusion.artifactDir}`,
547
+ usageDelivered
548
+ ? 'Usage: attached to this retrieval exactly once.'
549
+ : 'Usage: already attached by an earlier retrieval; not counted again.',
550
+ ].join('\n');
551
+ const result = useArtifact
552
+ ? {
553
+ content: textContent(
554
+ `${header}\nDelivery: artifact. The complete answer is ${fusion.artifactDir}/merged.md; it was not truncated.`,
555
+ ),
556
+ details,
557
+ }
558
+ : { content: textContent(`${header}\n\n${verified.mergedText}`), details };
559
+ if (!usageDelivered) return result;
560
+ const resultWithUsage: typeof result & { usage: FusionUsage } = {
561
+ ...result,
562
+ usage: cloneFusionUsage(verified.details.usage),
563
+ };
564
+ return resultWithUsage;
565
+ }
566
+
466
567
  const facts = task.delegate;
467
568
  if (facts === undefined) {
468
569
  throw new DelegateError(
469
- `bg_result task ${task.id} is not a bg_delegate task; use bg_logs for ordinary background tasks`,
570
+ `bg_result task ${task.id} has no retrievable delegate or Fusion result; use bg_logs for ordinary background tasks`,
470
571
  { code: 'task_unknown', childCreated: false },
471
572
  );
472
573
  }
@@ -565,10 +666,15 @@ export function registerDelegateExtension(
565
666
  renderResult(result, options: ToolRenderResultOptions, theme: Theme) {
566
667
  void options;
567
668
  const details = result.details;
669
+ const fusion = details.schema_version === 'pi-background-tasks.fusion-result-view.v1';
568
670
  if (details.state === 'running')
569
- return new Text(theme.fg('warning', `delegate ${details.task_id} still running`), 0, 0);
671
+ return new Text(
672
+ theme.fg('warning', `${fusion ? 'fusion' : 'delegate'} ${details.task_id} still running`),
673
+ 0,
674
+ 0,
675
+ );
570
676
  return new Text(
571
- `${theme.fg('success', '✓ delegate answer')} ${theme.fg('dim', `${String(details.answer_bytes ?? 0)}B · ${details.delivery}`)}`,
677
+ `${theme.fg('success', fusion ? '✓ fusion answer' : '✓ delegate answer')} ${theme.fg('dim', `${String(details.answer_bytes ?? 0)}B · ${details.delivery}`)}`,
572
678
  0,
573
679
  0,
574
680
  );
package/src/extension.ts CHANGED
@@ -204,8 +204,6 @@ function renderPlainResult(result: TextToolResult, options: ToolRenderResultOpti
204
204
  }
205
205
 
206
206
  export default function backgroundTasksExtension(pi: ExtensionAPI): void {
207
- registerFusionExtension(pi);
208
-
209
207
  const seenTaskIds = new Set<string>();
210
208
  let currentCtx: ExtensionContext | undefined;
211
209
  let dockOpen = false;
@@ -213,7 +211,6 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
213
211
  let latestKnownVersion: string | undefined;
214
212
  let updateCheckStarted = false;
215
213
 
216
- let eventService: BackgroundTaskExtensionService | undefined;
217
214
  const registry = new BackgroundTaskRegistry({
218
215
  onChange: () => {
219
216
  updateUi();
@@ -222,17 +219,25 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
222
219
  pi.sendMessage(message, options);
223
220
  },
224
221
  publishTerminal: (task) => {
225
- if (!eventService) throw new Error('Background task EventBus service is not installed');
226
222
  eventService.publishTerminal(task);
227
223
  },
228
224
  });
229
- eventService = installBackgroundTaskExtensionApi({
225
+ const eventService: BackgroundTaskExtensionService = installBackgroundTaskExtensionApi({
230
226
  events: pi.events,
231
227
  registry,
232
228
  getContext: () => currentCtx,
233
229
  isShuttingDown: () => registry.isShuttingDown(),
234
230
  });
235
231
 
232
+ registerFusionExtension(pi, {
233
+ startManagedTask: async (ctx, options) => {
234
+ currentCtx = ctx;
235
+ return registry.startManagedTask(ctx, options);
236
+ },
237
+ snapshot: (task) => registry.snapshot(task),
238
+ updateManagedTask: (task, state, line) => registry.updateManagedTask(task, state, line),
239
+ });
240
+
236
241
  registerDelegateExtension(pi, {
237
242
  startDelegateTask: async (ctx, options) => {
238
243
  currentCtx = ctx;
@@ -240,6 +245,7 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
240
245
  },
241
246
  snapshot: (task) => registry.snapshot(task),
242
247
  resolveTask: (idOrPrefix) => registry.resolveTask(idOrPrefix),
248
+ claimFusionUsage: (task) => registry.claimFusionUsage(task),
243
249
  });
244
250
 
245
251
  function unseenFinishedTasks(): BgTask[] {
@@ -359,6 +365,11 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
359
365
  return result;
360
366
  },
361
367
  rerunTask: async (task: BackgroundTaskForUi) => {
368
+ if (task.fusion !== undefined || task.delegate !== undefined) {
369
+ throw new Error(
370
+ 'Only shell-command tasks can be rerun from the dock; relaunch this typed workflow through its owning tool.',
371
+ );
372
+ }
362
373
  const rerunOptions: StartTaskOptions = {
363
374
  name: taskDisplayName(task),
364
375
  isAgent: task.isAgent,
@@ -499,7 +510,7 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
499
510
  ctx.ui.notify(`Background task cleanup failed:\n${failures.join('\n')}`, 'error');
500
511
  }
501
512
  } finally {
502
- eventService?.close();
513
+ eventService.close();
503
514
  }
504
515
  });
505
516