praxis-agent 0.26.0 → 0.28.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 (44) hide show
  1. package/dist/application/session-memory.d.ts +43 -10
  2. package/dist/application/session-memory.js +263 -62
  3. package/dist/application/session-service.d.ts +13 -4
  4. package/dist/application/session-service.js +139 -36
  5. package/dist/application/subagent-service.d.ts +1 -0
  6. package/dist/application/subagent-service.js +23 -1
  7. package/dist/cli-runtime.d.ts +4 -0
  8. package/dist/cli-runtime.js +10 -0
  9. package/dist/core/runtime.d.ts +10 -0
  10. package/dist/core/runtime.js +111 -28
  11. package/dist/core/tool-execution-scheduler.d.ts +38 -0
  12. package/dist/core/tool-execution-scheduler.js +95 -0
  13. package/dist/core/tool-scheduling-policy.d.ts +4 -0
  14. package/dist/core/tool-scheduling-policy.js +24 -0
  15. package/dist/extensions/claude-extension-tools.d.ts +1 -0
  16. package/dist/extensions/claude-extension-tools.js +6 -0
  17. package/dist/hooks/claude-hook-tools.d.ts +4 -0
  18. package/dist/hooks/claude-hook-tools.js +6 -0
  19. package/dist/mcp/claude-mcp-tools.d.ts +1 -0
  20. package/dist/mcp/claude-mcp-tools.js +42 -0
  21. package/dist/permissions/claude-permission-resolver.js +3 -3
  22. package/dist/permissions/permission-updates.d.ts +1 -0
  23. package/dist/permissions/permission-updates.js +13 -1
  24. package/dist/tools/claude-capabilities.d.ts +1 -0
  25. package/dist/tools/claude-capabilities.js +5 -0
  26. package/dist/tools/claude-interactive-tools.js +7 -0
  27. package/dist/tools/claude-lsp-tool.js +7 -0
  28. package/dist/tools/claude-scheduled-tools.d.ts +1 -0
  29. package/dist/tools/claude-scheduled-tools.js +7 -0
  30. package/dist/tools/claude-task-tools.d.ts +1 -0
  31. package/dist/tools/claude-task-tools.js +11 -0
  32. package/dist/tools/claude-user-message.d.ts +1 -0
  33. package/dist/tools/claude-user-message.js +7 -0
  34. package/dist/tools/claude-workflow-tools.d.ts +1 -0
  35. package/dist/tools/claude-workflow-tools.js +7 -0
  36. package/dist/tools/claude-worktree-tools.d.ts +1 -0
  37. package/dist/tools/claude-worktree-tools.js +7 -0
  38. package/dist/tools/filtered-tool-registry.d.ts +1 -0
  39. package/dist/tools/filtered-tool-registry.js +5 -0
  40. package/dist/tools/local-tools.d.ts +17 -0
  41. package/dist/tools/local-tools.js +86 -1
  42. package/dist/tools/web.d.ts +1 -0
  43. package/dist/tools/web.js +27 -0
  44. package/package.json +1 -1
@@ -16,6 +16,7 @@ import { selectClaudeSchemaAdapter, } from '../compatibility/claude/schema.js';
16
16
  import { findUnresolvedClaudeToolCalls, getClaudeContentBlocks, } from '../compatibility/claude/tool-links.js';
17
17
  import { createClaudeAgentSettingEntry, createClaudeHookAttachmentEntries, createClaudeLastPromptEntry, createClaudeRuleAttachmentEntry, translateProviderEvents, } from '../compatibility/claude/translation.js';
18
18
  import { AgentRunCancelledError, AgentRuntime, } from '../core/runtime.js';
19
+ import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
19
20
  import { BackgroundTaskRuntime, } from './background-task-runtime.js';
20
21
  import { usageCostUsd } from '../core/usage.js';
21
22
  import { ContextBudget, ContextRecoveryPlanner, estimateModelRequestTokens, isPromptTooLongError, } from '../core/context-budget.js';
@@ -61,6 +62,7 @@ function mainAgentToolNames(tools, agent) {
61
62
  }
62
63
  const emptyToolRegistry = {
63
64
  definitions: () => [],
65
+ schedulingPolicy: () => ({ concurrency: 'exclusive' }),
64
66
  prepare: async (call) => call,
65
67
  execute: async () => ({ content: '', isError: false }),
66
68
  };
@@ -307,10 +309,10 @@ Return ONLY an updated Markdown document that becomes the session's durable memo
307
309
  * branch can provide them. Larger suffixes stay conservative. */
308
310
  const MEMORY_COMPACT_MIN_SUFFIX_MESSAGES = 5;
309
311
  const MEMORY_COMPACT_MIN_SUFFIX_TOKENS = 10_000;
310
- /** Above this estimated compactor projection the selective path falls back to
311
- * the existing full-compaction behavior instead of building an oversized
312
- * projection. */
312
+ /** Above this estimated post-compact envelope the selective path falls back to
313
+ * full compaction instead of retaining an oversized active projection. */
313
314
  const MEMORY_COMPACT_MAX_PROJECTION_TOKENS = 40_000;
315
+ const MEMORY_COMPACT_MAX_SUMMARY_TOKENS = 8_192;
314
316
  function isTextBearingClaudeEntry(entry) {
315
317
  if (typeof entry.message !== 'object' ||
316
318
  entry.message === null ||
@@ -360,12 +362,38 @@ function claudeAssistantHasToolUse(entry, ids) {
360
362
  typeof block.id === 'string' &&
361
363
  ids.has(block.id));
362
364
  }
365
+ function claudeAssistantResponseId(entry) {
366
+ if (entry?.type !== 'assistant' ||
367
+ typeof entry.message !== 'object' ||
368
+ entry.message === null ||
369
+ Array.isArray(entry.message)) {
370
+ return null;
371
+ }
372
+ const id = entry.message.id;
373
+ return typeof id === 'string' && id.length > 0 ? id : null;
374
+ }
375
+ /** Claude transcripts may normalize one provider response into consecutive
376
+ * assistant records with the same message.id. Keep the whole response on one
377
+ * side of the compact boundary so thinking and tool siblings stay together. */
378
+ function completeClaudeAssistantResponseBoundary(activeEntries, startIndex, lowerBound) {
379
+ const responseId = claudeAssistantResponseId(activeEntries[startIndex]);
380
+ if (responseId === null)
381
+ return startIndex;
382
+ let boundary = startIndex;
383
+ for (let index = startIndex - 1; index >= lowerBound; index -= 1) {
384
+ const candidate = activeEntries[index];
385
+ if (claudeAssistantResponseId(candidate) !== responseId)
386
+ break;
387
+ boundary = index;
388
+ }
389
+ return boundary;
390
+ }
363
391
  /** Pulls the preserved-suffix boundary backward so a tool_use assistant entry
364
392
  * and its tool_result user entry stay siblings: the suffix never opens with an
365
393
  * orphaned tool_result and the compacted input never ends with a dangling
366
394
  * tool_use. Same-response thinking/tool blocks live in one assistant entry,
367
395
  * so entry-level cutting never splits them. */
368
- function completeClaudeToolPairBoundary(activeEntries, startIndex) {
396
+ function completeClaudeToolPairBoundary(activeEntries, startIndex, lowerBound) {
369
397
  let boundary = startIndex;
370
398
  while (boundary < activeEntries.length) {
371
399
  const entry = activeEntries[boundary];
@@ -375,7 +403,7 @@ function completeClaudeToolPairBoundary(activeEntries, startIndex) {
375
403
  if (ids.size === 0)
376
404
  break;
377
405
  let extended = false;
378
- for (let index = boundary - 1; index >= 0; index -= 1) {
406
+ for (let index = boundary - 1; index >= lowerBound; index -= 1) {
379
407
  const candidate = activeEntries[index];
380
408
  if (candidate && claudeAssistantHasToolUse(candidate, ids)) {
381
409
  boundary = index;
@@ -390,11 +418,18 @@ function completeClaudeToolPairBoundary(activeEntries, startIndex) {
390
418
  }
391
419
  /** Walks back from the active tail to the inclusive start of the recent suffix
392
420
  * retained verbatim, then completes sibling adjacency at the boundary. */
393
- function memoryPreservedSuffixStart(activeEntries) {
421
+ export function memoryPreservedSuffixStart(activeEntries) {
422
+ let latestCompactBoundary = 0;
423
+ for (let index = activeEntries.length - 1; index >= 0; index -= 1) {
424
+ if (activeEntries[index]?.isCompactSummary === true) {
425
+ latestCompactBoundary = index;
426
+ break;
427
+ }
428
+ }
394
429
  let start = activeEntries.length;
395
430
  let textBearing = 0;
396
431
  let estimatedTokens = 0;
397
- for (let index = activeEntries.length - 1; index >= 0; index -= 1) {
432
+ for (let index = activeEntries.length - 1; index >= latestCompactBoundary; index -= 1) {
398
433
  const entry = activeEntries[index];
399
434
  if (!entry)
400
435
  continue;
@@ -404,11 +439,13 @@ function memoryPreservedSuffixStart(activeEntries) {
404
439
  textBearing += 1;
405
440
  estimatedTokens += estimateModelRequestTokens(projectClaudeModelMessages([entry]));
406
441
  if (textBearing >= MEMORY_COMPACT_MIN_SUFFIX_MESSAGES &&
407
- (estimatedTokens >= MEMORY_COMPACT_MIN_SUFFIX_TOKENS || index === 0)) {
442
+ (estimatedTokens >= MEMORY_COMPACT_MIN_SUFFIX_TOKENS ||
443
+ index === latestCompactBoundary)) {
408
444
  break;
409
445
  }
410
446
  }
411
- return completeClaudeToolPairBoundary(activeEntries, start);
447
+ const toolSafeStart = completeClaudeToolPairBoundary(activeEntries, start, latestCompactBoundary);
448
+ return completeClaudeAssistantResponseBoundary(activeEntries, toolSafeStart, latestCompactBoundary);
412
449
  }
413
450
  function validPromptSuggestion(value) {
414
451
  const suggestion = value.trim();
@@ -558,6 +595,7 @@ export class ClaudeSessionService {
558
595
  activeCostSessionId;
559
596
  closeCostSavePromise;
560
597
  sessionMemoryControllers = new Map();
598
+ resolvedSessionMemoryProvider;
561
599
  hookLifecycle;
562
600
  runtimeCwd;
563
601
  constructor(options) {
@@ -708,6 +746,7 @@ export class ClaudeSessionService {
708
746
  await Promise.all([...this.backgroundNotificationWrites.values()]);
709
747
  this.hostedSubagents.clear();
710
748
  this.backgroundTasks.clear();
749
+ await Promise.all([...this.sessionMemoryControllers.values()].map((controller) => controller.close()));
711
750
  this.sessionMemoryControllers.clear();
712
751
  await this.workflowManager?.close();
713
752
  this.closeCostSavePromise ??= this.persistActiveSessionCost();
@@ -906,6 +945,7 @@ export class ClaudeSessionService {
906
945
  (rightIndex < 0 ? preferredOrder.length : rightIndex));
907
946
  });
908
947
  },
948
+ schedulingPolicy: (call) => resolveToolSchedulingPolicy(capabilityRegistry, call),
909
949
  prepare: (call, context) => capabilityRegistry.prepare(call, context),
910
950
  execute: (call, context) => capabilityRegistry.execute(call, context),
911
951
  };
@@ -2279,6 +2319,10 @@ export class ClaudeSessionService {
2279
2319
  const fileHistoryTools = fileHistory && interactiveMessageTools
2280
2320
  ? {
2281
2321
  definitions: () => interactiveMessageTools.definitions(),
2322
+ schedulingPolicy: (call) => ({
2323
+ ...resolveToolSchedulingPolicy(interactiveMessageTools, call),
2324
+ startAfterAssistant: true,
2325
+ }),
2282
2326
  prepare: (call, context) => interactiveMessageTools.prepare(call, context),
2283
2327
  execute: async (call, context) => {
2284
2328
  const path = call.name === 'Write' || call.name === 'Edit'
@@ -2685,7 +2729,7 @@ export class ClaudeSessionService {
2685
2729
  ];
2686
2730
  const irreducible = budget.evaluate(irreducibleMessages, definitions);
2687
2731
  budget.assertFits(irreducible);
2688
- const logicalParentUuid = compactionAnchorUuid;
2732
+ let logicalParentUuid = compactionAnchorUuid;
2689
2733
  if (!logicalParentUuid || historyMessages.length === 0) {
2690
2734
  budget.assertFits(predicted);
2691
2735
  throw new Error('Cannot compact an empty Claude transcript');
@@ -2693,8 +2737,17 @@ export class ClaudeSessionService {
2693
2737
  if (findUnresolvedClaudeToolCalls(snapshot.entries).length > 0) {
2694
2738
  throw new Error('Cannot compact a Claude session with unresolved tool calls');
2695
2739
  }
2696
- if (sessionMemory) {
2697
- await sessionMemory.waitForIdle();
2740
+ const memorySelection = sessionMemory
2741
+ ? await this.selectMemoryPreservedCompact(sessionId, selectClaudeActiveTranscript(snapshot.entries))
2742
+ : null;
2743
+ const compactorMessages = memorySelection
2744
+ ? [
2745
+ memorySelection.memoryMessage,
2746
+ ...projectClaudeModelMessages(memorySelection.compactedEntries),
2747
+ ]
2748
+ : historyMessages;
2749
+ if (memorySelection) {
2750
+ logicalParentUuid = memorySelection.logicalParentUuid;
2698
2751
  }
2699
2752
  this.options.eventSink?.({ type: 'state', state: 'compacting' });
2700
2753
  const compactEnvelope = budget.evaluate([
@@ -2716,7 +2769,7 @@ export class ClaudeSessionService {
2716
2769
  targetTokens = 1;
2717
2770
  }
2718
2771
  const compacted = await (this.options.compactor ?? new ModelCompactor(provider)).compact({
2719
- messages: historyMessages,
2772
+ messages: compactorMessages,
2720
2773
  targetTokens,
2721
2774
  contextWindowTokens: budget.contextWindowTokens,
2722
2775
  ...(signal ? { signal } : {}),
@@ -2748,6 +2801,11 @@ export class ClaudeSessionService {
2748
2801
  cwd: this.activeCwd(),
2749
2802
  claudeVersion: this.options.claudeVersion,
2750
2803
  gitBranch: null,
2804
+ ...(memorySelection
2805
+ ? {
2806
+ preservedUuids: memorySelection.preservedEntries.flatMap((entry) => typeof entry.uuid === 'string' ? [entry.uuid] : []),
2807
+ }
2808
+ : {}),
2751
2809
  createUuid: () => uuids.shift() ?? randomUUID(),
2752
2810
  now: () => timestamp,
2753
2811
  });
@@ -3032,7 +3090,7 @@ export class ClaudeSessionService {
3032
3090
  toolResultDirectory,
3033
3091
  observer,
3034
3092
  ...(this.options.effort ? { effort: this.options.effort } : {}),
3035
- ...(this.options.maxModelTurns
3093
+ ...(this.options.maxModelTurns !== undefined
3036
3094
  ? { maxModelTurns: this.options.maxModelTurns }
3037
3095
  : {}),
3038
3096
  ...(this.options.betas?.length ? { betas: this.options.betas } : {}),
@@ -3270,21 +3328,25 @@ export class ClaudeSessionService {
3270
3328
  });
3271
3329
  }
3272
3330
  if (sessionMemory && finalLeafUuid) {
3273
- const turnInputTokens = result.usage?.inputTokens ?? 0;
3274
- const warn = (error) => this.options.eventSink?.({
3275
- type: 'warning',
3276
- message: `Session memory extraction failed: ${error instanceof Error ? error.message : String(error)}`,
3277
- });
3331
+ const memorySnapshot = projectClaudeModelMessages(snapshot.entries);
3332
+ const providerVisibleMessages = [
3333
+ ...contextMessages,
3334
+ ...injectTurnContext(memorySnapshot),
3335
+ ];
3336
+ const currentContextTokens = budget
3337
+ ? budget.evaluate(providerVisibleMessages, definitions)
3338
+ .occupancyTokens
3339
+ : estimateModelRequestTokens(providerVisibleMessages, definitions);
3278
3340
  try {
3279
- await sessionMemory.observeDelta(turnInputTokens, currentTurnToolCalls, finalLeafUuid, projectClaudeModelMessages(snapshot.entries));
3280
- // Normal turns schedule extraction without awaiting it; failures
3281
- // surface as a warning while the sidecar stays retryable.
3282
- sessionMemory.waitForIdle().catch(warn);
3341
+ await sessionMemory.observeContext(currentContextTokens, currentTurnToolCalls, finalLeafUuid, memorySnapshot);
3283
3342
  }
3284
3343
  catch (error) {
3285
3344
  // A failed observation must not fail the user turn; the sidecar
3286
3345
  // retains a retryable error for the next observation.
3287
- warn(error);
3346
+ this.options.eventSink?.({
3347
+ type: 'warning',
3348
+ message: `Session memory observation failed: ${error instanceof Error ? error.message : String(error)}`,
3349
+ });
3288
3350
  }
3289
3351
  }
3290
3352
  turnCompleted = true;
@@ -3402,7 +3464,7 @@ export class ClaudeSessionService {
3402
3464
  return agentName;
3403
3465
  return null;
3404
3466
  }
3405
- /** Conservative selective-preservation seam for manual compact: when the
3467
+ /** Conservative selective-preservation seam for full compact: when the
3406
3468
  * durable session memory watermark matches an active entry, summarize the
3407
3469
  * last good memory artifact plus the post-watermark branch and retain a
3408
3470
  * recent suffix. Returns null to fall back to the existing full-compaction
@@ -3412,6 +3474,7 @@ export class ClaudeSessionService {
3412
3474
  const controller = this.sessionMemoryController(sessionId);
3413
3475
  if (controller === null)
3414
3476
  return null;
3477
+ await controller.waitForCompact();
3415
3478
  let watermark = null;
3416
3479
  let memorySummary = '';
3417
3480
  try {
@@ -3437,11 +3500,18 @@ export class ClaudeSessionService {
3437
3500
  return null;
3438
3501
  const compactedEntries = activeEntries.slice(watermarkIndex + 1, suffixStart);
3439
3502
  const preservedEntries = activeEntries.slice(suffixStart);
3440
- const selectedMessages = projectClaudeModelMessages(compactedEntries);
3441
- if (selectedMessages.length === 0)
3503
+ const preservedMessages = projectClaudeModelMessages(preservedEntries);
3504
+ if (projectClaudeModelMessages(compactedEntries).length === 0)
3442
3505
  return null;
3443
3506
  const memoryMessage = { role: 'user', content: memorySummary };
3444
- if (estimateModelRequestTokens([memoryMessage, ...selectedMessages]) >
3507
+ if (estimateModelRequestTokens([
3508
+ {
3509
+ role: 'user',
3510
+ content: formatClaudeCompactSummary(''),
3511
+ },
3512
+ ...preservedMessages,
3513
+ ]) +
3514
+ MEMORY_COMPACT_MAX_SUMMARY_TOKENS >
3445
3515
  MEMORY_COMPACT_MAX_PROJECTION_TOKENS) {
3446
3516
  return null;
3447
3517
  }
@@ -4045,7 +4115,9 @@ export class ClaudeSessionService {
4045
4115
  return false;
4046
4116
  if (this.options.sessionPersistence === false)
4047
4117
  return false;
4048
- return true;
4118
+ if (this.options.sessionKind === 'bg')
4119
+ return false;
4120
+ return this.sessionMemoryProviderFactory() !== null;
4049
4121
  }
4050
4122
  sessionMemoryController(sessionId) {
4051
4123
  if (!this.sessionMemoryEnabled() || !isClaudeSessionId(sessionId)) {
@@ -4059,7 +4131,11 @@ export class ClaudeSessionService {
4059
4131
  sessionId,
4060
4132
  sidecarRoot: this.paths(sessionId).praxisRoot,
4061
4133
  }),
4062
- extractor: (input) => this.extractSessionMemory(sessionId, input),
4134
+ extractor: (input) => this.extractSessionMemory(input),
4135
+ onExtractionError: (error) => this.options.eventSink?.({
4136
+ type: 'warning',
4137
+ message: `Session memory extraction failed: ${error instanceof Error ? error.message : String(error)}`,
4138
+ }),
4063
4139
  });
4064
4140
  this.sessionMemoryControllers.set(sessionId, controller);
4065
4141
  }
@@ -4110,10 +4186,16 @@ export class ClaudeSessionService {
4110
4186
  parts.push(`Tool result: ${message.content}`);
4111
4187
  }
4112
4188
  }
4113
- return this.boundSessionMemorySummary(parts.join('\n\n'));
4114
- }
4115
- async extractSessionMemory(sessionId, input) {
4116
- const provider = this.provider();
4189
+ // The successful watermark names the newest message in this snapshot, so
4190
+ // extraction must receive every message in full. Summary injection remains
4191
+ // bounded separately; provider refusal leaves the old watermark retryable.
4192
+ return parts.join('\n\n');
4193
+ }
4194
+ async extractSessionMemory(input) {
4195
+ const provider = this.sessionMemoryProvider();
4196
+ if (provider === null) {
4197
+ throw new SessionMemoryStateError('Session memory requires an isolated provider factory');
4198
+ }
4117
4199
  const messages = [
4118
4200
  { role: 'system', content: SESSION_MEMORY_EXTRACTION_PROMPT },
4119
4201
  ...(input.summary.trim().length === 0
@@ -4133,8 +4215,11 @@ export class ClaudeSessionService {
4133
4215
  },
4134
4216
  ]),
4135
4217
  ];
4136
- const metrics = await completeMeteredModelRequest(provider, { messages }, {
4137
- onMetrics: (recorded) => this.recordAuxiliaryMetrics(sessionId, recorded),
4218
+ // Session memory is operational side work: it never contributes to the
4219
+ // foreground result or the persisted session cost/usage tracker.
4220
+ const metrics = await completeMeteredModelRequest(provider, {
4221
+ messages,
4222
+ signal: input.signal,
4138
4223
  });
4139
4224
  if (metrics.toolCalls.length > 0) {
4140
4225
  throw new SessionMemoryStateError('Session memory extraction must not call tools');
@@ -4145,6 +4230,24 @@ export class ClaudeSessionService {
4145
4230
  }
4146
4231
  return summary;
4147
4232
  }
4233
+ sessionMemoryProvider() {
4234
+ if (this.resolvedSessionMemoryProvider !== undefined) {
4235
+ return this.resolvedSessionMemoryProvider;
4236
+ }
4237
+ const factory = this.sessionMemoryProviderFactory();
4238
+ this.resolvedSessionMemoryProvider = factory?.() ?? null;
4239
+ return this.resolvedSessionMemoryProvider;
4240
+ }
4241
+ sessionMemoryProviderFactory() {
4242
+ if (this.options.sessionMemoryProviderFactory) {
4243
+ return this.options.sessionMemoryProviderFactory;
4244
+ }
4245
+ const selectProvider = this.options.providerForMainModel ?? this.options.providerForModel;
4246
+ const foregroundModel = this.options.provider?.model;
4247
+ return foregroundModel && selectProvider
4248
+ ? () => selectProvider(foregroundModel)
4249
+ : null;
4250
+ }
4148
4251
  toolCapabilities() {
4149
4252
  const taskNames = this.options.taskToolNames ?? [];
4150
4253
  const input = {
@@ -45,6 +45,7 @@ export declare class StructuredOutputRegistry implements ToolRegistry {
45
45
  value: unknown;
46
46
  } | undefined);
47
47
  definitions(): readonly ModelToolDefinition[];
48
+ schedulingPolicy(call: ModelToolCall): import("../core/runtime.js").ToolSchedulingPolicy;
48
49
  prepare(call: ModelToolCall, context: ToolExecutionContext): Promise<ModelToolCall>;
49
50
  execute(call: ModelToolCall, context: ToolExecutionContext): Promise<ToolExecutionResult>;
50
51
  }
@@ -11,6 +11,7 @@ import { createClaudeHookAttachmentEntries, translateProviderEvents, } from '../
11
11
  import { injectFirstUserMessageContext, } from '../core/context.js';
12
12
  import { ContextBudget } from '../core/context-budget.js';
13
13
  import { AgentRuntime, } from '../core/runtime.js';
14
+ import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
14
15
  import { BUILTIN_STATUSLINE_AGENT_PATH, } from '../extensions/claude-extensions.js';
15
16
  import { ClaudeHookToolCoordinator } from '../hooks/claude-hook-tools.js';
16
17
  import { ClaudeSidechainStore } from '../persistence/claude-sidechain-store.js';
@@ -26,6 +27,7 @@ const DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
26
27
  const SIDECHAIN_DISCOVERY_MAX_DEPTH = 4;
27
28
  const structuredOnlyTools = {
28
29
  definitions: () => [],
30
+ schedulingPolicy: () => ({ concurrency: 'exclusive' }),
29
31
  prepare: async (call) => call,
30
32
  execute: async () => ({ content: '', isError: false }),
31
33
  };
@@ -68,6 +70,12 @@ export class StructuredOutputRegistry {
68
70
  ]),
69
71
  ];
70
72
  }
73
+ schedulingPolicy(call) {
74
+ if (call.name === 'StructuredOutput') {
75
+ return { concurrency: 'exclusive' };
76
+ }
77
+ return resolveToolSchedulingPolicy(this.base, call);
78
+ }
71
79
  prepare(call, context) {
72
80
  if (call.name !== 'StructuredOutput')
73
81
  return this.base.prepare(call, context);
@@ -106,6 +114,12 @@ class RestrictedToolRegistry {
106
114
  .definitions()
107
115
  .filter((definition) => this.allowed.has(definition.name));
108
116
  }
117
+ schedulingPolicy(call) {
118
+ if (!this.allowed.has(call.name)) {
119
+ return { concurrency: 'exclusive' };
120
+ }
121
+ return resolveToolSchedulingPolicy(this.base, call);
122
+ }
109
123
  prepare(call, context) {
110
124
  if (!this.allowed.has(call.name))
111
125
  throw new Error(`Tool ${call.name} is unavailable to this agent`);
@@ -1353,7 +1367,9 @@ export class ClaudeSubagentExecutor {
1353
1367
  const runtime = new AgentRuntime(options.provider, emit, {
1354
1368
  tools: runtimeTools,
1355
1369
  permissions: runtimePermissions,
1356
- maxModelTurns: customAgent?.maxTurns ?? 16,
1370
+ ...(customAgent?.maxTurns === undefined
1371
+ ? {}
1372
+ : { maxModelTurns: customAgent.maxTurns }),
1357
1373
  maxModelOutputBytes: this.options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES,
1358
1374
  maxToolCallsPerTurn: 32,
1359
1375
  maxToolInputBytes: 1024 * 1024,
@@ -1647,6 +1663,12 @@ class ClaudeSubagentToolRegistry {
1647
1663
  ...ordinary.slice(insertionIndex),
1648
1664
  ];
1649
1665
  }
1666
+ schedulingPolicy(call) {
1667
+ if (['Agent', 'SendMessage', 'TaskOutput', 'TaskStop'].includes(call.name)) {
1668
+ return { concurrency: 'exclusive', cancelOnInterrupt: true };
1669
+ }
1670
+ return resolveToolSchedulingPolicy(this.base, call);
1671
+ }
1650
1672
  async prepare(call, context) {
1651
1673
  if (call.name === 'Agent')
1652
1674
  return this.executor.prepare(call, this.depth);
@@ -178,6 +178,10 @@ export interface CliDependencies extends InteractiveServiceFactory {
178
178
  * to the existing default behavior (undefined).
179
179
  */
180
180
  export declare function resolveRuntimeModel(explicitModel: string | undefined, environment: NodeJS.ProcessEnv, settings: PraxisRuntimeSettings | undefined): string | undefined;
181
+ /** Builds the default CLI's isolated Session-memory adapter seam. Each
182
+ * invocation reconstructs the selected main-model provider stack instead of
183
+ * sharing foreground adapter-local cache and retry state. */
184
+ export declare function createSessionMemoryProviderFactory(providerForMainModel: ((model: string) => ModelProvider) | undefined, model: string | undefined): (() => ModelProvider) | undefined;
181
185
  export declare function resolveInteractiveRuntimeSettingsLocation(dataPlane: DataPlane, environment?: NodeJS.ProcessEnv): {
182
186
  configRoot: string;
183
187
  statePath: string;
@@ -749,6 +749,14 @@ export function resolveRuntimeModel(explicitModel, environment, settings) {
749
749
  (envModel !== undefined && envModel.trim() !== '' ? envModel : undefined) ??
750
750
  settingsModel);
751
751
  }
752
+ /** Builds the default CLI's isolated Session-memory adapter seam. Each
753
+ * invocation reconstructs the selected main-model provider stack instead of
754
+ * sharing foreground adapter-local cache and retry state. */
755
+ export function createSessionMemoryProviderFactory(providerForMainModel, model) {
756
+ if (!providerForMainModel || !model)
757
+ return undefined;
758
+ return () => providerForMainModel(model);
759
+ }
752
760
  export function resolveInteractiveRuntimeSettingsLocation(dataPlane, environment = process.env) {
753
761
  const configRoot = resolveDataPlaneRoot({ dataPlane, environment });
754
762
  return {
@@ -854,6 +862,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
854
862
  projectIdentity: await resolveClaudeProjectIdentity({ cwd }),
855
863
  sidecarPath: resolveUnknownCostSidecarPath(dataPlane, configRoot),
856
864
  });
865
+ const sessionMemoryProviderFactory = createSessionMemoryProviderFactory(providerForMainModel, model);
857
866
  const options = {
858
867
  configRoot,
859
868
  dataPlane,
@@ -879,6 +888,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
879
888
  : { maxBudgetUsd: cli.maxBudgetUsd }),
880
889
  pricing: ModelPricingRegistry.fromEnvironment(runtimeEnvironment.PRAXIS_PRICING_JSON),
881
890
  collectMetrics: true,
891
+ ...(sessionMemoryProviderFactory ? { sessionMemoryProviderFactory } : {}),
882
892
  ...(sessionKind === undefined ? {} : { sessionKind }),
883
893
  workspace,
884
894
  ...(cli.worktreeRequested
@@ -322,8 +322,15 @@ export interface ToolExecutionContext {
322
322
  permissionPhase?: 'request' | 'execute';
323
323
  permissionApproved?: boolean;
324
324
  }
325
+ export interface ToolSchedulingPolicy {
326
+ concurrency: 'concurrent' | 'exclusive';
327
+ cancelOnInterrupt?: boolean;
328
+ abortGroupOnError?: boolean;
329
+ startAfterAssistant?: boolean;
330
+ }
325
331
  export interface ToolRegistry {
326
332
  definitions(): readonly ModelToolDefinition[];
333
+ schedulingPolicy?(call: ModelToolCall): ToolSchedulingPolicy;
327
334
  prepare(call: ModelToolCall, context: ToolExecutionContext): Promise<ModelToolCall>;
328
335
  execute(call: ModelToolCall, context: ToolExecutionContext): Promise<ToolExecutionResult>;
329
336
  }
@@ -514,6 +521,9 @@ export declare class AgentRuntime {
514
521
  recoverToolCalls(calls: readonly ModelToolCall[], request: AgentToolRecoveryRequest): Promise<ToolExecutionResult[]>;
515
522
  executeDirectToolCall(call: ModelToolCall, request: AgentToolRecoveryRequest): Promise<ToolExecutionResult>;
516
523
  private completeToolCall;
524
+ private executeScheduledToolCall;
525
+ private presentToolResult;
526
+ private persistToolCall;
517
527
  private executeTool;
518
528
  private requireRecoveryApproval;
519
529
  private cancel;