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.
- package/README.md +7 -7
- package/TESTING.md +3 -3
- package/TEST_PLAN.md +2 -2
- package/docs/INDEX.md +25 -25
- package/docs/choose-a-workflow.md +4 -4
- package/docs/commands/bg-clear.md +1 -1
- package/docs/commands/bg-update.md +1 -1
- package/docs/commands/bg.md +1 -1
- package/docs/commands/fusion-models.md +1 -1
- package/docs/commands/fusion.md +5 -8
- package/docs/commands/jobs.md +1 -1
- package/docs/commands/kill.md +1 -1
- package/docs/commands/logs.md +1 -1
- package/docs/commands/task-manager.md +2 -2
- package/docs/concepts/completion-delivery.md +1 -0
- package/docs/getting-started.md +1 -1
- package/docs/manifest.json +59 -50
- package/docs/read-before-edit.md +1 -0
- package/docs/reference/runtime-contracts.md +48 -45
- package/docs/reference/shortcuts-and-dock.md +2 -2
- package/docs/subsystems/background-task-runtime.md +7 -1
- package/docs/subsystems/docs-freshness-gate.md +4 -4
- package/docs/subsystems/fusion.md +13 -9
- package/docs/subsystems/host-ui-and-telemetry.md +1 -1
- package/docs/tools/bg_delegate.md +1 -1
- package/docs/tools/bg_kill.md +1 -1
- package/docs/tools/bg_logs.md +1 -1
- package/docs/tools/bg_result.md +14 -10
- package/docs/tools/bg_run.md +1 -1
- package/docs/tools/bg_run_pi_attested.md +1 -1
- package/docs/tools/bg_status.md +1 -1
- package/docs/tools/fusion_investigate.md +6 -4
- package/docs/tools/fusion_reason.md +5 -5
- package/docs/tools/fusion_research.md +6 -2
- package/docs/tools/fusion_validate.md +5 -3
- package/package.json +1 -1
- package/src/core/common.ts +50 -2
- package/src/core/fusion/artifacts.ts +72 -20
- package/src/core/fusion/orchestrator.ts +154 -68
- package/src/core/fusion/result-package.ts +385 -0
- package/src/core/fusion/types.ts +9 -0
- package/src/core/registry.ts +187 -20
- package/src/delegate-extension.ts +130 -24
- package/src/extension.ts +17 -6
- 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
|
-
{
|
|
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({
|
|
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({
|
|
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 (
|
|
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
|
-
.
|
|
331
|
-
.
|
|
332
|
-
|
|
333
|
-
|
|
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,
|
|
453
|
+
pi.registerTool<typeof ResultParams, BackgroundResultDetails>({
|
|
429
454
|
name: DELEGATE_RESULT_TOOL_NAME,
|
|
430
|
-
label: '
|
|
455
|
+
label: 'Background Result',
|
|
431
456
|
description:
|
|
432
|
-
'Retrieve
|
|
433
|
-
promptSnippet: 'Retrieve the verified answer from a completed
|
|
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
|
|
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}
|
|
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(
|
|
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
|
|
513
|
+
eventService.close();
|
|
503
514
|
}
|
|
504
515
|
});
|
|
505
516
|
|