@pi-unipi/background-tasks 2.6.1
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/README.md +87 -0
- package/extensions/anthropic-attribution.ts +1 -0
- package/extensions/delegate-child.ts +1 -0
- package/extensions/fusion-child.ts +1 -0
- package/package.json +40 -0
- package/src/__tests__/anthropic-attribution.test.ts +195 -0
- package/src/__tests__/config.test.ts +137 -0
- package/src/__tests__/core.test.ts +493 -0
- package/src/__tests__/delegate-artifacts.test.ts +528 -0
- package/src/__tests__/delegate-budget.test.ts +456 -0
- package/src/__tests__/delegate-launch.test.ts +676 -0
- package/src/__tests__/delegate-result-package.test.ts +350 -0
- package/src/__tests__/delegate-seed.test.ts +392 -0
- package/src/__tests__/durable-fs.test.ts +559 -0
- package/src/__tests__/extension-api.test.ts +579 -0
- package/src/__tests__/fusion-artifacts.test.ts +1039 -0
- package/src/__tests__/fusion-budget.test.ts +1356 -0
- package/src/__tests__/fusion-claude-cache.test.ts +320 -0
- package/src/__tests__/fusion-config.test.ts +335 -0
- package/src/__tests__/fusion-context-prompts.test.ts +670 -0
- package/src/__tests__/fusion-evaluation.test.ts +315 -0
- package/src/__tests__/fusion-extraction-equivalence.test.ts +58 -0
- package/src/__tests__/fusion-golden-bytes.test.ts +35 -0
- package/src/__tests__/fusion-high-cardinality.test.ts +192 -0
- package/src/__tests__/fusion-model-selector.test.ts +205 -0
- package/src/__tests__/fusion-orchestrator.test.ts +1194 -0
- package/src/__tests__/fusion-rpc.test.ts +369 -0
- package/src/__tests__/fusion-sdk.test.ts +1226 -0
- package/src/__tests__/fusion-v5-core.test.ts +219 -0
- package/src/__tests__/fusion-validate-orchestrator.test.ts +240 -0
- package/src/__tests__/fusion-web-fetch.test.ts +485 -0
- package/src/__tests__/fusion-workflows.test.ts +59 -0
- package/src/__tests__/helpers/delegate-deterministic-seed.ts +109 -0
- package/src/__tests__/helpers/delegate-seed-subprocess.ts +10 -0
- package/src/__tests__/helpers/fusion-canonical-subprocess.ts +21 -0
- package/src/__tests__/helpers/fusion-canonical.ts +140 -0
- package/src/__tests__/helpers/fusion-fake-pi.ts +279 -0
- package/src/__tests__/helpers/fusion-golden-corpus.ts +500 -0
- package/src/__tests__/helpers/fusion-high-cardinality.ts +140 -0
- package/src/__tests__/helpers/normalize.ts +22 -0
- package/src/__tests__/helpers/pi-hook-contract-evidence.json +18 -0
- package/src/__tests__/pi-launch.test.ts +202 -0
- package/src/__tests__/registry.test.ts +1580 -0
- package/src/__tests__/scripted-provider/delegate-ambient-provider.test.ts +130 -0
- package/src/__tests__/scripted-provider/delegate-child-guard.test.ts +631 -0
- package/src/__tests__/scripted-provider/delegate-guard-provider.ts +403 -0
- package/src/__tests__/scripted-provider/follow-up.test.ts +448 -0
- package/src/__tests__/scripted-provider/fusion-output-recovery.test.ts +132 -0
- package/src/__tests__/scripted-provider/fusion-reason.test.ts +310 -0
- package/src/__tests__/scripted-provider/fusion-runtime-guard.test.ts +163 -0
- package/src/__tests__/scripted-provider/hook-contract-provider.ts +179 -0
- package/src/__tests__/scripted-provider/hook-probe-a.ts +3 -0
- package/src/__tests__/scripted-provider/hook-probe-b.ts +3 -0
- package/src/__tests__/scripted-provider/hook-probe-extension.ts +126 -0
- package/src/__tests__/scripted-provider/output-recovery-provider.ts +153 -0
- package/src/__tests__/scripted-provider/pi-hook-contract-evidence.json +18 -0
- package/src/__tests__/scripted-provider/pi-hook-contract.test.ts +477 -0
- package/src/__tests__/scripted-provider/runtime-guard-probe.ts +28 -0
- package/src/__tests__/scripted-provider/runtime-guard-provider.ts +49 -0
- package/src/__tests__/scripted-provider/scripted-provider-extension.ts +408 -0
- package/src/__tests__/task-manager.test.ts +479 -0
- package/src/__tests__/windows-taskkill.test.ts +161 -0
- package/src/anthropic-attribution-path.ts +21 -0
- package/src/anthropic-attribution.ts +1983 -0
- package/src/attested-pi-run.ts +612 -0
- package/src/child-process.ts +55 -0
- package/src/common.ts +8 -0
- package/src/config.ts +292 -0
- package/src/context-parent-snapshot.ts +142 -0
- package/src/context-token-budget.ts +903 -0
- package/src/context-visible-conversation-v2.ts +551 -0
- package/src/delegate/artifacts.ts +487 -0
- package/src/delegate/budget.ts +415 -0
- package/src/delegate/hook-contract-evidence.json +18 -0
- package/src/delegate/hook-contract.ts +154 -0
- package/src/delegate/launch.ts +497 -0
- package/src/delegate/result-package.ts +459 -0
- package/src/delegate/runner.ts +449 -0
- package/src/delegate/seed.ts +423 -0
- package/src/delegate/types.ts +323 -0
- package/src/delegate-child-extension.ts +978 -0
- package/src/delegate-extension.ts +806 -0
- package/src/durable-fs.ts +386 -0
- package/src/extension-api.ts +548 -0
- package/src/fixtures/delegate-context-incident.json +17 -0
- package/src/fixtures/fusion-golden-bytes.json +310 -0
- package/src/fixtures/fusion-validate-golden-bytes.json +282 -0
- package/src/fusion/artifacts.ts +967 -0
- package/src/fusion/budget.ts +1162 -0
- package/src/fusion/child-protocol.ts +305 -0
- package/src/fusion/claude-cache.ts +207 -0
- package/src/fusion/clean-context.ts +91 -0
- package/src/fusion/config.ts +449 -0
- package/src/fusion/context.ts +265 -0
- package/src/fusion/evaluation.ts +800 -0
- package/src/fusion/orchestrator.ts +1288 -0
- package/src/fusion/output-contract.ts +34 -0
- package/src/fusion/pi-child.ts +2373 -0
- package/src/fusion/prompts.ts +345 -0
- package/src/fusion/result-package.ts +959 -0
- package/src/fusion/source-policy.ts +257 -0
- package/src/fusion/types.ts +1139 -0
- package/src/fusion/web-fetch.ts +1060 -0
- package/src/fusion/workflows.ts +184 -0
- package/src/fusion-child-extension.ts +1052 -0
- package/src/fusion-extension.ts +1293 -0
- package/src/index.ts +295 -0
- package/src/pi-launch.ts +225 -0
- package/src/registry.ts +2424 -0
- package/src/settings-overlay.ts +208 -0
- package/src/task-manager.ts +774 -0
- package/src/tools.ts +530 -0
- package/src/turndown.d.ts +15 -0
- package/src/types.ts +963 -0
- package/src/ui/fusion-model-selector.ts +322 -0
- package/src/windows-taskkill.ts +250 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import type { EventBus } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_LOG_BYTES,
|
|
4
|
+
normalizeMaxBytes,
|
|
5
|
+
type BgLogsDetails,
|
|
6
|
+
type BgTaskSnapshot,
|
|
7
|
+
type JsonObject,
|
|
8
|
+
type StartTaskOptions,
|
|
9
|
+
} from './types.js';
|
|
10
|
+
import type { BackgroundTaskContext, BackgroundTaskRegistry } from './registry.js';
|
|
11
|
+
|
|
12
|
+
export const BG_REQUEST_CHANNEL = 'unipi-background-tasks:request:v1';
|
|
13
|
+
export const BG_RESPONSE_CHANNEL = 'unipi-background-tasks:response:v1';
|
|
14
|
+
export const BG_TERMINAL_CHANNEL = 'unipi-background-tasks:terminal:v1';
|
|
15
|
+
export const BG_REQUEST_SCHEMA = 'unipi-background-tasks.extension-request.v1';
|
|
16
|
+
export const BG_RESPONSE_SCHEMA = 'unipi-background-tasks.extension-response.v1';
|
|
17
|
+
export const BG_TERMINAL_SCHEMA = 'unipi-background-tasks.extension-terminal.v1';
|
|
18
|
+
|
|
19
|
+
const MAX_ERROR_CHARS = 240;
|
|
20
|
+
const MAX_REQUEST_ID_CHARS = 200;
|
|
21
|
+
|
|
22
|
+
export type BackgroundTaskExtensionOperation = 'capabilities' | 'run' | 'status' | 'logs' | 'kill';
|
|
23
|
+
|
|
24
|
+
export interface BackgroundTaskExtensionCapabilities {
|
|
25
|
+
api_version: 1;
|
|
26
|
+
run: boolean;
|
|
27
|
+
run_is_agent: boolean;
|
|
28
|
+
run_completion_trigger: boolean;
|
|
29
|
+
status: boolean;
|
|
30
|
+
logs: boolean;
|
|
31
|
+
logs_bounded: boolean;
|
|
32
|
+
kill: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const BG_EXTENSION_CAPABILITIES: BackgroundTaskExtensionCapabilities = Object.freeze({
|
|
36
|
+
api_version: 1,
|
|
37
|
+
run: true,
|
|
38
|
+
run_is_agent: true,
|
|
39
|
+
run_completion_trigger: true,
|
|
40
|
+
status: true,
|
|
41
|
+
logs: true,
|
|
42
|
+
logs_bounded: true,
|
|
43
|
+
kill: true,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export interface BackgroundTaskExtensionRunPayload {
|
|
47
|
+
name: string;
|
|
48
|
+
command: string;
|
|
49
|
+
isAgent: boolean;
|
|
50
|
+
timeoutSeconds?: number | undefined;
|
|
51
|
+
notifyOnCompletion: boolean;
|
|
52
|
+
triggerOnCompletion: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface BackgroundTaskExtensionStatusPayload {
|
|
56
|
+
taskId?: string | undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface BackgroundTaskExtensionLogsPayload {
|
|
60
|
+
taskId: string;
|
|
61
|
+
maxBytes?: number | undefined;
|
|
62
|
+
tail?: boolean | undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface BackgroundTaskExtensionKillPayload {
|
|
66
|
+
taskId: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type BackgroundTaskExtensionPayload =
|
|
70
|
+
| Record<PropertyKey, never>
|
|
71
|
+
| BackgroundTaskExtensionRunPayload
|
|
72
|
+
| BackgroundTaskExtensionStatusPayload
|
|
73
|
+
| BackgroundTaskExtensionLogsPayload
|
|
74
|
+
| BackgroundTaskExtensionKillPayload;
|
|
75
|
+
|
|
76
|
+
export interface BackgroundTaskExtensionRequest {
|
|
77
|
+
schema_version: typeof BG_REQUEST_SCHEMA;
|
|
78
|
+
request_id: string;
|
|
79
|
+
operation: BackgroundTaskExtensionOperation;
|
|
80
|
+
payload: BackgroundTaskExtensionPayload;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type BackgroundTaskExtensionResult =
|
|
84
|
+
| BackgroundTaskExtensionCapabilities
|
|
85
|
+
| BgTaskSnapshot
|
|
86
|
+
| { tasks: BgTaskSnapshot[] }
|
|
87
|
+
| (BgLogsDetails & { text: string })
|
|
88
|
+
| { task: BgTaskSnapshot; message: string };
|
|
89
|
+
|
|
90
|
+
export type BackgroundTaskExtensionResponse =
|
|
91
|
+
| {
|
|
92
|
+
schema_version: typeof BG_RESPONSE_SCHEMA;
|
|
93
|
+
request_id: string;
|
|
94
|
+
operation: string;
|
|
95
|
+
ok: true;
|
|
96
|
+
result: BackgroundTaskExtensionResult;
|
|
97
|
+
}
|
|
98
|
+
| {
|
|
99
|
+
schema_version: typeof BG_RESPONSE_SCHEMA;
|
|
100
|
+
request_id: string;
|
|
101
|
+
operation: string;
|
|
102
|
+
ok: false;
|
|
103
|
+
error: string;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export interface BackgroundTaskExtensionTerminal {
|
|
107
|
+
schema_version: typeof BG_TERMINAL_SCHEMA;
|
|
108
|
+
task: BgTaskSnapshot;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface BackgroundTaskExtensionService {
|
|
112
|
+
publishTerminal(task: BgTaskSnapshot): void;
|
|
113
|
+
close(): void;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface BackgroundTaskExtensionServiceOptions {
|
|
117
|
+
events: EventBus;
|
|
118
|
+
registry: BackgroundTaskRegistry;
|
|
119
|
+
getContext: () => BackgroundTaskContext | undefined;
|
|
120
|
+
isShuttingDown: () => boolean;
|
|
121
|
+
logger?: Pick<Console, 'error'> | undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
type JsonRecord = JsonObject;
|
|
125
|
+
|
|
126
|
+
interface ParsedRequest {
|
|
127
|
+
requestId: string;
|
|
128
|
+
operationEcho: string;
|
|
129
|
+
request?: BackgroundTaskExtensionRequest | undefined;
|
|
130
|
+
error?: string | undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface TerminalPublicationGate {
|
|
134
|
+
promise: Promise<void>;
|
|
135
|
+
releaseAfterResponse(): Promise<void>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isRecord(value: unknown): value is JsonRecord {
|
|
139
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function hasOwn(record: JsonRecord, key: string): boolean {
|
|
143
|
+
return Object.prototype.hasOwnProperty.call(record, key);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function assertClosed(record: JsonRecord, allowedKeys: readonly string[], label: string): void {
|
|
147
|
+
const allowed = new Set(allowedKeys);
|
|
148
|
+
for (const key of Object.keys(record)) {
|
|
149
|
+
if (!allowed.has(key)) throw new Error(`${label} contains unknown key ${key}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function requireRecord(value: unknown, label: string): JsonRecord {
|
|
154
|
+
if (!isRecord(value)) throw new Error(`${label} must be an object`);
|
|
155
|
+
return value;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function requireNonEmptyString(value: unknown, label: string): string {
|
|
159
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
160
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
161
|
+
}
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function requireBoolean(value: unknown, label: string): boolean {
|
|
166
|
+
if (typeof value !== 'boolean') throw new Error(`${label} must be boolean`);
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function requirePositiveInteger(value: unknown, label: string): number {
|
|
171
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
|
|
172
|
+
throw new Error(`${label} must be a positive integer`);
|
|
173
|
+
}
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function requireOperation(value: unknown, label: string): BackgroundTaskExtensionOperation {
|
|
178
|
+
if (
|
|
179
|
+
value === 'capabilities' ||
|
|
180
|
+
value === 'run' ||
|
|
181
|
+
value === 'status' ||
|
|
182
|
+
value === 'logs' ||
|
|
183
|
+
value === 'kill'
|
|
184
|
+
) {
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
throw new Error(`${label} must be one of capabilities, run, status, logs, kill`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function operationEcho(value: unknown): string {
|
|
191
|
+
return typeof value === 'string' && value.length > 0 ? value : 'malformed';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function requestIdEcho(value: unknown): string {
|
|
195
|
+
return typeof value === 'string' && value.length > 0 ? value : 'malformed';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function parseCapabilitiesPayload(value: unknown): Record<PropertyKey, never> {
|
|
199
|
+
const payload = requireRecord(value, 'capabilities.payload');
|
|
200
|
+
assertClosed(payload, [], 'capabilities.payload');
|
|
201
|
+
return {};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function parseRunPayload(value: unknown): BackgroundTaskExtensionRunPayload {
|
|
205
|
+
const payload = requireRecord(value, 'run.payload');
|
|
206
|
+
assertClosed(
|
|
207
|
+
payload,
|
|
208
|
+
['name', 'command', 'isAgent', 'timeoutSeconds', 'notifyOnCompletion', 'triggerOnCompletion'],
|
|
209
|
+
'run.payload',
|
|
210
|
+
);
|
|
211
|
+
const out: BackgroundTaskExtensionRunPayload = {
|
|
212
|
+
name: requireNonEmptyString(payload['name'], 'run.payload.name'),
|
|
213
|
+
command: requireNonEmptyString(payload['command'], 'run.payload.command'),
|
|
214
|
+
isAgent: requireBoolean(payload['isAgent'], 'run.payload.isAgent'),
|
|
215
|
+
notifyOnCompletion: requireBoolean(
|
|
216
|
+
payload['notifyOnCompletion'],
|
|
217
|
+
'run.payload.notifyOnCompletion',
|
|
218
|
+
),
|
|
219
|
+
triggerOnCompletion: requireBoolean(
|
|
220
|
+
payload['triggerOnCompletion'],
|
|
221
|
+
'run.payload.triggerOnCompletion',
|
|
222
|
+
),
|
|
223
|
+
};
|
|
224
|
+
if (hasOwn(payload, 'timeoutSeconds')) {
|
|
225
|
+
out.timeoutSeconds = requirePositiveInteger(
|
|
226
|
+
payload['timeoutSeconds'],
|
|
227
|
+
'run.payload.timeoutSeconds',
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function parseStatusPayload(value: unknown): BackgroundTaskExtensionStatusPayload {
|
|
234
|
+
const payload = requireRecord(value, 'status.payload');
|
|
235
|
+
assertClosed(payload, ['taskId'], 'status.payload');
|
|
236
|
+
const out: BackgroundTaskExtensionStatusPayload = {};
|
|
237
|
+
if (hasOwn(payload, 'taskId')) {
|
|
238
|
+
out.taskId = requireNonEmptyString(payload['taskId'], 'status.payload.taskId');
|
|
239
|
+
}
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function parseLogsPayload(value: unknown): BackgroundTaskExtensionLogsPayload {
|
|
244
|
+
const payload = requireRecord(value, 'logs.payload');
|
|
245
|
+
assertClosed(payload, ['taskId', 'maxBytes', 'tail'], 'logs.payload');
|
|
246
|
+
const out: BackgroundTaskExtensionLogsPayload = {
|
|
247
|
+
taskId: requireNonEmptyString(payload['taskId'], 'logs.payload.taskId'),
|
|
248
|
+
};
|
|
249
|
+
if (hasOwn(payload, 'maxBytes')) {
|
|
250
|
+
out.maxBytes = requirePositiveInteger(payload['maxBytes'], 'logs.payload.maxBytes');
|
|
251
|
+
}
|
|
252
|
+
if (hasOwn(payload, 'tail')) out.tail = requireBoolean(payload['tail'], 'logs.payload.tail');
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function parseKillPayload(value: unknown): BackgroundTaskExtensionKillPayload {
|
|
257
|
+
const payload = requireRecord(value, 'kill.payload');
|
|
258
|
+
assertClosed(payload, ['taskId'], 'kill.payload');
|
|
259
|
+
return { taskId: requireNonEmptyString(payload['taskId'], 'kill.payload.taskId') };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function parsePayload(
|
|
263
|
+
operation: BackgroundTaskExtensionOperation,
|
|
264
|
+
value: unknown,
|
|
265
|
+
): BackgroundTaskExtensionPayload {
|
|
266
|
+
switch (operation) {
|
|
267
|
+
case 'capabilities':
|
|
268
|
+
return parseCapabilitiesPayload(value);
|
|
269
|
+
case 'run':
|
|
270
|
+
return parseRunPayload(value);
|
|
271
|
+
case 'status':
|
|
272
|
+
return parseStatusPayload(value);
|
|
273
|
+
case 'logs':
|
|
274
|
+
return parseLogsPayload(value);
|
|
275
|
+
case 'kill':
|
|
276
|
+
return parseKillPayload(value);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function parseRequest(data: unknown): ParsedRequest {
|
|
281
|
+
if (!isRecord(data)) {
|
|
282
|
+
return {
|
|
283
|
+
requestId: 'malformed',
|
|
284
|
+
operationEcho: 'malformed',
|
|
285
|
+
error: 'request frame must be an object',
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const requestId = requestIdEcho(data['request_id']);
|
|
289
|
+
const opEcho = operationEcho(data['operation']);
|
|
290
|
+
try {
|
|
291
|
+
assertClosed(data, ['schema_version', 'request_id', 'operation', 'payload'], 'request');
|
|
292
|
+
if (data['schema_version'] !== BG_REQUEST_SCHEMA)
|
|
293
|
+
throw new Error('request schema_version mismatch');
|
|
294
|
+
const parsedRequestId = requireNonEmptyString(data['request_id'], 'request.request_id');
|
|
295
|
+
if (parsedRequestId.length > MAX_REQUEST_ID_CHARS) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`request.request_id must be at most ${String(MAX_REQUEST_ID_CHARS)} characters`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
const operation = requireOperation(data['operation'], 'request.operation');
|
|
301
|
+
if (!hasOwn(data, 'payload')) throw new Error('request.payload is required');
|
|
302
|
+
const payload = parsePayload(operation, data['payload']);
|
|
303
|
+
return {
|
|
304
|
+
requestId: parsedRequestId,
|
|
305
|
+
operationEcho: operation,
|
|
306
|
+
request: {
|
|
307
|
+
schema_version: BG_REQUEST_SCHEMA,
|
|
308
|
+
request_id: parsedRequestId,
|
|
309
|
+
operation,
|
|
310
|
+
payload,
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
} catch (error) {
|
|
314
|
+
return { requestId, operationEcho: opEcho, error: errorText(error) };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function createTerminalPublicationGate(): TerminalPublicationGate {
|
|
319
|
+
let released = false;
|
|
320
|
+
let resolveGate: () => void = () => {};
|
|
321
|
+
const promise = new Promise<void>((resolve) => {
|
|
322
|
+
resolveGate = resolve;
|
|
323
|
+
});
|
|
324
|
+
return {
|
|
325
|
+
promise,
|
|
326
|
+
async releaseAfterResponse() {
|
|
327
|
+
if (released) return;
|
|
328
|
+
released = true;
|
|
329
|
+
// Give response listeners one microtask turn to resolve their request promises
|
|
330
|
+
// and bind the returned task id before an early terminal event is emitted.
|
|
331
|
+
await Promise.resolve();
|
|
332
|
+
resolveGate();
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function combineTerminalPublicationGates(
|
|
338
|
+
existing: Promise<void> | undefined,
|
|
339
|
+
next: Promise<void> | undefined,
|
|
340
|
+
): Promise<void> | undefined {
|
|
341
|
+
if (existing === undefined) return next;
|
|
342
|
+
if (next === undefined) return existing;
|
|
343
|
+
return Promise.all([existing, next]).then(() => undefined);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function errorText(error: unknown): string {
|
|
347
|
+
return error instanceof Error ? error.message : String(error);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function boundedBackgroundTaskError(error: unknown): string {
|
|
351
|
+
const text = errorText(error).replace(/\s+/gu, ' ').trim();
|
|
352
|
+
if (text.length <= MAX_ERROR_CHARS) return text;
|
|
353
|
+
return `${text.slice(0, MAX_ERROR_CHARS - 1)}…`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function errorResponse(
|
|
357
|
+
requestId: string,
|
|
358
|
+
operation: string,
|
|
359
|
+
error: unknown,
|
|
360
|
+
): BackgroundTaskExtensionResponse {
|
|
361
|
+
return {
|
|
362
|
+
schema_version: BG_RESPONSE_SCHEMA,
|
|
363
|
+
request_id: requestId,
|
|
364
|
+
operation,
|
|
365
|
+
ok: false,
|
|
366
|
+
error: boundedBackgroundTaskError(error),
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function successResponse(
|
|
371
|
+
request: BackgroundTaskExtensionRequest,
|
|
372
|
+
result: BackgroundTaskExtensionResult,
|
|
373
|
+
): BackgroundTaskExtensionResponse {
|
|
374
|
+
return {
|
|
375
|
+
schema_version: BG_RESPONSE_SCHEMA,
|
|
376
|
+
request_id: request.request_id,
|
|
377
|
+
operation: request.operation,
|
|
378
|
+
ok: true,
|
|
379
|
+
result,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function runPayload(value: BackgroundTaskExtensionPayload): BackgroundTaskExtensionRunPayload {
|
|
384
|
+
return value as BackgroundTaskExtensionRunPayload;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function statusPayload(
|
|
388
|
+
value: BackgroundTaskExtensionPayload,
|
|
389
|
+
): BackgroundTaskExtensionStatusPayload {
|
|
390
|
+
return value as BackgroundTaskExtensionStatusPayload;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function logsPayload(value: BackgroundTaskExtensionPayload): BackgroundTaskExtensionLogsPayload {
|
|
394
|
+
return value as BackgroundTaskExtensionLogsPayload;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function killPayload(value: BackgroundTaskExtensionPayload): BackgroundTaskExtensionKillPayload {
|
|
398
|
+
return value as BackgroundTaskExtensionKillPayload;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
class InstalledBackgroundTaskExtensionService implements BackgroundTaskExtensionService {
|
|
402
|
+
private readonly events: EventBus;
|
|
403
|
+
private readonly registry: BackgroundTaskRegistry;
|
|
404
|
+
private readonly getContext: () => BackgroundTaskContext | undefined;
|
|
405
|
+
private readonly isShuttingDown: () => boolean;
|
|
406
|
+
private readonly logger: Pick<Console, 'error'>;
|
|
407
|
+
private readonly seenRequestIds = new Set<string>();
|
|
408
|
+
private readonly unsubscribe: () => void;
|
|
409
|
+
private closed = false;
|
|
410
|
+
|
|
411
|
+
constructor(options: BackgroundTaskExtensionServiceOptions) {
|
|
412
|
+
this.events = options.events;
|
|
413
|
+
this.registry = options.registry;
|
|
414
|
+
this.getContext = options.getContext;
|
|
415
|
+
this.isShuttingDown = options.isShuttingDown;
|
|
416
|
+
this.logger = options.logger ?? console;
|
|
417
|
+
this.unsubscribe = this.events.on(BG_REQUEST_CHANNEL, (data) => {
|
|
418
|
+
void this.handle(data);
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
publishTerminal(task: BgTaskSnapshot): void {
|
|
423
|
+
if (this.closed) throw new Error('unipi-background-tasks EventBus service is closed');
|
|
424
|
+
const terminal: BackgroundTaskExtensionTerminal = {
|
|
425
|
+
schema_version: BG_TERMINAL_SCHEMA,
|
|
426
|
+
task,
|
|
427
|
+
};
|
|
428
|
+
this.events.emit(BG_TERMINAL_CHANNEL, terminal);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
close(): void {
|
|
432
|
+
if (this.closed) return;
|
|
433
|
+
this.closed = true;
|
|
434
|
+
this.unsubscribe();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private async handle(data: unknown): Promise<void> {
|
|
438
|
+
const parsed = parseRequest(data);
|
|
439
|
+
if (parsed.error !== undefined || parsed.request === undefined) {
|
|
440
|
+
this.emitResponse(
|
|
441
|
+
errorResponse(parsed.requestId, parsed.operationEcho, parsed.error ?? 'malformed request'),
|
|
442
|
+
);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const request = parsed.request;
|
|
446
|
+
if (this.seenRequestIds.has(request.request_id)) {
|
|
447
|
+
this.emitResponse(
|
|
448
|
+
errorResponse(
|
|
449
|
+
request.request_id,
|
|
450
|
+
request.operation,
|
|
451
|
+
`duplicate request_id ${request.request_id}`,
|
|
452
|
+
),
|
|
453
|
+
);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
this.seenRequestIds.add(request.request_id);
|
|
457
|
+
const terminalGate =
|
|
458
|
+
request.operation === 'run' || request.operation === 'kill'
|
|
459
|
+
? createTerminalPublicationGate()
|
|
460
|
+
: undefined;
|
|
461
|
+
try {
|
|
462
|
+
if (this.closed) throw new Error('unipi-background-tasks EventBus service is closed');
|
|
463
|
+
if (this.isShuttingDown() || this.registry.isShuttingDown()) {
|
|
464
|
+
throw new Error('unipi-background-tasks EventBus service is shutting down');
|
|
465
|
+
}
|
|
466
|
+
const ctx = this.getContext();
|
|
467
|
+
if (ctx === undefined) {
|
|
468
|
+
throw new Error('unipi-background-tasks EventBus service is unavailable before session_start');
|
|
469
|
+
}
|
|
470
|
+
this.emitResponse(
|
|
471
|
+
successResponse(request, await this.execute(ctx, request, terminalGate?.promise)),
|
|
472
|
+
);
|
|
473
|
+
} catch (error) {
|
|
474
|
+
this.emitResponse(errorResponse(request.request_id, request.operation, error));
|
|
475
|
+
} finally {
|
|
476
|
+
await terminalGate?.releaseAfterResponse();
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private async execute(
|
|
481
|
+
ctx: BackgroundTaskContext,
|
|
482
|
+
request: BackgroundTaskExtensionRequest,
|
|
483
|
+
terminalPublicationGate?: Promise<void> | undefined,
|
|
484
|
+
): Promise<BackgroundTaskExtensionResult> {
|
|
485
|
+
switch (request.operation) {
|
|
486
|
+
case 'capabilities':
|
|
487
|
+
return { ...BG_EXTENSION_CAPABILITIES };
|
|
488
|
+
case 'run': {
|
|
489
|
+
const payload = runPayload(request.payload);
|
|
490
|
+
const options: StartTaskOptions = {
|
|
491
|
+
name: payload.name,
|
|
492
|
+
isAgent: payload.isAgent,
|
|
493
|
+
notifyOnCompletion: payload.notifyOnCompletion,
|
|
494
|
+
triggerOnCompletion: payload.triggerOnCompletion,
|
|
495
|
+
terminalPublicationGate,
|
|
496
|
+
};
|
|
497
|
+
if (payload.timeoutSeconds !== undefined) options.timeoutSeconds = payload.timeoutSeconds;
|
|
498
|
+
const task = await this.registry.startTask(ctx, payload.command, options);
|
|
499
|
+
return this.registry.snapshot(task);
|
|
500
|
+
}
|
|
501
|
+
case 'status': {
|
|
502
|
+
const payload = statusPayload(request.payload);
|
|
503
|
+
const tasks = payload.taskId
|
|
504
|
+
? [this.registry.resolveTask(payload.taskId)]
|
|
505
|
+
: this.registry.allTasks();
|
|
506
|
+
return { tasks: tasks.map((task) => this.registry.snapshot(task)) };
|
|
507
|
+
}
|
|
508
|
+
case 'logs': {
|
|
509
|
+
const payload = logsPayload(request.payload);
|
|
510
|
+
const task = this.registry.resolveTask(payload.taskId);
|
|
511
|
+
const logs = await this.registry.getTaskLogs(
|
|
512
|
+
task,
|
|
513
|
+
normalizeMaxBytes(payload.maxBytes, DEFAULT_LOG_BYTES),
|
|
514
|
+
payload.tail ?? true,
|
|
515
|
+
);
|
|
516
|
+
return { ...logs.details, text: logs.text };
|
|
517
|
+
}
|
|
518
|
+
case 'kill': {
|
|
519
|
+
const payload = killPayload(request.payload);
|
|
520
|
+
const task = this.registry.resolveTask(payload.taskId);
|
|
521
|
+
task.terminalPublicationGate = combineTerminalPublicationGates(
|
|
522
|
+
task.terminalPublicationGate,
|
|
523
|
+
terminalPublicationGate,
|
|
524
|
+
);
|
|
525
|
+
await this.registry.stopTask(task, 'user');
|
|
526
|
+
const snapshot = this.registry.snapshot(task);
|
|
527
|
+
return {
|
|
528
|
+
task: snapshot,
|
|
529
|
+
message: `Killed background task ${snapshot.name ?? snapshot.id} (${snapshot.id}). Output: ${snapshot.outputPath}`,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
private emitResponse(response: BackgroundTaskExtensionResponse): void {
|
|
536
|
+
try {
|
|
537
|
+
this.events.emit(BG_RESPONSE_CHANNEL, response);
|
|
538
|
+
} catch (error) {
|
|
539
|
+
this.logger.error('[background-tasks] EventBus response emit failed:', error);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export function installBackgroundTaskExtensionApi(
|
|
545
|
+
options: BackgroundTaskExtensionServiceOptions,
|
|
546
|
+
): BackgroundTaskExtensionService {
|
|
547
|
+
return new InstalledBackgroundTaskExtensionService(options);
|
|
548
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "pi-background-tasks.delegate-context-incident-fixture.v1",
|
|
3
|
+
"incident_task_id": "d670700fdaab973ed6a41436d37f4ef2e",
|
|
4
|
+
"child_prompt_sha256": "0dbf3ccf20e72150af56632fbd3e5fecf09ef5694561b6673f92b59fbdbbbc27",
|
|
5
|
+
"child_prompt_utf8_bytes": 173740,
|
|
6
|
+
"system_prompt_utf8_bytes": 1673,
|
|
7
|
+
"launch_utf8_bytes": 175413,
|
|
8
|
+
"launch_normal_bytes": 175152,
|
|
9
|
+
"launch_multibyte_bytes": 261,
|
|
10
|
+
"launch_dense_bytes": 0,
|
|
11
|
+
"old_one_byte_normal_two_byte_multibyte_forecast": 175795,
|
|
12
|
+
"provable_one_byte_per_token_forecast": 175925,
|
|
13
|
+
"context_window_tokens": 372000,
|
|
14
|
+
"allowed_input_tokens": 343328,
|
|
15
|
+
"calibrated_rate_bytes_per_token_x100": 289,
|
|
16
|
+
"finalization_reserve_tokens": 32768
|
|
17
|
+
}
|