@animus-labs/cortex 0.2.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/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/budget-guard.d.ts +75 -0
- package/dist/budget-guard.d.ts.map +1 -0
- package/dist/budget-guard.js +142 -0
- package/dist/budget-guard.js.map +1 -0
- package/dist/compaction/compaction.d.ts +99 -0
- package/dist/compaction/compaction.d.ts.map +1 -0
- package/dist/compaction/compaction.js +302 -0
- package/dist/compaction/compaction.js.map +1 -0
- package/dist/compaction/failsafe.d.ts +57 -0
- package/dist/compaction/failsafe.d.ts.map +1 -0
- package/dist/compaction/failsafe.js +135 -0
- package/dist/compaction/failsafe.js.map +1 -0
- package/dist/compaction/index.d.ts +381 -0
- package/dist/compaction/index.d.ts.map +1 -0
- package/dist/compaction/index.js +979 -0
- package/dist/compaction/index.js.map +1 -0
- package/dist/compaction/microcompaction.d.ts +219 -0
- package/dist/compaction/microcompaction.d.ts.map +1 -0
- package/dist/compaction/microcompaction.js +536 -0
- package/dist/compaction/microcompaction.js.map +1 -0
- package/dist/compaction/observational/buffering.d.ts +225 -0
- package/dist/compaction/observational/buffering.d.ts.map +1 -0
- package/dist/compaction/observational/buffering.js +354 -0
- package/dist/compaction/observational/buffering.js.map +1 -0
- package/dist/compaction/observational/constants.d.ts +70 -0
- package/dist/compaction/observational/constants.d.ts.map +1 -0
- package/dist/compaction/observational/constants.js +507 -0
- package/dist/compaction/observational/constants.js.map +1 -0
- package/dist/compaction/observational/index.d.ts +219 -0
- package/dist/compaction/observational/index.d.ts.map +1 -0
- package/dist/compaction/observational/index.js +641 -0
- package/dist/compaction/observational/index.js.map +1 -0
- package/dist/compaction/observational/observer.d.ts +97 -0
- package/dist/compaction/observational/observer.d.ts.map +1 -0
- package/dist/compaction/observational/observer.js +424 -0
- package/dist/compaction/observational/observer.js.map +1 -0
- package/dist/compaction/observational/recall-tool.d.ts +27 -0
- package/dist/compaction/observational/recall-tool.d.ts.map +1 -0
- package/dist/compaction/observational/recall-tool.js +93 -0
- package/dist/compaction/observational/recall-tool.js.map +1 -0
- package/dist/compaction/observational/reflector.d.ts +94 -0
- package/dist/compaction/observational/reflector.d.ts.map +1 -0
- package/dist/compaction/observational/reflector.js +167 -0
- package/dist/compaction/observational/reflector.js.map +1 -0
- package/dist/compaction/observational/types.d.ts +271 -0
- package/dist/compaction/observational/types.d.ts.map +1 -0
- package/dist/compaction/observational/types.js +15 -0
- package/dist/compaction/observational/types.js.map +1 -0
- package/dist/context-manager.d.ts +134 -0
- package/dist/context-manager.d.ts.map +1 -0
- package/dist/context-manager.js +170 -0
- package/dist/context-manager.js.map +1 -0
- package/dist/cortex-agent.d.ts +1020 -0
- package/dist/cortex-agent.d.ts.map +1 -0
- package/dist/cortex-agent.js +3589 -0
- package/dist/cortex-agent.js.map +1 -0
- package/dist/error-classifier.d.ts +48 -0
- package/dist/error-classifier.d.ts.map +1 -0
- package/dist/error-classifier.js +152 -0
- package/dist/error-classifier.js.map +1 -0
- package/dist/event-bridge.d.ts +166 -0
- package/dist/event-bridge.d.ts.map +1 -0
- package/dist/event-bridge.js +381 -0
- package/dist/event-bridge.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-client.d.ts +119 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +474 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/model-wrapper.d.ts +58 -0
- package/dist/model-wrapper.d.ts.map +1 -0
- package/dist/model-wrapper.js +86 -0
- package/dist/model-wrapper.js.map +1 -0
- package/dist/noop-logger.d.ts +4 -0
- package/dist/noop-logger.d.ts.map +1 -0
- package/dist/noop-logger.js +8 -0
- package/dist/noop-logger.js.map +1 -0
- package/dist/prompt-diagnostics.d.ts +47 -0
- package/dist/prompt-diagnostics.d.ts.map +1 -0
- package/dist/prompt-diagnostics.js +230 -0
- package/dist/prompt-diagnostics.js.map +1 -0
- package/dist/provider-manager.d.ts +224 -0
- package/dist/provider-manager.d.ts.map +1 -0
- package/dist/provider-manager.js +563 -0
- package/dist/provider-manager.js.map +1 -0
- package/dist/provider-registry.d.ts +115 -0
- package/dist/provider-registry.d.ts.map +1 -0
- package/dist/provider-registry.js +305 -0
- package/dist/provider-registry.js.map +1 -0
- package/dist/schema-converter.d.ts +20 -0
- package/dist/schema-converter.d.ts.map +1 -0
- package/dist/schema-converter.js +48 -0
- package/dist/schema-converter.js.map +1 -0
- package/dist/skill-preprocessor.d.ts +46 -0
- package/dist/skill-preprocessor.d.ts.map +1 -0
- package/dist/skill-preprocessor.js +237 -0
- package/dist/skill-preprocessor.js.map +1 -0
- package/dist/skill-registry.d.ts +107 -0
- package/dist/skill-registry.d.ts.map +1 -0
- package/dist/skill-registry.js +330 -0
- package/dist/skill-registry.js.map +1 -0
- package/dist/skill-tool.d.ts +54 -0
- package/dist/skill-tool.d.ts.map +1 -0
- package/dist/skill-tool.js +88 -0
- package/dist/skill-tool.js.map +1 -0
- package/dist/sub-agent-manager.d.ts +90 -0
- package/dist/sub-agent-manager.d.ts.map +1 -0
- package/dist/sub-agent-manager.js +192 -0
- package/dist/sub-agent-manager.js.map +1 -0
- package/dist/token-estimator.d.ts +23 -0
- package/dist/token-estimator.d.ts.map +1 -0
- package/dist/token-estimator.js +27 -0
- package/dist/token-estimator.js.map +1 -0
- package/dist/tool-contract.d.ts +68 -0
- package/dist/tool-contract.d.ts.map +1 -0
- package/dist/tool-contract.js +35 -0
- package/dist/tool-contract.js.map +1 -0
- package/dist/tool-result-persistence.d.ts +89 -0
- package/dist/tool-result-persistence.d.ts.map +1 -0
- package/dist/tool-result-persistence.js +152 -0
- package/dist/tool-result-persistence.js.map +1 -0
- package/dist/tools/bash/index.d.ts +71 -0
- package/dist/tools/bash/index.d.ts.map +1 -0
- package/dist/tools/bash/index.js +485 -0
- package/dist/tools/bash/index.js.map +1 -0
- package/dist/tools/bash/interactive.d.ts +47 -0
- package/dist/tools/bash/interactive.d.ts.map +1 -0
- package/dist/tools/bash/interactive.js +262 -0
- package/dist/tools/bash/interactive.js.map +1 -0
- package/dist/tools/bash/safety.d.ts +149 -0
- package/dist/tools/bash/safety.d.ts.map +1 -0
- package/dist/tools/bash/safety.js +1116 -0
- package/dist/tools/bash/safety.js.map +1 -0
- package/dist/tools/edit.d.ts +57 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +310 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/glob.d.ts +34 -0
- package/dist/tools/glob.d.ts.map +1 -0
- package/dist/tools/glob.js +268 -0
- package/dist/tools/glob.js.map +1 -0
- package/dist/tools/grep.d.ts +53 -0
- package/dist/tools/grep.d.ts.map +1 -0
- package/dist/tools/grep.js +673 -0
- package/dist/tools/grep.js.map +1 -0
- package/dist/tools/index.d.ts +62 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +52 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/read.d.ts +43 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +459 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/runtime.d.ts +62 -0
- package/dist/tools/runtime.d.ts.map +1 -0
- package/dist/tools/runtime.js +116 -0
- package/dist/tools/runtime.js.map +1 -0
- package/dist/tools/shared/cwd-tracker.d.ts +32 -0
- package/dist/tools/shared/cwd-tracker.d.ts.map +1 -0
- package/dist/tools/shared/cwd-tracker.js +44 -0
- package/dist/tools/shared/cwd-tracker.js.map +1 -0
- package/dist/tools/shared/edit-history.d.ts +55 -0
- package/dist/tools/shared/edit-history.d.ts.map +1 -0
- package/dist/tools/shared/edit-history.js +72 -0
- package/dist/tools/shared/edit-history.js.map +1 -0
- package/dist/tools/shared/edit-matcher.d.ts +83 -0
- package/dist/tools/shared/edit-matcher.d.ts.map +1 -0
- package/dist/tools/shared/edit-matcher.js +359 -0
- package/dist/tools/shared/edit-matcher.js.map +1 -0
- package/dist/tools/shared/file-mutation-lock.d.ts +22 -0
- package/dist/tools/shared/file-mutation-lock.d.ts.map +1 -0
- package/dist/tools/shared/file-mutation-lock.js +35 -0
- package/dist/tools/shared/file-mutation-lock.js.map +1 -0
- package/dist/tools/shared/gitignore.d.ts +17 -0
- package/dist/tools/shared/gitignore.d.ts.map +1 -0
- package/dist/tools/shared/gitignore.js +59 -0
- package/dist/tools/shared/gitignore.js.map +1 -0
- package/dist/tools/shared/pdf-extractor.d.ts +96 -0
- package/dist/tools/shared/pdf-extractor.d.ts.map +1 -0
- package/dist/tools/shared/pdf-extractor.js +196 -0
- package/dist/tools/shared/pdf-extractor.js.map +1 -0
- package/dist/tools/shared/read-registry.d.ts +66 -0
- package/dist/tools/shared/read-registry.d.ts.map +1 -0
- package/dist/tools/shared/read-registry.js +65 -0
- package/dist/tools/shared/read-registry.js.map +1 -0
- package/dist/tools/shared/safe-env.d.ts +18 -0
- package/dist/tools/shared/safe-env.d.ts.map +1 -0
- package/dist/tools/shared/safe-env.js +70 -0
- package/dist/tools/shared/safe-env.js.map +1 -0
- package/dist/tools/sub-agent.d.ts +91 -0
- package/dist/tools/sub-agent.d.ts.map +1 -0
- package/dist/tools/sub-agent.js +89 -0
- package/dist/tools/sub-agent.js.map +1 -0
- package/dist/tools/task-output.d.ts +38 -0
- package/dist/tools/task-output.d.ts.map +1 -0
- package/dist/tools/task-output.js +186 -0
- package/dist/tools/task-output.js.map +1 -0
- package/dist/tools/tool-search/index.d.ts +40 -0
- package/dist/tools/tool-search/index.d.ts.map +1 -0
- package/dist/tools/tool-search/index.js +110 -0
- package/dist/tools/tool-search/index.js.map +1 -0
- package/dist/tools/tool-search/registry.d.ts +82 -0
- package/dist/tools/tool-search/registry.d.ts.map +1 -0
- package/dist/tools/tool-search/registry.js +238 -0
- package/dist/tools/tool-search/registry.js.map +1 -0
- package/dist/tools/undo-edit.d.ts +51 -0
- package/dist/tools/undo-edit.d.ts.map +1 -0
- package/dist/tools/undo-edit.js +231 -0
- package/dist/tools/undo-edit.js.map +1 -0
- package/dist/tools/web-fetch/cache.d.ts +49 -0
- package/dist/tools/web-fetch/cache.d.ts.map +1 -0
- package/dist/tools/web-fetch/cache.js +89 -0
- package/dist/tools/web-fetch/cache.js.map +1 -0
- package/dist/tools/web-fetch/index.d.ts +53 -0
- package/dist/tools/web-fetch/index.d.ts.map +1 -0
- package/dist/tools/web-fetch/index.js +513 -0
- package/dist/tools/web-fetch/index.js.map +1 -0
- package/dist/tools/write.d.ts +59 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +316 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/types.d.ts +881 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/working-tags.d.ts +44 -0
- package/dist/working-tags.d.ts.map +1 -0
- package/dist/working-tags.js +103 -0
- package/dist/working-tags.js.map +1 -0
- package/package.json +87 -0
- package/src/budget-guard.ts +170 -0
- package/src/compaction/compaction.ts +386 -0
- package/src/compaction/failsafe.ts +185 -0
- package/src/compaction/index.ts +1199 -0
- package/src/compaction/microcompaction.ts +709 -0
- package/src/compaction/observational/buffering.ts +430 -0
- package/src/compaction/observational/constants.ts +532 -0
- package/src/compaction/observational/index.ts +837 -0
- package/src/compaction/observational/observer.ts +510 -0
- package/src/compaction/observational/recall-tool.ts +130 -0
- package/src/compaction/observational/reflector.ts +221 -0
- package/src/compaction/observational/types.ts +343 -0
- package/src/context-manager.ts +237 -0
- package/src/cortex-agent.ts +4297 -0
- package/src/error-classifier.ts +199 -0
- package/src/event-bridge.ts +508 -0
- package/src/index.ts +292 -0
- package/src/mcp-client.ts +582 -0
- package/src/model-wrapper.ts +128 -0
- package/src/noop-logger.ts +9 -0
- package/src/prompt-diagnostics.ts +296 -0
- package/src/provider-manager.ts +823 -0
- package/src/provider-registry.ts +386 -0
- package/src/schema-converter.ts +51 -0
- package/src/skill-preprocessor.ts +314 -0
- package/src/skill-registry.ts +378 -0
- package/src/skill-tool.ts +130 -0
- package/src/sub-agent-manager.ts +236 -0
- package/src/token-estimator.ts +26 -0
- package/src/tool-contract.ts +113 -0
- package/src/tool-result-persistence.ts +197 -0
- package/src/tools/bash/index.ts +633 -0
- package/src/tools/bash/interactive.ts +302 -0
- package/src/tools/bash/safety.ts +1297 -0
- package/src/tools/edit.ts +422 -0
- package/src/tools/glob.ts +330 -0
- package/src/tools/grep.ts +819 -0
- package/src/tools/index.ts +110 -0
- package/src/tools/read.ts +580 -0
- package/src/tools/runtime.ts +173 -0
- package/src/tools/shared/cwd-tracker.ts +50 -0
- package/src/tools/shared/edit-history.ts +96 -0
- package/src/tools/shared/edit-matcher.ts +457 -0
- package/src/tools/shared/file-mutation-lock.ts +40 -0
- package/src/tools/shared/gitignore.ts +61 -0
- package/src/tools/shared/pdf-extractor.ts +290 -0
- package/src/tools/shared/read-registry.ts +93 -0
- package/src/tools/shared/safe-env.ts +82 -0
- package/src/tools/sub-agent.ts +171 -0
- package/src/tools/task-output.ts +236 -0
- package/src/tools/tool-search/index.ts +167 -0
- package/src/tools/tool-search/registry.ts +278 -0
- package/src/tools/undo-edit.ts +314 -0
- package/src/tools/web-fetch/cache.ts +112 -0
- package/src/tools/web-fetch/index.ts +604 -0
- package/src/tools/write.ts +385 -0
- package/src/types.ts +1057 -0
- package/src/working-tags.ts +118 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaskOutput tool: companion tool for polling backgrounded processes.
|
|
3
|
+
*
|
|
4
|
+
* Auto-registered alongside the Bash tool. Provides three actions:
|
|
5
|
+
* - poll: get latest output and status
|
|
6
|
+
* - send: send input to process stdin
|
|
7
|
+
* - kill: send a signal to the process
|
|
8
|
+
*
|
|
9
|
+
* Reference: docs/cortex/tools/bash.md (Background Execution)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Type, type Static } from 'typebox';
|
|
13
|
+
import type { ToolContentDetails } from '../types.js';
|
|
14
|
+
import type { CortexToolRuntime } from './runtime.js';
|
|
15
|
+
import {
|
|
16
|
+
attachRuntimeAwareTool,
|
|
17
|
+
globalBackgroundTaskStore,
|
|
18
|
+
} from './runtime.js';
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Schema
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export const TaskOutputParams = Type.Object({
|
|
25
|
+
task_id: Type.String({ description: 'The task ID returned by a backgrounded Bash command' }),
|
|
26
|
+
action: Type.Union([
|
|
27
|
+
Type.Literal('poll'),
|
|
28
|
+
Type.Literal('send'),
|
|
29
|
+
Type.Literal('kill'),
|
|
30
|
+
], { description: 'Action to perform: poll (get output), send (send input), or kill (terminate)' }),
|
|
31
|
+
input: Type.Optional(
|
|
32
|
+
Type.String({ description: 'Input to send to the process stdin (only for "send" action)' }),
|
|
33
|
+
),
|
|
34
|
+
signal: Type.Optional(
|
|
35
|
+
Type.String({ description: 'Signal to send (only for "kill" action). Default: SIGTERM. Options: SIGINT, SIGTERM, SIGKILL' }),
|
|
36
|
+
),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export type TaskOutputParamsType = Static<typeof TaskOutputParams>;
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Details type
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
export interface TaskOutputDetails {
|
|
46
|
+
taskId: string;
|
|
47
|
+
action: string;
|
|
48
|
+
status: 'running' | 'completed' | 'failed' | 'not_found';
|
|
49
|
+
exitCode: number | null;
|
|
50
|
+
stdout: string;
|
|
51
|
+
stderr: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Tool factory
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
export interface TaskOutputToolConfig {
|
|
59
|
+
runtime?: CortexToolRuntime | undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createTaskOutputTool(config?: TaskOutputToolConfig): {
|
|
63
|
+
name: string;
|
|
64
|
+
description: string;
|
|
65
|
+
parameters: typeof TaskOutputParams;
|
|
66
|
+
execute: (params: TaskOutputParamsType) => Promise<ToolContentDetails<TaskOutputDetails>>;
|
|
67
|
+
} {
|
|
68
|
+
const backgroundTasks = config?.runtime?.backgroundTasks ?? globalBackgroundTaskStore;
|
|
69
|
+
|
|
70
|
+
const tool = {
|
|
71
|
+
name: 'TaskOutput',
|
|
72
|
+
description: 'Poll, send input to, or kill a backgrounded process.',
|
|
73
|
+
parameters: TaskOutputParams,
|
|
74
|
+
|
|
75
|
+
async execute(params: TaskOutputParamsType): Promise<ToolContentDetails<TaskOutputDetails>> {
|
|
76
|
+
const { task_id: taskId, action } = params;
|
|
77
|
+
|
|
78
|
+
const task = backgroundTasks.get(taskId);
|
|
79
|
+
if (!task) {
|
|
80
|
+
return {
|
|
81
|
+
content: [{ type: 'text', text: `Task not found: ${taskId}` }],
|
|
82
|
+
details: {
|
|
83
|
+
taskId,
|
|
84
|
+
action,
|
|
85
|
+
status: 'not_found',
|
|
86
|
+
exitCode: null,
|
|
87
|
+
stdout: '',
|
|
88
|
+
stderr: '',
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
switch (action) {
|
|
94
|
+
case 'poll': {
|
|
95
|
+
const status = task.completed
|
|
96
|
+
? (task.exitCode === 0 ? 'completed' : 'failed')
|
|
97
|
+
: 'running';
|
|
98
|
+
|
|
99
|
+
let text = `Status: ${status}`;
|
|
100
|
+
if (task.completed && task.exitCode !== null) {
|
|
101
|
+
text += ` (exit code: ${task.exitCode})`;
|
|
102
|
+
}
|
|
103
|
+
if (task.stdout) {
|
|
104
|
+
const output = task.stdout.length > 30000
|
|
105
|
+
? task.stdout.slice(-30000)
|
|
106
|
+
: task.stdout;
|
|
107
|
+
text += `\n\nOutput:\n${output}`;
|
|
108
|
+
}
|
|
109
|
+
if (task.stderr) {
|
|
110
|
+
text += `\n\nStderr:\n${task.stderr}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
content: [{ type: 'text', text }],
|
|
115
|
+
details: {
|
|
116
|
+
taskId,
|
|
117
|
+
action,
|
|
118
|
+
status: status as 'running' | 'completed' | 'failed',
|
|
119
|
+
exitCode: task.exitCode,
|
|
120
|
+
stdout: task.stdout,
|
|
121
|
+
stderr: task.stderr,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
case 'send': {
|
|
127
|
+
if (task.completed) {
|
|
128
|
+
return {
|
|
129
|
+
content: [{ type: 'text', text: `Task ${taskId} has already completed. Cannot send input.` }],
|
|
130
|
+
details: {
|
|
131
|
+
taskId,
|
|
132
|
+
action,
|
|
133
|
+
status: task.exitCode === 0 ? 'completed' : 'failed',
|
|
134
|
+
exitCode: task.exitCode,
|
|
135
|
+
stdout: task.stdout,
|
|
136
|
+
stderr: task.stderr,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const input = params.input ?? '';
|
|
142
|
+
try {
|
|
143
|
+
task.process.stdin?.write(input + '\n');
|
|
144
|
+
return {
|
|
145
|
+
content: [{ type: 'text', text: `Sent input to task ${taskId}.` }],
|
|
146
|
+
details: {
|
|
147
|
+
taskId,
|
|
148
|
+
action,
|
|
149
|
+
status: 'running',
|
|
150
|
+
exitCode: null,
|
|
151
|
+
stdout: task.stdout,
|
|
152
|
+
stderr: task.stderr,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
} catch (err) {
|
|
156
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
157
|
+
return {
|
|
158
|
+
content: [{ type: 'text', text: `Failed to send input to task ${taskId}: ${msg}` }],
|
|
159
|
+
details: {
|
|
160
|
+
taskId,
|
|
161
|
+
action,
|
|
162
|
+
status: 'running',
|
|
163
|
+
exitCode: null,
|
|
164
|
+
stdout: task.stdout,
|
|
165
|
+
stderr: task.stderr,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
case 'kill': {
|
|
172
|
+
if (task.completed) {
|
|
173
|
+
return {
|
|
174
|
+
content: [{ type: 'text', text: `Task ${taskId} has already completed.` }],
|
|
175
|
+
details: {
|
|
176
|
+
taskId,
|
|
177
|
+
action,
|
|
178
|
+
status: task.exitCode === 0 ? 'completed' : 'failed',
|
|
179
|
+
exitCode: task.exitCode,
|
|
180
|
+
stdout: task.stdout,
|
|
181
|
+
stderr: task.stderr,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const signal = (params.signal ?? 'SIGTERM') as NodeJS.Signals;
|
|
187
|
+
try {
|
|
188
|
+
task.process.kill(signal);
|
|
189
|
+
return {
|
|
190
|
+
content: [{ type: 'text', text: `Sent ${signal} to task ${taskId}.` }],
|
|
191
|
+
details: {
|
|
192
|
+
taskId,
|
|
193
|
+
action,
|
|
194
|
+
status: 'running',
|
|
195
|
+
exitCode: null,
|
|
196
|
+
stdout: task.stdout,
|
|
197
|
+
stderr: task.stderr,
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
} catch (err) {
|
|
201
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
202
|
+
return {
|
|
203
|
+
content: [{ type: 'text', text: `Failed to kill task ${taskId}: ${msg}` }],
|
|
204
|
+
details: {
|
|
205
|
+
taskId,
|
|
206
|
+
action,
|
|
207
|
+
status: 'running',
|
|
208
|
+
exitCode: null,
|
|
209
|
+
stdout: task.stdout,
|
|
210
|
+
stderr: task.stderr,
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
default:
|
|
217
|
+
return {
|
|
218
|
+
content: [{ type: 'text', text: `Unknown action: ${action}` }],
|
|
219
|
+
details: {
|
|
220
|
+
taskId,
|
|
221
|
+
action,
|
|
222
|
+
status: 'not_found',
|
|
223
|
+
exitCode: null,
|
|
224
|
+
stdout: '',
|
|
225
|
+
stderr: '',
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
return attachRuntimeAwareTool(tool, {
|
|
233
|
+
toolKind: 'TaskOutput',
|
|
234
|
+
cloneForRuntime: (runtime) => createTaskOutputTool({ runtime }),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToolSearch tool: load full tool schemas on demand.
|
|
3
|
+
*
|
|
4
|
+
* Auto-registered when `deferredTools.enabled` is true. The model uses this
|
|
5
|
+
* tool to discover and load tools that appear by name in the
|
|
6
|
+
* `_available_tools` slot but whose schemas are not yet in the agent's
|
|
7
|
+
* tools array.
|
|
8
|
+
*
|
|
9
|
+
* Once a tool is loaded, it persists in the agent's tools array for the
|
|
10
|
+
* rest of the session and can be called normally.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Type, type Static } from 'typebox';
|
|
14
|
+
import type { CortexTool } from '../../tool-contract.js';
|
|
15
|
+
import type { ToolContentDetails } from '../../types.js';
|
|
16
|
+
import type { DeferredToolRegistry, ToolSearchResult } from './registry.js';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Constants
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
export const TOOL_SEARCH_TOOL_NAME = 'ToolSearch';
|
|
23
|
+
|
|
24
|
+
const DEFAULT_MAX_RESULTS = 5;
|
|
25
|
+
const QUERY_DESCRIPTION = [
|
|
26
|
+
'Query for the tool(s) you want to load. Supported formats:',
|
|
27
|
+
'- "select:NameA,NameB" to load specific tools by exact name (preferred when you already know the names from the available-tools list)',
|
|
28
|
+
'- "exact_tool_name" to load a single tool by its exact name',
|
|
29
|
+
'- "prefix" to load all tools starting with the given prefix (e.g., "mcp__obsidian")',
|
|
30
|
+
'- "keyword another keyword" for keyword search across tool names and descriptions',
|
|
31
|
+
'- prefix any keyword with "+" to require it (e.g., "+slack send")',
|
|
32
|
+
].join('\n');
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Schema
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
export const ToolSearchParams = Type.Object({
|
|
39
|
+
query: Type.String({ description: QUERY_DESCRIPTION }),
|
|
40
|
+
max_results: Type.Optional(
|
|
41
|
+
Type.Number({
|
|
42
|
+
description: `Maximum number of tools to load when using keyword/prefix search. Default ${DEFAULT_MAX_RESULTS}. Ignored for select: queries (which always load every requested tool).`,
|
|
43
|
+
default: DEFAULT_MAX_RESULTS,
|
|
44
|
+
}),
|
|
45
|
+
),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export type ToolSearchParamsType = Static<typeof ToolSearchParams>;
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Details
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
export interface ToolSearchDetails {
|
|
55
|
+
query: string;
|
|
56
|
+
loaded: string[];
|
|
57
|
+
alreadyAvailable: string[];
|
|
58
|
+
notFound: string[];
|
|
59
|
+
totalDeferred: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Tool factory
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
export interface ToolSearchToolConfig {
|
|
67
|
+
/** The deferred tool registry shared with CortexAgent. */
|
|
68
|
+
registry: DeferredToolRegistry;
|
|
69
|
+
/**
|
|
70
|
+
* Called after the registry is updated. The agent uses this to refresh
|
|
71
|
+
* its tools array (so the newly discovered tools appear in the next API
|
|
72
|
+
* call) and update the `_available_tools` slot.
|
|
73
|
+
*/
|
|
74
|
+
onAfterDiscovery: () => void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function createToolSearchTool(
|
|
78
|
+
config: ToolSearchToolConfig,
|
|
79
|
+
): CortexTool<ToolSearchParamsType, ToolContentDetails<ToolSearchDetails>> {
|
|
80
|
+
return {
|
|
81
|
+
name: TOOL_SEARCH_TOOL_NAME,
|
|
82
|
+
description: [
|
|
83
|
+
'Load tool schemas on demand. Some tools are not loaded by default to save context tokens; their names appear in the "Available Tools" section but their parameters are not visible to you yet.',
|
|
84
|
+
'',
|
|
85
|
+
'Call this tool to load specific tools before using them. Once loaded, tools become callable on the next turn.',
|
|
86
|
+
'',
|
|
87
|
+
'Use a "select:Name1,Name2" query when you already know the tool names you need (most common). Use keyword queries when you need to discover tools by capability.',
|
|
88
|
+
].join('\n'),
|
|
89
|
+
parameters: ToolSearchParams,
|
|
90
|
+
alwaysLoad: true, // ToolSearch itself must never be deferred
|
|
91
|
+
executionMode: 'sequential',
|
|
92
|
+
async execute(params): Promise<ToolContentDetails<ToolSearchDetails>> {
|
|
93
|
+
const max = params.max_results ?? DEFAULT_MAX_RESULTS;
|
|
94
|
+
const result = config.registry.resolveQuery(params.query, max);
|
|
95
|
+
|
|
96
|
+
const alreadyAvailable = result.resolved
|
|
97
|
+
.map((t) => t.name)
|
|
98
|
+
.filter((n) => !result.newlyDiscovered.includes(n));
|
|
99
|
+
|
|
100
|
+
const totalDeferred = config.registry.getUndiscoveredNames().length;
|
|
101
|
+
|
|
102
|
+
// Only trigger downstream refresh when something actually changed.
|
|
103
|
+
if (result.newlyDiscovered.length > 0) {
|
|
104
|
+
config.onAfterDiscovery();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const text = formatResultText(params.query, result, alreadyAvailable, totalDeferred);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
content: [{ type: 'text', text }],
|
|
111
|
+
details: {
|
|
112
|
+
query: params.query,
|
|
113
|
+
loaded: result.newlyDiscovered,
|
|
114
|
+
alreadyAvailable,
|
|
115
|
+
notFound: result.notFound,
|
|
116
|
+
totalDeferred,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Result formatting
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
function formatResultText(
|
|
128
|
+
query: string,
|
|
129
|
+
result: ToolSearchResult,
|
|
130
|
+
alreadyAvailable: string[],
|
|
131
|
+
totalDeferredAfter: number,
|
|
132
|
+
): string {
|
|
133
|
+
const lines: string[] = [];
|
|
134
|
+
|
|
135
|
+
if (result.newlyDiscovered.length > 0) {
|
|
136
|
+
lines.push('Loaded the following tools (callable on the next turn):');
|
|
137
|
+
for (const name of result.newlyDiscovered) {
|
|
138
|
+
lines.push(`- ${name}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (alreadyAvailable.length > 0) {
|
|
143
|
+
if (lines.length > 0) lines.push('');
|
|
144
|
+
lines.push('Already loaded (no action needed):');
|
|
145
|
+
for (const name of alreadyAvailable) {
|
|
146
|
+
lines.push(`- ${name}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (result.notFound.length > 0) {
|
|
151
|
+
if (lines.length > 0) lines.push('');
|
|
152
|
+
lines.push('Not found in the deferred tool list:');
|
|
153
|
+
for (const name of result.notFound) {
|
|
154
|
+
lines.push(`- ${name}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (result.resolved.length === 0 && result.notFound.length === 0) {
|
|
159
|
+
lines.push(`No tools matched the query: "${query}"`);
|
|
160
|
+
if (totalDeferredAfter > 0) {
|
|
161
|
+
lines.push('');
|
|
162
|
+
lines.push(`There are ${totalDeferredAfter} tools still available. Refer to the "Available Tools" section for the full list.`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return lines.join('\n');
|
|
167
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeferredToolRegistry: tracks the deferred tool pool and which tools the
|
|
3
|
+
* agent has discovered (loaded) during this session.
|
|
4
|
+
*
|
|
5
|
+
* Lives on the CortexAgent instance. `refreshTools()` populates the deferred
|
|
6
|
+
* pool from the union of registered + MCP tools (filtered by deferral
|
|
7
|
+
* criteria), and `ToolSearch` updates the discovered set when the agent
|
|
8
|
+
* resolves a query.
|
|
9
|
+
*
|
|
10
|
+
* The slot content (`formatSlotContent`) is the canonical text that goes
|
|
11
|
+
* into the `_available_tools` slot. It is byte-stable for any given pool +
|
|
12
|
+
* discovered set so the prompt cache hits cleanly.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { CortexTool } from '../../tool-contract.js';
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Types
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Result of resolving a ToolSearch query.
|
|
23
|
+
*/
|
|
24
|
+
export interface ToolSearchResult {
|
|
25
|
+
/** Tools that were resolved by the query (newly loaded + already loaded). */
|
|
26
|
+
resolved: CortexTool[];
|
|
27
|
+
/** Names of tools newly added to the discovered set by this query. */
|
|
28
|
+
newlyDiscovered: string[];
|
|
29
|
+
/** Names of tools the query referenced that were not found in the pool. */
|
|
30
|
+
notFound: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Registry
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
export class DeferredToolRegistry {
|
|
38
|
+
/** Tools currently eligible for deferral, keyed by name. */
|
|
39
|
+
private deferredPool = new Map<string, CortexTool>();
|
|
40
|
+
/** Names the model has loaded via ToolSearch. Persists for the session. */
|
|
41
|
+
private discovered = new Set<string>();
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Replace the deferred pool. Called by `refreshTools()` whenever the
|
|
45
|
+
* underlying tool set changes (MCP server connect/disconnect, etc).
|
|
46
|
+
*
|
|
47
|
+
* Tools that were previously discovered remain in the discovered set even
|
|
48
|
+
* if they're temporarily missing from the pool (e.g., MCP server briefly
|
|
49
|
+
* disconnected). The next `refreshTools()` call will see them again.
|
|
50
|
+
*/
|
|
51
|
+
setDeferredPool(tools: readonly CortexTool[]): void {
|
|
52
|
+
this.deferredPool.clear();
|
|
53
|
+
for (const tool of tools) {
|
|
54
|
+
this.deferredPool.set(tool.name, tool);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Mark tool names as discovered. Subsequent `refreshTools()` calls will
|
|
60
|
+
* include their full schemas in the agent's tools array.
|
|
61
|
+
*
|
|
62
|
+
* Returns the subset that were newly added (i.e., were not already
|
|
63
|
+
* discovered). Used by ToolSearch to report what changed.
|
|
64
|
+
*/
|
|
65
|
+
markDiscovered(names: readonly string[]): string[] {
|
|
66
|
+
const added: string[] = [];
|
|
67
|
+
for (const name of names) {
|
|
68
|
+
if (!this.discovered.has(name)) {
|
|
69
|
+
this.discovered.add(name);
|
|
70
|
+
added.push(name);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return added;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The set of tool names the agent has loaded so far.
|
|
78
|
+
*/
|
|
79
|
+
getDiscovered(): ReadonlySet<string> {
|
|
80
|
+
return this.discovered;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Names currently in the deferred pool that have NOT yet been discovered.
|
|
85
|
+
* These are the names that should appear in the `_available_tools` slot.
|
|
86
|
+
* Returned sorted alphabetically for deterministic, cache-stable output.
|
|
87
|
+
*/
|
|
88
|
+
getUndiscoveredNames(): string[] {
|
|
89
|
+
const names: string[] = [];
|
|
90
|
+
for (const name of this.deferredPool.keys()) {
|
|
91
|
+
if (!this.discovered.has(name)) {
|
|
92
|
+
names.push(name);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return names.sort();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Format the `_available_tools` slot content. Names only (no descriptions),
|
|
100
|
+
* sorted, in a fixed format. Returns an empty string when there are no
|
|
101
|
+
* undiscovered tools (the slot will be set to empty content, which still
|
|
102
|
+
* occupies a slot index but contributes no tokens).
|
|
103
|
+
*/
|
|
104
|
+
formatSlotContent(): string {
|
|
105
|
+
const names = this.getUndiscoveredNames();
|
|
106
|
+
if (names.length === 0) {
|
|
107
|
+
return '';
|
|
108
|
+
}
|
|
109
|
+
const list = names.map((n) => `- ${n}`).join('\n');
|
|
110
|
+
return [
|
|
111
|
+
'# Available Tools',
|
|
112
|
+
'',
|
|
113
|
+
'The following tools are available but their schemas have not been loaded.',
|
|
114
|
+
'Use the ToolSearch tool to load a tool before calling it.',
|
|
115
|
+
'',
|
|
116
|
+
'Calling any of these tools directly (without loading first) will fail.',
|
|
117
|
+
'',
|
|
118
|
+
list,
|
|
119
|
+
].join('\n');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Resolve a ToolSearch query against the deferred pool.
|
|
124
|
+
*
|
|
125
|
+
* Query formats:
|
|
126
|
+
* - "select:NameA,NameB" Direct load by name. Bypasses scoring.
|
|
127
|
+
* - "ExactToolName" Exact name match if it exists in the pool.
|
|
128
|
+
* - "prefix__" Returns all tools starting with the prefix.
|
|
129
|
+
* - "keyword another" Keyword search; scored by name + description.
|
|
130
|
+
*
|
|
131
|
+
* Discovered tools that are referenced by the query are still returned
|
|
132
|
+
* (harmless no-op so the model gets confirmation), but they don't count
|
|
133
|
+
* as "newly discovered".
|
|
134
|
+
*/
|
|
135
|
+
resolveQuery(query: string, maxResults: number): ToolSearchResult {
|
|
136
|
+
const trimmed = query.trim();
|
|
137
|
+
|
|
138
|
+
// Direct select format
|
|
139
|
+
if (trimmed.startsWith('select:')) {
|
|
140
|
+
const requested = trimmed
|
|
141
|
+
.slice('select:'.length)
|
|
142
|
+
.split(',')
|
|
143
|
+
.map((s) => s.trim())
|
|
144
|
+
.filter((s) => s.length > 0);
|
|
145
|
+
return this.resolveByNames(requested);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Empty or whitespace-only query: return nothing useful
|
|
149
|
+
if (trimmed.length === 0) {
|
|
150
|
+
return { resolved: [], newlyDiscovered: [], notFound: [] };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Exact name match (single token, exists in pool)
|
|
154
|
+
if (!/\s/.test(trimmed) && this.deferredPool.has(trimmed)) {
|
|
155
|
+
return this.resolveByNames([trimmed]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Prefix match (single token, no spaces, ends with __ or matches a prefix)
|
|
159
|
+
if (!/\s/.test(trimmed)) {
|
|
160
|
+
const prefixMatches: string[] = [];
|
|
161
|
+
for (const name of this.deferredPool.keys()) {
|
|
162
|
+
if (name.startsWith(trimmed)) {
|
|
163
|
+
prefixMatches.push(name);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (prefixMatches.length > 0) {
|
|
167
|
+
prefixMatches.sort();
|
|
168
|
+
return this.resolveByNames(prefixMatches.slice(0, maxResults));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Keyword search with scoring
|
|
173
|
+
const scored = this.scoreKeywordQuery(trimmed);
|
|
174
|
+
const top = scored
|
|
175
|
+
.sort((a, b) => b.score - a.score)
|
|
176
|
+
.slice(0, maxResults)
|
|
177
|
+
.filter((entry) => entry.score > 0)
|
|
178
|
+
.map((entry) => entry.name);
|
|
179
|
+
|
|
180
|
+
if (top.length === 0) {
|
|
181
|
+
return { resolved: [], newlyDiscovered: [], notFound: [] };
|
|
182
|
+
}
|
|
183
|
+
return this.resolveByNames(top);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// -----------------------------------------------------------------------
|
|
187
|
+
// Internal helpers
|
|
188
|
+
// -----------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
private resolveByNames(names: readonly string[]): ToolSearchResult {
|
|
191
|
+
const resolved: CortexTool[] = [];
|
|
192
|
+
const notFound: string[] = [];
|
|
193
|
+
const toMarkDiscovered: string[] = [];
|
|
194
|
+
|
|
195
|
+
for (const name of names) {
|
|
196
|
+
const tool = this.deferredPool.get(name);
|
|
197
|
+
if (tool) {
|
|
198
|
+
resolved.push(tool);
|
|
199
|
+
toMarkDiscovered.push(name);
|
|
200
|
+
} else {
|
|
201
|
+
notFound.push(name);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const newlyDiscovered = this.markDiscovered(toMarkDiscovered);
|
|
206
|
+
return { resolved, newlyDiscovered, notFound };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private scoreKeywordQuery(query: string): Array<{ name: string; score: number }> {
|
|
210
|
+
// Split into terms; "+term" means required.
|
|
211
|
+
const tokens = query
|
|
212
|
+
.toLowerCase()
|
|
213
|
+
.split(/\s+/)
|
|
214
|
+
.filter((t) => t.length > 0);
|
|
215
|
+
|
|
216
|
+
const required = new Set<string>();
|
|
217
|
+
const optional: string[] = [];
|
|
218
|
+
for (const token of tokens) {
|
|
219
|
+
if (token.startsWith('+') && token.length > 1) {
|
|
220
|
+
required.add(token.slice(1));
|
|
221
|
+
} else {
|
|
222
|
+
optional.push(token);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const allTerms = [...required, ...optional];
|
|
227
|
+
if (allTerms.length === 0) return [];
|
|
228
|
+
|
|
229
|
+
const results: Array<{ name: string; score: number }> = [];
|
|
230
|
+
|
|
231
|
+
for (const [name, tool] of this.deferredPool.entries()) {
|
|
232
|
+
const lowerName = name.toLowerCase();
|
|
233
|
+
const lowerDesc = (tool.description ?? '').toLowerCase();
|
|
234
|
+
// Tool name parts: split by __ (MCP namespacing) and underscore for general matching
|
|
235
|
+
const nameParts = lowerName.split(/__|_/).filter((p) => p.length > 0);
|
|
236
|
+
|
|
237
|
+
// Required terms must all match somewhere
|
|
238
|
+
let satisfiesRequired = true;
|
|
239
|
+
for (const req of required) {
|
|
240
|
+
if (!lowerName.includes(req) && !lowerDesc.includes(req)) {
|
|
241
|
+
satisfiesRequired = false;
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (!satisfiesRequired) continue;
|
|
246
|
+
|
|
247
|
+
const isMcp = tool.isMcp === true;
|
|
248
|
+
let score = 0;
|
|
249
|
+
for (const term of allTerms) {
|
|
250
|
+
// Exact part match in name (highest weight)
|
|
251
|
+
if (nameParts.includes(term)) {
|
|
252
|
+
score += isMcp ? 12 : 10;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
// Partial part match
|
|
256
|
+
if (nameParts.some((p) => p.includes(term))) {
|
|
257
|
+
score += isMcp ? 6 : 5;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
// Full-name substring fallback
|
|
261
|
+
if (lowerName.includes(term)) {
|
|
262
|
+
score += 3;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
// Description substring
|
|
266
|
+
if (lowerDesc.includes(term)) {
|
|
267
|
+
score += 2;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (score > 0) {
|
|
272
|
+
results.push({ name, score });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return results;
|
|
277
|
+
}
|
|
278
|
+
}
|