polyrob 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (745) hide show
  1. agents/README.md +453 -0
  2. agents/__init__.py +164 -0
  3. agents/base_agent.py +743 -0
  4. agents/personality/character.py +119 -0
  5. agents/personality/character_manager.py +309 -0
  6. agents/personality/characters/rob.character.json +56 -0
  7. agents/personality/characters/trump.character.json +63 -0
  8. agents/personality/persona_render.py +78 -0
  9. agents/personality/persona_resolver.py +115 -0
  10. agents/prompt/__init__.py +58 -0
  11. agents/prompt/base_prompt.py +92 -0
  12. agents/prompt/system.py +417 -0
  13. agents/task/README_PATH_MANAGEMENT.md +141 -0
  14. agents/task/__init__.py +162 -0
  15. agents/task/agent/__init__.py +70 -0
  16. agents/task/agent/agent_state.py +408 -0
  17. agents/task/agent/async_delegation.py +274 -0
  18. agents/task/agent/autonomy_state.py +275 -0
  19. agents/task/agent/conversation.py +75 -0
  20. agents/task/agent/core/__init__.py +6 -0
  21. agents/task/agent/core/aux_metering.py +61 -0
  22. agents/task/agent/core/background_review.py +157 -0
  23. agents/task/agent/core/construction.py +1179 -0
  24. agents/task/agent/core/conversational_exit.py +49 -0
  25. agents/task/agent/core/correspondent_gate.py +227 -0
  26. agents/task/agent/core/curator.py +246 -0
  27. agents/task/agent/core/episodic_digest.py +103 -0
  28. agents/task/agent/core/error_recovery.py +480 -0
  29. agents/task/agent/core/history_io.py +257 -0
  30. agents/task/agent/core/llm_provisioning.py +287 -0
  31. agents/task/agent/core/llm_runner.py +547 -0
  32. agents/task/agent/core/logging_io.py +276 -0
  33. agents/task/agent/core/loop_detection.py +262 -0
  34. agents/task/agent/core/memory_prefetch.py +325 -0
  35. agents/task/agent/core/memory_writer.py +667 -0
  36. agents/task/agent/core/model_introspection.py +82 -0
  37. agents/task/agent/core/model_swap.py +177 -0
  38. agents/task/agent/core/next_action_internal.py +1331 -0
  39. agents/task/agent/core/output_validation.py +303 -0
  40. agents/task/agent/core/project_context.py +272 -0
  41. agents/task/agent/core/resources.py +93 -0
  42. agents/task/agent/core/result_processing.py +389 -0
  43. agents/task/agent/core/run_loop.py +661 -0
  44. agents/task/agent/core/safety_lifecycle.py +228 -0
  45. agents/task/agent/core/secret_guard.py +293 -0
  46. agents/task/agent/core/self_wake.py +280 -0
  47. agents/task/agent/core/session_metadata.py +104 -0
  48. agents/task/agent/core/step.py +781 -0
  49. agents/task/agent/core/step_execution.py +398 -0
  50. agents/task/agent/core/step_telemetry.py +222 -0
  51. agents/task/agent/core/test_aux_metering.py +50 -0
  52. agents/task/agent/core/test_billing_failover.py +68 -0
  53. agents/task/agent/core/test_fatal_error_classifier.py +38 -0
  54. agents/task/agent/core/test_judge_validation.py +275 -0
  55. agents/task/agent/core/test_model_swap.py +190 -0
  56. agents/task/agent/core/test_reflection_offload.py +54 -0
  57. agents/task/agent/core/turn_input.py +8 -0
  58. agents/task/agent/core/untrusted_wrap.py +103 -0
  59. agents/task/agent/core/user_ingress.py +228 -0
  60. agents/task/agent/hitl_manager.py +396 -0
  61. agents/task/agent/log_sanitize.py +36 -0
  62. agents/task/agent/message_manager/config.py +97 -0
  63. agents/task/agent/message_manager/service.py +758 -0
  64. agents/task/agent/message_manager/tests.py +305 -0
  65. agents/task/agent/message_manager/tool_call_builder.py +539 -0
  66. agents/task/agent/message_manager/tool_message_repair.py +364 -0
  67. agents/task/agent/message_manager/views.py +143 -0
  68. agents/task/agent/messages/__init__.py +8 -0
  69. agents/task/agent/messages/builders.py +309 -0
  70. agents/task/agent/messages/compactor.py +611 -0
  71. agents/task/agent/messages/context_references.py +271 -0
  72. agents/task/agent/messages/filters.py +333 -0
  73. agents/task/agent/messages/guidance.py +328 -0
  74. agents/task/agent/messages/persistence.py +448 -0
  75. agents/task/agent/messages/retrieval.py +506 -0
  76. agents/task/agent/messages/sqlite_persistence.py +85 -0
  77. agents/task/agent/messages/test_compaction_upgrade.py +349 -0
  78. agents/task/agent/messages/test_persistence_subdir_fix.py +28 -0
  79. agents/task/agent/messages/test_sqlite_persistence.py +54 -0
  80. agents/task/agent/messages/token_counter.py +532 -0
  81. agents/task/agent/orchestrator.py +776 -0
  82. agents/task/agent/profile_manager.py +105 -0
  83. agents/task/agent/profile_registry.py +173 -0
  84. agents/task/agent/prompts.py +1090 -0
  85. agents/task/agent/scenario_registry.py +226 -0
  86. agents/task/agent/service.py +509 -0
  87. agents/task/agent/session.py +984 -0
  88. agents/task/agent/skill_discovery.py +110 -0
  89. agents/task/agent/skill_frontmatter.py +94 -0
  90. agents/task/agent/skill_manager.py +1201 -0
  91. agents/task/agent/skill_store.py +627 -0
  92. agents/task/agent/skill_validation.py +56 -0
  93. agents/task/agent/skill_writer.py +492 -0
  94. agents/task/agent/sub_agent_manager.py +1137 -0
  95. agents/task/agent/test_session_disk_recovery.py +34 -0
  96. agents/task/agent/tests.py +513 -0
  97. agents/task/agent/tool_call_tracker.py +511 -0
  98. agents/task/agent/views.py +469 -0
  99. agents/task/config.py +432 -0
  100. agents/task/constants.py +1384 -0
  101. agents/task/flag_defaults.py +63 -0
  102. agents/task/goals/__init__.py +1 -0
  103. agents/task/goals/autonomy_marker.py +28 -0
  104. agents/task/goals/board.py +690 -0
  105. agents/task/goals/completion_judge.py +247 -0
  106. agents/task/goals/context.py +62 -0
  107. agents/task/goals/dispatcher.py +848 -0
  108. agents/task/goals/escalation.py +102 -0
  109. agents/task/goals/planner.py +114 -0
  110. agents/task/logging_config.py +155 -0
  111. agents/task/path.py +991 -0
  112. agents/task/robust_parse_config.py +400 -0
  113. agents/task/runtime/__init__.py +0 -0
  114. agents/task/runtime/run_as_session.py +130 -0
  115. agents/task/runtime_safety.py +308 -0
  116. agents/task/session/__init__.py +5 -0
  117. agents/task/session/browser_pool.py +130 -0
  118. agents/task/session/cleanup.py +612 -0
  119. agents/task/session/execution.py +397 -0
  120. agents/task/session/feed.py +200 -0
  121. agents/task/session/hitl_ingress.py +201 -0
  122. agents/task/session/hooks.py +83 -0
  123. agents/task/session/multi_agent.py +243 -0
  124. agents/task/session/workspace.py +91 -0
  125. agents/task/session_registry.py +91 -0
  126. agents/task/session_route.py +35 -0
  127. agents/task/sqlite_session_registry.py +261 -0
  128. agents/task/surface_config.py +185 -0
  129. agents/task/telemetry/__init__.py +58 -0
  130. agents/task/telemetry/event_log.py +180 -0
  131. agents/task/telemetry/formatters.py +642 -0
  132. agents/task/telemetry/manager.py +571 -0
  133. agents/task/telemetry/memory_events.py +66 -0
  134. agents/task/telemetry/self_events.py +53 -0
  135. agents/task/telemetry/sequence.py +160 -0
  136. agents/task/telemetry/service.py +1265 -0
  137. agents/task/telemetry/views.py +882 -0
  138. agents/task/templates.py +98 -0
  139. agents/task/test_aux_chain.py +151 -0
  140. agents/task/test_aux_router.py +41 -0
  141. agents/task/tests/test_parse_robustness.py +170 -0
  142. agents/task/tool_defaults.py +110 -0
  143. agents/task/utils.py +762 -0
  144. agents/task/utils_json.py +768 -0
  145. agents/task/utils_webview.py +86 -0
  146. agents/task/workspace_context.py +229 -0
  147. agents/task_agent_lite.py +2398 -0
  148. api/README.md +683 -0
  149. api/TASK_API_DOCS.md +151 -0
  150. api/__init__.py +0 -0
  151. api/a2a/__init__.py +37 -0
  152. api/a2a/agent_card.py +337 -0
  153. api/a2a/client.py +657 -0
  154. api/a2a/endpoints.py +375 -0
  155. api/a2a/models.py +268 -0
  156. api/a2a/streaming.py +419 -0
  157. api/a2a/task_handler.py +929 -0
  158. api/admin_endpoints.py +1584 -0
  159. api/app.py +923 -0
  160. api/auth_constants.py +28 -0
  161. api/auth_endpoints.py +402 -0
  162. api/auth_state.py +36 -0
  163. api/chat_via_task.py +42 -0
  164. api/conversation_manager.py +231 -0
  165. api/dependencies.py +186 -0
  166. api/eip8004_endpoints.py +452 -0
  167. api/hyperliquid_models.py +174 -0
  168. api/hyperliquid_routes.py +450 -0
  169. api/interfaces.py +207 -0
  170. api/jwt_middleware.py +83 -0
  171. api/kb/__init__.py +1 -0
  172. api/kb/endpoints.py +314 -0
  173. api/mcp_models.py +279 -0
  174. api/mcp_routes.py +438 -0
  175. api/middleware.py +526 -0
  176. api/models.py +176 -0
  177. api/openai_compat/__init__.py +0 -0
  178. api/openai_compat/model_map.py +25 -0
  179. api/openai_compat/models.py +59 -0
  180. api/openai_compat/router.py +140 -0
  181. api/payment_endpoints.py +444 -0
  182. api/payment_verification.py +138 -0
  183. api/polymarket_models.py +155 -0
  184. api/polymarket_routes.py +501 -0
  185. api/pricing_endpoints.py +89 -0
  186. api/session_routing.py +54 -0
  187. api/skill_endpoints.py +453 -0
  188. api/task_http_api.py +1856 -0
  189. api/webhooks.py +52 -0
  190. api/x402_endpoints.py +280 -0
  191. avatar/README.md +44 -0
  192. avatar/__init__.py +1 -0
  193. avatar/config/rob.json +7 -0
  194. avatar/mindprint.js +395 -0
  195. avatar/renders/rob.meta.json +20 -0
  196. avatar/renders/rob.png +0 -0
  197. avatar/studio.html +373 -0
  198. avatar/webview/avatar-live.js +44 -0
  199. cli/README.md +89 -0
  200. cli/__init__.py +0 -0
  201. cli/cli_surface.py +67 -0
  202. cli/commands/__init__.py +0 -0
  203. cli/commands/_bootstrap.py +49 -0
  204. cli/commands/_errors.py +53 -0
  205. cli/commands/chat.py +973 -0
  206. cli/commands/config.py +100 -0
  207. cli/commands/dashboard.py +103 -0
  208. cli/commands/doctor.py +222 -0
  209. cli/commands/email.py +174 -0
  210. cli/commands/gateway.py +346 -0
  211. cli/commands/goals.py +509 -0
  212. cli/commands/init.py +276 -0
  213. cli/commands/journey.py +30 -0
  214. cli/commands/kb.py +280 -0
  215. cli/commands/model.py +107 -0
  216. cli/commands/owner.py +431 -0
  217. cli/commands/pfp.py +232 -0
  218. cli/commands/run.py +414 -0
  219. cli/commands/serve.py +41 -0
  220. cli/commands/session.py +684 -0
  221. cli/commands/skill_install.py +786 -0
  222. cli/commands/skills.py +128 -0
  223. cli/commands/subagents.py +100 -0
  224. cli/commands/surface.py +81 -0
  225. cli/commands/telegram.py +185 -0
  226. cli/commands/todos.py +209 -0
  227. cli/commands/tools.py +102 -0
  228. cli/commands/update.py +316 -0
  229. cli/commands/whatsapp.py +175 -0
  230. cli/config_store.py +276 -0
  231. cli/gitignore.py +43 -0
  232. cli/inventory.py +46 -0
  233. cli/keys.py +101 -0
  234. cli/persona.py +17 -0
  235. cli/polyrob.py +137 -0
  236. cli/ui/__init__.py +64 -0
  237. cli/ui/activity.py +141 -0
  238. cli/ui/app.py +538 -0
  239. cli/ui/banner.py +233 -0
  240. cli/ui/blocks.py +534 -0
  241. cli/ui/bootstrap_notice.py +47 -0
  242. cli/ui/commands/__init__.py +41 -0
  243. cli/ui/commands/h_cron.py +64 -0
  244. cli/ui/commands/h_journey.py +164 -0
  245. cli/ui/commands/h_kb.py +121 -0
  246. cli/ui/commands/h_learn.py +100 -0
  247. cli/ui/commands/h_mcp.py +215 -0
  248. cli/ui/commands/h_self.py +113 -0
  249. cli/ui/commands/h_skills.py +314 -0
  250. cli/ui/commands/handlers.py +1672 -0
  251. cli/ui/commands/registry.py +426 -0
  252. cli/ui/dialog.py +425 -0
  253. cli/ui/event_registry.py +110 -0
  254. cli/ui/event_specs.py +45 -0
  255. cli/ui/events.py +433 -0
  256. cli/ui/identity.py +15 -0
  257. cli/ui/lifecycle.py +159 -0
  258. cli/ui/live_hooks.py +43 -0
  259. cli/ui/model_selector.py +532 -0
  260. cli/ui/persistent_loop.py +181 -0
  261. cli/ui/pick.py +113 -0
  262. cli/ui/plain_renderer.py +351 -0
  263. cli/ui/renderer.py +384 -0
  264. cli/ui/rich_renderer.py +533 -0
  265. cli/ui/secrets.py +115 -0
  266. cli/ui/state.py +376 -0
  267. cli/ui/statusbar.py +231 -0
  268. cli/ui/streaming.py +254 -0
  269. cli/ui/terminal_render.py +105 -0
  270. cli/ui/theme.py +132 -0
  271. cli/update/__init__.py +6 -0
  272. cli/update/context.py +121 -0
  273. cli/update/detect.py +176 -0
  274. cli/update/engine.py +95 -0
  275. cli/update/migrate_guarded.py +55 -0
  276. cli/update/process_guard.py +215 -0
  277. cli/update/runners.py +78 -0
  278. cli/update/snapshot.py +270 -0
  279. cli/update/versions.py +205 -0
  280. core/README.md +377 -0
  281. core/__init__.py +174 -0
  282. core/assets.py +87 -0
  283. core/async_bridge.py +76 -0
  284. core/autonomy_runtime.py +259 -0
  285. core/base_component.py +302 -0
  286. core/bootstrap.py +681 -0
  287. core/bot.py +310 -0
  288. core/config.py +833 -0
  289. core/constants.py +126 -0
  290. core/container.py +462 -0
  291. core/db_manifest.py +101 -0
  292. core/embedding.py +57 -0
  293. core/env.py +39 -0
  294. core/exceptions.py +287 -0
  295. core/flags.py +144 -0
  296. core/flags_catalog.py +345 -0
  297. core/home_migration.py +62 -0
  298. core/identity.py +118 -0
  299. core/initialization.py +1004 -0
  300. core/instance.py +537 -0
  301. core/interactive_gate.py +96 -0
  302. core/logging.py +436 -0
  303. core/owner_doc_writer.py +176 -0
  304. core/pairing.py +193 -0
  305. core/path_safety.py +20 -0
  306. core/paths.py +27 -0
  307. core/payment_config.py +21 -0
  308. core/permissions.py +322 -0
  309. core/runtime_config.py +96 -0
  310. core/runtime_paths.py +135 -0
  311. core/seams.py +35 -0
  312. core/secret_scan.py +25 -0
  313. core/secret_scrub.py +71 -0
  314. core/secrets.py +35 -0
  315. core/security_logging_filter.py +148 -0
  316. core/self_context_writer.py +333 -0
  317. core/self_evolution.py +294 -0
  318. core/session_context.py +47 -0
  319. core/sqlite_util.py +59 -0
  320. core/surfaces/__init__.py +16 -0
  321. core/surfaces/access.py +118 -0
  322. core/surfaces/binding.py +88 -0
  323. core/surfaces/bootstrap.py +78 -0
  324. core/surfaces/circuit.py +168 -0
  325. core/surfaces/continuity.py +77 -0
  326. core/surfaces/correspondents.py +228 -0
  327. core/surfaces/dispatcher.py +216 -0
  328. core/surfaces/envelopes.py +75 -0
  329. core/surfaces/gc.py +30 -0
  330. core/surfaces/idempotency.py +57 -0
  331. core/surfaces/inbound_webhook.py +124 -0
  332. core/surfaces/media.py +32 -0
  333. core/surfaces/message_router.py +91 -0
  334. core/surfaces/outbound_allowlist.py +80 -0
  335. core/surfaces/outbound_dispatcher.py +110 -0
  336. core/surfaces/outbound_mirror.py +37 -0
  337. core/surfaces/outbound_queue.py +107 -0
  338. core/surfaces/outbound_target.py +12 -0
  339. core/surfaces/owner_admin.py +29 -0
  340. core/surfaces/proactive.py +78 -0
  341. core/surfaces/progress.py +108 -0
  342. core/surfaces/rate_bucket.py +21 -0
  343. core/surfaces/registry.py +51 -0
  344. core/surfaces/rendering.py +53 -0
  345. core/surfaces/send_policy.py +14 -0
  346. core/surfaces/serialize.py +24 -0
  347. core/surfaces/session_chat_registry.py +112 -0
  348. core/surfaces/session_policy.py +60 -0
  349. core/surfaces/surface.py +288 -0
  350. core/surfaces/transcription.py +88 -0
  351. core/surfaces/voice_echo.py +26 -0
  352. core/surfaces/voice_guard.py +23 -0
  353. core/tickers.py +174 -0
  354. core/tool_catalog.py +192 -0
  355. core/version.py +75 -0
  356. core/wallet/__init__.py +5 -0
  357. core/wallet/agent_wallet.py +59 -0
  358. core/wallet/audit_sink.py +61 -0
  359. core/wallet/config.py +70 -0
  360. core/wallet/factory.py +87 -0
  361. core/wallet/policy.py +104 -0
  362. core/wallet/signer.py +51 -0
  363. cron/README.md +65 -0
  364. cron/__init__.py +0 -0
  365. cron/delivery.py +277 -0
  366. cron/digest.py +124 -0
  367. cron/jobs.py +207 -0
  368. cron/runner.py +297 -0
  369. cron/schedule.py +194 -0
  370. cron/scheduler.py +201 -0
  371. cron/service.py +57 -0
  372. cron/wake_gate.py +223 -0
  373. data/__init__.py +1 -0
  374. data/prompts/autov2_prompts.json +87 -0
  375. data/prompts/skills/browser-automation/SKILL.md +43 -0
  376. data/prompts/skills/coding-workflow/SKILL.md +36 -0
  377. data/prompts/skills/crypto-trading-safety/SKILL.md +53 -0
  378. data/prompts/skills/document-writing/SKILL.md +33 -0
  379. data/prompts/skills/email-comms/SKILL.md +33 -0
  380. data/prompts/skills/file-data-ops/SKILL.md +35 -0
  381. data/prompts/skills/hyperliquid-account-review/SKILL.md +48 -0
  382. data/prompts/skills/hyperliquid-market-data/SKILL.md +52 -0
  383. data/prompts/skills/hyperliquid-trading/SKILL.md +59 -0
  384. data/prompts/skills/lead-research/SKILL.md +86 -0
  385. data/prompts/skills/market-research-brief/SKILL.md +104 -0
  386. data/prompts/skills/person-analyzer/SKILL.md +124 -0
  387. data/prompts/skills/polymarket-market-research/SKILL.md +54 -0
  388. data/prompts/skills/polymarket-portfolio-review/SKILL.md +50 -0
  389. data/prompts/skills/polymarket-trading/SKILL.md +55 -0
  390. data/prompts/skills/presentation-creator/SKILL.md +36 -0
  391. data/prompts/skills/project-analyzer/SKILL.md +169 -0
  392. data/prompts/skills/rules.json +268 -0
  393. data/prompts/skills/secret-handling/SKILL.md +20 -0
  394. data/prompts/skills/skill-authoring/SKILL.md +31 -0
  395. data/prompts/skills/skill-security-review/SKILL.md +27 -0
  396. data/prompts/skills/social-discovery/SKILL.md +57 -0
  397. data/prompts/skills/task-planning/SKILL.md +33 -0
  398. data/prompts/skills/web-research/SKILL.md +39 -0
  399. data/prompts/skills/web-scraping/SKILL.md +73 -0
  400. data/prompts/skills/x-engagement/SKILL.md +69 -0
  401. data/prompts/system_prompts.json +4 -0
  402. data/subscription_channels.json +5 -0
  403. modules/README.md +503 -0
  404. modules/__init__.py +124 -0
  405. modules/auth/__init__.py +13 -0
  406. modules/auth/api_key_manager.py +157 -0
  407. modules/auth/identity_mapper.py +380 -0
  408. modules/auth/siwe_auth.py +273 -0
  409. modules/auth/tier_manager.py +173 -0
  410. modules/base_module.py +190 -0
  411. modules/credits/__init__.py +29 -0
  412. modules/credits/balance_manager.py +237 -0
  413. modules/credits/cost_utils.py +129 -0
  414. modules/credits/pricing.py +139 -0
  415. modules/credits/unified_ledger.py +148 -0
  416. modules/credits/usage_meter.py +310 -0
  417. modules/credits/usage_tracker.py +674 -0
  418. modules/database/__init__.py +33 -0
  419. modules/database/audit_log.py +322 -0
  420. modules/database/auth_tables.py +276 -0
  421. modules/database/connection.py +644 -0
  422. modules/database/connection_pool.py +305 -0
  423. modules/database/conversation_contexts.py +446 -0
  424. modules/database/database_manager.py +237 -0
  425. modules/database/hyperliquid.py +375 -0
  426. modules/database/polymarket.py +574 -0
  427. modules/database/user_mcp_servers.py +728 -0
  428. modules/database/user_profiles.py +562 -0
  429. modules/database/utils.py +107 -0
  430. modules/database/x402_tables.py +92 -0
  431. modules/eip8004/README.md +1069 -0
  432. modules/eip8004/__init__.py +65 -0
  433. modules/eip8004/contracts.py +498 -0
  434. modules/eip8004/models.py +271 -0
  435. modules/eip8004/registration.py +148 -0
  436. modules/eip8004/reputation.py +367 -0
  437. modules/eip8004/validation.py +277 -0
  438. modules/llm/__init__.py +97 -0
  439. modules/llm/adapters.py +963 -0
  440. modules/llm/anthropic_client.py +831 -0
  441. modules/llm/available_models.py +106 -0
  442. modules/llm/brain_scrubber.py +137 -0
  443. modules/llm/cache_hints.py +162 -0
  444. modules/llm/deepseek_client.py +562 -0
  445. modules/llm/gemini_client.py +1367 -0
  446. modules/llm/llm_client.py +1139 -0
  447. modules/llm/llm_client_registry.py +188 -0
  448. modules/llm/llm_factory.py +237 -0
  449. modules/llm/llm_manager.py +1036 -0
  450. modules/llm/messages.py +374 -0
  451. modules/llm/model_registry.py +1935 -0
  452. modules/llm/nvidia_client.py +125 -0
  453. modules/llm/openai_client.py +800 -0
  454. modules/llm/openrouter_client.py +969 -0
  455. modules/llm/profiles.py +178 -0
  456. modules/llm/test_anthropic_cache_capture.py +35 -0
  457. modules/llm/test_cached_pricing_derivation.py +24 -0
  458. modules/llm/think_scrubber.py +444 -0
  459. modules/llm/token_counter.py +268 -0
  460. modules/memory/__init__.py +35 -0
  461. modules/memory/backend_factory.py +83 -0
  462. modules/memory/cache_manager.py +186 -0
  463. modules/memory/episodic.py +108 -0
  464. modules/memory/local_vector_memory_provider.py +552 -0
  465. modules/memory/memory_manager.py +225 -0
  466. modules/memory/models.py +312 -0
  467. modules/memory/provider.py +210 -0
  468. modules/memory/registry.py +205 -0
  469. modules/memory/sqlite_memory_provider.py +767 -0
  470. modules/memory/task/__init__.py +52 -0
  471. modules/memory/task/compaction_manager.py +157 -0
  472. modules/memory/task/context_retriever.py +809 -0
  473. modules/memory/task/hierarchical_memory.py +1002 -0
  474. modules/memory/task/lexical_retriever.py +71 -0
  475. modules/memory/task/null_context_manager.py +213 -0
  476. modules/memory/task/phase_manager.py +625 -0
  477. modules/memory/task/reflection_service.py +157 -0
  478. modules/memory/task/semantic_retriever.py +218 -0
  479. modules/memory/task/task_context_manager.py +1165 -0
  480. modules/memory/task/test_forgetting_engages.py +24 -0
  481. modules/memory/task/test_reflection_llm.py +39 -0
  482. modules/memory/task/test_threat_scan.py +53 -0
  483. modules/memory/task/threat_scan.py +90 -0
  484. modules/memory/test_sqlite_memory_provider.py +106 -0
  485. modules/memory/user_profile_manager.py +527 -0
  486. modules/payments/__init__.py +11 -0
  487. modules/payments/deposit_monitor.py +478 -0
  488. modules/payments/price_oracle.py +70 -0
  489. modules/payments/treasury_sweeper.py +401 -0
  490. modules/payments/wallet_generator.py +145 -0
  491. modules/pfp/__init__.py +1 -0
  492. modules/pfp/config.py +92 -0
  493. modules/pfp/mesh.py +565 -0
  494. modules/pfp/push.py +69 -0
  495. modules/pfp/renderer.py +85 -0
  496. modules/pfp/store.py +82 -0
  497. modules/skills/__init__.py +1 -0
  498. modules/skills/skill_usage.py +248 -0
  499. modules/transcription/__init__.py +24 -0
  500. modules/transcription/base.py +20 -0
  501. modules/transcription/faster_whisper_transcriber.py +55 -0
  502. modules/x402/README.md +249 -0
  503. modules/x402/__init__.py +22 -0
  504. modules/x402/invoicing.py +416 -0
  505. modules/x402/middleware.py +364 -0
  506. modules/x402/settlement_watcher.py +145 -0
  507. modules/x402/x402_integration.py +344 -0
  508. polyrob-0.5.0.dist-info/METADATA +482 -0
  509. polyrob-0.5.0.dist-info/RECORD +745 -0
  510. polyrob-0.5.0.dist-info/WHEEL +5 -0
  511. polyrob-0.5.0.dist-info/entry_points.txt +2 -0
  512. polyrob-0.5.0.dist-info/licenses/LICENSE +21 -0
  513. polyrob-0.5.0.dist-info/top_level.txt +12 -0
  514. surfaces/README.md +70 -0
  515. surfaces/__init__.py +0 -0
  516. surfaces/email/__init__.py +0 -0
  517. surfaces/email/dedup.py +44 -0
  518. surfaces/email/harness.py +187 -0
  519. surfaces/email/inbound.py +159 -0
  520. surfaces/email/seed.py +60 -0
  521. surfaces/email/surface.py +105 -0
  522. surfaces/telegram/__init__.py +0 -0
  523. surfaces/telegram/dedup.py +83 -0
  524. surfaces/telegram/harness.py +886 -0
  525. surfaces/telegram/inbound.py +145 -0
  526. surfaces/telegram/interactive_tools.py +49 -0
  527. surfaces/telegram/markdown.py +347 -0
  528. surfaces/telegram/rate_limit.py +167 -0
  529. surfaces/telegram/surface.py +187 -0
  530. surfaces/telegram/voice.py +52 -0
  531. surfaces/whatsapp/__init__.py +0 -0
  532. surfaces/whatsapp/client.py +51 -0
  533. surfaces/whatsapp/harness.py +62 -0
  534. surfaces/whatsapp/inbound.py +132 -0
  535. surfaces/whatsapp/surface.py +64 -0
  536. surfaces/whatsapp/window.py +34 -0
  537. tools/README.md +578 -0
  538. tools/__init__.py +424 -0
  539. tools/alchemy/__init__.py +5 -0
  540. tools/alchemy/alchemy_tool.py +389 -0
  541. tools/anysite/__init__.py +17 -0
  542. tools/anysite/client.py +88 -0
  543. tools/anysite/tool.py +89 -0
  544. tools/base_tool.py +468 -0
  545. tools/browser/__init__.py +22 -0
  546. tools/browser/actions.py +118 -0
  547. tools/browser/browser.py +1457 -0
  548. tools/browser/browser_manager.py +690 -0
  549. tools/browser/context.py +1748 -0
  550. tools/browser/playwright_utils.py +137 -0
  551. tools/browser/views.py +56 -0
  552. tools/code_exec/SANDBOX_SECURITY.md +144 -0
  553. tools/code_exec/__init__.py +178 -0
  554. tools/code_exec/backend.py +73 -0
  555. tools/code_exec/backends/__init__.py +1 -0
  556. tools/code_exec/backends/docker.py +779 -0
  557. tools/code_exec/backends/local_subprocess.py +154 -0
  558. tools/code_exec/env_policy.py +45 -0
  559. tools/code_exec/result.py +42 -0
  560. tools/code_exec/sandbox_guard.py +59 -0
  561. tools/code_exec/tool.py +227 -0
  562. tools/coding/__init__.py +50 -0
  563. tools/coding/edit.py +107 -0
  564. tools/coding/search.py +113 -0
  565. tools/coding/tool.py +353 -0
  566. tools/collabland/__init__.py +5 -0
  567. tools/collabland/collabland_tool.py +471 -0
  568. tools/controller/__init__.py +0 -0
  569. tools/controller/_helpers.py +165 -0
  570. tools/controller/action_registration.py +1765 -0
  571. tools/controller/approval.py +244 -0
  572. tools/controller/approval_interactive.py +90 -0
  573. tools/controller/delegation.py +195 -0
  574. tools/controller/execution.py +797 -0
  575. tools/controller/execution_context.py +141 -0
  576. tools/controller/hooks.py +135 -0
  577. tools/controller/introspection.py +344 -0
  578. tools/controller/mcp_registrar.py +190 -0
  579. tools/controller/message_send.py +31 -0
  580. tools/controller/registry/__init__.py +4 -0
  581. tools/controller/registry/schema_generators.py +395 -0
  582. tools/controller/registry/schema_sanitizer.py +406 -0
  583. tools/controller/registry/service.py +1266 -0
  584. tools/controller/registry/views.py +151 -0
  585. tools/controller/service.py +409 -0
  586. tools/controller/tool_management.py +334 -0
  587. tools/controller/types.py +28 -0
  588. tools/controller/views.py +450 -0
  589. tools/cronjob_tools.py +166 -0
  590. tools/crypto_trade_gate.py +59 -0
  591. tools/descriptors.py +452 -0
  592. tools/dom/__init__.py +0 -0
  593. tools/dom/history_tree_processor/service.py +244 -0
  594. tools/dom/history_tree_processor/view.py +99 -0
  595. tools/dom/service.py +471 -0
  596. tools/dom/views.py +346 -0
  597. tools/email_tool.py +403 -0
  598. tools/exceptions.py +169 -0
  599. tools/filesystem.py +1038 -0
  600. tools/filesystem_docproc.py +331 -0
  601. tools/filesystem_pdf.py +755 -0
  602. tools/git/__init__.py +42 -0
  603. tools/git/tool.py +258 -0
  604. tools/github/__init__.py +33 -0
  605. tools/github/client.py +94 -0
  606. tools/github/tool.py +237 -0
  607. tools/goal_tools.py +309 -0
  608. tools/hyperliquid/__init__.py +56 -0
  609. tools/hyperliquid/models.py +233 -0
  610. tools/hyperliquid/service.py +1680 -0
  611. tools/knowledge_ingest.py +950 -0
  612. tools/mcp/README.md +535 -0
  613. tools/mcp/__init__.py +52 -0
  614. tools/mcp/catalog.py +115 -0
  615. tools/mcp/config.py +337 -0
  616. tools/mcp/mcp_tool.py +1415 -0
  617. tools/mcp/param_coercion.py +289 -0
  618. tools/mcp/protocol.py +1520 -0
  619. tools/mcp/rate_limit.py +50 -0
  620. tools/mcp/security.py +424 -0
  621. tools/mcp/self_install.py +111 -0
  622. tools/mcp/server_manager.py +829 -0
  623. tools/mcp/subscriptions.py +72 -0
  624. tools/mcp/user_mcp_service.py +899 -0
  625. tools/mcp/validation_tracker.py +89 -0
  626. tools/mcp/views.py +346 -0
  627. tools/oauth/__init__.py +17 -0
  628. tools/oauth/manager.py +75 -0
  629. tools/oauth/provider.py +69 -0
  630. tools/oauth/providers/__init__.py +1 -0
  631. tools/oauth/providers/generic_oauth2.py +96 -0
  632. tools/perplexity_tool.py +280 -0
  633. tools/polymarket/__init__.py +41 -0
  634. tools/polymarket/clob_adapter.py +76 -0
  635. tools/polymarket/models.py +182 -0
  636. tools/polymarket/service.py +2082 -0
  637. tools/self_env/__init__.py +61 -0
  638. tools/self_env/tool.py +274 -0
  639. tools/shell/__init__.py +80 -0
  640. tools/shell/backend_pool.py +118 -0
  641. tools/shell/discipline.py +62 -0
  642. tools/shell/executor.py +157 -0
  643. tools/shell/loopback_allow.py +87 -0
  644. tools/shell/process_registry.py +113 -0
  645. tools/shell/process_tool.py +131 -0
  646. tools/shell/state.py +129 -0
  647. tools/shell/tool.py +151 -0
  648. tools/task_tool.py +637 -0
  649. tools/twitter_tool.py +2086 -0
  650. tools/user_directory.py +179 -0
  651. tools/web_fetch/__init__.py +3 -0
  652. tools/web_fetch/fetcher.py +144 -0
  653. tools/web_fetch/render.py +44 -0
  654. tools/web_fetch/tool.py +60 -0
  655. tools/x402/__init__.py +60 -0
  656. tools/x402/client.py +47 -0
  657. tools/x402/invoice_tool.py +143 -0
  658. tools/x402/real_client.py +250 -0
  659. tools/x402/service.py +131 -0
  660. utils/README.md +498 -0
  661. utils/__init__.py +46 -0
  662. utils/auth_utils.py +114 -0
  663. utils/bounded_collections.py +162 -0
  664. utils/circuit_breaker.py +389 -0
  665. utils/gif_utils.py +435 -0
  666. utils/markdown_utils.py +291 -0
  667. utils/message_utils.py +147 -0
  668. utils/metrics.py +36 -0
  669. utils/path_validator.py +227 -0
  670. utils/rate_limit_manager.py +399 -0
  671. utils/result_size.py +296 -0
  672. utils/time_utils.py +206 -0
  673. utils/user_utils.py +188 -0
  674. webview/README.md +111 -0
  675. webview/RENAME_AND_ALIGN_HANDOFF.md +200 -0
  676. webview/__init__.py +8 -0
  677. webview/activity.py +636 -0
  678. webview/owner_auth.py +108 -0
  679. webview/pages.py +448 -0
  680. webview/repair_sessions.py +399 -0
  681. webview/server.py +4323 -0
  682. webview/server_launcher.py +112 -0
  683. webview/static/css/activity.css +89 -0
  684. webview/static/css/chat.css +1772 -0
  685. webview/static/css/components.css +1190 -0
  686. webview/static/css/config-panel.css +651 -0
  687. webview/static/css/pages/admin.css +766 -0
  688. webview/static/css/pages/profile.css +231 -0
  689. webview/static/css/pages/settings.css +297 -0
  690. webview/static/css/pages/signin.css +316 -0
  691. webview/static/css/style.css +4250 -0
  692. webview/static/css/variables.css +147 -0
  693. webview/static/css/workspace-fullwidth-fix.css +109 -0
  694. webview/static/img/favicon.ico +0 -0
  695. webview/static/js/activity.js +394 -0
  696. webview/static/js/admin/activity.js +186 -0
  697. webview/static/js/admin/dashboard.js +163 -0
  698. webview/static/js/admin/user_detail.js +400 -0
  699. webview/static/js/admin/users.js +174 -0
  700. webview/static/js/admin/utils.js +214 -0
  701. webview/static/js/chat.js +3178 -0
  702. webview/static/js/config-panel.js +522 -0
  703. webview/static/js/constants.js +20 -0
  704. webview/static/js/error-handler.js +226 -0
  705. webview/static/js/ethers.min.js +1 -0
  706. webview/static/js/event-filter.js +137 -0
  707. webview/static/js/event-store.js +351 -0
  708. webview/static/js/file-attachments.js +374 -0
  709. webview/static/js/file-loader.js +163 -0
  710. webview/static/js/index.js +103 -0
  711. webview/static/js/performance-utils.js +196 -0
  712. webview/static/js/profile.js +396 -0
  713. webview/static/js/screenshot.js +386 -0
  714. webview/static/js/session.js +1527 -0
  715. webview/static/js/settings.js +1288 -0
  716. webview/static/js/sidebar-data.js +766 -0
  717. webview/static/js/sidebar-toggle.js +296 -0
  718. webview/static/js/socket.io.min.js +7 -0
  719. webview/static/js/stats.js +355 -0
  720. webview/static/js/ui-utils.js +420 -0
  721. webview/static/js/workspace.js +966 -0
  722. webview/stats_service.py +613 -0
  723. webview/templates/__init__.py +1 -0
  724. webview/templates/__pycache__/__init__.cpython-311.pyc +0 -0
  725. webview/templates/activity.html +36 -0
  726. webview/templates/admin/activity.html +122 -0
  727. webview/templates/admin/dashboard.html +112 -0
  728. webview/templates/admin/user_detail.html +228 -0
  729. webview/templates/admin/users.html +105 -0
  730. webview/templates/autonomy.html +60 -0
  731. webview/templates/error.html +133 -0
  732. webview/templates/finance.html +90 -0
  733. webview/templates/identity.html +73 -0
  734. webview/templates/index.html +42 -0
  735. webview/templates/layout.html +125 -0
  736. webview/templates/memory.html +59 -0
  737. webview/templates/owner_login.html +15 -0
  738. webview/templates/profile.html +121 -0
  739. webview/templates/session.html +312 -0
  740. webview/templates/settings.html +534 -0
  741. webview/templates/sidebar.html +112 -0
  742. webview/templates/signin.html +213 -0
  743. webview/templates/status.html +14 -0
  744. webview/templates/system.html +45 -0
  745. webview/webgate.py +196 -0
@@ -0,0 +1,178 @@
1
+ """Declarative provider profiles (roadmap P8, Reference §28).
2
+
3
+ A ``ProviderProfile`` *describes* a provider — identity, auth, base URL, default
4
+ model, capability flags — and owns no client construction, credential rotation, or
5
+ streaming. Today POLYROB's ``modules/llm/*_client.py`` files conflate profile +
6
+ transport + adapter; this layer extracts the declarative half so it has a single
7
+ home. It is purely additive: nothing is rewired to consume it yet (that migration
8
+ is the rest of P8), so behavior is unchanged.
9
+
10
+ The default model deliberately reads from ``llm_client_registry.DEFAULT_MODELS`` so
11
+ the default-model *policy* stays single-sourced.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass
16
+ from typing import Dict, List, Optional
17
+
18
+ from modules.llm.llm_client_registry import get_default_model
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class ProviderProfile:
23
+ name: str
24
+ display_name: str
25
+ env_key: str # API-key environment variable
26
+ auth_type: str = "api_key" # api_key | oauth | aws
27
+ base_url: Optional[str] = None # None => provider SDK default
28
+ supports_native_tools: bool = True
29
+ supports_vision: bool = True
30
+ signup_url: Optional[str] = None
31
+ initializable: bool = True # False = key alone can't bootstrap a client
32
+ # (deepseek: direct client disabled → use OpenRouter)
33
+
34
+ @property
35
+ def default_model(self) -> str:
36
+ """The default model for this provider (single-sourced from the registry)."""
37
+ return get_default_model(self.name)
38
+
39
+
40
+ # NOTE: insertion order IS the canonical preference order for "first provider with
41
+ # a key" (see ``providers_with_keys`` / ``core.runtime_config``). OpenRouter is FIRST
42
+ # (2026-06-24): it is the preferred default client whenever its key is present —
43
+ # explicit ``-p`` and operator pins (DEFAULT_PROVIDER/CHAT_PROVIDER) still win.
44
+ PROFILES: Dict[str, ProviderProfile] = {
45
+ "openrouter": ProviderProfile(
46
+ name="openrouter", display_name="OpenRouter", env_key="OPENROUTER_API_KEY",
47
+ base_url="https://openrouter.ai/api/v1", supports_native_tools=True,
48
+ supports_vision=True, signup_url="https://openrouter.ai/",
49
+ ),
50
+ "anthropic": ProviderProfile(
51
+ name="anthropic", display_name="Anthropic", env_key="ANTHROPIC_API_KEY",
52
+ base_url="https://api.anthropic.com", supports_native_tools=True,
53
+ supports_vision=True, signup_url="https://console.anthropic.com/",
54
+ ),
55
+ "openai": ProviderProfile(
56
+ name="openai", display_name="OpenAI", env_key="OPENAI_API_KEY",
57
+ supports_native_tools=True, supports_vision=True,
58
+ signup_url="https://platform.openai.com/",
59
+ ),
60
+ "gemini": ProviderProfile(
61
+ name="gemini", display_name="Google Gemini", env_key="GEMINI_API_KEY",
62
+ supports_native_tools=True, supports_vision=True,
63
+ signup_url="https://aistudio.google.com/",
64
+ ),
65
+ "nvidia": ProviderProfile(
66
+ name="nvidia", display_name="NVIDIA NIM", env_key="NVIDIA_API_KEY",
67
+ base_url="https://integrate.api.nvidia.com/v1", supports_native_tools=True,
68
+ supports_vision=False, signup_url="https://build.nvidia.com/",
69
+ ),
70
+ "deepseek": ProviderProfile(
71
+ name="deepseek", display_name="DeepSeek", env_key="DEEPSEEK_API_KEY",
72
+ base_url="https://api.deepseek.com/v1", supports_native_tools=False,
73
+ supports_vision=False, signup_url="https://platform.deepseek.com/",
74
+ # Direct client disabled (tool-calling broken) — reach DeepSeek via OpenRouter
75
+ # (OPENROUTER_API_KEY + model deepseek/deepseek-chat). A DEEPSEEK_API_KEY alone
76
+ # cannot bootstrap the agent, so it must NOT count toward "has a usable key".
77
+ initializable=False,
78
+ ),
79
+ }
80
+
81
+
82
+ def providers_with_keys(env=None) -> List[str]:
83
+ """Return provider names whose API key is present in *env*, in PROFILES order.
84
+
85
+ This is the single source of truth for "which LLM providers have a key" — it
86
+ replaces the divergent per-call-site key lists across the CLI (config_store,
87
+ chat.py, doctor.py, model.py, banner.py). The returned order IS the canonical
88
+ preference order for "first provider with a key".
89
+
90
+ *env* defaults to ``os.environ``; pass a mapping for testability. A blank value
91
+ counts as absent.
92
+ """
93
+ import os
94
+ env = os.environ if env is None else env
95
+ return [p.name for p in PROFILES.values() if env.get(p.env_key)]
96
+
97
+
98
+ def initializable_providers_with_keys(env=None) -> List[str]:
99
+ """``providers_with_keys`` restricted to providers a key ALONE can bootstrap.
100
+
101
+ This is the SSOT for "does the user have a *usable* provider key" — the gating
102
+ oracle that ``should_warn_no_key`` / the runtime resolver / the env-backfill and
103
+ ``LLMManager._initialize``'s ``clients_to_try`` all derive from, so they can never
104
+ disagree. Excludes deepseek (direct client disabled — route via OpenRouter). The
105
+ raw ``providers_with_keys`` stays the DISPLAY oracle (doctor/webview show a key is
106
+ present even when it can't bootstrap directly).
107
+ """
108
+ import os
109
+ env = os.environ if env is None else env
110
+ return [p.name for p in PROFILES.values() if env.get(p.env_key) and p.initializable]
111
+
112
+
113
+ # Placeholder values that are "present" but not a real key (mirrors
114
+ # core.config.BotConfig.validate_api_keys, generalized across providers).
115
+ _PLACEHOLDER_KEYS = {
116
+ "your-openai-key", "your-anthropic-key", "your-api-key", "your-alchemy-key",
117
+ "your-gemini-key", "your-openrouter-key", "your-key", "changeme", "none", "null",
118
+ }
119
+ # Real provider API keys are comfortably longer than this; BotConfig blanks shorter
120
+ # ones for openai/anthropic. Every provider's real key (gemini ~39 is the shortest)
121
+ # clears 20, so the bar is safe to apply across providers.
122
+ _MIN_KEY_LEN = 20
123
+
124
+
125
+ def looks_like_real_key(value) -> bool:
126
+ """True when *value* is a plausibly-real API key (not blank / placeholder / stub).
127
+
128
+ Mirrors ``BotConfig.validate_api_keys`` so the GATING oracles agree with what the
129
+ LLM manager will actually accept — a malformed key must NOT pass a guard only to be
130
+ blanked by BotConfig and crash the manager with a misleading 'No API key found'.
131
+ """
132
+ if not value:
133
+ return False
134
+ v = str(value).strip()
135
+ if v.lower() in _PLACEHOLDER_KEYS:
136
+ return False
137
+ return len(v) >= _MIN_KEY_LEN
138
+
139
+
140
+ def usable_providers_with_keys(env=None) -> List[str]:
141
+ """Initializable providers whose key VALUE is well-formed (mirrors BotConfig).
142
+
143
+ THE gating oracle wherever real key values are available (``should_warn_no_key``,
144
+ the env-backfill, config-store resolution). Stricter than
145
+ ``initializable_providers_with_keys`` (which is presence-only, for the resolver's
146
+ name-based path) — it also rejects placeholder / too-short values. Raw
147
+ ``providers_with_keys`` remains the DISPLAY oracle.
148
+ """
149
+ import os
150
+ env = os.environ if env is None else env
151
+ return [
152
+ p.name for p in PROFILES.values()
153
+ if p.initializable and looks_like_real_key(env.get(p.env_key))
154
+ ]
155
+
156
+
157
+ def no_key_message() -> str:
158
+ """The single canonical no-key message (neutral module — no ``cli`` import).
159
+
160
+ Re-exported from ``cli.keys`` and reused by ``LLMManager._initialize``'s raise so
161
+ every no-key surface says the same thing.
162
+ """
163
+ return (
164
+ "No API key found. Run `polyrob init` to set one up, or put a provider key in "
165
+ "any of: process env, ./.polyrob/.env, ~/.polyrob/.env, root .env, "
166
+ "config/.env.development, or config/.env.production.\n"
167
+ "Supported providers: OPENROUTER_API_KEY (recommended), ANTHROPIC_API_KEY, "
168
+ "OPENAI_API_KEY, GEMINI_API_KEY, NVIDIA_API_KEY, DEEPSEEK_API_KEY "
169
+ "(direct client disabled — use OPENROUTER_API_KEY with model deepseek/deepseek-chat)."
170
+ )
171
+
172
+
173
+ def get_profile(name: str) -> Optional[ProviderProfile]:
174
+ return PROFILES.get(name)
175
+
176
+
177
+ def all_profiles() -> List[ProviderProfile]:
178
+ return list(PROFILES.values())
@@ -0,0 +1,35 @@
1
+ from types import SimpleNamespace
2
+ from modules.llm.anthropic_client import AnthropicClient
3
+ from modules.llm.model_registry import calculate_cost
4
+
5
+
6
+ def _client_with_usage(**usage):
7
+ c = AnthropicClient.__new__(AnthropicClient) # bypass __init__ (no network)
8
+ import logging; c.logger = logging.getLogger("t"); c.model_type = "claude-sonnet-4-5"
9
+ c.last_response = SimpleNamespace(usage=SimpleNamespace(**usage))
10
+ return c
11
+
12
+
13
+ def test_anthropic_folds_cache_tokens_into_prompt_and_exposes_reads():
14
+ c = _client_with_usage(input_tokens=100, output_tokens=20,
15
+ cache_read_input_tokens=900, cache_creation_input_tokens=50)
16
+ u = c._extract_usage_data()
17
+ assert u["prompt_tokens"] == 1050 # 100 + 900 + 50
18
+ assert u["cached_tokens"] == 900 # reads only
19
+ assert u["completion_tokens"] == 20
20
+
21
+
22
+ def test_anthropic_no_cache_fields_unchanged():
23
+ # Old-shape usage (no cache attrs) must behave exactly as before.
24
+ c = _client_with_usage(input_tokens=100, output_tokens=20)
25
+ u = c._extract_usage_data()
26
+ assert u["prompt_tokens"] == 100 and u["completion_tokens"] == 20
27
+ assert u["total_tokens"] == 120
28
+ assert u.get("cached_tokens", 0) == 0
29
+
30
+
31
+ def test_cost_uses_cached_rate_for_anthropic_reads():
32
+ # sonnet-4-5: input $3, cached $0.30 (after A1). 900 cached reads + 150 regular input.
33
+ cost = calculate_cost("claude-sonnet-4-5", input_tokens=1050, output_tokens=20, cached_tokens=900)
34
+ # regular = 1050-900 = 150 @ $3/M; cached = 900 @ $0.30/M
35
+ assert round(cost, 8) == round(150/1e6*3.0 + 900/1e6*0.3 + 20/1e6*15.0, 8)
@@ -0,0 +1,24 @@
1
+ """Test that cached_input_price is derived per provider when not explicitly set."""
2
+
3
+ from modules.llm.model_registry import get_model_config, ModelProvider
4
+
5
+
6
+ def test_anthropic_cached_price_derived_at_10pct():
7
+ """Anthropic cache reads should be 10% of input price."""
8
+ m = get_model_config("claude-sonnet-4-5")
9
+ assert m.pricing.input_price == 3.0
10
+ assert m.pricing.cached_input_price == 0.3 # 0.1x
11
+
12
+
13
+ def test_explicit_cached_price_is_preserved():
14
+ """DeepSeek sets 0.028 explicitly (model_registry.py:722) — must not be overwritten."""
15
+ m = get_model_config("deepseek-chat")
16
+ assert m.pricing.cached_input_price == 0.028
17
+
18
+
19
+ def test_derivation_only_fills_none_and_matches_multiplier():
20
+ """All models should have cached_input_price derived or preserved."""
21
+ for name in ("claude-sonnet-4-5", "gpt-5"):
22
+ m = get_model_config(name)
23
+ assert m.pricing.cached_input_price is not None
24
+ assert m.pricing.cached_input_price <= m.pricing.input_price
@@ -0,0 +1,444 @@
1
+ """Stateful scrubber for reasoning/thinking blocks in streamed assistant text.
2
+
3
+ UP-07: ported essentially verbatim from Reference (`agent/think_scrubber.py`,
4
+ the reference agent) — pure, zero-dependency. POLYROB wires it at the
5
+ LLM-adapter content seam (modules/llm/adapters.py) so every provider's assistant text is
6
+ scrubbed before it becomes an AIMessage. The legacy `agents/task/utils_json.py` regex
7
+ strip stays for the fallback JSON-from-text path; this module covers the native seam and
8
+ a broader tag set. See docs/plans/agent-upgrades-2026-06/UP-07-*.md.
9
+
10
+ ``run_agent._strip_think_blocks`` is regex-based and correct for a complete
11
+ string, but when it runs *per-delta* in ``_fire_stream_delta`` it destroys
12
+ the state that downstream consumers (CLI ``_stream_delta``, gateway
13
+ ``GatewayStreamConsumer._filter_and_accumulate``) rely on.
14
+
15
+ Concretely, when MiniMax-M2.7 streams
16
+
17
+ delta1 = "<think>"
18
+ delta2 = "Let me check their config"
19
+ delta3 = "</think>"
20
+
21
+ the per-delta regex erases delta1 entirely (case 2: unterminated-open at
22
+ boundary matches ``^<think>...``), so the downstream state machine never
23
+ sees the open tag, treats delta2 as regular content, and leaks reasoning
24
+ to the user. Consumers that don't run their own state machine (ACP,
25
+ api_server, TTS) never had any defence at all — they just emitted
26
+ whatever survived the upstream regex.
27
+
28
+ This module centralises the tag-suppression state machine at the
29
+ upstream layer so every stream_delta_callback sees text that has
30
+ already had reasoning blocks removed. Partial tags at delta
31
+ boundaries are held back until the next delta resolves them, and
32
+ end-of-stream flushing surfaces any held-back prose that turned out
33
+ not to be a real tag.
34
+
35
+ Usage::
36
+
37
+ scrubber = StreamingThinkScrubber()
38
+ for delta in stream:
39
+ visible = scrubber.feed(delta)
40
+ if visible:
41
+ emit(visible)
42
+ tail = scrubber.flush() # at end of stream
43
+ if tail:
44
+ emit(tail)
45
+
46
+ The scrubber is re-entrant per agent instance. Call ``reset()`` at
47
+ the top of each new turn so a hung block from an interrupted prior
48
+ stream cannot taint the next turn's output.
49
+
50
+ Tag variants handled (case-insensitive):
51
+ ``<think>``, ``<thinking>``, ``<reasoning>``, ``<thought>``,
52
+ ``<REASONING_SCRATCHPAD>``.
53
+
54
+ Block-boundary rule for opens: an opening tag is only treated as a
55
+ reasoning-block opener when it appears at the start of the stream,
56
+ after a newline (optionally followed by whitespace), or when only
57
+ whitespace has been emitted on the current line. This prevents prose
58
+ that *mentions* the tag name (e.g. ``"use <think> tags here"``) from
59
+ being incorrectly suppressed. Closed pairs (``<think>X</think>``) are
60
+ always suppressed regardless of boundary; a closed pair is an
61
+ intentional, bounded construct.
62
+ """
63
+
64
+ from __future__ import annotations
65
+
66
+ from typing import Tuple
67
+
68
+ __all__ = ["StreamingThinkScrubber", "scrub_think_blocks"]
69
+
70
+
71
+ def scrub_think_blocks(content: str) -> str:
72
+ """Scrub reasoning blocks from a COMPLETE assistant string (non-streaming).
73
+
74
+ Equivalent to feeding the whole string as one delta then flushing. Safe no-op when
75
+ there's no '<' (the overwhelming-majority fast path), so a non-reasoning completion
76
+ is never touched. Used by the adapter content seam where the whole completion arrives
77
+ at once (UP-07).
78
+ """
79
+ if not content or "<" not in content:
80
+ return content
81
+ s = StreamingThinkScrubber()
82
+ out = s.feed(content)
83
+ return out + s.flush()
84
+
85
+
86
+ class StreamingThinkScrubber:
87
+ """Stateful scrubber for streaming reasoning/thinking blocks.
88
+
89
+ State machine:
90
+ - ``_in_block``: True while inside an opened block, waiting for
91
+ a close tag. All text inside is discarded.
92
+ - ``_buf``: held-back partial-tag tail. Emitted / discarded on
93
+ the next ``feed()`` call or by ``flush()``.
94
+ - ``_last_emitted_ended_newline``: True iff the most recent
95
+ emission to the consumer ended with ``\\n``, or nothing has
96
+ been emitted yet (start-of-stream counts as a boundary). Used
97
+ to decide whether an open tag at buffer position 0 is at a
98
+ block boundary.
99
+ """
100
+
101
+ _OPEN_TAG_NAMES: Tuple[str, ...] = (
102
+ "think",
103
+ "thinking",
104
+ "reasoning",
105
+ "thought",
106
+ "REASONING_SCRATCHPAD",
107
+ )
108
+
109
+ # Materialise literal tag strings so the hot path does string
110
+ # operations, not regex compilation per feed().
111
+ _OPEN_TAGS: Tuple[str, ...] = tuple(f"<{name}>" for name in _OPEN_TAG_NAMES)
112
+ _CLOSE_TAGS: Tuple[str, ...] = tuple(f"</{name}>" for name in _OPEN_TAG_NAMES)
113
+
114
+ # Pre-compute the longest tag (for partial-tag hold-back bound).
115
+ _MAX_TAG_LEN: int = max(len(tag) for tag in _OPEN_TAGS + _CLOSE_TAGS)
116
+
117
+ def __init__(self) -> None:
118
+ self._in_block: bool = False
119
+ self._buf: str = ""
120
+ self._last_emitted_ended_newline: bool = True
121
+
122
+ def reset(self) -> None:
123
+ """Reset all state. Call at the top of every new turn."""
124
+ self._in_block = False
125
+ self._buf = ""
126
+ self._last_emitted_ended_newline = True
127
+
128
+ def feed(self, text: str) -> str:
129
+ """Feed one delta; return the scrubbed visible portion.
130
+
131
+ May return an empty string when the entire delta is reasoning
132
+ content or is being held back pending resolution of a partial
133
+ tag at the boundary.
134
+ """
135
+ if not text:
136
+ return ""
137
+ buf = self._buf + text
138
+ self._buf = ""
139
+ out: list[str] = []
140
+
141
+ while buf:
142
+ if self._in_block:
143
+ # Hunt for the earliest close tag.
144
+ close_idx, close_len = self._find_first_tag(
145
+ buf, self._CLOSE_TAGS,
146
+ )
147
+ if close_idx == -1:
148
+ # No close yet — hold back a potential partial
149
+ # close-tag prefix; discard everything else.
150
+ held = self._max_partial_suffix(buf, self._CLOSE_TAGS)
151
+ self._buf = buf[-held:] if held else ""
152
+ return "".join(out)
153
+ # Found close: discard block content + tag, continue.
154
+ buf = buf[close_idx + close_len:]
155
+ self._in_block = False
156
+ else:
157
+ # Priority 1 — closed <tag>X</tag> pair anywhere in
158
+ # buf. Closed pairs are always an intentional,
159
+ # bounded construct (even mid-line prose containing
160
+ # an open/close pair is almost certainly a model
161
+ # leaking reasoning inline), so no boundary gating.
162
+ pair = self._find_earliest_closed_pair(buf)
163
+ # Priority 2 — unterminated open tag at a block
164
+ # boundary. Boundary-gated so prose that mentions
165
+ # '<think>' isn't over-stripped.
166
+ open_idx, open_len = self._find_open_at_boundary(
167
+ buf, out,
168
+ )
169
+
170
+ # Pick whichever match comes earliest in the buffer.
171
+ if pair is not None and (
172
+ open_idx == -1 or pair[0] <= open_idx
173
+ ):
174
+ start_idx, end_idx = pair
175
+ preceding = buf[:start_idx]
176
+ if preceding:
177
+ preceding = self._strip_orphan_close_tags(preceding)
178
+ if preceding:
179
+ out.append(preceding)
180
+ self._last_emitted_ended_newline = (
181
+ preceding.endswith("\n")
182
+ )
183
+ buf = buf[end_idx:]
184
+ continue
185
+
186
+ if open_idx != -1:
187
+ # Unterminated open at boundary — emit preceding,
188
+ # enter block, continue loop with remainder.
189
+ preceding = buf[:open_idx]
190
+ if preceding:
191
+ preceding = self._strip_orphan_close_tags(preceding)
192
+ if preceding:
193
+ out.append(preceding)
194
+ self._last_emitted_ended_newline = (
195
+ preceding.endswith("\n")
196
+ )
197
+ self._in_block = True
198
+ buf = buf[open_idx + open_len:]
199
+ continue
200
+
201
+ # No resolvable tag structure in buf. Hold back any
202
+ # partial-tag prefix at the tail so a split tag
203
+ # across deltas isn't missed, then emit the rest.
204
+ held = self._max_partial_suffix(buf, self._OPEN_TAGS)
205
+ held_close = self._max_partial_suffix(
206
+ buf, self._CLOSE_TAGS,
207
+ )
208
+ held = max(held, held_close)
209
+ if held:
210
+ emit_text = buf[:-held]
211
+ self._buf = buf[-held:]
212
+ else:
213
+ emit_text = buf
214
+ self._buf = ""
215
+ if emit_text:
216
+ emit_text = self._strip_orphan_close_tags(emit_text)
217
+ if emit_text:
218
+ out.append(emit_text)
219
+ self._last_emitted_ended_newline = (
220
+ emit_text.endswith("\n")
221
+ )
222
+ return "".join(out)
223
+
224
+ return "".join(out)
225
+
226
+ def flush(self) -> str:
227
+ """End-of-stream flush.
228
+
229
+ If still inside an unterminated block, held-back content is
230
+ discarded — leaking partial reasoning is worse than a
231
+ truncated answer. Otherwise the held-back partial-tag tail is
232
+ emitted verbatim (it turned out not to be a real tag prefix).
233
+ """
234
+ if self._in_block:
235
+ self._buf = ""
236
+ self._in_block = False
237
+ return ""
238
+ tail = self._buf
239
+ self._buf = ""
240
+ if not tail:
241
+ return ""
242
+ tail = self._strip_orphan_close_tags(tail)
243
+ if tail:
244
+ self._last_emitted_ended_newline = tail.endswith("\n")
245
+ return tail
246
+
247
+ # ── internal helpers ───────────────────────────────────────────────
248
+
249
+ @staticmethod
250
+ def _find_first_tag(
251
+ buf: str, tags: Tuple[str, ...],
252
+ ) -> Tuple[int, int]:
253
+ """Return (earliest_index, tag_length) over *tags*, or (-1, 0).
254
+
255
+ Case-insensitive match.
256
+ """
257
+ buf_lower = buf.lower()
258
+ best_idx = -1
259
+ best_len = 0
260
+ for tag in tags:
261
+ idx = buf_lower.find(tag.lower())
262
+ if idx != -1 and (best_idx == -1 or idx < best_idx):
263
+ best_idx = idx
264
+ best_len = len(tag)
265
+ return best_idx, best_len
266
+
267
+ def _find_earliest_closed_pair(self, buf: str):
268
+ """Return (start_idx, end_idx) of the earliest closed pair, else None.
269
+
270
+ A closed pair is ``<tag>...</tag>`` of any variant. Matches are
271
+ case-insensitive and non-greedy (the closest close tag after
272
+ an open tag wins), matching the regex ``<tag>.*?</tag>``
273
+ semantics of ``_strip_think_blocks`` case 1. When two tag
274
+ variants could both match, the one whose open tag appears
275
+ earlier wins.
276
+ """
277
+ buf_lower = buf.lower()
278
+ best: "tuple[int, int] | None" = None
279
+ for open_tag, close_tag in zip(self._OPEN_TAGS, self._CLOSE_TAGS):
280
+ open_lower = open_tag.lower()
281
+ close_lower = close_tag.lower()
282
+ open_idx = buf_lower.find(open_lower)
283
+ if open_idx == -1:
284
+ continue
285
+ # P2-13: DEPTH-aware close matching. A nested same-variant block
286
+ # (<think>a<think>b</think>c</think>) used to match the FIRST inner close
287
+ # (non-greedy), which ended the block early and LEAKED the outer tail
288
+ # ("c") plus an orphan </think>. Count nested opens/closes and extend to
289
+ # the OUTER close. If the outer close isn't in the buffer yet (streaming,
290
+ # split mid-nest), matched_end stays -1 and we skip — the open-at-boundary
291
+ # / in-block path then handles it, same as an unterminated block.
292
+ depth = 1
293
+ pos = open_idx + len(open_lower)
294
+ matched_end = -1
295
+ while pos < len(buf_lower):
296
+ next_open = buf_lower.find(open_lower, pos)
297
+ next_close = buf_lower.find(close_lower, pos)
298
+ if next_close == -1:
299
+ break # no closing tag yet
300
+ if next_open != -1 and next_open < next_close:
301
+ depth += 1
302
+ pos = next_open + len(open_lower)
303
+ else:
304
+ depth -= 1
305
+ pos = next_close + len(close_lower)
306
+ if depth == 0:
307
+ matched_end = pos
308
+ break
309
+ if matched_end == -1:
310
+ continue
311
+ if best is None or open_idx < best[0]:
312
+ best = (open_idx, matched_end)
313
+ return best
314
+
315
+ def _find_open_at_boundary(
316
+ self, buf: str, already_emitted: list[str],
317
+ ) -> Tuple[int, int]:
318
+ """Return the earliest block-boundary open-tag (idx, len).
319
+
320
+ Returns (-1, 0) if no boundary-legal opener is present.
321
+ """
322
+ buf_lower = buf.lower()
323
+ best_idx = -1
324
+ best_len = 0
325
+ for tag in self._OPEN_TAGS:
326
+ tag_lower = tag.lower()
327
+ search_start = 0
328
+ while True:
329
+ idx = buf_lower.find(tag_lower, search_start)
330
+ if idx == -1:
331
+ break
332
+ if self._is_block_boundary(buf, idx, already_emitted):
333
+ if best_idx == -1 or idx < best_idx:
334
+ best_idx = idx
335
+ best_len = len(tag)
336
+ break # first boundary hit for this tag is enough
337
+ search_start = idx + 1
338
+ return best_idx, best_len
339
+
340
+ def _is_block_boundary(
341
+ self, buf: str, idx: int, already_emitted: list[str],
342
+ ) -> bool:
343
+ """True iff position *idx* in *buf* is a block boundary.
344
+
345
+ A block boundary is:
346
+ - buf position 0 AND the most recent emission ended with
347
+ a newline (or nothing has been emitted yet)
348
+ - any position whose preceding text on the current line
349
+ (since the last newline in buf) is whitespace-only, AND
350
+ if there is no newline in the preceding buf portion, the
351
+ most recent prior emission ended with a newline
352
+ """
353
+ if idx == 0:
354
+ # Check whether the last already-emitted chunk in THIS
355
+ # feed() call ended with a newline, otherwise fall back
356
+ # to the cross-feed flag.
357
+ if already_emitted:
358
+ return already_emitted[-1].endswith("\n")
359
+ return self._last_emitted_ended_newline
360
+ preceding = buf[:idx]
361
+ last_nl = preceding.rfind("\n")
362
+ if last_nl == -1:
363
+ # No newline in buf before the tag — boundary only if the
364
+ # prior emission ended with a newline AND everything since
365
+ # is whitespace.
366
+ if already_emitted:
367
+ prior_newline = already_emitted[-1].endswith("\n")
368
+ else:
369
+ prior_newline = self._last_emitted_ended_newline
370
+ return prior_newline and preceding.strip() == ""
371
+ # Newline present — text between it and the tag must be
372
+ # whitespace-only.
373
+ return preceding[last_nl + 1:].strip() == ""
374
+
375
+ @classmethod
376
+ def _max_partial_suffix(
377
+ cls, buf: str, tags: Tuple[str, ...],
378
+ ) -> int:
379
+ """Return the longest buf-suffix that is a prefix of any tag.
380
+
381
+ Only prefixes strictly shorter than the tag itself count
382
+ (full-length suffixes are the tag and are handled as matches,
383
+ not held-back partials). Case-insensitive.
384
+ """
385
+ if not buf:
386
+ return 0
387
+ buf_lower = buf.lower()
388
+ max_check = min(len(buf_lower), cls._MAX_TAG_LEN - 1)
389
+ for i in range(max_check, 0, -1):
390
+ suffix = buf_lower[-i:]
391
+ for tag in tags:
392
+ tag_lower = tag.lower()
393
+ if len(tag_lower) > i and tag_lower.startswith(suffix):
394
+ return i
395
+ return 0
396
+
397
+ @classmethod
398
+ def _strip_orphan_close_tags(cls, text: str) -> str:
399
+ """Remove *boundary* orphan close tags from *text*.
400
+
401
+ An orphan close tag has no matching open in the current scrubber
402
+ state. It is stripped (with trailing whitespace) ONLY when it sits
403
+ at a block boundary — the start of the text or after a newline with
404
+ nothing but whitespace before it on that line. A close tag that
405
+ appears mid-prose (e.g. the assistant legitimately writing
406
+ ``"look for </think> in logs"`` or generating code/docs about
407
+ reasoning tags) is genuine output and is preserved verbatim.
408
+
409
+ Before boundary-gating, this stripped EVERY close tag unconditionally,
410
+ silently deleting legitimate prose that merely mentioned one.
411
+ """
412
+ if "</" not in text:
413
+ return text
414
+ text_lower = text.lower()
415
+ out: list[str] = []
416
+ i = 0
417
+ while i < len(text):
418
+ matched = False
419
+ if text_lower[i:i + 2] == "</":
420
+ for tag in cls._CLOSE_TAGS:
421
+ tag_lower = tag.lower()
422
+ tag_len = len(tag_lower)
423
+ if text_lower[i:i + tag_len] != tag_lower:
424
+ continue
425
+ # Boundary gate: only an orphan close at the start of a
426
+ # line (preceding text on this line is whitespace-only) is
427
+ # treated as noise; mid-prose mentions are kept.
428
+ emitted = "".join(out)
429
+ last_nl = emitted.rfind("\n")
430
+ line_prefix = emitted[last_nl + 1:] if last_nl != -1 else emitted
431
+ if line_prefix.strip() != "":
432
+ break # mid-prose — keep the tag as literal text
433
+ # Skip the tag and any trailing whitespace,
434
+ # matching _strip_think_blocks case 3.
435
+ j = i + tag_len
436
+ while j < len(text) and text[j] in " \t\n\r":
437
+ j += 1
438
+ i = j
439
+ matched = True
440
+ break
441
+ if not matched:
442
+ out.append(text[i])
443
+ i += 1
444
+ return "".join(out)