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,1179 @@
1
+
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import base64
6
+ import http
7
+ import importlib
8
+ import json
9
+ import logging
10
+ import os
11
+ import re
12
+ import subprocess
13
+ import traceback
14
+ import time
15
+ from dataclasses import dataclass, field
16
+ from datetime import datetime
17
+ from io import BytesIO
18
+ from pathlib import Path
19
+ from typing import Any, Callable, Dict, List, Optional, Type, TypeVar, Union, Tuple
20
+ from collections import deque # ADDED: Import deque for bounded collections
21
+ from agents.task.agent.tool_call_tracker import ToolCallTracker # Robust tool call ID tracking
22
+
23
+ from dotenv import load_dotenv
24
+
25
+ # Import centralized constants
26
+ from agents.task.constants import (
27
+ IMG_TOKENS,
28
+ LoopDetectionConfig,
29
+ DEFAULT_USER_ID,
30
+ MemoryConfig,
31
+ MAX_MCP_PER_STEP
32
+ )
33
+
34
+ def _resolve_generate_gif(config_value, gif_enabled: bool):
35
+ """Gate GIF creation behind the ENABLE_GIF_CREATION operator flag (live-test F-GIF).
36
+
37
+ The flag (default false) is the master switch. When OFF, suppress GIF creation
38
+ entirely — a headless/autonomous server has no UI consumer for them. When ON,
39
+ honor the config value (a bool, or a str output path).
40
+ """
41
+ return config_value if gif_enabled else False
42
+
43
+
44
+ def _bump_eager_skill_usage(matched_skills, user_id, logger) -> None:
45
+ """SK-F3: record a provenance load for every eager-injected skill.
46
+
47
+ With SKILL_PROGRESSIVE_DISCLOSURE off, skills are embedded full-body directly
48
+ into the system message and the agent never calls ``load_skill`` — the only
49
+ other place usage is bumped (``action_registration.py``'s ``load_skill``
50
+ closure). Without this, the curator (W5) sees zero loads for an authored
51
+ skill that IS actively guiding the agent every turn and archives it as
52
+ unused. Pure module-level function so it's unit-testable without
53
+ constructing a full ``Agent``. Fail-open — a metrics write must never break
54
+ skill injection.
55
+ """
56
+ if not user_id:
57
+ return
58
+ try:
59
+ from modules.skills.skill_usage import get_skill_usage_store
60
+ _usage_store = get_skill_usage_store()
61
+ for _s in matched_skills:
62
+ _usage_store.bump_load(_s.skill_id, user_id)
63
+ except Exception as _bump_err:
64
+ logger.debug(f"eager skill usage bump skipped (non-fatal): {_bump_err}")
65
+
66
+
67
+ # Import POLYROB exceptions
68
+ from core.exceptions import (
69
+ AgentError,
70
+ ValidationError as ROBValidationError,
71
+ LLMResponseError,
72
+ ToolError
73
+ )
74
+
75
+ # Import billing exception for fail-fast handling
76
+ from core.exceptions import InsufficientCreditsError
77
+
78
+ # Import centralized utilities to avoid repeated inline imports
79
+ from agents.task.utils_json import normalize_action_schema
80
+ # Model limits now come from modules.llm.model_registry
81
+
82
+ # Native message types
83
+ from modules.llm.messages import (
84
+ AIMessage,
85
+ BaseMessage,
86
+ HumanMessage,
87
+ SystemMessage,
88
+ ToolMessage,
89
+ MessageOrigin,
90
+ make_control_message,
91
+ )
92
+ from modules.llm.adapters import BaseChatModel
93
+ from core.exceptions import (
94
+ RateLimitError,
95
+ LLMError,
96
+ LLMRateLimitError,
97
+ LLMAuthenticationError,
98
+ LLMConnectionError,
99
+ LLMContextLengthError,
100
+ LLMResponseError,
101
+ LLMPermanentError,
102
+ LLMProviderExhaustedError
103
+ )
104
+ # PIL Image imported locally in save_screenshot() method where needed
105
+ from pydantic import BaseModel, ConfigDict, ValidationError
106
+
107
+ from tools.browser.views import BrowserStateHistory, BrowserState
108
+ from agents.task.agent.message_manager.service import MessageManager
109
+ from agents.task.agent.core.llm_runner import LLMRunnerMixin
110
+ from agents.task.agent.core.memory_writer import MemoryWriterMixin
111
+ from agents.task.agent.core.step import StepMixin
112
+ from agents.task.agent.core.history_io import HistoryIOMixin
113
+ from agents.task.agent.core.logging_io import LoggingIOMixin
114
+ from agents.task.agent.core.safety_lifecycle import SafetyLifecycleMixin
115
+ from agents.task.agent.core.user_ingress import UserIngressMixin
116
+ from agents.task.agent.core.llm_provisioning import LLMProvisioningMixin
117
+ from agents.task.agent.core.model_introspection import ModelIntrospectionMixin
118
+ from agents.task.agent.core.loop_detection import LoopDetectionMixin
119
+ from agents.task.agent.core.resources import ResourceMixin
120
+ from agents.task.agent.core.session_metadata import SessionMetadataMixin
121
+ from agents.task.agent.core.error_recovery import ErrorRecoveryMixin
122
+ from agents.task.agent.core.step_telemetry import StepTelemetryMixin
123
+ from agents.task.agent.core.output_validation import OutputValidationMixin
124
+ from agents.task.agent.core.result_processing import ResultProcessingMixin
125
+ from agents.task.agent.core.next_action_internal import NextActionInternalMixin
126
+ from agents.task.agent.core.step_execution import StepExecutionMixin
127
+ from agents.task.agent.core.run_loop import RunLoopMixin
128
+ from agents.task.agent.prompts import SystemPrompt, AgentMessagePrompt
129
+ from agents.task.agent.views import (
130
+ ActionModel,
131
+ AgentBrain,
132
+ AgentError,
133
+ AgentHistory,
134
+ AgentHistoryList,
135
+ AgentOutput,
136
+ AgentStepInfo,
137
+ ActionResult,
138
+ )
139
+ from tools.browser.context import BrowserContext
140
+ from tools.dom.views import DOMElementNode, SelectorMap
141
+ from agents.task.telemetry.views import (
142
+ HumanApprovalRequestedEvent,
143
+ HumanApprovalDecisionEvent,
144
+ TodoStatusEvent,
145
+ AgentRunTelemetryEvent,
146
+ AgentEndTelemetryEvent,
147
+ ProviderFailureEvent,
148
+ ProviderFallbackSuccessEvent,
149
+ )
150
+ # ProductTelemetry no longer directly used - accessed via TelemetryManager
151
+ from agents.task.utils import time_execution_async, detect_llm_provider, extract_token_usage
152
+ # Safely import Google API exceptions
153
+ try:
154
+ from google.api_core.exceptions import ResourceExhausted
155
+ except ImportError:
156
+ # Create a dummy exception class if google-api-core is not installed
157
+ class ResourceExhausted(Exception):
158
+ pass
159
+
160
+ # Import from our own logging config
161
+ from agents.task.logging_config import get_task_logger
162
+
163
+ # Import centralized path management
164
+ from agents.task.path import pm
165
+
166
+ load_dotenv()
167
+
168
+ # Generic logger for the module itself (not instances)
169
+
170
+
171
+ def resolve_profile_overrides(updated_config, *, tool_calling_method,
172
+ max_actions_per_step, max_input_tokens, max_failures):
173
+ """Resolve profile overrides onto the four agent scalars they may set.
174
+
175
+ Replaces the old ``for key in [...]: locals()[key] = updated_config[key]`` loop,
176
+ which was a Python no-op (assigning to ``locals()`` does not rebind locals), so
177
+ these profile overrides were silently dropped. A present key in ``updated_config``
178
+ wins; otherwise the current value is kept. Returns the four values in fixed order.
179
+ """
180
+ return (
181
+ updated_config.get("tool_calling_method", tool_calling_method),
182
+ updated_config.get("max_actions_per_step", max_actions_per_step),
183
+ updated_config.get("max_input_tokens", max_input_tokens),
184
+ updated_config.get("max_failures", max_failures),
185
+ )
186
+
187
+
188
+ class AgentConstructionMixin:
189
+ """Agent.__init__ (the ~660L constructor) split whole out of the Agent class so
190
+ service.py drops under 700L (P9). Agent inherits __init__ from this mixin; from_params
191
+ (in service.py) still builds AgentConfig/AgentDeps and calls cls(config, deps). Imports
192
+ above are service.py's full set so every name the constructor references resolves."""
193
+
194
+ def __init__(self, config: AgentConfig, deps: AgentDeps):
195
+ """Initialize Agent from a config + deps pair.
196
+
197
+ The original 31-param constructor body is preserved verbatim below; the
198
+ parameters are simply unpacked from config/deps into the same local names
199
+ the body already uses (PR9 item #2 — typed construction surface).
200
+ """
201
+ # --- unpack deps ---
202
+ llm = deps.llm
203
+ orchestrator = deps.orchestrator
204
+ page_extraction_llm = deps.page_extraction_llm
205
+ system_prompt_class = deps.system_prompt_class
206
+ register_new_step_callback = deps.register_new_step_callback
207
+ register_done_callback = deps.register_done_callback
208
+ injected_controller = deps.controller # UP-05: least-privilege child controller (or None)
209
+ # --- unpack config ---
210
+ task = config.task
211
+ use_vision = config.use_vision
212
+ save_conversation_path = config.save_conversation_path
213
+ save_conversation_path_encoding = config.save_conversation_path_encoding
214
+ max_failures = config.max_failures
215
+ retry_delay = config.retry_delay
216
+ max_input_tokens = config.max_input_tokens
217
+ validate_output = config.validate_output
218
+ # Operator gate: MemoryConfig.ENABLE_GIF_CREATION (env, default false) is the
219
+ # master switch. AgentConfig.generate_gif defaults True, so without this gate a
220
+ # headless server makes per-conversation GIFs nobody consumes (live-test waste).
221
+ generate_gif = _resolve_generate_gif(config.generate_gif, MemoryConfig.ENABLE_GIF_CREATION)
222
+ sensitive_data = config.sensitive_data
223
+ available_file_paths = config.available_file_paths
224
+ include_attributes = config.include_attributes
225
+ max_error_length = config.max_error_length
226
+ max_actions_per_step = config.max_actions_per_step
227
+ initial_actions = config.initial_actions
228
+ tool_calling_method = config.tool_calling_method
229
+ agent_name = config.agent_name
230
+ session_config = config.session_config
231
+ profile_id = config.profile_id
232
+ profile_overrides = config.profile_overrides
233
+ use_native_tools = config.use_native_tools
234
+ step_timeout_seconds = config.step_timeout_seconds
235
+ stall_timeout_seconds = config.stall_timeout_seconds
236
+ max_step_timeout = config.max_step_timeout
237
+ is_sub_agent = config.is_sub_agent
238
+ parent_session_id = config.parent_session_id
239
+ role = config.role
240
+ # INPUT VALIDATION: Validate critical configuration parameters
241
+ if not isinstance(max_failures, int) or max_failures < 0:
242
+ raise ROBValidationError(f"max_failures must be a non-negative integer, got {max_failures}")
243
+
244
+ if not isinstance(retry_delay, int) or retry_delay < 0:
245
+ raise ROBValidationError(f"retry_delay must be a non-negative integer, got {retry_delay}")
246
+
247
+ if max_input_tokens is not None and (not isinstance(max_input_tokens, int) or max_input_tokens <= 0):
248
+ raise ROBValidationError(f"max_input_tokens must be a positive integer or None, got {max_input_tokens}")
249
+
250
+ if not isinstance(max_actions_per_step, int) or max_actions_per_step <= 0:
251
+ raise ROBValidationError(f"max_actions_per_step must be a positive integer, got {max_actions_per_step}")
252
+
253
+ if not isinstance(max_error_length, int) or max_error_length <= 0:
254
+ raise ROBValidationError(f"max_error_length must be a positive integer, got {max_error_length}")
255
+
256
+ # Validate task is not empty
257
+ if not task or not isinstance(task, str):
258
+ raise ROBValidationError("task must be a non-empty string")
259
+
260
+ # Validate orchestrator is provided (single source of truth for session state)
261
+ if not orchestrator:
262
+ raise ROBValidationError("orchestrator is required - Agent cannot run standalone")
263
+
264
+ # Store orchestrator reference (single source of truth for session_id and user_id)
265
+ # DO NOT copy session_id or user_id - use properties instead
266
+ self.orchestrator = orchestrator
267
+
268
+ # Store the agent name
269
+ self.agent_name = agent_name
270
+
271
+ # Sub-agent isolation flags
272
+ # Sub-agents skip H-MEM and message persistence to prevent context pollution
273
+ self._is_sub_agent = is_sub_agent
274
+ self._parent_session_id = parent_session_id
275
+ # Delegation role: a "leaf" agent cannot call delegate_task (roadmap P1).
276
+ # Sub-agents are spawned as "leaf"; the main agent defaults to "orchestrator".
277
+ self._role = "leaf" if is_sub_agent else role
278
+
279
+ # Initialize Tool Call Tracker for robust ID management
280
+ # Note: Uses orchestrator.session_id directly
281
+ self.tool_call_tracker = ToolCallTracker(
282
+ session_id=self.orchestrator.session_id,
283
+ logger=None # Will use its own logger
284
+ )
285
+
286
+ # Stall detection interval (state.last_activity_time set later after state is created)
287
+ self._stall_check_interval = 60 # Check every minute
288
+
289
+ # Use orchestrator's infrastructure (single instances per session)
290
+ self.session_manager = self.orchestrator.session_manager
291
+ self.telemetry_manager = self.orchestrator.telemetry_manager
292
+
293
+ # Initialize logger early so it's available for profile and session config processing
294
+ from agents.task.logging_config import get_task_logger
295
+ self.logger = get_task_logger(self.agent_name, self.orchestrator.session_id)
296
+
297
+ # RESTORATION: Load saved tool call tracker state from previous session
298
+ # NOTE: Moved after logger initialization to fix unreachable code issue
299
+ if self.session_id and orchestrator:
300
+ try:
301
+ from agents.task.path import pm
302
+ tool_calls_file = pm().create_file_path(
303
+ session_id=self.session_id,
304
+ subdir_name="data",
305
+ filename="tool_calls.json",
306
+ user_id=orchestrator.user_id if hasattr(orchestrator, 'user_id') else None
307
+ )
308
+
309
+ if tool_calls_file.exists():
310
+ if self.tool_call_tracker.load_from_file(tool_calls_file):
311
+ self.logger.info("📂 Restored tool call tracker state from previous session")
312
+ except Exception as e:
313
+ self.logger.debug(f"No tool call tracker state to restore: {e}")
314
+
315
+ # Apply profile configuration if specified (after logger is initialized)
316
+ if profile_id:
317
+ from agents.task.agent.profile_manager import ProfileManager
318
+
319
+ # Load and apply profile configuration
320
+ updated_config = ProfileManager.load_and_apply(profile_id, locals(), profile_overrides)
321
+
322
+ # Apply system message if profile provided one
323
+ if '_profile_system_message' in updated_config:
324
+ self._profile_system_message = updated_config['_profile_system_message']
325
+
326
+ # Create LLM from profile config if provided and not already set
327
+ if '_profile_llm_config' in updated_config and not llm:
328
+ llm_config = updated_config['_profile_llm_config']
329
+ llm = self._create_llm_from_config(llm_config)
330
+ if llm:
331
+ self.logger.info(f"Created LLM from profile config: {llm_config.get('provider')}/{llm_config.get('model')}")
332
+ else:
333
+ self.logger.warning(f"Could not create LLM from config: {llm_config}")
334
+
335
+ # Apply other profile settings
336
+ if '_enabled_actions' in updated_config:
337
+ self._enabled_actions = updated_config['_enabled_actions']
338
+ if '_profile_max_steps' in updated_config:
339
+ self._profile_max_steps = updated_config['_profile_max_steps']
340
+
341
+ # Apply profile settings explicitly. The old `locals()[key] = ...` loop was
342
+ # a no-op (assigning to locals() does not rebind), silently dropping these
343
+ # overrides — see resolve_profile_overrides.
344
+ tool_calling_method, max_actions_per_step, max_input_tokens, max_failures = \
345
+ resolve_profile_overrides(
346
+ updated_config,
347
+ tool_calling_method=tool_calling_method,
348
+ max_actions_per_step=max_actions_per_step,
349
+ max_input_tokens=max_input_tokens,
350
+ max_failures=max_failures,
351
+ )
352
+
353
+ # Initialize tool call adapter for normalizing tool calls across providers
354
+ # This is initialized later after controller is set up
355
+
356
+ # Process session config if provided
357
+ self.session_config = None
358
+ if session_config:
359
+ try:
360
+ from agents.task.config import TaskSessionConfig
361
+
362
+ # Create config from dict
363
+ self.session_config = TaskSessionConfig.from_dict(session_config)
364
+
365
+ # Apply config values (override individual parameters)
366
+
367
+ # Apply limits - override parameters if session config provides them
368
+ if self.session_config.limits.max_actions_per_step is not None:
369
+ max_actions_per_step = self.session_config.limits.max_actions_per_step
370
+ if self.session_config.limits.max_input_tokens is not None:
371
+ max_input_tokens = self.session_config.limits.max_input_tokens
372
+ if self.session_config.limits.max_failures is not None:
373
+ max_failures = self.session_config.limits.max_failures
374
+
375
+ # CRITICAL FIX: Apply LLM config from session_config
376
+ # Session config should OVERRIDE the profile's LLM (session is more specific)
377
+ # This was previously missing, causing user's model selection to be ignored!
378
+ if self.session_config.llm:
379
+ # Check if session config specifies a non-default model
380
+ # Default is gpt-5/openai - if user specified something different, use it
381
+ is_non_default_model = (
382
+ self.session_config.llm.model != "gpt-5" or
383
+ self.session_config.llm.provider != "openai"
384
+ )
385
+
386
+ if is_non_default_model or not llm:
387
+ llm_config = {
388
+ 'model': self.session_config.llm.model,
389
+ 'provider': self.session_config.llm.provider,
390
+ 'temperature': self.session_config.llm.temperature,
391
+ 'use_vision': self.session_config.llm.use_vision
392
+ }
393
+ self.logger.info(f"Creating LLM from session config: {llm_config['provider']}/{llm_config['model']}")
394
+ new_llm = self._create_llm_from_config(llm_config)
395
+ if new_llm:
396
+ llm = new_llm # Override the profile LLM
397
+ self.logger.info(f"✅ Created LLM from session config: {llm_config['provider']}/{llm_config['model']}")
398
+ else:
399
+ self.logger.error(f"❌ Failed to create LLM from session config: {llm_config}, keeping existing LLM")
400
+
401
+ # Save config to session
402
+ self._save_session_config()
403
+
404
+ self.logger.info("Applied session config")
405
+
406
+ except Exception as e:
407
+ self.logger.warning(f"Failed to apply session config: {e}")
408
+ # Continue with individual parameters as fallback
409
+
410
+
411
+ # HRE removed - was experimental feature
412
+
413
+
414
+ # Optional memory profiling setup behind environment flag
415
+ if os.getenv("PROFILE_MEM") == "1":
416
+ try:
417
+ import tracemalloc
418
+ import atexit
419
+ import pprint
420
+
421
+ tracemalloc.start()
422
+ self.logger.info("Memory profiling enabled - tracemalloc started")
423
+
424
+ @atexit.register
425
+ def _dump_memory_stats():
426
+ try:
427
+ top = tracemalloc.take_snapshot().statistics("lineno")[:20]
428
+ self.logger.info("Top 20 memory allocations:")
429
+ pprint.pprint(top)
430
+ except Exception as e:
431
+ self.logger.warning(f"Error dumping memory stats: {e}")
432
+ except ImportError:
433
+ self.logger.warning("tracemalloc not available for memory profiling")
434
+ except Exception as e:
435
+ self.logger.warning(f"Failed to set up memory profiling: {e}")
436
+
437
+ # Logger already initialized earlier, just log that we're ready
438
+ # (This used to be where logger was initialized, but we moved it earlier
439
+ # to be available for session_config processing)
440
+
441
+ # Set the agent_id - combine agent_name and clean session_id
442
+ # Format: agent_name_session_id (e.g., agent_1234-5678)
443
+ self.agent_id = f"{self.agent_name}_{self.orchestrator.session_id}"
444
+
445
+ # Debug log to track session ID usage
446
+ self.logger.debug(f"Agent initialized with ID: {self.agent_id}")
447
+
448
+ # Initialize HITL Manager (after agent_id is set)
449
+ from agents.task.agent.hitl_manager import HITLManager
450
+ self.hitl_manager = HITLManager(
451
+ session_id=self.orchestrator.session_id,
452
+ agent_id=self.agent_id,
453
+ logger=self.logger,
454
+ telemetry_manager=self.telemetry_manager
455
+ )
456
+
457
+ # NOTE: model_name will be available via property after MessageManager is created
458
+ # DO NOT extract it here - MessageManager is the single source of truth
459
+
460
+ # NOTE: Agent registration is handled by SessionOrchestrator.create_agent()
461
+ # Agent only uses SessionManager for metadata updates via _update_session_metadata()
462
+ # This prevents duplicate registration and ensures single source of truth
463
+
464
+ # Basic configuration
465
+ self.sensitive_data = sensitive_data
466
+ self.task = task
467
+ self.use_vision = use_vision
468
+ self.llm = llm
469
+
470
+ # Set chat_model_library for provider detection (needed by set_tool_calling_method)
471
+ # This must be set BEFORE calling set_tool_calling_method() at line 427
472
+ self.chat_model_library = self.llm.__class__.__name__
473
+
474
+ # Store agent type for telemetry
475
+ self.agent_type = self.__class__.__name__
476
+
477
+ self.use_native_tools = use_native_tools
478
+ # Store timeout configuration with validation
479
+ self.step_timeout_seconds = step_timeout_seconds or 600
480
+ self.stall_timeout_seconds = stall_timeout_seconds or 600
481
+ self.max_step_timeout = max_step_timeout
482
+
483
+ # Ensure step timeout doesn't exceed max
484
+ if self.step_timeout_seconds > self.max_step_timeout:
485
+ self.logger.warning(f"step_timeout_seconds {self.step_timeout_seconds} exceeds max {self.max_step_timeout}, using max")
486
+ self.step_timeout_seconds = self.max_step_timeout
487
+
488
+ # NOTE: Approval state now managed by HITLManager
489
+
490
+ # Page extraction LLM setup
491
+ if not page_extraction_llm:
492
+ self.page_extraction_llm = llm
493
+ else:
494
+ self.page_extraction_llm = page_extraction_llm
495
+
496
+ # File paths
497
+ self.available_file_paths = available_file_paths
498
+
499
+ # Normalize save_conversation_path if provided
500
+ if save_conversation_path:
501
+ # Import centralized path manager
502
+ from agents.task.path import pm
503
+
504
+ # Use centralized path manager to create proper file path
505
+ self.save_conversation_path = str(pm().create_file_path(
506
+ self.orchestrator.session_id,
507
+ "logs",
508
+ os.path.basename(save_conversation_path) or "conversation",
509
+ user_id=self.orchestrator.user_id # Pass user_id to PM
510
+ ))
511
+ self.logger.info(f"Using normalized conversation path: {self.save_conversation_path}")
512
+ else:
513
+ self.save_conversation_path = None
514
+
515
+ self.save_conversation_path_encoding = save_conversation_path_encoding
516
+
517
+ # State tracking
518
+ self._last_result = None
519
+ self._cancelled = False # Cancellation flag for stopping execution
520
+ self._deferred_mcp_actions: List[Dict[str, Any]] = [] # MCP actions deferred from previous step
521
+ self._max_mcp_deferrals = 2 # Cap deferrals to prevent infinite loops
522
+ # Set default for include_attributes if not provided
523
+ if include_attributes is None:
524
+ include_attributes = [
525
+ 'title',
526
+ 'type',
527
+ 'name',
528
+ 'role',
529
+ 'tabindex',
530
+ 'aria-label',
531
+ 'placeholder',
532
+ 'value',
533
+ 'alt',
534
+ 'aria-expanded',
535
+ ]
536
+ self.include_attributes = include_attributes
537
+ self.max_error_length = max_error_length
538
+ self.max_input_tokens = max_input_tokens # Store max_input_tokens for MessageManager
539
+ self.generate_gif = generate_gif
540
+
541
+
542
+
543
+ # Controller: an injected (least-privilege child) controller takes precedence
544
+ # (UP-05); otherwise the shared orchestrator controller. All downstream
545
+ # derivations (ActionModel, action_descriptions, mcp_servers_info,
546
+ # message_manager) follow from self.controller, so this is the only seam.
547
+ self.controller = injected_controller or self.orchestrator.controller
548
+ _ctl_src = "injected (least-privilege)" if injected_controller else "orchestrator"
549
+ self.logger.debug(f"Using {_ctl_src} controller with {len(self.controller.list_tools())} tools")
550
+
551
+ self.max_actions_per_step = max_actions_per_step
552
+
553
+ # WS-A capability gate: while the session is tainted by untrusted correspondent
554
+ # DATA, deny high-impact tools (money/comms/code-exec/delegation). Registered
555
+ # fail-CLOSED, gated on the access model + a real orchestrator taint flag. The
556
+ # owner clears the taint by sending a genuine turn. No-op when the flag is off.
557
+ try:
558
+ from agents.task.surface_config import SurfaceConfig
559
+ if SurfaceConfig.correspondent_access_enabled() and self.controller is not None \
560
+ and hasattr(self.controller, "register_pre_tool_call_hook"):
561
+ from agents.task.agent.core.correspondent_gate import (
562
+ build_tool_resolver, make_correspondent_gate_hook)
563
+ _orch = self.orchestrator
564
+ # Resolve each action's owning tool_id so the tool-id-level denylist
565
+ # actually fires — the pre-hook only ever sees the bare action name
566
+ # (run_code, goal_create, x402_fetch, dynamic MCP {server}_{tool}).
567
+ _gate = make_correspondent_gate_hook(
568
+ lambda: bool(getattr(_orch, "_correspondent_tainted", False)),
569
+ resolve_tool=build_tool_resolver(self.controller))
570
+ self.controller.register_pre_tool_call_hook(_gate, fail_mode="closed")
571
+ except Exception as e:
572
+ self.logger.debug(f"correspondent capability gate not registered: {e}")
573
+
574
+ # ToolCallAdapter removed - functionality integrated into MessageManager and Registry
575
+ # (tool calls now flow through ToolCallBuilder + Registry).
576
+
577
+ # Browser context obtained on-demand via property (see get_browser_context())
578
+
579
+ self.system_prompt_class = system_prompt_class
580
+
581
+ # Agent gets pre-configured ActionModel from Controller
582
+ self.ActionModel = self.controller.create_action_model()
583
+ self.AgentOutput = AgentOutput.type_with_custom_actions(self.ActionModel)
584
+
585
+ self._set_version_and_source()
586
+
587
+ # Token limits are now calculated by MessageManager (SINGLE SOURCE OF TRUTH)
588
+ # Just pass max_input_tokens override if provided, otherwise MessageManager auto-calculates
589
+
590
+ self.tool_calling_method = self.set_tool_calling_method(tool_calling_method)
591
+
592
+ # NOTE: Provider detection will happen after MessageManager is created
593
+ # For now, detect provider just to check native tools support
594
+ # This is a temporary detection - model_name property will delegate to MessageManager
595
+ _temp_model_name = self._extract_model_name(llm)
596
+ provider = detect_llm_provider(None, _temp_model_name)
597
+
598
+ # Preserve user intent - intersect user preference with provider capability.
599
+ # This also re-assigns self.use_native_tools so the agent's own flag matches
600
+ # what MessageManager is given (see _reconcile_native_tools).
601
+ use_native_tools = self._reconcile_native_tools(provider)
602
+
603
+ # Cache action descriptions for reuse (single call to controller).
604
+ # T1-03: in native mode the full parameter schemas already ship to the
605
+ # provider in the `tools` param, so the prompt gets a compact one-line
606
+ # index instead of the raw schema dump (2.5-6k redundant tokens/session).
607
+ # The JSON-fallback path keeps the full dump — there it IS the schema.
608
+ if use_native_tools:
609
+ self.action_descriptions = self.controller.get_prompt_action_index()
610
+ else:
611
+ self.action_descriptions = self.controller.get_prompt_description()
612
+
613
+ # Get MCP server info for dynamic prompt generation (no hardcoded examples)
614
+ self.mcp_servers_info = self.controller.get_mcp_servers_info()
615
+
616
+ # Hierarchical Memory System - MANDATORY for main agents, SKIPPED for sub-agents
617
+ # Sub-agents don't need H-MEM - they do focused tasks and return results
618
+ self.task_context_manager = None
619
+ self._created_task_context_manager = False
620
+
621
+ if self._is_sub_agent:
622
+ # SUB-AGENT ISOLATION: Use NullTaskContextManager to prevent context pollution
623
+ # FIX (Jan 2026): Use NullObject pattern instead of None to prevent AttributeError
624
+ # Sub-agents extract their own output which is returned to parent
625
+ from modules.memory.task.null_context_manager import NullTaskContextManager
626
+ self.task_context_manager = NullTaskContextManager()
627
+ self.logger.info(f"🔒 Sub-agent mode: Using NullTaskContextManager (parent: {self._parent_session_id[:20] if self._parent_session_id else 'unknown'}...)")
628
+ else:
629
+ # Main agent: Initialize H-MEM as usual
630
+ # Try to get from container first
631
+ try:
632
+ memory_manager = self.orchestrator.container.get_service('memory_manager')
633
+ if memory_manager and hasattr(memory_manager, 'task_context_manager'):
634
+ self.task_context_manager = memory_manager.task_context_manager
635
+ self.logger.info("✅ Using TaskContextManager from container")
636
+ except Exception as e:
637
+ self.logger.debug(f"Could not get TaskContextManager from container: {e}")
638
+
639
+ # If not available, create dedicated instance for this session
640
+ if not self.task_context_manager:
641
+ self.logger.warning(
642
+ "No memory_manager in container — creating a dedicated "
643
+ "TaskContextManager for this session (cross-agent memory will NOT "
644
+ "be shared)"
645
+ )
646
+ from modules.memory.task.task_context_manager import TaskContextManager
647
+ from core.config import BotConfig
648
+
649
+ # Create instance
650
+ config = self.orchestrator.container.config if self.orchestrator.container else BotConfig()
651
+ self.task_context_manager = TaskContextManager(
652
+ name=f"task_context_{self.session_id}",
653
+ config=config
654
+ )
655
+ self._created_task_context_manager = True
656
+
657
+ # Opt-in external cross-session memory backend (MEMORY_BACKEND=sqlite|local_vector).
658
+ # Registered once via the existing one-external-provider registry; the live
659
+ # prefetch/sync_turn wiring (step.py + _save_step_to_memory) already routes through
660
+ # registry.active(). For local_vector we hand it the container's already-loaded local
661
+ # embedding model (no second copy); it degrades to FTS5-only if absent.
662
+ # NOTE: data_dir lives on the container's BotConfig — Agent has no self.config.
663
+ try:
664
+ from modules.memory.backend_factory import maybe_register_memory_backend
665
+ _container = getattr(getattr(self, "orchestrator", None), "container", None)
666
+ _cfg = getattr(_container, "config", None)
667
+ data_dir = getattr(_cfg, "data_dir", "data") or "data"
668
+ _embedder = None
669
+ if _container is not None and getattr(_container, "has_service", None):
670
+ try:
671
+ if _container.has_service("embedding_model"):
672
+ _embedder = _container.get_service("embedding_model")
673
+ except Exception:
674
+ _embedder = None
675
+ _provider = maybe_register_memory_backend(data_dir=data_dir, embedding_model=_embedder)
676
+ # The Controller's session_search/memory_search registration gate ran at
677
+ # Controller.__init__ — BEFORE this backend registration — so on the FIRST
678
+ # session of a process (e.g. `polyrob run`) it saw no external provider and
679
+ # skipped the recall tools (W6). Re-run the gated registration now that the
680
+ # backend exists. Idempotent: guarded on not-already-registered so a later
681
+ # session (backend already up at Controller init) doesn't double-register.
682
+ if (_provider is not None and getattr(_provider, "is_external", False)
683
+ and self.controller is not None):
684
+ try:
685
+ _names = self.controller.registry.list_action_names()
686
+ if "session_search" not in _names:
687
+ self.controller._register_session_search_action()
688
+ # ME-D1: same first-session gap for the other provider-gated tools —
689
+ # their gates ran at Controller init, before the backend existed.
690
+ if "recent_activity" not in _names:
691
+ self.controller._register_recent_activity_action()
692
+ if "memory" not in _names:
693
+ self.controller._register_memory_tool_action()
694
+ except Exception as e:
695
+ self.logger.debug(f"memory tool re-registration skipped: {e}")
696
+ except Exception as e:
697
+ self.logger.debug(f"memory backend registration skipped: {e}")
698
+
699
+ # P2-24: recompute the prompt action index AFTER the memory-backend
700
+ # registration above (registers session_search/recent_activity/memory on the
701
+ # FIRST session of a process). It was computed earlier, before those tools
702
+ # existed, so the first session's system prompt omitted them while the per-
703
+ # step schemas included them. action_descriptions is not consumed until the
704
+ # MessageManager build below, so recomputing here is safe. Native only.
705
+ try:
706
+ if use_native_tools and self.controller is not None:
707
+ self.action_descriptions = self.controller.get_prompt_action_index()
708
+ except Exception as _aidx_err:
709
+ self.logger.debug(f"action-index recompute skipped: {_aidx_err}")
710
+
711
+ # Load skills for this session and embed into system message
712
+ # Skills are per-session prompt extensions that provide context-aware guidance
713
+ system_message = getattr(self, '_profile_system_message', None)
714
+ skill_content = None
715
+
716
+ try:
717
+ from agents.task.agent.skill_manager import get_skill_manager
718
+ skill_manager = get_skill_manager()
719
+
720
+ # Get tool_ids from controller (what tools are loaded for this session)
721
+ tool_ids = self.controller.list_tools() if self.controller else []
722
+
723
+ # Get available actions for more precise matching
724
+ available_actions = []
725
+ if self.controller and hasattr(self.controller, 'registry'):
726
+ available_actions = self.controller.registry.list_action_names()
727
+
728
+ # Match skills based on session context (include user's custom skills)
729
+ user_id = self.orchestrator.user_id if hasattr(self.orchestrator, 'user_id') else None
730
+ from agents.task.templates import seeded_skills_for
731
+ _seeded = seeded_skills_for(os.environ.get("POLYROB_PERSONA"))
732
+ matched_skills = skill_manager.get_skills_for_session(
733
+ tool_ids=tool_ids,
734
+ task=self.task,
735
+ available_actions=available_actions,
736
+ user_id=user_id,
737
+ seeded_skill_ids=_seeded or None,
738
+ )
739
+
740
+ # S9 fix (default OFF -> byte-identical): with progressive disclosure, optionally
741
+ # expose ALL available skills in the catalog so the agent can discover + load_skill
742
+ # any of them (not just trigger-matched ones). Matched skills stay first (priority).
743
+ from agents.task.constants import (
744
+ skill_progressive_disclosure as _prog_disc,
745
+ skill_catalog_include_all as _catalog_include_all,
746
+ )
747
+ # Access-time read (not the import-bound constant) so the local-mode
748
+ # default (ON under POLYROB_LOCAL) is honored for the terminal agent.
749
+ if _prog_disc() and _catalog_include_all():
750
+ try:
751
+ _seen = {s.skill_id for s in matched_skills}
752
+ # SK-F1: the builtin library is 23 rules and every authored skill
753
+ # defaults to priority 6 (the old cap-20 cut band) — 50 is comfortably
754
+ # above both so the catalog actually reflects the full library.
755
+ _extra = [s for s in skill_manager.get_catalog_skills(user_id=user_id, max_skills=50, tool_ids=tool_ids)
756
+ if s.skill_id not in _seen]
757
+ matched_skills = list(matched_skills) + _extra
758
+ except Exception as _cat_err:
759
+ self.logger.debug(f"catalog-include-all skipped (non-fatal): {_cat_err}")
760
+
761
+ if matched_skills:
762
+ # S-1: progressive disclosure. When enabled, inject only a compact
763
+ # <skill-catalog> and let the agent pull full bodies via load_skill;
764
+ # otherwise (default) embed full bodies as today. Either way the
765
+ # matched skills (with pre-loaded bodies) are handed to the controller
766
+ # so load_skill can serve them without re-reading disk.
767
+ from agents.task.constants import skill_progressive_disclosure
768
+ if skill_progressive_disclosure():
769
+ skill_content = skill_manager.format_skill_catalog(matched_skills)
770
+ if self.controller is not None:
771
+ self.controller._session_skills = {s.skill_id: s for s in matched_skills}
772
+ self.logger.info(
773
+ f"✨ Skill catalog ({len(matched_skills)}) for session "
774
+ f"[progressive disclosure]: {[s.skill_id for s in matched_skills]}"
775
+ )
776
+ else:
777
+ skill_content = skill_manager.format_skills_for_prompt(matched_skills)
778
+ self.logger.info(f"✨ Loaded {len(matched_skills)} skills for session: {[s.skill_id for s in matched_skills]}")
779
+ # SK-F3: with progressive disclosure OFF, skills are eager-injected
780
+ # full-body (no load_skill call), so usage was never recorded and the
781
+ # curator archived authored skills as "never used." Bump provenance
782
+ # here too, at the only choke point this path has.
783
+ _bump_eager_skill_usage(matched_skills, user_id, self.logger)
784
+
785
+ # Save skills info to session directory for webview
786
+ try:
787
+ from agents.task.path import pm
788
+ import json
789
+ # P2-25: precompute user-authored skill ids via the REAL method
790
+ # (_load_user_rules returns (rules, dir); the old hasattr referenced
791
+ # a nonexistent _load_user_skill_rules, so is_user_skill was always
792
+ # the startswith/endswith heuristic).
793
+ _user_skill_ids = set()
794
+ try:
795
+ if user_id and hasattr(skill_manager, '_load_user_rules'):
796
+ _ur, _ = skill_manager._load_user_rules(user_id)
797
+ _user_skill_ids = set((_ur or {}).keys())
798
+ except Exception:
799
+ _user_skill_ids = set()
800
+ skills_data = [
801
+ {
802
+ "id": s.skill_id,
803
+ "name": s.skill_id.replace("-", " ").title(),
804
+ "priority": s.priority,
805
+ "trigger_type": s.trigger_type,
806
+ "is_user_skill": s.skill_id.startswith("forked-") or s.skill_id.endswith("-custom") or s.skill_id in _user_skill_ids
807
+ }
808
+ for s in matched_skills
809
+ ]
810
+ skills_path = pm().create_file_path(
811
+ session_id=self.orchestrator.session_id,
812
+ subdir_name="",
813
+ filename="skills.json",
814
+ user_id=user_id
815
+ )
816
+ with open(skills_path, 'w') as f:
817
+ json.dump(skills_data, f, indent=2)
818
+ self.logger.debug(f"Saved skills info to {skills_path}")
819
+ except Exception as save_err:
820
+ self.logger.debug(f"Could not save skills info (non-fatal): {save_err}")
821
+ except Exception as e:
822
+ self.logger.debug(f"Could not load skills (non-fatal): {e}")
823
+
824
+ # PR13: never embed skills into the system message - keep it stable/cacheable.
825
+ # Skills are injected as a pinned user message via set_skill_message() below.
826
+ # Here we just strip the placeholder from any profile system message.
827
+ if system_message:
828
+ content = system_message.content.replace("{SKILLS_PLACEHOLDER}", "")
829
+ system_message = SystemMessage(content=content)
830
+
831
+ # Store skill content for embedding later if SystemPrompt generates the message
832
+ self._skill_content = skill_content
833
+
834
+ # Initialize message manager with proper session ID and optional profile system message
835
+ # MessageManager becomes the SINGLE SOURCE OF TRUTH for model_name and provider_name
836
+ self.message_manager = MessageManager(
837
+ llm=self.llm,
838
+ task=self.task,
839
+ # Always provide action descriptions for context, even with native tools
840
+ # This helps the LLM understand what actions are available
841
+ action_descriptions=self.action_descriptions,
842
+ system_prompt_class=self.system_prompt_class,
843
+ max_input_tokens=self.max_input_tokens,
844
+ include_attributes=self.include_attributes,
845
+ max_error_length=self.max_error_length,
846
+ max_actions_per_step=self.max_actions_per_step,
847
+ sensitive_data=self.sensitive_data,
848
+ session_id=self.orchestrator.session_id, # Pass clean session_id from orchestrator
849
+ system_message=system_message, # Pass profile system message with skills embedded
850
+ use_native_tools=use_native_tools, # Pass native tools flag
851
+ tool_call_tracker=self.tool_call_tracker, # Pass tool call tracker for ID management
852
+ task_context_manager=self.task_context_manager, # Pass hierarchical memory system
853
+ mcp_servers=self.mcp_servers_info, # Pass MCP server info for dynamic prompts
854
+ persona_block=getattr(config, 'persona_block', None), # S1: chat-mode persona
855
+ # Session's loaded tools → config-aware prompt gating (e.g. <anysite> and
856
+ # <browser-tools> only render when the tool is actually loaded this
857
+ # session, not on a global flag).
858
+ tool_ids=self.controller.list_tools() if self.controller else [],
859
+ # T1-06: the vision prompt section follows the session's real use_vision
860
+ # instead of claiming image abilities for use_vision=False sessions.
861
+ include_vision=bool(self.use_vision),
862
+ )
863
+
864
+ # Agent-level provider label SSOT. The step-loop billing path reads
865
+ # self.llm_provider (next_action_internal.py) and the B3 per-request swap
866
+ # guard reads it for idempotence; without this it stayed unset on a
867
+ # never-swapped agent (billing recorded 'unknown', and swap_model was the
868
+ # only writer). Seed it from the MessageManager's detected provider so it
869
+ # is consistent with the SSOT the model_name/provider_name properties mirror.
870
+ self.llm_provider = self.message_manager.provider_name
871
+
872
+ # PR13: strip any skills placeholder from the generated system message; skills
873
+ # are NOT embedded in the system prompt anymore (kept stable for prompt caching).
874
+ if not getattr(self, '_profile_system_message', None):
875
+ current_content = self.message_manager._system_message.content
876
+ if "{SKILLS_PLACEHOLDER}" in current_content:
877
+ new_content = current_content.replace("{SKILLS_PLACEHOLDER}", "")
878
+ self.message_manager._system_message = SystemMessage(content=new_content)
879
+ self.message_manager._system_message_tokens = self.message_manager._count_message_tokens(
880
+ self.message_manager._system_message
881
+ )
882
+
883
+ # PR13: pin skills as a foundation user message (origin=SKILL), not the system
884
+ # prompt. get_messages_for_llm() injects this; the system prompt stays stable.
885
+ self.message_manager.set_skill_message(skill_content)
886
+
887
+ # polyrob Phase C: pin operator-authored SOUL/IDENTITY self-context as a frozen
888
+ # foundation message, read ONCE at session start from <data_dir>/identity/. This
889
+ # is operator-write-only (the agent never authors it in this cut). Empty/absent
890
+ # docs => set_self_context_message(None) => inert / byte-identical. Like skills,
891
+ # it lives in the foundation, NOT the system prompt, so the prompt stays cacheable.
892
+ try:
893
+ from core.instance import (load_self_context, load_self_doc,
894
+ load_owner_doc, resolve_instance_id)
895
+ _container = getattr(getattr(self, "orchestrator", None), "container", None)
896
+ _cfg = getattr(_container, "config", None)
897
+ _data_dir = getattr(_cfg, "data_dir", "data") or "data"
898
+ # SOUL tier (operator-only, instance-global) + the evolving SELF tier
899
+ # (agent-writable, per-(instance,user)). Both frozen at session start.
900
+ # load_self_doc applies the load-side [BLOCKED] guard; empty => omitted.
901
+ _soul = load_self_context(_data_dir)
902
+ _uid = self.orchestrator.user_id if hasattr(self.orchestrator, "user_id") else None
903
+ _self_doc = load_self_doc(_data_dir, _uid, resolve_instance_id())
904
+ # Bounded owner-facts doc (agent-maintained, per-(instance,user)): durable
905
+ # facts/preferences about the OWNER, injected after SOUL and before the
906
+ # evolving SELF doc. Load-side [BLOCKED] guard applies; empty => omitted.
907
+ _owner_doc = load_owner_doc(_data_dir, _uid, resolve_instance_id())
908
+ if _owner_doc:
909
+ _owner_doc = "## Owner facts\n\n" + _owner_doc
910
+ # WS-A + T1-13: the owner clause ("You act on behalf of OWNER X") renders
911
+ # whenever a DISTINCT owner principal resolves; the correspondent-DATA frame
912
+ # sentence stays gated on the three-tier access model being on. With no
913
+ # distinct owner and the model off this is "" — byte-identical legacy.
914
+ _awareness = ""
915
+ try:
916
+ from core.instance import owner_awareness_line
917
+ _corr_on = False
918
+ try:
919
+ from agents.task.surface_config import SurfaceConfig
920
+ _corr_on = SurfaceConfig.correspondent_access_enabled()
921
+ except Exception:
922
+ _corr_on = False
923
+ _awareness = owner_awareness_line(include_correspondent_frame=_corr_on)
924
+ except Exception:
925
+ _awareness = ""
926
+ _combined = "\n\n".join(p for p in (_awareness, _soul, _owner_doc, _self_doc) if p)
927
+ self.message_manager.set_self_context_message(_combined)
928
+ except Exception as e:
929
+ self.logger.debug(f"Could not load self-context (non-fatal): {e}")
930
+
931
+ # C9 + Phase 2: auto-load project context (polyrob.md/AGENTS.md/CLAUDE.md/
932
+ # .cursorrules) as a frozen PROJECT_CONTEXT foundation message.
933
+ # - Local CLI single-owner: loaded TRUSTED (read as steering), gated on
934
+ # project_context_autoload() (default ON under POLYROB_LOCAL).
935
+ # - Server: loaded ONLY when project_context_server_mode() is opted in, and
936
+ # then injected UNTRUSTED-WRAPPED (framed as DATA, not instructions) since
937
+ # the file may come from a repo the operator merely opened.
938
+ # Server stays byte-identical by default (both gates OFF). Fully fail-open —
939
+ # any error keeps _project_context_message=None.
940
+ try:
941
+ from agents.task.constants import AutonomyConfig, local_mode_enabled
942
+ from agents.task.agent.core.project_context import build_project_context_message
943
+ _local = local_mode_enabled()
944
+ # Server tier searches the tenant's session workspace, NEVER the process
945
+ # CWD (the install dir). Resolve it via the path manager; on any failure
946
+ # leave it None so server-mode loads nothing rather than the install dir.
947
+ _workspace_dir = None
948
+ if not _local:
949
+ try:
950
+ from agents.task.path import pm
951
+ _workspace_dir = str(pm().get_workspace_dir(
952
+ self.orchestrator.session_id,
953
+ getattr(self.orchestrator, "user_id", None),
954
+ ))
955
+ except Exception:
956
+ _workspace_dir = None
957
+ _proj_ctx = build_project_context_message(
958
+ local=_local,
959
+ autoload=AutonomyConfig.project_context_autoload(),
960
+ server_mode=AutonomyConfig.project_context_server_mode(),
961
+ cwd=os.getcwd(),
962
+ workspace_dir=_workspace_dir,
963
+ cap_tokens=AutonomyConfig.project_context_max_tokens(),
964
+ )
965
+ self.message_manager.set_project_context_message(_proj_ctx)
966
+ except Exception as e:
967
+ self.logger.debug(f"Could not load project context (non-fatal): {e}")
968
+
969
+ # Model-identity SSOT: pin the model/provider the agent actually runs on, so
970
+ # it can answer "what model are you" from an authoritative foundation line
971
+ # instead of reading env/config files (which leaked secrets). Sourced from the
972
+ # MessageManager model_name SSOT + the detected provider. Fail-open/inert.
973
+ try:
974
+ _ident_model = getattr(self.message_manager, "model_name", None) or getattr(self, "model_name", None)
975
+ _ident_provider = (
976
+ getattr(self, "llm_provider", None)
977
+ or getattr(self.message_manager, "provider_name", None)
978
+ )
979
+ self.message_manager.set_runtime_identity(_ident_model, _ident_provider)
980
+ except Exception as e:
981
+ self.logger.debug(f"Could not pin runtime identity (non-fatal): {e}")
982
+
983
+ # A5: optional cheap auxiliary model for compaction only. Inert unless
984
+ # COMPACTION_MODEL is set; on any failure aux_llm stays None and
985
+ # llm_compact_history transparently uses the main model (current behaviour).
986
+ try:
987
+ self.message_manager.aux_llm = self._provision_compaction_llm()
988
+ except Exception as e:
989
+ self.logger.debug(f"compaction aux model provisioning skipped: {e}")
990
+ self.message_manager.aux_llm = None
991
+
992
+ # Reflection LLM: its own aux slot (B5), which inherits compaction's
993
+ # model+provider by default when unset (back-compat: reflection historically
994
+ # reused _provision_compaction_llm wholesale) — see
995
+ # constants.resolve_aux_chain(). Operators can now set AUX_MODEL_REFLECTION /
996
+ # AUX_FALLBACK_REFLECTION independently. UP-09: default ON via
997
+ # constants.reflection_llm_enabled_default() — the SAME helper the
998
+ # TaskContextManager runtime guard reads, so the two can never diverge
999
+ # (historical bug: TCM read BotConfig.get -> always False -> never fired).
1000
+ from agents.task.constants import reflection_llm_enabled_default
1001
+ if reflection_llm_enabled_default():
1002
+ try:
1003
+ self.task_context_manager.reflection_llm = self._provision_aux_llm("reflection")
1004
+ except Exception as e:
1005
+ self.logger.debug(f"reflection LLM provisioning skipped: {e}")
1006
+
1007
+ # CO-F1: the optional cheap aux 'judge' model for output validation
1008
+ # (_validate_output) is now provisioned LAZILY — only on the first actual
1009
+ # _validate_output() call, and only when validate_output is on (mirrors the
1010
+ # BackgroundReviewMixin pattern: aux models are resolved at the point of use,
1011
+ # not unconditionally at construction). Provisioning it here unconditionally
1012
+ # wasted an aux-model resolution on every session even while judge-backed
1013
+ # validation was entirely dead (validate_output defaulted False and nothing
1014
+ # ever flipped it, and the run loop never validated the true final answer).
1015
+ # See OutputValidationMixin._validate_output for the lazy provision site.
1016
+ self._judge_llm = None
1017
+
1018
+ # Log model name from MessageManager (single source of truth)
1019
+ self.logger.debug(f"Using model: {self.message_manager.model_name}")
1020
+
1021
+ if self.available_file_paths:
1022
+ self.message_manager.add_file_paths(self.available_file_paths)
1023
+ # Step callback
1024
+ self.register_new_step_callback = register_new_step_callback
1025
+ self.register_done_callback = register_done_callback
1026
+
1027
+ # Centralized agent state - MUST BE INITIALIZED EARLY
1028
+ from agents.task.agent.agent_state import AgentState
1029
+ self.state = AgentState(
1030
+ n_steps=1,
1031
+ max_steps=None, # Set later if profile provides it
1032
+ max_failures=max_failures,
1033
+ consecutive_failures=0,
1034
+ total_actions_count=0,
1035
+ paused=False,
1036
+ stopped=False
1037
+ )
1038
+
1039
+ # Update activity time now that state exists (used for stall detection)
1040
+ self.state.last_activity_time = time.time()
1041
+ self.state.last_step_start_time = time.time()
1042
+
1043
+ # RESTORATION: Try to load saved agent state from previous session
1044
+ if self.session_id and orchestrator:
1045
+ try:
1046
+ from agents.task.path import pm
1047
+ state_file = pm().create_file_path(
1048
+ session_id=self.session_id,
1049
+ subdir_name="data",
1050
+ filename="agent_state.json",
1051
+ user_id=orchestrator.user_id if hasattr(orchestrator, 'user_id') else None
1052
+ )
1053
+
1054
+ loaded_state = AgentState.load_from_file(state_file)
1055
+ if loaded_state:
1056
+ self.state = loaded_state
1057
+ self.logger.info(f"📂 Restored agent state from previous session: step {self.state.n_steps}")
1058
+
1059
+ # Update max_failures if current config provides new value
1060
+ if max_failures and max_failures != self.state.max_failures:
1061
+ self.state.max_failures = max_failures
1062
+ except Exception as e:
1063
+ self.logger.debug(f"No agent state to restore (new session or error): {e}")
1064
+
1065
+ # Initialize workspace context for file upload awareness
1066
+ from agents.task.workspace_context import get_workspace_context
1067
+ self.workspace_context = get_workspace_context()
1068
+
1069
+ # Tracking variables
1070
+ self.history: AgentHistoryList = AgentHistoryList(history=[])
1071
+
1072
+ self.max_failures = max_failures
1073
+ self.retry_delay = retry_delay
1074
+ self.validate_output = validate_output
1075
+ self.initial_actions = self._convert_initial_actions(initial_actions) if initial_actions else None
1076
+ if save_conversation_path:
1077
+ self.logger.info(f'Saving conversation to {save_conversation_path}')
1078
+
1079
+ # Stall detection tracking
1080
+ self._last_action_count = 0
1081
+ self._stall_check_interval = 30.0 # Check every 30 seconds
1082
+ self._stall_check_task = None
1083
+ self._llm_call_in_progress = False # Track when waiting for LLM response
1084
+ self._llm_call_start_time = None # When the current LLM call started
1085
+
1086
+ # Log registered commands
1087
+ # Use Controller's high-level API instead of directly accessing registry
1088
+ registered_commands = self.controller.get_action_names()
1089
+ self.logger.debug(f"Registered commands: {len(registered_commands)} commands - {', '.join(registered_commands)}")
1090
+
1091
+ # Create a session data dictionary for later use
1092
+ self.session_data = {
1093
+ "agent_id": self.agent_id,
1094
+ "task": self.task,
1095
+ "model_name": self.model_name,
1096
+ "created_at": datetime.now().isoformat(),
1097
+ "steps": [],
1098
+ "llm_requests": []
1099
+ }
1100
+
1101
+ # Native tools debug flag
1102
+ self._native_tools_debug = os.environ.get('NATIVE_TOOLS_DEBUG', '').lower() == 'true'
1103
+ if self._native_tools_debug:
1104
+ self.logger.setLevel(logging.DEBUG)
1105
+ self.logger.info("[NATIVE_TOOLS] Debug mode enabled")
1106
+
1107
+ # Loop detection variables with improved thresholds
1108
+ self._previous_actions = deque(maxlen=LoopDetectionConfig.MEMORY_WINDOW_SIZE) # Use consistent window size
1109
+ self._action_repetition_counter = 0
1110
+ self._max_allowed_repetitions = LoopDetectionConfig.MAX_ALLOWED_REPETITIONS
1111
+ self._last_browser_states = deque(maxlen=3) # MODIFIED: Use bounded deque instead of list
1112
+ self._unchanged_state_count = 0
1113
+ self._state_change_threshold = LoopDetectionConfig.STATE_CHANGE_THRESHOLD
1114
+
1115
+ # No need for these locks that can cause deadlocks
1116
+
1117
+ # FIXED: Initialize memory management for bounded collections
1118
+ self._initialize_memory_management()
1119
+
1120
+ # Get usage tracker if available (UNIFIED tracking - replaces usage_meter + telemetry)
1121
+ # This is the preferred method for token tracking
1122
+ if self.orchestrator and hasattr(self.orchestrator, 'usage_tracker'):
1123
+ self.usage_tracker = self.orchestrator.usage_tracker
1124
+ if self.usage_tracker:
1125
+ self.logger.info("✓ Unified usage tracker initialized (replaces meter + telemetry)")
1126
+ else:
1127
+ self.logger.debug("Unified usage tracker not available")
1128
+ else:
1129
+ self.usage_tracker = None
1130
+
1131
+ # A3: give the compactor + TaskContextManager (reflection) the metering
1132
+ # context so aux LLM calls bill through the same record_llm_usage() path
1133
+ # as the main step. Placed AFTER self.usage_tracker is assigned above
1134
+ # (not next to the :874/:886 aux-model provisioning, which runs before
1135
+ # usage_tracker exists) so these reads are never a stale None.
1136
+ self.message_manager.usage_tracker = self.usage_tracker
1137
+ self.message_manager.metering_user_id = getattr(self, "user_id", None)
1138
+ self.message_manager.metering_agent_id = getattr(self, "agent_id", None)
1139
+ # Capture the MAIN event loop here (construction runs on it). Reflection
1140
+ # consolidation runs on a worker thread (add_step_memory is offloaded via
1141
+ # asyncio.to_thread), where run_coroutine_sync would spin a THROWAWAY loop;
1142
+ # the usage tracker's DB connection binds an asyncio.Lock to the main loop,
1143
+ # so metering on a throwaway loop raises "bound to a different event loop"
1144
+ # and the fail-open swallow means reflection was NEVER billed. Passing the
1145
+ # main loop lets ReflectionService schedule the meter back onto it via
1146
+ # run_coroutine_threadsafe (the main loop is free — only the worker blocks —
1147
+ # so no deadlock). Fail-open: None => legacy run_coroutine_sync path.
1148
+ try:
1149
+ _main_loop = asyncio.get_running_loop()
1150
+ except RuntimeError:
1151
+ _main_loop = None
1152
+ self.task_context_manager.reflection_meter_ctx = {
1153
+ "usage_tracker": self.usage_tracker,
1154
+ "user_id": getattr(self, "user_id", None),
1155
+ "session_id": getattr(self, "session_id", ""),
1156
+ "agent_id": getattr(self, "agent_id", "") or "",
1157
+ "loop": _main_loop,
1158
+ }
1159
+
1160
+ # UsageMeter retired (C5) — LLMUsageTracker (self.usage_tracker, above) is
1161
+ # always available under the same precondition usage_meter needed
1162
+ # (db + balance_manager). Kept as None for back-compat attribute access.
1163
+ self.usage_meter = None
1164
+
1165
+ # Save initial task to task.json for webview chat
1166
+ self._save_initial_task()
1167
+
1168
+ # OPTIMIZATION: Setup tool output logging (Task 1 - Nov 14, 2025)
1169
+ self.tool_output_log_path = None
1170
+ if self.orchestrator and self.orchestrator.session_id and self.orchestrator.user_id:
1171
+ log_dir = (Path(self.orchestrator.session_path)
1172
+ if hasattr(self.orchestrator, 'session_path')
1173
+ else self.orchestrator._path_manager.get_subdir(
1174
+ self.orchestrator.session_id, "logs", self.orchestrator.user_id))
1175
+ log_dir.mkdir(parents=True, exist_ok=True)
1176
+ self.tool_output_log_path = log_dir / "tool_outputs.jsonl"
1177
+ self.logger.debug(f"Tool output logging enabled: {self.tool_output_log_path}")
1178
+
1179
+