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.
@@ -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
+ }