aiecs 1.0.1__py3-none-any.whl → 1.7.17__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 aiecs might be problematic. Click here for more details.

Files changed (340) hide show
  1. aiecs/__init__.py +13 -16
  2. aiecs/__main__.py +7 -7
  3. aiecs/aiecs_client.py +269 -75
  4. aiecs/application/executors/operation_executor.py +79 -54
  5. aiecs/application/knowledge_graph/__init__.py +7 -0
  6. aiecs/application/knowledge_graph/builder/__init__.py +37 -0
  7. aiecs/application/knowledge_graph/builder/data_quality.py +302 -0
  8. aiecs/application/knowledge_graph/builder/data_reshaping.py +293 -0
  9. aiecs/application/knowledge_graph/builder/document_builder.py +369 -0
  10. aiecs/application/knowledge_graph/builder/graph_builder.py +490 -0
  11. aiecs/application/knowledge_graph/builder/import_optimizer.py +396 -0
  12. aiecs/application/knowledge_graph/builder/schema_inference.py +462 -0
  13. aiecs/application/knowledge_graph/builder/schema_mapping.py +563 -0
  14. aiecs/application/knowledge_graph/builder/structured_pipeline.py +1384 -0
  15. aiecs/application/knowledge_graph/builder/text_chunker.py +317 -0
  16. aiecs/application/knowledge_graph/extractors/__init__.py +27 -0
  17. aiecs/application/knowledge_graph/extractors/base.py +98 -0
  18. aiecs/application/knowledge_graph/extractors/llm_entity_extractor.py +422 -0
  19. aiecs/application/knowledge_graph/extractors/llm_relation_extractor.py +347 -0
  20. aiecs/application/knowledge_graph/extractors/ner_entity_extractor.py +241 -0
  21. aiecs/application/knowledge_graph/fusion/__init__.py +78 -0
  22. aiecs/application/knowledge_graph/fusion/ab_testing.py +395 -0
  23. aiecs/application/knowledge_graph/fusion/abbreviation_expander.py +327 -0
  24. aiecs/application/knowledge_graph/fusion/alias_index.py +597 -0
  25. aiecs/application/knowledge_graph/fusion/alias_matcher.py +384 -0
  26. aiecs/application/knowledge_graph/fusion/cache_coordinator.py +343 -0
  27. aiecs/application/knowledge_graph/fusion/entity_deduplicator.py +433 -0
  28. aiecs/application/knowledge_graph/fusion/entity_linker.py +511 -0
  29. aiecs/application/knowledge_graph/fusion/evaluation_dataset.py +240 -0
  30. aiecs/application/knowledge_graph/fusion/knowledge_fusion.py +632 -0
  31. aiecs/application/knowledge_graph/fusion/matching_config.py +489 -0
  32. aiecs/application/knowledge_graph/fusion/name_normalizer.py +352 -0
  33. aiecs/application/knowledge_graph/fusion/relation_deduplicator.py +183 -0
  34. aiecs/application/knowledge_graph/fusion/semantic_name_matcher.py +464 -0
  35. aiecs/application/knowledge_graph/fusion/similarity_pipeline.py +534 -0
  36. aiecs/application/knowledge_graph/pattern_matching/__init__.py +21 -0
  37. aiecs/application/knowledge_graph/pattern_matching/pattern_matcher.py +342 -0
  38. aiecs/application/knowledge_graph/pattern_matching/query_executor.py +366 -0
  39. aiecs/application/knowledge_graph/profiling/__init__.py +12 -0
  40. aiecs/application/knowledge_graph/profiling/query_plan_visualizer.py +195 -0
  41. aiecs/application/knowledge_graph/profiling/query_profiler.py +223 -0
  42. aiecs/application/knowledge_graph/reasoning/__init__.py +27 -0
  43. aiecs/application/knowledge_graph/reasoning/evidence_synthesis.py +341 -0
  44. aiecs/application/knowledge_graph/reasoning/inference_engine.py +500 -0
  45. aiecs/application/knowledge_graph/reasoning/logic_form_parser.py +163 -0
  46. aiecs/application/knowledge_graph/reasoning/logic_parser/__init__.py +79 -0
  47. aiecs/application/knowledge_graph/reasoning/logic_parser/ast_builder.py +513 -0
  48. aiecs/application/knowledge_graph/reasoning/logic_parser/ast_nodes.py +913 -0
  49. aiecs/application/knowledge_graph/reasoning/logic_parser/ast_validator.py +866 -0
  50. aiecs/application/knowledge_graph/reasoning/logic_parser/error_handler.py +475 -0
  51. aiecs/application/knowledge_graph/reasoning/logic_parser/parser.py +396 -0
  52. aiecs/application/knowledge_graph/reasoning/logic_parser/query_context.py +208 -0
  53. aiecs/application/knowledge_graph/reasoning/logic_query_integration.py +170 -0
  54. aiecs/application/knowledge_graph/reasoning/query_planner.py +855 -0
  55. aiecs/application/knowledge_graph/reasoning/reasoning_engine.py +518 -0
  56. aiecs/application/knowledge_graph/retrieval/__init__.py +27 -0
  57. aiecs/application/knowledge_graph/retrieval/query_intent_classifier.py +211 -0
  58. aiecs/application/knowledge_graph/retrieval/retrieval_strategies.py +592 -0
  59. aiecs/application/knowledge_graph/retrieval/strategy_types.py +23 -0
  60. aiecs/application/knowledge_graph/search/__init__.py +59 -0
  61. aiecs/application/knowledge_graph/search/hybrid_search.py +457 -0
  62. aiecs/application/knowledge_graph/search/reranker.py +293 -0
  63. aiecs/application/knowledge_graph/search/reranker_strategies.py +535 -0
  64. aiecs/application/knowledge_graph/search/text_similarity.py +392 -0
  65. aiecs/application/knowledge_graph/traversal/__init__.py +15 -0
  66. aiecs/application/knowledge_graph/traversal/enhanced_traversal.py +305 -0
  67. aiecs/application/knowledge_graph/traversal/path_scorer.py +271 -0
  68. aiecs/application/knowledge_graph/validators/__init__.py +13 -0
  69. aiecs/application/knowledge_graph/validators/relation_validator.py +239 -0
  70. aiecs/application/knowledge_graph/visualization/__init__.py +11 -0
  71. aiecs/application/knowledge_graph/visualization/graph_visualizer.py +313 -0
  72. aiecs/common/__init__.py +9 -0
  73. aiecs/common/knowledge_graph/__init__.py +17 -0
  74. aiecs/common/knowledge_graph/runnable.py +471 -0
  75. aiecs/config/__init__.py +20 -5
  76. aiecs/config/config.py +762 -31
  77. aiecs/config/graph_config.py +131 -0
  78. aiecs/config/tool_config.py +435 -0
  79. aiecs/core/__init__.py +29 -13
  80. aiecs/core/interface/__init__.py +2 -2
  81. aiecs/core/interface/execution_interface.py +22 -22
  82. aiecs/core/interface/storage_interface.py +37 -88
  83. aiecs/core/registry/__init__.py +31 -0
  84. aiecs/core/registry/service_registry.py +92 -0
  85. aiecs/domain/__init__.py +270 -1
  86. aiecs/domain/agent/__init__.py +191 -0
  87. aiecs/domain/agent/base_agent.py +3949 -0
  88. aiecs/domain/agent/exceptions.py +99 -0
  89. aiecs/domain/agent/graph_aware_mixin.py +569 -0
  90. aiecs/domain/agent/hybrid_agent.py +1731 -0
  91. aiecs/domain/agent/integration/__init__.py +29 -0
  92. aiecs/domain/agent/integration/context_compressor.py +216 -0
  93. aiecs/domain/agent/integration/context_engine_adapter.py +587 -0
  94. aiecs/domain/agent/integration/protocols.py +281 -0
  95. aiecs/domain/agent/integration/retry_policy.py +218 -0
  96. aiecs/domain/agent/integration/role_config.py +213 -0
  97. aiecs/domain/agent/knowledge_aware_agent.py +1892 -0
  98. aiecs/domain/agent/lifecycle.py +291 -0
  99. aiecs/domain/agent/llm_agent.py +692 -0
  100. aiecs/domain/agent/memory/__init__.py +12 -0
  101. aiecs/domain/agent/memory/conversation.py +1124 -0
  102. aiecs/domain/agent/migration/__init__.py +14 -0
  103. aiecs/domain/agent/migration/conversion.py +163 -0
  104. aiecs/domain/agent/migration/legacy_wrapper.py +86 -0
  105. aiecs/domain/agent/models.py +894 -0
  106. aiecs/domain/agent/observability.py +479 -0
  107. aiecs/domain/agent/persistence.py +449 -0
  108. aiecs/domain/agent/prompts/__init__.py +29 -0
  109. aiecs/domain/agent/prompts/builder.py +159 -0
  110. aiecs/domain/agent/prompts/formatters.py +187 -0
  111. aiecs/domain/agent/prompts/template.py +255 -0
  112. aiecs/domain/agent/registry.py +253 -0
  113. aiecs/domain/agent/tool_agent.py +444 -0
  114. aiecs/domain/agent/tools/__init__.py +15 -0
  115. aiecs/domain/agent/tools/schema_generator.py +377 -0
  116. aiecs/domain/community/__init__.py +155 -0
  117. aiecs/domain/community/agent_adapter.py +469 -0
  118. aiecs/domain/community/analytics.py +432 -0
  119. aiecs/domain/community/collaborative_workflow.py +648 -0
  120. aiecs/domain/community/communication_hub.py +634 -0
  121. aiecs/domain/community/community_builder.py +320 -0
  122. aiecs/domain/community/community_integration.py +796 -0
  123. aiecs/domain/community/community_manager.py +803 -0
  124. aiecs/domain/community/decision_engine.py +849 -0
  125. aiecs/domain/community/exceptions.py +231 -0
  126. aiecs/domain/community/models/__init__.py +33 -0
  127. aiecs/domain/community/models/community_models.py +234 -0
  128. aiecs/domain/community/resource_manager.py +461 -0
  129. aiecs/domain/community/shared_context_manager.py +589 -0
  130. aiecs/domain/context/__init__.py +40 -10
  131. aiecs/domain/context/context_engine.py +1910 -0
  132. aiecs/domain/context/conversation_models.py +87 -53
  133. aiecs/domain/context/graph_memory.py +582 -0
  134. aiecs/domain/execution/model.py +12 -4
  135. aiecs/domain/knowledge_graph/__init__.py +19 -0
  136. aiecs/domain/knowledge_graph/models/__init__.py +52 -0
  137. aiecs/domain/knowledge_graph/models/entity.py +148 -0
  138. aiecs/domain/knowledge_graph/models/evidence.py +178 -0
  139. aiecs/domain/knowledge_graph/models/inference_rule.py +184 -0
  140. aiecs/domain/knowledge_graph/models/path.py +171 -0
  141. aiecs/domain/knowledge_graph/models/path_pattern.py +171 -0
  142. aiecs/domain/knowledge_graph/models/query.py +261 -0
  143. aiecs/domain/knowledge_graph/models/query_plan.py +181 -0
  144. aiecs/domain/knowledge_graph/models/relation.py +202 -0
  145. aiecs/domain/knowledge_graph/schema/__init__.py +23 -0
  146. aiecs/domain/knowledge_graph/schema/entity_type.py +131 -0
  147. aiecs/domain/knowledge_graph/schema/graph_schema.py +253 -0
  148. aiecs/domain/knowledge_graph/schema/property_schema.py +143 -0
  149. aiecs/domain/knowledge_graph/schema/relation_type.py +163 -0
  150. aiecs/domain/knowledge_graph/schema/schema_manager.py +691 -0
  151. aiecs/domain/knowledge_graph/schema/type_enums.py +209 -0
  152. aiecs/domain/task/dsl_processor.py +172 -56
  153. aiecs/domain/task/model.py +20 -8
  154. aiecs/domain/task/task_context.py +27 -24
  155. aiecs/infrastructure/__init__.py +0 -2
  156. aiecs/infrastructure/graph_storage/__init__.py +11 -0
  157. aiecs/infrastructure/graph_storage/base.py +837 -0
  158. aiecs/infrastructure/graph_storage/batch_operations.py +458 -0
  159. aiecs/infrastructure/graph_storage/cache.py +424 -0
  160. aiecs/infrastructure/graph_storage/distributed.py +223 -0
  161. aiecs/infrastructure/graph_storage/error_handling.py +380 -0
  162. aiecs/infrastructure/graph_storage/graceful_degradation.py +294 -0
  163. aiecs/infrastructure/graph_storage/health_checks.py +378 -0
  164. aiecs/infrastructure/graph_storage/in_memory.py +1197 -0
  165. aiecs/infrastructure/graph_storage/index_optimization.py +446 -0
  166. aiecs/infrastructure/graph_storage/lazy_loading.py +431 -0
  167. aiecs/infrastructure/graph_storage/metrics.py +344 -0
  168. aiecs/infrastructure/graph_storage/migration.py +400 -0
  169. aiecs/infrastructure/graph_storage/pagination.py +483 -0
  170. aiecs/infrastructure/graph_storage/performance_monitoring.py +456 -0
  171. aiecs/infrastructure/graph_storage/postgres.py +1563 -0
  172. aiecs/infrastructure/graph_storage/property_storage.py +353 -0
  173. aiecs/infrastructure/graph_storage/protocols.py +76 -0
  174. aiecs/infrastructure/graph_storage/query_optimizer.py +642 -0
  175. aiecs/infrastructure/graph_storage/schema_cache.py +290 -0
  176. aiecs/infrastructure/graph_storage/sqlite.py +1373 -0
  177. aiecs/infrastructure/graph_storage/streaming.py +487 -0
  178. aiecs/infrastructure/graph_storage/tenant.py +412 -0
  179. aiecs/infrastructure/messaging/celery_task_manager.py +92 -54
  180. aiecs/infrastructure/messaging/websocket_manager.py +51 -35
  181. aiecs/infrastructure/monitoring/__init__.py +22 -0
  182. aiecs/infrastructure/monitoring/executor_metrics.py +45 -11
  183. aiecs/infrastructure/monitoring/global_metrics_manager.py +212 -0
  184. aiecs/infrastructure/monitoring/structured_logger.py +3 -7
  185. aiecs/infrastructure/monitoring/tracing_manager.py +63 -35
  186. aiecs/infrastructure/persistence/__init__.py +14 -1
  187. aiecs/infrastructure/persistence/context_engine_client.py +184 -0
  188. aiecs/infrastructure/persistence/database_manager.py +67 -43
  189. aiecs/infrastructure/persistence/file_storage.py +180 -103
  190. aiecs/infrastructure/persistence/redis_client.py +74 -21
  191. aiecs/llm/__init__.py +73 -25
  192. aiecs/llm/callbacks/__init__.py +11 -0
  193. aiecs/llm/{custom_callbacks.py → callbacks/custom_callbacks.py} +26 -19
  194. aiecs/llm/client_factory.py +230 -37
  195. aiecs/llm/client_resolver.py +155 -0
  196. aiecs/llm/clients/__init__.py +38 -0
  197. aiecs/llm/clients/base_client.py +328 -0
  198. aiecs/llm/clients/google_function_calling_mixin.py +415 -0
  199. aiecs/llm/clients/googleai_client.py +314 -0
  200. aiecs/llm/clients/openai_client.py +158 -0
  201. aiecs/llm/clients/openai_compatible_mixin.py +367 -0
  202. aiecs/llm/clients/vertex_client.py +1186 -0
  203. aiecs/llm/clients/xai_client.py +201 -0
  204. aiecs/llm/config/__init__.py +51 -0
  205. aiecs/llm/config/config_loader.py +272 -0
  206. aiecs/llm/config/config_validator.py +206 -0
  207. aiecs/llm/config/model_config.py +143 -0
  208. aiecs/llm/protocols.py +149 -0
  209. aiecs/llm/utils/__init__.py +10 -0
  210. aiecs/llm/utils/validate_config.py +89 -0
  211. aiecs/main.py +140 -121
  212. aiecs/scripts/aid/VERSION_MANAGEMENT.md +138 -0
  213. aiecs/scripts/aid/__init__.py +19 -0
  214. aiecs/scripts/aid/module_checker.py +499 -0
  215. aiecs/scripts/aid/version_manager.py +235 -0
  216. aiecs/scripts/{DEPENDENCY_SYSTEM_SUMMARY.md → dependance_check/DEPENDENCY_SYSTEM_SUMMARY.md} +1 -0
  217. aiecs/scripts/{README_DEPENDENCY_CHECKER.md → dependance_check/README_DEPENDENCY_CHECKER.md} +1 -0
  218. aiecs/scripts/dependance_check/__init__.py +15 -0
  219. aiecs/scripts/dependance_check/dependency_checker.py +1835 -0
  220. aiecs/scripts/{dependency_fixer.py → dependance_check/dependency_fixer.py} +192 -90
  221. aiecs/scripts/{download_nlp_data.py → dependance_check/download_nlp_data.py} +203 -71
  222. aiecs/scripts/dependance_patch/__init__.py +7 -0
  223. aiecs/scripts/dependance_patch/fix_weasel/__init__.py +11 -0
  224. aiecs/scripts/{fix_weasel_validator.py → dependance_patch/fix_weasel/fix_weasel_validator.py} +21 -14
  225. aiecs/scripts/{patch_weasel_library.sh → dependance_patch/fix_weasel/patch_weasel_library.sh} +1 -1
  226. aiecs/scripts/knowledge_graph/__init__.py +3 -0
  227. aiecs/scripts/knowledge_graph/run_threshold_experiments.py +212 -0
  228. aiecs/scripts/migrations/multi_tenancy/README.md +142 -0
  229. aiecs/scripts/tools_develop/README.md +671 -0
  230. aiecs/scripts/tools_develop/README_CONFIG_CHECKER.md +273 -0
  231. aiecs/scripts/tools_develop/TOOLS_CONFIG_GUIDE.md +1287 -0
  232. aiecs/scripts/tools_develop/TOOL_AUTO_DISCOVERY.md +234 -0
  233. aiecs/scripts/tools_develop/__init__.py +21 -0
  234. aiecs/scripts/tools_develop/check_all_tools_config.py +548 -0
  235. aiecs/scripts/tools_develop/check_type_annotations.py +257 -0
  236. aiecs/scripts/tools_develop/pre-commit-schema-coverage.sh +66 -0
  237. aiecs/scripts/tools_develop/schema_coverage.py +511 -0
  238. aiecs/scripts/tools_develop/validate_tool_schemas.py +475 -0
  239. aiecs/scripts/tools_develop/verify_executor_config_fix.py +98 -0
  240. aiecs/scripts/tools_develop/verify_tools.py +352 -0
  241. aiecs/tasks/__init__.py +0 -1
  242. aiecs/tasks/worker.py +115 -47
  243. aiecs/tools/__init__.py +194 -72
  244. aiecs/tools/apisource/__init__.py +99 -0
  245. aiecs/tools/apisource/intelligence/__init__.py +19 -0
  246. aiecs/tools/apisource/intelligence/data_fusion.py +632 -0
  247. aiecs/tools/apisource/intelligence/query_analyzer.py +417 -0
  248. aiecs/tools/apisource/intelligence/search_enhancer.py +385 -0
  249. aiecs/tools/apisource/monitoring/__init__.py +9 -0
  250. aiecs/tools/apisource/monitoring/metrics.py +330 -0
  251. aiecs/tools/apisource/providers/__init__.py +112 -0
  252. aiecs/tools/apisource/providers/base.py +671 -0
  253. aiecs/tools/apisource/providers/census.py +397 -0
  254. aiecs/tools/apisource/providers/fred.py +535 -0
  255. aiecs/tools/apisource/providers/newsapi.py +409 -0
  256. aiecs/tools/apisource/providers/worldbank.py +352 -0
  257. aiecs/tools/apisource/reliability/__init__.py +12 -0
  258. aiecs/tools/apisource/reliability/error_handler.py +363 -0
  259. aiecs/tools/apisource/reliability/fallback_strategy.py +376 -0
  260. aiecs/tools/apisource/tool.py +832 -0
  261. aiecs/tools/apisource/utils/__init__.py +9 -0
  262. aiecs/tools/apisource/utils/validators.py +334 -0
  263. aiecs/tools/base_tool.py +415 -21
  264. aiecs/tools/docs/__init__.py +121 -0
  265. aiecs/tools/docs/ai_document_orchestrator.py +607 -0
  266. aiecs/tools/docs/ai_document_writer_orchestrator.py +2350 -0
  267. aiecs/tools/docs/content_insertion_tool.py +1320 -0
  268. aiecs/tools/docs/document_creator_tool.py +1464 -0
  269. aiecs/tools/docs/document_layout_tool.py +1160 -0
  270. aiecs/tools/docs/document_parser_tool.py +1016 -0
  271. aiecs/tools/docs/document_writer_tool.py +2008 -0
  272. aiecs/tools/knowledge_graph/__init__.py +17 -0
  273. aiecs/tools/knowledge_graph/graph_reasoning_tool.py +807 -0
  274. aiecs/tools/knowledge_graph/graph_search_tool.py +944 -0
  275. aiecs/tools/knowledge_graph/kg_builder_tool.py +524 -0
  276. aiecs/tools/langchain_adapter.py +300 -138
  277. aiecs/tools/schema_generator.py +455 -0
  278. aiecs/tools/search_tool/__init__.py +100 -0
  279. aiecs/tools/search_tool/analyzers.py +581 -0
  280. aiecs/tools/search_tool/cache.py +264 -0
  281. aiecs/tools/search_tool/constants.py +128 -0
  282. aiecs/tools/search_tool/context.py +224 -0
  283. aiecs/tools/search_tool/core.py +778 -0
  284. aiecs/tools/search_tool/deduplicator.py +119 -0
  285. aiecs/tools/search_tool/error_handler.py +242 -0
  286. aiecs/tools/search_tool/metrics.py +343 -0
  287. aiecs/tools/search_tool/rate_limiter.py +172 -0
  288. aiecs/tools/search_tool/schemas.py +275 -0
  289. aiecs/tools/statistics/__init__.py +80 -0
  290. aiecs/tools/statistics/ai_data_analysis_orchestrator.py +646 -0
  291. aiecs/tools/statistics/ai_insight_generator_tool.py +508 -0
  292. aiecs/tools/statistics/ai_report_orchestrator_tool.py +684 -0
  293. aiecs/tools/statistics/data_loader_tool.py +555 -0
  294. aiecs/tools/statistics/data_profiler_tool.py +638 -0
  295. aiecs/tools/statistics/data_transformer_tool.py +580 -0
  296. aiecs/tools/statistics/data_visualizer_tool.py +498 -0
  297. aiecs/tools/statistics/model_trainer_tool.py +507 -0
  298. aiecs/tools/statistics/statistical_analyzer_tool.py +472 -0
  299. aiecs/tools/task_tools/__init__.py +49 -36
  300. aiecs/tools/task_tools/chart_tool.py +200 -184
  301. aiecs/tools/task_tools/classfire_tool.py +268 -267
  302. aiecs/tools/task_tools/image_tool.py +220 -141
  303. aiecs/tools/task_tools/office_tool.py +226 -146
  304. aiecs/tools/task_tools/pandas_tool.py +477 -121
  305. aiecs/tools/task_tools/report_tool.py +390 -142
  306. aiecs/tools/task_tools/research_tool.py +149 -79
  307. aiecs/tools/task_tools/scraper_tool.py +339 -145
  308. aiecs/tools/task_tools/stats_tool.py +448 -209
  309. aiecs/tools/temp_file_manager.py +26 -24
  310. aiecs/tools/tool_executor/__init__.py +18 -16
  311. aiecs/tools/tool_executor/tool_executor.py +364 -52
  312. aiecs/utils/LLM_output_structor.py +74 -48
  313. aiecs/utils/__init__.py +14 -3
  314. aiecs/utils/base_callback.py +0 -3
  315. aiecs/utils/cache_provider.py +696 -0
  316. aiecs/utils/execution_utils.py +50 -31
  317. aiecs/utils/prompt_loader.py +1 -0
  318. aiecs/utils/token_usage_repository.py +37 -11
  319. aiecs/ws/socket_server.py +14 -4
  320. {aiecs-1.0.1.dist-info → aiecs-1.7.17.dist-info}/METADATA +52 -15
  321. aiecs-1.7.17.dist-info/RECORD +337 -0
  322. aiecs-1.7.17.dist-info/entry_points.txt +13 -0
  323. aiecs/config/registry.py +0 -19
  324. aiecs/domain/context/content_engine.py +0 -982
  325. aiecs/llm/base_client.py +0 -99
  326. aiecs/llm/openai_client.py +0 -125
  327. aiecs/llm/vertex_client.py +0 -186
  328. aiecs/llm/xai_client.py +0 -184
  329. aiecs/scripts/dependency_checker.py +0 -857
  330. aiecs/scripts/quick_dependency_check.py +0 -269
  331. aiecs/tools/task_tools/search_api.py +0 -7
  332. aiecs-1.0.1.dist-info/RECORD +0 -90
  333. aiecs-1.0.1.dist-info/entry_points.txt +0 -7
  334. /aiecs/scripts/{setup_nlp_data.sh → dependance_check/setup_nlp_data.sh} +0 -0
  335. /aiecs/scripts/{README_WEASEL_PATCH.md → dependance_patch/fix_weasel/README_WEASEL_PATCH.md} +0 -0
  336. /aiecs/scripts/{fix_weasel_validator.sh → dependance_patch/fix_weasel/fix_weasel_validator.sh} +0 -0
  337. /aiecs/scripts/{run_weasel_patch.sh → dependance_patch/fix_weasel/run_weasel_patch.sh} +0 -0
  338. {aiecs-1.0.1.dist-info → aiecs-1.7.17.dist-info}/WHEEL +0 -0
  339. {aiecs-1.0.1.dist-info → aiecs-1.7.17.dist-info}/licenses/LICENSE +0 -0
  340. {aiecs-1.0.1.dist-info → aiecs-1.7.17.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,3949 @@
1
+ """
2
+ Base AI Agent
3
+
4
+ Abstract base class for all AI agents in the AIECS system.
5
+ """
6
+
7
+ from abc import ABC, abstractmethod
8
+ from datetime import datetime
9
+ from typing import Dict, List, Any, Optional, Callable, Union, TYPE_CHECKING, AsyncIterator, Set
10
+ from dataclasses import dataclass
11
+ import logging
12
+ import time
13
+ import asyncio
14
+ import json
15
+
16
+ from .models import (
17
+ AgentState,
18
+ AgentType,
19
+ AgentConfiguration,
20
+ AgentGoal,
21
+ AgentMetrics,
22
+ AgentCapabilityDeclaration,
23
+ GoalStatus,
24
+ GoalPriority,
25
+ MemoryType,
26
+ )
27
+ from .exceptions import (
28
+ InvalidStateTransitionError,
29
+ ConfigurationError,
30
+ AgentInitializationError,
31
+ SerializationError,
32
+ )
33
+
34
+ # Import protocols for type hints
35
+ if TYPE_CHECKING:
36
+ from aiecs.llm.protocols import LLMClientProtocol
37
+ from aiecs.domain.agent.integration.protocols import (
38
+ ConfigManagerProtocol,
39
+ CheckpointerProtocol,
40
+ )
41
+ from aiecs.tools.base_tool import BaseTool
42
+ from aiecs.domain.context.context_engine import ContextEngine
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+
47
+ class OperationTimer:
48
+ """
49
+ Context manager for timing operations and tracking metrics.
50
+
51
+ Automatically records operation duration and can be used to track
52
+ operation-level performance metrics.
53
+
54
+ Example:
55
+ with agent.track_operation_time("llm_call") as timer:
56
+ result = llm.generate(prompt)
57
+ # timer.duration contains the elapsed time in seconds
58
+ """
59
+
60
+ def __init__(self, operation_name: str, agent: Optional["BaseAIAgent"] = None):
61
+ """
62
+ Initialize operation timer.
63
+
64
+ Args:
65
+ operation_name: Name of the operation being timed
66
+ agent: Optional agent instance for automatic metrics recording
67
+ """
68
+ self.operation_name = operation_name
69
+ self.agent = agent
70
+ self.start_time: Optional[float] = None
71
+ self.end_time: Optional[float] = None
72
+ self.duration: Optional[float] = None
73
+ self.error: Optional[Exception] = None
74
+
75
+ def __enter__(self) -> "OperationTimer":
76
+ """Start timing."""
77
+ self.start_time = time.time()
78
+ return self
79
+
80
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
81
+ """
82
+ Stop timing and record metrics.
83
+
84
+ Args:
85
+ exc_type: Exception type if an error occurred
86
+ exc_val: Exception value if an error occurred
87
+ exc_tb: Exception traceback if an error occurred
88
+
89
+ Returns:
90
+ False to propagate exceptions
91
+ """
92
+ self.end_time = time.time()
93
+ if self.start_time is not None:
94
+ self.duration = self.end_time - self.start_time
95
+
96
+ # Track error if one occurred
97
+ if exc_val is not None:
98
+ self.error = exc_val
99
+
100
+ # Record metrics if agent is provided
101
+ if self.agent and self.duration is not None:
102
+ self.agent._record_operation_metrics(
103
+ operation_name=self.operation_name,
104
+ duration=self.duration,
105
+ success=exc_val is None,
106
+ )
107
+
108
+ # Don't suppress exceptions
109
+ return None
110
+
111
+ def get_duration_ms(self) -> Optional[float]:
112
+ """Get duration in milliseconds."""
113
+ return self.duration * 1000 if self.duration is not None else None
114
+
115
+
116
+ @dataclass
117
+ class CacheConfig:
118
+ """
119
+ Configuration for tool result caching.
120
+
121
+ Provides control over caching behavior to improve performance and reduce costs
122
+ by avoiding redundant tool executions. Supports TTL-based expiration, size limits,
123
+ and automatic cleanup.
124
+
125
+ **Key Features:**
126
+ - TTL-based cache expiration (default and per-tool)
127
+ - Size limits to prevent memory exhaustion
128
+ - Automatic cleanup when capacity threshold reached
129
+ - Configurable cache key generation
130
+ - Input hashing for large parameters
131
+
132
+ Attributes:
133
+ enabled: Enable/disable caching globally
134
+ default_ttl: Default time-to-live in seconds for cached entries (default: 300 = 5 minutes)
135
+ tool_specific_ttl: Dictionary mapping tool names to custom TTL values (overrides default_ttl)
136
+ max_cache_size: Maximum number of cached entries before cleanup (default: 1000)
137
+ max_memory_mb: Maximum cache memory usage in MB (approximate, default: 100)
138
+ cleanup_interval: Interval in seconds between cleanup checks (default: 60)
139
+ cleanup_threshold: Capacity threshold (0.0-1.0) to trigger cleanup (default: 0.9 = 90%)
140
+ include_timestamp_in_key: Whether to include timestamp in cache key (default: False)
141
+ hash_large_inputs: Whether to hash inputs larger than 1KB for cache keys (default: True)
142
+
143
+ Examples:
144
+ # Example 1: Basic caching configuration
145
+ config = CacheConfig(
146
+ enabled=True,
147
+ default_ttl=300, # 5 minutes
148
+ max_cache_size=1000
149
+ )
150
+
151
+ # Example 2: Per-tool TTL overrides
152
+ config = CacheConfig(
153
+ enabled=True,
154
+ default_ttl=300,
155
+ tool_specific_ttl={
156
+ "search": 600, # Search results cached for 10 minutes
157
+ "calculator": 3600, # Calculator results cached for 1 hour
158
+ "weather": 1800 # Weather data cached for 30 minutes
159
+ }
160
+ )
161
+
162
+ # Example 3: Aggressive caching for expensive operations
163
+ config = CacheConfig(
164
+ enabled=True,
165
+ default_ttl=3600, # 1 hour default
166
+ max_cache_size=5000,
167
+ max_memory_mb=500,
168
+ cleanup_threshold=0.95 # Cleanup at 95% capacity
169
+ )
170
+
171
+ # Example 4: Disable caching for time-sensitive tools
172
+ config = CacheConfig(
173
+ enabled=False # Disable caching entirely
174
+ )
175
+
176
+ # Example 5: Cache with timestamp-aware keys
177
+ config = CacheConfig(
178
+ enabled=True,
179
+ default_ttl=300,
180
+ include_timestamp_in_key=True # Include timestamp for time-sensitive caching
181
+ )
182
+ """
183
+
184
+ # Cache enablement
185
+ enabled: bool = True # Enable/disable caching
186
+
187
+ # TTL settings
188
+ default_ttl: int = 300 # Default TTL in seconds (5 minutes)
189
+ tool_specific_ttl: Optional[Dict[str, int]] = None # Per-tool TTL overrides
190
+
191
+ # Size limits
192
+ max_cache_size: int = 1000 # Maximum number of cached entries
193
+ max_memory_mb: int = 100 # Maximum cache memory in MB (approximate)
194
+
195
+ # Cleanup settings
196
+ cleanup_interval: int = 60 # Cleanup interval in seconds
197
+ cleanup_threshold: float = 0.9 # Trigger cleanup at 90% capacity
198
+
199
+ # Cache key settings
200
+ include_timestamp_in_key: bool = False # Include timestamp in cache key
201
+ hash_large_inputs: bool = True # Hash inputs larger than 1KB
202
+
203
+ def __post_init__(self):
204
+ """Initialize defaults."""
205
+ if self.tool_specific_ttl is None:
206
+ self.tool_specific_ttl = {}
207
+
208
+ def get_ttl(self, tool_name: str) -> int:
209
+ """
210
+ Get TTL for a specific tool.
211
+
212
+ Args:
213
+ tool_name: Name of the tool
214
+
215
+ Returns:
216
+ TTL in seconds
217
+ """
218
+ if self.tool_specific_ttl is None:
219
+ return self.default_ttl
220
+ return self.tool_specific_ttl.get(tool_name, self.default_ttl)
221
+
222
+
223
+ class BaseAIAgent(ABC):
224
+ """
225
+ Abstract base class for AI agents.
226
+
227
+ Provides common functionality for agent lifecycle management,
228
+ state management, memory, goals, and metrics tracking.
229
+
230
+ This base class supports extensive flexibility and advanced features:
231
+
232
+ **Tool Flexibility:**
233
+ - Accept tool names (List[str]) for backward compatibility
234
+ - Accept pre-configured tool instances (Dict[str, BaseTool]) with preserved state
235
+ - Automatic tool loading and validation
236
+
237
+ **LLM Client Flexibility:**
238
+ - Accept any object implementing LLMClientProtocol (duck typing)
239
+ - No requirement for BaseLLMClient inheritance
240
+ - Custom LLM client wrappers fully supported
241
+
242
+ **Advanced Features:**
243
+ - ContextEngine integration for persistent conversation history
244
+ - Custom config managers for dynamic configuration
245
+ - Checkpointers for state persistence (LangGraph compatible)
246
+ - Agent collaboration (delegation, peer review, consensus)
247
+ - Agent learning from experiences
248
+ - Resource management (rate limiting, quotas)
249
+ - Performance tracking and health monitoring
250
+ - Tool result caching
251
+ - Parallel tool execution
252
+ - Streaming responses
253
+ - Error recovery strategies
254
+
255
+ Examples:
256
+ # Example 1: Basic agent with tool names (backward compatible)
257
+ agent = HybridAgent(
258
+ agent_id="agent1",
259
+ name="My Agent",
260
+ agent_type=AgentType.HYBRID,
261
+ config=config,
262
+ tools=["search", "calculator"] # Tool names loaded by subclass
263
+ )
264
+
265
+ # Example 2: Agent with tool instances (preserves tool state)
266
+ from aiecs.tools import BaseTool
267
+
268
+ class StatefulSearchTool(BaseTool):
269
+ def __init__(self, api_key: str):
270
+ self.api_key = api_key
271
+ self.call_count = 0 # State preserved
272
+
273
+ async def run_async(self, query: str):
274
+ self.call_count += 1
275
+ return f"Search results for: {query}"
276
+
277
+ agent = HybridAgent(
278
+ agent_id="agent1",
279
+ name="My Agent",
280
+ agent_type=AgentType.HYBRID,
281
+ config=config,
282
+ tools={
283
+ "search": StatefulSearchTool(api_key="..."),
284
+ "calculator": CalculatorTool()
285
+ },
286
+ llm_client=OpenAIClient()
287
+ )
288
+ # Tool state (call_count) is preserved across agent operations
289
+
290
+ # Example 3: Agent with custom LLM client (no BaseLLMClient inheritance)
291
+ class CustomLLMClient:
292
+ provider_name = "custom"
293
+
294
+ async def generate_text(self, messages, **kwargs):
295
+ # Custom implementation
296
+ return LLMResponse(content="...", provider="custom")
297
+
298
+ async def stream_text(self, messages, **kwargs):
299
+ async for token in self._custom_stream():
300
+ yield token
301
+
302
+ async def close(self):
303
+ # Cleanup
304
+ pass
305
+
306
+ agent = LLMAgent(
307
+ agent_id="agent1",
308
+ name="My LLM Agent",
309
+ agent_type=AgentType.CONVERSATIONAL,
310
+ config=config,
311
+ llm_client=CustomLLMClient() # Works without BaseLLMClient!
312
+ )
313
+
314
+ # Example 4: Agent with ContextEngine for persistent storage
315
+ from aiecs.domain.context import ContextEngine
316
+
317
+ context_engine = ContextEngine()
318
+ await context_engine.initialize()
319
+
320
+ agent = HybridAgent(
321
+ agent_id="agent1",
322
+ name="My Agent",
323
+ agent_type=AgentType.HYBRID,
324
+ config=config,
325
+ tools=["search"],
326
+ llm_client=OpenAIClient(),
327
+ context_engine=context_engine # Enables persistent conversation history
328
+ )
329
+ # Conversation history persists across agent restarts
330
+
331
+ # Example 5: Agent with custom config manager
332
+ class DatabaseConfigManager:
333
+ async def get_config(self, key: str):
334
+ # Load from database
335
+ return await db.get_config(key)
336
+
337
+ async def update_config(self, key: str, value: Any):
338
+ # Update in database
339
+ await db.update_config(key, value)
340
+
341
+ agent = HybridAgent(
342
+ agent_id="agent1",
343
+ name="My Agent",
344
+ agent_type=AgentType.HYBRID,
345
+ config=config,
346
+ tools=["search"],
347
+ llm_client=OpenAIClient(),
348
+ config_manager=DatabaseConfigManager() # Dynamic config loading
349
+ )
350
+
351
+ # Example 6: Agent with checkpointer for LangGraph integration
352
+ class RedisCheckpointer:
353
+ async def save(self, agent_id: str, state: Dict[str, Any]):
354
+ await redis.set(f"checkpoint:{agent_id}", json.dumps(state))
355
+
356
+ async def load(self, agent_id: str) -> Optional[Dict[str, Any]]:
357
+ data = await redis.get(f"checkpoint:{agent_id}")
358
+ return json.loads(data) if data else None
359
+
360
+ agent = HybridAgent(
361
+ agent_id="agent1",
362
+ name="My Agent",
363
+ agent_type=AgentType.HYBRID,
364
+ config=config,
365
+ tools=["search"],
366
+ llm_client=OpenAIClient(),
367
+ checkpointer=RedisCheckpointer() # LangGraph-compatible checkpointing
368
+ )
369
+
370
+ # Example 7: Agent with collaboration features
371
+ agent_registry = {
372
+ "agent2": other_agent_instance,
373
+ "agent3": another_agent_instance
374
+ }
375
+
376
+ agent = HybridAgent(
377
+ agent_id="agent1",
378
+ name="My Agent",
379
+ agent_type=AgentType.HYBRID,
380
+ config=config,
381
+ tools=["search"],
382
+ llm_client=OpenAIClient(),
383
+ collaboration_enabled=True,
384
+ agent_registry=agent_registry # Enable delegation and peer review
385
+ )
386
+
387
+ # Delegate task to another agent
388
+ result = await agent.delegate_task(
389
+ task_description="Analyze this data",
390
+ target_agent_id="agent2"
391
+ )
392
+
393
+ # Example 8: Agent with learning enabled
394
+ from aiecs.domain.agent.models import ResourceLimits
395
+
396
+ agent = HybridAgent(
397
+ agent_id="agent1",
398
+ name="My Agent",
399
+ agent_type=AgentType.HYBRID,
400
+ config=config,
401
+ tools=["search"],
402
+ llm_client=OpenAIClient(),
403
+ learning_enabled=True # Learn from past experiences
404
+ )
405
+
406
+ # Record experience
407
+ await agent.record_experience(
408
+ task_type="data_analysis",
409
+ approach="parallel_tools",
410
+ success=True,
411
+ execution_time=2.5
412
+ )
413
+
414
+ # Get recommended approach based on history
415
+ approach = await agent.get_recommended_approach("data_analysis")
416
+ print(f"Recommended: {approach}")
417
+
418
+ # Example 9: Agent with resource limits
419
+ from aiecs.domain.agent.models import ResourceLimits
420
+
421
+ resource_limits = ResourceLimits(
422
+ max_concurrent_tasks=5,
423
+ max_tokens_per_minute=10000,
424
+ max_tool_calls_per_minute=100
425
+ )
426
+
427
+ agent = HybridAgent(
428
+ agent_id="agent1",
429
+ name="My Agent",
430
+ agent_type=AgentType.HYBRID,
431
+ config=config,
432
+ tools=["search"],
433
+ llm_client=OpenAIClient(),
434
+ resource_limits=resource_limits # Rate limiting and quotas
435
+ )
436
+
437
+ # Check resource availability before executing
438
+ if await agent.check_resource_availability():
439
+ result = await agent.execute_task(task, context)
440
+ else:
441
+ await agent.wait_for_resources(timeout=30.0)
442
+
443
+ # Example 10: Agent with performance tracking
444
+ agent = HybridAgent(
445
+ agent_id="agent1",
446
+ name="My Agent",
447
+ agent_type=AgentType.HYBRID,
448
+ config=config,
449
+ tools=["search"],
450
+ llm_client=OpenAIClient()
451
+ )
452
+
453
+ # Track operation performance
454
+ with agent.track_operation_time("data_processing"):
455
+ result = await agent.execute_task(task, context)
456
+
457
+ # Get performance metrics
458
+ metrics = agent.get_performance_metrics()
459
+ print(f"Average response time: {metrics['avg_response_time']}s")
460
+ print(f"P95 response time: {metrics['p95_response_time']}s")
461
+
462
+ # Get health status
463
+ health = agent.get_health_status()
464
+ print(f"Health score: {health['score']}")
465
+ print(f"Status: {health['status']}")
466
+
467
+ # Example 11: Agent with tool caching
468
+ agent = HybridAgent(
469
+ agent_id="agent1",
470
+ name="My Agent",
471
+ agent_type=AgentType.HYBRID,
472
+ config=config,
473
+ tools=["search"],
474
+ llm_client=OpenAIClient()
475
+ )
476
+
477
+ # Execute tool with caching (30 second TTL)
478
+ result1 = await agent.execute_tool_with_cache(
479
+ tool_name="search",
480
+ operation="query",
481
+ parameters={"q": "AI"},
482
+ cache_ttl=30
483
+ )
484
+
485
+ # Second call uses cache (no API call)
486
+ result2 = await agent.execute_tool_with_cache(
487
+ tool_name="search",
488
+ operation="query",
489
+ parameters={"q": "AI"},
490
+ cache_ttl=30
491
+ )
492
+
493
+ # Get cache statistics
494
+ stats = agent.get_cache_stats()
495
+ print(f"Cache hit rate: {stats['hit_rate']:.1%}")
496
+
497
+ # Example 12: Agent with parallel tool execution
498
+ agent = HybridAgent(
499
+ agent_id="agent1",
500
+ name="My Agent",
501
+ agent_type=AgentType.HYBRID,
502
+ config=config,
503
+ tools=["search", "calculator", "translator"],
504
+ llm_client=OpenAIClient()
505
+ )
506
+
507
+ # Execute multiple independent tools in parallel (3-5x faster)
508
+ results = await agent.execute_tools_parallel([
509
+ {"tool": "search", "operation": "query", "parameters": {"q": "AI"}},
510
+ {"tool": "calculator", "operation": "add", "parameters": {"a": 1, "b": 2}},
511
+ {"tool": "translator", "operation": "translate", "parameters": {"text": "Hello"}}
512
+ ], max_concurrency=3)
513
+
514
+ # Example 13: Agent with streaming responses
515
+ agent = HybridAgent(
516
+ agent_id="agent1",
517
+ name="My Agent",
518
+ agent_type=AgentType.HYBRID,
519
+ config=config,
520
+ tools=["search"],
521
+ llm_client=OpenAIClient()
522
+ )
523
+
524
+ # Stream task execution (tokens + tool calls)
525
+ async for event in agent.execute_task_streaming(task, context):
526
+ if event['type'] == 'token':
527
+ print(event['content'], end='', flush=True)
528
+ elif event['type'] == 'tool_call':
529
+ print(f"\\nCalling {event['tool_name']}...")
530
+ elif event['type'] == 'result':
531
+ print(f"\\nFinal result: {event['output']}")
532
+
533
+ # Example 14: Agent with error recovery
534
+ agent = HybridAgent(
535
+ agent_id="agent1",
536
+ name="My Agent",
537
+ agent_type=AgentType.HYBRID,
538
+ config=config,
539
+ tools=["search"],
540
+ llm_client=OpenAIClient()
541
+ )
542
+
543
+ # Execute with automatic recovery strategies
544
+ result = await agent.execute_with_recovery(
545
+ task=task,
546
+ context=context,
547
+ strategies=["retry", "simplify", "fallback", "delegate"]
548
+ )
549
+ # Automatically tries retry → simplify → fallback → delegate if errors occur
550
+ """
551
+
552
+ def __init__(
553
+ self,
554
+ agent_id: str,
555
+ name: str,
556
+ agent_type: AgentType,
557
+ config: AgentConfiguration,
558
+ description: Optional[str] = None,
559
+ version: str = "1.0.0",
560
+ tools: Optional[Union[List[str], Dict[str, "BaseTool"]]] = None,
561
+ llm_client: Optional["LLMClientProtocol"] = None,
562
+ config_manager: Optional["ConfigManagerProtocol"] = None,
563
+ checkpointer: Optional["CheckpointerProtocol"] = None,
564
+ context_engine: Optional["ContextEngine"] = None,
565
+ collaboration_enabled: bool = False,
566
+ agent_registry: Optional[Dict[str, Any]] = None,
567
+ learning_enabled: bool = False,
568
+ resource_limits: Optional[Any] = None,
569
+ ):
570
+ """
571
+ Initialize the base agent.
572
+
573
+ Args:
574
+ agent_id: Unique identifier for the agent
575
+ name: Agent name
576
+ agent_type: Type of agent
577
+ config: Agent configuration
578
+ description: Optional agent description
579
+ version: Agent version
580
+ tools: Optional tools - either list of tool names or dict of tool instances.
581
+ List[str]: Tool names to be loaded by subclass
582
+ Dict[str, BaseTool]: Pre-configured tool instances with state
583
+ llm_client: Optional LLM client (any object implementing LLMClientProtocol).
584
+ Supports custom LLM clients without BaseLLMClient inheritance.
585
+ config_manager: Optional configuration manager for dynamic config loading
586
+ checkpointer: Optional checkpointer for state persistence (LangGraph compatible)
587
+ context_engine: Optional ContextEngine instance for persistent conversation history
588
+ collaboration_enabled: Enable agent collaboration features (delegation, peer review)
589
+ agent_registry: Registry of other agents for collaboration (agent_id -> agent instance)
590
+ learning_enabled: Enable agent learning from experiences
591
+ resource_limits: Optional resource limits configuration
592
+ and session management. If provided, enables persistent storage
593
+ across agent restarts.
594
+
595
+ Example:
596
+ # With tool instances and ContextEngine
597
+ from aiecs.domain.context import ContextEngine
598
+
599
+ context_engine = ContextEngine()
600
+ await context_engine.initialize()
601
+
602
+ agent = HybridAgent(
603
+ agent_id="agent1",
604
+ name="My Agent",
605
+ agent_type=AgentType.HYBRID,
606
+ config=config,
607
+ tools={
608
+ "search": SearchTool(api_key="..."),
609
+ "calculator": CalculatorTool()
610
+ },
611
+ llm_client=CustomLLMClient(), # Custom client, no inheritance needed
612
+ config_manager=DatabaseConfigManager(),
613
+ checkpointer=RedisCheckpointer(),
614
+ context_engine=context_engine # Enables persistent storage
615
+ )
616
+
617
+ # With tool names (backward compatible)
618
+ agent = HybridAgent(
619
+ agent_id="agent1",
620
+ name="My Agent",
621
+ agent_type=AgentType.HYBRID,
622
+ config=config,
623
+ tools=["search", "calculator"] # Loaded by subclass
624
+ )
625
+ """
626
+ # Identity
627
+ self.agent_id = agent_id
628
+ self.name = name
629
+ self.agent_type = agent_type
630
+ self.description = description or f"{agent_type.value} agent"
631
+ self.version = version
632
+
633
+ # Configuration
634
+ self._config = config
635
+ self._config_manager = config_manager
636
+
637
+ # State
638
+ self._state = AgentState.CREATED
639
+ self._previous_state: Optional[AgentState] = None
640
+
641
+ # Memory storage (in-memory dict, can be replaced with sophisticated
642
+ # storage)
643
+ self._memory: Dict[str, Any] = {}
644
+ self._memory_metadata: Dict[str, Dict[str, Any]] = {}
645
+
646
+ # Goals
647
+ self._goals: Dict[str, AgentGoal] = {}
648
+
649
+ # Capabilities
650
+ self._capabilities: Dict[str, AgentCapabilityDeclaration] = {}
651
+
652
+ # Metrics
653
+ self._metrics = AgentMetrics() # type: ignore[call-arg]
654
+
655
+ # Timestamps
656
+ self.created_at = datetime.utcnow()
657
+ self.updated_at = datetime.utcnow()
658
+ self.last_active_at: Optional[datetime] = None
659
+
660
+ # Current task tracking
661
+ self._current_task_id: Optional[str] = None
662
+
663
+ # Tools (optional - only set if tools provided)
664
+ self._tools_input = tools # Store original input
665
+ self._available_tools: Optional[List[str]] = None
666
+ self._tool_instances: Optional[Dict[str, "BaseTool"]] = None
667
+
668
+ # LLM client (optional)
669
+ self._llm_client = llm_client
670
+
671
+ # Checkpointer (optional)
672
+ self._checkpointer = checkpointer
673
+
674
+ # ContextEngine (optional - Phase 4 enhancement)
675
+ self._context_engine = context_engine
676
+
677
+ # Tool result cache (Phase 7 enhancement)
678
+ self._cache_config = CacheConfig()
679
+ self._tool_cache: Dict[str, Any] = {} # Cache key -> result
680
+ self._cache_timestamps: Dict[str, float] = {} # Cache key -> timestamp
681
+ self._cache_access_count: Dict[str, int] = {} # Cache key -> access count
682
+ self._last_cleanup_time = time.time()
683
+
684
+ # Agent collaboration (Phase 7 enhancement - tasks 1.15.15-1.15.22)
685
+ self._collaboration_enabled = collaboration_enabled
686
+ self._agent_registry = agent_registry or {}
687
+
688
+ # Agent learning (Phase 8 enhancement - tasks 1.16.4-1.16.10)
689
+ self._learning_enabled = learning_enabled
690
+ self._experiences: List[Any] = [] # List of Experience objects
691
+ self._max_experiences = 1000 # Limit stored experiences
692
+
693
+ # Resource management (Phase 8 enhancement - tasks 1.16.11-1.16.17)
694
+ from .models import ResourceLimits
695
+
696
+ self._resource_limits = resource_limits or ResourceLimits() # type: ignore[call-arg]
697
+ self._active_tasks: set = set() # Set of active task IDs
698
+ self._token_usage_window: List[tuple] = [] # List of (timestamp, token_count)
699
+ self._tool_call_window: List[float] = [] # List of timestamps
700
+
701
+ features = []
702
+ if context_engine:
703
+ features.append("ContextEngine")
704
+ if collaboration_enabled:
705
+ features.append("collaboration")
706
+ if learning_enabled:
707
+ features.append("learning")
708
+ if resource_limits:
709
+ features.append("resource limits")
710
+
711
+ feature_str = f" with {', '.join(features)}" if features else ""
712
+ logger.info(f"Agent initialized: {self.agent_id} ({self.name}, {self.agent_type.value}){feature_str}")
713
+
714
+ # ==================== State Management ====================
715
+
716
+ @property
717
+ def state(self) -> AgentState:
718
+ """Get current agent state."""
719
+ return self._state
720
+
721
+ def get_state(self) -> AgentState:
722
+ """Get current agent state."""
723
+ return self._state
724
+
725
+ def _transition_state(self, new_state: AgentState) -> None:
726
+ """
727
+ Transition to a new state with validation.
728
+
729
+ Args:
730
+ new_state: Target state
731
+
732
+ Raises:
733
+ InvalidStateTransitionError: If transition is invalid
734
+ """
735
+ # Define valid transitions
736
+ valid_transitions = {
737
+ AgentState.CREATED: {AgentState.INITIALIZING},
738
+ AgentState.INITIALIZING: {AgentState.ACTIVE, AgentState.ERROR},
739
+ AgentState.ACTIVE: {
740
+ AgentState.BUSY,
741
+ AgentState.IDLE,
742
+ AgentState.STOPPED,
743
+ AgentState.ERROR,
744
+ },
745
+ AgentState.BUSY: {AgentState.ACTIVE, AgentState.ERROR},
746
+ AgentState.IDLE: {AgentState.ACTIVE, AgentState.STOPPED},
747
+ AgentState.ERROR: {AgentState.ACTIVE, AgentState.STOPPED},
748
+ AgentState.STOPPED: set(), # Terminal state
749
+ }
750
+
751
+ if new_state not in valid_transitions.get(self._state, set()):
752
+ raise InvalidStateTransitionError(
753
+ agent_id=self.agent_id,
754
+ current_state=self._state.value,
755
+ attempted_state=new_state.value,
756
+ )
757
+
758
+ self._previous_state = self._state
759
+ self._state = new_state
760
+ self.updated_at = datetime.utcnow()
761
+
762
+ logger.info(f"Agent {self.agent_id} state: {self._previous_state.value} → {new_state.value}")
763
+
764
+ # ==================== Lifecycle Methods ====================
765
+
766
+ async def initialize(self) -> None:
767
+ """
768
+ Initialize the agent.
769
+
770
+ This method should be called before the agent can be used.
771
+ Override in subclasses to add initialization logic.
772
+
773
+ Raises:
774
+ AgentInitializationError: If initialization fails
775
+ """
776
+ try:
777
+ self._transition_state(AgentState.INITIALIZING)
778
+ logger.info(f"Initializing agent {self.agent_id}...")
779
+
780
+ # Subclass initialization
781
+ await self._initialize()
782
+
783
+ self._transition_state(AgentState.ACTIVE)
784
+ self.last_active_at = datetime.utcnow()
785
+ logger.info(f"Agent {self.agent_id} initialized successfully")
786
+
787
+ except Exception as e:
788
+ self._transition_state(AgentState.ERROR)
789
+ logger.error(f"Agent {self.agent_id} initialization failed: {e}")
790
+ raise AgentInitializationError(
791
+ f"Failed to initialize agent {self.agent_id}: {str(e)}",
792
+ agent_id=self.agent_id,
793
+ )
794
+
795
+ @abstractmethod
796
+ async def _initialize(self) -> None:
797
+ """
798
+ Subclass-specific initialization logic.
799
+
800
+ Override this method in subclasses to implement
801
+ custom initialization.
802
+ """
803
+
804
+ async def activate(self) -> None:
805
+ """Activate the agent."""
806
+ if self._state == AgentState.IDLE:
807
+ self._transition_state(AgentState.ACTIVE)
808
+ self.last_active_at = datetime.utcnow()
809
+ logger.info(f"Agent {self.agent_id} activated")
810
+ else:
811
+ logger.warning(f"Agent {self.agent_id} cannot be activated from state {self._state.value}")
812
+
813
+ async def deactivate(self) -> None:
814
+ """Deactivate the agent (enter idle state)."""
815
+ if self._state == AgentState.ACTIVE:
816
+ self._transition_state(AgentState.IDLE)
817
+ logger.info(f"Agent {self.agent_id} deactivated")
818
+ else:
819
+ logger.warning(f"Agent {self.agent_id} cannot be deactivated from state {self._state.value}")
820
+
821
+ async def shutdown(self) -> None:
822
+ """
823
+ Shutdown the agent.
824
+
825
+ Override in subclasses to add cleanup logic.
826
+ """
827
+ logger.info(f"Shutting down agent {self.agent_id}...")
828
+ await self._shutdown()
829
+ self._transition_state(AgentState.STOPPED)
830
+ logger.info(f"Agent {self.agent_id} shut down")
831
+
832
+ @abstractmethod
833
+ async def _shutdown(self) -> None:
834
+ """
835
+ Subclass-specific shutdown logic.
836
+
837
+ Override this method in subclasses to implement
838
+ custom cleanup.
839
+ """
840
+
841
+ # ==================== Tool and LLM Client Helper Methods ====================
842
+
843
+ def _load_tools(self) -> None:
844
+ """
845
+ Load tools from the tools input parameter.
846
+
847
+ Handles both List[str] (tool names) and Dict[str, BaseTool] (tool instances).
848
+ Sets _available_tools and _tool_instances appropriately.
849
+
850
+ This helper method should be called by subclasses during initialization
851
+ if they want to use BaseAIAgent's tool management.
852
+
853
+ Raises:
854
+ ConfigurationError: If tools input is invalid
855
+ """
856
+ if self._tools_input is None:
857
+ # No tools provided
858
+ return
859
+
860
+ if isinstance(self._tools_input, list):
861
+ # Tool names - store for subclass to load
862
+ self._available_tools = self._tools_input
863
+ logger.debug(f"Agent {self.agent_id}: Registered {len(self._tools_input)} tool names")
864
+
865
+ elif isinstance(self._tools_input, dict):
866
+ # Tool instances - validate and store
867
+ from aiecs.tools.base_tool import BaseTool
868
+
869
+ for tool_name, tool_instance in self._tools_input.items():
870
+ if not isinstance(tool_instance, BaseTool):
871
+ raise ConfigurationError(f"Tool '{tool_name}' must be a BaseTool instance, got {type(tool_instance)}")
872
+
873
+ self._tool_instances = self._tools_input
874
+ self._available_tools = list(self._tools_input.keys())
875
+ logger.debug(f"Agent {self.agent_id}: Registered {len(self._tools_input)} tool instances")
876
+
877
+ else:
878
+ raise ConfigurationError(f"Tools must be List[str] or Dict[str, BaseTool], got {type(self._tools_input)}")
879
+
880
+ def _validate_llm_client(self) -> None:
881
+ """
882
+ Validate that the LLM client implements the required protocol.
883
+
884
+ Checks that the LLM client has the required methods:
885
+ - generate_text
886
+ - stream_text
887
+ - close
888
+ - provider_name (property)
889
+
890
+ This helper method should be called by subclasses during initialization
891
+ if they want to use BaseAIAgent's LLM client validation.
892
+
893
+ Raises:
894
+ ConfigurationError: If LLM client doesn't implement required methods
895
+ """
896
+ if self._llm_client is None:
897
+ return
898
+
899
+ required_methods = ["generate_text", "stream_text", "close"]
900
+ required_properties = ["provider_name"]
901
+
902
+ for method_name in required_methods:
903
+ if not hasattr(self._llm_client, method_name):
904
+ raise ConfigurationError(f"LLM client must implement '{method_name}' method")
905
+ if not callable(getattr(self._llm_client, method_name)):
906
+ raise ConfigurationError(f"LLM client '{method_name}' must be callable")
907
+
908
+ for prop_name in required_properties:
909
+ if not hasattr(self._llm_client, prop_name):
910
+ raise ConfigurationError(f"LLM client must have '{prop_name}' property")
911
+
912
+ logger.debug(f"Agent {self.agent_id}: LLM client validated successfully")
913
+
914
+ def _get_tool_instances(self) -> Optional[Dict[str, "BaseTool"]]:
915
+ """
916
+ Get tool instances dictionary.
917
+
918
+ Returns:
919
+ Dictionary of tool instances, or None if no tool instances available
920
+ """
921
+ return self._tool_instances
922
+
923
+ def get_config_manager(self) -> Optional["ConfigManagerProtocol"]:
924
+ """
925
+ Get the configuration manager.
926
+
927
+ Returns:
928
+ Configuration manager instance, or None if not configured
929
+ """
930
+ return self._config_manager
931
+
932
+ # ==================== Abstract Execution Methods ====================
933
+
934
+ @abstractmethod
935
+ async def execute_task(self, task: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
936
+ """
937
+ Execute a task.
938
+
939
+ Args:
940
+ task: Task specification
941
+ context: Execution context
942
+
943
+ Returns:
944
+ Task execution result
945
+
946
+ Raises:
947
+ TaskExecutionError: If task execution fails
948
+
949
+ Note:
950
+ Subclasses can use `_execute_with_retry()` to wrap task execution
951
+ with automatic retry logic based on agent configuration.
952
+ """
953
+
954
+ @abstractmethod
955
+ async def process_message(self, message: str, sender_id: Optional[str] = None) -> Dict[str, Any]:
956
+ """
957
+ Process an incoming message.
958
+
959
+ Args:
960
+ message: Message content
961
+ sender_id: Optional sender identifier
962
+
963
+ Returns:
964
+ Response dictionary
965
+
966
+ Note:
967
+ Subclasses can use `_execute_with_retry()` to wrap message processing
968
+ with automatic retry logic based on agent configuration.
969
+ """
970
+
971
+ # ==================== Retry Logic Integration ====================
972
+
973
+ async def _execute_with_retry(self, func: Callable, *args, **kwargs) -> Any:
974
+ """
975
+ Execute a function with retry logic using agent's retry policy.
976
+
977
+ This helper method wraps function execution with automatic retry based on
978
+ the agent's configuration. It uses EnhancedRetryPolicy for sophisticated
979
+ error handling with exponential backoff and error classification.
980
+
981
+ Args:
982
+ func: Async function to execute
983
+ *args: Function positional arguments
984
+ **kwargs: Function keyword arguments
985
+
986
+ Returns:
987
+ Function result
988
+
989
+ Raises:
990
+ Exception: If all retries are exhausted
991
+
992
+ Example:
993
+ ```python
994
+ async def _execute_task_internal(self, task, context):
995
+ # Actual task execution logic
996
+ return result
997
+
998
+ async def execute_task(self, task, context):
999
+ return await self._execute_with_retry(
1000
+ self._execute_task_internal,
1001
+ task,
1002
+ context
1003
+ )
1004
+ ```
1005
+ """
1006
+ from .integration.retry_policy import EnhancedRetryPolicy
1007
+
1008
+ # Get retry policy from configuration
1009
+ retry_config = self._config.retry_policy
1010
+
1011
+ # Create retry policy instance
1012
+ retry_policy = EnhancedRetryPolicy(
1013
+ max_retries=retry_config.max_retries,
1014
+ base_delay=retry_config.base_delay,
1015
+ max_delay=retry_config.max_delay,
1016
+ exponential_base=retry_config.exponential_factor,
1017
+ jitter=retry_config.jitter_factor > 0,
1018
+ )
1019
+
1020
+ # Execute with retry
1021
+ return await retry_policy.execute_with_retry(func, *args, **kwargs)
1022
+
1023
+ # ==================== Memory Management ====================
1024
+
1025
+ async def add_to_memory(
1026
+ self,
1027
+ key: str,
1028
+ value: Any,
1029
+ memory_type: MemoryType = MemoryType.SHORT_TERM,
1030
+ metadata: Optional[Dict[str, Any]] = None,
1031
+ ) -> None:
1032
+ """
1033
+ Add an item to agent memory.
1034
+
1035
+ Args:
1036
+ key: Memory key
1037
+ value: Memory value
1038
+ memory_type: Type of memory (short_term or long_term)
1039
+ metadata: Optional metadata
1040
+ """
1041
+ self._memory[key] = value
1042
+ self._memory_metadata[key] = {
1043
+ "type": memory_type.value,
1044
+ "timestamp": datetime.utcnow(),
1045
+ "metadata": metadata or {},
1046
+ }
1047
+ logger.debug(f"Agent {self.agent_id} added memory: {key} ({memory_type.value})")
1048
+
1049
+ async def retrieve_memory(self, key: str, default: Any = None) -> Any:
1050
+ """
1051
+ Retrieve an item from memory.
1052
+
1053
+ Args:
1054
+ key: Memory key
1055
+ default: Default value if key not found
1056
+
1057
+ Returns:
1058
+ Memory value or default
1059
+ """
1060
+ return self._memory.get(key, default)
1061
+
1062
+ async def clear_memory(self, memory_type: Optional[MemoryType] = None) -> None:
1063
+ """
1064
+ Clear agent memory.
1065
+
1066
+ Args:
1067
+ memory_type: If specified, clear only this type of memory
1068
+ """
1069
+ if memory_type is None:
1070
+ self._memory.clear()
1071
+ self._memory_metadata.clear()
1072
+ logger.info(f"Agent {self.agent_id} cleared all memory")
1073
+ else:
1074
+ keys_to_remove = [k for k, v in self._memory_metadata.items() if v.get("type") == memory_type.value]
1075
+ for key in keys_to_remove:
1076
+ del self._memory[key]
1077
+ del self._memory_metadata[key]
1078
+ logger.info(f"Agent {self.agent_id} cleared {memory_type.value} memory")
1079
+
1080
+ def get_memory_summary(self) -> Dict[str, Any]:
1081
+ """Get a summary of agent memory."""
1082
+ return {
1083
+ "total_items": len(self._memory),
1084
+ "short_term_count": sum(1 for v in self._memory_metadata.values() if v.get("type") == MemoryType.SHORT_TERM.value),
1085
+ "long_term_count": sum(1 for v in self._memory_metadata.values() if v.get("type") == MemoryType.LONG_TERM.value),
1086
+ }
1087
+
1088
+ # ==================== Goal Management ====================
1089
+
1090
+ def set_goal(
1091
+ self,
1092
+ description: str,
1093
+ priority: GoalPriority = GoalPriority.MEDIUM,
1094
+ success_criteria: Optional[str] = None,
1095
+ deadline: Optional[datetime] = None,
1096
+ ) -> str:
1097
+ """
1098
+ Set a new goal for the agent.
1099
+
1100
+ Args:
1101
+ description: Goal description
1102
+ priority: Goal priority
1103
+ success_criteria: Success criteria
1104
+ deadline: Goal deadline
1105
+
1106
+ Returns:
1107
+ Goal ID
1108
+ """
1109
+ goal = AgentGoal( # type: ignore[call-arg]
1110
+ description=description,
1111
+ priority=priority,
1112
+ success_criteria=success_criteria,
1113
+ deadline=deadline,
1114
+ )
1115
+ self._goals[goal.goal_id] = goal
1116
+ logger.info(f"Agent {self.agent_id} set goal: {goal.goal_id} ({priority.value})")
1117
+ return goal.goal_id
1118
+
1119
+ def get_goals(self, status: Optional[GoalStatus] = None) -> List[AgentGoal]:
1120
+ """
1121
+ Get agent goals.
1122
+
1123
+ Args:
1124
+ status: Filter by status (optional)
1125
+
1126
+ Returns:
1127
+ List of goals
1128
+ """
1129
+ if status is None:
1130
+ return list(self._goals.values())
1131
+ return [g for g in self._goals.values() if g.status == status]
1132
+
1133
+ def get_goal(self, goal_id: str) -> Optional[AgentGoal]:
1134
+ """Get a specific goal by ID."""
1135
+ return self._goals.get(goal_id)
1136
+
1137
+ def update_goal_status(
1138
+ self,
1139
+ goal_id: str,
1140
+ status: GoalStatus,
1141
+ progress: Optional[float] = None,
1142
+ ) -> None:
1143
+ """
1144
+ Update goal status.
1145
+
1146
+ Args:
1147
+ goal_id: Goal ID
1148
+ status: New status
1149
+ progress: Optional progress percentage
1150
+ """
1151
+ if goal_id not in self._goals:
1152
+ logger.warning(f"Goal {goal_id} not found for agent {self.agent_id}")
1153
+ return
1154
+
1155
+ goal = self._goals[goal_id]
1156
+ goal.status = status
1157
+
1158
+ if progress is not None:
1159
+ goal.progress = progress
1160
+
1161
+ if status == GoalStatus.IN_PROGRESS and goal.started_at is None:
1162
+ goal.started_at = datetime.utcnow()
1163
+ elif status == GoalStatus.ACHIEVED:
1164
+ goal.achieved_at = datetime.utcnow()
1165
+
1166
+ logger.info(f"Agent {self.agent_id} updated goal {goal_id}: {status.value}")
1167
+
1168
+ # ==================== Configuration Management ====================
1169
+
1170
+ def get_config(self) -> AgentConfiguration:
1171
+ """Get agent configuration."""
1172
+ return self._config
1173
+
1174
+ def update_config(self, updates: Dict[str, Any]) -> None:
1175
+ """
1176
+ Update agent configuration.
1177
+
1178
+ Args:
1179
+ updates: Configuration updates
1180
+
1181
+ Raises:
1182
+ ConfigurationError: If configuration is invalid
1183
+ """
1184
+ try:
1185
+ # Update configuration
1186
+ for key, value in updates.items():
1187
+ if hasattr(self._config, key):
1188
+ setattr(self._config, key, value)
1189
+ else:
1190
+ logger.warning(f"Unknown config key: {key}")
1191
+
1192
+ self.updated_at = datetime.utcnow()
1193
+ logger.info(f"Agent {self.agent_id} configuration updated")
1194
+
1195
+ except Exception as e:
1196
+ raise ConfigurationError(
1197
+ f"Failed to update configuration: {str(e)}",
1198
+ agent_id=self.agent_id,
1199
+ )
1200
+
1201
+ # ==================== Capability Management ====================
1202
+
1203
+ def declare_capability(
1204
+ self,
1205
+ capability_type: str,
1206
+ level: str,
1207
+ description: Optional[str] = None,
1208
+ constraints: Optional[Dict[str, Any]] = None,
1209
+ ) -> None:
1210
+ """
1211
+ Declare an agent capability.
1212
+
1213
+ Args:
1214
+ capability_type: Type of capability
1215
+ level: Proficiency level
1216
+ description: Capability description
1217
+ constraints: Capability constraints
1218
+ """
1219
+ from .models import CapabilityLevel
1220
+
1221
+ capability = AgentCapabilityDeclaration(
1222
+ capability_type=capability_type,
1223
+ level=CapabilityLevel(level),
1224
+ description=description,
1225
+ constraints=constraints or {},
1226
+ )
1227
+ self._capabilities[capability_type] = capability
1228
+ logger.info(f"Agent {self.agent_id} declared capability: {capability_type} ({level})")
1229
+
1230
+ def has_capability(self, capability_type: str) -> bool:
1231
+ """Check if agent has a capability."""
1232
+ return capability_type in self._capabilities
1233
+
1234
+ def get_capabilities(self) -> List[AgentCapabilityDeclaration]:
1235
+ """Get all agent capabilities."""
1236
+ return list(self._capabilities.values())
1237
+
1238
+ # ==================== Metrics Tracking ====================
1239
+
1240
+ def get_metrics(self) -> AgentMetrics:
1241
+ """Get agent metrics."""
1242
+ return self._metrics
1243
+
1244
+ def update_metrics(
1245
+ self,
1246
+ execution_time: Optional[float] = None,
1247
+ success: bool = True,
1248
+ quality_score: Optional[float] = None,
1249
+ tokens_used: Optional[int] = None,
1250
+ tool_calls: Optional[int] = None,
1251
+ ) -> None:
1252
+ """
1253
+ Update agent metrics.
1254
+
1255
+ Args:
1256
+ execution_time: Task execution time
1257
+ success: Whether task succeeded
1258
+ quality_score: Quality score (0-1)
1259
+ tokens_used: Tokens used
1260
+ tool_calls: Number of tool calls
1261
+ """
1262
+ self._metrics.total_tasks_executed += 1
1263
+
1264
+ if success:
1265
+ self._metrics.successful_tasks += 1
1266
+ else:
1267
+ self._metrics.failed_tasks += 1
1268
+
1269
+ # Update success rate
1270
+ self._metrics.success_rate = self._metrics.successful_tasks / self._metrics.total_tasks_executed * 100
1271
+
1272
+ # Update execution time
1273
+ if execution_time is not None:
1274
+ self._metrics.total_execution_time += execution_time
1275
+ self._metrics.average_execution_time = self._metrics.total_execution_time / self._metrics.total_tasks_executed
1276
+
1277
+ if self._metrics.min_execution_time is None or execution_time < self._metrics.min_execution_time:
1278
+ self._metrics.min_execution_time = execution_time
1279
+ if self._metrics.max_execution_time is None or execution_time > self._metrics.max_execution_time:
1280
+ self._metrics.max_execution_time = execution_time
1281
+
1282
+ # Update quality score
1283
+ if quality_score is not None:
1284
+ if self._metrics.average_quality_score is None:
1285
+ self._metrics.average_quality_score = quality_score
1286
+ else:
1287
+ # Running average
1288
+ total_quality = self._metrics.average_quality_score * (self._metrics.total_tasks_executed - 1)
1289
+ self._metrics.average_quality_score = (total_quality + quality_score) / self._metrics.total_tasks_executed
1290
+
1291
+ # Update resource usage
1292
+ if tokens_used is not None:
1293
+ self._metrics.total_tokens_used += tokens_used
1294
+ if tool_calls is not None:
1295
+ self._metrics.total_tool_calls += tool_calls
1296
+
1297
+ self._metrics.updated_at = datetime.utcnow()
1298
+
1299
+ def update_cache_metrics(
1300
+ self,
1301
+ cache_read_tokens: Optional[int] = None,
1302
+ cache_creation_tokens: Optional[int] = None,
1303
+ cache_hit: Optional[bool] = None,
1304
+ ) -> None:
1305
+ """
1306
+ Update prompt cache metrics from LLM response.
1307
+
1308
+ This method tracks provider-level prompt caching statistics to monitor
1309
+ cache hit rates and token savings.
1310
+
1311
+ Args:
1312
+ cache_read_tokens: Tokens read from cache (indicates cache hit)
1313
+ cache_creation_tokens: Tokens used to create a new cache entry
1314
+ cache_hit: Whether the request hit a cached prompt prefix
1315
+
1316
+ Example:
1317
+ # After receiving LLM response
1318
+ agent.update_cache_metrics(
1319
+ cache_read_tokens=response.cache_read_tokens,
1320
+ cache_creation_tokens=response.cache_creation_tokens,
1321
+ cache_hit=response.cache_hit
1322
+ )
1323
+ """
1324
+ # Track LLM request count
1325
+ self._metrics.total_llm_requests += 1
1326
+
1327
+ # Track cache hit/miss
1328
+ if cache_hit is True:
1329
+ self._metrics.cache_hits += 1
1330
+ elif cache_hit is False:
1331
+ self._metrics.cache_misses += 1
1332
+ elif cache_read_tokens is not None and cache_read_tokens > 0:
1333
+ # Infer cache hit from tokens
1334
+ self._metrics.cache_hits += 1
1335
+ elif cache_creation_tokens is not None and cache_creation_tokens > 0:
1336
+ # Infer cache miss from creation tokens
1337
+ self._metrics.cache_misses += 1
1338
+
1339
+ # Update cache hit rate
1340
+ total_cache_requests = self._metrics.cache_hits + self._metrics.cache_misses
1341
+ if total_cache_requests > 0:
1342
+ self._metrics.cache_hit_rate = self._metrics.cache_hits / total_cache_requests
1343
+
1344
+ # Track cache tokens
1345
+ if cache_read_tokens is not None and cache_read_tokens > 0:
1346
+ self._metrics.total_cache_read_tokens += cache_read_tokens
1347
+ # Provider-level caching saves ~90% of token cost for cached tokens
1348
+ self._metrics.estimated_cache_savings_tokens += int(cache_read_tokens * 0.9)
1349
+
1350
+ if cache_creation_tokens is not None and cache_creation_tokens > 0:
1351
+ self._metrics.total_cache_creation_tokens += cache_creation_tokens
1352
+
1353
+ self._metrics.updated_at = datetime.utcnow()
1354
+ logger.debug(
1355
+ f"Agent {self.agent_id} cache metrics updated: "
1356
+ f"hit_rate={self._metrics.cache_hit_rate:.2%}, "
1357
+ f"read_tokens={cache_read_tokens}, creation_tokens={cache_creation_tokens}"
1358
+ )
1359
+
1360
+ def update_session_metrics(
1361
+ self,
1362
+ session_status: str,
1363
+ session_duration: Optional[float] = None,
1364
+ session_requests: int = 0,
1365
+ ) -> None:
1366
+ """
1367
+ Update session-level metrics.
1368
+
1369
+ This method should be called when a session is created, updated, or ended
1370
+ to track session-level statistics in agent metrics.
1371
+
1372
+ Args:
1373
+ session_status: Session status (active, completed, failed, expired)
1374
+ session_duration: Session duration in seconds (for ended sessions)
1375
+ session_requests: Number of requests in the session
1376
+
1377
+ Example:
1378
+ # When creating a session
1379
+ agent.update_session_metrics(session_status="active")
1380
+
1381
+ # When ending a session
1382
+ agent.update_session_metrics(
1383
+ session_status="completed",
1384
+ session_duration=300.5,
1385
+ session_requests=15
1386
+ )
1387
+ """
1388
+ # Update session counts based on status
1389
+ if session_status == "active":
1390
+ self._metrics.total_sessions += 1
1391
+ self._metrics.active_sessions += 1
1392
+ elif session_status == "completed":
1393
+ self._metrics.completed_sessions += 1
1394
+ if self._metrics.active_sessions > 0:
1395
+ self._metrics.active_sessions -= 1
1396
+ elif session_status == "failed":
1397
+ self._metrics.failed_sessions += 1
1398
+ if self._metrics.active_sessions > 0:
1399
+ self._metrics.active_sessions -= 1
1400
+ elif session_status == "expired":
1401
+ self._metrics.expired_sessions += 1
1402
+ if self._metrics.active_sessions > 0:
1403
+ self._metrics.active_sessions -= 1
1404
+
1405
+ # Update session request tracking
1406
+ if session_requests > 0:
1407
+ self._metrics.total_session_requests += session_requests
1408
+
1409
+ # Update average session duration
1410
+ if session_duration is not None and session_duration > 0:
1411
+ completed_count = self._metrics.completed_sessions + self._metrics.failed_sessions + self._metrics.expired_sessions
1412
+ if completed_count > 0:
1413
+ if self._metrics.average_session_duration is None:
1414
+ self._metrics.average_session_duration = session_duration
1415
+ else:
1416
+ # Running average
1417
+ total_duration = self._metrics.average_session_duration * (completed_count - 1)
1418
+ self._metrics.average_session_duration = (total_duration + session_duration) / completed_count
1419
+
1420
+ # Update average requests per session
1421
+ if self._metrics.total_sessions > 0:
1422
+ self._metrics.average_requests_per_session = self._metrics.total_session_requests / self._metrics.total_sessions
1423
+
1424
+ self._metrics.updated_at = datetime.utcnow()
1425
+ logger.debug(f"Agent {self.agent_id} session metrics updated: " f"status={session_status}, total_sessions={self._metrics.total_sessions}, " f"active_sessions={self._metrics.active_sessions}")
1426
+
1427
+ # ==================== Performance Tracking ====================
1428
+
1429
+ def track_operation_time(self, operation_name: str) -> OperationTimer:
1430
+ """
1431
+ Create a context manager for tracking operation time.
1432
+
1433
+ This method returns an OperationTimer that automatically records
1434
+ operation duration and updates agent metrics when the operation completes.
1435
+
1436
+ Args:
1437
+ operation_name: Name of the operation to track
1438
+
1439
+ Returns:
1440
+ OperationTimer context manager
1441
+
1442
+ Example:
1443
+ with agent.track_operation_time("llm_call") as timer:
1444
+ result = await llm.generate(prompt)
1445
+ # Metrics are automatically recorded
1446
+
1447
+ # Access duration if needed
1448
+ print(f"Operation took {timer.duration} seconds")
1449
+ """
1450
+ return OperationTimer(operation_name=operation_name, agent=self)
1451
+
1452
+ def _record_operation_metrics(self, operation_name: str, duration: float, success: bool = True) -> None:
1453
+ """
1454
+ Record operation-level metrics.
1455
+
1456
+ This method is called automatically by OperationTimer but can also
1457
+ be called manually to record operation metrics.
1458
+
1459
+ Args:
1460
+ operation_name: Name of the operation
1461
+ duration: Operation duration in seconds
1462
+ success: Whether the operation succeeded
1463
+
1464
+ Example:
1465
+ # Manual recording
1466
+ start = time.time()
1467
+ try:
1468
+ result = perform_operation()
1469
+ agent._record_operation_metrics("custom_op", time.time() - start, True)
1470
+ except Exception:
1471
+ agent._record_operation_metrics("custom_op", time.time() - start, False)
1472
+ raise
1473
+ """
1474
+ # Update operation counts
1475
+ if operation_name not in self._metrics.operation_counts:
1476
+ self._metrics.operation_counts[operation_name] = 0
1477
+ self._metrics.operation_total_time[operation_name] = 0.0
1478
+ self._metrics.operation_error_counts[operation_name] = 0
1479
+
1480
+ self._metrics.operation_counts[operation_name] += 1
1481
+ self._metrics.operation_total_time[operation_name] += duration
1482
+
1483
+ if not success:
1484
+ self._metrics.operation_error_counts[operation_name] += 1
1485
+
1486
+ # Add to operation history (keep last 100 operations)
1487
+ operation_record = {
1488
+ "operation": operation_name,
1489
+ "duration": duration,
1490
+ "success": success,
1491
+ "timestamp": datetime.utcnow().isoformat(),
1492
+ }
1493
+ self._metrics.operation_history.append(operation_record)
1494
+
1495
+ # Keep only last 100 operations
1496
+ if len(self._metrics.operation_history) > 100:
1497
+ self._metrics.operation_history = self._metrics.operation_history[-100:]
1498
+
1499
+ # Recalculate percentiles
1500
+ self._update_operation_percentiles()
1501
+
1502
+ self._metrics.updated_at = datetime.utcnow()
1503
+ logger.debug(f"Agent {self.agent_id} operation metrics recorded: " f"operation={operation_name}, duration={duration:.3f}s, success={success}")
1504
+
1505
+ def _update_operation_percentiles(self) -> None:
1506
+ """Update operation time percentiles from operation history."""
1507
+ if not self._metrics.operation_history:
1508
+ return
1509
+
1510
+ # Extract durations from operation history
1511
+ durations = [op["duration"] for op in self._metrics.operation_history]
1512
+
1513
+ # Calculate percentiles
1514
+ self._metrics.p50_operation_time = self._calculate_percentile(durations, 50)
1515
+ self._metrics.p95_operation_time = self._calculate_percentile(durations, 95)
1516
+ self._metrics.p99_operation_time = self._calculate_percentile(durations, 99)
1517
+
1518
+ def _calculate_percentile(self, values: List[float], percentile: int) -> Optional[float]:
1519
+ """
1520
+ Calculate percentile from a list of values.
1521
+
1522
+ Args:
1523
+ values: List of numeric values
1524
+ percentile: Percentile to calculate (0-100)
1525
+
1526
+ Returns:
1527
+ Percentile value or None if values is empty
1528
+
1529
+ Example:
1530
+ p95 = agent._calculate_percentile([1.0, 2.0, 3.0, 4.0, 5.0], 95)
1531
+ """
1532
+ if not values:
1533
+ return None
1534
+
1535
+ sorted_values = sorted(values)
1536
+ index = int(len(sorted_values) * percentile / 100)
1537
+
1538
+ # Handle edge cases
1539
+ if index >= len(sorted_values):
1540
+ index = len(sorted_values) - 1
1541
+
1542
+ return sorted_values[index]
1543
+
1544
+ def get_performance_metrics(self) -> Dict[str, Any]:
1545
+ """
1546
+ Get comprehensive performance metrics.
1547
+
1548
+ Returns detailed performance statistics including operation-level
1549
+ metrics, percentiles, and aggregated statistics.
1550
+
1551
+ Returns:
1552
+ Dictionary with performance metrics
1553
+
1554
+ Example:
1555
+ metrics = agent.get_performance_metrics()
1556
+ print(f"P95 latency: {metrics['p95_operation_time']}s")
1557
+ print(f"Total operations: {metrics['total_operations']}")
1558
+ for op_name, stats in metrics['operations'].items():
1559
+ print(f"{op_name}: {stats['count']} calls, avg {stats['avg_time']:.3f}s")
1560
+ """
1561
+ # Calculate per-operation statistics
1562
+ operations = {}
1563
+ for op_name, count in self._metrics.operation_counts.items():
1564
+ total_time = self._metrics.operation_total_time.get(op_name, 0.0)
1565
+ error_count = self._metrics.operation_error_counts.get(op_name, 0)
1566
+
1567
+ operations[op_name] = {
1568
+ "count": count,
1569
+ "total_time": total_time,
1570
+ "average_time": total_time / count if count > 0 else 0.0,
1571
+ "error_count": error_count,
1572
+ "error_rate": (error_count / count * 100) if count > 0 else 0.0,
1573
+ }
1574
+
1575
+ return {
1576
+ "total_operations": sum(self._metrics.operation_counts.values()),
1577
+ "operations": operations,
1578
+ "p50_operation_time": self._metrics.p50_operation_time,
1579
+ "p95_operation_time": self._metrics.p95_operation_time,
1580
+ "p99_operation_time": self._metrics.p99_operation_time,
1581
+ "recent_operations": self._metrics.operation_history[-10:], # Last 10 operations
1582
+ # Prompt cache metrics
1583
+ "prompt_cache": {
1584
+ "total_llm_requests": self._metrics.total_llm_requests,
1585
+ "cache_hits": self._metrics.cache_hits,
1586
+ "cache_misses": self._metrics.cache_misses,
1587
+ "cache_hit_rate": self._metrics.cache_hit_rate,
1588
+ "cache_hit_rate_pct": f"{self._metrics.cache_hit_rate * 100:.1f}%",
1589
+ "total_cache_read_tokens": self._metrics.total_cache_read_tokens,
1590
+ "total_cache_creation_tokens": self._metrics.total_cache_creation_tokens,
1591
+ "estimated_cache_savings_tokens": self._metrics.estimated_cache_savings_tokens,
1592
+ "estimated_cache_savings_cost": self._metrics.estimated_cache_savings_cost,
1593
+ },
1594
+ }
1595
+
1596
+ def get_health_status(self) -> Dict[str, Any]:
1597
+ """
1598
+ Get agent health status with health score calculation.
1599
+
1600
+ Calculates a health score (0-100) based on multiple factors:
1601
+ - Success rate (40% weight)
1602
+ - Error rate (30% weight)
1603
+ - Performance (20% weight)
1604
+ - Session health (10% weight)
1605
+
1606
+ Returns:
1607
+ Dictionary with health status and score
1608
+
1609
+ Example:
1610
+ health = agent.get_health_status()
1611
+ print(f"Health score: {health['health_score']}/100")
1612
+ print(f"Status: {health['status']}") # healthy, degraded, unhealthy
1613
+ if health['issues']:
1614
+ print(f"Issues: {', '.join(health['issues'])}")
1615
+ """
1616
+ issues = []
1617
+ health_score = 100.0
1618
+
1619
+ # Factor 1: Success rate (40% weight)
1620
+ success_rate = self._metrics.success_rate
1621
+ if success_rate < 50:
1622
+ issues.append("Low success rate")
1623
+ health_score -= 40
1624
+ elif success_rate < 80:
1625
+ issues.append("Moderate success rate")
1626
+ health_score -= 20
1627
+ elif success_rate < 95:
1628
+ health_score -= 10
1629
+
1630
+ # Factor 2: Error rate (30% weight)
1631
+ total_tasks = self._metrics.total_tasks_executed
1632
+ if total_tasks > 0:
1633
+ error_rate = (self._metrics.failed_tasks / total_tasks) * 100
1634
+ if error_rate > 50:
1635
+ issues.append("High error rate")
1636
+ health_score -= 30
1637
+ elif error_rate > 20:
1638
+ issues.append("Elevated error rate")
1639
+ health_score -= 15
1640
+ elif error_rate > 5:
1641
+ health_score -= 5
1642
+
1643
+ # Factor 3: Performance (20% weight)
1644
+ if self._metrics.p95_operation_time is not None:
1645
+ # Consider p95 > 5s as slow
1646
+ if self._metrics.p95_operation_time > 10:
1647
+ issues.append("Very slow operations (p95 > 10s)")
1648
+ health_score -= 20
1649
+ elif self._metrics.p95_operation_time > 5:
1650
+ issues.append("Slow operations (p95 > 5s)")
1651
+ health_score -= 10
1652
+
1653
+ # Factor 4: Session health (10% weight)
1654
+ if self._metrics.total_sessions > 0:
1655
+ session_failure_rate = (self._metrics.failed_sessions + self._metrics.expired_sessions) / self._metrics.total_sessions * 100
1656
+ if session_failure_rate > 30:
1657
+ issues.append("High session failure rate")
1658
+ health_score -= 10
1659
+ elif session_failure_rate > 10:
1660
+ health_score -= 5
1661
+
1662
+ # Ensure health score is in valid range
1663
+ health_score = max(0.0, min(100.0, health_score))
1664
+
1665
+ # Determine status
1666
+ if health_score >= 80:
1667
+ status = "healthy"
1668
+ elif health_score >= 50:
1669
+ status = "degraded"
1670
+ else:
1671
+ status = "unhealthy"
1672
+
1673
+ return {
1674
+ "health_score": health_score,
1675
+ "status": status,
1676
+ "issues": issues,
1677
+ "metrics_summary": {
1678
+ "success_rate": success_rate,
1679
+ "total_tasks": total_tasks,
1680
+ "total_sessions": self._metrics.total_sessions,
1681
+ "active_sessions": self._metrics.active_sessions,
1682
+ "p95_operation_time": self._metrics.p95_operation_time,
1683
+ },
1684
+ "timestamp": datetime.utcnow().isoformat(),
1685
+ }
1686
+
1687
+ def get_comprehensive_status(self) -> Dict[str, Any]:
1688
+ """
1689
+ Get comprehensive agent status combining all metrics.
1690
+
1691
+ Returns a complete view of agent state, health, performance,
1692
+ and operational metrics.
1693
+
1694
+ Returns:
1695
+ Dictionary with comprehensive status information
1696
+
1697
+ Example:
1698
+ status = agent.get_comprehensive_status()
1699
+ print(f"Agent: {status['agent_id']}")
1700
+ print(f"State: {status['state']}")
1701
+ print(f"Health: {status['health']['status']} ({status['health']['health_score']}/100)")
1702
+ print(f"Tasks: {status['metrics']['total_tasks_executed']}")
1703
+ print(f"Sessions: {status['metrics']['total_sessions']}")
1704
+ """
1705
+ return {
1706
+ "agent_id": self.agent_id,
1707
+ "name": self.name,
1708
+ "type": self.agent_type.value,
1709
+ "version": self.version,
1710
+ "state": self._state.value,
1711
+ "health": self.get_health_status(),
1712
+ "performance": self.get_performance_metrics(),
1713
+ "metrics": {
1714
+ # Task metrics
1715
+ "total_tasks_executed": self._metrics.total_tasks_executed,
1716
+ "successful_tasks": self._metrics.successful_tasks,
1717
+ "failed_tasks": self._metrics.failed_tasks,
1718
+ "success_rate": self._metrics.success_rate,
1719
+ # Execution time metrics
1720
+ "average_execution_time": self._metrics.average_execution_time,
1721
+ "total_execution_time": self._metrics.total_execution_time,
1722
+ # Session metrics
1723
+ "total_sessions": self._metrics.total_sessions,
1724
+ "active_sessions": self._metrics.active_sessions,
1725
+ "completed_sessions": self._metrics.completed_sessions,
1726
+ "failed_sessions": self._metrics.failed_sessions,
1727
+ "expired_sessions": self._metrics.expired_sessions,
1728
+ # Resource usage
1729
+ "total_tokens_used": self._metrics.total_tokens_used,
1730
+ "total_tool_calls": self._metrics.total_tool_calls,
1731
+ # Error tracking
1732
+ "error_count": self._metrics.error_count,
1733
+ "error_types": self._metrics.error_types,
1734
+ # Prompt cache metrics
1735
+ "cache_hit_rate": self._metrics.cache_hit_rate,
1736
+ "cache_hits": self._metrics.cache_hits,
1737
+ "cache_misses": self._metrics.cache_misses,
1738
+ "total_cache_read_tokens": self._metrics.total_cache_read_tokens,
1739
+ "estimated_cache_savings_tokens": self._metrics.estimated_cache_savings_tokens,
1740
+ },
1741
+ "capabilities": [cap.capability_type for cap in self.get_capabilities()],
1742
+ "active_goals": len([g for g in self._goals.values() if g.status == GoalStatus.IN_PROGRESS]),
1743
+ "timestamp": datetime.utcnow().isoformat(),
1744
+ }
1745
+
1746
+ def reset_metrics(self) -> None:
1747
+ """
1748
+ Reset performance and session metrics.
1749
+
1750
+ Resets all metrics to their initial state while preserving
1751
+ agent configuration and state.
1752
+
1753
+ Example:
1754
+ # Reset metrics at the start of a new monitoring period
1755
+ agent.reset_metrics()
1756
+ """
1757
+ self._metrics = AgentMetrics(last_reset_at=datetime.utcnow()) # type: ignore[call-arg]
1758
+ logger.info(f"Agent {self.agent_id} metrics reset")
1759
+
1760
+ # ==================== Serialization ====================
1761
+
1762
+ def to_dict(self) -> Dict[str, Any]:
1763
+ """
1764
+ Serialize agent to dictionary.
1765
+
1766
+ Includes health status and performance metrics for comprehensive
1767
+ agent state representation.
1768
+
1769
+ Returns:
1770
+ Dictionary representation
1771
+
1772
+ Raises:
1773
+ SerializationError: If serialization fails
1774
+ """
1775
+ try:
1776
+ return {
1777
+ "agent_id": self.agent_id,
1778
+ "name": self.name,
1779
+ "agent_type": self.agent_type.value,
1780
+ "description": self.description,
1781
+ "version": self.version,
1782
+ "state": self._state.value,
1783
+ "config": self._config.model_dump(),
1784
+ "goals": [g.model_dump() for g in self._goals.values()],
1785
+ "capabilities": [c.model_dump() for c in self._capabilities.values()],
1786
+ "metrics": self._metrics.model_dump(),
1787
+ "health_status": self.get_health_status(), # Phase 3 enhancement
1788
+ "performance_metrics": self.get_performance_metrics(), # Phase 3 enhancement
1789
+ "memory_summary": self.get_memory_summary(),
1790
+ "created_at": self.created_at.isoformat(),
1791
+ "updated_at": self.updated_at.isoformat(),
1792
+ "last_active_at": (self.last_active_at.isoformat() if self.last_active_at else None),
1793
+ }
1794
+ except Exception as e:
1795
+ raise SerializationError(
1796
+ f"Failed to serialize agent: {str(e)}",
1797
+ agent_id=self.agent_id,
1798
+ )
1799
+
1800
+ @classmethod
1801
+ def from_dict(cls, data: Dict[str, Any]) -> "BaseAIAgent":
1802
+ """
1803
+ Deserialize agent from dictionary.
1804
+
1805
+ Args:
1806
+ data: Dictionary representation
1807
+
1808
+ Returns:
1809
+ Agent instance
1810
+
1811
+ Raises:
1812
+ SerializationError: If deserialization fails
1813
+ """
1814
+ raise NotImplementedError("from_dict must be implemented by subclasses")
1815
+
1816
+ # ==================== Checkpointer Support ====================
1817
+
1818
+ async def save_checkpoint(self, session_id: str, checkpoint_id: Optional[str] = None) -> Optional[str]:
1819
+ """
1820
+ Save agent state checkpoint.
1821
+
1822
+ This method saves the current agent state using the configured checkpointer.
1823
+ If no checkpointer is configured, logs a warning and returns None.
1824
+
1825
+ Args:
1826
+ session_id: Session identifier for the checkpoint
1827
+ checkpoint_id: Optional checkpoint identifier (auto-generated if None)
1828
+
1829
+ Returns:
1830
+ Checkpoint ID if saved successfully, None otherwise
1831
+
1832
+ Example:
1833
+ # Save checkpoint with auto-generated ID
1834
+ checkpoint_id = await agent.save_checkpoint(session_id="session-123")
1835
+
1836
+ # Save checkpoint with custom ID
1837
+ checkpoint_id = await agent.save_checkpoint(
1838
+ session_id="session-123",
1839
+ checkpoint_id="v1.0"
1840
+ )
1841
+
1842
+ Note:
1843
+ Requires a checkpointer to be configured during agent initialization.
1844
+ The checkpoint includes full agent state from to_dict().
1845
+ """
1846
+ if not self._checkpointer:
1847
+ logger.warning(f"Agent {self.agent_id}: No checkpointer configured, cannot save checkpoint")
1848
+ return None
1849
+
1850
+ try:
1851
+ # Get current agent state
1852
+ checkpoint_data = self.to_dict()
1853
+
1854
+ # Add checkpoint metadata
1855
+ checkpoint_data["checkpoint_metadata"] = {
1856
+ "session_id": session_id,
1857
+ "checkpoint_id": checkpoint_id,
1858
+ "saved_at": datetime.utcnow().isoformat(),
1859
+ "agent_version": self.version,
1860
+ }
1861
+
1862
+ # Save using checkpointer
1863
+ saved_checkpoint_id = await self._checkpointer.save_checkpoint(
1864
+ agent_id=self.agent_id,
1865
+ session_id=session_id,
1866
+ checkpoint_data=checkpoint_data,
1867
+ )
1868
+
1869
+ logger.info(f"Agent {self.agent_id}: Checkpoint saved successfully " f"(session={session_id}, checkpoint={saved_checkpoint_id})")
1870
+ return saved_checkpoint_id
1871
+
1872
+ except Exception as e:
1873
+ logger.error(f"Agent {self.agent_id}: Failed to save checkpoint " f"(session={session_id}): {e}")
1874
+ return None
1875
+
1876
+ async def load_checkpoint(self, session_id: str, checkpoint_id: Optional[str] = None) -> bool:
1877
+ """
1878
+ Load agent state from checkpoint.
1879
+
1880
+ This method loads agent state from a saved checkpoint using the configured
1881
+ checkpointer. If no checkpointer is configured, logs a warning and returns False.
1882
+
1883
+ Args:
1884
+ session_id: Session identifier for the checkpoint
1885
+ checkpoint_id: Optional checkpoint identifier (loads latest if None)
1886
+
1887
+ Returns:
1888
+ True if checkpoint loaded successfully, False otherwise
1889
+
1890
+ Example:
1891
+ # Load latest checkpoint
1892
+ success = await agent.load_checkpoint(session_id="session-123")
1893
+
1894
+ # Load specific checkpoint
1895
+ success = await agent.load_checkpoint(
1896
+ session_id="session-123",
1897
+ checkpoint_id="v1.0"
1898
+ )
1899
+
1900
+ Note:
1901
+ Requires a checkpointer to be configured during agent initialization.
1902
+ This method updates the agent's internal state from the checkpoint.
1903
+ Not all state may be restorable (e.g., runtime objects, connections).
1904
+ """
1905
+ if not self._checkpointer:
1906
+ logger.warning(f"Agent {self.agent_id}: No checkpointer configured, cannot load checkpoint")
1907
+ return False
1908
+
1909
+ try:
1910
+ # Load checkpoint data
1911
+ checkpoint_data = await self._checkpointer.load_checkpoint(
1912
+ agent_id=self.agent_id,
1913
+ session_id=session_id,
1914
+ checkpoint_id=checkpoint_id,
1915
+ )
1916
+
1917
+ if not checkpoint_data:
1918
+ logger.warning(f"Agent {self.agent_id}: No checkpoint found " f"(session={session_id}, checkpoint={checkpoint_id or 'latest'})")
1919
+ return False
1920
+
1921
+ # Restore agent state from checkpoint
1922
+ self._restore_from_checkpoint(checkpoint_data)
1923
+
1924
+ logger.info(f"Agent {self.agent_id}: Checkpoint loaded successfully " f"(session={session_id}, checkpoint={checkpoint_id or 'latest'})")
1925
+ return True
1926
+
1927
+ except Exception as e:
1928
+ logger.error(f"Agent {self.agent_id}: Failed to load checkpoint " f"(session={session_id}, checkpoint={checkpoint_id or 'latest'}): {e}")
1929
+ return False
1930
+
1931
+ def _restore_from_checkpoint(self, checkpoint_data: Dict[str, Any]) -> None:
1932
+ """
1933
+ Restore agent state from checkpoint data.
1934
+
1935
+ This is an internal method that updates the agent's state from checkpoint data.
1936
+ Subclasses can override this to customize restoration logic.
1937
+
1938
+ Args:
1939
+ checkpoint_data: Checkpoint data dictionary
1940
+
1941
+ Note:
1942
+ This method restores basic agent state. Runtime objects like
1943
+ connections, file handles, etc. are not restored.
1944
+ """
1945
+ # Restore basic state
1946
+ if "state" in checkpoint_data:
1947
+ try:
1948
+ self._state = AgentState(checkpoint_data["state"])
1949
+ except (ValueError, KeyError):
1950
+ logger.warning("Could not restore state from checkpoint")
1951
+
1952
+ # Restore metrics
1953
+ if "metrics" in checkpoint_data:
1954
+ try:
1955
+ self._metrics = AgentMetrics(**checkpoint_data["metrics"])
1956
+ except Exception as e:
1957
+ logger.warning(f"Could not restore metrics from checkpoint: {e}")
1958
+
1959
+ # Restore goals
1960
+ if "goals" in checkpoint_data:
1961
+ try:
1962
+ self._goals = {}
1963
+ for goal_data in checkpoint_data["goals"]:
1964
+ goal = AgentGoal(**goal_data)
1965
+ self._goals[goal.goal_id] = goal
1966
+ except Exception as e:
1967
+ logger.warning(f"Could not restore goals from checkpoint: {e}")
1968
+
1969
+ # Update timestamps
1970
+ self.updated_at = datetime.utcnow()
1971
+
1972
+ logger.debug(f"Agent {self.agent_id}: State restored from checkpoint")
1973
+
1974
+ # ==================== Utility Methods ====================
1975
+
1976
+ def is_available(self) -> bool:
1977
+ """Check if agent is available for tasks."""
1978
+ return self._state == AgentState.ACTIVE
1979
+
1980
+ def is_busy(self) -> bool:
1981
+ """Check if agent is currently busy."""
1982
+ return self._state == AgentState.BUSY
1983
+
1984
+ async def execute_tool(self, tool_name: str, parameters: Dict[str, Any]) -> Any:
1985
+ """
1986
+ Execute a single tool with given parameters.
1987
+
1988
+ This is a default implementation that subclasses can override.
1989
+ For ToolAgent, this calls _execute_tool with operation from parameters.
1990
+
1991
+ Args:
1992
+ tool_name: Name of the tool to execute
1993
+ parameters: Tool parameters (may include 'operation' key)
1994
+
1995
+ Returns:
1996
+ Tool execution result
1997
+ """
1998
+ # Check if we have tool instances
1999
+ if hasattr(self, "_tool_instances") and self._tool_instances:
2000
+ tool = self._tool_instances.get(tool_name)
2001
+ if tool:
2002
+ # Make a copy to avoid modifying the original
2003
+ params = parameters.copy()
2004
+
2005
+ # Try to execute the tool directly (for custom tools with execute method)
2006
+ if hasattr(tool, "execute"):
2007
+ return await tool.execute(**params)
2008
+ # For standard tools with run_async
2009
+ elif hasattr(tool, "run_async"):
2010
+ # Check if operation is specified
2011
+ operation = params.pop("operation", None)
2012
+ if operation:
2013
+ return await tool.run_async(operation, **params)
2014
+ else:
2015
+ return await tool.run_async(**params)
2016
+
2017
+ raise NotImplementedError(f"execute_tool not implemented for {self.__class__.__name__}. " "Tool {tool_name} not found or doesn't have execute/run_async method.")
2018
+
2019
+ # ==================== Parallel Tool Execution (Phase 7) ====================
2020
+
2021
+ async def execute_tools_parallel(
2022
+ self,
2023
+ tool_calls: List[Dict[str, Any]],
2024
+ max_concurrency: int = 5,
2025
+ ) -> List[Dict[str, Any]]:
2026
+ """
2027
+ Execute multiple tools in parallel with concurrency limit.
2028
+
2029
+ Args:
2030
+ tool_calls: List of tool call dicts with 'tool_name' and 'parameters'
2031
+ max_concurrency: Maximum number of concurrent tool executions
2032
+
2033
+ Returns:
2034
+ List of results in same order as tool_calls
2035
+
2036
+ Example:
2037
+ tool_calls = [
2038
+ {"tool_name": "search", "parameters": {"query": "AI"}},
2039
+ {"tool_name": "calculator", "parameters": {"expression": "2+2"}},
2040
+ {"tool_name": "search", "parameters": {"query": "ML"}},
2041
+ ]
2042
+ results = await agent.execute_tools_parallel(tool_calls, max_concurrency=2)
2043
+ """
2044
+ if not tool_calls:
2045
+ return []
2046
+
2047
+ # Create semaphore for concurrency control
2048
+ semaphore = asyncio.Semaphore(max_concurrency)
2049
+
2050
+ async def execute_with_semaphore(tool_call: Dict[str, Any], index: int):
2051
+ """Execute tool with semaphore."""
2052
+ async with semaphore:
2053
+ tool_name = tool_call.get("tool_name")
2054
+ parameters = tool_call.get("parameters", {})
2055
+
2056
+ if tool_name is None:
2057
+ raise ValueError("tool_name is required in tool_call")
2058
+
2059
+ try:
2060
+ # Execute tool (subclass should implement execute_tool)
2061
+ result = await self.execute_tool(tool_name, parameters)
2062
+ return {"index": index, "success": True, "result": result}
2063
+ except Exception as e:
2064
+ logger.error(f"Tool {tool_name} failed: {e}")
2065
+ return {
2066
+ "index": index,
2067
+ "success": False,
2068
+ "error": str(e),
2069
+ "tool_name": tool_name,
2070
+ }
2071
+
2072
+ # Execute all tools in parallel
2073
+ tasks = [execute_with_semaphore(tool_call, i) for i, tool_call in enumerate(tool_calls)]
2074
+
2075
+ results_unordered = await asyncio.gather(*tasks, return_exceptions=True)
2076
+
2077
+ # Sort results by index to maintain order
2078
+ valid_results = [r for r in results_unordered if not isinstance(r, Exception) and isinstance(r, dict) and "index" in r]
2079
+ results_sorted = sorted(
2080
+ valid_results,
2081
+ key=lambda x: x["index"], # type: ignore[index]
2082
+ )
2083
+
2084
+ # Remove index from results
2085
+ return [{k: v for k, v in r.items() if k != "index"} for r in results_sorted]
2086
+
2087
+ async def analyze_tool_dependencies(self, tool_calls: List[Dict[str, Any]]) -> Dict[str, List[str]]:
2088
+ """
2089
+ Analyze dependencies between tool calls.
2090
+
2091
+ Detects if one tool's output is used as input to another tool.
2092
+
2093
+ Args:
2094
+ tool_calls: List of tool call dicts
2095
+
2096
+ Returns:
2097
+ Dict mapping tool index to list of dependency indices
2098
+
2099
+ Example:
2100
+ tool_calls = [
2101
+ {"tool_name": "search", "parameters": {"query": "AI"}},
2102
+ {"tool_name": "summarize", "parameters": {"text": "${0.result}"}},
2103
+ ]
2104
+ deps = await agent.analyze_tool_dependencies(tool_calls)
2105
+ # deps = {"1": ["0"]} # Tool 1 depends on tool 0
2106
+ """
2107
+ dependencies: Dict[str, List[str]] = {}
2108
+
2109
+ for i, tool_call in enumerate(tool_calls):
2110
+ deps = []
2111
+ parameters = tool_call.get("parameters", {})
2112
+
2113
+ # Check if parameters reference other tool results
2114
+ param_str = json.dumps(parameters)
2115
+
2116
+ # Look for ${index.field} patterns
2117
+ import re
2118
+
2119
+ matches = re.findall(r"\$\{(\d+)\.", param_str)
2120
+ deps = list(set(matches)) # Remove duplicates
2121
+
2122
+ if deps:
2123
+ dependencies[str(i)] = deps
2124
+
2125
+ return dependencies
2126
+
2127
+ async def execute_tools_with_dependencies(self, tool_calls: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
2128
+ """
2129
+ Execute tools respecting dependencies using topological sort.
2130
+
2131
+ Args:
2132
+ tool_calls: List of tool call dicts
2133
+
2134
+ Returns:
2135
+ List of results in same order as tool_calls
2136
+
2137
+ Example:
2138
+ tool_calls = [
2139
+ {"tool_name": "search", "parameters": {"query": "AI"}},
2140
+ {"tool_name": "summarize", "parameters": {"text": "${0.result}"}},
2141
+ ]
2142
+ results = await agent.execute_tools_with_dependencies(tool_calls)
2143
+ """
2144
+ # Analyze dependencies
2145
+ dependencies = await self.analyze_tool_dependencies(tool_calls)
2146
+
2147
+ # Topological sort
2148
+ executed: Set[int] = set()
2149
+ results: List[Optional[Dict[str, Any]]] = [None] * len(tool_calls)
2150
+
2151
+ def can_execute(index: int) -> bool:
2152
+ """Check if tool can be executed."""
2153
+ deps = dependencies.get(str(index), [])
2154
+ return all(int(dep) in executed for dep in deps)
2155
+
2156
+ # Execute tools in dependency order
2157
+ while len(executed) < len(tool_calls):
2158
+ # Find tools that can be executed
2159
+ ready = [i for i in range(len(tool_calls)) if i not in executed and can_execute(i)]
2160
+
2161
+ if not ready:
2162
+ # Circular dependency or error
2163
+ logger.error("Circular dependency detected or no tools ready")
2164
+ break
2165
+
2166
+ # Execute ready tools in parallel
2167
+ ready_calls = [tool_calls[i] for i in ready]
2168
+ ready_results = await self.execute_tools_parallel(ready_calls)
2169
+
2170
+ # Store results and mark as executed
2171
+ for i, result in zip(ready, ready_results):
2172
+ if result is not None:
2173
+ results[i] = result
2174
+ executed.add(i)
2175
+
2176
+ # Substitute results in dependent tool calls
2177
+ for j in range(len(tool_calls)):
2178
+ if j not in executed:
2179
+ tool_calls[j] = self._substitute_tool_result(tool_calls[j], i, result)
2180
+
2181
+ # Filter out None values and return
2182
+ return [r for r in results if r is not None]
2183
+
2184
+ def _substitute_tool_result(self, tool_call: Dict[str, Any], source_index: int, source_result: Dict[str, Any]) -> Dict[str, Any]:
2185
+ """
2186
+ Substitute tool result references in parameters.
2187
+
2188
+ Args:
2189
+ tool_call: Tool call dict
2190
+ source_index: Index of source tool
2191
+ source_result: Result from source tool
2192
+
2193
+ Returns:
2194
+ Updated tool call dict
2195
+ """
2196
+ import re
2197
+
2198
+ param_str = json.dumps(tool_call.get("parameters", {}))
2199
+
2200
+ # Replace ${index.field} with actual values
2201
+ pattern = rf"\$\{{{source_index}\.(\w+)\}}"
2202
+
2203
+ def replacer(match):
2204
+ field = match.group(1)
2205
+ value = source_result.get(field)
2206
+ return json.dumps(value) if value is not None else "null"
2207
+
2208
+ param_str = re.sub(pattern, replacer, param_str)
2209
+
2210
+ tool_call["parameters"] = json.loads(param_str)
2211
+ return tool_call
2212
+
2213
+ # ==================== Tool Result Caching (Phase 7) ====================
2214
+
2215
+ def _generate_cache_key(self, tool_name: str, parameters: Dict[str, Any]) -> str:
2216
+ """
2217
+ Generate cache key for tool result.
2218
+
2219
+ Args:
2220
+ tool_name: Name of the tool
2221
+ parameters: Tool parameters
2222
+
2223
+ Returns:
2224
+ Cache key string
2225
+
2226
+ Example:
2227
+ key = agent._generate_cache_key("search", {"query": "AI"})
2228
+ """
2229
+ # Sort parameters for consistent keys
2230
+ param_str = json.dumps(parameters, sort_keys=True)
2231
+
2232
+ # Hash large inputs
2233
+ if self._cache_config.hash_large_inputs and len(param_str) > 1024:
2234
+ import hashlib
2235
+
2236
+ param_hash = hashlib.md5(param_str.encode()).hexdigest()
2237
+ cache_key = f"{tool_name}:{param_hash}"
2238
+ else:
2239
+ cache_key = f"{tool_name}:{param_str}"
2240
+
2241
+ # Include timestamp if configured
2242
+ if self._cache_config.include_timestamp_in_key:
2243
+ timestamp = int(time.time() / 60) # Minute-level granularity
2244
+ cache_key = f"{cache_key}:{timestamp}"
2245
+
2246
+ return cache_key
2247
+
2248
+ async def execute_tool_with_cache(self, tool_name: str, parameters: Dict[str, Any]) -> Any:
2249
+ """
2250
+ Execute tool with caching support.
2251
+
2252
+ Args:
2253
+ tool_name: Name of the tool
2254
+ parameters: Tool parameters
2255
+
2256
+ Returns:
2257
+ Tool result (from cache or fresh execution)
2258
+
2259
+ Example:
2260
+ result = await agent.execute_tool_with_cache("search", {"query": "AI"})
2261
+ """
2262
+ if not self._cache_config.enabled:
2263
+ # Cache disabled, execute directly
2264
+ return await self.execute_tool(tool_name, parameters)
2265
+
2266
+ # Generate cache key
2267
+ cache_key = self._generate_cache_key(tool_name, parameters)
2268
+
2269
+ # Check cache
2270
+ if cache_key in self._tool_cache:
2271
+ # Check TTL
2272
+ cached_time = self._cache_timestamps.get(cache_key, 0)
2273
+ ttl = self._cache_config.get_ttl(tool_name)
2274
+ age = time.time() - cached_time
2275
+
2276
+ if age < ttl:
2277
+ # Cache hit
2278
+ self._cache_access_count[cache_key] = self._cache_access_count.get(cache_key, 0) + 1
2279
+ logger.debug(f"Cache hit for {tool_name} (age: {age:.1f}s)")
2280
+ return self._tool_cache[cache_key]
2281
+ else:
2282
+ # Cache expired
2283
+ logger.debug(f"Cache expired for {tool_name} (age: {age:.1f}s)")
2284
+ del self._tool_cache[cache_key]
2285
+ del self._cache_timestamps[cache_key]
2286
+ if cache_key in self._cache_access_count:
2287
+ del self._cache_access_count[cache_key]
2288
+
2289
+ # Cache miss - execute tool
2290
+ logger.debug(f"Cache miss for {tool_name}")
2291
+ result = await self.execute_tool(tool_name, parameters)
2292
+
2293
+ # Store in cache
2294
+ self._tool_cache[cache_key] = result
2295
+ self._cache_timestamps[cache_key] = time.time()
2296
+ self._cache_access_count[cache_key] = 0
2297
+
2298
+ # Cleanup if needed
2299
+ await self._cleanup_cache()
2300
+
2301
+ return result
2302
+
2303
+ def invalidate_cache(self, tool_name: Optional[str] = None, pattern: Optional[str] = None) -> int:
2304
+ """
2305
+ Invalidate cache entries.
2306
+
2307
+ Args:
2308
+ tool_name: Invalidate all entries for this tool (optional)
2309
+ pattern: Invalidate entries matching pattern (optional)
2310
+
2311
+ Returns:
2312
+ Number of entries invalidated
2313
+
2314
+ Example:
2315
+ # Invalidate all search results
2316
+ count = agent.invalidate_cache(tool_name="search")
2317
+
2318
+ # Invalidate all cache
2319
+ count = agent.invalidate_cache()
2320
+ """
2321
+ if tool_name is None and pattern is None:
2322
+ # Invalidate all
2323
+ count = len(self._tool_cache)
2324
+ self._tool_cache.clear()
2325
+ self._cache_timestamps.clear()
2326
+ self._cache_access_count.clear()
2327
+ logger.info(f"Invalidated all cache ({count} entries)")
2328
+ return count
2329
+
2330
+ # Invalidate matching entries
2331
+ keys_to_delete = []
2332
+
2333
+ for key in list(self._tool_cache.keys()):
2334
+ if tool_name and key.startswith(f"{tool_name}:"):
2335
+ keys_to_delete.append(key)
2336
+ elif pattern and pattern in key:
2337
+ keys_to_delete.append(key)
2338
+
2339
+ for key in keys_to_delete:
2340
+ del self._tool_cache[key]
2341
+ del self._cache_timestamps[key]
2342
+ if key in self._cache_access_count:
2343
+ del self._cache_access_count[key]
2344
+
2345
+ logger.info(f"Invalidated {len(keys_to_delete)} cache entries")
2346
+ return len(keys_to_delete)
2347
+
2348
+ def get_cache_stats(self) -> Dict[str, Any]:
2349
+ """
2350
+ Get cache statistics.
2351
+
2352
+ Returns:
2353
+ Dictionary with cache statistics
2354
+
2355
+ Example:
2356
+ stats = agent.get_cache_stats()
2357
+ print(f"Cache size: {stats['size']}")
2358
+ print(f"Hit rate: {stats['hit_rate']:.1%}")
2359
+ """
2360
+ total_entries = len(self._tool_cache)
2361
+ total_accesses = sum(self._cache_access_count.values())
2362
+
2363
+ # Calculate hit rate (approximate)
2364
+ cache_hits = sum(count for count in self._cache_access_count.values() if count > 0)
2365
+ hit_rate = cache_hits / total_accesses if total_accesses > 0 else 0.0
2366
+
2367
+ # Calculate memory usage (approximate)
2368
+ import sys
2369
+
2370
+ memory_bytes = sum(sys.getsizeof(v) for v in self._tool_cache.values())
2371
+ memory_mb = memory_bytes / (1024 * 1024)
2372
+
2373
+ # Per-tool stats
2374
+ tool_stats = {}
2375
+ for key in self._tool_cache.keys():
2376
+ tool_name = key.split(":")[0]
2377
+ if tool_name not in tool_stats:
2378
+ tool_stats[tool_name] = {"count": 0, "accesses": 0}
2379
+ tool_stats[tool_name]["count"] += 1
2380
+ tool_stats[tool_name]["accesses"] += self._cache_access_count.get(key, 0)
2381
+
2382
+ return {
2383
+ "enabled": self._cache_config.enabled,
2384
+ "size": total_entries,
2385
+ "max_size": self._cache_config.max_cache_size,
2386
+ "memory_mb": memory_mb,
2387
+ "max_memory_mb": self._cache_config.max_memory_mb,
2388
+ "total_accesses": total_accesses,
2389
+ "hit_rate": hit_rate,
2390
+ "tool_stats": tool_stats,
2391
+ }
2392
+
2393
+ async def _cleanup_cache(self) -> None:
2394
+ """
2395
+ Cleanup cache based on size and memory limits.
2396
+
2397
+ Removes least recently used entries when limits are exceeded.
2398
+ """
2399
+ # Check if cleanup needed
2400
+ current_time = time.time()
2401
+ if current_time - self._last_cleanup_time < self._cache_config.cleanup_interval:
2402
+ return
2403
+
2404
+ self._last_cleanup_time = current_time
2405
+
2406
+ # Check size limit
2407
+ if len(self._tool_cache) > self._cache_config.max_cache_size * self._cache_config.cleanup_threshold:
2408
+ # Remove oldest entries
2409
+ entries_to_remove = int(len(self._tool_cache) - self._cache_config.max_cache_size * 0.8)
2410
+
2411
+ # Sort by timestamp (oldest first)
2412
+ sorted_keys = sorted(self._cache_timestamps.items(), key=lambda x: x[1])
2413
+
2414
+ for key, _ in sorted_keys[:entries_to_remove]:
2415
+ del self._tool_cache[key]
2416
+ del self._cache_timestamps[key]
2417
+ if key in self._cache_access_count:
2418
+ del self._cache_access_count[key]
2419
+
2420
+ logger.debug(f"Cleaned up {entries_to_remove} cache entries (size limit)")
2421
+
2422
+ # ==================== Streaming Support (Phase 7 - Tasks 1.15.11-1.15.12) ====================
2423
+
2424
+ async def execute_task_streaming(self, task: Dict[str, Any], context: Dict[str, Any]) -> AsyncIterator[Dict[str, Any]]:
2425
+ """
2426
+ Execute a task with streaming results.
2427
+
2428
+ This method streams task execution events as they occur, including:
2429
+ - Status updates (started, thinking, acting, completed)
2430
+ - LLM tokens (for agents with LLM clients)
2431
+ - Tool calls and results (for agents with tools)
2432
+ - Final result
2433
+
2434
+ Args:
2435
+ task: Task specification
2436
+ context: Execution context
2437
+
2438
+ Yields:
2439
+ Dict[str, Any]: Event dictionaries with 'type' and event-specific data
2440
+
2441
+ Event types:
2442
+ - 'status': Status update (e.g., started, thinking, completed)
2443
+ - 'token': LLM token (for streaming text generation)
2444
+ - 'tool_call': Tool execution started
2445
+ - 'tool_result': Tool execution completed
2446
+ - 'result': Final task result
2447
+ - 'error': Error occurred
2448
+
2449
+ Example:
2450
+ ```python
2451
+ async for event in agent.execute_task_streaming(task, context):
2452
+ if event['type'] == 'token':
2453
+ print(event['content'], end='', flush=True)
2454
+ elif event['type'] == 'tool_call':
2455
+ print(f"\\nCalling tool: {event['tool_name']}")
2456
+ elif event['type'] == 'tool_result':
2457
+ print(f"Tool result: {event['result']}")
2458
+ elif event['type'] == 'result':
2459
+ print(f"\\nFinal result: {event['output']}")
2460
+ ```
2461
+
2462
+ Note:
2463
+ Subclasses should override this method to provide streaming support.
2464
+ Default implementation falls back to non-streaming execute_task.
2465
+ """
2466
+ # Default implementation: execute task and yield result
2467
+ yield {"type": "status", "status": "started", "timestamp": datetime.utcnow().isoformat()}
2468
+
2469
+ try:
2470
+ result = await self.execute_task(task, context)
2471
+ yield {"type": "result", **result}
2472
+ except Exception as e:
2473
+ yield {
2474
+ "type": "error",
2475
+ "error": str(e),
2476
+ "timestamp": datetime.utcnow().isoformat(),
2477
+ }
2478
+ raise
2479
+
2480
+ async def process_message_streaming(self, message: str, sender_id: Optional[str] = None) -> AsyncIterator[str]:
2481
+ """
2482
+ Process a message with streaming response.
2483
+
2484
+ This method streams the response text as it's generated, providing
2485
+ a better user experience for long responses.
2486
+
2487
+ Args:
2488
+ message: Message content
2489
+ sender_id: Optional sender identifier
2490
+
2491
+ Yields:
2492
+ str: Response text tokens/chunks
2493
+
2494
+ Example:
2495
+ ```python
2496
+ async for token in agent.process_message_streaming("Hello!"):
2497
+ print(token, end='', flush=True)
2498
+ ```
2499
+
2500
+ Note:
2501
+ Subclasses should override this method to provide streaming support.
2502
+ Default implementation falls back to non-streaming process_message.
2503
+ """
2504
+ # Default implementation: process message and yield result
2505
+ try:
2506
+ result = await self.process_message(message, sender_id)
2507
+ response = result.get("response", "")
2508
+ yield response
2509
+ except Exception as e:
2510
+ logger.error(f"Streaming message processing failed: {e}")
2511
+ raise
2512
+
2513
+ # ==================== Agent Collaboration (Phase 7 - Tasks 1.15.15-1.15.22) ====================
2514
+
2515
+ async def delegate_task(
2516
+ self,
2517
+ task: Dict[str, Any],
2518
+ required_capabilities: Optional[List[str]] = None,
2519
+ target_agent_id: Optional[str] = None,
2520
+ ) -> Dict[str, Any]:
2521
+ """
2522
+ Delegate a task to another capable agent.
2523
+
2524
+ Args:
2525
+ task: Task specification to delegate
2526
+ required_capabilities: Required capabilities for the task
2527
+ target_agent_id: Specific agent to delegate to (if None, finds capable agent)
2528
+
2529
+ Returns:
2530
+ Task execution result from delegated agent
2531
+
2532
+ Raises:
2533
+ ValueError: If collaboration not enabled or no capable agent found
2534
+
2535
+ Example:
2536
+ ```python
2537
+ # Delegate to specific agent
2538
+ result = await agent.delegate_task(
2539
+ task={"description": "Search for AI papers"},
2540
+ target_agent_id="search_agent"
2541
+ )
2542
+
2543
+ # Delegate to any capable agent
2544
+ result = await agent.delegate_task(
2545
+ task={"description": "Analyze data"},
2546
+ required_capabilities=["data_analysis", "statistics"]
2547
+ )
2548
+ ```
2549
+ """
2550
+ if not self._collaboration_enabled:
2551
+ raise ValueError("Agent collaboration is not enabled")
2552
+
2553
+ # Find target agent
2554
+ if target_agent_id:
2555
+ target_agent = self._agent_registry.get(target_agent_id)
2556
+ if not target_agent:
2557
+ raise ValueError(f"Agent {target_agent_id} not found in registry")
2558
+ elif required_capabilities:
2559
+ capable_agents = await self.find_capable_agents(required_capabilities)
2560
+ if not capable_agents:
2561
+ raise ValueError(f"No capable agents found for capabilities: {required_capabilities}")
2562
+ target_agent = capable_agents[0] # Use first capable agent
2563
+ else:
2564
+ raise ValueError("Either target_agent_id or required_capabilities must be provided")
2565
+
2566
+ logger.info(f"Agent {self.agent_id} delegating task to {target_agent.agent_id}")
2567
+
2568
+ # Delegate task
2569
+ try:
2570
+ result = await target_agent.execute_task(task, context={"delegated_by": self.agent_id})
2571
+ logger.info(f"Task delegation successful: {self.agent_id} -> {target_agent.agent_id}")
2572
+ return result
2573
+ except Exception as e:
2574
+ logger.error(f"Task delegation failed: {e}")
2575
+ raise
2576
+
2577
+ async def find_capable_agents(self, required_capabilities: List[str]) -> List[Any]:
2578
+ """
2579
+ Find agents with required capabilities.
2580
+
2581
+ Args:
2582
+ required_capabilities: List of required capability names
2583
+
2584
+ Returns:
2585
+ List of agents that have all required capabilities
2586
+
2587
+ Example:
2588
+ ```python
2589
+ agents = await agent.find_capable_agents(["search", "summarize"])
2590
+ for capable_agent in agents:
2591
+ print(f"Found: {capable_agent.name}")
2592
+ ```
2593
+ """
2594
+ if not self._collaboration_enabled:
2595
+ return []
2596
+
2597
+ capable_agents = []
2598
+ for agent_id, agent in self._agent_registry.items():
2599
+ # Skip self
2600
+ if agent_id == self.agent_id:
2601
+ continue
2602
+
2603
+ # Check if agent has all required capabilities
2604
+ agent_capabilities = getattr(agent, "capabilities", [])
2605
+ if all(cap in agent_capabilities for cap in required_capabilities):
2606
+ capable_agents.append(agent)
2607
+
2608
+ logger.debug(f"Found {len(capable_agents)} capable agents for {required_capabilities}")
2609
+ return capable_agents
2610
+
2611
+ async def request_peer_review(
2612
+ self,
2613
+ task: Dict[str, Any],
2614
+ result: Dict[str, Any],
2615
+ reviewer_id: Optional[str] = None,
2616
+ ) -> Dict[str, Any]:
2617
+ """
2618
+ Request peer review of a task result.
2619
+
2620
+ Args:
2621
+ task: Original task specification
2622
+ result: Task execution result to review
2623
+ reviewer_id: Specific reviewer agent ID (if None, selects automatically)
2624
+
2625
+ Returns:
2626
+ Review result with 'approved' (bool), 'feedback' (str), 'reviewer_id' (str)
2627
+
2628
+ Example:
2629
+ ```python
2630
+ result = await agent.execute_task(task, context)
2631
+ review = await agent.request_peer_review(task, result)
2632
+ if review['approved']:
2633
+ print(f"Approved: {review['feedback']}")
2634
+ else:
2635
+ print(f"Needs revision: {review['feedback']}")
2636
+ ```
2637
+ """
2638
+ if not self._collaboration_enabled:
2639
+ raise ValueError("Agent collaboration is not enabled")
2640
+
2641
+ # Find reviewer
2642
+ if reviewer_id:
2643
+ reviewer = self._agent_registry.get(reviewer_id)
2644
+ if not reviewer:
2645
+ raise ValueError(f"Reviewer {reviewer_id} not found in registry")
2646
+ else:
2647
+ # Select first available agent (excluding self)
2648
+ available_reviewers = [agent for agent_id, agent in self._agent_registry.items() if agent_id != self.agent_id]
2649
+ if not available_reviewers:
2650
+ raise ValueError("No reviewers available")
2651
+ reviewer = available_reviewers[0]
2652
+
2653
+ logger.info(f"Agent {self.agent_id} requesting review from {reviewer.agent_id}")
2654
+
2655
+ # Request review
2656
+ try:
2657
+ if hasattr(reviewer, "review_result"):
2658
+ review = await reviewer.review_result(task, result)
2659
+ else:
2660
+ # Fallback: use execute_task with review prompt
2661
+ task_desc = task.get("description", "")
2662
+ task_result = result.get("output", "")
2663
+ review_task = {
2664
+ "description": (f"Review this task result:\nTask: {task_desc}\nResult: {task_result}"),
2665
+ "task_id": f"review_{task.get('task_id', 'unknown')}",
2666
+ }
2667
+ review_result = await reviewer.execute_task(review_task, context={})
2668
+ review = {
2669
+ "approved": True, # Assume approved if no explicit review method
2670
+ "feedback": review_result.get("output", ""),
2671
+ "reviewer_id": reviewer.agent_id,
2672
+ }
2673
+
2674
+ logger.info(f"Review received from {reviewer.agent_id}")
2675
+ return review
2676
+ except Exception as e:
2677
+ logger.error(f"Peer review failed: {e}")
2678
+ raise
2679
+
2680
+ async def collaborate_on_task(
2681
+ self,
2682
+ task: Dict[str, Any],
2683
+ collaborator_ids: List[str],
2684
+ strategy: str = "parallel",
2685
+ ) -> Dict[str, Any]:
2686
+ """
2687
+ Collaborate with other agents on a task.
2688
+
2689
+ Args:
2690
+ task: Task specification
2691
+ collaborator_ids: List of agent IDs to collaborate with
2692
+ strategy: Collaboration strategy - 'parallel', 'sequential', or 'consensus'
2693
+
2694
+ Returns:
2695
+ Aggregated result based on strategy
2696
+
2697
+ Strategies:
2698
+ - parallel: All agents work simultaneously, results aggregated
2699
+ - sequential: Agents work in order, each building on previous results
2700
+ - consensus: All agents work independently, best result selected by voting
2701
+
2702
+ Example:
2703
+ ```python
2704
+ # Parallel collaboration
2705
+ result = await agent.collaborate_on_task(
2706
+ task={"description": "Analyze market trends"},
2707
+ collaborator_ids=["analyst1", "analyst2", "analyst3"],
2708
+ strategy="parallel"
2709
+ )
2710
+
2711
+ # Sequential collaboration (pipeline)
2712
+ result = await agent.collaborate_on_task(
2713
+ task={"description": "Research and summarize"},
2714
+ collaborator_ids=["researcher", "summarizer"],
2715
+ strategy="sequential"
2716
+ )
2717
+
2718
+ # Consensus collaboration
2719
+ result = await agent.collaborate_on_task(
2720
+ task={"description": "Make recommendation"},
2721
+ collaborator_ids=["expert1", "expert2", "expert3"],
2722
+ strategy="consensus"
2723
+ )
2724
+ ```
2725
+ """
2726
+ if not self._collaboration_enabled:
2727
+ raise ValueError("Agent collaboration is not enabled")
2728
+
2729
+ # Get collaborator agents
2730
+ collaborators = []
2731
+ for agent_id in collaborator_ids:
2732
+ agent = self._agent_registry.get(agent_id)
2733
+ if not agent:
2734
+ logger.warning(f"Collaborator {agent_id} not found, skipping")
2735
+ continue
2736
+ collaborators.append(agent)
2737
+
2738
+ if not collaborators:
2739
+ raise ValueError("No valid collaborators found")
2740
+
2741
+ logger.info(f"Agent {self.agent_id} collaborating with {len(collaborators)} agents " f"using {strategy} strategy")
2742
+
2743
+ # Execute based on strategy
2744
+ if strategy == "parallel":
2745
+ return await self._collaborate_parallel(task, collaborators)
2746
+ elif strategy == "sequential":
2747
+ return await self._collaborate_sequential(task, collaborators)
2748
+ elif strategy == "consensus":
2749
+ return await self._collaborate_consensus(task, collaborators)
2750
+ else:
2751
+ raise ValueError(f"Unknown collaboration strategy: {strategy}")
2752
+
2753
+ async def _collaborate_parallel(self, task: Dict[str, Any], collaborators: List[Any]) -> Dict[str, Any]:
2754
+ """
2755
+ Parallel collaboration: all agents work simultaneously.
2756
+
2757
+ Args:
2758
+ task: Task specification
2759
+ collaborators: List of collaborator agents
2760
+
2761
+ Returns:
2762
+ Aggregated result
2763
+ """
2764
+ # Execute task on all agents in parallel
2765
+ tasks = [agent.execute_task(task, context={"collaboration": "parallel"}) for agent in collaborators]
2766
+
2767
+ results = await asyncio.gather(*tasks, return_exceptions=True)
2768
+
2769
+ # Aggregate results
2770
+ return await self._aggregate_results(task, results, collaborators)
2771
+
2772
+ async def _collaborate_sequential(self, task: Dict[str, Any], collaborators: List[Any]) -> Dict[str, Any]:
2773
+ """
2774
+ Sequential collaboration: agents work in order, building on previous results.
2775
+
2776
+ Args:
2777
+ task: Task specification
2778
+ collaborators: List of collaborator agents (in execution order)
2779
+
2780
+ Returns:
2781
+ Final result from last agent
2782
+ """
2783
+ current_task = task.copy()
2784
+ results = []
2785
+
2786
+ for i, agent in enumerate(collaborators):
2787
+ logger.debug(f"Sequential step {i + 1}/{len(collaborators)}: {agent.agent_id}")
2788
+
2789
+ # Execute task
2790
+ result = await agent.execute_task(current_task, context={"collaboration": "sequential", "step": i + 1})
2791
+ results.append(result)
2792
+
2793
+ # Update task for next agent with previous result
2794
+ if i < len(collaborators) - 1:
2795
+ current_task = {
2796
+ "description": f"{task.get('description')}\n\nPrevious result: {result.get('output')}",
2797
+ "task_id": f"{task.get('task_id', 'unknown')}_step_{i + 2}",
2798
+ }
2799
+
2800
+ # Return final result
2801
+ return {
2802
+ "success": True,
2803
+ "output": results[-1].get("output") if results else "",
2804
+ "collaboration_strategy": "sequential",
2805
+ "steps": len(results),
2806
+ "all_results": results,
2807
+ "timestamp": datetime.utcnow().isoformat(),
2808
+ }
2809
+
2810
+ async def _collaborate_consensus(self, task: Dict[str, Any], collaborators: List[Any]) -> Dict[str, Any]:
2811
+ """
2812
+ Consensus collaboration: all agents work independently, best result selected.
2813
+
2814
+ Args:
2815
+ task: Task specification
2816
+ collaborators: List of collaborator agents
2817
+
2818
+ Returns:
2819
+ Best result selected by consensus
2820
+ """
2821
+ # Execute task on all agents in parallel
2822
+ tasks = [agent.execute_task(task, context={"collaboration": "consensus"}) for agent in collaborators]
2823
+
2824
+ results = await asyncio.gather(*tasks, return_exceptions=True)
2825
+
2826
+ # Select best result by consensus
2827
+ return await self._select_consensus_result(task, results, collaborators)
2828
+
2829
+ async def _aggregate_results(self, task: Dict[str, Any], results: List[Any], collaborators: List[Any]) -> Dict[str, Any]:
2830
+ """
2831
+ Aggregate results from parallel collaboration.
2832
+
2833
+ Args:
2834
+ task: Original task
2835
+ results: List of results from collaborators
2836
+ collaborators: List of collaborator agents
2837
+
2838
+ Returns:
2839
+ Aggregated result
2840
+ """
2841
+ successful_results = []
2842
+ errors = []
2843
+
2844
+ for i, result in enumerate(results):
2845
+ if isinstance(result, Exception):
2846
+ errors.append({"agent": collaborators[i].agent_id, "error": str(result)})
2847
+ else:
2848
+ successful_results.append({"agent": collaborators[i].agent_id, "result": result})
2849
+
2850
+ # Combine outputs
2851
+ combined_output = "\n\n".join([f"[{r['agent']}]: {r['result'].get('output', '')}" for r in successful_results])
2852
+
2853
+ return {
2854
+ "success": len(successful_results) > 0,
2855
+ "output": combined_output,
2856
+ "collaboration_strategy": "parallel",
2857
+ "successful_agents": len(successful_results),
2858
+ "failed_agents": len(errors),
2859
+ "results": successful_results,
2860
+ "errors": errors if errors else None,
2861
+ "timestamp": datetime.utcnow().isoformat(),
2862
+ }
2863
+
2864
+ async def _select_consensus_result(self, task: Dict[str, Any], results: List[Any], collaborators: List[Any]) -> Dict[str, Any]:
2865
+ """
2866
+ Select best result by consensus voting.
2867
+
2868
+ Args:
2869
+ task: Original task
2870
+ results: List of results from collaborators
2871
+ collaborators: List of collaborator agents
2872
+
2873
+ Returns:
2874
+ Best result selected by consensus
2875
+ """
2876
+ successful_results = []
2877
+
2878
+ for i, result in enumerate(results):
2879
+ if not isinstance(result, Exception):
2880
+ successful_results.append({"agent": collaborators[i].agent_id, "result": result, "votes": 0})
2881
+
2882
+ if not successful_results:
2883
+ return {
2884
+ "success": False,
2885
+ "output": "All collaborators failed",
2886
+ "collaboration_strategy": "consensus",
2887
+ "timestamp": datetime.utcnow().isoformat(),
2888
+ }
2889
+
2890
+ # Simple voting: each agent votes for best result (excluding their own)
2891
+ # In a real implementation, this could use LLM to evaluate quality
2892
+ for voter_idx, voter_result in enumerate(successful_results):
2893
+ # For now, use simple heuristic: longest output is "best"
2894
+ # In production, use LLM-based evaluation
2895
+ best_idx = max(
2896
+ range(len(successful_results)),
2897
+ key=lambda i: (len(successful_results[i]["result"].get("output", "")) if i != voter_idx else 0),
2898
+ )
2899
+ successful_results[best_idx]["votes"] += 1
2900
+
2901
+ # Select result with most votes
2902
+ best_result = max(successful_results, key=lambda r: r["votes"])
2903
+
2904
+ return {
2905
+ "success": True,
2906
+ "output": best_result["result"].get("output", ""),
2907
+ "collaboration_strategy": "consensus",
2908
+ "selected_agent": best_result["agent"],
2909
+ "votes": best_result["votes"],
2910
+ "total_agents": len(successful_results),
2911
+ "all_results": successful_results,
2912
+ "timestamp": datetime.utcnow().isoformat(),
2913
+ }
2914
+
2915
+ # ==================== Smart Context Management (Phase 8 - Tasks 1.16.1-1.16.3) ====================
2916
+
2917
+ async def get_relevant_context(
2918
+ self,
2919
+ query: str,
2920
+ context_items: List[Dict[str, Any]],
2921
+ max_items: Optional[int] = None,
2922
+ min_relevance_score: float = 0.5,
2923
+ ) -> List[Dict[str, Any]]:
2924
+ """
2925
+ Get relevant context items using semantic search and relevance scoring.
2926
+
2927
+ This method filters and ranks context items based on their relevance to
2928
+ the query, helping agents stay within token limits while maintaining
2929
+ the most important context.
2930
+
2931
+ Args:
2932
+ query: Query or task description to match against
2933
+ context_items: List of context items (dicts with 'content' field)
2934
+ max_items: Maximum number of items to return (None = no limit)
2935
+ min_relevance_score: Minimum relevance score (0.0-1.0)
2936
+
2937
+ Returns:
2938
+ List of relevant context items, sorted by relevance (highest first)
2939
+
2940
+ Example:
2941
+ ```python
2942
+ context_items = [
2943
+ {"content": "User prefers concise answers", "type": "preference"},
2944
+ {"content": "Previous task: data analysis", "type": "history"},
2945
+ {"content": "System configuration: prod", "type": "config"},
2946
+ ]
2947
+
2948
+ relevant = await agent.get_relevant_context(
2949
+ query="Analyze sales data",
2950
+ context_items=context_items,
2951
+ max_items=2,
2952
+ min_relevance_score=0.6
2953
+ )
2954
+ # Returns top 2 most relevant items with score >= 0.6
2955
+ ```
2956
+ """
2957
+ if not context_items:
2958
+ return []
2959
+
2960
+ # Score all items
2961
+ scored_items = []
2962
+ for item in context_items:
2963
+ score = await self.score_context_relevance(query, item)
2964
+ if score >= min_relevance_score:
2965
+ scored_items.append({**item, "_relevance_score": score})
2966
+
2967
+ # Sort by relevance (highest first)
2968
+ scored_items.sort(key=lambda x: x["_relevance_score"], reverse=True)
2969
+
2970
+ # Limit number of items
2971
+ if max_items is not None:
2972
+ scored_items = scored_items[:max_items]
2973
+
2974
+ logger.debug(f"Selected {len(scored_items)}/{len(context_items)} relevant context items " f"(min_score={min_relevance_score})")
2975
+
2976
+ return scored_items
2977
+
2978
+ async def score_context_relevance(self, query: str, context_item: Dict[str, Any]) -> float:
2979
+ """
2980
+ Score the relevance of a context item to a query.
2981
+
2982
+ Uses multiple signals to determine relevance:
2983
+ - Keyword overlap (basic)
2984
+ - Semantic similarity (if LLM client with embeddings available)
2985
+ - Recency (if timestamp available)
2986
+ - Type priority (if type specified)
2987
+
2988
+ Args:
2989
+ query: Query or task description
2990
+ context_item: Context item to score (dict with 'content' field)
2991
+
2992
+ Returns:
2993
+ Relevance score between 0.0 (not relevant) and 1.0 (highly relevant)
2994
+
2995
+ Example:
2996
+ ```python
2997
+ score = await agent.score_context_relevance(
2998
+ query="Analyze sales data",
2999
+ context_item={"content": "Previous analysis results", "type": "history"}
3000
+ )
3001
+ print(f"Relevance: {score:.2f}")
3002
+ ```
3003
+ """
3004
+ content = context_item.get("content", "")
3005
+ if not content:
3006
+ return 0.0
3007
+
3008
+ # Convert to lowercase for comparison
3009
+ query_lower = query.lower()
3010
+ content_lower = content.lower()
3011
+
3012
+ # 1. Keyword overlap score (0.0-0.5)
3013
+ query_words = set(query_lower.split())
3014
+ content_words = set(content_lower.split())
3015
+ if not query_words:
3016
+ keyword_score = 0.0
3017
+ else:
3018
+ overlap = len(query_words & content_words)
3019
+ keyword_score = min(0.5, (overlap / len(query_words)) * 0.5)
3020
+
3021
+ # 2. Semantic similarity score (0.0-0.3)
3022
+ # If LLM client with embeddings is available, use it
3023
+ semantic_score = 0.0
3024
+ if self._llm_client and hasattr(self._llm_client, "get_embeddings"):
3025
+ try:
3026
+ embeddings = await self._llm_client.get_embeddings([query, content])
3027
+ if len(embeddings) == 2:
3028
+ # Calculate cosine similarity
3029
+ import math
3030
+
3031
+ vec1, vec2 = embeddings[0], embeddings[1]
3032
+ dot_product = sum(a * b for a, b in zip(vec1, vec2))
3033
+ mag1 = math.sqrt(sum(a * a for a in vec1))
3034
+ mag2 = math.sqrt(sum(b * b for b in vec2))
3035
+ if mag1 > 0 and mag2 > 0:
3036
+ similarity = dot_product / (mag1 * mag2)
3037
+ semantic_score = max(0.0, similarity) * 0.3
3038
+ except Exception as e:
3039
+ logger.debug(f"Semantic similarity calculation failed: {e}")
3040
+
3041
+ # 3. Recency score (0.0-0.1)
3042
+ recency_score = 0.0
3043
+ if "timestamp" in context_item:
3044
+ try:
3045
+ from datetime import datetime
3046
+
3047
+ timestamp = context_item["timestamp"]
3048
+ if isinstance(timestamp, str):
3049
+ timestamp = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
3050
+ age_seconds = (datetime.utcnow() - timestamp).total_seconds()
3051
+ # Decay over 24 hours
3052
+ recency_score = max(0.0, 0.1 * (1.0 - min(1.0, age_seconds / 86400)))
3053
+ except Exception as e:
3054
+ logger.debug(f"Recency calculation failed: {e}")
3055
+
3056
+ # 4. Type priority score (0.0-0.1)
3057
+ type_score = 0.0
3058
+ item_type = context_item.get("type", "")
3059
+ priority_types = {"preference": 0.1, "constraint": 0.1, "requirement": 0.09}
3060
+ type_score = priority_types.get(item_type, 0.05)
3061
+
3062
+ # Combine scores
3063
+ total_score = keyword_score + semantic_score + recency_score + type_score
3064
+
3065
+ return min(1.0, total_score)
3066
+
3067
+ async def prune_context(
3068
+ self,
3069
+ context_items: List[Dict[str, Any]],
3070
+ max_tokens: int,
3071
+ query: Optional[str] = None,
3072
+ preserve_types: Optional[List[str]] = None,
3073
+ ) -> List[Dict[str, Any]]:
3074
+ """
3075
+ Prune context items to fit within token limit.
3076
+
3077
+ Uses relevance scoring to keep the most important context while
3078
+ staying within token limits. Optionally preserves certain types
3079
+ of context regardless of relevance.
3080
+
3081
+ Args:
3082
+ context_items: List of context items to prune
3083
+ max_tokens: Maximum total tokens allowed
3084
+ query: Optional query for relevance scoring
3085
+ preserve_types: Optional list of types to always preserve
3086
+
3087
+ Returns:
3088
+ Pruned list of context items that fit within token limit
3089
+
3090
+ Example:
3091
+ ```python
3092
+ pruned = await agent.prune_context(
3093
+ context_items=all_context,
3094
+ max_tokens=2000,
3095
+ query="Analyze data",
3096
+ preserve_types=["constraint", "requirement"]
3097
+ )
3098
+ print(f"Pruned from {len(all_context)} to {len(pruned)} items")
3099
+ ```
3100
+ """
3101
+ if not context_items:
3102
+ return []
3103
+
3104
+ preserve_types = preserve_types or []
3105
+
3106
+ # Separate preserved and regular items
3107
+ preserved_items = []
3108
+ regular_items = []
3109
+
3110
+ for item in context_items:
3111
+ if item.get("type") in preserve_types:
3112
+ preserved_items.append(item)
3113
+ else:
3114
+ regular_items.append(item)
3115
+
3116
+ # Score regular items if query provided
3117
+ if query and regular_items:
3118
+ scored_items = []
3119
+ for item in regular_items:
3120
+ score = await self.score_context_relevance(query, item)
3121
+ scored_items.append({**item, "_relevance_score": score})
3122
+ # Sort by relevance
3123
+ scored_items.sort(key=lambda x: x["_relevance_score"], reverse=True)
3124
+ regular_items = scored_items
3125
+
3126
+ # Estimate tokens (rough approximation: 1 token ≈ 4 characters)
3127
+ def estimate_tokens(item: Dict[str, Any]) -> int:
3128
+ content = str(item.get("content", ""))
3129
+ return len(content) // 4
3130
+
3131
+ # Add preserved items first
3132
+ result = []
3133
+ current_tokens = 0
3134
+
3135
+ for item in preserved_items:
3136
+ item_tokens = estimate_tokens(item)
3137
+ if current_tokens + item_tokens <= max_tokens:
3138
+ result.append(item)
3139
+ current_tokens += item_tokens
3140
+ else:
3141
+ logger.warning(f"Preserved item exceeds token limit, skipping: {item.get('type')}")
3142
+
3143
+ # Add regular items until token limit
3144
+ for item in regular_items:
3145
+ item_tokens = estimate_tokens(item)
3146
+ if current_tokens + item_tokens <= max_tokens:
3147
+ result.append(item)
3148
+ current_tokens += item_tokens
3149
+ else:
3150
+ break
3151
+
3152
+ logger.info(f"Pruned context from {len(context_items)} to {len(result)} items " f"({current_tokens}/{max_tokens} tokens)")
3153
+
3154
+ return result
3155
+
3156
+ # ==================== Agent Learning (Phase 8 - Tasks 1.16.4-1.16.10) ====================
3157
+
3158
+ async def record_experience(
3159
+ self,
3160
+ task: Dict[str, Any],
3161
+ result: Dict[str, Any],
3162
+ approach: str,
3163
+ tools_used: Optional[List[str]] = None,
3164
+ ) -> None:
3165
+ """
3166
+ Record an experience for learning and adaptation.
3167
+
3168
+ Args:
3169
+ task: Task specification
3170
+ result: Task execution result
3171
+ approach: Approach/strategy used
3172
+ tools_used: List of tools used (if any)
3173
+
3174
+ Example:
3175
+ ```python
3176
+ await agent.record_experience(
3177
+ task={"description": "Analyze data", "type": "analysis"},
3178
+ result={"success": True, "execution_time": 5.2},
3179
+ approach="statistical_analysis",
3180
+ tools_used=["pandas", "numpy"]
3181
+ )
3182
+ ```
3183
+ """
3184
+ if not self._learning_enabled:
3185
+ return
3186
+
3187
+ from .models import Experience
3188
+
3189
+ # Classify task
3190
+ task_type = await self._classify_task(task)
3191
+
3192
+ # Create experience record
3193
+ experience = Experience( # type: ignore[call-arg]
3194
+ agent_id=self.agent_id,
3195
+ task_type=task_type,
3196
+ task_description=task.get("description", ""),
3197
+ task_complexity=task.get("complexity"),
3198
+ approach=approach,
3199
+ tools_used=tools_used or [],
3200
+ execution_time=result.get("execution_time", 0.0),
3201
+ success=result.get("success", False),
3202
+ quality_score=result.get("quality_score"),
3203
+ error_type=result.get("error_type"),
3204
+ error_message=result.get("error"),
3205
+ context_size=result.get("context_size"),
3206
+ iterations=result.get("iterations"),
3207
+ metadata={"task_id": task.get("task_id")},
3208
+ )
3209
+
3210
+ # Add to experiences
3211
+ self._experiences.append(experience)
3212
+
3213
+ # Limit stored experiences
3214
+ if len(self._experiences) > self._max_experiences:
3215
+ self._experiences = self._experiences[-self._max_experiences :]
3216
+
3217
+ logger.debug(f"Recorded experience: {task_type} - " f"{'success' if experience.success else 'failure'} " f"({experience.execution_time:.2f}s)")
3218
+
3219
+ async def get_recommended_approach(self, task: Dict[str, Any]) -> Optional[Dict[str, Any]]:
3220
+ """
3221
+ Get recommended approach based on past experiences.
3222
+
3223
+ Analyzes similar past experiences to recommend the best approach
3224
+ for the current task.
3225
+
3226
+ Args:
3227
+ task: Task specification
3228
+
3229
+ Returns:
3230
+ Recommended approach dict with 'approach', 'confidence', 'reasoning'
3231
+ or None if no relevant experiences
3232
+
3233
+ Example:
3234
+ ```python
3235
+ recommendation = await agent.get_recommended_approach(
3236
+ task={"description": "Analyze sales data", "type": "analysis"}
3237
+ )
3238
+ if recommendation:
3239
+ print(f"Recommended: {recommendation['approach']}")
3240
+ print(f"Confidence: {recommendation['confidence']:.2f}")
3241
+ print(f"Reasoning: {recommendation['reasoning']}")
3242
+ ```
3243
+ """
3244
+ if not self._learning_enabled or not self._experiences:
3245
+ return None
3246
+
3247
+ # Classify current task
3248
+ task_type = await self._classify_task(task)
3249
+
3250
+ # Find similar experiences
3251
+ similar_experiences = [exp for exp in self._experiences if exp.task_type == task_type]
3252
+
3253
+ if not similar_experiences:
3254
+ return None
3255
+
3256
+ # Analyze successful experiences
3257
+ successful = [exp for exp in similar_experiences if exp.success]
3258
+ if not successful:
3259
+ return None
3260
+
3261
+ # Count approaches
3262
+ approach_stats: Dict[str, Dict[str, Any]] = {}
3263
+ for exp in successful:
3264
+ if exp.approach not in approach_stats:
3265
+ approach_stats[exp.approach] = {
3266
+ "count": 0,
3267
+ "total_time": 0.0,
3268
+ "avg_quality": 0.0,
3269
+ "quality_count": 0,
3270
+ }
3271
+ stats = approach_stats[exp.approach]
3272
+ stats["count"] += 1
3273
+ stats["total_time"] += exp.execution_time
3274
+ if exp.quality_score is not None:
3275
+ stats["avg_quality"] += exp.quality_score
3276
+ stats["quality_count"] += 1
3277
+
3278
+ # Calculate averages and scores
3279
+ for approach, stats in approach_stats.items():
3280
+ stats["avg_time"] = stats["total_time"] / stats["count"]
3281
+ if stats["quality_count"] > 0:
3282
+ stats["avg_quality"] = stats["avg_quality"] / stats["quality_count"]
3283
+ else:
3284
+ stats["avg_quality"] = 0.5 # Default
3285
+
3286
+ # Select best approach (balance success rate, quality, speed)
3287
+ best_approach = max(
3288
+ approach_stats.items(),
3289
+ key=lambda x: (
3290
+ x[1]["count"] / len(similar_experiences), # Success rate
3291
+ x[1]["avg_quality"], # Quality
3292
+ -x[1]["avg_time"], # Speed (negative for faster is better)
3293
+ ),
3294
+ )
3295
+
3296
+ approach_name, stats = best_approach
3297
+ confidence = min(1.0, stats["count"] / max(5, len(similar_experiences)))
3298
+
3299
+ return {
3300
+ "approach": approach_name,
3301
+ "confidence": confidence,
3302
+ "reasoning": (
3303
+ f"Based on {stats['count']} successful experiences with {task_type} tasks. " f"Average execution time: {stats['avg_time']:.2f}s, " f"Average quality: {stats['avg_quality']:.2f}"
3304
+ ),
3305
+ "stats": stats,
3306
+ }
3307
+
3308
+ async def get_learning_insights(self) -> Dict[str, Any]:
3309
+ """
3310
+ Get learning insights and analytics.
3311
+
3312
+ Provides analytics about agent learning including success rates,
3313
+ common patterns, and areas for improvement.
3314
+
3315
+ Returns:
3316
+ Dict with learning insights and statistics
3317
+
3318
+ Example:
3319
+ ```python
3320
+ insights = await agent.get_learning_insights()
3321
+ print(f"Total experiences: {insights['total_experiences']}")
3322
+ print(f"Success rate: {insights['overall_success_rate']:.2%}")
3323
+ print(f"Most common task: {insights['most_common_task_type']}")
3324
+ ```
3325
+ """
3326
+ if not self._learning_enabled or not self._experiences:
3327
+ return {
3328
+ "total_experiences": 0,
3329
+ "learning_enabled": self._learning_enabled,
3330
+ }
3331
+
3332
+ total = len(self._experiences)
3333
+ successful = sum(1 for exp in self._experiences if exp.success)
3334
+ failed = total - successful
3335
+
3336
+ # Task type distribution
3337
+ task_types: Dict[str, int] = {}
3338
+ for exp in self._experiences:
3339
+ task_types[exp.task_type] = task_types.get(exp.task_type, 0) + 1
3340
+
3341
+ # Approach effectiveness
3342
+ approach_success: Dict[str, Dict[str, int]] = {}
3343
+ for exp in self._experiences:
3344
+ if exp.approach not in approach_success:
3345
+ approach_success[exp.approach] = {"success": 0, "failure": 0}
3346
+ if exp.success:
3347
+ approach_success[exp.approach]["success"] += 1
3348
+ else:
3349
+ approach_success[exp.approach]["failure"] += 1
3350
+
3351
+ # Calculate success rates
3352
+ approach_rates = {approach: stats["success"] / (stats["success"] + stats["failure"]) for approach, stats in approach_success.items()}
3353
+
3354
+ # Error patterns
3355
+ error_types: Dict[str, int] = {}
3356
+ for exp in self._experiences:
3357
+ if not exp.success and exp.error_type:
3358
+ error_types[exp.error_type] = error_types.get(exp.error_type, 0) + 1
3359
+
3360
+ return {
3361
+ "total_experiences": total,
3362
+ "successful_experiences": successful,
3363
+ "failed_experiences": failed,
3364
+ "overall_success_rate": successful / total if total > 0 else 0.0,
3365
+ "task_type_distribution": task_types,
3366
+ "most_common_task_type": (max(task_types.items(), key=lambda x: x[1])[0] if task_types else None),
3367
+ "approach_effectiveness": approach_rates,
3368
+ "best_approach": (max(approach_rates.items(), key=lambda x: x[1])[0] if approach_rates else None),
3369
+ "error_patterns": error_types,
3370
+ "most_common_error": (max(error_types.items(), key=lambda x: x[1])[0] if error_types else None),
3371
+ "learning_enabled": self._learning_enabled,
3372
+ }
3373
+
3374
+ async def adapt_strategy(self, task: Dict[str, Any]) -> Dict[str, Any]:
3375
+ """
3376
+ Adapt strategy based on learning insights.
3377
+
3378
+ Analyzes past experiences to suggest strategy adaptations for
3379
+ the current task.
3380
+
3381
+ Args:
3382
+ task: Task specification
3383
+
3384
+ Returns:
3385
+ Dict with strategy adaptations and recommendations
3386
+
3387
+ Example:
3388
+ ```python
3389
+ adaptations = await agent.adapt_strategy(
3390
+ task={"description": "Complex analysis", "type": "analysis"}
3391
+ )
3392
+ print(f"Recommended approach: {adaptations['recommended_approach']}")
3393
+ print(f"Suggested tools: {adaptations['suggested_tools']}")
3394
+ ```
3395
+ """
3396
+ if not self._learning_enabled:
3397
+ return {"adapted": False, "reason": "Learning not enabled"}
3398
+
3399
+ # Get recommended approach
3400
+ recommendation = await self.get_recommended_approach(task)
3401
+
3402
+ if not recommendation:
3403
+ return {
3404
+ "adapted": False,
3405
+ "reason": "No relevant experiences found",
3406
+ }
3407
+
3408
+ # Classify task
3409
+ task_type = await self._classify_task(task)
3410
+
3411
+ # Find similar successful experiences
3412
+ similar_successful = [exp for exp in self._experiences if exp.task_type == task_type and exp.success]
3413
+
3414
+ # Analyze tool usage patterns
3415
+ tool_usage: Dict[str, int] = {}
3416
+ for exp in similar_successful:
3417
+ for tool in exp.tools_used:
3418
+ tool_usage[tool] = tool_usage.get(tool, 0) + 1
3419
+
3420
+ # Get most commonly used tools
3421
+ suggested_tools = sorted(tool_usage.items(), key=lambda x: x[1], reverse=True)[:5] # Top 5 tools
3422
+
3423
+ return {
3424
+ "adapted": True,
3425
+ "recommended_approach": recommendation["approach"],
3426
+ "confidence": recommendation["confidence"],
3427
+ "reasoning": recommendation["reasoning"],
3428
+ "suggested_tools": [tool for tool, _ in suggested_tools],
3429
+ "tool_usage_stats": dict(suggested_tools),
3430
+ "based_on_experiences": len(similar_successful),
3431
+ }
3432
+
3433
+ async def _classify_task(self, task: Dict[str, Any]) -> str:
3434
+ """
3435
+ Classify task into a type/category.
3436
+
3437
+ Uses simple heuristics to classify tasks. Can be overridden by
3438
+ subclasses for more sophisticated classification.
3439
+
3440
+ Args:
3441
+ task: Task specification
3442
+
3443
+ Returns:
3444
+ Task type string
3445
+
3446
+ Example:
3447
+ ```python
3448
+ task_type = await agent._classify_task(
3449
+ {"description": "Analyze sales data"}
3450
+ )
3451
+ # Returns: "analysis"
3452
+ ```
3453
+ """
3454
+ # Check explicit type
3455
+ if "type" in task:
3456
+ return task["type"]
3457
+
3458
+ # Simple keyword-based classification
3459
+ description = task.get("description", "").lower()
3460
+
3461
+ if any(word in description for word in ["analyze", "analysis", "examine"]):
3462
+ return "analysis"
3463
+ elif any(word in description for word in ["search", "find", "lookup"]):
3464
+ return "search"
3465
+ elif any(word in description for word in ["create", "generate", "write"]):
3466
+ return "generation"
3467
+ elif any(word in description for word in ["summarize", "summary"]):
3468
+ return "summarization"
3469
+ elif any(word in description for word in ["calculate", "compute"]):
3470
+ return "calculation"
3471
+ elif any(word in description for word in ["translate", "convert"]):
3472
+ return "translation"
3473
+ else:
3474
+ return "general"
3475
+
3476
+ # ==================== Resource Management (Phase 8 - Tasks 1.16.11-1.16.17) ====================
3477
+
3478
+ async def check_resource_availability(self) -> Dict[str, Any]:
3479
+ """
3480
+ Check if resources are available for task execution.
3481
+
3482
+ Checks against configured resource limits including:
3483
+ - Concurrent task limits
3484
+ - Token rate limits
3485
+ - Tool call rate limits
3486
+
3487
+ Returns:
3488
+ Dict with 'available' (bool) and details about resource status
3489
+
3490
+ Example:
3491
+ ```python
3492
+ status = await agent.check_resource_availability()
3493
+ if status['available']:
3494
+ await agent.execute_task(task, context)
3495
+ else:
3496
+ print(f"Resources unavailable: {status['reason']}")
3497
+ ```
3498
+ """
3499
+ if not self._resource_limits.enforce_limits:
3500
+ return {"available": True, "reason": "Limits not enforced"}
3501
+
3502
+ # Check concurrent task limit
3503
+ if len(self._active_tasks) >= self._resource_limits.max_concurrent_tasks:
3504
+ return {
3505
+ "available": False,
3506
+ "reason": "Concurrent task limit reached",
3507
+ "active_tasks": len(self._active_tasks),
3508
+ "max_tasks": self._resource_limits.max_concurrent_tasks,
3509
+ }
3510
+
3511
+ # Check token rate limits
3512
+ token_check = await self._check_token_rate_limit()
3513
+ if not token_check["available"]:
3514
+ return token_check
3515
+
3516
+ # Check tool call rate limits
3517
+ tool_check = await self._check_tool_call_rate_limit()
3518
+ if not tool_check["available"]:
3519
+ return tool_check
3520
+
3521
+ return {
3522
+ "available": True,
3523
+ "active_tasks": len(self._active_tasks),
3524
+ "max_tasks": self._resource_limits.max_concurrent_tasks,
3525
+ }
3526
+
3527
+ async def wait_for_resources(self, timeout: Optional[float] = None) -> bool:
3528
+ """
3529
+ Wait for resources to become available.
3530
+
3531
+ Args:
3532
+ timeout: Maximum time to wait in seconds (uses resource_wait_timeout_seconds if None)
3533
+
3534
+ Returns:
3535
+ True if resources became available, False if timeout
3536
+
3537
+ Example:
3538
+ ```python
3539
+ if await agent.wait_for_resources(timeout=30):
3540
+ await agent.execute_task(task, context)
3541
+ else:
3542
+ print("Timeout waiting for resources")
3543
+ ```
3544
+ """
3545
+ if timeout is None:
3546
+ timeout = self._resource_limits.resource_wait_timeout_seconds
3547
+
3548
+ start_time = time.time()
3549
+ check_interval = 0.5 # Check every 500ms
3550
+
3551
+ while time.time() - start_time < timeout:
3552
+ status = await self.check_resource_availability()
3553
+ if status["available"]:
3554
+ return True
3555
+
3556
+ # Wait before next check
3557
+ await asyncio.sleep(check_interval)
3558
+
3559
+ logger.warning(f"Timeout waiting for resources after {timeout}s")
3560
+ return False
3561
+
3562
+ async def get_resource_usage(self) -> Dict[str, Any]:
3563
+ """
3564
+ Get current resource usage statistics.
3565
+
3566
+ Returns:
3567
+ Dict with resource usage information
3568
+
3569
+ Example:
3570
+ ```python
3571
+ usage = await agent.get_resource_usage()
3572
+ print(f"Active tasks: {usage['active_tasks']}")
3573
+ print(f"Tokens/min: {usage['tokens_per_minute']}")
3574
+ print(f"Tool calls/min: {usage['tool_calls_per_minute']}")
3575
+ ```
3576
+ """
3577
+ current_time = time.time()
3578
+
3579
+ # Calculate token usage rates
3580
+ tokens_last_minute = sum(count for ts, count in self._token_usage_window if current_time - ts < 60)
3581
+ tokens_last_hour = sum(count for ts, count in self._token_usage_window if current_time - ts < 3600)
3582
+
3583
+ # Calculate tool call rates
3584
+ tool_calls_last_minute = sum(1 for ts in self._tool_call_window if current_time - ts < 60)
3585
+ tool_calls_last_hour = sum(1 for ts in self._tool_call_window if current_time - ts < 3600)
3586
+
3587
+ return {
3588
+ "active_tasks": len(self._active_tasks),
3589
+ "max_concurrent_tasks": self._resource_limits.max_concurrent_tasks,
3590
+ "task_utilization": len(self._active_tasks) / self._resource_limits.max_concurrent_tasks,
3591
+ "tokens_per_minute": tokens_last_minute,
3592
+ "tokens_per_hour": tokens_last_hour,
3593
+ "max_tokens_per_minute": self._resource_limits.max_tokens_per_minute,
3594
+ "max_tokens_per_hour": self._resource_limits.max_tokens_per_hour,
3595
+ "tool_calls_per_minute": tool_calls_last_minute,
3596
+ "tool_calls_per_hour": tool_calls_last_hour,
3597
+ "max_tool_calls_per_minute": self._resource_limits.max_tool_calls_per_minute,
3598
+ "max_tool_calls_per_hour": self._resource_limits.max_tool_calls_per_hour,
3599
+ "limits_enforced": self._resource_limits.enforce_limits,
3600
+ }
3601
+
3602
+ async def _check_token_rate_limit(self) -> Dict[str, Any]:
3603
+ """
3604
+ Check token rate limits.
3605
+
3606
+ Returns:
3607
+ Dict with 'available' (bool) and limit details
3608
+ """
3609
+ if not self._resource_limits.enforce_limits:
3610
+ return {"available": True}
3611
+
3612
+ current_time = time.time()
3613
+
3614
+ # Clean old entries (older than 1 hour)
3615
+ self._token_usage_window = [(ts, count) for ts, count in self._token_usage_window if current_time - ts < 3600]
3616
+
3617
+ # Check per-minute limit
3618
+ if self._resource_limits.max_tokens_per_minute is not None:
3619
+ tokens_last_minute = sum(count for ts, count in self._token_usage_window if current_time - ts < 60)
3620
+ if tokens_last_minute >= self._resource_limits.max_tokens_per_minute:
3621
+ return {
3622
+ "available": False,
3623
+ "reason": "Token rate limit (per minute) reached",
3624
+ "tokens_used": tokens_last_minute,
3625
+ "limit": self._resource_limits.max_tokens_per_minute,
3626
+ "window": "minute",
3627
+ }
3628
+
3629
+ # Check per-hour limit
3630
+ if self._resource_limits.max_tokens_per_hour is not None:
3631
+ tokens_last_hour = sum(count for ts, count in self._token_usage_window)
3632
+ if tokens_last_hour >= self._resource_limits.max_tokens_per_hour:
3633
+ return {
3634
+ "available": False,
3635
+ "reason": "Token rate limit (per hour) reached",
3636
+ "tokens_used": tokens_last_hour,
3637
+ "limit": self._resource_limits.max_tokens_per_hour,
3638
+ "window": "hour",
3639
+ }
3640
+
3641
+ return {"available": True}
3642
+
3643
+ async def _check_tool_call_rate_limit(self) -> Dict[str, Any]:
3644
+ """
3645
+ Check tool call rate limits.
3646
+
3647
+ Returns:
3648
+ Dict with 'available' (bool) and limit details
3649
+ """
3650
+ if not self._resource_limits.enforce_limits:
3651
+ return {"available": True}
3652
+
3653
+ current_time = time.time()
3654
+
3655
+ # Clean old entries (older than 1 hour)
3656
+ self._tool_call_window = [ts for ts in self._tool_call_window if current_time - ts < 3600]
3657
+
3658
+ # Check per-minute limit
3659
+ if self._resource_limits.max_tool_calls_per_minute is not None:
3660
+ calls_last_minute = sum(1 for ts in self._tool_call_window if current_time - ts < 60)
3661
+ if calls_last_minute >= self._resource_limits.max_tool_calls_per_minute:
3662
+ return {
3663
+ "available": False,
3664
+ "reason": "Tool call rate limit (per minute) reached",
3665
+ "calls_made": calls_last_minute,
3666
+ "limit": self._resource_limits.max_tool_calls_per_minute,
3667
+ "window": "minute",
3668
+ }
3669
+
3670
+ # Check per-hour limit
3671
+ if self._resource_limits.max_tool_calls_per_hour is not None:
3672
+ calls_last_hour = len(self._tool_call_window)
3673
+ if calls_last_hour >= self._resource_limits.max_tool_calls_per_hour:
3674
+ return {
3675
+ "available": False,
3676
+ "reason": "Tool call rate limit (per hour) reached",
3677
+ "calls_made": calls_last_hour,
3678
+ "limit": self._resource_limits.max_tool_calls_per_hour,
3679
+ "window": "hour",
3680
+ }
3681
+
3682
+ return {"available": True}
3683
+
3684
+ # ==================== Error Recovery (Phase 8 - Tasks 1.16.18-1.16.22) ====================
3685
+
3686
+ async def execute_with_recovery(
3687
+ self,
3688
+ task: Dict[str, Any],
3689
+ context: Dict[str, Any],
3690
+ strategies: Optional[List[str]] = None,
3691
+ ) -> Dict[str, Any]:
3692
+ """
3693
+ Execute task with advanced error recovery strategies.
3694
+
3695
+ Tries multiple recovery strategies in sequence until one succeeds:
3696
+ 1. Retry with exponential backoff
3697
+ 2. Simplify task and retry
3698
+ 3. Use fallback approach
3699
+ 4. Delegate to another agent
3700
+
3701
+ Args:
3702
+ task: Task specification
3703
+ context: Execution context
3704
+ strategies: List of strategy names to try (uses default chain if None)
3705
+
3706
+ Returns:
3707
+ Task execution result
3708
+
3709
+ Raises:
3710
+ TaskExecutionError: If all recovery strategies fail
3711
+
3712
+ Example:
3713
+ ```python
3714
+ result = await agent.execute_with_recovery(
3715
+ task={"description": "Complex analysis"},
3716
+ context={},
3717
+ strategies=["retry", "simplify", "delegate"]
3718
+ )
3719
+ ```
3720
+ """
3721
+ from .models import RecoveryStrategy
3722
+ from .exceptions import TaskExecutionError
3723
+
3724
+ # Default strategy chain
3725
+ if strategies is None:
3726
+ strategies = [
3727
+ RecoveryStrategy.RETRY,
3728
+ RecoveryStrategy.SIMPLIFY,
3729
+ RecoveryStrategy.FALLBACK,
3730
+ RecoveryStrategy.DELEGATE,
3731
+ ]
3732
+
3733
+ errors = []
3734
+
3735
+ for strategy in strategies:
3736
+ try:
3737
+ logger.info(f"Attempting recovery strategy: {strategy}")
3738
+
3739
+ if strategy == RecoveryStrategy.RETRY:
3740
+ # Retry with exponential backoff (using existing retry mechanism)
3741
+ result = await self._execute_with_retry(self.execute_task, task, context)
3742
+ logger.info(f"Recovery successful with strategy: {strategy}")
3743
+ return result
3744
+
3745
+ elif strategy == RecoveryStrategy.SIMPLIFY:
3746
+ # Simplify task and retry
3747
+ simplified_task = await self._simplify_task(task)
3748
+ result = await self.execute_task(simplified_task, context)
3749
+ logger.info(f"Recovery successful with strategy: {strategy}")
3750
+ return result
3751
+
3752
+ elif strategy == RecoveryStrategy.FALLBACK:
3753
+ # Use fallback approach
3754
+ result = await self._execute_with_fallback(task, context)
3755
+ logger.info(f"Recovery successful with strategy: {strategy}")
3756
+ return result
3757
+
3758
+ elif strategy == RecoveryStrategy.DELEGATE:
3759
+ # Delegate to another agent
3760
+ if self._collaboration_enabled:
3761
+ result = await self._delegate_to_capable_agent(task, context)
3762
+ logger.info(f"Recovery successful with strategy: {strategy}")
3763
+ return result
3764
+ else:
3765
+ logger.warning("Delegation not available (collaboration disabled)")
3766
+ continue
3767
+
3768
+ except Exception as e:
3769
+ logger.warning(f"Recovery strategy {strategy} failed: {e}")
3770
+ errors.append({"strategy": strategy, "error": str(e)})
3771
+ continue
3772
+
3773
+ # All strategies failed
3774
+ error_summary = "; ".join([f"{e['strategy']}: {e['error']}" for e in errors])
3775
+ raise TaskExecutionError(
3776
+ f"All recovery strategies failed. Errors: {error_summary}",
3777
+ agent_id=self.agent_id,
3778
+ task_id=task.get("task_id"),
3779
+ )
3780
+
3781
+ async def _simplify_task(self, task: Dict[str, Any]) -> Dict[str, Any]:
3782
+ """
3783
+ Simplify a task to make it easier to execute.
3784
+
3785
+ Strategies:
3786
+ - Reduce complexity by breaking into smaller parts
3787
+ - Remove optional requirements
3788
+ - Use simpler language
3789
+
3790
+ Args:
3791
+ task: Original task specification
3792
+
3793
+ Returns:
3794
+ Simplified task specification
3795
+
3796
+ Example:
3797
+ ```python
3798
+ simplified = await agent._simplify_task(
3799
+ {"description": "Perform comprehensive analysis with visualizations"}
3800
+ )
3801
+ # Returns: {"description": "Perform basic analysis"}
3802
+ ```
3803
+ """
3804
+ description = task.get("description", "")
3805
+
3806
+ # Simple heuristics for simplification
3807
+ simplified_description = description
3808
+
3809
+ # Remove complexity keywords
3810
+ complexity_words = [
3811
+ "comprehensive",
3812
+ "detailed",
3813
+ "thorough",
3814
+ "extensive",
3815
+ "in-depth",
3816
+ "complete",
3817
+ "full",
3818
+ "exhaustive",
3819
+ ]
3820
+ for word in complexity_words:
3821
+ simplified_description = simplified_description.replace(word, "basic")
3822
+
3823
+ # Remove optional requirements
3824
+ optional_phrases = [
3825
+ "with visualizations",
3826
+ "with charts",
3827
+ "with graphs",
3828
+ "with examples",
3829
+ "with details",
3830
+ "with explanations",
3831
+ ]
3832
+ for phrase in optional_phrases:
3833
+ simplified_description = simplified_description.replace(phrase, "")
3834
+
3835
+ # Clean up extra spaces
3836
+ simplified_description = " ".join(simplified_description.split())
3837
+
3838
+ simplified_task = task.copy()
3839
+ simplified_task["description"] = simplified_description
3840
+ simplified_task["simplified"] = True
3841
+ simplified_task["original_description"] = description
3842
+
3843
+ logger.debug(f"Simplified task: '{description}' -> '{simplified_description}'")
3844
+
3845
+ return simplified_task
3846
+
3847
+ async def _execute_with_fallback(self, task: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
3848
+ """
3849
+ Execute task with fallback approach.
3850
+
3851
+ Uses a simpler, more reliable approach when the primary approach fails.
3852
+
3853
+ Args:
3854
+ task: Task specification
3855
+ context: Execution context
3856
+
3857
+ Returns:
3858
+ Task execution result
3859
+
3860
+ Example:
3861
+ ```python
3862
+ result = await agent._execute_with_fallback(task, context)
3863
+ ```
3864
+ """
3865
+ # Create fallback task with reduced requirements
3866
+ fallback_task = task.copy()
3867
+ fallback_task["fallback_mode"] = True
3868
+
3869
+ # Reduce max_tokens if specified
3870
+ if "max_tokens" in context:
3871
+ context = context.copy()
3872
+ context["max_tokens"] = min(context["max_tokens"], 1000)
3873
+
3874
+ # Reduce temperature for more deterministic output
3875
+ if "temperature" in context:
3876
+ context = context.copy()
3877
+ context["temperature"] = 0.3
3878
+
3879
+ logger.info("Executing with fallback approach (reduced requirements)")
3880
+
3881
+ # Execute with modified parameters
3882
+ result = await self.execute_task(fallback_task, context)
3883
+ result["fallback_used"] = True
3884
+
3885
+ return result
3886
+
3887
+ async def _delegate_to_capable_agent(self, task: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
3888
+ """
3889
+ Delegate task to a capable agent as recovery strategy.
3890
+
3891
+ Finds an agent capable of handling the task and delegates to it.
3892
+
3893
+ Args:
3894
+ task: Task specification
3895
+ context: Execution context
3896
+
3897
+ Returns:
3898
+ Task execution result from delegated agent
3899
+
3900
+ Raises:
3901
+ ValueError: If no capable agent found
3902
+
3903
+ Example:
3904
+ ```python
3905
+ result = await agent._delegate_to_capable_agent(task, context)
3906
+ ```
3907
+ """
3908
+ if not self._collaboration_enabled:
3909
+ raise ValueError("Collaboration not enabled, cannot delegate")
3910
+
3911
+ # Try to classify task and find capable agents
3912
+ task_type = await self._classify_task(task)
3913
+
3914
+ # Look for agents with matching capabilities
3915
+ capable_agents = []
3916
+ for agent_id, agent in self._agent_registry.items():
3917
+ if agent_id == self.agent_id:
3918
+ continue # Skip self
3919
+
3920
+ # Check if agent has relevant capabilities
3921
+ agent_capabilities = getattr(agent, "capabilities", [])
3922
+ if task_type in agent_capabilities or "general" in agent_capabilities:
3923
+ capable_agents.append(agent)
3924
+
3925
+ if not capable_agents:
3926
+ # Try any available agent as last resort
3927
+ capable_agents = [agent for agent_id, agent in self._agent_registry.items() if agent_id != self.agent_id]
3928
+
3929
+ if not capable_agents:
3930
+ raise ValueError("No capable agents available for delegation")
3931
+
3932
+ # Delegate to first capable agent
3933
+ target_agent = capable_agents[0]
3934
+ logger.info(f"Delegating task to {target_agent.agent_id} for recovery")
3935
+
3936
+ result = await target_agent.execute_task(task, context={**context, "delegated_by": self.agent_id, "recovery_delegation": True})
3937
+
3938
+ result["delegated_to"] = target_agent.agent_id
3939
+ result["recovery_delegation"] = True
3940
+
3941
+ return result
3942
+
3943
+ def __str__(self) -> str:
3944
+ """String representation."""
3945
+ return f"Agent({self.agent_id}, {self.name}, {self.agent_type.value}, {self._state.value})"
3946
+
3947
+ def __repr__(self) -> str:
3948
+ """Detailed representation."""
3949
+ return f"BaseAIAgent(agent_id='{self.agent_id}', name='{self.name}', " f"type='{self.agent_type.value}', state='{self._state.value}')"