pi-background-tasks 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BACKGROUND-TASKS-INSTRUCTIONS.md +1 -1
- package/README.md +70 -25
- package/TESTING.md +42 -2
- package/TEST_PLAN.md +7 -4
- package/dist/extensions/anthropic-attribution-child.js +4 -0
- package/dist/extensions/anthropic-attribution-child.js.map +1 -0
- package/dist/extensions/anthropic-attribution.js +107 -0
- package/dist/extensions/anthropic-attribution.js.map +1 -0
- package/dist/extensions/background-tasks.js +2 -0
- package/dist/extensions/background-tasks.js.map +1 -0
- package/dist/extensions/delegate-child.js +2 -0
- package/dist/extensions/delegate-child.js.map +1 -0
- package/dist/extensions/fusion-child.js +2 -0
- package/dist/extensions/fusion-child.js.map +1 -0
- package/dist/package.json +5 -0
- package/dist/src/core/anthropic-attribution-path.js +24 -0
- package/dist/src/core/anthropic-attribution-path.js.map +1 -0
- package/dist/src/core/anthropic-attribution.js +2247 -0
- package/dist/src/core/anthropic-attribution.js.map +1 -0
- package/dist/src/core/attested-pi-contract.js +5 -0
- package/dist/src/core/attested-pi-contract.js.map +1 -0
- package/dist/src/core/attested-pi-run.js +749 -0
- package/dist/src/core/attested-pi-run.js.map +1 -0
- package/dist/src/core/canonical-json.js +19 -0
- package/dist/src/core/canonical-json.js.map +1 -0
- package/dist/src/core/common.js +787 -0
- package/dist/src/core/common.js.map +1 -0
- package/dist/src/core/config.js +78 -0
- package/dist/src/core/config.js.map +1 -0
- package/dist/src/core/context/parent-snapshot.js +75 -0
- package/dist/src/core/context/parent-snapshot.js.map +1 -0
- package/dist/src/core/context/token-budget.js +631 -0
- package/dist/src/core/context/token-budget.js.map +1 -0
- package/dist/src/core/context/visible-conversation-v2.js +390 -0
- package/dist/src/core/context/visible-conversation-v2.js.map +1 -0
- package/dist/src/core/delegate/artifacts.js +357 -0
- package/dist/src/core/delegate/artifacts.js.map +1 -0
- package/dist/src/core/delegate/budget.js +266 -0
- package/dist/src/core/delegate/budget.js.map +1 -0
- package/dist/src/core/delegate/facade-contract.js +7 -0
- package/dist/src/core/delegate/facade-contract.js.map +1 -0
- package/dist/src/core/delegate/hook-contract-evidence.json +18 -0
- package/dist/src/core/delegate/hook-contract.js +124 -0
- package/dist/src/core/delegate/hook-contract.js.map +1 -0
- package/dist/src/core/delegate/launch.js +366 -0
- package/dist/src/core/delegate/launch.js.map +1 -0
- package/dist/src/core/delegate/result-package.js +293 -0
- package/dist/src/core/delegate/result-package.js.map +1 -0
- package/dist/src/core/delegate/runner.js +398 -0
- package/dist/src/core/delegate/runner.js.map +1 -0
- package/dist/src/core/delegate/seed.js +334 -0
- package/dist/src/core/delegate/seed.js.map +1 -0
- package/dist/src/core/delegate/types.js +103 -0
- package/dist/src/core/delegate/types.js.map +1 -0
- package/dist/src/core/durable-fs.js +372 -0
- package/dist/src/core/durable-fs.js.map +1 -0
- package/dist/src/core/extension-api.js +382 -0
- package/dist/src/core/extension-api.js.map +1 -0
- package/dist/src/core/fusion/artifacts.js +654 -0
- package/dist/src/core/fusion/artifacts.js.map +1 -0
- package/dist/src/core/fusion/budget.js +949 -0
- package/dist/src/core/fusion/budget.js.map +1 -0
- package/dist/src/core/fusion/child-protocol.js +176 -0
- package/dist/src/core/fusion/child-protocol.js.map +1 -0
- package/dist/src/core/fusion/claude-cache.js +156 -0
- package/dist/src/core/fusion/claude-cache.js.map +1 -0
- package/dist/src/core/fusion/clean-context.js +64 -0
- package/dist/src/core/fusion/clean-context.js.map +1 -0
- package/dist/src/core/fusion/config.js +349 -0
- package/dist/src/core/fusion/config.js.map +1 -0
- package/dist/src/core/fusion/context.js +184 -0
- package/dist/src/core/fusion/context.js.map +1 -0
- package/dist/src/core/fusion/evaluation.js +696 -0
- package/dist/src/core/fusion/evaluation.js.map +1 -0
- package/dist/src/core/fusion/facade-contract.js +3 -0
- package/dist/src/core/fusion/facade-contract.js.map +1 -0
- package/dist/src/core/fusion/orchestrator.js +867 -0
- package/dist/src/core/fusion/orchestrator.js.map +1 -0
- package/dist/src/core/fusion/output-contract.js +27 -0
- package/dist/src/core/fusion/output-contract.js.map +1 -0
- package/dist/src/core/fusion/pi-child.js +1741 -0
- package/dist/src/core/fusion/pi-child.js.map +1 -0
- package/dist/src/core/fusion/prompts.js +260 -0
- package/dist/src/core/fusion/prompts.js.map +1 -0
- package/dist/src/core/fusion/result-package.js +857 -0
- package/dist/src/core/fusion/result-package.js.map +1 -0
- package/dist/src/core/fusion/source-policy.js +259 -0
- package/dist/src/core/fusion/source-policy.js.map +1 -0
- package/dist/src/core/fusion/types.js +207 -0
- package/dist/src/core/fusion/types.js.map +1 -0
- package/dist/src/core/fusion/web-fetch.js +777 -0
- package/dist/src/core/fusion/web-fetch.js.map +1 -0
- package/dist/src/core/fusion/workflows.js +126 -0
- package/dist/src/core/fusion/workflows.js.map +1 -0
- package/dist/src/core/lazy-module.js +181 -0
- package/dist/src/core/lazy-module.js.map +1 -0
- package/dist/src/core/pi-launch.js +431 -0
- package/dist/src/core/pi-launch.js.map +1 -0
- package/dist/src/core/registry.js +3331 -0
- package/dist/src/core/registry.js.map +1 -0
- package/dist/src/core/reload-shell-owner.js +1396 -0
- package/dist/src/core/reload-shell-owner.js.map +1 -0
- package/dist/src/core/shell-policy.js +80 -0
- package/dist/src/core/shell-policy.js.map +1 -0
- package/dist/src/core/task-durable.js +51 -0
- package/dist/src/core/task-durable.js.map +1 -0
- package/dist/src/core/update-check.js +92 -0
- package/dist/src/core/update-check.js.map +1 -0
- package/dist/src/core/windows-taskkill.js +185 -0
- package/dist/src/core/windows-taskkill.js.map +1 -0
- package/dist/src/delegate-child-extension.js +776 -0
- package/dist/src/delegate-child-extension.js.map +1 -0
- package/dist/src/delegate-extension.js +746 -0
- package/dist/src/delegate-extension.js.map +1 -0
- package/dist/src/extension.js +953 -0
- package/dist/src/extension.js.map +1 -0
- package/dist/src/fusion-child-extension.js +760 -0
- package/dist/src/fusion-child-extension.js.map +1 -0
- package/dist/src/fusion-extension.js +1030 -0
- package/dist/src/fusion-extension.js.map +1 -0
- package/dist/src/ui/background-tasks-manager.js +689 -0
- package/dist/src/ui/background-tasks-manager.js.map +1 -0
- package/dist/src/ui/fusion-model-selector.js +277 -0
- package/dist/src/ui/fusion-model-selector.js.map +1 -0
- package/docs/INDEX.md +35 -33
- package/docs/api/eventbus-v1.md +21 -5
- package/docs/choose-a-workflow.md +2 -0
- package/docs/commands/bg-clear.md +3 -3
- package/docs/commands/bg-update.md +3 -3
- package/docs/commands/bg.md +18 -6
- package/docs/commands/claude-cache.md +4 -4
- package/docs/commands/fusion-models.md +49 -4
- package/docs/commands/fusion.md +3 -3
- package/docs/commands/jobs.md +3 -3
- package/docs/commands/kill.md +3 -3
- package/docs/commands/logs.md +3 -3
- package/docs/commands/task-manager.md +5 -5
- package/docs/concepts/completion-delivery.md +6 -2
- package/docs/getting-started.md +7 -1
- package/docs/manifest.json +228 -42
- package/docs/operations/configuration.md +58 -5
- package/docs/operations/releasing.md +4 -2
- package/docs/operations/testing.md +22 -0
- package/docs/operations/troubleshooting.md +6 -2
- package/docs/read-before-edit.md +44 -31
- package/docs/reference/runtime-contracts.md +87 -51
- package/docs/reference/shortcuts-and-dock.md +28 -14
- package/docs/subsystems/anthropic-attribution.md +23 -11
- package/docs/subsystems/attested-pi-runs.md +12 -7
- package/docs/subsystems/background-task-runtime.md +53 -11
- package/docs/subsystems/child-launch-durability-and-safety.md +28 -9
- package/docs/subsystems/delegation.md +16 -3
- package/docs/subsystems/docs-freshness-gate.md +14 -7
- package/docs/subsystems/fusion.md +36 -11
- package/docs/subsystems/host-ui-and-telemetry.md +22 -9
- package/docs/tools/bg_delegate.md +3 -1
- package/docs/tools/bg_kill.md +3 -1
- package/docs/tools/bg_logs.md +3 -1
- package/docs/tools/bg_result.md +5 -3
- package/docs/tools/bg_run.md +31 -5
- package/docs/tools/bg_run_pi_attested.md +3 -1
- package/docs/tools/bg_status.md +3 -1
- package/docs/tools/fusion_investigate.md +3 -1
- package/docs/tools/fusion_reason.md +3 -1
- package/docs/tools/fusion_research.md +3 -1
- package/docs/tools/fusion_validate.md +3 -1
- package/extensions/anthropic-attribution-child.ts +3 -0
- package/extensions/anthropic-attribution.ts +166 -1
- package/package.json +12 -10
- package/src/core/anthropic-attribution-path.ts +5 -4
- package/src/core/anthropic-attribution.ts +293 -113
- package/src/core/attested-pi-contract.ts +4 -0
- package/src/core/attested-pi-run.ts +468 -81
- package/src/core/canonical-json.ts +21 -0
- package/src/core/common.ts +450 -40
- package/src/core/config.ts +121 -0
- package/src/core/context/visible-conversation-v2.ts +3 -6
- package/src/core/delegate/artifacts.ts +13 -9
- package/src/core/delegate/budget.ts +13 -6
- package/src/core/delegate/facade-contract.ts +6 -0
- package/src/core/delegate/result-package.ts +32 -14
- package/src/core/delegate/runner.ts +129 -58
- package/src/core/delegate/seed.ts +5 -7
- package/src/core/durable-fs.ts +178 -30
- package/src/core/extension-api.ts +36 -8
- package/src/core/fusion/artifacts.ts +17 -16
- package/src/core/fusion/clean-context.ts +8 -5
- package/src/core/fusion/config.ts +2 -1
- package/src/core/fusion/context.ts +2 -6
- package/src/core/fusion/facade-contract.ts +2 -0
- package/src/core/fusion/orchestrator.ts +3 -3
- package/src/core/fusion/prompts.ts +1 -1
- package/src/core/fusion/result-package.ts +259 -72
- package/src/core/fusion/source-policy.ts +43 -17
- package/src/core/lazy-module.ts +215 -0
- package/src/core/pi-launch.ts +390 -65
- package/src/core/registry.ts +2010 -381
- package/src/core/reload-shell-owner.ts +1662 -0
- package/src/core/shell-policy.ts +134 -0
- package/src/core/task-durable.ts +67 -0
- package/src/delegate-extension.ts +404 -144
- package/src/extension.ts +387 -114
- package/src/fusion-extension.ts +202 -96
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalJson } from '../canonical-json.js';
|
|
3
|
+
import { normalizeFusionDeclaredSources } from './source-policy.js';
|
|
4
|
+
import { FUSION_INPUT_SCHEMA_VERSION, FusionError, } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Public v1 clean builder. It is deliberately pure: callers provide cwd and
|
|
7
|
+
* normalized request text explicitly, and this module has no dependency on Pi
|
|
8
|
+
* session, snapshot, parent-context, or visible-conversation APIs.
|
|
9
|
+
*/
|
|
10
|
+
export const buildCleanFusionCanonicalInput = buildFusionCleanTaskCanonicalInput;
|
|
11
|
+
function sha256Text(value) {
|
|
12
|
+
return createHash('sha256').update(Buffer.from(value, 'utf8')).digest('hex');
|
|
13
|
+
}
|
|
14
|
+
export function buildFusionCleanTaskCanonicalInput(options) {
|
|
15
|
+
if (options.request.trim().length === 0) {
|
|
16
|
+
throw new FusionError('fusion request must not be blank', {
|
|
17
|
+
code: 'context_capture_failed',
|
|
18
|
+
childCreated: false,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
if (!['investigate', 'research', 'validate'].includes(options.workflow)) {
|
|
22
|
+
throw new FusionError('clean-task fusion input is available only to investigate, research, and validate workflows', {
|
|
23
|
+
code: 'context_capture_failed',
|
|
24
|
+
childCreated: false,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const declaredSources = normalizeFusionDeclaredSources(options.declaredSources ?? []);
|
|
28
|
+
if (options.workflow === 'research' && declaredSources.length === 0) {
|
|
29
|
+
throw new FusionError('fusion research requires at least one declared source URL and purpose', {
|
|
30
|
+
code: 'context_capture_failed',
|
|
31
|
+
childCreated: false,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
if (options.workflow !== 'research' && declaredSources.length > 0) {
|
|
35
|
+
throw new FusionError('declared sources are accepted only by the research workflow', {
|
|
36
|
+
code: 'context_capture_failed',
|
|
37
|
+
childCreated: false,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const request = {
|
|
41
|
+
source: options.source,
|
|
42
|
+
authority: 'explicit_text',
|
|
43
|
+
text: options.request,
|
|
44
|
+
sha256: sha256Text(options.request),
|
|
45
|
+
};
|
|
46
|
+
const input = {
|
|
47
|
+
schema_version: FUSION_INPUT_SCHEMA_VERSION,
|
|
48
|
+
workflow: options.workflow,
|
|
49
|
+
cwd: options.cwd,
|
|
50
|
+
request,
|
|
51
|
+
context: {
|
|
52
|
+
kind: 'clean_task',
|
|
53
|
+
policy_id: 'fusion-clean-task-v1',
|
|
54
|
+
declared_sources: declaredSources,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
input,
|
|
59
|
+
serialized: canonicalJson(input),
|
|
60
|
+
declaredSources,
|
|
61
|
+
transcriptLeafId: null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=clean-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clean-context.js","sourceRoot":"","sources":["../../../../src/core/fusion/clean-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,8BAA8B,EAAkC,MAAM,oBAAoB,CAAC;AACpG,OAAO,EACL,2BAA2B,EAC3B,WAAW,GAMZ,MAAM,YAAY,CAAC;AAUpB;;;;GAIG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG,kCAAkC,CAAC;AASjF,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC/E,CAAC;AAED,MAAM,UAAU,kCAAkC,CAChD,OAAyC;IAEzC,IAAI,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxC,MAAM,IAAI,WAAW,CAAC,kCAAkC,EAAE;YACxD,IAAI,EAAE,wBAAwB;YAC9B,YAAY,EAAE,KAAK;SACpB,CAAC,CAAC;IACL,CAAC;IACD,IAAI,CAAC,CAAC,aAAa,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,WAAW,CACnB,4FAA4F,EAC5F;YACE,IAAI,EAAE,wBAAwB;YAC9B,YAAY,EAAE,KAAK;SACpB,CACF,CAAC;IACJ,CAAC;IACD,MAAM,eAAe,GAAG,8BAA8B,CAAC,OAAO,CAAC,eAAe,IAAI,EAAE,CAAC,CAAC;IACtF,IAAI,OAAO,CAAC,QAAQ,KAAK,UAAU,IAAI,eAAe,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpE,MAAM,IAAI,WAAW,CAAC,uEAAuE,EAAE;YAC7F,IAAI,EAAE,wBAAwB;YAC9B,YAAY,EAAE,KAAK;SACpB,CAAC,CAAC;IACL,CAAC;IACD,IAAI,OAAO,CAAC,QAAQ,KAAK,UAAU,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClE,MAAM,IAAI,WAAW,CAAC,6DAA6D,EAAE;YACnF,IAAI,EAAE,wBAAwB;YAC9B,YAAY,EAAE,KAAK;SACpB,CAAC,CAAC;IACL,CAAC;IACD,MAAM,OAAO,GAA6B;QACxC,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,SAAS,EAAE,eAAe;QAC1B,IAAI,EAAE,OAAO,CAAC,OAAO;QACrB,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC;KACpC,CAAC;IACF,MAAM,KAAK,GAAoC;QAC7C,cAAc,EAAE,2BAA2B;QAC3C,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,OAAO;QACP,OAAO,EAAE;YACP,IAAI,EAAE,YAAY;YAClB,SAAS,EAAE,sBAAsB;YACjC,gBAAgB,EAAE,eAAe;SAClC;KACF,CAAC;IACF,OAAO;QACL,KAAK;QACL,UAAU,EAAE,aAAa,CAAC,KAAK,CAAC;QAChC,eAAe;QACf,gBAAgB,EAAE,IAAI;KACvB,CAAC;AACJ,CAAC","sourcesContent":["import { createHash } from 'node:crypto';\nimport { canonicalJson } from '../canonical-json.js';\nimport { normalizeFusionDeclaredSources, type DeclaredFusionSourceInput } from './source-policy.js';\nimport {\n FUSION_INPUT_SCHEMA_VERSION,\n FusionError,\n type FusionCanonicalRequestV3,\n type FusionCleanTaskCanonicalInputV5,\n type FusionDeclaredSourceV1,\n type FusionSource,\n type FusionWorkflowId,\n} from './types.js';\n\nexport interface BuildFusionCleanTaskInputOptions {\n cwd: string;\n source: FusionSource;\n request: string;\n workflow: Exclude<FusionWorkflowId, 'reason'>;\n declaredSources?: readonly DeclaredFusionSourceInput[] | undefined;\n}\n\n/**\n * Public v1 clean builder. It is deliberately pure: callers provide cwd and\n * normalized request text explicitly, and this module has no dependency on Pi\n * session, snapshot, parent-context, or visible-conversation APIs.\n */\nexport const buildCleanFusionCanonicalInput = buildFusionCleanTaskCanonicalInput;\n\nexport interface BuiltFusionCleanTaskCanonicalInput {\n input: FusionCleanTaskCanonicalInputV5;\n serialized: string;\n declaredSources: readonly FusionDeclaredSourceV1[];\n transcriptLeafId: null;\n}\n\nfunction sha256Text(value: string): string {\n return createHash('sha256').update(Buffer.from(value, 'utf8')).digest('hex');\n}\n\nexport function buildFusionCleanTaskCanonicalInput(\n options: BuildFusionCleanTaskInputOptions,\n): BuiltFusionCleanTaskCanonicalInput {\n if (options.request.trim().length === 0) {\n throw new FusionError('fusion request must not be blank', {\n code: 'context_capture_failed',\n childCreated: false,\n });\n }\n if (!['investigate', 'research', 'validate'].includes(options.workflow)) {\n throw new FusionError(\n 'clean-task fusion input is available only to investigate, research, and validate workflows',\n {\n code: 'context_capture_failed',\n childCreated: false,\n },\n );\n }\n const declaredSources = normalizeFusionDeclaredSources(options.declaredSources ?? []);\n if (options.workflow === 'research' && declaredSources.length === 0) {\n throw new FusionError('fusion research requires at least one declared source URL and purpose', {\n code: 'context_capture_failed',\n childCreated: false,\n });\n }\n if (options.workflow !== 'research' && declaredSources.length > 0) {\n throw new FusionError('declared sources are accepted only by the research workflow', {\n code: 'context_capture_failed',\n childCreated: false,\n });\n }\n const request: FusionCanonicalRequestV3 = {\n source: options.source,\n authority: 'explicit_text',\n text: options.request,\n sha256: sha256Text(options.request),\n };\n const input: FusionCleanTaskCanonicalInputV5 = {\n schema_version: FUSION_INPUT_SCHEMA_VERSION,\n workflow: options.workflow,\n cwd: options.cwd,\n request,\n context: {\n kind: 'clean_task',\n policy_id: 'fusion-clean-task-v1',\n declared_sources: declaredSources,\n },\n };\n return {\n input,\n serialized: canonicalJson(input),\n declaredSources,\n transcriptLeafId: null,\n };\n}\n"]}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { chmod, mkdir, open, readFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, join } from 'node:path';
|
|
4
|
+
import { getAgentDir } from '@earendil-works/pi-coding-agent';
|
|
5
|
+
import { isJsonObject, parseJsonText } from '../common.js';
|
|
6
|
+
import { replaceFileDurable } from '../durable-fs.js';
|
|
7
|
+
import { CLAUDE_CODE_200K_SUBSCRIPTION_CONTEXT_WINDOW } from '../anthropic-attribution.js';
|
|
8
|
+
import { CURRENT_MODEL_SELECTION } from './facade-contract.js';
|
|
9
|
+
import { FUSION_MODEL_CONFIG_SCHEMA_VERSION, FusionError, } from './types.js';
|
|
10
|
+
export const FUSION_MODEL_CONFIG_FILE = 'fusion-models.json';
|
|
11
|
+
export { CURRENT_MODEL_SELECTION } from './facade-contract.js';
|
|
12
|
+
export function defaultFusionModelConfig() {
|
|
13
|
+
return {
|
|
14
|
+
schema_version: FUSION_MODEL_CONFIG_SCHEMA_VERSION,
|
|
15
|
+
candidates: [CURRENT_MODEL_SELECTION, CURRENT_MODEL_SELECTION, CURRENT_MODEL_SELECTION],
|
|
16
|
+
evaluator: CURRENT_MODEL_SELECTION,
|
|
17
|
+
merger: CURRENT_MODEL_SELECTION,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function fusionModelConfigPath(agentDir = getAgentDir()) {
|
|
21
|
+
return join(agentDir, FUSION_MODEL_CONFIG_FILE);
|
|
22
|
+
}
|
|
23
|
+
function sha256Hex(bytes) {
|
|
24
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
25
|
+
}
|
|
26
|
+
async function revisionForPath(path) {
|
|
27
|
+
try {
|
|
28
|
+
const bytes = await readFile(path);
|
|
29
|
+
return { path, exists: true, sha256: sha256Hex(bytes) };
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (errorHasCode(error, 'ENOENT'))
|
|
33
|
+
return { path, exists: false, sha256: null };
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function errorHasCode(error, code) {
|
|
38
|
+
return isJsonObject(error) && error['code'] === code;
|
|
39
|
+
}
|
|
40
|
+
function keysOf(value) {
|
|
41
|
+
return Object.keys(value).sort();
|
|
42
|
+
}
|
|
43
|
+
function assertClosed(record, expected, label) {
|
|
44
|
+
const expectedSet = new Set(expected);
|
|
45
|
+
for (const key of Object.keys(record)) {
|
|
46
|
+
if (!expectedSet.has(key))
|
|
47
|
+
throw configError(`${label} contains unknown key ${key}`);
|
|
48
|
+
}
|
|
49
|
+
for (const key of expected) {
|
|
50
|
+
if (!Object.prototype.hasOwnProperty.call(record, key)) {
|
|
51
|
+
throw configError(`${label} is missing key ${key}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function configError(message) {
|
|
56
|
+
return new FusionError(message, { code: 'config_invalid', childCreated: false });
|
|
57
|
+
}
|
|
58
|
+
function requireSelection(value, label) {
|
|
59
|
+
if (typeof value !== 'string')
|
|
60
|
+
throw configError(`${label} must be a string`);
|
|
61
|
+
if (value === CURRENT_MODEL_SELECTION)
|
|
62
|
+
return value;
|
|
63
|
+
const trimmed = value.trim();
|
|
64
|
+
if (trimmed.length === 0)
|
|
65
|
+
throw configError(`${label} must not be blank`);
|
|
66
|
+
if (trimmed !== value)
|
|
67
|
+
throw configError(`${label} must not have surrounding whitespace`);
|
|
68
|
+
if (!trimmed.includes('/'))
|
|
69
|
+
throw configError(`${label} must be a qualified provider/model key`);
|
|
70
|
+
return trimmed;
|
|
71
|
+
}
|
|
72
|
+
function requireCandidateSelections(value) {
|
|
73
|
+
if (!Array.isArray(value))
|
|
74
|
+
throw configError('candidates must be an array');
|
|
75
|
+
if (value.length !== 3)
|
|
76
|
+
throw configError('candidates must contain exactly three entries');
|
|
77
|
+
const first = requireSelection(value[0], 'candidates[0]');
|
|
78
|
+
const second = requireSelection(value[1], 'candidates[1]');
|
|
79
|
+
const third = requireSelection(value[2], 'candidates[2]');
|
|
80
|
+
return [first, second, third];
|
|
81
|
+
}
|
|
82
|
+
export function parseFusionModelConfig(value) {
|
|
83
|
+
if (!isJsonObject(value) || Array.isArray(value))
|
|
84
|
+
throw configError('fusion model config must be an object');
|
|
85
|
+
const record = value;
|
|
86
|
+
assertClosed(record, ['schema_version', 'candidates', 'evaluator', 'merger'], 'fusion model config');
|
|
87
|
+
if (record['schema_version'] !== FUSION_MODEL_CONFIG_SCHEMA_VERSION) {
|
|
88
|
+
throw configError('fusion model config schema_version mismatch');
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
schema_version: FUSION_MODEL_CONFIG_SCHEMA_VERSION,
|
|
92
|
+
candidates: requireCandidateSelections(record['candidates']),
|
|
93
|
+
evaluator: requireSelection(record['evaluator'], 'evaluator'),
|
|
94
|
+
merger: requireSelection(record['merger'], 'merger'),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export async function loadFusionModelConfig(path = fusionModelConfigPath()) {
|
|
98
|
+
const revision = await revisionForPath(path);
|
|
99
|
+
if (!revision.exists)
|
|
100
|
+
return { config: defaultFusionModelConfig(), revision };
|
|
101
|
+
let parsed;
|
|
102
|
+
try {
|
|
103
|
+
parsed = parseJsonText(await readFile(path, 'utf8'));
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
throw configError(`fusion model config is not valid JSON at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
107
|
+
}
|
|
108
|
+
const config = parseFusionModelConfig(parsed);
|
|
109
|
+
return { config, revision };
|
|
110
|
+
}
|
|
111
|
+
function qualifiedModelKey(model) {
|
|
112
|
+
return `${model.provider}/${model.id}`;
|
|
113
|
+
}
|
|
114
|
+
function requireContextWindow(model, label) {
|
|
115
|
+
const value = model.contextWindow;
|
|
116
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
117
|
+
throw new FusionError(`${label} has no positive context window`, {
|
|
118
|
+
code: 'model_unavailable',
|
|
119
|
+
childCreated: false,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return Math.floor(value);
|
|
123
|
+
}
|
|
124
|
+
function transportContextWindow(model, label) {
|
|
125
|
+
const advertised = requireContextWindow(model, label);
|
|
126
|
+
return model.provider === 'anthropic'
|
|
127
|
+
? Math.min(advertised, CLAUDE_CODE_200K_SUBSCRIPTION_CONTEXT_WINDOW)
|
|
128
|
+
: advertised;
|
|
129
|
+
}
|
|
130
|
+
function requireMaxOutputTokens(model, label) {
|
|
131
|
+
const value = model.maxTokens;
|
|
132
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
133
|
+
throw new FusionError(`${label} has no positive maximum output token capacity`, {
|
|
134
|
+
code: 'model_unavailable',
|
|
135
|
+
childCreated: false,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return Math.floor(value);
|
|
139
|
+
}
|
|
140
|
+
function modelIndex(models) {
|
|
141
|
+
const out = new Map();
|
|
142
|
+
for (const model of models)
|
|
143
|
+
out.set(qualifiedModelKey(model), model);
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
const FRONTIER_MODEL_PATTERN = /(?:^|[-_/])(?:gpt|codex|claude|opus|sonnet|o[134](?:-[a-z0-9.]+)*)(?:[-_/]|$)/iu;
|
|
147
|
+
const TRUSTED_SUBSCRIPTION_ENDPOINTS = Object.freeze({
|
|
148
|
+
anthropic: 'https://api.anthropic.com',
|
|
149
|
+
'openai-codex': 'https://chatgpt.com/backend-api',
|
|
150
|
+
});
|
|
151
|
+
const AUTH_HEADER_NAMES = new Set(['authorization', 'proxy-authorization', 'x-api-key', 'api-key']);
|
|
152
|
+
function isKnownFrontierEndpoint(baseUrl) {
|
|
153
|
+
if (baseUrl === undefined || baseUrl.trim().length === 0)
|
|
154
|
+
return false;
|
|
155
|
+
try {
|
|
156
|
+
const hostname = new URL(baseUrl).hostname.toLowerCase().replace(/\.+$/u, '');
|
|
157
|
+
return (hostname === 'api.openai.com' ||
|
|
158
|
+
hostname === 'api.anthropic.com' ||
|
|
159
|
+
hostname === 'openrouter.ai' ||
|
|
160
|
+
hostname === 'chatgpt.com' ||
|
|
161
|
+
hostname.endsWith('.openai.azure.com') ||
|
|
162
|
+
hostname.endsWith('.cognitiveservices.azure.com') ||
|
|
163
|
+
hostname.endsWith('.ai.azure.com'));
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function assertTrustedSubscriptionEndpoint(model, slotLabel, provider) {
|
|
170
|
+
const expectedText = TRUSTED_SUBSCRIPTION_ENDPOINTS[provider];
|
|
171
|
+
const effectiveText = model.baseUrl?.trim() || expectedText;
|
|
172
|
+
let effective;
|
|
173
|
+
try {
|
|
174
|
+
effective = new URL(effectiveText);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
throw new FusionError(`${slotLabel} route ${model.provider}/${model.id} has a malformed subscription endpoint`, { code: 'model_unavailable', childCreated: false });
|
|
178
|
+
}
|
|
179
|
+
const expected = new URL(expectedText);
|
|
180
|
+
const effectivePath = effective.href.slice(effective.origin.length).replace(/\/+$/u, '');
|
|
181
|
+
const expectedPath = expected.href.slice(expected.origin.length).replace(/\/+$/u, '');
|
|
182
|
+
if (effective.protocol !== 'https:' ||
|
|
183
|
+
effective.username !== '' ||
|
|
184
|
+
effective.password !== '' ||
|
|
185
|
+
effective.search !== '' ||
|
|
186
|
+
effective.hash !== '' ||
|
|
187
|
+
effective.origin !== expected.origin ||
|
|
188
|
+
effectivePath !== expectedPath) {
|
|
189
|
+
throw new FusionError(`${slotLabel} route ${model.provider}/${model.id} does not use the trusted Pi subscription endpoint ${expectedText}`, { code: 'model_unavailable', childCreated: false });
|
|
190
|
+
}
|
|
191
|
+
const unsafeHeader = Object.keys(model.headers ?? {}).find((name) => AUTH_HEADER_NAMES.has(name.toLowerCase()));
|
|
192
|
+
if (unsafeHeader !== undefined) {
|
|
193
|
+
throw new FusionError(`${slotLabel} route ${model.provider}/${model.id} overrides subscription authentication header ${unsafeHeader}`, { code: 'model_unavailable', childCreated: false });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function assertSubscriptionRoute(model, slotLabel, registry) {
|
|
197
|
+
const provider = model.provider.toLowerCase();
|
|
198
|
+
const frontier = provider === 'openai' ||
|
|
199
|
+
provider === 'openrouter' ||
|
|
200
|
+
provider === 'anthropic' ||
|
|
201
|
+
provider === 'openai-codex' ||
|
|
202
|
+
provider.includes('azure') ||
|
|
203
|
+
FRONTIER_MODEL_PATTERN.test(`${provider}/${model.id}`) ||
|
|
204
|
+
isKnownFrontierEndpoint(model.baseUrl);
|
|
205
|
+
if (!frontier)
|
|
206
|
+
return;
|
|
207
|
+
if (provider !== 'anthropic' && provider !== 'openai-codex') {
|
|
208
|
+
throw new FusionError(`${slotLabel} route ${model.provider}/${model.id} is a frontier-model API channel; Fusion requires the Pi Anthropic or Codex subscription route`, { code: 'model_unavailable', childCreated: false });
|
|
209
|
+
}
|
|
210
|
+
assertTrustedSubscriptionEndpoint(model, slotLabel, provider);
|
|
211
|
+
if (registry.isUsingOAuth === undefined) {
|
|
212
|
+
throw new FusionError(`${slotLabel} route ${model.provider}/${model.id} cannot be admitted because ModelRegistry OAuth observation is unavailable`, { code: 'model_unavailable', childCreated: false });
|
|
213
|
+
}
|
|
214
|
+
if (!registry.isUsingOAuth(model)) {
|
|
215
|
+
throw new FusionError(`${slotLabel} route ${model.provider}/${model.id} is not using subscription OAuth; metered API credentials are forbidden for Fusion`, { code: 'model_unavailable', childCreated: false });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function resolveSelection(selection, slotLabel, availableByKey, currentModel, thinkingLevel, registry) {
|
|
219
|
+
if (selection === CURRENT_MODEL_SELECTION) {
|
|
220
|
+
if (currentModel === undefined) {
|
|
221
|
+
throw new FusionError(`${slotLabel} uses $current but Pi has no current model`, {
|
|
222
|
+
code: 'model_unavailable',
|
|
223
|
+
childCreated: false,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
const qualifiedId = qualifiedModelKey(currentModel);
|
|
227
|
+
const available = availableByKey.get(qualifiedId);
|
|
228
|
+
if (available === undefined) {
|
|
229
|
+
throw new FusionError(`${slotLabel} current model is not available to child Pi: ${qualifiedId}`, {
|
|
230
|
+
code: 'model_unavailable',
|
|
231
|
+
childCreated: false,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
assertSubscriptionRoute(available, slotLabel, registry);
|
|
235
|
+
return {
|
|
236
|
+
selection,
|
|
237
|
+
source: 'current',
|
|
238
|
+
provider: available.provider,
|
|
239
|
+
model: available.id,
|
|
240
|
+
qualifiedId,
|
|
241
|
+
thinkingLevel,
|
|
242
|
+
contextWindow: transportContextWindow(available, slotLabel),
|
|
243
|
+
maxOutputTokens: requireMaxOutputTokens(available, slotLabel),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const model = availableByKey.get(selection);
|
|
247
|
+
if (model === undefined) {
|
|
248
|
+
throw new FusionError(`${slotLabel} configured model is unavailable: ${selection}`, {
|
|
249
|
+
code: 'model_unavailable',
|
|
250
|
+
childCreated: false,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
assertSubscriptionRoute(model, slotLabel, registry);
|
|
254
|
+
return {
|
|
255
|
+
selection,
|
|
256
|
+
source: 'configured',
|
|
257
|
+
provider: model.provider,
|
|
258
|
+
model: model.id,
|
|
259
|
+
qualifiedId: selection,
|
|
260
|
+
thinkingLevel,
|
|
261
|
+
contextWindow: transportContextWindow(model, slotLabel),
|
|
262
|
+
maxOutputTokens: requireMaxOutputTokens(model, slotLabel),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
export function resolveFusionModels(input) {
|
|
266
|
+
const availableByKey = modelIndex(input.modelRegistry.getAvailable());
|
|
267
|
+
const [first, second, third] = input.config.candidates;
|
|
268
|
+
const resolve = (selection, slot) => resolveSelection(selection, slot, availableByKey, input.currentModel, input.thinkingLevel, input.modelRegistry);
|
|
269
|
+
return {
|
|
270
|
+
candidates: [
|
|
271
|
+
resolve(first, 'candidate 1'),
|
|
272
|
+
resolve(second, 'candidate 2'),
|
|
273
|
+
resolve(third, 'candidate 3'),
|
|
274
|
+
],
|
|
275
|
+
evaluator: resolve(input.config.evaluator, 'evaluator'),
|
|
276
|
+
merger: resolve(input.config.merger, 'merger'),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
async function delay(ms) {
|
|
280
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
281
|
+
}
|
|
282
|
+
async function withConfigLock(path, fn) {
|
|
283
|
+
const dir = dirname(path);
|
|
284
|
+
const lockPath = join(dir, `.${basename(path)}.lock`);
|
|
285
|
+
const started = Date.now();
|
|
286
|
+
let handle;
|
|
287
|
+
while (handle === undefined) {
|
|
288
|
+
try {
|
|
289
|
+
handle = await open(lockPath, 'wx', 0o600);
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
if (!errorHasCode(error, 'EEXIST'))
|
|
293
|
+
throw error;
|
|
294
|
+
if (Date.now() - started > 10_000) {
|
|
295
|
+
throw new FusionError(`timed out waiting for fusion model config lock: ${path}`, {
|
|
296
|
+
code: 'config_conflict',
|
|
297
|
+
childCreated: false,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
await delay(25);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
await handle.writeFile(`${String(process.pid)}\n`);
|
|
305
|
+
await handle.sync();
|
|
306
|
+
return await fn();
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
await handle.close();
|
|
310
|
+
await rm(lockPath, { force: true });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function prettyConfig(config) {
|
|
314
|
+
const sorted = {
|
|
315
|
+
schema_version: config.schema_version,
|
|
316
|
+
candidates: [...config.candidates],
|
|
317
|
+
evaluator: config.evaluator,
|
|
318
|
+
merger: config.merger,
|
|
319
|
+
};
|
|
320
|
+
return `${JSON.stringify(sorted, null, 2)}\n`;
|
|
321
|
+
}
|
|
322
|
+
function revisionsMatch(expected, current) {
|
|
323
|
+
if (expected.path !== current.path)
|
|
324
|
+
return false;
|
|
325
|
+
if (expected.exists !== current.exists)
|
|
326
|
+
return false;
|
|
327
|
+
return expected.sha256 === current.sha256;
|
|
328
|
+
}
|
|
329
|
+
export async function saveFusionModelConfig(path, config, expectedRevision) {
|
|
330
|
+
parseFusionModelConfig(config);
|
|
331
|
+
const dir = dirname(path);
|
|
332
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
333
|
+
await chmod(dir, 0o700);
|
|
334
|
+
return withConfigLock(path, async () => {
|
|
335
|
+
const current = await revisionForPath(path);
|
|
336
|
+
if (!revisionsMatch(expectedRevision, current)) {
|
|
337
|
+
throw new FusionError(`fusion model config changed on disk: ${path}`, {
|
|
338
|
+
code: 'config_conflict',
|
|
339
|
+
childCreated: false,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
await replaceFileDurable(path, prettyConfig(config));
|
|
343
|
+
return revisionForPath(path);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
export function describeFusionModelConfig(config) {
|
|
347
|
+
return keysOf(config).join(', ');
|
|
348
|
+
}
|
|
349
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../../src/core/fusion/config.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\nimport { chmod, mkdir, open, readFile, rm } from 'node:fs/promises';\nimport { basename, dirname, join } from 'node:path';\nimport { getAgentDir } from '@earendil-works/pi-coding-agent';\nimport type { Api, Model } from '@earendil-works/pi-ai';\nimport { isJsonObject, parseJsonText, type JsonObject } from '../common.js';\nimport { replaceFileDurable } from '../durable-fs.js';\nimport { CLAUDE_CODE_200K_SUBSCRIPTION_CONTEXT_WINDOW } from '../anthropic-attribution.js';\nimport { CURRENT_MODEL_SELECTION } from './facade-contract.js';\nimport {\n FUSION_MODEL_CONFIG_SCHEMA_VERSION,\n FusionError,\n type FusionModelConfigRevision,\n type FusionModelConfigV1,\n type FusionModelSelection,\n type FusionThinkingLevel,\n type LoadedFusionModelConfig,\n type ResolvedFusionModel,\n type ResolvedFusionModels,\n} from './types.js';\n\nexport const FUSION_MODEL_CONFIG_FILE = 'fusion-models.json';\nexport { CURRENT_MODEL_SELECTION } from './facade-contract.js';\n\nexport interface FusionModelRegistry {\n getAll(): Model<Api>[];\n getAvailable(): Model<Api>[];\n find?(provider: string, modelId: string): Model<Api> | undefined;\n isUsingOAuth?(model: Model<Api>): boolean;\n}\n\nexport interface ResolveFusionModelsInput {\n config: FusionModelConfigV1;\n modelRegistry: FusionModelRegistry;\n currentModel: Model<Api> | undefined;\n thinkingLevel: FusionThinkingLevel;\n}\n\nexport function defaultFusionModelConfig(): FusionModelConfigV1 {\n return {\n schema_version: FUSION_MODEL_CONFIG_SCHEMA_VERSION,\n candidates: [CURRENT_MODEL_SELECTION, CURRENT_MODEL_SELECTION, CURRENT_MODEL_SELECTION],\n evaluator: CURRENT_MODEL_SELECTION,\n merger: CURRENT_MODEL_SELECTION,\n };\n}\n\nexport function fusionModelConfigPath(agentDir = getAgentDir()): string {\n return join(agentDir, FUSION_MODEL_CONFIG_FILE);\n}\n\nfunction sha256Hex(bytes: Buffer): string {\n return createHash('sha256').update(bytes).digest('hex');\n}\n\nasync function revisionForPath(path: string): Promise<FusionModelConfigRevision> {\n try {\n const bytes = await readFile(path);\n return { path, exists: true, sha256: sha256Hex(bytes) };\n } catch (error) {\n if (errorHasCode(error, 'ENOENT')) return { path, exists: false, sha256: null };\n throw error;\n }\n}\n\nfunction errorHasCode(error: unknown, code: string): boolean {\n return isJsonObject(error) && error['code'] === code;\n}\n\nfunction keysOf(value: object): string[] {\n return Object.keys(value).sort();\n}\n\nfunction assertClosed(record: JsonObject, expected: readonly string[], label: string): void {\n const expectedSet = new Set(expected);\n for (const key of Object.keys(record)) {\n if (!expectedSet.has(key)) throw configError(`${label} contains unknown key ${key}`);\n }\n for (const key of expected) {\n if (!Object.prototype.hasOwnProperty.call(record, key)) {\n throw configError(`${label} is missing key ${key}`);\n }\n }\n}\n\nfunction configError(message: string): FusionError {\n return new FusionError(message, { code: 'config_invalid', childCreated: false });\n}\n\nfunction requireSelection(value: unknown, label: string): FusionModelSelection {\n if (typeof value !== 'string') throw configError(`${label} must be a string`);\n if (value === CURRENT_MODEL_SELECTION) return value;\n const trimmed = value.trim();\n if (trimmed.length === 0) throw configError(`${label} must not be blank`);\n if (trimmed !== value) throw configError(`${label} must not have surrounding whitespace`);\n if (!trimmed.includes('/')) throw configError(`${label} must be a qualified provider/model key`);\n return trimmed;\n}\n\nfunction requireCandidateSelections(\n value: unknown,\n): [FusionModelSelection, FusionModelSelection, FusionModelSelection] {\n if (!Array.isArray(value)) throw configError('candidates must be an array');\n if (value.length !== 3) throw configError('candidates must contain exactly three entries');\n const first = requireSelection(value[0], 'candidates[0]');\n const second = requireSelection(value[1], 'candidates[1]');\n const third = requireSelection(value[2], 'candidates[2]');\n return [first, second, third];\n}\n\nexport function parseFusionModelConfig(value: unknown): FusionModelConfigV1 {\n if (!isJsonObject(value) || Array.isArray(value))\n throw configError('fusion model config must be an object');\n const record: JsonObject = value;\n assertClosed(\n record,\n ['schema_version', 'candidates', 'evaluator', 'merger'],\n 'fusion model config',\n );\n if (record['schema_version'] !== FUSION_MODEL_CONFIG_SCHEMA_VERSION) {\n throw configError('fusion model config schema_version mismatch');\n }\n return {\n schema_version: FUSION_MODEL_CONFIG_SCHEMA_VERSION,\n candidates: requireCandidateSelections(record['candidates']),\n evaluator: requireSelection(record['evaluator'], 'evaluator'),\n merger: requireSelection(record['merger'], 'merger'),\n };\n}\n\nexport async function loadFusionModelConfig(\n path = fusionModelConfigPath(),\n): Promise<LoadedFusionModelConfig> {\n const revision = await revisionForPath(path);\n if (!revision.exists) return { config: defaultFusionModelConfig(), revision };\n let parsed: unknown;\n try {\n parsed = parseJsonText(await readFile(path, 'utf8'));\n } catch (error) {\n throw configError(\n `fusion model config is not valid JSON at ${path}: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n const config = parseFusionModelConfig(parsed);\n return { config, revision };\n}\n\nfunction qualifiedModelKey(model: Pick<Model<Api>, 'provider' | 'id'>): string {\n return `${model.provider}/${model.id}`;\n}\n\nfunction requireContextWindow(model: Model<Api>, label: string): number {\n const value = model.contextWindow;\n if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {\n throw new FusionError(`${label} has no positive context window`, {\n code: 'model_unavailable',\n childCreated: false,\n });\n }\n return Math.floor(value);\n}\n\nfunction transportContextWindow(model: Model<Api>, label: string): number {\n const advertised = requireContextWindow(model, label);\n return model.provider === 'anthropic'\n ? Math.min(advertised, CLAUDE_CODE_200K_SUBSCRIPTION_CONTEXT_WINDOW)\n : advertised;\n}\n\nfunction requireMaxOutputTokens(model: Model<Api>, label: string): number {\n const value = model.maxTokens;\n if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {\n throw new FusionError(`${label} has no positive maximum output token capacity`, {\n code: 'model_unavailable',\n childCreated: false,\n });\n }\n return Math.floor(value);\n}\n\nfunction modelIndex(models: readonly Model<Api>[]): Map<string, Model<Api>> {\n const out = new Map<string, Model<Api>>();\n for (const model of models) out.set(qualifiedModelKey(model), model);\n return out;\n}\n\nconst FRONTIER_MODEL_PATTERN =\n /(?:^|[-_/])(?:gpt|codex|claude|opus|sonnet|o[134](?:-[a-z0-9.]+)*)(?:[-_/]|$)/iu;\nconst TRUSTED_SUBSCRIPTION_ENDPOINTS = Object.freeze({\n anthropic: 'https://api.anthropic.com',\n 'openai-codex': 'https://chatgpt.com/backend-api',\n} as const);\nconst AUTH_HEADER_NAMES = new Set(['authorization', 'proxy-authorization', 'x-api-key', 'api-key']);\n\nfunction isKnownFrontierEndpoint(baseUrl: string | undefined): boolean {\n if (baseUrl === undefined || baseUrl.trim().length === 0) return false;\n try {\n const hostname = new URL(baseUrl).hostname.toLowerCase().replace(/\\.+$/u, '');\n return (\n hostname === 'api.openai.com' ||\n hostname === 'api.anthropic.com' ||\n hostname === 'openrouter.ai' ||\n hostname === 'chatgpt.com' ||\n hostname.endsWith('.openai.azure.com') ||\n hostname.endsWith('.cognitiveservices.azure.com') ||\n hostname.endsWith('.ai.azure.com')\n );\n } catch {\n return false;\n }\n}\n\nfunction assertTrustedSubscriptionEndpoint(\n model: Model<Api>,\n slotLabel: string,\n provider: keyof typeof TRUSTED_SUBSCRIPTION_ENDPOINTS,\n): void {\n const expectedText = TRUSTED_SUBSCRIPTION_ENDPOINTS[provider];\n const effectiveText = model.baseUrl?.trim() || expectedText;\n let effective: URL;\n try {\n effective = new URL(effectiveText);\n } catch {\n throw new FusionError(\n `${slotLabel} route ${model.provider}/${model.id} has a malformed subscription endpoint`,\n { code: 'model_unavailable', childCreated: false },\n );\n }\n const expected = new URL(expectedText);\n const effectivePath = effective.href.slice(effective.origin.length).replace(/\\/+$/u, '');\n const expectedPath = expected.href.slice(expected.origin.length).replace(/\\/+$/u, '');\n if (\n effective.protocol !== 'https:' ||\n effective.username !== '' ||\n effective.password !== '' ||\n effective.search !== '' ||\n effective.hash !== '' ||\n effective.origin !== expected.origin ||\n effectivePath !== expectedPath\n ) {\n throw new FusionError(\n `${slotLabel} route ${model.provider}/${model.id} does not use the trusted Pi subscription endpoint ${expectedText}`,\n { code: 'model_unavailable', childCreated: false },\n );\n }\n const unsafeHeader = Object.keys(model.headers ?? {}).find((name) =>\n AUTH_HEADER_NAMES.has(name.toLowerCase()),\n );\n if (unsafeHeader !== undefined) {\n throw new FusionError(\n `${slotLabel} route ${model.provider}/${model.id} overrides subscription authentication header ${unsafeHeader}`,\n { code: 'model_unavailable', childCreated: false },\n );\n }\n}\n\nfunction assertSubscriptionRoute(\n model: Model<Api>,\n slotLabel: string,\n registry: FusionModelRegistry,\n): void {\n const provider = model.provider.toLowerCase();\n const frontier =\n provider === 'openai' ||\n provider === 'openrouter' ||\n provider === 'anthropic' ||\n provider === 'openai-codex' ||\n provider.includes('azure') ||\n FRONTIER_MODEL_PATTERN.test(`${provider}/${model.id}`) ||\n isKnownFrontierEndpoint(model.baseUrl);\n if (!frontier) return;\n if (provider !== 'anthropic' && provider !== 'openai-codex') {\n throw new FusionError(\n `${slotLabel} route ${model.provider}/${model.id} is a frontier-model API channel; Fusion requires the Pi Anthropic or Codex subscription route`,\n { code: 'model_unavailable', childCreated: false },\n );\n }\n assertTrustedSubscriptionEndpoint(model, slotLabel, provider);\n if (registry.isUsingOAuth === undefined) {\n throw new FusionError(\n `${slotLabel} route ${model.provider}/${model.id} cannot be admitted because ModelRegistry OAuth observation is unavailable`,\n { code: 'model_unavailable', childCreated: false },\n );\n }\n if (!registry.isUsingOAuth(model)) {\n throw new FusionError(\n `${slotLabel} route ${model.provider}/${model.id} is not using subscription OAuth; metered API credentials are forbidden for Fusion`,\n { code: 'model_unavailable', childCreated: false },\n );\n }\n}\n\nfunction resolveSelection(\n selection: FusionModelSelection,\n slotLabel: string,\n availableByKey: Map<string, Model<Api>>,\n currentModel: Model<Api> | undefined,\n thinkingLevel: FusionThinkingLevel,\n registry: FusionModelRegistry,\n): ResolvedFusionModel {\n if (selection === CURRENT_MODEL_SELECTION) {\n if (currentModel === undefined) {\n throw new FusionError(`${slotLabel} uses $current but Pi has no current model`, {\n code: 'model_unavailable',\n childCreated: false,\n });\n }\n const qualifiedId = qualifiedModelKey(currentModel);\n const available = availableByKey.get(qualifiedId);\n if (available === undefined) {\n throw new FusionError(\n `${slotLabel} current model is not available to child Pi: ${qualifiedId}`,\n {\n code: 'model_unavailable',\n childCreated: false,\n },\n );\n }\n assertSubscriptionRoute(available, slotLabel, registry);\n return {\n selection,\n source: 'current',\n provider: available.provider,\n model: available.id,\n qualifiedId,\n thinkingLevel,\n contextWindow: transportContextWindow(available, slotLabel),\n maxOutputTokens: requireMaxOutputTokens(available, slotLabel),\n };\n }\n const model = availableByKey.get(selection);\n if (model === undefined) {\n throw new FusionError(`${slotLabel} configured model is unavailable: ${selection}`, {\n code: 'model_unavailable',\n childCreated: false,\n });\n }\n assertSubscriptionRoute(model, slotLabel, registry);\n return {\n selection,\n source: 'configured',\n provider: model.provider,\n model: model.id,\n qualifiedId: selection,\n thinkingLevel,\n contextWindow: transportContextWindow(model, slotLabel),\n maxOutputTokens: requireMaxOutputTokens(model, slotLabel),\n };\n}\n\nexport function resolveFusionModels(input: ResolveFusionModelsInput): ResolvedFusionModels {\n const availableByKey = modelIndex(input.modelRegistry.getAvailable());\n const [first, second, third] = input.config.candidates;\n const resolve = (selection: FusionModelSelection, slot: string): ResolvedFusionModel =>\n resolveSelection(\n selection,\n slot,\n availableByKey,\n input.currentModel,\n input.thinkingLevel,\n input.modelRegistry,\n );\n return {\n candidates: [\n resolve(first, 'candidate 1'),\n resolve(second, 'candidate 2'),\n resolve(third, 'candidate 3'),\n ],\n evaluator: resolve(input.config.evaluator, 'evaluator'),\n merger: resolve(input.config.merger, 'merger'),\n };\n}\n\nasync function delay(ms: number): Promise<void> {\n await new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nasync function withConfigLock<T>(path: string, fn: () => Promise<T>): Promise<T> {\n const dir = dirname(path);\n const lockPath = join(dir, `.${basename(path)}.lock`);\n const started = Date.now();\n let handle: Awaited<ReturnType<typeof open>> | undefined;\n while (handle === undefined) {\n try {\n handle = await open(lockPath, 'wx', 0o600);\n } catch (error) {\n if (!errorHasCode(error, 'EEXIST')) throw error;\n if (Date.now() - started > 10_000) {\n throw new FusionError(`timed out waiting for fusion model config lock: ${path}`, {\n code: 'config_conflict',\n childCreated: false,\n });\n }\n await delay(25);\n }\n }\n try {\n await handle.writeFile(`${String(process.pid)}\\n`);\n await handle.sync();\n return await fn();\n } finally {\n await handle.close();\n await rm(lockPath, { force: true });\n }\n}\n\nfunction prettyConfig(config: FusionModelConfigV1): string {\n const sorted = {\n schema_version: config.schema_version,\n candidates: [...config.candidates],\n evaluator: config.evaluator,\n merger: config.merger,\n };\n return `${JSON.stringify(sorted, null, 2)}\\n`;\n}\n\nfunction revisionsMatch(\n expected: FusionModelConfigRevision,\n current: FusionModelConfigRevision,\n): boolean {\n if (expected.path !== current.path) return false;\n if (expected.exists !== current.exists) return false;\n return expected.sha256 === current.sha256;\n}\n\nexport async function saveFusionModelConfig(\n path: string,\n config: FusionModelConfigV1,\n expectedRevision: FusionModelConfigRevision,\n): Promise<FusionModelConfigRevision> {\n parseFusionModelConfig(config);\n const dir = dirname(path);\n await mkdir(dir, { recursive: true, mode: 0o700 });\n await chmod(dir, 0o700);\n return withConfigLock(path, async () => {\n const current = await revisionForPath(path);\n if (!revisionsMatch(expectedRevision, current)) {\n throw new FusionError(`fusion model config changed on disk: ${path}`, {\n code: 'config_conflict',\n childCreated: false,\n });\n }\n await replaceFileDurable(path, prettyConfig(config));\n return revisionForPath(path);\n });\n}\n\nexport function describeFusionModelConfig(config: FusionModelConfigV1): string {\n return keysOf(config).join(', ');\n}\n"]}
|