@pi-unipi/background-tasks 2.6.2 → 2.6.4

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 (59) hide show
  1. package/package.json +3 -2
  2. package/src/tools.ts +3 -1
  3. package/src/__tests__/anthropic-attribution.test.ts +0 -195
  4. package/src/__tests__/config.test.ts +0 -137
  5. package/src/__tests__/core.test.ts +0 -493
  6. package/src/__tests__/delegate-artifacts.test.ts +0 -528
  7. package/src/__tests__/delegate-budget.test.ts +0 -456
  8. package/src/__tests__/delegate-launch.test.ts +0 -676
  9. package/src/__tests__/delegate-result-package.test.ts +0 -350
  10. package/src/__tests__/delegate-seed.test.ts +0 -392
  11. package/src/__tests__/durable-fs.test.ts +0 -559
  12. package/src/__tests__/extension-api.test.ts +0 -579
  13. package/src/__tests__/fusion-artifacts.test.ts +0 -1039
  14. package/src/__tests__/fusion-budget.test.ts +0 -1356
  15. package/src/__tests__/fusion-claude-cache.test.ts +0 -320
  16. package/src/__tests__/fusion-config.test.ts +0 -335
  17. package/src/__tests__/fusion-context-prompts.test.ts +0 -670
  18. package/src/__tests__/fusion-evaluation.test.ts +0 -315
  19. package/src/__tests__/fusion-extraction-equivalence.test.ts +0 -58
  20. package/src/__tests__/fusion-golden-bytes.test.ts +0 -35
  21. package/src/__tests__/fusion-high-cardinality.test.ts +0 -192
  22. package/src/__tests__/fusion-model-selector.test.ts +0 -205
  23. package/src/__tests__/fusion-orchestrator.test.ts +0 -1194
  24. package/src/__tests__/fusion-rpc.test.ts +0 -369
  25. package/src/__tests__/fusion-sdk.test.ts +0 -1226
  26. package/src/__tests__/fusion-v5-core.test.ts +0 -219
  27. package/src/__tests__/fusion-validate-orchestrator.test.ts +0 -240
  28. package/src/__tests__/fusion-web-fetch.test.ts +0 -485
  29. package/src/__tests__/fusion-workflows.test.ts +0 -59
  30. package/src/__tests__/helpers/delegate-deterministic-seed.ts +0 -109
  31. package/src/__tests__/helpers/delegate-seed-subprocess.ts +0 -10
  32. package/src/__tests__/helpers/fusion-canonical-subprocess.ts +0 -21
  33. package/src/__tests__/helpers/fusion-canonical.ts +0 -140
  34. package/src/__tests__/helpers/fusion-fake-pi.ts +0 -279
  35. package/src/__tests__/helpers/fusion-golden-corpus.ts +0 -500
  36. package/src/__tests__/helpers/fusion-high-cardinality.ts +0 -140
  37. package/src/__tests__/helpers/normalize.ts +0 -22
  38. package/src/__tests__/helpers/pi-hook-contract-evidence.json +0 -18
  39. package/src/__tests__/pi-launch.test.ts +0 -202
  40. package/src/__tests__/registry.test.ts +0 -1580
  41. package/src/__tests__/scripted-provider/delegate-ambient-provider.test.ts +0 -130
  42. package/src/__tests__/scripted-provider/delegate-child-guard.test.ts +0 -631
  43. package/src/__tests__/scripted-provider/delegate-guard-provider.ts +0 -403
  44. package/src/__tests__/scripted-provider/follow-up.test.ts +0 -448
  45. package/src/__tests__/scripted-provider/fusion-output-recovery.test.ts +0 -132
  46. package/src/__tests__/scripted-provider/fusion-reason.test.ts +0 -310
  47. package/src/__tests__/scripted-provider/fusion-runtime-guard.test.ts +0 -163
  48. package/src/__tests__/scripted-provider/hook-contract-provider.ts +0 -179
  49. package/src/__tests__/scripted-provider/hook-probe-a.ts +0 -3
  50. package/src/__tests__/scripted-provider/hook-probe-b.ts +0 -3
  51. package/src/__tests__/scripted-provider/hook-probe-extension.ts +0 -126
  52. package/src/__tests__/scripted-provider/output-recovery-provider.ts +0 -153
  53. package/src/__tests__/scripted-provider/pi-hook-contract-evidence.json +0 -18
  54. package/src/__tests__/scripted-provider/pi-hook-contract.test.ts +0 -477
  55. package/src/__tests__/scripted-provider/runtime-guard-probe.ts +0 -28
  56. package/src/__tests__/scripted-provider/runtime-guard-provider.ts +0 -49
  57. package/src/__tests__/scripted-provider/scripted-provider-extension.ts +0 -408
  58. package/src/__tests__/task-manager.test.ts +0 -479
  59. package/src/__tests__/windows-taskkill.test.ts +0 -161
@@ -1,408 +0,0 @@
1
- import { appendFileSync } from 'node:fs';
2
- import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
3
- import {
4
- createAssistantMessageEventStream,
5
- type AssistantMessage,
6
- type AssistantMessageEventStream,
7
- type Context,
8
- type Model,
9
- type Api,
10
- type Message,
11
- type TextContent,
12
- type ToolCall,
13
- } from '@earendil-works/pi-ai';
14
-
15
- const PROVIDER = 'pi-bg-scripted';
16
- const MODEL_ID = 'scripted-model';
17
- const API = 'pi-bg-scripted-api';
18
- const DEFAULT_USAGE = {
19
- input: 10,
20
- output: 5,
21
- cacheRead: 0,
22
- cacheWrite: 0,
23
- totalTokens: 15,
24
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
25
- };
26
-
27
- type Scenario =
28
- | 'bg-run-follow-up'
29
- | 'notify-false'
30
- | 'wake-false'
31
- | 'failed-follow-up'
32
- | 'display-only-bg'
33
- | 'json-tool-telemetry'
34
- | 'fusion-reason';
35
- type ScriptedStopReason = 'stop' | 'length' | 'toolUse';
36
-
37
- type JsonObject = Record<PropertyKey, unknown>;
38
-
39
- interface ScriptedToolCall extends Omit<ToolCall, 'arguments'> {
40
- arguments: JsonObject;
41
- }
42
-
43
- interface ScriptedAssistantMessage extends Omit<AssistantMessage, 'content' | 'stopReason'> {
44
- content: ScriptedBlock[];
45
- stopReason: ScriptedStopReason;
46
- }
47
-
48
- type ScriptedBlock = TextContent | ScriptedToolCall;
49
-
50
- function parseScenario(value: string | undefined): Scenario {
51
- if (
52
- value === 'bg-run-follow-up' ||
53
- value === 'notify-false' ||
54
- value === 'wake-false' ||
55
- value === 'failed-follow-up' ||
56
- value === 'display-only-bg' ||
57
- value === 'json-tool-telemetry' ||
58
- value === 'fusion-reason'
59
- )
60
- return value;
61
- return 'bg-run-follow-up';
62
- }
63
-
64
- function record(event: JsonObject): void {
65
- const path = process.env['UNIPI_BG_SCRIPTED_EVENTS'];
66
- if (!path) return;
67
- appendFileSync(path, `${JSON.stringify({ ...event, timestamp: Date.now() })}\n`, 'utf8');
68
- }
69
-
70
- function text(value: string): TextContent {
71
- return { type: 'text', text: value };
72
- }
73
-
74
- function toolCall(name: string, args: JsonObject, id: string): ScriptedToolCall {
75
- return { type: 'toolCall', id, name, arguments: args };
76
- }
77
-
78
- function assistant(
79
- content: ScriptedBlock[],
80
- stopReason: ScriptedStopReason,
81
- ): ScriptedAssistantMessage {
82
- return {
83
- role: 'assistant',
84
- content,
85
- api: API,
86
- provider: PROVIDER,
87
- model: MODEL_ID,
88
- usage: DEFAULT_USAGE,
89
- stopReason,
90
- timestamp: Date.now(),
91
- };
92
- }
93
-
94
- function shellNode(script: string): string {
95
- return `node -e ${JSON.stringify(script)}`;
96
- }
97
-
98
- function messageText(message: Message): string {
99
- return typeof message.content === 'string'
100
- ? message.content
101
- : Array.isArray(message.content)
102
- ? message.content.map((part) => ('text' in part ? part.text : part.type)).join(' ')
103
- : '';
104
- }
105
-
106
- function summarizeMessage(message: Message): string {
107
- const customType =
108
- 'customType' in message && typeof message.customType === 'string'
109
- ? message.customType
110
- : undefined;
111
- const toolName =
112
- 'toolName' in message && typeof message.toolName === 'string' ? message.toolName : undefined;
113
- return [message.role, customType, toolName, messageText(message)]
114
- .filter(Boolean)
115
- .join(':')
116
- .slice(0, 500);
117
- }
118
-
119
- interface EventDrivenContractCheck {
120
- readonly systemPrompt: boolean;
121
- readonly toolDescriptions: boolean;
122
- readonly launchReceipt: boolean;
123
- }
124
-
125
- function inspectEventDrivenContract(context: Context): EventDrivenContractCheck {
126
- const systemPrompt = context.systemPrompt ?? '';
127
- const toolDescription = (name: string): string =>
128
- context.tools?.find((tool) => tool.name === name)?.description ?? '';
129
- const bgRunResult = context.messages
130
- .filter(
131
- (message) =>
132
- message.role === 'toolResult' && 'toolName' in message && message.toolName === 'bg_run',
133
- )
134
- .map(messageText)
135
- .at(-1);
136
- return {
137
- systemPrompt:
138
- systemPrompt.includes('Do not call sleep, bg_status, or bg_logs merely to wait') &&
139
- systemPrompt.includes('automatically starts a follow-up agent turn') &&
140
- systemPrompt.includes('A running result is not an instruction to poll again') &&
141
- !systemPrompt.includes('After bg_run, use bg_status and bg_logs to inspect progress'),
142
- toolDescriptions:
143
- toolDescription('bg_run').includes('do not sleep or poll merely to wait') &&
144
- toolDescription('bg_status').includes('not a waiting primitive') &&
145
- toolDescription('bg_logs').includes('not a waiting primitive'),
146
- launchReceipt:
147
- bgRunResult?.includes('Terminal notification: enabled.') === true &&
148
- bgRunResult.includes('Automatic follow-up turn: enabled.') &&
149
- bgRunResult.includes('Next action: do not poll or sleep'),
150
- };
151
- }
152
-
153
- function responseFor(
154
- scenario: Scenario,
155
- callCount: number,
156
- contract: EventDrivenContractCheck,
157
- context: Context,
158
- ): ScriptedAssistantMessage {
159
- if (scenario === 'fusion-reason') {
160
- if (callCount === 1) {
161
- return assistant(
162
- [toolCall('fusion_reason', { prompt: 'scripted fusion prompt' }, 'call-fusion-reason')],
163
- 'toolUse',
164
- );
165
- }
166
- if (callCount === 2) {
167
- return assistant(
168
- [text('Fusion launched; waiting for its terminal notification without polling.')],
169
- 'stop',
170
- );
171
- }
172
- if (callCount === 3) {
173
- const transcript = context.messages.map(messageText).join('\n');
174
- const match =
175
- /<task-id>((?:reason|investigate|research|validate)-[0-9a-f]{32})<\/task-id>/u.exec(
176
- transcript,
177
- );
178
- if (match?.[1] === undefined) {
179
- return assistant([text('Fusion terminal notification did not contain a task id.')], 'stop');
180
- }
181
- return assistant(
182
- [toolCall('bg_result', { taskId: match[1], delivery: 'inline' }, 'call-fusion-result')],
183
- 'toolUse',
184
- );
185
- }
186
- return assistant([text('Parent observed verified Fusion result from bg_result.')], 'stop');
187
- }
188
-
189
- if (scenario === 'json-tool-telemetry') {
190
- if (callCount === 1) {
191
- return assistant(
192
- [
193
- toolCall('scripted_echo', { value: 'ok' }, 'call-scripted-ok'),
194
- toolCall('scripted_echo', { value: 'fail', fail: true }, 'call-scripted-fail'),
195
- ],
196
- 'toolUse',
197
- );
198
- }
199
- return assistant([text('JSON tool telemetry complete.')], 'stop');
200
- }
201
-
202
- if (scenario === 'bg-run-follow-up') {
203
- if (callCount === 1) {
204
- return assistant(
205
- [
206
- toolCall(
207
- 'bg_run',
208
- {
209
- name: 'Scripted Wakeup',
210
- command: shellNode(
211
- "setTimeout(() => { console.log('scripted wakeup done'); }, 150);",
212
- ),
213
- isAgent: false,
214
- notifyOnCompletion: true,
215
- },
216
- 'call-bg-run-wakeup',
217
- ),
218
- ],
219
- 'toolUse',
220
- );
221
- }
222
- if (callCount === 2) {
223
- if (!contract.systemPrompt || !contract.toolDescriptions || !contract.launchReceipt) {
224
- return assistant([toolCall('bg_status', {}, 'call-bug-181-regressive-poll')], 'toolUse');
225
- }
226
- return assistant(
227
- [text('Initial bg_run tool turn yielded without polling for the terminal event.')],
228
- 'stop',
229
- );
230
- }
231
- return assistant(
232
- [text('Follow-up turn observed background-task-notification for Scripted Wakeup.')],
233
- 'stop',
234
- );
235
- }
236
-
237
- if (scenario === 'notify-false') {
238
- if (callCount === 1) {
239
- return assistant(
240
- [
241
- toolCall(
242
- 'bg_run',
243
- {
244
- name: 'No Notify Scripted',
245
- command: shellNode("setTimeout(() => { console.log('quiet done'); }, 80);"),
246
- isAgent: false,
247
- notifyOnCompletion: false,
248
- triggerOnCompletion: true,
249
- },
250
- 'call-bg-run-no-notify',
251
- ),
252
- ],
253
- 'toolUse',
254
- );
255
- }
256
- return assistant([text('No-notify initial turn finished.')], 'stop');
257
- }
258
-
259
- if (scenario === 'wake-false') {
260
- if (callCount === 1) {
261
- return assistant(
262
- [
263
- toolCall(
264
- 'bg_run',
265
- {
266
- name: 'No Wake Scripted',
267
- command: shellNode("setTimeout(() => { console.log('notify only done'); }, 80);"),
268
- isAgent: false,
269
- notifyOnCompletion: true,
270
- triggerOnCompletion: false,
271
- },
272
- 'call-bg-run-no-wake',
273
- ),
274
- ],
275
- 'toolUse',
276
- );
277
- }
278
- return assistant([text('Notification-only initial turn finished.')], 'stop');
279
- }
280
-
281
- if (scenario === 'failed-follow-up') {
282
- if (callCount === 1) {
283
- return assistant(
284
- [
285
- toolCall(
286
- 'bg_run',
287
- {
288
- name: 'Failing Scripted',
289
- command: shellNode(
290
- "setTimeout(() => { console.error('scripted failure'); process.exit(7); }, 80);",
291
- ),
292
- isAgent: false,
293
- notifyOnCompletion: true,
294
- },
295
- 'call-bg-run-failed',
296
- ),
297
- ],
298
- 'toolUse',
299
- );
300
- }
301
- if (callCount === 2) return assistant([text('Failing task initial turn finished.')], 'stop');
302
- return assistant(
303
- [text('Follow-up turn observed failed background task notification.')],
304
- 'stop',
305
- );
306
- }
307
-
308
- return assistant([text(`Display-only scenario provider call ${String(callCount)}.`)], 'stop');
309
- }
310
-
311
- function pushMessage(stream: AssistantMessageEventStream, message: ScriptedAssistantMessage): void {
312
- const partial: AssistantMessage = { ...message, content: [] };
313
- stream.push({ type: 'start', partial: { ...partial } });
314
- message.content.forEach((block, contentIndex) => {
315
- if (block.type === 'text') {
316
- const partialText: TextContent = { type: 'text', text: '' };
317
- partial.content = [...partial.content, partialText];
318
- stream.push({ type: 'text_start', contentIndex, partial: { ...partial } });
319
- partialText.text = block.text;
320
- stream.push({ type: 'text_delta', contentIndex, delta: block.text, partial: { ...partial } });
321
- stream.push({ type: 'text_end', contentIndex, content: block.text, partial: { ...partial } });
322
- return;
323
- }
324
-
325
- const partialToolCall: ToolCall = {
326
- type: 'toolCall',
327
- id: block.id,
328
- name: block.name,
329
- arguments: {},
330
- };
331
- partial.content = [...partial.content, partialToolCall];
332
- stream.push({ type: 'toolcall_start', contentIndex, partial: { ...partial } });
333
- const json = JSON.stringify(block.arguments);
334
- stream.push({ type: 'toolcall_delta', contentIndex, delta: json, partial: { ...partial } });
335
- partialToolCall.arguments = block.arguments;
336
- stream.push({ type: 'toolcall_end', contentIndex, toolCall: block, partial: { ...partial } });
337
- });
338
- stream.push({ type: 'done', reason: message.stopReason, message });
339
- stream.end(message);
340
- }
341
-
342
- export default function scriptedProviderExtension(pi: ExtensionAPI): void {
343
- let callCount = 0;
344
- const scenario = parseScenario(process.env['UNIPI_BG_SCRIPTED_SCENARIO']);
345
-
346
- const ScriptedEchoParams = {
347
- type: 'object',
348
- properties: {
349
- value: { type: 'string' },
350
- fail: { type: 'boolean' },
351
- },
352
- additionalProperties: false,
353
- } as const;
354
- pi.registerTool<typeof ScriptedEchoParams, { value: string }>({
355
- name: 'scripted_echo',
356
- label: 'Scripted Echo',
357
- description: 'Test-only deterministic scripted provider tool',
358
- parameters: ScriptedEchoParams,
359
- execute(_toolCallId, input) {
360
- if (input.fail) return Promise.reject(new Error('scripted echo failed intentionally'));
361
- return Promise.resolve({
362
- content: [{ type: 'text' as const, text: input.value ?? 'ok' }],
363
- details: { value: input.value ?? 'ok' },
364
- });
365
- },
366
- });
367
-
368
- pi.registerProvider(PROVIDER, {
369
- name: 'Pi Background Tasks Scripted Provider',
370
- baseUrl: 'http://localhost:0',
371
- apiKey: 'UNIPI_BG_SCRIPTED_API_KEY',
372
- api: API,
373
- models: [
374
- {
375
- id: MODEL_ID,
376
- name: 'Scripted Model',
377
- reasoning: false,
378
- input: ['text'],
379
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
380
- contextWindow: 272_000,
381
- maxTokens: 4096,
382
- },
383
- ],
384
- streamSimple(_model: Model<Api>, context: Context): AssistantMessageEventStream {
385
- callCount += 1;
386
- const eventDrivenContract = inspectEventDrivenContract(context);
387
- record({
388
- type: 'provider_call',
389
- scenario,
390
- callCount,
391
- eventDrivenContract,
392
- roles: context.messages.map((message) => message.role),
393
- customTypes: context.messages.flatMap((message) =>
394
- 'customType' in message && typeof message.customType === 'string'
395
- ? [message.customType]
396
- : [],
397
- ),
398
- lastRole: context.messages.at(-1)?.role,
399
- summaries: context.messages.map(summarizeMessage),
400
- });
401
- const stream = createAssistantMessageEventStream();
402
- queueMicrotask(() => {
403
- pushMessage(stream, responseFor(scenario, callCount, eventDrivenContract, context));
404
- });
405
- return stream;
406
- },
407
- });
408
- }