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
cli/ui/state.py ADDED
@@ -0,0 +1,376 @@
1
+ """SessionState accumulator for the POLYROB CLI renderer (Phase 1).
2
+
3
+ ``SessionState`` is the single source of truth the renderer reads from.
4
+ It is updated in two ways:
5
+
6
+ 1. **Push** — ``update(event)`` is called for every incoming ``RenderEvent``.
7
+ 2. **Pull** — ``poll(agent)`` is called once per turn, reading live metrics
8
+ from the ``message_manager`` that are not available via the feed.
9
+
10
+ Design notes:
11
+ - No I/O; pure in-memory accumulation.
12
+ - Elapsed time uses ``time.monotonic()`` captured at construction (session
13
+ start) so it doesn't drift with wall-clock skew.
14
+ - Cost is a rolling sum of ``LLMCall.cost_estimate`` values. It is an
15
+ *estimate* — the authoritative total is in ``LLMUsageTracker``, available
16
+ on demand via ``/usage`` (Phase 4).
17
+ - All numeric fields default to zero / None so the renderer can always
18
+ format them safely.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import time
25
+ from pathlib import Path
26
+ from typing import Any, Optional
27
+
28
+ from cli.ui.events import (
29
+ AgentEnd,
30
+ AgentRegistration,
31
+ ErrorEvent,
32
+ IterationDone,
33
+ LLMCall,
34
+ RenderEvent,
35
+ SessionDone,
36
+ SessionStart,
37
+ Step,
38
+ ToolExec,
39
+ )
40
+ from cli.ui.lifecycle import TurnLifecycle
41
+
42
+
43
+ class SessionState:
44
+ """Mutable accumulator of session metrics for the CLI renderer.
45
+
46
+ Attributes:
47
+ model: LLM model name (set from SessionStart).
48
+ provider: LLM provider name (set from first LLMCall).
49
+ started_at: ``time.monotonic()`` at construction.
50
+ status: Human-readable status string.
51
+
52
+ tokens_in: Accumulated prompt tokens.
53
+ tokens_out: Accumulated completion tokens.
54
+ tokens_cached: Placeholder (not emitted by current feed; kept for
55
+ future compatibility with cache-token reporting).
56
+ tokens_total: Accumulated ``token_count`` (may differ from
57
+ in+out due to cache accounting).
58
+ cost_estimate_total: Running sum of ``LLMCall.cost_estimate`` values.
59
+
60
+ step: Current step number.
61
+ max_steps: Maximum steps (not in feed; settable externally).
62
+ tool_calls: Count of tool-execution events seen.
63
+ errors: Count of error events seen.
64
+
65
+ ctx_percent: Context window usage 0–100 (polled).
66
+ ctx_tokens: Raw token count in context (polled).
67
+ ctx_max: Max input tokens (polled).
68
+ compactions: Number of compaction events so far (polled).
69
+ """
70
+
71
+ def __init__(self, *, clock: Optional[Any] = None) -> None:
72
+ self._clock = clock or time.monotonic
73
+ self.model: str = ""
74
+ self.provider: str = ""
75
+ # ``started_at`` is the SESSION AGE anchor (used by /session). The live bar
76
+ # shows the per-turn WORK clock via ``lifecycle.active_elapsed()`` instead —
77
+ # session age must never be presented as "elapsed" work time.
78
+ self.started_at: float = self._clock()
79
+ self.status: str = "starting"
80
+
81
+ # The single source of truth for "is a turn active, and for how long".
82
+ # The bar's clock + status word derive from this; feed events do NOT
83
+ # mutate it (only the turn runner does, via begin_turn/end_turn).
84
+ self.lifecycle: TurnLifecycle = TurnLifecycle(clock=self._clock)
85
+
86
+ # Token accounting (accumulated from LLMCall events)
87
+ self.tokens_in: int = 0
88
+ self.tokens_out: int = 0
89
+ self.tokens_cached: int = 0
90
+ self.tokens_total: int = 0
91
+ self.cost_estimate_total: float = 0.0
92
+
93
+ # Step / progress
94
+ self.step: int = 0
95
+ self.max_steps: Optional[int] = None
96
+ self.tool_calls: int = 0
97
+ self.errors: int = 0
98
+
99
+ # Context window metrics (polled from message_manager each turn)
100
+ self.ctx_percent: float = 0.0
101
+ self.ctx_tokens: int = 0
102
+ self.ctx_max: int = 0
103
+ self.compactions: int = 0
104
+
105
+ # Sub-agent grouping: the first registered agent is the "main" agent;
106
+ # any other agent_id/agent_name seen is a sub-agent (delegate_task spawn).
107
+ # Both id and name are stored because step events carry only agent_name
108
+ # (the real AgentStepFormatter never emits agent_id in the step dict).
109
+ self.main_agent_id: str = ""
110
+ self.main_agent_name: str = ""
111
+ # True when the most-recent Step belonged to a sub-agent. tool_execution
112
+ # events carry no agent identity, so the renderer correlates a tool
113
+ # result to the step that declared it to suppress sub-agent tool churn
114
+ # from the default (non-verbose) transcript.
115
+ self.last_step_sub_agent: bool = False
116
+
117
+ # Live-info (D3): count of in-flight delegate_task sub-agents (fed by the
118
+ # orchestrator sub-agent lifecycle hooks the REPL registers) and the name
119
+ # of the most-recent tool action (the in-flight tool for the status line).
120
+ self.subagents_active: int = 0
121
+ self.last_tool: str = ""
122
+ # Slow-polled, cached autonomy snapshot for the 2nd status line (D4).
123
+ # {"goals": int, "cron": int, "review": bool}. None/empty → line hidden.
124
+ # NEVER read the goal/cron SQLite stores on the hot repaint path.
125
+ self.autonomy_snapshot: Optional[dict] = None
126
+
127
+ # poll_usage() bookkeeping: filenames already aggregated so each poll
128
+ # only reads NEW llm_usage files (cheap, incremental).
129
+ self._seen_usage_files: set[str] = set()
130
+
131
+ # ------------------------------------------------------------------
132
+ # Live sub-agent counter (fed by the REPL's orchestrator hooks)
133
+ # ------------------------------------------------------------------
134
+
135
+ def note_subagent_start(self) -> None:
136
+ """A delegate_task sub-agent began (orchestrator start hook)."""
137
+ self.subagents_active += 1
138
+
139
+ def note_subagent_end(self) -> None:
140
+ """A delegate_task sub-agent finished (orchestrator end hook). Floored at 0."""
141
+ self.subagents_active = max(0, self.subagents_active - 1)
142
+
143
+ # ------------------------------------------------------------------
144
+ # Push: event accumulation
145
+ # ------------------------------------------------------------------
146
+
147
+ def update(self, event: RenderEvent) -> None:
148
+ """Accumulate state from one ``RenderEvent``."""
149
+ # Extension seam (D2): a registered event mutates state via its spec's
150
+ # ``apply`` — no isinstance branch needed here for new core events.
151
+ from cli.ui.event_registry import RegisteredEvent, get_spec
152
+ if isinstance(event, RegisteredEvent):
153
+ spec = get_spec(event.type)
154
+ if spec is not None:
155
+ spec.apply(self, event)
156
+ return
157
+
158
+ # NOTE: feed events NEVER drive ``status`` — the TurnLifecycle (begin_turn/
159
+ # end_turn at the turn seam) is the single source of truth for the status
160
+ # word + work clock. A background autonomy turn emits the same feed events
161
+ # as a user turn, so letting them set ``status`` is exactly what flipped the
162
+ # idle bar to "running". Events here only accumulate counters/metadata.
163
+ if isinstance(event, SessionStart):
164
+ if event.model_name:
165
+ self.model = event.model_name
166
+
167
+ elif isinstance(event, LLMCall):
168
+ if event.provider and not self.provider:
169
+ self.provider = event.provider
170
+ if event.model_name and not self.model:
171
+ self.model = event.model_name
172
+ if event.prompt_tokens is not None:
173
+ self.tokens_in += event.prompt_tokens
174
+ if event.completion_tokens is not None:
175
+ self.tokens_out += event.completion_tokens
176
+ if event.token_count is not None:
177
+ self.tokens_total += event.token_count
178
+ if event.cost_estimate is not None:
179
+ self.cost_estimate_total += event.cost_estimate
180
+
181
+ elif isinstance(event, Step):
182
+ self.step = max(self.step, event.step)
183
+ data = event.raw.get("data", {}) or {}
184
+ identity = str(
185
+ data.get("agent_name")
186
+ or event.raw.get("agent_name")
187
+ or data.get("agent_id")
188
+ or event.raw.get("agent_id")
189
+ or ""
190
+ )
191
+ self.last_step_sub_agent = self.is_sub_agent(identity)
192
+
193
+ elif isinstance(event, ToolExec):
194
+ self.tool_calls += 1
195
+ if event.action_name:
196
+ self.last_tool = event.action_name
197
+
198
+ elif isinstance(event, ErrorEvent):
199
+ self.errors += 1
200
+
201
+ elif isinstance(event, IterationDone):
202
+ # No status write — the lifecycle settles the turn on respond() return,
203
+ # not on a feed "done" event (so a planning-only turn still settles).
204
+ pass
205
+
206
+ elif isinstance(event, SessionDone):
207
+ # Success/failure colors the turn-summary residue via the renderer's
208
+ # event buffer (turn_failed()), not a status write here.
209
+ pass
210
+
211
+ elif isinstance(event, AgentRegistration):
212
+ # First registration = main agent (used for sub-agent grouping).
213
+ # Store both id and name: step events carry only agent_name, so
214
+ # identity comparison must work on names too.
215
+ if not self.main_agent_id and event.agent_id:
216
+ self.main_agent_id = event.agent_id
217
+ if not self.main_agent_name and event.agent_name:
218
+ self.main_agent_name = event.agent_name
219
+ if event.model_name and not self.model:
220
+ self.model = event.model_name
221
+
222
+ elif isinstance(event, AgentEnd):
223
+ # Informational; sub-agent failures bump the error count.
224
+ if event.errors:
225
+ self.errors += len(event.errors)
226
+
227
+ def is_sub_agent(self, identity: str) -> bool:
228
+ """True if *identity* belongs to a sub-agent (not the main agent).
229
+
230
+ *identity* may be an agent_id OR an agent_name — a match on either
231
+ is treated as "this is the main agent". Step events from the real
232
+ ``AgentStepFormatter`` carry only ``agent_name`` (never ``agent_id``),
233
+ so name-based comparison is the primary path.
234
+
235
+ Before any registration is seen (both ``main_agent_id`` and
236
+ ``main_agent_name`` empty) nothing is treated as a sub-agent.
237
+ """
238
+ if not identity:
239
+ return False
240
+ if not self.main_agent_id and not self.main_agent_name:
241
+ return False
242
+ # Match on id OR name — whichever the caller supplies.
243
+ if self.main_agent_id and identity == self.main_agent_id:
244
+ return False
245
+ if self.main_agent_name and identity == self.main_agent_name:
246
+ return False
247
+ return True
248
+
249
+ # ------------------------------------------------------------------
250
+ # Pull: live metrics from the agent's message_manager
251
+ # ------------------------------------------------------------------
252
+
253
+ def reset_after_clear(self) -> None:
254
+ """Reset per-conversation live metrics after /clear wiped the history.
255
+
256
+ The context window + step counter are now stale, so zero them (and the
257
+ per-turn tallies). Cumulative session tokens/cost are PRESERVED (real spend)
258
+ so /status still reflects the session's total.
259
+ """
260
+ self.step = 0
261
+ self.tool_calls = 0
262
+ self.errors = 0
263
+ self.ctx_percent = 0.0
264
+ self.ctx_tokens = 0
265
+ self.compactions = 0
266
+
267
+ def poll(self, agent: Any) -> None:
268
+ """Pull live context-window metrics from *agent*'s message_manager.
269
+
270
+ *agent* must expose:
271
+ agent.message_manager.get_context_usage_percent() -> float
272
+ agent.message_manager.history.total_tokens -> int
273
+ agent.message_manager.max_input_tokens -> int
274
+ agent.message_manager._compaction_count -> int
275
+
276
+ Missing attributes are silently ignored (the call is best-effort).
277
+
278
+ Args:
279
+ agent: A running ``Agent`` instance (or any object with the
280
+ expected ``message_manager`` attributes).
281
+ """
282
+ try:
283
+ mm = agent.message_manager
284
+ except Exception:
285
+ return
286
+
287
+ try:
288
+ self.ctx_percent = mm.get_context_usage_percent()
289
+ except Exception:
290
+ pass
291
+
292
+ try:
293
+ self.ctx_tokens = mm.history.total_tokens
294
+ except Exception:
295
+ pass
296
+
297
+ try:
298
+ self.ctx_max = mm.max_input_tokens
299
+ except Exception:
300
+ pass
301
+
302
+ try:
303
+ self.compactions = mm._compaction_count
304
+ except Exception:
305
+ pass
306
+
307
+ # ------------------------------------------------------------------
308
+ # Pull: authoritative live tokens/cost from the session's llm_usage dir
309
+ # ------------------------------------------------------------------
310
+
311
+ def poll_usage(self, session_dir: Path) -> None:
312
+ """Aggregate tokens + cost from new ``data/llm_usage/*.json`` files.
313
+
314
+ This is the LIVE source of tokens/cost in the local CLI path:
315
+ ``capture_llm_usage`` skips the push feed (to avoid double-counting)
316
+ and instead writes one record per LLM call to
317
+ ``<session_dir>/data/llm_usage/llm_usage_<ts>.json`` with the same
318
+ shape the formatter uses (``prompt_tokens``, ``completion_tokens``,
319
+ ``token_count``, ``cost_estimate``).
320
+
321
+ Each call only reads files not seen on a previous poll, so it is cheap
322
+ to call every turn (and on the toolbar timer). Best-effort: malformed
323
+ or partially-written files are skipped silently.
324
+
325
+ Args:
326
+ session_dir: The session root (containing ``data/llm_usage/``).
327
+ """
328
+ try:
329
+ usage_dir = Path(session_dir) / "data" / "llm_usage"
330
+ if not usage_dir.is_dir():
331
+ return
332
+ for path in sorted(usage_dir.glob("llm_usage_*.json")):
333
+ name = path.name
334
+ if name in self._seen_usage_files:
335
+ continue
336
+ # Mark as seen up-front: a file that fails to parse once (e.g.
337
+ # mid-write) is unlikely to be the live source of truth, and we
338
+ # never want to re-add its tokens on a later poll.
339
+ self._seen_usage_files.add(name)
340
+ try:
341
+ record = json.loads(path.read_text(encoding="utf-8"))
342
+ except (json.JSONDecodeError, OSError):
343
+ continue
344
+ self._apply_usage_record(record)
345
+ except Exception:
346
+ # poll_usage must never raise into the render loop.
347
+ return
348
+
349
+ def _apply_usage_record(self, record: dict[str, Any]) -> None:
350
+ """Add one llm_usage record's tokens + cost into the running totals."""
351
+ prompt = record.get("prompt_tokens")
352
+ completion = record.get("completion_tokens")
353
+ total = record.get("token_count")
354
+ cost = record.get("cost_estimate")
355
+ if isinstance(prompt, (int, float)):
356
+ self.tokens_in += int(prompt)
357
+ if isinstance(completion, (int, float)):
358
+ self.tokens_out += int(completion)
359
+ if isinstance(total, (int, float)):
360
+ self.tokens_total += int(total)
361
+ if isinstance(cost, (int, float)):
362
+ self.cost_estimate_total += float(cost)
363
+ provider = record.get("provider")
364
+ if provider and not self.provider:
365
+ self.provider = str(provider)
366
+ model = record.get("model_name")
367
+ if model and not self.model:
368
+ self.model = str(model)
369
+
370
+ # ------------------------------------------------------------------
371
+ # Derived helpers
372
+ # ------------------------------------------------------------------
373
+
374
+ def elapsed(self) -> float:
375
+ """Seconds elapsed since this ``SessionState`` was constructed."""
376
+ return time.monotonic() - self.started_at
cli/ui/statusbar.py ADDED
@@ -0,0 +1,231 @@
1
+ """statusbar.py — bottom-toolbar line builder for the POLYROB CLI (Phase 2).
2
+
3
+ Pure builders (no I/O) that turn a ``SessionState`` into the bottom status
4
+ line shown by prompt_toolkit's ``bottom_toolbar``:
5
+
6
+ {model} · {in}↑ {out}↓ · ctx {ctx}% · ${cost} · {elapsed} · {spinner}{status}
7
+
8
+ Two surfaces:
9
+ - ``status_text(state)`` — a plain ``str`` (used by the PlainRenderer and
10
+ by tests; deterministic).
11
+ - ``status_formatted(state, spinner_frame)`` — prompt_toolkit
12
+ ``FormattedText`` (used by the live toolbar). Importing prompt_toolkit is
13
+ deferred so the plain path has no hard dependency.
14
+
15
+ The spinner frame is supplied by the caller (the toolbar ticks it on a timer)
16
+ so this module stays pure and time-independent.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import TYPE_CHECKING, List, Tuple
22
+
23
+ from cli.ui.theme import fmt_tokens, ICONS
24
+ from cli.ui.state import SessionState
25
+
26
+ if TYPE_CHECKING: # pragma: no cover - typing only
27
+ from prompt_toolkit.formatted_text import FormattedText
28
+
29
+
30
+ def _fmt_elapsed(seconds: float) -> str:
31
+ seconds = max(0.0, seconds)
32
+ if seconds < 60:
33
+ return f"{seconds:.0f}s"
34
+ minutes, secs = divmod(int(seconds), 60)
35
+ if minutes < 60:
36
+ return f"{minutes}m{secs:02d}s"
37
+ hours, minutes = divmod(minutes, 60)
38
+ return f"{hours}h{minutes:02d}m"
39
+
40
+
41
+ def _autonomy_active(state: SessionState) -> bool:
42
+ """True when a background (cron/goal/self-wake) turn is in flight.
43
+
44
+ Orthogonal to the foreground work clock/status — a background turn lights this
45
+ muted ``⟲ autonomy`` token WITHOUT touching the user's work clock or status word
46
+ (a user who walked away never sees a runaway 'working' for autonomous work).
47
+ """
48
+ lifecycle = getattr(state, "lifecycle", None)
49
+ return bool(lifecycle is not None and lifecycle.autonomy_busy())
50
+
51
+
52
+ #: The Claude-Code-style cooking glyph used when no animated spinner is supplied.
53
+ _COOKING_GLYPH = "✱ "
54
+
55
+
56
+ def _is_active(state: SessionState) -> bool:
57
+ """True when a user turn is in flight (drives the cooking affordance)."""
58
+ lifecycle = getattr(state, "lifecycle", None)
59
+ return bool(lifecycle is not None and lifecycle.is_active())
60
+
61
+
62
+ def cooking_text(state: SessionState, spinner: str = "") -> str:
63
+ """The live in-flight affordance: ``✱ cooking… 7s`` (Option A, issue 1).
64
+
65
+ Rendered in the pinned region while the turn runs, replacing the bare status
66
+ word + a separate elapsed segment. The ticking work clock
67
+ (``lifecycle.active_elapsed``, frozen on idle so it shows work time not session
68
+ age) is the liveness; an animated braille *spinner* (supplied by the persistent
69
+ app at 5 Hz) is used as the pulse glyph when available, else a static ``✱``.
70
+ """
71
+ lifecycle = getattr(state, "lifecycle", None)
72
+ elapsed = lifecycle.active_elapsed() if lifecycle is not None else 0.0
73
+ glyph = spinner if spinner else _COOKING_GLYPH
74
+ return f"{glyph}cooking… {_fmt_elapsed(elapsed)}"
75
+
76
+
77
+ def _segments(state: SessionState, spinner: str = "") -> List[str]:
78
+ """The ordered list of status segments (joined by ' · ')."""
79
+ segs: List[str] = []
80
+
81
+ model = state.model or "—"
82
+ segs.append(model)
83
+
84
+ tok = f"{fmt_tokens(state.tokens_in)}{ICONS.up} {fmt_tokens(state.tokens_out)}{ICONS.down}"
85
+ segs.append(tok)
86
+
87
+ # Live-info (D3): in-flight tool + active sub-agent count. The tool is the
88
+ # CURRENT action — shown only while a turn runs so it doesn't linger at the
89
+ # idle prompt.
90
+ last_tool = getattr(state, "last_tool", "")
91
+ if last_tool and _is_active(state):
92
+ segs.append(f"{ICONS.arrow}{last_tool}")
93
+ n_sub = getattr(state, "subagents_active", 0)
94
+ if n_sub:
95
+ segs.append(f"{n_sub} sub-agent{'s' if n_sub != 1 else ''}")
96
+
97
+ if _autonomy_active(state):
98
+ segs.append("⟲ autonomy")
99
+
100
+ if state.ctx_percent:
101
+ segs.append(f"ctx {state.ctx_percent:.0f}%")
102
+
103
+ segs.append(f"${state.cost_estimate_total:.4f}")
104
+
105
+ # Tail: while a turn runs, ONE Claude-style ``✱ cooking… Xs`` affordance (work
106
+ # clock folded in); when idle, the plain status word (ready/error/stopped) — no
107
+ # clock, so the bar never counts session age at the prompt.
108
+ if _is_active(state):
109
+ segs.append(cooking_text(state, spinner))
110
+ else:
111
+ status = state.status or ""
112
+ word = f"{spinner}{status}".strip()
113
+ if word:
114
+ segs.append(word)
115
+
116
+ return segs
117
+
118
+
119
+ def autonomy_line(state: SessionState) -> str:
120
+ """The second pinned line: model/provider + autonomy snapshot (D4).
121
+
122
+ ``glm-5.2 · openrouter autonomy: goals 1 · cron 2 · review on`` — built
123
+ from ``state.autonomy_snapshot`` (a slow-polled, cached dict; never read the
124
+ goal/cron SQLite stores on the hot repaint path). Returns "" when there is no
125
+ snapshot, so the line is hidden until autonomy data is available. Pure.
126
+ """
127
+ snap = getattr(state, "autonomy_snapshot", None)
128
+ if not snap:
129
+ return ""
130
+ auto: List[str] = []
131
+ goals = int(snap.get("goals", 0) or 0)
132
+ cron = int(snap.get("cron", 0) or 0)
133
+ if goals:
134
+ auto.append(f"goals {goals}")
135
+ if cron:
136
+ auto.append(f"cron {cron}")
137
+ if snap.get("review"):
138
+ auto.append("review on")
139
+ left = state.model or "—"
140
+ if state.provider:
141
+ left = f"{left} {ICONS.bullet} {state.provider}"
142
+ if not auto:
143
+ return left
144
+ return f"{left} autonomy: " + f" {ICONS.bullet} ".join(auto)
145
+
146
+
147
+ def status_text(state: SessionState, spinner: str = "") -> str:
148
+ """Build the plain-text status line (no ANSI)."""
149
+ return f" {ICONS.bullet} ".join(s for s in _segments(state, spinner) if s)
150
+
151
+
152
+ # ---------------------------------------------------------------------------
153
+ # prompt_toolkit FormattedText surface
154
+ # ---------------------------------------------------------------------------
155
+
156
+
157
+ def _status_class(status: str) -> str:
158
+ """Map a status word to a prompt_toolkit class name for styling.
159
+
160
+ Lifecycle vocabulary is working / ready / error / stopped; legacy words
161
+ (running/completed/done/failed) are still mapped for any injected/test state.
162
+ """
163
+ s = (status or "").lower()
164
+ if s in ("error", "failed", "stopped"):
165
+ return "class:status.error"
166
+ if s in ("ready", "completed", "done"):
167
+ return "class:status.ok"
168
+ return "class:status.running"
169
+
170
+
171
+ def status_formatted(
172
+ state: SessionState, spinner: str = "", *, include_model: bool = True
173
+ ) -> "FormattedText":
174
+ """Build a prompt_toolkit ``FormattedText`` status toolbar.
175
+
176
+ Each segment carries a class so the toolbar can be themed; the final
177
+ status word is colored by ``_status_class``. ``include_model=False`` omits
178
+ the leading model segment (the framed persistent input shows the model on
179
+ the box's top edge, so the status bar would otherwise repeat it).
180
+ """
181
+ from prompt_toolkit.formatted_text import FormattedText
182
+
183
+ sep = f" {ICONS.bullet} "
184
+ fragments: List[Tuple[str, str]] = [("", " ")]
185
+
186
+ if include_model:
187
+ model = state.model or "—"
188
+ fragments.append(("class:toolbar.model", model))
189
+ fragments.append(("", sep))
190
+
191
+ fragments.append(
192
+ (
193
+ "class:toolbar.tokens",
194
+ f"{fmt_tokens(state.tokens_in)}{ICONS.up} "
195
+ f"{fmt_tokens(state.tokens_out)}{ICONS.down}",
196
+ )
197
+ )
198
+ fragments.append(("", sep))
199
+
200
+ # Live-info (D3): in-flight tool (only while a turn runs) + sub-agent count.
201
+ last_tool = getattr(state, "last_tool", "")
202
+ if last_tool and _is_active(state):
203
+ fragments.append(("class:toolbar.tool", f"{ICONS.arrow}{last_tool}"))
204
+ fragments.append(("", sep))
205
+ n_sub = getattr(state, "subagents_active", 0)
206
+ if n_sub:
207
+ fragments.append(
208
+ ("class:toolbar.subagents", f"{n_sub} sub-agent{'s' if n_sub != 1 else ''}")
209
+ )
210
+ fragments.append(("", sep))
211
+
212
+ if _autonomy_active(state):
213
+ fragments.append(("class:toolbar.autonomy", "⟲ autonomy"))
214
+ fragments.append(("", sep))
215
+
216
+ if state.ctx_percent:
217
+ fragments.append(("class:toolbar.ctx", f"ctx {state.ctx_percent:.0f}%"))
218
+ fragments.append(("", sep))
219
+
220
+ fragments.append(("class:toolbar.cost", f"${state.cost_estimate_total:.4f}"))
221
+ fragments.append(("", sep))
222
+
223
+ if _is_active(state):
224
+ # The live cooking affordance (work clock folded in) — class:toolbar.elapsed
225
+ # so the seconds read as the running clock.
226
+ fragments.append(("class:toolbar.elapsed", cooking_text(state, spinner)))
227
+ else:
228
+ status_word = f"{spinner}{state.status}".strip()
229
+ fragments.append((_status_class(state.status), status_word))
230
+
231
+ return FormattedText(fragments)