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
agents/task/utils.py ADDED
@@ -0,0 +1,762 @@
1
+ """
2
+ Utility functions for the task package.
3
+
4
+ Note: All session directory and path management is now handled through
5
+ the centralized path management system in agents.task.path:
6
+
7
+ from agents.task.path import pm
8
+
9
+ # Examples:
10
+ clean_id = pm().clean_session_id(session_id)
11
+ workspace_dir = pm().get_workspace_dir(session_id)
12
+ file_path = pm().create_file_path(session_id, "workspace", "output.json")
13
+ """
14
+
15
+ import logging
16
+ import time
17
+ import json
18
+ from typing import Any, Callable, Coroutine, Dict, Optional, ParamSpec, TypeVar, Union, Generic, List
19
+ import os
20
+ from pathlib import Path
21
+ import threading
22
+ import warnings
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ def extract_session_from_agent_id(agent_id: str) -> Optional[str]:
27
+ """Extract session ID from agent_id format: name_sessionid
28
+
29
+ This utility consolidates duplicate logic found in 5+ places across the codebase.
30
+ Agent IDs follow the format "agent_name_session_id" where the session ID is
31
+ everything after the first underscore.
32
+
33
+ Args:
34
+ agent_id: Agent identifier in format "name_sessionid"
35
+
36
+ Returns:
37
+ Cleaned session ID or None if agent_id is invalid
38
+
39
+ Examples:
40
+ >>> extract_session_from_agent_id("agent_abc123")
41
+ "abc123"
42
+ >>> extract_session_from_agent_id("task_user_session_xyz")
43
+ "user_session_xyz"
44
+ >>> extract_session_from_agent_id("invalid")
45
+ None
46
+ """
47
+ from agents.task.path import pm
48
+
49
+ if not agent_id or '_' not in agent_id:
50
+ return None
51
+
52
+ # Split only on first underscore to preserve underscores in session_id
53
+ session_id = agent_id.split('_', 1)[1]
54
+ return pm().clean_session_id(session_id)
55
+
56
+ def fix_schema_for_provider(schema: Dict[str, Any], provider: str) -> Dict[str, Any]:
57
+ """Enforce additionalProperties: false recursively for LLM provider compatibility.
58
+
59
+ Both OpenAI and Anthropic require all object-typed nodes to carry
60
+ ``additionalProperties: false``; the only exception is when it is explicitly
61
+ set to ``True`` (indicating a ``Dict[str, Any]`` that must accept arbitrary keys,
62
+ e.g. MCP tool arguments). All other providers currently get the same treatment.
63
+
64
+ Mutates *schema* in place and also returns it (matches the prior contract of
65
+ fix_openai_schema / fix_anthropic_schema so existing callers are unaffected).
66
+
67
+ Args:
68
+ schema: JSON schema fragment to fix (mutated in place).
69
+ provider: Provider name string (e.g. "openai", "anthropic"). Currently
70
+ unused because both providers need the same normalisation, but kept for
71
+ future per-provider branching without changing the call sites.
72
+
73
+ Returns:
74
+ The same schema dict, normalised.
75
+ """
76
+ if isinstance(schema, dict):
77
+ # Enforce additionalProperties: false on object-typed nodes, UNLESS the
78
+ # caller explicitly opted in to True (Dict[str, Any] / open MCP args).
79
+ if schema.get('type') == 'object':
80
+ if schema.get('additionalProperties') is not True:
81
+ schema['additionalProperties'] = False
82
+
83
+ # Recurse into all nested schema locations.
84
+ for key, value in schema.items():
85
+ if key == 'properties' and isinstance(value, dict):
86
+ for prop_name, prop_schema in value.items():
87
+ schema['properties'][prop_name] = fix_schema_for_provider(prop_schema, provider)
88
+ elif key == 'items' and isinstance(value, dict):
89
+ schema['items'] = fix_schema_for_provider(value, provider)
90
+ elif key == 'definitions' and isinstance(value, dict):
91
+ for def_name, def_schema in value.items():
92
+ schema['definitions'][def_name] = fix_schema_for_provider(def_schema, provider)
93
+ elif key == '$defs' and isinstance(value, dict):
94
+ for def_name, def_schema in value.items():
95
+ schema['$defs'][def_name] = fix_schema_for_provider(def_schema, provider)
96
+ elif key == 'anyOf' and isinstance(value, list):
97
+ schema['anyOf'] = [fix_schema_for_provider(s, provider) for s in value]
98
+ elif key == 'oneOf' and isinstance(value, list):
99
+ schema['oneOf'] = [fix_schema_for_provider(s, provider) for s in value]
100
+ elif key == 'allOf' and isinstance(value, list):
101
+ schema['allOf'] = [fix_schema_for_provider(s, provider) for s in value]
102
+ elif isinstance(value, dict):
103
+ schema[key] = fix_schema_for_provider(value, provider)
104
+
105
+ return schema
106
+
107
+
108
+ def fix_openai_schema(schema: Dict[str, Any]) -> Dict[str, Any]:
109
+ """Backward-compat wrapper — delegates to fix_schema_for_provider."""
110
+ return fix_schema_for_provider(schema, "openai")
111
+
112
+
113
+ def fix_anthropic_schema(schema: Dict[str, Any]) -> Dict[str, Any]:
114
+ """Backward-compat wrapper — delegates to fix_schema_for_provider."""
115
+ return fix_schema_for_provider(schema, "anthropic")
116
+
117
+ def detect_llm_provider(result: Any, model_name: Optional[str] = None) -> str:
118
+ """Detect LLM provider from model name using the model_registry.
119
+
120
+ Args:
121
+ result: LLM response object (not used, kept for compatibility)
122
+ model_name: Model name for detection
123
+
124
+ Returns:
125
+ Provider name ('openai', 'anthropic', 'gemini', 'deepseek', 'openrouter', 'llama', or 'generic')
126
+ """
127
+ if not model_name:
128
+ return 'generic'
129
+
130
+ try:
131
+ # Use model_registry as the SINGLE SOURCE OF TRUTH (WS-2.3): the canonical
132
+ # enum→string map lives there, so a new provider is added in one place and
133
+ # GOOGLE→'gemini' can never drift back to 'google' here.
134
+ from modules.llm.model_registry import get_model_config, canonical_provider_name
135
+
136
+ model_config = get_model_config(model_name)
137
+ if model_config and model_config.provider:
138
+ return canonical_provider_name(model_config.provider, default='generic')
139
+
140
+ except ImportError:
141
+ logger.debug("model_registry not available for provider detection")
142
+
143
+ return 'generic'
144
+
145
+ def extract_token_usage(result: Any, provider: str) -> Dict[str, Optional[int]]:
146
+ """Extract token usage from LLM response using provider-specific paths.
147
+
148
+ Args:
149
+ result: LLM response object (could be raw LLM response or structured output)
150
+ provider: Provider name from detect_llm_provider
151
+
152
+ Returns:
153
+ Dictionary with token counts (total_tokens, prompt_tokens, completion_tokens, cached_tokens)
154
+ """
155
+ token_usage = {'total_tokens': None, 'prompt_tokens': None, 'completion_tokens': None, 'cached_tokens': None, 'cache_creation_tokens': None}
156
+
157
+ try:
158
+ # Handle structured output format: {'parsed': ..., 'raw': <llm_response>}
159
+ actual_result = result
160
+ if isinstance(result, dict) and 'raw' in result:
161
+ # Structured output - extract the raw LLM response
162
+ actual_result = result['raw']
163
+
164
+ # Try AIMessage format first (usage_metadata attribute)
165
+ if hasattr(actual_result, 'usage_metadata') and actual_result.usage_metadata:
166
+ usage = actual_result.usage_metadata
167
+ # Handle both dict and object formats (adapters may pass dict or object)
168
+ if isinstance(usage, dict):
169
+ # Dict format (from our adapters)
170
+ token_usage['prompt_tokens'] = (
171
+ usage.get('input_tokens') or
172
+ usage.get('prompt_tokens')
173
+ )
174
+ token_usage['completion_tokens'] = (
175
+ usage.get('output_tokens') or
176
+ usage.get('completion_tokens')
177
+ )
178
+ token_usage['total_tokens'] = usage.get('total_tokens')
179
+ token_usage['cached_tokens'] = usage.get('cache_read_input_tokens') or usage.get('cached_tokens')
180
+ token_usage['cache_creation_tokens'] = usage.get('cache_creation_input_tokens')
181
+ else:
182
+ # Object format
183
+ token_usage['prompt_tokens'] = (
184
+ getattr(usage, 'input_tokens', None) or
185
+ getattr(usage, 'prompt_tokens', None)
186
+ )
187
+ token_usage['completion_tokens'] = (
188
+ getattr(usage, 'output_tokens', None) or
189
+ getattr(usage, 'completion_tokens', None)
190
+ )
191
+ token_usage['cached_tokens'] = getattr(usage, 'cache_read_input_tokens', None)
192
+ # Calculate total if not already set
193
+ if token_usage['total_tokens'] is None and token_usage['prompt_tokens'] and token_usage['completion_tokens']:
194
+ token_usage['total_tokens'] = token_usage['prompt_tokens'] + token_usage['completion_tokens']
195
+
196
+ # Try response_metadata format
197
+ elif hasattr(actual_result, 'response_metadata'):
198
+ metadata = actual_result.response_metadata
199
+ if 'token_usage' in metadata:
200
+ usage = metadata['token_usage']
201
+ token_usage['prompt_tokens'] = usage.get('prompt_tokens')
202
+ token_usage['completion_tokens'] = usage.get('completion_tokens')
203
+ token_usage['total_tokens'] = usage.get('total_tokens')
204
+ token_usage['cached_tokens'] = usage.get('cached_tokens')
205
+
206
+ # Direct attribute access based on known provider patterns
207
+ elif provider == 'openai':
208
+ # OpenAI format: result.usage.{total_tokens, prompt_tokens, completion_tokens}
209
+ if hasattr(actual_result, 'usage') and actual_result.usage:
210
+ usage = actual_result.usage
211
+ token_usage['total_tokens'] = getattr(usage, 'total_tokens', None)
212
+ token_usage['prompt_tokens'] = getattr(usage, 'prompt_tokens', None)
213
+ token_usage['completion_tokens'] = getattr(usage, 'completion_tokens', None)
214
+ token_usage['cached_tokens'] = getattr(usage, 'cached_tokens', None)
215
+
216
+ elif provider == 'anthropic':
217
+ # Anthropic format: result.usage.{input_tokens, output_tokens}
218
+ if hasattr(actual_result, 'usage') and actual_result.usage:
219
+ usage = actual_result.usage
220
+ token_usage['prompt_tokens'] = getattr(usage, 'input_tokens', None)
221
+ token_usage['completion_tokens'] = getattr(usage, 'output_tokens', None)
222
+ token_usage['cached_tokens'] = getattr(usage, 'cache_read_input_tokens', None)
223
+ # Sometimes Anthropic puts these directly on result
224
+ elif hasattr(actual_result, 'input_tokens'):
225
+ token_usage['prompt_tokens'] = getattr(actual_result, 'input_tokens', None)
226
+ token_usage['completion_tokens'] = getattr(actual_result, 'output_tokens', None)
227
+
228
+ else:
229
+ # Generic fallback - try multiple common patterns
230
+ if hasattr(actual_result, 'usage') and actual_result.usage:
231
+ usage = actual_result.usage
232
+ # Try OpenAI pattern first
233
+ token_usage['total_tokens'] = getattr(usage, 'total_tokens', None)
234
+ token_usage['prompt_tokens'] = getattr(usage, 'prompt_tokens', None)
235
+ token_usage['completion_tokens'] = getattr(usage, 'completion_tokens', None)
236
+ token_usage['cached_tokens'] = getattr(usage, 'cached_tokens', None) or getattr(usage, 'cache_read_input_tokens', None)
237
+ # Try Anthropic pattern if OpenAI didn't work
238
+ if not token_usage['prompt_tokens']:
239
+ token_usage['prompt_tokens'] = getattr(usage, 'input_tokens', None)
240
+ if not token_usage['completion_tokens']:
241
+ token_usage['completion_tokens'] = getattr(usage, 'output_tokens', None)
242
+
243
+ # Calculate total if we have prompt + completion but no total
244
+ if (token_usage['total_tokens'] is None and
245
+ token_usage['prompt_tokens'] is not None and
246
+ token_usage['completion_tokens'] is not None):
247
+ token_usage['total_tokens'] = token_usage['prompt_tokens'] + token_usage['completion_tokens']
248
+
249
+ # Ensure all values are proper integers or None
250
+ for key in token_usage:
251
+ if token_usage[key] is not None:
252
+ try:
253
+ token_usage[key] = int(token_usage[key])
254
+ except (ValueError, TypeError):
255
+ token_usage[key] = None
256
+
257
+ except Exception as e:
258
+ logging.getLogger('task.utils').debug(f"Error extracting token usage: {e}")
259
+
260
+ return token_usage
261
+
262
+
263
+ # Define generic type variables for return type and parameters
264
+ R = TypeVar('R')
265
+ P = ParamSpec('P')
266
+ T = TypeVar('T')
267
+
268
+
269
+ def handle_with_fallback(
270
+ primary_func: Callable[[], T],
271
+ fallback_func: Callable[[], T],
272
+ logger: logging.Logger,
273
+ error_msg: str
274
+ ) -> T:
275
+ """Standard error handling with fallback.
276
+
277
+ Args:
278
+ primary_func: Primary function to try first
279
+ fallback_func: Fallback function to use if primary fails
280
+ logger: Logger to use for error messages
281
+ error_msg: Message to log on error
282
+
283
+ Returns:
284
+ Result from either primary or fallback function
285
+ """
286
+ try:
287
+ return primary_func()
288
+ except Exception as e:
289
+ logger.warning(f"{error_msg}: {e}")
290
+ return fallback_func()
291
+
292
+ def safe_operation(
293
+ func: Callable[[], T],
294
+ logger: logging.Logger,
295
+ error_msg: str,
296
+ default_value: Optional[T] = None
297
+ ) -> Optional[T]:
298
+ """Execute operation safely, returning default value on failure.
299
+
300
+ Args:
301
+ func: Function to execute
302
+ logger: Logger to use for error messages
303
+ error_msg: Message to log on error
304
+ default_value: Value to return if function fails
305
+
306
+ Returns:
307
+ Result from function or default value on error
308
+ """
309
+ try:
310
+ return func()
311
+ except Exception as e:
312
+ logger.error(f"{error_msg}: {e}")
313
+ return default_value
314
+
315
+
316
+ # Timing decorators have been moved to utils/time_utils.py
317
+ # Import from there for backwards compatibility
318
+ from utils.time_utils import time_execution_sync, time_execution_async
319
+
320
+
321
+ def log_llm_request(component: str, purpose: Optional[str] = None) -> Callable:
322
+ """
323
+ Decorator to log and track LLM requests with telemetry.
324
+
325
+ This decorator provides consistent logging and telemetry capture for LLM operations.
326
+ Used primarily in tests and theatre scripts for tracking.
327
+
328
+ Args:
329
+ component: Component name making the request
330
+ purpose: Purpose of the request (optional)
331
+
332
+ Returns:
333
+ Decorated function with logging and telemetry
334
+ """
335
+ import asyncio
336
+ import time
337
+ import logging
338
+ from functools import wraps
339
+
340
+ logger = logging.getLogger(__name__)
341
+
342
+ def decorator(func: Callable) -> Callable:
343
+ @wraps(func)
344
+ async def async_wrapper(*args, **kwargs):
345
+ start_time = time.time()
346
+
347
+ # Extract LLM info
348
+ llm = kwargs.get('llm') or (args[1] if len(args) > 1 else None)
349
+ agent = args[0] if args else None
350
+
351
+ model_name = getattr(llm, 'model_name', 'unknown') if llm else 'unknown'
352
+
353
+ # Log the request
354
+ purpose_str = f"/{purpose}" if purpose else ""
355
+ logger.info(f"🔄 LLM REQUEST [{component}{purpose_str}] - Model: {model_name}")
356
+
357
+ try:
358
+ # Execute the function
359
+ result = await func(*args, **kwargs)
360
+
361
+ # Extract token usage if available
362
+ if result and isinstance(result, dict):
363
+ raw = result.get('raw')
364
+ if raw and hasattr(raw, 'usage'):
365
+ usage = raw.usage
366
+ if usage:
367
+ prompt_tokens = getattr(usage, 'prompt_tokens', 0)
368
+ completion_tokens = getattr(usage, 'completion_tokens', 0)
369
+ total_tokens = getattr(usage, 'total_tokens', 0)
370
+
371
+ duration = time.time() - start_time
372
+ logger.info(f"Token usage - Prompt: {prompt_tokens}, Completion: {completion_tokens}, Total: {total_tokens}")
373
+ logger.info(f"Completed in {duration:.2f}s")
374
+
375
+ # Capture telemetry
376
+ try:
377
+ from agents.task import capture_llm_request
378
+ session_id = getattr(agent, 'agent_id', None) if agent else None
379
+
380
+ capture_llm_request(
381
+ component=component,
382
+ purpose=purpose,
383
+ model_name=model_name,
384
+ duration_seconds=duration,
385
+ success=True,
386
+ prompt_tokens=prompt_tokens,
387
+ completion_tokens=completion_tokens,
388
+ total_tokens=total_tokens,
389
+ session_id=session_id,
390
+ temperature=getattr(llm, 'temperature', None) if llm else None,
391
+ max_tokens=getattr(llm, 'max_tokens', None) if llm else None
392
+ )
393
+ except Exception as e:
394
+ logger.debug(f"Failed to capture telemetry: {e}")
395
+
396
+ return result
397
+
398
+ except Exception as e:
399
+ duration = time.time() - start_time
400
+ logger.error(f"LLM request failed after {duration:.2f}s: {e}")
401
+
402
+ # Capture telemetry for failure
403
+ try:
404
+ from agents.task import capture_llm_request
405
+ session_id = getattr(agent, 'agent_id', None) if agent else None
406
+
407
+ capture_llm_request(
408
+ component=component,
409
+ purpose=purpose,
410
+ model_name=model_name,
411
+ duration_seconds=duration,
412
+ success=False,
413
+ session_id=session_id
414
+ )
415
+ except Exception:
416
+ pass
417
+
418
+ raise
419
+
420
+ @wraps(func)
421
+ def sync_wrapper(*args, **kwargs):
422
+ # For sync functions, just pass through (tests mostly use async)
423
+ return func(*args, **kwargs)
424
+
425
+ # Return appropriate wrapper based on function type
426
+ if asyncio.iscoroutinefunction(func):
427
+ return async_wrapper
428
+ else:
429
+ return sync_wrapper
430
+
431
+ return decorator
432
+
433
+
434
+ # get_safe_singleton removed - use get_safe_singleton from agents.task.path instead
435
+
436
+
437
+ def save_registry_items(items: Dict[str, Any], directory: Path, item_type: str = "item") -> None:
438
+ """
439
+ Save registry items (profiles, scenarios, etc.) to disk.
440
+
441
+ Args:
442
+ items: Dictionary of items to save (id -> model)
443
+ directory: Directory to save items to
444
+ item_type: Type of item for logging messages
445
+ """
446
+ import json
447
+ import logging
448
+ logger = logging.getLogger(__name__)
449
+
450
+ for item_id, item in items.items():
451
+ item_file = directory / f"{item_id}.json"
452
+ try:
453
+ with open(item_file, 'w') as f:
454
+ # Handle both Pydantic models and dicts
455
+ data = item.model_dump() if hasattr(item, 'model_dump') else item
456
+ json.dump(data, f, indent=2)
457
+ logger.info(f"Saved default {item_type} '{item_id}' to {item_file}")
458
+ except Exception as e:
459
+ logger.error(f"Failed to save {item_type} '{item_id}': {e}")
460
+
461
+ def send_telemetry(event: Any, agent_id: Optional[str] = None, session_id: Optional[str] = None, user_id: Optional[str] = None) -> bool:
462
+ """
463
+ Send telemetry with consistent session/agent ID handling.
464
+
465
+ Args:
466
+ event: The telemetry event to send
467
+ agent_id: Optional agent ID for tracking
468
+ session_id: Optional session ID for tracking
469
+ user_id: Optional user ID for multi-tenant tracking
470
+
471
+ Returns:
472
+ True if telemetry was sent successfully, False otherwise
473
+ """
474
+ # Don't send telemetry if disabled in environment
475
+ if os.environ.get('ANONYMIZED_TELEMETRY', 'true').lower() != 'true':
476
+ return False
477
+
478
+ try:
479
+ # Use the get_telemetry function instead of directly importing ProductTelemetry
480
+ from agents.task.telemetry import get_telemetry
481
+ # Import path manager for clean_session_id
482
+ from agents.task.path import pm
483
+
484
+ # Try to get correct ID to use for tracking
485
+ tracking_id = None
486
+
487
+ # Use session_id as first choice if provided
488
+ if session_id:
489
+ tracking_id = pm().clean_session_id(session_id)
490
+ # Use agent_id as fallback, extract session ID from it if possible
491
+ elif agent_id:
492
+ tracking_id = extract_session_from_agent_id(agent_id) or pm().clean_session_id(agent_id)
493
+
494
+ # Send telemetry with proper tracking ID. NOTE: ProductTelemetry.capture()
495
+ # takes only (event, session_id) — telemetry is tenant-scoped by the
496
+ # session's on-disk path, not a user_id arg. Passing user_id= raised a
497
+ # swallowed TypeError, so this always returned False (audit 2026-07-04).
498
+ telemetry = get_telemetry()
499
+ telemetry.capture(event, session_id=tracking_id)
500
+ return True
501
+ except Exception as e:
502
+ # Silent fail for telemetry to avoid affecting main operation
503
+ logger.debug(f"Failed to send telemetry: {e}")
504
+ return False
505
+
506
+
507
+ # GIF creation utilities have been moved to utils/gif_utils.py
508
+ # For backwards compatibility, import them here
509
+ from utils.gif_utils import create_gif_with_retry, create_text_only_gif as _create_text_only_gif
510
+
511
+ # DEPRECATED FUNCTIONS REMOVED - Use modules.llm.llm_factory instead
512
+ # These functions were causing conflicts with the main LLM package and only supported OpenAI
513
+ # Legacy LLM factory functions have been completely removed
514
+ # Use the centralized LLM factory: modules.llm.llm_factory.create_chat_model
515
+
516
+ # Add the unified file locking implementation after the singleton decorator code
517
+
518
+ class SafeFileLock:
519
+ """
520
+ Cross-platform file locking implementation with fallback mechanism.
521
+
522
+ This provides a unified locking mechanism that works across:
523
+ 1. Linux/macOS (using filelock package if available)
524
+ 2. Windows (using filelock package if available)
525
+ 3. Any platform (using a simple lock file as fallback)
526
+
527
+ Usage:
528
+ with SafeFileLock('/path/to/file.lock'):
529
+ # Perform operations requiring exclusive access
530
+ """
531
+ __slots__ = ('lock_file', 'timeout', 'poll_interval', '_lock', '_locked', '_use_filelock', '_filelock')
532
+
533
+ # FIXED: Track acquired locks with bounded cache and proper cleanup
534
+ _active_locks = {} # Maps lock file paths to lock counters
535
+ _REGISTRY_LOCK = threading.RLock() # Lock for modifying _active_locks
536
+ _last_cleanup = 0 # Track last cleanup time
537
+ _MAX_CACHE_SIZE = 1000 # FIXED: Limit cache size to prevent memory leaks
538
+ _CLEANUP_INTERVAL = 60 # FIX 9: Reduced from 300 to 60 seconds for more aggressive cleanup
539
+ _STALE_LOCK_AGE = 300 # FIX 9: Consider locks stale after 5 minutes (was 3600/1 hour)
540
+
541
+ def __init__(self, lock_file: str, timeout: float = 60.0, poll_interval: float = 0.1):
542
+ """Initialize the SafeFileLock.
543
+
544
+ Args:
545
+ lock_file: Path to the lock file
546
+ timeout: Maximum time to wait for lock acquisition
547
+ poll_interval: Time between lock acquisition attempts
548
+ """
549
+ self.lock_file = os.path.abspath(lock_file)
550
+ self.timeout = timeout
551
+ self.poll_interval = poll_interval
552
+ self._lock = None
553
+ self._locked = False
554
+ self._use_filelock = False
555
+ self._filelock = None
556
+
557
+ # FIX 9: Check if our specific lock file is stale before trying to acquire
558
+ if os.path.exists(self.lock_file):
559
+ try:
560
+ file_age = time.time() - os.path.getmtime(self.lock_file)
561
+ if file_age > self._STALE_LOCK_AGE:
562
+ # Try to remove our specific stale lock file
563
+ try:
564
+ os.unlink(self.lock_file)
565
+ logger.debug(f"Removed stale lock on init: {self.lock_file} (age: {file_age:.1f}s)")
566
+ except (OSError, PermissionError):
567
+ logger.debug(f"Could not remove stale lock on init: {self.lock_file}")
568
+ except (OSError, FileNotFoundError):
569
+ pass
570
+
571
+ # Try to use filelock package if available
572
+ try:
573
+ import filelock
574
+ self._filelock = filelock.FileLock(self.lock_file, timeout=timeout)
575
+ self._use_filelock = True
576
+ except ImportError:
577
+ # Fall back to simple file-based locking
578
+ pass
579
+
580
+ # FIXED: Trigger cleanup periodically to prevent memory leaks
581
+ self._maybe_cleanup_stale_locks()
582
+
583
+ @classmethod
584
+ def _maybe_cleanup_stale_locks(cls):
585
+ """Cleanup stale locks and limit cache size to prevent memory leaks."""
586
+ current_time = time.time()
587
+
588
+ # Only cleanup every _CLEANUP_INTERVAL seconds
589
+ if current_time - cls._last_cleanup < cls._CLEANUP_INTERVAL:
590
+ return
591
+
592
+ with cls._REGISTRY_LOCK:
593
+ try:
594
+ # FIXED: Remove locks that are no longer held
595
+ stale_locks = []
596
+ for lock_path, count in list(cls._active_locks.items()):
597
+ # Remove locks with zero or negative count
598
+ if count <= 0:
599
+ stale_locks.append(lock_path)
600
+ continue
601
+
602
+ # FIX 9: More aggressive stale lock detection and cleanup
603
+ try:
604
+ if os.path.exists(lock_path):
605
+ # Check if lock file is older than _STALE_LOCK_AGE seconds
606
+ file_age = current_time - os.path.getmtime(lock_path)
607
+ if file_age > cls._STALE_LOCK_AGE: # FIX 9: Use configurable timeout
608
+ # Try to remove stale lock file
609
+ try:
610
+ os.unlink(lock_path)
611
+ stale_locks.append(lock_path)
612
+ logger.debug(f"Removed stale lock file {lock_path} (age: {file_age:.1f}s)")
613
+ except (OSError, PermissionError):
614
+ # Can't remove, but don't count it as active
615
+ logger.debug(f"Could not remove stale lock {lock_path}: permission denied")
616
+ pass
617
+ else:
618
+ # Lock file doesn't exist, remove from tracking
619
+ stale_locks.append(lock_path)
620
+ except (OSError, FileNotFoundError):
621
+ # File system error, remove from tracking
622
+ stale_locks.append(lock_path)
623
+
624
+ # Remove stale locks from tracking
625
+ for lock_path in stale_locks:
626
+ cls._active_locks.pop(lock_path, None)
627
+
628
+ # FIXED: Implement bounded cache - if cache is too large, remove oldest entries
629
+ if len(cls._active_locks) > cls._MAX_CACHE_SIZE:
630
+ # Sort by path and remove oldest (we don't have timestamps, so use lexicographic)
631
+ sorted_locks = sorted(cls._active_locks.items())
632
+ locks_to_remove = len(cls._active_locks) - (cls._MAX_CACHE_SIZE // 2) # Remove half
633
+
634
+ for i in range(locks_to_remove):
635
+ if i < len(sorted_locks):
636
+ lock_path, count = sorted_locks[i]
637
+ # Only remove if not currently held
638
+ if count <= 0:
639
+ cls._active_locks.pop(lock_path, None)
640
+
641
+ # Update last cleanup time
642
+ cls._last_cleanup = current_time
643
+
644
+ if stale_locks:
645
+ # Use basic logging to avoid circular imports
646
+ print(f"SafeFileLock: Cleaned up {len(stale_locks)} stale locks")
647
+
648
+ except Exception as e:
649
+ # Log cleanup errors but don't raise
650
+ print(f"SafeFileLock cleanup error: {e}")
651
+
652
+ def __enter__(self):
653
+ """Acquire the lock."""
654
+ if self._use_filelock and self._filelock:
655
+ try:
656
+ self._filelock.acquire(timeout=self.timeout)
657
+ self._locked = True
658
+
659
+ # Track in registry
660
+ with self._REGISTRY_LOCK:
661
+ self._active_locks[self.lock_file] = self._active_locks.get(self.lock_file, 0) + 1
662
+
663
+ return self
664
+ except Exception as e:
665
+ # Fall back to manual locking if filelock fails
666
+ self._use_filelock = False
667
+
668
+ # Manual file-based locking. Attempt acquisition at least once regardless of
669
+ # timeout (do-while), so a FREE lock is acquired even with timeout=0; only a
670
+ # CONTENDED lock fails fast at timeout=0. (The old `while elapsed < timeout`
671
+ # never ran the body at timeout=0, so a free lock could never be acquired —
672
+ # which silently disabled the goal dispatcher / cron tick on machines without
673
+ # the `filelock` package, where this fallback path is taken.)
674
+ start_time = time.time()
675
+
676
+ while True:
677
+ try:
678
+ # FIXED: Use exclusive create mode to prevent race conditions
679
+ fd = os.open(self.lock_file, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
680
+ try:
681
+ # Write PID to lock file for debugging
682
+ os.write(fd, f"{os.getpid()}\n".encode())
683
+ finally:
684
+ os.close(fd)
685
+
686
+ self._locked = True
687
+
688
+ # Track in registry
689
+ with self._REGISTRY_LOCK:
690
+ self._active_locks[self.lock_file] = self._active_locks.get(self.lock_file, 0) + 1
691
+
692
+ return self
693
+
694
+ except FileExistsError:
695
+ # Lock file already exists; retry until the timeout budget is spent.
696
+ if time.time() - start_time >= self.timeout:
697
+ break
698
+ time.sleep(self.poll_interval)
699
+ continue
700
+ except OSError as e:
701
+ # Other file system error
702
+ raise RuntimeError(f"Failed to acquire lock {self.lock_file}: {e}")
703
+
704
+ # Timeout reached
705
+ raise TimeoutError(f"Failed to acquire lock {self.lock_file} within {self.timeout} seconds")
706
+
707
+ def __exit__(self, exc_type, exc_val, exc_tb):
708
+ """Release the lock."""
709
+ if not self._locked:
710
+ return
711
+
712
+ try:
713
+ if self._use_filelock and self._filelock:
714
+ self._filelock.release()
715
+ else:
716
+ # Manual lock release
717
+ try:
718
+ # FIXED: Verify we still own the lock before removing
719
+ if os.path.exists(self.lock_file):
720
+ # Check if we own this lock by reading PID
721
+ try:
722
+ with open(self.lock_file, 'r') as f:
723
+ lock_pid = f.read().strip()
724
+ if lock_pid == str(os.getpid()):
725
+ os.unlink(self.lock_file)
726
+ else:
727
+ # Lock was taken by another process, don't remove
728
+ pass
729
+ except (OSError, ValueError):
730
+ # Can't read lock file, try to remove anyway
731
+ try:
732
+ os.unlink(self.lock_file)
733
+ except (OSError, FileNotFoundError):
734
+ pass
735
+ except (OSError, FileNotFoundError):
736
+ # Lock file already removed or can't be removed
737
+ pass
738
+
739
+ # Update registry
740
+ with self._REGISTRY_LOCK:
741
+ if self.lock_file in self._active_locks:
742
+ self._active_locks[self.lock_file] -= 1
743
+ # FIXED: Clean up zero-count entries immediately
744
+ if self._active_locks[self.lock_file] <= 0:
745
+ self._active_locks.pop(self.lock_file, None)
746
+
747
+ finally:
748
+ self._locked = False
749
+
750
+
751
+ def get_safe_file_lock(lock_file: str, timeout: float = 60.0) -> Any:
752
+ """Get a SafeFileLock instance.
753
+
754
+ Args:
755
+ lock_file: Path to the lock file
756
+ timeout: Timeout for lock acquisition
757
+
758
+ Returns:
759
+ SafeFileLock instance
760
+ """
761
+ return SafeFileLock(lock_file, timeout)
762
+