illuma-agents 1.0.10 → 1.0.12

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 (117) hide show
  1. package/LICENSE +1 -1
  2. package/dist/cjs/agents/AgentContext.cjs +236 -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 +44 -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/bedrock/index.cjs +121 -6
  13. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  14. package/dist/cjs/main.cjs +18 -0
  15. package/dist/cjs/main.cjs.map +1 -1
  16. package/dist/cjs/messages/cache.cjs +149 -54
  17. package/dist/cjs/messages/cache.cjs.map +1 -1
  18. package/dist/cjs/messages/tools.cjs +85 -0
  19. package/dist/cjs/messages/tools.cjs.map +1 -0
  20. package/dist/cjs/run.cjs +0 -8
  21. package/dist/cjs/run.cjs.map +1 -1
  22. package/dist/cjs/tools/CodeExecutor.cjs +4 -0
  23. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  24. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +438 -0
  25. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -0
  26. package/dist/cjs/tools/ToolNode.cjs +53 -15
  27. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  28. package/dist/cjs/tools/ToolSearchRegex.cjs +455 -0
  29. package/dist/cjs/tools/ToolSearchRegex.cjs.map +1 -0
  30. package/dist/cjs/tools/search/schema.cjs +7 -9
  31. package/dist/cjs/tools/search/schema.cjs.map +1 -1
  32. package/dist/cjs/utils/run.cjs +5 -1
  33. package/dist/cjs/utils/run.cjs.map +1 -1
  34. package/dist/esm/agents/AgentContext.mjs +236 -27
  35. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  36. package/dist/esm/common/enum.mjs +2 -0
  37. package/dist/esm/common/enum.mjs.map +1 -1
  38. package/dist/esm/events.mjs +4 -12
  39. package/dist/esm/events.mjs.map +1 -1
  40. package/dist/esm/graphs/Graph.mjs +45 -19
  41. package/dist/esm/graphs/Graph.mjs.map +1 -1
  42. package/dist/esm/instrumentation.mjs +1 -3
  43. package/dist/esm/instrumentation.mjs.map +1 -1
  44. package/dist/esm/llm/bedrock/index.mjs +121 -6
  45. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  46. package/dist/esm/main.mjs +3 -0
  47. package/dist/esm/main.mjs.map +1 -1
  48. package/dist/esm/messages/cache.mjs +149 -54
  49. package/dist/esm/messages/cache.mjs.map +1 -1
  50. package/dist/esm/messages/tools.mjs +82 -0
  51. package/dist/esm/messages/tools.mjs.map +1 -0
  52. package/dist/esm/run.mjs +0 -8
  53. package/dist/esm/run.mjs.map +1 -1
  54. package/dist/esm/tools/CodeExecutor.mjs +4 -0
  55. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  56. package/dist/esm/tools/ProgrammaticToolCalling.mjs +430 -0
  57. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -0
  58. package/dist/esm/tools/ToolNode.mjs +53 -15
  59. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  60. package/dist/esm/tools/ToolSearchRegex.mjs +448 -0
  61. package/dist/esm/tools/ToolSearchRegex.mjs.map +1 -0
  62. package/dist/esm/tools/search/schema.mjs +7 -9
  63. package/dist/esm/tools/search/schema.mjs.map +1 -1
  64. package/dist/esm/utils/run.mjs +5 -1
  65. package/dist/esm/utils/run.mjs.map +1 -1
  66. package/dist/types/agents/AgentContext.d.ts +72 -5
  67. package/dist/types/common/enum.d.ts +2 -0
  68. package/dist/types/graphs/Graph.d.ts +3 -2
  69. package/dist/types/index.d.ts +2 -0
  70. package/dist/types/llm/bedrock/index.d.ts +31 -4
  71. package/dist/types/messages/cache.d.ts +23 -8
  72. package/dist/types/messages/index.d.ts +1 -0
  73. package/dist/types/messages/tools.d.ts +17 -0
  74. package/dist/types/test/mockTools.d.ts +28 -0
  75. package/dist/types/tools/ProgrammaticToolCalling.d.ts +91 -0
  76. package/dist/types/tools/ToolNode.d.ts +10 -2
  77. package/dist/types/tools/ToolSearchRegex.d.ts +80 -0
  78. package/dist/types/types/graph.d.ts +14 -1
  79. package/dist/types/types/tools.d.ts +138 -0
  80. package/package.json +7 -8
  81. package/src/agents/AgentContext.ts +278 -27
  82. package/src/agents/__tests__/AgentContext.test.ts +805 -0
  83. package/src/common/enum.ts +2 -0
  84. package/src/events.ts +5 -12
  85. package/src/graphs/Graph.ts +57 -19
  86. package/src/index.ts +2 -0
  87. package/src/instrumentation.ts +1 -4
  88. package/src/llm/bedrock/__tests__/bedrock-caching.test.ts +473 -0
  89. package/src/llm/bedrock/index.ts +149 -12
  90. package/src/messages/__tests__/tools.test.ts +473 -0
  91. package/src/messages/cache.ts +163 -61
  92. package/src/messages/index.ts +1 -0
  93. package/src/messages/tools.ts +99 -0
  94. package/src/run.ts +0 -9
  95. package/src/scripts/code_exec_ptc.ts +334 -0
  96. package/src/scripts/image.ts +178 -0
  97. package/src/scripts/programmatic_exec.ts +396 -0
  98. package/src/scripts/programmatic_exec_agent.ts +231 -0
  99. package/src/scripts/test-tools-before-handoff.ts +5 -1
  100. package/src/scripts/tool_search_regex.ts +162 -0
  101. package/src/scripts/tools.ts +4 -1
  102. package/src/specs/thinking-prune.test.ts +52 -118
  103. package/src/test/mockTools.ts +366 -0
  104. package/src/tools/CodeExecutor.ts +4 -0
  105. package/src/tools/ProgrammaticToolCalling.ts +558 -0
  106. package/src/tools/ToolNode.ts +59 -18
  107. package/src/tools/ToolSearchRegex.ts +535 -0
  108. package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +318 -0
  109. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +853 -0
  110. package/src/tools/__tests__/ToolSearchRegex.integration.test.ts +161 -0
  111. package/src/tools/__tests__/ToolSearchRegex.test.ts +232 -0
  112. package/src/tools/search/jina-reranker.test.ts +16 -16
  113. package/src/tools/search/schema.ts +7 -9
  114. package/src/types/graph.ts +14 -1
  115. package/src/types/tools.ts +166 -0
  116. package/src/utils/run.ts +5 -1
  117. 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.12",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -15,12 +15,6 @@
15
15
  "description": "Illuma AI Agents Library",
16
16
  "author": "Varun Muppidi",
17
17
  "license": "MIT",
18
- "licenses": [
19
- {
20
- "type": "MIT",
21
- "url": "https://github.com/danny-avila/agents/blob/main/LICENSE"
22
- }
23
- ],
24
18
  "packageManager": "npm@10.5.2",
25
19
  "engines": {
26
20
  "node": ">=14.0.0"
@@ -34,7 +28,7 @@
34
28
  "scripts": {
35
29
  "prepare": "node husky-setup.js",
36
30
  "prepublishOnly": "npm run build",
37
- "build": "set NODE_ENV=production && rollup -c && tsc -p tsconfig.build.json",
31
+ "build": "cross-env NODE_ENV=production rollup -c && tsc -p tsconfig.build.json",
38
32
  "build:dev": "rollup -c",
39
33
  "start": "node dist/esm/main.js",
40
34
  "clean": "node ./config/clean.js",
@@ -53,6 +47,10 @@
53
47
  "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
48
  "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
49
  "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'",
50
+ "tool_search_regex": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/tool_search_regex.ts",
51
+ "programmatic_exec": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/programmatic_exec.ts",
52
+ "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'",
53
+ "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
54
  "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
55
  "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
56
  "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 +138,7 @@
140
138
  "@types/yargs-parser": "^21.0.3",
141
139
  "@typescript-eslint/eslint-plugin": "^8.24.0",
142
140
  "@typescript-eslint/parser": "^8.24.0",
141
+ "cross-env": "^10.1.0",
143
142
  "eslint": "^9.39.1",
144
143
  "eslint-import-resolver-typescript": "^3.7.0",
145
144
  "eslint-plugin-import": "^2.31.0",
@@ -32,12 +32,14 @@ export class AgentContext {
32
32
  tools,
33
33
  toolMap,
34
34
  toolEnd,
35
+ toolRegistry,
35
36
  instructions,
36
37
  additional_instructions,
37
38
  streamBuffer,
38
39
  maxContextTokens,
39
40
  reasoningKey,
40
41
  useLegacyContent,
42
+ dynamicContext,
41
43
  } = agentConfig;
42
44
 
43
45
  const agentContext = new AgentContext({
@@ -48,6 +50,7 @@ export class AgentContext {
48
50
  streamBuffer,
49
51
  tools,
50
52
  toolMap,
53
+ toolRegistry,
51
54
  instructions,
52
55
  additionalInstructions: additional_instructions,
53
56
  reasoningKey,
@@ -55,15 +58,21 @@ export class AgentContext {
55
58
  instructionTokens: 0,
56
59
  tokenCounter,
57
60
  useLegacyContent,
61
+ dynamicContext,
58
62
  });
59
63
 
60
64
  if (tokenCounter) {
65
+ // Initialize system runnable BEFORE async tool token calculation
66
+ // This ensures system message tokens are in instructionTokens before
67
+ // updateTokenMapWithInstructions is called
68
+ agentContext.initializeSystemRunnable();
69
+
61
70
  const tokenMap = indexTokenCountMap || {};
62
71
  agentContext.indexTokenCountMap = tokenMap;
63
72
  agentContext.tokenCalculationPromise = agentContext
64
73
  .calculateInstructionTokens(tokenCounter)
65
74
  .then(() => {
66
- // Update token map with instruction tokens
75
+ // Update token map with instruction tokens (includes system + tool tokens)
67
76
  agentContext.updateTokenMapWithInstructions(tokenMap);
68
77
  })
69
78
  .catch((err) => {
@@ -102,10 +111,23 @@ export class AgentContext {
102
111
  tools?: t.GraphTools;
103
112
  /** Tool map for this agent */
104
113
  toolMap?: t.ToolMap;
114
+ /**
115
+ * Tool definitions registry (includes deferred and programmatic tool metadata).
116
+ * Used for tool search and programmatic tool calling.
117
+ */
118
+ toolRegistry?: t.LCToolRegistry;
119
+ /** Set of tool names discovered via tool search (to be loaded) */
120
+ discoveredToolNames: Set<string> = new Set();
105
121
  /** Instructions for this agent */
106
122
  instructions?: string;
107
123
  /** Additional instructions for this agent */
108
124
  additionalInstructions?: string;
125
+ /**
126
+ * Dynamic context that changes per-request (e.g., current time, user info).
127
+ * This is NOT included in the system message to preserve cache.
128
+ * Instead, it's injected as a user message at the start of the conversation.
129
+ */
130
+ dynamicContext?: string;
109
131
  /** Reasoning key for this agent */
110
132
  reasoningKey: 'reasoning_content' | 'reasoning' = 'reasoning_content';
111
133
  /** Last token for reasoning detection */
@@ -117,12 +139,16 @@ export class AgentContext {
117
139
  ContentTypes.TEXT;
118
140
  /** Whether tools should end the workflow */
119
141
  toolEnd: boolean = false;
120
- /** System runnable for this agent */
121
- systemRunnable?: Runnable<
142
+ /** Cached system runnable (created lazily) */
143
+ private cachedSystemRunnable?: Runnable<
122
144
  BaseMessage[],
123
145
  (BaseMessage | SystemMessage)[],
124
146
  RunnableConfig<Record<string, unknown>>
125
147
  >;
148
+ /** Whether system runnable needs rebuild (set when discovered tools change) */
149
+ private systemRunnableStale: boolean = true;
150
+ /** Cached system message token count (separate from tool tokens) */
151
+ private systemMessageTokens: number = 0;
126
152
  /** Promise for token calculation initialization */
127
153
  tokenCalculationPromise?: Promise<void>;
128
154
  /** Format content blocks as strings (for legacy compatibility) */
@@ -137,8 +163,10 @@ export class AgentContext {
137
163
  tokenCounter,
138
164
  tools,
139
165
  toolMap,
166
+ toolRegistry,
140
167
  instructions,
141
168
  additionalInstructions,
169
+ dynamicContext,
142
170
  reasoningKey,
143
171
  toolEnd,
144
172
  instructionTokens,
@@ -152,8 +180,10 @@ export class AgentContext {
152
180
  tokenCounter?: t.TokenCounter;
153
181
  tools?: t.GraphTools;
154
182
  toolMap?: t.ToolMap;
183
+ toolRegistry?: t.LCToolRegistry;
155
184
  instructions?: string;
156
185
  additionalInstructions?: string;
186
+ dynamicContext?: string;
157
187
  reasoningKey?: 'reasoning_content' | 'reasoning';
158
188
  toolEnd?: boolean;
159
189
  instructionTokens?: number;
@@ -167,8 +197,10 @@ export class AgentContext {
167
197
  this.tokenCounter = tokenCounter;
168
198
  this.tools = tools;
169
199
  this.toolMap = toolMap;
200
+ this.toolRegistry = toolRegistry;
170
201
  this.instructions = instructions;
171
202
  this.additionalInstructions = additionalInstructions;
203
+ this.dynamicContext = dynamicContext;
172
204
  if (reasoningKey) {
173
205
  this.reasoningKey = reasoningKey;
174
206
  }
@@ -180,39 +212,145 @@ export class AgentContext {
180
212
  }
181
213
 
182
214
  this.useLegacyContent = useLegacyContent ?? false;
215
+ }
183
216
 
184
- this.systemRunnable = this.createSystemRunnable();
217
+ /**
218
+ * Builds instructions text for tools that are ONLY callable via programmatic code execution.
219
+ * These tools cannot be called directly by the LLM but are available through the
220
+ * run_tools_with_code tool.
221
+ *
222
+ * Includes:
223
+ * - Code_execution-only tools that are NOT deferred
224
+ * - Code_execution-only tools that ARE deferred but have been discovered via tool search
225
+ */
226
+ private buildProgrammaticOnlyToolsInstructions(): string {
227
+ if (!this.toolRegistry) return '';
228
+
229
+ const programmaticOnlyTools: t.LCTool[] = [];
230
+ for (const [name, toolDef] of this.toolRegistry) {
231
+ const allowedCallers = toolDef.allowed_callers ?? ['direct'];
232
+ const isCodeExecutionOnly =
233
+ allowedCallers.includes('code_execution') &&
234
+ !allowedCallers.includes('direct');
235
+
236
+ if (!isCodeExecutionOnly) continue;
237
+
238
+ // Include if: not deferred OR deferred but discovered
239
+ const isDeferred = toolDef.defer_loading === true;
240
+ const isDiscovered = this.discoveredToolNames.has(name);
241
+ if (!isDeferred || isDiscovered) {
242
+ programmaticOnlyTools.push(toolDef);
243
+ }
244
+ }
245
+
246
+ if (programmaticOnlyTools.length === 0) return '';
247
+
248
+ const toolDescriptions = programmaticOnlyTools
249
+ .map((tool) => {
250
+ let desc = `- **${tool.name}**`;
251
+ if (tool.description != null && tool.description !== '') {
252
+ desc += `: ${tool.description}`;
253
+ }
254
+ if (tool.parameters) {
255
+ desc += `\n Parameters: ${JSON.stringify(tool.parameters, null, 2).replace(/\n/g, '\n ')}`;
256
+ }
257
+ return desc;
258
+ })
259
+ .join('\n\n');
260
+
261
+ return (
262
+ '\n\n## Programmatic-Only Tools\n\n' +
263
+ 'The following tools are available exclusively through the `run_tools_with_code` tool. ' +
264
+ 'You cannot call these tools directly; instead, use `run_tools_with_code` with Python code that invokes them.\n\n' +
265
+ toolDescriptions
266
+ );
185
267
  }
186
268
 
187
269
  /**
188
- * Create system runnable from instructions and calculate tokens if tokenCounter is available
270
+ * Gets the system runnable, creating it lazily if needed.
271
+ * Includes instructions, additional instructions, and programmatic-only tools documentation.
272
+ * Only rebuilds when marked stale (via markToolsAsDiscovered).
189
273
  */
190
- private createSystemRunnable():
274
+ get systemRunnable():
191
275
  | Runnable<
192
276
  BaseMessage[],
193
277
  (BaseMessage | SystemMessage)[],
194
278
  RunnableConfig<Record<string, unknown>>
195
279
  >
196
280
  | undefined {
197
- let finalInstructions: string | BaseMessageFields | undefined =
198
- this.instructions;
281
+ // Return cached if not stale
282
+ if (!this.systemRunnableStale && this.cachedSystemRunnable !== undefined) {
283
+ return this.cachedSystemRunnable;
284
+ }
285
+
286
+ // Stale or first access - rebuild
287
+ const instructionsString = this.buildInstructionsString();
288
+ this.cachedSystemRunnable = this.buildSystemRunnable(instructionsString);
289
+ this.systemRunnableStale = false;
290
+ return this.cachedSystemRunnable;
291
+ }
292
+
293
+ /**
294
+ * Explicitly initializes the system runnable.
295
+ * Call this before async token calculation to ensure system message tokens are counted first.
296
+ */
297
+ initializeSystemRunnable(): void {
298
+ if (this.systemRunnableStale || this.cachedSystemRunnable === undefined) {
299
+ const instructionsString = this.buildInstructionsString();
300
+ this.cachedSystemRunnable = this.buildSystemRunnable(instructionsString);
301
+ this.systemRunnableStale = false;
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Builds the raw instructions string (without creating SystemMessage).
307
+ */
308
+ private buildInstructionsString(): string {
309
+ let result = this.instructions ?? '';
199
310
 
200
311
  if (
201
312
  this.additionalInstructions != null &&
202
313
  this.additionalInstructions !== ''
203
314
  ) {
204
- finalInstructions =
205
- finalInstructions != null && finalInstructions
206
- ? `${finalInstructions}\n\n${this.additionalInstructions}`
207
- : this.additionalInstructions;
315
+ result = result
316
+ ? `${result}\n\n${this.additionalInstructions}`
317
+ : this.additionalInstructions;
208
318
  }
209
319
 
210
- // Handle Anthropic prompt caching
211
- if (
212
- finalInstructions != null &&
213
- finalInstructions !== '' &&
214
- this.provider === Providers.ANTHROPIC
215
- ) {
320
+ const programmaticToolsDoc = this.buildProgrammaticOnlyToolsInstructions();
321
+ if (programmaticToolsDoc) {
322
+ result = result
323
+ ? `${result}${programmaticToolsDoc}`
324
+ : programmaticToolsDoc;
325
+ }
326
+
327
+ return result;
328
+ }
329
+
330
+ /**
331
+ * Build system runnable from pre-built instructions string.
332
+ * Only called when content has actually changed.
333
+ */
334
+ private buildSystemRunnable(
335
+ instructionsString: string
336
+ ):
337
+ | Runnable<
338
+ BaseMessage[],
339
+ (BaseMessage | SystemMessage)[],
340
+ RunnableConfig<Record<string, unknown>>
341
+ >
342
+ | undefined {
343
+ if (!instructionsString) {
344
+ // Remove previous tokens if we had a system message before
345
+ this.instructionTokens -= this.systemMessageTokens;
346
+ this.systemMessageTokens = 0;
347
+ return undefined;
348
+ }
349
+
350
+ let finalInstructions: string | BaseMessageFields = instructionsString;
351
+
352
+ // Handle Anthropic prompt caching (Direct API)
353
+ if (this.provider === Providers.ANTHROPIC) {
216
354
  const anthropicOptions = this.clientOptions as
217
355
  | t.AnthropicClientOptions
218
356
  | undefined;
@@ -228,7 +366,7 @@ export class AgentContext {
228
366
  content: [
229
367
  {
230
368
  type: 'text',
231
- text: this.instructions,
369
+ text: instructionsString,
232
370
  cache_control: { type: 'ephemeral' },
233
371
  },
234
372
  ],
@@ -236,19 +374,47 @@ export class AgentContext {
236
374
  }
237
375
  }
238
376
 
239
- if (finalInstructions != null && finalInstructions !== '') {
240
- const systemMessage = new SystemMessage(finalInstructions);
377
+ // Handle Bedrock prompt caching (Converse API)
378
+ // Adds cachePoint block after text content for system message caching
379
+ // NOTE: Both Claude and Nova models support cachePoint in system and messages
380
+ // (Nova does NOT support cachePoint in tools - that check is in bedrock/index.ts)
381
+ if (this.provider === Providers.BEDROCK) {
382
+ const bedrockOptions = this.clientOptions as
383
+ | t.BedrockAnthropicInput
384
+ | undefined;
385
+ const modelId = bedrockOptions?.model?.toLowerCase() ?? '';
386
+ const supportsCaching = modelId.includes('claude') || modelId.includes('anthropic') || modelId.includes('nova');
241
387
 
242
- if (this.tokenCounter) {
243
- this.instructionTokens += this.tokenCounter(systemMessage);
388
+ if (bedrockOptions?.promptCache === true && supportsCaching) {
389
+ // Always log system cache structure
390
+ console.log(`[Cache] 📝 System | chars=${instructionsString.length} | tokens=${this.systemMessageTokens} | model=${modelId}`);
391
+
392
+ finalInstructions = {
393
+ content: [
394
+ {
395
+ type: 'text',
396
+ text: instructionsString,
397
+ },
398
+ {
399
+ cachePoint: { type: 'default' },
400
+ },
401
+ ],
402
+ };
244
403
  }
404
+ }
405
+
406
+ const systemMessage = new SystemMessage(finalInstructions);
245
407
 
246
- return RunnableLambda.from((messages: BaseMessage[]) => {
247
- return [systemMessage, ...messages];
248
- }).withConfig({ runName: 'prompt' });
408
+ // Update token counts (subtract old, add new)
409
+ if (this.tokenCounter) {
410
+ this.instructionTokens -= this.systemMessageTokens;
411
+ this.systemMessageTokens = this.tokenCounter(systemMessage);
412
+ this.instructionTokens += this.systemMessageTokens;
249
413
  }
250
414
 
251
- return undefined;
415
+ return RunnableLambda.from((messages: BaseMessage[]) => {
416
+ return [systemMessage, ...messages];
417
+ }).withConfig({ runName: 'prompt' });
252
418
  }
253
419
 
254
420
  /**
@@ -256,6 +422,9 @@ export class AgentContext {
256
422
  */
257
423
  reset(): void {
258
424
  this.instructionTokens = 0;
425
+ this.systemMessageTokens = 0;
426
+ this.cachedSystemRunnable = undefined;
427
+ this.systemRunnableStale = true;
259
428
  this.lastToken = undefined;
260
429
  this.indexTokenCountMap = {};
261
430
  this.currentUsage = undefined;
@@ -263,6 +432,7 @@ export class AgentContext {
263
432
  this.lastStreamCall = undefined;
264
433
  this.tokenTypeSwitch = undefined;
265
434
  this.currentTokenType = ContentTypes.TEXT;
435
+ this.discoveredToolNames.clear();
266
436
  }
267
437
 
268
438
  /**
@@ -320,4 +490,85 @@ export class AgentContext {
320
490
  // Add tool tokens to existing instruction tokens (which may already include system message tokens)
321
491
  this.instructionTokens += toolTokens;
322
492
  }
493
+
494
+ /**
495
+ * Gets the tool registry for deferred tools (for tool search).
496
+ * @param onlyDeferred If true, only returns tools with defer_loading=true
497
+ * @returns LCToolRegistry with tool definitions
498
+ */
499
+ getDeferredToolRegistry(onlyDeferred: boolean = true): t.LCToolRegistry {
500
+ const registry: t.LCToolRegistry = new Map();
501
+
502
+ if (!this.toolRegistry) {
503
+ return registry;
504
+ }
505
+
506
+ for (const [name, toolDef] of this.toolRegistry) {
507
+ if (!onlyDeferred || toolDef.defer_loading === true) {
508
+ registry.set(name, toolDef);
509
+ }
510
+ }
511
+
512
+ return registry;
513
+ }
514
+
515
+ /**
516
+ * Marks tools as discovered via tool search.
517
+ * Discovered tools will be included in the next model binding.
518
+ * Only marks system runnable stale if NEW tools were actually added.
519
+ * @param toolNames - Array of discovered tool names
520
+ * @returns true if any new tools were discovered
521
+ */
522
+ markToolsAsDiscovered(toolNames: string[]): boolean {
523
+ let hasNewDiscoveries = false;
524
+ for (const name of toolNames) {
525
+ if (!this.discoveredToolNames.has(name)) {
526
+ this.discoveredToolNames.add(name);
527
+ hasNewDiscoveries = true;
528
+ }
529
+ }
530
+ if (hasNewDiscoveries) {
531
+ this.systemRunnableStale = true;
532
+ }
533
+ return hasNewDiscoveries;
534
+ }
535
+
536
+ /**
537
+ * Gets tools that should be bound to the LLM.
538
+ * Includes:
539
+ * 1. Non-deferred tools with allowed_callers: ['direct']
540
+ * 2. Discovered tools (from tool search)
541
+ * @returns Array of tools to bind to model
542
+ */
543
+ getToolsForBinding(): t.GraphTools | undefined {
544
+ if (!this.tools || !this.toolRegistry) {
545
+ return this.tools;
546
+ }
547
+
548
+ const toolsToInclude = this.tools.filter((tool) => {
549
+ if (!('name' in tool)) {
550
+ return true; // No name, include by default
551
+ }
552
+
553
+ const toolDef = this.toolRegistry?.get(tool.name);
554
+ if (!toolDef) {
555
+ return true; // Not in registry, include by default
556
+ }
557
+
558
+ // Check if discovered (overrides defer_loading)
559
+ if (this.discoveredToolNames.has(tool.name)) {
560
+ // Discovered tools must still have allowed_callers: ['direct']
561
+ const allowedCallers = toolDef.allowed_callers ?? ['direct'];
562
+ return allowedCallers.includes('direct');
563
+ }
564
+
565
+ // Not discovered: must be direct-callable AND not deferred
566
+ const allowedCallers = toolDef.allowed_callers ?? ['direct'];
567
+ return (
568
+ allowedCallers.includes('direct') && toolDef.defer_loading !== true
569
+ );
570
+ });
571
+
572
+ return toolsToInclude;
573
+ }
323
574
  }