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
|
@@ -24,6 +24,7 @@ export interface FusionModelRegistry {
|
|
|
24
24
|
getAll(): Model<Api>[];
|
|
25
25
|
getAvailable(): Model<Api>[];
|
|
26
26
|
find?(provider: string, modelId: string): Model<Api> | undefined;
|
|
27
|
+
isUsingOAuth?(model: Model<Api>): boolean;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export interface ResolveFusionModelsInput {
|
|
@@ -163,12 +164,119 @@ function modelIndex(models: readonly Model<Api>[]): Map<string, Model<Api>> {
|
|
|
163
164
|
return out;
|
|
164
165
|
}
|
|
165
166
|
|
|
167
|
+
const FRONTIER_MODEL_PATTERN =
|
|
168
|
+
/(?:^|[-_/])(?:gpt|codex|claude|opus|sonnet|o[134](?:-[a-z0-9.]+)*)(?:[-_/]|$)/iu;
|
|
169
|
+
const TRUSTED_SUBSCRIPTION_ENDPOINTS = Object.freeze({
|
|
170
|
+
anthropic: 'https://api.anthropic.com',
|
|
171
|
+
'openai-codex': 'https://chatgpt.com/backend-api',
|
|
172
|
+
} as const);
|
|
173
|
+
const AUTH_HEADER_NAMES = new Set(['authorization', 'proxy-authorization', 'x-api-key', 'api-key']);
|
|
174
|
+
|
|
175
|
+
function isKnownFrontierEndpoint(baseUrl: string | undefined): boolean {
|
|
176
|
+
if (baseUrl === undefined || baseUrl.trim().length === 0) return false;
|
|
177
|
+
try {
|
|
178
|
+
const hostname = new URL(baseUrl).hostname.toLowerCase().replace(/\.+$/u, '');
|
|
179
|
+
return (
|
|
180
|
+
hostname === 'api.openai.com' ||
|
|
181
|
+
hostname === 'api.anthropic.com' ||
|
|
182
|
+
hostname === 'openrouter.ai' ||
|
|
183
|
+
hostname === 'chatgpt.com' ||
|
|
184
|
+
hostname.endsWith('.openai.azure.com') ||
|
|
185
|
+
hostname.endsWith('.cognitiveservices.azure.com') ||
|
|
186
|
+
hostname.endsWith('.ai.azure.com')
|
|
187
|
+
);
|
|
188
|
+
} catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function assertTrustedSubscriptionEndpoint(
|
|
194
|
+
model: Model<Api>,
|
|
195
|
+
slotLabel: string,
|
|
196
|
+
provider: keyof typeof TRUSTED_SUBSCRIPTION_ENDPOINTS,
|
|
197
|
+
): void {
|
|
198
|
+
const expectedText = TRUSTED_SUBSCRIPTION_ENDPOINTS[provider];
|
|
199
|
+
const effectiveText = model.baseUrl?.trim() || expectedText;
|
|
200
|
+
let effective: URL;
|
|
201
|
+
try {
|
|
202
|
+
effective = new URL(effectiveText);
|
|
203
|
+
} catch {
|
|
204
|
+
throw new FusionError(
|
|
205
|
+
`${slotLabel} route ${model.provider}/${model.id} has a malformed subscription endpoint`,
|
|
206
|
+
{ code: 'model_unavailable', childCreated: false },
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
const expected = new URL(expectedText);
|
|
210
|
+
const effectivePath = effective.href.slice(effective.origin.length).replace(/\/+$/u, '');
|
|
211
|
+
const expectedPath = expected.href.slice(expected.origin.length).replace(/\/+$/u, '');
|
|
212
|
+
if (
|
|
213
|
+
effective.protocol !== 'https:' ||
|
|
214
|
+
effective.username !== '' ||
|
|
215
|
+
effective.password !== '' ||
|
|
216
|
+
effective.search !== '' ||
|
|
217
|
+
effective.hash !== '' ||
|
|
218
|
+
effective.origin !== expected.origin ||
|
|
219
|
+
effectivePath !== expectedPath
|
|
220
|
+
) {
|
|
221
|
+
throw new FusionError(
|
|
222
|
+
`${slotLabel} route ${model.provider}/${model.id} does not use the trusted Pi subscription endpoint ${expectedText}`,
|
|
223
|
+
{ code: 'model_unavailable', childCreated: false },
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
const unsafeHeader = Object.keys(model.headers ?? {}).find((name) =>
|
|
227
|
+
AUTH_HEADER_NAMES.has(name.toLowerCase()),
|
|
228
|
+
);
|
|
229
|
+
if (unsafeHeader !== undefined) {
|
|
230
|
+
throw new FusionError(
|
|
231
|
+
`${slotLabel} route ${model.provider}/${model.id} overrides subscription authentication header ${unsafeHeader}`,
|
|
232
|
+
{ code: 'model_unavailable', childCreated: false },
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function assertSubscriptionRoute(
|
|
238
|
+
model: Model<Api>,
|
|
239
|
+
slotLabel: string,
|
|
240
|
+
registry: FusionModelRegistry,
|
|
241
|
+
): void {
|
|
242
|
+
const provider = model.provider.toLowerCase();
|
|
243
|
+
const frontier =
|
|
244
|
+
provider === 'openai' ||
|
|
245
|
+
provider === 'openrouter' ||
|
|
246
|
+
provider === 'anthropic' ||
|
|
247
|
+
provider === 'openai-codex' ||
|
|
248
|
+
provider.includes('azure') ||
|
|
249
|
+
FRONTIER_MODEL_PATTERN.test(`${provider}/${model.id}`) ||
|
|
250
|
+
isKnownFrontierEndpoint(model.baseUrl);
|
|
251
|
+
if (!frontier) return;
|
|
252
|
+
if (provider !== 'anthropic' && provider !== 'openai-codex') {
|
|
253
|
+
throw new FusionError(
|
|
254
|
+
`${slotLabel} route ${model.provider}/${model.id} is a frontier-model API channel; Fusion requires the Pi Anthropic or Codex subscription route`,
|
|
255
|
+
{ code: 'model_unavailable', childCreated: false },
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
assertTrustedSubscriptionEndpoint(model, slotLabel, provider);
|
|
259
|
+
if (registry.isUsingOAuth === undefined) {
|
|
260
|
+
throw new FusionError(
|
|
261
|
+
`${slotLabel} route ${model.provider}/${model.id} cannot be admitted because ModelRegistry OAuth observation is unavailable`,
|
|
262
|
+
{ code: 'model_unavailable', childCreated: false },
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
if (!registry.isUsingOAuth(model)) {
|
|
266
|
+
throw new FusionError(
|
|
267
|
+
`${slotLabel} route ${model.provider}/${model.id} is not using subscription OAuth; metered API credentials are forbidden for Fusion`,
|
|
268
|
+
{ code: 'model_unavailable', childCreated: false },
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
166
273
|
function resolveSelection(
|
|
167
274
|
selection: FusionModelSelection,
|
|
168
275
|
slotLabel: string,
|
|
169
276
|
availableByKey: Map<string, Model<Api>>,
|
|
170
277
|
currentModel: Model<Api> | undefined,
|
|
171
278
|
thinkingLevel: FusionThinkingLevel,
|
|
279
|
+
registry: FusionModelRegistry,
|
|
172
280
|
): ResolvedFusionModel {
|
|
173
281
|
if (selection === CURRENT_MODEL_SELECTION) {
|
|
174
282
|
if (currentModel === undefined) {
|
|
@@ -188,6 +296,7 @@ function resolveSelection(
|
|
|
188
296
|
},
|
|
189
297
|
);
|
|
190
298
|
}
|
|
299
|
+
assertSubscriptionRoute(available, slotLabel, registry);
|
|
191
300
|
return {
|
|
192
301
|
selection,
|
|
193
302
|
source: 'current',
|
|
@@ -205,6 +314,7 @@ function resolveSelection(
|
|
|
205
314
|
childCreated: false,
|
|
206
315
|
});
|
|
207
316
|
}
|
|
317
|
+
assertSubscriptionRoute(model, slotLabel, registry);
|
|
208
318
|
return {
|
|
209
319
|
selection,
|
|
210
320
|
source: 'configured',
|
|
@@ -219,44 +329,23 @@ function resolveSelection(
|
|
|
219
329
|
export function resolveFusionModels(input: ResolveFusionModelsInput): ResolvedFusionModels {
|
|
220
330
|
const availableByKey = modelIndex(input.modelRegistry.getAvailable());
|
|
221
331
|
const [first, second, third] = input.config.candidates;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
'candidate 1',
|
|
227
|
-
availableByKey,
|
|
228
|
-
input.currentModel,
|
|
229
|
-
input.thinkingLevel,
|
|
230
|
-
),
|
|
231
|
-
resolveSelection(
|
|
232
|
-
second,
|
|
233
|
-
'candidate 2',
|
|
234
|
-
availableByKey,
|
|
235
|
-
input.currentModel,
|
|
236
|
-
input.thinkingLevel,
|
|
237
|
-
),
|
|
238
|
-
resolveSelection(
|
|
239
|
-
third,
|
|
240
|
-
'candidate 3',
|
|
241
|
-
availableByKey,
|
|
242
|
-
input.currentModel,
|
|
243
|
-
input.thinkingLevel,
|
|
244
|
-
),
|
|
245
|
-
],
|
|
246
|
-
evaluator: resolveSelection(
|
|
247
|
-
input.config.evaluator,
|
|
248
|
-
'evaluator',
|
|
249
|
-
availableByKey,
|
|
250
|
-
input.currentModel,
|
|
251
|
-
input.thinkingLevel,
|
|
252
|
-
),
|
|
253
|
-
merger: resolveSelection(
|
|
254
|
-
input.config.merger,
|
|
255
|
-
'merger',
|
|
332
|
+
const resolve = (selection: FusionModelSelection, slot: string): ResolvedFusionModel =>
|
|
333
|
+
resolveSelection(
|
|
334
|
+
selection,
|
|
335
|
+
slot,
|
|
256
336
|
availableByKey,
|
|
257
337
|
input.currentModel,
|
|
258
338
|
input.thinkingLevel,
|
|
259
|
-
|
|
339
|
+
input.modelRegistry,
|
|
340
|
+
);
|
|
341
|
+
return {
|
|
342
|
+
candidates: [
|
|
343
|
+
resolve(first, 'candidate 1'),
|
|
344
|
+
resolve(second, 'candidate 2'),
|
|
345
|
+
resolve(third, 'candidate 3'),
|
|
346
|
+
],
|
|
347
|
+
evaluator: resolve(input.config.evaluator, 'evaluator'),
|
|
348
|
+
merger: resolve(input.config.merger, 'merger'),
|
|
260
349
|
};
|
|
261
350
|
}
|
|
262
351
|
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type ReadonlyParentSessionManager,
|
|
14
14
|
} from '../context/parent-snapshot.js';
|
|
15
15
|
import type { Message } from '@earendil-works/pi-ai';
|
|
16
|
+
import { FUSION_REASON_TOOL_NAME } from './workflows.js';
|
|
16
17
|
import {
|
|
17
18
|
FUSION_BRANCH_FILTER_ID,
|
|
18
19
|
FUSION_COMMAND_CONTEXT_POLICY_ID,
|
|
@@ -22,8 +23,9 @@ import {
|
|
|
22
23
|
FUSION_TOOL_CONTEXT_POLICY_ID,
|
|
23
24
|
FusionError,
|
|
24
25
|
type FusionBranchFilterDescriptor,
|
|
25
|
-
type FusionCanonicalInputV3,
|
|
26
26
|
type FusionCanonicalRequestV3,
|
|
27
|
+
type FusionSessionProjectionCanonicalInputV5,
|
|
28
|
+
type FusionWorkflowId,
|
|
27
29
|
type FusionContextOmissionLedgerV2,
|
|
28
30
|
type FusionContextPolicyDescriptor,
|
|
29
31
|
type FusionConversationProjectionV3,
|
|
@@ -33,7 +35,17 @@ import {
|
|
|
33
35
|
type FusionSource,
|
|
34
36
|
} from './types.js';
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Re-exported from the workflow registry, which owns every workflow's tool name.
|
|
40
|
+
* Kept here so existing importers of this module keep working unchanged.
|
|
41
|
+
*/
|
|
42
|
+
export {
|
|
43
|
+
FUSION_BRAINSTORM_TOOL_NAME,
|
|
44
|
+
FUSION_REASON_TOOL_NAME,
|
|
45
|
+
FUSION_INVESTIGATE_TOOL_NAME,
|
|
46
|
+
FUSION_RESEARCH_TOOL_NAME,
|
|
47
|
+
FUSION_VALIDATE_TOOL_NAME,
|
|
48
|
+
} from './workflows.js';
|
|
37
49
|
|
|
38
50
|
/** Retained for source compatibility; Fusion's session access is the shared adapter. */
|
|
39
51
|
export type FusionReadonlySessionManager = ReadonlyParentSessionManager;
|
|
@@ -44,10 +56,11 @@ export interface BuildFusionCanonicalInputOptions {
|
|
|
44
56
|
request: string;
|
|
45
57
|
toolCallId?: string;
|
|
46
58
|
toolName?: string;
|
|
59
|
+
workflow?: FusionWorkflowId;
|
|
47
60
|
}
|
|
48
61
|
|
|
49
62
|
export interface BuiltFusionCanonicalInput {
|
|
50
|
-
input:
|
|
63
|
+
input: FusionSessionProjectionCanonicalInputV5;
|
|
51
64
|
serialized: string;
|
|
52
65
|
ledger: FusionContextOmissionLedgerV2;
|
|
53
66
|
transcriptLeafId: string | null;
|
|
@@ -202,7 +215,14 @@ export function buildFusionCanonicalInput(
|
|
|
202
215
|
childCreated: false,
|
|
203
216
|
});
|
|
204
217
|
}
|
|
205
|
-
const
|
|
218
|
+
const workflow = options.workflow ?? 'reason';
|
|
219
|
+
if (workflow !== 'reason') {
|
|
220
|
+
throw new FusionError('parent session projection is available only to the reason workflow', {
|
|
221
|
+
code: 'context_capture_failed',
|
|
222
|
+
childCreated: false,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
const toolName = options.toolName ?? FUSION_REASON_TOOL_NAME;
|
|
206
226
|
const snapshotOptions: ParentSnapshotOptions = {
|
|
207
227
|
toolName,
|
|
208
228
|
excludeActiveToolCallLeaf: options.source === 'tool',
|
|
@@ -222,12 +242,19 @@ export function buildFusionCanonicalInput(
|
|
|
222
242
|
text: options.request,
|
|
223
243
|
sha256: sha256Text(options.request),
|
|
224
244
|
};
|
|
225
|
-
const input:
|
|
245
|
+
const input: FusionSessionProjectionCanonicalInputV5 = {
|
|
226
246
|
schema_version: FUSION_INPUT_SCHEMA_VERSION,
|
|
247
|
+
workflow: 'reason',
|
|
227
248
|
cwd: ctx.cwd,
|
|
228
|
-
system_prompt: ctx.getSystemPrompt(),
|
|
229
249
|
request,
|
|
250
|
+
system_prompt: ctx.getSystemPrompt(),
|
|
230
251
|
conversation_projection: projected.projection,
|
|
252
|
+
context: {
|
|
253
|
+
kind: 'session_projection',
|
|
254
|
+
policy_id: 'fusion-session-projection-v1',
|
|
255
|
+
system_prompt: ctx.getSystemPrompt(),
|
|
256
|
+
conversation_projection: projected.projection,
|
|
257
|
+
},
|
|
231
258
|
};
|
|
232
259
|
return {
|
|
233
260
|
input,
|