praxis-agent 0.33.0 → 0.35.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/README.md +4 -0
- package/dist/application/background-agent-manager.d.ts +48 -3
- package/dist/application/background-agent-manager.js +249 -73
- package/dist/application/background-task-runtime.d.ts +20 -1
- package/dist/application/background-task-runtime.js +45 -6
- package/dist/application/managed-worktree.d.ts +8 -0
- package/dist/application/managed-worktree.js +54 -2
- package/dist/application/session-service.d.ts +12 -1
- package/dist/application/session-service.js +1849 -1628
- package/dist/application/subagent-service.d.ts +34 -7
- package/dist/application/subagent-service.js +720 -150
- package/dist/application/turn-lifecycle.d.ts +38 -0
- package/dist/application/turn-lifecycle.js +36 -0
- package/dist/cli/interactive.d.ts +1 -0
- package/dist/cli/interactive.js +43 -8
- package/dist/cli/protocol.js +5 -2
- package/dist/cli/tui/claude-style.d.ts +5 -48
- package/dist/cli/tui/claude-style.js +19 -73
- package/dist/cli/tui/task-panel.d.ts +1 -1
- package/dist/cli/tui/task-panel.js +2 -1
- package/dist/cli/tui/transcript-presentation.d.ts +89 -0
- package/dist/cli/tui/transcript-presentation.js +169 -0
- package/dist/cli/tui/transcript-viewport.d.ts +5 -34
- package/dist/cli/tui/transcript-viewport.js +639 -158
- package/dist/cli/tui/tui-view-model.d.ts +6 -2
- package/dist/cli/tui/tui-view-model.js +23 -6
- package/dist/compatibility/claude/context.d.ts +2 -2
- package/dist/compatibility/claude/context.js +2 -14
- package/dist/compatibility/claude/data-plane-adapter.d.ts +9 -0
- package/dist/compatibility/claude/data-plane-adapter.js +44 -0
- package/dist/compatibility/claude/paths.js +2 -2
- package/dist/compatibility/claude/projection.d.ts +4 -0
- package/dist/compatibility/claude/projection.js +82 -0
- package/dist/compatibility/claude/shared-resources.d.ts +7 -38
- package/dist/compatibility/claude/sidechain.d.ts +5 -0
- package/dist/compatibility/claude/sidechain.js +13 -4
- package/dist/compatibility/claude/transcript-codec.d.ts +21 -0
- package/dist/compatibility/claude/transcript-codec.js +512 -0
- package/dist/core/context.d.ts +25 -7
- package/dist/core/context.js +46 -0
- package/dist/core/prompt-composer.d.ts +8 -16
- package/dist/core/prompt-composer.js +65 -25
- package/dist/core/resources.d.ts +39 -0
- package/dist/core/resources.js +2 -0
- package/dist/core/runtime.d.ts +1 -1
- package/dist/core/session.d.ts +2 -0
- package/dist/core/session.js +5 -0
- package/dist/core/transcript-codec.d.ts +70 -0
- package/dist/core/transcript-codec.js +60 -0
- package/dist/core/transcript-event.d.ts +33 -0
- package/dist/core/transcript-event.js +198 -0
- package/dist/persistence/claude-sidechain-store.js +19 -3
- package/dist/persistence/data-plane-adapter.d.ts +32 -0
- package/dist/persistence/data-plane-adapter.js +2 -0
- package/dist/persistence/data-plane.d.ts +4 -20
- package/dist/persistence/data-plane.js +10 -47
- package/dist/persistence/native-data-plane-adapter.d.ts +9 -0
- package/dist/persistence/native-data-plane-adapter.js +39 -0
- package/dist/persistence/native-resources.d.ts +4 -4
- package/dist/persistence/native-transcript-codec.d.ts +13 -0
- package/dist/persistence/native-transcript-codec.js +102 -0
- package/dist/persistence/subagent-lifecycle-store.d.ts +66 -0
- package/dist/persistence/subagent-lifecycle-store.js +299 -0
- package/dist/tools/claude-user-message.d.ts +0 -1
- package/dist/tools/claude-user-message.js +0 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -236,6 +236,10 @@ npm ci
|
|
|
236
236
|
npm run check
|
|
237
237
|
```
|
|
238
238
|
|
|
239
|
+
`npm run build:native` compiles the current Praxis-owned core, provider
|
|
240
|
+
adapters, and native data-plane slice without the Claude compatibility adapter.
|
|
241
|
+
`npm run check` also enforces the corresponding source dependency direction.
|
|
242
|
+
|
|
239
243
|
Contributions use Conventional Commit pull-request titles and the protected
|
|
240
244
|
squash-merge workflow. Read
|
|
241
245
|
[CONTRIBUTING.md](https://github.com/Forest-Isle/Praxis/blob/main/CONTRIBUTING.md)
|
|
@@ -10,11 +10,15 @@ export interface BackgroundAgentRunResult {
|
|
|
10
10
|
isolationPath?: string;
|
|
11
11
|
isolationRetained?: boolean;
|
|
12
12
|
isolationWarning?: string;
|
|
13
|
+
notificationId?: string;
|
|
13
14
|
}
|
|
14
15
|
export declare class BackgroundAgentRunError extends Error {
|
|
15
16
|
readonly result?: BackgroundAgentRunResult | undefined;
|
|
16
17
|
constructor(message: string, result?: BackgroundAgentRunResult | undefined, cause?: unknown);
|
|
17
18
|
}
|
|
19
|
+
export declare class BackgroundAgentShutdownError extends Error {
|
|
20
|
+
constructor();
|
|
21
|
+
}
|
|
18
22
|
export interface BackgroundAgentTaskSpec {
|
|
19
23
|
agentId: string;
|
|
20
24
|
name?: string;
|
|
@@ -24,9 +28,18 @@ export interface BackgroundAgentTaskSpec {
|
|
|
24
28
|
toolUseId: string;
|
|
25
29
|
outputFile: string;
|
|
26
30
|
resolvedModel: string;
|
|
27
|
-
|
|
31
|
+
markBackground?(): void;
|
|
32
|
+
acknowledgeNotification?(notificationId: string): Promise<void>;
|
|
33
|
+
prepareNotificationDetached?(notificationId: string, model: string): Promise<void>;
|
|
34
|
+
confirmNotificationDetached?(notificationId: string): Promise<void>;
|
|
35
|
+
run(message: string, signal: AbortSignal, continuation: boolean, toolUseId: string): Promise<BackgroundAgentRunResult>;
|
|
36
|
+
}
|
|
37
|
+
type BackgroundAgentStatus = 'running' | 'completed' | 'failed' | 'stopped' | 'interrupted';
|
|
38
|
+
export interface BackgroundAgentNotificationIdentity {
|
|
39
|
+
agentId: string;
|
|
40
|
+
toolUseId: string;
|
|
41
|
+
status: 'completed' | 'failed' | 'killed';
|
|
28
42
|
}
|
|
29
|
-
type BackgroundAgentStatus = 'running' | 'completed' | 'failed' | 'stopped';
|
|
30
43
|
export interface BackgroundAgentSnapshot {
|
|
31
44
|
agentId: string;
|
|
32
45
|
status: BackgroundAgentStatus;
|
|
@@ -38,13 +51,33 @@ export interface BackgroundAgentSnapshot {
|
|
|
38
51
|
startedAt: number;
|
|
39
52
|
durationMs: number | null;
|
|
40
53
|
}
|
|
54
|
+
export declare function backgroundAgentNotificationMarkers(notification: BackgroundAgentNotificationIdentity): readonly string[];
|
|
41
55
|
export declare class BackgroundAgentManager {
|
|
42
56
|
private readonly tasks;
|
|
43
57
|
private readonly names;
|
|
58
|
+
private readonly closedSignal;
|
|
59
|
+
private resolveClosed;
|
|
44
60
|
private closed;
|
|
61
|
+
constructor();
|
|
45
62
|
launch(spec: BackgroundAgentTaskSpec): BackgroundAgentSnapshot;
|
|
63
|
+
adopt(options: {
|
|
64
|
+
spec: BackgroundAgentTaskSpec;
|
|
65
|
+
controller: AbortController;
|
|
66
|
+
operation: Promise<BackgroundAgentRunResult>;
|
|
67
|
+
startedAt: number;
|
|
68
|
+
}): BackgroundAgentSnapshot;
|
|
46
69
|
registerCompleted(spec: BackgroundAgentTaskSpec, result: BackgroundAgentRunResult): BackgroundAgentSnapshot;
|
|
70
|
+
registerInterrupted(spec: BackgroundAgentTaskSpec, error?: string): BackgroundAgentSnapshot;
|
|
71
|
+
registerTerminal(spec: BackgroundAgentTaskSpec, status: 'failed' | 'stopped', error: string): BackgroundAgentSnapshot;
|
|
72
|
+
registerPersistedNotification(agentId: string, notification: {
|
|
73
|
+
id: string;
|
|
74
|
+
status: 'completed' | 'failed' | 'stopped';
|
|
75
|
+
result: BackgroundAgentRunResult | null;
|
|
76
|
+
error: string | null;
|
|
77
|
+
toolUseId: string;
|
|
78
|
+
}): void;
|
|
47
79
|
has(agentId: string): boolean;
|
|
80
|
+
notificationClaimAgentIds(): string[];
|
|
48
81
|
snapshotById(agentId: string): BackgroundAgentSnapshot | null;
|
|
49
82
|
snapshots(): readonly BackgroundAgentSnapshot[];
|
|
50
83
|
output(agentId: string, options: {
|
|
@@ -52,11 +85,14 @@ export declare class BackgroundAgentManager {
|
|
|
52
85
|
timeout: number;
|
|
53
86
|
}): Promise<string>;
|
|
54
87
|
stop(agentId: string): string;
|
|
55
|
-
|
|
88
|
+
stopAndWait(agentId: string, timeout?: number): Promise<string>;
|
|
89
|
+
stopAll(): readonly string[];
|
|
90
|
+
close(drainMilliseconds?: number): Promise<void>;
|
|
56
91
|
send(agentId: string, message: string, summary: string | undefined, toolUseId: string): string;
|
|
57
92
|
notifications(options: {
|
|
58
93
|
waitForRunning: boolean;
|
|
59
94
|
excludeAgentId?: string;
|
|
95
|
+
consume?: boolean;
|
|
60
96
|
}): Promise<{
|
|
61
97
|
messages: string[];
|
|
62
98
|
usage: ModelUsage;
|
|
@@ -64,8 +100,17 @@ export declare class BackgroundAgentManager {
|
|
|
64
100
|
durationApiMs?: number;
|
|
65
101
|
durationApiWithoutRetriesMs?: number;
|
|
66
102
|
}>;
|
|
103
|
+
acknowledge(messages: readonly string[]): Promise<void>;
|
|
104
|
+
prepareNotificationsDetached(messages: readonly string[]): Promise<void>;
|
|
105
|
+
confirmNotificationsDetached(messages: readonly string[]): Promise<void>;
|
|
106
|
+
private forEachMatchingNotification;
|
|
107
|
+
acknowledgeDelivered(delivered: (notification: BackgroundAgentNotificationIdentity) => boolean): Promise<void>;
|
|
108
|
+
acknowledgeDeliveredAsDetached(delivered: (notification: BackgroundAgentNotificationIdentity) => boolean): Promise<void>;
|
|
67
109
|
private start;
|
|
110
|
+
private track;
|
|
111
|
+
private registerTask;
|
|
68
112
|
private resolveOptional;
|
|
113
|
+
private stopTask;
|
|
69
114
|
private assertNameAvailable;
|
|
70
115
|
private finishStopped;
|
|
71
116
|
private resolveRequired;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { isClaudeAgentId } from '../compatibility/claude/sidechain.js';
|
|
2
3
|
const MAX_TIMEOUT_MS = 600_000;
|
|
4
|
+
const DEFAULT_CLOSE_DRAIN_MS = 5_000;
|
|
3
5
|
export class BackgroundAgentRunError extends Error {
|
|
4
6
|
result;
|
|
5
7
|
constructor(message, result, cause) {
|
|
@@ -8,8 +10,14 @@ export class BackgroundAgentRunError extends Error {
|
|
|
8
10
|
this.name = 'BackgroundAgentRunError';
|
|
9
11
|
}
|
|
10
12
|
}
|
|
13
|
+
export class BackgroundAgentShutdownError extends Error {
|
|
14
|
+
constructor() {
|
|
15
|
+
super('Background agent manager closed');
|
|
16
|
+
this.name = 'BackgroundAgentShutdownError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
11
19
|
function assertAgentId(agentId) {
|
|
12
|
-
if (!
|
|
20
|
+
if (!isClaudeAgentId(agentId)) {
|
|
13
21
|
throw new Error(`Invalid background agent ID: ${agentId}`);
|
|
14
22
|
}
|
|
15
23
|
}
|
|
@@ -21,6 +29,13 @@ function escapeXml(value) {
|
|
|
21
29
|
.replaceAll('"', '"')
|
|
22
30
|
.replaceAll("'", ''');
|
|
23
31
|
}
|
|
32
|
+
export function backgroundAgentNotificationMarkers(notification) {
|
|
33
|
+
return [
|
|
34
|
+
`<task-id>${notification.agentId}</task-id>`,
|
|
35
|
+
`<tool-use-id>${escapeXml(notification.toolUseId)}</tool-use-id>`,
|
|
36
|
+
`<status>${notification.status}</status>`,
|
|
37
|
+
];
|
|
38
|
+
}
|
|
24
39
|
function waitBounded(operation, milliseconds) {
|
|
25
40
|
return new Promise((resolve, reject) => {
|
|
26
41
|
const timer = setTimeout(resolve, milliseconds);
|
|
@@ -149,71 +164,95 @@ function mergeToolModelUsage(map, breakdown) {
|
|
|
149
164
|
export class BackgroundAgentManager {
|
|
150
165
|
tasks = new Map();
|
|
151
166
|
names = new Map();
|
|
167
|
+
closedSignal;
|
|
168
|
+
resolveClosed;
|
|
152
169
|
closed = false;
|
|
170
|
+
constructor() {
|
|
171
|
+
this.closedSignal = new Promise((resolve) => {
|
|
172
|
+
this.resolveClosed = resolve;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
153
175
|
launch(spec) {
|
|
154
|
-
|
|
155
|
-
throw new Error('Background agent manager is closed');
|
|
156
|
-
assertAgentId(spec.agentId);
|
|
157
|
-
if (spec.name !== undefined &&
|
|
158
|
-
!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/u.test(spec.name)) {
|
|
159
|
-
throw new Error(`Invalid background agent name: ${spec.name}`);
|
|
160
|
-
}
|
|
161
|
-
if (this.tasks.has(spec.agentId)) {
|
|
162
|
-
throw new Error(`Background agent ${spec.agentId} already exists`);
|
|
163
|
-
}
|
|
164
|
-
this.assertNameAvailable(spec.name, spec.agentId);
|
|
165
|
-
if (spec.name !== undefined)
|
|
166
|
-
this.names.set(spec.name, spec.agentId);
|
|
167
|
-
const task = {
|
|
168
|
-
spec,
|
|
176
|
+
const { task } = this.registerTask(spec, {
|
|
169
177
|
status: 'running',
|
|
170
178
|
controller: null,
|
|
171
|
-
promise: null,
|
|
172
179
|
result: null,
|
|
173
180
|
error: null,
|
|
174
|
-
notifications: [],
|
|
175
181
|
generation: 0,
|
|
176
|
-
queuedMessages: [],
|
|
177
182
|
startedAt: Date.now(),
|
|
178
183
|
durationMs: null,
|
|
179
|
-
};
|
|
180
|
-
|
|
184
|
+
}, 'throw');
|
|
185
|
+
spec.markBackground?.();
|
|
181
186
|
this.start(task, spec.prompt, false, spec.toolUseId);
|
|
182
187
|
return this.snapshot(task);
|
|
183
188
|
}
|
|
189
|
+
adopt(options) {
|
|
190
|
+
const { spec } = options;
|
|
191
|
+
const { task } = this.registerTask(spec, {
|
|
192
|
+
status: 'running',
|
|
193
|
+
controller: options.controller,
|
|
194
|
+
result: null,
|
|
195
|
+
error: null,
|
|
196
|
+
generation: 1,
|
|
197
|
+
startedAt: options.startedAt,
|
|
198
|
+
durationMs: null,
|
|
199
|
+
}, 'throw');
|
|
200
|
+
spec.markBackground?.();
|
|
201
|
+
this.track(task, options.operation, 1, spec.toolUseId);
|
|
202
|
+
return this.snapshot(task);
|
|
203
|
+
}
|
|
184
204
|
registerCompleted(spec, result) {
|
|
185
|
-
|
|
186
|
-
throw new Error('Background agent manager is closed');
|
|
187
|
-
assertAgentId(spec.agentId);
|
|
188
|
-
if (spec.name !== undefined &&
|
|
189
|
-
!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/u.test(spec.name)) {
|
|
190
|
-
throw new Error(`Invalid background agent name: ${spec.name}`);
|
|
191
|
-
}
|
|
192
|
-
const existing = this.tasks.get(spec.agentId);
|
|
193
|
-
if (existing)
|
|
194
|
-
return this.snapshot(existing);
|
|
195
|
-
this.assertNameAvailable(spec.name, spec.agentId);
|
|
196
|
-
if (spec.name !== undefined)
|
|
197
|
-
this.names.set(spec.name, spec.agentId);
|
|
198
|
-
const task = {
|
|
199
|
-
spec,
|
|
205
|
+
const { task } = this.registerTask(spec, {
|
|
200
206
|
status: 'completed',
|
|
201
207
|
controller: null,
|
|
202
|
-
promise: null,
|
|
203
208
|
result,
|
|
204
209
|
error: null,
|
|
205
|
-
notifications: [],
|
|
206
210
|
generation: 0,
|
|
207
|
-
queuedMessages: [],
|
|
208
211
|
startedAt: Date.now() - result.durationMs,
|
|
209
212
|
durationMs: result.durationMs,
|
|
210
|
-
};
|
|
211
|
-
this.
|
|
213
|
+
}, 'return-existing');
|
|
214
|
+
return this.snapshot(task);
|
|
215
|
+
}
|
|
216
|
+
registerInterrupted(spec, error = 'Persisted agent was interrupted before completion') {
|
|
217
|
+
const { task } = this.registerTask(spec, {
|
|
218
|
+
status: 'interrupted',
|
|
219
|
+
controller: null,
|
|
220
|
+
result: null,
|
|
221
|
+
error,
|
|
222
|
+
generation: 0,
|
|
223
|
+
startedAt: Date.now(),
|
|
224
|
+
durationMs: 0,
|
|
225
|
+
}, 'return-existing');
|
|
212
226
|
return this.snapshot(task);
|
|
213
227
|
}
|
|
228
|
+
registerTerminal(spec, status, error) {
|
|
229
|
+
const { task } = this.registerTask(spec, {
|
|
230
|
+
status,
|
|
231
|
+
controller: null,
|
|
232
|
+
result: null,
|
|
233
|
+
error,
|
|
234
|
+
generation: 0,
|
|
235
|
+
startedAt: Date.now(),
|
|
236
|
+
durationMs: 0,
|
|
237
|
+
}, 'return-existing');
|
|
238
|
+
return this.snapshot(task);
|
|
239
|
+
}
|
|
240
|
+
registerPersistedNotification(agentId, notification) {
|
|
241
|
+
const task = this.tasks.get(agentId);
|
|
242
|
+
if (!task)
|
|
243
|
+
throw new Error(`No task found with ID: ${agentId}`);
|
|
244
|
+
if (task.notifications.some(({ id }) => id === notification.id))
|
|
245
|
+
return;
|
|
246
|
+
task.notifications.push(notification);
|
|
247
|
+
}
|
|
214
248
|
has(agentId) {
|
|
215
249
|
return this.tasks.has(this.resolveOptional(agentId) ?? agentId);
|
|
216
250
|
}
|
|
251
|
+
notificationClaimAgentIds() {
|
|
252
|
+
return [...this.tasks.entries()]
|
|
253
|
+
.filter(([, task]) => task.status === 'running' || task.notifications.length > 0)
|
|
254
|
+
.map(([agentId]) => agentId);
|
|
255
|
+
}
|
|
217
256
|
snapshotById(agentId) {
|
|
218
257
|
const task = this.tasks.get(agentId);
|
|
219
258
|
return task ? this.snapshot(task) : null;
|
|
@@ -254,32 +293,52 @@ export class BackgroundAgentManager {
|
|
|
254
293
|
const task = this.tasks.get(agentId);
|
|
255
294
|
if (!task)
|
|
256
295
|
throw new Error(`No task found with ID: ${agentId}`);
|
|
257
|
-
|
|
258
|
-
throw new Error(`Task ${agentId} is not running (status: ${task.status})`);
|
|
259
|
-
}
|
|
260
|
-
task.status = 'stopped';
|
|
261
|
-
task.error = 'Stopped by TaskStop';
|
|
262
|
-
task.durationMs ??= Date.now() - task.startedAt;
|
|
263
|
-
task.queuedMessages.length = 0;
|
|
264
|
-
task.controller.abort();
|
|
296
|
+
this.stopTask(task, 'Stopped by TaskStop');
|
|
265
297
|
return `Task ${agentId} stopped successfully`;
|
|
266
298
|
}
|
|
267
|
-
async
|
|
299
|
+
async stopAndWait(agentId, timeout = DEFAULT_CLOSE_DRAIN_MS) {
|
|
300
|
+
const message = this.stop(agentId);
|
|
301
|
+
const task = this.tasks.get(agentId);
|
|
302
|
+
if (task?.promise)
|
|
303
|
+
await waitBounded(task.promise, timeout);
|
|
304
|
+
return message;
|
|
305
|
+
}
|
|
306
|
+
stopAll() {
|
|
307
|
+
const stopped = [];
|
|
308
|
+
for (const task of this.tasks.values()) {
|
|
309
|
+
if (task.status !== 'running' || !task.controller)
|
|
310
|
+
continue;
|
|
311
|
+
this.stopTask(task, 'Stopped by explicit bulk kill');
|
|
312
|
+
stopped.push(task.spec.agentId);
|
|
313
|
+
}
|
|
314
|
+
return stopped;
|
|
315
|
+
}
|
|
316
|
+
async close(drainMilliseconds = DEFAULT_CLOSE_DRAIN_MS) {
|
|
268
317
|
if (this.closed)
|
|
269
318
|
return;
|
|
319
|
+
if (!Number.isFinite(drainMilliseconds) ||
|
|
320
|
+
drainMilliseconds < 0 ||
|
|
321
|
+
drainMilliseconds > MAX_TIMEOUT_MS) {
|
|
322
|
+
throw new Error(`close drain must be between 0 and ${MAX_TIMEOUT_MS} milliseconds`);
|
|
323
|
+
}
|
|
270
324
|
this.closed = true;
|
|
325
|
+
this.resolveClosed();
|
|
271
326
|
const running = [];
|
|
272
327
|
for (const task of this.tasks.values()) {
|
|
273
|
-
if (task.status
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
328
|
+
if (task.status === 'running' && task.controller) {
|
|
329
|
+
task.status = 'stopped';
|
|
330
|
+
task.error = 'Stopped because the background agent manager closed';
|
|
331
|
+
task.suppressNotifications = true;
|
|
332
|
+
task.notifications.length = 0;
|
|
333
|
+
task.queuedMessages.length = 0;
|
|
334
|
+
task.controller.abort(new BackgroundAgentShutdownError());
|
|
335
|
+
}
|
|
279
336
|
if (task.promise)
|
|
280
337
|
running.push(task.promise);
|
|
281
338
|
}
|
|
282
|
-
await Promise.allSettled(running);
|
|
339
|
+
await waitBounded(Promise.allSettled(running).then(() => undefined), drainMilliseconds);
|
|
340
|
+
this.tasks.clear();
|
|
341
|
+
this.names.clear();
|
|
283
342
|
}
|
|
284
343
|
send(agentId, message, summary, toolUseId) {
|
|
285
344
|
if (message.trim().length === 0)
|
|
@@ -317,8 +376,9 @@ export class BackgroundAgentManager {
|
|
|
317
376
|
.filter((task) => task.status === 'running')
|
|
318
377
|
.map((task) => task.promise)
|
|
319
378
|
.filter((promise) => promise !== null);
|
|
320
|
-
if (running.length > 0)
|
|
321
|
-
await Promise.race(running);
|
|
379
|
+
if (running.length > 0) {
|
|
380
|
+
await Promise.race([Promise.race(running), this.closedSignal]);
|
|
381
|
+
}
|
|
322
382
|
}
|
|
323
383
|
const notifications = [];
|
|
324
384
|
let usage = { inputTokens: 0, outputTokens: 0 };
|
|
@@ -352,16 +412,86 @@ export class BackgroundAgentManager {
|
|
|
352
412
|
const modelUsage = modelUsageByModel.size === 0
|
|
353
413
|
? undefined
|
|
354
414
|
: Object.fromEntries(modelUsageByModel);
|
|
415
|
+
if (options.consume !== false) {
|
|
416
|
+
for (const task of consumedTasks) {
|
|
417
|
+
if (!task.spec.acknowledgeNotification)
|
|
418
|
+
continue;
|
|
419
|
+
for (const notification of task.notifications) {
|
|
420
|
+
await task.spec.acknowledgeNotification(notification.id);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
355
424
|
const result = {
|
|
356
425
|
messages: notifications,
|
|
357
426
|
usage,
|
|
358
427
|
...(modelUsage === undefined ? {} : { modelUsage }),
|
|
359
428
|
...(durationSeen ? { durationApiMs, durationApiWithoutRetriesMs } : {}),
|
|
360
429
|
};
|
|
361
|
-
|
|
362
|
-
task
|
|
430
|
+
if (options.consume !== false) {
|
|
431
|
+
for (const task of consumedTasks)
|
|
432
|
+
task.notifications.splice(0);
|
|
433
|
+
}
|
|
363
434
|
return result;
|
|
364
435
|
}
|
|
436
|
+
async acknowledge(messages) {
|
|
437
|
+
await this.forEachMatchingNotification(messages, async (task, notification) => {
|
|
438
|
+
await task.spec.acknowledgeNotification?.(notification.id);
|
|
439
|
+
task.notifications.splice(task.notifications.indexOf(notification), 1);
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
async prepareNotificationsDetached(messages) {
|
|
443
|
+
await this.forEachMatchingNotification(messages, async (task, notification) => {
|
|
444
|
+
await task.spec.prepareNotificationDetached?.(notification.id, task.spec.resolvedModel);
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
async confirmNotificationsDetached(messages) {
|
|
448
|
+
await this.forEachMatchingNotification(messages, async (task, notification) => {
|
|
449
|
+
await task.spec.confirmNotificationDetached?.(notification.id);
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
async forEachMatchingNotification(messages, operation) {
|
|
453
|
+
const remaining = [...messages];
|
|
454
|
+
for (const task of this.tasks.values()) {
|
|
455
|
+
for (const notification of [...task.notifications]) {
|
|
456
|
+
const message = this.formatNotification(task, notification);
|
|
457
|
+
const index = remaining.indexOf(message);
|
|
458
|
+
if (index < 0)
|
|
459
|
+
continue;
|
|
460
|
+
await operation(task, notification);
|
|
461
|
+
remaining.splice(index, 1);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async acknowledgeDelivered(delivered) {
|
|
466
|
+
for (const [agentId, task] of this.tasks) {
|
|
467
|
+
for (const notification of [...task.notifications]) {
|
|
468
|
+
if (notification.status === 'interrupted')
|
|
469
|
+
continue;
|
|
470
|
+
const status = notification.status === 'stopped' ? 'killed' : notification.status;
|
|
471
|
+
if (!delivered({ agentId, toolUseId: notification.toolUseId, status })) {
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
await task.spec.acknowledgeNotification?.(notification.id);
|
|
475
|
+
task.notifications.splice(task.notifications.indexOf(notification), 1);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
async acknowledgeDeliveredAsDetached(delivered) {
|
|
480
|
+
for (const [agentId, task] of this.tasks) {
|
|
481
|
+
for (const notification of [...task.notifications]) {
|
|
482
|
+
if (notification.status === 'interrupted')
|
|
483
|
+
continue;
|
|
484
|
+
const status = notification.status === 'stopped' ? 'killed' : notification.status;
|
|
485
|
+
if (!delivered({ agentId, toolUseId: notification.toolUseId, status })) {
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
await task.spec.prepareNotificationDetached?.(notification.id, task.spec.resolvedModel);
|
|
489
|
+
await task.spec.confirmNotificationDetached?.(notification.id);
|
|
490
|
+
await task.spec.acknowledgeNotification?.(notification.id);
|
|
491
|
+
task.notifications.splice(task.notifications.indexOf(notification), 1);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
365
495
|
start(task, message, continuation, toolUseId) {
|
|
366
496
|
task.generation += 1;
|
|
367
497
|
const generation = task.generation;
|
|
@@ -372,8 +502,10 @@ export class BackgroundAgentManager {
|
|
|
372
502
|
task.controller = controller;
|
|
373
503
|
task.result = null;
|
|
374
504
|
task.error = null;
|
|
375
|
-
task
|
|
376
|
-
|
|
505
|
+
this.track(task, task.spec.run(message, controller.signal, continuation, toolUseId), generation, toolUseId);
|
|
506
|
+
}
|
|
507
|
+
track(task, operation, generation, toolUseId) {
|
|
508
|
+
task.promise = operation
|
|
377
509
|
.then((result) => {
|
|
378
510
|
if (task.generation !== generation)
|
|
379
511
|
return;
|
|
@@ -386,6 +518,7 @@ export class BackgroundAgentManager {
|
|
|
386
518
|
task.durationMs = result.durationMs;
|
|
387
519
|
task.error = null;
|
|
388
520
|
task.notifications.push({
|
|
521
|
+
id: result.notificationId ?? randomUUID(),
|
|
389
522
|
status: 'completed',
|
|
390
523
|
result,
|
|
391
524
|
error: null,
|
|
@@ -406,8 +539,9 @@ export class BackgroundAgentManager {
|
|
|
406
539
|
failedResult?.durationMs ?? Date.now() - task.startedAt;
|
|
407
540
|
task.error = error instanceof Error ? error.message : String(error);
|
|
408
541
|
task.notifications.push({
|
|
542
|
+
id: failedResult?.notificationId ?? randomUUID(),
|
|
409
543
|
status: 'failed',
|
|
410
|
-
result: null,
|
|
544
|
+
result: failedResult ?? null,
|
|
411
545
|
error: task.error,
|
|
412
546
|
toolUseId,
|
|
413
547
|
});
|
|
@@ -423,11 +557,50 @@ export class BackgroundAgentManager {
|
|
|
423
557
|
}
|
|
424
558
|
});
|
|
425
559
|
}
|
|
560
|
+
registerTask(spec, state, duplicate) {
|
|
561
|
+
if (this.closed)
|
|
562
|
+
throw new Error('Background agent manager is closed');
|
|
563
|
+
assertAgentId(spec.agentId);
|
|
564
|
+
if (spec.name !== undefined &&
|
|
565
|
+
!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/u.test(spec.name)) {
|
|
566
|
+
throw new Error(`Invalid background agent name: ${spec.name}`);
|
|
567
|
+
}
|
|
568
|
+
const existing = this.tasks.get(spec.agentId);
|
|
569
|
+
if (existing) {
|
|
570
|
+
if (duplicate === 'throw') {
|
|
571
|
+
throw new Error(`Background agent ${spec.agentId} already exists`);
|
|
572
|
+
}
|
|
573
|
+
return { task: existing, created: false };
|
|
574
|
+
}
|
|
575
|
+
this.assertNameAvailable(spec.name, spec.agentId);
|
|
576
|
+
if (spec.name !== undefined)
|
|
577
|
+
this.names.set(spec.name, spec.agentId);
|
|
578
|
+
const task = {
|
|
579
|
+
spec,
|
|
580
|
+
...state,
|
|
581
|
+
promise: null,
|
|
582
|
+
notifications: [],
|
|
583
|
+
queuedMessages: [],
|
|
584
|
+
suppressNotifications: false,
|
|
585
|
+
};
|
|
586
|
+
this.tasks.set(spec.agentId, task);
|
|
587
|
+
return { task, created: true };
|
|
588
|
+
}
|
|
426
589
|
resolveOptional(identifier) {
|
|
427
|
-
if (
|
|
590
|
+
if (isClaudeAgentId(identifier))
|
|
428
591
|
return identifier;
|
|
429
592
|
return this.names.get(identifier);
|
|
430
593
|
}
|
|
594
|
+
stopTask(task, reason) {
|
|
595
|
+
if (task.status !== 'running' || !task.controller) {
|
|
596
|
+
throw new Error(`Task ${task.spec.agentId} is not running (status: ${task.status})`);
|
|
597
|
+
}
|
|
598
|
+
task.status = 'stopped';
|
|
599
|
+
task.error = reason;
|
|
600
|
+
task.durationMs ??= Date.now() - task.startedAt;
|
|
601
|
+
task.queuedMessages.length = 0;
|
|
602
|
+
task.controller.abort();
|
|
603
|
+
}
|
|
431
604
|
assertNameAvailable(name, agentId) {
|
|
432
605
|
if (name === undefined)
|
|
433
606
|
return;
|
|
@@ -445,16 +618,19 @@ export class BackgroundAgentManager {
|
|
|
445
618
|
};
|
|
446
619
|
task.result = stoppedResult;
|
|
447
620
|
task.durationMs ??= stoppedResult.durationMs || Date.now() - task.startedAt;
|
|
448
|
-
task.
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
621
|
+
if (!task.suppressNotifications) {
|
|
622
|
+
task.notifications.push({
|
|
623
|
+
id: stoppedResult.notificationId ?? randomUUID(),
|
|
624
|
+
status: 'stopped',
|
|
625
|
+
result: stoppedResult,
|
|
626
|
+
error: task.error,
|
|
627
|
+
toolUseId: task.spec.toolUseId,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
454
630
|
}
|
|
455
631
|
resolveRequired(identifier) {
|
|
456
632
|
const resolved = this.resolveOptional(identifier);
|
|
457
|
-
if (!
|
|
633
|
+
if (!isClaudeAgentId(identifier) &&
|
|
458
634
|
!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/u.test(identifier)) {
|
|
459
635
|
assertAgentId(identifier);
|
|
460
636
|
}
|
|
@@ -12,7 +12,14 @@ export interface BackgroundBashTaskSource {
|
|
|
12
12
|
}
|
|
13
13
|
export interface BackgroundAgentTaskSource {
|
|
14
14
|
backgroundSnapshots(): readonly BackgroundAgentSnapshot[];
|
|
15
|
-
|
|
15
|
+
outputBackgroundTask(taskId: string, options: {
|
|
16
|
+
block: boolean;
|
|
17
|
+
timeout: number;
|
|
18
|
+
}): Promise<string>;
|
|
19
|
+
stopBackgroundTask(taskId: string): Promise<string>;
|
|
20
|
+
sendBackgroundMessage(agentId: string, message: string, summary: string | undefined, toolUseId: string): string;
|
|
21
|
+
hasForegroundTask(): boolean;
|
|
22
|
+
backgroundForegroundTask(): BackgroundAgentSnapshot;
|
|
16
23
|
}
|
|
17
24
|
export interface WorkflowTaskSource {
|
|
18
25
|
list(sessionId?: string): readonly WorkflowTaskSnapshot[];
|
|
@@ -29,6 +36,18 @@ export declare class BackgroundTaskRuntime {
|
|
|
29
36
|
registerAgents(sessionId: string, source: BackgroundAgentTaskSource): void;
|
|
30
37
|
snapshot(sessionId: string): Promise<BackgroundTaskSnapshot>;
|
|
31
38
|
stop(sessionId: string, taskId: string): Promise<void>;
|
|
39
|
+
outputAgent(sessionId: string, agentId: string, options: {
|
|
40
|
+
block: boolean;
|
|
41
|
+
timeout: number;
|
|
42
|
+
}): Promise<string | null>;
|
|
43
|
+
stopAgent(sessionId: string, agentId: string): Promise<string | null>;
|
|
44
|
+
sendAgent(sessionId: string, agentId: string, message: string, summary: string | undefined, toolUseId: string): string | null;
|
|
45
|
+
sendAgentWithOwner(sessionId: string, agentId: string, message: string, summary: string | undefined, toolUseId: string): {
|
|
46
|
+
content: string;
|
|
47
|
+
owner: BackgroundAgentTaskSource;
|
|
48
|
+
} | null;
|
|
49
|
+
backgroundForeground(sessionId: string): BackgroundAgentSnapshot;
|
|
32
50
|
clear(): void;
|
|
51
|
+
private agentSource;
|
|
33
52
|
}
|
|
34
53
|
//# sourceMappingURL=background-task-runtime.d.ts.map
|
|
@@ -38,22 +38,61 @@ export class BackgroundTaskRuntime {
|
|
|
38
38
|
await source.stopBackgroundTask(taskId);
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
if (!source.backgroundSnapshots().some(({ agentId }) => agentId === taskId)) {
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
source.stopBackgroundTask(taskId);
|
|
41
|
+
if ((await this.stopAgent(sessionId, taskId)) !== null)
|
|
46
42
|
return;
|
|
47
|
-
}
|
|
48
43
|
if (this.workflows?.hasForSession(sessionId, taskId)) {
|
|
49
44
|
await this.workflows.stopAndWait(taskId);
|
|
50
45
|
return;
|
|
51
46
|
}
|
|
52
47
|
throw new Error(`No task found with ID: ${taskId}`);
|
|
53
48
|
}
|
|
49
|
+
async outputAgent(sessionId, agentId, options) {
|
|
50
|
+
const source = this.agentSource(sessionId, agentId);
|
|
51
|
+
return source ? source.outputBackgroundTask(agentId, options) : null;
|
|
52
|
+
}
|
|
53
|
+
async stopAgent(sessionId, agentId) {
|
|
54
|
+
const source = this.agentSource(sessionId, agentId);
|
|
55
|
+
return source ? source.stopBackgroundTask(agentId) : null;
|
|
56
|
+
}
|
|
57
|
+
sendAgent(sessionId, agentId, message, summary, toolUseId) {
|
|
58
|
+
return (this.sendAgentWithOwner(sessionId, agentId, message, summary, toolUseId)
|
|
59
|
+
?.content ?? null);
|
|
60
|
+
}
|
|
61
|
+
sendAgentWithOwner(sessionId, agentId, message, summary, toolUseId) {
|
|
62
|
+
const source = this.agentSource(sessionId, agentId);
|
|
63
|
+
return source
|
|
64
|
+
? {
|
|
65
|
+
content: source.sendBackgroundMessage(agentId, message, summary, toolUseId),
|
|
66
|
+
owner: source,
|
|
67
|
+
}
|
|
68
|
+
: null;
|
|
69
|
+
}
|
|
70
|
+
backgroundForeground(sessionId) {
|
|
71
|
+
const sources = [...(this.agentSources.get(sessionId) ?? [])].reverse();
|
|
72
|
+
const source = sources.find((candidate) => candidate.hasForegroundTask());
|
|
73
|
+
if (!source)
|
|
74
|
+
throw new Error('No foreground agent is running');
|
|
75
|
+
return source.backgroundForegroundTask();
|
|
76
|
+
}
|
|
54
77
|
clear() {
|
|
55
78
|
this.bashSources.clear();
|
|
56
79
|
this.agentSources.clear();
|
|
57
80
|
}
|
|
81
|
+
agentSource(sessionId, identifier) {
|
|
82
|
+
const sources = [...(this.agentSources.get(sessionId) ?? [])];
|
|
83
|
+
const idMatches = sources.filter((source) => source
|
|
84
|
+
.backgroundSnapshots()
|
|
85
|
+
.some(({ agentId }) => agentId === identifier));
|
|
86
|
+
if (idMatches.length > 1) {
|
|
87
|
+
throw new Error(`Multiple background agent owners claim ID: ${identifier}`);
|
|
88
|
+
}
|
|
89
|
+
if (idMatches[0])
|
|
90
|
+
return idMatches[0];
|
|
91
|
+
const nameMatches = sources.filter((source) => source.backgroundSnapshots().some(({ name }) => name === identifier));
|
|
92
|
+
if (nameMatches.length > 1) {
|
|
93
|
+
throw new Error(`Multiple live background agents are named '${identifier}'`);
|
|
94
|
+
}
|
|
95
|
+
return nameMatches[0] ?? null;
|
|
96
|
+
}
|
|
58
97
|
}
|
|
59
98
|
//# sourceMappingURL=background-task-runtime.js.map
|
|
@@ -12,4 +12,12 @@ export declare function createManagedWorktree(options: {
|
|
|
12
12
|
directoryName: string;
|
|
13
13
|
label: 'Agent' | 'Workflow';
|
|
14
14
|
}): Promise<ManagedWorktree>;
|
|
15
|
+
/** Reattaches only to a real, registered Git worktree. Persisted metadata is
|
|
16
|
+
* untrusted input, so an arbitrary directory can never become an execution
|
|
17
|
+
* cwd merely because it exists. Restored worktrees are retained for audit. */
|
|
18
|
+
export declare function restoreManagedWorktree(options: {
|
|
19
|
+
cwd: string;
|
|
20
|
+
path: string;
|
|
21
|
+
label: 'Agent' | 'Workflow';
|
|
22
|
+
}): Promise<ManagedWorktree>;
|
|
15
23
|
//# sourceMappingURL=managed-worktree.d.ts.map
|