praxis-agent 0.54.0 → 0.55.1

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 CHANGED
@@ -253,11 +253,16 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
253
253
  recovery for malformed streamed tool arguments without tool execution or
254
254
  lost resumability, one default-on bounded Anthropic non-streaming replay for
255
255
  eligible stream/idle failures without exposing failed-attempt output, and
256
- token-only/no-API-dollar accounting for subscription runs. For each main user
257
- turn, failed provider attempts stay buffered; the first successful route,
258
- whether primary or fallback, is sticky and sealed through that turn's tool
259
- continuations; incompatible fallback routes fail closed, and each new main
260
- user turn starts from primary.
256
+ token-only/no-API-dollar accounting for subscription runs. Each main user
257
+ Turn and independent auxiliary Agent, Workflow, Team, recovery, or memory
258
+ Turn receives its own provider client. Session-memory requests reuse a
259
+ completion-scoped client but restart routing from primary for each request;
260
+ auto-mode critic and eval-judge requests remain independently constructed
261
+ one-shot clients. Failed attempts stay buffered, and the first successful
262
+ route, whether primary or fallback, stays sticky only through that logical
263
+ Turn's tool continuations; incompatible routes fail closed, and the next
264
+ independent Turn starts from primary. Recovery may persist only an optional
265
+ selected model, never provider route or wire state.
261
266
  - **Transactional self-update** — `praxis update` verifies the package before
262
267
  installing it, rejects concurrent updates, and can roll back after an
263
268
  interruption or crash.
@@ -342,7 +347,7 @@ normal/low-capability full-frame p95 budgets of `<16.7/<33 ms`.
342
347
  `npm run test:coverage` measures all production code under `src/**` with V8 and
343
348
  enforces global floors of 79% statements, 70% branches, 85% functions, and 81% lines,
344
349
  and rejects any production runtime module with zero covered statements (while allowing
345
- type-only modules). `npm run test:fixtures` executes the 70-behavior native contract; 62 behaviors
350
+ type-only modules). `npm run test:fixtures` executes the 71-behavior native contract; 63 behaviors
346
351
  are qualified and 8 are explicitly excluded. `npm run verify:fixture-contracts`
347
352
  performs the structural check and is part of `npm run check`.
348
353
  `npm run test:core-completion` is retained as a compatibility alias for
@@ -1,5 +1,6 @@
1
1
  import { AgentRunCancelledError, } from '../core/runtime.js';
2
2
  import { contextRecoveryMadeProgress, isPromptTooLongError, } from '../core/context-budget.js';
3
+ import { StaleContextGenerationError } from './context-preparation.js';
3
4
  function assertSignal(signal) {
4
5
  if (signal?.aborted)
5
6
  throw new AgentRunCancelledError();
@@ -95,7 +96,9 @@ export class ContextEngine {
95
96
  return { kind: 'retry', envelope: proposal.envelope };
96
97
  }
97
98
  catch (cause) {
98
- if (signal?.aborted || cause instanceof AgentRunCancelledError)
99
+ if (signal?.aborted ||
100
+ cause instanceof AgentRunCancelledError ||
101
+ cause instanceof StaleContextGenerationError)
99
102
  throw cause;
100
103
  return { kind: 'exhausted', error };
101
104
  }
@@ -0,0 +1,60 @@
1
+ import { type ContextAssembler, type ContextAssemblyOptions } from '../core/context.js';
2
+ import type { ModelMessage, ModelToolDefinition } from '../core/runtime.js';
3
+ import type { ContextEnvelope } from './context-engine.js';
4
+ export interface ContextPreparationProjection {
5
+ readonly generation: number;
6
+ readonly envelope: ContextEnvelope;
7
+ readonly stableSystemMessageCount: number;
8
+ }
9
+ export interface ContextPreparationSources {
10
+ readonly history: () => readonly ModelMessage[];
11
+ readonly memory: () => readonly ModelMessage[];
12
+ readonly activeTools: () => readonly ModelToolDefinition[];
13
+ }
14
+ export interface ContextPreparationProjectOptions {
15
+ readonly includeHistory?: boolean;
16
+ readonly includeMemory?: boolean;
17
+ readonly pendingMessages?: readonly ModelMessage[];
18
+ }
19
+ export interface ContextHistoryReplacement {
20
+ readonly generation: number;
21
+ readonly envelope: ContextEnvelope;
22
+ readonly stableSystemMessageCount: number;
23
+ commit<T>(replace: () => Promise<T>): Promise<{
24
+ generation: number;
25
+ value: T;
26
+ }>;
27
+ }
28
+ export declare class StaleContextGenerationError extends Error {
29
+ readonly expectedGeneration: number;
30
+ readonly actualGeneration: number;
31
+ constructor(expectedGeneration: number, actualGeneration: number);
32
+ }
33
+ export interface ContextPreparationOptions {
34
+ readonly assembler?: ContextAssembler;
35
+ readonly sources: ContextPreparationSources;
36
+ readonly agentMentions?: () => {
37
+ readonly prompt: string;
38
+ readonly messages: readonly string[];
39
+ };
40
+ readonly initialGeneration?: number;
41
+ }
42
+ /** Owns provider-visible context projection and guarded history replacement. */
43
+ export declare class ContextPreparation {
44
+ private readonly assembler;
45
+ private readonly sources;
46
+ private readonly agentMentions;
47
+ private generation;
48
+ private prepared;
49
+ private replacementQueue;
50
+ constructor(options: ContextPreparationOptions);
51
+ refresh(options?: ContextAssemblyOptions): Promise<void>;
52
+ project(options?: ContextPreparationProjectOptions): ContextPreparationProjection;
53
+ proposeHistoryReplacement(input: {
54
+ readonly historyMessages: readonly ModelMessage[];
55
+ readonly pendingMessages?: readonly ModelMessage[];
56
+ }): ContextHistoryReplacement;
57
+ private projectWithMessages;
58
+ private decorate;
59
+ }
60
+ //# sourceMappingURL=context-preparation.d.ts.map
@@ -0,0 +1,139 @@
1
+ import { injectFirstUserMessageContext, projectContextSnapshot, } from '../core/context.js';
2
+ import { assembleContextSnapshot } from '../core/prompt-composer.js';
3
+ export class StaleContextGenerationError extends Error {
4
+ expectedGeneration;
5
+ actualGeneration;
6
+ constructor(expectedGeneration, actualGeneration) {
7
+ super(`Stale context generation: expected ${expectedGeneration}, actual ${actualGeneration}; prepare a new history replacement`);
8
+ this.name = 'StaleContextGenerationError';
9
+ this.expectedGeneration = expectedGeneration;
10
+ this.actualGeneration = actualGeneration;
11
+ }
12
+ }
13
+ function validateGeneration(generation) {
14
+ if (!Number.isSafeInteger(generation) || generation < 1) {
15
+ throw new TypeError('Context generation must be a positive safe integer');
16
+ }
17
+ }
18
+ function cloneTools(tools) {
19
+ return tools.map((tool) => ({ ...tool }));
20
+ }
21
+ /** Owns provider-visible context projection and guarded history replacement. */
22
+ export class ContextPreparation {
23
+ assembler;
24
+ sources;
25
+ agentMentions;
26
+ generation;
27
+ prepared;
28
+ replacementQueue = Promise.resolve();
29
+ constructor(options) {
30
+ this.assembler = options.assembler;
31
+ this.sources = options.sources;
32
+ this.agentMentions = options.agentMentions;
33
+ this.generation = options.initialGeneration ?? 1;
34
+ validateGeneration(this.generation);
35
+ }
36
+ async refresh(options = {}) {
37
+ const snapshot = await assembleContextSnapshot(this.assembler, options);
38
+ const projection = projectContextSnapshot(snapshot);
39
+ const stableCount = projection.stableSystemSectionCount;
40
+ const prepared = {
41
+ stableSystemMessages: projection.systemMessages.slice(0, stableCount),
42
+ volatileSystemMessages: projection.systemMessages.slice(stableCount),
43
+ ...(projection.firstUserMessageContext === undefined
44
+ ? {}
45
+ : { firstUserMessageContext: projection.firstUserMessageContext }),
46
+ stableSystemMessageCount: stableCount,
47
+ };
48
+ this.prepared = prepared;
49
+ }
50
+ project(options = {}) {
51
+ return this.projectWithMessages((options.includeHistory ?? true) ? this.sources.history() : [], (options.includeMemory ?? true) ? this.sources.memory() : [], options.pendingMessages ?? []);
52
+ }
53
+ proposeHistoryReplacement(input) {
54
+ const baseGeneration = this.generation;
55
+ if (baseGeneration === Number.MAX_SAFE_INTEGER) {
56
+ throw new RangeError('Context generation cannot exceed Number.MAX_SAFE_INTEGER');
57
+ }
58
+ const generation = baseGeneration + 1;
59
+ const projection = this.projectWithMessages(input.historyMessages, [], input.pendingMessages ?? []);
60
+ let committed = false;
61
+ return {
62
+ generation,
63
+ envelope: projection.envelope,
64
+ stableSystemMessageCount: projection.stableSystemMessageCount,
65
+ commit: async (replace) => {
66
+ const operation = this.replacementQueue.then(async () => {
67
+ if (this.generation !== baseGeneration) {
68
+ throw new StaleContextGenerationError(baseGeneration, this.generation);
69
+ }
70
+ if (committed) {
71
+ throw new StaleContextGenerationError(baseGeneration, this.generation);
72
+ }
73
+ const value = await replace();
74
+ this.generation = generation;
75
+ committed = true;
76
+ return { generation, value };
77
+ });
78
+ this.replacementQueue = operation.then(() => undefined, () => undefined);
79
+ return operation;
80
+ },
81
+ };
82
+ }
83
+ projectWithMessages(historyMessages, memoryMessages, pendingMessages) {
84
+ const prepared = this.prepared;
85
+ if (!prepared) {
86
+ throw new Error('ContextPreparation must be refreshed before projecting');
87
+ }
88
+ const history = [...historyMessages];
89
+ const memory = [...memoryMessages];
90
+ const pending = [...pendingMessages];
91
+ const decoratedHistory = this.decorate([...history, ...memory, ...pending]);
92
+ const messages = [
93
+ ...prepared.stableSystemMessages,
94
+ ...prepared.volatileSystemMessages,
95
+ ...decoratedHistory,
96
+ ];
97
+ return {
98
+ generation: this.generation,
99
+ envelope: {
100
+ messages,
101
+ tools: cloneTools(this.sources.activeTools()),
102
+ },
103
+ stableSystemMessageCount: prepared.stableSystemMessageCount,
104
+ };
105
+ }
106
+ decorate(messages) {
107
+ const prepared = this.prepared;
108
+ if (!prepared) {
109
+ throw new Error('ContextPreparation must be refreshed before projecting');
110
+ }
111
+ const withFirstUserContext = injectFirstUserMessageContext(messages, prepared.firstUserMessageContext);
112
+ const mentionInput = this.agentMentions?.();
113
+ if (!mentionInput || mentionInput.messages.length === 0)
114
+ return withFirstUserContext;
115
+ let insertionIndex = withFirstUserContext.length;
116
+ let foundPrompt = false;
117
+ for (let index = withFirstUserContext.length - 1; index >= 0; index -= 1) {
118
+ const message = withFirstUserContext[index];
119
+ if (message?.role === 'user' &&
120
+ typeof message.content === 'string' &&
121
+ message.content.endsWith(mentionInput.prompt)) {
122
+ insertionIndex = index;
123
+ foundPrompt = true;
124
+ break;
125
+ }
126
+ }
127
+ if (!foundPrompt)
128
+ return withFirstUserContext;
129
+ return [
130
+ ...withFirstUserContext.slice(0, insertionIndex),
131
+ ...mentionInput.messages.map((content) => ({
132
+ role: 'user',
133
+ content,
134
+ })),
135
+ ...withFirstUserContext.slice(insertionIndex),
136
+ ];
137
+ }
138
+ }
139
+ //# sourceMappingURL=context-preparation.js.map
@@ -7,6 +7,8 @@ export interface NativeSidechainMetadata {
7
7
  readonly spawnDepth: number;
8
8
  readonly cwd: string;
9
9
  readonly promptId: string;
10
+ /** Provider-neutral selected model identifier for recovery. */
11
+ readonly model?: string;
10
12
  readonly name?: string;
11
13
  readonly permissionMode?: NativeSidechainPermissionMode;
12
14
  readonly isolation?: 'worktree';
@@ -19,6 +19,7 @@ const requiredKeys = [
19
19
  'promptId',
20
20
  ];
21
21
  const optionalKeys = [
22
+ 'model',
22
23
  'name',
23
24
  'permissionMode',
24
25
  'isolation',
@@ -74,6 +75,8 @@ function validateMetadata(value) {
74
75
  throw new Error('native sidechain metadata cwd is invalid');
75
76
  if (!nonBlank(record.promptId))
76
77
  throw new Error('native sidechain metadata promptId is invalid');
78
+ if (record.model !== undefined && !nonBlank(record.model))
79
+ throw new Error('native sidechain metadata model is invalid');
77
80
  if (record.name !== undefined && !nonBlank(record.name))
78
81
  throw new Error('native sidechain metadata name is invalid');
79
82
  if (record.permissionMode !== undefined &&
@@ -236,7 +236,7 @@ export declare class ClaudeSessionService {
236
236
  private readonly hookLifecycle;
237
237
  private readonly leadOperations;
238
238
  private readonly fileChangeWatcher;
239
- private readonly activeTurnInputs;
239
+ private readonly turnCoordinator;
240
240
  private runtimeCwd;
241
241
  constructor(options: ClaudeSessionServiceOptions);
242
242
  nextScheduledPrompt(signal?: AbortSignal): Promise<ScheduledPrompt | null>;
@@ -21,9 +21,9 @@ import { BackgroundTaskRuntime, } from './background-task-runtime.js';
21
21
  import { backgroundAgentNotificationMarkers, } from './background-agent-manager.js';
22
22
  import { usageCostUsd } from '../core/usage.js';
23
23
  import { isSessionId } from '../core/session.js';
24
- import { ActiveTurnInputMailbox, } from '../core/active-turn-input.js';
25
24
  import { ContextBudget, estimateModelRequestTokens, isPromptTooLongError, } from '../core/context-budget.js';
26
25
  import { ContextEngine } from './context-engine.js';
26
+ import { ContextPreparation } from './context-preparation.js';
27
27
  import { TurnMemoryCoordinator } from './turn-memory-coordinator.js';
28
28
  import { injectFirstUserMessageContext, projectContextSnapshot, } from '../core/context.js';
29
29
  import { assembleContextSnapshot } from '../core/prompt-composer.js';
@@ -38,7 +38,7 @@ import { SubagentLifecycleStore } from '../persistence/subagent-lifecycle-store.
38
38
  import { ModelCompactor } from './model-compactor.js';
39
39
  import { agentMemoryPrompt, ClaudeSubagentExecutor, StructuredOutputRegistry, } from './subagent-service.js';
40
40
  import { ScheduledPromptManager, } from './scheduled-prompt-manager.js';
41
- import { TurnTerminalController } from './turn-lifecycle.js';
41
+ import { TurnCoordinator } from './turn-lifecycle.js';
42
42
  import { ClaudeScheduledToolRegistry } from '../tools/claude-scheduled-tools.js';
43
43
  import { ClaudeTaskToolRegistry } from '../tools/claude-task-tools.js';
44
44
  import { ClaudeWorkflowToolRegistry } from '../tools/claude-workflow-tools.js';
@@ -684,12 +684,16 @@ export class ClaudeSessionService {
684
684
  hookLifecycle;
685
685
  leadOperations;
686
686
  fileChangeWatcher;
687
- activeTurnInputs = new Map();
687
+ turnCoordinator;
688
688
  runtimeCwd;
689
689
  constructor(options) {
690
690
  const dataPlane = options.dataPlane ?? 'native';
691
691
  assertNativeDataPlane(dataPlane);
692
692
  this.options = { ...options, dataPlane };
693
+ this.turnCoordinator = new TurnCoordinator({
694
+ eventSink: options.eventSink ?? (() => undefined),
695
+ createSteeringId: randomUUID,
696
+ });
693
697
  this.assertNativeTranscriptOptions();
694
698
  this.leadOperations = options.teamLeadOperations ?? null;
695
699
  this.hookLifecycle = new HookLifecycle(options.hooks, options.eventSink);
@@ -899,18 +903,7 @@ export class ClaudeSessionService {
899
903
  }
900
904
  async close() {
901
905
  this.closing = true;
902
- for (const { mailbox } of this.activeTurnInputs.values()) {
903
- if (!mailbox)
904
- continue;
905
- for (const item of mailbox.close()) {
906
- this.options.eventSink?.({
907
- type: 'user-input-rejected',
908
- id: item.id,
909
- content: item.content,
910
- reason: 'closed',
911
- });
912
- }
913
- }
906
+ this.turnCoordinator.close();
914
907
  await this.fileChangeWatcher?.close(5_000);
915
908
  await this.hookLifecycle.close();
916
909
  await this.drainDetachedHookRuns(5_000);
@@ -1117,6 +1110,9 @@ export class ClaudeSessionService {
1117
1110
  ...(this.options.providerForModel
1118
1111
  ? { providerForModel: this.options.providerForModel }
1119
1112
  : {}),
1113
+ ...(this.options.providerForTurn
1114
+ ? { providerForTurn: this.options.providerForTurn }
1115
+ : {}),
1120
1116
  baseTools: wrappedBase,
1121
1117
  ...(this.options.deferMcpTools === undefined
1122
1118
  ? {}
@@ -1327,28 +1323,10 @@ export class ClaudeSessionService {
1327
1323
  });
1328
1324
  }
1329
1325
  steer(sessionId, content) {
1330
- const active = this.activeTurnInputs.get(sessionId);
1331
- if (!active)
1332
- return { kind: 'no-active-turn' };
1333
- const mailbox = active.mailbox;
1334
- if (!mailbox)
1335
- return { kind: 'not-steerable' };
1336
- const result = mailbox.enqueue(content);
1337
- if (result.kind === 'accepted')
1338
- return result;
1339
- if (result.kind === 'empty')
1340
- return result;
1341
- return { kind: 'turn-completing' };
1326
+ return this.turnCoordinator.steer(sessionId, content);
1342
1327
  }
1343
1328
  withdrawSteering(sessionId, id) {
1344
- const active = this.activeTurnInputs.get(sessionId);
1345
- if (!active)
1346
- return { kind: 'no-active-turn' };
1347
- const mailbox = active.mailbox;
1348
- if (!mailbox)
1349
- return { kind: 'not-steerable' };
1350
- const result = mailbox.withdraw(id);
1351
- return result.kind === 'withdrawn' ? result : { kind: 'not-pending' };
1329
+ return this.turnCoordinator.withdrawSteering(sessionId, id);
1352
1330
  }
1353
1331
  async resumeShell(sessionId, command, signal, name, resumeSessionAt) {
1354
1332
  this.worktreeManager?.bindSession(sessionId);
@@ -2532,29 +2510,8 @@ export class ClaudeSessionService {
2532
2510
  const documents = submission.kind === 'prompt' ? (submission.documents ?? []) : [];
2533
2511
  const shellCommand = submission.kind === 'shell' ? submission.command : undefined;
2534
2512
  const skipUserPrompt = submission.kind === 'retry';
2535
- const controller = new TurnTerminalController(this.options.eventSink ?? (() => undefined));
2536
- let activeTurnInput;
2537
- let activeTurnRecord;
2538
- try {
2513
+ return this.turnCoordinator.run(request, async ({ emit, steering }) => {
2539
2514
  this.assertTurnWritable();
2540
- if (prompt.length === 0 && images.length === 0 && documents.length === 0)
2541
- throw new Error('Prompt must not be empty');
2542
- if (name !== undefined && name.length === 0) {
2543
- throw new Error('Session name must not be empty');
2544
- }
2545
- if (shellCommand !== undefined && shellCommand.trim().length === 0) {
2546
- throw new Error('Shell command must not be empty');
2547
- }
2548
- if (this.activeTurnInputs.has(sessionId)) {
2549
- throw new Error(`conflict: locked (session ${sessionId} already has an active turn)`);
2550
- }
2551
- activeTurnRecord =
2552
- shellCommand === undefined
2553
- ? {
2554
- mailbox: (activeTurnInput = new ActiveTurnInputMailbox(randomUUID)),
2555
- }
2556
- : {};
2557
- this.activeTurnInputs.set(sessionId, activeTurnRecord);
2558
2515
  await this.activateSessionCostTracker(sessionId);
2559
2516
  await this.ensureFileResources(sessionId, signal);
2560
2517
  this.worktreeManager?.bindSession(sessionId);
@@ -2858,6 +2815,9 @@ export class ClaudeSessionService {
2858
2815
  ...(this.options.providerForModel
2859
2816
  ? { providerForModel: this.options.providerForModel }
2860
2817
  : {}),
2818
+ ...(this.options.providerForTurn
2819
+ ? { providerForTurn: this.options.providerForTurn }
2820
+ : {}),
2861
2821
  baseTools,
2862
2822
  ...(this.options.deferMcpTools === undefined
2863
2823
  ? {}
@@ -3065,7 +3025,7 @@ export class ClaudeSessionService {
3065
3025
  deferPreToolUseOutcome: (call) => pendingRecoveryToolCallIds.has(call.id),
3066
3026
  })
3067
3027
  : null;
3068
- const runtime = new AgentRuntime(provider, controller.emit, {
3028
+ const runtime = new AgentRuntime(provider, emit, {
3069
3029
  emitInitialContextState: false,
3070
3030
  ...(this.options.emitToolUseSummaries
3071
3031
  ? {
@@ -3407,19 +3367,30 @@ export class ClaudeSessionService {
3407
3367
  let agentSystem = null;
3408
3368
  let planModeMessage;
3409
3369
  let sessionMemoryMessage = null;
3410
- let contextMessages = [];
3411
- let contextProjection = {
3412
- systemMessages: [],
3413
- stableSystemSectionCount: 0,
3414
- };
3415
- let stableSystemMessageCount = 0;
3370
+ let agentMentionMessages = [];
3371
+ const contextPreparation = new ContextPreparation({
3372
+ ...(this.options.contextAssembler
3373
+ ? { assembler: this.options.contextAssembler }
3374
+ : {}),
3375
+ sources: {
3376
+ history: activeTurnMessages,
3377
+ memory: () => projectMemoryRecallMessages,
3378
+ activeTools: () => provider.capabilities.tools
3379
+ ? (activeTurnTools?.definitions() ?? [])
3380
+ : [],
3381
+ },
3382
+ agentMentions: () => ({
3383
+ prompt: effectivePrompt,
3384
+ messages: agentMentionMessages,
3385
+ }),
3386
+ });
3416
3387
  refreshRuntimeContext = async () => {
3417
3388
  agentSystem = await this.mainAgentSystemPrompt(agent);
3418
3389
  planModeMessage =
3419
3390
  this.options.interactiveTools?.contextMessage(sessionId);
3420
3391
  // DEBUG_PLAN_CONTEXT
3421
3392
  sessionMemoryMessage = this.sessionMemoryMessage(await turnMemory.sessionSummary());
3422
- const assembledContext = await assembleContextSnapshot(this.options.contextAssembler, {
3393
+ await contextPreparation.refresh({
3423
3394
  cwd: this.activeCwd(),
3424
3395
  lifecycleId: sessionId,
3425
3396
  ...(agentSystem
@@ -3436,10 +3407,6 @@ export class ClaudeSessionService {
3436
3407
  : {}),
3437
3408
  },
3438
3409
  });
3439
- contextProjection = projectContextSnapshot(assembledContext);
3440
- stableSystemMessageCount =
3441
- contextProjection.stableSystemSectionCount;
3442
- contextMessages = [...contextProjection.systemMessages];
3443
3410
  };
3444
3411
  await refreshRuntimeContext();
3445
3412
  const expansion = shouldSkipUserPrompt()
@@ -3469,9 +3436,6 @@ export class ClaudeSessionService {
3469
3436
  let compactionDurationMs;
3470
3437
  let compactionDurationWithoutRetriesMs;
3471
3438
  let compactionModelUsage;
3472
- const currentDefinitions = () => provider.capabilities.tools
3473
- ? (activeTurnTools?.definitions() ?? [])
3474
- : [];
3475
3439
  const budget = this.contextBudget(provider);
3476
3440
  const contextEngine = new ContextEngine({
3477
3441
  ...(budget ? { budget } : {}),
@@ -3503,88 +3467,50 @@ export class ClaudeSessionService {
3503
3467
  ? { documents }
3504
3468
  : {}),
3505
3469
  }));
3506
- const agentMentionMessages = shellCommand === undefined && !shouldSkipUserPrompt()
3507
- ? (this.options.extensions?.agentMentionMessages(effectivePrompt) ?? [])
3508
- : [];
3509
- const injectAgentMentionContext = (messages) => {
3510
- if (agentMentionMessages.length === 0)
3511
- return [...messages];
3512
- let insertionIndex = messages.length;
3513
- let foundPrompt = false;
3514
- for (let index = messages.length - 1; index >= 0; index -= 1) {
3515
- const message = messages[index];
3516
- if (message?.role === 'user' &&
3517
- typeof message.content === 'string' &&
3518
- message.content.endsWith(effectivePrompt)) {
3519
- insertionIndex = index;
3520
- foundPrompt = true;
3521
- break;
3522
- }
3523
- }
3524
- if (!foundPrompt)
3525
- return [...messages];
3526
- return [
3527
- ...messages.slice(0, insertionIndex),
3528
- ...agentMentionMessages.map((content) => ({
3529
- role: 'user',
3530
- content,
3531
- })),
3532
- ...messages.slice(insertionIndex),
3533
- ];
3534
- };
3535
- const injectDynamicContext = (messages) => injectFirstUserMessageContext(messages, contextProjection.firstUserMessageContext);
3536
- const injectTurnContext = (messages) => injectAgentMentionContext(injectDynamicContext(messages));
3470
+ agentMentionMessages =
3471
+ shellCommand === undefined && !shouldSkipUserPrompt()
3472
+ ? (this.options.extensions?.agentMentionMessages(effectivePrompt) ?? [])
3473
+ : [];
3537
3474
  let compactionAnchorUuid = this.lastMessageUuid(snapshot.entries);
3538
3475
  const contextTransitionPort = (pendingMessages = [], preservedUserMessages = []) => ({
3539
3476
  current: () => {
3540
- const historyMessages = [
3541
- ...activeTurnMessages(),
3542
- ...projectMemoryRecallMessages,
3543
- ];
3544
- return {
3545
- messages: [
3546
- ...contextMessages,
3547
- ...injectTurnContext([
3548
- ...historyMessages,
3549
- ...pendingMessages,
3550
- ]),
3551
- ],
3552
- tools: currentDefinitions(),
3553
- };
3477
+ const projection = contextPreparation.project({
3478
+ pendingMessages,
3479
+ });
3480
+ return projection.envelope;
3554
3481
  },
3555
- irreducible: () => ({
3556
- messages: [
3557
- ...contextMessages,
3558
- ...injectTurnContext([
3559
- ...pendingMessages,
3560
- ...preservedUserMessages.map((content) => ({
3561
- role: 'user',
3562
- content,
3563
- })),
3564
- ]),
3482
+ irreducible: () => contextPreparation.project({
3483
+ includeHistory: false,
3484
+ includeMemory: false,
3485
+ pendingMessages: [
3486
+ ...pendingMessages,
3487
+ ...preservedUserMessages.map((content) => ({
3488
+ role: 'user',
3489
+ content,
3490
+ })),
3565
3491
  ],
3566
- tools: currentDefinitions(),
3567
- }),
3492
+ }).envelope,
3568
3493
  propose: async () => {
3569
3494
  const activeNativeLease = nativeLease;
3570
3495
  if (!budget)
3571
3496
  throw new Error('Context budget is unavailable');
3572
- const definitions = currentDefinitions();
3497
+ const definitions = contextPreparation.project().envelope.tools;
3573
3498
  const historyMessages = activeTurnMessages();
3574
3499
  if (historyMessages.length === 0)
3575
3500
  throw new Error('Cannot compact an empty native transcript');
3576
3501
  if (unresolvedActiveToolCallIds(historyMessages).length > 0)
3577
3502
  throw new Error('Cannot compact a native transcript with unresolved tool calls');
3578
- const irreducibleMessages = [
3579
- ...contextMessages,
3580
- ...injectTurnContext([
3503
+ const irreducibleMessages = contextPreparation.project({
3504
+ includeHistory: false,
3505
+ includeMemory: false,
3506
+ pendingMessages: [
3581
3507
  ...pendingMessages,
3582
3508
  ...preservedUserMessages.map((content) => ({
3583
3509
  role: 'user',
3584
3510
  content,
3585
3511
  })),
3586
- ]),
3587
- ];
3512
+ ],
3513
+ }).envelope.messages;
3588
3514
  let compactableMessages = historyMessages;
3589
3515
  let preservedMessages = [];
3590
3516
  let compactionLogicalParentId;
@@ -3751,14 +3677,15 @@ export class ClaudeSessionService {
3751
3677
  };
3752
3678
  let replayMessages = preservedMessages;
3753
3679
  try {
3754
- const replayReport = budget.evaluate([
3755
- ...contextMessages,
3756
- ...injectTurnContext([
3680
+ const replayReport = budget.evaluate(contextPreparation.project({
3681
+ includeHistory: false,
3682
+ includeMemory: false,
3683
+ pendingMessages: [
3757
3684
  summaryMessage,
3758
3685
  ...replayMessages,
3759
3686
  ...pendingMessages,
3760
- ]),
3761
- ], definitions);
3687
+ ],
3688
+ }).envelope.messages, definitions);
3762
3689
  if (replayReport.shouldCompact)
3763
3690
  throw new Error('replay overflow');
3764
3691
  }
@@ -3769,23 +3696,16 @@ export class ClaudeSessionService {
3769
3696
  // contains its contents and user prompts remain available.
3770
3697
  replayMessages = replayMessages.filter((message) => message.role === 'user' && !Array.isArray(message.content));
3771
3698
  }
3772
- const proposedMessages = [
3773
- ...contextMessages,
3774
- ...injectTurnContext([
3775
- summaryMessage,
3776
- ...replayMessages,
3777
- ...pendingMessages,
3778
- ]),
3779
- ];
3699
+ const replacement = contextPreparation.proposeHistoryReplacement({
3700
+ historyMessages: [summaryMessage, ...replayMessages],
3701
+ pendingMessages,
3702
+ });
3780
3703
  return {
3781
- envelope: {
3782
- messages: proposedMessages,
3783
- tools: definitions,
3784
- },
3704
+ envelope: replacement.envelope,
3785
3705
  commit: async () => {
3786
3706
  if (signal?.aborted)
3787
3707
  throw new AgentRunCancelledError();
3788
- const ids = await nativeLease.appendCompaction({
3708
+ const committed = await replacement.commit(() => nativeLease.appendCompaction({
3789
3709
  summary: compacted.summary,
3790
3710
  trigger: 'auto',
3791
3711
  preTokens,
@@ -3802,7 +3722,8 @@ export class ClaudeSessionService {
3802
3722
  preservePrefix: false,
3803
3723
  }
3804
3724
  : {}),
3805
- });
3725
+ }));
3726
+ const ids = committed.value;
3806
3727
  await this.runAdvisoryHook(sessionId, 'PostCompact', { trigger: 'auto', compact_summary: compacted.summary }, 'auto', signal);
3807
3728
  if (this.options.hooks) {
3808
3729
  const outcome = await this.hookLifecycle.refresh(sessionId, hookSession, signal);
@@ -4089,25 +4010,24 @@ export class ClaudeSessionService {
4089
4010
  };
4090
4011
  }
4091
4012
  if (shellCommand === undefined && budget) {
4092
- const definitions = currentDefinitions();
4093
4013
  await contextEngine.prepare(contextTransitionPort([], currentTurnUserMessages ?? []), signal);
4094
- budget.assertFits(budget.evaluate([
4095
- ...contextMessages,
4096
- ...injectTurnContext(activeTurnMessages()),
4097
- ], definitions));
4014
+ const projection = contextPreparation.project({
4015
+ includeMemory: false,
4016
+ });
4017
+ budget.assertFits(budget.evaluate(projection.envelope.messages, projection.envelope.tools));
4098
4018
  }
4099
4019
  let stopHookActive = false;
4020
+ const initialProjection = contextPreparation.project({
4021
+ includeMemory: false,
4022
+ });
4100
4023
  const runtimeRequest = {
4101
4024
  sessionId,
4102
- messages: [
4103
- ...contextMessages,
4104
- ...injectTurnContext(activeTurnMessages()),
4105
- ],
4106
- stableSystemMessageCount,
4025
+ messages: initialProjection.envelope.messages,
4026
+ stableSystemMessageCount: initialProjection.stableSystemMessageCount,
4107
4027
  cwd: this.activeCwd(),
4108
4028
  toolResultDirectory,
4109
4029
  observer,
4110
- ...(activeTurnInput ? { steering: activeTurnInput } : {}),
4030
+ ...(steering ? { steering } : {}),
4111
4031
  ...(this.options.effort ? { effort: this.options.effort } : {}),
4112
4032
  ...(this.options.maxModelTurns !== undefined
4113
4033
  ? { maxModelTurns: this.options.maxModelTurns }
@@ -4132,14 +4052,10 @@ export class ClaudeSessionService {
4132
4052
  if (shellCommand === undefined) {
4133
4053
  await contextEngine.prepare(contextTransitionPort([], currentTurnUserMessages ?? []), signal);
4134
4054
  }
4135
- runtimeRequest.stableSystemMessageCount = stableSystemMessageCount;
4136
- return [
4137
- ...contextMessages,
4138
- ...injectTurnContext([
4139
- ...activeTurnMessages(),
4140
- ...projectMemoryRecallMessages,
4141
- ]),
4142
- ];
4055
+ const projection = contextPreparation.project();
4056
+ runtimeRequest.stableSystemMessageCount =
4057
+ projection.stableSystemMessageCount;
4058
+ return projection.envelope.messages;
4143
4059
  },
4144
4060
  ...(this.options.hooks ||
4145
4061
  subagentExecutor ||
@@ -4268,14 +4184,10 @@ export class ClaudeSessionService {
4268
4184
  }
4269
4185
  // The single reactive retry must use the compacted transcript, not
4270
4186
  // the stale request copy captured before the compact boundary.
4271
- runtimeRequest.messages = [
4272
- ...contextMessages,
4273
- ...injectTurnContext([
4274
- ...activeTurnMessages(),
4275
- ...projectMemoryRecallMessages,
4276
- ]),
4277
- ];
4278
- runtimeRequest.stableSystemMessageCount = stableSystemMessageCount;
4187
+ const projection = contextPreparation.project();
4188
+ runtimeRequest.messages = projection.envelope.messages;
4189
+ runtimeRequest.stableSystemMessageCount =
4190
+ projection.stableSystemMessageCount;
4279
4191
  runtimeRequest.deferFailureKinds = true;
4280
4192
  try {
4281
4193
  result = await attemptMainTurn();
@@ -4411,11 +4323,11 @@ export class ClaudeSessionService {
4411
4323
  ? projectNativeSessionEntries(nativeLease.activeEvents()).at(-1)
4412
4324
  ?.uuid
4413
4325
  : undefined;
4414
- const providerVisibleMessages = [
4415
- ...contextMessages,
4416
- ...injectTurnContext(memorySnapshot),
4417
- ];
4418
- const definitions = currentDefinitions();
4326
+ const providerProjection = contextPreparation.project({
4327
+ includeMemory: false,
4328
+ });
4329
+ const providerVisibleMessages = providerProjection.envelope.messages;
4330
+ const definitions = providerProjection.envelope.tools;
4419
4331
  const currentContextTokens = contextEngine.report({
4420
4332
  messages: providerVisibleMessages,
4421
4333
  tools: definitions,
@@ -4501,27 +4413,8 @@ export class ClaudeSessionService {
4501
4413
  return scratchResult.value;
4502
4414
  });
4503
4415
  }
4504
- controller.complete();
4505
4416
  return result;
4506
- }
4507
- catch (error) {
4508
- controller.fail(error, signal);
4509
- throw error;
4510
- }
4511
- finally {
4512
- if (activeTurnInput !== undefined) {
4513
- for (const item of activeTurnInput.close()) {
4514
- this.options.eventSink?.({
4515
- type: 'user-input-rejected',
4516
- id: item.id,
4517
- content: item.content,
4518
- reason: signal?.aborted ? 'cancelled' : 'failed',
4519
- });
4520
- }
4521
- }
4522
- if (this.activeTurnInputs.get(sessionId) === activeTurnRecord)
4523
- this.activeTurnInputs.delete(sessionId);
4524
- }
4417
+ });
4525
4418
  }
4526
4419
  async ensureFileResources(sessionId, signal) {
4527
4420
  const resources = this.options.fileResources ?? [];
@@ -76,6 +76,7 @@ export interface ClaudeSubagentExecutorOptions {
76
76
  maxCalls?: number;
77
77
  maxOutputBytes?: number;
78
78
  providerForModel?: (model: string) => ModelProvider;
79
+ providerForTurn?: (model?: string) => ModelProvider;
79
80
  toolNames?: readonly string[];
80
81
  backgroundTaskNotifications?: (waitForRunning: boolean) => Promise<string[]>;
81
82
  notificationDelivered?: (notification: {
@@ -117,6 +118,7 @@ export declare class ClaudeSubagentExecutor {
117
118
  sendBackgroundMessage(agentId: string, message: string, summary: string | undefined, toolUseId: string): string;
118
119
  stopAllBackgroundTasks(): readonly string[];
119
120
  private cwd;
121
+ private providerForTurn;
120
122
  private agentDefinition;
121
123
  private resolveAgentInput;
122
124
  registry(sessionId: string, depth: number, promptIdForCall: (callId: string) => string | null, parentAgentId?: string): ToolRegistry;
@@ -586,6 +586,15 @@ export class ClaudeSubagentExecutor {
586
586
  cwd() {
587
587
  return this.options.cwdProvider?.() ?? this.options.cwd;
588
588
  }
589
+ providerForTurn(model) {
590
+ if (this.options.providerForTurn) {
591
+ return this.options.providerForTurn(model);
592
+ }
593
+ if (model !== undefined) {
594
+ return this.options.providerForModel?.(model) ?? this.options.provider;
595
+ }
596
+ return this.options.provider;
597
+ }
589
598
  agentDefinition(input) {
590
599
  return this.options.extensions?.agent(input.subagentType) ?? null;
591
600
  }
@@ -753,7 +762,9 @@ export class ClaudeSubagentExecutor {
753
762
  !this.options.extensions?.agent(input.subagentType)) {
754
763
  throw new Error(`Unknown Claude agent ${input.subagentType}`);
755
764
  }
756
- if (input.model && !this.options.providerForModel) {
765
+ if (input.model &&
766
+ !this.options.providerForModel &&
767
+ !this.options.providerForTurn) {
757
768
  throw new Error('Agent model overrides are unavailable for this provider');
758
769
  }
759
770
  if (input.permissionMode &&
@@ -850,6 +861,7 @@ export class ClaudeSubagentExecutor {
850
861
  ...(input.permissionMode
851
862
  ? { permissionMode: input.permissionMode }
852
863
  : {}),
864
+ ...(input.model ? { model: input.model } : {}),
853
865
  ...(input.isolation ? { isolation: input.isolation } : {}),
854
866
  ...(parentAgentId ? { parentAgentId } : {}),
855
867
  ...(initialIsolation ? { worktreePath: initialIsolation.cwd } : {}),
@@ -863,9 +875,7 @@ export class ClaudeSubagentExecutor {
863
875
  catch (error) {
864
876
  return settleInitialSetupFailure(error, initialIsolation);
865
877
  }
866
- const provider = input.model
867
- ? (this.options.providerForModel?.(input.model) ?? this.options.provider)
868
- : this.options.provider;
878
+ const provider = this.providerForTurn(input.model);
869
879
  const backgroundRun = this.createBackgroundAgentRun({
870
880
  input,
871
881
  parentCwd,
@@ -874,11 +884,14 @@ export class ClaudeSubagentExecutor {
874
884
  ...(initialIsolation ? { initialIsolation } : {}),
875
885
  createIsolation: () => this.createAgentWorktree(paths.praxisRoot, sessionId, agentId, parentCwd),
876
886
  execute: async (cwd, message, signal, continuation) => {
887
+ const turnProvider = continuation
888
+ ? this.providerForTurn(input.model)
889
+ : provider;
877
890
  const run = (lease) => this.runSidechain({
878
891
  ...lease,
879
892
  sessionId,
880
893
  input,
881
- provider,
894
+ provider: turnProvider,
882
895
  agentId,
883
896
  spawnDepth,
884
897
  promptId,
@@ -1346,13 +1359,12 @@ export class ClaudeSubagentExecutor {
1346
1359
  !this.options.extensions?.agent(options.agentType)) {
1347
1360
  throw new Error(`Unknown Claude agent ${options.agentType}`);
1348
1361
  }
1349
- if (options.model && !this.options.providerForModel) {
1362
+ if (options.model &&
1363
+ !this.options.providerForModel &&
1364
+ !this.options.providerForTurn) {
1350
1365
  throw new Error('Workflow agent model overrides are unavailable for this provider');
1351
1366
  }
1352
- const provider = options.model
1353
- ? (this.options.providerForModel?.(options.model) ??
1354
- this.options.provider)
1355
- : this.options.provider;
1367
+ const provider = this.providerForTurn(options.model);
1356
1368
  const input = {
1357
1369
  description: options.label ?? 'Workflow agent',
1358
1370
  prompt: options.prompt,
@@ -1395,6 +1407,7 @@ export class ClaudeSubagentExecutor {
1395
1407
  spawnDepth: 1,
1396
1408
  cwd: agentCwd,
1397
1409
  promptId: options.promptId,
1410
+ ...(input.model ? { model: input.model } : {}),
1398
1411
  ...(options.isolation ? { isolation: options.isolation } : {}),
1399
1412
  ...(isolation ? { worktreePath: isolation.cwd } : {}),
1400
1413
  });
@@ -1716,11 +1729,11 @@ export class ClaudeSubagentExecutor {
1716
1729
  prompt,
1717
1730
  subagentType: agentType,
1718
1731
  ...(name ? { name } : {}),
1732
+ ...(metadata?.model ? { model: metadata.model } : {}),
1719
1733
  ...(permissionMode ? { permissionMode } : {}),
1720
1734
  ...(isolation ? { isolation } : {}),
1721
1735
  runInBackground: true,
1722
1736
  };
1723
- const provider = this.options.provider;
1724
1737
  const recoveredPromptId = metadata?.promptId ?? randomUUID();
1725
1738
  const backgroundRun = this.createBackgroundAgentRun({
1726
1739
  input,
@@ -1731,6 +1744,9 @@ export class ClaudeSubagentExecutor {
1731
1744
  ...(restoredIsolation ? { initialIsolation: restoredIsolation } : {}),
1732
1745
  createIsolation: () => this.createAgentWorktree(paths.praxisRoot, sessionId, agentId, parentCwd),
1733
1746
  execute: async (cwd, message, signal, continuation) => {
1747
+ // Recovery never restores provider-native route state. Each recovered
1748
+ // execution, including every later follow-up, gets a fresh turn.
1749
+ const provider = this.providerForTurn(input.model);
1734
1750
  const run = (lease) => this.runSidechain({
1735
1751
  ...lease,
1736
1752
  sessionId,
@@ -1757,7 +1773,7 @@ export class ClaudeSubagentExecutor {
1757
1773
  prompt,
1758
1774
  toolUseId,
1759
1775
  outputFile: sidechainPaths.transcriptFile,
1760
- resolvedModel: provider.model ?? 'praxis/provider',
1776
+ resolvedModel: metadata?.model ?? this.options.provider.model ?? 'praxis/provider',
1761
1777
  lifecycle: backgroundRun.lifecycle,
1762
1778
  run: backgroundRun.run,
1763
1779
  markBackground: backgroundRun.markBackground,
@@ -14,6 +14,7 @@ export interface ClaudeTeamAgentRuntimeOptions {
14
14
  readonly hooks?: ClaudeHookRunner;
15
15
  readonly contextAssembler?: ContextAssembler;
16
16
  readonly providerForModel?: (model: string) => ModelProvider;
17
+ readonly providerForTurn?: (model?: string) => ModelProvider;
17
18
  readonly permissionResolverForMode?: (mode: AgentPermissionMode) => PermissionResolver;
18
19
  readonly eventSink?: RuntimeEventSink;
19
20
  readonly approveTool?: (call: ModelToolCall, originalCall?: ModelToolCall, decision?: PermissionDecision) => PermissionApproval | Promise<PermissionApproval>;
@@ -79,6 +79,9 @@ export class ClaudeTeamAgentRuntime {
79
79
  ...(this.options.providerForModel
80
80
  ? { providerForModel: this.options.providerForModel }
81
81
  : {}),
82
+ ...(this.options.providerForTurn
83
+ ? { providerForTurn: this.options.providerForTurn }
84
+ : {}),
82
85
  ...(this.options.permissionResolverForMode
83
86
  ? {
84
87
  permissionResolverForMode: this.options.permissionResolverForMode,
@@ -1,6 +1,5 @@
1
1
  import { type ModelDocument, type ModelImage, type RuntimeEventSink } from '../core/runtime.js';
2
- import type { LifecycleState } from '../core/agent-orchestration.js';
3
- export type TurnTerminalState = Extract<LifecycleState, 'completed' | 'failed' | 'cancelled'>;
2
+ import { type ActiveTurnInputCommandResult, type ActiveTurnInputPort } from '../core/active-turn-input.js';
4
3
  export type TurnActivation = {
5
4
  kind: 'start';
6
5
  sessionId: string;
@@ -28,13 +27,26 @@ export interface TurnRequest {
28
27
  submission: TurnSubmission;
29
28
  signal?: AbortSignal;
30
29
  }
31
- export declare class TurnTerminalController {
32
- private readonly sink;
33
- private terminal;
34
- constructor(sink: RuntimeEventSink);
35
- emit: RuntimeEventSink;
36
- complete(): void;
37
- fail(error: unknown, signal?: AbortSignal): void;
30
+ export interface TurnScope {
31
+ readonly emit: RuntimeEventSink;
32
+ readonly steering?: ActiveTurnInputPort;
33
+ }
34
+ export interface TurnCoordinatorOptions {
35
+ readonly eventSink: RuntimeEventSink;
36
+ readonly createSteeringId: () => string;
37
+ }
38
+ /** Owns the lifecycle and active-turn coordination for one session service. */
39
+ export declare class TurnCoordinator {
40
+ private readonly options;
41
+ private readonly activeTurns;
42
+ constructor(options: TurnCoordinatorOptions);
43
+ run<T>(request: TurnRequest, work: (scope: TurnScope) => Promise<T>): Promise<T>;
44
+ steer(sessionId: string, content: string): ActiveTurnInputCommandResult;
45
+ withdrawSteering(sessionId: string, id: string): ActiveTurnInputCommandResult;
46
+ close(): void;
47
+ private validateRequest;
48
+ private terminalState;
38
49
  private transition;
50
+ private rejectPending;
39
51
  }
40
52
  //# sourceMappingURL=turn-lifecycle.d.ts.map
@@ -1,36 +1,150 @@
1
1
  import { AgentRunCancelledError, ModelProviderError, } from '../core/runtime.js';
2
- export class TurnTerminalController {
3
- sink;
4
- terminal = false;
5
- constructor(sink) {
6
- this.sink = sink;
7
- }
8
- emit = (event) => {
9
- if (event.type === 'state' &&
10
- (event.state === 'completed' ||
11
- event.state === 'cancelled' ||
12
- event.state === 'failed')) {
13
- return;
2
+ import { ActiveTurnInputMailbox, } from '../core/active-turn-input.js';
3
+ /** Owns the lifecycle and active-turn coordination for one session service. */
4
+ export class TurnCoordinator {
5
+ options;
6
+ activeTurns = new Map();
7
+ constructor(options) {
8
+ this.options = options;
9
+ }
10
+ async run(request, work) {
11
+ const { sessionId } = request.activation;
12
+ const mailbox = request.submission.kind === 'shell'
13
+ ? undefined
14
+ : new ActiveTurnInputMailbox(this.options.createSteeringId);
15
+ const record = {
16
+ ...(mailbox ? { mailbox } : {}),
17
+ terminal: false,
18
+ };
19
+ let terminalState = 'failed';
20
+ let pendingFailure;
21
+ const scope = {
22
+ emit: (event) => {
23
+ if (event.type === 'state' &&
24
+ (event.state === 'completed' ||
25
+ event.state === 'failed' ||
26
+ event.state === 'cancelled')) {
27
+ return;
28
+ }
29
+ this.options.eventSink(event);
30
+ },
31
+ ...(mailbox ? { steering: mailbox } : {}),
32
+ };
33
+ try {
34
+ this.validateRequest(request);
35
+ if (this.activeTurns.has(sessionId)) {
36
+ throw new Error(`conflict: locked (session ${sessionId} already has an active turn)`);
37
+ }
38
+ this.activeTurns.set(sessionId, record);
39
+ const result = await work(scope);
40
+ terminalState = 'completed';
41
+ this.transition(record, 'completed');
42
+ return result;
43
+ }
44
+ catch (error) {
45
+ if (!record.terminal) {
46
+ terminalState = this.terminalState(error, request.signal);
47
+ this.transition(record, terminalState);
48
+ }
49
+ throw error;
50
+ }
51
+ finally {
52
+ try {
53
+ if (mailbox) {
54
+ pendingFailure = this.rejectPending(mailbox.close(), terminalState === 'cancelled' ? 'cancelled' : 'failed');
55
+ }
56
+ }
57
+ finally {
58
+ if (this.activeTurns.get(sessionId) === record) {
59
+ this.activeTurns.delete(sessionId);
60
+ }
61
+ }
62
+ if (pendingFailure) {
63
+ // A rejected-input sink failure intentionally retains its prior precedence.
64
+ // eslint-disable-next-line no-unsafe-finally -- compatibility is covered by the sink-error regression
65
+ throw pendingFailure.error;
66
+ }
14
67
  }
15
- this.sink(event);
16
- };
17
- complete() {
18
- this.transition('completed');
19
68
  }
20
- fail(error, signal) {
21
- if (this.terminal)
22
- return;
23
- const cancelled = signal?.aborted === true ||
69
+ steer(sessionId, content) {
70
+ const active = this.activeTurns.get(sessionId);
71
+ if (!active)
72
+ return { kind: 'no-active-turn' };
73
+ if (!active.mailbox)
74
+ return { kind: 'not-steerable' };
75
+ const result = active.mailbox.enqueue(content);
76
+ if (result.kind === 'accepted' || result.kind === 'empty')
77
+ return result;
78
+ return { kind: 'turn-completing' };
79
+ }
80
+ withdrawSteering(sessionId, id) {
81
+ const active = this.activeTurns.get(sessionId);
82
+ if (!active)
83
+ return { kind: 'no-active-turn' };
84
+ if (!active.mailbox)
85
+ return { kind: 'not-steerable' };
86
+ const result = active.mailbox.withdraw(id);
87
+ return result.kind === 'withdrawn' ? result : { kind: 'not-pending' };
88
+ }
89
+ close() {
90
+ let firstFailure;
91
+ for (const active of this.activeTurns.values()) {
92
+ if (!active.mailbox)
93
+ continue;
94
+ const failure = this.rejectPending(active.mailbox.close(), 'closed');
95
+ firstFailure ??= failure;
96
+ }
97
+ if (firstFailure)
98
+ throw firstFailure.error;
99
+ }
100
+ validateRequest(request) {
101
+ const { activation, submission } = request;
102
+ const prompt = submission.kind === 'shell'
103
+ ? `! ${submission.command}`
104
+ : submission.kind === 'retry'
105
+ ? submission.prompt
106
+ : submission.text;
107
+ const images = submission.kind === 'prompt' ? (submission.images ?? []) : [];
108
+ const documents = submission.kind === 'prompt' ? (submission.documents ?? []) : [];
109
+ if (prompt.length === 0 && images.length === 0 && documents.length === 0) {
110
+ throw new Error('Prompt must not be empty');
111
+ }
112
+ if (activation.name !== undefined && activation.name.length === 0) {
113
+ throw new Error('Session name must not be empty');
114
+ }
115
+ if (submission.kind === 'shell' && submission.command.trim().length === 0) {
116
+ throw new Error('Shell command must not be empty');
117
+ }
118
+ }
119
+ terminalState(error, signal) {
120
+ return signal?.aborted === true ||
24
121
  error instanceof AgentRunCancelledError ||
25
- (error instanceof ModelProviderError && error.kind === 'cancelled');
26
- this.transition(cancelled ? 'cancelled' : 'failed');
122
+ (error instanceof ModelProviderError && error.kind === 'cancelled')
123
+ ? 'cancelled'
124
+ : 'failed';
125
+ }
126
+ transition(record, state) {
127
+ if (record.terminal)
128
+ return;
129
+ record.terminal = true;
130
+ this.options.eventSink({ type: 'state', state });
27
131
  }
28
- transition(state) {
29
- if (this.terminal) {
30
- throw new Error('Turn terminal transition already emitted');
132
+ rejectPending(items, reason) {
133
+ let firstFailure;
134
+ for (const item of items) {
135
+ try {
136
+ this.options.eventSink({
137
+ type: 'user-input-rejected',
138
+ id: item.id,
139
+ content: item.content,
140
+ reason,
141
+ });
142
+ }
143
+ catch (error) {
144
+ firstFailure ??= { error };
145
+ }
31
146
  }
32
- this.terminal = true;
33
- this.sink({ type: 'state', state });
147
+ return firstFailure;
34
148
  }
35
149
  }
36
150
  //# sourceMappingURL=turn-lifecycle.js.map
@@ -1477,9 +1477,11 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1477
1477
  });
1478
1478
  if (memoryDirectory)
1479
1479
  await mkdir(memoryDirectory, { recursive: true });
1480
- const projectMemoryProviderFactory = providerForMainModel && model
1481
- ? () => providerForMainModel(model)
1482
- : undefined;
1480
+ const projectMemoryProviderFactory = providerForTurn
1481
+ ? () => providerForTurn()
1482
+ : providerForMainModel && model
1483
+ ? () => providerForMainModel(model)
1484
+ : undefined;
1483
1485
  const projectMemoryRecall = projectMemoryPolicy.recall &&
1484
1486
  memoryDirectory &&
1485
1487
  projectMemoryProviderFactory
@@ -1911,6 +1913,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1911
1913
  ...(hooks ? { hooks } : {}),
1912
1914
  ...(contextAssembler ? { contextAssembler } : {}),
1913
1915
  ...(providerForModel ? { providerForModel } : {}),
1916
+ ...(providerForTurn ? { providerForTurn } : {}),
1914
1917
  permissionResolverForMode,
1915
1918
  eventSink: runtimeEventSink,
1916
1919
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.54.0",
3
+ "version": "0.55.1",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",