illuma-agents 1.0.10 → 1.0.11

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 (141) hide show
  1. package/LICENSE +1 -1
  2. package/dist/cjs/agents/AgentContext.cjs +228 -27
  3. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  4. package/dist/cjs/common/enum.cjs +2 -0
  5. package/dist/cjs/common/enum.cjs.map +1 -1
  6. package/dist/cjs/events.cjs +3 -11
  7. package/dist/cjs/events.cjs.map +1 -1
  8. package/dist/cjs/graphs/Graph.cjs +27 -18
  9. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  10. package/dist/cjs/instrumentation.cjs +1 -3
  11. package/dist/cjs/instrumentation.cjs.map +1 -1
  12. package/dist/cjs/llm/anthropic/index.cjs +1 -1
  13. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  14. package/dist/cjs/llm/bedrock/index.cjs +122 -7
  15. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  16. package/dist/cjs/llm/google/index.cjs +1 -1
  17. package/dist/cjs/llm/google/index.cjs.map +1 -1
  18. package/dist/cjs/llm/openai/index.cjs +6 -6
  19. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  20. package/dist/cjs/llm/openrouter/index.cjs +1 -1
  21. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  22. package/dist/cjs/main.cjs +18 -0
  23. package/dist/cjs/main.cjs.map +1 -1
  24. package/dist/cjs/messages/cache.cjs +149 -54
  25. package/dist/cjs/messages/cache.cjs.map +1 -1
  26. package/dist/cjs/messages/tools.cjs +85 -0
  27. package/dist/cjs/messages/tools.cjs.map +1 -0
  28. package/dist/cjs/run.cjs +0 -8
  29. package/dist/cjs/run.cjs.map +1 -1
  30. package/dist/cjs/tools/CodeExecutor.cjs +4 -0
  31. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  32. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +438 -0
  33. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -0
  34. package/dist/cjs/tools/ToolNode.cjs +53 -15
  35. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  36. package/dist/cjs/tools/ToolSearchRegex.cjs +455 -0
  37. package/dist/cjs/tools/ToolSearchRegex.cjs.map +1 -0
  38. package/dist/cjs/tools/search/schema.cjs +7 -9
  39. package/dist/cjs/tools/search/schema.cjs.map +1 -1
  40. package/dist/cjs/utils/run.cjs +5 -1
  41. package/dist/cjs/utils/run.cjs.map +1 -1
  42. package/dist/esm/agents/AgentContext.mjs +228 -27
  43. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  44. package/dist/esm/common/enum.mjs +2 -0
  45. package/dist/esm/common/enum.mjs.map +1 -1
  46. package/dist/esm/events.mjs +4 -12
  47. package/dist/esm/events.mjs.map +1 -1
  48. package/dist/esm/graphs/Graph.mjs +27 -18
  49. package/dist/esm/graphs/Graph.mjs.map +1 -1
  50. package/dist/esm/instrumentation.mjs +1 -3
  51. package/dist/esm/instrumentation.mjs.map +1 -1
  52. package/dist/esm/llm/anthropic/index.mjs +1 -1
  53. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  54. package/dist/esm/llm/bedrock/index.mjs +122 -7
  55. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  56. package/dist/esm/llm/google/index.mjs +1 -1
  57. package/dist/esm/llm/google/index.mjs.map +1 -1
  58. package/dist/esm/llm/openai/index.mjs +6 -6
  59. package/dist/esm/llm/openai/index.mjs.map +1 -1
  60. package/dist/esm/llm/openrouter/index.mjs +1 -1
  61. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  62. package/dist/esm/main.mjs +3 -0
  63. package/dist/esm/main.mjs.map +1 -1
  64. package/dist/esm/messages/cache.mjs +149 -54
  65. package/dist/esm/messages/cache.mjs.map +1 -1
  66. package/dist/esm/messages/tools.mjs +82 -0
  67. package/dist/esm/messages/tools.mjs.map +1 -0
  68. package/dist/esm/run.mjs +0 -8
  69. package/dist/esm/run.mjs.map +1 -1
  70. package/dist/esm/tools/CodeExecutor.mjs +4 -0
  71. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  72. package/dist/esm/tools/ProgrammaticToolCalling.mjs +430 -0
  73. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -0
  74. package/dist/esm/tools/ToolNode.mjs +53 -15
  75. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  76. package/dist/esm/tools/ToolSearchRegex.mjs +448 -0
  77. package/dist/esm/tools/ToolSearchRegex.mjs.map +1 -0
  78. package/dist/esm/tools/search/schema.mjs +7 -9
  79. package/dist/esm/tools/search/schema.mjs.map +1 -1
  80. package/dist/esm/utils/run.mjs +5 -1
  81. package/dist/esm/utils/run.mjs.map +1 -1
  82. package/dist/types/agents/AgentContext.d.ts +65 -5
  83. package/dist/types/common/enum.d.ts +2 -0
  84. package/dist/types/graphs/Graph.d.ts +3 -2
  85. package/dist/types/index.d.ts +2 -0
  86. package/dist/types/llm/anthropic/index.d.ts +1 -1
  87. package/dist/types/llm/bedrock/index.d.ts +31 -4
  88. package/dist/types/llm/google/index.d.ts +1 -1
  89. package/dist/types/llm/openai/index.d.ts +3 -3
  90. package/dist/types/llm/openrouter/index.d.ts +1 -1
  91. package/dist/types/messages/cache.d.ts +23 -8
  92. package/dist/types/messages/index.d.ts +1 -0
  93. package/dist/types/messages/tools.d.ts +17 -0
  94. package/dist/types/test/mockTools.d.ts +28 -0
  95. package/dist/types/tools/ProgrammaticToolCalling.d.ts +91 -0
  96. package/dist/types/tools/ToolNode.d.ts +10 -2
  97. package/dist/types/tools/ToolSearchRegex.d.ts +80 -0
  98. package/dist/types/types/graph.d.ts +7 -1
  99. package/dist/types/types/tools.d.ts +138 -0
  100. package/package.json +7 -2
  101. package/src/agents/AgentContext.ts +267 -27
  102. package/src/agents/__tests__/AgentContext.test.ts +805 -0
  103. package/src/common/enum.ts +2 -0
  104. package/src/events.ts +5 -12
  105. package/src/graphs/Graph.ts +33 -19
  106. package/src/index.ts +2 -0
  107. package/src/instrumentation.ts +1 -4
  108. package/src/llm/anthropic/index.ts +2 -2
  109. package/src/llm/bedrock/__tests__/bedrock-caching.test.ts +473 -0
  110. package/src/llm/bedrock/index.ts +150 -13
  111. package/src/llm/google/index.ts +2 -2
  112. package/src/llm/openai/index.ts +9 -9
  113. package/src/llm/openrouter/index.ts +2 -2
  114. package/src/messages/__tests__/tools.test.ts +473 -0
  115. package/src/messages/cache.ts +163 -61
  116. package/src/messages/index.ts +1 -0
  117. package/src/messages/tools.ts +99 -0
  118. package/src/run.ts +0 -9
  119. package/src/scripts/code_exec_ptc.ts +334 -0
  120. package/src/scripts/image.ts +178 -0
  121. package/src/scripts/programmatic_exec.ts +396 -0
  122. package/src/scripts/programmatic_exec_agent.ts +231 -0
  123. package/src/scripts/test-tools-before-handoff.ts +5 -1
  124. package/src/scripts/tool_search_regex.ts +162 -0
  125. package/src/scripts/tools.ts +4 -1
  126. package/src/specs/thinking-prune.test.ts +52 -118
  127. package/src/test/mockTools.ts +366 -0
  128. package/src/tools/CodeExecutor.ts +4 -0
  129. package/src/tools/ProgrammaticToolCalling.ts +558 -0
  130. package/src/tools/ToolNode.ts +59 -18
  131. package/src/tools/ToolSearchRegex.ts +535 -0
  132. package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +318 -0
  133. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +853 -0
  134. package/src/tools/__tests__/ToolSearchRegex.integration.test.ts +161 -0
  135. package/src/tools/__tests__/ToolSearchRegex.test.ts +232 -0
  136. package/src/tools/search/jina-reranker.test.ts +16 -16
  137. package/src/tools/search/schema.ts +7 -9
  138. package/src/types/graph.ts +7 -1
  139. package/src/types/tools.ts +166 -0
  140. package/src/utils/run.ts +5 -1
  141. package/src/tools/search/direct-url.test.ts +0 -530
@@ -25,6 +25,8 @@ export type ToolNodeOptions = {
25
25
  loadRuntimeTools?: ToolRefGenerator;
26
26
  toolCallStepIds?: Map<string, string>;
27
27
  errorHandler?: (data: ToolErrorData, metadata?: Record<string, unknown>) => Promise<void>;
28
+ /** Tool registry for lazy computation of programmatic tools and tool search */
29
+ toolRegistry?: LCToolRegistry;
28
30
  };
29
31
  export type ToolNodeConstructorParams = ToolRefs & ToolNodeOptions;
30
32
  export type ToolEndEvent = {
@@ -59,3 +61,139 @@ export type ExecuteResult = {
59
61
  stderr: string;
60
62
  files?: FileRefs;
61
63
  };
64
+ /** JSON Schema type definition for tool parameters */
65
+ export type JsonSchemaType = {
66
+ type: 'string' | 'number' | 'integer' | 'float' | 'boolean' | 'array' | 'object';
67
+ enum?: string[];
68
+ items?: JsonSchemaType;
69
+ properties?: Record<string, JsonSchemaType>;
70
+ required?: string[];
71
+ description?: string;
72
+ additionalProperties?: boolean | JsonSchemaType;
73
+ };
74
+ /**
75
+ * Specifies which contexts can invoke a tool (inspired by Anthropic's allowed_callers)
76
+ * - 'direct': Only callable directly by the LLM (default if omitted)
77
+ * - 'code_execution': Only callable from within programmatic code execution
78
+ */
79
+ export type AllowedCaller = 'direct' | 'code_execution';
80
+ /** Tool definition with optional deferred loading and caller restrictions */
81
+ export type LCTool = {
82
+ name: string;
83
+ description?: string;
84
+ parameters?: JsonSchemaType;
85
+ /** When true, tool is not loaded into context initially (for tool search) */
86
+ defer_loading?: boolean;
87
+ /**
88
+ * Which contexts can invoke this tool.
89
+ * Default: ['direct'] (only callable directly by LLM)
90
+ * Options: 'direct', 'code_execution'
91
+ */
92
+ allowed_callers?: AllowedCaller[];
93
+ };
94
+ /** Map of tool names to tool definitions */
95
+ export type LCToolRegistry = Map<string, LCTool>;
96
+ export type ProgrammaticCache = {
97
+ toolMap: ToolMap;
98
+ toolDefs: LCTool[];
99
+ };
100
+ /** Parameters for creating a Tool Search Regex tool */
101
+ export type ToolSearchRegexParams = {
102
+ apiKey?: string;
103
+ toolRegistry?: LCToolRegistry;
104
+ onlyDeferred?: boolean;
105
+ baseUrl?: string;
106
+ [key: string]: unknown;
107
+ };
108
+ /** Simplified tool metadata for search purposes */
109
+ export type ToolMetadata = {
110
+ name: string;
111
+ description: string;
112
+ parameters?: JsonSchemaType;
113
+ };
114
+ /** Individual search result for a matching tool */
115
+ export type ToolSearchResult = {
116
+ tool_name: string;
117
+ match_score: number;
118
+ matched_field: string;
119
+ snippet: string;
120
+ };
121
+ /** Response from the tool search operation */
122
+ export type ToolSearchResponse = {
123
+ tool_references: ToolSearchResult[];
124
+ total_tools_searched: number;
125
+ pattern_used: string;
126
+ };
127
+ /** Artifact returned alongside the formatted search results */
128
+ export type ToolSearchArtifact = {
129
+ tool_references: ToolSearchResult[];
130
+ metadata: {
131
+ total_searched: number;
132
+ pattern: string;
133
+ error?: string;
134
+ };
135
+ };
136
+ /**
137
+ * Tool call requested by the Code API during programmatic execution
138
+ */
139
+ export type PTCToolCall = {
140
+ /** Unique ID like "call_001" */
141
+ id: string;
142
+ /** Tool name */
143
+ name: string;
144
+ /** Input parameters */
145
+ input: Record<string, any>;
146
+ };
147
+ /**
148
+ * Tool result sent back to the Code API
149
+ */
150
+ export type PTCToolResult = {
151
+ /** Matches PTCToolCall.id */
152
+ call_id: string;
153
+ /** Tool execution result (any JSON-serializable value) */
154
+ result: any;
155
+ /** Whether tool execution failed */
156
+ is_error: boolean;
157
+ /** Error details if is_error=true */
158
+ error_message?: string;
159
+ };
160
+ /**
161
+ * Response from the Code API for programmatic execution
162
+ */
163
+ export type ProgrammaticExecutionResponse = {
164
+ status: 'tool_call_required' | 'completed' | 'error' | unknown;
165
+ session_id?: string;
166
+ /** Present when status='tool_call_required' */
167
+ continuation_token?: string;
168
+ tool_calls?: PTCToolCall[];
169
+ /** Present when status='completed' */
170
+ stdout?: string;
171
+ stderr?: string;
172
+ files?: FileRefs;
173
+ /** Present when status='error' */
174
+ error?: string;
175
+ };
176
+ /**
177
+ * Artifact returned by the PTC tool
178
+ */
179
+ export type ProgrammaticExecutionArtifact = {
180
+ session_id?: string;
181
+ files?: FileRefs;
182
+ };
183
+ /**
184
+ * Initialization parameters for the PTC tool
185
+ */
186
+ export type ProgrammaticToolCallingParams = {
187
+ /** Code API key (or use CODE_API_KEY env var) */
188
+ apiKey?: string;
189
+ /** Code API base URL (or use CODE_BASEURL env var) */
190
+ baseUrl?: string;
191
+ /** Safety limit for round-trips (default: 20) */
192
+ maxRoundTrips?: number;
193
+ /** HTTP proxy URL */
194
+ proxy?: string;
195
+ /** Enable debug logging (or set PTC_DEBUG=true env var) */
196
+ debug?: boolean;
197
+ /** Environment variable key for API key */
198
+ [key: string]: unknown;
199
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "illuma-agents",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -34,7 +34,7 @@
34
34
  "scripts": {
35
35
  "prepare": "node husky-setup.js",
36
36
  "prepublishOnly": "npm run build",
37
- "build": "set NODE_ENV=production && rollup -c && tsc -p tsconfig.build.json",
37
+ "build": "cross-env NODE_ENV=production rollup -c && tsc -p tsconfig.build.json",
38
38
  "build:dev": "rollup -c",
39
39
  "start": "node dist/esm/main.js",
40
40
  "clean": "node ./config/clean.js",
@@ -53,6 +53,10 @@
53
53
  "memory": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/memory.ts --provider 'openAI' --name 'Jo' --location 'New York, NY'",
54
54
  "tool": "node --trace-warnings -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/tools.ts --provider 'openrouter' --name 'Jo' --location 'New York, NY'",
55
55
  "search": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/search.ts --provider 'bedrock' --name 'Jo' --location 'New York, NY'",
56
+ "tool_search_regex": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/tool_search_regex.ts",
57
+ "programmatic_exec": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/programmatic_exec.ts",
58
+ "code_exec_ptc": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/code_exec_ptc.ts --provider 'openAI' --name 'Jo' --location 'New York, NY'",
59
+ "programmatic_exec_agent": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/programmatic_exec_agent.ts --provider 'openAI' --name 'Jo' --location 'New York, NY'",
56
60
  "ant_web_search": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/ant_web_search.ts --name 'Jo' --location 'New York, NY'",
57
61
  "ant_web_search_edge_case": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/ant_web_search_edge_case.ts --name 'Jo' --location 'New York, NY'",
58
62
  "ant_web_search_error_edge_case": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/ant_web_search_error_edge_case.ts --name 'Jo' --location 'New York, NY'",
@@ -140,6 +144,7 @@
140
144
  "@types/yargs-parser": "^21.0.3",
141
145
  "@typescript-eslint/eslint-plugin": "^8.24.0",
142
146
  "@typescript-eslint/parser": "^8.24.0",
147
+ "cross-env": "^10.1.0",
143
148
  "eslint": "^9.39.1",
144
149
  "eslint-import-resolver-typescript": "^3.7.0",
145
150
  "eslint-plugin-import": "^2.31.0",
@@ -32,6 +32,7 @@ export class AgentContext {
32
32
  tools,
33
33
  toolMap,
34
34
  toolEnd,
35
+ toolRegistry,
35
36
  instructions,
36
37
  additional_instructions,
37
38
  streamBuffer,
@@ -48,6 +49,7 @@ export class AgentContext {
48
49
  streamBuffer,
49
50
  tools,
50
51
  toolMap,
52
+ toolRegistry,
51
53
  instructions,
52
54
  additionalInstructions: additional_instructions,
53
55
  reasoningKey,
@@ -58,12 +60,17 @@ export class AgentContext {
58
60
  });
59
61
 
60
62
  if (tokenCounter) {
63
+ // Initialize system runnable BEFORE async tool token calculation
64
+ // This ensures system message tokens are in instructionTokens before
65
+ // updateTokenMapWithInstructions is called
66
+ agentContext.initializeSystemRunnable();
67
+
61
68
  const tokenMap = indexTokenCountMap || {};
62
69
  agentContext.indexTokenCountMap = tokenMap;
63
70
  agentContext.tokenCalculationPromise = agentContext
64
71
  .calculateInstructionTokens(tokenCounter)
65
72
  .then(() => {
66
- // Update token map with instruction tokens
73
+ // Update token map with instruction tokens (includes system + tool tokens)
67
74
  agentContext.updateTokenMapWithInstructions(tokenMap);
68
75
  })
69
76
  .catch((err) => {
@@ -102,6 +109,13 @@ export class AgentContext {
102
109
  tools?: t.GraphTools;
103
110
  /** Tool map for this agent */
104
111
  toolMap?: t.ToolMap;
112
+ /**
113
+ * Tool definitions registry (includes deferred and programmatic tool metadata).
114
+ * Used for tool search and programmatic tool calling.
115
+ */
116
+ toolRegistry?: t.LCToolRegistry;
117
+ /** Set of tool names discovered via tool search (to be loaded) */
118
+ discoveredToolNames: Set<string> = new Set();
105
119
  /** Instructions for this agent */
106
120
  instructions?: string;
107
121
  /** Additional instructions for this agent */
@@ -117,12 +131,16 @@ export class AgentContext {
117
131
  ContentTypes.TEXT;
118
132
  /** Whether tools should end the workflow */
119
133
  toolEnd: boolean = false;
120
- /** System runnable for this agent */
121
- systemRunnable?: Runnable<
134
+ /** Cached system runnable (created lazily) */
135
+ private cachedSystemRunnable?: Runnable<
122
136
  BaseMessage[],
123
137
  (BaseMessage | SystemMessage)[],
124
138
  RunnableConfig<Record<string, unknown>>
125
139
  >;
140
+ /** Whether system runnable needs rebuild (set when discovered tools change) */
141
+ private systemRunnableStale: boolean = true;
142
+ /** Cached system message token count (separate from tool tokens) */
143
+ private systemMessageTokens: number = 0;
126
144
  /** Promise for token calculation initialization */
127
145
  tokenCalculationPromise?: Promise<void>;
128
146
  /** Format content blocks as strings (for legacy compatibility) */
@@ -137,6 +155,7 @@ export class AgentContext {
137
155
  tokenCounter,
138
156
  tools,
139
157
  toolMap,
158
+ toolRegistry,
140
159
  instructions,
141
160
  additionalInstructions,
142
161
  reasoningKey,
@@ -152,6 +171,7 @@ export class AgentContext {
152
171
  tokenCounter?: t.TokenCounter;
153
172
  tools?: t.GraphTools;
154
173
  toolMap?: t.ToolMap;
174
+ toolRegistry?: t.LCToolRegistry;
155
175
  instructions?: string;
156
176
  additionalInstructions?: string;
157
177
  reasoningKey?: 'reasoning_content' | 'reasoning';
@@ -167,6 +187,7 @@ export class AgentContext {
167
187
  this.tokenCounter = tokenCounter;
168
188
  this.tools = tools;
169
189
  this.toolMap = toolMap;
190
+ this.toolRegistry = toolRegistry;
170
191
  this.instructions = instructions;
171
192
  this.additionalInstructions = additionalInstructions;
172
193
  if (reasoningKey) {
@@ -180,39 +201,145 @@ export class AgentContext {
180
201
  }
181
202
 
182
203
  this.useLegacyContent = useLegacyContent ?? false;
204
+ }
205
+
206
+ /**
207
+ * Builds instructions text for tools that are ONLY callable via programmatic code execution.
208
+ * These tools cannot be called directly by the LLM but are available through the
209
+ * run_tools_with_code tool.
210
+ *
211
+ * Includes:
212
+ * - Code_execution-only tools that are NOT deferred
213
+ * - Code_execution-only tools that ARE deferred but have been discovered via tool search
214
+ */
215
+ private buildProgrammaticOnlyToolsInstructions(): string {
216
+ if (!this.toolRegistry) return '';
217
+
218
+ const programmaticOnlyTools: t.LCTool[] = [];
219
+ for (const [name, toolDef] of this.toolRegistry) {
220
+ const allowedCallers = toolDef.allowed_callers ?? ['direct'];
221
+ const isCodeExecutionOnly =
222
+ allowedCallers.includes('code_execution') &&
223
+ !allowedCallers.includes('direct');
224
+
225
+ if (!isCodeExecutionOnly) continue;
226
+
227
+ // Include if: not deferred OR deferred but discovered
228
+ const isDeferred = toolDef.defer_loading === true;
229
+ const isDiscovered = this.discoveredToolNames.has(name);
230
+ if (!isDeferred || isDiscovered) {
231
+ programmaticOnlyTools.push(toolDef);
232
+ }
233
+ }
234
+
235
+ if (programmaticOnlyTools.length === 0) return '';
236
+
237
+ const toolDescriptions = programmaticOnlyTools
238
+ .map((tool) => {
239
+ let desc = `- **${tool.name}**`;
240
+ if (tool.description != null && tool.description !== '') {
241
+ desc += `: ${tool.description}`;
242
+ }
243
+ if (tool.parameters) {
244
+ desc += `\n Parameters: ${JSON.stringify(tool.parameters, null, 2).replace(/\n/g, '\n ')}`;
245
+ }
246
+ return desc;
247
+ })
248
+ .join('\n\n');
183
249
 
184
- this.systemRunnable = this.createSystemRunnable();
250
+ return (
251
+ '\n\n## Programmatic-Only Tools\n\n' +
252
+ 'The following tools are available exclusively through the `run_tools_with_code` tool. ' +
253
+ 'You cannot call these tools directly; instead, use `run_tools_with_code` with Python code that invokes them.\n\n' +
254
+ toolDescriptions
255
+ );
185
256
  }
186
257
 
187
258
  /**
188
- * Create system runnable from instructions and calculate tokens if tokenCounter is available
259
+ * Gets the system runnable, creating it lazily if needed.
260
+ * Includes instructions, additional instructions, and programmatic-only tools documentation.
261
+ * Only rebuilds when marked stale (via markToolsAsDiscovered).
189
262
  */
190
- private createSystemRunnable():
263
+ get systemRunnable():
191
264
  | Runnable<
192
265
  BaseMessage[],
193
266
  (BaseMessage | SystemMessage)[],
194
267
  RunnableConfig<Record<string, unknown>>
195
268
  >
196
269
  | undefined {
197
- let finalInstructions: string | BaseMessageFields | undefined =
198
- this.instructions;
270
+ // Return cached if not stale
271
+ if (!this.systemRunnableStale && this.cachedSystemRunnable !== undefined) {
272
+ return this.cachedSystemRunnable;
273
+ }
274
+
275
+ // Stale or first access - rebuild
276
+ const instructionsString = this.buildInstructionsString();
277
+ this.cachedSystemRunnable = this.buildSystemRunnable(instructionsString);
278
+ this.systemRunnableStale = false;
279
+ return this.cachedSystemRunnable;
280
+ }
281
+
282
+ /**
283
+ * Explicitly initializes the system runnable.
284
+ * Call this before async token calculation to ensure system message tokens are counted first.
285
+ */
286
+ initializeSystemRunnable(): void {
287
+ if (this.systemRunnableStale || this.cachedSystemRunnable === undefined) {
288
+ const instructionsString = this.buildInstructionsString();
289
+ this.cachedSystemRunnable = this.buildSystemRunnable(instructionsString);
290
+ this.systemRunnableStale = false;
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Builds the raw instructions string (without creating SystemMessage).
296
+ */
297
+ private buildInstructionsString(): string {
298
+ let result = this.instructions ?? '';
199
299
 
200
300
  if (
201
301
  this.additionalInstructions != null &&
202
302
  this.additionalInstructions !== ''
203
303
  ) {
204
- finalInstructions =
205
- finalInstructions != null && finalInstructions
206
- ? `${finalInstructions}\n\n${this.additionalInstructions}`
207
- : this.additionalInstructions;
304
+ result = result
305
+ ? `${result}\n\n${this.additionalInstructions}`
306
+ : this.additionalInstructions;
208
307
  }
209
308
 
210
- // Handle Anthropic prompt caching
211
- if (
212
- finalInstructions != null &&
213
- finalInstructions !== '' &&
214
- this.provider === Providers.ANTHROPIC
215
- ) {
309
+ const programmaticToolsDoc = this.buildProgrammaticOnlyToolsInstructions();
310
+ if (programmaticToolsDoc) {
311
+ result = result
312
+ ? `${result}${programmaticToolsDoc}`
313
+ : programmaticToolsDoc;
314
+ }
315
+
316
+ return result;
317
+ }
318
+
319
+ /**
320
+ * Build system runnable from pre-built instructions string.
321
+ * Only called when content has actually changed.
322
+ */
323
+ private buildSystemRunnable(
324
+ instructionsString: string
325
+ ):
326
+ | Runnable<
327
+ BaseMessage[],
328
+ (BaseMessage | SystemMessage)[],
329
+ RunnableConfig<Record<string, unknown>>
330
+ >
331
+ | undefined {
332
+ if (!instructionsString) {
333
+ // Remove previous tokens if we had a system message before
334
+ this.instructionTokens -= this.systemMessageTokens;
335
+ this.systemMessageTokens = 0;
336
+ return undefined;
337
+ }
338
+
339
+ let finalInstructions: string | BaseMessageFields = instructionsString;
340
+
341
+ // Handle Anthropic prompt caching (Direct API)
342
+ if (this.provider === Providers.ANTHROPIC) {
216
343
  const anthropicOptions = this.clientOptions as
217
344
  | t.AnthropicClientOptions
218
345
  | undefined;
@@ -228,7 +355,7 @@ export class AgentContext {
228
355
  content: [
229
356
  {
230
357
  type: 'text',
231
- text: this.instructions,
358
+ text: instructionsString,
232
359
  cache_control: { type: 'ephemeral' },
233
360
  },
234
361
  ],
@@ -236,19 +363,47 @@ export class AgentContext {
236
363
  }
237
364
  }
238
365
 
239
- if (finalInstructions != null && finalInstructions !== '') {
240
- const systemMessage = new SystemMessage(finalInstructions);
366
+ // Handle Bedrock prompt caching (Converse API)
367
+ // Adds cachePoint block after text content for system message caching
368
+ // NOTE: Both Claude and Nova models support cachePoint in system and messages
369
+ // (Nova does NOT support cachePoint in tools - that check is in bedrock/index.ts)
370
+ if (this.provider === Providers.BEDROCK) {
371
+ const bedrockOptions = this.clientOptions as
372
+ | t.BedrockAnthropicInput
373
+ | undefined;
374
+ const modelId = bedrockOptions?.model?.toLowerCase() ?? '';
375
+ const supportsCaching = modelId.includes('claude') || modelId.includes('anthropic') || modelId.includes('nova');
241
376
 
242
- if (this.tokenCounter) {
243
- this.instructionTokens += this.tokenCounter(systemMessage);
377
+ if (bedrockOptions?.promptCache === true && supportsCaching) {
378
+ // Always log system cache structure
379
+ console.log(`[Cache] 📝 System | chars=${instructionsString.length} | tokens=${this.systemMessageTokens} | model=${modelId}`);
380
+
381
+ finalInstructions = {
382
+ content: [
383
+ {
384
+ type: 'text',
385
+ text: instructionsString,
386
+ },
387
+ {
388
+ cachePoint: { type: 'default' },
389
+ },
390
+ ],
391
+ };
244
392
  }
393
+ }
245
394
 
246
- return RunnableLambda.from((messages: BaseMessage[]) => {
247
- return [systemMessage, ...messages];
248
- }).withConfig({ runName: 'prompt' });
395
+ const systemMessage = new SystemMessage(finalInstructions);
396
+
397
+ // Update token counts (subtract old, add new)
398
+ if (this.tokenCounter) {
399
+ this.instructionTokens -= this.systemMessageTokens;
400
+ this.systemMessageTokens = this.tokenCounter(systemMessage);
401
+ this.instructionTokens += this.systemMessageTokens;
249
402
  }
250
403
 
251
- return undefined;
404
+ return RunnableLambda.from((messages: BaseMessage[]) => {
405
+ return [systemMessage, ...messages];
406
+ }).withConfig({ runName: 'prompt' });
252
407
  }
253
408
 
254
409
  /**
@@ -256,6 +411,9 @@ export class AgentContext {
256
411
  */
257
412
  reset(): void {
258
413
  this.instructionTokens = 0;
414
+ this.systemMessageTokens = 0;
415
+ this.cachedSystemRunnable = undefined;
416
+ this.systemRunnableStale = true;
259
417
  this.lastToken = undefined;
260
418
  this.indexTokenCountMap = {};
261
419
  this.currentUsage = undefined;
@@ -263,6 +421,7 @@ export class AgentContext {
263
421
  this.lastStreamCall = undefined;
264
422
  this.tokenTypeSwitch = undefined;
265
423
  this.currentTokenType = ContentTypes.TEXT;
424
+ this.discoveredToolNames.clear();
266
425
  }
267
426
 
268
427
  /**
@@ -320,4 +479,85 @@ export class AgentContext {
320
479
  // Add tool tokens to existing instruction tokens (which may already include system message tokens)
321
480
  this.instructionTokens += toolTokens;
322
481
  }
482
+
483
+ /**
484
+ * Gets the tool registry for deferred tools (for tool search).
485
+ * @param onlyDeferred If true, only returns tools with defer_loading=true
486
+ * @returns LCToolRegistry with tool definitions
487
+ */
488
+ getDeferredToolRegistry(onlyDeferred: boolean = true): t.LCToolRegistry {
489
+ const registry: t.LCToolRegistry = new Map();
490
+
491
+ if (!this.toolRegistry) {
492
+ return registry;
493
+ }
494
+
495
+ for (const [name, toolDef] of this.toolRegistry) {
496
+ if (!onlyDeferred || toolDef.defer_loading === true) {
497
+ registry.set(name, toolDef);
498
+ }
499
+ }
500
+
501
+ return registry;
502
+ }
503
+
504
+ /**
505
+ * Marks tools as discovered via tool search.
506
+ * Discovered tools will be included in the next model binding.
507
+ * Only marks system runnable stale if NEW tools were actually added.
508
+ * @param toolNames - Array of discovered tool names
509
+ * @returns true if any new tools were discovered
510
+ */
511
+ markToolsAsDiscovered(toolNames: string[]): boolean {
512
+ let hasNewDiscoveries = false;
513
+ for (const name of toolNames) {
514
+ if (!this.discoveredToolNames.has(name)) {
515
+ this.discoveredToolNames.add(name);
516
+ hasNewDiscoveries = true;
517
+ }
518
+ }
519
+ if (hasNewDiscoveries) {
520
+ this.systemRunnableStale = true;
521
+ }
522
+ return hasNewDiscoveries;
523
+ }
524
+
525
+ /**
526
+ * Gets tools that should be bound to the LLM.
527
+ * Includes:
528
+ * 1. Non-deferred tools with allowed_callers: ['direct']
529
+ * 2. Discovered tools (from tool search)
530
+ * @returns Array of tools to bind to model
531
+ */
532
+ getToolsForBinding(): t.GraphTools | undefined {
533
+ if (!this.tools || !this.toolRegistry) {
534
+ return this.tools;
535
+ }
536
+
537
+ const toolsToInclude = this.tools.filter((tool) => {
538
+ if (!('name' in tool)) {
539
+ return true; // No name, include by default
540
+ }
541
+
542
+ const toolDef = this.toolRegistry?.get(tool.name);
543
+ if (!toolDef) {
544
+ return true; // Not in registry, include by default
545
+ }
546
+
547
+ // Check if discovered (overrides defer_loading)
548
+ if (this.discoveredToolNames.has(tool.name)) {
549
+ // Discovered tools must still have allowed_callers: ['direct']
550
+ const allowedCallers = toolDef.allowed_callers ?? ['direct'];
551
+ return allowedCallers.includes('direct');
552
+ }
553
+
554
+ // Not discovered: must be direct-callable AND not deferred
555
+ const allowedCallers = toolDef.allowed_callers ?? ['direct'];
556
+ return (
557
+ allowedCallers.includes('direct') && toolDef.defer_loading !== true
558
+ );
559
+ });
560
+
561
+ return toolsToInclude;
562
+ }
323
563
  }