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,929 @@
1
+ """A2A Task Handler - Maps A2A tasks to POLYROB sessions.
2
+
3
+ This module handles the mapping between A2A protocol concepts
4
+ and POLYROB's internal session management:
5
+
6
+ A2A Concept → POLYROB Equivalent
7
+ ---------------------------------
8
+ Task → Session
9
+ TaskState → SessionStatus
10
+ Message → UserMessage/feed events
11
+ Artifact → Workspace files
12
+ contextId → user_id grouping
13
+ """
14
+
15
+ import logging
16
+ import os
17
+ from typing import Optional, Dict, Any, List, Tuple
18
+
19
+ from core.env import bool_env as _bool_env
20
+ from core.exceptions import SessionOwnershipError
21
+ from datetime import datetime
22
+ import uuid
23
+ import json
24
+ import asyncio
25
+
26
+ from api.dependencies import resolve_orchestrator
27
+
28
+ from api.a2a.models import (
29
+ A2ATaskState, A2ATask, A2ATaskStatus, A2AMessage, A2AArtifact,
30
+ SendMessageRequest, Part, TextPart, FilePart, DataPart,
31
+ PushNotificationConfig
32
+ )
33
+ from agents.task.agent.session import SessionStatus
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+ # H2 FIX: hold strong references to fire-and-forget session tasks. The event loop
38
+ # keeps only a weak reference to a bare asyncio.Task, so a task whose only reference
39
+ # was the create_task() return value can be garbage-collected mid-run and silently
40
+ # cancelled. These tasks run entire agent sessions, so that is a real session-death risk.
41
+ _BACKGROUND_SESSION_TASKS: set = set()
42
+
43
+
44
+ def _spawn_session_task(coro) -> "asyncio.Task":
45
+ """Schedule a background coroutine while retaining a strong reference to it."""
46
+ task = asyncio.create_task(coro)
47
+ _BACKGROUND_SESSION_TASKS.add(task)
48
+ task.add_done_callback(_BACKGROUND_SESSION_TASKS.discard)
49
+ return task
50
+
51
+
52
+ # =============================================================================
53
+ # Status Mapping
54
+ # =============================================================================
55
+
56
+ # Map POLYROB SessionStatus to A2A TaskState
57
+ # Note: "paused" was removed from SessionStatus - use cancelled for user interruption
58
+ ROB_TO_A2A_STATE: Dict[str, A2ATaskState] = {
59
+ "created": A2ATaskState.SUBMITTED,
60
+ "running": A2ATaskState.WORKING,
61
+ "resumed": A2ATaskState.WORKING,
62
+ "completed": A2ATaskState.COMPLETED,
63
+ "suspended": A2ATaskState.INPUT_REQUIRED,
64
+ "failed": A2ATaskState.FAILED,
65
+ "error": A2ATaskState.FAILED,
66
+ "cancelled": A2ATaskState.CANCELED,
67
+ }
68
+
69
+ # Map A2A TaskState to POLYROB SessionStatus
70
+ A2A_TO_ROB_STATE: Dict[A2ATaskState, str] = {
71
+ A2ATaskState.SUBMITTED: "created",
72
+ A2ATaskState.WORKING: "running",
73
+ A2ATaskState.INPUT_REQUIRED: "suspended",
74
+ A2ATaskState.AUTH_REQUIRED: "suspended",
75
+ A2ATaskState.COMPLETED: "completed",
76
+ A2ATaskState.FAILED: "failed",
77
+ A2ATaskState.CANCELED: "cancelled",
78
+ A2ATaskState.REJECTED: "cancelled",
79
+ }
80
+
81
+
82
+ # =============================================================================
83
+ # Task Handler
84
+ # =============================================================================
85
+
86
+ class A2ATaskHandler:
87
+ """Handles A2A task operations by delegating to TaskAgent."""
88
+
89
+ def __init__(self, container):
90
+ """Initialize handler with dependency container.
91
+
92
+ Args:
93
+ container: DependencyContainer with TaskAgent and services
94
+ """
95
+ self.container = container
96
+ self.logger = logging.getLogger("a2a.task_handler")
97
+
98
+ # Cache for push notification configs
99
+ self._push_configs: Dict[str, PushNotificationConfig] = {}
100
+
101
+ def _get_task_agent(self):
102
+ """Get TaskAgent from container."""
103
+ return self.container.get_agent("task_agent")
104
+
105
+ def _get_session_manager(self):
106
+ """Get SessionManager from container or TaskAgent."""
107
+ sm = self.container.get_service("session_manager")
108
+ if not sm:
109
+ agent = self._get_task_agent()
110
+ if agent:
111
+ sm = agent.session_manager
112
+ return sm
113
+
114
+ # =========================================================================
115
+ # Message/Part Extraction
116
+ # =========================================================================
117
+
118
+ def _extract_text_from_message(self, message: A2AMessage) -> str:
119
+ """Extract text content from A2A message parts.
120
+
121
+ Args:
122
+ message: A2A message with parts
123
+
124
+ Returns:
125
+ Concatenated text from all text parts
126
+ """
127
+ text_parts = []
128
+ for part in message.parts:
129
+ # Handle both dict and model formats
130
+ if isinstance(part, dict):
131
+ if part.get("kind") == "text" or "text" in part:
132
+ text_parts.append(part.get("text", ""))
133
+ elif hasattr(part, 'text'):
134
+ text_parts.append(part.text)
135
+ text = "\n".join(filter(None, text_parts))
136
+
137
+ # C1/A3: opt-in context-reference expansion for remote A2A intake.
138
+ # Filesystem refs are refused (no trusted session workspace here); only @url
139
+ # expands, behind the SSRF guard. Fails soft — never breaks task creation.
140
+ if text:
141
+ from agents.task.constants import AutonomyConfig
142
+ if AutonomyConfig.context_references_enabled():
143
+ try:
144
+ from agents.task.agent.messages.context_references import (
145
+ preprocess_context_references,
146
+ )
147
+ text = preprocess_context_references(
148
+ text, confine_to_root=True, allow_filesystem=False
149
+ )
150
+ except Exception as e:
151
+ self.logger.debug(f"context-ref expansion skipped: {e}")
152
+ return text
153
+
154
+ def _extract_files_from_message(self, message: A2AMessage) -> List[Dict[str, Any]]:
155
+ """Extract file parts from A2A message.
156
+
157
+ Args:
158
+ message: A2A message with parts
159
+
160
+ Returns:
161
+ List of file part dictionaries
162
+ """
163
+ files = []
164
+ for part in message.parts:
165
+ if isinstance(part, dict):
166
+ if part.get("kind") in ["file", "data"]:
167
+ files.append(part)
168
+ elif hasattr(part, 'kind') and part.kind in ["file", "data"]:
169
+ files.append(part.dict() if hasattr(part, 'dict') else part)
170
+ return files
171
+
172
+ def _extract_images_from_message(self, message: A2AMessage) -> List[Dict[str, Any]]:
173
+ """Extract image attachments for vision processing.
174
+
175
+ Args:
176
+ message: A2A message with parts
177
+
178
+ Returns:
179
+ List of image data dicts compatible with vision models
180
+ """
181
+ images = []
182
+ for part in message.parts:
183
+ part_dict = part if isinstance(part, dict) else (part.dict() if hasattr(part, 'dict') else {})
184
+
185
+ if part_dict.get("kind") == "file":
186
+ file_data = part_dict.get("file", {})
187
+ mime_type = file_data.get("mimeType", "")
188
+
189
+ # Check if it's an image
190
+ if mime_type.startswith("image/"):
191
+ if "bytes" in file_data:
192
+ # Inline base64 image
193
+ images.append({
194
+ 'type': 'image_url',
195
+ 'image_url': {
196
+ 'url': f'data:{mime_type};base64,{file_data["bytes"]}'
197
+ }
198
+ })
199
+ elif "uri" in file_data:
200
+ # URI reference
201
+ images.append({
202
+ 'type': 'image_url',
203
+ 'image_url': {
204
+ 'url': file_data["uri"]
205
+ }
206
+ })
207
+ return images
208
+
209
+ # =========================================================================
210
+ # State Conversion
211
+ # =========================================================================
212
+
213
+ def _session_status_to_a2a_state(self, status: str) -> A2ATaskState:
214
+ """Convert POLYROB session status to A2A task state.
215
+
216
+ Args:
217
+ status: POLYROB session status string
218
+
219
+ Returns:
220
+ Corresponding A2ATaskState
221
+ """
222
+ return ROB_TO_A2A_STATE.get(
223
+ status.lower() if status else "unknown",
224
+ A2ATaskState.UNKNOWN
225
+ )
226
+
227
+ def _a2a_state_to_session_status(self, state: A2ATaskState) -> str:
228
+ """Convert A2A task state to POLYROB session status.
229
+
230
+ Args:
231
+ state: A2A task state
232
+
233
+ Returns:
234
+ Corresponding POLYROB session status string
235
+ """
236
+ return A2A_TO_ROB_STATE.get(state, "unknown")
237
+
238
+ # =========================================================================
239
+ # Task Operations
240
+ # =========================================================================
241
+
242
+ async def create_task(
243
+ self,
244
+ request: SendMessageRequest,
245
+ user_id: str,
246
+ context_id: Optional[str] = None
247
+ ) -> A2ATask:
248
+ """Create a new A2A task (maps to POLYROB session).
249
+
250
+ Args:
251
+ request: SendMessageRequest with initial message
252
+ user_id: User/payer identifier
253
+ context_id: Optional context ID for grouping
254
+
255
+ Returns:
256
+ Created A2ATask
257
+
258
+ Raises:
259
+ RuntimeError: If TaskAgent unavailable
260
+ ValueError: If message has no text content
261
+ """
262
+ agent = self._get_task_agent()
263
+ if not agent:
264
+ raise RuntimeError("TaskAgent not available")
265
+
266
+ # Extract task text from message
267
+ task_text = self._extract_text_from_message(request.message)
268
+ if not task_text:
269
+ raise ValueError("No text content in message")
270
+
271
+ # Extract images for vision
272
+ image_attachments = self._extract_images_from_message(request.message)
273
+
274
+ # Get configuration from request
275
+ config = request.configuration or {}
276
+ metadata = request.message.metadata or {}
277
+
278
+ # Prepare session config
279
+ from agents.task_agent_lite import SessionRequest
280
+ session_request = SessionRequest(
281
+ task=task_text,
282
+ model=metadata.get("model", config.get("model", "gpt-5")),
283
+ provider=metadata.get("provider", config.get("provider", "openai")),
284
+ tools=metadata.get("tools", config.get("tools", ["browser", "filesystem"])),
285
+ max_steps=metadata.get("max_steps", config.get("max_steps", 50)),
286
+ use_vision=metadata.get("use_vision", config.get("use_vision", True))
287
+ )
288
+
289
+ # Allow client to specify task ID
290
+ task_id = request.message.taskId
291
+
292
+ # Create session
293
+ try:
294
+ session_info = await agent.create_session(
295
+ user_id=user_id,
296
+ request=session_request,
297
+ session_id=task_id
298
+ )
299
+ except SessionOwnershipError:
300
+ # C4: client-supplied taskId belongs to another tenant. Stay
301
+ # probe-resistant (mirror _authorize_owner: never a distinguishable 403).
302
+ raise ValueError(f"Task {task_id} could not be created")
303
+
304
+ session_id = session_info["id"]
305
+
306
+ # Store context ID mapping
307
+ if context_id or request.message.contextId:
308
+ ctx_id = context_id or request.message.contextId
309
+ sm = self._get_session_manager()
310
+ if sm:
311
+ sm.update_session_metadata(session_id, {
312
+ "a2a_context_id": ctx_id
313
+ })
314
+
315
+ # Store push notification config if provided
316
+ push_config = config.get("pushNotificationConfig")
317
+ if push_config:
318
+ self._push_configs[session_id] = PushNotificationConfig(**push_config)
319
+ sm = self._get_session_manager()
320
+ if sm:
321
+ sm.update_session_metadata(session_id, {
322
+ "a2a_push_url": push_config.get("url")
323
+ })
324
+
325
+ # Queue initial message with images if present
326
+ orchestrator = await resolve_orchestrator(session_id, agent)
327
+ if orchestrator and image_attachments:
328
+ try:
329
+ await orchestrator.submit_user_message(
330
+ agent_id=None,
331
+ text=task_text,
332
+ kind="a2a_initial",
333
+ metadata={"image_attachments": image_attachments}
334
+ )
335
+ self.logger.info(f"Queued initial message with {len(image_attachments)} images")
336
+ except Exception as e:
337
+ self.logger.error(f"Failed to queue initial message: {e}")
338
+
339
+ # Start session execution in background (strong ref retained — see
340
+ # _spawn_session_task). Wrapped so the registered webhook (if any) is
341
+ # notified when the run settles (T4-11).
342
+ _spawn_session_task(self._run_session_with_push(agent, user_id, session_id))
343
+
344
+ # Build A2A task response
345
+ return A2ATask(
346
+ id=session_id,
347
+ contextId=context_id or request.message.contextId or user_id,
348
+ status=A2ATaskStatus(
349
+ state=A2ATaskState.SUBMITTED,
350
+ timestamp=datetime.now().isoformat()
351
+ ),
352
+ metadata={
353
+ "session_id": session_id,
354
+ "task": task_text,
355
+ "created_at": datetime.now().isoformat(),
356
+ "model": session_request.model,
357
+ "tools": session_request.tools
358
+ }
359
+ )
360
+
361
+ @staticmethod
362
+ def _authorize_owner(session_info: dict, task_id: str, user_id: Optional[str]) -> None:
363
+ """Enforce task ownership. Raises not-found (never a distinguishable 403)
364
+ when ``user_id`` is supplied and does not own the session, so a caller
365
+ cannot use the error to probe another tenant's task-IDs. ``user_id=None``
366
+ means the caller already authorized (internal re-fetch) — no check."""
367
+ if user_id is not None and session_info.get("user_id") != user_id:
368
+ raise ValueError(f"Task {task_id} not found")
369
+
370
+ async def get_task(
371
+ self,
372
+ task_id: str,
373
+ history_length: Optional[int] = None,
374
+ user_id: Optional[str] = None
375
+ ) -> A2ATask:
376
+ """Get current status of an A2A task.
377
+
378
+ Args:
379
+ task_id: Task/session ID
380
+ history_length: Number of history messages to include
381
+ None = all, 0 = none, >0 = last N
382
+
383
+ Returns:
384
+ A2ATask with current status
385
+
386
+ Raises:
387
+ ValueError: If task not found
388
+ """
389
+ agent = self._get_task_agent()
390
+ if not agent:
391
+ raise RuntimeError("TaskAgent not available")
392
+
393
+ session_info = await agent.get_session_by_id(task_id)
394
+ if not session_info:
395
+ raise ValueError(f"Task {task_id} not found")
396
+ self._authorize_owner(session_info, task_id, user_id)
397
+
398
+ # Map status to A2A state
399
+ rob_status = session_info.get("status", "unknown")
400
+ a2a_state = self._session_status_to_a2a_state(rob_status)
401
+
402
+ # Collect artifacts if completed
403
+ artifacts = []
404
+ if A2ATaskState.is_terminal(a2a_state):
405
+ artifacts = await self._collect_artifacts(task_id, session_info)
406
+
407
+ # Build history if requested
408
+ history = None
409
+ if history_length is None or history_length > 0:
410
+ history = await self._build_history(task_id, session_info, history_length)
411
+
412
+ # Get context ID
413
+ context_id = session_info.get("metadata", {}).get("a2a_context_id")
414
+ if not context_id:
415
+ context_id = session_info.get("user_id", task_id)
416
+
417
+ return A2ATask(
418
+ id=task_id,
419
+ contextId=context_id,
420
+ status=A2ATaskStatus(
421
+ state=a2a_state,
422
+ timestamp=session_info.get("updated_at", datetime.now().isoformat())
423
+ ),
424
+ history=history,
425
+ artifacts=artifacts,
426
+ metadata={
427
+ "session_id": task_id,
428
+ "internal_status": rob_status,
429
+ "task": session_info.get("task"),
430
+ "created_at": session_info.get("created_at"),
431
+ "updated_at": session_info.get("updated_at"),
432
+ "model": session_info.get("config", {}).get("model"),
433
+ "tools": session_info.get("config", {}).get("tools", [])
434
+ }
435
+ )
436
+
437
+ async def send_message(
438
+ self,
439
+ task_id: str,
440
+ message: A2AMessage,
441
+ user_id: str
442
+ ) -> A2ATask:
443
+ """Send a message to an existing A2A task.
444
+
445
+ Args:
446
+ task_id: Task/session ID
447
+ message: Message to send
448
+ user_id: User identifier
449
+
450
+ Returns:
451
+ Updated A2ATask
452
+
453
+ Raises:
454
+ ValueError: If task not found or in terminal state
455
+ """
456
+ agent = self._get_task_agent()
457
+ if not agent:
458
+ raise RuntimeError("TaskAgent not available")
459
+
460
+ session_info = await agent.get_session_by_id(task_id)
461
+ if not session_info:
462
+ raise ValueError(f"Task {task_id} not found")
463
+ # Ownership guard BEFORE any write/inject/resume: a non-owner must not be
464
+ # able to inject a message into or resume another tenant's session.
465
+ self._authorize_owner(session_info, task_id, user_id)
466
+
467
+ # Check if task is in terminal state
468
+ current_state = self._session_status_to_a2a_state(
469
+ session_info.get("status", "unknown")
470
+ )
471
+ if A2ATaskState.is_terminal(current_state):
472
+ raise ValueError(
473
+ f"Cannot send message to task in terminal state: {current_state.value}"
474
+ )
475
+
476
+ # Extract text and images
477
+ text = self._extract_text_from_message(message)
478
+ image_attachments = self._extract_images_from_message(message)
479
+
480
+ # Get orchestrator and queue message
481
+ orchestrator = await resolve_orchestrator(task_id, agent)
482
+ if orchestrator:
483
+ metadata = message.metadata or {}
484
+ if image_attachments:
485
+ metadata["image_attachments"] = image_attachments
486
+
487
+ await orchestrator.submit_user_message(
488
+ agent_id=None,
489
+ text=text,
490
+ kind="a2a_message",
491
+ metadata=metadata
492
+ )
493
+ self.logger.info(f"Queued message to task {task_id}")
494
+
495
+ # Resume if suspended
496
+ status = session_info.get("status", "").lower()
497
+ if status in ["completed", "suspended", "failed", "error"]:
498
+ self.logger.info(f"Resuming task {task_id} from {status}")
499
+ _spawn_session_task(self._run_session_with_push(
500
+ agent,
501
+ session_info.get("user_id"),
502
+ task_id
503
+ ))
504
+
505
+ return await self.get_task(task_id)
506
+
507
+ async def cancel_task(self, task_id: str, user_id: str) -> A2ATask:
508
+ """Cancel an A2A task.
509
+
510
+ Args:
511
+ task_id: Task/session ID
512
+ user_id: User identifier
513
+
514
+ Returns:
515
+ Updated A2ATask with canceled state
516
+
517
+ Raises:
518
+ ValueError: If task not found or already terminal
519
+ """
520
+ agent = self._get_task_agent()
521
+ if not agent:
522
+ raise RuntimeError("TaskAgent not available")
523
+
524
+ session_info = await agent.get_session_by_id(task_id)
525
+ if not session_info:
526
+ raise ValueError(f"Task {task_id} not found")
527
+
528
+ # Check if already terminal
529
+ current_state = self._session_status_to_a2a_state(
530
+ session_info.get("status", "unknown")
531
+ )
532
+ if A2ATaskState.is_terminal(current_state):
533
+ raise ValueError(
534
+ f"Cannot cancel task in terminal state: {current_state.value}"
535
+ )
536
+
537
+ # Cancel via TaskAgent
538
+ success = await agent.cancel_session(
539
+ user_id=user_id,
540
+ session_id=task_id,
541
+ force=True
542
+ )
543
+
544
+ if not success:
545
+ raise ValueError(f"Failed to cancel task {task_id}")
546
+
547
+ # T4-11: the cancel outcome is known here — push CANCELED directly to the
548
+ # registered webhook (fail-open; a webhook failure never breaks the cancel).
549
+ try:
550
+ await self.send_push_notification(task_id, A2ATaskState.CANCELED)
551
+ except Exception as e:
552
+ self.logger.debug(f"push notify on cancel skipped: {e}")
553
+
554
+ return await self.get_task(task_id)
555
+
556
+ async def list_tasks(
557
+ self,
558
+ user_id: str,
559
+ context_id: Optional[str] = None,
560
+ page_token: Optional[str] = None,
561
+ page_size: int = 20
562
+ ) -> Tuple[List[A2ATask], Optional[str]]:
563
+ """List tasks for a user.
564
+
565
+ Args:
566
+ user_id: User identifier
567
+ context_id: Optional context filter
568
+ page_token: Pagination token
569
+ page_size: Number of results per page
570
+
571
+ Returns:
572
+ Tuple of (tasks, next_page_token)
573
+ """
574
+ sm = self._get_session_manager()
575
+ if not sm:
576
+ return [], None
577
+
578
+ # Get all sessions
579
+ all_sessions = sm._sessions
580
+
581
+ # Filter by user
582
+ user_sessions = [
583
+ (sid, info) for sid, info in all_sessions.items()
584
+ if info.get("user_id") == user_id
585
+ ]
586
+
587
+ # Filter by context if specified
588
+ if context_id:
589
+ user_sessions = [
590
+ (sid, info) for sid, info in user_sessions
591
+ if info.get("metadata", {}).get("a2a_context_id") == context_id
592
+ ]
593
+
594
+ # Sort by creation time (newest first)
595
+ user_sessions.sort(
596
+ key=lambda x: x[1].get("created_at", ""),
597
+ reverse=True
598
+ )
599
+
600
+ # Handle pagination
601
+ start_idx = 0
602
+ if page_token:
603
+ try:
604
+ start_idx = int(page_token)
605
+ except ValueError:
606
+ start_idx = 0
607
+
608
+ end_idx = start_idx + page_size
609
+ page_sessions = user_sessions[start_idx:end_idx]
610
+
611
+ # Build tasks
612
+ tasks = []
613
+ for session_id, session_info in page_sessions:
614
+ try:
615
+ task = await self.get_task(session_id, history_length=0)
616
+ tasks.append(task)
617
+ except Exception as e:
618
+ self.logger.warning(f"Failed to build task for {session_id}: {e}")
619
+
620
+ # Calculate next page token
621
+ next_token = None
622
+ if end_idx < len(user_sessions):
623
+ next_token = str(end_idx)
624
+
625
+ return tasks, next_token
626
+
627
+ # =========================================================================
628
+ # Push Notifications
629
+ # =========================================================================
630
+
631
+ async def _authorize_task(self, task_id: str, user_id: Optional[str]) -> None:
632
+ """Ownership gate for push-config ops: the caller must own ``task_id``.
633
+
634
+ Mirrors ``_authorize_owner`` (raises not-found, never a distinguishable 403,
635
+ so a caller can't probe another tenant's task ids). ``user_id=None`` means
636
+ the caller already authorized (internal use) — no check.
637
+ """
638
+ if user_id is None:
639
+ return
640
+ agent = self._get_task_agent()
641
+ if not agent:
642
+ raise RuntimeError("TaskAgent not available")
643
+ session_info = await agent.get_session_by_id(task_id)
644
+ if not session_info:
645
+ raise ValueError(f"Task {task_id} not found")
646
+ self._authorize_owner(session_info, task_id, user_id)
647
+
648
+ async def set_push_notification_config(
649
+ self,
650
+ task_id: str,
651
+ config: PushNotificationConfig,
652
+ user_id: Optional[str] = None
653
+ ) -> bool:
654
+ """Set push notification config for a task.
655
+
656
+ Args:
657
+ task_id: Task/session ID
658
+ config: Push notification configuration
659
+ user_id: Authenticated caller — must own the task (IDOR gate)
660
+
661
+ Returns:
662
+ True if successful
663
+ """
664
+ await self._authorize_task(task_id, user_id)
665
+
666
+ self._push_configs[task_id] = config
667
+
668
+ sm = self._get_session_manager()
669
+ if sm:
670
+ sm.update_session_metadata(task_id, {
671
+ "a2a_push_url": config.url,
672
+ "a2a_push_token": config.token
673
+ })
674
+
675
+ return True
676
+
677
+ async def get_push_notification_config(
678
+ self,
679
+ task_id: str,
680
+ user_id: Optional[str] = None
681
+ ) -> Optional[PushNotificationConfig]:
682
+ """Get push notification config for a task (caller must own the task)."""
683
+ await self._authorize_task(task_id, user_id)
684
+ return self._push_configs.get(task_id)
685
+
686
+ async def delete_push_notification_config(
687
+ self,
688
+ task_id: str,
689
+ user_id: Optional[str] = None
690
+ ) -> bool:
691
+ """Delete push notification config for a task (caller must own the task)."""
692
+ await self._authorize_task(task_id, user_id)
693
+ if task_id in self._push_configs:
694
+ del self._push_configs[task_id]
695
+ return True
696
+ return False
697
+
698
+ def _get_push_config(
699
+ self,
700
+ task_id: str,
701
+ session_info: Optional[Dict[str, Any]] = None
702
+ ) -> Optional[PushNotificationConfig]:
703
+ """Resolve the push config: in-memory first, then the session-metadata
704
+ mirror (restart-survivable — the in-memory dict dies with the process
705
+ while the metadata written at registration does not). T4-11."""
706
+ config = self._push_configs.get(task_id)
707
+ if config:
708
+ return config
709
+ try:
710
+ meta = (session_info or {}).get("metadata") or {}
711
+ url = meta.get("a2a_push_url")
712
+ if url:
713
+ return PushNotificationConfig(url=url, token=meta.get("a2a_push_token"))
714
+ except Exception:
715
+ pass
716
+ return None
717
+
718
+ async def _notify_push_state(
719
+ self,
720
+ task_id: str,
721
+ message: Optional[str] = None
722
+ ) -> bool:
723
+ """Push the task's CURRENT state to its registered webhook (T4-11).
724
+
725
+ Refetches the session so the delivered state reflects the real outcome
726
+ (completed/failed/suspended). No-op (False) when no webhook is
727
+ registered. Fail-open — a webhook failure never breaks the task."""
728
+ try:
729
+ agent = self._get_task_agent()
730
+ session_info = await agent.get_session_by_id(task_id) if agent else None
731
+ if not session_info:
732
+ return False
733
+ if self._get_push_config(task_id, session_info) is None:
734
+ return False
735
+ state = self._session_status_to_a2a_state(
736
+ session_info.get("status", "unknown"))
737
+ return await self.send_push_notification(task_id, state, message)
738
+ except Exception as e:
739
+ self.logger.debug(f"push state notify skipped for {task_id}: {e}")
740
+ return False
741
+
742
+ async def _run_session_with_push(self, agent, user_id: str, session_id: str) -> None:
743
+ """Run the session and, when it settles (normally or by crash), deliver
744
+ the terminal state to the registered webhook (T4-11 — the agent card
745
+ advertises pushNotifications, so a registered client must hear back)."""
746
+ try:
747
+ await agent.run_session(user_id, session_id)
748
+ finally:
749
+ try:
750
+ await self._notify_push_state(session_id)
751
+ except Exception as e: # never mask the run outcome
752
+ self.logger.debug(f"push notify after run skipped: {e}")
753
+
754
+ async def send_push_notification(
755
+ self,
756
+ task_id: str,
757
+ state: A2ATaskState,
758
+ message: Optional[str] = None
759
+ ) -> bool:
760
+ """Send push notification for task update.
761
+
762
+ Args:
763
+ task_id: Task/session ID
764
+ state: Current task state
765
+ message: Optional status message
766
+
767
+ Returns:
768
+ True if notification sent successfully
769
+ """
770
+ config = self._push_configs.get(task_id)
771
+ if not config:
772
+ # Restart-survivable fallback: rebuild from the metadata mirror.
773
+ try:
774
+ agent = self._get_task_agent()
775
+ session_info = await agent.get_session_by_id(task_id) if agent else None
776
+ config = self._get_push_config(task_id, session_info)
777
+ except Exception:
778
+ config = None
779
+ if not config:
780
+ return False
781
+
782
+ try:
783
+ import httpx
784
+
785
+ payload = {
786
+ "taskId": task_id,
787
+ "state": state.value,
788
+ "timestamp": datetime.now().isoformat()
789
+ }
790
+ if message:
791
+ payload["message"] = message
792
+
793
+ headers = {}
794
+ if config.token:
795
+ headers["X-A2A-Notification-Token"] = config.token
796
+ if config.authentication:
797
+ if "Bearer" in config.authentication.schemes:
798
+ headers["Authorization"] = f"Bearer {config.authentication.credentials}"
799
+
800
+ async with httpx.AsyncClient(timeout=10.0) as client:
801
+ response = await client.post(
802
+ config.url,
803
+ json=payload,
804
+ headers=headers
805
+ )
806
+ response.raise_for_status()
807
+
808
+ self.logger.info(f"Sent push notification for task {task_id}: {state.value}")
809
+ return True
810
+
811
+ except Exception as e:
812
+ self.logger.error(f"Failed to send push notification: {e}")
813
+ return False
814
+
815
+ # =========================================================================
816
+ # Helper Methods
817
+ # =========================================================================
818
+
819
+ async def _collect_artifacts(
820
+ self,
821
+ task_id: str,
822
+ session_info: Dict[str, Any]
823
+ ) -> List[A2AArtifact]:
824
+ """Collect artifacts from session workspace.
825
+
826
+ Args:
827
+ task_id: Task/session ID
828
+ session_info: Session metadata
829
+
830
+ Returns:
831
+ List of A2AArtifacts from workspace
832
+ """
833
+ from agents.task.path import pm
834
+ import mimetypes
835
+
836
+ artifacts = []
837
+ user_id = session_info.get("user_id")
838
+
839
+ workspace_dir = pm().get_workspace_dir(task_id, user_id)
840
+ if not workspace_dir or not workspace_dir.exists():
841
+ return artifacts
842
+
843
+ for file_path in workspace_dir.rglob("*"):
844
+ if file_path.is_file():
845
+ mime_type, _ = mimetypes.guess_type(str(file_path))
846
+ relative_path = str(file_path.relative_to(workspace_dir))
847
+
848
+ artifacts.append(A2AArtifact(
849
+ artifactId=str(uuid.uuid4()),
850
+ name=file_path.name,
851
+ description=f"Generated file: {relative_path}",
852
+ parts=[{
853
+ "kind": "file",
854
+ "file": {
855
+ "uri": f"/api/task/sessions/{task_id}/workspace/{relative_path}",
856
+ "name": file_path.name,
857
+ "mimeType": mime_type or "application/octet-stream"
858
+ }
859
+ }],
860
+ metadata={
861
+ "path": relative_path,
862
+ "size": file_path.stat().st_size
863
+ }
864
+ ))
865
+
866
+ return artifacts
867
+
868
+ async def _build_history(
869
+ self,
870
+ task_id: str,
871
+ session_info: Dict[str, Any],
872
+ limit: Optional[int] = None
873
+ ) -> List[A2AMessage]:
874
+ """Build message history from session feed.
875
+
876
+ Args:
877
+ task_id: Task/session ID
878
+ session_info: Session metadata
879
+ limit: Max messages to return (None = all)
880
+
881
+ Returns:
882
+ List of A2AMessages
883
+ """
884
+ from agents.task.path import pm
885
+
886
+ messages = []
887
+ user_id = session_info.get("user_id")
888
+
889
+ feed_dir = pm().get_subdir(task_id, "feed", user_id=user_id)
890
+ if not feed_dir or not feed_dir.exists():
891
+ return messages
892
+
893
+ # Collect feed files
894
+ feed_files = sorted(feed_dir.glob("*.json"))
895
+
896
+ # Apply limit
897
+ if limit and limit > 0:
898
+ feed_files = feed_files[-limit:]
899
+
900
+ for feed_file in feed_files:
901
+ try:
902
+ with open(feed_file) as f:
903
+ event = json.load(f)
904
+
905
+ event_type = event.get("type", "")
906
+ event_data = event.get("data", {})
907
+
908
+ # Convert to A2A message based on event type
909
+ if event_type in ["user_message", "message"]:
910
+ messages.append(A2AMessage(
911
+ messageId=str(uuid.uuid4()),
912
+ role="user",
913
+ parts=[{"kind": "text", "text": event_data.get("text", "")}],
914
+ metadata=event_data.get("metadata")
915
+ ))
916
+ elif event_type in ["agent_message", "step_result", "action_result"]:
917
+ text = event_data.get("text") or event_data.get("result", "")
918
+ if text:
919
+ messages.append(A2AMessage(
920
+ messageId=str(uuid.uuid4()),
921
+ role="agent",
922
+ parts=[{"kind": "text", "text": str(text)}],
923
+ metadata={"event_type": event_type}
924
+ ))
925
+
926
+ except Exception as e:
927
+ self.logger.warning(f"Failed to parse feed file {feed_file}: {e}")
928
+
929
+ return messages