@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.
Files changed (293) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/dist/budget-guard.d.ts +75 -0
  4. package/dist/budget-guard.d.ts.map +1 -0
  5. package/dist/budget-guard.js +142 -0
  6. package/dist/budget-guard.js.map +1 -0
  7. package/dist/compaction/compaction.d.ts +99 -0
  8. package/dist/compaction/compaction.d.ts.map +1 -0
  9. package/dist/compaction/compaction.js +302 -0
  10. package/dist/compaction/compaction.js.map +1 -0
  11. package/dist/compaction/failsafe.d.ts +57 -0
  12. package/dist/compaction/failsafe.d.ts.map +1 -0
  13. package/dist/compaction/failsafe.js +135 -0
  14. package/dist/compaction/failsafe.js.map +1 -0
  15. package/dist/compaction/index.d.ts +381 -0
  16. package/dist/compaction/index.d.ts.map +1 -0
  17. package/dist/compaction/index.js +979 -0
  18. package/dist/compaction/index.js.map +1 -0
  19. package/dist/compaction/microcompaction.d.ts +219 -0
  20. package/dist/compaction/microcompaction.d.ts.map +1 -0
  21. package/dist/compaction/microcompaction.js +536 -0
  22. package/dist/compaction/microcompaction.js.map +1 -0
  23. package/dist/compaction/observational/buffering.d.ts +225 -0
  24. package/dist/compaction/observational/buffering.d.ts.map +1 -0
  25. package/dist/compaction/observational/buffering.js +354 -0
  26. package/dist/compaction/observational/buffering.js.map +1 -0
  27. package/dist/compaction/observational/constants.d.ts +70 -0
  28. package/dist/compaction/observational/constants.d.ts.map +1 -0
  29. package/dist/compaction/observational/constants.js +507 -0
  30. package/dist/compaction/observational/constants.js.map +1 -0
  31. package/dist/compaction/observational/index.d.ts +219 -0
  32. package/dist/compaction/observational/index.d.ts.map +1 -0
  33. package/dist/compaction/observational/index.js +641 -0
  34. package/dist/compaction/observational/index.js.map +1 -0
  35. package/dist/compaction/observational/observer.d.ts +97 -0
  36. package/dist/compaction/observational/observer.d.ts.map +1 -0
  37. package/dist/compaction/observational/observer.js +424 -0
  38. package/dist/compaction/observational/observer.js.map +1 -0
  39. package/dist/compaction/observational/recall-tool.d.ts +27 -0
  40. package/dist/compaction/observational/recall-tool.d.ts.map +1 -0
  41. package/dist/compaction/observational/recall-tool.js +93 -0
  42. package/dist/compaction/observational/recall-tool.js.map +1 -0
  43. package/dist/compaction/observational/reflector.d.ts +94 -0
  44. package/dist/compaction/observational/reflector.d.ts.map +1 -0
  45. package/dist/compaction/observational/reflector.js +167 -0
  46. package/dist/compaction/observational/reflector.js.map +1 -0
  47. package/dist/compaction/observational/types.d.ts +271 -0
  48. package/dist/compaction/observational/types.d.ts.map +1 -0
  49. package/dist/compaction/observational/types.js +15 -0
  50. package/dist/compaction/observational/types.js.map +1 -0
  51. package/dist/context-manager.d.ts +134 -0
  52. package/dist/context-manager.d.ts.map +1 -0
  53. package/dist/context-manager.js +170 -0
  54. package/dist/context-manager.js.map +1 -0
  55. package/dist/cortex-agent.d.ts +1020 -0
  56. package/dist/cortex-agent.d.ts.map +1 -0
  57. package/dist/cortex-agent.js +3589 -0
  58. package/dist/cortex-agent.js.map +1 -0
  59. package/dist/error-classifier.d.ts +48 -0
  60. package/dist/error-classifier.d.ts.map +1 -0
  61. package/dist/error-classifier.js +152 -0
  62. package/dist/error-classifier.js.map +1 -0
  63. package/dist/event-bridge.d.ts +166 -0
  64. package/dist/event-bridge.d.ts.map +1 -0
  65. package/dist/event-bridge.js +381 -0
  66. package/dist/event-bridge.js.map +1 -0
  67. package/dist/index.d.ts +55 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +57 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/mcp-client.d.ts +119 -0
  72. package/dist/mcp-client.d.ts.map +1 -0
  73. package/dist/mcp-client.js +474 -0
  74. package/dist/mcp-client.js.map +1 -0
  75. package/dist/model-wrapper.d.ts +58 -0
  76. package/dist/model-wrapper.d.ts.map +1 -0
  77. package/dist/model-wrapper.js +86 -0
  78. package/dist/model-wrapper.js.map +1 -0
  79. package/dist/noop-logger.d.ts +4 -0
  80. package/dist/noop-logger.d.ts.map +1 -0
  81. package/dist/noop-logger.js +8 -0
  82. package/dist/noop-logger.js.map +1 -0
  83. package/dist/prompt-diagnostics.d.ts +47 -0
  84. package/dist/prompt-diagnostics.d.ts.map +1 -0
  85. package/dist/prompt-diagnostics.js +230 -0
  86. package/dist/prompt-diagnostics.js.map +1 -0
  87. package/dist/provider-manager.d.ts +224 -0
  88. package/dist/provider-manager.d.ts.map +1 -0
  89. package/dist/provider-manager.js +563 -0
  90. package/dist/provider-manager.js.map +1 -0
  91. package/dist/provider-registry.d.ts +115 -0
  92. package/dist/provider-registry.d.ts.map +1 -0
  93. package/dist/provider-registry.js +305 -0
  94. package/dist/provider-registry.js.map +1 -0
  95. package/dist/schema-converter.d.ts +20 -0
  96. package/dist/schema-converter.d.ts.map +1 -0
  97. package/dist/schema-converter.js +48 -0
  98. package/dist/schema-converter.js.map +1 -0
  99. package/dist/skill-preprocessor.d.ts +46 -0
  100. package/dist/skill-preprocessor.d.ts.map +1 -0
  101. package/dist/skill-preprocessor.js +237 -0
  102. package/dist/skill-preprocessor.js.map +1 -0
  103. package/dist/skill-registry.d.ts +107 -0
  104. package/dist/skill-registry.d.ts.map +1 -0
  105. package/dist/skill-registry.js +330 -0
  106. package/dist/skill-registry.js.map +1 -0
  107. package/dist/skill-tool.d.ts +54 -0
  108. package/dist/skill-tool.d.ts.map +1 -0
  109. package/dist/skill-tool.js +88 -0
  110. package/dist/skill-tool.js.map +1 -0
  111. package/dist/sub-agent-manager.d.ts +90 -0
  112. package/dist/sub-agent-manager.d.ts.map +1 -0
  113. package/dist/sub-agent-manager.js +192 -0
  114. package/dist/sub-agent-manager.js.map +1 -0
  115. package/dist/token-estimator.d.ts +23 -0
  116. package/dist/token-estimator.d.ts.map +1 -0
  117. package/dist/token-estimator.js +27 -0
  118. package/dist/token-estimator.js.map +1 -0
  119. package/dist/tool-contract.d.ts +68 -0
  120. package/dist/tool-contract.d.ts.map +1 -0
  121. package/dist/tool-contract.js +35 -0
  122. package/dist/tool-contract.js.map +1 -0
  123. package/dist/tool-result-persistence.d.ts +89 -0
  124. package/dist/tool-result-persistence.d.ts.map +1 -0
  125. package/dist/tool-result-persistence.js +152 -0
  126. package/dist/tool-result-persistence.js.map +1 -0
  127. package/dist/tools/bash/index.d.ts +71 -0
  128. package/dist/tools/bash/index.d.ts.map +1 -0
  129. package/dist/tools/bash/index.js +485 -0
  130. package/dist/tools/bash/index.js.map +1 -0
  131. package/dist/tools/bash/interactive.d.ts +47 -0
  132. package/dist/tools/bash/interactive.d.ts.map +1 -0
  133. package/dist/tools/bash/interactive.js +262 -0
  134. package/dist/tools/bash/interactive.js.map +1 -0
  135. package/dist/tools/bash/safety.d.ts +149 -0
  136. package/dist/tools/bash/safety.d.ts.map +1 -0
  137. package/dist/tools/bash/safety.js +1116 -0
  138. package/dist/tools/bash/safety.js.map +1 -0
  139. package/dist/tools/edit.d.ts +57 -0
  140. package/dist/tools/edit.d.ts.map +1 -0
  141. package/dist/tools/edit.js +310 -0
  142. package/dist/tools/edit.js.map +1 -0
  143. package/dist/tools/glob.d.ts +34 -0
  144. package/dist/tools/glob.d.ts.map +1 -0
  145. package/dist/tools/glob.js +268 -0
  146. package/dist/tools/glob.js.map +1 -0
  147. package/dist/tools/grep.d.ts +53 -0
  148. package/dist/tools/grep.d.ts.map +1 -0
  149. package/dist/tools/grep.js +673 -0
  150. package/dist/tools/grep.js.map +1 -0
  151. package/dist/tools/index.d.ts +62 -0
  152. package/dist/tools/index.d.ts.map +1 -0
  153. package/dist/tools/index.js +52 -0
  154. package/dist/tools/index.js.map +1 -0
  155. package/dist/tools/read.d.ts +43 -0
  156. package/dist/tools/read.d.ts.map +1 -0
  157. package/dist/tools/read.js +459 -0
  158. package/dist/tools/read.js.map +1 -0
  159. package/dist/tools/runtime.d.ts +62 -0
  160. package/dist/tools/runtime.d.ts.map +1 -0
  161. package/dist/tools/runtime.js +116 -0
  162. package/dist/tools/runtime.js.map +1 -0
  163. package/dist/tools/shared/cwd-tracker.d.ts +32 -0
  164. package/dist/tools/shared/cwd-tracker.d.ts.map +1 -0
  165. package/dist/tools/shared/cwd-tracker.js +44 -0
  166. package/dist/tools/shared/cwd-tracker.js.map +1 -0
  167. package/dist/tools/shared/edit-history.d.ts +55 -0
  168. package/dist/tools/shared/edit-history.d.ts.map +1 -0
  169. package/dist/tools/shared/edit-history.js +72 -0
  170. package/dist/tools/shared/edit-history.js.map +1 -0
  171. package/dist/tools/shared/edit-matcher.d.ts +83 -0
  172. package/dist/tools/shared/edit-matcher.d.ts.map +1 -0
  173. package/dist/tools/shared/edit-matcher.js +359 -0
  174. package/dist/tools/shared/edit-matcher.js.map +1 -0
  175. package/dist/tools/shared/file-mutation-lock.d.ts +22 -0
  176. package/dist/tools/shared/file-mutation-lock.d.ts.map +1 -0
  177. package/dist/tools/shared/file-mutation-lock.js +35 -0
  178. package/dist/tools/shared/file-mutation-lock.js.map +1 -0
  179. package/dist/tools/shared/gitignore.d.ts +17 -0
  180. package/dist/tools/shared/gitignore.d.ts.map +1 -0
  181. package/dist/tools/shared/gitignore.js +59 -0
  182. package/dist/tools/shared/gitignore.js.map +1 -0
  183. package/dist/tools/shared/pdf-extractor.d.ts +96 -0
  184. package/dist/tools/shared/pdf-extractor.d.ts.map +1 -0
  185. package/dist/tools/shared/pdf-extractor.js +196 -0
  186. package/dist/tools/shared/pdf-extractor.js.map +1 -0
  187. package/dist/tools/shared/read-registry.d.ts +66 -0
  188. package/dist/tools/shared/read-registry.d.ts.map +1 -0
  189. package/dist/tools/shared/read-registry.js +65 -0
  190. package/dist/tools/shared/read-registry.js.map +1 -0
  191. package/dist/tools/shared/safe-env.d.ts +18 -0
  192. package/dist/tools/shared/safe-env.d.ts.map +1 -0
  193. package/dist/tools/shared/safe-env.js +70 -0
  194. package/dist/tools/shared/safe-env.js.map +1 -0
  195. package/dist/tools/sub-agent.d.ts +91 -0
  196. package/dist/tools/sub-agent.d.ts.map +1 -0
  197. package/dist/tools/sub-agent.js +89 -0
  198. package/dist/tools/sub-agent.js.map +1 -0
  199. package/dist/tools/task-output.d.ts +38 -0
  200. package/dist/tools/task-output.d.ts.map +1 -0
  201. package/dist/tools/task-output.js +186 -0
  202. package/dist/tools/task-output.js.map +1 -0
  203. package/dist/tools/tool-search/index.d.ts +40 -0
  204. package/dist/tools/tool-search/index.d.ts.map +1 -0
  205. package/dist/tools/tool-search/index.js +110 -0
  206. package/dist/tools/tool-search/index.js.map +1 -0
  207. package/dist/tools/tool-search/registry.d.ts +82 -0
  208. package/dist/tools/tool-search/registry.d.ts.map +1 -0
  209. package/dist/tools/tool-search/registry.js +238 -0
  210. package/dist/tools/tool-search/registry.js.map +1 -0
  211. package/dist/tools/undo-edit.d.ts +51 -0
  212. package/dist/tools/undo-edit.d.ts.map +1 -0
  213. package/dist/tools/undo-edit.js +231 -0
  214. package/dist/tools/undo-edit.js.map +1 -0
  215. package/dist/tools/web-fetch/cache.d.ts +49 -0
  216. package/dist/tools/web-fetch/cache.d.ts.map +1 -0
  217. package/dist/tools/web-fetch/cache.js +89 -0
  218. package/dist/tools/web-fetch/cache.js.map +1 -0
  219. package/dist/tools/web-fetch/index.d.ts +53 -0
  220. package/dist/tools/web-fetch/index.d.ts.map +1 -0
  221. package/dist/tools/web-fetch/index.js +513 -0
  222. package/dist/tools/web-fetch/index.js.map +1 -0
  223. package/dist/tools/write.d.ts +59 -0
  224. package/dist/tools/write.d.ts.map +1 -0
  225. package/dist/tools/write.js +316 -0
  226. package/dist/tools/write.js.map +1 -0
  227. package/dist/types.d.ts +881 -0
  228. package/dist/types.d.ts.map +1 -0
  229. package/dist/types.js +16 -0
  230. package/dist/types.js.map +1 -0
  231. package/dist/working-tags.d.ts +44 -0
  232. package/dist/working-tags.d.ts.map +1 -0
  233. package/dist/working-tags.js +103 -0
  234. package/dist/working-tags.js.map +1 -0
  235. package/package.json +87 -0
  236. package/src/budget-guard.ts +170 -0
  237. package/src/compaction/compaction.ts +386 -0
  238. package/src/compaction/failsafe.ts +185 -0
  239. package/src/compaction/index.ts +1199 -0
  240. package/src/compaction/microcompaction.ts +709 -0
  241. package/src/compaction/observational/buffering.ts +430 -0
  242. package/src/compaction/observational/constants.ts +532 -0
  243. package/src/compaction/observational/index.ts +837 -0
  244. package/src/compaction/observational/observer.ts +510 -0
  245. package/src/compaction/observational/recall-tool.ts +130 -0
  246. package/src/compaction/observational/reflector.ts +221 -0
  247. package/src/compaction/observational/types.ts +343 -0
  248. package/src/context-manager.ts +237 -0
  249. package/src/cortex-agent.ts +4297 -0
  250. package/src/error-classifier.ts +199 -0
  251. package/src/event-bridge.ts +508 -0
  252. package/src/index.ts +292 -0
  253. package/src/mcp-client.ts +582 -0
  254. package/src/model-wrapper.ts +128 -0
  255. package/src/noop-logger.ts +9 -0
  256. package/src/prompt-diagnostics.ts +296 -0
  257. package/src/provider-manager.ts +823 -0
  258. package/src/provider-registry.ts +386 -0
  259. package/src/schema-converter.ts +51 -0
  260. package/src/skill-preprocessor.ts +314 -0
  261. package/src/skill-registry.ts +378 -0
  262. package/src/skill-tool.ts +130 -0
  263. package/src/sub-agent-manager.ts +236 -0
  264. package/src/token-estimator.ts +26 -0
  265. package/src/tool-contract.ts +113 -0
  266. package/src/tool-result-persistence.ts +197 -0
  267. package/src/tools/bash/index.ts +633 -0
  268. package/src/tools/bash/interactive.ts +302 -0
  269. package/src/tools/bash/safety.ts +1297 -0
  270. package/src/tools/edit.ts +422 -0
  271. package/src/tools/glob.ts +330 -0
  272. package/src/tools/grep.ts +819 -0
  273. package/src/tools/index.ts +110 -0
  274. package/src/tools/read.ts +580 -0
  275. package/src/tools/runtime.ts +173 -0
  276. package/src/tools/shared/cwd-tracker.ts +50 -0
  277. package/src/tools/shared/edit-history.ts +96 -0
  278. package/src/tools/shared/edit-matcher.ts +457 -0
  279. package/src/tools/shared/file-mutation-lock.ts +40 -0
  280. package/src/tools/shared/gitignore.ts +61 -0
  281. package/src/tools/shared/pdf-extractor.ts +290 -0
  282. package/src/tools/shared/read-registry.ts +93 -0
  283. package/src/tools/shared/safe-env.ts +82 -0
  284. package/src/tools/sub-agent.ts +171 -0
  285. package/src/tools/task-output.ts +236 -0
  286. package/src/tools/tool-search/index.ts +167 -0
  287. package/src/tools/tool-search/registry.ts +278 -0
  288. package/src/tools/undo-edit.ts +314 -0
  289. package/src/tools/web-fetch/cache.ts +112 -0
  290. package/src/tools/web-fetch/index.ts +604 -0
  291. package/src/tools/write.ts +385 -0
  292. package/src/types.ts +1057 -0
  293. 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
+ }