@pi-unipi/background-tasks 2.6.3 → 2.9.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/package.json +6 -5
- package/src/__tests__/anthropic-attribution.test.ts +0 -195
- package/src/__tests__/config.test.ts +0 -137
- package/src/__tests__/core.test.ts +0 -493
- package/src/__tests__/delegate-artifacts.test.ts +0 -528
- package/src/__tests__/delegate-budget.test.ts +0 -456
- package/src/__tests__/delegate-launch.test.ts +0 -676
- package/src/__tests__/delegate-result-package.test.ts +0 -350
- package/src/__tests__/delegate-seed.test.ts +0 -392
- package/src/__tests__/durable-fs.test.ts +0 -559
- package/src/__tests__/extension-api.test.ts +0 -579
- package/src/__tests__/fusion-artifacts.test.ts +0 -1039
- package/src/__tests__/fusion-budget.test.ts +0 -1356
- package/src/__tests__/fusion-claude-cache.test.ts +0 -320
- package/src/__tests__/fusion-config.test.ts +0 -335
- package/src/__tests__/fusion-context-prompts.test.ts +0 -670
- package/src/__tests__/fusion-evaluation.test.ts +0 -315
- package/src/__tests__/fusion-extraction-equivalence.test.ts +0 -58
- package/src/__tests__/fusion-golden-bytes.test.ts +0 -35
- package/src/__tests__/fusion-high-cardinality.test.ts +0 -192
- package/src/__tests__/fusion-model-selector.test.ts +0 -205
- package/src/__tests__/fusion-orchestrator.test.ts +0 -1194
- package/src/__tests__/fusion-rpc.test.ts +0 -369
- package/src/__tests__/fusion-sdk.test.ts +0 -1226
- package/src/__tests__/fusion-v5-core.test.ts +0 -219
- package/src/__tests__/fusion-validate-orchestrator.test.ts +0 -240
- package/src/__tests__/fusion-web-fetch.test.ts +0 -485
- package/src/__tests__/fusion-workflows.test.ts +0 -59
- package/src/__tests__/helpers/delegate-deterministic-seed.ts +0 -109
- package/src/__tests__/helpers/delegate-seed-subprocess.ts +0 -10
- package/src/__tests__/helpers/fusion-canonical-subprocess.ts +0 -21
- package/src/__tests__/helpers/fusion-canonical.ts +0 -140
- package/src/__tests__/helpers/fusion-fake-pi.ts +0 -279
- package/src/__tests__/helpers/fusion-golden-corpus.ts +0 -500
- package/src/__tests__/helpers/fusion-high-cardinality.ts +0 -140
- package/src/__tests__/helpers/normalize.ts +0 -22
- package/src/__tests__/helpers/pi-hook-contract-evidence.json +0 -18
- package/src/__tests__/pi-launch.test.ts +0 -202
- package/src/__tests__/registry.test.ts +0 -1580
- package/src/__tests__/scripted-provider/delegate-ambient-provider.test.ts +0 -130
- package/src/__tests__/scripted-provider/delegate-child-guard.test.ts +0 -631
- package/src/__tests__/scripted-provider/delegate-guard-provider.ts +0 -403
- package/src/__tests__/scripted-provider/follow-up.test.ts +0 -448
- package/src/__tests__/scripted-provider/fusion-output-recovery.test.ts +0 -132
- package/src/__tests__/scripted-provider/fusion-reason.test.ts +0 -310
- package/src/__tests__/scripted-provider/fusion-runtime-guard.test.ts +0 -163
- package/src/__tests__/scripted-provider/hook-contract-provider.ts +0 -179
- package/src/__tests__/scripted-provider/hook-probe-a.ts +0 -3
- package/src/__tests__/scripted-provider/hook-probe-b.ts +0 -3
- package/src/__tests__/scripted-provider/hook-probe-extension.ts +0 -126
- package/src/__tests__/scripted-provider/output-recovery-provider.ts +0 -153
- package/src/__tests__/scripted-provider/pi-hook-contract-evidence.json +0 -18
- package/src/__tests__/scripted-provider/pi-hook-contract.test.ts +0 -477
- package/src/__tests__/scripted-provider/runtime-guard-probe.ts +0 -28
- package/src/__tests__/scripted-provider/runtime-guard-provider.ts +0 -49
- package/src/__tests__/scripted-provider/scripted-provider-extension.ts +0 -408
- package/src/__tests__/task-manager.test.ts +0 -479
- package/src/__tests__/windows-taskkill.test.ts +0 -161
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { appendFileSync } from 'node:fs';
|
|
2
|
-
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Instrumented probe extension used by the Pi hook-contract characterisation
|
|
6
|
-
* gate. Its behaviour is selected by `UNIPI_BG_HOOK_PROBE_MODE` so one extension
|
|
7
|
-
* body can answer every question the delegate child-side guard depends on.
|
|
8
|
-
*/
|
|
9
|
-
export type HookProbeMode =
|
|
10
|
-
| 'observe'
|
|
11
|
-
| 'context-replace'
|
|
12
|
-
| 'context-throw'
|
|
13
|
-
| 'context-abort'
|
|
14
|
-
| 'tool-result-replace';
|
|
15
|
-
|
|
16
|
-
type JsonObject = Record<PropertyKey, unknown>;
|
|
17
|
-
|
|
18
|
-
export const HOOK_PROBE_INJECTED_TEXT = 'HOOK_PROBE_INJECTED_CONTEXT_MESSAGE';
|
|
19
|
-
export const HOOK_PROBE_REPLACED_TOOL_TEXT = 'HOOK_PROBE_REPLACED_TOOL_RESULT';
|
|
20
|
-
|
|
21
|
-
function record(event: JsonObject): void {
|
|
22
|
-
const path = process.env['UNIPI_BG_HOOK_PROBE_LOG'];
|
|
23
|
-
if (!path) return;
|
|
24
|
-
appendFileSync(path, `${JSON.stringify(event)}\n`, 'utf8');
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function mode(): HookProbeMode {
|
|
28
|
-
const value = process.env['UNIPI_BG_HOOK_PROBE_MODE'];
|
|
29
|
-
if (
|
|
30
|
-
value === 'context-replace' ||
|
|
31
|
-
value === 'context-throw' ||
|
|
32
|
-
value === 'context-abort' ||
|
|
33
|
-
value === 'tool-result-replace'
|
|
34
|
-
)
|
|
35
|
-
return value;
|
|
36
|
-
return 'observe';
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface ContentPartLike {
|
|
40
|
-
readonly type: string;
|
|
41
|
-
readonly text?: unknown;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function messageText(message: object): string {
|
|
45
|
-
const content: unknown = Reflect.get(message, 'content');
|
|
46
|
-
if (typeof content === 'string') return content;
|
|
47
|
-
if (!Array.isArray(content)) return '';
|
|
48
|
-
const parts: readonly ContentPartLike[] = content.filter(
|
|
49
|
-
(part): part is ContentPartLike =>
|
|
50
|
-
typeof part === 'object' && part !== null && typeof Reflect.get(part, 'type') === 'string',
|
|
51
|
-
);
|
|
52
|
-
return parts.map((part) => (typeof part.text === 'string' ? part.text : `<${part.type}>`)).join('');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Build the probe extension factory for one probe identity. Two distinct probe
|
|
57
|
-
* modules share this body so handler ordering across separate extensions can be
|
|
58
|
-
* observed directly.
|
|
59
|
-
*/
|
|
60
|
-
export function createHookProbeExtension(probeId: string): (pi: ExtensionAPI) => void {
|
|
61
|
-
return (pi: ExtensionAPI): void => {
|
|
62
|
-
let contextFires = 0;
|
|
63
|
-
let toolResultFires = 0;
|
|
64
|
-
|
|
65
|
-
pi.on('context', (event, ctx) => {
|
|
66
|
-
contextFires += 1;
|
|
67
|
-
record({
|
|
68
|
-
hook: 'context',
|
|
69
|
-
probeId,
|
|
70
|
-
contextFires,
|
|
71
|
-
roles: event.messages.map((message) => message.role),
|
|
72
|
-
texts: event.messages.map(messageText),
|
|
73
|
-
});
|
|
74
|
-
const selected = mode();
|
|
75
|
-
if (selected === 'context-throw' && probeId === 'probe-a') {
|
|
76
|
-
record({ hook: 'context_throwing', probeId });
|
|
77
|
-
throw new Error('HOOK_PROBE_CONTEXT_THROW');
|
|
78
|
-
}
|
|
79
|
-
if (selected === 'context-abort' && probeId === 'probe-a') {
|
|
80
|
-
record({ hook: 'context_aborting', probeId });
|
|
81
|
-
ctx.abort();
|
|
82
|
-
return undefined;
|
|
83
|
-
}
|
|
84
|
-
if (selected === 'context-replace' && probeId === 'probe-a') {
|
|
85
|
-
return {
|
|
86
|
-
messages: [
|
|
87
|
-
...event.messages,
|
|
88
|
-
{ role: 'user', content: HOOK_PROBE_INJECTED_TEXT, timestamp: Date.now() },
|
|
89
|
-
],
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
return undefined;
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
pi.on('tool_result', (event) => {
|
|
96
|
-
toolResultFires += 1;
|
|
97
|
-
record({
|
|
98
|
-
hook: 'tool_result',
|
|
99
|
-
probeId,
|
|
100
|
-
toolResultFires,
|
|
101
|
-
toolName: event.toolName,
|
|
102
|
-
toolCallId: event.toolCallId,
|
|
103
|
-
isError: event.isError,
|
|
104
|
-
texts: event.content.map((part) => (part.type === 'text' ? part.text : `<${part.type}>`)),
|
|
105
|
-
});
|
|
106
|
-
if (mode() === 'tool-result-replace' && probeId === 'probe-a') {
|
|
107
|
-
return { content: [{ type: 'text' as const, text: HOOK_PROBE_REPLACED_TOOL_TEXT }] };
|
|
108
|
-
}
|
|
109
|
-
return undefined;
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
pi.on('tool_execution_end', (event) => {
|
|
113
|
-
record({
|
|
114
|
-
hook: 'tool_execution_end',
|
|
115
|
-
probeId,
|
|
116
|
-
toolName: event.toolName,
|
|
117
|
-
toolCallId: event.toolCallId,
|
|
118
|
-
isError: event.isError,
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
pi.on('message_end', (event) => {
|
|
123
|
-
record({ hook: 'message_end', probeId, role: event.message.role });
|
|
124
|
-
});
|
|
125
|
-
};
|
|
126
|
-
}
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import { appendFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
3
|
-
import {
|
|
4
|
-
createAssistantMessageEventStream,
|
|
5
|
-
type Api,
|
|
6
|
-
type AssistantMessage,
|
|
7
|
-
type AssistantMessageEventStream,
|
|
8
|
-
type Context,
|
|
9
|
-
type Message,
|
|
10
|
-
type Model,
|
|
11
|
-
type TextContent,
|
|
12
|
-
} from '@earendil-works/pi-ai';
|
|
13
|
-
import { FUSION_CANDIDATE_OUTPUT_COMPRESSION_PROMPT } from '../../fusion/output-contract.js';
|
|
14
|
-
|
|
15
|
-
export const OUTPUT_RECOVERY_PROVIDER = 'pi-bg-output-recovery';
|
|
16
|
-
export const OUTPUT_RECOVERY_MODEL = 'output-recovery-model';
|
|
17
|
-
const API = 'pi-bg-output-recovery-api';
|
|
18
|
-
export const OUTPUT_RECOVERY_ORIGINAL = 'x'.repeat(49_152);
|
|
19
|
-
export const OUTPUT_RECOVERY_REPLACEMENT = 'compressed replacement';
|
|
20
|
-
|
|
21
|
-
interface OutputRecoveryLogEvent {
|
|
22
|
-
event: 'provider_call' | 'agent_settled';
|
|
23
|
-
pid: number;
|
|
24
|
-
calls?: number;
|
|
25
|
-
call?: number;
|
|
26
|
-
roles?: string[];
|
|
27
|
-
tools?: string[];
|
|
28
|
-
saw_original?: boolean;
|
|
29
|
-
saw_compression_prompt?: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function record(value: OutputRecoveryLogEvent): void {
|
|
33
|
-
const path = process.env['UNIPI_BG_OUTPUT_RECOVERY_LOG'];
|
|
34
|
-
if (path) appendFileSync(path, `${JSON.stringify(value)}\n`, 'utf8');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function messageText(message: Message): string {
|
|
38
|
-
if (typeof message.content === 'string') return message.content;
|
|
39
|
-
if (!Array.isArray(message.content)) return '';
|
|
40
|
-
return message.content.map((part) => ('text' in part ? part.text : '')).join('');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function pushText(stream: AssistantMessageEventStream, text: string): void {
|
|
44
|
-
const usage = {
|
|
45
|
-
input: 10,
|
|
46
|
-
output: text.length,
|
|
47
|
-
cacheRead: 0,
|
|
48
|
-
cacheWrite: 0,
|
|
49
|
-
totalTokens: 10 + text.length,
|
|
50
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
51
|
-
};
|
|
52
|
-
const message: AssistantMessage = {
|
|
53
|
-
role: 'assistant',
|
|
54
|
-
content: [{ type: 'text', text }],
|
|
55
|
-
api: API,
|
|
56
|
-
provider: OUTPUT_RECOVERY_PROVIDER,
|
|
57
|
-
model: OUTPUT_RECOVERY_MODEL,
|
|
58
|
-
usage,
|
|
59
|
-
stopReason: 'stop',
|
|
60
|
-
timestamp: Date.now(),
|
|
61
|
-
};
|
|
62
|
-
const partial: AssistantMessage = { ...message, content: [] };
|
|
63
|
-
const partialText: TextContent = { type: 'text', text: '' };
|
|
64
|
-
stream.push({ type: 'start', partial: { ...partial } });
|
|
65
|
-
partial.content = [partialText];
|
|
66
|
-
stream.push({ type: 'text_start', contentIndex: 0, partial: { ...partial } });
|
|
67
|
-
partialText.text = text;
|
|
68
|
-
stream.push({ type: 'text_delta', contentIndex: 0, delta: text, partial: { ...partial } });
|
|
69
|
-
stream.push({ type: 'text_end', contentIndex: 0, content: text, partial: { ...partial } });
|
|
70
|
-
stream.push({ type: 'done', reason: 'stop', message });
|
|
71
|
-
stream.end(message);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const RecoveryProbeParams = {
|
|
75
|
-
type: 'object',
|
|
76
|
-
properties: {},
|
|
77
|
-
additionalProperties: false,
|
|
78
|
-
} as const;
|
|
79
|
-
|
|
80
|
-
export default function outputRecoveryProvider(pi: ExtensionAPI): void {
|
|
81
|
-
let calls = 0;
|
|
82
|
-
let recoveryQueued = false;
|
|
83
|
-
|
|
84
|
-
pi.registerTool<typeof RecoveryProbeParams, Record<string, never>>({
|
|
85
|
-
name: 'recovery_probe',
|
|
86
|
-
label: 'Recovery Probe',
|
|
87
|
-
description: 'Test-only tool used to prove that output compression disables tools.',
|
|
88
|
-
parameters: RecoveryProbeParams,
|
|
89
|
-
execute() {
|
|
90
|
-
return Promise.resolve({ content: [{ type: 'text' as const, text: 'unused' }], details: {} });
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
pi.on('message_end', (event) => {
|
|
95
|
-
if (event.message.role !== 'assistant' || recoveryQueued) return;
|
|
96
|
-
const text = event.message.content
|
|
97
|
-
.flatMap((part) => (part.type === 'text' ? [part.text] : []))
|
|
98
|
-
.join('');
|
|
99
|
-
if (text !== OUTPUT_RECOVERY_ORIGINAL) return;
|
|
100
|
-
const recoveryPath = process.env['UNIPI_BG_OUTPUT_RECOVERY_ARTIFACT'];
|
|
101
|
-
if (!recoveryPath) throw new Error('UNIPI_BG_OUTPUT_RECOVERY_ARTIFACT is required');
|
|
102
|
-
writeFileSync(recoveryPath, text, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
|
|
103
|
-
recoveryQueued = true;
|
|
104
|
-
pi.setActiveTools([]);
|
|
105
|
-
pi.sendUserMessage(FUSION_CANDIDATE_OUTPUT_COMPRESSION_PROMPT, {
|
|
106
|
-
deliverAs: 'followUp',
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// OUR SDK lacks agent_settled; agent_end carries the same end-of-run signal for probes.
|
|
111
|
-
pi.on('agent_end', () => {
|
|
112
|
-
record({ event: 'agent_settled', pid: process.pid, calls });
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
pi.registerProvider(OUTPUT_RECOVERY_PROVIDER, {
|
|
116
|
-
name: 'Pi Background Tasks Output Recovery Provider',
|
|
117
|
-
baseUrl: 'http://localhost:0',
|
|
118
|
-
apiKey: 'UNIPI_BG_OUTPUT_RECOVERY_API_KEY',
|
|
119
|
-
api: API,
|
|
120
|
-
models: [
|
|
121
|
-
{
|
|
122
|
-
id: OUTPUT_RECOVERY_MODEL,
|
|
123
|
-
name: 'Output Recovery Model',
|
|
124
|
-
reasoning: false,
|
|
125
|
-
input: ['text'],
|
|
126
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
127
|
-
contextWindow: 200_000,
|
|
128
|
-
maxTokens: 64_000,
|
|
129
|
-
},
|
|
130
|
-
],
|
|
131
|
-
streamSimple(_model: Model<Api>, context: Context): AssistantMessageEventStream {
|
|
132
|
-
calls += 1;
|
|
133
|
-
const texts = context.messages.map(messageText);
|
|
134
|
-
const tools = Array.isArray(context.tools) ? context.tools.map((tool) => tool.name) : [];
|
|
135
|
-
record({
|
|
136
|
-
event: 'provider_call',
|
|
137
|
-
pid: process.pid,
|
|
138
|
-
call: calls,
|
|
139
|
-
roles: context.messages.map((message) => message.role),
|
|
140
|
-
tools,
|
|
141
|
-
saw_original: texts.includes(OUTPUT_RECOVERY_ORIGINAL),
|
|
142
|
-
saw_compression_prompt: texts.some((text) =>
|
|
143
|
-
text.includes('Compress and restructure only your immediately previous answer'),
|
|
144
|
-
),
|
|
145
|
-
});
|
|
146
|
-
const stream = createAssistantMessageEventStream();
|
|
147
|
-
queueMicrotask(() => {
|
|
148
|
-
pushText(stream, calls === 1 ? OUTPUT_RECOVERY_ORIGINAL : OUTPUT_RECOVERY_REPLACEMENT);
|
|
149
|
-
});
|
|
150
|
-
return stream;
|
|
151
|
-
},
|
|
152
|
-
});
|
|
153
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schema_version": "pi-background-tasks.delegate-hook-contract.v1",
|
|
3
|
-
"contract_id": "context-measure-abort-v1+tool-result-spill-v1",
|
|
4
|
-
"guarantees": {
|
|
5
|
-
"context_fires_before_every_model_call": true,
|
|
6
|
-
"context_result_messages_reach_provider": true,
|
|
7
|
-
"context_abort_blocks_provider_call": true,
|
|
8
|
-
"context_abort_skips_stream_invocation": false,
|
|
9
|
-
"context_abort_terminates_run": true,
|
|
10
|
-
"context_throw_blocks_provider_call": false,
|
|
11
|
-
"context_throw_isolated_to_throwing_handler": true,
|
|
12
|
-
"tool_result_fires_before_transcript_entry": true,
|
|
13
|
-
"tool_result_replacement_reaches_provider": true,
|
|
14
|
-
"tool_result_replacement_preserves_identity": true,
|
|
15
|
-
"tool_result_chains_in_load_order": true,
|
|
16
|
-
"handlers_run_in_extension_load_order": true
|
|
17
|
-
}
|
|
18
|
-
}
|