polyrob 0.5.0__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.
Files changed (745) hide show
  1. agents/README.md +453 -0
  2. agents/__init__.py +164 -0
  3. agents/base_agent.py +743 -0
  4. agents/personality/character.py +119 -0
  5. agents/personality/character_manager.py +309 -0
  6. agents/personality/characters/rob.character.json +56 -0
  7. agents/personality/characters/trump.character.json +63 -0
  8. agents/personality/persona_render.py +78 -0
  9. agents/personality/persona_resolver.py +115 -0
  10. agents/prompt/__init__.py +58 -0
  11. agents/prompt/base_prompt.py +92 -0
  12. agents/prompt/system.py +417 -0
  13. agents/task/README_PATH_MANAGEMENT.md +141 -0
  14. agents/task/__init__.py +162 -0
  15. agents/task/agent/__init__.py +70 -0
  16. agents/task/agent/agent_state.py +408 -0
  17. agents/task/agent/async_delegation.py +274 -0
  18. agents/task/agent/autonomy_state.py +275 -0
  19. agents/task/agent/conversation.py +75 -0
  20. agents/task/agent/core/__init__.py +6 -0
  21. agents/task/agent/core/aux_metering.py +61 -0
  22. agents/task/agent/core/background_review.py +157 -0
  23. agents/task/agent/core/construction.py +1179 -0
  24. agents/task/agent/core/conversational_exit.py +49 -0
  25. agents/task/agent/core/correspondent_gate.py +227 -0
  26. agents/task/agent/core/curator.py +246 -0
  27. agents/task/agent/core/episodic_digest.py +103 -0
  28. agents/task/agent/core/error_recovery.py +480 -0
  29. agents/task/agent/core/history_io.py +257 -0
  30. agents/task/agent/core/llm_provisioning.py +287 -0
  31. agents/task/agent/core/llm_runner.py +547 -0
  32. agents/task/agent/core/logging_io.py +276 -0
  33. agents/task/agent/core/loop_detection.py +262 -0
  34. agents/task/agent/core/memory_prefetch.py +325 -0
  35. agents/task/agent/core/memory_writer.py +667 -0
  36. agents/task/agent/core/model_introspection.py +82 -0
  37. agents/task/agent/core/model_swap.py +177 -0
  38. agents/task/agent/core/next_action_internal.py +1331 -0
  39. agents/task/agent/core/output_validation.py +303 -0
  40. agents/task/agent/core/project_context.py +272 -0
  41. agents/task/agent/core/resources.py +93 -0
  42. agents/task/agent/core/result_processing.py +389 -0
  43. agents/task/agent/core/run_loop.py +661 -0
  44. agents/task/agent/core/safety_lifecycle.py +228 -0
  45. agents/task/agent/core/secret_guard.py +293 -0
  46. agents/task/agent/core/self_wake.py +280 -0
  47. agents/task/agent/core/session_metadata.py +104 -0
  48. agents/task/agent/core/step.py +781 -0
  49. agents/task/agent/core/step_execution.py +398 -0
  50. agents/task/agent/core/step_telemetry.py +222 -0
  51. agents/task/agent/core/test_aux_metering.py +50 -0
  52. agents/task/agent/core/test_billing_failover.py +68 -0
  53. agents/task/agent/core/test_fatal_error_classifier.py +38 -0
  54. agents/task/agent/core/test_judge_validation.py +275 -0
  55. agents/task/agent/core/test_model_swap.py +190 -0
  56. agents/task/agent/core/test_reflection_offload.py +54 -0
  57. agents/task/agent/core/turn_input.py +8 -0
  58. agents/task/agent/core/untrusted_wrap.py +103 -0
  59. agents/task/agent/core/user_ingress.py +228 -0
  60. agents/task/agent/hitl_manager.py +396 -0
  61. agents/task/agent/log_sanitize.py +36 -0
  62. agents/task/agent/message_manager/config.py +97 -0
  63. agents/task/agent/message_manager/service.py +758 -0
  64. agents/task/agent/message_manager/tests.py +305 -0
  65. agents/task/agent/message_manager/tool_call_builder.py +539 -0
  66. agents/task/agent/message_manager/tool_message_repair.py +364 -0
  67. agents/task/agent/message_manager/views.py +143 -0
  68. agents/task/agent/messages/__init__.py +8 -0
  69. agents/task/agent/messages/builders.py +309 -0
  70. agents/task/agent/messages/compactor.py +611 -0
  71. agents/task/agent/messages/context_references.py +271 -0
  72. agents/task/agent/messages/filters.py +333 -0
  73. agents/task/agent/messages/guidance.py +328 -0
  74. agents/task/agent/messages/persistence.py +448 -0
  75. agents/task/agent/messages/retrieval.py +506 -0
  76. agents/task/agent/messages/sqlite_persistence.py +85 -0
  77. agents/task/agent/messages/test_compaction_upgrade.py +349 -0
  78. agents/task/agent/messages/test_persistence_subdir_fix.py +28 -0
  79. agents/task/agent/messages/test_sqlite_persistence.py +54 -0
  80. agents/task/agent/messages/token_counter.py +532 -0
  81. agents/task/agent/orchestrator.py +776 -0
  82. agents/task/agent/profile_manager.py +105 -0
  83. agents/task/agent/profile_registry.py +173 -0
  84. agents/task/agent/prompts.py +1090 -0
  85. agents/task/agent/scenario_registry.py +226 -0
  86. agents/task/agent/service.py +509 -0
  87. agents/task/agent/session.py +984 -0
  88. agents/task/agent/skill_discovery.py +110 -0
  89. agents/task/agent/skill_frontmatter.py +94 -0
  90. agents/task/agent/skill_manager.py +1201 -0
  91. agents/task/agent/skill_store.py +627 -0
  92. agents/task/agent/skill_validation.py +56 -0
  93. agents/task/agent/skill_writer.py +492 -0
  94. agents/task/agent/sub_agent_manager.py +1137 -0
  95. agents/task/agent/test_session_disk_recovery.py +34 -0
  96. agents/task/agent/tests.py +513 -0
  97. agents/task/agent/tool_call_tracker.py +511 -0
  98. agents/task/agent/views.py +469 -0
  99. agents/task/config.py +432 -0
  100. agents/task/constants.py +1384 -0
  101. agents/task/flag_defaults.py +63 -0
  102. agents/task/goals/__init__.py +1 -0
  103. agents/task/goals/autonomy_marker.py +28 -0
  104. agents/task/goals/board.py +690 -0
  105. agents/task/goals/completion_judge.py +247 -0
  106. agents/task/goals/context.py +62 -0
  107. agents/task/goals/dispatcher.py +848 -0
  108. agents/task/goals/escalation.py +102 -0
  109. agents/task/goals/planner.py +114 -0
  110. agents/task/logging_config.py +155 -0
  111. agents/task/path.py +991 -0
  112. agents/task/robust_parse_config.py +400 -0
  113. agents/task/runtime/__init__.py +0 -0
  114. agents/task/runtime/run_as_session.py +130 -0
  115. agents/task/runtime_safety.py +308 -0
  116. agents/task/session/__init__.py +5 -0
  117. agents/task/session/browser_pool.py +130 -0
  118. agents/task/session/cleanup.py +612 -0
  119. agents/task/session/execution.py +397 -0
  120. agents/task/session/feed.py +200 -0
  121. agents/task/session/hitl_ingress.py +201 -0
  122. agents/task/session/hooks.py +83 -0
  123. agents/task/session/multi_agent.py +243 -0
  124. agents/task/session/workspace.py +91 -0
  125. agents/task/session_registry.py +91 -0
  126. agents/task/session_route.py +35 -0
  127. agents/task/sqlite_session_registry.py +261 -0
  128. agents/task/surface_config.py +185 -0
  129. agents/task/telemetry/__init__.py +58 -0
  130. agents/task/telemetry/event_log.py +180 -0
  131. agents/task/telemetry/formatters.py +642 -0
  132. agents/task/telemetry/manager.py +571 -0
  133. agents/task/telemetry/memory_events.py +66 -0
  134. agents/task/telemetry/self_events.py +53 -0
  135. agents/task/telemetry/sequence.py +160 -0
  136. agents/task/telemetry/service.py +1265 -0
  137. agents/task/telemetry/views.py +882 -0
  138. agents/task/templates.py +98 -0
  139. agents/task/test_aux_chain.py +151 -0
  140. agents/task/test_aux_router.py +41 -0
  141. agents/task/tests/test_parse_robustness.py +170 -0
  142. agents/task/tool_defaults.py +110 -0
  143. agents/task/utils.py +762 -0
  144. agents/task/utils_json.py +768 -0
  145. agents/task/utils_webview.py +86 -0
  146. agents/task/workspace_context.py +229 -0
  147. agents/task_agent_lite.py +2398 -0
  148. api/README.md +683 -0
  149. api/TASK_API_DOCS.md +151 -0
  150. api/__init__.py +0 -0
  151. api/a2a/__init__.py +37 -0
  152. api/a2a/agent_card.py +337 -0
  153. api/a2a/client.py +657 -0
  154. api/a2a/endpoints.py +375 -0
  155. api/a2a/models.py +268 -0
  156. api/a2a/streaming.py +419 -0
  157. api/a2a/task_handler.py +929 -0
  158. api/admin_endpoints.py +1584 -0
  159. api/app.py +923 -0
  160. api/auth_constants.py +28 -0
  161. api/auth_endpoints.py +402 -0
  162. api/auth_state.py +36 -0
  163. api/chat_via_task.py +42 -0
  164. api/conversation_manager.py +231 -0
  165. api/dependencies.py +186 -0
  166. api/eip8004_endpoints.py +452 -0
  167. api/hyperliquid_models.py +174 -0
  168. api/hyperliquid_routes.py +450 -0
  169. api/interfaces.py +207 -0
  170. api/jwt_middleware.py +83 -0
  171. api/kb/__init__.py +1 -0
  172. api/kb/endpoints.py +314 -0
  173. api/mcp_models.py +279 -0
  174. api/mcp_routes.py +438 -0
  175. api/middleware.py +526 -0
  176. api/models.py +176 -0
  177. api/openai_compat/__init__.py +0 -0
  178. api/openai_compat/model_map.py +25 -0
  179. api/openai_compat/models.py +59 -0
  180. api/openai_compat/router.py +140 -0
  181. api/payment_endpoints.py +444 -0
  182. api/payment_verification.py +138 -0
  183. api/polymarket_models.py +155 -0
  184. api/polymarket_routes.py +501 -0
  185. api/pricing_endpoints.py +89 -0
  186. api/session_routing.py +54 -0
  187. api/skill_endpoints.py +453 -0
  188. api/task_http_api.py +1856 -0
  189. api/webhooks.py +52 -0
  190. api/x402_endpoints.py +280 -0
  191. avatar/README.md +44 -0
  192. avatar/__init__.py +1 -0
  193. avatar/config/rob.json +7 -0
  194. avatar/mindprint.js +395 -0
  195. avatar/renders/rob.meta.json +20 -0
  196. avatar/renders/rob.png +0 -0
  197. avatar/studio.html +373 -0
  198. avatar/webview/avatar-live.js +44 -0
  199. cli/README.md +89 -0
  200. cli/__init__.py +0 -0
  201. cli/cli_surface.py +67 -0
  202. cli/commands/__init__.py +0 -0
  203. cli/commands/_bootstrap.py +49 -0
  204. cli/commands/_errors.py +53 -0
  205. cli/commands/chat.py +973 -0
  206. cli/commands/config.py +100 -0
  207. cli/commands/dashboard.py +103 -0
  208. cli/commands/doctor.py +222 -0
  209. cli/commands/email.py +174 -0
  210. cli/commands/gateway.py +346 -0
  211. cli/commands/goals.py +509 -0
  212. cli/commands/init.py +276 -0
  213. cli/commands/journey.py +30 -0
  214. cli/commands/kb.py +280 -0
  215. cli/commands/model.py +107 -0
  216. cli/commands/owner.py +431 -0
  217. cli/commands/pfp.py +232 -0
  218. cli/commands/run.py +414 -0
  219. cli/commands/serve.py +41 -0
  220. cli/commands/session.py +684 -0
  221. cli/commands/skill_install.py +786 -0
  222. cli/commands/skills.py +128 -0
  223. cli/commands/subagents.py +100 -0
  224. cli/commands/surface.py +81 -0
  225. cli/commands/telegram.py +185 -0
  226. cli/commands/todos.py +209 -0
  227. cli/commands/tools.py +102 -0
  228. cli/commands/update.py +316 -0
  229. cli/commands/whatsapp.py +175 -0
  230. cli/config_store.py +276 -0
  231. cli/gitignore.py +43 -0
  232. cli/inventory.py +46 -0
  233. cli/keys.py +101 -0
  234. cli/persona.py +17 -0
  235. cli/polyrob.py +137 -0
  236. cli/ui/__init__.py +64 -0
  237. cli/ui/activity.py +141 -0
  238. cli/ui/app.py +538 -0
  239. cli/ui/banner.py +233 -0
  240. cli/ui/blocks.py +534 -0
  241. cli/ui/bootstrap_notice.py +47 -0
  242. cli/ui/commands/__init__.py +41 -0
  243. cli/ui/commands/h_cron.py +64 -0
  244. cli/ui/commands/h_journey.py +164 -0
  245. cli/ui/commands/h_kb.py +121 -0
  246. cli/ui/commands/h_learn.py +100 -0
  247. cli/ui/commands/h_mcp.py +215 -0
  248. cli/ui/commands/h_self.py +113 -0
  249. cli/ui/commands/h_skills.py +314 -0
  250. cli/ui/commands/handlers.py +1672 -0
  251. cli/ui/commands/registry.py +426 -0
  252. cli/ui/dialog.py +425 -0
  253. cli/ui/event_registry.py +110 -0
  254. cli/ui/event_specs.py +45 -0
  255. cli/ui/events.py +433 -0
  256. cli/ui/identity.py +15 -0
  257. cli/ui/lifecycle.py +159 -0
  258. cli/ui/live_hooks.py +43 -0
  259. cli/ui/model_selector.py +532 -0
  260. cli/ui/persistent_loop.py +181 -0
  261. cli/ui/pick.py +113 -0
  262. cli/ui/plain_renderer.py +351 -0
  263. cli/ui/renderer.py +384 -0
  264. cli/ui/rich_renderer.py +533 -0
  265. cli/ui/secrets.py +115 -0
  266. cli/ui/state.py +376 -0
  267. cli/ui/statusbar.py +231 -0
  268. cli/ui/streaming.py +254 -0
  269. cli/ui/terminal_render.py +105 -0
  270. cli/ui/theme.py +132 -0
  271. cli/update/__init__.py +6 -0
  272. cli/update/context.py +121 -0
  273. cli/update/detect.py +176 -0
  274. cli/update/engine.py +95 -0
  275. cli/update/migrate_guarded.py +55 -0
  276. cli/update/process_guard.py +215 -0
  277. cli/update/runners.py +78 -0
  278. cli/update/snapshot.py +270 -0
  279. cli/update/versions.py +205 -0
  280. core/README.md +377 -0
  281. core/__init__.py +174 -0
  282. core/assets.py +87 -0
  283. core/async_bridge.py +76 -0
  284. core/autonomy_runtime.py +259 -0
  285. core/base_component.py +302 -0
  286. core/bootstrap.py +681 -0
  287. core/bot.py +310 -0
  288. core/config.py +833 -0
  289. core/constants.py +126 -0
  290. core/container.py +462 -0
  291. core/db_manifest.py +101 -0
  292. core/embedding.py +57 -0
  293. core/env.py +39 -0
  294. core/exceptions.py +287 -0
  295. core/flags.py +144 -0
  296. core/flags_catalog.py +345 -0
  297. core/home_migration.py +62 -0
  298. core/identity.py +118 -0
  299. core/initialization.py +1004 -0
  300. core/instance.py +537 -0
  301. core/interactive_gate.py +96 -0
  302. core/logging.py +436 -0
  303. core/owner_doc_writer.py +176 -0
  304. core/pairing.py +193 -0
  305. core/path_safety.py +20 -0
  306. core/paths.py +27 -0
  307. core/payment_config.py +21 -0
  308. core/permissions.py +322 -0
  309. core/runtime_config.py +96 -0
  310. core/runtime_paths.py +135 -0
  311. core/seams.py +35 -0
  312. core/secret_scan.py +25 -0
  313. core/secret_scrub.py +71 -0
  314. core/secrets.py +35 -0
  315. core/security_logging_filter.py +148 -0
  316. core/self_context_writer.py +333 -0
  317. core/self_evolution.py +294 -0
  318. core/session_context.py +47 -0
  319. core/sqlite_util.py +59 -0
  320. core/surfaces/__init__.py +16 -0
  321. core/surfaces/access.py +118 -0
  322. core/surfaces/binding.py +88 -0
  323. core/surfaces/bootstrap.py +78 -0
  324. core/surfaces/circuit.py +168 -0
  325. core/surfaces/continuity.py +77 -0
  326. core/surfaces/correspondents.py +228 -0
  327. core/surfaces/dispatcher.py +216 -0
  328. core/surfaces/envelopes.py +75 -0
  329. core/surfaces/gc.py +30 -0
  330. core/surfaces/idempotency.py +57 -0
  331. core/surfaces/inbound_webhook.py +124 -0
  332. core/surfaces/media.py +32 -0
  333. core/surfaces/message_router.py +91 -0
  334. core/surfaces/outbound_allowlist.py +80 -0
  335. core/surfaces/outbound_dispatcher.py +110 -0
  336. core/surfaces/outbound_mirror.py +37 -0
  337. core/surfaces/outbound_queue.py +107 -0
  338. core/surfaces/outbound_target.py +12 -0
  339. core/surfaces/owner_admin.py +29 -0
  340. core/surfaces/proactive.py +78 -0
  341. core/surfaces/progress.py +108 -0
  342. core/surfaces/rate_bucket.py +21 -0
  343. core/surfaces/registry.py +51 -0
  344. core/surfaces/rendering.py +53 -0
  345. core/surfaces/send_policy.py +14 -0
  346. core/surfaces/serialize.py +24 -0
  347. core/surfaces/session_chat_registry.py +112 -0
  348. core/surfaces/session_policy.py +60 -0
  349. core/surfaces/surface.py +288 -0
  350. core/surfaces/transcription.py +88 -0
  351. core/surfaces/voice_echo.py +26 -0
  352. core/surfaces/voice_guard.py +23 -0
  353. core/tickers.py +174 -0
  354. core/tool_catalog.py +192 -0
  355. core/version.py +75 -0
  356. core/wallet/__init__.py +5 -0
  357. core/wallet/agent_wallet.py +59 -0
  358. core/wallet/audit_sink.py +61 -0
  359. core/wallet/config.py +70 -0
  360. core/wallet/factory.py +87 -0
  361. core/wallet/policy.py +104 -0
  362. core/wallet/signer.py +51 -0
  363. cron/README.md +65 -0
  364. cron/__init__.py +0 -0
  365. cron/delivery.py +277 -0
  366. cron/digest.py +124 -0
  367. cron/jobs.py +207 -0
  368. cron/runner.py +297 -0
  369. cron/schedule.py +194 -0
  370. cron/scheduler.py +201 -0
  371. cron/service.py +57 -0
  372. cron/wake_gate.py +223 -0
  373. data/__init__.py +1 -0
  374. data/prompts/autov2_prompts.json +87 -0
  375. data/prompts/skills/browser-automation/SKILL.md +43 -0
  376. data/prompts/skills/coding-workflow/SKILL.md +36 -0
  377. data/prompts/skills/crypto-trading-safety/SKILL.md +53 -0
  378. data/prompts/skills/document-writing/SKILL.md +33 -0
  379. data/prompts/skills/email-comms/SKILL.md +33 -0
  380. data/prompts/skills/file-data-ops/SKILL.md +35 -0
  381. data/prompts/skills/hyperliquid-account-review/SKILL.md +48 -0
  382. data/prompts/skills/hyperliquid-market-data/SKILL.md +52 -0
  383. data/prompts/skills/hyperliquid-trading/SKILL.md +59 -0
  384. data/prompts/skills/lead-research/SKILL.md +86 -0
  385. data/prompts/skills/market-research-brief/SKILL.md +104 -0
  386. data/prompts/skills/person-analyzer/SKILL.md +124 -0
  387. data/prompts/skills/polymarket-market-research/SKILL.md +54 -0
  388. data/prompts/skills/polymarket-portfolio-review/SKILL.md +50 -0
  389. data/prompts/skills/polymarket-trading/SKILL.md +55 -0
  390. data/prompts/skills/presentation-creator/SKILL.md +36 -0
  391. data/prompts/skills/project-analyzer/SKILL.md +169 -0
  392. data/prompts/skills/rules.json +268 -0
  393. data/prompts/skills/secret-handling/SKILL.md +20 -0
  394. data/prompts/skills/skill-authoring/SKILL.md +31 -0
  395. data/prompts/skills/skill-security-review/SKILL.md +27 -0
  396. data/prompts/skills/social-discovery/SKILL.md +57 -0
  397. data/prompts/skills/task-planning/SKILL.md +33 -0
  398. data/prompts/skills/web-research/SKILL.md +39 -0
  399. data/prompts/skills/web-scraping/SKILL.md +73 -0
  400. data/prompts/skills/x-engagement/SKILL.md +69 -0
  401. data/prompts/system_prompts.json +4 -0
  402. data/subscription_channels.json +5 -0
  403. modules/README.md +503 -0
  404. modules/__init__.py +124 -0
  405. modules/auth/__init__.py +13 -0
  406. modules/auth/api_key_manager.py +157 -0
  407. modules/auth/identity_mapper.py +380 -0
  408. modules/auth/siwe_auth.py +273 -0
  409. modules/auth/tier_manager.py +173 -0
  410. modules/base_module.py +190 -0
  411. modules/credits/__init__.py +29 -0
  412. modules/credits/balance_manager.py +237 -0
  413. modules/credits/cost_utils.py +129 -0
  414. modules/credits/pricing.py +139 -0
  415. modules/credits/unified_ledger.py +148 -0
  416. modules/credits/usage_meter.py +310 -0
  417. modules/credits/usage_tracker.py +674 -0
  418. modules/database/__init__.py +33 -0
  419. modules/database/audit_log.py +322 -0
  420. modules/database/auth_tables.py +276 -0
  421. modules/database/connection.py +644 -0
  422. modules/database/connection_pool.py +305 -0
  423. modules/database/conversation_contexts.py +446 -0
  424. modules/database/database_manager.py +237 -0
  425. modules/database/hyperliquid.py +375 -0
  426. modules/database/polymarket.py +574 -0
  427. modules/database/user_mcp_servers.py +728 -0
  428. modules/database/user_profiles.py +562 -0
  429. modules/database/utils.py +107 -0
  430. modules/database/x402_tables.py +92 -0
  431. modules/eip8004/README.md +1069 -0
  432. modules/eip8004/__init__.py +65 -0
  433. modules/eip8004/contracts.py +498 -0
  434. modules/eip8004/models.py +271 -0
  435. modules/eip8004/registration.py +148 -0
  436. modules/eip8004/reputation.py +367 -0
  437. modules/eip8004/validation.py +277 -0
  438. modules/llm/__init__.py +97 -0
  439. modules/llm/adapters.py +963 -0
  440. modules/llm/anthropic_client.py +831 -0
  441. modules/llm/available_models.py +106 -0
  442. modules/llm/brain_scrubber.py +137 -0
  443. modules/llm/cache_hints.py +162 -0
  444. modules/llm/deepseek_client.py +562 -0
  445. modules/llm/gemini_client.py +1367 -0
  446. modules/llm/llm_client.py +1139 -0
  447. modules/llm/llm_client_registry.py +188 -0
  448. modules/llm/llm_factory.py +237 -0
  449. modules/llm/llm_manager.py +1036 -0
  450. modules/llm/messages.py +374 -0
  451. modules/llm/model_registry.py +1935 -0
  452. modules/llm/nvidia_client.py +125 -0
  453. modules/llm/openai_client.py +800 -0
  454. modules/llm/openrouter_client.py +969 -0
  455. modules/llm/profiles.py +178 -0
  456. modules/llm/test_anthropic_cache_capture.py +35 -0
  457. modules/llm/test_cached_pricing_derivation.py +24 -0
  458. modules/llm/think_scrubber.py +444 -0
  459. modules/llm/token_counter.py +268 -0
  460. modules/memory/__init__.py +35 -0
  461. modules/memory/backend_factory.py +83 -0
  462. modules/memory/cache_manager.py +186 -0
  463. modules/memory/episodic.py +108 -0
  464. modules/memory/local_vector_memory_provider.py +552 -0
  465. modules/memory/memory_manager.py +225 -0
  466. modules/memory/models.py +312 -0
  467. modules/memory/provider.py +210 -0
  468. modules/memory/registry.py +205 -0
  469. modules/memory/sqlite_memory_provider.py +767 -0
  470. modules/memory/task/__init__.py +52 -0
  471. modules/memory/task/compaction_manager.py +157 -0
  472. modules/memory/task/context_retriever.py +809 -0
  473. modules/memory/task/hierarchical_memory.py +1002 -0
  474. modules/memory/task/lexical_retriever.py +71 -0
  475. modules/memory/task/null_context_manager.py +213 -0
  476. modules/memory/task/phase_manager.py +625 -0
  477. modules/memory/task/reflection_service.py +157 -0
  478. modules/memory/task/semantic_retriever.py +218 -0
  479. modules/memory/task/task_context_manager.py +1165 -0
  480. modules/memory/task/test_forgetting_engages.py +24 -0
  481. modules/memory/task/test_reflection_llm.py +39 -0
  482. modules/memory/task/test_threat_scan.py +53 -0
  483. modules/memory/task/threat_scan.py +90 -0
  484. modules/memory/test_sqlite_memory_provider.py +106 -0
  485. modules/memory/user_profile_manager.py +527 -0
  486. modules/payments/__init__.py +11 -0
  487. modules/payments/deposit_monitor.py +478 -0
  488. modules/payments/price_oracle.py +70 -0
  489. modules/payments/treasury_sweeper.py +401 -0
  490. modules/payments/wallet_generator.py +145 -0
  491. modules/pfp/__init__.py +1 -0
  492. modules/pfp/config.py +92 -0
  493. modules/pfp/mesh.py +565 -0
  494. modules/pfp/push.py +69 -0
  495. modules/pfp/renderer.py +85 -0
  496. modules/pfp/store.py +82 -0
  497. modules/skills/__init__.py +1 -0
  498. modules/skills/skill_usage.py +248 -0
  499. modules/transcription/__init__.py +24 -0
  500. modules/transcription/base.py +20 -0
  501. modules/transcription/faster_whisper_transcriber.py +55 -0
  502. modules/x402/README.md +249 -0
  503. modules/x402/__init__.py +22 -0
  504. modules/x402/invoicing.py +416 -0
  505. modules/x402/middleware.py +364 -0
  506. modules/x402/settlement_watcher.py +145 -0
  507. modules/x402/x402_integration.py +344 -0
  508. polyrob-0.5.0.dist-info/METADATA +482 -0
  509. polyrob-0.5.0.dist-info/RECORD +745 -0
  510. polyrob-0.5.0.dist-info/WHEEL +5 -0
  511. polyrob-0.5.0.dist-info/entry_points.txt +2 -0
  512. polyrob-0.5.0.dist-info/licenses/LICENSE +21 -0
  513. polyrob-0.5.0.dist-info/top_level.txt +12 -0
  514. surfaces/README.md +70 -0
  515. surfaces/__init__.py +0 -0
  516. surfaces/email/__init__.py +0 -0
  517. surfaces/email/dedup.py +44 -0
  518. surfaces/email/harness.py +187 -0
  519. surfaces/email/inbound.py +159 -0
  520. surfaces/email/seed.py +60 -0
  521. surfaces/email/surface.py +105 -0
  522. surfaces/telegram/__init__.py +0 -0
  523. surfaces/telegram/dedup.py +83 -0
  524. surfaces/telegram/harness.py +886 -0
  525. surfaces/telegram/inbound.py +145 -0
  526. surfaces/telegram/interactive_tools.py +49 -0
  527. surfaces/telegram/markdown.py +347 -0
  528. surfaces/telegram/rate_limit.py +167 -0
  529. surfaces/telegram/surface.py +187 -0
  530. surfaces/telegram/voice.py +52 -0
  531. surfaces/whatsapp/__init__.py +0 -0
  532. surfaces/whatsapp/client.py +51 -0
  533. surfaces/whatsapp/harness.py +62 -0
  534. surfaces/whatsapp/inbound.py +132 -0
  535. surfaces/whatsapp/surface.py +64 -0
  536. surfaces/whatsapp/window.py +34 -0
  537. tools/README.md +578 -0
  538. tools/__init__.py +424 -0
  539. tools/alchemy/__init__.py +5 -0
  540. tools/alchemy/alchemy_tool.py +389 -0
  541. tools/anysite/__init__.py +17 -0
  542. tools/anysite/client.py +88 -0
  543. tools/anysite/tool.py +89 -0
  544. tools/base_tool.py +468 -0
  545. tools/browser/__init__.py +22 -0
  546. tools/browser/actions.py +118 -0
  547. tools/browser/browser.py +1457 -0
  548. tools/browser/browser_manager.py +690 -0
  549. tools/browser/context.py +1748 -0
  550. tools/browser/playwright_utils.py +137 -0
  551. tools/browser/views.py +56 -0
  552. tools/code_exec/SANDBOX_SECURITY.md +144 -0
  553. tools/code_exec/__init__.py +178 -0
  554. tools/code_exec/backend.py +73 -0
  555. tools/code_exec/backends/__init__.py +1 -0
  556. tools/code_exec/backends/docker.py +779 -0
  557. tools/code_exec/backends/local_subprocess.py +154 -0
  558. tools/code_exec/env_policy.py +45 -0
  559. tools/code_exec/result.py +42 -0
  560. tools/code_exec/sandbox_guard.py +59 -0
  561. tools/code_exec/tool.py +227 -0
  562. tools/coding/__init__.py +50 -0
  563. tools/coding/edit.py +107 -0
  564. tools/coding/search.py +113 -0
  565. tools/coding/tool.py +353 -0
  566. tools/collabland/__init__.py +5 -0
  567. tools/collabland/collabland_tool.py +471 -0
  568. tools/controller/__init__.py +0 -0
  569. tools/controller/_helpers.py +165 -0
  570. tools/controller/action_registration.py +1765 -0
  571. tools/controller/approval.py +244 -0
  572. tools/controller/approval_interactive.py +90 -0
  573. tools/controller/delegation.py +195 -0
  574. tools/controller/execution.py +797 -0
  575. tools/controller/execution_context.py +141 -0
  576. tools/controller/hooks.py +135 -0
  577. tools/controller/introspection.py +344 -0
  578. tools/controller/mcp_registrar.py +190 -0
  579. tools/controller/message_send.py +31 -0
  580. tools/controller/registry/__init__.py +4 -0
  581. tools/controller/registry/schema_generators.py +395 -0
  582. tools/controller/registry/schema_sanitizer.py +406 -0
  583. tools/controller/registry/service.py +1266 -0
  584. tools/controller/registry/views.py +151 -0
  585. tools/controller/service.py +409 -0
  586. tools/controller/tool_management.py +334 -0
  587. tools/controller/types.py +28 -0
  588. tools/controller/views.py +450 -0
  589. tools/cronjob_tools.py +166 -0
  590. tools/crypto_trade_gate.py +59 -0
  591. tools/descriptors.py +452 -0
  592. tools/dom/__init__.py +0 -0
  593. tools/dom/history_tree_processor/service.py +244 -0
  594. tools/dom/history_tree_processor/view.py +99 -0
  595. tools/dom/service.py +471 -0
  596. tools/dom/views.py +346 -0
  597. tools/email_tool.py +403 -0
  598. tools/exceptions.py +169 -0
  599. tools/filesystem.py +1038 -0
  600. tools/filesystem_docproc.py +331 -0
  601. tools/filesystem_pdf.py +755 -0
  602. tools/git/__init__.py +42 -0
  603. tools/git/tool.py +258 -0
  604. tools/github/__init__.py +33 -0
  605. tools/github/client.py +94 -0
  606. tools/github/tool.py +237 -0
  607. tools/goal_tools.py +309 -0
  608. tools/hyperliquid/__init__.py +56 -0
  609. tools/hyperliquid/models.py +233 -0
  610. tools/hyperliquid/service.py +1680 -0
  611. tools/knowledge_ingest.py +950 -0
  612. tools/mcp/README.md +535 -0
  613. tools/mcp/__init__.py +52 -0
  614. tools/mcp/catalog.py +115 -0
  615. tools/mcp/config.py +337 -0
  616. tools/mcp/mcp_tool.py +1415 -0
  617. tools/mcp/param_coercion.py +289 -0
  618. tools/mcp/protocol.py +1520 -0
  619. tools/mcp/rate_limit.py +50 -0
  620. tools/mcp/security.py +424 -0
  621. tools/mcp/self_install.py +111 -0
  622. tools/mcp/server_manager.py +829 -0
  623. tools/mcp/subscriptions.py +72 -0
  624. tools/mcp/user_mcp_service.py +899 -0
  625. tools/mcp/validation_tracker.py +89 -0
  626. tools/mcp/views.py +346 -0
  627. tools/oauth/__init__.py +17 -0
  628. tools/oauth/manager.py +75 -0
  629. tools/oauth/provider.py +69 -0
  630. tools/oauth/providers/__init__.py +1 -0
  631. tools/oauth/providers/generic_oauth2.py +96 -0
  632. tools/perplexity_tool.py +280 -0
  633. tools/polymarket/__init__.py +41 -0
  634. tools/polymarket/clob_adapter.py +76 -0
  635. tools/polymarket/models.py +182 -0
  636. tools/polymarket/service.py +2082 -0
  637. tools/self_env/__init__.py +61 -0
  638. tools/self_env/tool.py +274 -0
  639. tools/shell/__init__.py +80 -0
  640. tools/shell/backend_pool.py +118 -0
  641. tools/shell/discipline.py +62 -0
  642. tools/shell/executor.py +157 -0
  643. tools/shell/loopback_allow.py +87 -0
  644. tools/shell/process_registry.py +113 -0
  645. tools/shell/process_tool.py +131 -0
  646. tools/shell/state.py +129 -0
  647. tools/shell/tool.py +151 -0
  648. tools/task_tool.py +637 -0
  649. tools/twitter_tool.py +2086 -0
  650. tools/user_directory.py +179 -0
  651. tools/web_fetch/__init__.py +3 -0
  652. tools/web_fetch/fetcher.py +144 -0
  653. tools/web_fetch/render.py +44 -0
  654. tools/web_fetch/tool.py +60 -0
  655. tools/x402/__init__.py +60 -0
  656. tools/x402/client.py +47 -0
  657. tools/x402/invoice_tool.py +143 -0
  658. tools/x402/real_client.py +250 -0
  659. tools/x402/service.py +131 -0
  660. utils/README.md +498 -0
  661. utils/__init__.py +46 -0
  662. utils/auth_utils.py +114 -0
  663. utils/bounded_collections.py +162 -0
  664. utils/circuit_breaker.py +389 -0
  665. utils/gif_utils.py +435 -0
  666. utils/markdown_utils.py +291 -0
  667. utils/message_utils.py +147 -0
  668. utils/metrics.py +36 -0
  669. utils/path_validator.py +227 -0
  670. utils/rate_limit_manager.py +399 -0
  671. utils/result_size.py +296 -0
  672. utils/time_utils.py +206 -0
  673. utils/user_utils.py +188 -0
  674. webview/README.md +111 -0
  675. webview/RENAME_AND_ALIGN_HANDOFF.md +200 -0
  676. webview/__init__.py +8 -0
  677. webview/activity.py +636 -0
  678. webview/owner_auth.py +108 -0
  679. webview/pages.py +448 -0
  680. webview/repair_sessions.py +399 -0
  681. webview/server.py +4323 -0
  682. webview/server_launcher.py +112 -0
  683. webview/static/css/activity.css +89 -0
  684. webview/static/css/chat.css +1772 -0
  685. webview/static/css/components.css +1190 -0
  686. webview/static/css/config-panel.css +651 -0
  687. webview/static/css/pages/admin.css +766 -0
  688. webview/static/css/pages/profile.css +231 -0
  689. webview/static/css/pages/settings.css +297 -0
  690. webview/static/css/pages/signin.css +316 -0
  691. webview/static/css/style.css +4250 -0
  692. webview/static/css/variables.css +147 -0
  693. webview/static/css/workspace-fullwidth-fix.css +109 -0
  694. webview/static/img/favicon.ico +0 -0
  695. webview/static/js/activity.js +394 -0
  696. webview/static/js/admin/activity.js +186 -0
  697. webview/static/js/admin/dashboard.js +163 -0
  698. webview/static/js/admin/user_detail.js +400 -0
  699. webview/static/js/admin/users.js +174 -0
  700. webview/static/js/admin/utils.js +214 -0
  701. webview/static/js/chat.js +3178 -0
  702. webview/static/js/config-panel.js +522 -0
  703. webview/static/js/constants.js +20 -0
  704. webview/static/js/error-handler.js +226 -0
  705. webview/static/js/ethers.min.js +1 -0
  706. webview/static/js/event-filter.js +137 -0
  707. webview/static/js/event-store.js +351 -0
  708. webview/static/js/file-attachments.js +374 -0
  709. webview/static/js/file-loader.js +163 -0
  710. webview/static/js/index.js +103 -0
  711. webview/static/js/performance-utils.js +196 -0
  712. webview/static/js/profile.js +396 -0
  713. webview/static/js/screenshot.js +386 -0
  714. webview/static/js/session.js +1527 -0
  715. webview/static/js/settings.js +1288 -0
  716. webview/static/js/sidebar-data.js +766 -0
  717. webview/static/js/sidebar-toggle.js +296 -0
  718. webview/static/js/socket.io.min.js +7 -0
  719. webview/static/js/stats.js +355 -0
  720. webview/static/js/ui-utils.js +420 -0
  721. webview/static/js/workspace.js +966 -0
  722. webview/stats_service.py +613 -0
  723. webview/templates/__init__.py +1 -0
  724. webview/templates/__pycache__/__init__.cpython-311.pyc +0 -0
  725. webview/templates/activity.html +36 -0
  726. webview/templates/admin/activity.html +122 -0
  727. webview/templates/admin/dashboard.html +112 -0
  728. webview/templates/admin/user_detail.html +228 -0
  729. webview/templates/admin/users.html +105 -0
  730. webview/templates/autonomy.html +60 -0
  731. webview/templates/error.html +133 -0
  732. webview/templates/finance.html +90 -0
  733. webview/templates/identity.html +73 -0
  734. webview/templates/index.html +42 -0
  735. webview/templates/layout.html +125 -0
  736. webview/templates/memory.html +59 -0
  737. webview/templates/owner_login.html +15 -0
  738. webview/templates/profile.html +121 -0
  739. webview/templates/session.html +312 -0
  740. webview/templates/settings.html +534 -0
  741. webview/templates/sidebar.html +112 -0
  742. webview/templates/signin.html +213 -0
  743. webview/templates/status.html +14 -0
  744. webview/templates/system.html +45 -0
  745. webview/webgate.py +196 -0
@@ -0,0 +1,1266 @@
1
+ import asyncio
2
+ import os
3
+ import threading
4
+ from inspect import iscoroutinefunction, signature
5
+ from typing import Any, Callable, Dict, List, Optional, Type, TYPE_CHECKING
6
+ import logging
7
+
8
+ # Native types
9
+ from modules.llm.adapters import BaseChatModel
10
+ from pydantic import BaseModel, Field, create_model
11
+
12
+ from tools.browser.context import BrowserContext
13
+ from tools.controller.registry.views import (
14
+ ActionModel,
15
+ ActionRegistry,
16
+ RegisteredAction,
17
+ )
18
+
19
+ if TYPE_CHECKING:
20
+ from tools.controller.execution_context import ActionExecutionContext
21
+ from tools.controller.registry.schema_generators import (
22
+ get_schema_generator,
23
+ ToolSchemaGenerator,
24
+ )
25
+
26
+ # Controller handles all tool management
27
+
28
+ # Import logging config
29
+ from agents.task.logging_config import get_task_logger
30
+
31
+ # Import telemetry - handle possible import errors
32
+ try:
33
+ from agents.task.telemetry import get_telemetry
34
+ from agents.task.telemetry.views import (
35
+ ControllerRegisteredFunctionsTelemetryEvent,
36
+ RegisteredFunction,
37
+ )
38
+ except ImportError:
39
+ # Create dummy telemetry if imports fail
40
+ def get_telemetry():
41
+ class DummyTelemetry:
42
+ def capture(self, *args, **kwargs):
43
+ pass
44
+ return DummyTelemetry()
45
+
46
+ class ControllerRegisteredFunctionsTelemetryEvent:
47
+ def __init__(self, **kwargs):
48
+ pass
49
+
50
+ class RegisteredFunction:
51
+ def __init__(self, **kwargs):
52
+ pass
53
+
54
+
55
+ class Registry:
56
+ """Service for registering and managing actions"""
57
+
58
+ def __init__(
59
+ self, exclude_actions: Optional[list[str]] = None, output_model: Optional[Type[BaseModel]] = None, session_id: Optional[str] = None, enforce_execution_context: bool = False
60
+ ):
61
+ """Registry of actions that the agent can use.
62
+
63
+ Args:
64
+ exclude_actions: List of action names to exclude from the registry.
65
+ output_model: Model to use for action results.
66
+ session_id: Session ID for telemetry purposes.
67
+ enforce_execution_context: If True, require execution_context and disallow legacy parameters.
68
+ """
69
+ self.exclude_actions = exclude_actions if exclude_actions is not None else []
70
+ self.output_model = output_model
71
+ self.session_id = session_id
72
+ self.enforce_execution_context = enforce_execution_context
73
+
74
+ # Initialize logger
75
+ self.logger = get_task_logger("registry", session_id)
76
+
77
+ # Initialize telemetry
78
+ self.telemetry = get_telemetry()
79
+
80
+ # Initialize registry with thread safety
81
+ self.registry = ActionRegistry()
82
+ self._registry_lock = threading.RLock() # Thread-safe registry access for both sync and async
83
+
84
+ # Tool-schema memoization: provider schema lists are byte-stable for a given
85
+ # set of registered actions, but were regenerated on every step (~3.7k tokens'
86
+ # worth of work + re-send per call). Cache them keyed on (provider, action-set,
87
+ # exclusions); the key changes whenever actions are added/removed, so it
88
+ # self-invalidates without explicit bumping.
89
+ self._provider_schema_cache: dict = {}
90
+
91
+ # Track action registration for logging only
92
+ self._action_registration_logged = set() # Track logged actions
93
+
94
+ # Register default actions
95
+ self._register_default_actions()
96
+
97
+
98
+ def _set_tool_attribution(self, function: Callable, tool_name: str) -> None:
99
+ """Set tool attribution on a function (SINGLE source of truth for attribution).
100
+
101
+ Handles all the edge cases: bound methods, wrapped functions, etc.
102
+ This is the ONLY place where _tool and _service should be set.
103
+
104
+ Args:
105
+ function: The callable to set attribution on
106
+ tool_name: Name of the tool to attribute to
107
+ """
108
+ import inspect
109
+
110
+ # Skip if already set to avoid overwriting
111
+ if hasattr(function, '_tool') and function._tool == tool_name:
112
+ return
113
+
114
+ # Try to set attributes, handle cases where it's not possible
115
+ try:
116
+ # For bound methods, we can't set attributes directly
117
+ # Store in a registry instead (future: use WeakKeyDictionary)
118
+ if inspect.ismethod(function):
119
+ # Can't set attributes on bound methods
120
+ # The RegisteredAction object will store this instead
121
+ pass
122
+ else:
123
+ # For regular functions and unbound methods
124
+ function._tool = tool_name
125
+ function._service = tool_name # Keep for backward compat
126
+ except (AttributeError, TypeError):
127
+ # Some callables don't support attribute assignment
128
+ # RegisteredAction will store the tool name instead
129
+ pass
130
+
131
+ def _create_param_model(self, function: Callable) -> Type[BaseModel]:
132
+ """Creates a Pydantic model from function signature
133
+
134
+ Handles missing type annotations by providing reasonable defaults.
135
+ """
136
+ sig = signature(function)
137
+ import inspect
138
+
139
+ params = {}
140
+ for name, param in sig.parameters.items():
141
+ # Skip self, browser, LLM, file path, and execution_context parameters
142
+ if name in ['self', 'browser', 'page_extraction_llm', 'available_file_paths', 'execution_context']:
143
+ continue
144
+
145
+ # Handle missing type annotations with str as default
146
+ if param.annotation is inspect._empty:
147
+ annotation = str
148
+ else:
149
+ annotation = param.annotation
150
+
151
+ # Handle missing default values
152
+ if param.default is inspect._empty:
153
+ params[name] = (annotation, ...)
154
+ else:
155
+ params[name] = (annotation, param.default)
156
+
157
+ # Create the model with a unique name based on the function
158
+ return create_model(
159
+ f'{function.__name__}_parameters',
160
+ __base__=ActionModel,
161
+ **params, # type: ignore
162
+ )
163
+
164
+ def action(
165
+ self,
166
+ description: str,
167
+ param_model: Optional[Type[BaseModel]] = None,
168
+ tool: Optional[str] = None
169
+ ):
170
+ """Decorator for registering actions
171
+
172
+ Args:
173
+ description: Description of what the action does
174
+ param_model: Optional Pydantic model for parameters
175
+ tool: Optional tool name to attribute the action to
176
+ """
177
+ def decorator(func: Callable):
178
+ # Skip registration if action is in exclude_actions
179
+ if self.exclude_actions and func.__name__ in self.exclude_actions:
180
+ return func
181
+
182
+ # Create param model from function if not provided
183
+ actual_param_model = param_model or self._create_param_model(func)
184
+
185
+ # Wrap sync functions to make them async
186
+ if not iscoroutinefunction(func):
187
+ async def async_wrapper(*args, **kwargs):
188
+ return await asyncio.to_thread(func, *args, **kwargs)
189
+
190
+ # Copy the signature and other metadata from the original function
191
+ async_wrapper.__signature__ = signature(func)
192
+ async_wrapper.__name__ = func.__name__
193
+ async_wrapper.__annotations__ = func.__annotations__
194
+ wrapped_func = async_wrapper
195
+ else:
196
+ wrapped_func = func
197
+
198
+ action = RegisteredAction(
199
+ name=func.__name__,
200
+ description=description,
201
+ function=wrapped_func,
202
+ param_model=actual_param_model,
203
+ tool=tool
204
+ )
205
+ self.registry.actions[func.__name__] = action
206
+
207
+ # Log registration with tool attribution if provided
208
+ if tool:
209
+ self.logger.debug(f"Registered action '{func.__name__}' with tool '{tool}'")
210
+ else:
211
+ self.logger.debug(f"Registered action '{func.__name__}'")
212
+
213
+ return func
214
+
215
+ return decorator
216
+
217
+ def remove_action(self, action_name: str) -> bool:
218
+ """Unregister an action by name. Returns True if it existed and was removed.
219
+
220
+ Thread-safe and busts the per-provider schema cache so the removed action
221
+ stops appearing in generated tool schemas. Without this, Controller.remove_tool
222
+ was a silent no-op (it probed for a non-existent remove_action), leaving a
223
+ "removed" tool's actions fully registered and callable — a capability-revocation
224
+ hole.
225
+ """
226
+ with self._registry_lock:
227
+ if action_name not in self.registry.actions:
228
+ return False
229
+ del self.registry.actions[action_name]
230
+ self._action_registration_logged.discard(action_name)
231
+ # Schema cache is keyed on the action-set; bust it so the dropped action
232
+ # is no longer emitted to providers.
233
+ self._provider_schema_cache.clear()
234
+ self.logger.debug(f"Removed action '{action_name}'")
235
+ return True
236
+
237
+ async def execute_action(
238
+ self,
239
+ action_name: str,
240
+ params: dict,
241
+ execution_context: Optional['ActionExecutionContext'] = None,
242
+ # Legacy parameters for backward compatibility
243
+ browser: Optional[BrowserContext] = None,
244
+ page_extraction_llm: Optional[BaseChatModel] = None,
245
+ sensitive_data: Optional[Dict[str, str]] = None,
246
+ available_file_paths: Optional[list[str]] = None,
247
+ session_id: Optional[str] = None,
248
+ ) -> Any:
249
+ """Execute a registered action with thread-safe registry access"""
250
+
251
+ # Enforce execution context if required
252
+ if self.enforce_execution_context:
253
+ if not execution_context:
254
+ raise ValueError("execution_context is required when enforce_execution_context=True")
255
+ if any([browser, page_extraction_llm, sensitive_data, available_file_paths, session_id]):
256
+ self.logger.debug("Legacy parameters ignored when enforce_execution_context=True")
257
+
258
+ # Build execution context from parameters if not provided
259
+ if not execution_context:
260
+ # Import here to avoid circular dependency
261
+ from tools.controller.execution_context import ActionExecutionContext
262
+ execution_context = ActionExecutionContext(
263
+ browser_context=browser,
264
+ session_id=session_id or self.session_id,
265
+ workspace_dir=getattr(self, 'workspace_dir', None),
266
+ available_file_paths=available_file_paths or [],
267
+ sensitive_data=sensitive_data or {},
268
+ )
269
+ else:
270
+ # Override context with explicit parameters if provided
271
+ if browser:
272
+ execution_context.browser_context = browser
273
+ if sensitive_data:
274
+ execution_context.sensitive_data = sensitive_data
275
+ if available_file_paths:
276
+ execution_context.available_file_paths = available_file_paths
277
+ if session_id:
278
+ execution_context.session_id = session_id
279
+
280
+ with self._registry_lock:
281
+ if action_name not in self.registry.actions:
282
+ raise ValueError(f'Action {action_name} not found')
283
+
284
+ action = self.registry.actions[action_name]
285
+ try:
286
+ # Handle None params - convert to empty dict
287
+ if params is None:
288
+ params = {}
289
+ # WS-1.3: systematic camelCase→snake_case reconciliation against the
290
+ # target model's fields (e.g. kimi-k2.6 emits filePath for file_path).
291
+ # Safe for genuinely-camelCase tool params (MCP): only renames when the
292
+ # snake_case form is an actual field of this action's param model.
293
+ if action.param_model is not None:
294
+ try:
295
+ from agents.task.utils_json import reconcile_field_names_to_model
296
+ params = reconcile_field_names_to_model(
297
+ params, set(action.param_model.model_fields.keys())
298
+ )
299
+ except Exception as _e:
300
+ self.logger.debug(f"Field-name reconciliation skipped: {_e}")
301
+ # Create the validated Pydantic model
302
+ validated_params = action.param_model(**params)
303
+
304
+ # Check if the first parameter is a Pydantic model
305
+ sig = signature(action.function)
306
+ parameters = list(sig.parameters.values())
307
+ # Guard against missing/invalid annotations before issubclass
308
+ is_pydantic = False
309
+ if parameters:
310
+ first_anno = parameters[0].annotation
311
+ try:
312
+ is_pydantic = isinstance(first_anno, type) and issubclass(first_anno, BaseModel)
313
+ except TypeError:
314
+ is_pydantic = False
315
+ # PEP 563 / `from __future__ import annotations`: the annotation is a
316
+ # *string* (e.g. "GoalCreateAction"), so the isinstance/issubclass
317
+ # check above is False and we'd wrongly splat the model fields as
318
+ # kwargs into a `(self, params: Model, ...)` signature → TypeError.
319
+ # When the action was registered with an explicit param_model, trust
320
+ # it: if the first param's stringized annotation names that model,
321
+ # route the validated model as a single positional arg.
322
+ if (
323
+ not is_pydantic
324
+ and isinstance(first_anno, str)
325
+ and action.param_model is not None
326
+ and first_anno == action.param_model.__name__
327
+ ):
328
+ is_pydantic = True
329
+ parameter_names = [param.name for param in parameters]
330
+
331
+ if execution_context.sensitive_data:
332
+ validated_params = self._replace_sensitive_data(validated_params, execution_context.sensitive_data)
333
+
334
+ # Check if action accepts execution_context parameter
335
+ if 'execution_context' in parameter_names:
336
+ # Modern approach: pass the whole context
337
+ extra_args = {'execution_context': execution_context}
338
+ else:
339
+ # Legacy approach: pass individual parameters
340
+ if 'browser' in parameter_names and not execution_context.browser_context:
341
+ raise ValueError(f'Action {action_name} requires browser but none provided.')
342
+ if 'page_extraction_llm' in parameter_names and not page_extraction_llm:
343
+ raise ValueError(f'Action {action_name} requires page_extraction_llm but none provided.')
344
+ if 'available_file_paths' in parameter_names and not execution_context.available_file_paths:
345
+ raise ValueError(f'Action {action_name} requires available_file_paths but none provided.')
346
+
347
+ # Prepare arguments based on parameter type
348
+ extra_args = {}
349
+ if 'browser' in parameter_names:
350
+ extra_args['browser'] = execution_context.browser_context
351
+ if 'page_extraction_llm' in parameter_names:
352
+ extra_args['page_extraction_llm'] = page_extraction_llm
353
+ if 'available_file_paths' in parameter_names:
354
+ extra_args['available_file_paths'] = execution_context.available_file_paths
355
+
356
+ # NOTE: Runtime tool mutation removed per upgrade instructions
357
+ # All per-execution data should flow via execution_context only
358
+ # Tools should not be mutated at runtime
359
+
360
+ # EXCEPTION: User context propagation for tools that need it
361
+ # This is NOT mutation - it's telling the tool which user's credentials to use
362
+ # Tools like PolymarketTool and MCPTool require user_id to load correct credentials
363
+ if execution_context and execution_context.user_id:
364
+ # Check if action function is a bound method with set_user_context
365
+ if hasattr(action.function, '__self__'):
366
+ tool_instance = action.function.__self__
367
+ if hasattr(tool_instance, 'set_user_context'):
368
+ tool_instance.set_user_context(execution_context.user_id)
369
+ self.logger.debug(f"Set user context '{execution_context.user_id}' on {action.tool or 'unknown'} tool")
370
+
371
+ # Execute the function and explicitly await the result
372
+ if is_pydantic:
373
+ result = await action.function(validated_params, **extra_args)
374
+ else:
375
+ result = await action.function(**validated_params.model_dump(), **extra_args)
376
+
377
+ # Handle the case where result might be another coroutine (nested async calls)
378
+ import inspect
379
+ if inspect.iscoroutine(result):
380
+ result = await result
381
+
382
+ return result
383
+
384
+ except Exception as e:
385
+ raise RuntimeError(f'Error executing action {action_name}: {str(e)}') from e
386
+
387
+ def _replace_sensitive_data(self, params: BaseModel, sensitive_data: Dict[str, str]) -> BaseModel:
388
+ """Replaces the sensitive data in the params"""
389
+ # if there are any str with <secret>placeholder</secret> in the params, replace them with the actual value from sensitive_data
390
+
391
+ import re
392
+
393
+ secret_pattern = re.compile(r'<secret>(.*?)</secret>')
394
+
395
+ def replace_secrets(value):
396
+ if isinstance(value, str):
397
+ matches = secret_pattern.findall(value)
398
+ for placeholder in matches:
399
+ if placeholder in sensitive_data:
400
+ value = value.replace(f'<secret>{placeholder}</secret>', sensitive_data[placeholder])
401
+ return value
402
+ elif isinstance(value, dict):
403
+ return {k: replace_secrets(v) for k, v in value.items()}
404
+ elif isinstance(value, list):
405
+ return [replace_secrets(v) for v in value]
406
+ return value
407
+
408
+ # Only write back fields where a secret was actually substituted. Writing the
409
+ # model_dump()'d value back for EVERY field coerces non-string field types
410
+ # (a nested BaseModel becomes a dict, a datetime a str, etc.) even when no
411
+ # secret was present.
412
+ for key, value in params.model_dump().items():
413
+ new_value = replace_secrets(value)
414
+ if new_value != value:
415
+ params.__dict__[key] = new_value
416
+ return params
417
+
418
+ def create_action_model(self) -> Type[ActionModel]:
419
+ """Creates a Pydantic model from registered actions"""
420
+ fields = {
421
+ name: (
422
+ Optional[action.param_model],
423
+ Field(default=None, description=action.description),
424
+ )
425
+ for name, action in self.registry.actions.items()
426
+ }
427
+
428
+ # Group actions by tool for better logging
429
+ actions_by_tool = {}
430
+ # Create thread-safe snapshot to avoid iterator corruption
431
+ for name, action in list(self.registry.actions.items()):
432
+ tool = action.tool or "default"
433
+ if tool not in actions_by_tool:
434
+ actions_by_tool[tool] = []
435
+ actions_by_tool[tool].append(name)
436
+
437
+ # Log actions by tool
438
+ for tool, actions in actions_by_tool.items():
439
+ self.logger.debug(f"Tool '{tool}' has {len(actions)} actions: {', '.join(actions[:5])}..." if len(actions) > 5 else f"Tool '{tool}' has {len(actions)} actions: {', '.join(actions)}")
440
+
441
+ # Prepare registered functions for telemetry
442
+ registered_functions = [
443
+ RegisteredFunction(
444
+ name=name,
445
+ params=action.param_model.model_json_schema(),
446
+ service=action.tool or "default" # SINGULAR: tool field only, no fallback
447
+ )
448
+ for name, action in self.registry.actions.items()
449
+ ]
450
+
451
+ self.telemetry.capture(
452
+ ControllerRegisteredFunctionsTelemetryEvent(
453
+ registered_functions=registered_functions
454
+ ),
455
+ session_id=self.session_id
456
+ )
457
+
458
+ return create_model('ActionModel', __base__=ActionModel, **fields) # type:ignore
459
+
460
+ def get_prompt_description(self) -> str:
461
+ """Get a description of all actions for the prompt"""
462
+ return self.registry.get_prompt_description()
463
+
464
+ def get_prompt_action_index(self) -> str:
465
+ """Compact one-line-per-action index for native tool mode (T1-03)."""
466
+ return self.registry.get_prompt_action_index()
467
+
468
+ def get_action_schema(self, action_name: str) -> Dict[str, Any]:
469
+ """Get the expected schema for a specific action
470
+
471
+ Args:
472
+ action_name: Name of the action
473
+
474
+ Returns:
475
+ Dictionary containing field information for the action
476
+ """
477
+ if action_name not in self.registry.actions:
478
+ return {}
479
+
480
+ action = self.registry.actions[action_name]
481
+ schema = {}
482
+
483
+ if action.param_model:
484
+ try:
485
+ for field_name, field_info in action.param_model.model_fields.items():
486
+ schema[field_name] = {
487
+ 'required': field_info.is_required(),
488
+ 'type': str(field_info.annotation)
489
+ }
490
+ except Exception:
491
+ pass
492
+
493
+ return schema
494
+
495
+ def get_actions(self) -> Dict[str, Any]:
496
+ """Get all registered actions with their details.
497
+
498
+ This is a stable public API method to access actions without
499
+ directly accessing internal registry structures.
500
+
501
+ Returns:
502
+ Dictionary mapping action names to their details
503
+ """
504
+ if hasattr(self.registry, 'actions'):
505
+ return dict(self.registry.actions)
506
+ return {}
507
+
508
+ def get_action_names(self) -> list[str]:
509
+ """Get list of all registered action names.
510
+
511
+ Returns:
512
+ List of action names
513
+ """
514
+ if hasattr(self.registry, 'actions'):
515
+ return list(self.registry.actions.keys())
516
+ return []
517
+
518
+ def get_action_count(self) -> int:
519
+ """Get the number of registered actions.
520
+
521
+ Returns:
522
+ Number of registered actions
523
+ """
524
+ if hasattr(self.registry, 'actions'):
525
+ return len(self.registry.actions)
526
+ return 0
527
+
528
+ def get_action_details(self, action_name: str) -> Optional[Any]:
529
+ """Get details for a specific action.
530
+
531
+ Args:
532
+ action_name: Name of the action to get details for
533
+
534
+ Returns:
535
+ Action details if found, None otherwise
536
+ """
537
+ if hasattr(self.registry, 'actions'):
538
+ return self.registry.actions.get(action_name)
539
+ return None
540
+
541
+ def has_action(self, action_name: str) -> bool:
542
+ """Check if an action is registered.
543
+
544
+ Args:
545
+ action_name: Name of the action to check
546
+
547
+ Returns:
548
+ True if action is registered, False otherwise
549
+ """
550
+ if hasattr(self.registry, 'actions'):
551
+ return action_name in self.registry.actions
552
+ return False
553
+
554
+ def get_actions_by_service(self, service_name: str) -> Dict[str, Any]:
555
+ """Get all actions for a specific tool/service.
556
+
557
+ Args:
558
+ service_name: Name of the tool/service
559
+
560
+ Returns:
561
+ Dictionary of actions belonging to the tool
562
+ """
563
+ if not hasattr(self.registry, 'actions'):
564
+ return {}
565
+
566
+ service_actions = {}
567
+ # Create thread-safe snapshot to avoid iterator corruption
568
+ for name, action in list(self.registry.actions.items()):
569
+ # Check both 'tool' and '_tool' attributes (tool is the primary attribute)
570
+ if ((hasattr(action, 'tool') and action.tool == service_name) or
571
+ (hasattr(action, '_tool') and action._tool == service_name)):
572
+ service_actions[name] = action
573
+ return service_actions
574
+
575
+ def get_tools(self) -> list[str]:
576
+ """Get list of all tools that have registered actions.
577
+
578
+ Returns:
579
+ List of unique tool names
580
+ """
581
+ if not hasattr(self.registry, 'actions'):
582
+ return []
583
+
584
+ tools = set()
585
+ for action in self.registry.actions.values():
586
+ if hasattr(action, 'tool') and action.tool:
587
+ tools.add(action.tool)
588
+ return list(tools)
589
+
590
+ # Keep deprecated alias for backward compatibility
591
+ def _register_default_actions(self) -> None:
592
+ """Register default actions if available.
593
+
594
+ Handles core actions like 'done' which are not tool-specific.
595
+ """
596
+ # NOTE: The 'done' action is now registered by the Controller
597
+ # which has more sophisticated logic including todo progress checking
598
+ # and quality gate enforcement. This method is kept for future
599
+ # default actions that might be needed at the registry level.
600
+ pass
601
+
602
+ def extract_tool_actions(self, tool_name: str, tool: Any) -> Dict[str, Callable]:
603
+ """Extract all available actions from a tool.
604
+
605
+ This is the public API for extracting actions. It returns action functions
606
+ with metadata attached (_tool, _service, _description, _param_model).
607
+ The returned actions are NOT yet registered - call wrap_function() to register.
608
+
609
+ Args:
610
+ tool_name: Name of the tool
611
+ tool: Tool instance
612
+
613
+ Returns:
614
+ Dictionary of action_name -> action_function
615
+ """
616
+ actions = {}
617
+
618
+ # Method 1: Check for get_actions method - preferred approach
619
+ if hasattr(tool, 'get_actions') and callable(getattr(tool, 'get_actions')):
620
+ try:
621
+ self.logger.debug(f"Attempting to get actions from {tool_name}.get_actions()")
622
+ service_actions = tool.get_actions()
623
+ if service_actions and isinstance(service_actions, dict):
624
+ # Normalize actions - some services return {name: function} and others
625
+ # return {name: {function: func, description: desc, param_model: model}}
626
+ for name, action in service_actions.items():
627
+ if callable(action):
628
+ # Simple action is directly callable
629
+ actions[name] = action
630
+ # Fix missing _tool attribute - check if it's a bound method first
631
+ try:
632
+ if not hasattr(action, '_tool') or action._tool is None:
633
+ self._set_tool_attribution(action, tool_name)
634
+ # Keep _service for backward compat
635
+ if not hasattr(action, '_service') or action._service is None:
636
+ action._service = tool_name
637
+ self.logger.debug(f"Applied tool name '{tool_name}' to action '{name}'")
638
+ except AttributeError:
639
+ # Can't set attributes on bound methods, skip
640
+ self.logger.debug(f"Skipping attribute setting for bound method '{name}' from tool '{tool_name}'")
641
+ elif isinstance(action, dict) and 'function' in action:
642
+ # Complex action has additional metadata
643
+ if callable(action['function']):
644
+ # Store the function with its metadata as attributes
645
+ func = action['function']
646
+
647
+ # Add description as attribute if available
648
+ if 'description' in action:
649
+ func._description = action['description']
650
+
651
+ # Add param_model as attribute if available
652
+ if 'param_model' in action:
653
+ func._param_model = action['param_model']
654
+
655
+ # Add tool name as attribute
656
+ self._set_tool_attribution(func, tool_name)
657
+ # Keep _service for backward compat
658
+ func._service = tool_name
659
+
660
+ # Store normalized action
661
+ actions[name] = func
662
+ else:
663
+ self.logger.warning(f"Action '{name}' from tool '{tool_name}' has non-callable function")
664
+ else:
665
+ self.logger.warning(f"Action '{name}' from tool '{tool_name}' has invalid format")
666
+ except Exception as e:
667
+ self.logger.warning(f"❌ Error calling get_actions() on tool '{tool_name}': {e}")
668
+ # Try the register_decorated_actions method as a fallback
669
+ if hasattr(tool, 'register_decorated_actions') and callable(tool.register_decorated_actions):
670
+ try:
671
+ self.logger.debug(f"Attempting to fix tool '{tool_name}' with register_decorated_actions")
672
+ tool.register_decorated_actions()
673
+ # Try get_actions again
674
+ tool_actions = tool.get_actions()
675
+ if tool_actions and isinstance(tool_actions, dict):
676
+ for name, action in tool_actions.items():
677
+ if callable(action):
678
+ actions[name] = action
679
+ if not hasattr(action, '_tool') and not hasattr(action, '_service'):
680
+ self._set_tool_attribution(action, tool_name)
681
+ elif isinstance(action, dict) and 'function' in action:
682
+ func = action['function']
683
+ self._set_tool_attribution(func, tool_name)
684
+ actions[name] = func
685
+ except Exception as e2:
686
+ self.logger.warning(f"Failed to use register_decorated_actions fallback for '{tool_name}': {e2}")
687
+
688
+ # Method 2: Check for actions attribute - fallback for legacy
689
+ if hasattr(tool, 'actions') and isinstance(tool.actions, dict):
690
+ for name, action in tool.actions.items():
691
+ if callable(action):
692
+ # Set required metadata if missing
693
+ if not hasattr(action, '_tool'):
694
+ self._set_tool_attribution(action, tool_name)
695
+ # Keep _service for backward compat
696
+ if not hasattr(action, '_service'):
697
+ action._service = tool_name
698
+ if not hasattr(action, '_description'):
699
+ action._description = f"Execute {name} from {tool_name} tool"
700
+ if not hasattr(action, '_param_model'):
701
+ try:
702
+ action._param_model = self._create_param_model(action)
703
+ except Exception:
704
+ continue # Skip if can't create param model
705
+
706
+ actions[name] = action
707
+ else:
708
+ self.logger.warning(f"Action '{name}' from tool '{tool_name}' is not callable")
709
+
710
+ # Method 3: Look for methods explicitly decorated with @action
711
+ # STRICT: Only accept methods with explicit action_info AND param_model
712
+ if not actions:
713
+ for attr_name in dir(tool):
714
+ # Skip private methods, properties and special methods
715
+ if attr_name.startswith('_') or attr_name in ('logger', 'config', 'container', 'status'):
716
+ continue
717
+
718
+ attr = getattr(tool, attr_name)
719
+
720
+ # STRICT: Only accept if explicitly marked as action with both action_info and param_model
721
+ if callable(attr) and not isinstance(attr, property) and hasattr(attr, 'action_info'):
722
+ action_info = attr.action_info
723
+
724
+ # STRICT: Require both description and param_model in action_info
725
+ if 'description' not in action_info:
726
+ self.logger.debug(f"Skipping {attr_name}: action_info missing description")
727
+ continue
728
+
729
+ if 'param_model' not in action_info:
730
+ self.logger.debug(f"Skipping {attr_name}: action_info missing param_model")
731
+ continue
732
+
733
+ # Add required metadata
734
+ self._set_tool_attribution(attr, tool_name)
735
+ attr._service = tool_name # Keep for backward compat
736
+ attr._description = action_info['description']
737
+ attr._param_model = action_info['param_model']
738
+
739
+ actions[attr_name] = attr
740
+
741
+ # Log found actions
742
+ if actions:
743
+ self.logger.debug(f"Found {len(actions)} actions for service '{tool_name}': {', '.join(list(actions.keys())[:5])}{'...' if len(actions) > 5 else ''}")
744
+ else:
745
+ self.logger.debug(f"No actions found for service '{tool_name}'")
746
+
747
+ return actions
748
+
749
+ def wrap_function(self, name: str, function: Callable, description: str, tool: Optional[str] = None, param_model: Optional[Type[BaseModel]] = None) -> None:
750
+ """Register a function as an action with the registry.
751
+
752
+ Args:
753
+ name: Name of the function
754
+ function: The function to register
755
+ description: Description of what the function does
756
+ tool: Optional tool name to attribute the action to
757
+ param_model: Optional parameter model to use instead of creating one
758
+ """
759
+ # Skip registration if action is in exclude_actions (defensive check)
760
+ if self.exclude_actions and name in self.exclude_actions:
761
+ self.logger.debug(f"Skipping excluded action: {name}")
762
+ return
763
+
764
+ # Check for collision with existing action
765
+ if name in self.registry.actions:
766
+ existing = self.registry.actions[name]
767
+ existing_tool = existing.tool if hasattr(existing, 'tool') else 'core'
768
+ new_tool = tool if tool else 'core'
769
+ if existing_tool != new_tool:
770
+ # Make cross-tool collisions a hard error to enforce proper namespacing
771
+ error_msg = (
772
+ f"Action name collision: '{name}' already registered by tool '{existing_tool}', "
773
+ f"cannot override with tool '{new_tool}'. Use namespaced action names (e.g., '{new_tool}_{name}')"
774
+ )
775
+ self.logger.error(error_msg)
776
+ if self.enforce_execution_context:
777
+ # In strict mode, raise an error
778
+ raise ValueError(error_msg)
779
+ else:
780
+ # In legacy mode, skip registration with warning
781
+ self.logger.warning(f"Skipping registration of '{name}' from tool '{new_tool}' due to collision")
782
+ return
783
+ else:
784
+ # Same tool re-registering - log at INFO level for visibility
785
+ # This can indicate unintentional duplicate registrations
786
+ self.logger.info(
787
+ f"Re-registering action '{name}' for tool '{new_tool}' "
788
+ f"(previous registration will be replaced)"
789
+ )
790
+ # Continue to update the registration
791
+
792
+ try:
793
+ # Skip trying to set attributes on bound methods
794
+ import inspect
795
+ if tool and not inspect.ismethod(function):
796
+ try:
797
+ if not hasattr(function, '_tool'):
798
+ self._set_tool_attribution(function, tool)
799
+ except AttributeError:
800
+ # Can't set attribute on this type of callable, skip
801
+ pass
802
+
803
+ # Handle param_model with controlled auto-generation
804
+ if param_model is not None:
805
+ # Use the provided param_model
806
+ pass
807
+ elif hasattr(function, '_param_model') and function._param_model is not None:
808
+ param_model = function._param_model
809
+ else:
810
+ # Auto-generate param_model with warning
811
+ self.logger.warning(
812
+ f"Action '{name}' missing param_model - auto-generating from function signature. "
813
+ f"Consider providing explicit param_model for better type safety."
814
+ )
815
+ try:
816
+ param_model = self._create_param_model(function)
817
+ # Cache on function for future use
818
+ if not inspect.ismethod(function):
819
+ try:
820
+ function._param_model = param_model
821
+ except AttributeError:
822
+ pass # Can't cache on this type
823
+ except Exception as e:
824
+ self.logger.error(f"Failed to auto-generate param_model for '{name}': {e}")
825
+ return # Fail if auto-generation fails
826
+
827
+ # Validate param_model is a proper Pydantic BaseModel subclass
828
+ from pydantic import BaseModel
829
+ if param_model is None or not isinstance(param_model, type) or not issubclass(param_model, BaseModel):
830
+ self.logger.error(f"Invalid param_model for action '{name}': must be a subclass of BaseModel")
831
+ return # Fail fast if param_model is invalid
832
+
833
+ # Wrap sync functions to make them async
834
+ if not iscoroutinefunction(function):
835
+ async def async_wrapper(*args, **kwargs):
836
+ return await asyncio.to_thread(function, *args, **kwargs)
837
+
838
+ # Copy the signature and other metadata
839
+ async_wrapper.__signature__ = signature(function)
840
+ async_wrapper.__name__ = function.__name__ if hasattr(function, '__name__') else name
841
+ async_wrapper.__doc__ = function.__doc__ if hasattr(function, '__doc__') and function.__doc__ else description
842
+
843
+ if hasattr(function, '__annotations__'):
844
+ async_wrapper.__annotations__ = function.__annotations__
845
+
846
+ # Copy important attributes for action registration
847
+ if hasattr(function, '_description'):
848
+ async_wrapper._description = function._description
849
+ else:
850
+ async_wrapper._description = description
851
+
852
+ if hasattr(function, '_param_model'):
853
+ async_wrapper._param_model = function._param_model
854
+ else:
855
+ async_wrapper._param_model = param_model
856
+
857
+ if hasattr(function, '_tool'):
858
+ async_wrapper._tool = function._tool
859
+ elif tool:
860
+ async_wrapper._tool = tool
861
+
862
+ wrapped_func = async_wrapper
863
+ else:
864
+ wrapped_func = function
865
+
866
+ # Set missing attributes on the function (skip for method objects)
867
+ import inspect
868
+ if not inspect.ismethod(wrapped_func):
869
+ try:
870
+ if not hasattr(wrapped_func, '_description'):
871
+ wrapped_func._description = description
872
+ if not hasattr(wrapped_func, '_param_model'):
873
+ wrapped_func._param_model = param_model
874
+ if not hasattr(wrapped_func, '_tool'):
875
+ self._set_tool_attribution(wrapped_func, tool)
876
+ except AttributeError:
877
+ # Can't set attributes on this callable, continue without them
878
+ pass
879
+
880
+ # Create the action registration
881
+ action = RegisteredAction(
882
+ name=name,
883
+ description=description,
884
+ function=wrapped_func,
885
+ param_model=param_model,
886
+ tool=tool # The tool this action belongs to (None for core actions)
887
+ )
888
+
889
+ # Check for collision with existing action
890
+ if name in self.registry.actions:
891
+ existing = self.registry.actions[name]
892
+ existing_tool = existing.tool if hasattr(existing, 'tool') else 'core'
893
+ new_tool = tool if tool else 'core'
894
+
895
+ if existing_tool != new_tool:
896
+ # Different tools trying to register same action name - this is a collision
897
+ raise ValueError(
898
+ f"Action name collision: '{name}' already registered by '{existing_tool}', "
899
+ f"cannot register for '{new_tool}'. Use namespaced names to avoid collisions."
900
+ )
901
+ # H5: same tool re-registering — actually REPLACE (the first collision
902
+ # check already logged "will be replaced"). The old code returned here,
903
+ # silently discarding the rebuilt action and leaving the stale closure
904
+ # bound to the previous tool instance. Bust the schema cache so the
905
+ # replacement is re-emitted.
906
+ self._provider_schema_cache.clear()
907
+
908
+ # Register with the registry
909
+ self.registry.actions[name] = action
910
+
911
+ # Log action registration - only if not already logged
912
+ log_key = f"{tool}:{name}" if tool else name
913
+ if log_key not in self._action_registration_logged:
914
+ self._action_registration_logged.add(log_key)
915
+ if tool:
916
+ self.logger.debug(f"Registered action '{name}' from tool '{tool}'")
917
+ else:
918
+ self.logger.debug(f"Registered action '{name}'")
919
+ except Exception as e:
920
+ self.logger.warning(f"Failed to register action '{name}': {str(e)}", exc_info=True)
921
+
922
+ # NOTE: fix_missing_action_descriptions() removed per upgrade instructions
923
+ # Metadata repairs should require explicit @action/metadata; no auto-repair scans
924
+
925
+
926
+ def get_all_actions_for_provider(self, provider: str) -> List[Dict[str, Any]]:
927
+ """Generate provider-specific schemas for ALL actions (core + tools)
928
+
929
+ This is what the LLM actually uses - both core actions and tool actions.
930
+
931
+ Args:
932
+ provider: The LLM provider name (openai, anthropic, gemini, etc.)
933
+
934
+ Returns:
935
+ List of all action schemas in the provider's expected format
936
+ """
937
+ # Memoization: if the registered-action set and exclusions are unchanged,
938
+ # return the previously generated schema list for this provider. The cache
939
+ # key embeds the full action-name set so any registration/removal busts it.
940
+ cache_key = (
941
+ provider,
942
+ frozenset(self.registry.actions.keys()),
943
+ frozenset(self.exclude_actions),
944
+ )
945
+ cached = self._provider_schema_cache.get(cache_key)
946
+ if cached is not None:
947
+ return cached
948
+
949
+ # Get all non-excluded actions, deduplicated by object identity
950
+ # This is necessary because create_alias() adds the same RegisteredAction
951
+ # under multiple keys (the original name and the alias name).
952
+ # Without deduplication, we'd generate duplicate schemas.
953
+ seen_actions = set() # Track by object id
954
+ all_actions = []
955
+
956
+ for name, action in self.registry.actions.items():
957
+ if name in self.exclude_actions:
958
+ continue
959
+ # Deduplicate by RegisteredAction object identity
960
+ action_id = id(action)
961
+ if action_id in seen_actions:
962
+ self.logger.debug(f"Skipping alias '{name}' -> '{action.name}' (already included)")
963
+ continue
964
+ seen_actions.add(action_id)
965
+ all_actions.append(action)
966
+
967
+ if not all_actions:
968
+ self.logger.debug(f"No actions available for provider {provider}")
969
+ return []
970
+
971
+ # Get the appropriate schema generator
972
+ generator = get_schema_generator(provider)
973
+
974
+ # Generate and return all action schemas
975
+ actions = generator.generate_tools_list(all_actions)
976
+
977
+ # UP-10 2.5: sanitize the emitted tools list for broad backend compatibility
978
+ # (nullable-union collapse, $ref-sibling strip, top-level combinator strip,
979
+ # bare-string/array-type normalization). Runs BEFORE the cache write and is
980
+ # composed downstream of DROP_TOOL (sanitize first => fewer tools dropped).
981
+ # Gated TOOL_SCHEMA_SANITIZE (default on; =false restores pre-port bytes).
982
+ if os.getenv("TOOL_SCHEMA_SANITIZE", "true").lower() != "false":
983
+ try:
984
+ from tools.controller.registry.schema_sanitizer import sanitize_emitted_tools
985
+ actions = sanitize_emitted_tools(actions, provider)
986
+ except Exception as e:
987
+ self.logger.warning(f"schema sanitizer skipped (fail-open): {e}")
988
+
989
+ # Log breakdown of action types
990
+ core_count = sum(1 for a in all_actions if a.tool is None)
991
+ tool_count = sum(1 for a in all_actions if a.tool is not None)
992
+ alias_count = len(self.registry.actions) - len(all_actions)
993
+ self.logger.debug(
994
+ f"Generated {len(actions)} action schemas for provider {provider} "
995
+ f"({core_count} core, {tool_count} tool, {alias_count} aliases deduplicated)"
996
+ )
997
+
998
+ self._provider_schema_cache[cache_key] = actions
999
+ return actions
1000
+
1001
+ # Convenience API methods for accessing registry
1002
+ def get_action(self, name: str) -> Optional[RegisteredAction]:
1003
+ """Get a registered action by name.
1004
+
1005
+ Args:
1006
+ name: The action name
1007
+
1008
+ Returns:
1009
+ RegisteredAction if found, None otherwise
1010
+ """
1011
+ return self.registry.actions.get(name)
1012
+
1013
+ def create_alias(self, alias_name: str, target_name: str) -> bool:
1014
+ """Create a thread-safe alias for an existing action.
1015
+
1016
+ Args:
1017
+ alias_name: New name for the alias
1018
+ target_name: Existing action name to alias
1019
+
1020
+ Returns:
1021
+ True if alias created, False if target doesn't exist
1022
+ """
1023
+ with self._registry_lock:
1024
+ # Check if target exists
1025
+ if target_name not in self.registry.actions:
1026
+ return False
1027
+
1028
+ # Create alias by copying the RegisteredAction reference
1029
+ self.registry.actions[alias_name] = self.registry.actions[target_name]
1030
+ return True
1031
+
1032
+ def list_action_names(self) -> List[str]:
1033
+ """Get list of all registered action names.
1034
+
1035
+ Returns:
1036
+ List of action names
1037
+ """
1038
+ return list(self.registry.actions.keys())
1039
+
1040
+ def list_actions(self) -> List[str]:
1041
+ """Get list of all registered action names.
1042
+
1043
+ Alias for list_action_names().
1044
+ """
1045
+ return self.list_action_names()
1046
+
1047
+ def get_actions_by_tool(self, tool: str) -> List[RegisteredAction]:
1048
+ """Get all actions registered for a specific tool.
1049
+
1050
+ Args:
1051
+ tool: The tool name
1052
+
1053
+ Returns:
1054
+ List of RegisteredAction instances for this tool
1055
+ """
1056
+ return [
1057
+ action for action in self.registry.actions.values()
1058
+ if action.tool == tool
1059
+ ]
1060
+
1061
+ def supports_native_tools(self, provider: str) -> bool:
1062
+ """Check if a provider supports native tool/function calling
1063
+
1064
+ Args:
1065
+ provider: The LLM provider name
1066
+
1067
+ Returns:
1068
+ True if the provider has native tool support
1069
+ """
1070
+ provider_lower = provider.lower()
1071
+
1072
+ # Providers with native tool support
1073
+ # DeepSeek V3+ supports OpenAI-compatible function calling
1074
+ # Docs: https://api-docs.deepseek.com/guides/function_calling
1075
+ # OpenRouter models support native tool calling via OpenAI-compatible API
1076
+ native_providers = ["openai", "anthropic", "gemini", "google", "deepseek", "openrouter", "nvidia"]
1077
+
1078
+ return any(p in provider_lower for p in native_providers)
1079
+
1080
+ def tool_call_to_action(self, tool_name: str, args: Dict[str, Any]) -> ActionModel:
1081
+ """Convert a single tool call to an ActionModel (simplified single validation path)
1082
+
1083
+ Args:
1084
+ tool_name: Name of the tool/action
1085
+ args: Arguments for the tool call
1086
+
1087
+ Returns:
1088
+ ActionModel instance ready for execution
1089
+
1090
+ Raises:
1091
+ ValueError: If the tool is not registered or validation fails
1092
+ """
1093
+ # 1. Get action from registry (with fuzzy match)
1094
+ action = self._get_action_with_fuzzy_match(tool_name)
1095
+
1096
+ # 2. Validate parameters directly
1097
+ try:
1098
+ if action.param_model:
1099
+ validated_args = action.param_model(**args)
1100
+ args = validated_args.model_dump()
1101
+ except Exception as validation_error:
1102
+ # SINGULAR RESPONSIBILITY: If validation fails, caller did not normalize properly
1103
+ # Do NOT hide errors with fallback corrections - fail fast
1104
+ self.logger.error(
1105
+ f"Validation failed for {tool_name}. "
1106
+ f"Tool calls MUST be normalized with normalize_and_correct() before validation. "
1107
+ f"Args: {args}, Error: {validation_error}"
1108
+ )
1109
+ # P3: the raised message is what the LLM sees — keep it actionable (the field
1110
+ # error + params are appended by the caller). The internal
1111
+ # "normalize_and_correct()" hint is dev-only noise for the model, so it stays
1112
+ # in the logger.error above, not in the model-facing error.
1113
+ raise ValueError(
1114
+ f"Invalid arguments for {tool_name}: {validation_error}."
1115
+ )
1116
+
1117
+ # 4. Create ActionModel — key by the RESOLVED registered action name, not the
1118
+ # caller's (possibly fuzzy-matched) tool_name. Keying by the unresolved name
1119
+ # builds an ActionModel with a key that matches no field, which ActionModel
1120
+ # silently drops → an empty no-op action (the fuzzy match would validate args
1121
+ # but then execute nothing).
1122
+ resolved_name = getattr(action, "name", tool_name) or tool_name
1123
+ action_data = {resolved_name: args if args is not None else {}}
1124
+ action_model_class = self.create_action_model()
1125
+ return action_model_class(**action_data)
1126
+
1127
+ def _get_action_with_fuzzy_match(self, tool_name: str) -> RegisteredAction:
1128
+ """Get action from registry with fuzzy matching fallback."""
1129
+ # First try exact match
1130
+ if tool_name in self.registry.actions:
1131
+ return self.registry.actions[tool_name]
1132
+
1133
+ # Try fuzzy matching - look for actions that end with the tool name
1134
+ fuzzy_matches = [
1135
+ action_name for action_name in self.registry.actions.keys()
1136
+ if action_name.endswith(f"_{tool_name}") or action_name == tool_name
1137
+ ]
1138
+
1139
+ if fuzzy_matches:
1140
+ tool_name = fuzzy_matches[0]
1141
+ self.logger.info(f"Fuzzy matched to '{tool_name}'")
1142
+ return self.registry.actions[tool_name]
1143
+
1144
+ # Not found - provide helpful error
1145
+ available = list(self.registry.actions.keys())[:10]
1146
+ available_str = ", ".join(available)
1147
+ if len(self.registry.actions) > 10:
1148
+ available_str += f", ... ({len(self.registry.actions)} total)"
1149
+
1150
+ similar = [
1151
+ name for name in self.registry.actions.keys()
1152
+ if tool_name in name or name.split('_')[-1] == tool_name.split('_')[-1]
1153
+ ]
1154
+ error_msg = f"Unknown tool: '{tool_name}'. Available: {available_str}."
1155
+ if similar:
1156
+ error_msg += f" Similar: {similar[:5]}"
1157
+
1158
+ raise ValueError(error_msg)
1159
+
1160
+
1161
+ def tool_calls_to_actions(self, tool_calls: List[Dict[str, Any]]) -> List[ActionModel]:
1162
+ """Convert multiple tool calls to ActionModels
1163
+
1164
+ Args:
1165
+ tool_calls: List of tool calls, each with 'name' or 'function.name' and 'arguments'
1166
+
1167
+ Returns:
1168
+ List of ActionModel instances ready for execution
1169
+
1170
+ Note:
1171
+ Failed validations are tracked in self._last_validation_errors as a dict mapping
1172
+ tool_call_id -> error_message. Callers can check this to provide error responses.
1173
+ """
1174
+ actions = []
1175
+ # Track validation errors for caller to handle (keyed by tool_call_id)
1176
+ self._last_validation_errors: Dict[str, str] = {}
1177
+
1178
+ for call in tool_calls:
1179
+ tool_name = call.get('name', 'unknown')
1180
+ tool_call_id = call.get('id')
1181
+
1182
+ try:
1183
+ args = call.get('args', {})
1184
+
1185
+ if not tool_name or tool_name == 'unknown':
1186
+ error_msg = f"Tool call missing 'name': {call}"
1187
+ self.logger.warning(error_msg)
1188
+ if tool_call_id:
1189
+ self._last_validation_errors[tool_call_id] = error_msg
1190
+ continue
1191
+
1192
+ # Ensure args is a dict (should already be from normalization)
1193
+ if isinstance(args, str):
1194
+ import json
1195
+ try:
1196
+ args = json.loads(args) if args else {}
1197
+ except json.JSONDecodeError:
1198
+ error_msg = f"Invalid JSON arguments for {tool_name}: {args}"
1199
+ self.logger.warning(error_msg)
1200
+ if tool_call_id:
1201
+ self._last_validation_errors[tool_call_id] = error_msg
1202
+ args = {}
1203
+
1204
+ action = self.tool_call_to_action(tool_name, args)
1205
+ if tool_call_id:
1206
+ action._tool_call_id = tool_call_id
1207
+ actions.append(action)
1208
+
1209
+ except ValueError as e:
1210
+ error_str = str(e)
1211
+
1212
+ # FIXED: Differentiate between "not found" and "validation failed" errors
1213
+ if "Unknown tool" in error_str or "not found" in error_str.lower():
1214
+ self.logger.error(f"❌ Tool '{tool_name}' not found in registry!")
1215
+ self.logger.error(f" Available tools (first 30): {list(self.registry.actions.keys())[:30]}")
1216
+ error_msg = f"Tool '{tool_name}' not found. Check available tools with mcp_list_tools."
1217
+ else:
1218
+ # Validation failed - tool exists but args are wrong
1219
+ self.logger.error(f"❌ Validation failed for tool '{tool_name}'")
1220
+ self.logger.error(f" Error: {e}")
1221
+ error_msg = f"Validation failed for '{tool_name}': {error_str}"
1222
+
1223
+ # Show expected schema for the tool
1224
+ if tool_name in self.registry.actions:
1225
+ action_info = self.registry.actions[tool_name]
1226
+ if action_info.param_model:
1227
+ try:
1228
+ schema = action_info.param_model.model_json_schema()
1229
+ required = schema.get('required', [])
1230
+ props = list(schema.get('properties', {}).keys())
1231
+ self.logger.error(f" Required params: {required}")
1232
+ self.logger.error(f" All params: {props}")
1233
+ error_msg += f" Required: {required}. All params: {props}."
1234
+ except Exception:
1235
+ pass
1236
+
1237
+ # FIXED: Track error for caller instead of appending None
1238
+ if tool_call_id:
1239
+ self._last_validation_errors[tool_call_id] = error_msg
1240
+
1241
+ self.logger.warning(f" Skipping action '{tool_name}' due to error")
1242
+ continue
1243
+
1244
+ except Exception as e:
1245
+ error_msg = f"Failed to convert tool call {tool_name}: {e}"
1246
+ self.logger.error(error_msg)
1247
+ if tool_call_id:
1248
+ self._last_validation_errors[tool_call_id] = error_msg
1249
+ continue
1250
+
1251
+ # Log summary of validation errors
1252
+ if self._last_validation_errors:
1253
+ self.logger.warning(
1254
+ f"⚠️ {len(self._last_validation_errors)} tool calls failed validation: "
1255
+ f"{list(self._last_validation_errors.keys())}"
1256
+ )
1257
+
1258
+ return actions
1259
+
1260
+ def get_last_validation_errors(self) -> Dict[str, str]:
1261
+ """Get validation errors from the last tool_calls_to_actions call.
1262
+
1263
+ Returns:
1264
+ Dict mapping tool_call_id -> error_message for failed validations
1265
+ """
1266
+ return getattr(self, '_last_validation_errors', {})