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/app.py ADDED
@@ -0,0 +1,538 @@
1
+ """app.py — prompt_toolkit PromptSession setup for the POLYROB CLI (Phase 2).
2
+
3
+ Builds the interactive input surface:
4
+ - ``PromptSession`` with ``FileHistory(~/.polyrob/history)``.
5
+ - A ``bottom_toolbar`` callable that pulls live metrics from ``SessionState``
6
+ (via ``statusbar.status_formatted``) and ticks a spinner.
7
+ - Key bindings:
8
+ * ``Enter`` → submit
9
+ * ``Meta+Enter`` (Alt/Esc-Enter) → insert newline (multi-line input)
10
+ * ``Ctrl-C`` → interrupt the current turn (keeps the loop)
11
+ * ``Ctrl-D`` → exit
12
+ - ``prompt_async()`` for the input read (the REPL already runs under asyncio —
13
+ we do NOT start a second loop; proposal §14).
14
+
15
+ Slash autocomplete is Phase 4 — a ``completer`` seam is exposed (param) but no
16
+ registry is built here.
17
+
18
+ Everything is factored so the prompt_toolkit objects can be constructed
19
+ headlessly in tests (no TTY needed); only ``prompt_async`` requires a terminal.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import time
25
+ from pathlib import Path
26
+ from typing import TYPE_CHECKING, Any, Callable, Optional
27
+
28
+ from cli.ui import statusbar
29
+ from cli.ui.state import SessionState
30
+ from cli.ui.theme import ICONS, SPINNER_FRAMES
31
+
32
+ if TYPE_CHECKING: # pragma: no cover - typing only
33
+ from prompt_toolkit import PromptSession
34
+ from prompt_toolkit.completion import Completer
35
+ from prompt_toolkit.key_binding import KeyBindings
36
+
37
+
38
+ from core.paths import polyrob_home
39
+ DEFAULT_HISTORY_PATH = polyrob_home() / "history"
40
+
41
+
42
+ def default_history_path() -> Path:
43
+ """Return (and ensure the parent of) the default history file path."""
44
+ DEFAULT_HISTORY_PATH.parent.mkdir(parents=True, exist_ok=True)
45
+ return DEFAULT_HISTORY_PATH
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Key bindings
50
+ # ---------------------------------------------------------------------------
51
+
52
+
53
+ def build_key_bindings() -> "KeyBindings":
54
+ """Construct the REPL key bindings.
55
+
56
+ - ``Enter`` submits the buffer.
57
+ - ``Meta+Enter`` (``escape, enter``) inserts a newline (multi-line input).
58
+ - ``Ctrl-C`` raises ``KeyboardInterrupt`` so the loop can interrupt the
59
+ current turn without exiting.
60
+ - ``Ctrl-D`` on an empty buffer raises ``EOFError`` to exit.
61
+ """
62
+ from prompt_toolkit.key_binding import KeyBindings
63
+
64
+ kb = KeyBindings()
65
+
66
+ @kb.add("enter")
67
+ def _(event: Any) -> None:
68
+ event.current_buffer.validate_and_handle()
69
+
70
+ @kb.add("escape", "enter")
71
+ def _(event: Any) -> None:
72
+ event.current_buffer.insert_text("\n")
73
+
74
+ @kb.add("c-c")
75
+ def _(event: Any) -> None:
76
+ # Surface as KeyboardInterrupt; the REPL catches it and keeps looping.
77
+ event.app.exit(exception=KeyboardInterrupt)
78
+
79
+ @kb.add("c-d")
80
+ def _(event: Any) -> None:
81
+ if not event.current_buffer.text:
82
+ event.app.exit(exception=EOFError)
83
+
84
+ return kb
85
+
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # Distinctive input frame (prompt fragments + rprompt)
89
+ # ---------------------------------------------------------------------------
90
+
91
+ #: The left-edge bar glyph shared by the prompt and the bottom toolbar, so the
92
+ #: input region reads as one bordered area (a true 4-sided box isn't possible
93
+ #: for an INLINE prompt_toolkit prompt without going full-screen, which would
94
+ #: forfeit native scrollback — see the CLI review).
95
+ BORDER_BAR = "▌"
96
+ PROMPT_CARET = "❯"
97
+
98
+ #: Max rows the thin input grows to before it stops expanding (a long paste can't
99
+ #: swallow the whole terminal; the buffer still scrolls internally past this).
100
+ MAX_INPUT_ROWS = 10
101
+
102
+
103
+ def input_height_dimension(line_count: int, max_rows: int = MAX_INPUT_ROWS) -> Any:
104
+ """Height for the thin growing input: 1 row when empty, +1 per content line,
105
+ clamped to ``max_rows`` (bug B).
106
+
107
+ The input is NOT a fixed-height box — it reserves exactly one row when empty
108
+ and grows as the user types/pastes newlines, the way Claude Code's prompt
109
+ does. Pure (no TTY) so the growth policy is unit-testable.
110
+ """
111
+ from prompt_toolkit.layout.dimension import Dimension
112
+
113
+ preferred = min(max(1, line_count), max_rows)
114
+ return Dimension(min=1, max=max_rows, preferred=preferred)
115
+
116
+
117
+ def separator_label(state: SessionState) -> str:
118
+ """The ``rob · model · provider`` label shown on the thin separator rule.
119
+
120
+ Replaces the old 4-sided ``Frame`` title — the agent identity + active model
121
+ sit on the rule above the input instead of a heavy box edge. Pure.
122
+ """
123
+ from cli.ui.identity import agent_display_name
124
+ name = agent_display_name()
125
+ model = (state.model or "").strip()
126
+ provider = (state.provider or "").strip()
127
+ label = name
128
+ if model:
129
+ label = f"{name} {ICONS.bullet} {model}"
130
+ if provider:
131
+ label = f"{label} {ICONS.bullet} {provider}"
132
+ return label
133
+
134
+
135
+ def build_prompt_fragments(state: SessionState, *, prompt_text: Optional[str] = None) -> Any:
136
+ """Build the REPL prompt as themeable prompt_toolkit ``FormattedText``.
137
+
138
+ Default: a distinctive left-border bar + caret (``▌ ❯ ``) so the input line
139
+ is visually distinct from the conversation scrollback. ``prompt_text`` forces
140
+ a literal string (kept for back-compat / tests). Pure — snapshot-testable
141
+ without a TTY.
142
+ """
143
+ from prompt_toolkit.formatted_text import FormattedText
144
+
145
+ if prompt_text is not None:
146
+ return FormattedText([("", prompt_text)])
147
+ return FormattedText(
148
+ [
149
+ ("class:prompt.border", BORDER_BAR),
150
+ ("class:prompt.caret", f" {PROMPT_CARET} "),
151
+ ]
152
+ )
153
+
154
+
155
+ def build_rprompt(state: SessionState) -> Any:
156
+ """Build the right-aligned ``model · provider`` affordance (Cursor-style).
157
+
158
+ Returns empty ``FormattedText`` until a model is known (set after the first
159
+ LLM call), so the prompt isn't cluttered at session start. Pure.
160
+ """
161
+ from prompt_toolkit.formatted_text import FormattedText
162
+
163
+ model = (state.model or "").strip()
164
+ provider = (state.provider or "").strip()
165
+ if not model and not provider:
166
+ return FormattedText([])
167
+ label = model or "—"
168
+ if provider:
169
+ label = f"{label} {ICONS.bullet} {provider}"
170
+ return FormattedText([("class:prompt.rprompt", f" {label} ")])
171
+
172
+
173
+ # ---------------------------------------------------------------------------
174
+ # Bottom toolbar
175
+ # ---------------------------------------------------------------------------
176
+
177
+
178
+ def make_bottom_toolbar(
179
+ state: SessionState,
180
+ *,
181
+ clock: Callable[[], float] = time.monotonic,
182
+ ) -> Callable[[], Any]:
183
+ """Return a ``bottom_toolbar`` callable that renders live status.
184
+
185
+ The callable is invoked by prompt_toolkit on every repaint; it ticks a
186
+ spinner frame from the monotonic clock and delegates formatting to
187
+ ``statusbar.status_formatted``. A leading border bar makes the toolbar read
188
+ as the bottom edge of the framed input region (matching the prompt's bar).
189
+ """
190
+ from prompt_toolkit.formatted_text import FormattedText
191
+
192
+ def _toolbar() -> Any:
193
+ frame_idx = int(clock() * 5) % len(SPINNER_FRAMES)
194
+ spinner = ""
195
+ # Spinner is gated on the lifecycle being active (a turn in flight), NOT on
196
+ # the status string — so it can't animate while idle and survives any
197
+ # status-vocabulary change. Defensive getattr for a lifecycle-less state.
198
+ _lc = getattr(state, "lifecycle", None)
199
+ if _lc is not None and _lc.is_active():
200
+ spinner = SPINNER_FRAMES[frame_idx] + " "
201
+ status = statusbar.status_formatted(state, spinner=spinner)
202
+ return FormattedText(
203
+ [("class:prompt.border", BORDER_BAR)] + list(status)
204
+ )
205
+
206
+ return _toolbar
207
+
208
+
209
+ def toolbar_style():
210
+ """Return the prompt_toolkit ``Style`` for the bottom toolbar classes."""
211
+ from prompt_toolkit.styles import Style
212
+
213
+ return Style.from_dict(
214
+ {
215
+ "bottom-toolbar": "noreverse",
216
+ "toolbar.model": "bold",
217
+ "toolbar.tokens": "",
218
+ "toolbar.ctx": "",
219
+ "toolbar.cost": "",
220
+ "toolbar.elapsed": "",
221
+ "toolbar.autonomy": "ansibrightblack",
222
+ "status.ok": "ansigreen",
223
+ "status.running": "ansiyellow",
224
+ "status.error": "ansired",
225
+ # Distinctive input frame.
226
+ "prompt.border": "ansicyan bold",
227
+ "prompt.caret": "ansicyan bold",
228
+ "prompt.rprompt": "ansibrightblack",
229
+ "prompt.hint": "ansibrightblack",
230
+ "prompt.frame.title": "ansicyan bold",
231
+ # The bordered input box (opencode-style) — cyan edge, default body.
232
+ "frame.border": "ansicyan",
233
+ "frame.label": "ansicyan bold",
234
+ # Arrow-key model selector (cli/ui/model_selector.py).
235
+ "picker.hint": "ansibrightblack",
236
+ "picker.note": "ansiyellow",
237
+ "picker.group": "ansicyan bold",
238
+ "picker.row": "",
239
+ "picker.sel": "reverse bold",
240
+ }
241
+ )
242
+
243
+
244
+ # ---------------------------------------------------------------------------
245
+ # PromptSession
246
+ # ---------------------------------------------------------------------------
247
+
248
+
249
+ def build_prompt_session(
250
+ state: SessionState,
251
+ *,
252
+ history_path: Optional[Path] = None,
253
+ completer: "Optional[Completer]" = None,
254
+ prompt_text: Optional[str] = None,
255
+ clock: Callable[[], float] = time.monotonic,
256
+ ) -> "PromptSession":
257
+ """Construct the REPL ``PromptSession`` (no TTY required to build it).
258
+
259
+ Args:
260
+ state: Shared ``SessionState`` (drives prompt/rprompt/toolbar).
261
+ history_path: FileHistory path (default ``~/.polyrob/history``).
262
+ completer: Optional prompt_toolkit ``Completer`` (Phase 4 seam).
263
+ prompt_text: Force a literal prompt prefix; ``None`` (default) uses the
264
+ distinctive framed ``▌ ❯`` prompt + live ``model · provider``
265
+ rprompt.
266
+ clock: Monotonic clock (injectable for tests).
267
+ """
268
+ from prompt_toolkit import PromptSession
269
+ from prompt_toolkit.history import FileHistory
270
+
271
+ hpath = history_path or default_history_path()
272
+ hpath.parent.mkdir(parents=True, exist_ok=True)
273
+
274
+ # Callables so the prompt frame + rprompt repaint live (the model/provider
275
+ # become known only after the first LLM call).
276
+ return PromptSession(
277
+ message=lambda: build_prompt_fragments(state, prompt_text=prompt_text),
278
+ rprompt=lambda: build_rprompt(state),
279
+ history=FileHistory(str(hpath)),
280
+ key_bindings=build_key_bindings(),
281
+ bottom_toolbar=make_bottom_toolbar(state, clock=clock),
282
+ completer=completer,
283
+ complete_while_typing=False,
284
+ multiline=False,
285
+ style=toolbar_style(),
286
+ refresh_interval=0.2, # ~5 Hz toolbar repaint for the spinner/elapsed
287
+ )
288
+
289
+
290
+ def make_prompt_reader(session: "PromptSession") -> Callable[[], Any]:
291
+ """Adapt a ``PromptSession`` into the REPL's ``read_line`` async seam.
292
+
293
+ Returns a zero-arg coroutine that reads one line via ``prompt_async`` and
294
+ raises ``EOFError`` on Ctrl-D (so the REPL loop breaks cleanly).
295
+ """
296
+
297
+ async def read_line() -> str:
298
+ try:
299
+ return await session.prompt_async()
300
+ except EOFError:
301
+ raise
302
+ # KeyboardInterrupt propagates to the loop, which keeps looping.
303
+
304
+ return read_line
305
+
306
+
307
+ # ---------------------------------------------------------------------------
308
+ # Persistent bottom-anchored Application (D4/D5 — gated POLYROB_PERSISTENT_INPUT)
309
+ # ---------------------------------------------------------------------------
310
+
311
+
312
+ def persistent_input_enabled() -> bool:
313
+ """True when the persistent bottom-anchored input is active (default ON).
314
+
315
+ The persistent ``Application`` keeps the input box PINNED at the bottom and
316
+ LIVE during a turn (status/spinner repaint, tool output scrolls above) — the
317
+ Claude-Code / opencode experience. It is the default for an interactive TTY;
318
+ set ``POLYROB_PERSISTENT_INPUT=0`` (or ``off``/``false``/``no``) to force the
319
+ legacy ephemeral ``prompt_async`` path. Non-TTY / ``--plain`` always use the
320
+ legacy path regardless (the caller gates on ``is_tty`` + ``not plain``), and a
321
+ build/start failure fails open to it too.
322
+ """
323
+ import os
324
+
325
+ return os.environ.get("POLYROB_PERSISTENT_INPUT", "1").strip().lower() not in (
326
+ "0", "false", "no", "off", "",
327
+ )
328
+
329
+
330
+ def build_app(
331
+ state: SessionState,
332
+ *,
333
+ on_submit: Callable[[str], Any],
334
+ completer: "Optional[Completer]" = None,
335
+ on_interrupt: Optional[Callable[[], None]] = None,
336
+ clock: Callable[[], float] = time.monotonic,
337
+ output: Any = None,
338
+ input: Any = None,
339
+ ) -> Any:
340
+ """Build the persistent, NON-full-screen bottom-anchored ``Application``.
341
+
342
+ The layout reserves only the bottom region — an input window (``▌ ❯``), a live
343
+ status window, and a conditional autonomy window. Content (agent messages,
344
+ summaries) keeps scrolling natively ABOVE via ``run_in_terminal`` (so native
345
+ scrollback is preserved — this is NOT a full-screen transcript app). Because
346
+ the ``Application`` runs continuously, the status repaints LIVE during a turn
347
+ (the toolbar-freeze is gone).
348
+
349
+ Args:
350
+ state: Shared ``SessionState`` (drives the status/autonomy lines).
351
+ on_submit: ``(text) -> None`` called on Enter for a non-blank line. The
352
+ caller schedules the turn as a background task.
353
+ completer: Optional slash completer.
354
+ on_interrupt: Optional ``() -> None`` called on Ctrl-C (cancel the turn).
355
+ clock: Monotonic clock (spinner tick; injectable for tests).
356
+ output/input: prompt_toolkit Output/Input (DummyOutput/pipe in tests).
357
+
358
+ Returns ``(app, input_buffer)``. Buildable headlessly (no TTY); ``app.run_async``
359
+ requires a terminal.
360
+ """
361
+ from prompt_toolkit.application import Application
362
+ from prompt_toolkit.buffer import Buffer
363
+ from prompt_toolkit.filters import Condition
364
+ from prompt_toolkit.formatted_text import FormattedText
365
+ from prompt_toolkit.key_binding import merge_key_bindings
366
+ from prompt_toolkit.layout import HSplit, Layout, VSplit, Window
367
+ from prompt_toolkit.layout.containers import ConditionalContainer
368
+ from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
369
+
370
+ from cli.ui.model_selector import ReplPicker
371
+
372
+ def _accept(buff: Any) -> bool:
373
+ text = buff.text
374
+ if text.strip():
375
+ on_submit(text)
376
+ buff.reset() # clear the input after submit
377
+ return False # don't keep the text in the buffer
378
+
379
+ input_buffer = Buffer(
380
+ multiline=False,
381
+ accept_handler=_accept,
382
+ completer=completer,
383
+ complete_while_typing=False,
384
+ )
385
+
386
+ # The arrow-key model selector (/model) renders as a conditional list ABOVE
387
+ # the input and borrows this buffer as its search field while open. It runs
388
+ # entirely on the event loop (resolved via a Future), so it never blocks the
389
+ # REPL or routes through patch_stdout's StdoutProxy (the old picker's bug).
390
+ picker = ReplPicker(input_buffer)
391
+ input_buffer.on_text_changed += lambda _: picker.on_search_changed()
392
+
393
+ def _caret_prefix() -> Any:
394
+ # The ❯ caret prefixes every input row (thin design — no box border).
395
+ return FormattedText([("class:prompt.caret", f"{PROMPT_CARET} ")])
396
+
397
+ def _separator_label() -> Any:
398
+ # The agent name + active model sit on the thin rule above the input —
399
+ # the heavy 4-sided Frame is gone (bug B).
400
+ return FormattedText(
401
+ [("class:prompt.frame.title", f"─ {separator_label(state)} ")]
402
+ )
403
+
404
+ def _input_height() -> Any:
405
+ # 1 row when empty; grows with typed/pasted newlines; clamped.
406
+ return input_height_dimension(input_buffer.document.line_count)
407
+
408
+ def _status() -> Any:
409
+ frame_idx = int(clock() * 5) % len(SPINNER_FRAMES)
410
+ spinner = ""
411
+ # Spinner is gated on the lifecycle being active (a turn in flight), NOT on
412
+ # the status string — so it can't animate while idle and survives any
413
+ # status-vocabulary change. Defensive getattr for a lifecycle-less state.
414
+ _lc = getattr(state, "lifecycle", None)
415
+ if _lc is not None and _lc.is_active():
416
+ spinner = SPINNER_FRAMES[frame_idx] + " "
417
+ # Model lives on the box's top edge (the frame title), so omit it here.
418
+ status = statusbar.status_formatted(state, spinner=spinner, include_model=False)
419
+ # Indent one space so the status bar aligns under the box body.
420
+ return FormattedText([("", " ")] + list(status))
421
+
422
+ def _hint() -> Any:
423
+ # A quiet keybinding hint line (opencode/Claude-Code parity).
424
+ return FormattedText(
425
+ [("class:prompt.hint",
426
+ f" ⏎ send {ICONS.bullet} ⌥⏎ newline {ICONS.bullet} ^C stop "
427
+ f"{ICONS.bullet} /help")]
428
+ )
429
+
430
+ input_window = Window(
431
+ BufferControl(buffer=input_buffer, input_processors=[]),
432
+ get_line_prefix=lambda *a, **k: _caret_prefix(),
433
+ height=_input_height,
434
+ # Without this the input window is GREEDY: in the bottom region it absorbs
435
+ # all free vertical space up to its max, pushing the status/hint rows to
436
+ # the far bottom with a huge empty gap (the "stretched input" bug). Pin it
437
+ # to its content height so the region stays compact and only grows as the
438
+ # user types/pastes.
439
+ dont_extend_height=True,
440
+ wrap_lines=True,
441
+ )
442
+ # A thin titled rule above the input: ``─ rob · model · provider ───────``.
443
+ separator = VSplit(
444
+ [
445
+ Window(
446
+ FormattedTextControl(_separator_label),
447
+ height=1,
448
+ dont_extend_width=True,
449
+ ),
450
+ Window(char="─", height=1, style="class:prompt.border"),
451
+ ]
452
+ )
453
+ status_window = Window(FormattedTextControl(_status), height=1)
454
+ hint_window = Window(FormattedTextControl(_hint), height=1)
455
+ autonomy_window = ConditionalContainer(
456
+ Window(
457
+ FormattedTextControl(
458
+ lambda: FormattedText(
459
+ [("", " "), ("class:prompt.rprompt", statusbar.autonomy_line(state))]
460
+ )
461
+ ),
462
+ height=1,
463
+ ),
464
+ filter=Condition(lambda: bool(statusbar.autonomy_line(state))),
465
+ )
466
+
467
+ layout = Layout(
468
+ HSplit([
469
+ picker.container(),
470
+ separator,
471
+ input_window,
472
+ status_window,
473
+ autonomy_window,
474
+ hint_window,
475
+ ])
476
+ )
477
+
478
+ kb = merge_key_bindings([
479
+ _persistent_key_bindings(on_interrupt=on_interrupt, is_picker_active=picker.is_active),
480
+ picker.key_bindings(),
481
+ ])
482
+
483
+ app = Application(
484
+ layout=layout,
485
+ key_bindings=kb,
486
+ style=toolbar_style(),
487
+ full_screen=False,
488
+ refresh_interval=0.2,
489
+ erase_when_done=True,
490
+ output=output,
491
+ input=input,
492
+ )
493
+ picker.app = app
494
+ app._picker = picker # /model reaches the picker via get_app_or_none()._picker
495
+ return app, input_buffer
496
+
497
+
498
+ def _persistent_key_bindings(*, on_interrupt: Optional[Callable[[], None]] = None,
499
+ is_picker_active: Optional[Callable[[], bool]] = None) -> Any:
500
+ """Key bindings for the persistent Application.
501
+
502
+ - ``Enter`` submits (the buffer's accept handler runs ``on_submit``).
503
+ - ``Meta+Enter`` inserts a newline (multi-line input).
504
+ - ``Ctrl-C`` calls ``on_interrupt`` (cancel the in-flight turn) without exiting.
505
+ - ``Ctrl-D`` on an empty buffer exits the app (EOF).
506
+
507
+ ``is_picker_active`` gates every binding on ``~active`` so that while the
508
+ arrow-key model selector is open, Enter/Ctrl-C/Ctrl-D route to the picker's
509
+ own bindings (submit/cancel select a model, not the turn) rather than here.
510
+ Typed characters still flow into the shared buffer — that IS the picker's
511
+ search field.
512
+ """
513
+ from prompt_toolkit.filters import Condition
514
+ from prompt_toolkit.key_binding import KeyBindings
515
+
516
+ kb = KeyBindings()
517
+ _pa = is_picker_active or (lambda: False)
518
+ not_picking = Condition(lambda: not _pa())
519
+
520
+ @kb.add("enter", filter=not_picking)
521
+ def _(event: Any) -> None:
522
+ event.current_buffer.validate_and_handle()
523
+
524
+ @kb.add("escape", "enter", filter=not_picking)
525
+ def _(event: Any) -> None:
526
+ event.current_buffer.insert_text("\n")
527
+
528
+ @kb.add("c-c", filter=not_picking)
529
+ def _(event: Any) -> None:
530
+ if on_interrupt is not None:
531
+ on_interrupt()
532
+
533
+ @kb.add("c-d", filter=not_picking)
534
+ def _(event: Any) -> None:
535
+ if not event.current_buffer.text:
536
+ event.app.exit(exception=EOFError)
537
+
538
+ return kb