pi-background-tasks 1.0.7 → 2.1.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 +8 -8
- package/TESTING.md +3 -3
- package/TEST_PLAN.md +6 -6
- package/docs/INDEX.md +25 -25
- package/docs/choose-a-workflow.md +4 -4
- package/docs/commands/bg-clear.md +1 -1
- package/docs/commands/bg-update.md +1 -1
- package/docs/commands/bg.md +1 -1
- package/docs/commands/fusion-models.md +1 -1
- package/docs/commands/fusion.md +5 -8
- package/docs/commands/jobs.md +1 -1
- package/docs/commands/kill.md +1 -1
- package/docs/commands/logs.md +1 -1
- package/docs/commands/task-manager.md +2 -2
- package/docs/concepts/completion-delivery.md +1 -0
- package/docs/getting-started.md +1 -1
- package/docs/manifest.json +69 -50
- package/docs/operations/configuration.md +5 -3
- package/docs/read-before-edit.md +3 -0
- package/docs/reference/runtime-contracts.md +87 -82
- package/docs/reference/shortcuts-and-dock.md +2 -2
- package/docs/subsystems/background-task-runtime.md +7 -1
- package/docs/subsystems/docs-freshness-gate.md +5 -5
- package/docs/subsystems/fusion.md +22 -15
- package/docs/subsystems/host-ui-and-telemetry.md +1 -1
- package/docs/tools/bg_delegate.md +1 -1
- package/docs/tools/bg_kill.md +1 -1
- package/docs/tools/bg_logs.md +1 -1
- package/docs/tools/bg_result.md +14 -10
- package/docs/tools/bg_run.md +1 -1
- package/docs/tools/bg_run_pi_attested.md +1 -1
- package/docs/tools/bg_status.md +1 -1
- package/docs/tools/fusion_investigate.md +6 -4
- package/docs/tools/fusion_reason.md +5 -5
- package/docs/tools/fusion_research.md +6 -2
- package/docs/tools/fusion_validate.md +5 -3
- package/package.json +1 -1
- package/src/core/common.ts +50 -2
- package/src/core/fusion/anthropic-attribution.ts +1930 -0
- package/src/core/fusion/artifacts.ts +168 -21
- package/src/core/fusion/budget.ts +23 -23
- package/src/core/fusion/child-protocol.ts +115 -10
- package/src/core/fusion/claude-cache.ts +21 -0
- package/src/core/fusion/config.ts +10 -2
- package/src/core/fusion/orchestrator.ts +281 -77
- package/src/core/fusion/output-contract.ts +34 -0
- package/src/core/fusion/pi-child.ts +420 -12
- package/src/core/fusion/prompts.ts +11 -1
- package/src/core/fusion/result-package.ts +412 -0
- package/src/core/fusion/types.ts +67 -0
- package/src/core/registry.ts +187 -20
- package/src/delegate-extension.ts +130 -24
- package/src/extension.ts +17 -6
- package/src/fusion-child-extension.ts +117 -2
- package/src/fusion-extension.ts +308 -154
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
readFileSync,
|
|
9
9
|
writeSync,
|
|
10
10
|
} from 'node:fs';
|
|
11
|
-
import { dirname } from 'node:path';
|
|
11
|
+
import { dirname, isAbsolute } from 'node:path';
|
|
12
12
|
import { parseJsonText } from './core/common.js';
|
|
13
13
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
14
14
|
import { Type, type Static } from 'typebox';
|
|
@@ -32,11 +32,13 @@ import {
|
|
|
32
32
|
parseFusionSourcePolicy,
|
|
33
33
|
} from './core/fusion/source-policy.js';
|
|
34
34
|
import {
|
|
35
|
+
applyFusionClaudePromptCachingScopeHeader,
|
|
35
36
|
nonAnthropicFusionCacheObservation,
|
|
36
37
|
normalizeFusionClaudeCachePayload,
|
|
37
38
|
type FusionClaudeCacheObservation,
|
|
38
39
|
} from './core/fusion/claude-cache.js';
|
|
39
40
|
import {
|
|
41
|
+
FUSION_CANDIDATE_OUTPUT_RECOVERY_PATH_ENV,
|
|
40
42
|
FUSION_CHILD_MAX_PROVIDER_REQUESTS,
|
|
41
43
|
FUSION_CHILD_MAX_TOOL_CALLS,
|
|
42
44
|
FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES,
|
|
@@ -59,12 +61,19 @@ import {
|
|
|
59
61
|
type FusionRuntimeGuardCode,
|
|
60
62
|
type FusionRuntimeGuardRecord,
|
|
61
63
|
} from './core/fusion/child-protocol.js';
|
|
64
|
+
import {
|
|
65
|
+
FUSION_CANDIDATE_MAX_OUTPUT_BYTES,
|
|
66
|
+
FUSION_CANDIDATE_OUTPUT_COMPRESSION_PROMPT,
|
|
67
|
+
fusionJsonRenderedTextBytes,
|
|
68
|
+
} from './core/fusion/output-contract.js';
|
|
62
69
|
|
|
63
70
|
export {
|
|
64
71
|
FUSION_CLAUDE_CACHE_BREAKPOINT_LIMIT,
|
|
65
72
|
FUSION_CLAUDE_CACHE_DEFAULT_RETENTION,
|
|
66
73
|
FUSION_CLAUDE_CACHE_OBSERVATION_SCHEMA_VERSION,
|
|
67
74
|
FUSION_CLAUDE_CACHE_RETENTION_ENV,
|
|
75
|
+
FUSION_CLAUDE_PROMPT_CACHING_SCOPE_BETA,
|
|
76
|
+
applyFusionClaudePromptCachingScopeHeader,
|
|
68
77
|
nonAnthropicFusionCacheObservation,
|
|
69
78
|
normalizeFusionClaudeCachePayload,
|
|
70
79
|
resolveFusionClaudeCachePolicy,
|
|
@@ -75,6 +84,7 @@ export {
|
|
|
75
84
|
} from './core/fusion/claude-cache.js';
|
|
76
85
|
|
|
77
86
|
export {
|
|
87
|
+
FUSION_CANDIDATE_OUTPUT_RECOVERY_PATH_ENV,
|
|
78
88
|
FUSION_CHILD_MAX_PROVIDER_REQUESTS,
|
|
79
89
|
FUSION_CHILD_MAX_TOOL_CALLS,
|
|
80
90
|
FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES,
|
|
@@ -103,6 +113,12 @@ export {
|
|
|
103
113
|
type FusionRuntimeGuardRecord,
|
|
104
114
|
} from './core/fusion/child-protocol.js';
|
|
105
115
|
|
|
116
|
+
export {
|
|
117
|
+
FUSION_CANDIDATE_MAX_OUTPUT_BYTES,
|
|
118
|
+
FUSION_CANDIDATE_OUTPUT_COMPRESSION_PROMPT,
|
|
119
|
+
fusionJsonRenderedTextBytes,
|
|
120
|
+
} from './core/fusion/output-contract.js';
|
|
121
|
+
|
|
106
122
|
const FUSION_CHILD_O_NOFOLLOW = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
|
|
107
123
|
|
|
108
124
|
const FusionWebFetchParams = Type.Object(
|
|
@@ -253,6 +269,24 @@ function createToolCallLog(path: string): void {
|
|
|
253
269
|
fsyncParentDirectorySync(path);
|
|
254
270
|
}
|
|
255
271
|
|
|
272
|
+
function createCandidateOutputRecoveryArtifact(path: string, text: string): void {
|
|
273
|
+
if (!isAbsolute(path)) {
|
|
274
|
+
throw new Error(`${FUSION_CANDIDATE_OUTPUT_RECOVERY_PATH_ENV} must be an absolute path`);
|
|
275
|
+
}
|
|
276
|
+
const bytes = Buffer.from(text, 'utf8');
|
|
277
|
+
withRegularFileDescriptorSync(
|
|
278
|
+
path,
|
|
279
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | FUSION_CHILD_O_NOFOLLOW,
|
|
280
|
+
0o600,
|
|
281
|
+
'fusion oversized candidate response',
|
|
282
|
+
(fd) => {
|
|
283
|
+
writeAllSync(fd, bytes, 'fusion oversized candidate response');
|
|
284
|
+
fsyncSync(fd);
|
|
285
|
+
},
|
|
286
|
+
);
|
|
287
|
+
fsyncParentDirectorySync(path);
|
|
288
|
+
}
|
|
289
|
+
|
|
256
290
|
function appendToolCallLogLine(path: string, record: FusionToolCallLogRecord): void {
|
|
257
291
|
// The log is an audit trail, not a payload copy: raw tool arguments/results may
|
|
258
292
|
// contain secrets, so only byte counts and SHA-256 digests are persisted.
|
|
@@ -670,6 +704,15 @@ function fusionWebFetchResultText(result: Awaited<ReturnType<typeof fusionWebFet
|
|
|
670
704
|
*/
|
|
671
705
|
export default function fusionChildExtension(pi: ExtensionAPI): void {
|
|
672
706
|
const toolCallLogPath = process.env[FUSION_TOOL_CALL_LOG_PATH_ENV];
|
|
707
|
+
const candidateOutputRecoveryPath = process.env[FUSION_CANDIDATE_OUTPUT_RECOVERY_PATH_ENV];
|
|
708
|
+
if (
|
|
709
|
+
candidateOutputRecoveryPath !== undefined &&
|
|
710
|
+
(candidateOutputRecoveryPath.trim().length === 0 || !isAbsolute(candidateOutputRecoveryPath))
|
|
711
|
+
) {
|
|
712
|
+
throw new Error(
|
|
713
|
+
`${FUSION_CANDIDATE_OUTPUT_RECOVERY_PATH_ENV} must be an absolute non-blank path`,
|
|
714
|
+
);
|
|
715
|
+
}
|
|
673
716
|
const researchEnabled = process.env[FUSION_RESEARCH_ENABLED_ENV];
|
|
674
717
|
if (researchEnabled !== undefined && researchEnabled !== '1') {
|
|
675
718
|
throw new Error(`${FUSION_RESEARCH_ENABLED_ENV} must be unset or exactly 1`);
|
|
@@ -684,10 +727,17 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
|
|
|
684
727
|
let providerRequestCount = 0;
|
|
685
728
|
let toolCallCount = 0;
|
|
686
729
|
let runtimeGuardFailed = false;
|
|
730
|
+
let outputRecoveryFailed = false;
|
|
731
|
+
let outputRecoveryPhase: 'eligible' | 'queued' | 'finished' = 'eligible';
|
|
687
732
|
let settlementPublished = false;
|
|
688
733
|
const childResultRecords: FusionChildResultMetadata[] = [];
|
|
689
734
|
let pendingCacheObservation: FusionClaudeCacheObservation | undefined;
|
|
690
735
|
|
|
736
|
+
pi.on('before_provider_headers', (event, ctx) => {
|
|
737
|
+
if (ctx.model?.provider !== 'anthropic') return;
|
|
738
|
+
applyFusionClaudePromptCachingScopeHeader(event.headers);
|
|
739
|
+
});
|
|
740
|
+
|
|
691
741
|
pi.on('before_provider_request', async (event, ctx) => {
|
|
692
742
|
providerRequestCount += 1;
|
|
693
743
|
if (runtimeGuardFailed) {
|
|
@@ -820,6 +870,15 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
|
|
|
820
870
|
pi.on('tool_call', async (event, ctx) => {
|
|
821
871
|
try {
|
|
822
872
|
requireOpen('tool_call');
|
|
873
|
+
if (outputRecoveryPhase === 'queued') {
|
|
874
|
+
outputRecoveryFailed = true;
|
|
875
|
+
latchAuditProcessFailure();
|
|
876
|
+
ctx.abort();
|
|
877
|
+
return {
|
|
878
|
+
block: true,
|
|
879
|
+
reason: 'fusion candidate output compression is a no-tool continuation',
|
|
880
|
+
};
|
|
881
|
+
}
|
|
823
882
|
if (runtimeGuardFailed) {
|
|
824
883
|
ctx.abort();
|
|
825
884
|
return { block: true, reason: 'fusion child runtime guard already refused the run' };
|
|
@@ -978,13 +1037,68 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
|
|
|
978
1037
|
if (event.message.role !== 'assistant') return;
|
|
979
1038
|
const cacheObservation = pendingCacheObservation;
|
|
980
1039
|
if (cacheObservation === undefined) {
|
|
1040
|
+
// Authentication and other pre-transport failures can produce an assistant
|
|
1041
|
+
// error without ever reaching before_provider_request. Preserve Pi's original
|
|
1042
|
+
// provider diagnostic and let settlement report no_records; emitting a second
|
|
1043
|
+
// cache-policy error would mask the actionable failure. A successful result
|
|
1044
|
+
// without an observation remains a hard invariant violation.
|
|
1045
|
+
if (event.message.stopReason === 'error') return;
|
|
981
1046
|
latchAuditProcessFailure();
|
|
982
1047
|
throw new Error('fusion child assistant result has no matching cache-policy observation');
|
|
983
1048
|
}
|
|
984
1049
|
pendingCacheObservation = undefined;
|
|
985
|
-
const
|
|
1050
|
+
const text = event.message.content
|
|
1051
|
+
.flatMap((part) => (part.type === 'text' ? [part.text] : []))
|
|
1052
|
+
.join('');
|
|
1053
|
+
const renderedBytes = fusionJsonRenderedTextBytes(text);
|
|
1054
|
+
const isReplacement = outputRecoveryPhase === 'queued';
|
|
1055
|
+
const shouldRecover =
|
|
1056
|
+
candidateOutputRecoveryPath !== undefined &&
|
|
1057
|
+
outputRecoveryPhase === 'eligible' &&
|
|
1058
|
+
event.message.stopReason === 'stop' &&
|
|
1059
|
+
renderedBytes > FUSION_CANDIDATE_MAX_OUTPUT_BYTES;
|
|
1060
|
+
const recoveryRole = isReplacement
|
|
1061
|
+
? 'replacement'
|
|
1062
|
+
: shouldRecover
|
|
1063
|
+
? 'oversized_original'
|
|
1064
|
+
: 'none';
|
|
1065
|
+
const record = buildFusionChildResultMetadata(event.message, cacheObservation, {
|
|
1066
|
+
candidateLimitBytes:
|
|
1067
|
+
candidateOutputRecoveryPath === undefined ? null : FUSION_CANDIDATE_MAX_OUTPUT_BYTES,
|
|
1068
|
+
recoveryRole,
|
|
1069
|
+
});
|
|
1070
|
+
if (shouldRecover) {
|
|
1071
|
+
try {
|
|
1072
|
+
createCandidateOutputRecoveryArtifact(candidateOutputRecoveryPath, text);
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
outputRecoveryFailed = true;
|
|
1075
|
+
latchAuditProcessFailure();
|
|
1076
|
+
throw error;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
986
1079
|
await writeMetadata(record);
|
|
987
1080
|
childResultRecords.push(record);
|
|
1081
|
+
if (shouldRecover) {
|
|
1082
|
+
outputRecoveryPhase = 'queued';
|
|
1083
|
+
try {
|
|
1084
|
+
pi.setActiveTools([]);
|
|
1085
|
+
pi.sendUserMessage(FUSION_CANDIDATE_OUTPUT_COMPRESSION_PROMPT, {
|
|
1086
|
+
deliverAs: 'followUp',
|
|
1087
|
+
});
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
outputRecoveryFailed = true;
|
|
1090
|
+
latchAuditProcessFailure();
|
|
1091
|
+
throw error;
|
|
1092
|
+
}
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
if (isReplacement) {
|
|
1096
|
+
outputRecoveryPhase = 'finished';
|
|
1097
|
+
if (renderedBytes > FUSION_CANDIDATE_MAX_OUTPUT_BYTES) {
|
|
1098
|
+
outputRecoveryFailed = true;
|
|
1099
|
+
latchAuditProcessFailure();
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
988
1102
|
});
|
|
989
1103
|
pi.on('agent_settled', async (_event, ctx) => {
|
|
990
1104
|
if (settlementPublished) {
|
|
@@ -1001,6 +1115,7 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
|
|
|
1001
1115
|
childResultRecords,
|
|
1002
1116
|
runtimeGuardFailed,
|
|
1003
1117
|
cacheObservationFailed,
|
|
1118
|
+
outputRecoveryFailed,
|
|
1004
1119
|
);
|
|
1005
1120
|
if (settlement.status !== 'complete') latchAuditProcessFailure();
|
|
1006
1121
|
await writeSettlement(settlement);
|