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,1384 @@
1
+ """
2
+ Centralized constants for the Task agent system.
3
+ This provides a single source of truth for history management, trimming, and memory limits.
4
+ """
5
+
6
+ import os
7
+
8
+ from core.env import bool_env as _core_bool_env
9
+
10
+ # Token counting defaults - Use modules.llm.token_counter for actual counting
11
+ # IMG_TOKENS kept for backward compatibility but should use token_counter._count_multimodal_tokens
12
+ IMG_TOKENS = 800 # Default estimate for image tokens (actual counting done by modules.llm)
13
+ DEFAULT_IMG_TOKENS = IMG_TOKENS # Alias for compatibility
14
+ ESTIMATED_CHARS_PER_TOKEN = 4 # Updated to match model_registry defaults
15
+
16
+ # Default user ID for anonymous sessions. The canonical token lives in
17
+ # core.identity (the tenant SSOT); aliased here so the value is defined once.
18
+ from core.identity import ANON_USER_ID as DEFAULT_USER_ID # noqa: E402
19
+
20
+ # Token recalibration
21
+ CALIBRATION_CHECK_INTERVAL = 5 # Recalibrate token counts every N steps
22
+
23
+ # Error message display
24
+ ERROR_PREVIEW_COUNT = 3 # Number of errors to show in previews
25
+
26
+ # Loop detection configuration
27
+ class LoopDetectionConfig:
28
+ """Centralized loop detection configuration - Optimized for reliability"""
29
+ # Development mode: tighter thresholds for faster loop detection
30
+ IS_DEVELOPMENT = os.getenv('ENVIRONMENT', 'development') == 'development'
31
+
32
+ # Use tighter thresholds in development for faster debugging
33
+ if IS_DEVELOPMENT:
34
+ MAX_REPETITIONS = int(os.getenv('MAX_REPETITIONS', '2')) # FIX 4: Aggressive loop detection
35
+ UNCHANGED_STATE_THRESHOLD = int(os.getenv('UNCHANGED_STATE_THRESHOLD', '3')) # FIX 4: Aggressive
36
+ STATE_CHANGE_THRESHOLD = 3 # FIX 4: Detect loops after 2-3 repetitions
37
+ MAX_ALLOWED_REPETITIONS = 2 # FIX 4: Catch loops after just 2 repetitions
38
+ else:
39
+ MAX_REPETITIONS = int(os.getenv('MAX_REPETITIONS', '3')) # FIX 4: Stricter for production
40
+ UNCHANGED_STATE_THRESHOLD = int(os.getenv('UNCHANGED_STATE_THRESHOLD', '4')) # FIX 4: Stricter
41
+ STATE_CHANGE_THRESHOLD = 4 # FIX 4: Lower threshold
42
+ MAX_ALLOWED_REPETITIONS = 3 # FIX 4: Catch loops faster in production
43
+
44
+ ACTION_SIMILARITY_THRESHOLD = 0.95 # Strict matching to avoid false positives
45
+ MEMORY_WINDOW_SIZE = 25 # Larger window for better context
46
+ DETECTION_WINDOW = 15 # Longer detection window
47
+
48
+ # Aliases for compatibility
49
+ LOOP_WARNING_THRESHOLD = MAX_ALLOWED_REPETITIONS # Threshold for warning about loops
50
+ ALTERNATING_PATTERN_MIN = 4 # Minimum pattern length to detect alternating actions
51
+
52
+ # Memory management configuration
53
+ class MemoryConfig:
54
+ """Memory optimization settings for AutoV2 - Optimized for performance"""
55
+ MAX_SCREENSHOTS_IN_GIF = 10 # Reduced to prevent OOM
56
+ MAX_HISTORY_SIZE = int(os.getenv('MAX_MEMORY_CACHE_SIZE', '30')) # Configurable via env
57
+ SCREENSHOT_JPEG_QUALITY = int(os.getenv('SCREENSHOT_JPEG_QUALITY', '70')) # Configurable
58
+ ENABLE_GIF_CREATION = _core_bool_env('ENABLE_GIF_CREATION', False) # Disabled by default
59
+ CLEAR_SCREENSHOTS_AFTER_GIF = True # Remove base64 data after GIF creation
60
+ MAX_SCREENSHOT_SIZE_MB = 3 # Reduced max size per screenshot
61
+ # UNIFIED: Single source from environment, default 50
62
+ CLEANUP_INTERVAL = int(os.getenv('MEMORY_CLEANUP_INTERVAL', '50')) # Clean every N operations
63
+
64
+ # History and Message Management
65
+ MAX_HISTORY_SIZE = 30 # Maximum number of messages to keep in history
66
+ PROTECT_RECENT_MESSAGES = 10 # Number of recent messages to protect from trimming
67
+ PROTECT_INITIAL_MESSAGES = 3 # Number of initial messages (system prompts) to protect
68
+
69
+ # Token and Context Management - REMOVED: Use modules.llm.model_registry instead
70
+ # TOKEN_BUFFER_SIZE, MIN_CONTEXT_TOKENS, MAX_CONTEXT_PERCENTAGE moved to model_registry
71
+ # For context management, use: get_model_config(model_name).safe_input_tokens
72
+
73
+ # Tool Call Management
74
+ # Dynamic lookahead window that scales with context size
75
+ def get_tool_lookahead_window(total_messages: int = 0) -> int:
76
+ """Get dynamic lookahead window based on context size.
77
+
78
+ For large contexts (1M+ tokens), tool responses may be far from their calls.
79
+ This scales the window appropriately to handle million-token contexts.
80
+
81
+ Args:
82
+ total_messages: Total number of messages in context
83
+
84
+ Returns:
85
+ Lookahead window size
86
+ """
87
+ if total_messages > 10000: # Extreme scale (2M+ tokens)
88
+ return 1000
89
+ elif total_messages > 5000: # Million-token scale contexts
90
+ return 600
91
+ elif total_messages > 2000: # Very large context
92
+ return 300
93
+ elif total_messages > 1000: # Large context
94
+ return 150
95
+ elif total_messages > 500: # Medium-large context
96
+ return 75
97
+ elif total_messages > 200: # Medium context
98
+ return 40
99
+ else:
100
+ return 20 # Default for small contexts
101
+
102
+ TOOL_LOOKAHEAD_WINDOW = 15 # Default, use get_tool_lookahead_window() for dynamic sizing
103
+ PLACEHOLDER_CONTENT = "[Awaiting tool response]" # Content for placeholder ToolMessages
104
+ PLACEHOLDER_SOURCE = "tool_placeholder" # Metadata source for placeholders
105
+
106
+ # Memory and Performance
107
+ # REMOVED DUPLICATE: Use MemoryConfig.CLEANUP_INTERVAL instead
108
+ MEMORY_CHECK_INTERVAL = 10 # Check memory usage every N steps
109
+ MEMORY_MESSAGE_THRESHOLD_MB = 50 # Trim if message history uses > 50MB
110
+ MEMORY_HIGH_WATERMARK_MB = 1000 # Warn if total memory over 1GB
111
+ MAX_RESULT_PREVIEW_LENGTH = 500 # Maximum length for action result previews
112
+ LARGE_CONTENT_THRESHOLD = 50000 # Threshold for considering content "large"
113
+
114
+ # Retry and Timeout Settings
115
+ MAX_LLM_RETRIES = 3 # Maximum retries for LLM calls
116
+ MAX_LLM_CREATION_RETRIES = 2 # Retries when creating LLM instances
117
+
118
+
119
+ # ========== AUX-MODEL ROUTER (A5/A1 + generalized) ==========
120
+ # Provider -> cheap auxiliary model, used for compaction (`llm_compact_history`) and the
121
+ # judge aux task (output validation) via resolve_aux_model() below.
122
+ # NOTE: this map has no "openrouter" key (intentional) — an openrouter session's aux
123
+ # tasks resolve to None and fall back to the main model.
124
+ # (Reference parity: auxiliary.compression.model — a small/fast model for compaction.)
125
+ COMPACTION_AUX_MODEL_MAP = {
126
+ "anthropic": "claude-haiku-4-5",
127
+ "openai": "gpt-5-mini",
128
+ "gemini": "gemini-flash",
129
+ "google": "gemini-flash",
130
+ }
131
+
132
+ # Alias: the generalized router reuses the same cheap-model map for all aux tasks.
133
+ AUX_MODEL_MAP = COMPACTION_AUX_MODEL_MAP
134
+
135
+ # Per-task explicit override env var names. Compaction keeps its legacy knob.
136
+ # UP-10 2.1: only `compaction` (live) and `judge` (wired into output validation,
137
+ # default-off/fail-open) remain. `planner`/`vision` had zero call sites and were
138
+ # removed as dead config surface.
139
+ _AUX_TASK_ENV = {
140
+ "compaction": "COMPACTION_MODEL",
141
+ "judge": "AUX_MODEL_JUDGE",
142
+ }
143
+
144
+
145
+ def resolve_aux_model(task, provider=None):
146
+ """Which model should aux `task` use? (None => use the main model.)
147
+
148
+ Precedence: 1) per-task explicit env (e.g. AUX_MODEL_JUDGE / COMPACTION_MODEL);
149
+ 2) provider cheap-map default when AUX_AUTO=true (or, for compaction only, the
150
+ legacy COMPACTION_AUTO_AUX=true); 3) None.
151
+ """
152
+ explicit = os.getenv(_AUX_TASK_ENV.get(task, ""), "")
153
+ if explicit:
154
+ return explicit
155
+ auto = _core_bool_env("AUX_AUTO", False)
156
+ if task == "compaction":
157
+ auto = auto or _core_bool_env("COMPACTION_AUTO_AUX", False)
158
+ if auto and provider:
159
+ return AUX_MODEL_MAP.get(provider.lower())
160
+ return None
161
+
162
+
163
+ _AUX_SLOTS = ("compaction", "judge", "reflection")
164
+
165
+
166
+ def _parse_provider_model(token, default_provider=None):
167
+ """Parse one fallback-chain token ('provider/model' or bare 'model')."""
168
+ token = (token or "").strip()
169
+ if not token:
170
+ return None
171
+ if "/" in token:
172
+ head, tail = token.split("/", 1)
173
+ tail = tail.strip()
174
+ head = head.strip()
175
+ return {"model": tail, "provider": head or default_provider} if tail else None
176
+ return {"model": token, "provider": default_provider}
177
+
178
+
179
+ def resolve_aux_chain(task, provider=None):
180
+ """Ordered aux-model candidates for `task` (primary first, then per-task fallbacks).
181
+
182
+ B5 (Hermes `auxiliary.<task>.fallback_chain` parity): each of the 3 real aux
183
+ call sites (compaction/judge/reflection) can be pointed at its own model+
184
+ provider plus an ordered fallback list, instead of a single model string.
185
+ Empty list => caller uses the main model (unchanged runtime contract).
186
+
187
+ Env per slot (<SLOT> = task.upper()):
188
+ AUX_MODEL_<SLOT> primary model (legacy: COMPACTION_MODEL / AUX_MODEL_JUDGE)
189
+ AUX_PROVIDER_<SLOT> primary provider (legacy: COMPACTION_PROVIDER / AUX_PROVIDER)
190
+ AUX_FALLBACK_<SLOT> comma list of provider/model (or bare model) candidates
191
+
192
+ `reflection` inherits compaction's config (model AND provider, as a pair) when
193
+ its own env is unset (back-compat: reflection historically reused
194
+ _provision_compaction_llm wholesale). If reflection sets its OWN model, none of
195
+ compaction's config is consulted — including its provider, so a compaction
196
+ provider never silently leaks onto a reflection-specific model.
197
+
198
+ Provider precedence for reflection specifically:
199
+ 1) AUX_PROVIDER_REFLECTION (explicit reflection override)
200
+ 2) compaction's provider (AUX_PROVIDER_COMPACTION or legacy COMPACTION_PROVIDER)
201
+ — ONLY when reflection's model was itself inherited from compaction
202
+ 3) AUX_PROVIDER (generic aux fallback)
203
+ """
204
+ if task not in _AUX_SLOTS:
205
+ # Fixed slot set by design (the 3 real aux call sites) — an unknown task
206
+ # never gets a chain, even under AUX_AUTO (dead-config trap guard, UP-10).
207
+ return []
208
+ slot = task.upper()
209
+ inherited_from_compaction = False
210
+ if task == "reflection":
211
+ # Reflection is NOT in _AUX_TASK_ENV, so resolve_aux_model("reflection", ...)'s
212
+ # only possible effect is the global-AUX_AUTO cheap-map early-exit — which
213
+ # would pre-empt the compaction inheritance below and drop the paired
214
+ # COMPACTION_PROVIDER (pre-B5, reflection reused _provision_compaction_llm
215
+ # wholesale, so AUX_AUTO + COMPACTION_PROVIDER resolved as a pair). Go
216
+ # straight from own-env to the compaction branch, which itself honors
217
+ # AUX_AUTO/COMPACTION_AUTO_AUX via resolve_aux_model("compaction", ...).
218
+ primary_model = os.getenv("AUX_MODEL_REFLECTION", "")
219
+ if not primary_model:
220
+ primary_model = os.getenv("AUX_MODEL_COMPACTION", "") or resolve_aux_model("compaction", provider)
221
+ if primary_model:
222
+ inherited_from_compaction = True
223
+ else:
224
+ primary_model = os.getenv(f"AUX_MODEL_{slot}", "") or resolve_aux_model(task, provider)
225
+ if not primary_model:
226
+ return []
227
+
228
+ primary_provider = os.getenv(f"AUX_PROVIDER_{slot}") or None
229
+ if task == "compaction":
230
+ primary_provider = primary_provider or os.getenv("COMPACTION_PROVIDER") or None
231
+ elif task == "reflection":
232
+ if not primary_provider and inherited_from_compaction:
233
+ primary_provider = os.getenv("AUX_PROVIDER_COMPACTION") or os.getenv("COMPACTION_PROVIDER") or None
234
+ primary_provider = primary_provider or os.getenv("AUX_PROVIDER") or None
235
+ else:
236
+ primary_provider = primary_provider or os.getenv("AUX_PROVIDER") or None
237
+
238
+ chain = [{"model": primary_model, "provider": primary_provider}]
239
+
240
+ raw_fb = os.getenv(f"AUX_FALLBACK_{slot}", "")
241
+ if not raw_fb and task == "reflection" and inherited_from_compaction:
242
+ raw_fb = os.getenv("AUX_FALLBACK_COMPACTION", "")
243
+ for tok in raw_fb.split(","):
244
+ cand = _parse_provider_model(tok)
245
+ if cand:
246
+ chain.append(cand)
247
+ return chain
248
+
249
+
250
+ def reflection_llm_enabled_default() -> bool:
251
+ """Whether H-MEM phase reflection synthesizes summaries via the aux LLM (UP-09).
252
+
253
+ Default **ON** (mirrors the MEMORY_BACKEND default-on precedent). Disable with
254
+ REFLECTION_LLM_ENABLED in {none, off, false, 0, no, ''}.
255
+
256
+ SINGLE SOURCE OF TRUTH: both the model-provisioning site (construction.py) and the
257
+ runtime guard (TaskContextManager.__init__) MUST read this helper. The historical bug
258
+ (Fusion-validated 2026-06-16) was that the runtime guard read
259
+ `BotConfig.get("REFLECTION_LLM_ENABLED", False)` — and `BotConfig.get` is
260
+ `getattr(self, key, default)` with no such attribute, so it was ALWAYS False — while
261
+ construction.py read os.getenv. The two sources disagreed and reflection never fired.
262
+ """
263
+ val = os.getenv("REFLECTION_LLM_ENABLED", "true").strip().lower()
264
+ return val not in ("none", "off", "false", "0", "no", "")
265
+
266
+
267
+ # --- Autonomy & continuous-learning loops (Reference-parity, 2026-06-16) ---------
268
+ #
269
+ # Shared flag helpers for the four loops POLYROB lacked: self-wake re-entry,
270
+ # writable-skills + background-review, cron run-loop+delivery, durable goal board,
271
+ # and the curator. SINGLE SOURCE OF TRUTH for the falsey-set semantics, mirroring
272
+ # reflection_llm_enabled_default() above. All loops default-OFF + fail-open except
273
+ # MEMORY_SEARCH_TOOL (read-only, tenant-scoped) and CRON_RUN_LOOP (fixes a live bug
274
+ # where cron built a session but never ran the agent loop).
275
+
276
+ _FALSEY = ("none", "off", "false", "0", "no", "")
277
+
278
+
279
+ def _bool_env(name: str, default: bool) -> bool:
280
+ """Read a boolean env var with POLYROB's falsey-set semantics.
281
+
282
+ Delegates to the repo-wide SSOT (``core.env.bool_env``) so this module shares
283
+ one parser with everything else instead of reimplementing it (the reflection-gate
284
+ bug was a parser/source mismatch). Kept as a thin wrapper so in-module callers
285
+ (and this module's public ``_bool_env`` symbol) are unaffected.
286
+ """
287
+ return _core_bool_env(name, default)
288
+
289
+
290
+ def _int_env(name: str, default: int) -> int:
291
+ try:
292
+ return int(os.getenv(name, str(default)))
293
+ except (TypeError, ValueError):
294
+ return default
295
+
296
+
297
+ # --- Local (terminal-native, single-user) profile -------------------------
298
+ # When POLYROB_LOCAL is truthy, the *safe* autonomy/learning flags default ON as a
299
+ # group, so a terminal user gets the W1-W7 loops without setting ~6 env vars.
300
+ # Multi-tenant server entry (main.py / uvicorn) never sets POLYROB_LOCAL, so its
301
+ # defaults are unchanged. An explicit per-flag value (e.g. GOALS_ENABLED=off)
302
+ # still wins — only the *default* moves.
303
+ #
304
+ # Excludes anything with a multi-tenant blast radius even on one machine:
305
+ # CODE_EXEC_ENABLED (not a sandbox) and the sub-agent concurrency caps.
306
+ _SAFE_LOCAL_FLAGS = frozenset({
307
+ "SELF_WAKE_ENABLED",
308
+ "SKILLS_WRITABLE",
309
+ "SELF_CONTEXT_WRITABLE",
310
+ # Bounded owner-facts doc (USER.md-equivalent): safe on a single-user CLI
311
+ # (own tenant, quarantine-then-promote); multi-tenant server stays OFF.
312
+ "OWNER_DOC_WRITABLE",
313
+ "BACKGROUND_REVIEW_ENABLED",
314
+ "GOALS_ENABLED",
315
+ "CURATOR_ENABLED",
316
+ "INSIGHTS_TOOL",
317
+ "CODING_TOOLS_ENABLED",
318
+ # P0-D: structured git over the confined workspace. Safe on a single-user CLI
319
+ # (own repo); multi-tenant server stays OFF by default. git_push is separately
320
+ # approval-gated + leaf-blocked (Task 9).
321
+ "GIT_TOOLS_ENABLED",
322
+ # NOTE (FL-D9): SKILL_CATALOG_INCLUDE_ALL was here, but its resolver
323
+ # (skill_catalog_include_all(), below) hardcodes `_bool_env("SKILL_CATALOG_INCLUDE_ALL",
324
+ # True)` directly and never consults `_safe_autonomy_default`/this set — the entry
325
+ # was dead (default is already ON everywhere). Removed 2026-07 (behavior-neutral).
326
+ # KB (knowledge-base) feature: safe on a single-user CLI (read/write own KB),
327
+ # multi-tenant default stays OFF until per-tenant isolation is verified.
328
+ "KB_ENABLED",
329
+ # C1: context-reference expansion (@file/@folder/@diff/@url). Safe on a single-user
330
+ # CLI where the workspace is trusted; multi-tenant server stays OFF by default so
331
+ # accidental file-inclusion from a shared workspace is not the default.
332
+ "CONTEXT_REFERENCES_ENABLED",
333
+ # C9: auto-load CLAUDE.md/AGENTS.md/.cursorrules as a PROJECT_CONTEXT foundation
334
+ # message. Safe on a single-user CLI (reads from cwd/git-root); server stays OFF
335
+ # (multi-tenant workspaces may not have a project context file).
336
+ "PROJECT_CONTEXT_AUTOLOAD",
337
+ # T13: KB auto-prefetch — inject KB recall alongside memory recall at step start.
338
+ # Safe on a single-user CLI (reads own KB); multi-tenant server stays OFF by default
339
+ # because KB_ENABLED itself is also local-only by default.
340
+ "KB_AUTO_PREFETCH",
341
+ # Task 2/3/4: episodic activity ledger — durable per-run provenance rows +
342
+ # digest injection + continuity bridge. Safe on a single-user CLI (own
343
+ # tenant); multi-tenant server stays OFF by default.
344
+ "EPISODIC_MEMORY_ENABLED",
345
+ "EPISODIC_DIGEST_INJECT",
346
+ "CONTINUITY_BRIDGE_ENABLED",
347
+ # AU-F1.1: the goal-board dispatcher ticks under POLYROB_LOCAL (GOALS_ENABLED is
348
+ # in this set), but without the planner nothing ever proposes an objective's next
349
+ # goal -- the board sits idle even though the ticker runs (the "idle since Jul 1"
350
+ # incident). Safe on a single-user CLI (own tenant's own objectives); multi-tenant
351
+ # server stays OFF by default. Existing gates (GOALS_ENABLED, an active objective,
352
+ # a thin ready-queue, the planner cooldown) still apply regardless of this default.
353
+ "GOAL_PLANNER_ENABLED",
354
+ # §7.1: self-evolution transparency — proactively notify the owner of a pending
355
+ # identity/skill proposal + expose the approve/reject/list surface. Safe on a
356
+ # single-user CLI (own tenant); multi-tenant server stays OFF by default (an
357
+ # unsolicited push to a shared owner channel is opt-in there).
358
+ "SELF_EVOLUTION_TRANSPARENCY",
359
+ # Task 5: gated `message` action (owner/allowlist -> MessageRouter send). Safe
360
+ # on a single-user CLI (own tenant, own owner-bound targets); multi-tenant
361
+ # server stays OFF by default (arbitrary outbound send is opt-in there).
362
+ "MESSAGE_TOOL_ENABLED",
363
+ })
364
+
365
+
366
+ def local_mode_enabled() -> bool:
367
+ """True when running as the single-user terminal-native agent.
368
+
369
+ Canonical flag: ``POLYROB_LOCAL``. ``ROB_LOCAL`` is accepted as a deprecated
370
+ back-compat alias (older docs/scripts referenced it) — either being truthy
371
+ enables local mode, so a doc that still says ``ROB_LOCAL`` isn't a silent no-op.
372
+ """
373
+ return _bool_env("POLYROB_LOCAL", False) or _bool_env("ROB_LOCAL", False)
374
+
375
+
376
+ def message_tool_enabled() -> bool:
377
+ """Whether the gated `message` action (owner/allowlist -> MessageRouter) is
378
+ registered. Default OFF; ON under POLYROB_LOCAL (single-user CLI) via the
379
+ _SAFE_LOCAL_FLAGS group. An explicit MESSAGE_TOOL_ENABLED always wins.
380
+ """
381
+ return _bool_env("MESSAGE_TOOL_ENABLED", _safe_autonomy_default("MESSAGE_TOOL_ENABLED"))
382
+
383
+
384
+ def task_personality_block_enabled() -> bool:
385
+ """Whether the persona/<identity> block is injected into the task agent.
386
+
387
+ S1 (chat consolidation): injects the chat agent's character/personality into
388
+ the unified Task agent's <identity> block so chat-mode carries persona without
389
+ a separate ChatAgent. The persona TEXT is rendered from a Character via the
390
+ pure agents/personality/persona_render.render_persona_block; the task-agent
391
+ core only ever sees a str (it never imports the chat stack). This access-time
392
+ gate is the live seam — the module-level TASK_PERSONALITY_BLOCK constant that
393
+ used to exist was a dead decoy.
394
+
395
+ Access-time (not import-time) so it sees POLYROB_LOCAL set via
396
+ os.environ.setdefault in bootstrap. Defaults ON under local mode (single-user
397
+ CLI wants its persona; persona_block resolves to "" when OFF => byte-identical
398
+ system prompt), OFF on the multi-tenant server. An explicit
399
+ TASK_PERSONALITY_BLOCK env always wins.
400
+ """
401
+ raw = os.getenv("TASK_PERSONALITY_BLOCK")
402
+ if raw is None or raw.strip() == "":
403
+ return local_mode_enabled()
404
+ return _bool_env("TASK_PERSONALITY_BLOCK", False)
405
+
406
+
407
+ def memory_prefetch_cadence(autonomous: bool = False) -> int:
408
+ """Steps between automatic memory re-prefetch (Phase 1.3).
409
+
410
+ 0 = prefetch on the FIRST step only (legacy, prod-safe). N>0 = ALSO prefetch
411
+ every N steps so a long task keeps re-recalling phase-relevant memory instead of
412
+ recalling once at step 1 and never again.
413
+
414
+ Resolved at ACCESS time (not import) so it sees POLYROB_LOCAL even though that is
415
+ set via os.environ.setdefault in bootstrap, which may run after this module is
416
+ first imported. Defaults to 3 under local mode AND for an autonomous session
417
+ (SA-06 — a server goal/cron run otherwise recalled once at step 1, where the
418
+ brain enrichment is dead, and never again); 0 for server chat. An explicit
419
+ ``MEMORY_PREFETCH_CADENCE`` (incl. ``0``) always wins.
420
+ """
421
+ return _int_env("MEMORY_PREFETCH_CADENCE",
422
+ 3 if (local_mode_enabled() or autonomous) else 0)
423
+
424
+
425
+ def hmem_tail_placement() -> bool:
426
+ """Whether in-session hierarchical memory is placed as a dynamic SUFFIX after the
427
+ conversation (Phase 0.1) instead of in the foundation ahead of it.
428
+
429
+ The H-MEM block changes every step; in the foundation prefix it invalidated the
430
+ prompt cache for everything after it (skills tail + all conversation) on every
431
+ step. As a tail suffix, the stable foundation + growing conversation form a
432
+ cacheable prefix and only the small H-MEM suffix is reprocessed.
433
+
434
+ Resolved at access time. Default ON everywhere (T1-09, 2026-07-06): it soaked
435
+ locally since 2026-06 with no regressions, while the OFF server default broke the
436
+ server prompt cache every step. Explicit ``HMEM_TAIL_PLACEMENT=false`` restores
437
+ the legacy foundation placement.
438
+ """
439
+ return _bool_env("HMEM_TAIL_PLACEMENT", True)
440
+
441
+
442
+ def ticker_idle_backoff_enabled() -> bool:
443
+ """Whether idle background tickers (cron, goal dispatch) back off their poll
444
+ interval when a tick finds no due work, instead of firing at a fixed cadence
445
+ forever.
446
+
447
+ A fixed 60s ticker costs nothing on a multi-tenant server with steady job
448
+ volume, but on a single-user local CLI (POLYROB_LOCAL) it is close to the
449
+ only thing keeping the process from ever going idle -- a real contributor
450
+ to laptop battery drain. Backoff only kicks in on demonstrably idle ticks
451
+ (nothing ran, nothing failed) and resets to the base interval the moment
452
+ work resumes, so responsiveness is unaffected once something is actually
453
+ happening.
454
+
455
+ Resolved at access time. Defaults ON under POLYROB_LOCAL, OFF on the
456
+ multi-tenant server (byte-identical fixed-cadence legacy) so a precisely
457
+ time-scheduled cron job on a shared server never slips. Explicit
458
+ ``TICKER_IDLE_BACKOFF_ENABLED`` always wins.
459
+ """
460
+ return _bool_env("TICKER_IDLE_BACKOFF_ENABLED", local_mode_enabled())
461
+
462
+
463
+ def ticker_idle_backoff_max_multiplier() -> int:
464
+ """Cap on how many multiples of a ticker's base interval an idle backoff may
465
+ reach (e.g. 5x a 60s base = 300s = 5 minutes worst-case staleness before a
466
+ newly-due job is noticed). Explicit ``TICKER_IDLE_BACKOFF_MAX_MULTIPLIER``
467
+ always wins; default 5.
468
+ """
469
+ return _int_env("TICKER_IDLE_BACKOFF_MAX_MULTIPLIER", 5)
470
+
471
+
472
+ def embedder_needed() -> bool:
473
+ """Whether this deployment actually needs the sentence-transformers embedder (torch).
474
+
475
+ SSOT for both the CLI (maybe_register_cli_embedder) and the server (initialize_modules):
476
+ only build the heavy embedder when KB is enabled, MEMORY_BACKEND=local_vector (hybrid
477
+ vector recall), or local mode. The default MEMORY_BACKEND=sqlite uses FTS5 keyword recall
478
+ and needs no embeddings. See docs/plans/2026-06-26-runtime-architecture-finalization-FUSION.md (P1-EMB).
479
+ """
480
+ return (
481
+ AutonomyConfig.kb_enabled()
482
+ or os.getenv("MEMORY_BACKEND", "sqlite").lower() == "local_vector"
483
+ or local_mode_enabled()
484
+ )
485
+
486
+
487
+ def _safe_autonomy_default(flag_name: str) -> bool:
488
+ """Default for a safe autonomy flag: ON under local mode, else OFF."""
489
+ return local_mode_enabled() if flag_name in _SAFE_LOCAL_FLAGS else False
490
+
491
+
492
+ # --- W1-1: AUTONOMY_POSTURE — one coherent switch for the shipped-but-dark loops ----
493
+ #
494
+ # Five autonomy flags shipped wired but default-OFF in BOTH modes, each behind its own
495
+ # env var, so making an instance actually verify + report its autonomous work meant
496
+ # flipping five independent flags with no single lever (the "activation, not machinery"
497
+ # gap). AUTONOMY_POSTURE is a second axis (orthogonal to _SAFE_LOCAL_FLAGS) that moves
498
+ # the DEFAULTS of that group together. An explicit per-flag env ALWAYS wins (only the
499
+ # default moves), and the unset/`silent` posture is byte-identical to today.
500
+ #
501
+ # silent (default) — today's behavior: autonomy runs but is unverified + silent.
502
+ # owner-visible — the agent's autonomous work becomes VERIFIED + owner-visible:
503
+ # completion judge, blocker->owner escalation (+ ask), self-wake
504
+ # delivery, continuity bridge. Safe for single-user local; on a
505
+ # multi-tenant server it is an opt-in (unsolicited pushes / aux cost).
506
+ # full — owner-visible PLUS time-based initiative (cron ticker).
507
+ _POSTURE_OWNER_VISIBLE_FLAGS = frozenset({
508
+ "GOAL_COMPLETION_JUDGE",
509
+ "GOAL_BLOCKER_ESCALATION",
510
+ "GOAL_SELF_WAKE_ENABLED",
511
+ "AUTONOMOUS_CONTINUITY_BRIDGE",
512
+ # Continuity/learning trio: ON under the local profile, and an owner-visible
513
+ # posture also turns it on server-side (memory + verification, no
514
+ # unsolicited-cost initiative). Explicit env wins.
515
+ "EPISODIC_MEMORY_ENABLED",
516
+ "EPISODIC_DIGEST_INJECT",
517
+ "REFLECTION_ON_SESSION_CLOSE",
518
+ # Budget-aware dispatch: protective + owner-legible (raises an ask, holds spend).
519
+ "BUDGET_AWARE_AUTONOMY",
520
+ })
521
+ _POSTURE_FULL_FLAGS = _POSTURE_OWNER_VISIBLE_FLAGS | {"CRON_ENABLED", "WAKE_CHANGE_GATE"}
522
+ _AUTONOMY_POSTURES = ("silent", "owner-visible", "full")
523
+
524
+
525
+ def autonomy_posture() -> str:
526
+ """Resolved AUTONOMY_POSTURE (silent|owner-visible|full).
527
+
528
+ Default `silent` in BOTH modes (byte-identical to pre-W1-1). An unknown value
529
+ degrades to `silent` so a typo never silently activates autonomy. Access-time so
530
+ tests/bootstrap env changes are seen.
531
+ """
532
+ raw = (os.getenv("AUTONOMY_POSTURE") or "").strip().lower()
533
+ return raw if raw in _AUTONOMY_POSTURES else "silent"
534
+
535
+
536
+ def _posture_autonomy_default(flag_name: str) -> bool:
537
+ """Default for a posture-governed autonomy flag, given the resolved posture."""
538
+ posture = autonomy_posture()
539
+ if posture == "full":
540
+ return flag_name in _POSTURE_FULL_FLAGS
541
+ if posture == "owner-visible":
542
+ return flag_name in _POSTURE_OWNER_VISIBLE_FLAGS
543
+ return False # silent: today's defaults
544
+
545
+
546
+ # --- AGENT_COMPUTE_POSTURE — the compute-capability ladder (computer-use parity) ----
547
+ #
548
+ # A THIRD capability axis, orthogonal to POLYROB_LOCAL (single-user trust profile)
549
+ # and AUTONOMY_POSTURE (which autonomy loops run): how much host/compute capability
550
+ # the agent has.
551
+ #
552
+ # 0 confined (default) — today's docker sandbox, no persistent shell.
553
+ # 1 sandbox-dev — persistent networked sandbox + importable pip installs +
554
+ # `shell` scoped INTO the container + loopback port-publish.
555
+ # 2 self-maintain — posture 1 + the approval-gated `self_env` verbs.
556
+ # 3 host — Hermes-parity host access; requires POLYROB_LOCAL and a
557
+ # single-tenant box (refused on network-facing surfaces).
558
+ #
559
+ # SECURITY CONTRACT:
560
+ # - Default CLOSED: unset/garbage/out-of-range -> 0. Garbage NEVER rounds up —
561
+ # only the literal values 0|1|2|3 are accepted (a typo'd "9" must not grant the
562
+ # host tier).
563
+ # - FROZEN AT IMPORT: the value is snapshotted once at module import so a
564
+ # mid-process env mutation (e.g. a prompt-injected write that reached an
565
+ # env-mutating surface) can never raise the running posture. Operators set it
566
+ # in real process env (systemd EnvironmentFile / shell / dotenv loaded at
567
+ # process start, see main.py) — never at runtime.
568
+ # - The posture is the "may" side only; the existing correspondent-taint gate and
569
+ # delegation blocklist stay the "never" side. `compute_posture_allows` is the
570
+ # ONE predicate every posture-gated capability must call.
571
+
572
+ def _resolve_compute_posture(raw) -> int:
573
+ """PURE: parse one env value -> posture int. Only literal 0|1|2|3 accepted;
574
+ anything else (unset/garbage/out-of-range) degrades CLOSED to 0."""
575
+ try:
576
+ val = int(str(raw).strip())
577
+ except (TypeError, ValueError):
578
+ return 0
579
+ return val if val in (0, 1, 2, 3) else 0
580
+
581
+
582
+ _COMPUTE_POSTURE_FROZEN = _resolve_compute_posture(os.getenv("AGENT_COMPUTE_POSTURE"))
583
+
584
+
585
+ def compute_posture() -> int:
586
+ """Resolved AGENT_COMPUTE_POSTURE (0-3), FROZEN at import (see block comment)."""
587
+ return _COMPUTE_POSTURE_FROZEN
588
+
589
+
590
+ def _refreeze_compute_posture_for_tests() -> int:
591
+ """TEST-ONLY seam: re-snapshot the frozen posture from the current env.
592
+
593
+ Production code must never call this — the freeze is the security property.
594
+ """
595
+ global _COMPUTE_POSTURE_FROZEN
596
+ _COMPUTE_POSTURE_FROZEN = _resolve_compute_posture(os.getenv("AGENT_COMPUTE_POSTURE"))
597
+ return _COMPUTE_POSTURE_FROZEN
598
+
599
+
600
+ # Turn kinds that mark a forged (non-user) re-entry — kept in sync lazily with
601
+ # agents.task.agent.core.self_wake.FORGED_TURN_KINDS (the SSOT, stdlib-only module);
602
+ # this literal fallback only serves if that import ever fails, and failing CLOSED
603
+ # here means "treat as forged", never "let it through".
604
+ _FORGED_TURN_KINDS_FALLBACK = ("self_wake", "delegation_result")
605
+
606
+
607
+ def compute_posture_allows(execution_context, min_posture: int) -> bool:
608
+ """THE single gate predicate for posture-gated compute capabilities.
609
+
610
+ True only when ALL hold:
611
+ (a) frozen ``compute_posture() >= min_posture``;
612
+ (b) the session tenant is the OWNER — ``is_owner_local_safe`` (principal
613
+ match always wins; the POLYROB_LOCAL bypass is honored ONLY for the
614
+ CLI's ``local`` operator tenant, so a forgeable network sender under
615
+ POLYROB_LOCAL=1 on a public surface is never auto-owned);
616
+ (c) not a leaf/sub-agent context;
617
+ (d) not a forged self-wake/delegation-result re-entry turn (the
618
+ ``metadata["turn_kind"]`` stamp, SK-F10).
619
+
620
+ An autonomous goal/cron session of the owner tenant PASSES (no forged stamp;
621
+ role is orchestrator) — deliberate: WS-8 provisions those runs with the
622
+ compute toolset, and the correspondent-taint gate + delegation blocklist
623
+ remain the independent "never" side. ``min_posture <= 0`` is the
624
+ unconditional baseline (posture-0 capabilities keep their own gates).
625
+ Fail-CLOSED: any fault in resolution denies.
626
+ """
627
+ if min_posture <= 0:
628
+ return True
629
+ try:
630
+ if compute_posture() < min_posture:
631
+ return False
632
+ if execution_context is None:
633
+ return False
634
+ if getattr(execution_context, "is_sub_agent", False):
635
+ return False
636
+ if getattr(execution_context, "role", "leaf") != "orchestrator":
637
+ return False
638
+ metadata = getattr(execution_context, "metadata", None) or {}
639
+ try:
640
+ from agents.task.agent.core.self_wake import FORGED_TURN_KINDS as _kinds
641
+ except Exception:
642
+ _kinds = _FORGED_TURN_KINDS_FALLBACK
643
+ if metadata.get("turn_kind") in _kinds:
644
+ return False
645
+ from core.instance import is_owner_local_safe, resolve_owner_principal
646
+ return is_owner_local_safe(
647
+ getattr(execution_context, "user_id", None),
648
+ owner_principal=resolve_owner_principal(),
649
+ local_enabled=local_mode_enabled(),
650
+ )
651
+ except Exception:
652
+ return False # fail-closed: can't prove entitlement -> deny
653
+
654
+
655
+ class AutonomyConfig:
656
+ """Feature flags + caps for the autonomy/continuous-learning loops.
657
+
658
+ Read through this class (not raw os.getenv) so every loop shares one parser and
659
+ the defaults are documented in one place. Evaluated at access time (classmethod /
660
+ property-free) so tests can monkeypatch env between calls.
661
+ """
662
+
663
+ # W1 — self-wake rail
664
+ @staticmethod
665
+ def self_wake_enabled() -> bool:
666
+ return _bool_env("SELF_WAKE_ENABLED", _safe_autonomy_default("SELF_WAKE_ENABLED"))
667
+
668
+ @staticmethod
669
+ def self_wake_max_reentries() -> int:
670
+ return _int_env("SELF_WAKE_MAX_REENTRIES", 3)
671
+
672
+ @staticmethod
673
+ def self_wake_idle_backoff_sec() -> float:
674
+ try:
675
+ return float(os.getenv("SELF_WAKE_IDLE_BACKOFF_SEC", "30"))
676
+ except (TypeError, ValueError):
677
+ return 30.0
678
+
679
+ # W2 — writable skills + background review
680
+ @staticmethod
681
+ def skills_writable() -> bool:
682
+ return _bool_env("SKILLS_WRITABLE", _safe_autonomy_default("SKILLS_WRITABLE"))
683
+
684
+ @staticmethod
685
+ def skills_writable_require_review() -> bool:
686
+ return _bool_env("SKILLS_WRITABLE_REQUIRE_REVIEW", True)
687
+
688
+ @staticmethod
689
+ def skill_overwrite_protect() -> bool:
690
+ # An agent/background overwrite of an existing ACTIVE skill becomes a .pending
691
+ # proposal (owner promotes); all overwrites archive the prior body. Default ON.
692
+ return _bool_env("SKILL_OVERWRITE_PROTECT", True)
693
+
694
+ # polyrob C-write — evolving SELF identity (agent-writable per-(instance,user) doc)
695
+ @staticmethod
696
+ def self_context_writable() -> bool:
697
+ return _bool_env("SELF_CONTEXT_WRITABLE", _safe_autonomy_default("SELF_CONTEXT_WRITABLE"))
698
+
699
+ @staticmethod
700
+ def self_context_require_review() -> bool:
701
+ return _bool_env("SELF_CONTEXT_REQUIRE_REVIEW", True)
702
+
703
+ # Bounded owner-facts doc (USER.md-equivalent) — agent-maintained per-(instance,
704
+ # user) document of durable owner facts/preferences, injected on the SELF/SOUL
705
+ # seam. Same quarantine-then-promote model as SELF; ON under the local profile.
706
+ @staticmethod
707
+ def owner_doc_writable() -> bool:
708
+ return _bool_env("OWNER_DOC_WRITABLE", _safe_autonomy_default("OWNER_DOC_WRITABLE"))
709
+
710
+ @staticmethod
711
+ def owner_doc_require_review() -> bool:
712
+ return _bool_env("OWNER_DOC_REQUIRE_REVIEW", True)
713
+
714
+ # §7.1 — self-evolution transparency + owner control loop
715
+ @staticmethod
716
+ def self_evolution_transparency() -> bool:
717
+ return _bool_env("SELF_EVOLUTION_TRANSPARENCY",
718
+ _safe_autonomy_default("SELF_EVOLUTION_TRANSPARENCY"))
719
+
720
+ @staticmethod
721
+ def background_review_enabled() -> bool:
722
+ return _bool_env("BACKGROUND_REVIEW_ENABLED", _safe_autonomy_default("BACKGROUND_REVIEW_ENABLED"))
723
+
724
+ @staticmethod
725
+ def bg_review_interval() -> int:
726
+ return _int_env("BG_REVIEW_INTERVAL", 10)
727
+
728
+ @staticmethod
729
+ def bg_review_max_steps() -> int:
730
+ return _int_env("BG_REVIEW_MAX_STEPS", 8)
731
+
732
+ # W3 — cron run-loop + delivery
733
+ @staticmethod
734
+ def cron_run_loop() -> bool:
735
+ return _bool_env("CRON_RUN_LOOP", True)
736
+
737
+ @staticmethod
738
+ def cron_delivery_enabled() -> bool:
739
+ return _bool_env("CRON_DELIVERY_ENABLED", False)
740
+
741
+ # Owner daily digest: a cron job carrying payload.digest is composed
742
+ # deterministically ($0, no model turn) from the ledger + event log + open
743
+ # asks and pushed via the cron delivery rail. Default OFF.
744
+ @staticmethod
745
+ def owner_digest_enabled() -> bool:
746
+ return _bool_env("OWNER_DIGEST_ENABLED", False)
747
+
748
+ # W4 — durable goal board
749
+ @staticmethod
750
+ def goals_enabled() -> bool:
751
+ return _bool_env("GOALS_ENABLED", _safe_autonomy_default("GOALS_ENABLED"))
752
+
753
+ @staticmethod
754
+ def goal_max_retries() -> int:
755
+ return _int_env("GOAL_MAX_RETRIES", 2)
756
+
757
+ @staticmethod
758
+ def goal_claim_ttl_sec() -> int:
759
+ return _int_env("GOAL_CLAIM_TTL_SEC", 900)
760
+
761
+ @staticmethod
762
+ def goal_max_run_seconds() -> int:
763
+ """H11: hard wall-clock cap on a single goal run (mirrors cron's per-job cap).
764
+ A goal is otherwise bounded only by max_steps, so one hung step (tool/LLM/browser)
765
+ blocks forever and permanently occupies a GOAL_MAX_CONCURRENT slot."""
766
+ return _int_env("GOAL_MAX_RUN_SECONDS", 1800)
767
+
768
+ @staticmethod
769
+ def goal_dispatch_interval_sec() -> int:
770
+ return _int_env("GOAL_DISPATCH_INTERVAL_SEC", 60)
771
+
772
+ @staticmethod
773
+ def goal_max_concurrent() -> int:
774
+ return _int_env("GOAL_MAX_CONCURRENT", 2)
775
+
776
+ @staticmethod
777
+ def goal_dedup_threshold() -> float:
778
+ try:
779
+ return float(os.getenv("GOAL_DEDUP_THRESHOLD", "0.6"))
780
+ except (TypeError, ValueError):
781
+ return 0.6
782
+
783
+ @staticmethod
784
+ def goal_planner_enabled() -> bool:
785
+ return _bool_env("GOAL_PLANNER_ENABLED", _safe_autonomy_default("GOAL_PLANNER_ENABLED"))
786
+
787
+ @staticmethod
788
+ def goal_planner_min_ready() -> int:
789
+ return _int_env("GOAL_PLANNER_MIN_READY", 2)
790
+
791
+ @staticmethod
792
+ def goal_planner_cooldown_sec() -> int:
793
+ return _int_env("GOAL_PLANNER_COOLDOWN_SEC", 3600)
794
+
795
+ @staticmethod
796
+ def goal_planner_history_n() -> int:
797
+ return _int_env("GOAL_PLANNER_HISTORY_N", 10)
798
+
799
+ @staticmethod
800
+ def goal_daily_quota() -> int:
801
+ """Max goal runs started per trailing 24h; <=0 disables the rail."""
802
+ return _int_env("GOAL_DAILY_QUOTA", 6)
803
+
804
+ @staticmethod
805
+ def goal_self_wake_enabled() -> bool:
806
+ # Was unconditional; redundant-cost finding (grok livetest 2026-06-27).
807
+ # W1-1: default governed by AUTONOMY_POSTURE (owner-visible/full turn it on).
808
+ return _bool_env("GOAL_SELF_WAKE_ENABLED",
809
+ _posture_autonomy_default("GOAL_SELF_WAKE_ENABLED"))
810
+
811
+ # Budget-aware dispatch: before claiming a goal, consult the unified ledger for
812
+ # the tenant's trailing spend; over budget -> raise an owner-visible ask and
813
+ # hold the goal instead of burning credits. Default governed by AUTONOMY_POSTURE
814
+ # (owner-visible/full) — a protective, owner-legible behavior. Explicit env wins.
815
+ @staticmethod
816
+ def budget_aware_autonomy() -> bool:
817
+ return _bool_env("BUDGET_AWARE_AUTONOMY",
818
+ _posture_autonomy_default("BUDGET_AWARE_AUTONOMY"))
819
+
820
+ @staticmethod
821
+ def autonomy_budget_usd() -> float:
822
+ """Trailing-window spend ceiling for autonomous goal dispatch (USD).
823
+ <=0 disables the ceiling (never over budget)."""
824
+ try:
825
+ return float(os.getenv("AUTONOMY_BUDGET_USD", "10"))
826
+ except (TypeError, ValueError):
827
+ return 10.0
828
+
829
+ @staticmethod
830
+ def autonomy_budget_window_days() -> int:
831
+ return _int_env("AUTONOMY_BUDGET_WINDOW_DAYS", 1)
832
+
833
+ # §3.2 (goal-completion-verification, 2026-07-05) — judge a completed goal's
834
+ # acceptance with a cheap aux model. 'unmet' -> record_failure, 'unclear'/error/
835
+ # timeout -> pass (fail-open). W1-1: default governed by AUTONOMY_POSTURE.
836
+ @staticmethod
837
+ def goal_completion_judge() -> bool:
838
+ return _bool_env("GOAL_COMPLETION_JUDGE",
839
+ _posture_autonomy_default("GOAL_COMPLETION_JUDGE"))
840
+
841
+ @staticmethod
842
+ def goal_judge_timeout_sec() -> int:
843
+ return _int_env("GOAL_JUDGE_TIMEOUT_SEC", 60)
844
+
845
+ # Wake change-gate: a change-gated cron review
846
+ # tick skips the paid model call when the tenant's observable state hasn't
847
+ # moved since the last tick (cron/wake_gate.py). Posture `full` turns it on
848
+ # by default — it pairs with CRON_ENABLED; per-job opt-in via
849
+ # payload.change_gated, delivery jobs never gated.
850
+ @staticmethod
851
+ def wake_change_gate() -> bool:
852
+ return _bool_env("WAKE_CHANGE_GATE",
853
+ _posture_autonomy_default("WAKE_CHANGE_GATE"))
854
+
855
+ # §7.2 — blocker → owner escalation. When a goal trips the circuit breaker
856
+ # (status='blocked') OR the pipeline drains, surface a concrete ask to the owner
857
+ # instead of dying silently. Default OFF (an unsolicited owner push is opt-in).
858
+ @staticmethod
859
+ def goal_blocker_escalation() -> bool:
860
+ # W1-1: default governed by AUTONOMY_POSTURE (owner-visible/full turn it on).
861
+ return _bool_env("GOAL_BLOCKER_ESCALATION",
862
+ _posture_autonomy_default("GOAL_BLOCKER_ESCALATION"))
863
+
864
+ @staticmethod
865
+ def goal_empty_pipeline_escalate_after() -> int:
866
+ """Consecutive planner runs that leave the ready queue EMPTY before the
867
+ stall escalates to the owner (rides GOAL_BLOCKER_ESCALATION)."""
868
+ return _int_env("GOAL_EMPTY_PIPELINE_ESCALATE_AFTER", 2)
869
+
870
+ # §7.5 — autonomous continuity bridge. Carry a recent-activity summary INTO a
871
+ # goal/cron tick (opposite scoping to the chat digest) so autonomous runs stop
872
+ # re-deriving "nothing new" every tick. Default OFF (additive context; verify
873
+ # token cost before flipping on).
874
+ @staticmethod
875
+ def autonomous_continuity_bridge() -> bool:
876
+ # W1-1: default governed by AUTONOMY_POSTURE (owner-visible/full turn it on).
877
+ return _bool_env("AUTONOMOUS_CONTINUITY_BRIDGE",
878
+ _posture_autonomy_default("AUTONOMOUS_CONTINUITY_BRIDGE"))
879
+
880
+ # W5 — curator
881
+ @staticmethod
882
+ def curator_enabled() -> bool:
883
+ return _bool_env("CURATOR_ENABLED", _safe_autonomy_default("CURATOR_ENABLED"))
884
+
885
+ @staticmethod
886
+ def curator_interval_hours() -> int:
887
+ return _int_env("CURATOR_INTERVAL_HOURS", 168)
888
+
889
+ @staticmethod
890
+ def curator_stale_days() -> int:
891
+ return _int_env("CURATOR_STALE_DAYS", 30)
892
+
893
+ @staticmethod
894
+ def curator_archive_days() -> int:
895
+ return _int_env("CURATOR_ARCHIVE_DAYS", 90)
896
+
897
+ # (curator_llm_merge / CURATOR_LLM_MERGE removed 2026-06-29 — the Phase-2 merge step
898
+ # it gated was a logged no-op with no merge policy. Re-add under its own flag when a
899
+ # concrete policy exists.)
900
+
901
+ # W6 — cross-session search tool (read-only, default-on)
902
+ @staticmethod
903
+ def memory_search_tool() -> bool:
904
+ return _bool_env("MEMORY_SEARCH_TOOL", True)
905
+
906
+ # W7 — insights tool (read-only authored-skill reuse metric)
907
+ @staticmethod
908
+ def insights_tool() -> bool:
909
+ return _bool_env("INSIGHTS_TOOL", _safe_autonomy_default("INSIGHTS_TOOL"))
910
+
911
+ # KB — knowledge-base feature gate (Task 2 / local_vector prerequisite)
912
+ @staticmethod
913
+ def kb_enabled() -> bool:
914
+ return _bool_env("KB_ENABLED", _safe_autonomy_default("KB_ENABLED"))
915
+
916
+ # C1 — context-reference expansion (@file/@folder/@diff/@url)
917
+ # Default ON under POLYROB_LOCAL (single-user CLI), OFF on the server.
918
+ @staticmethod
919
+ def context_references_enabled() -> bool:
920
+ return _bool_env(
921
+ "CONTEXT_REFERENCES_ENABLED",
922
+ _safe_autonomy_default("CONTEXT_REFERENCES_ENABLED"),
923
+ )
924
+
925
+ # C9 — auto-load CLAUDE.md/AGENTS.md/.cursorrules as a PROJECT_CONTEXT foundation
926
+ # message. Default ON under POLYROB_LOCAL (single-user CLI), OFF on the server.
927
+ @staticmethod
928
+ def project_context_autoload() -> bool:
929
+ return _bool_env(
930
+ "PROJECT_CONTEXT_AUTOLOAD",
931
+ _safe_autonomy_default("PROJECT_CONTEXT_AUTOLOAD"),
932
+ )
933
+
934
+ @staticmethod
935
+ def project_context_max_tokens() -> int:
936
+ return _int_env("PROJECT_CONTEXT_MAX_TOKENS", 20000)
937
+
938
+ # Phase 2 — server-side project-context opt-in. When ON (and NOT local mode),
939
+ # the loader runs on the server and the file is injected UNTRUSTED-WRAPPED
940
+ # (framed as DATA, not instructions). Default OFF and deliberately NOT a
941
+ # safe-local flag — POLYROB_LOCAL must not flip it on, so the multi-tenant
942
+ # server stays byte-identical unless an operator explicitly opts in.
943
+ @staticmethod
944
+ def project_context_server_mode() -> bool:
945
+ return _bool_env("PROJECT_CONTEXT_SERVER_MODE", False)
946
+
947
+ # T13 — KB auto-prefetch (inject KB recall alongside memory recall at step start)
948
+ # Default ON under POLYROB_LOCAL (single-user CLI), OFF on multi-tenant server.
949
+ @staticmethod
950
+ def kb_auto_prefetch() -> bool:
951
+ return _bool_env("KB_AUTO_PREFETCH", _safe_autonomy_default("KB_AUTO_PREFETCH"))
952
+
953
+ # Task 2 — episodic activity ledger (durable per-run provenance rows).
954
+ # Default ON under POLYROB_LOCAL (single-user CLI) OR AUTONOMY_POSTURE
955
+ # owner-visible/full (verified + owner-visible autonomy implies a durable
956
+ # activity ledger, so episodic is part of the posture group).
957
+ @staticmethod
958
+ def episodic_memory_enabled() -> bool:
959
+ return _bool_env("EPISODIC_MEMORY_ENABLED",
960
+ _safe_autonomy_default("EPISODIC_MEMORY_ENABLED")
961
+ or _posture_autonomy_default("EPISODIC_MEMORY_ENABLED"))
962
+
963
+ # Task 3 — inject a recent-episodes digest into the session.
964
+ @staticmethod
965
+ def episodic_digest_inject() -> bool:
966
+ return _bool_env("EPISODIC_DIGEST_INJECT",
967
+ _safe_autonomy_default("EPISODIC_DIGEST_INJECT")
968
+ or _posture_autonomy_default("EPISODIC_DIGEST_INJECT"))
969
+
970
+ # Session-close reflection (consolidate a short session's findings
971
+ # at close; one extra aux call per closed session). Posture-governed so an
972
+ # owner-visible instance actually learns from its autonomous runs. Consumer:
973
+ # modules/memory/task/task_context_manager.py (reads this resolver lazily).
974
+ @staticmethod
975
+ def reflection_on_session_close() -> bool:
976
+ return _bool_env("REFLECTION_ON_SESSION_CLOSE",
977
+ _posture_autonomy_default("REFLECTION_ON_SESSION_CLOSE"))
978
+
979
+ # Restart-durable autonomy state (background delegations + reentry
980
+ # budgets in autonomy_state.db). Default ON; off restores volatile registries.
981
+ @staticmethod
982
+ def autonomy_state_durable() -> bool:
983
+ return _bool_env("AUTONOMY_STATE_DURABLE", True)
984
+
985
+ # Task 4 — cross-session continuity bridge (thread_key stitching).
986
+ @staticmethod
987
+ def continuity_bridge_enabled() -> bool:
988
+ return _bool_env("CONTINUITY_BRIDGE_ENABLED", _safe_autonomy_default("CONTINUITY_BRIDGE_ENABLED"))
989
+
990
+ # Task 4 — LLM-generated continuity summary at reset. Intentionally NOT in
991
+ # _SAFE_LOCAL_FLAGS: OFF everywhere by default (adds latency at reset).
992
+ @staticmethod
993
+ def continuity_llm_summary() -> bool:
994
+ return _bool_env("CONTINUITY_LLM_SUMMARY", False) # OFF everywhere (latency at reset)
995
+
996
+ # Task 2 — episodic row retention window (days); pruning consumer TBD.
997
+ @staticmethod
998
+ def episodic_retention_days() -> int:
999
+ return _int_env("EPISODIC_RETENTION_DAYS", 90)
1000
+
1001
+ # T16 — interrupt-and-redirect: Ctrl-C mid-turn prompts for a redirect instruction
1002
+ # that becomes the next turn instead of silently aborting. Default OFF; NOT in
1003
+ # _SAFE_LOCAL_FLAGS (must be opt-in — changes SIGINT UX for all local users).
1004
+ @staticmethod
1005
+ def interrupt_redirect_enabled() -> bool:
1006
+ return _bool_env("INTERRUPT_REDIRECT", False)
1007
+
1008
+
1009
+ class TimeoutConfig:
1010
+ """Centralized timeout configuration - SINGLE SOURCE OF TRUTH.
1011
+
1012
+ All timeout values should be accessed through this class to ensure
1013
+ consistency across the codebase. Tool-specific timeouts account for
1014
+ the different latency characteristics of each tool type.
1015
+
1016
+ Sub-agent settings are loaded from BotConfig (core/config.py) which supports
1017
+ both config file and environment variable sources.
1018
+ """
1019
+
1020
+ # Lazy-loaded config reference
1021
+ _config = None
1022
+
1023
+ @classmethod
1024
+ def _get_config(cls):
1025
+ """Get BotConfig instance (lazy load to avoid circular imports)."""
1026
+ if cls._config is None:
1027
+ try:
1028
+ from core.config import BotConfig
1029
+ cls._config = BotConfig()
1030
+ except Exception:
1031
+ cls._config = None
1032
+ return cls._config
1033
+
1034
+ # ========== TOOL EXECUTION TIMEOUTS ==========
1035
+ # These are applied in controller/service.py multi_act()
1036
+ TOOL_TIMEOUTS = {
1037
+ 'mcp': int(os.getenv('MCP_TIMEOUT_SECONDS', '180')), # MCP: network/subprocess latency
1038
+ 'browser': int(os.getenv('BROWSER_TIMEOUT_SECONDS', '120')), # Browser: page loads, DOM operations
1039
+ 'filesystem': int(os.getenv('FILESYSTEM_TIMEOUT_SECONDS', '30')), # Filesystem: fast local I/O
1040
+ 'polymarket': int(os.getenv('POLYMARKET_TIMEOUT_SECONDS', '60')), # Polymarket API
1041
+ 'default': int(os.getenv('DEFAULT_TOOL_TIMEOUT_SECONDS', '60')), # Default for unknown tools
1042
+ }
1043
+
1044
+ # ========== SUB-AGENT CONTROLS ==========
1045
+ # Loaded from BotConfig (core/config.py) - supports config file + env vars
1046
+ # FIX (Jan 2026): Sub-agent system disabled by default - too chaotic
1047
+ # Use the getter methods for dynamic config-based values
1048
+
1049
+ @classmethod
1050
+ def get_sub_agents_enabled(cls) -> bool:
1051
+ """Check if sub-agents are enabled (from BotConfig)."""
1052
+ config = cls._get_config()
1053
+ if config:
1054
+ return config.sub_agents_enabled
1055
+ return _core_bool_env('SUB_AGENTS_ENABLED', True)
1056
+
1057
+ @classmethod
1058
+ def get_subagent_least_privilege(cls) -> bool:
1059
+ """UP-05: narrow a delegated child's toolset (drop code_execution/cronjob
1060
+ tool_ids + suppress delegation actions on leaf children via a dedicated
1061
+ child Controller). Default ON — the prior behaviour leaked the full parent
1062
+ toolset to children. Set SUBAGENT_LEAST_PRIVILEGE=false for the legacy
1063
+ shared-controller path (byte-identical to pre-UP-05)."""
1064
+ return _core_bool_env('SUBAGENT_LEAST_PRIVILEGE', True)
1065
+
1066
+ @classmethod
1067
+ def get_sub_agent_timeout(cls) -> int:
1068
+ """Sub-agent timeout in seconds (from BotConfig)."""
1069
+ config = cls._get_config()
1070
+ if config:
1071
+ return config.sub_agent_timeout
1072
+ return int(os.getenv('SUB_AGENT_TIMEOUT_SECONDS', '600'))
1073
+
1074
+ @classmethod
1075
+ def get_parallel_subtasks_timeout(cls) -> int:
1076
+ """Parallel subtasks timeout in seconds (from BotConfig)."""
1077
+ config = cls._get_config()
1078
+ if config:
1079
+ return config.parallel_subtasks_timeout
1080
+ return int(os.getenv('PARALLEL_SUBTASKS_TIMEOUT_SECONDS', '900'))
1081
+
1082
+ @classmethod
1083
+ def get_max_concurrent_sub_agents(cls) -> int:
1084
+ """Maximum concurrent sub-agents (from BotConfig)."""
1085
+ config = cls._get_config()
1086
+ if config:
1087
+ return config.max_concurrent_sub_agents
1088
+ return int(os.getenv('MAX_CONCURRENT_SUB_AGENTS', '3'))
1089
+
1090
+ @classmethod
1091
+ def get_max_sub_agent_depth(cls) -> int:
1092
+ """Maximum sub-agent nesting depth (from BotConfig)."""
1093
+ config = cls._get_config()
1094
+ if config:
1095
+ return config.max_sub_agent_depth
1096
+ return int(os.getenv('MAX_SUB_AGENT_DEPTH', '1'))
1097
+
1098
+ @classmethod
1099
+ def get_max_async_sub_agents(cls) -> int:
1100
+ """Max concurrent BACKGROUND sub-agents (UP-12 delegate_task background=true).
1101
+
1102
+ Bounds how many background delegation slots may be live at once. Floors at 1
1103
+ and never exceeds get_max_concurrent_sub_agents() (background + sync share the
1104
+ same SubAgentManager semaphore, so the background cap must not exceed the total).
1105
+ Env: MAX_ASYNC_SUB_AGENTS (default 2).
1106
+ """
1107
+ raw = int(os.getenv('MAX_ASYNC_SUB_AGENTS', '2'))
1108
+ ceiling = cls.get_max_concurrent_sub_agents()
1109
+ return max(1, min(raw, ceiling))
1110
+
1111
+ # Legacy attribute access - for backward compatibility
1112
+ # These call the getter methods
1113
+ SUB_AGENTS_ENABLED = True # C-DELEG: ON by default; use get_sub_agents_enabled() for dynamic value
1114
+ SUB_AGENT_TIMEOUT = 600
1115
+ PARALLEL_SUBTASKS_TIMEOUT = 900
1116
+ MAX_CONCURRENT_SUB_AGENTS = 3
1117
+ MAX_SUB_AGENT_DEPTH = 1
1118
+
1119
+ # ========== STEP/SESSION TIMEOUTS ==========
1120
+ STEP_TIMEOUT = float(os.getenv('STEP_TIMEOUT_SECONDS', '300')) # 5 min per step
1121
+ STALL_TIMEOUT = float(os.getenv('STALL_TIMEOUT_SECONDS', '300')) # 5 min stall detection
1122
+
1123
+ # ========== LLM REQUEST TIMEOUTS ==========
1124
+ LLM_REQUEST_TIMEOUT = int(os.getenv('LLM_REQUEST_TIMEOUT_SECONDS', '120')) # Standard LLM request
1125
+ LLM_STREAM_TIMEOUT = int(os.getenv('LLM_STREAM_TIMEOUT_SECONDS', '300')) # Streaming needs longer
1126
+ LLM_BASE_TIMEOUT = float(os.getenv('LLM_BASE_TIMEOUT_SECONDS', '30')) # Base (adjusted by tokens)
1127
+
1128
+ # ========== BROWSER CLEANUP TIMEOUTS ==========
1129
+ BROWSER_CLOSE = float(os.getenv('BROWSER_CLOSE_TIMEOUT', '3.0'))
1130
+ BROWSER_CONTEXT_CLOSE = float(os.getenv('BROWSER_CONTEXT_CLOSE_TIMEOUT', '8.0'))
1131
+ BROWSER_INSTANCE_CLOSE = float(os.getenv('BROWSER_INSTANCE_CLOSE_TIMEOUT', '5.0'))
1132
+ PROCESS_KILL = float(os.getenv('PROCESS_KILL_TIMEOUT', '2.0'))
1133
+
1134
+ @classmethod
1135
+ def get_tool_timeout(cls, tool_name: str) -> int:
1136
+ """Get timeout for a specific tool.
1137
+
1138
+ Args:
1139
+ tool_name: Name of the tool (e.g., 'mcp', 'browser', 'filesystem')
1140
+
1141
+ Returns:
1142
+ Timeout in seconds for the tool
1143
+ """
1144
+ if not tool_name:
1145
+ return cls.TOOL_TIMEOUTS['default']
1146
+ return cls.TOOL_TIMEOUTS.get(tool_name.lower(), cls.TOOL_TIMEOUTS['default'])
1147
+
1148
+ @classmethod
1149
+ def get_llm_timeout(cls, streaming: bool = False) -> int:
1150
+ """Get timeout for LLM requests.
1151
+
1152
+ Args:
1153
+ streaming: Whether this is a streaming request
1154
+
1155
+ Returns:
1156
+ Timeout in seconds for the LLM request
1157
+ """
1158
+ return cls.LLM_STREAM_TIMEOUT if streaming else cls.LLM_REQUEST_TIMEOUT
1159
+
1160
+
1161
+ # Legacy constants for backward compatibility (deprecated - use TimeoutConfig instead)
1162
+ DEFAULT_STEP_TIMEOUT = TimeoutConfig.STEP_TIMEOUT
1163
+ DEFAULT_STALL_TIMEOUT = TimeoutConfig.STALL_TIMEOUT
1164
+ LLM_BASE_TIMEOUT = TimeoutConfig.LLM_BASE_TIMEOUT
1165
+ BROWSER_CLOSE_TIMEOUT = TimeoutConfig.BROWSER_CLOSE
1166
+ BROWSER_CONTEXT_CLOSE_TIMEOUT = TimeoutConfig.BROWSER_CONTEXT_CLOSE
1167
+ BROWSER_INSTANCE_CLOSE_TIMEOUT = TimeoutConfig.BROWSER_INSTANCE_CLOSE
1168
+ PROCESS_KILL_TIMEOUT = TimeoutConfig.PROCESS_KILL
1169
+
1170
+ # Agent Limits (defaults, can be overridden)
1171
+ DEFAULT_MAX_FAILURES = 5 # Maximum consecutive failures before stopping
1172
+ DEFAULT_MAX_ERROR_LENGTH = 400 # Maximum length for error messages
1173
+ DEFAULT_MAX_ACTIONS_PER_STEP = 10 # Maximum actions per step
1174
+ DEFAULT_MIN_INPUT_TOKENS = 1000 # Minimum safe input tokens
1175
+
1176
+ # MCP Tool Throttling (single source of truth)
1177
+ # MCP actions (scraping, searches, APIs) are expensive and execute SEQUENTIALLY
1178
+ # Each MCP action takes 30-180 seconds. Limiting prevents timeout cascades.
1179
+ MAX_MCP_PER_STEP = int(os.getenv('MAX_MCP_PER_STEP', '3')) # Configurable via env
1180
+
1181
+ # Context-compaction hysteresis (flow-efficiency D3-a)
1182
+ # LLM compaction (llm_compact_history) is an EXTRA LLM call. Without a cooldown it
1183
+ # re-fires every step once usage stays >=85% (a single large MCP result can re-cross
1184
+ # the line each step), doubling call cost on long runs. Enforce a minimum step gap
1185
+ # between LLM compactions; the >=95% emergency prune (non-LLM) remains the safety net.
1186
+ COMPACTION_COOLDOWN_STEPS = int(os.getenv('COMPACTION_COOLDOWN_STEPS', '3'))
1187
+
1188
+ # Compaction payload tuning (Reference-parity context upgrade, 2026-06).
1189
+ # These govern HOW the LLM compaction summarizes, not WHEN it fires (the tiered
1190
+ # thresholds in CompactionManager own the trigger). Previously the summary input
1191
+ # was silently truncated to the last 50 messages, each clipped to 500 chars, with
1192
+ # tool results dropped entirely -> the summary lost most of what it claimed to keep.
1193
+ COMPACTION_KEEP_RECENT = int(os.getenv('COMPACTION_KEEP_RECENT', '10')) # min recent msgs kept verbatim
1194
+ COMPACTION_TAIL_TOKEN_RATIO = float(os.getenv('COMPACTION_TAIL_TOKEN_RATIO', '0.20')) # C3: tail kept by token budget too
1195
+ COMPACTION_PER_MSG_CHARS = int(os.getenv('COMPACTION_PER_MSG_CHARS', '3000')) # A2: per-message head+tail budget into summarizer
1196
+ COMPACTION_TOOL_RESULT_CHARS = int(os.getenv('COMPACTION_TOOL_RESULT_CHARS', '2000')) # A2: tool-result budget (was: dropped)
1197
+ COMPACTION_MAX_SUMMARY_TOKENS = int(os.getenv('COMPACTION_MAX_SUMMARY_TOKENS', '12000')) # A3: ceiling for summary budget
1198
+ COMPACTION_MIN_SUMMARY_TOKENS = int(os.getenv('COMPACTION_MIN_SUMMARY_TOKENS', '2000')) # A3: floor for summary budget
1199
+ COMPACTION_MIN_SAVINGS_PCT = float(os.getenv('COMPACTION_MIN_SAVINGS_PCT', '10.0')) # B4: anti-thrash back-off threshold
1200
+ COMPACTION_CHECKPOINT = _core_bool_env('COMPACTION_CHECKPOINT', True) # C2: dump pre-compaction trajectory
1201
+ # A5: route the (expensive, repeated) summarization call to a cheaper auxiliary model.
1202
+ # Empty -> use the main model (backward-compatible). Format: "provider:model" or "model".
1203
+ COMPACTION_AUX_MODEL = os.getenv('COMPACTION_AUX_MODEL', '')
1204
+
1205
+ # Bounded tool-free reasoning turns (flow-efficiency D1-a, the seed resolution).
1206
+ # POLYROB forces >=1 function call every step. This allows a SMALL number of tool-free
1207
+ # "planning" turns to be treated as legitimate (gentle nudge) rather than a hard
1208
+ # error on the very first empty response. The 3-consecutive thinking-loop
1209
+ # escalation remains the hard backstop, so the loop bound is unchanged.
1210
+ ALLOWED_REASONING_TURNS = int(os.getenv('ALLOWED_REASONING_TURNS', '1'))
1211
+
1212
+ # S-1: progressive skill disclosure. When ON (the DEFAULT — see the function below),
1213
+ # only a compact <skill-catalog> (ids + one-line descriptions, ~0.15k) is injected and
1214
+ # the agent pulls a skill's full body on demand via the load_skill(skill_id) tool. When
1215
+ # OFF, skills are eager-injected full-body as a pinned user message (~3.2k tok).
1216
+ def skill_progressive_disclosure() -> bool:
1217
+ """Access-time gate (mirrors skill_catalog_include_all). Default ON.
1218
+
1219
+ Falsey-set SSOT (`core.env.bool_env`): only none/off/false/0/no/'' disable.
1220
+ Access-time read (not an import-bound constant) so an env override always wins.
1221
+ """
1222
+ return _core_bool_env('SKILL_PROGRESSIVE_DISCLOSURE', True)
1223
+
1224
+ # NOTE: the catalog-include-all policy is the access-time `skill_catalog_include_all()`
1225
+ # function below (defaults ON). A dead module-level constant that defaulted to 'false'
1226
+ # used to live here and was a decoy — no caller read it while the function returned True
1227
+ # (Phase 0.6: removed to end the "is it on or off?" confusion).
1228
+
1229
+
1230
+ def skill_catalog_include_all() -> bool:
1231
+ """Whether the <skill-catalog> lists ALL skills (not just trigger-matched).
1232
+
1233
+ P2-1a: defaults **ON everywhere** (model-chosen disclosure). The substring/regex
1234
+ trigger matcher is lossy — a paraphrased task can match zero skills, and on the
1235
+ server an unmatched skill is then undiscoverable (neither in the catalog nor
1236
+ load_skill-able). Listing the full compact catalog (id + one-line desc, bounded by
1237
+ ``get_catalog_skills(max_skills)``, ~20-30 tok each, cache-stable) lets the agent
1238
+ discover and ``load_skill`` what it judges relevant. ~600 tok at the current library
1239
+ size. Set ``SKILL_CATALOG_INCLUDE_ALL=false`` to restore trigger-matched-only.
1240
+ Access-time read (not the import-bound constant) so an env override always wins.
1241
+ """
1242
+ return _bool_env("SKILL_CATALOG_INCLUDE_ALL", True)
1243
+
1244
+ # UP-06: wrap untrusted tool-result content (mcp/browser/web/perplexity) in
1245
+ # <untrusted_tool_result> delimiters before it enters message history, + a <security>
1246
+ # system-prompt line teaching the model the wrapped content is DATA, not instructions.
1247
+ # Security control — default ON. OFF => byte-identical to legacy (no wrapper, no <security>).
1248
+ UNTRUSTED_TOOL_RESULT_WRAP = _core_bool_env('UNTRUSTED_TOOL_RESULT_WRAP', True)
1249
+
1250
+ # S1 (chat consolidation): the persona/<identity> injection gate lives in the
1251
+ # access-time `task_personality_block_enabled()` function above (there is no
1252
+ # module-level constant here — see that docstring for the full rationale).
1253
+
1254
+ # S2 (chat consolidation): tool-light "chat mode" for TaskAgent.chat_once. The
1255
+ # toolset must be NON-EMPTY — an empty tool_ids list trips the orchestrator's
1256
+ # comprehensive-default fallback (loads browser/mcp/etc). "task" is the harmless
1257
+ # TODO tool; chat mode otherwise relies on send_message/done + conversational-exit.
1258
+ CHAT_TOOL_IDS = [t.strip() for t in os.getenv('CHAT_TOOL_IDS', 'task').split(',') if t.strip()] or ['task']
1259
+ CHAT_MAX_STEPS = _int_env('CHAT_MAX_STEPS', 8)
1260
+
1261
+ # Chat consolidation (HANDOFF-C, 2026-06-19): the HTTP /api/chat/message endpoint
1262
+ # is now served SOLELY by the unified task agent (TaskAgent.chat_once); the legacy
1263
+ # ChatAgent was retired, so there is no toggle/fallback target anymore.
1264
+ #
1265
+ # The chat path does NO credit pre-check (matching the legacy behavior), so
1266
+ # chat_once skips create_session's pre-check by default (per-call billing still
1267
+ # applies). Set CHAT_SKIP_CREDIT_CHECK=off to apply the credit gate to chat.
1268
+ CHAT_SKIP_CREDIT_CHECK = os.getenv('CHAT_SKIP_CREDIT_CHECK', 'true').strip().lower() not in ('0', 'false', 'no', 'off', 'none', '')
1269
+
1270
+ # Human-in-the-Loop Defaults
1271
+ DEFAULT_RECENT_MESSAGES_LIMIT = 10 # Number of recent messages to fetch for context
1272
+
1273
+ # Screenshot and GIF Settings
1274
+ MAX_SCREENSHOTS_FOR_GIF = 20 # Maximum screenshots to include in GIF
1275
+
1276
+ # User Interaction Limits
1277
+ DEFAULT_MAX_USER_GUIDANCE_TOKENS = 1000 # Maximum tokens for user guidance messages
1278
+ DEFAULT_MAX_USER_MESSAGES_PER_STEP = 3 # Maximum user messages to process per step
1279
+
1280
+ # Session Management
1281
+ # NOTE (B3): the per-user session cap SSOT is BotConfig.max_sessions_per_user
1282
+ # (core/config.py, default 10), read at runtime in task_agent_lite. A duplicate
1283
+ # `MAX_SESSIONS_PER_USER = 100` used to live here but had ZERO call sites and only
1284
+ # misled readers — removed. Do not reintroduce a second cap constant here.
1285
+ SESSION_CLEANUP_DELAY = 2.0 # Delay before cleaning up completed sessions
1286
+ STALL_CHECK_INTERVAL = 30.0 # Check for stalls every 30 seconds
1287
+
1288
+ # Model-specific Tool Calling Instructions
1289
+ # Some models (Grok, certain OpenRouter models) struggle with nested argument placement
1290
+ # These instructions are injected into the system prompt for affected models
1291
+ GROK_TOOL_CALL_INSTRUCTIONS = """
1292
+ ## 🚨 CRITICAL: MCP TOOL CALL FORMAT 🚨
1293
+
1294
+ **Do NOT send empty parameters — a tool with no params does nothing and fails.**
1295
+
1296
+ MCP tools are registered as DIRECT actions with FLAT parameters (there is no
1297
+ nested `arguments={...}` wrapper). Call the tool by its `{server}_{tool}` name
1298
+ and pass its parameters directly:
1299
+
1300
+ ### ✅ CORRECT (direct, flat params):
1301
+ ```python
1302
+ anysite_api(endpoint="/api/linkedin/search", params={"keywords": "CEO founder", "count": 10})
1303
+ webparser_parse(url="https://example.com")
1304
+ ```
1305
+
1306
+ ### ❌ WRONG (the deprecated nested form — DO NOT use):
1307
+ ```python
1308
+ mcp_execute_tool(server_name="anysite", tool_name="search", arguments={})
1309
+ ```
1310
+
1311
+ ### THE RULE:
1312
+ - Call the tool DIRECTLY by its `{server}_{tool}` name; pass params inline/flat.
1313
+ - NEVER pass empty params — if you don't know what a tool needs, discover it
1314
+ first (e.g. `mcp_list_tools`) before calling.
1315
+ """
1316
+
1317
+ # Models that need explicit tool argument instructions
1318
+ MODELS_NEEDING_TOOL_INSTRUCTIONS = [
1319
+ 'grok',
1320
+ 'x-ai',
1321
+ 'xai',
1322
+ ]
1323
+
1324
+ # ---------------------------------------------------------------------------
1325
+ # P-1: per-model-family operational guidance.
1326
+ # Short, high-signal nudges appended to the system prompt for non-Anthropic
1327
+ # families whose tool-calling/agent behavior benefits from explicit framing.
1328
+ # Kept terse so the cached system prefix stays small. Anthropic/Claude needs
1329
+ # none (the prompt is authored for it); Grok keeps its dedicated argument-format
1330
+ # block (MODELS_NEEDING_TOOL_INSTRUCTIONS) above and is not duplicated here.
1331
+ # ---------------------------------------------------------------------------
1332
+ GEMINI_FAMILY_INSTRUCTIONS = """## MODEL NOTE (Gemini)
1333
+ - Call exactly one function per step unless several are truly independent; after an
1334
+ optional brief planning turn, include at least one function call each step.
1335
+ - Use the exact tool names from the schema; do not invent wrapper names.
1336
+ - To reply to the user and finish, call done(text=...). A single non-blocking
1337
+ send_message does not end your turn; if you only reply without acting for a couple of
1338
+ steps the runtime ends the turn for you.
1339
+ """
1340
+
1341
+ OPENAI_FAMILY_INSTRUCTIONS = """## MODEL NOTE (GPT)
1342
+ - Emit native function calls, not JSON-in-text. Every step must include ≥1 function call.
1343
+ - Prefer one decisive action per step over long deliberation.
1344
+ - To reply and finish in one shot, use done(text=...); reserve non-blocking
1345
+ send_message for a status update you immediately follow with more tool calls.
1346
+ """
1347
+
1348
+ KIMI_FAMILY_INSTRUCTIONS = """## MODEL NOTE (Kimi)
1349
+ - Return tool calls as structured function calls only — never write tool-call
1350
+ delimiter tokens (e.g. <|tool_call_begin|>) into your text content.
1351
+ - One function call per step is enough; don't repeat the same message across steps.
1352
+ - To greet/answer and finish, call done(text=...). A single non-blocking send_message
1353
+ does not end your turn; if you only reply without acting for a couple of steps the
1354
+ runtime ends the turn for you, so prefer done() to finish deliberately.
1355
+ """
1356
+
1357
+ # Substring → guidance. First match on the lowercased model name wins.
1358
+ MODEL_FAMILY_INSTRUCTIONS = [
1359
+ ('kimi', KIMI_FAMILY_INSTRUCTIONS),
1360
+ ('gemini', GEMINI_FAMILY_INSTRUCTIONS),
1361
+ ('gpt', OPENAI_FAMILY_INSTRUCTIONS),
1362
+ ]
1363
+
1364
+ # T1-10: families that reliably emit the brain-state JSON without a per-step
1365
+ # reminder. The prompt is authored for Claude (MODEL_FAMILY_INSTRUCTIONS gives it
1366
+ # no family note), so the INJECT_FORMAT_HINT_EARLY native-mode nag is pure noise
1367
+ # there — one wasted uncached message per step. Same needle mechanism as the
1368
+ # family notes: matched on the model name (plus the resolved provider).
1369
+ FORMAT_NAG_EXEMPT_FAMILIES = ('claude',)
1370
+
1371
+
1372
+ def format_nag_exempt(model_name: str, provider: str = "") -> bool:
1373
+ """Whether the per-step brain-state format reminder should be skipped."""
1374
+ name = (model_name or "").lower()
1375
+ if any(needle in name for needle in FORMAT_NAG_EXEMPT_FAMILIES):
1376
+ return True
1377
+ return (provider or "").lower() == "anthropic"
1378
+
1379
+ # OpenRouter models that may need extra help
1380
+ OPENROUTER_MODELS_NEEDING_HELP = [
1381
+ 'grok',
1382
+ 'mistral',
1383
+ 'mixtral',
1384
+ ]