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,611 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from modules.llm.messages import (
7
+ AIMessage,
8
+ BaseMessage,
9
+ HumanMessage,
10
+ MessageOrigin,
11
+ SystemMessage,
12
+ ToolMessage,
13
+ make_control_message,
14
+ )
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def _transient_compaction_errors() -> tuple:
20
+ """Exception types that mean "retry later", not "this prompt is bad" (Phase 0.3).
21
+
22
+ A rate-limit / auth blip / connection drop during summarization should ABORT
23
+ compaction and keep full context, not trigger permanent lossy fallback. Imported
24
+ lazily-at-module-load and tolerant of a missing symbol so a core.exceptions
25
+ refactor can't break import of this hot module.
26
+ """
27
+ names = ("LLMRateLimitError", "LLMAuthenticationError", "LLMConnectionError")
28
+ import core.exceptions as _exc
29
+ types = tuple(t for t in (getattr(_exc, n, None) for n in names) if isinstance(t, type))
30
+ # TimeoutError is always transient; include the stdlib one as a floor.
31
+ return types + (TimeoutError,)
32
+
33
+
34
+ _TRANSIENT_COMPACTION_ERRORS = _transient_compaction_errors()
35
+
36
+ # Compaction payload tuning (Reference-parity upgrade — see
37
+ # docs/REFERENCE_VS_ROB_CONTEXT_SYSTEM_2026-06.md §9).
38
+ _PER_MSG_CAP = 3000 # head+tail char budget per ordinary message (was a hard [:500])
39
+ _TOOL_RESULT_CAP = 3000 # head+tail char budget for a tool result (was the literal "[Tool result]")
40
+ _TAIL_TOKEN_RATIO = 0.20 # protected-tail token budget = max_input_tokens * ratio (C3)
41
+ _MIN_KEEP_RECENT = 10 # floor for the protected tail
42
+ _THRASH_SAVINGS_FLOOR = 0.10 # if last 2 compactions each saved < this, stop trying (B4)
43
+ _STATIC_FALLBACK_CAP = 8000 # char ceiling for the deterministic fallback summary (A6)
44
+ _COMPACTED_MARKER = "[COMPACTED SESSION HISTORY]"
45
+ # Absolute char budget for a single summarization prompt (~150K tokens). When the middle
46
+ # exceeds this, summarize in iterative windows so a SMALL auxiliary model (A5) can't be
47
+ # overflowed — Reference-style chunked/iterative summarization. No data is dropped.
48
+ _SUMMARIZE_INPUT_BUDGET_CHARS = 600_000
49
+
50
+
51
+ def _head_tail(text: str, cap: int) -> str:
52
+ """Truncate ``text`` to ``cap`` chars keeping the head AND tail (Reference-style).
53
+
54
+ A flat ``[:cap]`` throws away the end of a message, which is often where the
55
+ decision/result lives. Head+tail preserves both ends with an elision marker.
56
+ """
57
+ if len(text) <= cap:
58
+ return text
59
+ half = max(1, cap // 2)
60
+ return f"{text[:half]}\n…[{len(text) - cap} chars elided]…\n{text[-half:]}"
61
+
62
+
63
+ class CompactorMixin:
64
+ # Empty slots so the composed MessageManager keeps its own __slots__ (no __dict__).
65
+ __slots__ = ()
66
+
67
+ def clear_history_keep_system(self, keep_last_n: int = 2) -> None:
68
+ """Clear message history but keep system messages and last N messages.
69
+
70
+ This is used for loop detection recovery - removes context while preserving
71
+ essential system setup and recent conversation flow.
72
+
73
+ Args:
74
+ keep_last_n: Number of most recent messages to keep (default 2)
75
+ """
76
+ # SECURITY FIX: Use _history_lock to protect deque operations
77
+ with self._history_lock:
78
+ messages_to_keep = []
79
+
80
+ # Keep all SystemMessage instances
81
+ for managed_msg in self.history.messages:
82
+ msg = managed_msg.message
83
+ if isinstance(msg, SystemMessage):
84
+ messages_to_keep.append(managed_msg)
85
+
86
+ # Add last N messages if available
87
+ if len(self.history.messages) >= keep_last_n:
88
+ messages_to_keep.extend(list(self.history.messages)[-keep_last_n:])
89
+
90
+ # Clear and re-add kept messages
91
+ self.history.messages.clear()
92
+ for managed_msg in messages_to_keep:
93
+ self.history.messages.append(managed_msg)
94
+
95
+ # Recalculate token count
96
+ self.history.total_tokens = sum(m.metadata.input_tokens + (m.metadata.output_tokens or 0) for m in self.history.messages)
97
+ self.logger.info(f"Cleared history, kept {len(messages_to_keep)} messages ({self.history.total_tokens} tokens)")
98
+
99
+ def emergency_context_prune(self) -> None:
100
+ """Emergency pruning of message history to recover from context overflow.
101
+
102
+ Keeps only:
103
+ 1. System message
104
+ 2. Initial task (first human message)
105
+ 3. Last N messages with complete tool call pairs
106
+
107
+ CRITICAL: Preserves complete AIMessage+ToolMessage pairs to prevent sequence validation errors.
108
+ Hierarchical memory will be re-injected on next LLM call.
109
+ """
110
+ self.logger.warning("🚨 EMERGENCY CONTEXT PRUNING INITIATED")
111
+
112
+ messages = list(self.history.messages)
113
+ original_count = len(messages)
114
+ original_tokens = self.history.total_tokens
115
+
116
+ if original_count <= 5:
117
+ self.logger.info("Message count already minimal, skipping prune")
118
+ return
119
+
120
+ preserved = []
121
+
122
+ # Keep system message (if exists) - but it's stored separately, not in history.messages
123
+ # This is just for safety in case old code added it
124
+ remaining_messages = messages
125
+ if messages and isinstance(messages[0].message if hasattr(messages[0], 'message') else messages[0], SystemMessage):
126
+ preserved.append(messages[0])
127
+ remaining_messages = messages[1:]
128
+
129
+ # Keep initial task (first human message) - but it's also stored separately
130
+ # This is for safety in case old code added it
131
+ for i, managed_msg in enumerate(remaining_messages):
132
+ msg = managed_msg.message if hasattr(managed_msg, 'message') else managed_msg
133
+ if isinstance(msg, HumanMessage):
134
+ preserved.append(managed_msg)
135
+ remaining_messages = remaining_messages[i+1:]
136
+ break
137
+
138
+ # ✅ FIX: Keep last N messages with complete tool call pairs
139
+ # Start with last 3 messages and expand if needed to complete pairs
140
+ min_recent_messages = 3
141
+ recent_messages = []
142
+
143
+ # Work backwards from the end to collect complete tool call pairs
144
+ idx = len(remaining_messages) - 1
145
+ messages_collected = 0
146
+ pending_tool_call_ids = set() # Tool calls waiting for responses
147
+
148
+ while idx >= 0 and messages_collected < min_recent_messages + len(pending_tool_call_ids) * 2:
149
+ managed_msg = remaining_messages[idx]
150
+ msg = managed_msg.message if hasattr(managed_msg, 'message') else managed_msg
151
+
152
+ # Check if this is a ToolMessage
153
+ if isinstance(msg, ToolMessage) and hasattr(msg, 'tool_call_id'):
154
+ recent_messages.insert(0, managed_msg)
155
+ pending_tool_call_ids.add(msg.tool_call_id) # Mark that we need the corresponding AIMessage
156
+ messages_collected += 1
157
+
158
+ # Check if this is an AIMessage with tool_calls
159
+ elif isinstance(msg, AIMessage) and hasattr(msg, 'tool_calls') and msg.tool_calls:
160
+ recent_messages.insert(0, managed_msg)
161
+ messages_collected += 1
162
+
163
+ # Remove tool calls that we've already seen responses for
164
+ for tc in msg.tool_calls:
165
+ tc_id = tc.get('id') if isinstance(tc, dict) else getattr(tc, 'id', None)
166
+ if tc_id in pending_tool_call_ids:
167
+ pending_tool_call_ids.discard(tc_id)
168
+ else:
169
+ # This tool call doesn't have a response yet - we need to keep searching
170
+ # But we've already added this AIMessage, so just track it
171
+ pass
172
+
173
+ else:
174
+ # Regular message (HumanMessage, SystemMessage without tool calls, etc.)
175
+ recent_messages.insert(0, managed_msg)
176
+ messages_collected += 1
177
+
178
+ idx -= 1
179
+
180
+ # If we still have pending tool call IDs, log a warning
181
+ if pending_tool_call_ids:
182
+ self.logger.warning(
183
+ f"⚠️ Emergency prune: Could not find AIMessages for tool calls: {pending_tool_call_ids}. "
184
+ f"Message sequence may be invalid."
185
+ )
186
+
187
+ preserved.extend(recent_messages)
188
+
189
+ # Reconstruct history
190
+ self.history.messages.clear()
191
+ self.history.total_tokens = 0
192
+
193
+ for managed_msg in preserved:
194
+ msg = managed_msg.message if hasattr(managed_msg, 'message') else managed_msg
195
+ # Re-add each message to recalculate tokens
196
+ self._add_message_with_tokens(msg, _internal=True)
197
+
198
+ new_count = len(self.history.messages)
199
+ new_tokens = self.history.total_tokens
200
+
201
+ # B4 recovery: an emergency prune materially changes the history, so any prior
202
+ # "compaction isn't helping" (anti-thrash) signal is now stale. Reset it so LLM
203
+ # compaction gets a fresh chance once usage climbs back into the 85-95% band.
204
+ self._compaction_savings = []
205
+
206
+ self.logger.warning(
207
+ f"✅ Emergency pruning complete:\n"
208
+ f" Messages: {original_count} → {new_count} (removed {original_count - new_count})\n"
209
+ f" Tokens: {original_tokens:,} → {new_tokens:,} (freed {original_tokens - new_tokens:,})\n"
210
+ f" Usage: {self.get_context_usage_percent():.1f}%"
211
+ )
212
+
213
+ # ------------------------------------------------------------------ #
214
+ # Compaction tail sizing (C3) + anti-thrash (B4)
215
+ # ------------------------------------------------------------------ #
216
+ def _compaction_keep_recent(self, messages: list, min_keep: int = _MIN_KEEP_RECENT) -> int:
217
+ """How many recent messages to keep intact (token-budget OR count, generous).
218
+
219
+ C3: instead of a flat ``keep_recent=10``, protect the tail by a token budget
220
+ (``max_input_tokens * ratio``) when that protects MORE than the floor — so a
221
+ token-dense history keeps the meaningful recent turns, not just the last 10.
222
+ """
223
+ max_in = int(getattr(self, "max_input_tokens", 0) or 0)
224
+ budget = int(max_in * _TAIL_TOKEN_RATIO)
225
+ if budget <= 0:
226
+ return min_keep
227
+ count = 0
228
+ used = 0
229
+ for msg in reversed(messages):
230
+ tokens = max(1, len(str(getattr(msg, "content", ""))) // 4)
231
+ if used + tokens > budget:
232
+ break
233
+ used += tokens
234
+ count += 1
235
+ return max(count, min_keep)
236
+
237
+ def _compaction_is_thrashing(self) -> bool:
238
+ """B4: if the last two compactions each saved < floor, stop calling the LLM.
239
+
240
+ The ≥95% emergency prune (a non-LLM net) still runs from ``step.py`` — this
241
+ only suppresses the *expensive* LLM compaction when it has stopped helping.
242
+ """
243
+ savings = getattr(self, "_compaction_savings", [])
244
+ recent = savings[-2:]
245
+ return len(recent) == 2 and all(s < _THRASH_SAVINGS_FLOOR for s in recent)
246
+
247
+ def _record_compaction_savings(self, original_tokens: int, new_tokens: int) -> None:
248
+ saved = (original_tokens - new_tokens) / max(original_tokens, 1)
249
+ savings = list(getattr(self, "_compaction_savings", []))
250
+ savings.append(max(0.0, saved))
251
+ self._compaction_savings = savings[-2:]
252
+
253
+ # ------------------------------------------------------------------ #
254
+ # Pre-compaction checkpoint (C2 — scoped-down lineage)
255
+ # ------------------------------------------------------------------ #
256
+ def _resolve_checkpoint_dir(self) -> Optional[str]:
257
+ explicit = getattr(self, "_compaction_checkpoint_dir", None)
258
+ if explicit:
259
+ return explicit
260
+ session_id = getattr(self, "session_id", None)
261
+ if not session_id:
262
+ return None
263
+ try:
264
+ from agents.task.path import pm
265
+ return str(pm().get_history_dir(session_id))
266
+ except Exception:
267
+ return None
268
+
269
+ def _write_compaction_checkpoint(self, messages: list) -> None:
270
+ """Persist the raw pre-compaction trajectory so it stays recoverable (C2)."""
271
+ checkpoint_dir = self._resolve_checkpoint_dir()
272
+ if not checkpoint_dir:
273
+ return
274
+ try:
275
+ import os
276
+ import json
277
+ os.makedirs(checkpoint_dir, exist_ok=True)
278
+ n = getattr(self, "_compaction_count", 0)
279
+ path = os.path.join(checkpoint_dir, f"compaction_{n}.json")
280
+ payload = [
281
+ {"role": m.__class__.__name__, "content": str(getattr(m, "content", ""))}
282
+ for m in messages
283
+ ]
284
+ with open(path, "w", encoding="utf-8") as fh:
285
+ json.dump(payload, fh, ensure_ascii=False)
286
+ self._compaction_count = n + 1
287
+ except Exception as exc: # never let audit I/O block compaction
288
+ self.logger.debug(f"compaction checkpoint skipped: {exc}")
289
+
290
+ # ------------------------------------------------------------------ #
291
+ # LLM compaction
292
+ # ------------------------------------------------------------------ #
293
+ async def _run_pre_compress_hook(self) -> None:
294
+ """Invoke the active memory provider's on_pre_compress hook (P1-4).
295
+
296
+ Lets a provider persist anything in the volatile message tail that hasn't been
297
+ promoted to durable memory before compaction summarizes it away. Fail-open: a
298
+ hook error never blocks compaction. No-op under the default NullMemoryProvider.
299
+ """
300
+ try:
301
+ from modules.memory.registry import get_memory_registry
302
+ provider = get_memory_registry().active()
303
+ if provider is None or not getattr(provider, "is_external", False):
304
+ return
305
+ await provider.on_pre_compress(session_id=getattr(self, "session_id", "") or "")
306
+ except Exception as e:
307
+ self.logger.debug(f"on_pre_compress hook skipped: {e}")
308
+
309
+ async def llm_compact_history(self) -> bool:
310
+ """LLM-driven compaction of message history.
311
+
312
+ Reference-parity upgrade (see docs/REFERENCE_VS_ROB_CONTEXT_SYSTEM_2026-06.md):
313
+ - A1: feed the WHOLE middle to the summarizer (no silent [-50:] drop).
314
+ - A2: serialize tool results + raise the per-message cap (no [Tool result] literal).
315
+ - A3: structured, sectioned summary template scaled to the volume compacted.
316
+ - A4: iterative update — feed any prior compacted summary back in to update.
317
+ - A5: route to a cheap auxiliary model (``aux_llm``) when configured.
318
+ - A6: deterministic static fallback when the summarizer errors.
319
+ - B4: anti-thrash back-off; C2: pre-compaction checkpoint; C3: token-budget tail.
320
+
321
+ Returns:
322
+ True if compaction (or static fallback) was performed, False otherwise.
323
+ """
324
+ original_messages = [m.message for m in self.history.messages]
325
+ original_count = len(original_messages)
326
+ original_tokens = self.history.total_tokens
327
+
328
+ if original_count < 15:
329
+ self.logger.debug("Not enough messages to compact")
330
+ return False
331
+
332
+ # B4: stop burning an LLM call when the last two compactions barely helped.
333
+ if self._compaction_is_thrashing():
334
+ self.logger.info("Skipping LLM compaction (anti-thrash: last 2 saved < 10%)")
335
+ return False
336
+
337
+ # C3: protect the tail by token budget OR count, whichever is more generous.
338
+ keep_recent = self._compaction_keep_recent(original_messages)
339
+ # CX-H2: never cut inside an AIMessage(tool_calls)→ToolMessage pair — a tail
340
+ # that starts with orphan ToolMessages gets them silently dropped by
341
+ # repair_tool_message_pairs on every subsequent LLM call. Pull leading
342
+ # orphans back into the summarized span.
343
+ while keep_recent < len(original_messages):
344
+ # D2: original_messages is already unwrapped to BaseMessage above
345
+ # (`[m.message for m in self.history.messages]`), so head IS the
346
+ # message — the old `hasattr(head, 'message')` unwrap branch was inert.
347
+ head_msg = original_messages[-keep_recent]
348
+ if isinstance(head_msg, ToolMessage):
349
+ keep_recent += 1 # extend the tail to include the owning AIMessage
350
+ else:
351
+ break
352
+ messages_to_summarize = original_messages[:-keep_recent]
353
+ recent_messages = original_messages[-keep_recent:]
354
+
355
+ if len(messages_to_summarize) < 5:
356
+ self.logger.debug("Not enough messages to summarize")
357
+ return False
358
+
359
+ # C2: snapshot the raw trajectory before we mutate it.
360
+ self._write_compaction_checkpoint(original_messages)
361
+
362
+ # A4: separate any prior compacted summary so it is UPDATED, not re-summarized.
363
+ prior_summary: Optional[str] = None
364
+ to_summarize: list = []
365
+ for msg in messages_to_summarize:
366
+ text = str(getattr(msg, "content", ""))
367
+ origin = getattr(msg, "origin", "")
368
+ # Envelope-safe + reload-safe: match the typed origin OR the marker anywhere
369
+ # in the leading slice (the marker now sits just inside <compacted-history>).
370
+ if origin == MessageOrigin.COMPACTION_SUMMARY or _COMPACTED_MARKER in text[:160]:
371
+ prior_summary = text # keep the most recent prior summary
372
+ else:
373
+ to_summarize.append(msg)
374
+
375
+ # P1-4: give the active memory provider a chance to flush salient context
376
+ # before we summarize the middle away. Fail-open and inert by default
377
+ # (NullMemoryProvider's on_pre_compress is a no-op).
378
+ await self._run_pre_compress_hook()
379
+
380
+ # A5: prefer a cheap auxiliary model for compaction; fall back to the main LLM.
381
+ compaction_llm = getattr(self, "aux_llm", None) or self.llm
382
+
383
+ try:
384
+ summary_text = await self._run_summarization(to_summarize, prior_summary, compaction_llm)
385
+ if not (summary_text and summary_text.strip()):
386
+ # An empty LLM summary would clear the history and insert an empty
387
+ # digest — permanent DATA LOSS. Fall back to the deterministic static
388
+ # summary; if even that is empty, abort and keep the FULL context (the
389
+ # >=95% emergency prune is the overflow backstop).
390
+ self.logger.warning("LLM compaction returned an empty summary; using static fallback")
391
+ static = self._build_static_fallback_summary(to_summarize)
392
+ if static and prior_summary:
393
+ static = f"{prior_summary}\n\n{static}"
394
+ if not (static and static.strip()):
395
+ self.logger.warning("Static fallback also empty; keeping full context, will retry")
396
+ return False
397
+ summary_text = static
398
+ self._rebuild_with_summary(summary_text, len(to_summarize), recent_messages)
399
+ self._record_compaction_savings(original_tokens, self.history.total_tokens)
400
+
401
+ self.logger.info(
402
+ f"✅ LLM compaction complete: {original_count} → {len(self.history.messages)} messages, "
403
+ f"{original_tokens:,} → {self.history.total_tokens:,} tokens"
404
+ )
405
+ return True
406
+
407
+ except _TRANSIENT_COMPACTION_ERRORS as e:
408
+ # Phase 0.3 (Hermes parity): a transient/credential failure must NOT cause
409
+ # permanent lossy compaction. Abort, keep the FULL context, and retry next
410
+ # step. The >=95% mechanical emergency prune is the overflow backstop, so
411
+ # this is safe even if the failure persists.
412
+ self.logger.warning(
413
+ f"LLM compaction aborted (transient: {type(e).__name__}: {e}); "
414
+ "keeping full context, will retry next step"
415
+ )
416
+ return False
417
+
418
+ except Exception as e:
419
+ # A6: build a deterministic summary locally instead of dropping straight to prune.
420
+ self.logger.error(f"LLM compaction failed: {e}, building static fallback summary")
421
+ static = self._build_static_fallback_summary(to_summarize)
422
+ # BUG 4 fix: preserve the prior compacted summary in the fallback so
423
+ # iterative compaction does not drop the earlier digest on a non-transient
424
+ # error. The happy path already passes prior_summary into _run_summarization;
425
+ # the except branch must do the same by prepending it to the static output.
426
+ if static and prior_summary:
427
+ static = f"{prior_summary}\n\n{static}"
428
+ if static:
429
+ self._rebuild_with_summary(static, len(to_summarize), recent_messages)
430
+ self._record_compaction_savings(original_tokens, self.history.total_tokens)
431
+ self.logger.warning("Static fallback summary applied (summarizer unavailable)")
432
+ return True
433
+ self.logger.error("Static fallback empty; falling back to emergency prune")
434
+ self.emergency_context_prune()
435
+ return True
436
+
437
+ def _window_messages(self, messages: list, budget_chars: int) -> list:
438
+ """Split messages into ordered windows whose capped char-size each fit a budget."""
439
+ windows: list = []
440
+ current: list = []
441
+ size = 0
442
+ for msg in messages:
443
+ raw = str(getattr(msg, "content", ""))
444
+ cap = _TOOL_RESULT_CAP if "Tool" in msg.__class__.__name__ else _PER_MSG_CAP
445
+ msg_chars = min(len(raw), cap) + 20 # +role/label overhead
446
+ if current and size + msg_chars > budget_chars:
447
+ windows.append(current)
448
+ current = []
449
+ size = 0
450
+ current.append(msg)
451
+ size += msg_chars
452
+ if current:
453
+ windows.append(current)
454
+ return windows or [[]]
455
+
456
+ async def _run_summarization(self, messages: list, prior_summary: Optional[str], compaction_llm) -> str:
457
+ """Summarize ``messages`` in one shot, or in iterative windows when huge (A1/A4).
458
+
459
+ Iterative windowing means a SMALL auxiliary model can summarize an arbitrarily
460
+ large middle without overflowing: each window is summarized with the running
461
+ summary fed back in (Reference-style), so nothing is dropped.
462
+ """
463
+ windows = self._window_messages(messages, _SUMMARIZE_INPUT_BUDGET_CHARS)
464
+ if len(windows) > 1:
465
+ self.logger.info(f"Compaction input large; summarizing in {len(windows)} iterative windows")
466
+ running = prior_summary
467
+ for window in windows:
468
+ prompt = self._build_compaction_prompt(window, prior_summary=running)
469
+ import time as _time
470
+ _t0 = _time.time()
471
+ response = await compaction_llm.ainvoke([HumanMessage(content=prompt)])
472
+ # A3: meter this aux LLM call through the single deduction path (fail-open).
473
+ from agents.task.agent.core.aux_metering import meter_aux_llm
474
+ await meter_aux_llm(
475
+ usage_tracker=getattr(self, "usage_tracker", None),
476
+ user_id=getattr(self, "metering_user_id", None),
477
+ session_id=getattr(self, "session_id", ""),
478
+ agent_id=getattr(self, "metering_agent_id", "") or "",
479
+ llm=compaction_llm, response=response, duration_seconds=_time.time() - _t0,
480
+ component="compaction", purpose="compaction",
481
+ )
482
+ running = response.content if hasattr(response, 'content') else str(response)
483
+ return running or ""
484
+
485
+ def _rebuild_with_summary(self, summary_text: str, summarized_count: int, recent_messages: list) -> None:
486
+ """Replace the summarized middle with one compacted message + recent tail.
487
+
488
+ The summary is a LOSSY, LLM-synthesized digest — NOT a user turn. Tag it with
489
+ MessageOrigin.COMPACTION_SUMMARY and an <compacted-history> envelope so the model
490
+ reads it as derived context, not an instruction (the system prompt's source-precedence
491
+ rule ranks it below recent messages / current files).
492
+ """
493
+ body = (f"{_COMPACTED_MARKER}\n\n"
494
+ f"The following is a structured summary of {summarized_count} earlier messages:\n\n"
495
+ f"{summary_text}\n\n"
496
+ f"[END COMPACTED HISTORY - Recent conversation follows]")
497
+ compacted_msg = make_control_message(body, MessageOrigin.COMPACTION_SUMMARY)
498
+ self.history.messages.clear()
499
+ self.history.total_tokens = 0
500
+ self._add_message_with_tokens(compacted_msg, _internal=True)
501
+ for msg in recent_messages:
502
+ self._add_message_with_tokens(msg, _internal=True)
503
+
504
+ def _build_static_fallback_summary(self, messages: list) -> str:
505
+ """A6: deterministic, no-LLM summary from tool names / files / errors / asks."""
506
+ import re
507
+
508
+ files: list = []
509
+ errors: list = []
510
+ user_asks: list = []
511
+ tool_calls = 0
512
+ for msg in messages:
513
+ role = msg.__class__.__name__.replace("Message", "")
514
+ content = str(getattr(msg, "content", ""))
515
+ if "Tool" in role:
516
+ tool_calls += 1
517
+ for token in re.findall(r"[\w./\\-]+\.[A-Za-z0-9]{1,6}", content):
518
+ if token not in files:
519
+ files.append(token)
520
+ for line in content.splitlines():
521
+ low = line.lower()
522
+ if "error" in low or "exception" in low or "traceback" in low:
523
+ errors.append(line.strip()[:200])
524
+ elif role == "Human":
525
+ snippet = content.strip()
526
+ if snippet:
527
+ user_asks.append(snippet[:300])
528
+
529
+ files = files[:40]
530
+ errors = errors[:20]
531
+ sections = [
532
+ "## Active Task",
533
+ user_asks[0] if user_asks else "(unknown — static fallback)",
534
+ "## In Progress",
535
+ f"(static fallback after summarizer failure; {tool_calls} tool calls across {len(messages)} messages)",
536
+ "## Resolved Questions",
537
+ "(not derivable without the summarizer)",
538
+ "## Pending User Asks",
539
+ "\n".join(f"- {u}" for u in user_asks[-3:]) or "(none)",
540
+ "## Relevant Files & Data",
541
+ "\n".join(f"- {f}" for f in files) or "(none)",
542
+ "## Remaining Work",
543
+ ("Errors seen:\n" + "\n".join(f"- {e}" for e in errors)) if errors else "(none recorded)",
544
+ ]
545
+ return "\n".join(sections)[:_STATIC_FALLBACK_CAP]
546
+
547
+ def _build_compaction_prompt(self, messages: list, prior_summary: Optional[str] = None) -> str:
548
+ """Build a STRUCTURED summarization prompt from the full middle (A1/A2/A3/A4).
549
+
550
+ Args:
551
+ messages: messages to summarize (the whole middle, not the last 50).
552
+ prior_summary: an existing compacted summary to UPDATE in place (A4).
553
+ """
554
+ conversation_text = []
555
+ for msg in messages:
556
+ role = msg.__class__.__name__.replace("Message", "")
557
+ raw = str(msg.content) if hasattr(msg, 'content') else str(msg)
558
+ if 'Tool' in role:
559
+ # A2: serialize the tool output (head+tail) instead of a bare marker.
560
+ conversation_text.append(f"[Tool result] {_head_tail(raw, _TOOL_RESULT_CAP)}")
561
+ else:
562
+ conversation_text.append(f"{role}: {_head_tail(raw, _PER_MSG_CAP)}")
563
+
564
+ # A1: include the ENTIRE middle, oldest first.
565
+ conversation = "\n".join(conversation_text)
566
+
567
+ # A3: scale the target length to the volume compacted (~5 chars/word, 20% ratio).
568
+ total_chars = sum(len(t) for t in conversation_text)
569
+ budget_words = max(400, min(int(total_chars * _TAIL_TOKEN_RATIO / 5), 2400))
570
+
571
+ # A4: feed the prior summary back so it is merged/updated, not re-summarized blind.
572
+ prior_block = ""
573
+ if prior_summary:
574
+ prior_block = (
575
+ "## PRIOR SUMMARY (update this — PRESERVE all existing information, "
576
+ "merge in new facts, drop nothing):\n"
577
+ f"{prior_summary}\n\n"
578
+ )
579
+
580
+ return f"""Summarize the conversation below into a STRUCTURED running memory.
581
+ Fill every section; write "(none)" where empty. Preserve concrete data, IDs, file
582
+ paths, tool outcomes, and decisions verbatim where short.
583
+
584
+ ## Active Task
585
+ ## Goal & Constraints
586
+ ## Completed Actions (with outcomes)
587
+ ## In Progress
588
+ ## Blocked / Open Questions
589
+ ## Key Decisions
590
+ ## Resolved Questions
591
+ ## Pending User Asks
592
+ ## Relevant Files & Data
593
+ ## Remaining Work
594
+
595
+ Target length: ~{budget_words} words. Focus on WHAT was accomplished, not HOW.
596
+
597
+ TEMPORAL ANCHORING: record finished work as DONE (past tense, with its outcome) under
598
+ Completed Actions. Do NOT list an already-completed action under In Progress or
599
+ Remaining Work — after this summary replaces the raw history, anything left phrased as
600
+ a pending "to-do" will be RE-RUN. Only genuinely-unfinished work belongs in Remaining
601
+ Work.
602
+
603
+ {prior_block}Conversation to summarize:
604
+ {conversation}
605
+
606
+ STRUCTURED SUMMARY:"""
607
+
608
+ # (check_and_compact_if_needed removed 2026-06-29 dead-loop prune: it had ZERO
609
+ # callers. The live compaction policy is the hardcoded 85/95/70 thresholds in
610
+ # agent/core/step.py, not CompactionManager.should_compact_messages — this method
611
+ # was a dead alternate path that never ran.)