pi-background-tasks 0.7.6 → 0.7.7
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/PUBLISHING.md +7 -7
- package/README.md +177 -13
- package/TESTING.md +94 -0
- package/TEST_PLAN.md +21 -4
- package/extensions/delegate-child.ts +1 -0
- package/package.json +4 -3
- package/src/core/common.ts +41 -0
- package/src/core/context/parent-snapshot.ts +142 -0
- package/src/core/context/token-budget.ts +890 -0
- package/src/core/context/visible-conversation-v2.ts +551 -0
- package/src/core/delegate/artifacts.ts +479 -0
- package/src/core/delegate/budget.ts +370 -0
- package/src/core/delegate/hook-contract-evidence.json +18 -0
- package/src/core/delegate/hook-contract.ts +153 -0
- package/src/core/delegate/launch.ts +459 -0
- package/src/core/delegate/result-package.ts +443 -0
- package/src/core/delegate/runner.ts +406 -0
- package/src/core/delegate/seed.ts +411 -0
- package/src/core/delegate/types.ts +304 -0
- package/src/core/fusion/artifacts.ts +15 -0
- package/src/core/fusion/budget.ts +444 -54
- package/src/core/fusion/context.ts +108 -509
- package/src/core/fusion/orchestrator.ts +116 -5
- package/src/core/fusion/prompts.ts +5 -1
- package/src/core/fusion/types.ts +154 -36
- package/src/core/registry.ts +174 -0
- package/src/delegate-child-extension.ts +673 -0
- package/src/delegate-extension.ts +587 -0
- package/src/extension.ts +10 -0
- package/src/fusion-extension.ts +2 -0
package/src/core/registry.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type JsonObject,
|
|
28
28
|
type KillKind,
|
|
29
29
|
type StartAttestedPiTaskOptions,
|
|
30
|
+
type StartDelegateTaskOptions,
|
|
30
31
|
type StartTaskOptions,
|
|
31
32
|
type TaskContextUsage,
|
|
32
33
|
type TaskStatus,
|
|
@@ -52,6 +53,7 @@ import {
|
|
|
52
53
|
} from './attested-pi-run.js';
|
|
53
54
|
import {
|
|
54
55
|
assertWindowsCommandLineWithinLimit,
|
|
56
|
+
piLaunchArgv,
|
|
55
57
|
resolvePiLaunch,
|
|
56
58
|
type PiLaunchSpec,
|
|
57
59
|
} from './pi-launch.js';
|
|
@@ -87,8 +89,15 @@ interface OutputEventSource {
|
|
|
87
89
|
on(event: 'data', listener: (data: Buffer | string) => void): unknown;
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
interface ChildStdin {
|
|
93
|
+
write(data: Buffer, callback: (error?: Error | null) => void): boolean;
|
|
94
|
+
end(callback?: () => void): unknown;
|
|
95
|
+
once(event: 'error', listener: (error: Error) => void): unknown;
|
|
96
|
+
}
|
|
97
|
+
|
|
90
98
|
export interface BackgroundTaskChildProcess {
|
|
91
99
|
pid?: number | undefined;
|
|
100
|
+
stdin?: ChildStdin | null | undefined;
|
|
92
101
|
stdout?: OutputEventSource | null | undefined;
|
|
93
102
|
stderr?: OutputEventSource | null | undefined;
|
|
94
103
|
kill(signal?: NodeJS.Signals): boolean;
|
|
@@ -678,6 +687,32 @@ function noopOnChange(): void {
|
|
|
678
687
|
return undefined;
|
|
679
688
|
}
|
|
680
689
|
|
|
690
|
+
/**
|
|
691
|
+
* Deliver the delegate prompt bytes over stdin.
|
|
692
|
+
*
|
|
693
|
+
* A failure to deliver the seed is loud: the caller terminates the task rather
|
|
694
|
+
* than letting a child run without the context it was supposed to receive.
|
|
695
|
+
*/
|
|
696
|
+
function writeDelegateStdin(
|
|
697
|
+
child: BackgroundTaskChildProcess,
|
|
698
|
+
bytes: Buffer,
|
|
699
|
+
onError: (error: Error) => void,
|
|
700
|
+
): void {
|
|
701
|
+
const stdin = child.stdin;
|
|
702
|
+
if (stdin === undefined || stdin === null) {
|
|
703
|
+
onError(new Error('delegate child stdin pipe is unavailable'));
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
stdin.once('error', onError);
|
|
707
|
+
stdin.write(bytes, (error?: Error | null) => {
|
|
708
|
+
if (error !== undefined && error !== null) {
|
|
709
|
+
onError(error);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
stdin.end();
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
681
716
|
export class BackgroundTaskRegistry {
|
|
682
717
|
private readonly tasks = new Map<string, BgTask>();
|
|
683
718
|
private runtimeDir: RuntimeDir | undefined;
|
|
@@ -927,6 +962,145 @@ export class BackgroundTaskRegistry {
|
|
|
927
962
|
}
|
|
928
963
|
}
|
|
929
964
|
|
|
965
|
+
/**
|
|
966
|
+
* Start a prepared delegate child.
|
|
967
|
+
*
|
|
968
|
+
* The caller has already completed preflight, so by the time this runs the
|
|
969
|
+
* seed, budget plan, and artifact directory exist and the argv is fixed. The
|
|
970
|
+
* child is launched directly, never through a shell, and its terminal state
|
|
971
|
+
* flows through the same durable notification path as `bg_run`.
|
|
972
|
+
*/
|
|
973
|
+
async startDelegateTask(
|
|
974
|
+
ctx: BackgroundTaskContext,
|
|
975
|
+
request: StartDelegateTaskOptions,
|
|
976
|
+
): Promise<BgTask> {
|
|
977
|
+
if (this.shuttingDown)
|
|
978
|
+
throw new Error('Cannot start a delegate task while Pi is shutting down');
|
|
979
|
+
|
|
980
|
+
const launch = resolvePiLaunch({ platform: this.platform });
|
|
981
|
+
assertWindowsCommandLineWithinLimit(launch, request.argv, this.platform, 'bg-delegate');
|
|
982
|
+
|
|
983
|
+
const dir = await this.ensureRuntimeDir(ctx);
|
|
984
|
+
const id = request.facts.taskId;
|
|
985
|
+
const outputAbsPath = join(dir.abs, `${id}.output`);
|
|
986
|
+
const metadataAbsPath = join(dir.abs, `${id}.json`);
|
|
987
|
+
const outputPath = join(dir.display, `${id}.output`);
|
|
988
|
+
|
|
989
|
+
const task: BgTask = {
|
|
990
|
+
id,
|
|
991
|
+
name: normalizeTaskName(request.name) ?? 'Delegate task',
|
|
992
|
+
command: ['pi', ...request.argv].map(shellQuote).join(' '),
|
|
993
|
+
status: 'running',
|
|
994
|
+
outputPath,
|
|
995
|
+
outputAbsPath,
|
|
996
|
+
metadataAbsPath,
|
|
997
|
+
cwd: ctx.cwd,
|
|
998
|
+
startTime: this.now(),
|
|
999
|
+
exitCode: undefined,
|
|
1000
|
+
pid: undefined,
|
|
1001
|
+
bytesWritten: 0,
|
|
1002
|
+
isAgent: true,
|
|
1003
|
+
notified: false,
|
|
1004
|
+
notifyOnCompletion: request.notifyOnCompletion,
|
|
1005
|
+
triggerOnCompletion: request.triggerOnCompletion,
|
|
1006
|
+
timeoutSeconds: request.timeoutSeconds,
|
|
1007
|
+
model: request.facts.route.qualifiedId,
|
|
1008
|
+
delegate: request.facts,
|
|
1009
|
+
waiters: [],
|
|
1010
|
+
};
|
|
1011
|
+
this.tasks.set(id, task);
|
|
1012
|
+
|
|
1013
|
+
const stream = createWriteStream(outputAbsPath, { flags: 'a', encoding: 'utf8' });
|
|
1014
|
+
task.stream = stream;
|
|
1015
|
+
stream.on('error', (error) => {
|
|
1016
|
+
task.error = `Output file write failed: ${error.message}`;
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
try {
|
|
1020
|
+
const child = this.spawn(launch.executable, piLaunchArgv(launch, [...request.argv]), {
|
|
1021
|
+
cwd: ctx.cwd,
|
|
1022
|
+
detached: this.platform !== 'win32',
|
|
1023
|
+
shell: false,
|
|
1024
|
+
// The seed travels over stdin, never as a shell or positional argument,
|
|
1025
|
+
// so the bytes the child reads are exactly the bytes that were persisted
|
|
1026
|
+
// and hashed, with no quoting or command-line length limit in the path.
|
|
1027
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1028
|
+
env: request.env,
|
|
1029
|
+
windowsHide: true,
|
|
1030
|
+
});
|
|
1031
|
+
task.child = child;
|
|
1032
|
+
task.pid = child.pid;
|
|
1033
|
+
writeDelegateStdin(child, request.stdinBytes, (error) => {
|
|
1034
|
+
this.writeNotice(task, `\n[delegate stdin write failed: ${error.message}]\n`);
|
|
1035
|
+
if (task.status === 'running') {
|
|
1036
|
+
task.killKind = 'user';
|
|
1037
|
+
task.error = `Delegate seed could not be delivered: ${error.message}`;
|
|
1038
|
+
try {
|
|
1039
|
+
this.requestKill(task, 'SIGTERM');
|
|
1040
|
+
} catch {
|
|
1041
|
+
void this.finalizeTask(task, 'failed', null, undefined, task.error);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
child.stdout?.on('data', (data) => {
|
|
1047
|
+
this.appendChildOutput(task, data, 'stdout');
|
|
1048
|
+
});
|
|
1049
|
+
child.stderr?.on('data', (data) => {
|
|
1050
|
+
this.appendChildOutput(task, data, 'stderr');
|
|
1051
|
+
});
|
|
1052
|
+
child.on('error', (error) => {
|
|
1053
|
+
this.writeNotice(task, `\n[delegate spawn error: ${error.message}]\n`);
|
|
1054
|
+
void this.finalizeTask(task, 'failed', null, undefined, error.message);
|
|
1055
|
+
});
|
|
1056
|
+
child.on('close', (code, signalName) => {
|
|
1057
|
+
let status: TaskStatus;
|
|
1058
|
+
let error: string | undefined;
|
|
1059
|
+
if (task.killKind === 'user' || task.killKind === 'shutdown') {
|
|
1060
|
+
status = 'killed';
|
|
1061
|
+
} else if (task.killKind === 'timeout') {
|
|
1062
|
+
status = 'failed';
|
|
1063
|
+
error = task.error ?? `Timed out after ${String(request.timeoutSeconds ?? 0)}s`;
|
|
1064
|
+
} else if ((code ?? 0) === 0) {
|
|
1065
|
+
status = 'completed';
|
|
1066
|
+
} else {
|
|
1067
|
+
status = 'failed';
|
|
1068
|
+
error = `Exited with code ${code === null ? 'null' : String(code)}${signalName ? ` (${signalName})` : ''}`;
|
|
1069
|
+
}
|
|
1070
|
+
void this.finalizeTask(task, status, code, signalName, error);
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
if (request.timeoutSeconds !== undefined) {
|
|
1074
|
+
task.timeoutHandle = setTimeout(() => {
|
|
1075
|
+
if (task.status !== 'running') return;
|
|
1076
|
+
task.killKind = 'timeout';
|
|
1077
|
+
task.error = `Timed out after ${String(request.timeoutSeconds)}s`;
|
|
1078
|
+
this.writeNotice(task, `\n[delegate timeout: ${task.error}]\n`);
|
|
1079
|
+
try {
|
|
1080
|
+
this.requestKill(task, 'SIGTERM');
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
void this.finalizeTask(
|
|
1083
|
+
task,
|
|
1084
|
+
'failed',
|
|
1085
|
+
null,
|
|
1086
|
+
undefined,
|
|
1087
|
+
`${task.error}; kill failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
}, request.timeoutSeconds * 1000);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
await this.writeMetadata(task);
|
|
1094
|
+
this.onChange();
|
|
1095
|
+
return task;
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1098
|
+
this.writeNotice(task, `\n[delegate spawn exception: ${message}]\n`);
|
|
1099
|
+
await this.finalizeTask(task, 'failed', null, undefined, message);
|
|
1100
|
+
throw new Error(`Failed to start delegate task: ${message}`);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
930
1104
|
async startAttestedPiTask(
|
|
931
1105
|
ctx: BackgroundTaskContext,
|
|
932
1106
|
request: StartAttestedPiTaskOptions,
|