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,758 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import copy
5
+ import json
6
+ import logging
7
+ import threading
8
+ import time
9
+ from collections import deque
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List, Optional, Type, Tuple, Union
13
+
14
+ from agents.task.agent.views import ActionResult, AgentBrain, AgentOutput, AgentStepInfo
15
+
16
+ # Native message types
17
+ from modules.llm.messages import (
18
+ AIMessage,
19
+ BaseMessage,
20
+ HumanMessage,
21
+ SystemMessage,
22
+ ToolMessage,
23
+ MessageOrigin,
24
+ make_control_message,
25
+ )
26
+ from modules.llm.adapters import BaseChatModel
27
+
28
+ from tools.browser.views import BrowserState
29
+
30
+ # Import centralized LLM functionality - Single source of truth
31
+ try:
32
+ # Use model_registry as the canonical source for model configs
33
+ from modules.llm.model_registry import get_model_config
34
+ from modules.llm import count_tokens, count_messages_tokens
35
+ LLM_MODULE_AVAILABLE = True
36
+ except ImportError:
37
+ LLM_MODULE_AVAILABLE = False
38
+ # Create a logger here since we need it for the warning
39
+ logger = logging.getLogger(__name__)
40
+ logger.warning("LLM module not available, using fallback token counting")
41
+
42
+ # Import centralized constants
43
+ from agents.task.constants import IMG_TOKENS
44
+ # Model limits now come from modules.llm.model_registry
45
+
46
+ from agents.task.agent.message_manager.views import MessageHistory, MessageMetadata, ManagedMessage
47
+ from agents.task.agent.message_manager.tool_call_builder import (
48
+ ToolCallBuilder,
49
+ detect_and_remove_duplicate_tool_calls,
50
+ repair_and_normalize,
51
+ validate_tool_message_pairs
52
+ )
53
+ # MessageTrimmer removed - trimming disabled
54
+ from agents.task.agent.message_manager.config import MessageManagerConfig
55
+
56
+ from agents.task.agent.messages.token_counter import TokenCounterMixin
57
+ from agents.task.agent.messages.compactor import CompactorMixin
58
+ from agents.task.agent.messages.persistence import PersistenceMixin
59
+ from agents.task.agent.messages.filters import FiltersMixin
60
+ from agents.task.agent.messages.guidance import GuidanceMixin
61
+ from agents.task.agent.messages.builders import MessageBuildersMixin
62
+ from agents.task.agent.messages.retrieval import MessageRetrievalMixin
63
+
64
+ logger = logging.getLogger(__name__)
65
+
66
+ # Token counting is now centralized in modules/llm
67
+ # No local fallbacks needed
68
+
69
+ class MessageManager(TokenCounterMixin, CompactorMixin, PersistenceMixin, FiltersMixin, GuidanceMixin, MessageBuildersMixin, MessageRetrievalMixin):
70
+ __slots__ = (
71
+ 'llm', 'system_prompt_class', 'history', 'task', 'action_descriptions',
72
+ 'IMG_TOKENS', 'include_attributes',
73
+ 'max_error_length', 'max_actions_per_step',
74
+ 'sensitive_data', 'session_id', 'add_task_message',
75
+ 'max_input_tokens', 'safe_input_tokens', 'completion_reserve', 'system_prompt',
76
+ 'logger', 'use_native_tools',
77
+ 'tool_call_tracker', # Tool call tracker for ID management
78
+ 'task_context_manager', # Phase 2: Hierarchical memory system
79
+ 'compaction_manager', # FIX (Jan 2026): CompactionManager for context-aware compaction
80
+ 'mcp_servers', # MCP server info for dynamic prompt generation
81
+ '_ephemeral_messages', # One-shot messages to include on next LLM call
82
+ '_model_name', # Cached model name
83
+ '_provider_name', # Cached provider name
84
+ '_history_checkpoint', # Checkpoint for message history rollback
85
+ '_checkpoint_token_count', # Token count at checkpoint time
86
+ '_system_message', # SystemMessage stored separately to prevent deque eviction
87
+ '_system_message_tokens', # Token count for system message
88
+ '_initial_task_message', # Initial task message stored separately to prevent deque eviction
89
+ '_initial_task_tokens', # Token count for initial task message
90
+ '_skill_message', # PR13: skills injected as a pinned foundation user message (not in system prompt)
91
+ '_skill_message_tokens', # Token cost of the pinned skill message
92
+ '_self_context_message', # polyrob: frozen SOUL/identity doc pinned in the foundation
93
+ '_self_context_tokens', # Token cost of the pinned self-context message
94
+ '_project_context_message', # C9: auto-loaded CLAUDE.md/AGENTS.md frozen foundation message
95
+ '_project_context_tokens', # Token cost of the pinned project-context message
96
+ '_runtime_identity_message', # model/provider the agent actually runs (swap-refreshable)
97
+ '_runtime_identity_tokens', # Token cost of the pinned runtime-identity message
98
+ '_history_lock', # SECURITY FIX: Lock to protect deque operations from race conditions
99
+ 'aux_llm', # A5: optional cheap auxiliary model used only for llm_compact_history
100
+ '_compaction_savings', # B4: last-2 compaction savings ratios for anti-thrash
101
+ '_compaction_count', # C2: monotonic index for pre-compaction checkpoint files
102
+ '_compaction_checkpoint_dir', # C2: explicit checkpoint dir override (else pm() history dir)
103
+ )
104
+
105
+ def __init__(
106
+ self,
107
+ llm: BaseChatModel,
108
+ task: str,
109
+ action_descriptions: str,
110
+ system_prompt_class: Type, # Type of SystemPrompt
111
+ max_input_tokens: Optional[int] = None, # None = auto-calculate from model
112
+ image_tokens: int = IMG_TOKENS,
113
+ include_attributes: list[str] = [],
114
+ max_error_length: int = 400,
115
+ max_actions_per_step: int = 10,
116
+ sensitive_data: Optional[Dict[str, str]] = None,
117
+ session_id: Optional[str] = None,
118
+ include_examples: bool = True,
119
+ add_task_message: bool = True,
120
+ system_message: Optional[SystemMessage] = None, # NEW: Accept prebuilt system message
121
+ use_native_tools: bool = True, # Default to True to match Agent and config
122
+ tool_call_tracker=None, # Tool call tracker for ID management
123
+ task_context_manager=None, # Phase 2: Hierarchical memory system
124
+ mcp_servers: Optional[Dict[str, List[str]]] = None, # MCP server info for dynamic prompts
125
+ persona_block: Optional[str] = None, # S1: chat-mode persona for SystemPrompt <identity>
126
+ tool_ids: Optional[List[str]] = None, # Session's loaded tool_ids for config-aware prompt gating
127
+ include_vision: bool = True, # T1-06: session's use_vision → gates the vision prompt section
128
+ ):
129
+ # S1 (chat consolidation): persona text forwarded to the SystemPrompt class
130
+ # when this manager builds the system message itself (no prebuilt profile
131
+ # message). None/"" => byte-identical legacy prompt.
132
+ self._persona_block = persona_block
133
+ # Set up logger with session ID
134
+ from agents.task.logging_config import get_task_logger
135
+ self.logger = get_task_logger("messages", session_id)
136
+
137
+ self.llm = llm
138
+ self.system_prompt_class = system_prompt_class
139
+
140
+ # Cache model detection on init (single source of truth)
141
+ self._model_name = self._detect_model_name()
142
+ # Use consolidated provider detection from utils
143
+ from agents.task.utils import detect_llm_provider
144
+ self._provider_name = detect_llm_provider(None, self._model_name)
145
+
146
+ # Calculate token limits from model (SINGLE SOURCE OF TRUTH)
147
+ self.max_input_tokens, self.safe_input_tokens, self.completion_reserve = \
148
+ self._calculate_token_limits(llm, max_input_tokens)
149
+
150
+ # Calculate adaptive message limit based on context window
151
+ # ADAPTIVE SCALING (Nov 4, 2025): Scale message history to model capacity
152
+ # Modern models have 128k-2M token contexts - use them effectively!
153
+ import os
154
+ max_messages_override = os.getenv('TASK_MAX_MESSAGES')
155
+ if max_messages_override:
156
+ # P3: a non-numeric TASK_MAX_MESSAGES used to crash MessageManager.__init__
157
+ # (and thus session creation). Ignore an invalid value with a warning.
158
+ try:
159
+ adaptive_max_messages = int(max_messages_override)
160
+ self.logger.info(f"Using environment override for max messages: {adaptive_max_messages}")
161
+ except ValueError:
162
+ self.logger.warning(
163
+ f"Ignoring non-numeric TASK_MAX_MESSAGES={max_messages_override!r}; "
164
+ "using adaptive default")
165
+ max_messages_override = None
166
+ if not max_messages_override:
167
+ # PHASE 1 FIX: Adaptive scaling based on model context window
168
+ # Average message size: ~160 tokens (system/user/assistant/tool messages)
169
+ # Target: Use 10-20% of context window for message history
170
+ from modules.llm.model_registry import get_model_config
171
+ model_config = get_model_config(self._model_name)
172
+
173
+ if model_config and model_config.context_window:
174
+ context_window = model_config.context_window
175
+
176
+ # Scale based on model capacity
177
+ if context_window >= 1_000_000:
178
+ # Large context models (1M+): Use 10% of window for messages
179
+ # 1M * 0.10 / 160 tokens/msg ≈ 625 messages
180
+ message_budget_pct = 0.10
181
+ self.logger.info(f"Detected large context model ({context_window:,} tokens)")
182
+ elif context_window >= 200_000:
183
+ # Medium-large context models (200K-1M): Use 12%
184
+ message_budget_pct = 0.12
185
+ self.logger.info(f"Detected medium-large context model ({context_window:,} tokens)")
186
+ elif context_window >= 100_000:
187
+ # Medium context models (100K-200K): Use 15%
188
+ message_budget_pct = 0.15
189
+ self.logger.info(f"Detected medium context model ({context_window:,} tokens)")
190
+ else:
191
+ # Small context models (<100K): Use 30%
192
+ message_budget_pct = 0.30
193
+ self.logger.info(f"Detected small context model ({context_window:,} tokens)")
194
+
195
+ # Calculate max messages from budget
196
+ avg_tokens_per_message = 160
197
+ message_token_budget = int(context_window * message_budget_pct)
198
+ adaptive_max_messages = int(message_token_budget / avg_tokens_per_message)
199
+
200
+ # Apply reasonable bounds
201
+ adaptive_max_messages = max(50, min(adaptive_max_messages, 2000))
202
+
203
+ self.logger.info(
204
+ f"Adaptive message history: {adaptive_max_messages} messages "
205
+ f"(~{message_token_budget:,} tokens, {message_budget_pct:.1%} of {context_window:,} window)"
206
+ )
207
+ else:
208
+ # Fallback for unknown models
209
+ adaptive_max_messages = 100
210
+ self.logger.warning(
211
+ f"Could not determine context window for {self._model_name}, "
212
+ f"using fallback: {adaptive_max_messages} messages"
213
+ )
214
+
215
+ # Message History vs H-MEM (complementary systems):
216
+ # - Message History: "Did I already read file X? What was the exact error?"
217
+ # - H-MEM: "What phase am I in? What concepts have I learned?"
218
+
219
+ self.history = MessageHistory(max_messages=adaptive_max_messages)
220
+ self.logger.info(
221
+ f"MessageManager initialized with LIMITED history (max_messages={adaptive_max_messages}). "
222
+ f"Long-term memory: TaskContextManager (hierarchical). "
223
+ f"Token limits: max={self.max_input_tokens}, safe={self.safe_input_tokens}"
224
+ )
225
+ self.task = task
226
+ self.action_descriptions = action_descriptions
227
+ self.IMG_TOKENS = image_tokens
228
+ self.include_attributes = include_attributes
229
+ self.max_error_length = max_error_length
230
+ self.max_actions_per_step = max_actions_per_step
231
+ self.sensitive_data = sensitive_data
232
+ # HISTORY_SECRET_SCRUB (default ON): the pattern backstop for UNregistered
233
+ # secrets lives inside _filter_sensitive_data, but every caller gated it
234
+ # behind `if self.sensitive_data:` — which is empty by default, so the
235
+ # default-ON backstop never ran and sk-/AKIA/Bearer/PEM leaking through a
236
+ # tool result persisted to message_history.json + compaction checkpoints in
237
+ # the clear. Cache the flag once so the write/ephemeral paths can invoke the
238
+ # filter even with no allowlist registered.
239
+ from core.env import bool_env as _bool_env
240
+ self._history_secret_scrub = _bool_env("HISTORY_SECRET_SCRUB", True)
241
+ self.session_id = session_id
242
+ self.add_task_message = add_task_message
243
+
244
+ # Store tool call tracker for ID management
245
+ self.tool_call_tracker = tool_call_tracker
246
+
247
+ # Store task context manager for hierarchical memory (Phase 2)
248
+ self.task_context_manager = task_context_manager
249
+
250
+ # FIX (Jan 2026): Initialize CompactionManager for context-aware compaction
251
+ # This is now the single source of truth for compaction thresholds and logic
252
+ from modules.memory.task.compaction_manager import CompactionManager
253
+ self.compaction_manager = CompactionManager.for_model(self._model_name)
254
+
255
+ # Store MCP server info for dynamic prompt generation
256
+ self.mcp_servers = mcp_servers or {}
257
+
258
+ # Session's loaded tool_ids — lets SystemPrompt gate config-aware sections
259
+ # (e.g. <anysite>, <browser-tools>) on the tools actually loaded this session,
260
+ # not a global flag. None is preserved (NOT collapsed to []): it means the
261
+ # caller never declared a tool set, and SystemPrompt keeps legacy sections on.
262
+ self.tool_ids = list(tool_ids) if tool_ids is not None else None
263
+
264
+ # Store native tools flag for message conversion
265
+ self.use_native_tools = use_native_tools
266
+
267
+ # Initialize ephemeral message buffer (one-shot, not counted toward history)
268
+ self._ephemeral_messages: List[BaseMessage] = []
269
+
270
+ # PR13: skills are injected as a pinned foundation user message (set later),
271
+ # not embedded in the system prompt (keeps the system prompt stable/cacheable).
272
+ self._skill_message: Optional[BaseMessage] = None
273
+ # Token cost of the pinned skill message, kept in the foundation totals so
274
+ # compaction/overflow math sees skills (which left the system prompt in PR13).
275
+ self._skill_message_tokens: int = 0
276
+
277
+ # polyrob Phase C: SOUL/IDENTITY self-context is pinned as a frozen foundation
278
+ # user message (set later), NOT embedded in the system prompt — same rationale
279
+ # as skills (keeps the system prompt stable/cacheable). Empty => inert.
280
+ self._self_context_message: Optional[BaseMessage] = None
281
+ self._self_context_tokens: int = 0
282
+
283
+ # C9: auto-loaded project context (CLAUDE.md/AGENTS.md/.cursorrules) is pinned
284
+ # as a frozen foundation message (set later), CLI-only, default-OFF on server.
285
+ # Placed AFTER self-context and BEFORE the initial task (same pattern as
286
+ # self_context but project-scoped rather than identity-scoped). Empty => inert.
287
+ self._project_context_message: Optional[BaseMessage] = None
288
+ self._project_context_tokens: int = 0
289
+
290
+ # Model-identity SSOT: the model/provider the agent is ACTUALLY running on,
291
+ # pinned as a frozen foundation message so the agent can state it truthfully
292
+ # instead of grepping config/.env (which leaked secrets). Set at session
293
+ # start from the built LLM and refreshed on a live /model swap. Empty => inert.
294
+ self._runtime_identity_message: Optional[BaseMessage] = None
295
+ self._runtime_identity_tokens: int = 0
296
+
297
+ # Initialize checkpoint attributes for message history persistence
298
+ self._history_checkpoint: List[ManagedMessage] = []
299
+ self._checkpoint_token_count: int = 0
300
+
301
+ # SECURITY FIX: Initialize lock to protect deque operations from race conditions
302
+ # This prevents data corruption when list/deque conversions happen during concurrent access
303
+ self._history_lock = threading.RLock() # RLock allows reentrant locking
304
+
305
+ # Initialize system message - CRITICAL FIX: Store separately from history to prevent deque eviction
306
+ # The system message is NOT added to self.history.messages because the deque has a max length
307
+ # and will automatically drop old messages, potentially losing the system message.
308
+ # Instead, we store it separately and prepend it when get_messages() is called.
309
+ if system_message is not None:
310
+ # Use the provided system message directly
311
+ self.logger.debug("Using provided system message from profile")
312
+ self._system_message = system_message
313
+ self.system_prompt = system_message
314
+ else:
315
+ # Create system message from prompt class
316
+ # SystemPrompt accepts action_descriptions, max_actions_per_step, use_native_tools, model_name, provider, and mcp_servers
317
+ # Pass model_name and provider so SystemPrompt can inject model-specific instructions (e.g., for Grok)
318
+ # Pass mcp_servers so SystemPrompt can generate dynamic examples using actual server/tool names
319
+ prompt_instance = self.system_prompt_class(
320
+ self.action_descriptions,
321
+ max_actions_per_step=self.max_actions_per_step,
322
+ use_native_tools=use_native_tools,
323
+ model_name=self._model_name,
324
+ provider=self._provider_name,
325
+ mcp_servers=self.mcp_servers,
326
+ persona_block=self._persona_block,
327
+ tool_ids=self.tool_ids,
328
+ include_vision=include_vision,
329
+ )
330
+
331
+ # SystemPrompt has get_system_message() method that returns a SystemMessage
332
+ system_message = prompt_instance.get_system_message()
333
+ self._system_message = system_message
334
+ self.system_prompt = system_message
335
+
336
+ # Calculate and store system message token count separately
337
+ self._system_message_tokens = self._count_message_tokens(self._system_message)
338
+ self.logger.debug(f"System message stored separately ({self._system_message_tokens} tokens) - will not be evicted by deque")
339
+
340
+ # CRITICAL FIX: Store initial task separately (like SystemMessage) to prevent deque eviction
341
+ # This ensures the original task is ALWAYS visible to the agent, even after 100+ steps
342
+ # Without this, after 20 messages, the agent loses context of what it's supposed to do
343
+ if self.add_task_message:
344
+ # Check if this is a new session (empty history - system message stored separately)
345
+ history_size = len(self.history.messages)
346
+
347
+ if history_size == 0:
348
+ # New session - store initial task SEPARATELY (not in deque)
349
+ self._initial_task_message = HumanMessage(content=self.task)
350
+ self._initial_task_tokens = self._count_message_tokens(self._initial_task_message)
351
+ self.logger.info(f"Initial task message stored separately ({self._initial_task_tokens} tokens) - will not be evicted by deque")
352
+ else:
353
+ # Continuing session - task already established
354
+ # Set to None so it's not included in get_messages()
355
+ self._initial_task_message = None
356
+ self._initial_task_tokens = 0
357
+ self.logger.info(
358
+ f"Skipping initial task message for continuous execution "
359
+ f"(history already has {history_size} messages)"
360
+ )
361
+ else:
362
+ # Task message disabled
363
+ self._initial_task_message = None
364
+ self._initial_task_tokens = 0
365
+
366
+ # Log token usage at initialization (include all separate storage tokens)
367
+ model_name = self.model_name
368
+ total_tokens = self.history.total_tokens + self._system_message_tokens + self._initial_task_tokens
369
+ self.logger.info(
370
+ f"Initialized MessageManager with {total_tokens} tokens for model {model_name} "
371
+ f"(system: {self._system_message_tokens}, task: {self._initial_task_tokens}, history: {self.history.total_tokens})"
372
+ )
373
+ self.logger.debug(f"Token limit: {self.max_input_tokens}, remaining: {self.max_input_tokens - total_tokens}")
374
+
375
+ def _detect_model_name(self) -> str:
376
+ """Detect and cache model name from LLM instance (single source of truth)."""
377
+ # Try to get specific model name from various attributes
378
+ for attr in ['model_name', 'model', '_model_name', 'deployment_name']:
379
+ if hasattr(self.llm, attr):
380
+ model = getattr(self.llm, attr, None)
381
+ if model:
382
+ return model
383
+
384
+ # Fallback to provider or class name
385
+ return self._provider_name if hasattr(self, '_provider_name') and self._provider_name != "unknown" else self.llm.__class__.__name__
386
+
387
+ # REMOVED: _detect_provider_name - now using consolidated detect_llm_provider from utils.py
388
+
389
+ @property
390
+ def model_name(self) -> str:
391
+ """Public accessor for cached model name (single source of truth)."""
392
+ return self._model_name
393
+
394
+ @model_name.setter
395
+ def model_name(self, value: str) -> None:
396
+ """Update model name and re-detect provider (for LLM fallback scenarios)."""
397
+ self._model_name = value
398
+ # Re-detect provider based on new model name
399
+ from agents.task.utils import detect_llm_provider
400
+ self._provider_name = detect_llm_provider(None, value)
401
+ self.logger.info(f"Model name updated to: {value} (provider: {self._provider_name})")
402
+
403
+ @property
404
+ def provider_name(self) -> str:
405
+ """Public accessor for cached provider name (single source of truth)."""
406
+ return self._provider_name
407
+
408
+ @provider_name.setter
409
+ def provider_name(self, value: str) -> None:
410
+ """Set the cached provider name authoritatively (for live model swaps).
411
+
412
+ The ``model_name`` setter re-detects the provider FROM the model name
413
+ (``detect_llm_provider``); this setter lets ``swap_model`` override that
414
+ with the registry-canonical provider label after the model is updated."""
415
+ self._provider_name = value
416
+
417
+ def recalibrate_for_model(self, model_name: str) -> None:
418
+ """Recompute token budgets + compaction manager for a newly-swapped model.
419
+
420
+ Init-time values (``max_input_tokens``/``safe_input_tokens``/
421
+ ``completion_reserve`` and the ``CompactionManager``) are sized for the
422
+ ORIGINAL model. After a live ``swap_model`` they must be re-derived for the
423
+ new model's context window. Reuses ``_calculate_token_limits`` (the init-time
424
+ SSOT formula) so the math isn't duplicated, and rebuilds the compaction
425
+ manager the same way ``__init__`` does. Unlike ``recalibrate_token_counts``
426
+ (shrink-only, for a smaller fallback), this always adopts the new model's
427
+ budget in either direction.
428
+ """
429
+ self._model_name = model_name
430
+ # Re-derive from the new model (None override => auto-calc from the registry,
431
+ # still honouring the TASK_MAX_INPUT_TOKENS env cap inside the helper).
432
+ self.max_input_tokens, self.safe_input_tokens, self.completion_reserve = \
433
+ self._calculate_token_limits(self.llm, None)
434
+ from modules.memory.task.compaction_manager import CompactionManager
435
+ self.compaction_manager = CompactionManager.for_model(self._model_name)
436
+ self.logger.info(
437
+ f"Recalibrated for swapped model {model_name}: "
438
+ f"max_input={self.max_input_tokens}, safe_input={self.safe_input_tokens}, "
439
+ f"completion_reserve={self.completion_reserve}"
440
+ )
441
+
442
+
443
+ @classmethod
444
+ def from_config(
445
+ cls,
446
+ llm: BaseChatModel,
447
+ task: str,
448
+ action_descriptions: str,
449
+ system_prompt_class: Type, # Type of SystemPrompt
450
+ config: MessageManagerConfig,
451
+ logger: Optional[logging.Logger] = None
452
+ ) -> 'MessageManager':
453
+ """Create MessageManager from config object.
454
+
455
+ SINGULAR RESPONSIBILITY: Config object contains ALL settings.
456
+ No fallbacks, no deprecated parameters, no duplication.
457
+
458
+ Args:
459
+ llm: The language model
460
+ task: Task description
461
+ action_descriptions: Available actions
462
+ system_prompt_class: System prompt class
463
+ config: Configuration object with all settings (REQUIRED)
464
+ logger: Optional logger instance for DI
465
+
466
+ Returns:
467
+ Configured MessageManager instance
468
+ """
469
+ if config is None:
470
+ raise ValueError("config is required - no defaults, explicit configuration only")
471
+
472
+ instance = cls(
473
+ llm=llm,
474
+ task=task,
475
+ action_descriptions=action_descriptions,
476
+ system_prompt_class=system_prompt_class,
477
+ max_input_tokens=config.max_input_tokens,
478
+ max_actions_per_step=config.max_actions_per_step,
479
+ **config.to_dict()
480
+ )
481
+
482
+ # Override logger if provided (DI pattern)
483
+ if logger is not None:
484
+ instance.logger = logger
485
+
486
+ return instance
487
+
488
+
489
+ def _add_message_with_tokens(self, message: BaseMessage, position: Optional[int] = None, _internal: bool = False) -> None:
490
+ """Add message with token count metadata and auto-trim if needed.
491
+
492
+ MessageManager keeps ONLY recent conversation for immediate context.
493
+ Long-term memory is handled by TaskContextManager.
494
+
495
+ Args:
496
+ message: The message to add
497
+ position: Optional position to insert at
498
+ _internal: If True, bypass single-writer enforcement for AI/Tool messages (used internally)
499
+ """
500
+
501
+ # filter out sensitive data from the message. Run when an allowlist is
502
+ # registered OR the HISTORY_SECRET_SCRUB backstop is on (default) — the
503
+ # backstop must scrub unregistered secrets even with an empty allowlist.
504
+ if self.sensitive_data or self._history_secret_scrub:
505
+ message = self._filter_sensitive_data(message)
506
+
507
+ token_count = self._count_message_tokens(message)
508
+ from datetime import datetime
509
+ metadata = MessageMetadata(
510
+ input_tokens=token_count,
511
+ timestamp=datetime.now().isoformat(),
512
+ )
513
+
514
+ # SECURITY FIX: Protect all deque operations with lock to prevent race conditions
515
+ # The list/deque conversion and trimming must be atomic
516
+ with self._history_lock:
517
+ # Add message WITHOUT updating total_tokens (will be done via increment)
518
+ managed_msg = ManagedMessage(message=message, metadata=metadata)
519
+ maxlen = self.history.max_messages
520
+ if position is None:
521
+ # A deque(maxlen).append() auto-evicts the leftmost message when full
522
+ # WITHOUT decrementing total_tokens, and the trim loop below can't
523
+ # compensate (len can never EXCEED maxlen). Subtract the soon-to-be
524
+ # evicted message's tokens first so total_tokens doesn't drift upward.
525
+ msgs = self.history.messages
526
+ if maxlen and msgs and len(msgs) >= maxlen:
527
+ ev = msgs[0]
528
+ if getattr(ev, 'metadata', None):
529
+ self.history.total_tokens = max(
530
+ 0, self.history.total_tokens - ev.metadata.input_tokens)
531
+ msgs.append(managed_msg)
532
+ else:
533
+ # FIXED: deque with maxlen doesn't support insert() when full
534
+ # Convert to list, insert, then back to deque
535
+ messages_list = list(self.history.messages)
536
+ messages_list.insert(position, managed_msg)
537
+ # deque(maxlen) construction silently drops the leftmost overflow;
538
+ # decrement those tokens too so total_tokens stays consistent.
539
+ if maxlen and len(messages_list) > maxlen:
540
+ for ev in messages_list[: len(messages_list) - maxlen]:
541
+ if getattr(ev, 'metadata', None):
542
+ self.history.total_tokens = max(
543
+ 0, self.history.total_tokens - ev.metadata.input_tokens)
544
+ self.history.messages = deque(messages_list, maxlen=maxlen)
545
+
546
+ # Auto-trim if exceeding limit
547
+ while len(self.history.messages) > self.history.max_messages:
548
+ # Keep system message (index 0) and task message (index 1, if present)
549
+ # Start trimming from index 2 onwards to preserve context structure
550
+ # System: index 0 (always kept)
551
+ # Task: index 1 (kept if it's the initial HumanMessage with task)
552
+ # Hierarchical context: injected dynamically, not in history
553
+ # Recent messages: index 2+ (these get trimmed)
554
+
555
+ # Find the oldest trimmable message (skip system and initial task)
556
+ remove_index = 2 if len(self.history.messages) > 2 else 1
557
+
558
+ # Safety check: don't trim if we only have system + task messages
559
+ if remove_index >= len(self.history.messages):
560
+ self.logger.warning("Cannot trim - would remove essential messages")
561
+ break
562
+
563
+ removed_msg = self.history.messages.pop(remove_index)
564
+
565
+ # Decrement token count for removed message
566
+ if hasattr(removed_msg, 'metadata') and removed_msg.metadata:
567
+ removed_tokens = removed_msg.metadata.input_tokens
568
+ self.history.total_tokens = max(0, self.history.total_tokens - removed_tokens)
569
+
570
+ self.logger.debug(f"Trimmed old message from recent history: {type(removed_msg.message).__name__}")
571
+
572
+ # Use incremental token counting for new message
573
+ self._increment_token_count(token_count)
574
+
575
+
576
+ def invalidate_hmem_cache(self) -> None:
577
+ """Invalidate the H-MEM context cache.
578
+
579
+ FIX (Dec 12, 2025): Now calls TaskContextManager.invalidate_context_cache()
580
+ to force a fresh hierarchical search on next context injection.
581
+
582
+ Note: The cache now auto-invalidates per step, so this is only needed
583
+ when you want immediate cache invalidation (e.g., after major findings).
584
+ """
585
+ if self.task_context_manager and self.session_id:
586
+ try:
587
+ self.task_context_manager.invalidate_context_cache(self.session_id)
588
+ except Exception as e:
589
+ self.logger.debug(f"H-MEM cache invalidation failed: {e}")
590
+
591
+
592
+ def add_message(self, message: BaseMessage, position: Optional[int] = None, _internal: bool = False) -> None:
593
+ """Add a message to history - public wrapper for _add_message_with_tokens
594
+
595
+ IMPORTANT: This method enforces single-writer constraints for AI and Tool messages.
596
+ Only MessageManager methods (add_model_output, add_tool_response) should add these types.
597
+
598
+ Args:
599
+ message: The message to add
600
+ position: Optional position to insert at
601
+ _internal: Private flag used by internal methods to bypass enforcement
602
+ """
603
+ # ENFORCEMENT: Single Writer for AI/Tool Messages
604
+ # Prevent external callers from directly adding AI or Tool messages
605
+ # AIMessage, ToolMessage imported at module level from modules.llm.messages
606
+ if not _internal:
607
+ if isinstance(message, (AIMessage, ToolMessage)):
608
+ from core.exceptions import BotError
609
+
610
+ error_msg = (
611
+ f"Direct addition of {type(message).__name__} not allowed. "
612
+ f"Use add_model_output() for AI messages or add_tool_response() for Tool messages."
613
+ )
614
+ self.logger.error(f"[SINGLE_WRITER] {error_msg}")
615
+
616
+ class MessageFlowError(BotError):
617
+ """Error in message flow control"""
618
+ pass
619
+
620
+ raise MessageFlowError(error_msg)
621
+
622
+ self._add_message_with_tokens(message, position)
623
+
624
+ def add_human_message(self, content: str, position: Optional[int] = None) -> None:
625
+ """Create a HumanMessage with the given content and add it to history"""
626
+ message = HumanMessage(content=content)
627
+ self._add_message_with_tokens(message, position)
628
+
629
+ def append_user_turn(self, text: str) -> None:
630
+ """Append a plain user message at the END of history (a conversational turn).
631
+
632
+ Contrast with inject_user_guidance, which inserts at position 1 with
633
+ task-directive framing for the HITL resume path. This is an ordinary
634
+ trailing turn, routed through _add_message_with_tokens so it participates
635
+ in token accounting and overflow trimming like any other user message.
636
+ """
637
+ self._add_message_with_tokens(HumanMessage(content=text))
638
+
639
+ def set_skill_message(self, content: Optional[str]) -> None:
640
+ """PR13: pin skills as a foundation user message instead of the system prompt.
641
+
642
+ Stored as a SKILL-origin control message and injected in the foundation
643
+ (after the initial task) by get_messages(). Keeps the system prompt stable
644
+ for prompt caching and keeps injected skills distinguishable from user input.
645
+ Pass falsy content to clear.
646
+ """
647
+ if content and content.strip():
648
+ self._skill_message = make_control_message(content, MessageOrigin.SKILL)
649
+ self._skill_message_tokens = self._count_message_tokens(self._skill_message)
650
+ else:
651
+ self._skill_message = None
652
+ self._skill_message_tokens = 0
653
+
654
+ def set_self_context_message(self, content: Optional[str]) -> None:
655
+ """polyrob Phase C: pin the SOUL/IDENTITY self-context as a frozen foundation
656
+ user message instead of the system prompt.
657
+
658
+ Stored as a SELF_CONTEXT-origin control message and injected in the
659
+ foundation (before skills) by get_messages()/get_messages_for_llm(). Keeps the
660
+ system prompt stable for prompt caching and reads as an authoritative
661
+ self-definition block distinct from user input. Set once at session start
662
+ (frozen); pass falsy content to clear (the inert default).
663
+ """
664
+ if content and content.strip():
665
+ self._self_context_message = make_control_message(content, MessageOrigin.SELF_CONTEXT)
666
+ self._self_context_tokens = self._count_message_tokens(self._self_context_message)
667
+ else:
668
+ self._self_context_message = None
669
+ self._self_context_tokens = 0
670
+
671
+ def set_project_context_message(self, content: Optional[str]) -> None:
672
+ """C9: pin the auto-loaded project context (CLAUDE.md/AGENTS.md/.cursorrules) as a
673
+ frozen foundation message (CLI-only, default-OFF on server).
674
+
675
+ Stored as a PROJECT_CONTEXT-origin control message and injected in the
676
+ foundation AFTER self-context and BEFORE the initial task by
677
+ get_messages()/get_messages_for_llm(). Keeps the system prompt stable for
678
+ prompt caching. Set once at session start (frozen); pass falsy content to
679
+ clear (the inert default on server / when no context file found).
680
+ """
681
+ if content and content.strip():
682
+ self._project_context_message = make_control_message(content, MessageOrigin.PROJECT_CONTEXT)
683
+ self._project_context_tokens = self._count_message_tokens(self._project_context_message)
684
+ else:
685
+ self._project_context_message = None
686
+ self._project_context_tokens = 0
687
+
688
+ def set_runtime_identity(self, model: Optional[str], provider: Optional[str]) -> None:
689
+ """Pin the model/provider the agent is ACTUALLY running on as a foundation message.
690
+
691
+ So the agent can answer "what model are you" truthfully instead of reading
692
+ config/.env (which leaked secrets). Set at session start from the built LLM
693
+ and refreshed on a live /model swap. Falsy model => clear (inert). Kept in the
694
+ foundation (not the system prompt) so the prompt stays cacheable; on a swap the
695
+ refreshed line is one short block, so the cache cost is negligible.
696
+ """
697
+ if model and str(model).strip():
698
+ prov = str(provider).strip() if provider else "unknown"
699
+ content = (
700
+ f"You are currently running on model \"{model}\" via provider \"{prov}\". "
701
+ "State this if asked which model/provider you are — do NOT read env/config "
702
+ "files to answer; this line is authoritative."
703
+ )
704
+ self._runtime_identity_message = make_control_message(content, MessageOrigin.RUNTIME_IDENTITY)
705
+ self._runtime_identity_tokens = self._count_message_tokens(self._runtime_identity_message)
706
+ else:
707
+ self._runtime_identity_message = None
708
+ self._runtime_identity_tokens = 0
709
+
710
+ def add_file_paths(self, file_paths: list[str]) -> None:
711
+ """Add file paths message with properly escaped paths.
712
+
713
+ Args:
714
+ file_paths: List of file paths to add
715
+ """
716
+ # Sanitize file paths to prevent injection
717
+ def sanitize_paths(paths: list[str]) -> list[str]:
718
+ if not paths:
719
+ return []
720
+ return [p.replace('"""', '\\"\\"\\"').replace('\\', '\\\\') for p in paths]
721
+
722
+ safe_paths = sanitize_paths(file_paths)
723
+ content = f'Here are file paths you can use: {safe_paths}'
724
+ msg = HumanMessage(content=content)
725
+ self._add_message_with_tokens(msg)
726
+
727
+ def add_new_task(self, new_task: str) -> None:
728
+ """Add a new task message with properly escaped task text.
729
+
730
+ Args:
731
+ new_task: The new task description
732
+ """
733
+ # Reuse the same escaping function from task_instructions
734
+ def escape_task_text(text: str) -> str:
735
+ if not text:
736
+ return ""
737
+ # Replace potential control sequences
738
+ escaped = (text.replace('"""', '\"\"\"') # Escape triple quotes
739
+ .replace('\\', '\\\\') # Escape backslashes
740
+ .replace('{', '{{') # Escape curly braces for f-strings
741
+ .replace('}', '}}'))
742
+ return escaped
743
+
744
+ # Apply escaping directly instead of extracting from another message
745
+ safe_task = escape_task_text(new_task)
746
+ content = f'Your new ultimate task is: """{safe_task}""". Take the previous context into account and finish your new ultimate task. '
747
+ msg = HumanMessage(content=content)
748
+ self._add_message_with_tokens(msg)
749
+
750
+ def add_plan(self, plan: Optional[str], position: Optional[int] = None) -> None:
751
+ """Add a plan message as AIMessage.
752
+
753
+ This is an internal method that bypasses enforcement.
754
+ """
755
+ if plan:
756
+ msg = AIMessage(content=plan)
757
+ self._add_message_with_tokens(msg, position, _internal=True)
758
+