praxis-agent 0.35.0 → 0.36.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.
Files changed (38) hide show
  1. package/README.md +5 -1
  2. package/dist/application/background-agent-manager.d.ts +16 -6
  3. package/dist/application/background-agent-manager.js +139 -97
  4. package/dist/application/context-engine.d.ts +56 -0
  5. package/dist/application/context-engine.js +129 -0
  6. package/dist/application/session-service.d.ts +8 -4
  7. package/dist/application/session-service.js +528 -434
  8. package/dist/application/subagent-service.js +358 -177
  9. package/dist/application/top-level-agent-manager.d.ts +1 -0
  10. package/dist/application/top-level-agent-manager.js +1025 -403
  11. package/dist/application/transcript-projection.d.ts +33 -0
  12. package/dist/application/transcript-projection.js +110 -0
  13. package/dist/application/turn-lifecycle.d.ts +2 -0
  14. package/dist/application/turn-memory-coordinator.d.ts +43 -0
  15. package/dist/application/turn-memory-coordinator.js +98 -0
  16. package/dist/cli/agents-dashboard.js +10 -5
  17. package/dist/cli/interactive.js +264 -204
  18. package/dist/cli/tui/claude-style.js +154 -130
  19. package/dist/cli/tui/task-panel.js +10 -1
  20. package/dist/cli/tui/theme.d.ts +46 -3
  21. package/dist/cli/tui/theme.js +414 -56
  22. package/dist/cli/tui/tool-permission.js +9 -5
  23. package/dist/cli/tui/tui-interaction-router.d.ts +78 -0
  24. package/dist/cli/tui/tui-interaction-router.js +127 -0
  25. package/dist/cli-runtime.d.ts +2 -2
  26. package/dist/core/agent-orchestration.d.ts +31 -0
  27. package/dist/core/agent-orchestration.js +235 -0
  28. package/dist/core/runtime.d.ts +5 -0
  29. package/dist/core/runtime.js +15 -1
  30. package/dist/persistence/claude-job-store.d.ts +58 -0
  31. package/dist/persistence/claude-job-store.js +483 -1
  32. package/dist/persistence/native-transcript-reader.d.ts +53 -0
  33. package/dist/persistence/native-transcript-reader.js +283 -0
  34. package/dist/persistence/subagent-lifecycle-store.d.ts +41 -8
  35. package/dist/persistence/subagent-lifecycle-store.js +373 -146
  36. package/dist/platform/exclusive-file-lease.d.ts +3 -0
  37. package/dist/platform/exclusive-file-lease.js +3 -0
  38. package/package.json +1 -1
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Praxis
2
2
 
3
+ [English](README.md) | [简体中文](README_zh.md)
4
+
3
5
  [![CI](https://github.com/Forest-Isle/Praxis/actions/workflows/ci.yml/badge.svg)](https://github.com/Forest-Isle/Praxis/actions/workflows/ci.yml)
4
6
  [![CodeQL](https://github.com/Forest-Isle/Praxis/actions/workflows/codeql.yml/badge.svg)](https://github.com/Forest-Isle/Praxis/actions/workflows/codeql.yml)
5
7
  [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/Forest-Isle/Praxis/badge)](https://scorecard.dev/viewer/?uri=github.com/Forest-Isle/Praxis)
@@ -148,7 +150,9 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
148
150
  - **Durable local work** — resumable sessions, full-history forks, file
149
151
  checkpoints, tasks, foreground/background subagents, top-level agents, and
150
152
  Claude-compatible main-thread agent definitions with native prompt, model,
151
- tool, memory, first-turn, and resume behavior.
153
+ tool, memory, first-turn, and resume behavior. Agent execution uses one
154
+ durable lifecycle vocabulary with bounded cancellation and drain,
155
+ continuation, notifications, and single-owner orphan recovery.
152
156
  - **Claude-compatible ecosystem** — shared instructions with recursive `@`
153
157
  imports, memory, skills, commands, agents, hooks, settings, MCP servers,
154
158
  plugins, and transcript data.
@@ -1,4 +1,5 @@
1
1
  import type { ModelUsage, ModelUsageByModel } from '../core/runtime.js';
2
+ import { type LifecycleState } from '../core/agent-orchestration.js';
2
3
  export interface BackgroundAgentRunResult {
3
4
  text: string;
4
5
  usage: ModelUsage;
@@ -19,6 +20,10 @@ export declare class BackgroundAgentRunError extends Error {
19
20
  export declare class BackgroundAgentShutdownError extends Error {
20
21
  constructor();
21
22
  }
23
+ export interface BackgroundAgentLifecycleSource {
24
+ current(): LifecycleState;
25
+ subscribe(listener: (state: LifecycleState) => void): () => void;
26
+ }
22
27
  export interface BackgroundAgentTaskSpec {
23
28
  agentId: string;
24
29
  name?: string;
@@ -28,13 +33,15 @@ export interface BackgroundAgentTaskSpec {
28
33
  toolUseId: string;
29
34
  outputFile: string;
30
35
  resolvedModel: string;
36
+ lifecycle: BackgroundAgentLifecycleSource;
31
37
  markBackground?(): void;
32
38
  acknowledgeNotification?(notificationId: string): Promise<void>;
33
39
  prepareNotificationDetached?(notificationId: string, model: string): Promise<void>;
34
40
  confirmNotificationDetached?(notificationId: string): Promise<void>;
35
41
  run(message: string, signal: AbortSignal, continuation: boolean, toolUseId: string): Promise<BackgroundAgentRunResult>;
36
42
  }
37
- type BackgroundAgentStatus = 'running' | 'completed' | 'failed' | 'stopped' | 'interrupted';
43
+ type BackgroundAgentStatus = LifecycleState;
44
+ export declare function legacyBackgroundStatus(state: BackgroundAgentStatus): 'running' | 'stopped' | 'interrupted' | 'completed' | 'failed';
38
45
  export interface BackgroundAgentNotificationIdentity {
39
46
  agentId: string;
40
47
  toolUseId: string;
@@ -66,12 +73,15 @@ export declare class BackgroundAgentManager {
66
73
  operation: Promise<BackgroundAgentRunResult>;
67
74
  startedAt: number;
68
75
  }): BackgroundAgentSnapshot;
69
- registerCompleted(spec: BackgroundAgentTaskSpec, result: BackgroundAgentRunResult): BackgroundAgentSnapshot;
70
- registerInterrupted(spec: BackgroundAgentTaskSpec, error?: string): BackgroundAgentSnapshot;
71
- registerTerminal(spec: BackgroundAgentTaskSpec, status: 'failed' | 'stopped', error: string): BackgroundAgentSnapshot;
76
+ registerPersisted(spec: BackgroundAgentTaskSpec, presentation?: {
77
+ result?: BackgroundAgentRunResult | null;
78
+ error?: string | null;
79
+ startedAt?: number;
80
+ durationMs?: number | null;
81
+ }): BackgroundAgentSnapshot;
72
82
  registerPersistedNotification(agentId: string, notification: {
73
83
  id: string;
74
- status: 'completed' | 'failed' | 'stopped';
84
+ status: 'completed' | 'failed' | 'cancelled';
75
85
  result: BackgroundAgentRunResult | null;
76
86
  error: string | null;
77
87
  toolUseId: string;
@@ -112,7 +122,7 @@ export declare class BackgroundAgentManager {
112
122
  private resolveOptional;
113
123
  private stopTask;
114
124
  private assertNameAvailable;
115
- private finishStopped;
125
+ private finishCancelled;
116
126
  private resolveRequired;
117
127
  private snapshot;
118
128
  private formatOutput;
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { isTerminalLifecycleState, } from '../core/agent-orchestration.js';
2
3
  import { isClaudeAgentId } from '../compatibility/claude/sidechain.js';
3
4
  const MAX_TIMEOUT_MS = 600_000;
4
5
  const DEFAULT_CLOSE_DRAIN_MS = 5_000;
@@ -16,6 +17,25 @@ export class BackgroundAgentShutdownError extends Error {
16
17
  this.name = 'BackgroundAgentShutdownError';
17
18
  }
18
19
  }
20
+ export function legacyBackgroundStatus(state) {
21
+ if (state === 'queued' ||
22
+ state === 'running' ||
23
+ state === 'waiting' ||
24
+ state === 'cancelling')
25
+ return 'running';
26
+ if (state === 'cancelled')
27
+ return 'stopped';
28
+ if (state === 'orphaned')
29
+ return 'interrupted';
30
+ return state;
31
+ }
32
+ function notificationWireStatus(status) {
33
+ if (status === 'completed' || status === 'failed')
34
+ return status;
35
+ if (status === 'cancelled')
36
+ return 'killed';
37
+ return null;
38
+ }
19
39
  function assertAgentId(agentId) {
20
40
  if (!isClaudeAgentId(agentId)) {
21
41
  throw new Error(`Invalid background agent ID: ${agentId}`);
@@ -174,11 +194,11 @@ export class BackgroundAgentManager {
174
194
  }
175
195
  launch(spec) {
176
196
  const { task } = this.registerTask(spec, {
177
- status: 'running',
197
+ status: spec.lifecycle.current(),
178
198
  controller: null,
179
199
  result: null,
180
200
  error: null,
181
- generation: 0,
201
+ runSequence: 0,
182
202
  startedAt: Date.now(),
183
203
  durationMs: null,
184
204
  }, 'throw');
@@ -189,11 +209,11 @@ export class BackgroundAgentManager {
189
209
  adopt(options) {
190
210
  const { spec } = options;
191
211
  const { task } = this.registerTask(spec, {
192
- status: 'running',
212
+ status: spec.lifecycle.current(),
193
213
  controller: options.controller,
194
214
  result: null,
195
215
  error: null,
196
- generation: 1,
216
+ runSequence: 1,
197
217
  startedAt: options.startedAt,
198
218
  durationMs: null,
199
219
  }, 'throw');
@@ -201,39 +221,18 @@ export class BackgroundAgentManager {
201
221
  this.track(task, options.operation, 1, spec.toolUseId);
202
222
  return this.snapshot(task);
203
223
  }
204
- registerCompleted(spec, result) {
205
- const { task } = this.registerTask(spec, {
206
- status: 'completed',
207
- controller: null,
208
- result,
209
- error: null,
210
- generation: 0,
211
- startedAt: Date.now() - result.durationMs,
212
- durationMs: result.durationMs,
213
- }, 'return-existing');
214
- return this.snapshot(task);
215
- }
216
- registerInterrupted(spec, error = 'Persisted agent was interrupted before completion') {
224
+ registerPersisted(spec, presentation = {}) {
217
225
  const { task } = this.registerTask(spec, {
218
- status: 'interrupted',
226
+ status: spec.lifecycle.current(),
219
227
  controller: null,
220
- result: null,
221
- error,
222
- generation: 0,
223
- startedAt: Date.now(),
224
- durationMs: 0,
225
- }, 'return-existing');
226
- return this.snapshot(task);
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,
228
+ result: presentation.result ?? null,
229
+ error: presentation.error ?? null,
230
+ runSequence: 0,
231
+ startedAt: presentation.startedAt ??
232
+ (presentation.durationMs === undefined
233
+ ? Date.now()
234
+ : Date.now() - (presentation.durationMs ?? 0)),
235
+ durationMs: presentation.durationMs ?? null,
237
236
  }, 'return-existing');
238
237
  return this.snapshot(task);
239
238
  }
@@ -250,7 +249,12 @@ export class BackgroundAgentManager {
250
249
  }
251
250
  notificationClaimAgentIds() {
252
251
  return [...this.tasks.entries()]
253
- .filter(([, task]) => task.status === 'running' || task.notifications.length > 0)
252
+ .filter(([, task]) => task.promise !== null ||
253
+ task.status === 'queued' ||
254
+ task.status === 'running' ||
255
+ task.status === 'waiting' ||
256
+ task.status === 'cancelling' ||
257
+ task.notifications.length > 0)
254
258
  .map(([agentId]) => agentId);
255
259
  }
256
260
  snapshotById(agentId) {
@@ -281,7 +285,7 @@ export class BackgroundAgentManager {
281
285
  // The retrieval outcome reflects the task's state after any wait: success
282
286
  // once terminal, not_ready for non-blocking retrievals, and timeout for a
283
287
  // blocking retrieval whose window (including zero) closed while live.
284
- const retrieval = task.status !== 'running'
288
+ const retrieval = isTerminalLifecycleState(task.status)
285
289
  ? 'success'
286
290
  : options.block
287
291
  ? 'timeout'
@@ -299,14 +303,21 @@ export class BackgroundAgentManager {
299
303
  async stopAndWait(agentId, timeout = DEFAULT_CLOSE_DRAIN_MS) {
300
304
  const message = this.stop(agentId);
301
305
  const task = this.tasks.get(agentId);
302
- if (task?.promise)
306
+ if (task?.promise) {
303
307
  await waitBounded(task.promise, timeout);
308
+ }
309
+ if (!task || task.promise || !isTerminalLifecycleState(task.status)) {
310
+ return `Task ${agentId} cancellation is still in progress`;
311
+ }
312
+ if (task.status !== 'cancelled') {
313
+ return `Task ${agentId} cancellation ended with status ${legacyBackgroundStatus(task.status)}`;
314
+ }
304
315
  return message;
305
316
  }
306
317
  stopAll() {
307
318
  const stopped = [];
308
319
  for (const task of this.tasks.values()) {
309
- if (task.status !== 'running' || !task.controller)
320
+ if (!task.promise || !task.controller || task.cancellationRequested)
310
321
  continue;
311
322
  this.stopTask(task, 'Stopped by explicit bulk kill');
312
323
  stopped.push(task.spec.agentId);
@@ -325,8 +336,8 @@ export class BackgroundAgentManager {
325
336
  this.resolveClosed();
326
337
  const running = [];
327
338
  for (const task of this.tasks.values()) {
328
- if (task.status === 'running' && task.controller) {
329
- task.status = 'stopped';
339
+ if (task.promise && task.controller) {
340
+ task.cancellationRequested = true;
330
341
  task.error = 'Stopped because the background agent manager closed';
331
342
  task.suppressNotifications = true;
332
343
  task.notifications.length = 0;
@@ -337,6 +348,8 @@ export class BackgroundAgentManager {
337
348
  running.push(task.promise);
338
349
  }
339
350
  await waitBounded(Promise.allSettled(running).then(() => undefined), drainMilliseconds);
351
+ for (const task of this.tasks.values())
352
+ task.unsubscribeLifecycle?.();
340
353
  this.tasks.clear();
341
354
  this.names.clear();
342
355
  }
@@ -355,11 +368,21 @@ export class BackgroundAgentManager {
355
368
  });
356
369
  }
357
370
  const priorStatus = task.status;
358
- if (task.promise && task.status === 'running') {
371
+ if (task.cancellationRequested &&
372
+ (task.promise !== null || task.status !== 'cancelled')) {
373
+ return JSON.stringify({
374
+ success: false,
375
+ message: `Agent "${agentId}" is cancelling and cannot receive a continuation yet.`,
376
+ });
377
+ }
378
+ if (task.promise) {
359
379
  task.queuedMessages.push({ message, toolUseId });
360
380
  }
361
381
  else {
362
- this.start(task, message, true, toolUseId);
382
+ task.queuedMessages.push({ message, toolUseId });
383
+ const next = task.queuedMessages.shift();
384
+ if (next)
385
+ this.start(task, next.message, true, next.toolUseId);
363
386
  }
364
387
  return JSON.stringify({
365
388
  success: true,
@@ -373,7 +396,10 @@ export class BackgroundAgentManager {
373
396
  !eligibleTasks.some(([, task]) => task.notifications.length > 0)) {
374
397
  const running = eligibleTasks
375
398
  .map(([, task]) => task)
376
- .filter((task) => task.status === 'running')
399
+ .filter((task) => task.promise !== null ||
400
+ task.status === 'queued' ||
401
+ task.status === 'running' ||
402
+ task.status === 'cancelling')
377
403
  .map((task) => task.promise)
378
404
  .filter((promise) => promise !== null);
379
405
  if (running.length > 0) {
@@ -386,12 +412,16 @@ export class BackgroundAgentManager {
386
412
  let durationApiMs = 0;
387
413
  let durationApiWithoutRetriesMs = 0;
388
414
  let durationSeen = false;
389
- const consumedTasks = [];
415
+ const consumedNotifications = [];
390
416
  for (const [, task] of eligibleTasks) {
391
417
  if (task.notifications.length === 0)
392
418
  continue;
393
419
  for (const notification of task.notifications) {
394
- notifications.push(this.formatNotification(task, notification));
420
+ const message = this.formatNotification(task, notification);
421
+ if (message === null)
422
+ continue;
423
+ notifications.push(message);
424
+ consumedNotifications.push({ task, notification });
395
425
  if (notification.result) {
396
426
  assertValidResultUsage(notification.result.usage);
397
427
  usage = addUsageChecked(undefined, usage, notification.result.usage);
@@ -407,18 +437,13 @@ export class BackgroundAgentManager {
407
437
  durationApiWithoutRetriesMs = addApiDuration(notification.result.durationApiWithoutRetriesMs ?? total, durationApiWithoutRetriesMs, 'durationApiWithoutRetriesMs');
408
438
  }
409
439
  }
410
- consumedTasks.push(task);
411
440
  }
412
441
  const modelUsage = modelUsageByModel.size === 0
413
442
  ? undefined
414
443
  : Object.fromEntries(modelUsageByModel);
415
444
  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
- }
445
+ for (const { task, notification } of consumedNotifications) {
446
+ await task.spec.acknowledgeNotification?.(notification.id);
422
447
  }
423
448
  }
424
449
  const result = {
@@ -428,8 +453,9 @@ export class BackgroundAgentManager {
428
453
  ...(durationSeen ? { durationApiMs, durationApiWithoutRetriesMs } : {}),
429
454
  };
430
455
  if (options.consume !== false) {
431
- for (const task of consumedTasks)
432
- task.notifications.splice(0);
456
+ for (const { task, notification } of consumedNotifications) {
457
+ task.notifications.splice(task.notifications.indexOf(notification), 1);
458
+ }
433
459
  }
434
460
  return result;
435
461
  }
@@ -454,6 +480,8 @@ export class BackgroundAgentManager {
454
480
  for (const task of this.tasks.values()) {
455
481
  for (const notification of [...task.notifications]) {
456
482
  const message = this.formatNotification(task, notification);
483
+ if (message === null)
484
+ continue;
457
485
  const index = remaining.indexOf(message);
458
486
  if (index < 0)
459
487
  continue;
@@ -465,9 +493,9 @@ export class BackgroundAgentManager {
465
493
  async acknowledgeDelivered(delivered) {
466
494
  for (const [agentId, task] of this.tasks) {
467
495
  for (const notification of [...task.notifications]) {
468
- if (notification.status === 'interrupted')
496
+ const status = notificationWireStatus(notification.status);
497
+ if (status === null)
469
498
  continue;
470
- const status = notification.status === 'stopped' ? 'killed' : notification.status;
471
499
  if (!delivered({ agentId, toolUseId: notification.toolUseId, status })) {
472
500
  continue;
473
501
  }
@@ -479,9 +507,9 @@ export class BackgroundAgentManager {
479
507
  async acknowledgeDeliveredAsDetached(delivered) {
480
508
  for (const [agentId, task] of this.tasks) {
481
509
  for (const notification of [...task.notifications]) {
482
- if (notification.status === 'interrupted')
510
+ const status = notificationWireStatus(notification.status);
511
+ if (status === null)
483
512
  continue;
484
- const status = notification.status === 'stopped' ? 'killed' : notification.status;
485
513
  if (!delivered({ agentId, toolUseId: notification.toolUseId, status })) {
486
514
  continue;
487
515
  }
@@ -493,67 +521,71 @@ export class BackgroundAgentManager {
493
521
  }
494
522
  }
495
523
  start(task, message, continuation, toolUseId) {
496
- task.generation += 1;
497
- const generation = task.generation;
524
+ task.runSequence += 1;
525
+ const runSequence = task.runSequence;
498
526
  const controller = new AbortController();
499
- task.status = 'running';
527
+ task.cancellationRequested = false;
500
528
  task.startedAt = Date.now();
501
529
  task.durationMs = null;
502
530
  task.controller = controller;
503
531
  task.result = null;
504
532
  task.error = null;
505
- this.track(task, task.spec.run(message, controller.signal, continuation, toolUseId), generation, toolUseId);
533
+ this.track(task, task.spec.run(message, controller.signal, continuation, toolUseId), runSequence, toolUseId);
506
534
  }
507
- track(task, operation, generation, toolUseId) {
535
+ track(task, operation, runSequence, toolUseId) {
508
536
  task.promise = operation
509
537
  .then((result) => {
510
- if (task.generation !== generation)
538
+ if (task.runSequence !== runSequence)
511
539
  return;
512
- if (task.status === 'stopped') {
513
- this.finishStopped(task, result);
540
+ if (task.status === 'cancelled') {
541
+ this.finishCancelled(task, result);
514
542
  return;
515
543
  }
516
- task.status = 'completed';
517
544
  task.result = result;
518
545
  task.durationMs = result.durationMs;
519
546
  task.error = null;
520
- task.notifications.push({
521
- id: result.notificationId ?? randomUUID(),
522
- status: 'completed',
523
- result,
524
- error: null,
525
- toolUseId,
526
- });
547
+ if (task.status === 'completed')
548
+ task.notifications.push({
549
+ id: result.notificationId ?? randomUUID(),
550
+ status: 'completed',
551
+ result,
552
+ error: null,
553
+ toolUseId,
554
+ });
527
555
  })
528
556
  .catch((error) => {
529
- if (task.generation !== generation)
557
+ if (task.runSequence !== runSequence)
530
558
  return;
531
- if (task.status === 'stopped') {
532
- this.finishStopped(task, error instanceof BackgroundAgentRunError ? error.result : undefined);
559
+ if (task.status === 'cancelled') {
560
+ this.finishCancelled(task, error instanceof BackgroundAgentRunError ? error.result : undefined);
533
561
  return;
534
562
  }
535
563
  const failedResult = error instanceof BackgroundAgentRunError ? error.result : undefined;
536
- task.status = 'failed';
537
564
  task.result = null;
538
565
  task.durationMs =
539
566
  failedResult?.durationMs ?? Date.now() - task.startedAt;
540
567
  task.error = error instanceof Error ? error.message : String(error);
541
- task.notifications.push({
542
- id: failedResult?.notificationId ?? randomUUID(),
543
- status: 'failed',
544
- result: failedResult ?? null,
545
- error: task.error,
546
- toolUseId,
547
- });
568
+ if (task.status === 'failed')
569
+ task.notifications.push({
570
+ id: failedResult?.notificationId ?? randomUUID(),
571
+ status: 'failed',
572
+ result: failedResult ?? null,
573
+ error: task.error,
574
+ toolUseId,
575
+ });
548
576
  })
549
577
  .finally(() => {
550
- if (task.generation !== generation)
578
+ if (task.runSequence !== runSequence)
551
579
  return;
552
580
  task.controller = null;
553
581
  task.promise = null;
554
- const next = task.queuedMessages.shift();
555
- if (next && task.status !== 'stopped') {
556
- this.start(task, next.message, true, next.toolUseId);
582
+ if (!task.cancellationRequested &&
583
+ (task.status === 'completed' ||
584
+ task.status === 'failed' ||
585
+ task.status === 'cancelled')) {
586
+ const next = task.queuedMessages.shift();
587
+ if (next)
588
+ this.start(task, next.message, true, next.toolUseId);
557
589
  }
558
590
  });
559
591
  }
@@ -582,7 +614,13 @@ export class BackgroundAgentManager {
582
614
  notifications: [],
583
615
  queuedMessages: [],
584
616
  suppressNotifications: false,
617
+ cancellationRequested: false,
618
+ unsubscribeLifecycle: null,
585
619
  };
620
+ task.unsubscribeLifecycle = spec.lifecycle.subscribe((state) => {
621
+ if (!this.closed)
622
+ task.status = state;
623
+ });
586
624
  this.tasks.set(spec.agentId, task);
587
625
  return { task, created: true };
588
626
  }
@@ -592,10 +630,11 @@ export class BackgroundAgentManager {
592
630
  return this.names.get(identifier);
593
631
  }
594
632
  stopTask(task, reason) {
595
- if (task.status !== 'running' || !task.controller) {
596
- throw new Error(`Task ${task.spec.agentId} is not running (status: ${task.status})`);
633
+ if (!task.promise || !task.controller || task.cancellationRequested) {
634
+ const status = legacyBackgroundStatus(task.status);
635
+ throw new Error(`Task ${task.spec.agentId} is not running (status: ${status})`);
597
636
  }
598
- task.status = 'stopped';
637
+ task.cancellationRequested = true;
599
638
  task.error = reason;
600
639
  task.durationMs ??= Date.now() - task.startedAt;
601
640
  task.queuedMessages.length = 0;
@@ -609,7 +648,7 @@ export class BackgroundAgentManager {
609
648
  throw new Error(`Background agent name already exists: ${name}`);
610
649
  }
611
650
  }
612
- finishStopped(task, result) {
651
+ finishCancelled(task, result) {
613
652
  const stoppedResult = result ?? {
614
653
  text: task.error ?? 'Stopped by TaskStop',
615
654
  usage: { inputTokens: 0, outputTokens: 0 },
@@ -621,7 +660,7 @@ export class BackgroundAgentManager {
621
660
  if (!task.suppressNotifications) {
622
661
  task.notifications.push({
623
662
  id: stoppedResult.notificationId ?? randomUUID(),
624
- status: 'stopped',
663
+ status: 'cancelled',
625
664
  result: stoppedResult,
626
665
  error: task.error,
627
666
  toolUseId: task.spec.toolUseId,
@@ -653,11 +692,12 @@ export class BackgroundAgentManager {
653
692
  }
654
693
  formatOutput(task, retrieval) {
655
694
  const output = task.result?.text ?? task.error ?? '';
695
+ const status = legacyBackgroundStatus(task.status);
656
696
  return [
657
697
  `<retrieval_status>${retrieval}</retrieval_status>`,
658
698
  `<task_id>${task.spec.agentId}</task_id>`,
659
699
  '<task_type>local_agent</task_type>',
660
- `<status>${task.status}</status>`,
700
+ `<status>${status}</status>`,
661
701
  ...(task.result?.isolationPath
662
702
  ? [
663
703
  `<worktree_path>${escapeXml(task.result.isolationPath)}</worktree_path>`,
@@ -674,7 +714,9 @@ export class BackgroundAgentManager {
674
714
  }
675
715
  formatNotification(task, notification) {
676
716
  const result = notification.result?.text ?? notification.error ?? '';
677
- const status = notification.status === 'stopped' ? 'killed' : notification.status;
717
+ const status = notificationWireStatus(notification.status);
718
+ if (status === null)
719
+ return null;
678
720
  const usage = notification.result
679
721
  ? `<usage><total_tokens>${notification.result.usage.inputTokens + notification.result.usage.outputTokens}</total_tokens><tool_uses>${notification.result.toolUseCount}</tool_uses><duration_ms>${notification.result.durationMs}</duration_ms></usage>`
680
722
  : '';
@@ -0,0 +1,56 @@
1
+ import { type ModelProviderError, type ModelMessage, type ModelToolDefinition, type ModelUsage } from '../core/runtime.js';
2
+ import { type ContextBudget, type ContextBudgetReport } from '../core/context-budget.js';
3
+ export interface ContextEnvelope {
4
+ readonly messages: readonly ModelMessage[];
5
+ readonly tools: readonly ModelToolDefinition[];
6
+ readonly outputTokens?: number;
7
+ }
8
+ export interface ContextCompactionProposal {
9
+ readonly envelope: ContextEnvelope;
10
+ commit(): Promise<void>;
11
+ }
12
+ export interface ContextTransitionInput {
13
+ readonly trigger: 'auto' | 'reactive';
14
+ readonly before: ContextBudgetReport;
15
+ readonly irreducible: ContextBudgetReport;
16
+ readonly signal?: AbortSignal;
17
+ }
18
+ export interface ContextTransitionPort {
19
+ current(): ContextEnvelope;
20
+ irreducible(): ContextEnvelope;
21
+ propose(input: ContextTransitionInput): Promise<ContextCompactionProposal>;
22
+ }
23
+ export interface ContextEngineMemoryPort {
24
+ beforeCompact?(): Promise<void>;
25
+ afterCompact?(): Promise<void>;
26
+ }
27
+ export interface ContextEngineOptions {
28
+ budget?: ContextBudget;
29
+ memory?: ContextEngineMemoryPort;
30
+ autoCompact?: boolean;
31
+ }
32
+ export type ContextRecoveryResult = {
33
+ readonly kind: 'not-applicable';
34
+ } | {
35
+ readonly kind: 'retry';
36
+ readonly envelope: ContextEnvelope;
37
+ } | {
38
+ readonly kind: 'exhausted';
39
+ readonly error: ModelProviderError;
40
+ };
41
+ /** Owns context measurement, bounded recovery, and the proposal/commit seam. */
42
+ export declare class ContextEngine {
43
+ private recoveryUsed;
44
+ private readonly budget;
45
+ private readonly memory;
46
+ private readonly autoCompact;
47
+ constructor(options?: ContextEngineOptions);
48
+ prepare(port: ContextTransitionPort, signal?: AbortSignal): Promise<ContextEnvelope>;
49
+ recover(error: unknown, port: ContextTransitionPort, signal?: AbortSignal): Promise<ContextRecoveryResult>;
50
+ observeUsage(usage: ModelUsage, messages: readonly ModelMessage[], tools: readonly ModelToolDefinition[]): void;
51
+ report(envelope: ContextEnvelope, options?: {
52
+ promptTooLong?: boolean;
53
+ }): ContextBudgetReport | undefined;
54
+ private reportRequired;
55
+ }
56
+ //# sourceMappingURL=context-engine.d.ts.map