snippbot 0.1.0b1__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.
- snippbot/__init__.py +18 -0
- snippbot/__main__.py +6 -0
- snippbot/api/__init__.py +16 -0
- snippbot/api/agents.py +603 -0
- snippbot/api/api_handler.py +2611 -0
- snippbot/api/approval.py +111 -0
- snippbot/api/assets.py +629 -0
- snippbot/api/audio.py +276 -0
- snippbot/api/auth_helpers.py +79 -0
- snippbot/api/browser.py +507 -0
- snippbot/api/browser_ws.py +214 -0
- snippbot/api/channels.py +1167 -0
- snippbot/api/chat.py +3812 -0
- snippbot/api/chat_files.py +366 -0
- snippbot/api/custom_providers.py +518 -0
- snippbot/api/device_auth.py +480 -0
- snippbot/api/device_ws.py +43 -0
- snippbot/api/devices.py +1537 -0
- snippbot/api/dispatcher.py +369 -0
- snippbot/api/email.py +114 -0
- snippbot/api/equipment.py +177 -0
- snippbot/api/events_ws.py +98 -0
- snippbot/api/execution.py +2761 -0
- snippbot/api/files.py +351 -0
- snippbot/api/game_engine.py +438 -0
- snippbot/api/game_saves.py +166 -0
- snippbot/api/health.py +117 -0
- snippbot/api/hooks.py +735 -0
- snippbot/api/image_gen.py +307 -0
- snippbot/api/insights.py +584 -0
- snippbot/api/internal_token.py +74 -0
- snippbot/api/issues.py +711 -0
- snippbot/api/license.py +96 -0
- snippbot/api/marketplace.py +3753 -0
- snippbot/api/marketplace_auth.py +171 -0
- snippbot/api/marketplace_oauth.py +315 -0
- snippbot/api/mcp_refresh.py +264 -0
- snippbot/api/memory_capture.py +402 -0
- snippbot/api/memory_maintenance.py +349 -0
- snippbot/api/monitor.py +241 -0
- snippbot/api/nodes.py +263 -0
- snippbot/api/package_builder.py +33 -0
- snippbot/api/package_credentials.py +254 -0
- snippbot/api/recall_feedback_task.py +99 -0
- snippbot/api/reflection_capture.py +819 -0
- snippbot/api/remote_sessions.py +796 -0
- snippbot/api/router_settings.py +80 -0
- snippbot/api/routes.py +13111 -0
- snippbot/api/sandbox.py +706 -0
- snippbot/api/scenarios.py +129 -0
- snippbot/api/scheduler.py +932 -0
- snippbot/api/security.py +318 -0
- snippbot/api/setup.py +1384 -0
- snippbot/api/skill_builder.py +1751 -0
- snippbot/api/sub_agents.py +793 -0
- snippbot/api/sync.py +108 -0
- snippbot/api/thinking.py +730 -0
- snippbot/api/tool_metadata_cache.py +145 -0
- snippbot/api/wallet.py +846 -0
- snippbot/api/workflows.py +843 -0
- snippbot/api/workspaces.py +435 -0
- snippbot/api/world.py +334 -0
- snippbot/api/world_map.py +211 -0
- snippbot/channel_adapter/__init__.py +5 -0
- snippbot/channel_adapter/__main__.py +92 -0
- snippbot/channel_adapter/adapters/__init__.py +50 -0
- snippbot/channel_adapter/adapters/discord.py +376 -0
- snippbot/channel_adapter/adapters/email.py +477 -0
- snippbot/channel_adapter/adapters/google_chat.py +504 -0
- snippbot/channel_adapter/adapters/slack.py +311 -0
- snippbot/channel_adapter/adapters/teams.py +312 -0
- snippbot/channel_adapter/adapters/telegram.py +377 -0
- snippbot/channel_adapter/adapters/webhook.py +321 -0
- snippbot/channel_adapter/adapters/whatsapp.py +286 -0
- snippbot/channel_adapter/app.py +504 -0
- snippbot/channel_adapter/attachments.py +92 -0
- snippbot/channel_adapter/base.py +146 -0
- snippbot/channel_adapter/conversations.py +256 -0
- snippbot/channel_adapter/dispatch.py +332 -0
- snippbot/channel_adapter/loader.py +190 -0
- snippbot/channel_adapter/manager.py +120 -0
- snippbot/channel_adapter/models.py +85 -0
- snippbot/channel_adapter/rate_limiter.py +101 -0
- snippbot/channel_adapter/router.py +161 -0
- snippbot/channel_adapter/space_history.py +182 -0
- snippbot/channel_adapter/tunnel.py +134 -0
- snippbot/cli.py +11 -0
- snippbot/daemon.py +435 -0
- snippbot/discovery.py +117 -0
- snippbot/server.py +2346 -0
- snippbot/ui/_x9b404ae321ef/snake.html +283 -0
- snippbot/ui/_x9b404ae321ef/space-platformer.html +1132 -0
- snippbot/ui/_x9b404ae321ef/space_jumper.html +904 -0
- snippbot/ui/_x9b404ae321ef/tower-defense.html +928 -0
- snippbot/ui/assets/BrowserPage-BrImTNge.js +14 -0
- snippbot/ui/assets/BrowserPage-BrImTNge.js.map +1 -0
- snippbot/ui/assets/DeviceDetailPage-CCKl05v2.js +2 -0
- snippbot/ui/assets/DeviceDetailPage-CCKl05v2.js.map +1 -0
- snippbot/ui/assets/DevicesPage-BCSJuf6M.js +6 -0
- snippbot/ui/assets/DevicesPage-BCSJuf6M.js.map +1 -0
- snippbot/ui/assets/SandboxPage-CqC7ScBJ.js +27 -0
- snippbot/ui/assets/SandboxPage-CqC7ScBJ.js.map +1 -0
- snippbot/ui/assets/SecurityDashboardPage-qDDNLxfv.js +2 -0
- snippbot/ui/assets/SecurityDashboardPage-qDDNLxfv.js.map +1 -0
- snippbot/ui/assets/StepOutputPreview-BZV40eAE.css +1 -0
- snippbot/ui/assets/StepOutputPreview-DswkVuEc.js +13 -0
- snippbot/ui/assets/StepOutputPreview-DswkVuEc.js.map +1 -0
- snippbot/ui/assets/WorkflowBuilderPage-BrdjcQCK.js +367 -0
- snippbot/ui/assets/WorkflowBuilderPage-BrdjcQCK.js.map +1 -0
- snippbot/ui/assets/WorkflowRunPage-DfB_yOVw.js +2 -0
- snippbot/ui/assets/WorkflowRunPage-DfB_yOVw.js.map +1 -0
- snippbot/ui/assets/WorkflowsPage-C-0d296F.js +2 -0
- snippbot/ui/assets/WorkflowsPage-C-0d296F.js.map +1 -0
- snippbot/ui/assets/index-BKZiwq1o.js +2 -0
- snippbot/ui/assets/index-BKZiwq1o.js.map +1 -0
- snippbot/ui/assets/index-CqrRydCQ.js +1275 -0
- snippbot/ui/assets/index-CqrRydCQ.js.map +1 -0
- snippbot/ui/assets/index-vyj8Wd2X.css +1 -0
- snippbot/ui/assets/logo-alt-D_oF903h.png +0 -0
- snippbot/ui/assets/logo-alt-icon-Dt-XLW7h.png +0 -0
- snippbot/ui/assets/logo-d_fNF9Hk.png +0 -0
- snippbot/ui/assets/logo-icon-C0v6NQ9j.png +0 -0
- snippbot/ui/assets/state-BOtVR23t.js +26 -0
- snippbot/ui/assets/state-BOtVR23t.js.map +1 -0
- snippbot/ui/assets/stepIcons-BL48dla-.js +72 -0
- snippbot/ui/assets/stepIcons-BL48dla-.js.map +1 -0
- snippbot/ui/assets/ui-BDp1HqTB.js +10 -0
- snippbot/ui/assets/ui-BDp1HqTB.js.map +1 -0
- snippbot/ui/assets/vendor-C1G_MATc.js +60 -0
- snippbot/ui/assets/vendor-C1G_MATc.js.map +1 -0
- snippbot/ui/audio/ambient-setup.mp3 +0 -0
- snippbot/ui/favicon.png +0 -0
- snippbot/ui/frames/common.png +0 -0
- snippbot/ui/frames/epic.png +0 -0
- snippbot/ui/frames/legendary.png +0 -0
- snippbot/ui/frames/rare.png +0 -0
- snippbot/ui/frames/uncommon.png +0 -0
- snippbot/ui/game/title-bg.png +0 -0
- snippbot/ui/index.html +28 -0
- snippbot/ui/manifest.json +48 -0
- snippbot/ui/old_frames/common.png +0 -0
- snippbot/ui/old_frames/epic.png +0 -0
- snippbot/ui/old_frames/legendary.png +0 -0
- snippbot/ui/old_frames/rare.png +0 -0
- snippbot/ui/old_frames/uncommon.png +0 -0
- snippbot/ui/sw.js +137 -0
- snippbot/wallet/__init__.py +34 -0
- snippbot/wallet/manager.py +656 -0
- snippbot/websocket/__init__.py +1 -0
- snippbot/websocket/websocket_handler.py +640 -0
- snippbot-0.1.0b1.dist-info/METADATA +177 -0
- snippbot-0.1.0b1.dist-info/RECORD +708 -0
- snippbot-0.1.0b1.dist-info/WHEEL +4 -0
- snippbot-0.1.0b1.dist-info/entry_points.txt +2 -0
- snippbot_cli/__init__.py +7 -0
- snippbot_cli/checks.py +696 -0
- snippbot_cli/commands/__init__.py +1 -0
- snippbot_cli/commands/agents.py +200 -0
- snippbot_cli/commands/auth.py +361 -0
- snippbot_cli/commands/channel.py +147 -0
- snippbot_cli/commands/completions.py +102 -0
- snippbot_cli/commands/config.py +168 -0
- snippbot_cli/commands/device.py +241 -0
- snippbot_cli/commands/dispatcher.py +547 -0
- snippbot_cli/commands/doctor.py +148 -0
- snippbot_cli/commands/export.py +133 -0
- snippbot_cli/commands/marketplace.py +2686 -0
- snippbot_cli/commands/project.py +195 -0
- snippbot_cli/commands/reset.py +660 -0
- snippbot_cli/commands/secrets.py +263 -0
- snippbot_cli/commands/security.py +432 -0
- snippbot_cli/commands/setup.py +366 -0
- snippbot_cli/commands/start.py +123 -0
- snippbot_cli/commands/status.py +92 -0
- snippbot_cli/commands/stop.py +167 -0
- snippbot_cli/daemon_client.py +101 -0
- snippbot_cli/main.py +47 -0
- snippbot_cli/user_config.py +437 -0
- snippbot_core/__init__.py +22 -0
- snippbot_core/agent_settings.py +188 -0
- snippbot_core/approvals.py +513 -0
- snippbot_core/assets/__init__.py +10 -0
- snippbot_core/assets/folder_store.py +403 -0
- snippbot_core/assets/store.py +383 -0
- snippbot_core/audio/__init__.py +15 -0
- snippbot_core/audio/models.py +98 -0
- snippbot_core/audio/providers/__init__.py +4 -0
- snippbot_core/audio/providers/base.py +55 -0
- snippbot_core/audio/providers/elevenlabs_stt.py +88 -0
- snippbot_core/audio/providers/elevenlabs_tts.py +153 -0
- snippbot_core/audio/providers/hume_tts.py +172 -0
- snippbot_core/audio/providers/local_stt.py +106 -0
- snippbot_core/audio/providers/local_tts.py +164 -0
- snippbot_core/audio/providers/openai_stt.py +76 -0
- snippbot_core/audio/providers/openai_tts.py +110 -0
- snippbot_core/audio/service.py +191 -0
- snippbot_core/audio/settings.py +96 -0
- snippbot_core/audit_service.py +367 -0
- snippbot_core/autonomy/__init__.py +22 -0
- snippbot_core/autonomy/bridge.py +140 -0
- snippbot_core/browser_settings.py +306 -0
- snippbot_core/capability_inference.py +131 -0
- snippbot_core/channel_store.py +1070 -0
- snippbot_core/chat/__init__.py +77 -0
- snippbot_core/chat/agent_store.py +585 -0
- snippbot_core/chat/agent_sync.py +244 -0
- snippbot_core/chat/chat_store.py +1410 -0
- snippbot_core/chat/command_processor.py +684 -0
- snippbot_core/chat/complexity_classifier.py +126 -0
- snippbot_core/chat/context_builder.py +721 -0
- snippbot_core/chat/context_refs.py +626 -0
- snippbot_core/chat/context_window.py +1500 -0
- snippbot_core/chat/gateway_client.py +626 -0
- snippbot_core/chat/model_router.py +217 -0
- snippbot_core/chat/models.py +190 -0
- snippbot_core/chat/prepare.py +1116 -0
- snippbot_core/chat/providers/__init__.py +33 -0
- snippbot_core/chat/providers/anthropic.py +213 -0
- snippbot_core/chat/providers/anthropic_api.py +846 -0
- snippbot_core/chat/providers/auth_checks.py +57 -0
- snippbot_core/chat/providers/base.py +120 -0
- snippbot_core/chat/providers/claude_native.py +2073 -0
- snippbot_core/chat/providers/custom.py +391 -0
- snippbot_core/chat/providers/deepseek.py +64 -0
- snippbot_core/chat/providers/gemini.py +577 -0
- snippbot_core/chat/providers/grok.py +25 -0
- snippbot_core/chat/providers/groq.py +23 -0
- snippbot_core/chat/providers/mistral.py +19 -0
- snippbot_core/chat/providers/openai.py +19 -0
- snippbot_core/chat/providers/openai_compat.py +522 -0
- snippbot_core/chat/providers/openrouter.py +36 -0
- snippbot_core/chat/providers/process_manager.py +538 -0
- snippbot_core/chat/providers/registry.py +473 -0
- snippbot_core/chat/roster.py +501 -0
- snippbot_core/chat/router_settings.py +218 -0
- snippbot_core/chat/routing.py +226 -0
- snippbot_core/chat/runtime_context.py +230 -0
- snippbot_core/chat/session.py +1001 -0
- snippbot_core/chat/session_context.py +90 -0
- snippbot_core/chat/sse.py +74 -0
- snippbot_core/chat/token_counter.py +426 -0
- snippbot_core/chat/tool_dispatch.py +364 -0
- snippbot_core/chat/tool_profile_merge.py +42 -0
- snippbot_core/chat/tool_result_aging.py +334 -0
- snippbot_core/chat/turn.py +1004 -0
- snippbot_core/chat/turn_orchestrator.py +113 -0
- snippbot_core/claude_cli.py +140 -0
- snippbot_core/compat.py +26 -0
- snippbot_core/config.py +772 -0
- snippbot_core/dag_builder.py +796 -0
- snippbot_core/db/__init__.py +17 -0
- snippbot_core/db/migrations.py +239 -0
- snippbot_core/device/__init__.py +25 -0
- snippbot_core/device/auth.py +1114 -0
- snippbot_core/device/capabilities.py +320 -0
- snippbot_core/device/execution.py +274 -0
- snippbot_core/device/file_transfer.py +507 -0
- snippbot_core/device/groups.py +251 -0
- snippbot_core/device/health.py +230 -0
- snippbot_core/device/manager.py +232 -0
- snippbot_core/device/pairing.py +380 -0
- snippbot_core/device/protocol.py +1499 -0
- snippbot_core/device/queue.py +193 -0
- snippbot_core/device/registry.py +1633 -0
- snippbot_core/device/router.py +278 -0
- snippbot_core/digest/__init__.py +18 -0
- snippbot_core/digest/composer.py +158 -0
- snippbot_core/digest/scheduler.py +187 -0
- snippbot_core/digest/store.py +189 -0
- snippbot_core/dispatch/__init__.py +131 -0
- snippbot_core/dispatch/bridge.py +347 -0
- snippbot_core/dispatch/decision_log.py +230 -0
- snippbot_core/dispatch/dispatcher.py +376 -0
- snippbot_core/dispatch/executors.py +447 -0
- snippbot_core/dispatch/idempotency.py +95 -0
- snippbot_core/dispatch/inject.py +244 -0
- snippbot_core/dispatch/policy_gate.py +82 -0
- snippbot_core/dispatch/proactive.py +379 -0
- snippbot_core/dispatch/proactivity_gate.py +92 -0
- snippbot_core/dispatch/settings.py +459 -0
- snippbot_core/dispatch/shadow.py +246 -0
- snippbot_core/dispatch/tier1_deterministic.py +100 -0
- snippbot_core/dispatch/tier2_policy.py +65 -0
- snippbot_core/dispatch/tier3_classifier.py +359 -0
- snippbot_core/dispatch/tier3_provider.py +386 -0
- snippbot_core/dispatch/tier4_fallback.py +67 -0
- snippbot_core/dispatch/types.py +218 -0
- snippbot_core/email/__init__.py +32 -0
- snippbot_core/email/config.py +29 -0
- snippbot_core/email/imap_client.py +317 -0
- snippbot_core/email/insight_email_scheduler.py +169 -0
- snippbot_core/email/service.py +156 -0
- snippbot_core/email/store.py +187 -0
- snippbot_core/email/subscriber.py +147 -0
- snippbot_core/email/templates.py +195 -0
- snippbot_core/event_bus.py +342 -0
- snippbot_core/events.py +1271 -0
- snippbot_core/execution_orchestrator.py +1401 -0
- snippbot_core/execution_store.py +1225 -0
- snippbot_core/executor.py +682 -0
- snippbot_core/files/__init__.py +18 -0
- snippbot_core/files/models.py +81 -0
- snippbot_core/files/pdf_extractor.py +77 -0
- snippbot_core/files/storage.py +201 -0
- snippbot_core/files/store.py +373 -0
- snippbot_core/game/__init__.py +10 -0
- snippbot_core/game/combat.py +624 -0
- snippbot_core/game/engine.py +358 -0
- snippbot_core/game/game_save_store.py +256 -0
- snippbot_core/game/gm_prompts.py +365 -0
- snippbot_core/game/world_store.py +1031 -0
- snippbot_core/history.py +797 -0
- snippbot_core/hooks/__init__.py +46 -0
- snippbot_core/hooks/analytics.py +346 -0
- snippbot_core/hooks/bridge.py +105 -0
- snippbot_core/hooks/bundled/__init__.py +122 -0
- snippbot_core/hooks/bundled/audit_logger.py +111 -0
- snippbot_core/hooks/bundled/boot_md.py +149 -0
- snippbot_core/hooks/bundled/context_files.py +144 -0
- snippbot_core/hooks/bundled/dispatcher_audit.py +93 -0
- snippbot_core/hooks/bundled/session_memory.py +110 -0
- snippbot_core/hooks/chains.py +208 -0
- snippbot_core/hooks/dispatcher.py +314 -0
- snippbot_core/hooks/engine.py +531 -0
- snippbot_core/hooks/executor.py +530 -0
- snippbot_core/hooks/filters.py +314 -0
- snippbot_core/hooks/models.py +441 -0
- snippbot_core/hooks/sandbox.py +222 -0
- snippbot_core/hooks/sdk.py +280 -0
- snippbot_core/hooks/store.py +1502 -0
- snippbot_core/hooks/webhook_auth.py +237 -0
- snippbot_core/idle_messages.py +59 -0
- snippbot_core/image/__init__.py +0 -0
- snippbot_core/image/generate.py +149 -0
- snippbot_core/image/ocr.py +113 -0
- snippbot_core/image/scene_prompts.py +129 -0
- snippbot_core/image/vision.py +324 -0
- snippbot_core/insight_generator.py +2176 -0
- snippbot_core/insight_queue.py +1278 -0
- snippbot_core/interface_settings.py +263 -0
- snippbot_core/issues/__init__.py +50 -0
- snippbot_core/issues/investigator.py +471 -0
- snippbot_core/issues/magic_link.py +86 -0
- snippbot_core/issues/models.py +314 -0
- snippbot_core/issues/notifier.py +175 -0
- snippbot_core/issues/security_audit.py +295 -0
- snippbot_core/issues/snapshot.py +67 -0
- snippbot_core/issues/store.py +1056 -0
- snippbot_core/license/__init__.py +14 -0
- snippbot_core/license/models.py +103 -0
- snippbot_core/license/store.py +136 -0
- snippbot_core/license/validator.py +155 -0
- snippbot_core/marketplace/__init__.py +64 -0
- snippbot_core/marketplace/agent_config_support.py +428 -0
- snippbot_core/marketplace/auto_updater.py +636 -0
- snippbot_core/marketplace/bundled.py +236 -0
- snippbot_core/marketplace/channel_support.py +201 -0
- snippbot_core/marketplace/essentials.py +45 -0
- snippbot_core/marketplace/essentials.toml +29 -0
- snippbot_core/marketplace/hook_support.py +302 -0
- snippbot_core/marketplace/ignore.py +203 -0
- snippbot_core/marketplace/installer.py +2015 -0
- snippbot_core/marketplace/job_support.py +251 -0
- snippbot_core/marketplace/manifest.py +953 -0
- snippbot_core/marketplace/manifest_facts.py +333 -0
- snippbot_core/marketplace/mcp_support.py +195 -0
- snippbot_core/marketplace/oauth_manager.py +534 -0
- snippbot_core/marketplace/oauth_providers.py +83 -0
- snippbot_core/marketplace/packaging.py +522 -0
- snippbot_core/marketplace/permission_grants.py +377 -0
- snippbot_core/marketplace/registry_store.py +428 -0
- snippbot_core/marketplace/sandbox_support.py +359 -0
- snippbot_core/marketplace/tool_support.py +997 -0
- snippbot_core/marketplace/workflow_support.py +828 -0
- snippbot_core/mcp/__init__.py +27 -0
- snippbot_core/mcp/bridge_registry.py +140 -0
- snippbot_core/mcp/catalog.py +202 -0
- snippbot_core/mcp/catalog_data.json +492 -0
- snippbot_core/mcp/client.py +651 -0
- snippbot_core/mcp/manager.py +368 -0
- snippbot_core/mcp/oauth.py +195 -0
- snippbot_core/mcp/sandbox.py +408 -0
- snippbot_core/mcp/tool_bridge_server.py +786 -0
- snippbot_core/memory/__init__.py +97 -0
- snippbot_core/memory/audit_log.py +338 -0
- snippbot_core/memory/clustering.py +349 -0
- snippbot_core/memory/consolidation.py +822 -0
- snippbot_core/memory/episodic.py +1097 -0
- snippbot_core/memory/forgetting.py +644 -0
- snippbot_core/memory/hybrid_search.py +697 -0
- snippbot_core/memory/keyword_search.py +622 -0
- snippbot_core/memory/knowledge_graph.py +1547 -0
- snippbot_core/memory/llm_extraction.py +718 -0
- snippbot_core/memory/recall_feedback.py +203 -0
- snippbot_core/memory/sensory_buffer.py +214 -0
- snippbot_core/memory/session.py +143 -0
- snippbot_core/memory/vector_index.py +740 -0
- snippbot_core/memory/write_pipeline.py +628 -0
- snippbot_core/memory_search.py +339 -0
- snippbot_core/memory_settings.py +482 -0
- snippbot_core/models_dev_catalog.py +387 -0
- snippbot_core/multimodal/__init__.py +44 -0
- snippbot_core/multimodal/image_blocks.py +230 -0
- snippbot_core/node_registry.py +399 -0
- snippbot_core/package_builder/__init__.py +70 -0
- snippbot_core/package_builder/engine.py +2116 -0
- snippbot_core/package_builder/fix_verifier.py +510 -0
- snippbot_core/package_builder/packager.py +100 -0
- snippbot_core/package_builder/plan.py +680 -0
- snippbot_core/package_builder/plugins/__init__.py +190 -0
- snippbot_core/package_builder/plugins/hook_plugin.py +389 -0
- snippbot_core/package_builder/plugins/mcp_server_plugin.py +497 -0
- snippbot_core/package_builder/plugins/tool_plugin.py +535 -0
- snippbot_core/package_builder/plugins/workflow_plugin.py +732 -0
- snippbot_core/package_builder/quality_audit.py +547 -0
- snippbot_core/package_builder/rate_limiter.py +242 -0
- snippbot_core/package_builder/sandbox.py +399 -0
- snippbot_core/package_builder/session.py +424 -0
- snippbot_core/package_builder/spec.py +246 -0
- snippbot_core/package_builder/store.py +582 -0
- snippbot_core/package_builder/system_prompt.py +561 -0
- snippbot_core/package_builder/telemetry.py +234 -0
- snippbot_core/payments/__init__.py +26 -0
- snippbot_core/payments/spend_authorization.py +231 -0
- snippbot_core/permissions.py +1099 -0
- snippbot_core/proactivity.py +975 -0
- snippbot_core/proactivity_settings.py +142 -0
- snippbot_core/profile_settings.py +299 -0
- snippbot_core/project_store.py +1349 -0
- snippbot_core/projects/__init__.py +7 -0
- snippbot_core/projects/plan_buffer.py +177 -0
- snippbot_core/projects/refine_agent.py +597 -0
- snippbot_core/projects/refine_lock.py +82 -0
- snippbot_core/projects/refine_session_registry.py +99 -0
- snippbot_core/projects/refine_tools.py +177 -0
- snippbot_core/provider_settings.py +555 -0
- snippbot_core/provider_sync.py +1082 -0
- snippbot_core/provider_sync_task.py +150 -0
- snippbot_core/providers.py +141 -0
- snippbot_core/push/__init__.py +30 -0
- snippbot_core/push/models.py +147 -0
- snippbot_core/push/service.py +393 -0
- snippbot_core/push/store.py +451 -0
- snippbot_core/py.typed +0 -0
- snippbot_core/remote_session/__init__.py +111 -0
- snippbot_core/remote_session/fan_out.py +287 -0
- snippbot_core/remote_session/manager.py +1010 -0
- snippbot_core/remote_session/models.py +361 -0
- snippbot_core/remote_session/security_gate.py +753 -0
- snippbot_core/remote_session/security_store.py +1393 -0
- snippbot_core/remote_session/settings.py +306 -0
- snippbot_core/remote_session/store.py +1041 -0
- snippbot_core/risk_scorer.py +512 -0
- snippbot_core/sandbox/__init__.py +66 -0
- snippbot_core/sandbox/audit.py +630 -0
- snippbot_core/sandbox/config.py +400 -0
- snippbot_core/sandbox/dockerfiles/Dockerfile.base +7 -0
- snippbot_core/sandbox/dockerfiles/Dockerfile.datascience +11 -0
- snippbot_core/sandbox/dockerfiles/Dockerfile.node +9 -0
- snippbot_core/sandbox/dockerfiles/Dockerfile.python +9 -0
- snippbot_core/sandbox/dockerfiles/Dockerfile.rust +9 -0
- snippbot_core/sandbox/gpu.py +311 -0
- snippbot_core/sandbox/manager.py +1317 -0
- snippbot_core/sandbox/network.py +863 -0
- snippbot_core/sandbox/pool.py +242 -0
- snippbot_core/sandbox/runtime/__init__.py +20 -0
- snippbot_core/sandbox/runtime/base.py +390 -0
- snippbot_core/sandbox/runtime/docker.py +1133 -0
- snippbot_core/sandbox/runtime/podman.py +1207 -0
- snippbot_core/sandbox/runtime/process.py +985 -0
- snippbot_core/sandbox/smart.py +205 -0
- snippbot_core/sandbox/snapshot.py +520 -0
- snippbot_core/sandbox/templates.py +894 -0
- snippbot_core/scheduler/__init__.py +31 -0
- snippbot_core/scheduler/chains.py +372 -0
- snippbot_core/scheduler/chat_commands.py +247 -0
- snippbot_core/scheduler/conditions.py +509 -0
- snippbot_core/scheduler/delivery.py +1058 -0
- snippbot_core/scheduler/engine.py +652 -0
- snippbot_core/scheduler/executor.py +953 -0
- snippbot_core/scheduler/models.py +493 -0
- snippbot_core/scheduler/nl_parser.py +716 -0
- snippbot_core/scheduler/nl_patterns.py +220 -0
- snippbot_core/scheduler/retry.py +102 -0
- snippbot_core/scheduler/schedule_types.py +431 -0
- snippbot_core/scheduler/sessions.py +174 -0
- snippbot_core/scheduler/skills_loader.py +366 -0
- snippbot_core/scheduler/smart.py +203 -0
- snippbot_core/scheduler/store.py +1382 -0
- snippbot_core/security/__init__.py +219 -0
- snippbot_core/security/csrf.py +323 -0
- snippbot_core/security/db_encryption.py +420 -0
- snippbot_core/security/dep_scanner.py +233 -0
- snippbot_core/security/dlp.py +374 -0
- snippbot_core/security/egress.py +312 -0
- snippbot_core/security/env_filter.py +157 -0
- snippbot_core/security/error_sanitizer.py +57 -0
- snippbot_core/security/file_permissions.py +267 -0
- snippbot_core/security/input_validator.py +155 -0
- snippbot_core/security/master_key.py +303 -0
- snippbot_core/security/mcp_validation.py +178 -0
- snippbot_core/security/package_audit.py +1176 -0
- snippbot_core/security/package_signing.py +631 -0
- snippbot_core/security/prompt_injection.py +527 -0
- snippbot_core/security/rate_limiter.py +348 -0
- snippbot_core/security/registry_security.py +281 -0
- snippbot_core/security/sandbox_profile.py +314 -0
- snippbot_core/security/scanner.py +1027 -0
- snippbot_core/security/secret_store.py +578 -0
- snippbot_core/security/skill_vetting.py +551 -0
- snippbot_core/security/tls.py +192 -0
- snippbot_core/security/url_validation.py +259 -0
- snippbot_core/security/workflow_audit.py +406 -0
- snippbot_core/security_settings.py +339 -0
- snippbot_core/settings_manager.py +279 -0
- snippbot_core/settings_store.py +79 -0
- snippbot_core/skill_builder/__init__.py +53 -0
- snippbot_core/skill_builder/engine.py +13 -0
- snippbot_core/skill_builder/packager.py +7 -0
- snippbot_core/skill_builder/session.py +7 -0
- snippbot_core/skill_builder/spec.py +7 -0
- snippbot_core/skill_builder/system_prompt.py +7 -0
- snippbot_core/skills_store.py +950 -0
- snippbot_core/snipp/__init__.py +110 -0
- snippbot_core/snipp/balance.py +403 -0
- snippbot_core/snipp/encrypted_storage.py +479 -0
- snippbot_core/snipp/recovery_phrase.py +459 -0
- snippbot_core/snipp/signing.py +478 -0
- snippbot_core/snipp/wallet.py +340 -0
- snippbot_core/strategy.py +231 -0
- snippbot_core/sub_agent/__init__.py +57 -0
- snippbot_core/sub_agent/aggregator.py +376 -0
- snippbot_core/sub_agent/concurrency.py +200 -0
- snippbot_core/sub_agent/event_digest.py +176 -0
- snippbot_core/sub_agent/lifecycle.py +534 -0
- snippbot_core/sub_agent/messaging.py +336 -0
- snippbot_core/sub_agent/models.py +545 -0
- snippbot_core/sub_agent/orchestrator.py +639 -0
- snippbot_core/sub_agent/resource_manager.py +227 -0
- snippbot_core/sub_agent/role_prompts.py +242 -0
- snippbot_core/sub_agent/session.py +1340 -0
- snippbot_core/sub_agent/shared_context.py +238 -0
- snippbot_core/sub_agent/spawner.py +264 -0
- snippbot_core/sub_agent/store.py +987 -0
- snippbot_core/sub_agent/team_models.py +123 -0
- snippbot_core/sub_agent/team_prompts.py +188 -0
- snippbot_core/subprocess_utils.py +80 -0
- snippbot_core/sync.py +222 -0
- snippbot_core/task_executor.py +1703 -0
- snippbot_core/thinking/__init__.py +25 -0
- snippbot_core/thinking/daemon.py +955 -0
- snippbot_core/thinking/engagement.py +261 -0
- snippbot_core/thinking/engine.py +632 -0
- snippbot_core/thinking/models.py +251 -0
- snippbot_core/thinking/store.py +626 -0
- snippbot_core/thinking/triggers.py +193 -0
- snippbot_core/tier_inference.py +187 -0
- snippbot_core/tools/__init__.py +22 -0
- snippbot_core/tools/_python_dispatcher.py +226 -0
- snippbot_core/tools/browser/__init__.py +14 -0
- snippbot_core/tools/browser/actions.py +438 -0
- snippbot_core/tools/browser/auth_manager.py +439 -0
- snippbot_core/tools/browser/device_emulation.py +378 -0
- snippbot_core/tools/browser/dom_snapshot.py +722 -0
- snippbot_core/tools/browser/exceptions.py +126 -0
- snippbot_core/tools/browser/file_handler.py +261 -0
- snippbot_core/tools/browser/live_stream.py +293 -0
- snippbot_core/tools/browser/manager.py +884 -0
- snippbot_core/tools/browser/network_manager.py +511 -0
- snippbot_core/tools/browser/profiles.py +438 -0
- snippbot_core/tools/browser/recorder.py +514 -0
- snippbot_core/tools/browser/screen_recorder.py +995 -0
- snippbot_core/tools/browser/ssrf_guard.py +331 -0
- snippbot_core/tools/browser/stealth.py +141 -0
- snippbot_core/tools/browser/tab_manager.py +277 -0
- snippbot_core/tools/browser_manager.py +9 -0
- snippbot_core/tools/channel_sender.py +406 -0
- snippbot_core/tools/definitions.py +1681 -0
- snippbot_core/tools/execution_mode.py +104 -0
- snippbot_core/tools/executor.py +4917 -0
- snippbot_core/tools/invocation_log.py +182 -0
- snippbot_core/tools/marketplace_types.py +39 -0
- snippbot_core/tools/registry.py +584 -0
- snippbot_core/tools/search_providers.py +349 -0
- snippbot_core/tools/summarization.py +255 -0
- snippbot_core/tools/workflow_dispatch.py +296 -0
- snippbot_core/user_model.py +1709 -0
- snippbot_core/worker_pool.py +118 -0
- snippbot_core/workflows/__init__.py +51 -0
- snippbot_core/workflows/dry_run.py +211 -0
- snippbot_core/workflows/dsl.py +365 -0
- snippbot_core/workflows/executor.py +825 -0
- snippbot_core/workflows/models.py +675 -0
- snippbot_core/workflows/parser.py +144 -0
- snippbot_core/workflows/run_store.py +556 -0
- snippbot_core/workflows/scheduler.py +455 -0
- snippbot_core/workflows/schema.py +616 -0
- snippbot_core/workflows/state_machine.py +277 -0
- snippbot_core/workflows/step_executors/__init__.py +502 -0
- snippbot_core/workflows/step_executors/approval_gate.py +197 -0
- snippbot_core/workflows/step_executors/conditional.py +103 -0
- snippbot_core/workflows/step_executors/llm_step.py +405 -0
- snippbot_core/workflows/step_executors/loop_step.py +157 -0
- snippbot_core/workflows/step_executors/parallel_step.py +197 -0
- snippbot_core/workflows/step_executors/subworkflow.py +116 -0
- snippbot_core/workflows/step_executors/tool_step.py +138 -0
- snippbot_core/workflows/store.py +568 -0
- snippbot_core/workflows/template_store.py +590 -0
- snippbot_core/workflows/version_control.py +149 -0
- snippbot_core/workspace/__init__.py +67 -0
- snippbot_core/workspace/evolution.py +278 -0
- snippbot_core/workspace/identity_proposals.py +362 -0
- snippbot_core/workspace/pending_writes.py +256 -0
- snippbot_core/workspace/tools.py +877 -0
- snippbot_core/workspace_manager.py +493 -0
- vps/__init__.py +7 -0
- vps/config/strategy-presets.json +34 -0
- vps/data/.gitkeep +0 -0
- vps/data/memory_schema.sql +80 -0
- vps/data/schema.sql +268 -0
- vps/data/user_model_schema.sql +125 -0
- vps/lib/__init__.py +101 -0
- vps/lib/adaptive_throttle.py +976 -0
- vps/lib/agent_workspace.py +731 -0
- vps/lib/anticipation.py +1264 -0
- vps/lib/api_handler.py +2586 -0
- vps/lib/approvals.py +505 -0
- vps/lib/auth.py +970 -0
- vps/lib/awakening_api.py +639 -0
- vps/lib/benchmark.py +351 -0
- vps/lib/chaos.py +959 -0
- vps/lib/checkpoint.py +495 -0
- vps/lib/config.py +227 -0
- vps/lib/connection_pool.py +658 -0
- vps/lib/credentials.py +839 -0
- vps/lib/daemon.py +437 -0
- vps/lib/dag_builder.py +871 -0
- vps/lib/delight_tracker.py +707 -0
- vps/lib/error_handler.py +880 -0
- vps/lib/event_bus.py +341 -0
- vps/lib/events.py +354 -0
- vps/lib/executor.py +681 -0
- vps/lib/expertise_detector.py +716 -0
- vps/lib/failure_handler.py +946 -0
- vps/lib/failure_learning.py +1194 -0
- vps/lib/feedback_learner.py +1033 -0
- vps/lib/focus_mode.py +1047 -0
- vps/lib/frustration_detector.py +713 -0
- vps/lib/history.py +837 -0
- vps/lib/idle_daemon.py +640 -0
- vps/lib/insight_delivery.py +863 -0
- vps/lib/insight_queue.py +1070 -0
- vps/lib/interest_detector.py +750 -0
- vps/lib/memory/__init__.py +49 -0
- vps/lib/memory/consolidation.py +643 -0
- vps/lib/memory/episodic.py +954 -0
- vps/lib/memory/forgetting.py +644 -0
- vps/lib/memory/hybrid_search.py +697 -0
- vps/lib/memory/keyword_search.py +565 -0
- vps/lib/memory/knowledge_graph.py +1242 -0
- vps/lib/memory/vector_index.py +734 -0
- vps/lib/memory/write_pipeline.py +622 -0
- vps/lib/memory_search.py +326 -0
- vps/lib/metrics.py +513 -0
- vps/lib/parallel_executor.py +871 -0
- vps/lib/permissions.py +1044 -0
- vps/lib/preference_learner.py +714 -0
- vps/lib/proactivity.py +780 -0
- vps/lib/profiling.py +659 -0
- vps/lib/project_store.py +926 -0
- vps/lib/prometheus.py +596 -0
- vps/lib/query_optimizer.py +700 -0
- vps/lib/rate_limiter.py +416 -0
- vps/lib/recovery.py +813 -0
- vps/lib/replay.py +933 -0
- vps/lib/request_validator.py +665 -0
- vps/lib/resource_limits.py +458 -0
- vps/lib/risk_scorer.py +512 -0
- vps/lib/room_reader.py +690 -0
- vps/lib/sandbox.py +1159 -0
- vps/lib/snipp/__init__.py +497 -0
- vps/lib/snipp/aggregation.py +449 -0
- vps/lib/snipp/api_interceptor.py +494 -0
- vps/lib/snipp/audit.py +459 -0
- vps/lib/snipp/balance.py +379 -0
- vps/lib/snipp/encrypted_storage.py +479 -0
- vps/lib/snipp/escrow.py +932 -0
- vps/lib/snipp/rating.py +855 -0
- vps/lib/snipp/receipt.py +400 -0
- vps/lib/snipp/receipt_store.py +554 -0
- vps/lib/snipp/recovery_phrase.py +459 -0
- vps/lib/snipp/reputation_sync.py +544 -0
- vps/lib/snipp/request_router.py +961 -0
- vps/lib/snipp/revision_handler.py +550 -0
- vps/lib/snipp/reward_callback.py +549 -0
- vps/lib/snipp/service_registry.py +812 -0
- vps/lib/snipp/signing.py +368 -0
- vps/lib/snipp/snipp_client.py +567 -0
- vps/lib/snipp/utils.py +152 -0
- vps/lib/snipp/wallet.py +243 -0
- vps/lib/snipp/work_claim.py +550 -0
- vps/lib/speculative.py +802 -0
- vps/lib/strategy.py +231 -0
- vps/lib/style_adapter.py +661 -0
- vps/lib/tool_selector.py +1121 -0
- vps/lib/user_goals_api.py +824 -0
- vps/lib/user_model.py +1563 -0
- vps/lib/websocket_handler.py +545 -0
snippbot/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Snippbot - Autonomous AI agents for your local machine.
|
|
2
|
+
|
|
3
|
+
This is the main package for running Snippbot locally. It provides:
|
|
4
|
+
- HTTP API server for the UI and external integrations
|
|
5
|
+
- WebSocket server for real-time updates
|
|
6
|
+
- Background daemon for autonomous task execution
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
snippbot start # Start the daemon
|
|
10
|
+
snippbot status # Check daemon status
|
|
11
|
+
snippbot config set ... # Configure settings
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.0b1"
|
|
15
|
+
|
|
16
|
+
from snippbot.server import create_app, run_server
|
|
17
|
+
|
|
18
|
+
__all__ = ["__version__", "create_app", "run_server"]
|
snippbot/__main__.py
ADDED
snippbot/api/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""REST API handlers.
|
|
2
|
+
|
|
3
|
+
This module provides the HTTP API for Snippbot:
|
|
4
|
+
- /api/projects - Project management
|
|
5
|
+
- /api/tasks - Task management
|
|
6
|
+
- /api/memory - Memory search and episodes
|
|
7
|
+
- /api/insights - Proactive insights
|
|
8
|
+
- /api/goals - Goal tracking
|
|
9
|
+
- /api/snipp - Token economy
|
|
10
|
+
- /api/daemon - Daemon control
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from snippbot.api.routes import api_routes
|
|
14
|
+
from snippbot.api.health import health_routes
|
|
15
|
+
|
|
16
|
+
__all__ = ["api_routes", "health_routes"]
|
snippbot/api/agents.py
ADDED
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
"""Agent profile and conversation roster API endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
from starlette.requests import Request
|
|
13
|
+
from starlette.responses import JSONResponse
|
|
14
|
+
from starlette.routing import Route
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# PROFILE.md metadata extraction — used to enrich gateway agents (which only
|
|
21
|
+
# expose `role` from their own parser) with archetype + expertise tags.
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
_PROFILE_SEARCH_DIRS: list[Path] = []
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _profile_search_dirs() -> list[Path]:
|
|
28
|
+
"""Directories to scan for `<agent_id>/workspace/PROFILE.md`.
|
|
29
|
+
|
|
30
|
+
Matches the gateway's discovery paths (see claude_gateway.config.discover_agents):
|
|
31
|
+
repo-root `vps/agents/` plus the two per-user install locations.
|
|
32
|
+
`parents[5]` resolves to the monorepo root when this file lives at
|
|
33
|
+
`packages/local/src/snippbot/api/agents.py`.
|
|
34
|
+
"""
|
|
35
|
+
global _PROFILE_SEARCH_DIRS
|
|
36
|
+
if _PROFILE_SEARCH_DIRS:
|
|
37
|
+
return _PROFILE_SEARCH_DIRS
|
|
38
|
+
here = Path(__file__).resolve()
|
|
39
|
+
candidates = [
|
|
40
|
+
here.parents[5] / "vps" / "agents" if len(here.parents) >= 6 else None,
|
|
41
|
+
Path.home() / ".snippai" / "agents",
|
|
42
|
+
Path.home() / ".snippbot" / "agents",
|
|
43
|
+
Path.home() / "projects" / "snippbot" / "vps" / "agents",
|
|
44
|
+
]
|
|
45
|
+
_PROFILE_SEARCH_DIRS = [p for p in candidates if p is not None and p.exists()]
|
|
46
|
+
return _PROFILE_SEARCH_DIRS
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _read_profile_metadata(agent_id: str) -> dict[str, Any]:
|
|
50
|
+
"""Read `- **Field:** value` lines from an agent's profile.
|
|
51
|
+
|
|
52
|
+
Tries IDENTITY.md (post-Phase-5 canonical), then PROFILE.md (v1).
|
|
53
|
+
Extracts Role, Archetype, Vibe, and an optional Expertise line
|
|
54
|
+
(comma-separated tags). Returns an empty dict if neither file can be
|
|
55
|
+
found or read — callers must not rely on any field being present.
|
|
56
|
+
"""
|
|
57
|
+
for base in _profile_search_dirs():
|
|
58
|
+
workspace = base / agent_id / "workspace"
|
|
59
|
+
for filename in ("IDENTITY.md", "PROFILE.md"):
|
|
60
|
+
profile_path = workspace / filename
|
|
61
|
+
if not profile_path.is_file():
|
|
62
|
+
continue
|
|
63
|
+
try:
|
|
64
|
+
text = profile_path.read_text(encoding="utf-8", errors="replace")
|
|
65
|
+
except OSError:
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
meta: dict[str, Any] = {}
|
|
69
|
+
for line in text.splitlines():
|
|
70
|
+
m = re.match(r"^\s*-\s+\*\*([A-Za-z][A-Za-z ]*):\*\*\s*(.*)$", line)
|
|
71
|
+
if not m:
|
|
72
|
+
continue
|
|
73
|
+
field = m.group(1).strip().lower()
|
|
74
|
+
value = m.group(2).strip()
|
|
75
|
+
if not value:
|
|
76
|
+
continue
|
|
77
|
+
if field == "role":
|
|
78
|
+
meta["role"] = value
|
|
79
|
+
elif field == "archetype":
|
|
80
|
+
meta["archetype"] = value
|
|
81
|
+
elif field == "vibe":
|
|
82
|
+
meta["vibe"] = value
|
|
83
|
+
elif field == "expertise":
|
|
84
|
+
tags: list[str] = []
|
|
85
|
+
seen: set[str] = set()
|
|
86
|
+
for raw in value.split(","):
|
|
87
|
+
tag = raw.strip()
|
|
88
|
+
if tag and tag.lower() not in seen:
|
|
89
|
+
seen.add(tag.lower())
|
|
90
|
+
tags.append(tag)
|
|
91
|
+
if tags:
|
|
92
|
+
meta["expertise_tags"] = tags
|
|
93
|
+
return meta
|
|
94
|
+
return {}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _compose_short_bio(role: Optional[str], archetype: Optional[str]) -> Optional[str]:
|
|
98
|
+
"""Blend role + archetype into a single short_bio string for the popover."""
|
|
99
|
+
role = (role or "").strip() or None
|
|
100
|
+
archetype = (archetype or "").strip() or None
|
|
101
|
+
if role and archetype:
|
|
102
|
+
return f"{role} — {archetype}"
|
|
103
|
+
return role or archetype
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
# Lazy singletons (same pattern as chat.py)
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
_agent_store = None
|
|
110
|
+
_roster_store = None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def get_agent_store():
|
|
114
|
+
global _agent_store
|
|
115
|
+
if _agent_store is None:
|
|
116
|
+
from snippbot_core.chat.agent_store import AgentStore
|
|
117
|
+
_agent_store = AgentStore()
|
|
118
|
+
return _agent_store
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def get_roster_store():
|
|
122
|
+
global _roster_store
|
|
123
|
+
if _roster_store is None:
|
|
124
|
+
from snippbot_core.chat.roster import RosterStore
|
|
125
|
+
_roster_store = RosterStore()
|
|
126
|
+
return _roster_store
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ===========================================================================
|
|
130
|
+
# Agent profile endpoints
|
|
131
|
+
# ===========================================================================
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
async def list_available_tools(request: Request) -> JSONResponse:
|
|
135
|
+
"""GET /api/tools/available — list all tool names + priority + source.
|
|
136
|
+
|
|
137
|
+
Powers the Agent Tool Profile picker (PLAN_AGENT_TOOL_PROFILES Phase 4).
|
|
138
|
+
Shape: ``{"tools": [{"name", "description", "priority", "source"}], "count"}``.
|
|
139
|
+
|
|
140
|
+
Includes MCP tools and is not filtered by ``chat_only``, so the UI can
|
|
141
|
+
expose every tool an agent might possibly invoke.
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
from snippbot_core.tools.registry import get_tool_registry
|
|
145
|
+
|
|
146
|
+
defs = await get_tool_registry().async_get_enabled_tool_definitions(
|
|
147
|
+
include_mcp=True,
|
|
148
|
+
chat_only=True,
|
|
149
|
+
)
|
|
150
|
+
# Pull `_source` off the raw (un-cleaned) tool list — `async_get_enabled_tool_definitions`
|
|
151
|
+
# strips it. Need to call the inner helper to retain metadata.
|
|
152
|
+
all_with_meta = await get_tool_registry().async_get_all_tools(
|
|
153
|
+
include_mcp=True,
|
|
154
|
+
chat_only=True,
|
|
155
|
+
)
|
|
156
|
+
source_by_name: dict[str, str] = {
|
|
157
|
+
t.get("name", ""): t.get("_source", "unknown") for t in all_with_meta
|
|
158
|
+
}
|
|
159
|
+
tools: list[dict[str, Any]] = []
|
|
160
|
+
for d in defs:
|
|
161
|
+
name = d.get("name")
|
|
162
|
+
if not name:
|
|
163
|
+
continue
|
|
164
|
+
tools.append({
|
|
165
|
+
"name": name,
|
|
166
|
+
"description": (d.get("description") or "")[:300],
|
|
167
|
+
"priority": d.get("_priority", 5),
|
|
168
|
+
"source": source_by_name.get(name, "unknown"),
|
|
169
|
+
})
|
|
170
|
+
return JSONResponse({"tools": tools, "count": len(tools)})
|
|
171
|
+
except Exception:
|
|
172
|
+
logger.exception("Failed to list available tools")
|
|
173
|
+
return JSONResponse({"error": "tool registry unavailable"}, status_code=500)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
async def list_all_agents(request: Request) -> JSONResponse:
|
|
177
|
+
"""GET /api/agents/all — unified list of gateway agents + AgentStore profiles.
|
|
178
|
+
|
|
179
|
+
Returns normalized shape: ``{id, handle, name, short_bio?, source,
|
|
180
|
+
avatar_url?, expertise_tags?, preferred_model?}``.
|
|
181
|
+
|
|
182
|
+
Source discriminator lets the UI show badges or scope behavior (e.g.
|
|
183
|
+
only AgentStore profiles are editable). Handles are deduped — if the
|
|
184
|
+
same handle exists in both sources, the AgentStore profile wins (user
|
|
185
|
+
config beats gateway convention).
|
|
186
|
+
|
|
187
|
+
Fixes B1 by giving the frontend a single source of truth; fixes B2 by
|
|
188
|
+
exposing AgentStore profiles with their true `handle` rather than
|
|
189
|
+
their UUID `id`, so `@!handle` round-trips correctly through
|
|
190
|
+
`parse_mentions` → `handle_to_id` on the backend.
|
|
191
|
+
"""
|
|
192
|
+
from snippbot.api.chat import get_gateway_client
|
|
193
|
+
|
|
194
|
+
store = get_agent_store()
|
|
195
|
+
gateway_client = get_gateway_client()
|
|
196
|
+
|
|
197
|
+
# Fetch both sources in parallel.
|
|
198
|
+
store_task = asyncio.to_thread(store.list_agents)
|
|
199
|
+
try:
|
|
200
|
+
gateway_agents, store_profiles = await asyncio.gather(
|
|
201
|
+
gateway_client.list_agents(), store_task, return_exceptions=True
|
|
202
|
+
)
|
|
203
|
+
except Exception as exc: # pragma: no cover — gather shouldn't raise
|
|
204
|
+
logger.warning("Failed to list unified agents: %s", exc)
|
|
205
|
+
return JSONResponse({"error": str(exc)}, status_code=500)
|
|
206
|
+
|
|
207
|
+
if isinstance(gateway_agents, Exception):
|
|
208
|
+
logger.debug("Gateway agent list failed (using empty): %s", gateway_agents)
|
|
209
|
+
gateway_agents = []
|
|
210
|
+
if isinstance(store_profiles, Exception):
|
|
211
|
+
logger.warning("AgentStore list failed (using empty): %s", store_profiles)
|
|
212
|
+
store_profiles = []
|
|
213
|
+
|
|
214
|
+
# Pre-build gateway display map so a store row with the same handle can
|
|
215
|
+
# borrow the gateway's richer display fields (name, bio, tags, avatar)
|
|
216
|
+
# when the store row was auto-seeded with only an id/handle (e.g. to
|
|
217
|
+
# persist `preferred_model` for a gateway-only agent like Elon).
|
|
218
|
+
gateway_by_handle: dict[str, dict[str, Any]] = {}
|
|
219
|
+
for agent in gateway_agents or []:
|
|
220
|
+
gw_id = agent.get("id", "") if isinstance(agent, dict) else ""
|
|
221
|
+
if not gw_id:
|
|
222
|
+
continue
|
|
223
|
+
profile_meta = await asyncio.to_thread(_read_profile_metadata, gw_id)
|
|
224
|
+
role = agent.get("role") or profile_meta.get("role") or agent.get("description")
|
|
225
|
+
archetype = profile_meta.get("archetype")
|
|
226
|
+
gateway_by_handle[gw_id] = {
|
|
227
|
+
"id": gw_id,
|
|
228
|
+
"name": agent.get("display_name") or agent.get("name") or gw_id,
|
|
229
|
+
"short_bio": _compose_short_bio(role, archetype),
|
|
230
|
+
"expertise_tags": profile_meta.get("expertise_tags") or [],
|
|
231
|
+
"avatar_url": f"/gateway/api/agents/{gw_id}/avatar",
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
seen_handles: set[str] = set()
|
|
235
|
+
merged: list[dict] = []
|
|
236
|
+
|
|
237
|
+
# Store profiles first, enriched with gateway fields when the store row
|
|
238
|
+
# matches a gateway handle and the store field is empty/default.
|
|
239
|
+
for profile in store_profiles:
|
|
240
|
+
handle = profile.handle
|
|
241
|
+
if handle in seen_handles:
|
|
242
|
+
continue
|
|
243
|
+
seen_handles.add(handle)
|
|
244
|
+
gw = gateway_by_handle.get(handle, {})
|
|
245
|
+
# A store row is considered a "stub" for this field if it's empty OR
|
|
246
|
+
# equals the id/handle (which is how insert_agent_with_id seeds them).
|
|
247
|
+
store_name_is_stub = not profile.name or profile.name == profile.id or profile.name == profile.handle
|
|
248
|
+
merged.append({
|
|
249
|
+
"id": profile.id,
|
|
250
|
+
"handle": handle,
|
|
251
|
+
"name": (gw.get("name") if store_name_is_stub and gw.get("name") else None) or profile.name,
|
|
252
|
+
"short_bio": profile.short_bio or gw.get("short_bio"),
|
|
253
|
+
"avatar_url": profile.avatar_url or gw.get("avatar_url"),
|
|
254
|
+
"expertise_tags": profile.expertise_tags or gw.get("expertise_tags") or [],
|
|
255
|
+
"preferred_model": profile.preferred_model,
|
|
256
|
+
"tool_profile": profile.tool_profile,
|
|
257
|
+
"source": "store",
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
# Gateway agents without a matching store row.
|
|
261
|
+
for agent_id, gw in gateway_by_handle.items():
|
|
262
|
+
if agent_id in seen_handles:
|
|
263
|
+
continue
|
|
264
|
+
seen_handles.add(agent_id)
|
|
265
|
+
entry: dict[str, Any] = {
|
|
266
|
+
"id": agent_id,
|
|
267
|
+
"handle": agent_id, # gateway convention: handle == id
|
|
268
|
+
"name": gw["name"],
|
|
269
|
+
"short_bio": gw["short_bio"],
|
|
270
|
+
"avatar_url": gw["avatar_url"],
|
|
271
|
+
"source": "gateway",
|
|
272
|
+
}
|
|
273
|
+
if gw["expertise_tags"]:
|
|
274
|
+
entry["expertise_tags"] = gw["expertise_tags"]
|
|
275
|
+
merged.append(entry)
|
|
276
|
+
|
|
277
|
+
return JSONResponse(merged)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# Caps on tool_profile shape (S2 audit finding). Single-user local app, so
|
|
281
|
+
# these are defence-in-depth against a misbehaving UI or malformed JSON, not
|
|
282
|
+
# adversarial input.
|
|
283
|
+
_TOOL_PROFILE_MAX_TOOLS_LIST_LEN = 1000
|
|
284
|
+
_TOOL_PROFILE_MAX_TOOL_NAME_LEN = 200
|
|
285
|
+
_TOOL_PROFILE_ALLOWED_KEYS = frozenset({"mode", "tools", "include_core", "max_tools"})
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _validate_tool_profile(tp: Any) -> Optional[str]:
|
|
289
|
+
"""Return an error message if ``tp`` is not a valid tool_profile, else None.
|
|
290
|
+
|
|
291
|
+
Schema:
|
|
292
|
+
{
|
|
293
|
+
"mode": "allowlist" | "blocklist", # required when tp is set
|
|
294
|
+
"tools": [str, ...], # required, can be empty
|
|
295
|
+
"include_core": bool, # optional, default true
|
|
296
|
+
"max_tools": int, # optional, positive
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
Unknown keys are rejected (S3) so the persisted row stays canonical.
|
|
300
|
+
``tools`` is capped at 1000 names of 200 chars each (S2).
|
|
301
|
+
"""
|
|
302
|
+
if not isinstance(tp, dict):
|
|
303
|
+
return "'tool_profile' must be an object or null"
|
|
304
|
+
unknown = set(tp.keys()) - _TOOL_PROFILE_ALLOWED_KEYS
|
|
305
|
+
if unknown:
|
|
306
|
+
return f"tool_profile contains unknown keys: {sorted(unknown)}"
|
|
307
|
+
mode = tp.get("mode")
|
|
308
|
+
if mode not in ("allowlist", "blocklist"):
|
|
309
|
+
return "tool_profile.mode must be 'allowlist' or 'blocklist'"
|
|
310
|
+
tools = tp.get("tools", [])
|
|
311
|
+
if not isinstance(tools, list) or not all(isinstance(t, str) for t in tools):
|
|
312
|
+
return "tool_profile.tools must be a list of strings"
|
|
313
|
+
if len(tools) > _TOOL_PROFILE_MAX_TOOLS_LIST_LEN:
|
|
314
|
+
return f"tool_profile.tools must contain at most {_TOOL_PROFILE_MAX_TOOLS_LIST_LEN} entries"
|
|
315
|
+
if any(len(t) > _TOOL_PROFILE_MAX_TOOL_NAME_LEN for t in tools):
|
|
316
|
+
return f"tool_profile.tools entries must be ≤ {_TOOL_PROFILE_MAX_TOOL_NAME_LEN} chars"
|
|
317
|
+
if "include_core" in tp and not isinstance(tp["include_core"], bool):
|
|
318
|
+
return "tool_profile.include_core must be a boolean"
|
|
319
|
+
if "max_tools" in tp:
|
|
320
|
+
mt = tp["max_tools"]
|
|
321
|
+
if not isinstance(mt, int) or isinstance(mt, bool) or mt <= 0:
|
|
322
|
+
return "tool_profile.max_tools must be a positive integer"
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
async def update_agent(request: Request) -> JSONResponse:
|
|
327
|
+
"""PATCH /api/agents/{agent_id} — edit profile fields.
|
|
328
|
+
|
|
329
|
+
If the agent doesn't have a chat-DB row yet (e.g. a gateway-only agent
|
|
330
|
+
like 'elon' or 'donna'), and the caller is setting `preferred_model`,
|
|
331
|
+
a minimal row is auto-seeded so the preference can be persisted. This
|
|
332
|
+
keeps the multi-agent chat path's `agent_store.get_agent(...)` read
|
|
333
|
+
consistent with what gets written here.
|
|
334
|
+
"""
|
|
335
|
+
agent_id = request.path_params["agent_id"]
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
body = await request.json()
|
|
339
|
+
except Exception:
|
|
340
|
+
return JSONResponse({"error": "Invalid JSON body"}, status_code=400)
|
|
341
|
+
|
|
342
|
+
# Validate field types: each editable field must be the right shape if
|
|
343
|
+
# present. Rejects accidental int/object writes that would corrupt the row
|
|
344
|
+
# and 500 subsequent reads at pydantic validation time.
|
|
345
|
+
_str_fields = ("name", "handle", "short_bio", "preferred_model", "avatar_url", "system_prompt")
|
|
346
|
+
for _field in _str_fields:
|
|
347
|
+
if _field in body and body[_field] is not None and not isinstance(body[_field], str):
|
|
348
|
+
return JSONResponse(
|
|
349
|
+
{"error": f"'{_field}' must be a string"},
|
|
350
|
+
status_code=400,
|
|
351
|
+
)
|
|
352
|
+
if "expertise_tags" in body and body["expertise_tags"] is not None:
|
|
353
|
+
if not isinstance(body["expertise_tags"], list) or not all(isinstance(t, str) for t in body["expertise_tags"]):
|
|
354
|
+
return JSONResponse(
|
|
355
|
+
{"error": "'expertise_tags' must be a list of strings"},
|
|
356
|
+
status_code=400,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
tool_profile_present = "tool_profile" in body
|
|
360
|
+
tool_profile = body.get("tool_profile") if tool_profile_present else None
|
|
361
|
+
if tool_profile_present and tool_profile is not None:
|
|
362
|
+
err = _validate_tool_profile(tool_profile)
|
|
363
|
+
if err:
|
|
364
|
+
return JSONResponse({"error": err}, status_code=400)
|
|
365
|
+
|
|
366
|
+
store = get_agent_store()
|
|
367
|
+
|
|
368
|
+
existing = await asyncio.to_thread(store.get_agent, agent_id)
|
|
369
|
+
if existing is None:
|
|
370
|
+
# PLAN_PER_AGENT_THINKING_MODE.md soak audit: don't lazy-seed rows
|
|
371
|
+
# for unknown agent IDs. Option A's startup sync now eagerly seeds
|
|
372
|
+
# every gateway-folder agent, so a missing row genuinely means
|
|
373
|
+
# "no such agent" (or the sync hasn't completed yet). Fall back to
|
|
374
|
+
# checking the gateway directories: if a matching folder exists,
|
|
375
|
+
# seed (handles the brief startup-race window or a folder added
|
|
376
|
+
# mid-session); otherwise 404 to prevent orphan rows that would
|
|
377
|
+
# be deleted on the next sync (the "recreate-delete loop" the
|
|
378
|
+
# audit flagged).
|
|
379
|
+
gateway_folder_exists = any(
|
|
380
|
+
(root / agent_id).is_dir() for root in _profile_search_dirs()
|
|
381
|
+
)
|
|
382
|
+
if not gateway_folder_exists:
|
|
383
|
+
return JSONResponse(
|
|
384
|
+
{
|
|
385
|
+
"error": (
|
|
386
|
+
f"Agent '{agent_id}' not found. Create a gateway "
|
|
387
|
+
f"agent folder under vps/agents/<handle>/ first."
|
|
388
|
+
)
|
|
389
|
+
},
|
|
390
|
+
status_code=404,
|
|
391
|
+
)
|
|
392
|
+
await asyncio.to_thread(
|
|
393
|
+
store.insert_agent_with_id,
|
|
394
|
+
agent_id=agent_id,
|
|
395
|
+
name=body.get("name") or agent_id,
|
|
396
|
+
handle=body.get("handle") or agent_id,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# Use store sentinel so "field omitted from body" is distinguishable
|
|
400
|
+
# from "tool_profile: null" (which clears the profile).
|
|
401
|
+
from snippbot_core.chat.agent_store import AgentStore as _AS
|
|
402
|
+
tp_arg: Any = body["tool_profile"] if tool_profile_present else _AS._UNSET
|
|
403
|
+
|
|
404
|
+
updated = await asyncio.to_thread(
|
|
405
|
+
store.update_agent,
|
|
406
|
+
agent_id=agent_id,
|
|
407
|
+
name=body.get("name"),
|
|
408
|
+
handle=body.get("handle"),
|
|
409
|
+
short_bio=body.get("short_bio"),
|
|
410
|
+
expertise_tags=body.get("expertise_tags"),
|
|
411
|
+
preferred_model=body.get("preferred_model"),
|
|
412
|
+
avatar_url=body.get("avatar_url"),
|
|
413
|
+
system_prompt=body.get("system_prompt"),
|
|
414
|
+
tool_profile=tp_arg,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
if not updated and existing is not None:
|
|
418
|
+
return JSONResponse({"error": "No fields to update"}, status_code=400)
|
|
419
|
+
|
|
420
|
+
agent = await asyncio.to_thread(store.get_agent, agent_id)
|
|
421
|
+
|
|
422
|
+
# PLAN_AGENT_TOOL_PROFILES Phase 5.4 — fire telemetry when the tool
|
|
423
|
+
# profile changed. Best-effort: dashboard observability must never
|
|
424
|
+
# block a successful PATCH.
|
|
425
|
+
if tool_profile_present:
|
|
426
|
+
try:
|
|
427
|
+
from snippbot_core.event_bus import get_event_bus
|
|
428
|
+
from snippbot_core.events import AGENT_TOOL_PROFILE_UPDATED
|
|
429
|
+
|
|
430
|
+
tp_val = body["tool_profile"]
|
|
431
|
+
payload: dict[str, Any] = {
|
|
432
|
+
"agent_id": agent_id,
|
|
433
|
+
"source": "api",
|
|
434
|
+
}
|
|
435
|
+
if tp_val is None:
|
|
436
|
+
payload.update({"mode": None, "tool_count": 0, "include_core": True})
|
|
437
|
+
elif isinstance(tp_val, dict):
|
|
438
|
+
payload.update({
|
|
439
|
+
"mode": tp_val.get("mode"),
|
|
440
|
+
"tool_count": len(tp_val.get("tools") or []),
|
|
441
|
+
"include_core": bool(tp_val.get("include_core", True)),
|
|
442
|
+
})
|
|
443
|
+
get_event_bus().publish(AGENT_TOOL_PROFILE_UPDATED, payload)
|
|
444
|
+
except Exception:
|
|
445
|
+
logger.debug("Failed to publish AGENT_TOOL_PROFILE_UPDATED", exc_info=True)
|
|
446
|
+
|
|
447
|
+
return JSONResponse(agent.model_dump(mode="json"))
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
# ===========================================================================
|
|
451
|
+
# Conversation roster endpoints
|
|
452
|
+
# ===========================================================================
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
async def list_roster(request: Request) -> JSONResponse:
|
|
456
|
+
"""GET /api/chat/conversations/{conversation_id}/agents — list active roster."""
|
|
457
|
+
conversation_id = request.path_params["conversation_id"]
|
|
458
|
+
store = get_roster_store()
|
|
459
|
+
members = await asyncio.to_thread(store.active_members, conversation_id)
|
|
460
|
+
return JSONResponse([m.to_dict() for m in members])
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
async def add_to_roster(request: Request) -> JSONResponse:
|
|
464
|
+
"""POST /api/chat/conversations/{conversation_id}/agents — add agent to roster."""
|
|
465
|
+
conversation_id = request.path_params["conversation_id"]
|
|
466
|
+
|
|
467
|
+
try:
|
|
468
|
+
body = await request.json()
|
|
469
|
+
except Exception:
|
|
470
|
+
return JSONResponse({"error": "Invalid JSON body"}, status_code=400)
|
|
471
|
+
|
|
472
|
+
agent_id = body.get("agent_id")
|
|
473
|
+
if not agent_id:
|
|
474
|
+
return JSONResponse({"error": "'agent_id' is required"}, status_code=400)
|
|
475
|
+
|
|
476
|
+
# NOTE: We don't verify the agent exists in AgentStore — roster members
|
|
477
|
+
# can reference either AgentStore profiles OR gateway-managed agents
|
|
478
|
+
# (Snippbot workspace agents like Donna, Elon, etc.). The roster stores
|
|
479
|
+
# the agent_id as a free-form reference.
|
|
480
|
+
|
|
481
|
+
roster_store = get_roster_store()
|
|
482
|
+
try:
|
|
483
|
+
member = await asyncio.to_thread(
|
|
484
|
+
roster_store.add_member,
|
|
485
|
+
conversation_id=conversation_id,
|
|
486
|
+
agent_id=agent_id,
|
|
487
|
+
role=body.get("role", "member"),
|
|
488
|
+
)
|
|
489
|
+
except Exception as exc:
|
|
490
|
+
logger.warning("Failed to add agent to roster: %s", exc)
|
|
491
|
+
return JSONResponse({"error": str(exc)}, status_code=400)
|
|
492
|
+
|
|
493
|
+
return JSONResponse(member.to_dict(), status_code=201)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
async def update_roster_member(request: Request) -> JSONResponse:
|
|
497
|
+
"""PATCH /api/chat/conversations/{conversation_id}/agents/{agent_id} — update role/muted."""
|
|
498
|
+
conversation_id = request.path_params["conversation_id"]
|
|
499
|
+
agent_id = request.path_params["agent_id"]
|
|
500
|
+
|
|
501
|
+
try:
|
|
502
|
+
body = await request.json()
|
|
503
|
+
except Exception:
|
|
504
|
+
return JSONResponse({"error": "Invalid JSON body"}, status_code=400)
|
|
505
|
+
|
|
506
|
+
roster_store = get_roster_store()
|
|
507
|
+
|
|
508
|
+
# Verify membership exists
|
|
509
|
+
member = await asyncio.to_thread(roster_store.get_member, conversation_id, agent_id)
|
|
510
|
+
if member is None:
|
|
511
|
+
return JSONResponse({"error": "Agent is not in this conversation roster"}, status_code=404)
|
|
512
|
+
|
|
513
|
+
role = body.get("role")
|
|
514
|
+
muted = body.get("muted")
|
|
515
|
+
|
|
516
|
+
# Promote to lead — `roster_store.promote_lead` atomically updates
|
|
517
|
+
# both conversation_agents.role and conversations.lead_agent_id in a
|
|
518
|
+
# single transaction (fixes B6 / M5 / N7 — they used to be separate
|
|
519
|
+
# thread-worker calls with a crash-window race).
|
|
520
|
+
# Pass thinking_store so the slot auto-fills on lead promotion when the
|
|
521
|
+
# new lead is Jarvis-default (PLAN_PER_AGENT_THINKING_MODE.md).
|
|
522
|
+
if role == "lead":
|
|
523
|
+
from snippbot_core.thinking.store import get_thinking_store
|
|
524
|
+
thinking_store = get_thinking_store()
|
|
525
|
+
promoted = await asyncio.to_thread(
|
|
526
|
+
roster_store.promote_lead,
|
|
527
|
+
conversation_id=conversation_id,
|
|
528
|
+
new_lead_agent_id=agent_id,
|
|
529
|
+
thinking_store=thinking_store,
|
|
530
|
+
)
|
|
531
|
+
if not promoted:
|
|
532
|
+
return JSONResponse({"error": "Failed to promote agent to lead"}, status_code=400)
|
|
533
|
+
|
|
534
|
+
# Handle muted separately if also provided
|
|
535
|
+
if muted is not None:
|
|
536
|
+
await asyncio.to_thread(
|
|
537
|
+
roster_store.update_member,
|
|
538
|
+
conversation_id=conversation_id,
|
|
539
|
+
agent_id=agent_id,
|
|
540
|
+
muted=muted,
|
|
541
|
+
)
|
|
542
|
+
else:
|
|
543
|
+
updated = await asyncio.to_thread(
|
|
544
|
+
roster_store.update_member,
|
|
545
|
+
conversation_id=conversation_id,
|
|
546
|
+
agent_id=agent_id,
|
|
547
|
+
role=role,
|
|
548
|
+
muted=muted,
|
|
549
|
+
)
|
|
550
|
+
if not updated:
|
|
551
|
+
return JSONResponse({"error": "No fields to update"}, status_code=400)
|
|
552
|
+
|
|
553
|
+
# Return the updated member
|
|
554
|
+
updated_member = await asyncio.to_thread(roster_store.get_member, conversation_id, agent_id)
|
|
555
|
+
return JSONResponse(updated_member.to_dict())
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
async def remove_from_roster(request: Request) -> JSONResponse:
|
|
559
|
+
"""DELETE /api/chat/conversations/{conversation_id}/agents/{agent_id} — remove from roster."""
|
|
560
|
+
conversation_id = request.path_params["conversation_id"]
|
|
561
|
+
agent_id = request.path_params["agent_id"]
|
|
562
|
+
|
|
563
|
+
roster_store = get_roster_store()
|
|
564
|
+
removed = await asyncio.to_thread(
|
|
565
|
+
roster_store.remove_member,
|
|
566
|
+
conversation_id=conversation_id,
|
|
567
|
+
agent_id=agent_id,
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
if not removed:
|
|
571
|
+
return JSONResponse({"error": "Agent is not in this conversation roster"}, status_code=404)
|
|
572
|
+
|
|
573
|
+
return JSONResponse({"ok": True, "conversation_id": conversation_id, "agent_id": agent_id})
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
# ===========================================================================
|
|
577
|
+
# Route table
|
|
578
|
+
# ===========================================================================
|
|
579
|
+
|
|
580
|
+
# NOTE: These routes are spread into api_routes which is mounted under /api
|
|
581
|
+
# in server.py, so paths here must NOT include the /api prefix.
|
|
582
|
+
#
|
|
583
|
+
# GET /agents, GET /agents/all, and POST /agents are intentionally NOT
|
|
584
|
+
# registered here — they're defined directly in `routes.py` (next to the
|
|
585
|
+
# workspace-manager handlers) and registered earlier in api_routes so that
|
|
586
|
+
# Starlette's first-match resolves them deterministically. The unified
|
|
587
|
+
# AgentStore-aware handler `list_all_agents` (imported by routes.py) is
|
|
588
|
+
# the one bound to /agents/all. Adding duplicates here would re-introduce
|
|
589
|
+
# the route shadow this module deleted.
|
|
590
|
+
agent_routes = [
|
|
591
|
+
Route("/agents/{agent_id}", update_agent, methods=["PATCH"]),
|
|
592
|
+
Route("/tools/available", list_available_tools, methods=["GET"]),
|
|
593
|
+
]
|
|
594
|
+
|
|
595
|
+
# Roster routes must be injected into chat_routes (which is Mounted at /chat),
|
|
596
|
+
# because Starlette matches Mount("/chat") before reaching top-level routes.
|
|
597
|
+
# Paths are relative to the /chat mount prefix.
|
|
598
|
+
roster_routes = [
|
|
599
|
+
Route("/conversations/{conversation_id}/agents", list_roster, methods=["GET"]),
|
|
600
|
+
Route("/conversations/{conversation_id}/agents", add_to_roster, methods=["POST"]),
|
|
601
|
+
Route("/conversations/{conversation_id}/agents/{agent_id}", update_roster_member, methods=["PATCH"]),
|
|
602
|
+
Route("/conversations/{conversation_id}/agents/{agent_id}", remove_from_roster, methods=["DELETE"]),
|
|
603
|
+
]
|