pi-background-tasks 0.7.4 → 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 +199 -15
- package/TESTING.md +95 -1
- 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 +17 -2
- package/src/core/fusion/budget.ts +884 -210
- package/src/core/fusion/context.ts +118 -520
- package/src/core/fusion/orchestrator.ts +133 -20
- package/src/core/fusion/prompts.ts +11 -7
- package/src/core/fusion/types.ts +230 -86
- 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 +4 -0
package/src/core/common.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { open } from 'node:fs/promises';
|
|
|
3
3
|
import { extname, isAbsolute, join, win32 } from 'node:path';
|
|
4
4
|
import { DEFAULT_MAX_BYTES } from '@earendil-works/pi-coding-agent';
|
|
5
5
|
import type { BackgroundTaskChildProcess } from './registry.js';
|
|
6
|
+
import type { DelegateBudgetRouteSource } from './delegate/types.js';
|
|
6
7
|
|
|
7
8
|
export const TASK_STATUS_VALUES = ['running', 'completed', 'failed', 'killed'] as const;
|
|
8
9
|
export const TERMINAL_TASK_STATUS_VALUES = ['completed', 'failed', 'killed'] as const;
|
|
@@ -60,6 +61,7 @@ export interface BgTaskSnapshot {
|
|
|
60
61
|
model?: string | undefined;
|
|
61
62
|
telemetryUnavailableReason?: string | undefined;
|
|
62
63
|
attestationPath?: string | undefined;
|
|
64
|
+
delegate?: DelegateTaskFacts | undefined;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
export interface AttestedPiTaskFiles {
|
|
@@ -73,6 +75,30 @@ export interface AttestedPiTaskSnapshot extends BgTaskSnapshot {
|
|
|
73
75
|
attestedPi?: AttestedPiTaskFiles | undefined;
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
/** Delegate-specific task facts surfaced through snapshots and `bg_result`. */
|
|
79
|
+
export interface DelegateTaskFacts {
|
|
80
|
+
taskId: string;
|
|
81
|
+
launchNonce: string;
|
|
82
|
+
artifactDir: string;
|
|
83
|
+
artifactDirAbs: string;
|
|
84
|
+
seedSha256: string;
|
|
85
|
+
childSessionId: string;
|
|
86
|
+
route: { provider: string; model: string; qualifiedId: string };
|
|
87
|
+
budget: DelegateBudgetRouteSource;
|
|
88
|
+
autoDeliver: 'never' | 'when_small' | 'always';
|
|
89
|
+
/** Set once the run reaches a terminal state and its result has been evaluated. */
|
|
90
|
+
outcome?: DelegateTaskOutcome | undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface DelegateTaskOutcome {
|
|
94
|
+
status: 'committed' | 'failed' | 'cancelled';
|
|
95
|
+
errorCode?: string | undefined;
|
|
96
|
+
answerBytes?: number | undefined;
|
|
97
|
+
answerSha256?: string | undefined;
|
|
98
|
+
turns?: number | undefined;
|
|
99
|
+
toolCalls?: number | undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
76
102
|
export interface BgTask extends Omit<BgTaskSnapshot, 'name'> {
|
|
77
103
|
name: string;
|
|
78
104
|
outputAbsPath: string;
|
|
@@ -102,6 +128,7 @@ export interface BgTask extends Omit<BgTaskSnapshot, 'name'> {
|
|
|
102
128
|
telemetryUnavailableReason?: string | undefined;
|
|
103
129
|
attestationPath?: string | undefined;
|
|
104
130
|
attestedPi?: AttestedPiTaskFiles | undefined;
|
|
131
|
+
delegate?: DelegateTaskFacts | undefined;
|
|
105
132
|
metadataWriteChain?: Promise<void> | undefined;
|
|
106
133
|
waiters: Array<() => void>;
|
|
107
134
|
}
|
|
@@ -198,6 +225,19 @@ export interface StartTaskOptions {
|
|
|
198
225
|
terminalPublicationGate?: Promise<void> | undefined;
|
|
199
226
|
}
|
|
200
227
|
|
|
228
|
+
/** Prepared delegate launch handed to the registry after preflight has succeeded. */
|
|
229
|
+
export interface StartDelegateTaskOptions {
|
|
230
|
+
name: string;
|
|
231
|
+
argv: readonly string[];
|
|
232
|
+
/** Prompt bytes delivered over stdin, never as a shell or positional argument. */
|
|
233
|
+
stdinBytes: Buffer;
|
|
234
|
+
env: NodeJS.ProcessEnv;
|
|
235
|
+
facts: DelegateTaskFacts;
|
|
236
|
+
notifyOnCompletion: boolean;
|
|
237
|
+
triggerOnCompletion: boolean;
|
|
238
|
+
timeoutSeconds?: number | undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
201
241
|
export interface StartAttestedPiTaskOptions {
|
|
202
242
|
name: string;
|
|
203
243
|
provider: string;
|
|
@@ -687,6 +727,7 @@ export function snapshot(task: BgTask): BgTaskSnapshot {
|
|
|
687
727
|
model: task.model,
|
|
688
728
|
telemetryUnavailableReason: task.telemetryUnavailableReason,
|
|
689
729
|
attestationPath: task.attestationPath,
|
|
730
|
+
delegate: task.delegate,
|
|
690
731
|
};
|
|
691
732
|
}
|
|
692
733
|
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildSessionContext,
|
|
3
|
+
convertToLlm,
|
|
4
|
+
type SessionEntry,
|
|
5
|
+
} from '@earendil-works/pi-coding-agent';
|
|
6
|
+
import type { Message } from '@earendil-works/pi-ai';
|
|
7
|
+
import { isJsonObject, type JsonObject } from '../common.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Pi session adapter shared by every consumer of the visible-conversation
|
|
11
|
+
* transform.
|
|
12
|
+
*
|
|
13
|
+
* Responsible for exactly one thing: turning the parent's live Pi session into a
|
|
14
|
+
* frozen `Message[]` snapshot, with the in-flight tool call that requested the
|
|
15
|
+
* snapshot (and therefore its sibling calls) excluded from the branch.
|
|
16
|
+
*
|
|
17
|
+
* The transform itself lives in `visible-conversation-v2.ts` and never sees a
|
|
18
|
+
* `SessionManager`.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export interface ReadonlyParentSessionManager {
|
|
22
|
+
getLeafId(): string | null;
|
|
23
|
+
getLeafEntry(): SessionEntry | undefined;
|
|
24
|
+
getEntries(): SessionEntry[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ParentContextSource {
|
|
28
|
+
cwd: string;
|
|
29
|
+
sessionManager: ReadonlyParentSessionManager;
|
|
30
|
+
getSystemPrompt(): string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ParentSnapshotOptions {
|
|
34
|
+
/** Tool call currently executing, when the snapshot is requested from a tool. */
|
|
35
|
+
toolCallId?: string | undefined;
|
|
36
|
+
/** Tool name used for leaf matching when no explicit call id is available. */
|
|
37
|
+
toolName: string;
|
|
38
|
+
/** Commands have no in-flight tool call and therefore never exclude a leaf. */
|
|
39
|
+
excludeActiveToolCallLeaf: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ParentSnapshot {
|
|
43
|
+
messages: readonly Message[];
|
|
44
|
+
leafId: string | null;
|
|
45
|
+
activeToolCallLeafExcluded: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function entriesById(entries: readonly SessionEntry[]): Map<string, SessionEntry> {
|
|
49
|
+
const byId = new Map<string, SessionEntry>();
|
|
50
|
+
for (const entry of entries) byId.set(entry.id, entry);
|
|
51
|
+
return byId;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readArray(record: JsonObject, key: string): readonly unknown[] | undefined {
|
|
55
|
+
const value = record[key];
|
|
56
|
+
return Array.isArray(value) ? value : undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function recordOf(value: unknown): JsonObject | undefined {
|
|
60
|
+
if (!isJsonObject(value) || Array.isArray(value)) return undefined;
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function entryMessage(entry: SessionEntry): JsonObject | undefined {
|
|
65
|
+
if (entry.type !== 'message') return undefined;
|
|
66
|
+
return recordOf(entry.message);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function toolCallPartMatches(
|
|
70
|
+
part: unknown,
|
|
71
|
+
toolCallId: string | undefined,
|
|
72
|
+
toolName: string,
|
|
73
|
+
): boolean {
|
|
74
|
+
const record = recordOf(part);
|
|
75
|
+
if (record === undefined || record['type'] !== 'toolCall') return false;
|
|
76
|
+
if (toolCallId !== undefined) return record['id'] === toolCallId;
|
|
77
|
+
return record['name'] === toolName;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function messageContainsToolCall(
|
|
81
|
+
message: JsonObject,
|
|
82
|
+
toolCallId: string | undefined,
|
|
83
|
+
toolName: string,
|
|
84
|
+
): boolean {
|
|
85
|
+
if (message['role'] !== 'assistant') return false;
|
|
86
|
+
const content = readArray(message, 'content');
|
|
87
|
+
if (content === undefined) return false;
|
|
88
|
+
for (const part of content) {
|
|
89
|
+
if (toolCallPartMatches(part, toolCallId, toolName)) return true;
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface EffectiveLeaf {
|
|
95
|
+
leafId: string | null;
|
|
96
|
+
activeToolCallLeafExcluded: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function effectiveLeafForTool(
|
|
100
|
+
sessionManager: ReadonlyParentSessionManager,
|
|
101
|
+
toolCallId: string | undefined,
|
|
102
|
+
toolName: string,
|
|
103
|
+
): EffectiveLeaf {
|
|
104
|
+
const leaf = sessionManager.getLeafEntry();
|
|
105
|
+
if (leaf === undefined)
|
|
106
|
+
return { leafId: sessionManager.getLeafId(), activeToolCallLeafExcluded: false };
|
|
107
|
+
const message = entryMessage(leaf);
|
|
108
|
+
if (message !== undefined && messageContainsToolCall(message, toolCallId, toolName)) {
|
|
109
|
+
return { leafId: leaf.parentId, activeToolCallLeafExcluded: true };
|
|
110
|
+
}
|
|
111
|
+
return { leafId: sessionManager.getLeafId(), activeToolCallLeafExcluded: false };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function resolveEffectiveLeaf(
|
|
115
|
+
sessionManager: ReadonlyParentSessionManager,
|
|
116
|
+
options: ParentSnapshotOptions,
|
|
117
|
+
): EffectiveLeaf {
|
|
118
|
+
if (!options.excludeActiveToolCallLeaf)
|
|
119
|
+
return { leafId: sessionManager.getLeafId(), activeToolCallLeafExcluded: false };
|
|
120
|
+
return effectiveLeafForTool(sessionManager, options.toolCallId, options.toolName);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Freeze the parent conversation into LLM messages.
|
|
125
|
+
*
|
|
126
|
+
* Callers must complete every downstream use of the returned snapshot without
|
|
127
|
+
* re-reading the session, so the seed cannot drift while a child is being
|
|
128
|
+
* launched.
|
|
129
|
+
*/
|
|
130
|
+
export function snapshotParentConversation(
|
|
131
|
+
ctx: ParentContextSource,
|
|
132
|
+
options: ParentSnapshotOptions,
|
|
133
|
+
): ParentSnapshot {
|
|
134
|
+
const entries = ctx.sessionManager.getEntries();
|
|
135
|
+
const leaf = resolveEffectiveLeaf(ctx.sessionManager, options);
|
|
136
|
+
const sessionContext = buildSessionContext(entries, leaf.leafId, entriesById(entries));
|
|
137
|
+
return {
|
|
138
|
+
messages: convertToLlm(sessionContext.messages),
|
|
139
|
+
leafId: leaf.leafId,
|
|
140
|
+
activeToolCallLeafExcluded: leaf.activeToolCallLeafExcluded,
|
|
141
|
+
};
|
|
142
|
+
}
|