camel-ai 0.2.82__py3-none-any.whl → 0.2.83a6__py3-none-any.whl

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.

Potentially problematic release.


This version of camel-ai might be problematic. Click here for more details.

Files changed (481) hide show
  1. camel/__init__.py +3 -3
  2. camel/agents/__init__.py +2 -2
  3. camel/agents/_types.py +2 -2
  4. camel/agents/_utils.py +2 -2
  5. camel/agents/base.py +2 -2
  6. camel/agents/chat_agent.py +765 -541
  7. camel/agents/critic_agent.py +2 -2
  8. camel/agents/deductive_reasoner_agent.py +2 -2
  9. camel/agents/embodied_agent.py +2 -2
  10. camel/agents/knowledge_graph_agent.py +2 -2
  11. camel/agents/mcp_agent.py +2 -2
  12. camel/agents/multi_hop_generator_agent.py +2 -2
  13. camel/agents/programmed_agent_instruction.py +2 -2
  14. camel/agents/repo_agent.py +2 -2
  15. camel/agents/role_assignment_agent.py +2 -2
  16. camel/agents/search_agent.py +2 -2
  17. camel/agents/task_agent.py +2 -2
  18. camel/agents/tool_agents/__init__.py +2 -2
  19. camel/agents/tool_agents/base.py +2 -2
  20. camel/agents/tool_agents/hugging_face_tool_agent.py +2 -2
  21. camel/benchmarks/__init__.py +2 -2
  22. camel/benchmarks/apibank.py +2 -2
  23. camel/benchmarks/apibench.py +2 -2
  24. camel/benchmarks/base.py +2 -2
  25. camel/benchmarks/browsecomp.py +2 -2
  26. camel/benchmarks/gaia.py +2 -2
  27. camel/benchmarks/mock_website/mock_web.py +2 -2
  28. camel/benchmarks/mock_website/shopping_mall/app.py +2 -2
  29. camel/benchmarks/nexus.py +2 -2
  30. camel/benchmarks/ragbench.py +2 -2
  31. camel/bots/__init__.py +2 -2
  32. camel/bots/discord/__init__.py +2 -2
  33. camel/bots/discord/discord_app.py +2 -2
  34. camel/bots/discord/discord_installation.py +2 -2
  35. camel/bots/discord/discord_store.py +2 -2
  36. camel/bots/slack/__init__.py +2 -2
  37. camel/bots/slack/models.py +2 -2
  38. camel/bots/slack/slack_app.py +2 -2
  39. camel/bots/telegram_bot.py +2 -2
  40. camel/configs/__init__.py +8 -2
  41. camel/configs/aihubmix_config.py +2 -2
  42. camel/configs/aiml_config.py +2 -2
  43. camel/configs/amd_config.py +2 -2
  44. camel/configs/anthropic_config.py +2 -2
  45. camel/configs/base_config.py +2 -2
  46. camel/configs/bedrock_config.py +2 -2
  47. camel/configs/cerebras_config.py +2 -2
  48. camel/configs/cohere_config.py +2 -2
  49. camel/configs/cometapi_config.py +2 -2
  50. camel/configs/crynux_config.py +2 -2
  51. camel/configs/deepseek_config.py +2 -2
  52. camel/configs/function_gemma_config.py +59 -0
  53. camel/configs/gemini_config.py +2 -2
  54. camel/configs/groq_config.py +2 -2
  55. camel/configs/internlm_config.py +2 -2
  56. camel/configs/litellm_config.py +2 -2
  57. camel/configs/lmstudio_config.py +2 -2
  58. camel/configs/minimax_config.py +2 -2
  59. camel/configs/mistral_config.py +2 -2
  60. camel/configs/modelscope_config.py +2 -2
  61. camel/configs/moonshot_config.py +2 -2
  62. camel/configs/nebius_config.py +2 -2
  63. camel/configs/netmind_config.py +2 -2
  64. camel/configs/novita_config.py +2 -2
  65. camel/configs/nvidia_config.py +2 -2
  66. camel/configs/ollama_config.py +2 -2
  67. camel/configs/openai_config.py +2 -2
  68. camel/configs/openrouter_config.py +2 -2
  69. camel/configs/ppio_config.py +2 -2
  70. camel/configs/qianfan_config.py +2 -2
  71. camel/configs/qwen_config.py +2 -2
  72. camel/configs/reka_config.py +2 -2
  73. camel/configs/samba_config.py +2 -2
  74. camel/configs/sglang_config.py +2 -2
  75. camel/configs/siliconflow_config.py +2 -2
  76. camel/configs/togetherai_config.py +2 -2
  77. camel/configs/vllm_config.py +2 -2
  78. camel/configs/watsonx_config.py +2 -2
  79. camel/configs/yi_config.py +2 -2
  80. camel/configs/zhipuai_config.py +2 -2
  81. camel/data_collectors/__init__.py +2 -2
  82. camel/data_collectors/alpaca_collector.py +2 -2
  83. camel/data_collectors/base.py +2 -2
  84. camel/data_collectors/sharegpt_collector.py +2 -2
  85. camel/datagen/__init__.py +2 -2
  86. camel/datagen/cot_datagen.py +2 -2
  87. camel/datagen/evol_instruct/__init__.py +2 -2
  88. camel/datagen/evol_instruct/evol_instruct.py +2 -2
  89. camel/datagen/evol_instruct/scorer.py +2 -2
  90. camel/datagen/evol_instruct/templates.py +2 -2
  91. camel/datagen/self_improving_cot.py +2 -2
  92. camel/datagen/self_instruct/__init__.py +2 -2
  93. camel/datagen/self_instruct/filter/__init__.py +2 -2
  94. camel/datagen/self_instruct/filter/filter_function.py +2 -2
  95. camel/datagen/self_instruct/filter/filter_registry.py +2 -2
  96. camel/datagen/self_instruct/filter/instruction_filter.py +2 -2
  97. camel/datagen/self_instruct/self_instruct.py +2 -2
  98. camel/datagen/self_instruct/templates.py +2 -2
  99. camel/datagen/source2synth/__init__.py +2 -2
  100. camel/datagen/source2synth/data_processor.py +2 -2
  101. camel/datagen/source2synth/models.py +2 -2
  102. camel/datagen/source2synth/user_data_processor_config.py +2 -2
  103. camel/datahubs/__init__.py +2 -2
  104. camel/datahubs/base.py +2 -2
  105. camel/datahubs/huggingface.py +2 -2
  106. camel/datahubs/models.py +2 -2
  107. camel/datasets/__init__.py +2 -2
  108. camel/datasets/base_generator.py +2 -2
  109. camel/datasets/few_shot_generator.py +2 -2
  110. camel/datasets/models.py +2 -2
  111. camel/datasets/self_instruct_generator.py +2 -2
  112. camel/datasets/static_dataset.py +2 -2
  113. camel/embeddings/__init__.py +2 -2
  114. camel/embeddings/azure_embedding.py +2 -2
  115. camel/embeddings/base.py +2 -2
  116. camel/embeddings/gemini_embedding.py +2 -2
  117. camel/embeddings/jina_embedding.py +2 -2
  118. camel/embeddings/mistral_embedding.py +2 -2
  119. camel/embeddings/openai_compatible_embedding.py +2 -2
  120. camel/embeddings/openai_embedding.py +2 -2
  121. camel/embeddings/sentence_transformers_embeddings.py +2 -2
  122. camel/embeddings/together_embedding.py +2 -2
  123. camel/embeddings/vlm_embedding.py +2 -2
  124. camel/environments/__init__.py +2 -2
  125. camel/environments/models.py +2 -2
  126. camel/environments/multi_step.py +2 -2
  127. camel/environments/rlcards_env.py +2 -2
  128. camel/environments/single_step.py +2 -2
  129. camel/environments/tic_tac_toe.py +2 -2
  130. camel/extractors/__init__.py +2 -2
  131. camel/extractors/base.py +2 -2
  132. camel/extractors/python_strategies.py +2 -2
  133. camel/generators.py +2 -2
  134. camel/human.py +2 -2
  135. camel/interpreters/__init__.py +2 -2
  136. camel/interpreters/base.py +2 -2
  137. camel/interpreters/docker_interpreter.py +2 -2
  138. camel/interpreters/e2b_interpreter.py +2 -2
  139. camel/interpreters/internal_python_interpreter.py +2 -2
  140. camel/interpreters/interpreter_error.py +2 -2
  141. camel/interpreters/ipython_interpreter.py +2 -2
  142. camel/interpreters/microsandbox_interpreter.py +2 -2
  143. camel/interpreters/subprocess_interpreter.py +2 -2
  144. camel/loaders/__init__.py +2 -2
  145. camel/loaders/apify_reader.py +2 -2
  146. camel/loaders/base_io.py +2 -2
  147. camel/loaders/base_loader.py +2 -2
  148. camel/loaders/chunkr_reader.py +2 -2
  149. camel/loaders/crawl4ai_reader.py +2 -2
  150. camel/loaders/firecrawl_reader.py +2 -2
  151. camel/loaders/jina_url_reader.py +2 -2
  152. camel/loaders/markitdown.py +2 -2
  153. camel/loaders/mineru_extractor.py +2 -2
  154. camel/loaders/mistral_reader.py +2 -2
  155. camel/loaders/scrapegraph_reader.py +2 -2
  156. camel/loaders/unstructured_io.py +2 -2
  157. camel/logger.py +2 -2
  158. camel/memories/__init__.py +2 -2
  159. camel/memories/agent_memories.py +2 -2
  160. camel/memories/base.py +2 -2
  161. camel/memories/blocks/__init__.py +2 -2
  162. camel/memories/blocks/chat_history_block.py +2 -2
  163. camel/memories/blocks/vectordb_block.py +2 -2
  164. camel/memories/context_creators/__init__.py +2 -2
  165. camel/memories/context_creators/score_based.py +89 -2
  166. camel/memories/records.py +2 -2
  167. camel/messages/__init__.py +2 -2
  168. camel/messages/base.py +2 -2
  169. camel/messages/conversion/__init__.py +2 -2
  170. camel/messages/conversion/alpaca.py +2 -2
  171. camel/messages/conversion/conversation_models.py +2 -2
  172. camel/messages/conversion/sharegpt/__init__.py +2 -2
  173. camel/messages/conversion/sharegpt/function_call_formatter.py +2 -2
  174. camel/messages/conversion/sharegpt/hermes/__init__.py +2 -2
  175. camel/messages/conversion/sharegpt/hermes/hermes_function_formatter.py +2 -2
  176. camel/messages/func_message.py +2 -2
  177. camel/models/__init__.py +4 -2
  178. camel/models/_utils.py +2 -2
  179. camel/models/aihubmix_model.py +2 -2
  180. camel/models/aiml_model.py +2 -2
  181. camel/models/amd_model.py +2 -2
  182. camel/models/anthropic_model.py +2 -2
  183. camel/models/aws_bedrock_model.py +2 -2
  184. camel/models/azure_openai_model.py +4 -28
  185. camel/models/base_audio_model.py +2 -2
  186. camel/models/base_model.py +192 -14
  187. camel/models/cerebras_model.py +2 -2
  188. camel/models/cohere_model.py +4 -30
  189. camel/models/cometapi_model.py +2 -2
  190. camel/models/crynux_model.py +2 -2
  191. camel/models/deepseek_model.py +4 -28
  192. camel/models/fish_audio_model.py +2 -2
  193. camel/models/function_gemma_model.py +889 -0
  194. camel/models/gemini_model.py +4 -28
  195. camel/models/groq_model.py +2 -2
  196. camel/models/internlm_model.py +2 -2
  197. camel/models/litellm_model.py +3 -17
  198. camel/models/lmstudio_model.py +2 -2
  199. camel/models/minimax_model.py +2 -2
  200. camel/models/mistral_model.py +4 -30
  201. camel/models/model_factory.py +4 -2
  202. camel/models/model_manager.py +2 -2
  203. camel/models/modelscope_model.py +2 -2
  204. camel/models/moonshot_model.py +3 -15
  205. camel/models/nebius_model.py +2 -2
  206. camel/models/nemotron_model.py +2 -2
  207. camel/models/netmind_model.py +2 -2
  208. camel/models/novita_model.py +2 -2
  209. camel/models/nvidia_model.py +2 -2
  210. camel/models/ollama_model.py +2 -2
  211. camel/models/openai_audio_models.py +2 -2
  212. camel/models/openai_compatible_model.py +4 -28
  213. camel/models/openai_model.py +4 -43
  214. camel/models/openrouter_model.py +2 -2
  215. camel/models/ppio_model.py +2 -2
  216. camel/models/qianfan_model.py +2 -2
  217. camel/models/qwen_model.py +2 -2
  218. camel/models/reka_model.py +4 -30
  219. camel/models/reward/__init__.py +2 -2
  220. camel/models/reward/base_reward_model.py +2 -2
  221. camel/models/reward/evaluator.py +2 -2
  222. camel/models/reward/nemotron_model.py +2 -2
  223. camel/models/reward/skywork_model.py +2 -2
  224. camel/models/samba_model.py +4 -30
  225. camel/models/sglang_model.py +4 -30
  226. camel/models/siliconflow_model.py +2 -2
  227. camel/models/stub_model.py +2 -2
  228. camel/models/togetherai_model.py +2 -2
  229. camel/models/vllm_model.py +2 -2
  230. camel/models/volcano_model.py +147 -4
  231. camel/models/watsonx_model.py +4 -30
  232. camel/models/yi_model.py +2 -2
  233. camel/models/zhipuai_model.py +2 -2
  234. camel/parsers/__init__.py +2 -2
  235. camel/parsers/mcp_tool_call_parser.py +2 -2
  236. camel/personas/__init__.py +2 -2
  237. camel/personas/persona.py +2 -2
  238. camel/personas/persona_hub.py +2 -2
  239. camel/prompts/__init__.py +2 -2
  240. camel/prompts/ai_society.py +2 -2
  241. camel/prompts/base.py +2 -2
  242. camel/prompts/code.py +2 -2
  243. camel/prompts/evaluation.py +2 -2
  244. camel/prompts/generate_text_embedding_data.py +2 -2
  245. camel/prompts/image_craft.py +2 -2
  246. camel/prompts/misalignment.py +2 -2
  247. camel/prompts/multi_condition_image_craft.py +2 -2
  248. camel/prompts/object_recognition.py +2 -2
  249. camel/prompts/persona_hub.py +2 -2
  250. camel/prompts/prompt_templates.py +2 -2
  251. camel/prompts/role_description_prompt_template.py +2 -2
  252. camel/prompts/solution_extraction.py +2 -2
  253. camel/prompts/task_prompt_template.py +2 -2
  254. camel/prompts/translation.py +2 -2
  255. camel/prompts/video_description_prompt.py +2 -2
  256. camel/responses/__init__.py +2 -2
  257. camel/responses/agent_responses.py +2 -2
  258. camel/retrievers/__init__.py +2 -2
  259. camel/retrievers/auto_retriever.py +2 -2
  260. camel/retrievers/base.py +2 -2
  261. camel/retrievers/bm25_retriever.py +2 -2
  262. camel/retrievers/cohere_rerank_retriever.py +2 -2
  263. camel/retrievers/hybrid_retrival.py +2 -2
  264. camel/retrievers/vector_retriever.py +2 -2
  265. camel/runtimes/__init__.py +2 -2
  266. camel/runtimes/api.py +2 -2
  267. camel/runtimes/base.py +2 -2
  268. camel/runtimes/configs.py +2 -2
  269. camel/runtimes/daytona_runtime.py +2 -2
  270. camel/runtimes/docker_runtime.py +2 -2
  271. camel/runtimes/llm_guard_runtime.py +2 -2
  272. camel/runtimes/remote_http_runtime.py +2 -2
  273. camel/runtimes/ubuntu_docker_runtime.py +2 -2
  274. camel/runtimes/utils/__init__.py +2 -2
  275. camel/runtimes/utils/function_risk_toolkit.py +2 -2
  276. camel/runtimes/utils/ignore_risk_toolkit.py +2 -2
  277. camel/schemas/__init__.py +2 -2
  278. camel/schemas/base.py +2 -2
  279. camel/schemas/openai_converter.py +2 -2
  280. camel/schemas/outlines_converter.py +2 -2
  281. camel/services/agent_openapi_server.py +2 -2
  282. camel/societies/__init__.py +2 -2
  283. camel/societies/babyagi_playing.py +2 -2
  284. camel/societies/role_playing.py +2 -2
  285. camel/societies/workforce/__init__.py +2 -2
  286. camel/societies/workforce/base.py +2 -2
  287. camel/societies/workforce/events.py +4 -2
  288. camel/societies/workforce/prompts.py +9 -8
  289. camel/societies/workforce/role_playing_worker.py +2 -2
  290. camel/societies/workforce/single_agent_worker.py +2 -2
  291. camel/societies/workforce/structured_output_handler.py +2 -2
  292. camel/societies/workforce/task_channel.py +2 -2
  293. camel/societies/workforce/utils.py +2 -2
  294. camel/societies/workforce/worker.py +2 -2
  295. camel/societies/workforce/workflow_memory_manager.py +2 -2
  296. camel/societies/workforce/workforce.py +132 -71
  297. camel/societies/workforce/workforce_callback.py +2 -2
  298. camel/societies/workforce/workforce_logger.py +2 -2
  299. camel/societies/workforce/workforce_metrics.py +2 -2
  300. camel/storages/__init__.py +2 -2
  301. camel/storages/graph_storages/__init__.py +2 -2
  302. camel/storages/graph_storages/base.py +2 -2
  303. camel/storages/graph_storages/graph_element.py +2 -2
  304. camel/storages/graph_storages/nebula_graph.py +2 -2
  305. camel/storages/graph_storages/neo4j_graph.py +2 -2
  306. camel/storages/key_value_storages/__init__.py +2 -2
  307. camel/storages/key_value_storages/base.py +2 -2
  308. camel/storages/key_value_storages/in_memory.py +2 -2
  309. camel/storages/key_value_storages/json.py +2 -2
  310. camel/storages/key_value_storages/mem0_cloud.py +2 -2
  311. camel/storages/key_value_storages/redis.py +2 -2
  312. camel/storages/object_storages/__init__.py +2 -2
  313. camel/storages/object_storages/amazon_s3.py +2 -2
  314. camel/storages/object_storages/azure_blob.py +2 -2
  315. camel/storages/object_storages/base.py +2 -2
  316. camel/storages/object_storages/google_cloud.py +2 -2
  317. camel/storages/vectordb_storages/__init__.py +2 -2
  318. camel/storages/vectordb_storages/base.py +2 -2
  319. camel/storages/vectordb_storages/chroma.py +2 -2
  320. camel/storages/vectordb_storages/faiss.py +2 -2
  321. camel/storages/vectordb_storages/milvus.py +2 -2
  322. camel/storages/vectordb_storages/oceanbase.py +2 -2
  323. camel/storages/vectordb_storages/pgvector.py +2 -2
  324. camel/storages/vectordb_storages/qdrant.py +2 -2
  325. camel/storages/vectordb_storages/surreal.py +2 -2
  326. camel/storages/vectordb_storages/tidb.py +2 -2
  327. camel/storages/vectordb_storages/weaviate.py +2 -2
  328. camel/tasks/__init__.py +2 -2
  329. camel/tasks/task.py +2 -2
  330. camel/tasks/task_prompt.py +2 -2
  331. camel/terminators/__init__.py +2 -2
  332. camel/terminators/base.py +2 -2
  333. camel/terminators/response_terminator.py +2 -2
  334. camel/terminators/token_limit_terminator.py +2 -2
  335. camel/toolkits/__init__.py +6 -3
  336. camel/toolkits/aci_toolkit.py +2 -2
  337. camel/toolkits/arxiv_toolkit.py +2 -2
  338. camel/toolkits/ask_news_toolkit.py +2 -2
  339. camel/toolkits/async_browser_toolkit.py +2 -2
  340. camel/toolkits/audio_analysis_toolkit.py +2 -2
  341. camel/toolkits/base.py +47 -5
  342. camel/toolkits/bohrium_toolkit.py +2 -2
  343. camel/toolkits/browser_toolkit.py +2 -2
  344. camel/toolkits/browser_toolkit_commons.py +2 -2
  345. camel/toolkits/code_execution.py +2 -2
  346. camel/toolkits/context_summarizer_toolkit.py +2 -2
  347. camel/toolkits/craw4ai_toolkit.py +2 -2
  348. camel/toolkits/dappier_toolkit.py +2 -2
  349. camel/toolkits/data_commons_toolkit.py +2 -2
  350. camel/toolkits/dingtalk.py +2 -2
  351. camel/toolkits/earth_science_toolkit.py +2 -2
  352. camel/toolkits/edgeone_pages_mcp_toolkit.py +2 -2
  353. camel/toolkits/excel_toolkit.py +2 -2
  354. camel/toolkits/file_toolkit.py +2 -2
  355. camel/toolkits/function_tool.py +95 -25
  356. camel/toolkits/github_toolkit.py +2 -2
  357. camel/toolkits/gmail_toolkit.py +2 -2
  358. camel/toolkits/google_calendar_toolkit.py +2 -2
  359. camel/toolkits/google_drive_mcp_toolkit.py +2 -2
  360. camel/toolkits/google_maps_toolkit.py +2 -2
  361. camel/toolkits/google_scholar_toolkit.py +2 -2
  362. camel/toolkits/human_toolkit.py +2 -2
  363. camel/toolkits/hybrid_browser_toolkit/__init__.py +2 -2
  364. camel/toolkits/hybrid_browser_toolkit/config_loader.py +2 -2
  365. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +2 -2
  366. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +89 -104
  367. camel/toolkits/hybrid_browser_toolkit/installer.py +2 -2
  368. camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +25 -14
  369. camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +6 -0
  370. camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +2 -2
  371. camel/toolkits/hybrid_browser_toolkit_py/__init__.py +2 -2
  372. camel/toolkits/hybrid_browser_toolkit_py/actions.py +2 -2
  373. camel/toolkits/hybrid_browser_toolkit_py/agent.py +2 -2
  374. camel/toolkits/hybrid_browser_toolkit_py/browser_session.py +2 -2
  375. camel/toolkits/hybrid_browser_toolkit_py/config_loader.py +2 -2
  376. camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +2 -2
  377. camel/toolkits/hybrid_browser_toolkit_py/snapshot.py +2 -2
  378. camel/toolkits/image_analysis_toolkit.py +2 -2
  379. camel/toolkits/image_generation_toolkit.py +2 -2
  380. camel/toolkits/jina_reranker_toolkit.py +2 -2
  381. camel/toolkits/klavis_toolkit.py +2 -2
  382. camel/toolkits/linkedin_toolkit.py +2 -2
  383. camel/toolkits/markitdown_toolkit.py +2 -2
  384. camel/toolkits/math_toolkit.py +2 -2
  385. camel/toolkits/mcp_toolkit.py +2 -2
  386. camel/toolkits/memory_toolkit.py +2 -2
  387. camel/toolkits/meshy_toolkit.py +2 -2
  388. camel/toolkits/message_agent_toolkit.py +2 -2
  389. camel/toolkits/message_integration.py +6 -2
  390. camel/toolkits/microsoft_outlook_mail_toolkit.py +1885 -0
  391. camel/toolkits/mineru_toolkit.py +2 -2
  392. camel/toolkits/minimax_mcp_toolkit.py +2 -2
  393. camel/toolkits/networkx_toolkit.py +2 -2
  394. camel/toolkits/note_taking_toolkit.py +2 -2
  395. camel/toolkits/notion_mcp_toolkit.py +2 -2
  396. camel/toolkits/notion_toolkit.py +2 -2
  397. camel/toolkits/open_api_specs/biztoc/__init__.py +2 -2
  398. camel/toolkits/open_api_specs/coursera/__init__.py +2 -2
  399. camel/toolkits/open_api_specs/create_qr_code/__init__.py +2 -2
  400. camel/toolkits/open_api_specs/klarna/__init__.py +2 -2
  401. camel/toolkits/open_api_specs/nasa_apod/__init__.py +2 -2
  402. camel/toolkits/open_api_specs/outschool/__init__.py +2 -2
  403. camel/toolkits/open_api_specs/outschool/paths/__init__.py +2 -2
  404. camel/toolkits/open_api_specs/outschool/paths/get_classes.py +2 -2
  405. camel/toolkits/open_api_specs/outschool/paths/search_teachers.py +2 -2
  406. camel/toolkits/open_api_specs/security_config.py +2 -2
  407. camel/toolkits/open_api_specs/speak/__init__.py +2 -2
  408. camel/toolkits/open_api_specs/web_scraper/__init__.py +2 -2
  409. camel/toolkits/open_api_specs/web_scraper/paths/__init__.py +2 -2
  410. camel/toolkits/open_api_specs/web_scraper/paths/scraper.py +2 -2
  411. camel/toolkits/open_api_toolkit.py +2 -2
  412. camel/toolkits/openbb_toolkit.py +2 -2
  413. camel/toolkits/origene_mcp_toolkit.py +2 -2
  414. camel/toolkits/playwright_mcp_toolkit.py +2 -2
  415. camel/toolkits/pptx_toolkit.py +2 -2
  416. camel/toolkits/pubmed_toolkit.py +2 -2
  417. camel/toolkits/pulse_mcp_search_toolkit.py +2 -2
  418. camel/toolkits/pyautogui_toolkit.py +2 -2
  419. camel/toolkits/reddit_toolkit.py +2 -2
  420. camel/toolkits/resend_toolkit.py +2 -2
  421. camel/toolkits/retrieval_toolkit.py +2 -2
  422. camel/toolkits/screenshot_toolkit.py +2 -2
  423. camel/toolkits/search_toolkit.py +70 -13
  424. camel/toolkits/searxng_toolkit.py +2 -2
  425. camel/toolkits/semantic_scholar_toolkit.py +2 -2
  426. camel/toolkits/slack_toolkit.py +2 -2
  427. camel/toolkits/sql_toolkit.py +2 -2
  428. camel/toolkits/stripe_toolkit.py +2 -2
  429. camel/toolkits/sympy_toolkit.py +2 -2
  430. camel/toolkits/task_planning_toolkit.py +2 -2
  431. camel/toolkits/terminal_toolkit/__init__.py +2 -2
  432. camel/toolkits/terminal_toolkit/terminal_toolkit.py +323 -112
  433. camel/toolkits/terminal_toolkit/utils.py +179 -52
  434. camel/toolkits/thinking_toolkit.py +2 -2
  435. camel/toolkits/twitter_toolkit.py +2 -2
  436. camel/toolkits/vertex_ai_veo_toolkit.py +2 -2
  437. camel/toolkits/video_analysis_toolkit.py +2 -2
  438. camel/toolkits/video_download_toolkit.py +2 -2
  439. camel/toolkits/weather_toolkit.py +2 -2
  440. camel/toolkits/web_deploy_toolkit.py +2 -2
  441. camel/toolkits/wechat_official_toolkit.py +2 -2
  442. camel/toolkits/whatsapp_toolkit.py +2 -2
  443. camel/toolkits/wolfram_alpha_toolkit.py +2 -2
  444. camel/toolkits/zapier_toolkit.py +2 -2
  445. camel/types/__init__.py +2 -2
  446. camel/types/agents/__init__.py +2 -2
  447. camel/types/agents/tool_calling_record.py +2 -2
  448. camel/types/enums.py +5 -4
  449. camel/types/mcp_registries.py +2 -2
  450. camel/types/openai_types.py +2 -2
  451. camel/types/unified_model_type.py +10 -6
  452. camel/utils/__init__.py +5 -2
  453. camel/utils/agent_context.py +41 -0
  454. camel/utils/async_func.py +2 -2
  455. camel/utils/chunker/__init__.py +2 -2
  456. camel/utils/chunker/base.py +2 -2
  457. camel/utils/chunker/code_chunker.py +2 -2
  458. camel/utils/chunker/uio_chunker.py +2 -2
  459. camel/utils/commons.py +2 -2
  460. camel/utils/constants.py +2 -2
  461. camel/utils/context_utils.py +2 -2
  462. camel/utils/deduplication.py +2 -2
  463. camel/utils/filename.py +2 -2
  464. camel/utils/langfuse.py +18 -10
  465. camel/utils/mcp.py +2 -2
  466. camel/utils/mcp_client.py +2 -2
  467. camel/utils/message_summarizer.py +2 -2
  468. camel/utils/response_format.py +2 -2
  469. camel/utils/token_counting.py +2 -2
  470. camel/utils/tool_result.py +2 -2
  471. camel/verifiers/__init__.py +2 -2
  472. camel/verifiers/base.py +2 -2
  473. camel/verifiers/math_verifier.py +2 -2
  474. camel/verifiers/models.py +2 -2
  475. camel/verifiers/physics_verifier.py +2 -2
  476. camel/verifiers/python_verifier.py +2 -2
  477. {camel_ai-0.2.82.dist-info → camel_ai-0.2.83a6.dist-info}/METADATA +34 -29
  478. camel_ai-0.2.83a6.dist-info/RECORD +511 -0
  479. camel_ai-0.2.82.dist-info/RECORD +0 -507
  480. {camel_ai-0.2.82.dist-info → camel_ai-0.2.83a6.dist-info}/WHEEL +0 -0
  481. {camel_ai-0.2.82.dist-info → camel_ai-0.2.83a6.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,4 @@
1
- # ========= Copyright 2023-2025 @ CAMEL-AI.org. All Rights Reserved. =========
1
+ # ========= Copyright 2023-2026 @ CAMEL-AI.org. All Rights Reserved. =========
2
2
  # Licensed under the Apache License, Version 2.0 (the "License");
3
3
  # you may not use this file except in compliance with the License.
4
4
  # You may obtain a copy of the License at
@@ -10,7 +10,7 @@
10
10
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
11
  # See the License for the specific language governing permissions and
12
12
  # limitations under the License.
13
- # ========= Copyright 2023-2025 @ CAMEL-AI.org. All Rights Reserved. =========
13
+ # ========= Copyright 2023-2026 @ CAMEL-AI.org. All Rights Reserved. =========
14
14
  from __future__ import annotations
15
15
 
16
16
  import asyncio
@@ -70,7 +70,6 @@ from camel.logger import get_logger
70
70
  from camel.memories import (
71
71
  AgentMemory,
72
72
  ChatHistoryMemory,
73
- ContextRecord,
74
73
  MemoryRecord,
75
74
  ScoreBasedContextCreator,
76
75
  )
@@ -105,16 +104,7 @@ from camel.utils import (
105
104
  )
106
105
  from camel.utils.commons import dependencies_required
107
106
  from camel.utils.context_utils import ContextUtility
108
-
109
- TOKEN_LIMIT_ERROR_MARKERS = (
110
- "context_length_exceeded",
111
- "prompt is too long",
112
- "exceeded your current quota",
113
- "tokens must be reduced",
114
- "context length",
115
- "token count",
116
- "context limit",
117
- )
107
+ from camel.utils.tool_result import ToolResult
118
108
 
119
109
  if TYPE_CHECKING:
120
110
  from camel.terminators import ResponseTerminator
@@ -397,6 +387,10 @@ class ChatAgent(BaseAgent):
397
387
  window that triggers summarization. If `None`, will trigger
398
388
  summarization when the context window is full.
399
389
  (default: :obj:`None`)
390
+ token_limit (int, optional): The maximum number of tokens allowed for
391
+ the context window. If `None`, uses the model's default token
392
+ limit. This can be used to restrict the context size below the
393
+ model's maximum capacity. (default: :obj:`None`)
400
394
  output_language (str, optional): The language to be output by the
401
395
  agent. (default: :obj:`None`)
402
396
  tools (Optional[List[Union[FunctionTool, Callable]]], optional): List
@@ -416,7 +410,10 @@ class ChatAgent(BaseAgent):
416
410
  directly return the request instead of processing it.
417
411
  (default: :obj:`None`)
418
412
  response_terminators (List[ResponseTerminator], optional): List of
419
- :obj:`ResponseTerminator` bind to one chat agent.
413
+ :obj:`ResponseTerminator` to check if task is complete. When set,
414
+ the agent will keep prompting the model until a terminator signals
415
+ completion. Note: You must define the termination signal (e.g.,
416
+ a keyword) in your system prompt so the model knows what to output.
420
417
  (default: :obj:`None`)
421
418
  scheduling_strategy (str): name of function that defines how to select
422
419
  the next model in ModelManager. (default: :str:`round_robin`)
@@ -454,10 +451,12 @@ class ChatAgent(BaseAgent):
454
451
  step_timeout (Optional[float], optional): Timeout in seconds for the
455
452
  entire step operation. If None, no timeout is applied.
456
453
  (default: :obj:`None`)
457
- stream_accumulate (bool, optional): When True, partial streaming
458
- updates return accumulated content (current behavior). When False,
459
- partial updates return only the incremental delta. (default:
460
- :obj:`True`)
454
+ stream_accumulate (Optional[bool], optional): When True, partial
455
+ streaming updates return accumulated content. When False, partial
456
+ updates return only the incremental delta (recommended).
457
+ If None, defaults to False with a deprecation warning for users
458
+ who previously relied on the old default (True).
459
+ (default: :obj:`None`, which behaves as :obj:`False`)
461
460
  summary_window_ratio (float, optional): Maximum fraction of the total
462
461
  context window that can be occupied by summary information. Used
463
462
  to limit how much of the model's context is reserved for
@@ -507,7 +506,7 @@ class ChatAgent(BaseAgent):
507
506
  retry_attempts: int = 3,
508
507
  retry_delay: float = 1.0,
509
508
  step_timeout: Optional[float] = Constants.TIMEOUT_THRESHOLD,
510
- stream_accumulate: bool = True,
509
+ stream_accumulate: Optional[bool] = None,
511
510
  summary_window_ratio: float = 0.6,
512
511
  ) -> None:
513
512
  if isinstance(model, ModelManager):
@@ -528,10 +527,16 @@ class ChatAgent(BaseAgent):
528
527
  self._tool_output_history: List[_ToolOutputHistoryEntry] = []
529
528
 
530
529
  # Set up memory
530
+ if token_limit is not None:
531
+ effective_token_limit = token_limit
532
+ else:
533
+ effective_token_limit = self.model_backend.token_limit
531
534
  context_creator = ScoreBasedContextCreator(
532
535
  self.model_backend.token_counter,
533
- self.model_backend.token_limit,
536
+ effective_token_limit,
534
537
  )
538
+ self._token_limit = effective_token_limit
539
+ self._summary_token_count = 0
535
540
 
536
541
  self._memory: AgentMemory = memory or ChatHistoryMemory(
537
542
  context_creator,
@@ -568,7 +573,6 @@ class ChatAgent(BaseAgent):
568
573
  f"{summarize_threshold}% of the total token limit."
569
574
  )
570
575
  self.summarize_threshold = summarize_threshold
571
- self._reset_summary_state()
572
576
 
573
577
  # Set up role name and role type
574
578
  self.role_name: str = (
@@ -616,20 +620,48 @@ class ChatAgent(BaseAgent):
616
620
  self.step_timeout = step_timeout
617
621
  self._context_utility: Optional[ContextUtility] = None
618
622
  self._context_summary_agent: Optional["ChatAgent"] = None
619
- self.stream_accumulate = stream_accumulate
623
+
624
+ # Store whether user explicitly set stream_accumulate
625
+ # Warning will be issued only when streaming is actually used
626
+ self._stream_accumulate_explicit = stream_accumulate is not None
627
+ self.stream_accumulate = (
628
+ stream_accumulate if stream_accumulate is not None else False
629
+ )
620
630
  self._last_tool_call_record: Optional[ToolCallingRecord] = None
621
631
  self._last_tool_call_signature: Optional[str] = None
622
- self._last_token_limit_tool_signature: Optional[str] = None
623
632
  self.summary_window_ratio = summary_window_ratio
624
633
 
625
634
  def reset(self):
626
635
  r"""Resets the :obj:`ChatAgent` to its initial state."""
627
636
  self.terminated = False
628
637
  self.init_messages()
629
- self._reset_summary_state()
630
638
  for terminator in self.response_terminators:
631
639
  terminator.reset()
632
640
 
641
+ def _update_token_cache(
642
+ self,
643
+ usage_dict: Dict[str, Any],
644
+ message_count: int,
645
+ ) -> None:
646
+ r"""Update the token count cache from LLM response usage.
647
+
648
+ Args:
649
+ usage_dict (Dict[str, Any]): Usage dictionary from LLM response.
650
+ message_count (int): Number of messages sent to the LLM.
651
+ """
652
+ prompt_tokens = usage_dict.get("prompt_tokens", 0)
653
+ completion_tokens = usage_dict.get("completion_tokens", 0)
654
+
655
+ if prompt_tokens == 0:
656
+ return
657
+
658
+ total_tokens = prompt_tokens + completion_tokens
659
+ context_creator = self.memory.get_context_creator()
660
+ if hasattr(context_creator, 'set_cached_token_count'):
661
+ context_creator.set_cached_token_count(
662
+ total_tokens, message_count + 1
663
+ )
664
+
633
665
  def _resolve_models(
634
666
  self,
635
667
  model: Optional[
@@ -774,6 +806,11 @@ class ChatAgent(BaseAgent):
774
806
  r"""Returns a dictionary of internal tools."""
775
807
  return self._internal_tools
776
808
 
809
+ @property
810
+ def token_limit(self) -> int:
811
+ r"""Returns the token limit for the agent's context window."""
812
+ return self._token_limit
813
+
777
814
  @property
778
815
  def output_language(self) -> Optional[str]:
779
816
  r"""Returns the output language for the agent."""
@@ -833,122 +870,6 @@ class ChatAgent(BaseAgent):
833
870
  for func_tool in self._internal_tools.values()
834
871
  ]
835
872
 
836
- @staticmethod
837
- def _is_token_limit_error(error: Exception) -> bool:
838
- r"""Return True when the exception message indicates a token limit."""
839
- error_message = str(error).lower()
840
- return any(
841
- marker in error_message for marker in TOKEN_LIMIT_ERROR_MARKERS
842
- )
843
-
844
- @staticmethod
845
- def _is_tool_related_record(record: MemoryRecord) -> bool:
846
- r"""Determine whether the given memory record
847
- belongs to a tool call."""
848
- if record.role_at_backend in {
849
- OpenAIBackendRole.TOOL,
850
- OpenAIBackendRole.FUNCTION,
851
- }:
852
- return True
853
-
854
- if (
855
- record.role_at_backend == OpenAIBackendRole.ASSISTANT
856
- and isinstance(record.message, FunctionCallingMessage)
857
- ):
858
- return True
859
-
860
- return False
861
-
862
- def _find_indices_to_remove_for_last_tool_pair(
863
- self, recent_records: List[ContextRecord]
864
- ) -> List[int]:
865
- """Find indices of records that should be removed to clean up the most
866
- recent incomplete tool interaction pair.
867
-
868
- This method identifies tool call/result pairs by tool_call_id and
869
- returns the exact indices to remove, allowing non-contiguous deletions.
870
-
871
- Logic:
872
- - If the last record is a tool result (TOOL/FUNCTION) with a
873
- tool_call_id, find the matching assistant call anywhere in history
874
- and return both indices.
875
- - If the last record is an assistant tool call without a result yet,
876
- return just that index.
877
- - For normal messages (non tool-related): remove just the last one.
878
- - Fallback: If no tool_call_id is available, use heuristic (last 2 if
879
- tool-related, otherwise last 1).
880
-
881
- Returns:
882
- List[int]: Indices to remove (may be non-contiguous).
883
- """
884
- if not recent_records:
885
- return []
886
-
887
- last_idx = len(recent_records) - 1
888
- last_record = recent_records[last_idx].memory_record
889
-
890
- # Case A: Last is an ASSISTANT tool call with no result yet
891
- if (
892
- last_record.role_at_backend == OpenAIBackendRole.ASSISTANT
893
- and isinstance(last_record.message, FunctionCallingMessage)
894
- and last_record.message.result is None
895
- ):
896
- return [last_idx]
897
-
898
- # Case B: Last is TOOL/FUNCTION result, try id-based pairing
899
- if last_record.role_at_backend in {
900
- OpenAIBackendRole.TOOL,
901
- OpenAIBackendRole.FUNCTION,
902
- }:
903
- tool_id = None
904
- if isinstance(last_record.message, FunctionCallingMessage):
905
- tool_id = last_record.message.tool_call_id
906
-
907
- if tool_id:
908
- for idx in range(len(recent_records) - 2, -1, -1):
909
- rec = recent_records[idx].memory_record
910
- if rec.role_at_backend != OpenAIBackendRole.ASSISTANT:
911
- continue
912
-
913
- # Check if this assistant message contains the tool_call_id
914
- matched = False
915
-
916
- # Case 1: FunctionCallingMessage (single tool call)
917
- if isinstance(rec.message, FunctionCallingMessage):
918
- if rec.message.tool_call_id == tool_id:
919
- matched = True
920
-
921
- # Case 2: BaseMessage with multiple tool_calls in meta_dict
922
- elif (
923
- hasattr(rec.message, "meta_dict")
924
- and rec.message.meta_dict
925
- ):
926
- tool_calls_list = rec.message.meta_dict.get(
927
- "tool_calls", []
928
- )
929
- if isinstance(tool_calls_list, list):
930
- for tc in tool_calls_list:
931
- if (
932
- isinstance(tc, dict)
933
- and tc.get("id") == tool_id
934
- ):
935
- matched = True
936
- break
937
-
938
- if matched:
939
- # Return both assistant call and tool result indices
940
- return [idx, last_idx]
941
-
942
- # Fallback: no tool_call_id, use heuristic
943
- if self._is_tool_related_record(last_record):
944
- # Remove last 2 (assume they are paired)
945
- return [last_idx - 1, last_idx] if last_idx > 0 else [last_idx]
946
- else:
947
- return [last_idx]
948
-
949
- # Default: non tool-related tail => remove last one
950
- return [last_idx]
951
-
952
873
  @staticmethod
953
874
  def _serialize_tool_args(args: Dict[str, Any]) -> str:
954
875
  try:
@@ -991,39 +912,6 @@ class ChatAgent(BaseAgent):
991
912
  signature = None
992
913
  self._last_tool_call_signature = signature
993
914
 
994
- def _format_tool_limit_notice(self) -> Optional[str]:
995
- record = self._last_tool_call_record
996
- description = self._describe_tool_call(record)
997
- if description is None:
998
- return None
999
- notice_lines = [
1000
- "[Tool Call Causing Token Limit]",
1001
- description,
1002
- ]
1003
-
1004
- if record is not None:
1005
- result = record.result
1006
- if isinstance(result, bytes):
1007
- result_repr = result.decode(errors="replace")
1008
- elif isinstance(result, str):
1009
- result_repr = result
1010
- else:
1011
- try:
1012
- result_repr = json.dumps(
1013
- result, ensure_ascii=False, sort_keys=True
1014
- )
1015
- except (TypeError, ValueError):
1016
- result_repr = str(result)
1017
-
1018
- result_length = len(result_repr)
1019
- notice_lines.append(f"Tool result length: {result_length}")
1020
- if self.model_backend.token_limit != 999999999:
1021
- notice_lines.append(
1022
- f"Token limit: {self.model_backend.token_limit}"
1023
- )
1024
-
1025
- return "\n".join(notice_lines)
1026
-
1027
915
  @staticmethod
1028
916
  def _append_user_messages_section(
1029
917
  summary_content: str, user_messages: List[str]
@@ -1051,21 +939,104 @@ class ChatAgent(BaseAgent):
1051
939
  def _reset_summary_state(self) -> None:
1052
940
  self._summary_token_count = 0 # Total tokens in summary messages
1053
941
 
942
+ def _get_context_with_summarization(
943
+ self,
944
+ ) -> Tuple[List[OpenAIMessage], int]:
945
+ r"""Get context and trigger summarization if needed."""
946
+ openai_messages, num_tokens = self.memory.get_context()
947
+
948
+ if self.summarize_threshold is None or num_tokens > self.token_limit:
949
+ return openai_messages, num_tokens
950
+
951
+ summary_token_count = self._summary_token_count
952
+
953
+ if summary_token_count > self.token_limit * self.summary_window_ratio:
954
+ logger.warning(
955
+ f"Summary tokens ({summary_token_count}) "
956
+ f"exceed limit, full compression."
957
+ )
958
+ summary = self.summarize(include_summaries=True)
959
+ self._update_memory_with_summary(
960
+ summary.get("summary", ""), include_summaries=True
961
+ )
962
+ return self.memory.get_context()
963
+
964
+ threshold = self._calculate_next_summary_threshold()
965
+ if num_tokens > threshold:
966
+ logger.warning(
967
+ f"Token count ({num_tokens}) exceed threshold "
968
+ f"({threshold}). Triggering summarization."
969
+ )
970
+ summary = self.summarize(include_summaries=False)
971
+ self._update_memory_with_summary(
972
+ summary.get("summary", ""), include_summaries=False
973
+ )
974
+ return self.memory.get_context()
975
+
976
+ return openai_messages, num_tokens
977
+
978
+ async def _get_context_with_summarization_async(
979
+ self,
980
+ ) -> Tuple[List[OpenAIMessage], int]:
981
+ r"""Async version: get context and trigger summarization if needed."""
982
+ openai_messages, num_tokens = self.memory.get_context()
983
+
984
+ if self.summarize_threshold is None or num_tokens > self.token_limit:
985
+ return openai_messages, num_tokens
986
+
987
+ summary_token_count = self._summary_token_count
988
+
989
+ if summary_token_count > self.token_limit * self.summary_window_ratio:
990
+ logger.warning(
991
+ f"Summary tokens ({summary_token_count}) "
992
+ f"exceed limit, full compression."
993
+ )
994
+ summary = await self.asummarize(include_summaries=True)
995
+ self._update_memory_with_summary(
996
+ summary.get("summary", ""), include_summaries=True
997
+ )
998
+ return self.memory.get_context()
999
+
1000
+ threshold = self._calculate_next_summary_threshold()
1001
+ if num_tokens > threshold:
1002
+ logger.warning(
1003
+ f"Token count ({num_tokens}) exceed threshold "
1004
+ f"({threshold}). Triggering summarization."
1005
+ )
1006
+ summary = await self.asummarize(include_summaries=False)
1007
+ self._update_memory_with_summary(
1008
+ summary.get("summary", ""), include_summaries=False
1009
+ )
1010
+ return self.memory.get_context()
1011
+
1012
+ return openai_messages, num_tokens
1013
+
1054
1014
  def _calculate_next_summary_threshold(self) -> int:
1055
1015
  r"""Calculate the next token threshold that should trigger
1056
1016
  summarization.
1057
1017
 
1058
1018
  The threshold calculation follows a progressive strategy:
1059
- - First time: token_limit * (summarize_threshold / 100)
1060
- - Subsequent times: (limit - summary_token) / 2 + summary_token
1019
+ - First time (or after full compression):
1020
+ token_limit * (summarize_threshold / 100)
1021
+ - After progressive compression:
1022
+ (token_limit - summary_tokens) * (summarize_threshold / 100)
1023
+ + summary_tokens
1061
1024
 
1062
- This ensures that as summaries accumulate, the threshold adapts
1063
- to maintain a reasonable balance between context and summaries.
1025
+ This ensures that as summaries accumulate through progressive
1026
+ compression, the threshold adapts to maintain a reasonable balance
1027
+ between context and summaries. After full compression, the threshold
1028
+ resets to the initial value to prevent frequent re-summarization.
1064
1029
 
1065
1030
  Returns:
1066
1031
  int: The token count threshold for next summarization.
1067
1032
  """
1068
- token_limit = self.model_backend.token_limit
1033
+ if self.summarize_threshold is None:
1034
+ raise ValueError(
1035
+ "Cannot calculate summary threshold when "
1036
+ "summarize_threshold is None"
1037
+ )
1038
+
1039
+ token_limit = self.token_limit
1069
1040
  summary_token_count = self._summary_token_count
1070
1041
 
1071
1042
  # First summarization: use the percentage threshold
@@ -1094,17 +1065,21 @@ class ChatAgent(BaseAgent):
1094
1065
  summary_content: str = summary
1095
1066
 
1096
1067
  existing_summaries = []
1097
- if not include_summaries:
1098
- messages, _ = self.memory.get_context()
1099
- for msg in messages:
1100
- content = msg.get('content', '')
1101
- if isinstance(content, str) and content.startswith(
1102
- '[CONTEXT_SUMMARY]'
1103
- ):
1104
- existing_summaries.append(msg)
1068
+ last_user_message: Optional[str] = None
1069
+ messages, _ = self.memory.get_context()
1070
+ for msg in messages:
1071
+ content = msg.get('content', '')
1072
+ role = msg.get('role', '')
1073
+ if role == 'user' and isinstance(content, str) and content:
1074
+ last_user_message = content
1075
+ if (
1076
+ not include_summaries
1077
+ and isinstance(content, str)
1078
+ and content.startswith('[CONTEXT_SUMMARY]')
1079
+ ):
1080
+ existing_summaries.append(msg)
1105
1081
 
1106
- # Clear memory
1107
- self.clear_memory()
1082
+ self.clear_memory(reset_summary_state=False)
1108
1083
 
1109
1084
  # Restore old summaries (for progressive compression)
1110
1085
  for old_summary in existing_summaries:
@@ -1121,16 +1096,24 @@ class ChatAgent(BaseAgent):
1121
1096
  role_name="assistant", content=summary_content
1122
1097
  )
1123
1098
  self.update_memory(new_summary_msg, OpenAIBackendRole.ASSISTANT)
1124
- input_message = BaseMessage.make_assistant_message(
1125
- role_name="assistant",
1126
- content=(
1127
- "Please continue the conversation from "
1128
- "where we left it off without asking the user any further "
1129
- "questions. Continue with the last task that you were "
1130
- "asked to work on."
1131
- ),
1132
- )
1133
- self.update_memory(input_message, OpenAIBackendRole.ASSISTANT)
1099
+
1100
+ # Restore last user message to maintain conversation structure
1101
+ # The summary already contains all user messages, but we keep the
1102
+ # latest one so the model knows what to respond to
1103
+ if last_user_message:
1104
+ # Avoid duplicate prefix - check if already prefixed
1105
+ context_prefix = (
1106
+ "Based on the previous CONTEXT_SUMMARY, "
1107
+ "continue with my current message: "
1108
+ )
1109
+ if not last_user_message.startswith(context_prefix):
1110
+ last_user_message = f"{context_prefix}{last_user_message}"
1111
+ user_msg = BaseMessage.make_user_message(
1112
+ role_name="user",
1113
+ content=last_user_message,
1114
+ )
1115
+ self.update_memory(user_msg, OpenAIBackendRole.USER)
1116
+
1134
1117
  # Update token count
1135
1118
  try:
1136
1119
  summary_tokens = (
@@ -1139,13 +1122,15 @@ class ChatAgent(BaseAgent):
1139
1122
  )
1140
1123
  )
1141
1124
 
1142
- if include_summaries: # Full compression - reset count
1125
+ if (
1126
+ include_summaries
1127
+ ): # Full compression - reset and set to new summary tokens only
1143
1128
  self._summary_token_count = summary_tokens
1144
1129
  logger.info(
1145
1130
  f"Full compression: Summary with {summary_tokens} tokens. "
1146
- f"Total summary tokens reset to: {summary_tokens}"
1131
+ f"Total summary tokens set to: {summary_tokens}"
1147
1132
  )
1148
- else: # Progressive compression - accumulate
1133
+ else: # Progressive compression - accumulate on existing count
1149
1134
  self._summary_token_count += summary_tokens
1150
1135
  logger.info(
1151
1136
  f"Progressive compression: New summary "
@@ -1178,6 +1163,50 @@ class ChatAgent(BaseAgent):
1178
1163
  except (TypeError, ValueError):
1179
1164
  return str(result)
1180
1165
 
1166
+ def _truncate_tool_result(
1167
+ self, func_name: str, result: Any
1168
+ ) -> Tuple[Any, bool]:
1169
+ r"""Truncate tool result if it exceeds the maximum token limit.
1170
+
1171
+ Args:
1172
+ func_name (str): The name of the tool function called.
1173
+ result (Any): The result returned by the tool execution.
1174
+
1175
+ Returns:
1176
+ Tuple[Any, bool]: A tuple containing:
1177
+ - The (possibly truncated) result
1178
+ - A boolean indicating whether truncation occurred
1179
+ """
1180
+ serialized = self._serialize_tool_result(result)
1181
+ # Use summarize_threshold if set, otherwise default to 90%
1182
+ threshold_ratio = (
1183
+ min(0.9, self.summarize_threshold / 100)
1184
+ if self.summarize_threshold is not None
1185
+ else 0.9
1186
+ )
1187
+ max_tokens = int(self.token_limit * threshold_ratio)
1188
+ result_tokens = self._get_token_count(serialized)
1189
+
1190
+ if result_tokens <= max_tokens:
1191
+ return result, False
1192
+
1193
+ # Reserve ~100 tokens for notice, use char-based truncation directly
1194
+ target_tokens = max(max_tokens - 100, 100)
1195
+ truncated = serialized[: target_tokens * 3]
1196
+
1197
+ notice = (
1198
+ f"\n\n[TRUNCATED] Tool '{func_name}' output truncated "
1199
+ f"({result_tokens} > {max_tokens} tokens). "
1200
+ f"Tool executed successfully."
1201
+ )
1202
+
1203
+ logger.warning(
1204
+ f"Tool '{func_name}' result truncated: "
1205
+ f"{result_tokens} -> ~{target_tokens} tokens"
1206
+ )
1207
+
1208
+ return notice + truncated, True
1209
+
1181
1210
  def _clean_snapshot_line(self, line: str) -> str:
1182
1211
  r"""Clean a single snapshot line by removing prefixes and references.
1183
1212
 
@@ -1702,6 +1731,7 @@ class ChatAgent(BaseAgent):
1702
1731
  ),
1703
1732
  model=self.model_backend,
1704
1733
  agent_id=f"{self.agent_id}_context_summarizer",
1734
+ token_limit=self.token_limit,
1705
1735
  summarize_threshold=None,
1706
1736
  )
1707
1737
  else:
@@ -1994,6 +2024,8 @@ class ChatAgent(BaseAgent):
1994
2024
  ),
1995
2025
  model=self.model_backend,
1996
2026
  agent_id=f"{self.agent_id}_context_summarizer",
2027
+ token_limit=self.token_limit,
2028
+ summarize_threshold=None,
1997
2029
  )
1998
2030
  else:
1999
2031
  self._context_summary_agent.reset()
@@ -2137,6 +2169,7 @@ class ChatAgent(BaseAgent):
2137
2169
  ),
2138
2170
  model=self.model_backend,
2139
2171
  agent_id=f"{self.agent_id}_context_summarizer",
2172
+ token_limit=self.token_limit,
2140
2173
  summarize_threshold=None,
2141
2174
  )
2142
2175
  else:
@@ -2280,14 +2313,24 @@ class ChatAgent(BaseAgent):
2280
2313
  result["status"] = error_message
2281
2314
  return result
2282
2315
 
2283
- def clear_memory(self) -> None:
2316
+ def clear_memory(self, reset_summary_state: bool = True):
2284
2317
  r"""Clear the agent's memory and reset to initial state.
2285
2318
 
2286
- Returns:
2287
- None
2319
+ Args:
2320
+ reset_summary_state (bool): Whether to reset the summary token
2321
+ count. Set to False when preserving summary state during
2322
+ summarization. Defaults to True for full memory clearing.
2288
2323
  """
2289
2324
  self.memory.clear()
2290
2325
 
2326
+ if reset_summary_state:
2327
+ self._reset_summary_state()
2328
+
2329
+ # Reset token cache when memory is cleared
2330
+ context_creator = self.memory.get_context_creator()
2331
+ if hasattr(context_creator, 'clear_cache'):
2332
+ context_creator.clear_cache()
2333
+
2291
2334
  if self.system_message is not None:
2292
2335
  self.memory.write_record(
2293
2336
  MemoryRecord(
@@ -2327,7 +2370,6 @@ class ChatAgent(BaseAgent):
2327
2370
  r"""Initializes the stored messages list with the current system
2328
2371
  message.
2329
2372
  """
2330
- self._reset_summary_state()
2331
2373
  self.clear_memory()
2332
2374
 
2333
2375
  def update_system_message(
@@ -2655,7 +2697,6 @@ class ChatAgent(BaseAgent):
2655
2697
  # Explicitly set the tools to empty list to avoid calling tools
2656
2698
  response = self._get_model_response(
2657
2699
  openai_messages=[openai_message],
2658
- num_tokens=0,
2659
2700
  response_format=response_format,
2660
2701
  tool_schemas=[],
2661
2702
  prev_num_openai_messages=0,
@@ -2687,7 +2728,6 @@ class ChatAgent(BaseAgent):
2687
2728
  openai_message: OpenAIMessage = {"role": "user", "content": prompt}
2688
2729
  response = await self._aget_model_response(
2689
2730
  openai_messages=[openai_message],
2690
- num_tokens=0,
2691
2731
  response_format=response_format,
2692
2732
  tool_schemas=[],
2693
2733
  prev_num_openai_messages=0,
@@ -2755,6 +2795,11 @@ class ChatAgent(BaseAgent):
2755
2795
  response_format: Optional[Type[BaseModel]] = None,
2756
2796
  ) -> ChatAgentResponse:
2757
2797
  r"""Implementation of non-streaming step logic."""
2798
+ # Set agent_id in context-local storage for logging
2799
+ from camel.utils.agent_context import set_current_agent_id
2800
+
2801
+ set_current_agent_id(self.agent_id)
2802
+
2758
2803
  # Set Langfuse session_id using agent_id for trace grouping
2759
2804
  try:
2760
2805
  from camel.utils.langfuse import set_current_agent_session_id
@@ -2807,122 +2852,24 @@ class ChatAgent(BaseAgent):
2807
2852
  time.sleep(0.001)
2808
2853
 
2809
2854
  try:
2810
- openai_messages, num_tokens = self.memory.get_context()
2811
- if self.summarize_threshold is not None:
2812
- threshold = self._calculate_next_summary_threshold()
2813
- summary_token_count = self._summary_token_count
2814
- token_limit = self.model_backend.token_limit
2815
-
2816
- if num_tokens <= token_limit:
2817
- if (
2818
- summary_token_count
2819
- > token_limit * self.summary_window_ratio
2820
- ):
2821
- logger.info(
2822
- f"Summary tokens ({summary_token_count}) "
2823
- f"exceed limit, full compression."
2824
- )
2825
- # Summarize everything (including summaries)
2826
- summary = self.summarize(include_summaries=True)
2827
- self._update_memory_with_summary(
2828
- summary.get("summary", ""),
2829
- include_summaries=True,
2830
- )
2831
- elif num_tokens > threshold:
2832
- logger.info(
2833
- f"Token count ({num_tokens}) exceed threshold "
2834
- f"({threshold}). Triggering summarization."
2835
- )
2836
- # Only summarize non-summary content
2837
- summary = self.summarize(include_summaries=False)
2838
- self._update_memory_with_summary(
2839
- summary.get("summary", ""),
2840
- include_summaries=False,
2841
- )
2855
+ openai_messages, num_tokens = (
2856
+ self._get_context_with_summarization()
2857
+ )
2842
2858
  accumulated_context_tokens += num_tokens
2843
2859
  except RuntimeError as e:
2844
2860
  return self._step_terminate(
2845
2861
  e.args[1], tool_call_records, "max_tokens_exceeded"
2846
2862
  )
2847
- # Get response from model backend with token limit error handling
2848
- try:
2849
- response = self._get_model_response(
2850
- openai_messages,
2851
- num_tokens=num_tokens,
2852
- current_iteration=iteration_count,
2853
- response_format=response_format,
2854
- tool_schemas=[]
2855
- if disable_tools
2856
- else self._get_full_tool_schemas(),
2857
- prev_num_openai_messages=prev_num_openai_messages,
2858
- )
2859
- except Exception as exc:
2860
- logger.exception("Model error: %s", exc)
2861
-
2862
- if self._is_token_limit_error(exc):
2863
- tool_signature = self._last_tool_call_signature
2864
- if (
2865
- tool_signature is not None
2866
- and tool_signature
2867
- == self._last_token_limit_tool_signature
2868
- ):
2869
- description = self._describe_tool_call(
2870
- self._last_tool_call_record
2871
- )
2872
- repeated_msg = (
2873
- "Context exceeded again by the same tool call."
2874
- )
2875
- if description:
2876
- repeated_msg += f" {description}"
2877
- raise RuntimeError(repeated_msg) from exc
2878
-
2879
- user_message_count = sum(
2880
- 1
2881
- for msg in openai_messages
2882
- if getattr(msg, "role", None) == "user"
2883
- )
2884
- if (
2885
- user_message_count == 1
2886
- and getattr(openai_messages[-1], "role", None)
2887
- == "user"
2888
- ):
2889
- raise RuntimeError(
2890
- "The provided user input alone exceeds the "
2891
- "context window. Please shorten the input."
2892
- ) from exc
2893
-
2894
- logger.warning(
2895
- "Token limit exceeded error detected. "
2896
- "Summarizing context."
2897
- )
2898
-
2899
- recent_records: List[ContextRecord]
2900
- try:
2901
- recent_records = self.memory.retrieve()
2902
- except Exception: # pragma: no cover - defensive guard
2903
- recent_records = []
2904
-
2905
- indices_to_remove = (
2906
- self._find_indices_to_remove_for_last_tool_pair(
2907
- recent_records
2908
- )
2909
- )
2910
- self.memory.remove_records_by_indices(indices_to_remove)
2911
-
2912
- summary = self.summarize(include_summaries=False)
2913
- tool_notice = self._format_tool_limit_notice()
2914
- summary_messages = summary.get("summary", "")
2915
-
2916
- if tool_notice:
2917
- summary_messages += "\n\n" + tool_notice
2918
-
2919
- self._update_memory_with_summary(
2920
- summary_messages, include_summaries=False
2921
- )
2922
- self._last_token_limit_tool_signature = tool_signature
2923
- return self._step_impl(input_message, response_format)
2924
-
2925
- raise
2863
+ # Get response from model backend
2864
+ response = self._get_model_response(
2865
+ openai_messages,
2866
+ current_iteration=iteration_count,
2867
+ response_format=response_format,
2868
+ tool_schemas=[]
2869
+ if disable_tools
2870
+ else self._get_full_tool_schemas(),
2871
+ prev_num_openai_messages=prev_num_openai_messages,
2872
+ )
2926
2873
 
2927
2874
  prev_num_openai_messages = len(openai_messages)
2928
2875
  iteration_count += 1
@@ -2932,6 +2879,9 @@ class ChatAgent(BaseAgent):
2932
2879
  step_token_usage, response.usage_dict
2933
2880
  )
2934
2881
 
2882
+ # Update token cache from LLM response
2883
+ self._update_token_cache(response.usage_dict, len(openai_messages))
2884
+
2935
2885
  # Terminate Agent if stop_event is set
2936
2886
  if self.stop_event and self.stop_event.is_set():
2937
2887
  # Use the _step_terminate to terminate the agent with reason
@@ -2981,6 +2931,43 @@ class ChatAgent(BaseAgent):
2981
2931
  # If we're still here, continue the loop
2982
2932
  continue
2983
2933
 
2934
+ # No tool calls - check if we should terminate based on terminators
2935
+ if self.response_terminators:
2936
+ # Check terminators to see if task is complete
2937
+ termination_results = [
2938
+ terminator.is_terminated(response.output_messages)
2939
+ for terminator in self.response_terminators
2940
+ ]
2941
+ should_terminate = any(
2942
+ terminated for terminated, _ in termination_results
2943
+ )
2944
+
2945
+ if should_terminate:
2946
+ # Task is complete, exit the loop
2947
+ break
2948
+
2949
+ # Task not complete - prompt the model to continue
2950
+ if (
2951
+ self.max_iteration is not None
2952
+ and iteration_count >= self.max_iteration
2953
+ ):
2954
+ logger.warning(
2955
+ f"Max iteration {self.max_iteration} reached without "
2956
+ "termination signal"
2957
+ )
2958
+ break
2959
+
2960
+ # Add a continuation prompt to memory as a user message
2961
+ continue_message = BaseMessage(
2962
+ role_name="user",
2963
+ role_type=RoleType.USER,
2964
+ content="Please continue.",
2965
+ meta_dict={},
2966
+ )
2967
+ self.update_memory(continue_message, OpenAIBackendRole.USER)
2968
+ continue
2969
+
2970
+ # No terminators configured, use original behavior
2984
2971
  break
2985
2972
 
2986
2973
  self._format_response_if_needed(response, response_format)
@@ -3044,6 +3031,10 @@ class ChatAgent(BaseAgent):
3044
3031
  asyncio.TimeoutError: If the step operation exceeds the configured
3045
3032
  timeout.
3046
3033
  """
3034
+ # Set agent_id in context-local storage for logging
3035
+ from camel.utils.agent_context import set_current_agent_id
3036
+
3037
+ set_current_agent_id(self.agent_id)
3047
3038
 
3048
3039
  try:
3049
3040
  from camel.utils.langfuse import set_current_agent_session_id
@@ -3081,6 +3072,10 @@ class ChatAgent(BaseAgent):
3081
3072
  response_format: Optional[Type[BaseModel]] = None,
3082
3073
  ) -> ChatAgentResponse:
3083
3074
  r"""Internal async method for non-streaming astep logic."""
3075
+ # Set agent_id in context-local storage for logging
3076
+ from camel.utils.agent_context import set_current_agent_id
3077
+
3078
+ set_current_agent_id(self.agent_id)
3084
3079
 
3085
3080
  try:
3086
3081
  from camel.utils.langfuse import set_current_agent_session_id
@@ -3128,128 +3123,25 @@ class ChatAgent(BaseAgent):
3128
3123
  loop = asyncio.get_event_loop()
3129
3124
  await loop.run_in_executor(None, self.pause_event.wait)
3130
3125
  try:
3131
- openai_messages, num_tokens = self.memory.get_context()
3132
- if self.summarize_threshold is not None:
3133
- threshold = self._calculate_next_summary_threshold()
3134
- summary_token_count = self._summary_token_count
3135
- token_limit = self.model_backend.token_limit
3136
-
3137
- if num_tokens <= token_limit:
3138
- if (
3139
- summary_token_count
3140
- > token_limit * self.summary_window_ratio
3141
- ):
3142
- logger.info(
3143
- f"Summary tokens ({summary_token_count}) "
3144
- f"exceed limit, full compression."
3145
- )
3146
- # Summarize everything (including summaries)
3147
- summary = await self.asummarize(
3148
- include_summaries=True
3149
- )
3150
- self._update_memory_with_summary(
3151
- summary.get("summary", ""),
3152
- include_summaries=True,
3153
- )
3154
- elif num_tokens > threshold:
3155
- logger.info(
3156
- f"Token count ({num_tokens}) exceed threshold "
3157
- "({threshold}). Triggering summarization."
3158
- )
3159
- # Only summarize non-summary content
3160
- summary = await self.asummarize(
3161
- include_summaries=False
3162
- )
3163
- self._update_memory_with_summary(
3164
- summary.get("summary", ""),
3165
- include_summaries=False,
3166
- )
3126
+ (
3127
+ openai_messages,
3128
+ num_tokens,
3129
+ ) = await self._get_context_with_summarization_async()
3167
3130
  accumulated_context_tokens += num_tokens
3168
3131
  except RuntimeError as e:
3169
3132
  return self._step_terminate(
3170
3133
  e.args[1], tool_call_records, "max_tokens_exceeded"
3171
3134
  )
3172
- # Get response from model backend with token limit error handling
3173
- try:
3174
- response = await self._aget_model_response(
3175
- openai_messages,
3176
- num_tokens=num_tokens,
3177
- current_iteration=iteration_count,
3178
- response_format=response_format,
3179
- tool_schemas=[]
3180
- if disable_tools
3181
- else self._get_full_tool_schemas(),
3182
- prev_num_openai_messages=prev_num_openai_messages,
3183
- )
3184
- except Exception as exc:
3185
- logger.exception("Model error: %s", exc)
3186
-
3187
- if self._is_token_limit_error(exc):
3188
- tool_signature = self._last_tool_call_signature
3189
- if (
3190
- tool_signature is not None
3191
- and tool_signature
3192
- == self._last_token_limit_tool_signature
3193
- ):
3194
- description = self._describe_tool_call(
3195
- self._last_tool_call_record
3196
- )
3197
- repeated_msg = (
3198
- "Context exceeded again by the same tool call."
3199
- )
3200
- if description:
3201
- repeated_msg += f" {description}"
3202
- raise RuntimeError(repeated_msg) from exc
3203
-
3204
- user_message_count = sum(
3205
- 1
3206
- for msg in openai_messages
3207
- if getattr(msg, "role", None) == "user"
3208
- )
3209
- if (
3210
- user_message_count == 1
3211
- and getattr(openai_messages[-1], "role", None)
3212
- == "user"
3213
- ):
3214
- raise RuntimeError(
3215
- "The provided user input alone exceeds the"
3216
- "context window. Please shorten the input."
3217
- ) from exc
3218
-
3219
- logger.warning(
3220
- "Token limit exceeded error detected. "
3221
- "Summarizing context."
3222
- )
3223
-
3224
- recent_records: List[ContextRecord]
3225
- try:
3226
- recent_records = self.memory.retrieve()
3227
- except Exception: # pragma: no cover - defensive guard
3228
- recent_records = []
3229
-
3230
- indices_to_remove = (
3231
- self._find_indices_to_remove_for_last_tool_pair(
3232
- recent_records
3233
- )
3234
- )
3235
- self.memory.remove_records_by_indices(indices_to_remove)
3236
-
3237
- summary = await self.asummarize()
3238
-
3239
- tool_notice = self._format_tool_limit_notice()
3240
- summary_messages = summary.get("summary", "")
3241
-
3242
- if tool_notice:
3243
- summary_messages += "\n\n" + tool_notice
3244
- self._update_memory_with_summary(
3245
- summary_messages, include_summaries=False
3246
- )
3247
- self._last_token_limit_tool_signature = tool_signature
3248
- return await self._astep_non_streaming_task(
3249
- input_message, response_format
3250
- )
3251
-
3252
- raise
3135
+ # Get response from model backend
3136
+ response = await self._aget_model_response(
3137
+ openai_messages,
3138
+ current_iteration=iteration_count,
3139
+ response_format=response_format,
3140
+ tool_schemas=[]
3141
+ if disable_tools
3142
+ else self._get_full_tool_schemas(),
3143
+ prev_num_openai_messages=prev_num_openai_messages,
3144
+ )
3253
3145
 
3254
3146
  prev_num_openai_messages = len(openai_messages)
3255
3147
  iteration_count += 1
@@ -3259,6 +3151,9 @@ class ChatAgent(BaseAgent):
3259
3151
  step_token_usage, response.usage_dict
3260
3152
  )
3261
3153
 
3154
+ # Update token cache from LLM response
3155
+ self._update_token_cache(response.usage_dict, len(openai_messages))
3156
+
3262
3157
  # Terminate Agent if stop_event is set
3263
3158
  if self.stop_event and self.stop_event.is_set():
3264
3159
  # Use the _step_terminate to terminate the agent with reason
@@ -3311,6 +3206,43 @@ class ChatAgent(BaseAgent):
3311
3206
  # If we're still here, continue the loop
3312
3207
  continue
3313
3208
 
3209
+ # No tool calls - check if we should terminate based on terminators
3210
+ if self.response_terminators:
3211
+ # Check terminators to see if task is complete
3212
+ termination_results = [
3213
+ terminator.is_terminated(response.output_messages)
3214
+ for terminator in self.response_terminators
3215
+ ]
3216
+ should_terminate = any(
3217
+ terminated for terminated, _ in termination_results
3218
+ )
3219
+
3220
+ if should_terminate:
3221
+ # Task is complete, exit the loop
3222
+ break
3223
+
3224
+ # Task not complete - prompt the model to continue
3225
+ if (
3226
+ self.max_iteration is not None
3227
+ and iteration_count >= self.max_iteration
3228
+ ):
3229
+ logger.warning(
3230
+ f"Max iteration {self.max_iteration} reached without "
3231
+ "termination signal"
3232
+ )
3233
+ break
3234
+
3235
+ # Add a continuation prompt to memory as a user message
3236
+ continue_message = BaseMessage(
3237
+ role_name="user",
3238
+ role_type=RoleType.USER,
3239
+ content="Please continue.",
3240
+ meta_dict={},
3241
+ )
3242
+ self.update_memory(continue_message, OpenAIBackendRole.USER)
3243
+ continue
3244
+
3245
+ # No terminators configured, use original behavior
3314
3246
  break
3315
3247
 
3316
3248
  await self._aformat_response_if_needed(response, response_format)
@@ -3327,8 +3259,6 @@ class ChatAgent(BaseAgent):
3327
3259
  if self.prune_tool_calls_from_memory and tool_call_records:
3328
3260
  self.memory.clean_tool_calls()
3329
3261
 
3330
- self._last_token_limit_user_signature = None
3331
-
3332
3262
  return self._convert_to_chatagent_response(
3333
3263
  response,
3334
3264
  tool_call_records,
@@ -3356,9 +3286,11 @@ class ChatAgent(BaseAgent):
3356
3286
  tracker (Dict[str, int]): The token usage tracker to update.
3357
3287
  usage_dict (Dict[str, int]): The usage dictionary with new values.
3358
3288
  """
3359
- tracker["prompt_tokens"] += usage_dict.get("prompt_tokens", 0)
3360
- tracker["completion_tokens"] += usage_dict.get("completion_tokens", 0)
3361
- tracker["total_tokens"] += usage_dict.get("total_tokens", 0)
3289
+ tracker["prompt_tokens"] += usage_dict.get("prompt_tokens") or 0
3290
+ tracker["completion_tokens"] += (
3291
+ usage_dict.get("completion_tokens") or 0
3292
+ )
3293
+ tracker["total_tokens"] += usage_dict.get("total_tokens") or 0
3362
3294
 
3363
3295
  def _convert_to_chatagent_response(
3364
3296
  self,
@@ -3398,17 +3330,21 @@ class ChatAgent(BaseAgent):
3398
3330
  r"""Log final messages or warnings about multiple responses."""
3399
3331
  if len(output_messages) == 1:
3400
3332
  self.record_message(output_messages[0])
3333
+ elif len(output_messages) == 0:
3334
+ logger.warning(
3335
+ "No messages returned in `step()`. The model returned an "
3336
+ "empty response."
3337
+ )
3401
3338
  else:
3402
3339
  logger.warning(
3403
- "Multiple messages returned in `step()`. Record "
3404
- "selected message manually using `record_message()`."
3340
+ f"{len(output_messages)} messages returned in `step()`. "
3341
+ "Record selected message manually using `record_message()`."
3405
3342
  )
3406
3343
 
3407
3344
  @observe()
3408
3345
  def _get_model_response(
3409
3346
  self,
3410
3347
  openai_messages: List[OpenAIMessage],
3411
- num_tokens: int,
3412
3348
  current_iteration: int = 0,
3413
3349
  response_format: Optional[Type[BaseModel]] = None,
3414
3350
  tool_schemas: Optional[List[Dict[str, Any]]] = None,
@@ -3425,8 +3361,6 @@ class ChatAgent(BaseAgent):
3425
3361
  if response:
3426
3362
  break
3427
3363
  except RateLimitError as e:
3428
- if self._is_token_limit_error(e):
3429
- raise
3430
3364
  last_error = e
3431
3365
  if attempt < self.retry_attempts - 1:
3432
3366
  delay = min(self.retry_delay * (2**attempt), 60.0)
@@ -3473,7 +3407,6 @@ class ChatAgent(BaseAgent):
3473
3407
  async def _aget_model_response(
3474
3408
  self,
3475
3409
  openai_messages: List[OpenAIMessage],
3476
- num_tokens: int,
3477
3410
  current_iteration: int = 0,
3478
3411
  response_format: Optional[Type[BaseModel]] = None,
3479
3412
  tool_schemas: Optional[List[Dict[str, Any]]] = None,
@@ -3490,8 +3423,6 @@ class ChatAgent(BaseAgent):
3490
3423
  if response:
3491
3424
  break
3492
3425
  except RateLimitError as e:
3493
- if self._is_token_limit_error(e):
3494
- raise
3495
3426
  last_error = e
3496
3427
  if attempt < self.retry_attempts - 1:
3497
3428
  delay = min(self.retry_delay * (2**attempt), 60.0)
@@ -3873,26 +3804,31 @@ class ChatAgent(BaseAgent):
3873
3804
  func_name = tool_call_request.tool_name
3874
3805
  args = tool_call_request.args
3875
3806
  tool_call_id = tool_call_request.tool_call_id
3876
- tool = self._internal_tools[func_name]
3877
- try:
3878
- raw_result = tool(**args)
3879
- if self.mask_tool_output:
3880
- with self._secure_result_store_lock:
3881
- self._secure_result_store[tool_call_id] = raw_result
3882
- result = (
3883
- "[The tool has been executed successfully, but the output"
3884
- " from the tool is masked. You can move forward]"
3885
- )
3886
- mask_flag = True
3887
- else:
3888
- result = raw_result
3889
- mask_flag = False
3890
- except Exception as e:
3891
- # Capture the error message to prevent framework crash
3892
- error_msg = f"Error executing tool '{func_name}': {e!s}"
3807
+ tool = self._internal_tools.get(func_name)
3808
+ mask_flag = False
3809
+
3810
+ if tool is None:
3811
+ error_msg = f"Tool '{func_name}' not found in registered tools"
3893
3812
  result = f"Tool execution failed: {error_msg}"
3894
- mask_flag = False
3895
- logger.warning(f"{error_msg} with result: {result}")
3813
+ logger.warning(error_msg)
3814
+ else:
3815
+ try:
3816
+ raw_result = tool(**args)
3817
+ if self.mask_tool_output:
3818
+ with self._secure_result_store_lock:
3819
+ self._secure_result_store[tool_call_id] = raw_result
3820
+ result = (
3821
+ "[The tool has been executed successfully, but the "
3822
+ "output from the tool is masked. You can move forward]"
3823
+ )
3824
+ mask_flag = True
3825
+ else:
3826
+ result = raw_result
3827
+ except Exception as e:
3828
+ # Capture the error message to prevent framework crash
3829
+ error_msg = f"Error executing tool '{func_name}': {e!s}"
3830
+ result = f"Tool execution failed: {error_msg}"
3831
+ logger.warning(f"{error_msg} with result: {result}")
3896
3832
 
3897
3833
  return self._record_tool_calling(
3898
3834
  func_name,
@@ -3907,50 +3843,69 @@ class ChatAgent(BaseAgent):
3907
3843
  self,
3908
3844
  tool_call_request: ToolCallRequest,
3909
3845
  ) -> ToolCallingRecord:
3846
+ import asyncio
3847
+
3910
3848
  func_name = tool_call_request.tool_name
3911
3849
  args = tool_call_request.args
3912
3850
  tool_call_id = tool_call_request.tool_call_id
3913
- tool = self._internal_tools[func_name]
3914
- import asyncio
3851
+ tool = self._internal_tools.get(func_name)
3852
+ mask_flag = False
3915
3853
 
3916
- try:
3917
- # Try different invocation paths in order of preference
3918
- if hasattr(tool, 'func') and hasattr(tool.func, 'async_call'):
3919
- # Case: FunctionTool wrapping an MCP tool
3920
- result = await tool.func.async_call(**args)
3854
+ if tool is None:
3855
+ error_msg = f"Tool '{func_name}' not found in registered tools"
3856
+ result = f"Tool execution failed: {error_msg}"
3857
+ logger.warning(error_msg)
3858
+ else:
3859
+ try:
3860
+ # Try different invocation paths in order of preference
3861
+ if hasattr(tool, 'func') and hasattr(tool.func, 'async_call'):
3862
+ # Case: FunctionTool wrapping an MCP tool
3863
+ raw_result = await tool.func.async_call(**args)
3921
3864
 
3922
- elif hasattr(tool, 'async_call') and callable(tool.async_call):
3923
- # Case: tool itself has async_call
3924
- result = await tool.async_call(**args)
3865
+ elif hasattr(tool, 'async_call') and callable(tool.async_call):
3866
+ # Case: tool itself has async_call
3867
+ raw_result = await tool.async_call(**args)
3925
3868
 
3926
- elif hasattr(tool, 'func') and asyncio.iscoroutinefunction(
3927
- tool.func
3928
- ):
3929
- # Case: tool wraps a direct async function
3930
- result = await tool.func(**args)
3869
+ elif hasattr(tool, 'func') and asyncio.iscoroutinefunction(
3870
+ tool.func
3871
+ ):
3872
+ # Case: tool wraps a direct async function
3873
+ raw_result = await tool.func(**args)
3931
3874
 
3932
- elif asyncio.iscoroutinefunction(tool):
3933
- # Case: tool is itself a coroutine function
3934
- result = await tool(**args)
3875
+ elif asyncio.iscoroutinefunction(tool):
3876
+ # Case: tool is itself a coroutine function
3877
+ raw_result = await tool(**args)
3935
3878
 
3936
- else:
3937
- # Fallback: synchronous call
3938
- # Use functools.partial to properly capture args
3939
- loop = asyncio.get_running_loop()
3940
- result = await loop.run_in_executor(
3941
- None, functools.partial(tool, **args)
3942
- )
3879
+ else:
3880
+ # Fallback: synchronous call
3881
+ # Use functools.partial to properly capture args
3882
+ loop = asyncio.get_running_loop()
3883
+ raw_result = await loop.run_in_executor(
3884
+ None, functools.partial(tool, **args)
3885
+ )
3943
3886
 
3944
- except Exception as e:
3945
- # Capture the error message to prevent framework crash
3946
- error_msg = f"Error executing async tool '{func_name}': {e!s}"
3947
- result = f"Tool execution failed: {error_msg}"
3948
- logger.warning(error_msg)
3887
+ if self.mask_tool_output:
3888
+ with self._secure_result_store_lock:
3889
+ self._secure_result_store[tool_call_id] = raw_result
3890
+ result = (
3891
+ "[The tool has been executed successfully, but the "
3892
+ "output from the tool is masked. You can move forward]"
3893
+ )
3894
+ mask_flag = True
3895
+ else:
3896
+ result = raw_result
3897
+
3898
+ except Exception as e:
3899
+ # Capture the error message to prevent framework crash
3900
+ error_msg = f"Error executing async tool '{func_name}': {e!s}"
3901
+ result = f"Tool execution failed: {error_msg}"
3902
+ logger.warning(f"{error_msg} with result: {result}")
3949
3903
  return self._record_tool_calling(
3950
3904
  func_name,
3951
3905
  args,
3952
3906
  result,
3953
3907
  tool_call_id,
3908
+ mask_output=mask_flag,
3954
3909
  extra_content=tool_call_request.extra_content,
3955
3910
  )
3956
3911
 
@@ -3982,6 +3937,13 @@ class ChatAgent(BaseAgent):
3982
3937
  ToolCallingRecord: A struct containing information about
3983
3938
  this tool call.
3984
3939
  """
3940
+ # Truncate tool result if it exceeds the maximum token limit
3941
+ # This prevents single tool calls from exceeding context window
3942
+ truncated_result, was_truncated = self._truncate_tool_result(
3943
+ func_name, result
3944
+ )
3945
+ result_for_memory = truncated_result if was_truncated else result
3946
+
3985
3947
  assist_msg = FunctionCallingMessage(
3986
3948
  role_name=self.role_name,
3987
3949
  role_type=self.role_type,
@@ -3998,7 +3960,7 @@ class ChatAgent(BaseAgent):
3998
3960
  meta_dict=None,
3999
3961
  content="",
4000
3962
  func_name=func_name,
4001
- result=result,
3963
+ result=result_for_memory,
4002
3964
  tool_call_id=tool_call_id,
4003
3965
  mask_output=mask_output,
4004
3966
  extra_content=extra_content,
@@ -4028,7 +3990,7 @@ class ChatAgent(BaseAgent):
4028
3990
 
4029
3991
  # Register tool output for snapshot cleaning if enabled
4030
3992
  if self._enable_snapshot_clean and not mask_output and func_records:
4031
- serialized_result = self._serialize_tool_result(result)
3993
+ serialized_result = self._serialize_tool_result(result_for_memory)
4032
3994
  self._register_tool_output_for_cache(
4033
3995
  func_name,
4034
3996
  tool_call_id,
@@ -4036,14 +3998,74 @@ class ChatAgent(BaseAgent):
4036
3998
  cast(List[MemoryRecord], func_records),
4037
3999
  )
4038
4000
 
4001
+ if isinstance(result, ToolResult) and result.images:
4002
+ try:
4003
+ import base64
4004
+ import io
4005
+
4006
+ try:
4007
+ from PIL import Image
4008
+ except ImportError:
4009
+ logger.warning(
4010
+ f"Tool '{func_name}' returned images but PIL "
4011
+ "is not installed. Install with: pip install "
4012
+ "Pillow. Skipping visual context injection."
4013
+ )
4014
+ # Continue without injecting images
4015
+ result = (
4016
+ result.text if hasattr(result, 'text') else str(result)
4017
+ )
4018
+ else:
4019
+ logger.info(
4020
+ f"Tool '{func_name}' returned ToolResult with "
4021
+ f"{len(result.images)} image(s), injecting into "
4022
+ "context"
4023
+ )
4024
+
4025
+ # Convert base64 images to PIL Image objects
4026
+ pil_images: List[Union[Image.Image, str]] = []
4027
+ for img_data in result.images:
4028
+ if img_data.startswith('data:image/'):
4029
+ # Extract base64 data
4030
+ base64_str = img_data.split(',', 1)[1]
4031
+ img_bytes = base64.b64decode(base64_str)
4032
+ pil_img = Image.open(io.BytesIO(img_bytes))
4033
+ pil_images.append(pil_img)
4034
+
4035
+ if pil_images:
4036
+ # Create a user message with the image(s)
4037
+ visual_msg = BaseMessage.make_user_message(
4038
+ role_name="Tool",
4039
+ content=f"[Visual output from {func_name}]",
4040
+ image_list=pil_images,
4041
+ )
4042
+
4043
+ # Inject into conversation context with slight
4044
+ # timestamp increment
4045
+ self.update_memory(
4046
+ visual_msg,
4047
+ OpenAIBackendRole.USER,
4048
+ timestamp=base_timestamp + 2e-6,
4049
+ return_records=False,
4050
+ )
4051
+ logger.info(
4052
+ f"Successfully injected {len(pil_images)} "
4053
+ "image(s) into agent context"
4054
+ )
4055
+ except Exception as e:
4056
+ logger.error(
4057
+ f"Failed to inject visual content from {func_name}: {e}"
4058
+ )
4059
+
4039
4060
  # Record information about this tool call
4061
+ # Note: tool_record contains the original result for the caller,
4062
+ # while result_for_memory (possibly truncated) is stored in memory
4040
4063
  tool_record = ToolCallingRecord(
4041
4064
  tool_name=func_name,
4042
4065
  args=args,
4043
4066
  result=result,
4044
4067
  tool_call_id=tool_call_id,
4045
4068
  )
4046
-
4047
4069
  self._update_last_tool_call_state(tool_record)
4048
4070
  return tool_record
4049
4071
 
@@ -4077,7 +4099,9 @@ class ChatAgent(BaseAgent):
4077
4099
 
4078
4100
  # Get context for streaming
4079
4101
  try:
4080
- openai_messages, num_tokens = self.memory.get_context()
4102
+ openai_messages, num_tokens = (
4103
+ self._get_context_with_summarization()
4104
+ )
4081
4105
  except RuntimeError as e:
4082
4106
  yield self._step_terminate(e.args[1], [], "max_tokens_exceeded")
4083
4107
  return
@@ -4090,9 +4114,36 @@ class ChatAgent(BaseAgent):
4090
4114
  def _get_token_count(self, content: str) -> int:
4091
4115
  r"""Get token count for content with fallback."""
4092
4116
  if hasattr(self.model_backend, 'token_counter'):
4093
- return len(self.model_backend.token_counter.encode(content))
4094
- else:
4095
- return len(content.split())
4117
+ try:
4118
+ return len(self.model_backend.token_counter.encode(content))
4119
+ except BaseException as e:
4120
+ logger.debug(
4121
+ f"Token counting failed, using char fallback: {e}"
4122
+ )
4123
+ # Conservative estimate: ~3 chars per token
4124
+ return len(content) // 3
4125
+
4126
+ def _warn_stream_accumulate_deprecation(self) -> None:
4127
+ r"""Issue deprecation warning for stream_accumulate default change.
4128
+
4129
+ Only warns once per agent instance, and only if the user didn't
4130
+ explicitly set stream_accumulate.
4131
+ """
4132
+ if not self._stream_accumulate_explicit:
4133
+ import warnings
4134
+
4135
+ warnings.warn(
4136
+ "The default value of 'stream_accumulate' has changed from "
4137
+ "True to False. In streaming mode, each chunk now returns "
4138
+ "only the incremental delta instead of accumulated content. "
4139
+ "To suppress this warning, explicitly set "
4140
+ "stream_accumulate=False (recommended) or stream_accumulate="
4141
+ "True if you need the old behavior.",
4142
+ DeprecationWarning,
4143
+ stacklevel=5,
4144
+ )
4145
+ # Only warn once per agent instance
4146
+ self._stream_accumulate_explicit = True
4096
4147
 
4097
4148
  def _stream_response(
4098
4149
  self,
@@ -4102,6 +4153,8 @@ class ChatAgent(BaseAgent):
4102
4153
  ) -> Generator[ChatAgentResponse, None, None]:
4103
4154
  r"""Internal method to handle streaming responses with tool calls."""
4104
4155
 
4156
+ self._warn_stream_accumulate_deprecation()
4157
+
4105
4158
  tool_call_records: List[ToolCallingRecord] = []
4106
4159
  accumulated_tool_calls: Dict[str, Any] = {}
4107
4160
  step_token_usage = self._create_token_usage_tracker()
@@ -4136,12 +4189,22 @@ class ChatAgent(BaseAgent):
4136
4189
  return
4137
4190
 
4138
4191
  # Handle streaming response
4139
- if isinstance(response, Stream) or inspect.isgenerator(response):
4192
+ # Check for Stream, generator, or third-party wrappers
4193
+ if (
4194
+ isinstance(response, Stream)
4195
+ or inspect.isgenerator(response)
4196
+ or (
4197
+ hasattr(response, '__iter__')
4198
+ and hasattr(response, '__enter__')
4199
+ and not hasattr(response, 'get_final_completion')
4200
+ and not isinstance(response, ChatCompletion)
4201
+ )
4202
+ ):
4140
4203
  (
4141
4204
  stream_completed,
4142
4205
  tool_calls_complete,
4143
4206
  ) = yield from self._process_stream_chunks_with_accumulator(
4144
- response,
4207
+ response, # type: ignore[arg-type]
4145
4208
  content_accumulator,
4146
4209
  accumulated_tool_calls,
4147
4210
  tool_call_records,
@@ -4180,11 +4243,9 @@ class ChatAgent(BaseAgent):
4180
4243
  # Stream completed without tool calls
4181
4244
  accumulated_tool_calls.clear()
4182
4245
  break
4183
- elif hasattr(response, '__enter__') and hasattr(
4184
- response, '__exit__'
4185
- ):
4246
+ elif hasattr(response, 'get_final_completion'):
4186
4247
  # Handle structured output stream (ChatCompletionStreamManager)
4187
- with response as stream:
4248
+ with response as stream: # type: ignore[union-attr]
4188
4249
  parsed_object = None
4189
4250
 
4190
4251
  for event in stream:
@@ -4273,7 +4334,9 @@ class ChatAgent(BaseAgent):
4273
4334
  return
4274
4335
  else:
4275
4336
  # Handle non-streaming response (fallback)
4276
- model_response = self._handle_batch_response(response)
4337
+ model_response = self._handle_batch_response(
4338
+ response # type: ignore[arg-type]
4339
+ )
4277
4340
  yield self._convert_to_chatagent_response(
4278
4341
  model_response,
4279
4342
  tool_call_records,
@@ -4410,12 +4473,20 @@ class ChatAgent(BaseAgent):
4410
4473
  content_accumulator.get_full_reasoning_content()
4411
4474
  or None
4412
4475
  )
4476
+ # In delta mode, final response content should be empty
4477
+ # since all content was already yielded incrementally
4478
+ display_content = (
4479
+ final_content if self.stream_accumulate else ""
4480
+ )
4481
+ display_reasoning = (
4482
+ final_reasoning if self.stream_accumulate else None
4483
+ )
4413
4484
  final_message = BaseMessage(
4414
4485
  role_name=self.role_name,
4415
4486
  role_type=self.role_type,
4416
4487
  meta_dict={},
4417
- content=final_content,
4418
- reasoning_content=final_reasoning,
4488
+ content=display_content,
4489
+ reasoning_content=display_reasoning,
4419
4490
  )
4420
4491
 
4421
4492
  if response_format:
@@ -4466,13 +4537,52 @@ class ChatAgent(BaseAgent):
4466
4537
  bool: True if any tool call is complete, False otherwise.
4467
4538
  """
4468
4539
 
4540
+ index_map_key = '_index_to_key_map'
4541
+ if index_map_key not in accumulated_tool_calls:
4542
+ accumulated_tool_calls[index_map_key] = {}
4543
+ index_map = accumulated_tool_calls[index_map_key]
4544
+
4469
4545
  for delta_tool_call in tool_call_deltas:
4470
- index = delta_tool_call.index
4546
+ index = getattr(delta_tool_call, 'index', None)
4471
4547
  tool_call_id = getattr(delta_tool_call, 'id', None)
4472
4548
 
4549
+ # Determine entry key
4550
+ if index is not None:
4551
+ index_str = str(index)
4552
+ if tool_call_id:
4553
+ # New ID provided: check if it differs from current mapping
4554
+ current_key = index_map.get(index_str)
4555
+ if current_key is None:
4556
+ # First time seeing this index, use tool_call_id as key
4557
+ entry_key = tool_call_id
4558
+ elif current_key in accumulated_tool_calls:
4559
+ existing_id = accumulated_tool_calls[current_key].get(
4560
+ 'id'
4561
+ )
4562
+ if existing_id and existing_id != tool_call_id:
4563
+ # ID changed: use new ID as key
4564
+ entry_key = tool_call_id
4565
+ else:
4566
+ # No existing ID or same ID: keep current key
4567
+ entry_key = current_key
4568
+ else:
4569
+ entry_key = current_key
4570
+ # Update mapping
4571
+ index_map[index_str] = entry_key
4572
+ else:
4573
+ # No ID in this chunk: use existing mapping or index as
4574
+ # string
4575
+ entry_key = index_map.get(index_str, index_str)
4576
+ if index_str not in index_map:
4577
+ index_map[index_str] = entry_key
4578
+ elif tool_call_id is not None:
4579
+ entry_key = tool_call_id
4580
+ else:
4581
+ entry_key = '0' # Default fallback as string
4582
+
4473
4583
  # Initialize tool call entry if not exists
4474
- if index not in accumulated_tool_calls:
4475
- accumulated_tool_calls[index] = {
4584
+ if entry_key not in accumulated_tool_calls:
4585
+ accumulated_tool_calls[entry_key] = {
4476
4586
  'id': '',
4477
4587
  'type': 'function',
4478
4588
  'function': {'name': '', 'arguments': ''},
@@ -4480,7 +4590,7 @@ class ChatAgent(BaseAgent):
4480
4590
  'complete': False,
4481
4591
  }
4482
4592
 
4483
- tool_call_entry = accumulated_tool_calls[index]
4593
+ tool_call_entry = accumulated_tool_calls[entry_key]
4484
4594
 
4485
4595
  # Accumulate tool call data
4486
4596
  if tool_call_id:
@@ -4512,6 +4622,9 @@ class ChatAgent(BaseAgent):
4512
4622
  # Check if any tool calls are complete
4513
4623
  any_complete = False
4514
4624
  for _index, tool_call_entry in accumulated_tool_calls.items():
4625
+ # Skip internal mapping key
4626
+ if _index == '_index_to_key_map':
4627
+ continue
4515
4628
  if (
4516
4629
  tool_call_entry['id']
4517
4630
  and tool_call_entry['function']['name']
@@ -4539,6 +4652,9 @@ class ChatAgent(BaseAgent):
4539
4652
 
4540
4653
  tool_calls_to_execute = []
4541
4654
  for _tool_call_index, tool_call_data in accumulated_tool_calls.items():
4655
+ # Skip internal mapping key
4656
+ if _tool_call_index == '_index_to_key_map':
4657
+ continue
4542
4658
  if tool_call_data.get('complete', False):
4543
4659
  tool_calls_to_execute.append(tool_call_data)
4544
4660
 
@@ -4618,6 +4734,27 @@ class ChatAgent(BaseAgent):
4618
4734
  tool = self._internal_tools[function_name]
4619
4735
  try:
4620
4736
  result = tool(**args)
4737
+
4738
+ # Handle mask_tool_output
4739
+ if self.mask_tool_output:
4740
+ with self._secure_result_store_lock:
4741
+ self._secure_result_store[tool_call_id] = result
4742
+ result = (
4743
+ "[The tool has been executed successfully, but the"
4744
+ " output from the tool is masked. You can move"
4745
+ " forward]"
4746
+ )
4747
+
4748
+ # Truncate tool result if it exceeds the maximum token
4749
+ # limit. This prevents single tool calls from exceeding
4750
+ # context window
4751
+ truncated_result, was_truncated = (
4752
+ self._truncate_tool_result(function_name, result)
4753
+ )
4754
+ result_for_memory = (
4755
+ truncated_result if was_truncated else result
4756
+ )
4757
+
4621
4758
  # First, create and record the assistant message with tool
4622
4759
  # call
4623
4760
  assist_msg = FunctionCallingMessage(
@@ -4638,8 +4775,9 @@ class ChatAgent(BaseAgent):
4638
4775
  meta_dict=None,
4639
4776
  content="",
4640
4777
  func_name=function_name,
4641
- result=result,
4778
+ result=result_for_memory,
4642
4779
  tool_call_id=tool_call_id,
4780
+ mask_output=self.mask_tool_output,
4643
4781
  extra_content=extra_content,
4644
4782
  )
4645
4783
 
@@ -4675,7 +4813,7 @@ class ChatAgent(BaseAgent):
4675
4813
  f"Error executing tool '{function_name}': {e!s}"
4676
4814
  )
4677
4815
  result = {"error": error_msg}
4678
- logger.warning(error_msg)
4816
+ logger.warning(f"{error_msg} with result: {result}")
4679
4817
 
4680
4818
  # Record error response
4681
4819
  func_msg = FunctionCallingMessage(
@@ -4700,10 +4838,32 @@ class ChatAgent(BaseAgent):
4700
4838
  self._update_last_tool_call_state(tool_record)
4701
4839
  return tool_record
4702
4840
  else:
4703
- logger.warning(
4704
- f"Tool '{function_name}' not found in internal tools"
4841
+ error_msg = (
4842
+ f"Tool '{function_name}' not found in registered tools"
4843
+ )
4844
+ result = {"error": error_msg}
4845
+ logger.warning(error_msg)
4846
+
4847
+ func_msg = FunctionCallingMessage(
4848
+ role_name=self.role_name,
4849
+ role_type=self.role_type,
4850
+ meta_dict=None,
4851
+ content="",
4852
+ func_name=function_name,
4853
+ result=result,
4854
+ tool_call_id=tool_call_id,
4855
+ extra_content=extra_content,
4705
4856
  )
4706
- return None
4857
+ self.update_memory(func_msg, OpenAIBackendRole.FUNCTION)
4858
+
4859
+ tool_record = ToolCallingRecord(
4860
+ tool_name=function_name,
4861
+ args=args,
4862
+ result=result,
4863
+ tool_call_id=tool_call_id,
4864
+ )
4865
+ self._update_last_tool_call_state(tool_record)
4866
+ return tool_record
4707
4867
 
4708
4868
  except Exception as e:
4709
4869
  logger.error(f"Error processing tool call: {e}")
@@ -4772,6 +4932,26 @@ class ChatAgent(BaseAgent):
4772
4932
  None, functools.partial(tool, **args)
4773
4933
  )
4774
4934
 
4935
+ # Handle mask_tool_output
4936
+ if self.mask_tool_output:
4937
+ with self._secure_result_store_lock:
4938
+ self._secure_result_store[tool_call_id] = result
4939
+ result = (
4940
+ "[The tool has been executed successfully, but the"
4941
+ " output from the tool is masked. You can move"
4942
+ " forward]"
4943
+ )
4944
+
4945
+ # Truncate tool result if it exceeds the maximum token
4946
+ # limit. This prevents single tool calls from exceeding
4947
+ # context window
4948
+ truncated_result, was_truncated = (
4949
+ self._truncate_tool_result(function_name, result)
4950
+ )
4951
+ result_for_memory = (
4952
+ truncated_result if was_truncated else result
4953
+ )
4954
+
4775
4955
  # Create the tool response message
4776
4956
  func_msg = FunctionCallingMessage(
4777
4957
  role_name=self.role_name,
@@ -4779,8 +4959,9 @@ class ChatAgent(BaseAgent):
4779
4959
  meta_dict=None,
4780
4960
  content="",
4781
4961
  func_name=function_name,
4782
- result=result,
4962
+ result=result_for_memory,
4783
4963
  tool_call_id=tool_call_id,
4964
+ mask_output=self.mask_tool_output,
4784
4965
  extra_content=extra_content,
4785
4966
  )
4786
4967
  func_ts = time.time_ns() / 1_000_000_000
@@ -4804,7 +4985,7 @@ class ChatAgent(BaseAgent):
4804
4985
  f"Error executing async tool '{function_name}': {e!s}"
4805
4986
  )
4806
4987
  result = {"error": error_msg}
4807
- logger.warning(error_msg)
4988
+ logger.warning(f"{error_msg} with result: {result}")
4808
4989
 
4809
4990
  # Record error response
4810
4991
  func_msg = FunctionCallingMessage(
@@ -4833,10 +5014,32 @@ class ChatAgent(BaseAgent):
4833
5014
  self._update_last_tool_call_state(tool_record)
4834
5015
  return tool_record
4835
5016
  else:
4836
- logger.warning(
4837
- f"Tool '{function_name}' not found in internal tools"
5017
+ error_msg = (
5018
+ f"Tool '{function_name}' not found in registered tools"
5019
+ )
5020
+ result = {"error": error_msg}
5021
+ logger.warning(error_msg)
5022
+
5023
+ func_msg = FunctionCallingMessage(
5024
+ role_name=self.role_name,
5025
+ role_type=self.role_type,
5026
+ meta_dict=None,
5027
+ content="",
5028
+ func_name=function_name,
5029
+ result=result,
5030
+ tool_call_id=tool_call_id,
5031
+ extra_content=extra_content,
5032
+ )
5033
+ self.update_memory(func_msg, OpenAIBackendRole.FUNCTION)
5034
+
5035
+ tool_record = ToolCallingRecord(
5036
+ tool_name=function_name,
5037
+ args=args,
5038
+ result=result,
5039
+ tool_call_id=tool_call_id,
4838
5040
  )
4839
- return None
5041
+ self._update_last_tool_call_state(tool_record)
5042
+ return tool_record
4840
5043
 
4841
5044
  except Exception as e:
4842
5045
  logger.error(f"Error processing async tool call: {e}")
@@ -4882,7 +5085,10 @@ class ChatAgent(BaseAgent):
4882
5085
 
4883
5086
  # Get context for streaming
4884
5087
  try:
4885
- openai_messages, num_tokens = self.memory.get_context()
5088
+ (
5089
+ openai_messages,
5090
+ num_tokens,
5091
+ ) = await self._get_context_with_summarization_async()
4886
5092
  except RuntimeError as e:
4887
5093
  yield self._step_terminate(e.args[1], [], "max_tokens_exceeded")
4888
5094
  return
@@ -4910,6 +5116,8 @@ class ChatAgent(BaseAgent):
4910
5116
  ) -> AsyncGenerator[ChatAgentResponse, None]:
4911
5117
  r"""Async method to handle streaming responses with tool calls."""
4912
5118
 
5119
+ self._warn_stream_accumulate_deprecation()
5120
+
4913
5121
  tool_call_records: List[ToolCallingRecord] = []
4914
5122
  accumulated_tool_calls: Dict[str, Any] = {}
4915
5123
  step_token_usage = self._create_token_usage_tracker()
@@ -4945,11 +5153,16 @@ class ChatAgent(BaseAgent):
4945
5153
  return
4946
5154
 
4947
5155
  # Handle streaming response
4948
- # Note: Also check for async generators since some model backends
4949
- # (e.g., GeminiModel) wrap AsyncStream in async generators for
4950
- # additional processing
4951
- if isinstance(response, AsyncStream) or inspect.isasyncgen(
4952
- response
5156
+ # Check for AsyncStream, async generator, or third-party wrappers
5157
+ if (
5158
+ isinstance(response, AsyncStream)
5159
+ or inspect.isasyncgen(response)
5160
+ or (
5161
+ hasattr(response, '__aiter__')
5162
+ and hasattr(response, '__aenter__')
5163
+ and not hasattr(response, 'get_final_completion')
5164
+ and not isinstance(response, ChatCompletion)
5165
+ )
4953
5166
  ):
4954
5167
  stream_completed = False
4955
5168
  tool_calls_complete = False
@@ -4958,7 +5171,7 @@ class ChatAgent(BaseAgent):
4958
5171
  async for (
4959
5172
  item
4960
5173
  ) in self._aprocess_stream_chunks_with_accumulator(
4961
- response,
5174
+ response, # type: ignore[arg-type]
4962
5175
  content_accumulator,
4963
5176
  accumulated_tool_calls,
4964
5177
  tool_call_records,
@@ -5005,12 +5218,10 @@ class ChatAgent(BaseAgent):
5005
5218
  # Stream completed without tool calls
5006
5219
  accumulated_tool_calls.clear()
5007
5220
  break
5008
- elif hasattr(response, '__aenter__') and hasattr(
5009
- response, '__aexit__'
5010
- ):
5221
+ elif hasattr(response, 'get_final_completion'):
5011
5222
  # Handle structured output stream
5012
5223
  # (AsyncChatCompletionStreamManager)
5013
- async with response as stream:
5224
+ async with response as stream: # type: ignore[union-attr]
5014
5225
  parsed_object = None
5015
5226
 
5016
5227
  async for event in stream:
@@ -5101,7 +5312,9 @@ class ChatAgent(BaseAgent):
5101
5312
  return
5102
5313
  else:
5103
5314
  # Handle non-streaming response (fallback)
5104
- model_response = self._handle_batch_response(response)
5315
+ model_response = self._handle_batch_response(
5316
+ response # type: ignore[arg-type]
5317
+ )
5105
5318
  yield self._convert_to_chatagent_response(
5106
5319
  model_response,
5107
5320
  tool_call_records,
@@ -5279,12 +5492,20 @@ class ChatAgent(BaseAgent):
5279
5492
  content_accumulator.get_full_reasoning_content()
5280
5493
  or None
5281
5494
  )
5495
+ # In delta mode, final response content should be empty
5496
+ # since all content was already yielded incrementally
5497
+ display_content = (
5498
+ final_content if self.stream_accumulate else ""
5499
+ )
5500
+ display_reasoning = (
5501
+ final_reasoning if self.stream_accumulate else None
5502
+ )
5282
5503
  final_message = BaseMessage(
5283
5504
  role_name=self.role_name,
5284
5505
  role_type=self.role_type,
5285
5506
  meta_dict={},
5286
- content=final_content,
5287
- reasoning_content=final_reasoning,
5507
+ content=display_content,
5508
+ reasoning_content=display_reasoning,
5288
5509
  )
5289
5510
 
5290
5511
  if response_format:
@@ -5332,6 +5553,9 @@ class ChatAgent(BaseAgent):
5332
5553
  # statuses immediately
5333
5554
  tool_tasks = []
5334
5555
  for _tool_call_index, tool_call_data in accumulated_tool_calls.items():
5556
+ # Skip internal mapping key
5557
+ if _tool_call_index == '_index_to_key_map':
5558
+ continue
5335
5559
  if tool_call_data.get('complete', False):
5336
5560
  function_name = tool_call_data['function']['name']
5337
5561
  try: