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.
- agents/README.md +453 -0
- agents/__init__.py +164 -0
- agents/base_agent.py +743 -0
- agents/personality/character.py +119 -0
- agents/personality/character_manager.py +309 -0
- agents/personality/characters/rob.character.json +56 -0
- agents/personality/characters/trump.character.json +63 -0
- agents/personality/persona_render.py +78 -0
- agents/personality/persona_resolver.py +115 -0
- agents/prompt/__init__.py +58 -0
- agents/prompt/base_prompt.py +92 -0
- agents/prompt/system.py +417 -0
- agents/task/README_PATH_MANAGEMENT.md +141 -0
- agents/task/__init__.py +162 -0
- agents/task/agent/__init__.py +70 -0
- agents/task/agent/agent_state.py +408 -0
- agents/task/agent/async_delegation.py +274 -0
- agents/task/agent/autonomy_state.py +275 -0
- agents/task/agent/conversation.py +75 -0
- agents/task/agent/core/__init__.py +6 -0
- agents/task/agent/core/aux_metering.py +61 -0
- agents/task/agent/core/background_review.py +157 -0
- agents/task/agent/core/construction.py +1179 -0
- agents/task/agent/core/conversational_exit.py +49 -0
- agents/task/agent/core/correspondent_gate.py +227 -0
- agents/task/agent/core/curator.py +246 -0
- agents/task/agent/core/episodic_digest.py +103 -0
- agents/task/agent/core/error_recovery.py +480 -0
- agents/task/agent/core/history_io.py +257 -0
- agents/task/agent/core/llm_provisioning.py +287 -0
- agents/task/agent/core/llm_runner.py +547 -0
- agents/task/agent/core/logging_io.py +276 -0
- agents/task/agent/core/loop_detection.py +262 -0
- agents/task/agent/core/memory_prefetch.py +325 -0
- agents/task/agent/core/memory_writer.py +667 -0
- agents/task/agent/core/model_introspection.py +82 -0
- agents/task/agent/core/model_swap.py +177 -0
- agents/task/agent/core/next_action_internal.py +1331 -0
- agents/task/agent/core/output_validation.py +303 -0
- agents/task/agent/core/project_context.py +272 -0
- agents/task/agent/core/resources.py +93 -0
- agents/task/agent/core/result_processing.py +389 -0
- agents/task/agent/core/run_loop.py +661 -0
- agents/task/agent/core/safety_lifecycle.py +228 -0
- agents/task/agent/core/secret_guard.py +293 -0
- agents/task/agent/core/self_wake.py +280 -0
- agents/task/agent/core/session_metadata.py +104 -0
- agents/task/agent/core/step.py +781 -0
- agents/task/agent/core/step_execution.py +398 -0
- agents/task/agent/core/step_telemetry.py +222 -0
- agents/task/agent/core/test_aux_metering.py +50 -0
- agents/task/agent/core/test_billing_failover.py +68 -0
- agents/task/agent/core/test_fatal_error_classifier.py +38 -0
- agents/task/agent/core/test_judge_validation.py +275 -0
- agents/task/agent/core/test_model_swap.py +190 -0
- agents/task/agent/core/test_reflection_offload.py +54 -0
- agents/task/agent/core/turn_input.py +8 -0
- agents/task/agent/core/untrusted_wrap.py +103 -0
- agents/task/agent/core/user_ingress.py +228 -0
- agents/task/agent/hitl_manager.py +396 -0
- agents/task/agent/log_sanitize.py +36 -0
- agents/task/agent/message_manager/config.py +97 -0
- agents/task/agent/message_manager/service.py +758 -0
- agents/task/agent/message_manager/tests.py +305 -0
- agents/task/agent/message_manager/tool_call_builder.py +539 -0
- agents/task/agent/message_manager/tool_message_repair.py +364 -0
- agents/task/agent/message_manager/views.py +143 -0
- agents/task/agent/messages/__init__.py +8 -0
- agents/task/agent/messages/builders.py +309 -0
- agents/task/agent/messages/compactor.py +611 -0
- agents/task/agent/messages/context_references.py +271 -0
- agents/task/agent/messages/filters.py +333 -0
- agents/task/agent/messages/guidance.py +328 -0
- agents/task/agent/messages/persistence.py +448 -0
- agents/task/agent/messages/retrieval.py +506 -0
- agents/task/agent/messages/sqlite_persistence.py +85 -0
- agents/task/agent/messages/test_compaction_upgrade.py +349 -0
- agents/task/agent/messages/test_persistence_subdir_fix.py +28 -0
- agents/task/agent/messages/test_sqlite_persistence.py +54 -0
- agents/task/agent/messages/token_counter.py +532 -0
- agents/task/agent/orchestrator.py +776 -0
- agents/task/agent/profile_manager.py +105 -0
- agents/task/agent/profile_registry.py +173 -0
- agents/task/agent/prompts.py +1090 -0
- agents/task/agent/scenario_registry.py +226 -0
- agents/task/agent/service.py +509 -0
- agents/task/agent/session.py +984 -0
- agents/task/agent/skill_discovery.py +110 -0
- agents/task/agent/skill_frontmatter.py +94 -0
- agents/task/agent/skill_manager.py +1201 -0
- agents/task/agent/skill_store.py +627 -0
- agents/task/agent/skill_validation.py +56 -0
- agents/task/agent/skill_writer.py +492 -0
- agents/task/agent/sub_agent_manager.py +1137 -0
- agents/task/agent/test_session_disk_recovery.py +34 -0
- agents/task/agent/tests.py +513 -0
- agents/task/agent/tool_call_tracker.py +511 -0
- agents/task/agent/views.py +469 -0
- agents/task/config.py +432 -0
- agents/task/constants.py +1384 -0
- agents/task/flag_defaults.py +63 -0
- agents/task/goals/__init__.py +1 -0
- agents/task/goals/autonomy_marker.py +28 -0
- agents/task/goals/board.py +690 -0
- agents/task/goals/completion_judge.py +247 -0
- agents/task/goals/context.py +62 -0
- agents/task/goals/dispatcher.py +848 -0
- agents/task/goals/escalation.py +102 -0
- agents/task/goals/planner.py +114 -0
- agents/task/logging_config.py +155 -0
- agents/task/path.py +991 -0
- agents/task/robust_parse_config.py +400 -0
- agents/task/runtime/__init__.py +0 -0
- agents/task/runtime/run_as_session.py +130 -0
- agents/task/runtime_safety.py +308 -0
- agents/task/session/__init__.py +5 -0
- agents/task/session/browser_pool.py +130 -0
- agents/task/session/cleanup.py +612 -0
- agents/task/session/execution.py +397 -0
- agents/task/session/feed.py +200 -0
- agents/task/session/hitl_ingress.py +201 -0
- agents/task/session/hooks.py +83 -0
- agents/task/session/multi_agent.py +243 -0
- agents/task/session/workspace.py +91 -0
- agents/task/session_registry.py +91 -0
- agents/task/session_route.py +35 -0
- agents/task/sqlite_session_registry.py +261 -0
- agents/task/surface_config.py +185 -0
- agents/task/telemetry/__init__.py +58 -0
- agents/task/telemetry/event_log.py +180 -0
- agents/task/telemetry/formatters.py +642 -0
- agents/task/telemetry/manager.py +571 -0
- agents/task/telemetry/memory_events.py +66 -0
- agents/task/telemetry/self_events.py +53 -0
- agents/task/telemetry/sequence.py +160 -0
- agents/task/telemetry/service.py +1265 -0
- agents/task/telemetry/views.py +882 -0
- agents/task/templates.py +98 -0
- agents/task/test_aux_chain.py +151 -0
- agents/task/test_aux_router.py +41 -0
- agents/task/tests/test_parse_robustness.py +170 -0
- agents/task/tool_defaults.py +110 -0
- agents/task/utils.py +762 -0
- agents/task/utils_json.py +768 -0
- agents/task/utils_webview.py +86 -0
- agents/task/workspace_context.py +229 -0
- agents/task_agent_lite.py +2398 -0
- api/README.md +683 -0
- api/TASK_API_DOCS.md +151 -0
- api/__init__.py +0 -0
- api/a2a/__init__.py +37 -0
- api/a2a/agent_card.py +337 -0
- api/a2a/client.py +657 -0
- api/a2a/endpoints.py +375 -0
- api/a2a/models.py +268 -0
- api/a2a/streaming.py +419 -0
- api/a2a/task_handler.py +929 -0
- api/admin_endpoints.py +1584 -0
- api/app.py +923 -0
- api/auth_constants.py +28 -0
- api/auth_endpoints.py +402 -0
- api/auth_state.py +36 -0
- api/chat_via_task.py +42 -0
- api/conversation_manager.py +231 -0
- api/dependencies.py +186 -0
- api/eip8004_endpoints.py +452 -0
- api/hyperliquid_models.py +174 -0
- api/hyperliquid_routes.py +450 -0
- api/interfaces.py +207 -0
- api/jwt_middleware.py +83 -0
- api/kb/__init__.py +1 -0
- api/kb/endpoints.py +314 -0
- api/mcp_models.py +279 -0
- api/mcp_routes.py +438 -0
- api/middleware.py +526 -0
- api/models.py +176 -0
- api/openai_compat/__init__.py +0 -0
- api/openai_compat/model_map.py +25 -0
- api/openai_compat/models.py +59 -0
- api/openai_compat/router.py +140 -0
- api/payment_endpoints.py +444 -0
- api/payment_verification.py +138 -0
- api/polymarket_models.py +155 -0
- api/polymarket_routes.py +501 -0
- api/pricing_endpoints.py +89 -0
- api/session_routing.py +54 -0
- api/skill_endpoints.py +453 -0
- api/task_http_api.py +1856 -0
- api/webhooks.py +52 -0
- api/x402_endpoints.py +280 -0
- avatar/README.md +44 -0
- avatar/__init__.py +1 -0
- avatar/config/rob.json +7 -0
- avatar/mindprint.js +395 -0
- avatar/renders/rob.meta.json +20 -0
- avatar/renders/rob.png +0 -0
- avatar/studio.html +373 -0
- avatar/webview/avatar-live.js +44 -0
- cli/README.md +89 -0
- cli/__init__.py +0 -0
- cli/cli_surface.py +67 -0
- cli/commands/__init__.py +0 -0
- cli/commands/_bootstrap.py +49 -0
- cli/commands/_errors.py +53 -0
- cli/commands/chat.py +973 -0
- cli/commands/config.py +100 -0
- cli/commands/dashboard.py +103 -0
- cli/commands/doctor.py +222 -0
- cli/commands/email.py +174 -0
- cli/commands/gateway.py +346 -0
- cli/commands/goals.py +509 -0
- cli/commands/init.py +276 -0
- cli/commands/journey.py +30 -0
- cli/commands/kb.py +280 -0
- cli/commands/model.py +107 -0
- cli/commands/owner.py +431 -0
- cli/commands/pfp.py +232 -0
- cli/commands/run.py +414 -0
- cli/commands/serve.py +41 -0
- cli/commands/session.py +684 -0
- cli/commands/skill_install.py +786 -0
- cli/commands/skills.py +128 -0
- cli/commands/subagents.py +100 -0
- cli/commands/surface.py +81 -0
- cli/commands/telegram.py +185 -0
- cli/commands/todos.py +209 -0
- cli/commands/tools.py +102 -0
- cli/commands/update.py +316 -0
- cli/commands/whatsapp.py +175 -0
- cli/config_store.py +276 -0
- cli/gitignore.py +43 -0
- cli/inventory.py +46 -0
- cli/keys.py +101 -0
- cli/persona.py +17 -0
- cli/polyrob.py +137 -0
- cli/ui/__init__.py +64 -0
- cli/ui/activity.py +141 -0
- cli/ui/app.py +538 -0
- cli/ui/banner.py +233 -0
- cli/ui/blocks.py +534 -0
- cli/ui/bootstrap_notice.py +47 -0
- cli/ui/commands/__init__.py +41 -0
- cli/ui/commands/h_cron.py +64 -0
- cli/ui/commands/h_journey.py +164 -0
- cli/ui/commands/h_kb.py +121 -0
- cli/ui/commands/h_learn.py +100 -0
- cli/ui/commands/h_mcp.py +215 -0
- cli/ui/commands/h_self.py +113 -0
- cli/ui/commands/h_skills.py +314 -0
- cli/ui/commands/handlers.py +1672 -0
- cli/ui/commands/registry.py +426 -0
- cli/ui/dialog.py +425 -0
- cli/ui/event_registry.py +110 -0
- cli/ui/event_specs.py +45 -0
- cli/ui/events.py +433 -0
- cli/ui/identity.py +15 -0
- cli/ui/lifecycle.py +159 -0
- cli/ui/live_hooks.py +43 -0
- cli/ui/model_selector.py +532 -0
- cli/ui/persistent_loop.py +181 -0
- cli/ui/pick.py +113 -0
- cli/ui/plain_renderer.py +351 -0
- cli/ui/renderer.py +384 -0
- cli/ui/rich_renderer.py +533 -0
- cli/ui/secrets.py +115 -0
- cli/ui/state.py +376 -0
- cli/ui/statusbar.py +231 -0
- cli/ui/streaming.py +254 -0
- cli/ui/terminal_render.py +105 -0
- cli/ui/theme.py +132 -0
- cli/update/__init__.py +6 -0
- cli/update/context.py +121 -0
- cli/update/detect.py +176 -0
- cli/update/engine.py +95 -0
- cli/update/migrate_guarded.py +55 -0
- cli/update/process_guard.py +215 -0
- cli/update/runners.py +78 -0
- cli/update/snapshot.py +270 -0
- cli/update/versions.py +205 -0
- core/README.md +377 -0
- core/__init__.py +174 -0
- core/assets.py +87 -0
- core/async_bridge.py +76 -0
- core/autonomy_runtime.py +259 -0
- core/base_component.py +302 -0
- core/bootstrap.py +681 -0
- core/bot.py +310 -0
- core/config.py +833 -0
- core/constants.py +126 -0
- core/container.py +462 -0
- core/db_manifest.py +101 -0
- core/embedding.py +57 -0
- core/env.py +39 -0
- core/exceptions.py +287 -0
- core/flags.py +144 -0
- core/flags_catalog.py +345 -0
- core/home_migration.py +62 -0
- core/identity.py +118 -0
- core/initialization.py +1004 -0
- core/instance.py +537 -0
- core/interactive_gate.py +96 -0
- core/logging.py +436 -0
- core/owner_doc_writer.py +176 -0
- core/pairing.py +193 -0
- core/path_safety.py +20 -0
- core/paths.py +27 -0
- core/payment_config.py +21 -0
- core/permissions.py +322 -0
- core/runtime_config.py +96 -0
- core/runtime_paths.py +135 -0
- core/seams.py +35 -0
- core/secret_scan.py +25 -0
- core/secret_scrub.py +71 -0
- core/secrets.py +35 -0
- core/security_logging_filter.py +148 -0
- core/self_context_writer.py +333 -0
- core/self_evolution.py +294 -0
- core/session_context.py +47 -0
- core/sqlite_util.py +59 -0
- core/surfaces/__init__.py +16 -0
- core/surfaces/access.py +118 -0
- core/surfaces/binding.py +88 -0
- core/surfaces/bootstrap.py +78 -0
- core/surfaces/circuit.py +168 -0
- core/surfaces/continuity.py +77 -0
- core/surfaces/correspondents.py +228 -0
- core/surfaces/dispatcher.py +216 -0
- core/surfaces/envelopes.py +75 -0
- core/surfaces/gc.py +30 -0
- core/surfaces/idempotency.py +57 -0
- core/surfaces/inbound_webhook.py +124 -0
- core/surfaces/media.py +32 -0
- core/surfaces/message_router.py +91 -0
- core/surfaces/outbound_allowlist.py +80 -0
- core/surfaces/outbound_dispatcher.py +110 -0
- core/surfaces/outbound_mirror.py +37 -0
- core/surfaces/outbound_queue.py +107 -0
- core/surfaces/outbound_target.py +12 -0
- core/surfaces/owner_admin.py +29 -0
- core/surfaces/proactive.py +78 -0
- core/surfaces/progress.py +108 -0
- core/surfaces/rate_bucket.py +21 -0
- core/surfaces/registry.py +51 -0
- core/surfaces/rendering.py +53 -0
- core/surfaces/send_policy.py +14 -0
- core/surfaces/serialize.py +24 -0
- core/surfaces/session_chat_registry.py +112 -0
- core/surfaces/session_policy.py +60 -0
- core/surfaces/surface.py +288 -0
- core/surfaces/transcription.py +88 -0
- core/surfaces/voice_echo.py +26 -0
- core/surfaces/voice_guard.py +23 -0
- core/tickers.py +174 -0
- core/tool_catalog.py +192 -0
- core/version.py +75 -0
- core/wallet/__init__.py +5 -0
- core/wallet/agent_wallet.py +59 -0
- core/wallet/audit_sink.py +61 -0
- core/wallet/config.py +70 -0
- core/wallet/factory.py +87 -0
- core/wallet/policy.py +104 -0
- core/wallet/signer.py +51 -0
- cron/README.md +65 -0
- cron/__init__.py +0 -0
- cron/delivery.py +277 -0
- cron/digest.py +124 -0
- cron/jobs.py +207 -0
- cron/runner.py +297 -0
- cron/schedule.py +194 -0
- cron/scheduler.py +201 -0
- cron/service.py +57 -0
- cron/wake_gate.py +223 -0
- data/__init__.py +1 -0
- data/prompts/autov2_prompts.json +87 -0
- data/prompts/skills/browser-automation/SKILL.md +43 -0
- data/prompts/skills/coding-workflow/SKILL.md +36 -0
- data/prompts/skills/crypto-trading-safety/SKILL.md +53 -0
- data/prompts/skills/document-writing/SKILL.md +33 -0
- data/prompts/skills/email-comms/SKILL.md +33 -0
- data/prompts/skills/file-data-ops/SKILL.md +35 -0
- data/prompts/skills/hyperliquid-account-review/SKILL.md +48 -0
- data/prompts/skills/hyperliquid-market-data/SKILL.md +52 -0
- data/prompts/skills/hyperliquid-trading/SKILL.md +59 -0
- data/prompts/skills/lead-research/SKILL.md +86 -0
- data/prompts/skills/market-research-brief/SKILL.md +104 -0
- data/prompts/skills/person-analyzer/SKILL.md +124 -0
- data/prompts/skills/polymarket-market-research/SKILL.md +54 -0
- data/prompts/skills/polymarket-portfolio-review/SKILL.md +50 -0
- data/prompts/skills/polymarket-trading/SKILL.md +55 -0
- data/prompts/skills/presentation-creator/SKILL.md +36 -0
- data/prompts/skills/project-analyzer/SKILL.md +169 -0
- data/prompts/skills/rules.json +268 -0
- data/prompts/skills/secret-handling/SKILL.md +20 -0
- data/prompts/skills/skill-authoring/SKILL.md +31 -0
- data/prompts/skills/skill-security-review/SKILL.md +27 -0
- data/prompts/skills/social-discovery/SKILL.md +57 -0
- data/prompts/skills/task-planning/SKILL.md +33 -0
- data/prompts/skills/web-research/SKILL.md +39 -0
- data/prompts/skills/web-scraping/SKILL.md +73 -0
- data/prompts/skills/x-engagement/SKILL.md +69 -0
- data/prompts/system_prompts.json +4 -0
- data/subscription_channels.json +5 -0
- modules/README.md +503 -0
- modules/__init__.py +124 -0
- modules/auth/__init__.py +13 -0
- modules/auth/api_key_manager.py +157 -0
- modules/auth/identity_mapper.py +380 -0
- modules/auth/siwe_auth.py +273 -0
- modules/auth/tier_manager.py +173 -0
- modules/base_module.py +190 -0
- modules/credits/__init__.py +29 -0
- modules/credits/balance_manager.py +237 -0
- modules/credits/cost_utils.py +129 -0
- modules/credits/pricing.py +139 -0
- modules/credits/unified_ledger.py +148 -0
- modules/credits/usage_meter.py +310 -0
- modules/credits/usage_tracker.py +674 -0
- modules/database/__init__.py +33 -0
- modules/database/audit_log.py +322 -0
- modules/database/auth_tables.py +276 -0
- modules/database/connection.py +644 -0
- modules/database/connection_pool.py +305 -0
- modules/database/conversation_contexts.py +446 -0
- modules/database/database_manager.py +237 -0
- modules/database/hyperliquid.py +375 -0
- modules/database/polymarket.py +574 -0
- modules/database/user_mcp_servers.py +728 -0
- modules/database/user_profiles.py +562 -0
- modules/database/utils.py +107 -0
- modules/database/x402_tables.py +92 -0
- modules/eip8004/README.md +1069 -0
- modules/eip8004/__init__.py +65 -0
- modules/eip8004/contracts.py +498 -0
- modules/eip8004/models.py +271 -0
- modules/eip8004/registration.py +148 -0
- modules/eip8004/reputation.py +367 -0
- modules/eip8004/validation.py +277 -0
- modules/llm/__init__.py +97 -0
- modules/llm/adapters.py +963 -0
- modules/llm/anthropic_client.py +831 -0
- modules/llm/available_models.py +106 -0
- modules/llm/brain_scrubber.py +137 -0
- modules/llm/cache_hints.py +162 -0
- modules/llm/deepseek_client.py +562 -0
- modules/llm/gemini_client.py +1367 -0
- modules/llm/llm_client.py +1139 -0
- modules/llm/llm_client_registry.py +188 -0
- modules/llm/llm_factory.py +237 -0
- modules/llm/llm_manager.py +1036 -0
- modules/llm/messages.py +374 -0
- modules/llm/model_registry.py +1935 -0
- modules/llm/nvidia_client.py +125 -0
- modules/llm/openai_client.py +800 -0
- modules/llm/openrouter_client.py +969 -0
- modules/llm/profiles.py +178 -0
- modules/llm/test_anthropic_cache_capture.py +35 -0
- modules/llm/test_cached_pricing_derivation.py +24 -0
- modules/llm/think_scrubber.py +444 -0
- modules/llm/token_counter.py +268 -0
- modules/memory/__init__.py +35 -0
- modules/memory/backend_factory.py +83 -0
- modules/memory/cache_manager.py +186 -0
- modules/memory/episodic.py +108 -0
- modules/memory/local_vector_memory_provider.py +552 -0
- modules/memory/memory_manager.py +225 -0
- modules/memory/models.py +312 -0
- modules/memory/provider.py +210 -0
- modules/memory/registry.py +205 -0
- modules/memory/sqlite_memory_provider.py +767 -0
- modules/memory/task/__init__.py +52 -0
- modules/memory/task/compaction_manager.py +157 -0
- modules/memory/task/context_retriever.py +809 -0
- modules/memory/task/hierarchical_memory.py +1002 -0
- modules/memory/task/lexical_retriever.py +71 -0
- modules/memory/task/null_context_manager.py +213 -0
- modules/memory/task/phase_manager.py +625 -0
- modules/memory/task/reflection_service.py +157 -0
- modules/memory/task/semantic_retriever.py +218 -0
- modules/memory/task/task_context_manager.py +1165 -0
- modules/memory/task/test_forgetting_engages.py +24 -0
- modules/memory/task/test_reflection_llm.py +39 -0
- modules/memory/task/test_threat_scan.py +53 -0
- modules/memory/task/threat_scan.py +90 -0
- modules/memory/test_sqlite_memory_provider.py +106 -0
- modules/memory/user_profile_manager.py +527 -0
- modules/payments/__init__.py +11 -0
- modules/payments/deposit_monitor.py +478 -0
- modules/payments/price_oracle.py +70 -0
- modules/payments/treasury_sweeper.py +401 -0
- modules/payments/wallet_generator.py +145 -0
- modules/pfp/__init__.py +1 -0
- modules/pfp/config.py +92 -0
- modules/pfp/mesh.py +565 -0
- modules/pfp/push.py +69 -0
- modules/pfp/renderer.py +85 -0
- modules/pfp/store.py +82 -0
- modules/skills/__init__.py +1 -0
- modules/skills/skill_usage.py +248 -0
- modules/transcription/__init__.py +24 -0
- modules/transcription/base.py +20 -0
- modules/transcription/faster_whisper_transcriber.py +55 -0
- modules/x402/README.md +249 -0
- modules/x402/__init__.py +22 -0
- modules/x402/invoicing.py +416 -0
- modules/x402/middleware.py +364 -0
- modules/x402/settlement_watcher.py +145 -0
- modules/x402/x402_integration.py +344 -0
- polyrob-0.5.0.dist-info/METADATA +482 -0
- polyrob-0.5.0.dist-info/RECORD +745 -0
- polyrob-0.5.0.dist-info/WHEEL +5 -0
- polyrob-0.5.0.dist-info/entry_points.txt +2 -0
- polyrob-0.5.0.dist-info/licenses/LICENSE +21 -0
- polyrob-0.5.0.dist-info/top_level.txt +12 -0
- surfaces/README.md +70 -0
- surfaces/__init__.py +0 -0
- surfaces/email/__init__.py +0 -0
- surfaces/email/dedup.py +44 -0
- surfaces/email/harness.py +187 -0
- surfaces/email/inbound.py +159 -0
- surfaces/email/seed.py +60 -0
- surfaces/email/surface.py +105 -0
- surfaces/telegram/__init__.py +0 -0
- surfaces/telegram/dedup.py +83 -0
- surfaces/telegram/harness.py +886 -0
- surfaces/telegram/inbound.py +145 -0
- surfaces/telegram/interactive_tools.py +49 -0
- surfaces/telegram/markdown.py +347 -0
- surfaces/telegram/rate_limit.py +167 -0
- surfaces/telegram/surface.py +187 -0
- surfaces/telegram/voice.py +52 -0
- surfaces/whatsapp/__init__.py +0 -0
- surfaces/whatsapp/client.py +51 -0
- surfaces/whatsapp/harness.py +62 -0
- surfaces/whatsapp/inbound.py +132 -0
- surfaces/whatsapp/surface.py +64 -0
- surfaces/whatsapp/window.py +34 -0
- tools/README.md +578 -0
- tools/__init__.py +424 -0
- tools/alchemy/__init__.py +5 -0
- tools/alchemy/alchemy_tool.py +389 -0
- tools/anysite/__init__.py +17 -0
- tools/anysite/client.py +88 -0
- tools/anysite/tool.py +89 -0
- tools/base_tool.py +468 -0
- tools/browser/__init__.py +22 -0
- tools/browser/actions.py +118 -0
- tools/browser/browser.py +1457 -0
- tools/browser/browser_manager.py +690 -0
- tools/browser/context.py +1748 -0
- tools/browser/playwright_utils.py +137 -0
- tools/browser/views.py +56 -0
- tools/code_exec/SANDBOX_SECURITY.md +144 -0
- tools/code_exec/__init__.py +178 -0
- tools/code_exec/backend.py +73 -0
- tools/code_exec/backends/__init__.py +1 -0
- tools/code_exec/backends/docker.py +779 -0
- tools/code_exec/backends/local_subprocess.py +154 -0
- tools/code_exec/env_policy.py +45 -0
- tools/code_exec/result.py +42 -0
- tools/code_exec/sandbox_guard.py +59 -0
- tools/code_exec/tool.py +227 -0
- tools/coding/__init__.py +50 -0
- tools/coding/edit.py +107 -0
- tools/coding/search.py +113 -0
- tools/coding/tool.py +353 -0
- tools/collabland/__init__.py +5 -0
- tools/collabland/collabland_tool.py +471 -0
- tools/controller/__init__.py +0 -0
- tools/controller/_helpers.py +165 -0
- tools/controller/action_registration.py +1765 -0
- tools/controller/approval.py +244 -0
- tools/controller/approval_interactive.py +90 -0
- tools/controller/delegation.py +195 -0
- tools/controller/execution.py +797 -0
- tools/controller/execution_context.py +141 -0
- tools/controller/hooks.py +135 -0
- tools/controller/introspection.py +344 -0
- tools/controller/mcp_registrar.py +190 -0
- tools/controller/message_send.py +31 -0
- tools/controller/registry/__init__.py +4 -0
- tools/controller/registry/schema_generators.py +395 -0
- tools/controller/registry/schema_sanitizer.py +406 -0
- tools/controller/registry/service.py +1266 -0
- tools/controller/registry/views.py +151 -0
- tools/controller/service.py +409 -0
- tools/controller/tool_management.py +334 -0
- tools/controller/types.py +28 -0
- tools/controller/views.py +450 -0
- tools/cronjob_tools.py +166 -0
- tools/crypto_trade_gate.py +59 -0
- tools/descriptors.py +452 -0
- tools/dom/__init__.py +0 -0
- tools/dom/history_tree_processor/service.py +244 -0
- tools/dom/history_tree_processor/view.py +99 -0
- tools/dom/service.py +471 -0
- tools/dom/views.py +346 -0
- tools/email_tool.py +403 -0
- tools/exceptions.py +169 -0
- tools/filesystem.py +1038 -0
- tools/filesystem_docproc.py +331 -0
- tools/filesystem_pdf.py +755 -0
- tools/git/__init__.py +42 -0
- tools/git/tool.py +258 -0
- tools/github/__init__.py +33 -0
- tools/github/client.py +94 -0
- tools/github/tool.py +237 -0
- tools/goal_tools.py +309 -0
- tools/hyperliquid/__init__.py +56 -0
- tools/hyperliquid/models.py +233 -0
- tools/hyperliquid/service.py +1680 -0
- tools/knowledge_ingest.py +950 -0
- tools/mcp/README.md +535 -0
- tools/mcp/__init__.py +52 -0
- tools/mcp/catalog.py +115 -0
- tools/mcp/config.py +337 -0
- tools/mcp/mcp_tool.py +1415 -0
- tools/mcp/param_coercion.py +289 -0
- tools/mcp/protocol.py +1520 -0
- tools/mcp/rate_limit.py +50 -0
- tools/mcp/security.py +424 -0
- tools/mcp/self_install.py +111 -0
- tools/mcp/server_manager.py +829 -0
- tools/mcp/subscriptions.py +72 -0
- tools/mcp/user_mcp_service.py +899 -0
- tools/mcp/validation_tracker.py +89 -0
- tools/mcp/views.py +346 -0
- tools/oauth/__init__.py +17 -0
- tools/oauth/manager.py +75 -0
- tools/oauth/provider.py +69 -0
- tools/oauth/providers/__init__.py +1 -0
- tools/oauth/providers/generic_oauth2.py +96 -0
- tools/perplexity_tool.py +280 -0
- tools/polymarket/__init__.py +41 -0
- tools/polymarket/clob_adapter.py +76 -0
- tools/polymarket/models.py +182 -0
- tools/polymarket/service.py +2082 -0
- tools/self_env/__init__.py +61 -0
- tools/self_env/tool.py +274 -0
- tools/shell/__init__.py +80 -0
- tools/shell/backend_pool.py +118 -0
- tools/shell/discipline.py +62 -0
- tools/shell/executor.py +157 -0
- tools/shell/loopback_allow.py +87 -0
- tools/shell/process_registry.py +113 -0
- tools/shell/process_tool.py +131 -0
- tools/shell/state.py +129 -0
- tools/shell/tool.py +151 -0
- tools/task_tool.py +637 -0
- tools/twitter_tool.py +2086 -0
- tools/user_directory.py +179 -0
- tools/web_fetch/__init__.py +3 -0
- tools/web_fetch/fetcher.py +144 -0
- tools/web_fetch/render.py +44 -0
- tools/web_fetch/tool.py +60 -0
- tools/x402/__init__.py +60 -0
- tools/x402/client.py +47 -0
- tools/x402/invoice_tool.py +143 -0
- tools/x402/real_client.py +250 -0
- tools/x402/service.py +131 -0
- utils/README.md +498 -0
- utils/__init__.py +46 -0
- utils/auth_utils.py +114 -0
- utils/bounded_collections.py +162 -0
- utils/circuit_breaker.py +389 -0
- utils/gif_utils.py +435 -0
- utils/markdown_utils.py +291 -0
- utils/message_utils.py +147 -0
- utils/metrics.py +36 -0
- utils/path_validator.py +227 -0
- utils/rate_limit_manager.py +399 -0
- utils/result_size.py +296 -0
- utils/time_utils.py +206 -0
- utils/user_utils.py +188 -0
- webview/README.md +111 -0
- webview/RENAME_AND_ALIGN_HANDOFF.md +200 -0
- webview/__init__.py +8 -0
- webview/activity.py +636 -0
- webview/owner_auth.py +108 -0
- webview/pages.py +448 -0
- webview/repair_sessions.py +399 -0
- webview/server.py +4323 -0
- webview/server_launcher.py +112 -0
- webview/static/css/activity.css +89 -0
- webview/static/css/chat.css +1772 -0
- webview/static/css/components.css +1190 -0
- webview/static/css/config-panel.css +651 -0
- webview/static/css/pages/admin.css +766 -0
- webview/static/css/pages/profile.css +231 -0
- webview/static/css/pages/settings.css +297 -0
- webview/static/css/pages/signin.css +316 -0
- webview/static/css/style.css +4250 -0
- webview/static/css/variables.css +147 -0
- webview/static/css/workspace-fullwidth-fix.css +109 -0
- webview/static/img/favicon.ico +0 -0
- webview/static/js/activity.js +394 -0
- webview/static/js/admin/activity.js +186 -0
- webview/static/js/admin/dashboard.js +163 -0
- webview/static/js/admin/user_detail.js +400 -0
- webview/static/js/admin/users.js +174 -0
- webview/static/js/admin/utils.js +214 -0
- webview/static/js/chat.js +3178 -0
- webview/static/js/config-panel.js +522 -0
- webview/static/js/constants.js +20 -0
- webview/static/js/error-handler.js +226 -0
- webview/static/js/ethers.min.js +1 -0
- webview/static/js/event-filter.js +137 -0
- webview/static/js/event-store.js +351 -0
- webview/static/js/file-attachments.js +374 -0
- webview/static/js/file-loader.js +163 -0
- webview/static/js/index.js +103 -0
- webview/static/js/performance-utils.js +196 -0
- webview/static/js/profile.js +396 -0
- webview/static/js/screenshot.js +386 -0
- webview/static/js/session.js +1527 -0
- webview/static/js/settings.js +1288 -0
- webview/static/js/sidebar-data.js +766 -0
- webview/static/js/sidebar-toggle.js +296 -0
- webview/static/js/socket.io.min.js +7 -0
- webview/static/js/stats.js +355 -0
- webview/static/js/ui-utils.js +420 -0
- webview/static/js/workspace.js +966 -0
- webview/stats_service.py +613 -0
- webview/templates/__init__.py +1 -0
- webview/templates/__pycache__/__init__.cpython-311.pyc +0 -0
- webview/templates/activity.html +36 -0
- webview/templates/admin/activity.html +122 -0
- webview/templates/admin/dashboard.html +112 -0
- webview/templates/admin/user_detail.html +228 -0
- webview/templates/admin/users.html +105 -0
- webview/templates/autonomy.html +60 -0
- webview/templates/error.html +133 -0
- webview/templates/finance.html +90 -0
- webview/templates/identity.html +73 -0
- webview/templates/index.html +42 -0
- webview/templates/layout.html +125 -0
- webview/templates/memory.html +59 -0
- webview/templates/owner_login.html +15 -0
- webview/templates/profile.html +121 -0
- webview/templates/session.html +312 -0
- webview/templates/settings.html +534 -0
- webview/templates/sidebar.html +112 -0
- webview/templates/signin.html +213 -0
- webview/templates/status.html +14 -0
- webview/templates/system.html +45 -0
- 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)()
|