@pi-unipi/background-tasks 2.6.1
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 +87 -0
- package/extensions/anthropic-attribution.ts +1 -0
- package/extensions/delegate-child.ts +1 -0
- package/extensions/fusion-child.ts +1 -0
- package/package.json +40 -0
- package/src/__tests__/anthropic-attribution.test.ts +195 -0
- package/src/__tests__/config.test.ts +137 -0
- package/src/__tests__/core.test.ts +493 -0
- package/src/__tests__/delegate-artifacts.test.ts +528 -0
- package/src/__tests__/delegate-budget.test.ts +456 -0
- package/src/__tests__/delegate-launch.test.ts +676 -0
- package/src/__tests__/delegate-result-package.test.ts +350 -0
- package/src/__tests__/delegate-seed.test.ts +392 -0
- package/src/__tests__/durable-fs.test.ts +559 -0
- package/src/__tests__/extension-api.test.ts +579 -0
- package/src/__tests__/fusion-artifacts.test.ts +1039 -0
- package/src/__tests__/fusion-budget.test.ts +1356 -0
- package/src/__tests__/fusion-claude-cache.test.ts +320 -0
- package/src/__tests__/fusion-config.test.ts +335 -0
- package/src/__tests__/fusion-context-prompts.test.ts +670 -0
- package/src/__tests__/fusion-evaluation.test.ts +315 -0
- package/src/__tests__/fusion-extraction-equivalence.test.ts +58 -0
- package/src/__tests__/fusion-golden-bytes.test.ts +35 -0
- package/src/__tests__/fusion-high-cardinality.test.ts +192 -0
- package/src/__tests__/fusion-model-selector.test.ts +205 -0
- package/src/__tests__/fusion-orchestrator.test.ts +1194 -0
- package/src/__tests__/fusion-rpc.test.ts +369 -0
- package/src/__tests__/fusion-sdk.test.ts +1226 -0
- package/src/__tests__/fusion-v5-core.test.ts +219 -0
- package/src/__tests__/fusion-validate-orchestrator.test.ts +240 -0
- package/src/__tests__/fusion-web-fetch.test.ts +485 -0
- package/src/__tests__/fusion-workflows.test.ts +59 -0
- package/src/__tests__/helpers/delegate-deterministic-seed.ts +109 -0
- package/src/__tests__/helpers/delegate-seed-subprocess.ts +10 -0
- package/src/__tests__/helpers/fusion-canonical-subprocess.ts +21 -0
- package/src/__tests__/helpers/fusion-canonical.ts +140 -0
- package/src/__tests__/helpers/fusion-fake-pi.ts +279 -0
- package/src/__tests__/helpers/fusion-golden-corpus.ts +500 -0
- package/src/__tests__/helpers/fusion-high-cardinality.ts +140 -0
- package/src/__tests__/helpers/normalize.ts +22 -0
- package/src/__tests__/helpers/pi-hook-contract-evidence.json +18 -0
- package/src/__tests__/pi-launch.test.ts +202 -0
- package/src/__tests__/registry.test.ts +1580 -0
- package/src/__tests__/scripted-provider/delegate-ambient-provider.test.ts +130 -0
- package/src/__tests__/scripted-provider/delegate-child-guard.test.ts +631 -0
- package/src/__tests__/scripted-provider/delegate-guard-provider.ts +403 -0
- package/src/__tests__/scripted-provider/follow-up.test.ts +448 -0
- package/src/__tests__/scripted-provider/fusion-output-recovery.test.ts +132 -0
- package/src/__tests__/scripted-provider/fusion-reason.test.ts +310 -0
- package/src/__tests__/scripted-provider/fusion-runtime-guard.test.ts +163 -0
- package/src/__tests__/scripted-provider/hook-contract-provider.ts +179 -0
- package/src/__tests__/scripted-provider/hook-probe-a.ts +3 -0
- package/src/__tests__/scripted-provider/hook-probe-b.ts +3 -0
- package/src/__tests__/scripted-provider/hook-probe-extension.ts +126 -0
- package/src/__tests__/scripted-provider/output-recovery-provider.ts +153 -0
- package/src/__tests__/scripted-provider/pi-hook-contract-evidence.json +18 -0
- package/src/__tests__/scripted-provider/pi-hook-contract.test.ts +477 -0
- package/src/__tests__/scripted-provider/runtime-guard-probe.ts +28 -0
- package/src/__tests__/scripted-provider/runtime-guard-provider.ts +49 -0
- package/src/__tests__/scripted-provider/scripted-provider-extension.ts +408 -0
- package/src/__tests__/task-manager.test.ts +479 -0
- package/src/__tests__/windows-taskkill.test.ts +161 -0
- package/src/anthropic-attribution-path.ts +21 -0
- package/src/anthropic-attribution.ts +1983 -0
- package/src/attested-pi-run.ts +612 -0
- package/src/child-process.ts +55 -0
- package/src/common.ts +8 -0
- package/src/config.ts +292 -0
- package/src/context-parent-snapshot.ts +142 -0
- package/src/context-token-budget.ts +903 -0
- package/src/context-visible-conversation-v2.ts +551 -0
- package/src/delegate/artifacts.ts +487 -0
- package/src/delegate/budget.ts +415 -0
- package/src/delegate/hook-contract-evidence.json +18 -0
- package/src/delegate/hook-contract.ts +154 -0
- package/src/delegate/launch.ts +497 -0
- package/src/delegate/result-package.ts +459 -0
- package/src/delegate/runner.ts +449 -0
- package/src/delegate/seed.ts +423 -0
- package/src/delegate/types.ts +323 -0
- package/src/delegate-child-extension.ts +978 -0
- package/src/delegate-extension.ts +806 -0
- package/src/durable-fs.ts +386 -0
- package/src/extension-api.ts +548 -0
- package/src/fixtures/delegate-context-incident.json +17 -0
- package/src/fixtures/fusion-golden-bytes.json +310 -0
- package/src/fixtures/fusion-validate-golden-bytes.json +282 -0
- package/src/fusion/artifacts.ts +967 -0
- package/src/fusion/budget.ts +1162 -0
- package/src/fusion/child-protocol.ts +305 -0
- package/src/fusion/claude-cache.ts +207 -0
- package/src/fusion/clean-context.ts +91 -0
- package/src/fusion/config.ts +449 -0
- package/src/fusion/context.ts +265 -0
- package/src/fusion/evaluation.ts +800 -0
- package/src/fusion/orchestrator.ts +1288 -0
- package/src/fusion/output-contract.ts +34 -0
- package/src/fusion/pi-child.ts +2373 -0
- package/src/fusion/prompts.ts +345 -0
- package/src/fusion/result-package.ts +959 -0
- package/src/fusion/source-policy.ts +257 -0
- package/src/fusion/types.ts +1139 -0
- package/src/fusion/web-fetch.ts +1060 -0
- package/src/fusion/workflows.ts +184 -0
- package/src/fusion-child-extension.ts +1052 -0
- package/src/fusion-extension.ts +1293 -0
- package/src/index.ts +295 -0
- package/src/pi-launch.ts +225 -0
- package/src/registry.ts +2424 -0
- package/src/settings-overlay.ts +208 -0
- package/src/task-manager.ts +774 -0
- package/src/tools.ts +530 -0
- package/src/turndown.d.ts +15 -0
- package/src/types.ts +963 -0
- package/src/ui/fusion-model-selector.ts +322 -0
- package/src/windows-taskkill.ts +250 -0
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import type {
|
|
4
|
+
ExtensionAPI,
|
|
5
|
+
ExtensionContext,
|
|
6
|
+
Theme,
|
|
7
|
+
ToolRenderResultOptions,
|
|
8
|
+
} from '@earendil-works/pi-coding-agent';
|
|
9
|
+
import { Text } from '@earendil-works/pi-tui';
|
|
10
|
+
import { Type, type Static } from 'typebox';
|
|
11
|
+
import type { BgTask, BgTaskSnapshot, StartDelegateTaskOptions } from './types.js';
|
|
12
|
+
import { truncateChars } from './types.js';
|
|
13
|
+
import { sha256Buffer } from './attested-pi-run.js';
|
|
14
|
+
import { readFusionCommittedResult, readFusionFailureResult } from './fusion/result-package.js';
|
|
15
|
+
import {
|
|
16
|
+
cloneFusionUsage,
|
|
17
|
+
type FusionFailureResultView,
|
|
18
|
+
type FusionUsage,
|
|
19
|
+
type FusionWorkflowId,
|
|
20
|
+
} from './fusion/types.js';
|
|
21
|
+
import {
|
|
22
|
+
DELEGATE_AUTO_DELIVER_MODES,
|
|
23
|
+
DELEGATE_CAPABILITIES,
|
|
24
|
+
DELEGATE_EXTENSION_MODES,
|
|
25
|
+
DELEGATE_RESULT_TOOL_NAME,
|
|
26
|
+
DELEGATE_TOOL_NAME,
|
|
27
|
+
DelegateError,
|
|
28
|
+
type DelegateAutoDeliverMode,
|
|
29
|
+
type DelegateCapability,
|
|
30
|
+
type DelegateDeliveryMode,
|
|
31
|
+
type DelegateBudgetRouteSource,
|
|
32
|
+
type DelegateExtensionMode,
|
|
33
|
+
type DelegateRoute,
|
|
34
|
+
} from './delegate/types.js';
|
|
35
|
+
import {
|
|
36
|
+
DELEGATE_INLINE_ANSWER_BYTES,
|
|
37
|
+
DELEGATE_DEFAULT_MAX_TOOL_CALLS,
|
|
38
|
+
DELEGATE_DEFAULT_MAX_TURNS,
|
|
39
|
+
DELEGATE_DEFAULT_TIMEOUT_SECONDS,
|
|
40
|
+
} from './delegate/budget.js';
|
|
41
|
+
import { resolveDelegateRoute } from './delegate/launch.js';
|
|
42
|
+
import {
|
|
43
|
+
decideDelegateDelivery,
|
|
44
|
+
evaluateDelegateTerminal,
|
|
45
|
+
inlineTooLarge,
|
|
46
|
+
prepareDelegateLaunch,
|
|
47
|
+
} from './delegate/runner.js';
|
|
48
|
+
import { loadDelegateHookContractEvidence } from './delegate/launch.js';
|
|
49
|
+
import type { DelegateHookContractEvidence } from './delegate/hook-contract.js';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* `bg_delegate` and `bg_result` registration.
|
|
53
|
+
*
|
|
54
|
+
* `bg_delegate` launches one background child Pi agent seeded with a frozen
|
|
55
|
+
* projection of the current conversation. `bg_result` retrieves that child's
|
|
56
|
+
* hash-verified answer. They ship together: a delegate without a safe retrieval
|
|
57
|
+
* path would have no way to return its work.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Shipped copy of the observed Pi hook-contract evidence.
|
|
62
|
+
*
|
|
63
|
+
* It is produced by executing a real Pi agent loop in
|
|
64
|
+
* `tests/scripted-provider/pi-hook-contract.test.ts`, and a package test asserts
|
|
65
|
+
* the shipped copy is byte-identical to the recorded one, so the runtime gate
|
|
66
|
+
* and the gate that proved it can never drift apart.
|
|
67
|
+
*/
|
|
68
|
+
const HOOK_EVIDENCE_PATH = fileURLToPath(
|
|
69
|
+
new URL('./delegate/hook-contract-evidence.json', import.meta.url),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
export const DelegateParams = Type.Object(
|
|
73
|
+
{
|
|
74
|
+
name: Type.String({
|
|
75
|
+
description: 'Short human-readable task name shown in the bg footer dock. Use 2-6 words.',
|
|
76
|
+
}),
|
|
77
|
+
prompt: Type.String({
|
|
78
|
+
description:
|
|
79
|
+
'Authoritative instruction for the delegate. The projected conversation is supporting background only.',
|
|
80
|
+
}),
|
|
81
|
+
route: Type.Optional(
|
|
82
|
+
Type.Object(
|
|
83
|
+
{
|
|
84
|
+
provider: Type.String({ description: 'Exact provider name to pin.' }),
|
|
85
|
+
model: Type.String({ description: 'Exact provider-local model id to pin.' }),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
additionalProperties: false,
|
|
89
|
+
description: 'Explicit route. Defaults to the current model.',
|
|
90
|
+
},
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
capability: Type.Optional(
|
|
94
|
+
Type.String({
|
|
95
|
+
description: `Capability profile. Only "inspect" (read/search/list, no shell, no writes, no network, no recursion) is supported.`,
|
|
96
|
+
}),
|
|
97
|
+
),
|
|
98
|
+
extensionMode: Type.Optional(
|
|
99
|
+
Type.String({
|
|
100
|
+
description:
|
|
101
|
+
'Extension discovery: isolated | ambient. Default isolated. Ambient is for extension-registered providers and executes arbitrary discovered extension code, weakening process isolation.',
|
|
102
|
+
}),
|
|
103
|
+
),
|
|
104
|
+
maxTurns: Type.Optional(
|
|
105
|
+
Type.Number({
|
|
106
|
+
description: `Maximum agent turns. Default ${String(DELEGATE_DEFAULT_MAX_TURNS)}.`,
|
|
107
|
+
}),
|
|
108
|
+
),
|
|
109
|
+
maxToolCalls: Type.Optional(
|
|
110
|
+
Type.Number({
|
|
111
|
+
description: `Maximum tool calls. Default ${String(DELEGATE_DEFAULT_MAX_TOOL_CALLS)}.`,
|
|
112
|
+
}),
|
|
113
|
+
),
|
|
114
|
+
timeoutSeconds: Type.Optional(
|
|
115
|
+
Type.Number({
|
|
116
|
+
description: `Wall-clock timeout. Default ${String(DELEGATE_DEFAULT_TIMEOUT_SECONDS)}.`,
|
|
117
|
+
}),
|
|
118
|
+
),
|
|
119
|
+
autoDeliver: Type.Optional(
|
|
120
|
+
Type.String({
|
|
121
|
+
description:
|
|
122
|
+
'Whether the completion notification carries the answer: never | when_small | always. Default never; retrieve with bg_result.',
|
|
123
|
+
}),
|
|
124
|
+
),
|
|
125
|
+
notifyOnCompletion: Type.Optional(
|
|
126
|
+
Type.Boolean({ description: 'Deliver the durable terminal notification. Default true.' }),
|
|
127
|
+
),
|
|
128
|
+
triggerOnCompletion: Type.Optional(
|
|
129
|
+
Type.Boolean({ description: 'Let that notification start a follow-up turn. Default true.' }),
|
|
130
|
+
),
|
|
131
|
+
},
|
|
132
|
+
{ additionalProperties: false },
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const ResultParams = Type.Object(
|
|
136
|
+
{
|
|
137
|
+
taskId: Type.String({
|
|
138
|
+
description: 'Background delegate or Fusion task id returned by its launch tool.',
|
|
139
|
+
}),
|
|
140
|
+
delivery: Type.Optional(
|
|
141
|
+
Type.String({
|
|
142
|
+
description:
|
|
143
|
+
'inline returns the verified answer text; artifact returns metadata plus the artifact reference. Oversized answers are never truncated.',
|
|
144
|
+
}),
|
|
145
|
+
),
|
|
146
|
+
},
|
|
147
|
+
{ additionalProperties: false },
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
type DelegateParamsValue = Static<typeof DelegateParams>;
|
|
151
|
+
type ResultParamsValue = Static<typeof ResultParams>;
|
|
152
|
+
|
|
153
|
+
const DELEGATE_PARAM_KEYS = new Set([
|
|
154
|
+
'name',
|
|
155
|
+
'prompt',
|
|
156
|
+
'route',
|
|
157
|
+
'capability',
|
|
158
|
+
'extensionMode',
|
|
159
|
+
'maxTurns',
|
|
160
|
+
'maxToolCalls',
|
|
161
|
+
'timeoutSeconds',
|
|
162
|
+
'autoDeliver',
|
|
163
|
+
'notifyOnCompletion',
|
|
164
|
+
'triggerOnCompletion',
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
export interface DelegateLaunchDetails {
|
|
168
|
+
schema_version: 'unipi-background-tasks.delegate-launch.v1';
|
|
169
|
+
task: BgTaskSnapshot;
|
|
170
|
+
route: { provider: string; model: string; qualified_id: string; origin: string };
|
|
171
|
+
child_session_id: string;
|
|
172
|
+
artifact_dir: string;
|
|
173
|
+
seed_sha256: string;
|
|
174
|
+
seed_utf8_bytes: number;
|
|
175
|
+
budget: DelegateBudgetRouteSource;
|
|
176
|
+
extension_mode: DelegateExtensionMode;
|
|
177
|
+
auto_deliver: DelegateAutoDeliverMode;
|
|
178
|
+
notify_on_completion: boolean;
|
|
179
|
+
trigger_on_completion: boolean;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface FusionBackgroundResultDetails {
|
|
183
|
+
schema_version: 'unipi-background-tasks.fusion-result-view.v1';
|
|
184
|
+
task_id: string;
|
|
185
|
+
state: 'running' | 'committed' | 'failed' | 'cancelled';
|
|
186
|
+
delivery: DelegateDeliveryMode | 'none';
|
|
187
|
+
workflow: FusionWorkflowId;
|
|
188
|
+
artifact_dir: string;
|
|
189
|
+
answer_bytes?: number | undefined;
|
|
190
|
+
answer_sha256?: string | undefined;
|
|
191
|
+
usage_delivered?: boolean | undefined;
|
|
192
|
+
answer?: { present: false; reason: 'run_did_not_commit' } | undefined;
|
|
193
|
+
summary_status?: FusionFailureResultView['summary_status'] | undefined;
|
|
194
|
+
failure_summary_ref?: FusionFailureResultView['failure_summary_ref'] | undefined;
|
|
195
|
+
failure?: FusionFailureResultView['failure'] | undefined;
|
|
196
|
+
progress?: FusionFailureResultView['progress'] | undefined;
|
|
197
|
+
usage_so_far?: FusionFailureResultView['usage_so_far'] | undefined;
|
|
198
|
+
attempts?: FusionFailureResultView['attempts'] | undefined;
|
|
199
|
+
evidence_artifacts?: FusionFailureResultView['evidence_artifacts'] | undefined;
|
|
200
|
+
remediation_ids?: FusionFailureResultView['remediation_ids'] | undefined;
|
|
201
|
+
summary_unavailable_reason?: FusionFailureResultView['summary_unavailable_reason'] | undefined;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export type BackgroundResultDetails = DelegateResultDetails | FusionBackgroundResultDetails;
|
|
205
|
+
|
|
206
|
+
export interface DelegateResultDetails {
|
|
207
|
+
schema_version: 'unipi-background-tasks.delegate-result-view.v1';
|
|
208
|
+
task_id: string;
|
|
209
|
+
state: 'running' | 'committed' | 'failed' | 'cancelled';
|
|
210
|
+
delivery: DelegateDeliveryMode | 'none';
|
|
211
|
+
route?: { provider: string; model: string } | undefined;
|
|
212
|
+
budget?: DelegateBudgetRouteSource | undefined;
|
|
213
|
+
extension_mode?: DelegateExtensionMode | undefined;
|
|
214
|
+
answer_bytes?: number | undefined;
|
|
215
|
+
answer_sha256?: string | undefined;
|
|
216
|
+
turns?: number | undefined;
|
|
217
|
+
tool_calls?: number | undefined;
|
|
218
|
+
usage?: { status: string } | undefined;
|
|
219
|
+
artifact_dir?: string | undefined;
|
|
220
|
+
error_code?: string | undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function textContent(text: string) {
|
|
224
|
+
return [{ type: 'text' as const, text }];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function isRecord(value: unknown): value is Record<PropertyKey, unknown> {
|
|
228
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function requireCapability(value: unknown): DelegateCapability {
|
|
232
|
+
if (value === undefined) return 'inspect';
|
|
233
|
+
if (value === 'inspect') return 'inspect';
|
|
234
|
+
throw new DelegateError(
|
|
235
|
+
`bg_delegate capability must be one of ${DELEGATE_CAPABILITIES.join(', ')}. Writable profiles are deliberately out of scope in this version.`,
|
|
236
|
+
{ code: 'invalid_arguments', childCreated: false },
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function requireExtensionMode(value: unknown): DelegateExtensionMode {
|
|
241
|
+
if (value === undefined) return 'isolated';
|
|
242
|
+
if (value === 'isolated' || value === 'ambient') return value;
|
|
243
|
+
throw new DelegateError(
|
|
244
|
+
`bg_delegate extensionMode must be one of ${DELEGATE_EXTENSION_MODES.join(', ')}`,
|
|
245
|
+
{ code: 'invalid_arguments', childCreated: false },
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function requireAutoDeliver(value: unknown): DelegateAutoDeliverMode {
|
|
250
|
+
if (value === undefined) return 'never';
|
|
251
|
+
if (value === 'never' || value === 'when_small' || value === 'always') return value;
|
|
252
|
+
throw new DelegateError(
|
|
253
|
+
`bg_delegate autoDeliver must be one of ${DELEGATE_AUTO_DELIVER_MODES.join(', ')}`,
|
|
254
|
+
{ code: 'invalid_arguments', childCreated: false },
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function requireDelivery(value: unknown): DelegateDeliveryMode | undefined {
|
|
259
|
+
if (value === undefined) return undefined;
|
|
260
|
+
if (value === 'inline' || value === 'artifact') return value;
|
|
261
|
+
throw new DelegateError('bg_result delivery must be inline or artifact', {
|
|
262
|
+
code: 'invalid_arguments',
|
|
263
|
+
childCreated: false,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function requireRoute(value: unknown): DelegateRoute | undefined {
|
|
268
|
+
if (value === undefined) return undefined;
|
|
269
|
+
if (!isRecord(value)) {
|
|
270
|
+
throw new DelegateError('bg_delegate route must be an object', {
|
|
271
|
+
code: 'invalid_arguments',
|
|
272
|
+
childCreated: false,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
const provider = value['provider'];
|
|
276
|
+
const model = value['model'];
|
|
277
|
+
if (typeof provider !== 'string' || provider.length === 0)
|
|
278
|
+
throw new DelegateError('bg_delegate route.provider must be a non-empty string', {
|
|
279
|
+
code: 'invalid_arguments',
|
|
280
|
+
childCreated: false,
|
|
281
|
+
});
|
|
282
|
+
if (typeof model !== 'string' || model.length === 0)
|
|
283
|
+
throw new DelegateError('bg_delegate route.model must be a non-empty string', {
|
|
284
|
+
code: 'invalid_arguments',
|
|
285
|
+
childCreated: false,
|
|
286
|
+
});
|
|
287
|
+
return { provider, model };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function optionalPositiveInteger(value: unknown, label: string): number | undefined {
|
|
291
|
+
if (value === undefined) return undefined;
|
|
292
|
+
if (
|
|
293
|
+
typeof value !== 'number' ||
|
|
294
|
+
!Number.isFinite(value) ||
|
|
295
|
+
!Number.isInteger(value) ||
|
|
296
|
+
value <= 0
|
|
297
|
+
)
|
|
298
|
+
throw new DelegateError(`bg_delegate ${label} must be a positive integer`, {
|
|
299
|
+
code: 'invalid_arguments',
|
|
300
|
+
childCreated: false,
|
|
301
|
+
});
|
|
302
|
+
return value;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export interface DelegateExtensionDependencies {
|
|
306
|
+
startDelegateTask: (ctx: ExtensionContext, options: StartDelegateTaskOptions) => Promise<BgTask>;
|
|
307
|
+
snapshot: (task: BgTask) => BgTaskSnapshot;
|
|
308
|
+
resolveTask: (idOrPrefix: string) => BgTask;
|
|
309
|
+
claimFusionUsage: (task: BgTask) => Promise<boolean>;
|
|
310
|
+
/** Overridable so tests can supply observed evidence without touching disk. */
|
|
311
|
+
loadHookEvidence?: (() => Promise<DelegateHookContractEvidence>) | undefined;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function defaultHookEvidence(): Promise<DelegateHookContractEvidence> {
|
|
315
|
+
let raw: string;
|
|
316
|
+
try {
|
|
317
|
+
raw = await readFile(HOOK_EVIDENCE_PATH, 'utf8');
|
|
318
|
+
} catch (error) {
|
|
319
|
+
throw new DelegateError(
|
|
320
|
+
`bg_delegate cannot verify the Pi hook contract: the recorded evidence at ${HOOK_EVIDENCE_PATH} is unreadable (${error instanceof Error ? error.message : String(error)}). No child was created.`,
|
|
321
|
+
{
|
|
322
|
+
code: 'delegate_hook_contract_unsupported',
|
|
323
|
+
childCreated: false,
|
|
324
|
+
remediation: [
|
|
325
|
+
'Run the Pi hook characterisation gate to regenerate the evidence for this Pi build.',
|
|
326
|
+
'The guard is never bypassed when its evidence is missing.',
|
|
327
|
+
],
|
|
328
|
+
},
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
return loadDelegateHookContractEvidence(raw);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function registerDelegateExtension(
|
|
335
|
+
pi: ExtensionAPI,
|
|
336
|
+
deps: DelegateExtensionDependencies,
|
|
337
|
+
): void {
|
|
338
|
+
const loadEvidence = deps.loadHookEvidence ?? defaultHookEvidence;
|
|
339
|
+
|
|
340
|
+
pi.registerTool<typeof DelegateParams, DelegateLaunchDetails>({
|
|
341
|
+
name: DELEGATE_TOOL_NAME,
|
|
342
|
+
label: 'Background Delegate',
|
|
343
|
+
description:
|
|
344
|
+
'Launch one background Pi agent seeded with a frozen projection of the current conversation, then return a launch receipt immediately. The child has its own session, a route pinned at launch that is never substituted, and read-only tools. Extension discovery is isolated by default; ambient mode supports extension-registered providers but executes arbitrary discovered extension code. Retrieve its verified answer with bg_result.',
|
|
345
|
+
promptSnippet:
|
|
346
|
+
'Delegate an investigation to a background agent that already has this conversation as context',
|
|
347
|
+
promptGuidelines: [
|
|
348
|
+
'Use bg_delegate when work should continue in the background and the worker needs what you already know: it is seeded with a projection of this conversation.',
|
|
349
|
+
'The prompt is authoritative. State exactly what you want investigated and what the answer should contain.',
|
|
350
|
+
'The delegate is inspect-only at the model-visible tool boundary: it can read, search, and list files, but cannot run shell commands, edit or write files, use the network, or delegate further.',
|
|
351
|
+
'Extension discovery is isolated by default. Use extensionMode:"ambient" only when the pinned provider is registered by an ambient user/project extension.',
|
|
352
|
+
'Ambient mode executes arbitrary discovered extension code in the child process. Tool allowlists do not sandbox extension code, so ambient mode weakens inspect-only process isolation.',
|
|
353
|
+
'Facts that exist only inside omitted tool output are not available to the delegate. Restate such findings in the prompt.',
|
|
354
|
+
'bg_delegate returns immediately. Do not poll; retrieve the answer with bg_result after the terminal notification arrives.',
|
|
355
|
+
],
|
|
356
|
+
parameters: DelegateParams,
|
|
357
|
+
prepareArguments(args): DelegateParamsValue {
|
|
358
|
+
if (!isRecord(args))
|
|
359
|
+
throw new DelegateError('bg_delegate arguments must be an object', {
|
|
360
|
+
code: 'invalid_arguments',
|
|
361
|
+
childCreated: false,
|
|
362
|
+
});
|
|
363
|
+
const unknownKeys = Object.keys(args).filter((key) => !DELEGATE_PARAM_KEYS.has(key));
|
|
364
|
+
if (unknownKeys.length > 0) {
|
|
365
|
+
throw new DelegateError(
|
|
366
|
+
`bg_delegate contains unsupported key(s): ${unknownKeys.sort().join(', ')}`,
|
|
367
|
+
{ code: 'invalid_arguments', childCreated: false },
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
const name = args['name'];
|
|
371
|
+
const prompt = args['prompt'];
|
|
372
|
+
if (typeof name !== 'string' || name.trim().length === 0)
|
|
373
|
+
throw new DelegateError('bg_delegate requires a non-empty name', {
|
|
374
|
+
code: 'invalid_arguments',
|
|
375
|
+
childCreated: false,
|
|
376
|
+
});
|
|
377
|
+
if (typeof prompt !== 'string' || prompt.trim().length === 0)
|
|
378
|
+
throw new DelegateError('bg_delegate requires a non-blank prompt', {
|
|
379
|
+
code: 'invalid_arguments',
|
|
380
|
+
childCreated: false,
|
|
381
|
+
});
|
|
382
|
+
const prepared: DelegateParamsValue = { name, prompt };
|
|
383
|
+
const route = requireRoute(args['route']);
|
|
384
|
+
if (route !== undefined) prepared.route = route;
|
|
385
|
+
prepared.capability = requireCapability(args['capability']);
|
|
386
|
+
prepared.extensionMode = requireExtensionMode(args['extensionMode']);
|
|
387
|
+
prepared.autoDeliver = requireAutoDeliver(args['autoDeliver']);
|
|
388
|
+
const maxTurns = optionalPositiveInteger(args['maxTurns'], 'maxTurns');
|
|
389
|
+
if (maxTurns !== undefined) prepared.maxTurns = maxTurns;
|
|
390
|
+
const maxToolCalls = optionalPositiveInteger(args['maxToolCalls'], 'maxToolCalls');
|
|
391
|
+
if (maxToolCalls !== undefined) prepared.maxToolCalls = maxToolCalls;
|
|
392
|
+
const timeoutSeconds = optionalPositiveInteger(args['timeoutSeconds'], 'timeoutSeconds');
|
|
393
|
+
if (timeoutSeconds !== undefined) prepared.timeoutSeconds = timeoutSeconds;
|
|
394
|
+
const notify = args['notifyOnCompletion'];
|
|
395
|
+
if (typeof notify === 'boolean') prepared.notifyOnCompletion = notify;
|
|
396
|
+
const trigger = args['triggerOnCompletion'];
|
|
397
|
+
if (typeof trigger === 'boolean') prepared.triggerOnCompletion = trigger;
|
|
398
|
+
return prepared;
|
|
399
|
+
},
|
|
400
|
+
async execute(toolCallId, params, _signal, _onUpdate, ctx) {
|
|
401
|
+
const capability = requireCapability(params.capability);
|
|
402
|
+
const extensionMode = requireExtensionMode(params.extensionMode);
|
|
403
|
+
const autoDeliver = requireAutoDeliver(params.autoDeliver);
|
|
404
|
+
const hookEvidence = await loadEvidence();
|
|
405
|
+
const route = resolveDelegateRoute({
|
|
406
|
+
requested: params.route,
|
|
407
|
+
currentModel:
|
|
408
|
+
ctx.model === undefined
|
|
409
|
+
? undefined
|
|
410
|
+
: {
|
|
411
|
+
provider: ctx.model.provider,
|
|
412
|
+
id: ctx.model.id,
|
|
413
|
+
contextWindow: ctx.model.contextWindow,
|
|
414
|
+
},
|
|
415
|
+
availableModels: ctx.modelRegistry.getAll().map((model) => ({
|
|
416
|
+
provider: model.provider,
|
|
417
|
+
id: model.id,
|
|
418
|
+
contextWindow: model.contextWindow,
|
|
419
|
+
})),
|
|
420
|
+
thinkingLevel: pi.getThinkingLevel(),
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const prepared = await prepareDelegateLaunch({
|
|
424
|
+
ctx: {
|
|
425
|
+
cwd: ctx.cwd,
|
|
426
|
+
sessionManager: ctx.sessionManager,
|
|
427
|
+
getSystemPrompt: () => ctx.getSystemPrompt(),
|
|
428
|
+
},
|
|
429
|
+
toolCallId,
|
|
430
|
+
prompt: params.prompt,
|
|
431
|
+
capability,
|
|
432
|
+
extensionMode,
|
|
433
|
+
route,
|
|
434
|
+
limitOverrides: {
|
|
435
|
+
maxTurns: params.maxTurns,
|
|
436
|
+
maxToolCalls: params.maxToolCalls,
|
|
437
|
+
timeoutSeconds: params.timeoutSeconds,
|
|
438
|
+
},
|
|
439
|
+
hookEvidence,
|
|
440
|
+
cwd: ctx.cwd,
|
|
441
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
442
|
+
autoDeliver,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const launchOptions: StartDelegateTaskOptions = {
|
|
446
|
+
name: params.name,
|
|
447
|
+
argv: prepared.argv,
|
|
448
|
+
stdinBytes: prepared.stdinBytes,
|
|
449
|
+
env: prepared.env,
|
|
450
|
+
facts: prepared.facts,
|
|
451
|
+
notifyOnCompletion: params.notifyOnCompletion ?? true,
|
|
452
|
+
triggerOnCompletion: params.triggerOnCompletion ?? true,
|
|
453
|
+
timeoutSeconds: prepared.preflight.limits.timeout_seconds,
|
|
454
|
+
};
|
|
455
|
+
const task = await deps.startDelegateTask(ctx, launchOptions);
|
|
456
|
+
|
|
457
|
+
const details: DelegateLaunchDetails = {
|
|
458
|
+
schema_version: 'unipi-background-tasks.delegate-launch.v1',
|
|
459
|
+
task: deps.snapshot(task),
|
|
460
|
+
route: {
|
|
461
|
+
provider: route.provider,
|
|
462
|
+
model: route.model,
|
|
463
|
+
qualified_id: route.qualified_id,
|
|
464
|
+
origin: route.origin,
|
|
465
|
+
},
|
|
466
|
+
child_session_id: prepared.preflight.childSessionId,
|
|
467
|
+
artifact_dir: prepared.facts.artifactDir,
|
|
468
|
+
seed_sha256: prepared.facts.seedSha256,
|
|
469
|
+
seed_utf8_bytes: Buffer.byteLength(prepared.preflight.seed.serialized, 'utf8'),
|
|
470
|
+
budget: prepared.facts.budget,
|
|
471
|
+
extension_mode: extensionMode,
|
|
472
|
+
auto_deliver: autoDeliver,
|
|
473
|
+
notify_on_completion: launchOptions.notifyOnCompletion,
|
|
474
|
+
trigger_on_completion: launchOptions.triggerOnCompletion,
|
|
475
|
+
};
|
|
476
|
+
return {
|
|
477
|
+
content: textContent(
|
|
478
|
+
[
|
|
479
|
+
`Started delegate ${params.name} (${task.id})`,
|
|
480
|
+
`Route pinned: ${route.qualified_id} (${route.origin}); it is never substituted.`,
|
|
481
|
+
`Child session: ${prepared.preflight.childSessionId} (separate from this session)`,
|
|
482
|
+
`Artifacts: ${prepared.facts.artifactDir}`,
|
|
483
|
+
`Seed: ${String(Buffer.byteLength(prepared.preflight.seed.serialized, 'utf8'))} bytes, sha256 ${prepared.facts.seedSha256}`,
|
|
484
|
+
`Child prompt: ${String(prepared.preflight.plan.child_prompt_utf8_bytes)} bytes; launch estimate ${String(prepared.preflight.plan.launch_input_tokens_upper_bound)} / ${String(prepared.preflight.plan.route.allowed_input_tokens)} allowed input tokens; protected retained-growth runway ${String(prepared.preflight.plan.retained_growth_budget_tokens)} tokens.`,
|
|
485
|
+
`Estimator: family ${prepared.facts.budget.family}, source ${prepared.facts.budget.rate_source.source}, rate ${String(prepared.facts.budget.rate_source.effective_rate_bytes_per_token_x100)}/100 B/tok + ${String(prepared.facts.budget.rate_source.affine_f_tokens)} tokens${prepared.facts.budget.rate_source.warning === null ? '' : `; warning: ${prepared.facts.budget.rate_source.warning}`}`,
|
|
486
|
+
`Capability: ${capability} (read/search/list only)`,
|
|
487
|
+
`Extension mode: ${extensionMode}${extensionMode === 'ambient' ? ' — WARNING: arbitrary discovered extension code executes in the child; the tool allowlist does not sandbox it, so inspect-only process isolation is weakened.' : ' (ambient extension discovery disabled)'}`,
|
|
488
|
+
`Limits: ${String(prepared.preflight.limits.max_turns)} turns, ${String(prepared.preflight.limits.max_tool_calls)} tool calls, ${String(prepared.preflight.limits.timeout_seconds)}s`,
|
|
489
|
+
`Auto-deliver: ${autoDeliver}`,
|
|
490
|
+
launchOptions.notifyOnCompletion
|
|
491
|
+
? `Terminal notification: enabled.${launchOptions.triggerOnCompletion ? ' It will start a follow-up turn.' : ' It will not start a turn.'}`
|
|
492
|
+
: 'Terminal notification: disabled.',
|
|
493
|
+
`Retrieve the verified answer with ${DELEGATE_RESULT_TOOL_NAME}({taskId:"${task.id}"}). Do not poll.`,
|
|
494
|
+
].join('\n'),
|
|
495
|
+
),
|
|
496
|
+
details,
|
|
497
|
+
};
|
|
498
|
+
},
|
|
499
|
+
renderCall(args, theme) {
|
|
500
|
+
return new Text(
|
|
501
|
+
`${theme.fg('toolTitle', theme.bold('bg_delegate '))}${theme.fg('muted', truncateChars(args.name, 60))}`,
|
|
502
|
+
0,
|
|
503
|
+
0,
|
|
504
|
+
);
|
|
505
|
+
},
|
|
506
|
+
renderResult(result, _options, theme) {
|
|
507
|
+
const details = result.details;
|
|
508
|
+
return new Text(
|
|
509
|
+
`${theme.fg('success', '✓ delegated')} ${theme.fg('accent', details.task.id)}\n${theme.fg('dim', `route ${details.route.qualified_id} · extensions ${details.extension_mode} · seed ${String(details.seed_utf8_bytes)}B · ${details.artifact_dir}`)}`,
|
|
510
|
+
0,
|
|
511
|
+
0,
|
|
512
|
+
);
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
pi.registerTool<typeof ResultParams, BackgroundResultDetails>({
|
|
517
|
+
name: DELEGATE_RESULT_TOOL_NAME,
|
|
518
|
+
label: 'Background Result',
|
|
519
|
+
description:
|
|
520
|
+
'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.',
|
|
521
|
+
promptSnippet: 'Retrieve the verified answer from a completed delegate or Fusion task',
|
|
522
|
+
promptGuidelines: [
|
|
523
|
+
'Call bg_result once the delegate or Fusion terminal notification has arrived. It never blocks and must not be polled.',
|
|
524
|
+
'A not-ready result means the task is still running; end the turn and wait for the notification.',
|
|
525
|
+
],
|
|
526
|
+
parameters: ResultParams,
|
|
527
|
+
prepareArguments(args): ResultParamsValue {
|
|
528
|
+
if (!isRecord(args))
|
|
529
|
+
throw new DelegateError('bg_result arguments must be an object', {
|
|
530
|
+
code: 'invalid_arguments',
|
|
531
|
+
childCreated: false,
|
|
532
|
+
});
|
|
533
|
+
const taskId = args['taskId'];
|
|
534
|
+
if (typeof taskId !== 'string' || taskId.trim().length === 0)
|
|
535
|
+
throw new DelegateError('bg_result requires taskId', {
|
|
536
|
+
code: 'invalid_arguments',
|
|
537
|
+
childCreated: false,
|
|
538
|
+
});
|
|
539
|
+
const prepared: ResultParamsValue = { taskId };
|
|
540
|
+
const delivery = requireDelivery(args['delivery']);
|
|
541
|
+
if (delivery !== undefined) prepared.delivery = delivery;
|
|
542
|
+
return prepared;
|
|
543
|
+
},
|
|
544
|
+
async execute(_toolCallId, params) {
|
|
545
|
+
let task: BgTask;
|
|
546
|
+
try {
|
|
547
|
+
task = deps.resolveTask(params.taskId);
|
|
548
|
+
} catch (error) {
|
|
549
|
+
throw new DelegateError(
|
|
550
|
+
`bg_result does not know task ${params.taskId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
551
|
+
{ code: 'task_unknown', childCreated: false },
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
const fusion = task.fusion;
|
|
555
|
+
if (fusion !== undefined) {
|
|
556
|
+
const requestedDelivery = requireDelivery(params.delivery);
|
|
557
|
+
if (task.status === 'running') {
|
|
558
|
+
const details: FusionBackgroundResultDetails = {
|
|
559
|
+
schema_version: 'unipi-background-tasks.fusion-result-view.v1',
|
|
560
|
+
task_id: task.id,
|
|
561
|
+
state: 'running',
|
|
562
|
+
delivery: 'none',
|
|
563
|
+
workflow: fusion.workflow,
|
|
564
|
+
artifact_dir: fusion.artifactDir,
|
|
565
|
+
};
|
|
566
|
+
return {
|
|
567
|
+
content: textContent(
|
|
568
|
+
`Fusion ${task.id} is still running. bg_result never blocks. End this turn; the terminal notification will wake you, then call bg_result again.`,
|
|
569
|
+
),
|
|
570
|
+
details,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
if (task.status !== 'completed' || fusion.outcome?.status !== 'committed') {
|
|
574
|
+
const terminal = await readFusionFailureResult({
|
|
575
|
+
artifactDirAbs: fusion.artifactDirAbs,
|
|
576
|
+
artifactDir: fusion.artifactDir,
|
|
577
|
+
runId: fusion.runId,
|
|
578
|
+
workflow: fusion.workflow,
|
|
579
|
+
});
|
|
580
|
+
const state =
|
|
581
|
+
fusion.outcome?.status === 'cancelled' || task.status === 'killed'
|
|
582
|
+
? 'cancelled'
|
|
583
|
+
: 'failed';
|
|
584
|
+
const details: FusionBackgroundResultDetails = {
|
|
585
|
+
schema_version: 'unipi-background-tasks.fusion-result-view.v1',
|
|
586
|
+
task_id: task.id,
|
|
587
|
+
state,
|
|
588
|
+
delivery: 'none',
|
|
589
|
+
workflow: fusion.workflow,
|
|
590
|
+
artifact_dir: fusion.artifactDir,
|
|
591
|
+
answer: terminal.answer,
|
|
592
|
+
summary_status: terminal.summary_status,
|
|
593
|
+
...(terminal.failure_summary_ref === undefined
|
|
594
|
+
? {}
|
|
595
|
+
: { failure_summary_ref: terminal.failure_summary_ref }),
|
|
596
|
+
...(terminal.failure === undefined ? {} : { failure: terminal.failure }),
|
|
597
|
+
...(terminal.progress === undefined ? {} : { progress: terminal.progress }),
|
|
598
|
+
...(terminal.usage_so_far === undefined ? {} : { usage_so_far: terminal.usage_so_far }),
|
|
599
|
+
...(terminal.attempts === undefined ? {} : { attempts: terminal.attempts }),
|
|
600
|
+
...(terminal.evidence_artifacts === undefined
|
|
601
|
+
? {}
|
|
602
|
+
: { evidence_artifacts: terminal.evidence_artifacts }),
|
|
603
|
+
...(terminal.remediation_ids === undefined
|
|
604
|
+
? {}
|
|
605
|
+
: { remediation_ids: terminal.remediation_ids }),
|
|
606
|
+
...(terminal.summary_unavailable_reason === undefined
|
|
607
|
+
? {}
|
|
608
|
+
: { summary_unavailable_reason: terminal.summary_unavailable_reason }),
|
|
609
|
+
};
|
|
610
|
+
return {
|
|
611
|
+
content: textContent(
|
|
612
|
+
`Fusion ${task.id} ${state}; no answer was committed. Terminal evidence status: ${terminal.summary_status}. Delivery is none; use only the manifest-bound artifact references in details.`,
|
|
613
|
+
),
|
|
614
|
+
details,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
const verified = await readFusionCommittedResult({
|
|
618
|
+
artifactDirAbs: fusion.artifactDirAbs,
|
|
619
|
+
artifactDir: fusion.artifactDir,
|
|
620
|
+
runId: fusion.runId,
|
|
621
|
+
workflow: fusion.workflow,
|
|
622
|
+
});
|
|
623
|
+
const answerBytes = Buffer.byteLength(verified.mergedText, 'utf8');
|
|
624
|
+
const answerSha256 = sha256Buffer(Buffer.from(verified.mergedText, 'utf8'));
|
|
625
|
+
const useArtifact =
|
|
626
|
+
requestedDelivery === 'artifact' ||
|
|
627
|
+
(requestedDelivery === undefined && answerBytes > DELEGATE_INLINE_ANSWER_BYTES);
|
|
628
|
+
if (requestedDelivery === 'inline' && answerBytes > DELEGATE_INLINE_ANSWER_BYTES) {
|
|
629
|
+
throw new Error(
|
|
630
|
+
`Fusion result ${task.id} is ${String(answerBytes)} bytes, above the ${String(DELEGATE_INLINE_ANSWER_BYTES)}-byte inline limit. Use delivery:"artifact"; nothing was truncated.`,
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
const usageDelivered = await deps.claimFusionUsage(task);
|
|
634
|
+
const details: FusionBackgroundResultDetails = {
|
|
635
|
+
schema_version: 'unipi-background-tasks.fusion-result-view.v1',
|
|
636
|
+
task_id: task.id,
|
|
637
|
+
state: 'committed',
|
|
638
|
+
delivery: useArtifact ? 'artifact' : 'inline',
|
|
639
|
+
workflow: fusion.workflow,
|
|
640
|
+
artifact_dir: fusion.artifactDir,
|
|
641
|
+
answer_bytes: answerBytes,
|
|
642
|
+
answer_sha256: answerSha256,
|
|
643
|
+
usage_delivered: usageDelivered,
|
|
644
|
+
};
|
|
645
|
+
const header = [
|
|
646
|
+
`Fusion ${task.id} completed (${fusion.workflow}).`,
|
|
647
|
+
`Answer: ${String(answerBytes)} bytes, ${answerSha256} (verified).`,
|
|
648
|
+
`Artifacts: ${fusion.artifactDir}`,
|
|
649
|
+
usageDelivered
|
|
650
|
+
? 'Usage: attached to this retrieval exactly once.'
|
|
651
|
+
: 'Usage: already attached by an earlier retrieval; not counted again.',
|
|
652
|
+
].join('\n');
|
|
653
|
+
const result = useArtifact
|
|
654
|
+
? {
|
|
655
|
+
content: textContent(
|
|
656
|
+
`${header}\nDelivery: artifact. The complete answer is ${fusion.artifactDir}/merged.md; it was not truncated.`,
|
|
657
|
+
),
|
|
658
|
+
details,
|
|
659
|
+
}
|
|
660
|
+
: { content: textContent(`${header}\n\n${verified.mergedText}`), details };
|
|
661
|
+
if (!usageDelivered) return result;
|
|
662
|
+
const resultWithUsage: typeof result & { usage: FusionUsage } = {
|
|
663
|
+
...result,
|
|
664
|
+
usage: cloneFusionUsage(verified.details.usage),
|
|
665
|
+
};
|
|
666
|
+
return resultWithUsage;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const facts = task.delegate;
|
|
670
|
+
if (facts === undefined) {
|
|
671
|
+
throw new DelegateError(
|
|
672
|
+
`bg_result task ${task.id} has no retrievable delegate or Fusion result; use bg_logs for ordinary background tasks`,
|
|
673
|
+
{ code: 'task_unknown', childCreated: false },
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
if (task.status === 'running') {
|
|
677
|
+
const details: DelegateResultDetails = {
|
|
678
|
+
schema_version: 'unipi-background-tasks.delegate-result-view.v1',
|
|
679
|
+
task_id: task.id,
|
|
680
|
+
state: 'running',
|
|
681
|
+
delivery: 'none',
|
|
682
|
+
artifact_dir: facts.artifactDir,
|
|
683
|
+
budget: facts.budget,
|
|
684
|
+
extension_mode: facts.extensionMode,
|
|
685
|
+
};
|
|
686
|
+
return {
|
|
687
|
+
content: textContent(
|
|
688
|
+
`Delegate ${task.id} is still running. This is not an error and bg_result never blocks. End this turn; the terminal notification will wake you, then call bg_result again.`,
|
|
689
|
+
),
|
|
690
|
+
details,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const terminal = await evaluateDelegateTerminal({
|
|
695
|
+
artifactDirAbs: facts.artifactDirAbs,
|
|
696
|
+
taskId: facts.taskId,
|
|
697
|
+
launchNonce: facts.launchNonce,
|
|
698
|
+
seedSha256: facts.seedSha256,
|
|
699
|
+
route: { provider: facts.route.provider, model: facts.route.model },
|
|
700
|
+
taskStatus: task.status === 'completed' ? 'completed' : task.status,
|
|
701
|
+
taskError: task.error,
|
|
702
|
+
taskOutputPath: task.outputPath,
|
|
703
|
+
taskOutputAbsPath: task.outputAbsPath,
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
if (terminal.error !== undefined || terminal.result === undefined) {
|
|
707
|
+
const failure =
|
|
708
|
+
terminal.error ??
|
|
709
|
+
new DelegateError(`delegate ${task.id} produced no verified answer`, {
|
|
710
|
+
code: 'result_unavailable',
|
|
711
|
+
childCreated: true,
|
|
712
|
+
taskId: task.id,
|
|
713
|
+
artifactDir: facts.artifactDir,
|
|
714
|
+
});
|
|
715
|
+
throw new Error(failure.describe(), { cause: failure });
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const verified = terminal.result;
|
|
719
|
+
const requestedDelivery = requireDelivery(params.delivery);
|
|
720
|
+
const decision = decideDelegateDelivery(
|
|
721
|
+
verified.package.answer.byte_length,
|
|
722
|
+
requestedDelivery,
|
|
723
|
+
);
|
|
724
|
+
if (requestedDelivery === 'inline' && decision.mode === 'artifact') {
|
|
725
|
+
const failure = inlineTooLarge(
|
|
726
|
+
task.id,
|
|
727
|
+
facts.artifactDir,
|
|
728
|
+
verified.package.answer.byte_length,
|
|
729
|
+
);
|
|
730
|
+
throw new Error(failure.describe(), { cause: failure });
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const details: DelegateResultDetails = {
|
|
734
|
+
schema_version: 'unipi-background-tasks.delegate-result-view.v1',
|
|
735
|
+
task_id: task.id,
|
|
736
|
+
state: 'committed',
|
|
737
|
+
delivery: decision.mode,
|
|
738
|
+
route: verified.package.route,
|
|
739
|
+
budget: facts.budget,
|
|
740
|
+
extension_mode: facts.extensionMode,
|
|
741
|
+
answer_bytes: verified.package.answer.byte_length,
|
|
742
|
+
answer_sha256: verified.package.answer.sha256,
|
|
743
|
+
turns: verified.package.turns,
|
|
744
|
+
tool_calls: verified.package.tool_calls,
|
|
745
|
+
usage: { status: verified.package.usage.status },
|
|
746
|
+
artifact_dir: facts.artifactDir,
|
|
747
|
+
};
|
|
748
|
+
const header = [
|
|
749
|
+
`Delegate ${task.id} completed on ${verified.package.route.provider}/${verified.package.route.model}.`,
|
|
750
|
+
`Answer: ${String(verified.package.answer.byte_length)} bytes, sha256 ${verified.package.answer.sha256} (verified).`,
|
|
751
|
+
`Turns: ${String(verified.package.turns)} · tool calls: ${String(verified.package.tool_calls)} · usage: ${verified.package.usage.status}`,
|
|
752
|
+
`Artifacts: ${facts.artifactDir}`,
|
|
753
|
+
`Estimator: family ${facts.budget.family}, source ${facts.budget.rate_source.source}, rate ${String(facts.budget.rate_source.effective_rate_bytes_per_token_x100)}/100 B/tok + ${String(facts.budget.rate_source.affine_f_tokens)} tokens${facts.budget.rate_source.warning === null ? '' : `; warning: ${facts.budget.rate_source.warning}`}`,
|
|
754
|
+
].join('\n');
|
|
755
|
+
if (decision.mode === 'artifact') {
|
|
756
|
+
return {
|
|
757
|
+
content: textContent(
|
|
758
|
+
`${header}\nDelivery: artifact (${decision.reason}). The complete verified answer is in ${facts.artifactDir}/result.json. It was not truncated.`,
|
|
759
|
+
),
|
|
760
|
+
details,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
return { content: textContent(`${header}\n\n${verified.answer}`), details };
|
|
764
|
+
},
|
|
765
|
+
renderCall(args, theme) {
|
|
766
|
+
return new Text(
|
|
767
|
+
`${theme.fg('toolTitle', theme.bold('bg_result '))}${theme.fg('accent', args.taskId)}`,
|
|
768
|
+
0,
|
|
769
|
+
0,
|
|
770
|
+
);
|
|
771
|
+
},
|
|
772
|
+
renderResult(result, options: ToolRenderResultOptions, theme: Theme) {
|
|
773
|
+
void options;
|
|
774
|
+
const details = result.details;
|
|
775
|
+
const fusion = details.schema_version === 'unipi-background-tasks.fusion-result-view.v1';
|
|
776
|
+
if (details.state === 'running')
|
|
777
|
+
return new Text(
|
|
778
|
+
theme.fg('warning', `${fusion ? 'fusion' : 'delegate'} ${details.task_id} still running`),
|
|
779
|
+
0,
|
|
780
|
+
0,
|
|
781
|
+
);
|
|
782
|
+
if (fusion && (details.state === 'failed' || details.state === 'cancelled')) {
|
|
783
|
+
return new Text(
|
|
784
|
+
theme.fg('warning', `${details.state} fusion; no committed answer · ${details.summary_status ?? 'unavailable'}`),
|
|
785
|
+
0,
|
|
786
|
+
0,
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
return new Text(
|
|
790
|
+
`${theme.fg('success', fusion ? '✓ fusion answer' : '✓ delegate answer')} ${theme.fg('dim', `${String(details.answer_bytes ?? 0)}B · ${details.delivery}`)}`,
|
|
791
|
+
0,
|
|
792
|
+
0,
|
|
793
|
+
);
|
|
794
|
+
},
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
pi.on('session_start', () => {
|
|
798
|
+
const active = pi.getActiveTools();
|
|
799
|
+
const missing = [DELEGATE_TOOL_NAME, DELEGATE_RESULT_TOOL_NAME].filter(
|
|
800
|
+
(name) => !active.includes(name),
|
|
801
|
+
);
|
|
802
|
+
if (missing.length > 0) pi.setActiveTools([...active, ...missing]);
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
export { DELEGATE_INLINE_ANSWER_BYTES };
|