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.
Files changed (72) hide show
  1. package/BACKGROUND-TASKS-INSTRUCTIONS.md +63 -0
  2. package/PUBLISHING.md +43 -29
  3. package/README.md +234 -385
  4. package/TESTING.md +15 -9
  5. package/TEST_PLAN.md +46 -13
  6. package/docs/INDEX.md +157 -0
  7. package/docs/api/eventbus-v1.md +166 -0
  8. package/docs/assets/architecture.svg +78 -0
  9. package/docs/assets/footer-dock.svg +47 -0
  10. package/docs/assets/logo.svg +49 -0
  11. package/docs/attestations.json +189 -0
  12. package/docs/choose-a-workflow.md +98 -0
  13. package/docs/commands/bg-clear.md +70 -0
  14. package/docs/commands/bg-update.md +82 -0
  15. package/docs/commands/bg.md +90 -0
  16. package/docs/commands/fusion-models.md +70 -0
  17. package/docs/commands/fusion.md +69 -0
  18. package/docs/commands/jobs.md +74 -0
  19. package/docs/commands/kill.md +82 -0
  20. package/docs/commands/logs.md +90 -0
  21. package/docs/commands/task-manager.md +109 -0
  22. package/docs/concepts/completion-delivery.md +66 -0
  23. package/docs/concepts/context-projection-and-budgeting.md +79 -0
  24. package/docs/getting-started.md +122 -0
  25. package/docs/manifest.json +1825 -0
  26. package/docs/operations/configuration.md +110 -0
  27. package/docs/operations/releasing.md +67 -0
  28. package/docs/operations/testing.md +101 -0
  29. package/docs/operations/troubleshooting.md +38 -0
  30. package/docs/read-before-edit.md +94 -0
  31. package/docs/reference/runtime-contracts.md +213 -0
  32. package/docs/reference/shortcuts-and-dock.md +70 -0
  33. package/docs/subsystems/attested-pi-runs.md +141 -0
  34. package/docs/subsystems/background-task-runtime.md +85 -0
  35. package/docs/subsystems/child-launch-durability-and-safety.md +57 -0
  36. package/docs/subsystems/delegation.md +190 -0
  37. package/docs/subsystems/docs-freshness-gate.md +26 -0
  38. package/docs/subsystems/fusion.md +121 -0
  39. package/docs/subsystems/host-ui-and-telemetry.md +83 -0
  40. package/docs/tools/bg_delegate.md +193 -0
  41. package/docs/tools/bg_kill.md +114 -0
  42. package/docs/tools/bg_logs.md +133 -0
  43. package/docs/tools/bg_result.md +120 -0
  44. package/docs/tools/bg_run.md +168 -0
  45. package/docs/tools/bg_run_pi_attested.md +170 -0
  46. package/docs/tools/bg_status.md +111 -0
  47. package/docs/tools/fusion_investigate.md +116 -0
  48. package/docs/tools/fusion_reason.md +75 -0
  49. package/docs/tools/fusion_research.md +162 -0
  50. package/docs/tools/fusion_validate.md +206 -0
  51. package/logo.png +0 -0
  52. package/package.json +29 -6
  53. package/src/core/delegate/budget.ts +1 -1
  54. package/src/core/delegate/launch.ts +6 -0
  55. package/src/core/fusion/artifacts.ts +80 -5
  56. package/src/core/fusion/budget.ts +129 -28
  57. package/src/core/fusion/child-protocol.ts +82 -0
  58. package/src/core/fusion/clean-context.ts +91 -0
  59. package/src/core/fusion/config.ts +124 -35
  60. package/src/core/fusion/context.ts +33 -6
  61. package/src/core/fusion/evaluation.ts +392 -15
  62. package/src/core/fusion/orchestrator.ts +274 -25
  63. package/src/core/fusion/pi-child.ts +635 -10
  64. package/src/core/fusion/prompts.ts +167 -6
  65. package/src/core/fusion/source-policy.ts +257 -0
  66. package/src/core/fusion/types.ts +232 -5
  67. package/src/core/fusion/web-fetch.ts +993 -0
  68. package/src/core/fusion/workflows.ts +184 -0
  69. package/src/extension.ts +3 -3
  70. package/src/fusion-child-extension.ts +370 -54
  71. package/src/fusion-extension.ts +625 -125
  72. 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
- return {
223
- candidates: [
224
- resolveSelection(
225
- first,
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
- export const FUSION_BRAINSTORM_TOOL_NAME = 'fusion_brainstorm';
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: FusionCanonicalInputV3;
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 toolName = options.toolName ?? FUSION_BRAINSTORM_TOOL_NAME;
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: FusionCanonicalInputV3 = {
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,