pi-background-tasks 0.7.7 → 1.0.3
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/BACKGROUND-TASKS-INSTRUCTIONS.md +63 -0
- package/PUBLISHING.md +43 -29
- package/README.md +234 -385
- package/TESTING.md +15 -9
- package/TEST_PLAN.md +46 -13
- package/docs/INDEX.md +157 -0
- package/docs/api/eventbus-v1.md +166 -0
- package/docs/assets/architecture.svg +78 -0
- package/docs/assets/footer-dock.svg +47 -0
- package/docs/assets/logo.svg +49 -0
- package/docs/attestations.json +189 -0
- package/docs/choose-a-workflow.md +98 -0
- package/docs/commands/bg-clear.md +70 -0
- package/docs/commands/bg-update.md +82 -0
- package/docs/commands/bg.md +90 -0
- package/docs/commands/fusion-models.md +70 -0
- package/docs/commands/fusion.md +69 -0
- package/docs/commands/jobs.md +74 -0
- package/docs/commands/kill.md +82 -0
- package/docs/commands/logs.md +90 -0
- package/docs/commands/task-manager.md +109 -0
- package/docs/concepts/completion-delivery.md +66 -0
- package/docs/concepts/context-projection-and-budgeting.md +79 -0
- package/docs/getting-started.md +122 -0
- package/docs/manifest.json +1825 -0
- package/docs/operations/configuration.md +110 -0
- package/docs/operations/releasing.md +67 -0
- package/docs/operations/testing.md +101 -0
- package/docs/operations/troubleshooting.md +38 -0
- package/docs/read-before-edit.md +94 -0
- package/docs/reference/runtime-contracts.md +213 -0
- package/docs/reference/shortcuts-and-dock.md +70 -0
- package/docs/subsystems/attested-pi-runs.md +141 -0
- package/docs/subsystems/background-task-runtime.md +85 -0
- package/docs/subsystems/child-launch-durability-and-safety.md +57 -0
- package/docs/subsystems/delegation.md +190 -0
- package/docs/subsystems/docs-freshness-gate.md +26 -0
- package/docs/subsystems/fusion.md +121 -0
- package/docs/subsystems/host-ui-and-telemetry.md +83 -0
- package/docs/tools/bg_delegate.md +193 -0
- package/docs/tools/bg_kill.md +114 -0
- package/docs/tools/bg_logs.md +133 -0
- package/docs/tools/bg_result.md +120 -0
- package/docs/tools/bg_run.md +168 -0
- package/docs/tools/bg_run_pi_attested.md +170 -0
- package/docs/tools/bg_status.md +111 -0
- package/docs/tools/fusion_investigate.md +116 -0
- package/docs/tools/fusion_reason.md +75 -0
- package/docs/tools/fusion_research.md +162 -0
- package/docs/tools/fusion_validate.md +206 -0
- package/logo.png +0 -0
- package/package.json +29 -6
- package/src/core/delegate/budget.ts +1 -1
- package/src/core/delegate/launch.ts +6 -0
- package/src/core/fusion/artifacts.ts +80 -5
- package/src/core/fusion/budget.ts +129 -28
- package/src/core/fusion/child-protocol.ts +82 -0
- package/src/core/fusion/clean-context.ts +91 -0
- package/src/core/fusion/config.ts +124 -35
- package/src/core/fusion/context.ts +33 -6
- package/src/core/fusion/evaluation.ts +392 -15
- package/src/core/fusion/orchestrator.ts +274 -25
- package/src/core/fusion/pi-child.ts +635 -10
- package/src/core/fusion/prompts.ts +167 -6
- package/src/core/fusion/source-policy.ts +257 -0
- package/src/core/fusion/types.ts +232 -5
- package/src/core/fusion/web-fetch.ts +993 -0
- package/src/core/fusion/workflows.ts +184 -0
- package/src/extension.ts +3 -3
- package/src/fusion-child-extension.ts +370 -54
- package/src/fusion-extension.ts +625 -125
- package/src/testing/normalize.ts +0 -22
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type FusionBudgetPlanV1,
|
|
16
16
|
type FusionCalibrationViolation,
|
|
17
17
|
type FusionCandidateId,
|
|
18
|
+
type FusionCapability,
|
|
18
19
|
type FusionContextOmissionLedgerV2,
|
|
19
20
|
type FusionChildRunResult,
|
|
20
21
|
type FusionModelConfigV1,
|
|
@@ -23,14 +24,21 @@ import {
|
|
|
23
24
|
type FusionState,
|
|
24
25
|
type FusionTerminalState,
|
|
25
26
|
type FusionUsage,
|
|
27
|
+
type FusionWorkflowId,
|
|
26
28
|
type ResolvedFusionModels,
|
|
27
29
|
} from './types.js';
|
|
30
|
+
import { fusionWorkflowProfile, type FusionWorkflowProfile } from './workflows.js';
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Run ids are prefixed by workflow so an artifact directory is self-describing.
|
|
34
|
+
* The prefix set is closed: an unknown prefix must fail rather than be accepted.
|
|
35
|
+
*/
|
|
36
|
+
const RUN_ID_PATTERN = /^(reason|investigate|research|validate)-[0-9a-f]{32}$/;
|
|
30
37
|
|
|
31
38
|
interface MutableFusionArtifactManifest {
|
|
32
39
|
schema_version: typeof FUSION_MANIFEST_SCHEMA_VERSION;
|
|
33
40
|
run_id: string;
|
|
41
|
+
workflow: FusionWorkflowId;
|
|
34
42
|
source: FusionSource;
|
|
35
43
|
state: FusionState;
|
|
36
44
|
created_at: string;
|
|
@@ -43,6 +51,13 @@ interface MutableFusionArtifactManifest {
|
|
|
43
51
|
merger: string;
|
|
44
52
|
thinking_level: string;
|
|
45
53
|
};
|
|
54
|
+
capabilities: {
|
|
55
|
+
candidate: FusionCapability;
|
|
56
|
+
evaluation: FusionCapability;
|
|
57
|
+
merge: FusionCapability;
|
|
58
|
+
};
|
|
59
|
+
context: { kind: import('./types.js').FusionContextKind; policy_id: string; ledger_artifact?: string; source_policy_artifact?: string };
|
|
60
|
+
tool_policy: { candidate_tools: readonly string[]; evaluation_tools: readonly []; merge_tools: readonly [] };
|
|
46
61
|
usage: FusionUsage;
|
|
47
62
|
attempts: FusionAttemptArtifactRecord[];
|
|
48
63
|
artifacts: Record<string, FusionArtifactRef>;
|
|
@@ -54,9 +69,15 @@ export interface CreateFusionArtifactStoreOptions {
|
|
|
54
69
|
cwd: string;
|
|
55
70
|
sessionId?: string | undefined;
|
|
56
71
|
runId?: string | undefined;
|
|
72
|
+
profile?: FusionWorkflowProfile | undefined;
|
|
57
73
|
source: FusionSource;
|
|
58
74
|
config: FusionModelConfigV1;
|
|
59
75
|
models: ResolvedFusionModels;
|
|
76
|
+
capabilities?: {
|
|
77
|
+
candidate: FusionCapability;
|
|
78
|
+
evaluation: FusionCapability;
|
|
79
|
+
merge: FusionCapability;
|
|
80
|
+
};
|
|
60
81
|
now?: () => Date;
|
|
61
82
|
}
|
|
62
83
|
|
|
@@ -83,8 +104,8 @@ export interface RecordFusionFailedAttemptInput {
|
|
|
83
104
|
usage?: FusionUsage;
|
|
84
105
|
}
|
|
85
106
|
|
|
86
|
-
function makeRunId(): string {
|
|
87
|
-
return
|
|
107
|
+
function makeRunId(profile: FusionWorkflowProfile): string {
|
|
108
|
+
return `${profile.runIdPrefix}${randomBytes(16).toString('hex')}`;
|
|
88
109
|
}
|
|
89
110
|
|
|
90
111
|
function modelsForManifest(models: ResolvedFusionModels): MutableFusionArtifactManifest['models'] {
|
|
@@ -149,6 +170,7 @@ function publicManifest(manifest: MutableFusionArtifactManifest): FusionArtifact
|
|
|
149
170
|
const out: FusionArtifactManifest = {
|
|
150
171
|
schema_version: manifest.schema_version,
|
|
151
172
|
run_id: manifest.run_id,
|
|
173
|
+
workflow: manifest.workflow,
|
|
152
174
|
source: manifest.source,
|
|
153
175
|
state: manifest.state,
|
|
154
176
|
created_at: manifest.created_at,
|
|
@@ -156,6 +178,9 @@ function publicManifest(manifest: MutableFusionArtifactManifest): FusionArtifact
|
|
|
156
178
|
cwd: manifest.cwd,
|
|
157
179
|
config: manifest.config,
|
|
158
180
|
models: manifest.models,
|
|
181
|
+
capabilities: manifest.capabilities,
|
|
182
|
+
context: { ...manifest.context },
|
|
183
|
+
tool_policy: { candidate_tools: [...manifest.tool_policy.candidate_tools], evaluation_tools: [], merge_tools: [] },
|
|
159
184
|
usage: cloneFusionUsage(manifest.usage),
|
|
160
185
|
attempts: [...manifest.attempts],
|
|
161
186
|
artifacts: { ...manifest.artifacts },
|
|
@@ -183,6 +208,14 @@ function calibrationViolationName(prefix: string): string {
|
|
|
183
208
|
return `${prefix}.calibration-violation.json`;
|
|
184
209
|
}
|
|
185
210
|
|
|
211
|
+
function artifactRefSha256Hex(value: string): string {
|
|
212
|
+
const hex = value.startsWith('sha256:') ? value.slice('sha256:'.length) : value;
|
|
213
|
+
if (!/^[0-9a-f]{64}$/u.test(hex)) {
|
|
214
|
+
throw errorForArtifact(`fusion artifact sha256 is not a lowercase hex digest: ${value}`);
|
|
215
|
+
}
|
|
216
|
+
return hex;
|
|
217
|
+
}
|
|
218
|
+
|
|
186
219
|
export class FusionArtifactStore {
|
|
187
220
|
private readonly runDirAbs: string;
|
|
188
221
|
private readonly runDirDisplay: string;
|
|
@@ -203,8 +236,14 @@ export class FusionArtifactStore {
|
|
|
203
236
|
}
|
|
204
237
|
|
|
205
238
|
static async create(options: CreateFusionArtifactStoreOptions): Promise<FusionArtifactStore> {
|
|
206
|
-
const
|
|
239
|
+
const profile = fusionWorkflowProfile(options.profile?.id ?? 'reason');
|
|
240
|
+
const runId = options.runId ?? makeRunId(profile);
|
|
207
241
|
if (!RUN_ID_PATTERN.test(runId)) throw errorForArtifact(`invalid fusion run id: ${runId}`);
|
|
242
|
+
if (!runId.startsWith(profile.runIdPrefix)) {
|
|
243
|
+
throw errorForArtifact(
|
|
244
|
+
`fusion run id ${runId} does not carry the ${profile.id} workflow prefix ${profile.runIdPrefix}`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
208
247
|
const sessionSegment = sanitizePathSegment(
|
|
209
248
|
options.sessionId ?? `session-${String(process.pid)}`,
|
|
210
249
|
);
|
|
@@ -217,6 +256,7 @@ export class FusionArtifactStore {
|
|
|
217
256
|
const manifest: MutableFusionArtifactManifest = {
|
|
218
257
|
schema_version: FUSION_MANIFEST_SCHEMA_VERSION,
|
|
219
258
|
run_id: runId,
|
|
259
|
+
workflow: profile.id,
|
|
220
260
|
source: options.source,
|
|
221
261
|
state: 'initializing',
|
|
222
262
|
created_at: timestamp,
|
|
@@ -224,6 +264,13 @@ export class FusionArtifactStore {
|
|
|
224
264
|
cwd: options.cwd,
|
|
225
265
|
config: options.config,
|
|
226
266
|
models: modelsForManifest(options.models),
|
|
267
|
+
capabilities: options.capabilities ?? {
|
|
268
|
+
candidate: 'reason',
|
|
269
|
+
evaluation: 'reason',
|
|
270
|
+
merge: 'reason',
|
|
271
|
+
},
|
|
272
|
+
context: { kind: profile.contextKind, policy_id: profile.contextKind === 'session_projection' ? 'fusion-session-projection-v1' : 'fusion-clean-task-v1' },
|
|
273
|
+
tool_policy: { candidate_tools: profile.candidateTools, evaluation_tools: [], merge_tools: [] },
|
|
227
274
|
usage: cloneFusionUsage(EMPTY_FUSION_USAGE),
|
|
228
275
|
attempts: [],
|
|
229
276
|
artifacts: {},
|
|
@@ -250,6 +297,10 @@ export class FusionArtifactStore {
|
|
|
250
297
|
return this.runDirAbs;
|
|
251
298
|
}
|
|
252
299
|
|
|
300
|
+
childToolCallLogPath(stage: FusionStage, slot: 1 | 2 | 3 | undefined, attempt: number): string {
|
|
301
|
+
return this.artifactPath(`${attemptPrefix(stage, slot, attempt)}.tool-calls.jsonl`);
|
|
302
|
+
}
|
|
303
|
+
|
|
253
304
|
snapshot(): FusionArtifactManifest {
|
|
254
305
|
return publicManifest(this.manifest);
|
|
255
306
|
}
|
|
@@ -294,7 +345,23 @@ export class FusionArtifactStore {
|
|
|
294
345
|
* while the full omission accounting stays locally auditable.
|
|
295
346
|
*/
|
|
296
347
|
async writeContextLedger(ledger: FusionContextOmissionLedgerV2): Promise<void> {
|
|
297
|
-
await this.writeArtifact('context-omission-ledger.json', canonicalJson(ledger));
|
|
348
|
+
const ref = await this.writeArtifact('context-omission-ledger.json', canonicalJson(ledger));
|
|
349
|
+
await this.updateManifest((manifest) => {
|
|
350
|
+
manifest.context.ledger_artifact = ref.path;
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async writeSourcePolicy(serialized: string): Promise<void> {
|
|
355
|
+
const ref = await this.writeArtifact('source-policy.private.json', serialized);
|
|
356
|
+
await this.updateManifest((manifest) => {
|
|
357
|
+
manifest.context.source_policy_artifact = ref.path;
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
sourcePolicyLaunchReference(): { path: string; sha256: string } {
|
|
362
|
+
const ref = this.manifest.artifacts['source-policy.private.json'];
|
|
363
|
+
if (ref === undefined) throw errorForArtifact('research source policy has not been written');
|
|
364
|
+
return { path: this.artifactPath(ref.path), sha256: artifactRefSha256Hex(ref.sha256) };
|
|
298
365
|
}
|
|
299
366
|
|
|
300
367
|
/** Route capacities and the pre-candidate whole-workflow feasibility decision. */
|
|
@@ -338,6 +405,10 @@ export class FusionArtifactStore {
|
|
|
338
405
|
responseName(prefix, input.responseKind),
|
|
339
406
|
input.result.text,
|
|
340
407
|
);
|
|
408
|
+
const toolCallsRef =
|
|
409
|
+
input.result.toolCallTrace === undefined
|
|
410
|
+
? undefined
|
|
411
|
+
: await this.writeArtifact(`${prefix}.tool-calls.jsonl`, input.result.toolCallTrace.bytes);
|
|
341
412
|
await this.updateManifest((manifest) => {
|
|
342
413
|
const record: FusionAttemptArtifactRecord = {
|
|
343
414
|
stage: input.result.stage,
|
|
@@ -352,6 +423,10 @@ export class FusionArtifactStore {
|
|
|
352
423
|
qualifiedId: input.result.qualifiedId,
|
|
353
424
|
usage: cloneFusionUsage(input.result.usage),
|
|
354
425
|
};
|
|
426
|
+
if (toolCallsRef !== undefined && input.result.toolCallTrace !== undefined) {
|
|
427
|
+
record.tool_calls_path = toolCallsRef.path;
|
|
428
|
+
record.tool_calls = { ...input.result.toolCallTrace.summary };
|
|
429
|
+
}
|
|
355
430
|
if (input.result.slot !== undefined) record.slot = input.result.slot;
|
|
356
431
|
manifest.attempts.push(record);
|
|
357
432
|
});
|
|
@@ -14,10 +14,6 @@ import {
|
|
|
14
14
|
isUsableContextWindow,
|
|
15
15
|
} from '../context/token-budget.js';
|
|
16
16
|
import {
|
|
17
|
-
FUSION_CANDIDATE_SYSTEM_PROMPT,
|
|
18
|
-
FUSION_EVALUATION_REPAIR_SYSTEM_PROMPT,
|
|
19
|
-
FUSION_EVALUATOR_SYSTEM_PROMPT,
|
|
20
|
-
FUSION_MERGER_SYSTEM_PROMPT,
|
|
21
17
|
buildBlindEvaluationInput,
|
|
22
18
|
buildCandidatePrompt,
|
|
23
19
|
buildEvaluationPrompt,
|
|
@@ -26,6 +22,7 @@ import {
|
|
|
26
22
|
buildMergePrompt,
|
|
27
23
|
type AnonymousFusionCandidate,
|
|
28
24
|
} from './prompts.js';
|
|
25
|
+
import { FUSION_REASON_WORKFLOW, type FusionWorkflowProfile } from './workflows.js';
|
|
29
26
|
import {
|
|
30
27
|
FUSION_BUDGET_PLAN_SCHEMA_VERSION,
|
|
31
28
|
FUSION_CALIBRATION_VIOLATION_SCHEMA_VERSION,
|
|
@@ -45,6 +42,7 @@ import {
|
|
|
45
42
|
type FusionBudgetWarning,
|
|
46
43
|
type FusionCalibrationViolation,
|
|
47
44
|
type FusionCanonicalInputV3,
|
|
45
|
+
type FusionCapability,
|
|
48
46
|
type FusionEvaluationV1,
|
|
49
47
|
type FusionRouteCapacity,
|
|
50
48
|
type FusionStage,
|
|
@@ -103,18 +101,61 @@ export const FUSION_BUDGET_POLICY: FusionBudgetPolicyDescriptor = {
|
|
|
103
101
|
utilization_warning_threshold_basis_points: FUSION_UTILIZATION_WARNING_THRESHOLD_BASIS_POINTS,
|
|
104
102
|
};
|
|
105
103
|
|
|
106
|
-
const
|
|
107
|
-
'Start a fresh Pi conversation, or run
|
|
108
|
-
"Raise the route's context window with a larger-context model via /fusion-models.",
|
|
109
|
-
'Restate only the required prior findings
|
|
104
|
+
const REASON_EMPTY_REMEDIATION: readonly string[] = Object.freeze([
|
|
105
|
+
'Start a fresh Pi conversation, or run fusion_reason earlier in the session.',
|
|
106
|
+
"Raise the route's context window with a larger-context subscription model via /fusion-models.",
|
|
107
|
+
'Restate only the required prior findings in the fusion_reason prompt.',
|
|
110
108
|
]);
|
|
111
109
|
|
|
112
|
-
const
|
|
113
|
-
'Provide a shorter
|
|
114
|
-
'Start a fresh Pi conversation, or run
|
|
115
|
-
"Raise the route's context window with a larger-context model via /fusion-models.",
|
|
110
|
+
const REASON_REQUEST_REMEDIATION: readonly string[] = Object.freeze([
|
|
111
|
+
'Provide a shorter fusion_reason prompt.',
|
|
112
|
+
'Start a fresh Pi conversation, or run fusion_reason earlier in the session.',
|
|
113
|
+
"Raise the route's context window with a larger-context subscription model via /fusion-models.",
|
|
116
114
|
]);
|
|
117
115
|
|
|
116
|
+
const INVESTIGATE_EMPTY_REMEDIATION: readonly string[] = Object.freeze([
|
|
117
|
+
'Split the repository investigation into smaller independently complete path or subsystem scopes.',
|
|
118
|
+
"Raise the route's context window with a larger-context subscription model via /fusion-models.",
|
|
119
|
+
]);
|
|
120
|
+
const INVESTIGATE_REQUEST_REMEDIATION: readonly string[] = Object.freeze([
|
|
121
|
+
'Narrow the fusion_investigate objective, repository scope, or required evidence.',
|
|
122
|
+
"Raise the route's context window with a larger-context subscription model via /fusion-models.",
|
|
123
|
+
]);
|
|
124
|
+
const RESEARCH_EMPTY_REMEDIATION: readonly string[] = Object.freeze([
|
|
125
|
+
'Split the research into smaller independently complete source sets.',
|
|
126
|
+
"Raise the route's context window with a larger-context subscription model via /fusion-models.",
|
|
127
|
+
]);
|
|
128
|
+
const RESEARCH_REQUEST_REMEDIATION: readonly string[] = Object.freeze([
|
|
129
|
+
'Narrow the fusion_research question or split large declared-source sets across independent runs.',
|
|
130
|
+
"Raise the route's context window with a larger-context subscription model via /fusion-models.",
|
|
131
|
+
]);
|
|
132
|
+
const VALIDATE_EMPTY_REMEDIATION: readonly string[] = Object.freeze([
|
|
133
|
+
'Split validation into smaller independently complete change or acceptance-criterion scopes.',
|
|
134
|
+
"Raise the route's context window with a larger-context subscription model via /fusion-models.",
|
|
135
|
+
]);
|
|
136
|
+
const VALIDATE_REQUEST_REMEDIATION: readonly string[] = Object.freeze([
|
|
137
|
+
'Narrow the fusion_validate scope, acceptance criteria, or supplied verification evidence.',
|
|
138
|
+
"Raise the route's context window with a larger-context subscription model via /fusion-models.",
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
function cleanRemediation(
|
|
142
|
+
profile: FusionWorkflowProfile,
|
|
143
|
+
requestDeterminesFeasibility: boolean,
|
|
144
|
+
): readonly string[] {
|
|
145
|
+
if (profile.id === 'investigate') {
|
|
146
|
+
return requestDeterminesFeasibility
|
|
147
|
+
? INVESTIGATE_REQUEST_REMEDIATION
|
|
148
|
+
: INVESTIGATE_EMPTY_REMEDIATION;
|
|
149
|
+
}
|
|
150
|
+
if (profile.id === 'research') {
|
|
151
|
+
return requestDeterminesFeasibility ? RESEARCH_REQUEST_REMEDIATION : RESEARCH_EMPTY_REMEDIATION;
|
|
152
|
+
}
|
|
153
|
+
if (profile.id === 'validate') {
|
|
154
|
+
return requestDeterminesFeasibility ? VALIDATE_REQUEST_REMEDIATION : VALIDATE_EMPTY_REMEDIATION;
|
|
155
|
+
}
|
|
156
|
+
return requestDeterminesFeasibility ? REASON_REQUEST_REMEDIATION : REASON_EMPTY_REMEDIATION;
|
|
157
|
+
}
|
|
158
|
+
|
|
118
159
|
const RESERVATION_REMEDIATION: readonly string[] = Object.freeze([
|
|
119
160
|
'Route the blocking stage to a model with larger byte capacity.',
|
|
120
161
|
'Keep producer output contracts intact; do not shrink or truncate child answers.',
|
|
@@ -456,16 +497,28 @@ function replaceRequestText(input: FusionCanonicalInputV3, text: string): Fusion
|
|
|
456
497
|
}
|
|
457
498
|
|
|
458
499
|
function visibleTextBytes(input: FusionCanonicalInputV3): number {
|
|
500
|
+
const projection = input.context?.kind === 'session_projection'
|
|
501
|
+
? input.context.conversation_projection
|
|
502
|
+
: 'conversation_projection' in input
|
|
503
|
+
? input.conversation_projection
|
|
504
|
+
: undefined;
|
|
505
|
+
if (projection === undefined) return 0;
|
|
459
506
|
let total = 0;
|
|
460
|
-
for (const entry of
|
|
507
|
+
for (const entry of projection.entries) {
|
|
461
508
|
if (entry[0] === 't') total += utf8Bytes(JSON.stringify(entry[4]));
|
|
462
509
|
}
|
|
463
510
|
return total;
|
|
464
511
|
}
|
|
465
512
|
|
|
466
513
|
function omissionReceiptBytes(input: FusionCanonicalInputV3): number {
|
|
514
|
+
const projection = input.context?.kind === 'session_projection'
|
|
515
|
+
? input.context.conversation_projection
|
|
516
|
+
: 'conversation_projection' in input
|
|
517
|
+
? input.conversation_projection
|
|
518
|
+
: undefined;
|
|
519
|
+
if (projection === undefined) return 0;
|
|
467
520
|
let total = 0;
|
|
468
|
-
for (const entry of
|
|
521
|
+
for (const entry of projection.entries) {
|
|
469
522
|
if (entry[0] === 'o') total += utf8Bytes(JSON.stringify(entry));
|
|
470
523
|
}
|
|
471
524
|
return total;
|
|
@@ -526,10 +579,11 @@ function dominantRemediation(
|
|
|
526
579
|
verdict: FusionBudgetEmptyRequestVerdict,
|
|
527
580
|
composition: FusionBudgetStageComposition | undefined,
|
|
528
581
|
dominantByteClass: string,
|
|
582
|
+
profile: FusionWorkflowProfile,
|
|
529
583
|
): readonly string[] {
|
|
530
584
|
if (dominantByteClass === 'dense_ascii') return DENSE_REMEDIATION;
|
|
531
585
|
if (dominantByteClass === 'multibyte') return MULTIBYTE_REMEDIATION;
|
|
532
|
-
if (composition === undefined) return remediationFor(verdict);
|
|
586
|
+
if (composition === undefined) return remediationFor(verdict, profile);
|
|
533
587
|
const entries = [
|
|
534
588
|
{ name: 'visible', bytes: composition.visible_text_bytes },
|
|
535
589
|
{ name: 'request', bytes: composition.request_bytes },
|
|
@@ -538,13 +592,24 @@ function dominantRemediation(
|
|
|
538
592
|
const dominant = entries[0];
|
|
539
593
|
if (dominant?.name === 'reservation') return RESERVATION_REMEDIATION;
|
|
540
594
|
if (dominant?.name === 'request' && !verdict.still_fails_with_empty_request) {
|
|
541
|
-
return
|
|
595
|
+
return profile.contextKind === 'clean_task'
|
|
596
|
+
? cleanRemediation(profile, true)
|
|
597
|
+
: REASON_REQUEST_REMEDIATION;
|
|
542
598
|
}
|
|
543
|
-
return
|
|
599
|
+
if (profile.contextKind === 'clean_task') return cleanRemediation(profile, false);
|
|
600
|
+
return REASON_EMPTY_REMEDIATION;
|
|
544
601
|
}
|
|
545
602
|
|
|
546
|
-
function remediationFor(
|
|
547
|
-
|
|
603
|
+
function remediationFor(
|
|
604
|
+
verdict: FusionBudgetEmptyRequestVerdict,
|
|
605
|
+
profile: FusionWorkflowProfile,
|
|
606
|
+
): readonly string[] {
|
|
607
|
+
if (profile.contextKind === 'clean_task') {
|
|
608
|
+
return cleanRemediation(profile, !verdict.still_fails_with_empty_request);
|
|
609
|
+
}
|
|
610
|
+
return verdict.still_fails_with_empty_request
|
|
611
|
+
? REASON_EMPTY_REMEDIATION
|
|
612
|
+
: REASON_REQUEST_REMEDIATION;
|
|
548
613
|
}
|
|
549
614
|
|
|
550
615
|
function formatEmptyRequestVerdict(verdict: FusionBudgetEmptyRequestVerdict): string {
|
|
@@ -677,11 +742,20 @@ export class FusionBudget {
|
|
|
677
742
|
readonly routes: readonly FusionRouteCapacity[];
|
|
678
743
|
readonly limiting: FusionRouteCapacity;
|
|
679
744
|
private readonly contextPolicyId: string;
|
|
680
|
-
|
|
681
|
-
|
|
745
|
+
private readonly candidateCapability: FusionCapability;
|
|
746
|
+
private readonly profile: FusionWorkflowProfile;
|
|
747
|
+
|
|
748
|
+
constructor(
|
|
749
|
+
models: ResolvedFusionModels,
|
|
750
|
+
contextPolicyId: string,
|
|
751
|
+
candidateCapability: FusionCapability = FUSION_REASON_WORKFLOW.candidateCapability,
|
|
752
|
+
profile: FusionWorkflowProfile = FUSION_REASON_WORKFLOW,
|
|
753
|
+
) {
|
|
682
754
|
this.routes = fusionRouteCapacities(models);
|
|
683
755
|
this.limiting = fusionLimitingRoute(this.routes);
|
|
684
756
|
this.contextPolicyId = contextPolicyId;
|
|
757
|
+
this.candidateCapability = candidateCapability;
|
|
758
|
+
this.profile = profile;
|
|
685
759
|
}
|
|
686
760
|
|
|
687
761
|
get allowedInputTokens(): number {
|
|
@@ -708,6 +782,7 @@ export class FusionBudget {
|
|
|
708
782
|
}
|
|
709
783
|
|
|
710
784
|
private drafts(input: FusionCanonicalInputV3): readonly StageForecastDraft[] {
|
|
785
|
+
const candidateSystemPrompt = this.profile.candidateSystemPrompt(this.candidateCapability);
|
|
711
786
|
const candidatePrompt = buildCandidatePrompt(input);
|
|
712
787
|
const blindInput = buildBlindEvaluationInput(input, EMPTY_CANDIDATES);
|
|
713
788
|
const evaluationPrompt = buildEvaluationPrompt(blindInput);
|
|
@@ -724,7 +799,7 @@ export class FusionBudget {
|
|
|
724
799
|
slot: 1,
|
|
725
800
|
route: this.routeForStage('candidate', 1),
|
|
726
801
|
conditional: false,
|
|
727
|
-
system_prompt:
|
|
802
|
+
system_prompt: candidateSystemPrompt,
|
|
728
803
|
empty_user_prompt: candidatePrompt,
|
|
729
804
|
upstream_output_contract_bytes: 0,
|
|
730
805
|
},
|
|
@@ -733,7 +808,7 @@ export class FusionBudget {
|
|
|
733
808
|
slot: 2,
|
|
734
809
|
route: this.routeForStage('candidate', 2),
|
|
735
810
|
conditional: false,
|
|
736
|
-
system_prompt:
|
|
811
|
+
system_prompt: candidateSystemPrompt,
|
|
737
812
|
empty_user_prompt: candidatePrompt,
|
|
738
813
|
upstream_output_contract_bytes: 0,
|
|
739
814
|
},
|
|
@@ -742,7 +817,7 @@ export class FusionBudget {
|
|
|
742
817
|
slot: 3,
|
|
743
818
|
route: this.routeForStage('candidate', 3),
|
|
744
819
|
conditional: false,
|
|
745
|
-
system_prompt:
|
|
820
|
+
system_prompt: candidateSystemPrompt,
|
|
746
821
|
empty_user_prompt: candidatePrompt,
|
|
747
822
|
upstream_output_contract_bytes: 0,
|
|
748
823
|
},
|
|
@@ -750,7 +825,7 @@ export class FusionBudget {
|
|
|
750
825
|
budget_stage: 'evaluation',
|
|
751
826
|
route: this.routeForStage('evaluation'),
|
|
752
827
|
conditional: false,
|
|
753
|
-
system_prompt:
|
|
828
|
+
system_prompt: this.profile.evaluatorSystemPrompt,
|
|
754
829
|
empty_user_prompt: evaluationPrompt,
|
|
755
830
|
upstream_output_contract_bytes: 3 * FUSION_CANDIDATE_MAX_OUTPUT_BYTES,
|
|
756
831
|
},
|
|
@@ -758,7 +833,7 @@ export class FusionBudget {
|
|
|
758
833
|
budget_stage: 'merge',
|
|
759
834
|
route: this.routeForStage('merge'),
|
|
760
835
|
conditional: false,
|
|
761
|
-
system_prompt:
|
|
836
|
+
system_prompt: this.profile.mergerSystemPrompt,
|
|
762
837
|
empty_user_prompt: mergePrompt,
|
|
763
838
|
upstream_output_contract_bytes:
|
|
764
839
|
3 * FUSION_CANDIDATE_MAX_OUTPUT_BYTES + FUSION_EVALUATION_MAX_OUTPUT_BYTES,
|
|
@@ -767,7 +842,7 @@ export class FusionBudget {
|
|
|
767
842
|
budget_stage: 'evaluation_repair',
|
|
768
843
|
route: this.routeForStage('evaluation_repair'),
|
|
769
844
|
conditional: true,
|
|
770
|
-
system_prompt:
|
|
845
|
+
system_prompt: this.profile.evaluationRepairSystemPrompt,
|
|
771
846
|
empty_user_prompt: repairPrompt,
|
|
772
847
|
upstream_output_contract_bytes:
|
|
773
848
|
3 * FUSION_CANDIDATE_MAX_OUTPUT_BYTES +
|
|
@@ -842,7 +917,12 @@ export class FusionBudget {
|
|
|
842
917
|
): FusionError {
|
|
843
918
|
const composition = plan.primary_blocker_composition;
|
|
844
919
|
const dominantByteClass = primary.input_only_estimate.rateSource.dominant_byte_class;
|
|
845
|
-
const remediation = dominantRemediation(
|
|
920
|
+
const remediation = dominantRemediation(
|
|
921
|
+
plan.empty_request,
|
|
922
|
+
composition,
|
|
923
|
+
dominantByteClass,
|
|
924
|
+
this.profile,
|
|
925
|
+
);
|
|
846
926
|
const tokensOver = primary.input_only_input_tokens_upper_bound - primary.allowed_input_tokens;
|
|
847
927
|
const budget: FusionBudgetErrorDetail = {
|
|
848
928
|
budget_stage: primary.budget_stage,
|
|
@@ -916,6 +996,25 @@ export class FusionBudget {
|
|
|
916
996
|
return new FusionError(message, details);
|
|
917
997
|
}
|
|
918
998
|
|
|
999
|
+
private planMetadata(input?: FusionCanonicalInputV3): Pick<FusionBudgetPlanV1, 'workflow' | 'context' | 'fixed_candidate_policy' | 'tool_policy'> {
|
|
1000
|
+
return {
|
|
1001
|
+
workflow: this.profile.id,
|
|
1002
|
+
context: {
|
|
1003
|
+
kind: this.profile.contextKind,
|
|
1004
|
+
policy_id: input?.context?.policy_id ?? this.contextPolicyId,
|
|
1005
|
+
},
|
|
1006
|
+
fixed_candidate_policy: {
|
|
1007
|
+
capability: this.candidateCapability,
|
|
1008
|
+
tools: this.profile.candidateTools,
|
|
1009
|
+
},
|
|
1010
|
+
tool_policy: {
|
|
1011
|
+
candidate_tools: this.profile.candidateTools,
|
|
1012
|
+
evaluation_tools: [] as readonly [],
|
|
1013
|
+
merge_tools: [] as readonly [],
|
|
1014
|
+
},
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
|
|
919
1018
|
plan(input: FusionCanonicalInputV3): FusionBudgetPlanV1 {
|
|
920
1019
|
const stages = this.entries(input);
|
|
921
1020
|
const blockers = selectBlockers(stages);
|
|
@@ -923,6 +1022,7 @@ export class FusionBudget {
|
|
|
923
1022
|
const emptyRequest = this.emptyRequestVerdict(input, stages);
|
|
924
1023
|
const base: FusionBudgetPlanV1 = {
|
|
925
1024
|
schema_version: FUSION_BUDGET_PLAN_SCHEMA_VERSION,
|
|
1025
|
+
...this.planMetadata(input),
|
|
926
1026
|
policy: FUSION_BUDGET_POLICY,
|
|
927
1027
|
routes: this.routes,
|
|
928
1028
|
stages,
|
|
@@ -985,6 +1085,7 @@ export class FusionBudget {
|
|
|
985
1085
|
const blocker = blockerFromEntry(entry);
|
|
986
1086
|
const plan: FusionBudgetPlanV1 = {
|
|
987
1087
|
schema_version: FUSION_BUDGET_PLAN_SCHEMA_VERSION,
|
|
1088
|
+
...this.planMetadata(),
|
|
988
1089
|
policy: FUSION_BUDGET_POLICY,
|
|
989
1090
|
routes: this.routes,
|
|
990
1091
|
stages: [entry],
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import type { Usage } from '@earendil-works/pi-ai';
|
|
3
|
+
|
|
4
|
+
export const FUSION_CHILD_RESULT_SCHEMA_VERSION =
|
|
5
|
+
'pi-background-tasks.fusion-child-result.v2' as const;
|
|
6
|
+
export const FUSION_CHILD_RESULT_PREFIX = '\u001ePI_FUSION_CHILD_RESULT ';
|
|
7
|
+
export const FUSION_TOOL_CALL_LOG_PATH_ENV = 'PI_FUSION_TOOL_CALL_LOG_PATH';
|
|
8
|
+
export const FUSION_RESEARCH_ENABLED_ENV = 'PI_FUSION_RESEARCH_ENABLED';
|
|
9
|
+
export const FUSION_SOURCE_POLICY_PATH_ENV = 'PI_FUSION_SOURCE_POLICY_PATH';
|
|
10
|
+
export const FUSION_SOURCE_POLICY_SHA256_ENV = 'PI_FUSION_SOURCE_POLICY_SHA256';
|
|
11
|
+
export const FUSION_TOOL_CALL_SEAL_SCHEMA_VERSION =
|
|
12
|
+
'pi-background-tasks.fusion-tool-call-seal.v1' as const;
|
|
13
|
+
export const FUSION_TOOL_CALL_SEAL_SUFFIX = '.seal.json';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Aggregate ceiling on tool-result bytes a single candidate child may accumulate.
|
|
17
|
+
*
|
|
18
|
+
* v1 deliberately has no tool-call-count cap, so this byte budget is the only bound on
|
|
19
|
+
* how much a read-only candidate can pull into its context. 8 MiB is generous for
|
|
20
|
+
* targeted grep/read investigation while still preventing an unbounded read loop from
|
|
21
|
+
* degrading into an opaque provider-side context failure.
|
|
22
|
+
*/
|
|
23
|
+
export const FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES = 8 * 1024 * 1024;
|
|
24
|
+
|
|
25
|
+
export interface FusionChildTextBlockMetadata {
|
|
26
|
+
utf8_bytes: number;
|
|
27
|
+
sha256: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type FusionChildResultUsageMetadata = Usage;
|
|
31
|
+
|
|
32
|
+
export interface FusionChildResultMetadata {
|
|
33
|
+
schema_version: typeof FUSION_CHILD_RESULT_SCHEMA_VERSION;
|
|
34
|
+
provider: string;
|
|
35
|
+
model: string;
|
|
36
|
+
stop_reason: string;
|
|
37
|
+
text_blocks: FusionChildTextBlockMetadata[];
|
|
38
|
+
text_sha256: string;
|
|
39
|
+
usage: FusionChildResultUsageMetadata;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function protocolSha256(value: string | Buffer): string {
|
|
43
|
+
return createHash('sha256').update(value).digest('hex');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function buildFusionChildResultMetadata(message: {
|
|
47
|
+
provider: string;
|
|
48
|
+
model: string;
|
|
49
|
+
stopReason: string;
|
|
50
|
+
content: ReadonlyArray<{ type: string; text?: string }>;
|
|
51
|
+
usage: Usage;
|
|
52
|
+
}): FusionChildResultMetadata {
|
|
53
|
+
const textBlocks = message.content.flatMap((part) =>
|
|
54
|
+
part.type === 'text' && typeof part.text === 'string' ? [part.text] : [],
|
|
55
|
+
);
|
|
56
|
+
const usage: FusionChildResultUsageMetadata = {
|
|
57
|
+
input: message.usage.input,
|
|
58
|
+
output: message.usage.output,
|
|
59
|
+
cacheRead: message.usage.cacheRead,
|
|
60
|
+
cacheWrite: message.usage.cacheWrite,
|
|
61
|
+
totalTokens: message.usage.totalTokens,
|
|
62
|
+
cost: {
|
|
63
|
+
input: message.usage.cost.input,
|
|
64
|
+
output: message.usage.cost.output,
|
|
65
|
+
cacheRead: message.usage.cost.cacheRead,
|
|
66
|
+
cacheWrite: message.usage.cost.cacheWrite,
|
|
67
|
+
total: message.usage.cost.total,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
schema_version: FUSION_CHILD_RESULT_SCHEMA_VERSION,
|
|
72
|
+
provider: message.provider,
|
|
73
|
+
model: message.model,
|
|
74
|
+
stop_reason: message.stopReason,
|
|
75
|
+
text_blocks: textBlocks.map((text) => ({
|
|
76
|
+
utf8_bytes: Buffer.byteLength(text, 'utf8'),
|
|
77
|
+
sha256: protocolSha256(text),
|
|
78
|
+
})),
|
|
79
|
+
text_sha256: protocolSha256(textBlocks.join('')),
|
|
80
|
+
usage,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalJson } from '../attested-pi-run.js';
|
|
3
|
+
import { normalizeFusionDeclaredSources, type DeclaredFusionSourceInput } from './source-policy.js';
|
|
4
|
+
import {
|
|
5
|
+
FUSION_INPUT_SCHEMA_VERSION,
|
|
6
|
+
FusionError,
|
|
7
|
+
type FusionCanonicalRequestV3,
|
|
8
|
+
type FusionCleanTaskCanonicalInputV5,
|
|
9
|
+
type FusionDeclaredSourceV1,
|
|
10
|
+
type FusionSource,
|
|
11
|
+
type FusionWorkflowId,
|
|
12
|
+
} from './types.js';
|
|
13
|
+
|
|
14
|
+
export interface BuildFusionCleanTaskInputOptions {
|
|
15
|
+
cwd: string;
|
|
16
|
+
source: FusionSource;
|
|
17
|
+
request: string;
|
|
18
|
+
workflow: Exclude<FusionWorkflowId, 'reason'>;
|
|
19
|
+
declaredSources?: readonly DeclaredFusionSourceInput[] | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Public v1 clean builder. It is deliberately pure: callers provide cwd and
|
|
24
|
+
* normalized request text explicitly, and this module has no dependency on Pi
|
|
25
|
+
* session, snapshot, parent-context, or visible-conversation APIs.
|
|
26
|
+
*/
|
|
27
|
+
export const buildCleanFusionCanonicalInput = buildFusionCleanTaskCanonicalInput;
|
|
28
|
+
|
|
29
|
+
export interface BuiltFusionCleanTaskCanonicalInput {
|
|
30
|
+
input: FusionCleanTaskCanonicalInputV5;
|
|
31
|
+
serialized: string;
|
|
32
|
+
declaredSources: readonly FusionDeclaredSourceV1[];
|
|
33
|
+
transcriptLeafId: null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sha256Text(value: string): string {
|
|
37
|
+
return createHash('sha256').update(Buffer.from(value, 'utf8')).digest('hex');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function buildFusionCleanTaskCanonicalInput(
|
|
41
|
+
options: BuildFusionCleanTaskInputOptions,
|
|
42
|
+
): BuiltFusionCleanTaskCanonicalInput {
|
|
43
|
+
if (options.request.trim().length === 0) {
|
|
44
|
+
throw new FusionError('fusion request must not be blank', {
|
|
45
|
+
code: 'context_capture_failed',
|
|
46
|
+
childCreated: false,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (!['investigate', 'research', 'validate'].includes(options.workflow)) {
|
|
50
|
+
throw new FusionError('clean-task fusion input is available only to investigate, research, and validate workflows', {
|
|
51
|
+
code: 'context_capture_failed',
|
|
52
|
+
childCreated: false,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const declaredSources = normalizeFusionDeclaredSources(options.declaredSources ?? []);
|
|
56
|
+
if (options.workflow === 'research' && declaredSources.length === 0) {
|
|
57
|
+
throw new FusionError('fusion research requires at least one declared source URL and purpose', {
|
|
58
|
+
code: 'context_capture_failed',
|
|
59
|
+
childCreated: false,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (options.workflow !== 'research' && declaredSources.length > 0) {
|
|
63
|
+
throw new FusionError('declared sources are accepted only by the research workflow', {
|
|
64
|
+
code: 'context_capture_failed',
|
|
65
|
+
childCreated: false,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const request: FusionCanonicalRequestV3 = {
|
|
69
|
+
source: options.source,
|
|
70
|
+
authority: 'explicit_text',
|
|
71
|
+
text: options.request,
|
|
72
|
+
sha256: sha256Text(options.request),
|
|
73
|
+
};
|
|
74
|
+
const input: FusionCleanTaskCanonicalInputV5 = {
|
|
75
|
+
schema_version: FUSION_INPUT_SCHEMA_VERSION,
|
|
76
|
+
workflow: options.workflow,
|
|
77
|
+
cwd: options.cwd,
|
|
78
|
+
request,
|
|
79
|
+
context: {
|
|
80
|
+
kind: 'clean_task',
|
|
81
|
+
policy_id: 'fusion-clean-task-v1',
|
|
82
|
+
declared_sources: declaredSources,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
input,
|
|
87
|
+
serialized: canonicalJson(input),
|
|
88
|
+
declaredSources,
|
|
89
|
+
transcriptLeafId: null,
|
|
90
|
+
};
|
|
91
|
+
}
|