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,984 @@
1
+ """
2
+ Simplified Session Manager
3
+
4
+ This is a cleaned up version of SessionManager with:
5
+ - Feed scanning removed (overly complex)
6
+ - Metadata tracking simplified
7
+ - Direct session management
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import uuid
12
+ from pathlib import Path
13
+ from typing import Dict, List, Optional, Any, Union
14
+ from datetime import datetime
15
+ import json
16
+ import logging
17
+ import threading
18
+ import shutil
19
+ from enum import Enum
20
+
21
+ from agents.task.utils import get_safe_file_lock
22
+ from agents.task.path import pm, get_safe_singleton
23
+ from agents.task.constants import DEFAULT_USER_ID
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class SessionStatus(Enum):
29
+ """Session status enumeration for continuous chat.
30
+
31
+ State machine:
32
+ - CREATED → RUNNING, CANCELLED
33
+ - RUNNING → COMPLETED, FAILED, CANCELLED, SUSPENDED
34
+ - COMPLETED → RESUMED, CANCELLED
35
+ - RESUMED → RUNNING, CANCELLED
36
+ - SUSPENDED → RESUMED, CANCELLED
37
+ - FAILED → RESUMED, CANCELLED
38
+ - CANCELLED → (terminal state)
39
+
40
+ Note: PAUSED was removed in favor of CANCELLED for user interruption.
41
+ The continuous chat design uses COMPLETED → RESUMED flow for follow-up messages.
42
+ """
43
+ CREATED = "created" # Initial state after creation
44
+ RUNNING = "running" # Currently executing
45
+ COMPLETED = "completed" # Finished successfully (waiting for follow-up)
46
+ RESUMED = "resumed" # Continuous chat resume (transitional)
47
+ SUSPENDED = "suspended" # Evicted from memory, persisted to disk
48
+ FAILED = "failed" # Execution failed
49
+ CANCELLED = "cancelled" # User cancelled (terminal)
50
+
51
+ # Keep old name for backwards compatibility
52
+ SessionState = SessionStatus
53
+
54
+
55
+ def get_user_status(internal_status: str) -> str:
56
+ """Convert internal status to user-facing status.
57
+
58
+ Args:
59
+ internal_status: Internal session status (running, completed, etc.)
60
+
61
+ Returns:
62
+ User-facing status:
63
+ - "active" - Agent is working (can cancel)
64
+ - "idle" - Agent finished (can send follow-up)
65
+ - "stopped" - Agent was cancelled or failed
66
+
67
+ Note: "paused" was removed. Use CANCELLED for user interruption,
68
+ and COMPLETED → RESUMED flow for continuous chat.
69
+ """
70
+ status_map = {
71
+ "running": "active",
72
+ "created": "active",
73
+ "resumed": "active",
74
+ "completed": "idle",
75
+ "cancelled": "stopped",
76
+ "failed": "stopped",
77
+ "suspended": "idle",
78
+ "error": "stopped",
79
+ }
80
+ return status_map.get(internal_status.lower(), "idle")
81
+
82
+
83
+ class SessionManager:
84
+ """
85
+ Simplified central manager for all agent sessions.
86
+
87
+ This is the source of truth for:
88
+ 1. Session ID management and cleaning
89
+ 2. Path normalization and standardization
90
+ 3. Directory/file management for sessions
91
+ 4. Basic session state tracking
92
+ """
93
+
94
+ def __init__(self, base_dir: Optional[str] = None):
95
+ """Initialize singleton session manager with specified base directory."""
96
+ global logger
97
+ from agents.task.logging_config import get_task_logger
98
+ logger = get_task_logger('session')
99
+ self.logger = logger
100
+
101
+ # Initialize base directory
102
+ self.base_dir = Path(base_dir) if base_dir else pm().data_root
103
+ self.base_dir.mkdir(parents=True, exist_ok=True)
104
+
105
+ # Initialize locking for thread safety
106
+ self._lock = threading.RLock()
107
+
108
+ # Simple session tracking
109
+ self._sessions = {} # {session_id: session_info}
110
+ self._user_sessions = {} # {user_id: [session_ids]}
111
+
112
+ logger.info(f"SessionManager initialized with base_dir: {self.base_dir}")
113
+
114
+ # Load existing sessions from disk
115
+ self._load_sessions_from_disk()
116
+
117
+ def _load_sessions_from_disk(self):
118
+ """Load all sessions from disk on startup."""
119
+ logger.info("Loading sessions from disk...")
120
+ loaded_count = 0
121
+
122
+ try:
123
+ # Search both layouts: NEW flat (data_root/<user>/<session>/metadata.json,
124
+ # written by path.py:get_session_root) AND legacy (<user>/sessions/<session>/...).
125
+ _seen = set()
126
+ for _pattern in ('*/*/metadata.json', '*/sessions/*/metadata.json'):
127
+ for metadata_path in self.base_dir.glob(_pattern):
128
+ if metadata_path in _seen:
129
+ continue
130
+ _seen.add(metadata_path)
131
+ try:
132
+ with open(metadata_path) as f:
133
+ session_info = json.load(f)
134
+
135
+ session_id = session_info.get('id')
136
+ if not session_id:
137
+ continue
138
+
139
+ # Load into memory
140
+ self._sessions[session_id] = session_info
141
+
142
+ # Mark running sessions as suspended (they were interrupted)
143
+ if session_info.get('status') == 'running':
144
+ session_info['status'] = 'suspended'
145
+ session_info['suspended_at'] = datetime.now().isoformat()
146
+ self.logger.info(f"Marked session {session_id} as suspended (was running)")
147
+
148
+ # Track by user
149
+ user_id = session_info.get('user_id', DEFAULT_USER_ID)
150
+ if user_id not in self._user_sessions:
151
+ self._user_sessions[user_id] = []
152
+ if session_id not in self._user_sessions[user_id]:
153
+ self._user_sessions[user_id].append(session_id)
154
+
155
+ loaded_count += 1
156
+
157
+ except Exception as e:
158
+ logger.warning(f"Failed to load session from {metadata_path}: {e}")
159
+
160
+ logger.info(f"Loaded {loaded_count} sessions from disk")
161
+
162
+ except Exception as e:
163
+ logger.error(f"Error loading sessions from disk: {e}")
164
+
165
+ def create_session(self, session_id: str = None, user_id: Optional[str] = None) -> str:
166
+ """
167
+ Create or get a session.
168
+
169
+ Args:
170
+ session_id: Optional session ID. If not provided, generates a new one.
171
+ user_id: Optional user ID for multi-user support
172
+
173
+ Returns:
174
+ The cleaned session ID
175
+ """
176
+ with self._lock:
177
+ # Generate or clean session ID
178
+ if not session_id:
179
+ session_id = str(uuid.uuid4())
180
+
181
+ # Always clean the session ID
182
+ session_id = pm().clean_session_id(session_id)
183
+
184
+ # Use default user if not specified
185
+ user_id = user_id or DEFAULT_USER_ID
186
+
187
+ # Check if session already exists
188
+ if session_id in self._sessions:
189
+ self.logger.debug(f"Session {session_id} already exists")
190
+ return session_id
191
+
192
+ # Create session info
193
+ session_info = {
194
+ 'id': session_id,
195
+ 'user_id': user_id,
196
+ 'created_at': datetime.now().isoformat(),
197
+ 'status': 'created', # Use 'created' for consistency
198
+ 'agents': []
199
+ }
200
+
201
+ # Store session
202
+ self._sessions[session_id] = session_info
203
+
204
+ # Track user sessions
205
+ if user_id not in self._user_sessions:
206
+ self._user_sessions[user_id] = []
207
+ self._user_sessions[user_id].append(session_id)
208
+
209
+ # Create session directory
210
+ session_dir = self.get_session_dir(session_id, user_id)
211
+ session_dir.mkdir(parents=True, exist_ok=True)
212
+
213
+ # Create standard subdirectories
214
+ for subdir in ['logs', 'workspace', 'artifacts', 'screenshots', 'feed', 'telemetry']:
215
+ (session_dir / subdir).mkdir(exist_ok=True)
216
+
217
+ # Save minimal metadata
218
+ self._save_metadata(session_id, user_id)
219
+
220
+ self.logger.debug(f"Created session {session_id} for user {user_id}")
221
+ return session_id
222
+
223
+ def get_session_dir(self, session_id: str, user_id: Optional[str] = None) -> Path:
224
+ """
225
+ Get the directory path for a session.
226
+
227
+ Args:
228
+ session_id: The session ID
229
+ user_id: Optional user ID
230
+
231
+ Returns:
232
+ Path to the session directory
233
+ """
234
+ session_id = pm().clean_session_id(session_id)
235
+
236
+ # Get user ID from tracking if not provided
237
+ if not user_id and session_id in self._sessions:
238
+ user_id = self._sessions[session_id].get('user_id', DEFAULT_USER_ID)
239
+
240
+ user_id = user_id or DEFAULT_USER_ID
241
+
242
+ # Use path manager for consistent paths
243
+ return pm().get_session_root(session_id, user_id)
244
+
245
+ def get_workspace_dir(self, session_id: str, user_id: Optional[str] = None) -> Path:
246
+ """Get workspace directory for a session - delegates to PathManager."""
247
+ # Use PathManager as single source of truth for workspace paths
248
+ return pm().get_workspace_dir(session_id, user_id)
249
+
250
+ def get_subdirectory(self, session_id: str, subdir_name: str, user_id: Optional[str] = None) -> Path:
251
+ """Get a subdirectory within the session directory.
252
+
253
+ Args:
254
+ session_id: Session ID
255
+ subdir_name: Name of the subdirectory
256
+ user_id: Optional user ID
257
+
258
+ Returns:
259
+ Path to the subdirectory
260
+ """
261
+ # Delegate to PathManager for consistent path handling
262
+ return pm().get_subdir(session_id, subdir_name, user_id)
263
+
264
+ def register_agent(self, session_id: str, agent_name: str, agent_id: str = None,
265
+ role: str = None, agent_type: str = None, model_name: str = None,
266
+ user_id: str = None, **kwargs) -> None:
267
+ """
268
+ Register an agent with a session.
269
+
270
+ Args:
271
+ session_id: The session ID
272
+ agent_name: Name of the agent
273
+ agent_id: Optional agent ID
274
+ role: Optional agent role
275
+ agent_type: Optional agent type/class name
276
+ model_name: Optional model name
277
+ user_id: Optional user ID
278
+ **kwargs: Additional metadata to ignore
279
+ """
280
+ with self._lock:
281
+ session_id = pm().clean_session_id(session_id)
282
+
283
+ if session_id not in self._sessions:
284
+ # Auto-create session if it doesn't exist (for backwards compatibility)
285
+ self.logger.warning(f"Session {session_id} not found, creating it now")
286
+ self.create_session(session_id, user_id)
287
+
288
+ agent_info = {
289
+ 'name': agent_name,
290
+ 'id': agent_id or agent_name,
291
+ 'role': role,
292
+ 'type': agent_type,
293
+ 'model': model_name,
294
+ 'registered_at': datetime.now().isoformat()
295
+ }
296
+
297
+ # Check for duplicate agent registration and update instead of duplicating
298
+ existing_agents = self._sessions[session_id].get('agents', [])
299
+ for existing_agent in existing_agents:
300
+ if existing_agent.get('id') == agent_info['id']:
301
+ self.logger.debug(f"Agent {agent_name} already registered, updating info")
302
+ # Update existing agent info instead of adding duplicate
303
+ existing_agent.update(agent_info)
304
+ self._save_metadata(session_id)
305
+ self.update_agents_summary(session_id) # Update agents.json for webview
306
+ return
307
+
308
+ # Add new agent
309
+ self._sessions[session_id]['agents'].append(agent_info)
310
+ self.logger.debug(f"Registered agent {agent_name} with session {session_id}")
311
+ self._save_metadata(session_id)
312
+ self.update_agents_summary(session_id) # Update agents.json for webview
313
+
314
+ def update_session_status(self, session_id: str, status: Union[str, SessionStatus]) -> None:
315
+ """Update session status with enum validation.
316
+
317
+ Args:
318
+ session_id: Session identifier
319
+ status: New status (string or SessionStatus enum)
320
+ """
321
+ with self._lock:
322
+ session_id = pm().clean_session_id(session_id)
323
+
324
+ if session_id not in self._sessions:
325
+ self.logger.warning(f"Cannot update status for non-existent session {session_id}")
326
+ return
327
+
328
+ # Get previous status for event emission
329
+ previous_status = self._sessions[session_id].get('status')
330
+
331
+ # Convert string to enum if needed
332
+ if isinstance(status, str):
333
+ try:
334
+ status = SessionStatus(status.lower())
335
+ except ValueError:
336
+ self.logger.warning(f"Invalid status value '{status}', using as-is")
337
+ # Use string as-is for backward compatibility
338
+ new_status_str = status.lower()
339
+ self._sessions[session_id]['status'] = new_status_str
340
+ self._sessions[session_id]['updated_at'] = datetime.now().isoformat()
341
+ self._save_metadata(session_id)
342
+ self._save_status_summary(session_id) # Also save status.json for WebView
343
+ # Emit status event to feed for WebSocket updates
344
+ self._emit_status_event(session_id, new_status_str, previous_status)
345
+ return
346
+
347
+ # Get current status for validation
348
+ current = self._sessions[session_id].get('status')
349
+ if isinstance(current, str):
350
+ try:
351
+ current = SessionStatus(current.lower())
352
+ except ValueError:
353
+ # Current status is invalid, allow transition
354
+ self.logger.warning(f"Current status '{current}' invalid, allowing transition")
355
+ current = None
356
+
357
+ # Validate state transition
358
+ if current and not self._is_valid_transition(current, status):
359
+ self.logger.warning(
360
+ f"Invalid state transition for {session_id}: "
361
+ f"{current.value if isinstance(current, SessionStatus) else current} -> {status.value}"
362
+ )
363
+ # Allow it anyway but log warning
364
+
365
+ # Update with enum value
366
+ self._sessions[session_id]['status'] = status.value
367
+ self._sessions[session_id]['updated_at'] = datetime.now().isoformat()
368
+ self._save_metadata(session_id)
369
+ self._save_status_summary(session_id) # Also save status.json for WebView
370
+ self.logger.debug(f"Updated session {session_id} status to {status.value}")
371
+
372
+ # Emit status event to feed for WebSocket updates
373
+ self._emit_status_event(session_id, status.value, previous_status)
374
+
375
+ def _emit_status_event(self, session_id: str, new_status: str, previous_status: Optional[str] = None) -> None:
376
+ """Emit a status event to the feed for WebSocket real-time updates.
377
+
378
+ This allows the frontend to receive status changes immediately via WebSocket
379
+ instead of relying on polling.
380
+
381
+ Args:
382
+ session_id: Session identifier
383
+ new_status: New status value
384
+ previous_status: Previous status value (optional)
385
+ """
386
+ import time
387
+
388
+ try:
389
+ # Get the user_id from session info
390
+ session_info = self._sessions.get(session_id, {})
391
+ user_id = session_info.get('user_id')
392
+
393
+ # Get feed directory
394
+ feed_dir = pm().get_subdir(session_id, "feed", user_id=user_id)
395
+ if not feed_dir:
396
+ self.logger.warning(f"Cannot emit status event - no feed dir for {session_id}")
397
+ return
398
+
399
+ # Create status event
400
+ timestamp = time.time()
401
+ status_event = {
402
+ "type": "status",
403
+ "timestamp": timestamp,
404
+ "data": {
405
+ "status": new_status,
406
+ "previous_status": previous_status,
407
+ "session_id": session_id
408
+ }
409
+ }
410
+
411
+ # Write to feed with timestamp-based filename
412
+ filename = f"status_{int(timestamp * 1000)}.json"
413
+ status_path = feed_dir / filename
414
+
415
+ # Simple atomic write
416
+ temp_path = status_path.with_suffix('.tmp')
417
+ try:
418
+ with open(temp_path, 'w') as f:
419
+ json.dump(status_event, f, indent=2)
420
+ temp_path.replace(status_path)
421
+ self.logger.debug(f"📊 Emitted status event: {new_status} (was: {previous_status})")
422
+ except Exception as e:
423
+ self.logger.error(f"Failed to write status event: {e}")
424
+ if temp_path.exists():
425
+ try:
426
+ temp_path.unlink()
427
+ except Exception:
428
+ pass
429
+
430
+ except Exception as e:
431
+ self.logger.error(f"Error emitting status event: {e}", exc_info=True)
432
+
433
+ def _is_valid_transition(self, from_status: SessionStatus, to_status: SessionStatus) -> bool:
434
+ """Validate if status transition is allowed.
435
+
436
+ Args:
437
+ from_status: Current status
438
+ to_status: Target status
439
+
440
+ Returns:
441
+ True if transition is valid
442
+ """
443
+ # Same-state transitions are always valid (no-op)
444
+ if from_status == to_status:
445
+ return True
446
+
447
+ # Define valid state transitions (PAUSED removed - use CANCELLED for interruption)
448
+ VALID_TRANSITIONS = {
449
+ SessionStatus.CREATED: [SessionStatus.RUNNING, SessionStatus.CANCELLED],
450
+ SessionStatus.RUNNING: [SessionStatus.COMPLETED, SessionStatus.FAILED, SessionStatus.CANCELLED, SessionStatus.SUSPENDED],
451
+ SessionStatus.COMPLETED: [SessionStatus.RESUMED, SessionStatus.CANCELLED],
452
+ SessionStatus.RESUMED: [SessionStatus.RUNNING, SessionStatus.CANCELLED],
453
+ SessionStatus.SUSPENDED: [SessionStatus.RESUMED, SessionStatus.CANCELLED],
454
+ SessionStatus.FAILED: [SessionStatus.RESUMED, SessionStatus.CANCELLED],
455
+ SessionStatus.CANCELLED: [] # Terminal state - no transitions out
456
+ }
457
+
458
+ allowed = VALID_TRANSITIONS.get(from_status, [])
459
+ return to_status in allowed
460
+
461
+ def try_transition_status(self, session_id: str, from_status: str, to_status: str) -> bool:
462
+ """Atomically try to transition status. Returns False if current status doesn't match.
463
+
464
+ This provides concurrency protection without locks - the status field itself
465
+ acts as the synchronization mechanism.
466
+
467
+ Args:
468
+ session_id: Session ID to transition
469
+ from_status: Expected current status
470
+ to_status: New status to set
471
+
472
+ Returns:
473
+ True if transition succeeded, False if current status doesn't match expected
474
+ """
475
+ with self._lock:
476
+ session_id = pm().clean_session_id(session_id)
477
+
478
+ if session_id not in self._sessions:
479
+ self.logger.warning(f"Cannot transition status for unknown session {session_id}")
480
+ return False
481
+
482
+ current = self._sessions[session_id].get('status')
483
+
484
+ # Check if current status matches expected
485
+ if current != from_status:
486
+ self.logger.debug(f"Status transition failed for {session_id}: expected {from_status}, got {current}")
487
+ return False
488
+
489
+ # Perform transition
490
+ self._sessions[session_id]['status'] = to_status
491
+ self._sessions[session_id]['updated_at'] = datetime.now().isoformat()
492
+ self._save_metadata(session_id)
493
+
494
+ self.logger.info(f"Session {session_id}: {from_status} → {to_status}")
495
+ return True
496
+
497
+ def get_session_info(self, session_id: str) -> Dict:
498
+ """Get session information."""
499
+ with self._lock:
500
+ session_id = pm().clean_session_id(session_id)
501
+ return self._sessions.get(session_id, {})
502
+
503
+ def get_active_sessions(self, user_id: Optional[str] = None) -> List[str]:
504
+ """Get list of active session IDs (created or running)."""
505
+ with self._lock:
506
+ active_statuses = ['created', 'running', 'resumed']
507
+ if user_id:
508
+ user_sessions = self._user_sessions.get(user_id, [])
509
+ return [sid for sid in user_sessions
510
+ if self._sessions.get(sid, {}).get('status') in active_statuses]
511
+ else:
512
+ return [sid for sid, info in self._sessions.items()
513
+ if info.get('status') in active_statuses]
514
+
515
+ def cleanup_session(self, session_id: str, delete_files: bool = False) -> bool:
516
+ """
517
+ Cleanup a session.
518
+
519
+ Args:
520
+ session_id: The session ID to cleanup
521
+ delete_files: Whether to delete session files
522
+
523
+ Returns:
524
+ True if cleanup was successful
525
+ """
526
+ with self._lock:
527
+ session_id = pm().clean_session_id(session_id)
528
+
529
+ if session_id not in self._sessions:
530
+ return False
531
+
532
+ session_info = self._sessions[session_id]
533
+ user_id = session_info.get('user_id')
534
+
535
+ # Remove from tracking
536
+ del self._sessions[session_id]
537
+
538
+ if user_id in self._user_sessions:
539
+ self._user_sessions[user_id] = [
540
+ sid for sid in self._user_sessions[user_id]
541
+ if sid != session_id
542
+ ]
543
+ if not self._user_sessions[user_id]:
544
+ del self._user_sessions[user_id]
545
+
546
+ # Optionally delete files
547
+ if delete_files:
548
+ session_dir = self.get_session_dir(session_id, user_id)
549
+ if session_dir.exists():
550
+ try:
551
+ shutil.rmtree(session_dir)
552
+ self.logger.info(f"Deleted session files for {session_id}")
553
+ except Exception as e:
554
+ self.logger.error(f"Failed to delete session files: {e}")
555
+
556
+ self.logger.info(f"Cleaned up session {session_id}")
557
+ return True
558
+
559
+ def _save_metadata(self, session_id: str, user_id: Optional[str] = None) -> None:
560
+ """Save session metadata to file."""
561
+ try:
562
+ session_info = self._sessions.get(session_id)
563
+ if not session_info:
564
+ return
565
+
566
+ session_dir = self.get_session_dir(session_id, user_id or session_info.get('user_id'))
567
+ metadata_file = session_dir / 'metadata.json'
568
+
569
+ # Convert SessionState enums to strings before saving
570
+ serializable_info = {}
571
+ for key, value in session_info.items():
572
+ if isinstance(value, SessionState):
573
+ serializable_info[key] = value.value # Use the string value
574
+ else:
575
+ serializable_info[key] = value
576
+
577
+ # Use file lock to avoid concurrent writes by multiple processes/threads
578
+ try:
579
+ lock = get_safe_file_lock(str(metadata_file) + ".lock", timeout=10.0)
580
+ with lock:
581
+ with open(metadata_file, 'w') as f:
582
+ json.dump(serializable_info, f, indent=2, default=str)
583
+ except Exception:
584
+ with open(metadata_file, 'w') as f:
585
+ json.dump(serializable_info, f, indent=2, default=str)
586
+ except Exception as e:
587
+ self.logger.warning(f"Failed to save metadata for {session_id}: {e}")
588
+
589
+ def load_session(self, session_id: str, user_id: Optional[str] = None) -> bool:
590
+ """
591
+ Load session from disk if it exists.
592
+
593
+ Args:
594
+ session_id: The session ID to load
595
+ user_id: Optional user ID
596
+
597
+ Returns:
598
+ True if session was loaded successfully
599
+ """
600
+ with self._lock:
601
+ session_id = pm().clean_session_id(session_id)
602
+
603
+ # Check if already loaded
604
+ if session_id in self._sessions:
605
+ return True
606
+
607
+ # Try to load from disk
608
+ session_dir = self.get_session_dir(session_id, user_id)
609
+ metadata_file = session_dir / 'metadata.json'
610
+
611
+ if not metadata_file.exists():
612
+ return False
613
+
614
+ try:
615
+ with open(metadata_file, 'r') as f:
616
+ session_info = json.load(f)
617
+
618
+ # Store in memory
619
+ self._sessions[session_id] = session_info
620
+
621
+ # Update user tracking
622
+ user_id = session_info.get('user_id', DEFAULT_USER_ID)
623
+ if user_id not in self._user_sessions:
624
+ self._user_sessions[user_id] = []
625
+ if session_id not in self._user_sessions[user_id]:
626
+ self._user_sessions[user_id].append(session_id)
627
+
628
+ self.logger.info(f"Loaded session {session_id} from disk")
629
+ return True
630
+
631
+ except Exception as e:
632
+ self.logger.error(f"Failed to load session {session_id}: {e}")
633
+ return False
634
+
635
+ def session_exists(self, session_id: str) -> bool:
636
+ """
637
+ Check if a session exists.
638
+
639
+ Args:
640
+ session_id: Session ID to check
641
+
642
+ Returns:
643
+ True if the session exists, False otherwise
644
+ """
645
+ session_id = pm().clean_session_id(session_id)
646
+
647
+ # Check in-memory sessions
648
+ if session_id in self._sessions:
649
+ return True
650
+
651
+ # Check on disk
652
+ session_root = pm().get_session_root(session_id)
653
+ metadata_path = session_root / 'metadata.json'
654
+
655
+ return metadata_path.exists()
656
+
657
+ def update_session_metadata(self, session_id: str, metadata_update: Dict[str, Any]) -> None:
658
+ """
659
+ Update the metadata for a session.
660
+
661
+ Args:
662
+ session_id: The session ID
663
+ metadata_update: The metadata to update
664
+ """
665
+ with self._lock:
666
+ session_id = pm().clean_session_id(session_id)
667
+
668
+ # Ensure session exists in memory
669
+ if session_id not in self._sessions:
670
+ # Try to load from disk
671
+ if not self.load_session(session_id):
672
+ self.logger.warning(f"Session {session_id} not found for metadata update")
673
+ return
674
+
675
+ # Update in-memory data
676
+ session_info = self._sessions[session_id]
677
+
678
+ # Deep merge metadata
679
+ for key, value in metadata_update.items():
680
+ if key == 'agents' and isinstance(value, list):
681
+ # Merge agent lists without duplicates
682
+ existing = session_info.get('agents', [])
683
+ merged = {(a.get('id') or a.get('name')): a for a in existing}
684
+ for agent in value:
685
+ agent_id = agent.get('id') or agent.get('name')
686
+ if agent_id:
687
+ merged[agent_id] = agent
688
+ session_info['agents'] = list(merged.values())
689
+ else:
690
+ session_info[key] = value
691
+
692
+ # Save to disk
693
+ self._save_metadata(session_id)
694
+
695
+ def get_task_phase(self, session_id: str) -> int:
696
+ """Get current task phase for continuous conversation.
697
+
698
+ Args:
699
+ session_id: Session identifier
700
+
701
+ Returns:
702
+ Current phase number (0 if not set)
703
+ """
704
+ with self._lock:
705
+ session_id = pm().clean_session_id(session_id)
706
+
707
+ if session_id not in self._sessions:
708
+ return 0
709
+
710
+ return self._sessions[session_id].get('task_phase', 0)
711
+
712
+ def increment_task_phase(self, session_id: str) -> int:
713
+ """Increment task phase counter for continuous conversation.
714
+
715
+ Each user continuation increments the phase:
716
+ - Phase 1: Initial task
717
+ - Phase 2: First continuation
718
+ - Phase 3: Second continuation, etc.
719
+
720
+ Args:
721
+ session_id: Session identifier
722
+
723
+ Returns:
724
+ New phase number
725
+ """
726
+ with self._lock:
727
+ session_id = pm().clean_session_id(session_id)
728
+
729
+ if session_id not in self._sessions:
730
+ self.logger.warning(f"Cannot increment phase for unknown session {session_id}")
731
+ return 0
732
+
733
+ session_info = self._sessions[session_id]
734
+
735
+ # Get current phase (default to 0)
736
+ current_phase = session_info.get('task_phase', 0)
737
+ new_phase = current_phase + 1
738
+
739
+ # Update session metadata
740
+ session_info['task_phase'] = new_phase
741
+ session_info['phase_updated_at'] = datetime.now().isoformat()
742
+
743
+ # Track phase history for analytics
744
+ if 'phase_history' not in session_info:
745
+ session_info['phase_history'] = []
746
+
747
+ session_info['phase_history'].append({
748
+ 'phase': new_phase,
749
+ 'started_at': datetime.now().isoformat(),
750
+ 'status': 'active'
751
+ })
752
+
753
+ # Save to disk
754
+ self._save_metadata(session_id)
755
+
756
+ self.logger.info(f"✅ Session {session_id}: Phase {current_phase} → {new_phase}")
757
+ return new_phase
758
+
759
+ def add_to_feed(self, session_id: str, event_type: str, data: Dict[str, Any]) -> None:
760
+ """
761
+ Add an event to the session feed.
762
+
763
+ Args:
764
+ session_id: The session ID
765
+ event_type: Type of event (e.g., 'step', 'action', 'result')
766
+ data: Event data to store
767
+ """
768
+ import time
769
+ import json
770
+
771
+ session_id = pm().clean_session_id(session_id)
772
+
773
+ # Get feed directory
774
+ feed_dir = self.get_subdirectory(session_id, "feed")
775
+ feed_dir.mkdir(parents=True, exist_ok=True)
776
+
777
+ # Create feed entry
778
+ timestamp = time.time()
779
+ feed_entry = {
780
+ 'timestamp': timestamp,
781
+ 'type': event_type, # Use 'type' to match webview expectations
782
+ 'data': data
783
+ }
784
+
785
+ # Write to feed file
786
+ feed_file = feed_dir / f"{event_type}_{int(timestamp * 1000)}.json"
787
+ with open(feed_file, 'w') as f:
788
+ json.dump(feed_entry, f, indent=2)
789
+
790
+ self.logger.debug(f"Added {event_type} event to feed for session {session_id}")
791
+
792
+ def _save_summary_file(self, session_id: str, filename: str, data: Dict[str, Any]) -> None:
793
+ """Save a summary JSON file using existing atomic write pattern.
794
+
795
+ This method follows the same pattern as _save_metadata() for consistency.
796
+
797
+ Args:
798
+ session_id: The session ID
799
+ filename: Name of the file (e.g., 'task.json', 'agents.json')
800
+ data: Data to save
801
+ """
802
+ try:
803
+ session_info = self._sessions.get(session_id)
804
+ if not session_info:
805
+ return
806
+
807
+ session_dir = self.get_session_dir(session_id, session_info.get('user_id'))
808
+ summary_file = session_dir / filename
809
+
810
+ # Use file lock to avoid concurrent writes (SAME PATTERN as _save_metadata)
811
+ try:
812
+ lock = get_safe_file_lock(str(summary_file) + ".lock", timeout=10.0)
813
+ with lock:
814
+ with open(summary_file, 'w') as f:
815
+ json.dump(data, f, indent=2, default=str)
816
+ except Exception:
817
+ # Fallback without lock
818
+ with open(summary_file, 'w') as f:
819
+ json.dump(data, f, indent=2, default=str)
820
+
821
+ self.logger.debug(f"Saved {filename} for session {session_id}")
822
+ except Exception as e:
823
+ self.logger.warning(f"Failed to save {filename} for {session_id}: {e}")
824
+
825
+ def update_agents_summary(self, session_id: str) -> None:
826
+ """Update agents.json summary file for webview.
827
+
828
+ Extracts agent info from session metadata and writes to agents.json.
829
+ This is called automatically when agents are registered.
830
+ """
831
+ from datetime import datetime
832
+
833
+ session_id = pm().clean_session_id(session_id)
834
+
835
+ if session_id not in self._sessions:
836
+ return
837
+
838
+ session_info = self._sessions[session_id]
839
+ agents_list = session_info.get('agents', [])
840
+
841
+ # Build last_updated timestamp
842
+ summary_data = {
843
+ "agents": agents_list,
844
+ "last_updated": datetime.now().isoformat()
845
+ }
846
+
847
+ self._save_summary_file(session_id, 'agents.json', summary_data)
848
+
849
+ def _save_status_summary(self, session_id: str) -> None:
850
+ """Save status.json summary file for WebView.
851
+
852
+ This is called automatically when session status is updated.
853
+ WebView expects this file to exist for session status queries.
854
+ """
855
+ session_id = pm().clean_session_id(session_id)
856
+
857
+ if session_id not in self._sessions:
858
+ return
859
+
860
+ session_info = self._sessions[session_id]
861
+
862
+ # Build status summary data that WebView expects
863
+ status_data = {
864
+ "status": session_info.get('status', 'unknown'),
865
+ "created_at": session_info.get('created_at'),
866
+ "updated_at": session_info.get('updated_at', datetime.now().isoformat()),
867
+ "session_id": session_id
868
+ }
869
+
870
+ self._save_summary_file(session_id, 'status.json', status_data)
871
+
872
+ def get_all_sessions(self) -> List[Dict[str, Any]]:
873
+ """
874
+ Get all sessions from memory and disk.
875
+
876
+ Returns:
877
+ List of session info dictionaries
878
+ """
879
+ with self._lock:
880
+ all_sessions = []
881
+
882
+ # First get all from memory
883
+ for session_id, session_info in self._sessions.items():
884
+ all_sessions.append(session_info)
885
+
886
+ # Then check disk for any not in memory using PathManager's structure
887
+ from agents.task.path import pm
888
+ try:
889
+ # Search both layouts: NEW flat (data_root/<user>/<session>/metadata.json)
890
+ # AND legacy (data_root/<user>/sessions/<session>/metadata.json).
891
+ _seen = set()
892
+ for _pattern in ('*/*/metadata.json', '*/sessions/*/metadata.json'):
893
+ for metadata_path in pm().data_root.glob(_pattern):
894
+ if metadata_path in _seen:
895
+ continue
896
+ _seen.add(metadata_path)
897
+ # Try to load from disk
898
+ try:
899
+ with open(metadata_path) as f:
900
+ session_info = json.load(f)
901
+ # L2 FIX: derive the id from the JSON `id` field (matching
902
+ # _load_sessions_from_disk), falling back to the directory name.
903
+ # Using the dir name alone desynced the dedup check from the
904
+ # in-memory map (keyed on the cleaned id) for sessions whose dir
905
+ # name != stored id, producing duplicate/extra list entries.
906
+ session_id = session_info.get('id') or metadata_path.parent.name
907
+ if session_id not in self._sessions:
908
+ all_sessions.append(session_info)
909
+ except Exception as e:
910
+ self.logger.warning(f"Failed to load session at {metadata_path} from disk: {e}")
911
+ except Exception as e:
912
+ # Fallback to old path structure if new approach fails
913
+ sessions_dir = self.base_dir / "task" / "sessions"
914
+ if sessions_dir.exists():
915
+ for session_dir in sessions_dir.iterdir():
916
+ if session_dir.is_dir():
917
+ session_id = session_dir.name
918
+ if session_id not in self._sessions:
919
+ metadata_path = session_dir / "metadata.json"
920
+ if metadata_path.exists():
921
+ try:
922
+ with open(metadata_path) as f:
923
+ session_info = json.load(f)
924
+ all_sessions.append(session_info)
925
+ except Exception as e:
926
+ self.logger.warning(f"Failed to load session {session_id} from disk: {e}")
927
+
928
+ return all_sessions
929
+
930
+ def cleanup_old_workspaces(self, max_age_days: int = 7) -> int:
931
+ """Clean up workspaces for old completed sessions.
932
+
933
+ Args:
934
+ max_age_days: Maximum age in days before cleanup
935
+
936
+ Returns:
937
+ Number of workspaces cleaned
938
+ """
939
+ import shutil
940
+ from datetime import datetime, timedelta
941
+
942
+ cleaned_count = 0
943
+ cutoff_time = datetime.now() - timedelta(days=max_age_days)
944
+
945
+ for session_id, session_info in list(self._sessions.items()):
946
+ # Only clean completed/failed/cancelled sessions
947
+ status = session_info.get('status', '')
948
+ if status not in ['completed', 'failed', 'cancelled']:
949
+ continue
950
+
951
+ # Check age
952
+ updated_str = session_info.get('updated_at')
953
+ if not updated_str:
954
+ continue
955
+
956
+ try:
957
+ updated_time = datetime.fromisoformat(updated_str)
958
+ if updated_time > cutoff_time:
959
+ continue # Too recent
960
+
961
+ # Get workspace directory
962
+ user_id = session_info.get('user_id')
963
+ workspace_dir = pm().get_workspace_dir(session_id, user_id)
964
+
965
+ if workspace_dir.exists():
966
+ # Check size before deleting
967
+ size_mb = sum(f.stat().st_size for f in workspace_dir.rglob('*') if f.is_file()) / 1024 / 1024
968
+
969
+ # Delete workspace
970
+ shutil.rmtree(workspace_dir)
971
+ self.logger.info(f"Cleaned up workspace for {session_id} ({size_mb:.1f} MB, age: {(datetime.now() - updated_time).days} days)")
972
+ cleaned_count += 1
973
+
974
+ except Exception as e:
975
+ self.logger.warning(f"Failed to clean workspace for {session_id}: {e}")
976
+
977
+ return cleaned_count
978
+
979
+
980
+ # Singleton access - use centralized get_safe_singleton
981
+ def get_session_manager() -> SessionManager:
982
+ """Get the singleton SessionManager instance using centralized singleton pattern."""
983
+ from agents.task.path import get_safe_singleton
984
+ return get_safe_singleton(SessionManager)()