@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,423 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalJson } from '../attested-pi-run.js';
|
|
3
|
+
import {
|
|
4
|
+
UnsupportedConversationBlockError,
|
|
5
|
+
projectVisibleConversationV2,
|
|
6
|
+
type ProjectedConversationV2,
|
|
7
|
+
} from '../context-visible-conversation-v2.js';
|
|
8
|
+
import {
|
|
9
|
+
snapshotParentConversation,
|
|
10
|
+
type ParentContextSource,
|
|
11
|
+
type ParentSnapshotOptions,
|
|
12
|
+
} from '../context-parent-snapshot.js';
|
|
13
|
+
import {
|
|
14
|
+
DELEGATE_BRANCH_FILTER_ID,
|
|
15
|
+
DELEGATE_CONTEXT_POLICY_ID,
|
|
16
|
+
DELEGATE_LEDGER_SCHEMA_VERSION,
|
|
17
|
+
DELEGATE_SEED_SCHEMA_VERSION,
|
|
18
|
+
DELEGATE_TOOL_NAME,
|
|
19
|
+
DelegateError,
|
|
20
|
+
type DelegateBranchFilterDescriptor,
|
|
21
|
+
type DelegateCapability,
|
|
22
|
+
type DelegateContextPolicyDescriptor,
|
|
23
|
+
type DelegateConversationProjection,
|
|
24
|
+
type DelegateLedgerV1,
|
|
25
|
+
type DelegateExtensionMode,
|
|
26
|
+
type DelegateLimits,
|
|
27
|
+
type DelegatePinnedRoute,
|
|
28
|
+
type DelegateSeedV1,
|
|
29
|
+
type DelegateTaskDirective,
|
|
30
|
+
} from './types.js';
|
|
31
|
+
|
|
32
|
+
export interface BuildDelegateSeedOptions {
|
|
33
|
+
taskId: string;
|
|
34
|
+
launchNonce: string;
|
|
35
|
+
toolCallId: string | undefined;
|
|
36
|
+
directive: string;
|
|
37
|
+
capability: DelegateCapability;
|
|
38
|
+
extensionMode: DelegateExtensionMode;
|
|
39
|
+
route: DelegatePinnedRoute;
|
|
40
|
+
limits: DelegateLimits;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface BuiltDelegateSeed {
|
|
44
|
+
seed: DelegateSeedV1;
|
|
45
|
+
/** Exact bytes persisted and written to the child. Nothing re-serializes them. */
|
|
46
|
+
serialized: string;
|
|
47
|
+
sha256: string;
|
|
48
|
+
ledger: DelegateLedgerV1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function sha256Text(value: string): string {
|
|
52
|
+
return createHash('sha256').update(Buffer.from(value, 'utf8')).digest('hex');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function policyDescriptor(): DelegateContextPolicyDescriptor {
|
|
56
|
+
return {
|
|
57
|
+
id: DELEGATE_CONTEXT_POLICY_ID,
|
|
58
|
+
transform: 'visible-conversation-ledger-v2',
|
|
59
|
+
version: 1,
|
|
60
|
+
receipt_format: 'omitted_activity.v2',
|
|
61
|
+
user_text: 'verbatim',
|
|
62
|
+
assistant_text: 'verbatim',
|
|
63
|
+
assistant_thinking: 'ledger_only',
|
|
64
|
+
tool_call_arguments: 'ledger_only',
|
|
65
|
+
tool_results: 'ledger_only',
|
|
66
|
+
tool_payload_preview_bytes: 0,
|
|
67
|
+
images: 'marker_or_ledger_only',
|
|
68
|
+
unknown_block_behavior: 'error',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Seal the shared transform under delegate's own envelope.
|
|
74
|
+
*
|
|
75
|
+
* Delegate never imports Fusion's canonical-input builder and never emits
|
|
76
|
+
* `fusion-input.v4`. The ledger root commits only to ledger rows, so the two
|
|
77
|
+
* consumers can seal identical bodies without either affecting the other.
|
|
78
|
+
*/
|
|
79
|
+
function sealDelegateProjection(
|
|
80
|
+
projected: ProjectedConversationV2,
|
|
81
|
+
branchFilter: DelegateBranchFilterDescriptor,
|
|
82
|
+
): { projection: DelegateConversationProjection; ledger: DelegateLedgerV1 } {
|
|
83
|
+
return {
|
|
84
|
+
projection: {
|
|
85
|
+
policy: policyDescriptor(),
|
|
86
|
+
branch_filter: branchFilter,
|
|
87
|
+
entries: projected.entries,
|
|
88
|
+
accounting: projected.accounting,
|
|
89
|
+
},
|
|
90
|
+
ledger: {
|
|
91
|
+
schema_version: DELEGATE_LEDGER_SCHEMA_VERSION,
|
|
92
|
+
policy_id: DELEGATE_CONTEXT_POLICY_ID,
|
|
93
|
+
transform: 'visible-conversation-ledger-v2',
|
|
94
|
+
entries: projected.ledger.entries,
|
|
95
|
+
projection_map: projected.ledger.projection_map,
|
|
96
|
+
root_sha256: projected.ledger.root_sha256,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Build the frozen delegate seed.
|
|
103
|
+
*
|
|
104
|
+
* The projection excludes the assistant message carrying the in-flight
|
|
105
|
+
* `bg_delegate` call. That message is excluded as a whole, so when several
|
|
106
|
+
* `bg_delegate` calls share one assistant message every sibling call is excluded
|
|
107
|
+
* for every child: two delegates launched together receive identical projected
|
|
108
|
+
* history, and neither can observe the other's arguments.
|
|
109
|
+
*
|
|
110
|
+
* The directive is authoritative; projected history is supporting, untrusted
|
|
111
|
+
* context. That relationship is stated to the child explicitly in its system
|
|
112
|
+
* prompt, and recorded structurally here as `authority: 'explicit_text'`.
|
|
113
|
+
*/
|
|
114
|
+
export function buildDelegateSeed(
|
|
115
|
+
ctx: ParentContextSource,
|
|
116
|
+
options: BuildDelegateSeedOptions,
|
|
117
|
+
): BuiltDelegateSeed {
|
|
118
|
+
const directiveText = options.directive;
|
|
119
|
+
if (directiveText.trim().length === 0) {
|
|
120
|
+
throw new DelegateError('bg_delegate prompt must not be blank', {
|
|
121
|
+
code: 'invalid_arguments',
|
|
122
|
+
childCreated: false,
|
|
123
|
+
remediation: ['Provide a non-blank prompt describing what the delegate should investigate.'],
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
const snapshotOptions: ParentSnapshotOptions = {
|
|
127
|
+
toolName: DELEGATE_TOOL_NAME,
|
|
128
|
+
excludeActiveToolCallLeaf: true,
|
|
129
|
+
};
|
|
130
|
+
if (options.toolCallId !== undefined) snapshotOptions.toolCallId = options.toolCallId;
|
|
131
|
+
|
|
132
|
+
let snapshot: ReturnType<typeof snapshotParentConversation>;
|
|
133
|
+
let projected: ProjectedConversationV2;
|
|
134
|
+
try {
|
|
135
|
+
snapshot = snapshotParentConversation(ctx, snapshotOptions);
|
|
136
|
+
projected = projectVisibleConversationV2(snapshot.messages);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (error instanceof UnsupportedConversationBlockError) {
|
|
139
|
+
throw new DelegateError(
|
|
140
|
+
`bg_delegate could not project the parent conversation: ${error.message}`,
|
|
141
|
+
{
|
|
142
|
+
code: 'seed_projection_failed',
|
|
143
|
+
childCreated: false,
|
|
144
|
+
remediation: [
|
|
145
|
+
'The parent conversation contains a block type this package version does not know how to project.',
|
|
146
|
+
'Nothing was dropped or guessed and no child was created. Report the block type so the transform can be versioned.',
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const branchFilter: DelegateBranchFilterDescriptor = {
|
|
155
|
+
id: DELEGATE_BRANCH_FILTER_ID,
|
|
156
|
+
tool_name: DELEGATE_TOOL_NAME,
|
|
157
|
+
tool_call_id: options.toolCallId ?? null,
|
|
158
|
+
active_tool_call_leaf_excluded: snapshot.activeToolCallLeafExcluded,
|
|
159
|
+
};
|
|
160
|
+
const sealed = sealDelegateProjection(projected, branchFilter);
|
|
161
|
+
const directive: DelegateTaskDirective = {
|
|
162
|
+
text: directiveText,
|
|
163
|
+
sha256: sha256Text(directiveText),
|
|
164
|
+
authority: 'explicit_text',
|
|
165
|
+
};
|
|
166
|
+
const seed: DelegateSeedV1 = {
|
|
167
|
+
schema_version: DELEGATE_SEED_SCHEMA_VERSION,
|
|
168
|
+
task_id: options.taskId,
|
|
169
|
+
launch_nonce: options.launchNonce,
|
|
170
|
+
cwd: ctx.cwd,
|
|
171
|
+
capability: options.capability,
|
|
172
|
+
extension_mode: options.extensionMode,
|
|
173
|
+
route: options.route,
|
|
174
|
+
parent_system_prompt: ctx.getSystemPrompt(),
|
|
175
|
+
parent_leaf_id: snapshot.leafId,
|
|
176
|
+
directive,
|
|
177
|
+
conversation_projection: sealed.projection,
|
|
178
|
+
limits: options.limits,
|
|
179
|
+
};
|
|
180
|
+
const serialized = canonicalJson(seed);
|
|
181
|
+
return { seed, serialized, sha256: sha256Text(serialized), ledger: sealed.ledger };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const isRecord = (value: unknown): value is Record<PropertyKey, unknown> =>
|
|
185
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Receive-side seed validation performed inside the child before its first model
|
|
189
|
+
* call. A seed that does not match its declared length, hash, schema, task id,
|
|
190
|
+
* and launch nonce is a loud refusal, never a best-effort continue.
|
|
191
|
+
*/
|
|
192
|
+
export function verifyDelegateSeedBytes(
|
|
193
|
+
raw: string,
|
|
194
|
+
expected: { sha256: string; taskId: string; launchNonce: string },
|
|
195
|
+
): DelegateSeedV1 {
|
|
196
|
+
const actual = sha256Text(raw);
|
|
197
|
+
if (actual !== expected.sha256) {
|
|
198
|
+
throw new DelegateError(
|
|
199
|
+
`delegate seed hash mismatch: expected ${expected.sha256}, received ${actual}`,
|
|
200
|
+
{
|
|
201
|
+
code: 'seed_hash_mismatch',
|
|
202
|
+
childCreated: true,
|
|
203
|
+
taskId: expected.taskId,
|
|
204
|
+
remediation: ['The seed bytes delivered to the child differ from the persisted seed.'],
|
|
205
|
+
},
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
let parsed: unknown;
|
|
209
|
+
try {
|
|
210
|
+
parsed = JSON.parse(raw);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
throw new DelegateError(
|
|
213
|
+
`delegate seed is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
214
|
+
{ code: 'seed_hash_mismatch', childCreated: true, taskId: expected.taskId },
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
if (!isRecord(parsed)) {
|
|
218
|
+
throw new DelegateError('delegate seed must be a JSON object', {
|
|
219
|
+
code: 'seed_hash_mismatch',
|
|
220
|
+
childCreated: true,
|
|
221
|
+
taskId: expected.taskId,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
if (parsed['schema_version'] !== DELEGATE_SEED_SCHEMA_VERSION) {
|
|
225
|
+
throw new DelegateError(
|
|
226
|
+
`delegate seed schema_version must be ${DELEGATE_SEED_SCHEMA_VERSION}`,
|
|
227
|
+
{ code: 'seed_hash_mismatch', childCreated: true, taskId: expected.taskId },
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (parsed['task_id'] !== expected.taskId || parsed['launch_nonce'] !== expected.launchNonce) {
|
|
231
|
+
throw new DelegateError('delegate seed task identity does not match this child', {
|
|
232
|
+
code: 'seed_hash_mismatch',
|
|
233
|
+
childCreated: true,
|
|
234
|
+
taskId: expected.taskId,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
// The hash pins the exact bytes; this rebuild pins the shape the child is
|
|
238
|
+
// allowed to act on. Every field the child reads is validated and copied
|
|
239
|
+
// explicitly, so a structurally malformed seed cannot reach the agent loop by
|
|
240
|
+
// way of an unchecked assertion.
|
|
241
|
+
return rebuildSeed(parsed, expected.taskId);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function requireString(
|
|
245
|
+
record: Record<PropertyKey, unknown>,
|
|
246
|
+
key: string,
|
|
247
|
+
taskId: string,
|
|
248
|
+
): string {
|
|
249
|
+
const value = record[key];
|
|
250
|
+
if (typeof value !== 'string') {
|
|
251
|
+
throw new DelegateError(`delegate seed field ${key} must be a string`, {
|
|
252
|
+
code: 'seed_hash_mismatch',
|
|
253
|
+
childCreated: true,
|
|
254
|
+
taskId,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
return value;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function requireRecord(
|
|
261
|
+
record: Record<PropertyKey, unknown>,
|
|
262
|
+
key: string,
|
|
263
|
+
taskId: string,
|
|
264
|
+
): Record<PropertyKey, unknown> {
|
|
265
|
+
const value = record[key];
|
|
266
|
+
if (!isRecord(value)) {
|
|
267
|
+
throw new DelegateError(`delegate seed field ${key} must be an object`, {
|
|
268
|
+
code: 'seed_hash_mismatch',
|
|
269
|
+
childCreated: true,
|
|
270
|
+
taskId,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return value;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function requireNonNegativeInteger(
|
|
277
|
+
record: Record<PropertyKey, unknown>,
|
|
278
|
+
key: string,
|
|
279
|
+
taskId: string,
|
|
280
|
+
): number {
|
|
281
|
+
const value = record[key];
|
|
282
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
|
283
|
+
throw new DelegateError(`delegate seed field ${key} must be a non-negative safe integer`, {
|
|
284
|
+
code: 'seed_hash_mismatch',
|
|
285
|
+
childCreated: true,
|
|
286
|
+
taskId,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return value;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Explicit, field-by-field reconstruction of the seed fields the child consumes. */
|
|
293
|
+
function rebuildSeed(record: Record<PropertyKey, unknown>, taskId: string): DelegateSeedV1 {
|
|
294
|
+
const capability = requireString(record, 'capability', taskId);
|
|
295
|
+
if (capability !== 'inspect') {
|
|
296
|
+
throw new DelegateError(`delegate seed capability ${capability} is not supported`, {
|
|
297
|
+
code: 'delegate_isolation_unsupported',
|
|
298
|
+
childCreated: true,
|
|
299
|
+
taskId,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
const extensionMode = requireString(record, 'extension_mode', taskId);
|
|
303
|
+
if (extensionMode !== 'isolated' && extensionMode !== 'ambient') {
|
|
304
|
+
throw new DelegateError(`delegate seed extension mode ${extensionMode} is not recognised`, {
|
|
305
|
+
code: 'seed_hash_mismatch',
|
|
306
|
+
childCreated: true,
|
|
307
|
+
taskId,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
const routeRecord = requireRecord(record, 'route', taskId);
|
|
311
|
+
const routeOrigin = requireString(routeRecord, 'origin', taskId);
|
|
312
|
+
if (routeOrigin !== 'parent_current' && routeOrigin !== 'explicit') {
|
|
313
|
+
throw new DelegateError(`delegate seed route origin ${routeOrigin} is not recognised`, {
|
|
314
|
+
code: 'seed_hash_mismatch',
|
|
315
|
+
childCreated: true,
|
|
316
|
+
taskId,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
const route: DelegatePinnedRoute = {
|
|
320
|
+
provider: requireString(routeRecord, 'provider', taskId),
|
|
321
|
+
model: requireString(routeRecord, 'model', taskId),
|
|
322
|
+
qualified_id: requireString(routeRecord, 'qualified_id', taskId),
|
|
323
|
+
context_window_tokens: requireNonNegativeInteger(routeRecord, 'context_window_tokens', taskId),
|
|
324
|
+
thinking_level: requireString(routeRecord, 'thinking_level', taskId),
|
|
325
|
+
origin: routeOrigin,
|
|
326
|
+
};
|
|
327
|
+
const limitsRecord = requireRecord(record, 'limits', taskId);
|
|
328
|
+
const limits: DelegateLimits = {
|
|
329
|
+
max_turns: requireNonNegativeInteger(limitsRecord, 'max_turns', taskId),
|
|
330
|
+
max_tool_calls: requireNonNegativeInteger(limitsRecord, 'max_tool_calls', taskId),
|
|
331
|
+
timeout_seconds: requireNonNegativeInteger(limitsRecord, 'timeout_seconds', taskId),
|
|
332
|
+
max_tool_result_bytes: requireNonNegativeInteger(limitsRecord, 'max_tool_result_bytes', taskId),
|
|
333
|
+
max_total_tool_output_bytes: requireNonNegativeInteger(
|
|
334
|
+
limitsRecord,
|
|
335
|
+
'max_total_tool_output_bytes',
|
|
336
|
+
taskId,
|
|
337
|
+
),
|
|
338
|
+
max_answer_bytes: requireNonNegativeInteger(limitsRecord, 'max_answer_bytes', taskId),
|
|
339
|
+
allowed_input_tokens: requireNonNegativeInteger(limitsRecord, 'allowed_input_tokens', taskId),
|
|
340
|
+
};
|
|
341
|
+
const directiveRecord = requireRecord(record, 'directive', taskId);
|
|
342
|
+
const authority = requireString(directiveRecord, 'authority', taskId);
|
|
343
|
+
if (authority !== 'explicit_text') {
|
|
344
|
+
throw new DelegateError(`delegate seed directive authority ${authority} is not recognised`, {
|
|
345
|
+
code: 'seed_hash_mismatch',
|
|
346
|
+
childCreated: true,
|
|
347
|
+
taskId,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
const directive: DelegateTaskDirective = {
|
|
351
|
+
text: requireString(directiveRecord, 'text', taskId),
|
|
352
|
+
sha256: requireString(directiveRecord, 'sha256', taskId),
|
|
353
|
+
authority,
|
|
354
|
+
};
|
|
355
|
+
if (sha256Text(directive.text) !== directive.sha256) {
|
|
356
|
+
throw new DelegateError('delegate seed directive hash does not match its text', {
|
|
357
|
+
code: 'seed_hash_mismatch',
|
|
358
|
+
childCreated: true,
|
|
359
|
+
taskId,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
const parentLeafId = record['parent_leaf_id'];
|
|
363
|
+
if (parentLeafId !== null && typeof parentLeafId !== 'string') {
|
|
364
|
+
throw new DelegateError('delegate seed parent_leaf_id must be a string or null', {
|
|
365
|
+
code: 'seed_hash_mismatch',
|
|
366
|
+
childCreated: true,
|
|
367
|
+
taskId,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
const projection = requireRecord(record, 'conversation_projection', taskId);
|
|
371
|
+
const projectionPolicy = requireRecord(projection, 'policy', taskId);
|
|
372
|
+
if (requireString(projectionPolicy, 'id', taskId) !== DELEGATE_CONTEXT_POLICY_ID) {
|
|
373
|
+
throw new DelegateError('delegate seed projection policy id is not the delegate policy', {
|
|
374
|
+
code: 'seed_hash_mismatch',
|
|
375
|
+
childCreated: true,
|
|
376
|
+
taskId,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
if (!Array.isArray(projection['entries'])) {
|
|
380
|
+
throw new DelegateError('delegate seed projection entries must be an array', {
|
|
381
|
+
code: 'seed_hash_mismatch',
|
|
382
|
+
childCreated: true,
|
|
383
|
+
taskId,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
schema_version: DELEGATE_SEED_SCHEMA_VERSION,
|
|
388
|
+
task_id: requireString(record, 'task_id', taskId),
|
|
389
|
+
launch_nonce: requireString(record, 'launch_nonce', taskId),
|
|
390
|
+
cwd: requireString(record, 'cwd', taskId),
|
|
391
|
+
capability,
|
|
392
|
+
extension_mode: extensionMode,
|
|
393
|
+
route,
|
|
394
|
+
parent_system_prompt: requireString(record, 'parent_system_prompt', taskId),
|
|
395
|
+
parent_leaf_id: parentLeafId,
|
|
396
|
+
directive,
|
|
397
|
+
conversation_projection: readDelegateProjection(projection),
|
|
398
|
+
limits,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* The projection body is already hash-pinned by the seed digest verified above,
|
|
404
|
+
* so it is carried through without re-deriving values that would only duplicate
|
|
405
|
+
* the parent's computation.
|
|
406
|
+
*/
|
|
407
|
+
function readDelegateProjection(record: Record<PropertyKey, unknown>): DelegateConversationProjection {
|
|
408
|
+
const projection: unknown = record;
|
|
409
|
+
if (!isDelegateProjection(projection)) {
|
|
410
|
+
throw new DelegateError('delegate seed conversation_projection is malformed', {
|
|
411
|
+
code: 'seed_hash_mismatch',
|
|
412
|
+
childCreated: true,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
return projection;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function isDelegateProjection(value: unknown): value is DelegateConversationProjection {
|
|
419
|
+
if (!isRecord(value)) return false;
|
|
420
|
+
if (!isRecord(value['policy']) || !isRecord(value['branch_filter'])) return false;
|
|
421
|
+
if (!isRecord(value['accounting'])) return false;
|
|
422
|
+
return Array.isArray(value['entries']);
|
|
423
|
+
}
|