@framers/agentos 0.10.22 → 0.10.24

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 (204) hide show
  1. package/README.md +1 -1
  2. package/dist/api/AgentOS.d.ts.map +1 -1
  3. package/dist/api/AgentOS.js +3 -2
  4. package/dist/api/AgentOS.js.map +1 -1
  5. package/dist/api/agent.d.ts.map +1 -1
  6. package/dist/api/agent.js +25 -10
  7. package/dist/api/agent.js.map +1 -1
  8. package/dist/api/embedText.d.ts.map +1 -1
  9. package/dist/api/embedText.js +53 -17
  10. package/dist/api/embedText.js.map +1 -1
  11. package/dist/api/generateImage.d.ts.map +1 -1
  12. package/dist/api/generateImage.js +51 -7
  13. package/dist/api/generateImage.js.map +1 -1
  14. package/dist/api/generateText.d.ts +50 -7
  15. package/dist/api/generateText.d.ts.map +1 -1
  16. package/dist/api/generateText.js +265 -35
  17. package/dist/api/generateText.js.map +1 -1
  18. package/dist/api/model.d.ts +25 -1
  19. package/dist/api/model.d.ts.map +1 -1
  20. package/dist/api/model.js +108 -3
  21. package/dist/api/model.js.map +1 -1
  22. package/dist/api/runtime/SelfImprovementSessionManager.d.ts +34 -2
  23. package/dist/api/runtime/SelfImprovementSessionManager.d.ts.map +1 -1
  24. package/dist/api/runtime/SelfImprovementSessionManager.js +119 -33
  25. package/dist/api/runtime/SelfImprovementSessionManager.js.map +1 -1
  26. package/dist/api/runtime/global-default.d.ts +9 -1
  27. package/dist/api/runtime/global-default.d.ts.map +1 -1
  28. package/dist/api/runtime/global-default.js.map +1 -1
  29. package/dist/api/runtime/provider-defaults.d.ts.map +1 -1
  30. package/dist/api/runtime/provider-defaults.js +10 -4
  31. package/dist/api/runtime/provider-defaults.js.map +1 -1
  32. package/dist/api/runtime/tool-emulation/history.d.ts +16 -0
  33. package/dist/api/runtime/tool-emulation/history.d.ts.map +1 -0
  34. package/dist/api/runtime/tool-emulation/history.js +89 -0
  35. package/dist/api/runtime/tool-emulation/history.js.map +1 -0
  36. package/dist/api/runtime/tool-emulation/index.d.ts +1 -0
  37. package/dist/api/runtime/tool-emulation/index.d.ts.map +1 -1
  38. package/dist/api/runtime/tool-emulation/index.js +1 -0
  39. package/dist/api/runtime/tool-emulation/index.js.map +1 -1
  40. package/dist/api/runtime/tool-emulation/loop.d.ts +6 -0
  41. package/dist/api/runtime/tool-emulation/loop.d.ts.map +1 -1
  42. package/dist/api/runtime/tool-emulation/loop.js +1 -0
  43. package/dist/api/runtime/tool-emulation/loop.js.map +1 -1
  44. package/dist/api/sessionTranscript.d.ts +34 -0
  45. package/dist/api/sessionTranscript.d.ts.map +1 -1
  46. package/dist/api/sessionTranscript.js +61 -1
  47. package/dist/api/sessionTranscript.js.map +1 -1
  48. package/dist/api/streamText.d.ts.map +1 -1
  49. package/dist/api/streamText.js +120 -24
  50. package/dist/api/streamText.js.map +1 -1
  51. package/dist/api/types/AgentOSInput.d.ts +11 -0
  52. package/dist/api/types/AgentOSInput.d.ts.map +1 -1
  53. package/dist/api/types.d.ts +14 -6
  54. package/dist/api/types.d.ts.map +1 -1
  55. package/dist/api/types.js.map +1 -1
  56. package/dist/cognition/emergent/AdaptPersonalityTool.d.ts +13 -4
  57. package/dist/cognition/emergent/AdaptPersonalityTool.d.ts.map +1 -1
  58. package/dist/cognition/emergent/AdaptPersonalityTool.js +32 -23
  59. package/dist/cognition/emergent/AdaptPersonalityTool.js.map +1 -1
  60. package/dist/cognition/emergent/EmergentCapabilityEngine.d.ts +18 -6
  61. package/dist/cognition/emergent/EmergentCapabilityEngine.d.ts.map +1 -1
  62. package/dist/cognition/emergent/EmergentCapabilityEngine.js.map +1 -1
  63. package/dist/cognition/emergent/SelfEvaluateTool.d.ts +10 -3
  64. package/dist/cognition/emergent/SelfEvaluateTool.d.ts.map +1 -1
  65. package/dist/cognition/emergent/SelfEvaluateTool.js +3 -2
  66. package/dist/cognition/emergent/SelfEvaluateTool.js.map +1 -1
  67. package/dist/cognition/substrate/CognitiveMemoryBridge.d.ts +51 -2
  68. package/dist/cognition/substrate/CognitiveMemoryBridge.d.ts.map +1 -1
  69. package/dist/cognition/substrate/CognitiveMemoryBridge.js +84 -2
  70. package/dist/cognition/substrate/CognitiveMemoryBridge.js.map +1 -1
  71. package/dist/cognition/substrate/ConversationHistoryManager.d.ts +8 -4
  72. package/dist/cognition/substrate/ConversationHistoryManager.d.ts.map +1 -1
  73. package/dist/cognition/substrate/ConversationHistoryManager.js +17 -5
  74. package/dist/cognition/substrate/ConversationHistoryManager.js.map +1 -1
  75. package/dist/cognition/substrate/GMI.d.ts +92 -5
  76. package/dist/cognition/substrate/GMI.d.ts.map +1 -1
  77. package/dist/cognition/substrate/GMI.js +291 -65
  78. package/dist/cognition/substrate/GMI.js.map +1 -1
  79. package/dist/cognition/substrate/GMIManager.d.ts +12 -0
  80. package/dist/cognition/substrate/GMIManager.d.ts.map +1 -1
  81. package/dist/cognition/substrate/GMIManager.js +22 -16
  82. package/dist/cognition/substrate/GMIManager.js.map +1 -1
  83. package/dist/cognition/substrate/IGMI.d.ts +23 -0
  84. package/dist/cognition/substrate/IGMI.d.ts.map +1 -1
  85. package/dist/cognition/substrate/IGMI.js.map +1 -1
  86. package/dist/cognition/substrate/MetapromptExecutor.d.ts +183 -28
  87. package/dist/cognition/substrate/MetapromptExecutor.d.ts.map +1 -1
  88. package/dist/cognition/substrate/MetapromptExecutor.js +431 -108
  89. package/dist/cognition/substrate/MetapromptExecutor.js.map +1 -1
  90. package/dist/cognition/substrate/personas/IPersonaDefinition.d.ts +5 -0
  91. package/dist/cognition/substrate/personas/IPersonaDefinition.d.ts.map +1 -1
  92. package/dist/cognition/substrate/personas/PersonaValidation.d.ts.map +1 -1
  93. package/dist/cognition/substrate/personas/PersonaValidation.js +30 -0
  94. package/dist/cognition/substrate/personas/PersonaValidation.js.map +1 -1
  95. package/dist/cognition/substrate/personas/SoulLoader.d.ts +14 -1
  96. package/dist/cognition/substrate/personas/SoulLoader.d.ts.map +1 -1
  97. package/dist/cognition/substrate/personas/SoulLoader.js +45 -12
  98. package/dist/cognition/substrate/personas/SoulLoader.js.map +1 -1
  99. package/dist/cognition/substrate/personas/definitions/default_assistant_persona.json +3 -35
  100. package/dist/cognition/substrate/personas/hexaco.d.ts +34 -0
  101. package/dist/cognition/substrate/personas/hexaco.d.ts.map +1 -0
  102. package/dist/cognition/substrate/personas/hexaco.js +66 -0
  103. package/dist/cognition/substrate/personas/hexaco.js.map +1 -0
  104. package/dist/cognition/substrate/userFeedback.d.ts +69 -0
  105. package/dist/cognition/substrate/userFeedback.d.ts.map +1 -0
  106. package/dist/cognition/substrate/userFeedback.js +107 -0
  107. package/dist/cognition/substrate/userFeedback.js.map +1 -0
  108. package/dist/core/conversation/ConversationMessage.d.ts +9 -0
  109. package/dist/core/conversation/ConversationMessage.d.ts.map +1 -1
  110. package/dist/core/conversation/ConversationMessage.js.map +1 -1
  111. package/dist/core/llm/IPromptEngine.d.ts +9 -0
  112. package/dist/core/llm/IPromptEngine.d.ts.map +1 -1
  113. package/dist/core/llm/IPromptEngine.js.map +1 -1
  114. package/dist/core/llm/PromptEngine.d.ts +30 -1
  115. package/dist/core/llm/PromptEngine.d.ts.map +1 -1
  116. package/dist/core/llm/PromptEngine.js +229 -92
  117. package/dist/core/llm/PromptEngine.js.map +1 -1
  118. package/dist/core/llm/providers/AIModelProviderManager.d.ts +20 -0
  119. package/dist/core/llm/providers/AIModelProviderManager.d.ts.map +1 -1
  120. package/dist/core/llm/providers/AIModelProviderManager.js +35 -5
  121. package/dist/core/llm/providers/AIModelProviderManager.js.map +1 -1
  122. package/dist/core/llm/providers/IProvider.d.ts +28 -8
  123. package/dist/core/llm/providers/IProvider.d.ts.map +1 -1
  124. package/dist/core/llm/providers/implementations/AnthropicProvider.d.ts +135 -49
  125. package/dist/core/llm/providers/implementations/AnthropicProvider.d.ts.map +1 -1
  126. package/dist/core/llm/providers/implementations/AnthropicProvider.js +323 -114
  127. package/dist/core/llm/providers/implementations/AnthropicProvider.js.map +1 -1
  128. package/dist/core/llm/providers/implementations/ClaudeCodeProvider.d.ts.map +1 -1
  129. package/dist/core/llm/providers/implementations/ClaudeCodeProvider.js +15 -1
  130. package/dist/core/llm/providers/implementations/ClaudeCodeProvider.js.map +1 -1
  131. package/dist/core/llm/providers/implementations/GeminiCLIProvider.d.ts +1 -1
  132. package/dist/core/llm/providers/implementations/GeminiCLIProvider.d.ts.map +1 -1
  133. package/dist/core/llm/providers/implementations/GeminiCLIProvider.js +51 -24
  134. package/dist/core/llm/providers/implementations/GeminiCLIProvider.js.map +1 -1
  135. package/dist/core/llm/providers/implementations/GeminiProvider.d.ts +25 -6
  136. package/dist/core/llm/providers/implementations/GeminiProvider.d.ts.map +1 -1
  137. package/dist/core/llm/providers/implementations/GeminiProvider.js +108 -90
  138. package/dist/core/llm/providers/implementations/GeminiProvider.js.map +1 -1
  139. package/dist/core/llm/providers/implementations/MistralProvider.d.ts +11 -0
  140. package/dist/core/llm/providers/implementations/MistralProvider.d.ts.map +1 -1
  141. package/dist/core/llm/providers/implementations/MistralProvider.js +32 -2
  142. package/dist/core/llm/providers/implementations/MistralProvider.js.map +1 -1
  143. package/dist/core/llm/providers/implementations/OllamaProvider.js +7 -7
  144. package/dist/core/llm/providers/implementations/OllamaProvider.js.map +1 -1
  145. package/dist/core/llm/providers/implementations/OpenAIProvider.d.ts +53 -2
  146. package/dist/core/llm/providers/implementations/OpenAIProvider.d.ts.map +1 -1
  147. package/dist/core/llm/providers/implementations/OpenAIProvider.js +84 -17
  148. package/dist/core/llm/providers/implementations/OpenAIProvider.js.map +1 -1
  149. package/dist/core/llm/providers/implementations/OpenRouterProvider.d.ts.map +1 -1
  150. package/dist/core/llm/providers/implementations/OpenRouterProvider.js +8 -3
  151. package/dist/core/llm/providers/implementations/OpenRouterProvider.js.map +1 -1
  152. package/dist/core/llm/providers/implementations/TogetherProvider.d.ts +3 -3
  153. package/dist/core/llm/providers/implementations/TogetherProvider.d.ts.map +1 -1
  154. package/dist/core/llm/providers/implementations/TogetherProvider.js +25 -13
  155. package/dist/core/llm/providers/implementations/TogetherProvider.js.map +1 -1
  156. package/dist/core/llm/providers/implementations/cacheLeakDetector.d.ts +15 -7
  157. package/dist/core/llm/providers/implementations/cacheLeakDetector.d.ts.map +1 -1
  158. package/dist/core/llm/providers/implementations/cacheLeakDetector.js +12 -7
  159. package/dist/core/llm/providers/implementations/cacheLeakDetector.js.map +1 -1
  160. package/dist/core/llm/providers/model-cache-capabilities.d.ts +24 -9
  161. package/dist/core/llm/providers/model-cache-capabilities.d.ts.map +1 -1
  162. package/dist/core/llm/providers/model-cache-capabilities.js +59 -44
  163. package/dist/core/llm/providers/model-cache-capabilities.js.map +1 -1
  164. package/dist/core/llm/providers/model-effort.d.ts +14 -0
  165. package/dist/core/llm/providers/model-effort.d.ts.map +1 -1
  166. package/dist/core/llm/providers/model-effort.js +22 -0
  167. package/dist/core/llm/providers/model-effort.js.map +1 -1
  168. package/dist/core/llm/providers/model-forced-tool-choice.d.ts +13 -6
  169. package/dist/core/llm/providers/model-forced-tool-choice.d.ts.map +1 -1
  170. package/dist/core/llm/providers/model-forced-tool-choice.js +14 -7
  171. package/dist/core/llm/providers/model-forced-tool-choice.js.map +1 -1
  172. package/dist/core/llm/providers/model-thinking.d.ts +64 -13
  173. package/dist/core/llm/providers/model-thinking.d.ts.map +1 -1
  174. package/dist/core/llm/providers/model-thinking.js +52 -12
  175. package/dist/core/llm/providers/model-thinking.js.map +1 -1
  176. package/dist/core/llm/providers/openrouter-only-params.d.ts +26 -7
  177. package/dist/core/llm/providers/openrouter-only-params.d.ts.map +1 -1
  178. package/dist/core/llm/providers/openrouter-only-params.js +58 -9
  179. package/dist/core/llm/providers/openrouter-only-params.js.map +1 -1
  180. package/dist/core/llm/providers/url-secrets.d.ts +42 -0
  181. package/dist/core/llm/providers/url-secrets.d.ts.map +1 -0
  182. package/dist/core/llm/providers/url-secrets.js +64 -0
  183. package/dist/core/llm/providers/url-secrets.js.map +1 -0
  184. package/dist/core/llm/streaming/StreamingBatcher.d.ts +4 -3
  185. package/dist/core/llm/streaming/StreamingBatcher.d.ts.map +1 -1
  186. package/dist/core/llm/streaming/StreamingBatcher.js +8 -3
  187. package/dist/core/llm/streaming/StreamingBatcher.js.map +1 -1
  188. package/dist/core/safety/LLMProviderHealthRegistry.d.ts +4 -0
  189. package/dist/core/safety/LLMProviderHealthRegistry.d.ts.map +1 -1
  190. package/dist/core/safety/LLMProviderHealthRegistry.js +26 -0
  191. package/dist/core/safety/LLMProviderHealthRegistry.js.map +1 -1
  192. package/dist/core/utils/sha256.d.ts +19 -0
  193. package/dist/core/utils/sha256.d.ts.map +1 -0
  194. package/dist/core/utils/sha256.js +101 -0
  195. package/dist/core/utils/sha256.js.map +1 -0
  196. package/dist/index.d.ts +1 -1
  197. package/dist/index.d.ts.map +1 -1
  198. package/dist/index.js +1 -1
  199. package/dist/index.js.map +1 -1
  200. package/dist/orchestration/pipeline/query/types.js +3 -3
  201. package/dist/orchestration/pipeline/query/types.js.map +1 -1
  202. package/dist/orchestration/planning/ProviderAssignmentEngine.js +2 -2
  203. package/dist/orchestration/planning/ProviderAssignmentEngine.js.map +1 -1
  204. package/package.json +1 -1
@@ -17,7 +17,46 @@
17
17
  * @implements {IPromptEngine}
18
18
  */
19
19
  import { ContextualElementType, PromptEngineError, } from './IPromptEngine.js';
20
+ import { resolveCacheCapabilities } from './providers/model-cache-capabilities.js';
20
21
  import { MessageRole } from '../conversation/ConversationMessage.js'; // Corrected import: Used alias and added MessageRole
22
+ import { sha256Hex } from '../utils/sha256.js';
23
+ /**
24
+ * Text of `value` for a prompt cache key, built by JSON.stringify so each
25
+ * value serializes as prompt construction's JSON copy sees it (a `toJSON`
26
+ * hook gets its property key; functions and undefined drop out). Three kinds
27
+ * of value that JSON cannot carry as-is become placeholders: a bigint, an
28
+ * object seen before (so a cycle cannot throw), and a typed array (hashed, so
29
+ * large audio or image bytes are not turned into text). Each placeholder is
30
+ * also listed after the JSON with the position it took, so an input string
31
+ * that looks like a placeholder never reads as one.
32
+ */
33
+ function cacheKeyText(value) {
34
+ const seen = new Map();
35
+ const placeholders = [];
36
+ let position = 0;
37
+ const text = JSON.stringify(value, (_key, current) => {
38
+ position += 1;
39
+ const placeholder = (description) => {
40
+ placeholders.push(`${position}:${description}`);
41
+ return `\u0000${description}`;
42
+ };
43
+ if (typeof current === 'bigint')
44
+ return placeholder(`bigint:${current}`);
45
+ if (current === null || typeof current !== 'object')
46
+ return current;
47
+ if (ArrayBuffer.isView(current)) {
48
+ const digest = sha256Hex(new Uint8Array(current.buffer, current.byteOffset, current.byteLength));
49
+ return placeholder(`bytes:${current.constructor?.name ?? 'view'}:${digest}`);
50
+ }
51
+ const order = seen.get(current);
52
+ if (order !== undefined)
53
+ return placeholder(`ref:${order}`);
54
+ seen.set(current, seen.size);
55
+ return current;
56
+ });
57
+ // JSON escapes control characters, so a raw \u0001 cannot occur in `text`.
58
+ return `${text ?? 'undefined'}\u0001${placeholders.join(',')}`;
59
+ }
21
60
  /**
22
61
  * Comprehensive implementation of the IPromptEngine interface, providing
23
62
  * sophisticated adaptive prompting capabilities for AgentOS GMIs.
@@ -32,6 +71,10 @@ export class PromptEngine {
32
71
  this.statistics = this.getInitialStatistics();
33
72
  this.defaultTemplates = {
34
73
  'openai_chat': this.createOpenAIChatTemplate(),
74
+ // GeminiProvider takes OpenAI-style ChatMessage[] and converts it to
75
+ // Gemini contents itself, so a caller that names this format gets the
76
+ // chat template rather than an empty prompt.
77
+ 'google_gemini': this.createOpenAIChatTemplate(),
35
78
  'anthropic_messages': this.createAnthropicMessagesTemplate(),
36
79
  'generic_completion': this.createGenericCompletionTemplate(),
37
80
  };
@@ -110,6 +153,10 @@ export class PromptEngine {
110
153
  this.statistics.cacheMisses++;
111
154
  }
112
155
  try {
156
+ // Copied now, in the same step as the cache key and before the first
157
+ // await, so the key and the prompt describe the same input even when
158
+ // the caller changes its components object while this call runs.
159
+ const components = JSON.parse(JSON.stringify(baseComponents));
113
160
  let selectedElements = [];
114
161
  // Corrected: use contextualPromptElements directly from IPersonaDefinition
115
162
  if (executionContext?.activePersona?.contextualPromptElements) {
@@ -128,7 +175,7 @@ export class PromptEngine {
128
175
  this.recordPerformanceTimer(timerId, Date.now() - constructionStart); // Pass actual duration
129
176
  selectedElements.forEach(el => this.statistics.contextualElementSelections[el.id || el.type] = (this.statistics.contextualElementSelections[el.id || el.type] || 0) + 1);
130
177
  }
131
- const augmentedComponents = this.augmentBaseComponents(baseComponents, selectedElements, executionContext);
178
+ const augmentedComponents = this.augmentBaseComponents(components, selectedElements, executionContext);
132
179
  const timerIdBudget = 'tokenBudgeting';
133
180
  const budgetStart = Date.now();
134
181
  this.startPerformanceTimer(timerIdBudget);
@@ -259,7 +306,7 @@ export class PromptEngine {
259
306
  if (components.userInput)
260
307
  estimatedTokens += await this.estimateTokenCount(components.userInput, modelTargetInfo.modelId);
261
308
  if (components.conversationHistory)
262
- estimatedTokens += await this.calculateTokensForMessages(components.conversationHistory, modelTargetInfo.modelId); // Corrected: Call fixed calculateTokensForMessages
309
+ estimatedTokens += await this.calculateTokensForMessages(components.conversationHistory, modelTargetInfo.modelId, this.thinkingSentFor(components.conversationHistory, modelTargetInfo));
263
310
  if (estimatedTokens > modelTargetInfo.maxContextTokens) {
264
311
  issues.push({ type: 'warning', code: 'POTENTIAL_TOKEN_OVERFLOW', message: `Estimated initial token count (${estimatedTokens}) exceeds model's max context (${modelTargetInfo.maxContextTokens}). Relying on truncation/summarization.`, component: 'OverallPrompt' });
265
312
  }
@@ -347,40 +394,56 @@ export class PromptEngine {
347
394
  throw new PromptEngineError('Invalid `tokenCounting` strategy in configuration.', 'INVALID_CONFIG_PARAM', 'Initialization');
348
395
  }
349
396
  }
397
+ /**
398
+ * Builds the key a constructed prompt is cached under. A cached result is
399
+ * served to any later call with the same key, so the key covers everything
400
+ * that shapes the prompt: every component (the whole history, retrieved and
401
+ * memory context, tools), the model target, the template, and the
402
+ * execution context that templates and contextual elements read (the
403
+ * persona definition, mood, task, skill level, language, signals,
404
+ * preferences). The working memory handle is left out, since contextual
405
+ * criteria do not query it. The key is a SHA-256 digest, so it carries none
406
+ * of that content into logs.
407
+ */
350
408
  generateCacheKey(components, modelInfo, executionContext, templateName) {
351
- /**
352
- * Generates a stable cache key for a prospective prompt construction result.
353
- * Key Composition Strategy (intentional truncation for privacy & size):
354
- * - First 50 chars of each system prompt concatenated (order sensitive)
355
- * - First 100 chars of user input
356
- * - Last turn content excerpt (first 50 chars) for light history sensitivity
357
- * - Tool id list (joined)
358
- * - Model id, template name, persona id, mood, task hint
359
- *
360
- * Hashing: Simple 32‑bit additive hash -> base36 to keep key short; collision risk acceptable for cache.
361
- *
362
- * NOTE: Omits retrievedContext & full history intentionally to avoid large keys and leaking RAG content into logs.
363
- */
364
- const relevantData = {
365
- system: components.systemPrompts?.map(p => p.content.substring(0, 50)).join(';'),
366
- userInput: components.userInput?.substring(0, 100),
367
- historyLastTurn: components.conversationHistory?.[components.conversationHistory.length - 1]?.content?.toString().substring(0, 50),
368
- tools: components.tools?.map(t => t.id).join(','),
369
- modelId: modelInfo.modelId,
370
- template: templateName,
371
- personaId: executionContext?.activePersona.id,
372
- mood: executionContext?.currentMood,
373
- task: executionContext?.taskHint,
374
- userPreferences: this.buildUserPreferenceCacheKey(executionContext?.userPreferences),
375
- };
376
- const keyString = Object.values(relevantData).filter(v => v !== undefined).join('||');
377
- let hash = 0;
378
- for (let i = 0; i < keyString.length; i++) {
379
- const char = keyString.charCodeAt(i);
380
- hash = ((hash << 5) - hash) + char;
381
- hash |= 0;
382
- }
383
- return `promptcache:${modelInfo.modelId}:${hash.toString(36)}`;
409
+ const { workingMemory: _workingMemory, ...context } = executionContext ?? {};
410
+ const persona = executionContext?.activePersona;
411
+ // The components serialize at the root and as given, as prompt
412
+ // construction's JSON copy serializes them, so a toJSON hook or a getter
413
+ // it reads sees the same object and key.
414
+ const parts = [
415
+ cacheKeyText(components),
416
+ // A tool class may define its name and schema as getters, which JSON
417
+ // serialization of own properties would miss.
418
+ cacheKeyText(components.tools?.map((tool) => ({
419
+ id: tool.id,
420
+ name: tool.name,
421
+ description: tool.description,
422
+ inputSchema: tool.inputSchema,
423
+ }))),
424
+ cacheKeyText({
425
+ modelInfo,
426
+ templateName,
427
+ context,
428
+ // Read the way prompt construction reads them, so values a context
429
+ // or persona object holds on its prototype still count.
430
+ read: {
431
+ personaId: persona?.id,
432
+ contextualPromptElements: persona?.contextualPromptElements,
433
+ preferencePrompts: this.buildUserPreferencePrompts(executionContext),
434
+ currentMood: executionContext?.currentMood,
435
+ userSkillLevel: executionContext?.userSkillLevel,
436
+ taskHint: executionContext?.taskHint,
437
+ taskComplexity: executionContext?.taskComplexity,
438
+ language: executionContext?.language,
439
+ conversationSignals: executionContext?.conversationSignals,
440
+ },
441
+ }),
442
+ ];
443
+ // Hashed synchronously: constructPrompt copies the components in the same
444
+ // step, so the key and the prompt describe the same input.
445
+ const digest = sha256Hex(parts.map((part) => `${part.length}:${part}`).join(''));
446
+ return `promptcache:${modelInfo.modelId}:${digest}`;
384
447
  }
385
448
  setupCacheEviction() {
386
449
  /**
@@ -390,7 +453,7 @@ export class PromptEngine {
390
453
  * No size‑based LRU yet; maxCacheSizeBytes reserved for future implementation.
391
454
  */
392
455
  const interval = (this.config.performance.cacheTimeoutSeconds / 2) * 1000;
393
- setInterval(() => {
456
+ const timer = setInterval(() => {
394
457
  const now = Date.now();
395
458
  for (const [key, entry] of this.cache.entries()) {
396
459
  if ((now - entry.timestamp) > this.config.performance.cacheTimeoutSeconds * 1000) {
@@ -398,6 +461,8 @@ export class PromptEngine {
398
461
  }
399
462
  }
400
463
  }, Math.max(interval, 60000));
464
+ // Eviction is housekeeping; it must not keep the process alive.
465
+ timer.unref?.();
401
466
  }
402
467
  startPerformanceTimer(timerId) {
403
468
  if (!this.statistics.performanceTimers[timerId]) {
@@ -415,9 +480,9 @@ export class PromptEngine {
415
480
  // averageTimeMs will be calculated in getEngineStatistics to avoid repetitive division
416
481
  }
417
482
  }
418
- augmentBaseComponents(base, selectedElements, executionContext) {
483
+ augmentBaseComponents(augmented, selectedElements, executionContext) {
419
484
  /**
420
- * Applies selected contextual elements onto the immutable base components producing a mutable augmented copy.
485
+ * Applies selected contextual elements onto `augmented`, the copy of the base components constructPrompt owns, and returns it.
421
486
  * Merging Rules:
422
487
  * - System prompt augmenters append as new system prompts with synthetic source tag.
423
488
  * - Few‑shot examples accumulate under customComponents.fewShotExamples.
@@ -425,7 +490,6 @@ export class PromptEngine {
425
490
  * - All other element types fall back to a dynamic bucket keyed by normalized type.
426
491
  * Sorting: Final systemPrompts sorted ascending by priority to preserve intended ordering.
427
492
  */
428
- const augmented = JSON.parse(JSON.stringify(base));
429
493
  if (!augmented.systemPrompts)
430
494
  augmented.systemPrompts = [];
431
495
  if (!augmented.customComponents)
@@ -536,20 +600,6 @@ export class PromptEngine {
536
600
  }
537
601
  return undefined;
538
602
  }
539
- buildUserPreferenceCacheKey(preferences) {
540
- if (!preferences || typeof preferences !== 'object') {
541
- return undefined;
542
- }
543
- const verbosity = this.normalizeVerbosityPreference(preferences.verbosity);
544
- const preferredFormat = this.normalizePreferredFormatPreference(preferences);
545
- if (!verbosity && !preferredFormat) {
546
- return undefined;
547
- }
548
- return JSON.stringify({
549
- ...(verbosity ? { verbosity } : {}),
550
- ...(preferredFormat ? { preferredFormat } : {}),
551
- });
552
- }
553
603
  async applyTokenBudget(components, modelInfo, issues) {
554
604
  /**
555
605
  * Enforces token budget across prompt sections (system, userInput, history, RAG context, tools).
@@ -565,7 +615,8 @@ export class PromptEngine {
565
615
  details: { truncatedComponents: [], summarizedComponents: [], removedComponents: [], originalEstimatedTokenCount: 0 }
566
616
  };
567
617
  const budget = modelInfo.optimalContextTokens || modelInfo.maxContextTokens;
568
- let currentTokens = await this.calculateTotalTokens(optimized, modelInfo.modelId);
618
+ const sendsThinking = this.thinkingSentFor(optimized.conversationHistory, modelInfo);
619
+ let currentTokens = await this.calculateTotalTokens(optimized, modelInfo.modelId, sendsThinking);
569
620
  modifications.details.originalEstimatedTokenCount = currentTokens;
570
621
  const budgets = {
571
622
  system: 0.20 * budget,
@@ -575,38 +626,59 @@ export class PromptEngine {
575
626
  tools: 0.10 * budget,
576
627
  };
577
628
  if (optimized.conversationHistory && optimized.conversationHistory.length > 0) {
578
- let historyTokens = await this.calculateTokensForMessages(optimized.conversationHistory, modelInfo.modelId);
629
+ const historyTokens = await this.calculateTokensForMessages(optimized.conversationHistory, modelInfo.modelId, sendsThinking);
579
630
  if (historyTokens > budgets.history || currentTokens > budget) {
580
- const originalCount = optimized.conversationHistory.length;
581
- if (this.utilityAI && this.config.historyManagement.summarizationTriggerRatio > 0 && (historyTokens / budgets.history > this.config.historyManagement.summarizationTriggerRatio)) {
582
- try {
583
- const { summaryMessages, finalTokenCount } = await this.utilityAI.summarizeConversationHistory(optimized.conversationHistory, (currentTokens > budget) ? budgets.history - (currentTokens - budget) : budgets.history, modelInfo, this.config.historyManagement.preserveImportantMessages);
584
- optimized.conversationHistory = summaryMessages;
585
- modifications.details.summarizedComponents.push('conversationHistory');
586
- modifications.wasModified = true;
587
- currentTokens = currentTokens - historyTokens + finalTokenCount;
588
- historyTokens = finalTokenCount;
589
- }
590
- catch (e) {
591
- issues?.push({ type: 'warning', code: 'HISTORY_SUMMARIZATION_FAILED', message: `History summarization failed: ${e instanceof Error ? e.message : String(e)}` });
592
- optimized.conversationHistory = this.truncateMessages(optimized.conversationHistory, budgets.history, (content) => this.estimateTokenCount(content, modelInfo.modelId));
593
- modifications.details.truncatedComponents.push('conversationHistory');
594
- modifications.wasModified = true;
595
- const newHistoryTokens = await this.calculateTokensForMessages(optimized.conversationHistory, modelInfo.modelId);
596
- currentTokens = currentTokens - historyTokens + newHistoryTokens;
597
- historyTokens = newHistoryTokens;
598
- }
631
+ // The turn being answered stays whole: summarizing or truncating it
632
+ // would drop the request itself (a structured user message has no
633
+ // userInput copy) or split a tool call from its result. Only the
634
+ // messages before it are reduced, to what the budget leaves them.
635
+ const history = optimized.conversationHistory;
636
+ const turnCount = Math.min(Math.max(0, Math.floor(optimized.currentTurnMessageCount ?? 0)), history.length);
637
+ const turnMessages = history.slice(history.length - turnCount);
638
+ const earlier = history.slice(0, history.length - turnCount);
639
+ const turnTokens = await this.calculateTokensForMessages(turnMessages, modelInfo.modelId, sendsThinking);
640
+ const earlierTokens = historyTokens - turnTokens;
641
+ // The earlier history keeps its share of the history budget, and no
642
+ // more than fits beside everything else in the prompt.
643
+ const earlierBudget = Math.max(0, budgets.history - turnTokens);
644
+ const earlierTarget = Math.max(0, Math.min(earlierBudget, budget - (currentTokens - earlierTokens)));
645
+ // When the turn is kept, or the request travels as userInput, there
646
+ // is always a message to send, so the earlier history may go
647
+ // entirely.
648
+ const minEarlierKept = turnMessages.length > 0 || optimized.userInput ? 0 : 1;
649
+ let reducedEarlier = earlier;
650
+ if (earlier.length > 0 && earlierTarget <= 0 && minEarlierKept === 0) {
651
+ // Nothing earlier fits; a summary would still add a message.
652
+ reducedEarlier = [];
653
+ modifications.details.truncatedComponents.push('conversationHistory');
654
+ modifications.wasModified = true;
599
655
  }
600
- else {
601
- optimized.conversationHistory = this.truncateMessages(optimized.conversationHistory, budgets.history, (content) => this.estimateTokenCount(content, modelInfo.modelId));
602
- if (optimized.conversationHistory.length < originalCount) {
603
- modifications.details.truncatedComponents.push('conversationHistory');
604
- modifications.wasModified = true;
656
+ else if (earlier.length > 0) {
657
+ if (this.utilityAI && this.config.historyManagement.summarizationTriggerRatio > 0 && (historyTokens / budgets.history > this.config.historyManagement.summarizationTriggerRatio)) {
658
+ try {
659
+ const { summaryMessages } = await this.utilityAI.summarizeConversationHistory(earlier, earlierTarget, modelInfo, this.config.historyManagement.preserveImportantMessages);
660
+ reducedEarlier = summaryMessages;
661
+ modifications.details.summarizedComponents.push('conversationHistory');
662
+ modifications.wasModified = true;
663
+ }
664
+ catch (e) {
665
+ issues?.push({ type: 'warning', code: 'HISTORY_SUMMARIZATION_FAILED', message: `History summarization failed: ${e instanceof Error ? e.message : String(e)}` });
666
+ reducedEarlier = this.truncateMessages([...earlier], earlierTarget, (content) => this.estimateTokenCount(content, modelInfo.modelId), minEarlierKept, sendsThinking);
667
+ modifications.details.truncatedComponents.push('conversationHistory');
668
+ modifications.wasModified = true;
669
+ }
670
+ }
671
+ else {
672
+ reducedEarlier = this.truncateMessages([...earlier], earlierTarget, (content) => this.estimateTokenCount(content, modelInfo.modelId), minEarlierKept, sendsThinking);
673
+ if (reducedEarlier.length < earlier.length) {
674
+ modifications.details.truncatedComponents.push('conversationHistory');
675
+ modifications.wasModified = true;
676
+ }
605
677
  }
606
- const newHistoryTokens = await this.calculateTokensForMessages(optimized.conversationHistory, modelInfo.modelId);
607
- currentTokens = currentTokens - historyTokens + newHistoryTokens;
608
- historyTokens = newHistoryTokens;
609
678
  }
679
+ optimized.conversationHistory = [...reducedEarlier, ...turnMessages];
680
+ const newEarlierTokens = await this.calculateTokensForMessages(reducedEarlier, modelInfo.modelId, sendsThinking);
681
+ currentTokens = currentTokens - earlierTokens + newEarlierTokens;
610
682
  }
611
683
  }
612
684
  if (optimized.retrievedContext) {
@@ -647,14 +719,14 @@ export class PromptEngine {
647
719
  }
648
720
  return { optimizedComponents: optimized, modifications };
649
721
  }
650
- async calculateTotalTokens(components, modelId) {
722
+ async calculateTotalTokens(components, modelId, sendsThinking = () => false) {
651
723
  let total = 0;
652
724
  if (components.systemPrompts)
653
725
  total += (await Promise.all(components.systemPrompts.map(sp => this.estimateTokenCount(sp.content, modelId)))).reduce((a, b) => a + b, 0);
654
726
  if (components.userInput)
655
727
  total += await this.estimateTokenCount(components.userInput, modelId);
656
728
  if (components.conversationHistory)
657
- total += await this.calculateTokensForMessages(components.conversationHistory, modelId);
729
+ total += await this.calculateTokensForMessages(components.conversationHistory, modelId, sendsThinking);
658
730
  if (components.retrievedContext) {
659
731
  const contextStr = typeof components.retrievedContext === 'string' ? components.retrievedContext : components.retrievedContext.map(r => r.content).join('\n');
660
732
  total += await this.estimateTokenCount(contextStr, modelId);
@@ -663,7 +735,37 @@ export class PromptEngine {
663
735
  total += components.tools.length * 50;
664
736
  return total;
665
737
  }
666
- async calculateTokensForMessages(messages, modelId) {
738
+ /**
739
+ * Tells which history messages' thinking the target model receives. Only
740
+ * Anthropic sends thinking back: all of it to models that keep prior
741
+ * thinking in context, otherwise only the latest assistant turn's. Other
742
+ * providers drop it. AnthropicProvider's
743
+ * `AGENTOS_ANTHROPIC_STRIP_PRIOR_THINKING` override (`1`/`true` strips,
744
+ * `0`/`false` replays) wins over the model's retention, as it does there.
745
+ *
746
+ * @param history The history the prompt is built from.
747
+ * @param modelInfo The target model.
748
+ * @returns Whether a message's thinking blocks reach the model.
749
+ */
750
+ thinkingSentFor(history, modelInfo) {
751
+ if (modelInfo.providerId !== 'anthropic' || !history?.length)
752
+ return () => false;
753
+ const stripEnv = process.env.AGENTOS_ANTHROPIC_STRIP_PRIOR_THINKING;
754
+ const replaysAll = stripEnv === '1' || stripEnv === 'true'
755
+ ? false
756
+ : stripEnv === '0' || stripEnv === 'false'
757
+ ? true
758
+ : resolveCacheCapabilities(modelInfo.modelId).retainsPriorThinkingInContext;
759
+ if (replaysAll)
760
+ return () => true;
761
+ let lastAssistant;
762
+ for (const msg of history) {
763
+ if (msg.role === MessageRole.ASSISTANT)
764
+ lastAssistant = msg;
765
+ }
766
+ return (msg) => msg === lastAssistant;
767
+ }
768
+ async calculateTokensForMessages(messages, modelId, sendsThinking = () => false) {
667
769
  if (!messages)
668
770
  return 0;
669
771
  let sum = 0;
@@ -691,15 +793,42 @@ export class PromptEngine {
691
793
  // This case should not be common if msg.content is Array for multi-part, but as a fallback:
692
794
  sum += await this.estimateTokenCount(JSON.stringify(msg.content), modelId);
693
795
  }
796
+ // Thinking the target receives with the message is input too.
797
+ if (sendsThinking(msg)) {
798
+ for (const block of msg.thinkingBlocks ?? []) {
799
+ sum += await this.estimateTokenCount(block.type === 'thinking' ? block.thinking : block.data, modelId);
800
+ }
801
+ }
694
802
  sum += 5; // Overhead for role, name, etc.
695
803
  }
696
804
  return sum;
697
805
  }
698
- truncateMessages(messages, targetTokenCount, _estimateFn) {
699
- let currentTokens = messages.reduce((sum, msg) => sum + (typeof msg.content === 'string' ? msg.content.length : (Array.isArray(msg.content) ? JSON.stringify(msg.content).length : 50)), 0) / 4; // More robust rough estimate
700
- while (currentTokens > targetTokenCount && messages.length > 1) {
701
- const removedMsg = messages.shift();
702
- currentTokens -= (typeof removedMsg?.content === 'string' ? removedMsg.content.length : (Array.isArray(removedMsg?.content) ? JSON.stringify(removedMsg?.content).length : 50)) / 4;
806
+ /**
807
+ * Drops the oldest messages until the rest fit `targetTokenCount`, keeping
808
+ * at least `minKept`. A tool result left first would reach the provider
809
+ * without the assistant call it answers, which chat APIs reject, so leading
810
+ * tool results go too.
811
+ */
812
+ truncateMessages(messages, targetTokenCount, _estimateFn, minKept = 1, sendsThinking = () => false) {
813
+ // Rough estimate: four characters a token, over the content and any
814
+ // thinking the target receives with the message.
815
+ const roughTokens = (msg) => {
816
+ if (!msg)
817
+ return 0;
818
+ const contentChars = typeof msg.content === 'string'
819
+ ? msg.content.length
820
+ : (Array.isArray(msg.content) ? JSON.stringify(msg.content).length : 50);
821
+ const thinkingChars = sendsThinking(msg)
822
+ ? (msg.thinkingBlocks ?? []).reduce((chars, block) => chars + (block.type === 'thinking' ? block.thinking.length : block.data.length), 0)
823
+ : 0;
824
+ return (contentChars + thinkingChars) / 4;
825
+ };
826
+ let currentTokens = messages.reduce((sum, msg) => sum + roughTokens(msg), 0);
827
+ while (currentTokens > targetTokenCount && messages.length > minKept) {
828
+ currentTokens -= roughTokens(messages.shift());
829
+ }
830
+ while (messages.length > minKept && messages[0]?.role === MessageRole.TOOL) {
831
+ messages.shift();
703
832
  }
704
833
  return messages;
705
834
  }
@@ -854,6 +983,11 @@ export class PromptEngine {
854
983
  chatMsg.content = null;
855
984
  }
856
985
  }
986
+ // Anthropic rejects a tool-loop request whose earlier assistant
987
+ // turn lost its signed thinking, so the blocks ride along verbatim.
988
+ if (role === 'assistant' && msg.thinkingBlocks && msg.thinkingBlocks.length > 0) {
989
+ chatMsg.thinkingBlocks = [...msg.thinkingBlocks];
990
+ }
857
991
  messages.push(chatMsg);
858
992
  });
859
993
  }
@@ -876,8 +1010,11 @@ export class PromptEngine {
876
1010
  const ragContent = typeof components.retrievedContext === 'string' ? components.retrievedContext : components.retrievedContext.map(r => `Source: ${r.source}\nContent: ${r.content}`).join('\n\n');
877
1011
  if (messages.length > 0 && messages[messages.length - 1].role === 'user') {
878
1012
  const lastUserMsg = messages[messages.length - 1];
879
- const newUserContent = `Context:\n${ragContent}\n\nUser Query: ${lastUserMsg.content}`;
880
- messages[messages.length - 1].content = newUserContent;
1013
+ // A multimodal message keeps its parts: the context goes in front
1014
+ // as a text part instead of flattening the parts into a string.
1015
+ lastUserMsg.content = Array.isArray(lastUserMsg.content)
1016
+ ? [{ type: 'text', text: `Context:\n${ragContent}\n\nUser Query:` }, ...lastUserMsg.content]
1017
+ : `Context:\n${ragContent}\n\nUser Query: ${lastUserMsg.content}`;
881
1018
  }
882
1019
  else {
883
1020
  messages.push({ role: 'user', content: `Based on the following context:\n${ragContent}\n\nPlease respond to the implicit or explicit user query.` });