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,288 @@
1
+ """Surface ABC: thin required surface (Hermes parity) + fat shared defaults.
2
+
3
+ Required (every surface MUST implement): surface_id, capabilities, send(), start(),
4
+ stop().
5
+
6
+ Shared, default-provided (a surface gets these for free):
7
+ - stream(): buffers deltas and flushes one send() on finalize, OR — when the surface
8
+ opts into incremental streaming AND advertises supports_edit — runs the generic
9
+ live-edit engine below (open one message, edit it in place as deltas arrive,
10
+ flood-throttle, split overflow on finalize). The engine is surface-AGNOSTIC; a
11
+ surface plugs in four small transport primitives (_open_stream_message,
12
+ _edit_stream_message, _send_stream_overflow, _stream_target) plus two policy hooks
13
+ (_incremental_streaming_enabled, _edit_min_interval_sec). This is what lets Telegram
14
+ today and WhatsApp/etc. tomorrow share one streaming state machine.
15
+ - identify(): defaults to None.
16
+
17
+ The principle: the base abstraction holds the maximum shared machinery; each surface
18
+ adds only what is genuinely transport-specific on top.
19
+
20
+ STATUS (incremental streaming): wired end-to-end. feed.py publishes a turn's deltas with a
21
+ stable per-TURN stream_id (the session_key); under an edit-capable surface with the flag on,
22
+ they open+edit ONE live message. The turn's discrete reply (the curated send_message text)
23
+ arrives via send(), which calls _finalize_live_on_send() to COMMIT that same bubble in place
24
+ — so the PERSISTED final is the clean reply, never raw stream tokens, and there is no
25
+ duplicate message. A bounded _MAX_LIVE_STREAMS cap bounds any un-finalized stream (e.g. a
26
+ turn that streams but ends via done() instead of a reply).
27
+
28
+ CAVEAT (why it stays opt-in / default-OFF): the INTERMEDIATE frames shown mid-stream are the
29
+ raw model deltas, scrubbed per-chunk by MessageRouter (scrub_brain_blocks) — best-effort, so
30
+ a brain/think block straddling a chunk boundary can briefly surface before the clean final
31
+ commit. The persisted final is always clean. Enable TELEGRAM_INCREMENTAL_STREAM only where
32
+ that intermediate exposure is acceptable; OFF keeps the buffered one-send-on-finalize path.
33
+ """
34
+ import logging
35
+ import time as _time
36
+ from abc import ABC, abstractmethod
37
+ from dataclasses import dataclass, field
38
+ from typing import Any, List, Optional
39
+
40
+ from core.surfaces.envelopes import (
41
+ OutboundMessage, SendResult, SurfaceCapabilities, Identity,
42
+ )
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+
47
+ @dataclass
48
+ class SurfaceConfigField:
49
+ name: str
50
+ secret: bool = False
51
+ required: bool = True
52
+ env_alias: str | None = None
53
+ help: str = ""
54
+
55
+
56
+ @dataclass
57
+ class SurfaceConfigSchema:
58
+ fields: list = field(default_factory=list)
59
+
60
+
61
+ def split_message(text: str, limit: int) -> List[str]:
62
+ """Split text into <=limit-sized chunks (shared by send() and the stream engine)."""
63
+ if not text:
64
+ return [text]
65
+ return [text[i:i + limit] for i in range(0, len(text), limit)]
66
+
67
+
68
+ class _LiveStream:
69
+ """In-progress streamed reply for one stream_id: the single message we keep editing
70
+ in place, plus accumulated text and last-render bookkeeping. Surface-agnostic —
71
+ ``target`` is whatever address the concrete surface's _stream_target() returns."""
72
+ __slots__ = ("target", "message_id", "text", "rendered", "last_edit")
73
+
74
+ def __init__(self, target: Any) -> None:
75
+ self.target = target
76
+ self.message_id: Optional[Any] = None
77
+ self.text = ""
78
+ self.rendered = "" # last text actually pushed to the surface
79
+ self.last_edit = 0.0 # monotonic ts of last edit (flood throttle)
80
+
81
+
82
+ class Surface(ABC):
83
+ def __init__(self) -> None:
84
+ self._stream_buffers: dict[str, list] = {}
85
+ self._live: dict[str, _LiveStream] = {}
86
+
87
+ @property
88
+ @abstractmethod
89
+ def surface_id(self) -> str: ...
90
+
91
+ @property
92
+ @abstractmethod
93
+ def capabilities(self) -> SurfaceCapabilities: ...
94
+
95
+ @abstractmethod
96
+ async def send(self, msg: OutboundMessage) -> SendResult: ...
97
+
98
+ @abstractmethod
99
+ async def start(self, container) -> None: ...
100
+
101
+ @abstractmethod
102
+ async def stop(self) -> None: ...
103
+
104
+ async def stream(self, msg: OutboundMessage) -> None:
105
+ """Route a streamed delta: live-edit when the surface opts in + supports edit,
106
+ else the buffered default (commit one send() on finalize)."""
107
+ if self._incremental_streaming_enabled() and self.capabilities.supports_edit:
108
+ return await self._stream_incremental(msg)
109
+ key = msg.stream_id or msg.session_key
110
+ if msg.partial:
111
+ buf = self._stream_buffers.get(key)
112
+ if buf is None:
113
+ # M4: bound the number of buffered streams (evict oldest). The streaming
114
+ # mirror may never emit a partial=False finalize, so without this the
115
+ # buffer map grows for the daemon's lifetime.
116
+ if len(self._stream_buffers) >= self._MAX_LIVE_STREAMS:
117
+ oldest = next(iter(self._stream_buffers))
118
+ self._stream_buffers.pop(oldest, None)
119
+ buf = self._stream_buffers[key] = []
120
+ buf.append(msg.text)
121
+ # And bound each un-finalized stream's chunk list (drop oldest chunks) so a
122
+ # single never-flushed key can't grow without limit.
123
+ if len(buf) > self._MAX_STREAM_CHUNKS:
124
+ del buf[:len(buf) - self._MAX_STREAM_CHUNKS]
125
+ return
126
+ buffered = "".join(self._stream_buffers.pop(key, []))
127
+ final_text = buffered + (msg.text or "")
128
+ await self.send(OutboundMessage(
129
+ session_key=msg.session_key, text=final_text, kind=msg.kind,
130
+ partial=False, stream_id=msg.stream_id, reply_to=msg.reply_to,
131
+ ))
132
+
133
+ async def identify(self, raw: dict) -> Optional[Identity]:
134
+ return None
135
+
136
+ @classmethod
137
+ def config_schema(cls) -> "SurfaceConfigSchema":
138
+ return SurfaceConfigSchema(fields=[])
139
+
140
+ @classmethod
141
+ def validate_config(cls, values: dict) -> tuple[bool, str]:
142
+ missing = [f.name for f in cls.config_schema().fields
143
+ if f.required and not values.get(f.name)]
144
+ return (not missing, "" if not missing else f"missing: {', '.join(missing)}")
145
+
146
+ def render_outbound(self, text: str) -> List[str]:
147
+ """Escape + split outbound text per this surface's capabilities. Surfaces should
148
+ use this instead of re-implementing per-platform chunking."""
149
+ from core.surfaces.rendering import render_for_flavor
150
+ caps = self.capabilities
151
+ return render_for_flavor(text, caps.markdown_flavor, caps.max_message_bytes)
152
+
153
+ def can_send_now(self, session_key: str, *, now: Optional[float] = None) -> "SendDecision":
154
+ """Whether a (proactive) send to this chat is allowed right now. Base default:
155
+ ALLOW (free outbound). A windowed surface overrides this. Pure + sync so producers
156
+ can consult it cheaply before enqueuing."""
157
+ from core.surfaces.send_policy import SendDecision
158
+ return SendDecision.ALLOW
159
+
160
+ async def _finalize_live_on_send(self, msg: OutboundMessage) -> bool:
161
+ """If an in-flight live stream exists for this message's session, COMMIT it with
162
+ this (clean, curated) discrete text instead of letting the surface send a separate
163
+ message — so the turn's discrete reply finalizes the streamed bubble in place (no
164
+ duplicate, and the persisted final text is the clean reply, not raw stream tokens).
165
+
166
+ A surface's send() calls this first and returns early when it returns True. No-op
167
+ (returns False) when incremental streaming is off, the surface can't edit, or no
168
+ live stream is open for this session — so the normal send path is unchanged."""
169
+ if not (self._incremental_streaming_enabled() and self.capabilities.supports_edit):
170
+ return False
171
+ key = msg.stream_id or msg.session_key
172
+ st = self._live.pop(key, None)
173
+ if st is None or st.message_id is None:
174
+ return False
175
+ await self._commit_final_stream(st, msg.text or "")
176
+ return True
177
+
178
+ # --- incremental streaming engine (generic; surfaces provide the primitives) ---
179
+ #
180
+ # A surface enables this by returning True from _incremental_streaming_enabled()
181
+ # and advertising capabilities.supports_edit, then implementing the transport
182
+ # primitives below. Everything here is shared.
183
+
184
+ def _incremental_streaming_enabled(self) -> bool:
185
+ """Opt-in switch (default OFF -> buffered)."""
186
+ return False
187
+
188
+ def _stream_target(self, msg: OutboundMessage) -> Any:
189
+ """The surface-specific address to send/edit against (default: the session_key;
190
+ Telegram parses the chat id out of it)."""
191
+ return msg.session_key
192
+
193
+ def _edit_min_interval_sec(self) -> float:
194
+ """Minimum seconds between in-place edits (flood-control). 0 = edit every delta."""
195
+ return 0.0
196
+
197
+ async def _open_stream_message(self, target: Any, text: str) -> Optional[Any]:
198
+ """Send the first visible delta as a new message; return its id (for editing)."""
199
+ raise NotImplementedError
200
+
201
+ async def _edit_stream_message(self, target: Any, message_id: Any, text: str) -> None:
202
+ """Replace the open message's text in place."""
203
+ raise NotImplementedError
204
+
205
+ async def _send_stream_overflow(self, target: Any, text: str) -> None:
206
+ """Emit text past the first message's size limit as a fresh message."""
207
+ raise NotImplementedError
208
+
209
+ def _on_stream_error(self, target: Any, op: str, e: Exception) -> None:
210
+ """Handle a transport error during streaming (default: log). A surface can
211
+ override to record provider back-off (e.g. Telegram RetryAfter)."""
212
+ logger.debug("surface %s stream %s failed for %s: %s", self.surface_id, op, target, e)
213
+
214
+ # Defensive cap on in-flight streams. The engine finalizes (and pops) a stream when a
215
+ # matching partial=False arrives; until the live producers emit such a finalize with a
216
+ # STABLE per-turn stream_id (current feed.py uses a per-STEP stream_id and no finalize
217
+ # reaches stream()), an enabled surface could otherwise accumulate one entry per step.
218
+ # This bound makes the worst case a bounded ring, never an unbounded leak. See the
219
+ # module docstring: incremental streaming is engine-correct but not yet wired live —
220
+ # keep the opt-in flag OFF until the producer emits per-turn finalizes.
221
+ _MAX_LIVE_STREAMS = 256
222
+ _MAX_STREAM_CHUNKS = 4096 # cap buffered deltas per un-finalized stream (M4)
223
+
224
+ async def _stream_incremental(self, msg: OutboundMessage) -> None:
225
+ key = msg.stream_id or msg.session_key
226
+ if msg.partial:
227
+ st = self._live.get(key)
228
+ if st is None:
229
+ if len(self._live) >= self._MAX_LIVE_STREAMS:
230
+ # Evict the oldest in-flight stream (insertion order) so the map stays
231
+ # bounded even if finalizes never arrive.
232
+ oldest = next(iter(self._live))
233
+ self._live.pop(oldest, None)
234
+ st = _LiveStream(self._stream_target(msg))
235
+ self._live[key] = st
236
+ st.text += (msg.text or "")
237
+ await self._render_live(st, final=False)
238
+ return
239
+ # Finalize: commit the full text and drop the live-stream state.
240
+ st = self._live.pop(key, None)
241
+ final_text = (st.text if st else "") + (msg.text or "")
242
+ if st is None or st.message_id is None:
243
+ # No live message was opened (all-whitespace partials, or a bare finalize)
244
+ # -> a normal send so the reply still lands.
245
+ await self.send(OutboundMessage(
246
+ session_key=msg.session_key, text=final_text, kind=msg.kind,
247
+ ))
248
+ return
249
+ await self._commit_final_stream(st, final_text)
250
+
251
+ async def _render_live(self, st: _LiveStream, *, final: bool) -> None:
252
+ max_bytes = self.capabilities.max_message_bytes
253
+ text = (st.text or "")[:max_bytes]
254
+ if not text.strip():
255
+ return
256
+ if st.message_id is None:
257
+ try:
258
+ st.message_id = await self._open_stream_message(st.target, text)
259
+ st.rendered = text
260
+ st.last_edit = _time.monotonic()
261
+ except Exception as e: # fail-open: never raise into the agent loop
262
+ self._on_stream_error(st.target, "open", e)
263
+ return
264
+ if not final:
265
+ if _time.monotonic() - st.last_edit < self._edit_min_interval_sec():
266
+ return
267
+ if text == st.rendered:
268
+ return # an unchanged edit is rejected by some surfaces ("not modified")
269
+ await self._do_edit(st, text)
270
+
271
+ async def _commit_final_stream(self, st: _LiveStream, final_text: str) -> None:
272
+ chunks = split_message(final_text, self.capabilities.max_message_bytes)
273
+ first = chunks[0] if chunks else ""
274
+ if first and first != st.rendered:
275
+ await self._do_edit(st, first)
276
+ for chunk in chunks[1:]:
277
+ try:
278
+ await self._send_stream_overflow(st.target, chunk)
279
+ except Exception as e:
280
+ self._on_stream_error(st.target, "overflow", e)
281
+
282
+ async def _do_edit(self, st: _LiveStream, text: str) -> None:
283
+ try:
284
+ await self._edit_stream_message(st.target, st.message_id, text)
285
+ st.rendered = text
286
+ st.last_edit = _time.monotonic()
287
+ except Exception as e:
288
+ self._on_stream_error(st.target, "edit", e)
@@ -0,0 +1,88 @@
1
+ """Surface-agnostic voice→text seam. The Transcriber is built ONCE and registered on the
2
+ container (was a lazy per-Telegram-harness instance — invisible to Email/WhatsApp). Every
3
+ surface normalizes inbound audio to Media (core.surfaces.media) and calls
4
+ transcribe_inbound_media(); the untranscribed-voice outcome is handled by one shared guard
5
+ (core.surfaces.voice_guard) so no surface can silently route an empty voice turn."""
6
+ import logging
7
+ from typing import List, Optional
8
+
9
+ from core.surfaces.media import Media
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def voice_present(media: List[Media]) -> bool:
15
+ return any(getattr(m, "kind", None) in ("voice", "audio") for m in (media or []))
16
+
17
+
18
+ def get_transcriber(container):
19
+ """Build-once, container-registered Transcriber. Falls back to NullTranscriber when
20
+ faster-whisper isn't importable (build_transcriber already degrades). Registered so
21
+ every surface shares one model load instead of each harness building its own."""
22
+ existing = container.get_service("transcriber") if container else None
23
+ if existing is not None:
24
+ return existing
25
+ from agents.task.surface_config import SurfaceConfig
26
+ from modules.transcription import build_transcriber
27
+ t = build_transcriber(SurfaceConfig.voice_transcription_model())
28
+ try:
29
+ container.register_service("transcriber", t)
30
+ except Exception:
31
+ pass
32
+ return t
33
+
34
+
35
+ async def _audio_bytes(media: Media) -> Optional[bytes]:
36
+ if media.data:
37
+ return media.data
38
+ if media.url:
39
+ try:
40
+ import aiohttp
41
+ async with aiohttp.ClientSession() as s:
42
+ async with s.get(media.url) as r:
43
+ return await r.read()
44
+ except Exception as e:
45
+ logger.debug("transcription: media url fetch failed: %s", e)
46
+ return None
47
+ return None
48
+
49
+
50
+ async def transcribe_inbound_media(container, media: List[Media]) -> Optional[str]:
51
+ """Transcribe the first voice/audio Media to text, or None. Fail-open."""
52
+ from agents.task.surface_config import SurfaceConfig
53
+ if not SurfaceConfig.voice_transcription_enabled():
54
+ return None
55
+ target = next((m for m in (media or []) if getattr(m, "kind", None) in ("voice", "audio")), None)
56
+ if target is None:
57
+ return None
58
+ audio = await _audio_bytes(target)
59
+ if not audio:
60
+ return None
61
+ try:
62
+ transcriber = get_transcriber(container)
63
+ text = await transcriber.transcribe(audio, mime=target.mime or "audio/ogg")
64
+ except Exception as e: # belt-and-suspenders; transcriber is itself fail-open
65
+ logger.debug("transcribe_inbound_media failed: %s", e)
66
+ return None
67
+ return (text or "").strip() or None
68
+
69
+
70
+ def log_transcription_readiness(container) -> None:
71
+ """One-line startup signal so a 'voice silently dropped' deploy is visible. WARN when
72
+ voice is enabled but the real engine isn't importable (the #1 root cause)."""
73
+ from agents.task.surface_config import SurfaceConfig
74
+ if not SurfaceConfig.voice_transcription_enabled():
75
+ logger.info("voice transcription: DISABLED (VOICE_TRANSCRIPTION_ENABLED=false)")
76
+ return
77
+ try:
78
+ import faster_whisper # noqa: F401
79
+ logger.info("voice transcription: ENABLED (faster-whisper present, model=%s)",
80
+ SurfaceConfig.voice_transcription_model())
81
+ except Exception:
82
+ msg = ("voice transcription ENABLED but faster-whisper is NOT installed — voice "
83
+ "messages will be refused with a notice. Install the 'voice' extra or set "
84
+ "VOICE_TRANSCRIPTION_ENABLED=false.")
85
+ if SurfaceConfig.voice_transcription_required():
86
+ logger.error("STARTUP: %s", msg)
87
+ else:
88
+ logger.warning("STARTUP: %s", msg)
@@ -0,0 +1,26 @@
1
+ """Shared voice-transcript echo builder. Mirrors core.surfaces.voice_guard: one place so
2
+ every surface renders an identical persistent 'here's what I heard' message before the
3
+ agent answers. Transport-free."""
4
+ from typing import List, Optional
5
+
6
+ from core.surfaces.media import Media
7
+
8
+ _ECHO_CAP = 3500 # keep under Telegram's 4096 limit; the FULL transcript still routes to the agent
9
+
10
+
11
+ def voice_transcript(media: List[Media]) -> Optional[str]:
12
+ """Return the first voice/audio Media's stamped transcript (stripped), else None."""
13
+ for m in media or []:
14
+ if getattr(m, "kind", None) in ("voice", "audio"):
15
+ t = getattr(m, "transcript", None)
16
+ if t and t.strip():
17
+ return t.strip()
18
+ return None
19
+
20
+
21
+ def voice_echo_message(transcript: str) -> str:
22
+ """Render the persistent echo shown before the agent's answer. Capped, ellipsis on overflow."""
23
+ t = (transcript or "").strip()
24
+ if len(t) > _ECHO_CAP:
25
+ t = t[:_ECHO_CAP].rstrip() + "…"
26
+ return f'🎙️ Transcript: "{t}"'
@@ -0,0 +1,23 @@
1
+ """One shared voice guard so NO surface can silently route an untranscribed voice note.
2
+ A voice message that produced no usable transcript -> the surface tells the user (loudly)
3
+ and does NOT dispatch an empty turn to the agent (the live bug: empty voice turns reaching
4
+ the agent, which then 'understood nothing')."""
5
+ from typing import List
6
+
7
+ from core.surfaces.media import Media
8
+
9
+
10
+ def voice_needs_guard(media: List[Media], inbound_text) -> bool:
11
+ """True iff the inbound carries voice/audio but yielded no usable text."""
12
+ has_voice = any(getattr(m, "kind", None) in ("voice", "audio") for m in (media or []))
13
+ if not has_voice:
14
+ return False
15
+ return not (inbound_text or "").strip()
16
+
17
+
18
+ def voice_unavailable_message(enabled: bool) -> str:
19
+ if not enabled:
20
+ return ("🎤 I can't process voice messages right now — voice transcription is "
21
+ "turned off. Please send your message as text.")
22
+ return ("🎤 I couldn't transcribe that voice message (it may be empty, too noisy, or "
23
+ "transcription isn't available on this server). Please try again or send text.")
core/tickers.py ADDED
@@ -0,0 +1,174 @@
1
+ """Shared interval-ticker loop + lifespan supervisor (consolidates cron/goal/curator).
2
+
3
+ The canonical loop is lifted verbatim from ``cron/runner.py::CronTicker.run_forever``
4
+ so all four ticker variants share identical scheduling semantics.
5
+ """
6
+ import asyncio
7
+ import logging
8
+ import os
9
+ from typing import Any, Awaitable, Callable, Optional
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def _heartbeat_interval_sec() -> int:
15
+ try:
16
+ return max(30, int(os.getenv("AUTONOMY_HEARTBEAT_INTERVAL_SEC", "300")))
17
+ except Exception:
18
+ return 300
19
+
20
+
21
+ class IntervalTicker:
22
+ """Run an async tick coroutine on a fixed interval until a stop event fires.
23
+
24
+ Optionally backs off the interval on consecutive idle ticks (``is_active``)
25
+ so a low-traffic ticker doesn't keep waking the process on a fixed cadence
26
+ forever -- the dominant reason a single-user local CLI session never lets
27
+ the OS scheduler (and on a laptop, App Nap) go idle. Backoff is strictly
28
+ opt-in: omit ``is_active`` (every ticker built before this was added) and
29
+ the loop is byte-identical to the original fixed-interval implementation.
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ tick_coro: Callable[[], Awaitable[Any]],
35
+ interval_seconds: int,
36
+ *,
37
+ is_active: Optional[Callable[[Any], bool]] = None,
38
+ max_interval_seconds: Optional[int] = None,
39
+ backoff_factor: float = 2.0,
40
+ ) -> None:
41
+ self.tick_coro = tick_coro
42
+ self.interval_seconds = interval_seconds
43
+ self._is_active = is_active
44
+ self._max_interval = max_interval_seconds or interval_seconds
45
+ self._backoff_factor = backoff_factor
46
+ self._current_interval = interval_seconds
47
+
48
+ async def run_forever(self, stop_event: Optional[asyncio.Event] = None) -> None:
49
+ while not (stop_event is not None and stop_event.is_set()):
50
+ active = True
51
+ try:
52
+ result = await self.tick_coro()
53
+ if self._is_active is not None:
54
+ active = bool(self._is_active(result))
55
+ except Exception as e: # a tick must never kill the loop
56
+ logger.error("ticker tick failed: %s", e, exc_info=True)
57
+ active = True # never back off after an error -- retry promptly
58
+
59
+ if self._is_active is not None:
60
+ if active:
61
+ self._current_interval = self.interval_seconds
62
+ else:
63
+ self._current_interval = min(
64
+ self._max_interval,
65
+ max(self.interval_seconds, self._current_interval * self._backoff_factor),
66
+ )
67
+
68
+ wait_s = self._current_interval
69
+ try:
70
+ await asyncio.wait_for(
71
+ stop_event.wait() if stop_event else asyncio.sleep(wait_s),
72
+ timeout=wait_s,
73
+ )
74
+ except asyncio.TimeoutError:
75
+ pass
76
+
77
+
78
+ class TickerSupervisor:
79
+ """Manage a collection of named ticker tasks for API lifespan use.
80
+
81
+ Accepts any object with a ``run_forever(stop_event)`` coroutine method —
82
+ :class:`IntervalTicker` instances or existing ticker classes (CronTicker,
83
+ GoalTicker, CuratorTicker) that delegate their own ``run_forever`` here.
84
+
85
+ Usage::
86
+
87
+ sup = TickerSupervisor()
88
+ sup.register("cron", cron_ticker, enabled=cron_enabled())
89
+ await sup.start_all() # on startup
90
+ ...
91
+ await sup.stop_all() # on shutdown (in finally)
92
+ """
93
+
94
+ def __init__(self) -> None:
95
+ self._entries: list = [] # [(name, ticker, enabled)]
96
+ self._tasks: dict = {} # name -> (task, stop_event)
97
+ self._hb_task: Optional[asyncio.Task] = None
98
+ self._hb_stop: Optional[asyncio.Event] = None
99
+
100
+ def register(self, name: str, ticker: Any, enabled: bool) -> None:
101
+ """Register a ticker. Only enabled tickers are started in :meth:`start_all`."""
102
+ self._entries.append((name, ticker, enabled))
103
+
104
+ def _emit_heartbeats(self) -> None:
105
+ """Record one autonomy_tick per running ticker (liveness signal).
106
+
107
+ The audit (2026-07-04) found NO automated 'is the loop alive' signal — ops
108
+ relied on a manual human tick-log. This makes idle-but-alive observable, and
109
+ flags a ticker whose task has died. Fail-open; lazy import keeps core clean.
110
+ """
111
+ try:
112
+ from agents.task.telemetry.event_log import get_event_log, event_log_enabled
113
+ if not event_log_enabled():
114
+ return
115
+ log = get_event_log()
116
+ for name, (task, _stop) in self._tasks.items():
117
+ alive = not task.done()
118
+ log.record("autonomy_tick", source="supervisor", loop=name,
119
+ alive=alive, reason=None if alive else "task_exited")
120
+ # Keep the event log bounded (retention discipline; fail-open).
121
+ try:
122
+ import time as _t
123
+ days = max(1, int(os.getenv("TELEMETRY_EVENT_LOG_RETENTION_DAYS", "30")))
124
+ log.prune(older_than_ts=_t.time() - days * 86400)
125
+ except Exception:
126
+ pass
127
+ except Exception:
128
+ pass
129
+
130
+ async def _heartbeat_loop(self) -> None:
131
+ interval = _heartbeat_interval_sec()
132
+ while not (self._hb_stop is not None and self._hb_stop.is_set()):
133
+ try:
134
+ await asyncio.wait_for(self._hb_stop.wait(), timeout=interval)
135
+ except asyncio.TimeoutError:
136
+ pass
137
+ if self._hb_stop is not None and self._hb_stop.is_set():
138
+ break
139
+ self._emit_heartbeats()
140
+
141
+ async def start_all(self) -> None:
142
+ """Start all enabled tickers as background asyncio tasks."""
143
+ for name, ticker, enabled in self._entries:
144
+ if not enabled:
145
+ continue
146
+ stop = asyncio.Event()
147
+ task = asyncio.create_task(ticker.run_forever(stop_event=stop))
148
+ self._tasks[name] = (task, stop)
149
+ logger.info("✅ %s ticker started", name)
150
+ # Liveness heartbeat (only if something is actually running).
151
+ if self._tasks:
152
+ self._hb_stop = asyncio.Event()
153
+ self._hb_task = asyncio.create_task(self._heartbeat_loop())
154
+
155
+ async def stop_all(self) -> None:
156
+ """Signal stop and cancel all running tickers, awaiting their completion."""
157
+ if self._hb_stop is not None:
158
+ self._hb_stop.set()
159
+ if self._hb_task is not None:
160
+ self._hb_task.cancel()
161
+ try:
162
+ await self._hb_task
163
+ except (asyncio.CancelledError, Exception):
164
+ pass
165
+ self._hb_task = None
166
+ for name, (task, stop) in self._tasks.items():
167
+ stop.set()
168
+ task.cancel()
169
+ try:
170
+ await task
171
+ except (asyncio.CancelledError, Exception):
172
+ pass
173
+ logger.info("%s ticker stopped", name)
174
+ self._tasks.clear()