praxis-agent 0.49.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -212,7 +212,13 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
212
212
  imports, memory, skills, commands, agents, hooks, settings, MCP servers,
213
213
  plugins, and append-only `praxis.transcript` JSONL sessions under `~/.praxis`,
214
214
  with bounded MCP connection, discovery, and tool operations plus safe
215
- disconnect recovery that never replays an already-dispatched call.
215
+ disconnect recovery that never replays an already-dispatched call. Default
216
+ tool selection defers `mcp__*` schemas behind a turn-scoped `ToolSearch`;
217
+ each query activates at most eight deterministic matches for the next model
218
+ request, and published MCP tool descriptions are capped at 2,048 Unicode
219
+ code points. Explicit concrete `--tools` selections load selected tools
220
+ directly, while `--disallowedTools ToolSearch` restores the complete tool
221
+ list.
216
222
  - **Provider-neutral models** — native Provider Registry/Vault routing, API
217
223
  adapters, an experimental Codex OAuth adapter, explicit capability checks,
218
224
  separate per-attempt connect, byte-idle, and absolute-total timeouts, typed
@@ -304,7 +310,7 @@ normal/low-capability full-frame p95 budgets of `<16.7/<33 ms`.
304
310
  `npm run test:coverage` measures all production code under `src/**` with V8 and
305
311
  enforces global floors of 79% statements, 70% branches, 85% functions, and 81% lines,
306
312
  and rejects any production runtime module with zero covered statements (while allowing
307
- type-only modules). `npm run test:fixtures` executes the 69-behavior native contract; 61 behaviors
313
+ type-only modules). `npm run test:fixtures` executes the 70-behavior native contract; 62 behaviors
308
314
  are qualified and 8 are explicitly excluded. `npm run verify:fixture-contracts`
309
315
  performs the structural check and is part of `npm run check`.
310
316
  `npm run test:core-completion` is retained as a compatibility alias for
@@ -39,6 +39,7 @@ export interface ClaudeSessionServiceOptions {
39
39
  claudeVersion: string;
40
40
  provider?: ModelProvider;
41
41
  tools?: ToolRegistry;
42
+ deferMcpTools?: boolean;
42
43
  permissions?: PermissionResolver;
43
44
  permissionResolverForMode?: (mode: AgentPermissionMode) => PermissionResolver;
44
45
  permissionMode?: ClaudePermissionMode;
@@ -49,6 +49,7 @@ import { ClaudeWorktreeToolRegistry } from '../tools/claude-worktree-tools.js';
49
49
  import { completeMeteredModelRequest } from './metered-model-completion.js';
50
50
  import { SessionMemoryController, SessionMemoryStateError, SessionMemoryStore, } from './session-memory.js';
51
51
  import { FilteredToolRegistry } from '../tools/filtered-tool-registry.js';
52
+ import { DeferredToolCatalog } from '../tools/deferred-tool-catalog.js';
52
53
  import { ClaudeCapabilityToolRegistry, resolveClaudeToolCapabilities, } from '../tools/claude-capabilities.js';
53
54
  import { generateToolUseSummary } from './tool-use-summary.js';
54
55
  import { ClaudeUserMessageToolRegistry, } from '../tools/claude-user-message.js';
@@ -1108,6 +1109,9 @@ export class ClaudeSessionService {
1108
1109
  ? { providerForModel: this.options.providerForModel }
1109
1110
  : {}),
1110
1111
  baseTools: wrappedBase,
1112
+ ...(this.options.deferMcpTools === undefined
1113
+ ? {}
1114
+ : { deferMcpTools: this.options.deferMcpTools }),
1111
1115
  permissions: this.options.permissions,
1112
1116
  ...(this.options.permissionResolverForMode
1113
1117
  ? {
@@ -2846,6 +2850,9 @@ export class ClaudeSessionService {
2846
2850
  ? { providerForModel: this.options.providerForModel }
2847
2851
  : {}),
2848
2852
  baseTools,
2853
+ ...(this.options.deferMcpTools === undefined
2854
+ ? {}
2855
+ : { deferMcpTools: this.options.deferMcpTools }),
2849
2856
  permissions: turnPermissions,
2850
2857
  ...(this.options.permissionResolverForMode
2851
2858
  ? {
@@ -3024,9 +3031,16 @@ export class ClaudeSessionService {
3024
3031
  const structuredTools = this.options.structuredOutputSchema && structuredCapture
3025
3032
  ? new StructuredOutputRegistry(agentScopedTools ?? this.options.tools ?? emptyToolRegistry, this.options.structuredOutputSchema, structuredCapture)
3026
3033
  : agentScopedTools;
3027
- const hookTools = this.options.hooks && structuredTools && turnPermissions
3034
+ const activeTurnTools = structuredTools
3035
+ ? new DeferredToolCatalog(structuredTools).startTurn({
3036
+ enabled: this.options.deferMcpTools !== false &&
3037
+ !(agent && agent.tools !== undefined),
3038
+ restoredToolNames: unresolvedToolCalls.map((call) => call.name),
3039
+ })
3040
+ : undefined;
3041
+ const hookTools = this.options.hooks && activeTurnTools && turnPermissions
3028
3042
  ? new ClaudeHookToolCoordinator({
3029
- tools: structuredTools,
3043
+ tools: activeTurnTools,
3030
3044
  permissions: turnPermissions,
3031
3045
  hooks: this.options.hooks,
3032
3046
  session: hookSession,
@@ -3063,7 +3077,7 @@ export class ClaudeSessionService {
3063
3077
  ...(hookTools
3064
3078
  ? { tools: hookTools, permissions: hookTools }
3065
3079
  : {
3066
- ...(structuredTools ? { tools: structuredTools } : {}),
3080
+ ...(activeTurnTools ? { tools: activeTurnTools } : {}),
3067
3081
  ...(turnPermissions ? { permissions: turnPermissions } : {}),
3068
3082
  }),
3069
3083
  });
@@ -3446,8 +3460,8 @@ export class ClaudeSessionService {
3446
3460
  let compactionDurationMs;
3447
3461
  let compactionDurationWithoutRetriesMs;
3448
3462
  let compactionModelUsage;
3449
- const definitions = provider.capabilities.tools
3450
- ? (structuredTools?.definitions() ?? [])
3463
+ const currentDefinitions = () => provider.capabilities.tools
3464
+ ? (activeTurnTools?.definitions() ?? [])
3451
3465
  : [];
3452
3466
  const budget = this.contextBudget(provider);
3453
3467
  const contextEngine = new ContextEngine({
@@ -3526,7 +3540,7 @@ export class ClaudeSessionService {
3526
3540
  ...pendingMessages,
3527
3541
  ]),
3528
3542
  ],
3529
- tools: definitions,
3543
+ tools: currentDefinitions(),
3530
3544
  };
3531
3545
  },
3532
3546
  irreducible: () => ({
@@ -3540,12 +3554,13 @@ export class ClaudeSessionService {
3540
3554
  })),
3541
3555
  ]),
3542
3556
  ],
3543
- tools: definitions,
3557
+ tools: currentDefinitions(),
3544
3558
  }),
3545
3559
  propose: async () => {
3546
3560
  const activeNativeLease = nativeLease;
3547
3561
  if (!budget)
3548
3562
  throw new Error('Context budget is unavailable');
3563
+ const definitions = currentDefinitions();
3549
3564
  const historyMessages = activeTurnMessages();
3550
3565
  if (historyMessages.length === 0)
3551
3566
  throw new Error('Cannot compact an empty native transcript');
@@ -3754,7 +3769,10 @@ export class ClaudeSessionService {
3754
3769
  ]),
3755
3770
  ];
3756
3771
  return {
3757
- envelope: { messages: proposedMessages, tools: definitions },
3772
+ envelope: {
3773
+ messages: proposedMessages,
3774
+ tools: definitions,
3775
+ },
3758
3776
  commit: async () => {
3759
3777
  if (signal?.aborted)
3760
3778
  throw new AgentRunCancelledError();
@@ -4062,6 +4080,7 @@ export class ClaudeSessionService {
4062
4080
  };
4063
4081
  }
4064
4082
  if (shellCommand === undefined && budget) {
4083
+ const definitions = currentDefinitions();
4065
4084
  await contextEngine.prepare(contextTransitionPort([], currentTurnUserMessages ?? []), signal);
4066
4085
  budget.assertFits(budget.evaluate([
4067
4086
  ...contextMessages,
@@ -4387,6 +4406,7 @@ export class ClaudeSessionService {
4387
4406
  ...contextMessages,
4388
4407
  ...injectTurnContext(memorySnapshot),
4389
4408
  ];
4409
+ const definitions = currentDefinitions();
4390
4410
  const currentContextTokens = contextEngine.report({
4391
4411
  messages: providerVisibleMessages,
4392
4412
  tools: definitions,
@@ -59,6 +59,7 @@ export interface ClaudeSubagentExecutorOptions {
59
59
  claudeVersion: string;
60
60
  provider: ModelProvider;
61
61
  baseTools: ToolRegistry;
62
+ deferMcpTools?: boolean;
62
63
  permissions: PermissionResolver;
63
64
  permissionResolverForMode?: (mode: AgentPermissionMode) => PermissionResolver;
64
65
  parentPermissionMode?: () => AgentPermissionMode;
@@ -10,6 +10,7 @@ import { ContextBudget } from '../core/context-budget.js';
10
10
  import { AgentRuntime, } from '../core/runtime.js';
11
11
  import { composePermissionResolvers } from '../core/permission-policy.js';
12
12
  import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
13
+ import { DeferredToolCatalog } from '../tools/deferred-tool-catalog.js';
13
14
  import { BUILTIN_STATUSLINE_AGENT_PATH, } from '../extensions/claude-extensions.js';
14
15
  import { ClaudeHookToolCoordinator } from '../hooks/claude-hook-tools.js';
15
16
  import { InMemorySidechainStore } from '../persistence/in-memory-sidechain-store.js';
@@ -2059,9 +2060,14 @@ export class ClaudeSubagentExecutor {
2059
2060
  ? new RestrictedToolRegistry(structuredOnlyTools, ['Read', 'Edit'])
2060
2061
  : structuredOnlyTools, options.outputSchema, options.structuredOutput)
2061
2062
  : scopedTools;
2063
+ const activeAgentTools = new DeferredToolCatalog(agentTools).startTurn({
2064
+ enabled: this.options.deferMcpTools !== false &&
2065
+ customAgent?.tools === undefined,
2066
+ restoredToolNames: nativeRecoveryCalls.map((call) => call.name),
2067
+ });
2062
2068
  const runtimeTools = scopedHooks
2063
2069
  ? new ClaudeHookToolCoordinator({
2064
- tools: agentTools,
2070
+ tools: activeAgentTools,
2065
2071
  permissions,
2066
2072
  hooks: scopedHooks,
2067
2073
  session: hookSession,
@@ -2072,7 +2078,7 @@ export class ClaudeSubagentExecutor {
2072
2078
  }
2073
2079
  : {}),
2074
2080
  })
2075
- : agentTools;
2081
+ : activeAgentTools;
2076
2082
  const runtimePermissions = scopedHooks
2077
2083
  ? runtimeTools
2078
2084
  : permissions;
@@ -2241,9 +2247,7 @@ export class ClaudeSubagentExecutor {
2241
2247
  : { reserveTokens: this.options.contextReserveTokens }),
2242
2248
  })
2243
2249
  : null;
2244
- const definitions = options.provider.capabilities.tools
2245
- ? runtimeTools.definitions()
2246
- : [];
2250
+ const currentDefinitions = () => options.provider.capabilities.tools ? runtimeTools.definitions() : [];
2247
2251
  let observedMessages;
2248
2252
  let stableSystemMessageCount = 0;
2249
2253
  const assembleMessages = async () => {
@@ -2272,6 +2276,7 @@ export class ClaudeSubagentExecutor {
2272
2276
  ];
2273
2277
  observedMessages = messages;
2274
2278
  if (contextBudget) {
2279
+ const definitions = currentDefinitions();
2275
2280
  contextBudget.assertFits(contextBudget.evaluate(messages, definitions));
2276
2281
  }
2277
2282
  return messages;
@@ -2393,7 +2398,7 @@ export class ClaudeSubagentExecutor {
2393
2398
  ...(options.signal ? { signal: options.signal } : {}),
2394
2399
  };
2395
2400
  const result = await runtime.run(runtimeRequest);
2396
- contextBudget?.observeUsage(result.usage, observedMessages ?? [], definitions);
2401
+ contextBudget?.observeUsage(result.usage, observedMessages ?? [], currentDefinitions());
2397
2402
  this.options.eventSink?.({
2398
2403
  type: 'task-progress',
2399
2404
  taskId: options.agentId,
@@ -24,6 +24,11 @@ import { executeProviderAuthCommand } from './cli/provider-auth-command.js';
24
24
  import { type PluginEvalDependencies } from './plugins/claude-plugin-eval.js';
25
25
  import { type ProjectEvalDependencies } from './evals/project-eval.js';
26
26
  import { type SelfUpdateResult } from './maintenance/self-update.js';
27
+ export declare function shouldDeferMcpTools(input: {
28
+ simpleMode: boolean;
29
+ tools?: readonly string[] | undefined;
30
+ disallowedTools: readonly string[];
31
+ }): boolean;
27
32
  export { parseContextEnvironment, parseProviderEnvironment };
28
33
  export interface CliIO {
29
34
  stdout(message: string | Uint8Array): void;
@@ -77,6 +77,16 @@ import { formatDoctorReport, runDoctor } from './maintenance/doctor.js';
77
77
  import { runSelfUpdate, } from './maintenance/self-update.js';
78
78
  import { executeClaudeProjectPurge, planClaudeProjectPurge, } from './application/claude-project-purge.js';
79
79
  const VERSION = createRequire(import.meta.url)('../package.json').version;
80
+ export function shouldDeferMcpTools(input) {
81
+ if (input.tools?.includes('ToolSearch')) {
82
+ throw new Error('Unknown tool in --tools: ToolSearch');
83
+ }
84
+ return (!input.simpleMode &&
85
+ !input.disallowedTools.includes('ToolSearch') &&
86
+ (input.tools === undefined ||
87
+ (input.tools.length > 0 &&
88
+ input.tools.every((tool) => tool === 'default'))));
89
+ }
80
90
  function fileResourceBaseUrl(environment, providerEnvironment) {
81
91
  return (environment.PRAXIS_FILES_BASE_URL ??
82
92
  providerEnvironment?.baseUrl ??
@@ -981,6 +991,11 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
981
991
  ? {}
982
992
  : { permissionMode: interactivePermissionMode }),
983
993
  }, cwd);
994
+ const deferMcpTools = shouldDeferMcpTools({
995
+ simpleMode,
996
+ tools: cli.tools,
997
+ disallowedTools: cli.disallowedTools,
998
+ });
984
999
  if (experimentalNativeTranscriptWrites) {
985
1000
  const incompatible = [
986
1001
  ['sessionPersistence', cli.sessionPersistence === false],
@@ -1208,6 +1223,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1208
1223
  pricing: ModelPricingRegistry.fromEnvironment(runtimeEnvironment.PRAXIS_PRICING_JSON),
1209
1224
  }),
1210
1225
  collectMetrics: true,
1226
+ deferMcpTools,
1211
1227
  ...(!experimentalNativeTranscriptWrites && sessionMemoryProviderFactory
1212
1228
  ? { sessionMemoryProviderFactory }
1213
1229
  : {}),
@@ -337,9 +337,6 @@ export class AgentRuntime {
337
337
  let linesRemoved = 0;
338
338
  let activeAttemptHasPresentation = false;
339
339
  let activeAttemptDiscarded = false;
340
- const definitions = this.provider.capabilities.tools
341
- ? (this.options.tools?.definitions() ?? [])
342
- : [];
343
340
  const maxModelTurns = request.maxModelTurns ?? this.options.maxModelTurns;
344
341
  if (maxModelTurns !== undefined &&
345
342
  (!Number.isSafeInteger(maxModelTurns) || maxModelTurns <= 0)) {
@@ -414,6 +411,9 @@ export class AgentRuntime {
414
411
  stableSystemMessageCount: request.stableSystemMessageCount,
415
412
  }),
416
413
  };
414
+ const definitions = this.provider.capabilities.tools
415
+ ? (this.options.tools?.definitions() ?? [])
416
+ : [];
417
417
  if (definitions.length > 0)
418
418
  providerRequest.tools = definitions;
419
419
  if (request.signal)
@@ -20,9 +20,11 @@ export declare class ClaudeHookToolCoordinator implements ToolRegistry, Permissi
20
20
  private readonly prepared;
21
21
  constructor(options: ClaudeHookToolCoordinatorOptions);
22
22
  definitions(): readonly import("../core/runtime.js").ModelToolDefinition[];
23
- schedulingPolicy(): {
23
+ schedulingPolicy(call: ModelToolCall): {
24
24
  concurrency: "exclusive";
25
25
  startAfterAssistant: boolean;
26
+ abortGroupOnError?: boolean;
27
+ cancelOnInterrupt?: boolean;
26
28
  };
27
29
  prepare(call: ModelToolCall, context: ToolExecutionContext): Promise<ModelToolCall>;
28
30
  resolve(call: ModelToolCall, context?: PermissionResolutionContext): Promise<PermissionDecision>;
@@ -8,8 +8,15 @@ export class ClaudeHookToolCoordinator {
8
8
  definitions() {
9
9
  return this.options.tools.definitions();
10
10
  }
11
- schedulingPolicy() {
11
+ schedulingPolicy(call) {
12
+ const wrapped = this.options.tools.schedulingPolicy?.(call);
12
13
  return {
14
+ ...(wrapped?.cancelOnInterrupt === undefined
15
+ ? {}
16
+ : { cancelOnInterrupt: wrapped.cancelOnInterrupt }),
17
+ ...(wrapped?.abortGroupOnError === undefined
18
+ ? {}
19
+ : { abortGroupOnError: wrapped.abortGroupOnError }),
13
20
  concurrency: 'exclusive',
14
21
  startAfterAssistant: true,
15
22
  };
@@ -21,6 +21,10 @@ function sanitizeMcpUnicode(value) {
21
21
  .replace(/[\p{Cf}\p{Co}\p{Cn}]/gu, '')
22
22
  .replace(/[\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF\uE000-\uF8FF]/gu, '');
23
23
  }
24
+ const MAX_MCP_TOOL_DESCRIPTION_CODE_POINTS = 2_048;
25
+ function capMcpToolDescription(value) {
26
+ return [...value].slice(0, MAX_MCP_TOOL_DESCRIPTION_CODE_POINTS).join('');
27
+ }
24
28
  const MAX_RESOURCE_BYTES = 25 * 1024 * 1024;
25
29
  const NO_MCP_RESOURCES = 'No resources found. MCP servers may still provide tools even if they have no resources.';
26
30
  const MCP_RESOURCE_TOOL_DEFINITIONS = [
@@ -1250,7 +1254,7 @@ export class ClaudeMcpToolRegistry {
1250
1254
  sensitiveValues,
1251
1255
  definition: {
1252
1256
  name,
1253
- description: redactSensitiveText(tool.description ?? `MCP tool ${tool.name} from ${serverName}`, sensitiveValues),
1257
+ description: capMcpToolDescription(redactSensitiveText(tool.description ?? `MCP tool ${tool.name} from ${serverName}`, sensitiveValues)),
1254
1258
  inputSchema: redactSensitiveValue(tool.inputSchema, sensitiveValues),
1255
1259
  },
1256
1260
  };
@@ -12,6 +12,7 @@ import { validateSedSafety } from './sed-safety.js';
12
12
  import { parseShellRule, shellRuleMatches } from './shell-rule-matching.js';
13
13
  import { effectiveAdditionalDirectories, effectivePermissionMode, filePermissionSuggestions, permissionRuleValueFromString, permissionRuleStringIsValid, permissionRuleValueToString, shellInputIsReadOnly, shellPermissionSuggestions, shellSubcommands, skillPermissionSuggestions, } from './permission-updates.js';
14
14
  const DEFAULT_BEHAVIOR = {
15
+ ToolSearch: 'allow',
15
16
  Agent: 'allow',
16
17
  SendMessage: 'allow',
17
18
  SendUserMessage: 'allow',
@@ -0,0 +1,11 @@
1
+ import type { ToolRegistry } from '../core/runtime.js';
2
+ export declare class DeferredToolCatalog {
3
+ private readonly base;
4
+ private readonly definitions;
5
+ constructor(base: ToolRegistry);
6
+ startTurn(options?: {
7
+ enabled?: boolean;
8
+ restoredToolNames?: readonly string[];
9
+ }): ToolRegistry;
10
+ }
11
+ //# sourceMappingURL=deferred-tool-catalog.d.ts.map
@@ -0,0 +1,145 @@
1
+ import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
2
+ const TOOL_SEARCH = 'ToolSearch';
3
+ const MAX_MATCHES = 8;
4
+ const isDeferred = (definition) => definition.name.startsWith('mcp__');
5
+ const truncateCodePoints = (value, max) => Array.from(value).slice(0, max).join('');
6
+ const toolSearchDefinition = {
7
+ name: TOOL_SEARCH,
8
+ description: 'Search available MCP tools. A successful search activates matching tools, whose schemas appear on the next model request.',
9
+ inputSchema: {
10
+ type: 'object',
11
+ properties: {
12
+ query: { type: 'string', minLength: 1, maxLength: 256 },
13
+ },
14
+ required: ['query'],
15
+ additionalProperties: false,
16
+ },
17
+ };
18
+ function invalidSearch() {
19
+ return { content: 'Invalid ToolSearch input', isError: true };
20
+ }
21
+ function summary(definition) {
22
+ const text = definition.description || '(no description)';
23
+ return Array.from(text).length > 240
24
+ ? `${truncateCodePoints(text, 237)}...`
25
+ : text;
26
+ }
27
+ class ActiveDeferredToolRegistry {
28
+ base;
29
+ definitionsByOrder;
30
+ active = new Set();
31
+ deferred;
32
+ constructor(base, definitionsByOrder, restoredToolNames) {
33
+ this.base = base;
34
+ this.definitionsByOrder = definitionsByOrder;
35
+ this.deferred = definitionsByOrder.filter(isDeferred);
36
+ for (const name of restoredToolNames) {
37
+ if (this.deferred.some((definition) => definition.name === name))
38
+ this.active.add(name);
39
+ }
40
+ }
41
+ definitions() {
42
+ const visible = this.definitionsByOrder.filter((definition) => !isDeferred(definition) || this.active.has(definition.name));
43
+ return [...visible, toolSearchDefinition];
44
+ }
45
+ schedulingPolicy(call) {
46
+ if (call.name === TOOL_SEARCH)
47
+ return { concurrency: 'exclusive', cancelOnInterrupt: true };
48
+ this.assertAvailable(call.name);
49
+ return resolveToolSchedulingPolicy(this.base, call);
50
+ }
51
+ async prepare(call, context) {
52
+ if (call.name === TOOL_SEARCH)
53
+ return call;
54
+ this.assertAvailable(call.name);
55
+ return this.base.prepare(call, context);
56
+ }
57
+ async execute(call, context) {
58
+ if (call.name === TOOL_SEARCH)
59
+ return this.search(call);
60
+ this.assertAvailable(call.name);
61
+ return this.base.execute(call, context);
62
+ }
63
+ assertAvailable(name) {
64
+ if (this.deferred.some((definition) => definition.name === name) &&
65
+ !this.active.has(name)) {
66
+ throw new Error(`Tool ${name} is inactive; call ToolSearch first`);
67
+ }
68
+ }
69
+ search(call) {
70
+ const input = call.input;
71
+ if (!input ||
72
+ typeof input !== 'object' ||
73
+ Array.isArray(input) ||
74
+ Object.keys(input).length !== 1 ||
75
+ typeof input.query !== 'string' ||
76
+ input.query.trim().length < 1 ||
77
+ Array.from(input.query).length > 256)
78
+ return invalidSearch();
79
+ const query = input.query.trim().toLowerCase();
80
+ const tokens = query.split(/\s+/);
81
+ const candidates = this.deferred
82
+ .map((definition, index) => ({ definition, index }))
83
+ .filter(({ definition }) => {
84
+ const haystack = `${definition.name} ${definition.description}`.toLowerCase();
85
+ return tokens.every((token) => haystack.includes(token));
86
+ })
87
+ .sort((left, right) => {
88
+ const score = (item) => {
89
+ const name = item.definition.name.toLowerCase();
90
+ if (name === query)
91
+ return 0;
92
+ if (name.startsWith(query))
93
+ return 1;
94
+ if (name.includes(query))
95
+ return 2;
96
+ if (item.definition.description.toLowerCase().includes(query))
97
+ return 3;
98
+ return 4;
99
+ };
100
+ return score(left) - score(right) || left.index - right.index;
101
+ })
102
+ .slice(0, MAX_MATCHES);
103
+ const newlyActivated = candidates.filter(({ definition }) => !this.active.has(definition.name));
104
+ const alreadyActive = candidates.filter(({ definition }) => this.active.has(definition.name));
105
+ for (const { definition } of candidates)
106
+ this.active.add(definition.name);
107
+ const names = candidates.map(({ definition }) => definition.name);
108
+ let content = names.length
109
+ ? 'Matching MCP tools were found; their schemas will appear on the next model request.'
110
+ : 'No matching MCP tools found; no schemas were activated.';
111
+ if (names.length) {
112
+ if (newlyActivated.length) {
113
+ content += `\nActivated: ${newlyActivated.map(({ definition }) => definition.name).join(', ')}`;
114
+ }
115
+ if (alreadyActive.length) {
116
+ content += `\nAlready active: ${alreadyActive.map(({ definition }) => definition.name).join(', ')}`;
117
+ }
118
+ content += `\n${candidates.map(({ definition }) => `${definition.name}: ${summary(definition)}`).join('\n')}`;
119
+ }
120
+ return { content: truncateCodePoints(content, 4096), isError: false };
121
+ }
122
+ }
123
+ export class DeferredToolCatalog {
124
+ base;
125
+ definitions;
126
+ constructor(base) {
127
+ this.base = base;
128
+ this.definitions = [...base.definitions()];
129
+ }
130
+ startTurn(options = {}) {
131
+ const deferred = this.definitions.some(isDeferred);
132
+ if (options.enabled === false || !deferred)
133
+ return this.base;
134
+ const seen = new Set();
135
+ for (const definition of this.definitions) {
136
+ if (seen.has(definition.name))
137
+ throw new Error(`Duplicate tool definition: ${definition.name}`);
138
+ seen.add(definition.name);
139
+ }
140
+ if (seen.has(TOOL_SEARCH))
141
+ throw new Error(`Tool definition collision: ${TOOL_SEARCH}`);
142
+ return new ActiveDeferredToolRegistry(this.base, this.definitions, options.restoredToolNames ?? []);
143
+ }
144
+ }
145
+ //# sourceMappingURL=deferred-tool-catalog.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.49.0",
3
+ "version": "0.51.0",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",