@swarmclawai/swarmclaw 0.8.4 → 0.8.7
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.
- package/README.md +9 -9
- package/bin/swarmclaw.js +5 -1
- package/bin/worker-cmd.js +73 -0
- package/package.json +2 -1
- package/src/app/api/agents/[id]/route.ts +17 -7
- package/src/app/api/agents/route.ts +21 -8
- package/src/app/api/approvals/route.test.ts +6 -6
- package/src/app/api/approvals/route.ts +2 -1
- package/src/app/api/auth/route.ts +2 -3
- package/src/app/api/chatrooms/[id]/chat/route.test.ts +299 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +3 -2
- package/src/app/api/chatrooms/[id]/route.ts +7 -6
- package/src/app/api/chats/[id]/chat/route.test.ts +496 -0
- package/src/app/api/chats/[id]/chat/route.ts +7 -3
- package/src/app/api/chats/[id]/clear/route.ts +9 -9
- package/src/app/api/chats/[id]/devserver/route.ts +2 -1
- package/src/app/api/chats/[id]/edit-resend/route.ts +3 -4
- package/src/app/api/chats/[id]/fork/route.ts +3 -5
- package/src/app/api/chats/[id]/restore/route.ts +6 -7
- package/src/app/api/chats/[id]/retry/route.ts +3 -4
- package/src/app/api/chats/[id]/route.ts +61 -62
- package/src/app/api/chats/route.ts +7 -1
- package/src/app/api/connectors/[id]/route.ts +7 -8
- package/src/app/api/connectors/route.ts +5 -4
- package/src/app/api/eval/run/route.ts +2 -1
- package/src/app/api/eval/suite/route.ts +2 -1
- package/src/app/api/external-agents/route.test.ts +1 -1
- package/src/app/api/external-agents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +1 -1
- package/src/app/api/gateways/[id]/route.ts +7 -5
- package/src/app/api/gateways/route.ts +1 -1
- package/src/app/api/knowledge/upload/route.ts +1 -1
- package/src/app/api/logs/route.ts +5 -7
- package/src/app/api/memory-images/[filename]/route.ts +2 -3
- package/src/app/api/openclaw/agent-files/route.ts +4 -3
- package/src/app/api/openclaw/approvals/route.ts +3 -4
- package/src/app/api/openclaw/config-sync/route.ts +3 -2
- package/src/app/api/openclaw/cron/route.ts +3 -2
- package/src/app/api/openclaw/dotenv-keys/route.ts +2 -1
- package/src/app/api/openclaw/exec-config/route.ts +3 -2
- package/src/app/api/openclaw/gateway/route.ts +5 -4
- package/src/app/api/openclaw/history/route.ts +3 -2
- package/src/app/api/openclaw/media/route.ts +2 -1
- package/src/app/api/openclaw/permissions/route.ts +3 -2
- package/src/app/api/openclaw/sandbox-env/route.ts +3 -2
- package/src/app/api/openclaw/skills/install/route.ts +2 -1
- package/src/app/api/openclaw/skills/remove/route.ts +2 -1
- package/src/app/api/openclaw/skills/route.ts +3 -2
- package/src/app/api/orchestrator/run/route.ts +5 -14
- package/src/app/api/perf/route.ts +43 -0
- package/src/app/api/plugins/dependencies/route.ts +2 -1
- package/src/app/api/plugins/install/route.ts +2 -1
- package/src/app/api/plugins/marketplace/route.ts +3 -2
- package/src/app/api/plugins/settings/route.ts +2 -1
- package/src/app/api/preview-server/route.ts +11 -10
- package/src/app/api/projects/[id]/route.ts +1 -1
- package/src/app/api/schedules/[id]/route.test.ts +128 -0
- package/src/app/api/schedules/[id]/route.ts +43 -43
- package/src/app/api/schedules/[id]/run/route.ts +11 -62
- package/src/app/api/schedules/route.ts +21 -87
- package/src/app/api/settings/route.ts +2 -0
- package/src/app/api/setup/doctor/route.ts +9 -8
- package/src/app/api/tasks/[id]/approve/route.ts +33 -30
- package/src/app/api/tasks/[id]/route.ts +12 -35
- package/src/app/api/tasks/import/github/route.ts +2 -1
- package/src/app/api/tasks/route.ts +79 -91
- package/src/app/api/wallets/[id]/approve/route.ts +2 -1
- package/src/app/api/wallets/[id]/route.ts +13 -19
- package/src/app/api/wallets/[id]/send/route.ts +2 -1
- package/src/app/api/wallets/route.ts +2 -1
- package/src/app/api/webhooks/[id]/route.ts +2 -1
- package/src/app/api/webhooks/route.test.ts +3 -1
- package/src/app/page.tsx +23 -331
- package/src/cli/index.js +19 -0
- package/src/cli/index.ts +38 -7
- package/src/cli/spec.js +9 -0
- package/src/components/activity/activity-feed.tsx +7 -4
- package/src/components/agents/agent-card.tsx +32 -6
- package/src/components/agents/agent-chat-list.tsx +55 -22
- package/src/components/agents/agent-files-editor.tsx +3 -2
- package/src/components/agents/agent-sheet.tsx +123 -22
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/agents/openclaw-skills-panel.tsx +2 -1
- package/src/components/agents/trash-list.tsx +1 -1
- package/src/components/auth/access-key-gate.tsx +8 -2
- package/src/components/auth/setup-wizard.tsx +10 -9
- package/src/components/auth/user-picker.tsx +3 -2
- package/src/components/chat/chat-area.tsx +20 -1
- package/src/components/chat/chat-card.tsx +18 -3
- package/src/components/chat/chat-header.tsx +24 -4
- package/src/components/chat/chat-list.tsx +2 -11
- package/src/components/chat/heartbeat-history-panel.tsx +2 -1
- package/src/components/chat/message-bubble.tsx +45 -6
- package/src/components/chat/message-list.tsx +280 -145
- package/src/components/chat/streaming-bubble.tsx +217 -60
- package/src/components/chat/swarm-panel.test.ts +274 -0
- package/src/components/chat/swarm-panel.tsx +410 -0
- package/src/components/chat/swarm-status-card.tsx +346 -0
- package/src/components/chat/tool-call-bubble.tsx +48 -23
- package/src/components/chatrooms/chatroom-list.tsx +8 -5
- package/src/components/chatrooms/chatroom-message.tsx +10 -7
- package/src/components/chatrooms/chatroom-view.tsx +12 -9
- package/src/components/connectors/connector-health.tsx +6 -4
- package/src/components/connectors/connector-list.tsx +16 -11
- package/src/components/connectors/connector-sheet.tsx +12 -6
- package/src/components/home/home-view.tsx +38 -24
- package/src/components/input/chat-input.tsx +10 -1
- package/src/components/layout/app-layout.tsx +2 -38
- package/src/components/layout/sheet-layer.tsx +50 -0
- package/src/components/mcp-servers/mcp-server-list.tsx +37 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +12 -2
- package/src/components/plugins/plugin-list.tsx +8 -4
- package/src/components/plugins/plugin-sheet.tsx +2 -1
- package/src/components/providers/provider-list.tsx +3 -2
- package/src/components/providers/provider-sheet.tsx +2 -1
- package/src/components/runs/run-list.tsx +11 -7
- package/src/components/schedules/schedule-card.tsx +5 -3
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/attachment-chip.tsx +19 -3
- package/src/components/shared/notification-center.tsx +6 -3
- package/src/components/shared/settings/plugin-manager.tsx +3 -2
- package/src/components/shared/settings/section-embedding.tsx +2 -1
- package/src/components/shared/settings/section-orchestrator.tsx +2 -1
- package/src/components/shared/settings/section-user-preferences.tsx +107 -0
- package/src/components/shared/settings/settings-page.tsx +13 -9
- package/src/components/skills/clawhub-browser.tsx +15 -4
- package/src/components/skills/skill-list.tsx +15 -4
- package/src/components/tasks/approvals-panel.tsx +2 -1
- package/src/components/tasks/task-board.tsx +35 -37
- package/src/components/tasks/task-sheet.tsx +4 -3
- package/src/components/ui/full-screen-loader.tsx +164 -0
- package/src/components/wallets/wallet-approval-dialog.tsx +2 -1
- package/src/components/wallets/wallet-panel.tsx +6 -5
- package/src/components/wallets/wallet-section.tsx +3 -2
- package/src/components/webhooks/webhook-list.tsx +4 -5
- package/src/components/webhooks/webhook-sheet.tsx +6 -6
- package/src/hooks/use-app-bootstrap.ts +202 -0
- package/src/hooks/use-mounted-ref.ts +14 -0
- package/src/hooks/use-now.ts +31 -0
- package/src/hooks/use-openclaw-gateway.ts +2 -1
- package/src/instrumentation.ts +20 -8
- package/src/lib/agent-default-tools.test.ts +52 -0
- package/src/lib/agent-default-tools.ts +40 -0
- package/src/lib/api-client.test.ts +21 -0
- package/src/lib/api-client.ts +6 -11
- package/src/lib/canvas-content.test.ts +360 -0
- package/src/lib/chat-streaming-state.test.ts +49 -2
- package/src/lib/chat-streaming-state.ts +26 -10
- package/src/lib/fetch-timeout.test.ts +54 -0
- package/src/lib/fetch-timeout.ts +60 -3
- package/src/lib/live-tool-events.test.ts +77 -0
- package/src/lib/live-tool-events.ts +73 -0
- package/src/lib/local-observability.test.ts +2 -2
- package/src/lib/openclaw-endpoint.test.ts +1 -1
- package/src/lib/providers/anthropic.ts +12 -16
- package/src/lib/providers/index.ts +4 -2
- package/src/lib/providers/ollama.ts +9 -6
- package/src/lib/providers/openai.ts +11 -14
- package/src/lib/runtime-env.test.ts +8 -8
- package/src/lib/schedule-dedupe-advanced.test.ts +2 -2
- package/src/lib/schedule-dedupe.test.ts +1 -1
- package/src/lib/schedule-dedupe.ts +3 -2
- package/src/lib/server/agent-thread-session.test.ts +6 -6
- package/src/lib/server/agent-thread-session.ts +6 -9
- package/src/lib/server/alert-dispatch.ts +2 -1
- package/src/lib/server/api-routes.test.ts +6 -6
- package/src/lib/server/approval-connector-notify.test.ts +4 -4
- package/src/lib/server/approvals-auto-approve.test.ts +29 -29
- package/src/lib/server/approvals.test.ts +317 -0
- package/src/lib/server/approvals.ts +5 -4
- package/src/lib/server/autonomy-runtime.test.ts +11 -11
- package/src/lib/server/browser-state.ts +2 -2
- package/src/lib/server/capability-router.test.ts +1 -1
- package/src/lib/server/capability-router.ts +3 -2
- package/src/lib/server/chat-execution-advanced.test.ts +15 -2
- package/src/lib/server/chat-execution-connector-delivery.ts +67 -0
- package/src/lib/server/chat-execution-disabled.test.ts +3 -3
- package/src/lib/server/chat-execution-eval-history.test.ts +3 -3
- package/src/lib/server/chat-execution-heartbeat.test.ts +42 -1
- package/src/lib/server/chat-execution-session-sync.test.ts +119 -0
- package/src/lib/server/chat-execution-tool-events.ts +116 -0
- package/src/lib/server/chat-execution-utils.test.ts +479 -0
- package/src/lib/server/chat-execution-utils.ts +533 -0
- package/src/lib/server/chat-execution.ts +153 -748
- package/src/lib/server/chat-streaming-utils.ts +174 -0
- package/src/lib/server/chat-turn-tool-routing.ts +310 -0
- package/src/lib/server/chatroom-session-persistence.test.ts +2 -2
- package/src/lib/server/clawhub-client.ts +2 -1
- package/src/lib/server/collection-helpers.test.ts +92 -0
- package/src/lib/server/collection-helpers.ts +25 -3
- package/src/lib/server/connectors/access.ts +146 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +1 -1
- package/src/lib/server/connectors/bluebubbles.ts +4 -4
- package/src/lib/server/connectors/commands.ts +367 -0
- package/src/lib/server/connectors/connector-routing.test.ts +4 -4
- package/src/lib/server/connectors/delivery.ts +142 -0
- package/src/lib/server/connectors/discord.ts +37 -40
- package/src/lib/server/connectors/email.ts +11 -10
- package/src/lib/server/connectors/googlechat.ts +4 -4
- package/src/lib/server/connectors/inbound-audio-transcription.ts +2 -1
- package/src/lib/server/connectors/ingress-delivery.ts +23 -0
- package/src/lib/server/connectors/manager-roundtrip.test.ts +300 -0
- package/src/lib/server/connectors/manager.test.ts +352 -77
- package/src/lib/server/connectors/manager.ts +134 -673
- package/src/lib/server/connectors/matrix.ts +4 -4
- package/src/lib/server/connectors/message-sentinel.ts +7 -0
- package/src/lib/server/connectors/openclaw.test.ts +1 -1
- package/src/lib/server/connectors/openclaw.ts +8 -10
- package/src/lib/server/connectors/outbox.test.ts +192 -0
- package/src/lib/server/connectors/outbox.ts +369 -0
- package/src/lib/server/connectors/pairing.test.ts +18 -1
- package/src/lib/server/connectors/pairing.ts +49 -4
- package/src/lib/server/connectors/policy.ts +9 -3
- package/src/lib/server/connectors/reconnect-state.ts +71 -0
- package/src/lib/server/connectors/response-media.ts +256 -0
- package/src/lib/server/connectors/runtime-state.ts +67 -0
- package/src/lib/server/connectors/session.test.ts +357 -0
- package/src/lib/server/connectors/session.ts +422 -0
- package/src/lib/server/connectors/signal.ts +7 -7
- package/src/lib/server/connectors/slack.ts +43 -43
- package/src/lib/server/connectors/teams.ts +4 -4
- package/src/lib/server/connectors/telegram.ts +37 -43
- package/src/lib/server/connectors/types.ts +31 -1
- package/src/lib/server/connectors/whatsapp.test.ts +108 -0
- package/src/lib/server/connectors/whatsapp.ts +106 -34
- package/src/lib/server/context-manager.test.ts +409 -0
- package/src/lib/server/cost.test.ts +1 -1
- package/src/lib/server/daemon-policy.ts +78 -0
- package/src/lib/server/daemon-state-connectors.test.ts +167 -0
- package/src/lib/server/daemon-state.test.ts +283 -55
- package/src/lib/server/daemon-state.ts +106 -109
- package/src/lib/server/data-dir.test.ts +5 -5
- package/src/lib/server/data-dir.ts +4 -0
- package/src/lib/server/delegation-jobs-advanced.test.ts +1 -1
- package/src/lib/server/delegation-jobs.test.ts +87 -0
- package/src/lib/server/delegation-jobs.ts +42 -48
- package/src/lib/server/devserver-launch.ts +1 -1
- package/src/lib/server/document-utils.ts +7 -9
- package/src/lib/server/elevenlabs.ts +2 -1
- package/src/lib/server/embeddings.test.ts +105 -0
- package/src/lib/server/ethereum.ts +3 -2
- package/src/lib/server/eval/agent-regression.ts +3 -2
- package/src/lib/server/eval/runner.ts +2 -1
- package/src/lib/server/eval/scorer.ts +2 -1
- package/src/lib/server/evm-swap.ts +2 -1
- package/src/lib/server/gateway/protocol.test.ts +1 -1
- package/src/lib/server/guardian.ts +2 -1
- package/src/lib/server/heartbeat-blocked-suppression.test.ts +151 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +6 -6
- package/src/lib/server/heartbeat-service.test.ts +406 -0
- package/src/lib/server/heartbeat-service.ts +54 -7
- package/src/lib/server/heartbeat-wake.test.ts +19 -0
- package/src/lib/server/heartbeat-wake.ts +17 -16
- package/src/lib/server/integrity-monitor.test.ts +149 -0
- package/src/lib/server/json-utils.ts +22 -0
- package/src/lib/server/knowledge-db.test.ts +13 -13
- package/src/lib/server/link-understanding.ts +2 -1
- package/src/lib/server/llm-response-cache.test.ts +1 -1
- package/src/lib/server/main-agent-loop-advanced.test.ts +65 -3
- package/src/lib/server/main-agent-loop.test.ts +6 -6
- package/src/lib/server/main-agent-loop.ts +21 -7
- package/src/lib/server/mcp-client.test.ts +1 -1
- package/src/lib/server/mcp-conformance.test.ts +1 -1
- package/src/lib/server/mcp-conformance.ts +3 -2
- package/src/lib/server/memory-consolidation.ts +2 -1
- package/src/lib/server/memory-db.test.ts +485 -0
- package/src/lib/server/memory-db.ts +39 -26
- package/src/lib/server/memory-graph.test.ts +2 -2
- package/src/lib/server/memory-policy.test.ts +7 -7
- package/src/lib/server/memory-retrieval.test.ts +1 -1
- package/src/lib/server/openclaw-config-sync.ts +2 -1
- package/src/lib/server/openclaw-deploy.test.ts +1 -1
- package/src/lib/server/openclaw-deploy.ts +8 -12
- package/src/lib/server/openclaw-exec-config.ts +2 -1
- package/src/lib/server/openclaw-gateway.ts +6 -7
- package/src/lib/server/openclaw-skills-normalize.ts +2 -1
- package/src/lib/server/openclaw-sync.ts +7 -5
- package/src/lib/server/orchestrator-lg-structure.test.ts +17 -0
- package/src/lib/server/orchestrator-lg.ts +199 -327
- package/src/lib/server/path-utils.ts +31 -0
- package/src/lib/server/perf.ts +161 -0
- package/src/lib/server/plugins-approval-guidance.ts +115 -0
- package/src/lib/server/plugins.test.ts +1 -1
- package/src/lib/server/plugins.ts +22 -132
- package/src/lib/server/process-manager.ts +5 -8
- package/src/lib/server/provider-health.test.ts +137 -0
- package/src/lib/server/provider-health.ts +3 -3
- package/src/lib/server/provider-model-discovery.ts +3 -12
- package/src/lib/server/queue-followups.test.ts +9 -9
- package/src/lib/server/queue-reconcile.test.ts +2 -2
- package/src/lib/server/queue-recovery.test.ts +269 -0
- package/src/lib/server/queue.test.ts +570 -0
- package/src/lib/server/queue.ts +62 -455
- package/src/lib/server/resolve-image.ts +30 -0
- package/src/lib/server/runtime-settings.test.ts +4 -4
- package/src/lib/server/runtime-storage-write-paths.test.ts +60 -0
- package/src/lib/server/schedule-normalization.test.ts +279 -0
- package/src/lib/server/schedule-service.ts +263 -0
- package/src/lib/server/scheduler.ts +17 -74
- package/src/lib/server/session-mailbox.test.ts +191 -0
- package/src/lib/server/session-run-manager.test.ts +640 -0
- package/src/lib/server/session-run-manager.ts +59 -15
- package/src/lib/server/session-tools/autonomy-tools.test.ts +20 -20
- package/src/lib/server/session-tools/calendar.ts +2 -1
- package/src/lib/server/session-tools/canvas.ts +2 -1
- package/src/lib/server/session-tools/chatroom.ts +2 -1
- package/src/lib/server/session-tools/connector.ts +26 -28
- package/src/lib/server/session-tools/context-mgmt.ts +3 -2
- package/src/lib/server/session-tools/crawl.ts +4 -3
- package/src/lib/server/session-tools/crud.ts +105 -324
- package/src/lib/server/session-tools/delegate-fallback.test.ts +9 -9
- package/src/lib/server/session-tools/delegate.ts +6 -8
- package/src/lib/server/session-tools/discovery-approvals.test.ts +15 -15
- package/src/lib/server/session-tools/discovery.ts +4 -3
- package/src/lib/server/session-tools/document.ts +2 -1
- package/src/lib/server/session-tools/email.ts +2 -1
- package/src/lib/server/session-tools/extract.ts +2 -1
- package/src/lib/server/session-tools/file.ts +4 -3
- package/src/lib/server/session-tools/http.ts +2 -1
- package/src/lib/server/session-tools/human-loop.ts +2 -1
- package/src/lib/server/session-tools/image-gen.ts +4 -3
- package/src/lib/server/session-tools/index.ts +26 -30
- package/src/lib/server/session-tools/mailbox.ts +2 -1
- package/src/lib/server/session-tools/manage-connectors.test.ts +4 -4
- package/src/lib/server/session-tools/manage-schedules.test.ts +12 -12
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +5 -5
- package/src/lib/server/session-tools/manage-tasks.test.ts +2 -2
- package/src/lib/server/session-tools/monitor.ts +2 -1
- package/src/lib/server/session-tools/platform.ts +2 -1
- package/src/lib/server/session-tools/plugin-creator.ts +2 -1
- package/src/lib/server/session-tools/replicate.ts +3 -2
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +6 -6
- package/src/lib/server/session-tools/shell.ts +4 -9
- package/src/lib/server/session-tools/subagent.ts +322 -170
- package/src/lib/server/session-tools/table.ts +6 -5
- package/src/lib/server/session-tools/wallet-tool.test.ts +3 -3
- package/src/lib/server/session-tools/wallet.ts +7 -6
- package/src/lib/server/session-tools/web-browser-config.test.ts +1 -0
- package/src/lib/server/session-tools/web-utils.ts +317 -0
- package/src/lib/server/session-tools/web.ts +62 -328
- package/src/lib/server/skill-prompt-budget.test.ts +1 -1
- package/src/lib/server/skills-normalize.ts +2 -1
- package/src/lib/server/storage-item-access.test.ts +302 -0
- package/src/lib/server/storage.ts +366 -314
- package/src/lib/server/stream-agent-chat.test.ts +82 -3
- package/src/lib/server/stream-agent-chat.ts +146 -510
- package/src/lib/server/stream-continuation.ts +412 -0
- package/src/lib/server/subagent-lineage.test.ts +647 -0
- package/src/lib/server/subagent-lineage.ts +435 -0
- package/src/lib/server/subagent-runtime.test.ts +484 -0
- package/src/lib/server/subagent-runtime.ts +419 -0
- package/src/lib/server/subagent-swarm.test.ts +391 -0
- package/src/lib/server/subagent-swarm.ts +564 -0
- package/src/lib/server/system-events.ts +3 -3
- package/src/lib/server/task-followups.test.ts +491 -0
- package/src/lib/server/task-followups.ts +391 -0
- package/src/lib/server/task-lifecycle.test.ts +205 -0
- package/src/lib/server/task-lifecycle.ts +200 -0
- package/src/lib/server/task-quality-gate.test.ts +1 -1
- package/src/lib/server/task-resume.ts +208 -0
- package/src/lib/server/task-service.test.ts +108 -0
- package/src/lib/server/task-service.ts +264 -0
- package/src/lib/server/task-validation.test.ts +1 -1
- package/src/lib/server/test-utils/run-with-temp-data-dir.ts +42 -0
- package/src/lib/server/tool-capability-policy.test.ts +2 -2
- package/src/lib/server/tool-capability-policy.ts +3 -2
- package/src/lib/server/tool-planning.ts +2 -1
- package/src/lib/server/tool-retry.ts +2 -3
- package/src/lib/server/wake-dispatcher.test.ts +303 -0
- package/src/lib/server/wake-dispatcher.ts +318 -0
- package/src/lib/server/wake-mode.test.ts +161 -0
- package/src/lib/server/wake-mode.ts +174 -0
- package/src/lib/server/wallet-service.ts +8 -9
- package/src/lib/server/watch-jobs.ts +2 -1
- package/src/lib/server/workspace-context.ts +2 -2
- package/src/lib/shared-utils.test.ts +142 -0
- package/src/lib/shared-utils.ts +62 -0
- package/src/lib/tool-event-summary.ts +2 -1
- package/src/lib/view-routes.test.ts +100 -0
- package/src/lib/wallet.test.ts +322 -6
- package/src/proxy.test.ts +4 -4
- package/src/proxy.ts +2 -3
- package/src/stores/set-if-changed.ts +40 -0
- package/src/stores/slices/agent-slice.ts +111 -0
- package/src/stores/slices/auth-slice.ts +25 -0
- package/src/stores/slices/data-slice.ts +301 -0
- package/src/stores/slices/index.ts +7 -0
- package/src/stores/slices/session-slice.ts +112 -0
- package/src/stores/slices/task-slice.ts +63 -0
- package/src/stores/slices/ui-slice.ts +192 -0
- package/src/stores/use-app-store.ts +17 -822
- package/src/stores/use-approval-store.ts +2 -1
- package/src/stores/use-chat-store.ts +8 -1
- package/src/types/index.ts +10 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { MessageToolEvent } from '@/types'
|
|
2
|
+
import { canonicalizePluginId } from './tool-aliases'
|
|
3
|
+
import { extractSuggestions } from './suggestions'
|
|
4
|
+
import { isDirectMemoryWriteRequest } from './memory-policy'
|
|
5
|
+
import {
|
|
6
|
+
isBroadGoal,
|
|
7
|
+
looksLikeExternalWalletTask,
|
|
8
|
+
looksLikeOpenEndedDeliverableTask,
|
|
9
|
+
} from './stream-continuation'
|
|
10
|
+
|
|
11
|
+
export function getExplicitRequiredToolNames(userMessage: string, enabledPlugins: string[]): string[] {
|
|
12
|
+
const normalized = userMessage.toLowerCase()
|
|
13
|
+
const required: string[] = []
|
|
14
|
+
const hasEnabledTool = (toolId: string) => enabledPlugins.some((enabled) => (canonicalizePluginId(enabled) || enabled) === toolId)
|
|
15
|
+
|
|
16
|
+
if (hasEnabledTool('ask_human')
|
|
17
|
+
&& (/\bask_human\b/.test(normalized) || /ask the human/.test(normalized) || /request_input/.test(normalized))) {
|
|
18
|
+
required.push('ask_human')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (hasEnabledTool('email')
|
|
22
|
+
&& (/\bemail\b/.test(normalized) || /send a welcome email/.test(normalized) || /send an email/.test(normalized))) {
|
|
23
|
+
required.push('email')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (
|
|
27
|
+
hasEnabledTool('shell')
|
|
28
|
+
&& (
|
|
29
|
+
/\bcurl request\b/.test(normalized)
|
|
30
|
+
|| /\b(?:run|execute|do|use|try)\b[\s\S]{0,40}\bcurl\b/.test(normalized)
|
|
31
|
+
|| /\brun (?:this )?command\b/.test(normalized)
|
|
32
|
+
|| /\buse (?:the )?(?:shell|terminal)\b/.test(normalized)
|
|
33
|
+
|| /\bin (?:the )?terminal\b/.test(normalized)
|
|
34
|
+
)
|
|
35
|
+
) {
|
|
36
|
+
required.push('shell')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return required
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function shouldForceExternalServiceSummary(params: {
|
|
43
|
+
userMessage: string
|
|
44
|
+
finalResponse: string
|
|
45
|
+
hasToolCalls: boolean
|
|
46
|
+
toolEventCount: number
|
|
47
|
+
}): boolean {
|
|
48
|
+
if (!looksLikeExternalWalletTask(params.userMessage)) return false
|
|
49
|
+
if (!params.hasToolCalls || params.toolEventCount === 0) return false
|
|
50
|
+
const trimmed = params.finalResponse.trim()
|
|
51
|
+
if (!trimmed) return true
|
|
52
|
+
if (/\b(blocker|blocked|cannot|can't|requires|need|missing|last reversible step|next step)\b/i.test(trimmed)) return false
|
|
53
|
+
if (trimmed.length >= 240 && !/(let me|i'll|i will|checking|verify|promising|look into|explore|access their interface)/i.test(trimmed)) return false
|
|
54
|
+
return /:$/.test(trimmed) || /(let me|i'll|i will|checking|verify|promising|look into|explore|access their interface)/i.test(trimmed) || trimmed.length < 240
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function resolveToolAction(input: unknown): string {
|
|
58
|
+
if (input && typeof input === 'object' && !Array.isArray(input)) {
|
|
59
|
+
const action = (input as Record<string, unknown>).action
|
|
60
|
+
return typeof action === 'string' ? action.trim().toLowerCase() : ''
|
|
61
|
+
}
|
|
62
|
+
if (typeof input !== 'string') return ''
|
|
63
|
+
const trimmed = input.trim()
|
|
64
|
+
if (!trimmed.startsWith('{')) return ''
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(trimmed) as Record<string, unknown>
|
|
67
|
+
return typeof parsed.action === 'string' ? parsed.action.trim().toLowerCase() : ''
|
|
68
|
+
} catch {
|
|
69
|
+
return ''
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function shouldTerminateOnSuccessfulMemoryMutation(params: {
|
|
74
|
+
toolName: string
|
|
75
|
+
toolInput: unknown
|
|
76
|
+
toolOutput: string
|
|
77
|
+
}): boolean {
|
|
78
|
+
const canonicalToolName = canonicalizePluginId(params.toolName) || params.toolName
|
|
79
|
+
if (canonicalToolName !== 'memory') return false
|
|
80
|
+
const exactToolName = String(params.toolName || '').trim().toLowerCase()
|
|
81
|
+
const action = exactToolName === 'memory_store'
|
|
82
|
+
? 'store'
|
|
83
|
+
: exactToolName === 'memory_update'
|
|
84
|
+
? 'update'
|
|
85
|
+
: resolveToolAction(params.toolInput)
|
|
86
|
+
if (action !== 'store' && action !== 'update') return false
|
|
87
|
+
const output = extractSuggestions(params.toolOutput || '').clean.trim()
|
|
88
|
+
if (!output || /^error[:\s]/i.test(output)) return false
|
|
89
|
+
if (!/^(stored|updated) memory\b/i.test(output)) return false
|
|
90
|
+
return /no further memory lookup is needed unless the user asked you to verify/i.test(output)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getWalletApprovalBoundaryAction(output: string): string | null {
|
|
94
|
+
if (!output.includes('plugin_wallet_')) return null
|
|
95
|
+
if (/"type":"plugin_wallet_transfer_request"/.test(output)) return 'send'
|
|
96
|
+
const actionMatch = output.match(/"action":"([^"]+)"/)
|
|
97
|
+
const action = actionMatch?.[1] || ''
|
|
98
|
+
if (!action) return null
|
|
99
|
+
const readOnlyActions = new Set([
|
|
100
|
+
'balance',
|
|
101
|
+
'address',
|
|
102
|
+
'transactions',
|
|
103
|
+
'encode_contract_call',
|
|
104
|
+
'simulate_transaction',
|
|
105
|
+
])
|
|
106
|
+
return readOnlyActions.has(action) ? null : action
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function isWalletSimulationResult(toolName: string, output: string): boolean {
|
|
110
|
+
return toolName === 'wallet_tool' && /"status":"simulated"/.test(output)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function updateStreamedToolEvents(
|
|
114
|
+
events: MessageToolEvent[],
|
|
115
|
+
event: { type: 'call' | 'result'; name: string; input?: string; output?: string; toolCallId?: string },
|
|
116
|
+
) {
|
|
117
|
+
if (event.type === 'call') {
|
|
118
|
+
events.push({
|
|
119
|
+
name: event.name,
|
|
120
|
+
input: event.input || '',
|
|
121
|
+
toolCallId: event.toolCallId,
|
|
122
|
+
})
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
const index = event.toolCallId
|
|
126
|
+
? events.findLastIndex((entry) => entry.toolCallId === event.toolCallId && !entry.output)
|
|
127
|
+
: events.findLastIndex((entry) => entry.name === event.name && !entry.output)
|
|
128
|
+
if (index === -1) return
|
|
129
|
+
events[index] = {
|
|
130
|
+
...events[index],
|
|
131
|
+
output: event.output || '',
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function compactThreadRecallText(text: string, maxChars = 180): string {
|
|
136
|
+
const compact = extractSuggestions(text || '').clean.replace(/\s+/g, ' ').trim()
|
|
137
|
+
if (!compact) return ''
|
|
138
|
+
return compact.length > maxChars ? `${compact.slice(0, maxChars - 3)}...` : compact
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const DIRECT_MEMORY_WRITE_CONFIRMATION_ONLY_RE = /\b(?:then|and then|after that)?\s*(?:confirm|recap|repeat|summarize|tell me|say)\b[\s\S]{0,120}\b(?:stored|saved|updated|remembered|wrote|write)\b/i
|
|
142
|
+
const DIRECT_MEMORY_WRITE_EXTRA_ACTION_RE = /\b(?:then|and then|after that|also)\b[\s\S]{0,160}\b(?:write|create|send|email|message|delegate|research|search|browse|open|edit|build|schedule|plan|review|analy[sz]e)\b/i
|
|
143
|
+
|
|
144
|
+
export function isNarrowDirectMemoryWriteTurn(message: string): boolean {
|
|
145
|
+
const trimmed = String(message || '').trim()
|
|
146
|
+
if (!trimmed || !isDirectMemoryWriteRequest(trimmed)) return false
|
|
147
|
+
if (looksLikeOpenEndedDeliverableTask(trimmed)) return false
|
|
148
|
+
if (DIRECT_MEMORY_WRITE_EXTRA_ACTION_RE.test(trimmed) && !DIRECT_MEMORY_WRITE_CONFIRMATION_ONLY_RE.test(trimmed)) {
|
|
149
|
+
return false
|
|
150
|
+
}
|
|
151
|
+
return !isBroadGoal(trimmed) || DIRECT_MEMORY_WRITE_CONFIRMATION_ONLY_RE.test(trimmed) || !/[?]$/.test(trimmed)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const CURRENT_THREAD_RECALL_BLOCKED_TOOL_IDS = new Set([
|
|
155
|
+
'memory',
|
|
156
|
+
'manage_sessions',
|
|
157
|
+
'web',
|
|
158
|
+
'context_mgmt',
|
|
159
|
+
])
|
|
160
|
+
|
|
161
|
+
export function shouldAllowToolForCurrentThreadRecall(toolName: string): boolean {
|
|
162
|
+
const canonicalToolName = canonicalizePluginId(toolName) || toolName.trim().toLowerCase()
|
|
163
|
+
return !CURRENT_THREAD_RECALL_BLOCKED_TOOL_IDS.has(canonicalToolName)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const DIRECT_MEMORY_WRITE_ALLOWED_TOOL_IDS = new Set([
|
|
167
|
+
'memory_store',
|
|
168
|
+
'memory_update',
|
|
169
|
+
])
|
|
170
|
+
|
|
171
|
+
export function shouldAllowToolForDirectMemoryWrite(toolName: string): boolean {
|
|
172
|
+
const rawToolName = toolName.trim().toLowerCase()
|
|
173
|
+
return DIRECT_MEMORY_WRITE_ALLOWED_TOOL_IDS.has(rawToolName)
|
|
174
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat Turn — Post-LLM Tool Routing
|
|
3
|
+
*
|
|
4
|
+
* After the LLM produces a response, this module handles forced tool
|
|
5
|
+
* invocations (explicitly requested by the user), auto-delegation
|
|
6
|
+
* (routing coding tasks to CLI backends), and auto-routing (browsing,
|
|
7
|
+
* research, memory intents).
|
|
8
|
+
*
|
|
9
|
+
* Extracted from chat-execution.ts for testability and readability.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { StructuredToolInterface } from '@langchain/core/tools'
|
|
13
|
+
import type { MessageToolEvent, SSEEvent } from '@/types'
|
|
14
|
+
import { loadAgents } from './storage'
|
|
15
|
+
import { buildSessionTools } from './session-tools'
|
|
16
|
+
import { resolveConcreteToolPolicyBlock, type PluginPolicyDecision } from './tool-capability-policy'
|
|
17
|
+
import { resolveActiveProjectContext } from './project-context'
|
|
18
|
+
import { genId } from '@/lib/id'
|
|
19
|
+
import { rankDelegatesByHealth } from './provider-health'
|
|
20
|
+
import { routeTaskIntent, type CapabilityRoutingDecision } from './capability-router'
|
|
21
|
+
import {
|
|
22
|
+
type DelegateTool,
|
|
23
|
+
type SessionWithTools,
|
|
24
|
+
enabledDelegationTools,
|
|
25
|
+
extractConnectorMessageArgs,
|
|
26
|
+
extractDelegationTask,
|
|
27
|
+
findFirstUrl,
|
|
28
|
+
hasToolEnabled,
|
|
29
|
+
hasDirectLocalCodingTools,
|
|
30
|
+
isMemoryListIntent,
|
|
31
|
+
requestedToolNamesFromMessage,
|
|
32
|
+
translateRequestedToolInvocation,
|
|
33
|
+
} from './chat-execution-utils'
|
|
34
|
+
import { errorMessage } from '@/lib/shared-utils'
|
|
35
|
+
|
|
36
|
+
interface ToolRoutingSession extends SessionWithTools {
|
|
37
|
+
agentId?: string | null
|
|
38
|
+
cwd: string
|
|
39
|
+
memoryScopeMode?: string | null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ToolRoutingContext {
|
|
43
|
+
session: ToolRoutingSession
|
|
44
|
+
sessionId: string
|
|
45
|
+
message: string
|
|
46
|
+
effectiveMessage: string
|
|
47
|
+
enabledPlugins: string[]
|
|
48
|
+
toolPolicy: PluginPolicyDecision
|
|
49
|
+
appSettings: Record<string, unknown>
|
|
50
|
+
internal: boolean
|
|
51
|
+
source: string
|
|
52
|
+
toolEvents: MessageToolEvent[]
|
|
53
|
+
emit: (ev: SSEEvent) => void
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ToolRoutingResult {
|
|
57
|
+
/** Tool names that were actually invoked */
|
|
58
|
+
calledNames: Set<string>
|
|
59
|
+
/** Updated full response (may be modified by delegate output) */
|
|
60
|
+
fullResponse: string
|
|
61
|
+
/** Updated error message (may be cleared on failover success) */
|
|
62
|
+
errorMessage: string | undefined
|
|
63
|
+
/** Missed requested tools (for warning) */
|
|
64
|
+
missedRequestedTools: string[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function extractDelegateResponse(outputText: string): string | null {
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(outputText) as Record<string, unknown>
|
|
70
|
+
if (typeof parsed.response === 'string' && parsed.response.trim()) return parsed.response.trim()
|
|
71
|
+
if (typeof parsed.result === 'string' && parsed.result.trim()) return parsed.result.trim()
|
|
72
|
+
return null
|
|
73
|
+
} catch {
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Core: Invoke a single session tool
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
async function invokeSessionTool(
|
|
83
|
+
ctx: ToolRoutingContext,
|
|
84
|
+
toolName: string,
|
|
85
|
+
args: Record<string, unknown>,
|
|
86
|
+
failurePrefix: string,
|
|
87
|
+
calledNames: Set<string>,
|
|
88
|
+
): Promise<{ invoked: boolean; responseOverride: string | null }> {
|
|
89
|
+
const blockedReason = resolveConcreteToolPolicyBlock(toolName, ctx.toolPolicy, ctx.appSettings)
|
|
90
|
+
if (blockedReason) {
|
|
91
|
+
ctx.emit({ t: 'err', text: `Capability policy blocked tool invocation "${toolName}": ${blockedReason}` })
|
|
92
|
+
return { invoked: false, responseOverride: null }
|
|
93
|
+
}
|
|
94
|
+
if (
|
|
95
|
+
(ctx.appSettings as Record<string, unknown>).safetyRequireApprovalForOutbound === true
|
|
96
|
+
&& toolName === 'connector_message_tool'
|
|
97
|
+
&& ctx.source !== 'chat'
|
|
98
|
+
) {
|
|
99
|
+
ctx.emit({ t: 'err', text: 'Outbound connector messaging requires explicit user approval.' })
|
|
100
|
+
return { invoked: false, responseOverride: null }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const agent = ctx.session.agentId ? loadAgents()[ctx.session.agentId] : null
|
|
104
|
+
const agentRecord = agent as Record<string, unknown> | null
|
|
105
|
+
const activeProjectContext = resolveActiveProjectContext(ctx.session as unknown as { agentId?: string | null; cwd?: string | null; projectId?: string | null })
|
|
106
|
+
const { tools, cleanup } = await buildSessionTools(ctx.session.cwd, ctx.enabledPlugins, {
|
|
107
|
+
agentId: ctx.session.agentId || null,
|
|
108
|
+
sessionId: ctx.sessionId,
|
|
109
|
+
platformAssignScope: (agentRecord?.platformAssignScope as 'self' | 'all') || 'self',
|
|
110
|
+
mcpServerIds: agentRecord?.mcpServerIds as string[] | undefined,
|
|
111
|
+
mcpDisabledTools: agentRecord?.mcpDisabledTools as string[] | undefined,
|
|
112
|
+
projectId: activeProjectContext.projectId,
|
|
113
|
+
projectRoot: activeProjectContext.projectRoot,
|
|
114
|
+
projectName: activeProjectContext.project?.name || null,
|
|
115
|
+
projectDescription: activeProjectContext.project?.description || null,
|
|
116
|
+
memoryScopeMode: (ctx.session.memoryScopeMode ?? agentRecord?.memoryScopeMode as string | null ?? null) as 'all' | 'auto' | 'global' | 'agent' | 'session' | 'project' | null,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const directTool = tools.find((t) => t?.name === toolName) as StructuredToolInterface | undefined
|
|
121
|
+
const availableToolNames = tools.map((candidate) => candidate?.name).filter(Boolean)
|
|
122
|
+
const translated = directTool
|
|
123
|
+
? { toolName, args }
|
|
124
|
+
: translateRequestedToolInvocation(toolName, args, ctx.message, availableToolNames)
|
|
125
|
+
const selectedTool = directTool || tools.find((t) => t?.name === translated.toolName) as StructuredToolInterface | undefined
|
|
126
|
+
if (!selectedTool?.invoke) return { invoked: false, responseOverride: null }
|
|
127
|
+
|
|
128
|
+
const toolCallId = genId()
|
|
129
|
+
ctx.emit({ t: 'tool_call', toolName, toolInput: JSON.stringify(translated.args), toolCallId })
|
|
130
|
+
const toolOutput = await selectedTool.invoke(translated.args)
|
|
131
|
+
const outputText = typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput)
|
|
132
|
+
ctx.emit({ t: 'tool_result', toolName, toolOutput: outputText, toolCallId })
|
|
133
|
+
|
|
134
|
+
const delegateResponse = (
|
|
135
|
+
toolName === 'delegate'
|
|
136
|
+
|| toolName.startsWith('delegate_to_')
|
|
137
|
+
) ? extractDelegateResponse(outputText) : null
|
|
138
|
+
|
|
139
|
+
calledNames.add(toolName)
|
|
140
|
+
|
|
141
|
+
if (delegateResponse) {
|
|
142
|
+
return { invoked: true, responseOverride: delegateResponse }
|
|
143
|
+
}
|
|
144
|
+
return { invoked: true, responseOverride: null }
|
|
145
|
+
} catch (forceErr: unknown) {
|
|
146
|
+
ctx.emit({ t: 'err', text: `${failurePrefix}: ${errorMessage(forceErr)}` })
|
|
147
|
+
return { invoked: false, responseOverride: null }
|
|
148
|
+
} finally {
|
|
149
|
+
await cleanup()
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Main: Run all post-LLM tool routing
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
const FORCED_DELEGATION_TOOLS: DelegateTool[] = [
|
|
158
|
+
'delegate_to_claude_code',
|
|
159
|
+
'delegate_to_codex_cli',
|
|
160
|
+
'delegate_to_opencode_cli',
|
|
161
|
+
'delegate_to_gemini_cli',
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
export async function runPostLlmToolRouting(
|
|
165
|
+
ctx: ToolRoutingContext,
|
|
166
|
+
currentResponse: string,
|
|
167
|
+
currentError: string | undefined,
|
|
168
|
+
): Promise<ToolRoutingResult> {
|
|
169
|
+
const calledNames = new Set((ctx.toolEvents || []).map((t) => t.name))
|
|
170
|
+
let fullResponse = currentResponse
|
|
171
|
+
let errorMessage = currentError
|
|
172
|
+
|
|
173
|
+
const requestedToolNames = (!ctx.internal && ctx.source === 'chat')
|
|
174
|
+
? requestedToolNamesFromMessage(ctx.message)
|
|
175
|
+
: []
|
|
176
|
+
const routingDecision: CapabilityRoutingDecision | null = (!ctx.internal && ctx.source === 'chat')
|
|
177
|
+
? routeTaskIntent(ctx.message, ctx.enabledPlugins, ctx.appSettings)
|
|
178
|
+
: null
|
|
179
|
+
|
|
180
|
+
// --- Forced connector_message_tool ---
|
|
181
|
+
if (requestedToolNames.includes('connector_message_tool') && !calledNames.has('connector_message_tool')) {
|
|
182
|
+
const forcedArgs = extractConnectorMessageArgs(ctx.message)
|
|
183
|
+
if (forcedArgs) {
|
|
184
|
+
const result = await invokeSessionTool(
|
|
185
|
+
ctx, 'connector_message_tool',
|
|
186
|
+
forcedArgs as unknown as Record<string, unknown>,
|
|
187
|
+
'Forced connector_message_tool invocation failed',
|
|
188
|
+
calledNames,
|
|
189
|
+
)
|
|
190
|
+
if (result.responseOverride) fullResponse = result.responseOverride
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// --- Forced delegation tools ---
|
|
195
|
+
for (const toolName of FORCED_DELEGATION_TOOLS) {
|
|
196
|
+
if (!requestedToolNames.includes(toolName)) continue
|
|
197
|
+
if (calledNames.has(toolName)) continue
|
|
198
|
+
const task = extractDelegationTask(ctx.message, toolName)
|
|
199
|
+
if (!task) continue
|
|
200
|
+
const result = await invokeSessionTool(ctx, toolName, { task }, `Forced ${toolName} invocation failed`, calledNames)
|
|
201
|
+
if (result.responseOverride) fullResponse = result.responseOverride
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// --- Auto-delegation for coding intent ---
|
|
205
|
+
const hasDelegationCall = FORCED_DELEGATION_TOOLS.some((t) => calledNames.has(t))
|
|
206
|
+
const enabledDelegates = enabledDelegationTools(ctx.session)
|
|
207
|
+
const shouldAutoDelegateCoding = (!ctx.internal && ctx.source === 'chat')
|
|
208
|
+
&& enabledDelegates.length > 0
|
|
209
|
+
&& !hasDelegationCall
|
|
210
|
+
&& calledNames.size === 0
|
|
211
|
+
&& !requestedToolNames.length
|
|
212
|
+
&& !hasDirectLocalCodingTools(ctx.session)
|
|
213
|
+
&& routingDecision?.intent === 'coding'
|
|
214
|
+
|
|
215
|
+
if (shouldAutoDelegateCoding) {
|
|
216
|
+
const baseDelegationOrder = routingDecision?.preferredDelegates?.length
|
|
217
|
+
? routingDecision.preferredDelegates
|
|
218
|
+
: FORCED_DELEGATION_TOOLS
|
|
219
|
+
const delegationOrder = rankDelegatesByHealth(baseDelegationOrder as DelegateTool[])
|
|
220
|
+
.filter((tool) => enabledDelegates.includes(tool))
|
|
221
|
+
for (const delegateTool of delegationOrder) {
|
|
222
|
+
const result = await invokeSessionTool(ctx, delegateTool, { task: ctx.effectiveMessage.trim() }, 'Auto-delegation failed', calledNames)
|
|
223
|
+
if (result.invoked) {
|
|
224
|
+
if (result.responseOverride) fullResponse = result.responseOverride
|
|
225
|
+
break
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// --- Provider failover via delegation ---
|
|
231
|
+
const shouldFailoverDelegate = (!ctx.internal && ctx.source === 'chat')
|
|
232
|
+
&& !!errorMessage
|
|
233
|
+
&& !(fullResponse || '').trim()
|
|
234
|
+
&& enabledDelegates.length > 0
|
|
235
|
+
&& !hasDelegationCall
|
|
236
|
+
&& (routingDecision?.intent === 'coding' || routingDecision?.intent === 'general')
|
|
237
|
+
if (shouldFailoverDelegate) {
|
|
238
|
+
const preferred = routingDecision?.preferredDelegates?.length
|
|
239
|
+
? routingDecision.preferredDelegates
|
|
240
|
+
: FORCED_DELEGATION_TOOLS
|
|
241
|
+
const fallbackOrder = rankDelegatesByHealth(preferred as DelegateTool[])
|
|
242
|
+
.filter((tool) => enabledDelegates.includes(tool))
|
|
243
|
+
for (const delegateTool of fallbackOrder) {
|
|
244
|
+
const result = await invokeSessionTool(
|
|
245
|
+
ctx, delegateTool,
|
|
246
|
+
{ task: ctx.effectiveMessage.trim() },
|
|
247
|
+
`Provider failover via ${delegateTool} failed`,
|
|
248
|
+
calledNames,
|
|
249
|
+
)
|
|
250
|
+
if (result.invoked) {
|
|
251
|
+
if (result.responseOverride) fullResponse = result.responseOverride
|
|
252
|
+
errorMessage = undefined
|
|
253
|
+
break
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// --- Auto-routing: browsing, research, memory ---
|
|
259
|
+
const canAutoRoute = (!ctx.internal && ctx.source === 'chat')
|
|
260
|
+
&& !!routingDecision
|
|
261
|
+
&& calledNames.size === 0
|
|
262
|
+
&& requestedToolNames.length === 0
|
|
263
|
+
|
|
264
|
+
if (canAutoRoute && routingDecision?.intent === 'browsing' && routingDecision.primaryUrl && hasToolEnabled(ctx.session, 'browser')) {
|
|
265
|
+
const result = await invokeSessionTool(
|
|
266
|
+
ctx, 'browser',
|
|
267
|
+
{ action: 'read_page', url: routingDecision.primaryUrl },
|
|
268
|
+
'Auto browser routing failed',
|
|
269
|
+
calledNames,
|
|
270
|
+
)
|
|
271
|
+
if (result.responseOverride) fullResponse = result.responseOverride
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (canAutoRoute && routingDecision?.intent === 'research') {
|
|
275
|
+
const routeUrl = routingDecision.primaryUrl || findFirstUrl(ctx.message)
|
|
276
|
+
if (routeUrl && hasToolEnabled(ctx.session, 'web_fetch')) {
|
|
277
|
+
const result = await invokeSessionTool(ctx, 'web_fetch', { url: routeUrl }, 'Auto web_fetch routing failed', calledNames)
|
|
278
|
+
if (result.responseOverride) fullResponse = result.responseOverride
|
|
279
|
+
} else if (hasToolEnabled(ctx.session, 'web_search')) {
|
|
280
|
+
const result = await invokeSessionTool(ctx, 'web_search', { query: ctx.effectiveMessage.trim(), maxResults: 5 }, 'Auto web_search routing failed', calledNames)
|
|
281
|
+
if (result.responseOverride) fullResponse = result.responseOverride
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (canAutoRoute && calledNames.size === 0 && hasToolEnabled(ctx.session, 'memory') && isMemoryListIntent(ctx.message)) {
|
|
286
|
+
const result = await invokeSessionTool(
|
|
287
|
+
ctx, 'memory_tool',
|
|
288
|
+
{ action: 'list', key: '', scope: 'auto' },
|
|
289
|
+
'Auto memory listing failed',
|
|
290
|
+
calledNames,
|
|
291
|
+
)
|
|
292
|
+
if (result.responseOverride) fullResponse = result.responseOverride
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// --- Missed requested tools ---
|
|
296
|
+
const missed = requestedToolNames.filter((name) => !calledNames.has(name))
|
|
297
|
+
|
|
298
|
+
// When tool output is the only content and LLM produced nothing, provide a brief notice
|
|
299
|
+
if (calledNames.size > 0 && !fullResponse.trim()) {
|
|
300
|
+
const toolLabel = Array.from(calledNames).pop()?.replace(/_/g, ' ') || 'tool'
|
|
301
|
+
fullResponse = `Used **${toolLabel}** — see tool output above for details.`
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
calledNames,
|
|
306
|
+
fullResponse,
|
|
307
|
+
errorMessage,
|
|
308
|
+
missedRequestedTools: missed,
|
|
309
|
+
}
|
|
310
|
+
}
|
|
@@ -35,9 +35,9 @@ function runWithTempDataDir(script: string) {
|
|
|
35
35
|
describe('chatroom synthetic session persistence', () => {
|
|
36
36
|
it('reuses stored synthetic sessions and preserves delegate resume state', () => {
|
|
37
37
|
const output = runWithTempDataDir(`
|
|
38
|
-
const helpersMod = await import('./src/lib/server/chatroom-helpers
|
|
38
|
+
const helpersMod = await import('./src/lib/server/chatroom-helpers')
|
|
39
39
|
const helpers = helpersMod.default || helpersMod
|
|
40
|
-
const storageMod = await import('./src/lib/server/storage
|
|
40
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
41
41
|
const storage = storageMod.default || storageMod
|
|
42
42
|
const now = Date.now()
|
|
43
43
|
const agent = {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ClawHubSkill } from '@/types'
|
|
2
|
+
import { errorMessage } from '@/lib/shared-utils'
|
|
2
3
|
|
|
3
4
|
export interface ClawHubSearchResult {
|
|
4
5
|
skills: ClawHubSkill[]
|
|
@@ -71,7 +72,7 @@ export async function searchClawHub(query: string, page = 1, limit = 20): Promis
|
|
|
71
72
|
|
|
72
73
|
return { skills, total, page, nextCursor: data.nextCursor }
|
|
73
74
|
} catch (err: unknown) {
|
|
74
|
-
console.warn('[clawhub] search failed:',
|
|
75
|
+
console.warn('[clawhub] search failed:', errorMessage(err))
|
|
75
76
|
return { skills: [], total: 0, page }
|
|
76
77
|
}
|
|
77
78
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { mutateItem, deleteItem, type CollectionOps } from './collection-helpers'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Tests for collection-helpers reliability fixes:
|
|
8
|
+
* - mutateItem uses atomic patchStoredItem when ops.table is set
|
|
9
|
+
* - deleteItem uses row-level deleteStoredItem when ops.table is set
|
|
10
|
+
*
|
|
11
|
+
* Since patchStoredItem requires a real SQLite connection, we test the
|
|
12
|
+
* logic branching by verifying that the table-based path is taken when
|
|
13
|
+
* ops.table is set, and the legacy path when it is not.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
function makeInMemoryOps<T>(initial: Record<string, T>): CollectionOps<T> & { data: Record<string, T>; saveCount: number } {
|
|
17
|
+
const data = { ...initial }
|
|
18
|
+
const ops = {
|
|
19
|
+
data,
|
|
20
|
+
saveCount: 0,
|
|
21
|
+
load: () => ({ ...data }),
|
|
22
|
+
save: (next: Record<string, T>) => {
|
|
23
|
+
Object.keys(data).forEach((k) => delete data[k])
|
|
24
|
+
Object.assign(data, next)
|
|
25
|
+
ops.saveCount++
|
|
26
|
+
},
|
|
27
|
+
topic: 'test',
|
|
28
|
+
}
|
|
29
|
+
return ops
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('collection-helpers', () => {
|
|
33
|
+
describe('mutateItem (legacy path — no table)', () => {
|
|
34
|
+
it('mutates an existing item via load-all/save-all', () => {
|
|
35
|
+
const ops = makeInMemoryOps({ a: { name: 'Alice', score: 10 } })
|
|
36
|
+
const result = mutateItem(ops, 'a', (item) => ({ ...item, score: 20 }))
|
|
37
|
+
|
|
38
|
+
assert.ok(result)
|
|
39
|
+
assert.equal((result as Record<string, unknown>).score, 20)
|
|
40
|
+
assert.equal(ops.saveCount, 1)
|
|
41
|
+
assert.equal(ops.data.a.score, 20)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('returns null for missing item', () => {
|
|
45
|
+
const ops = makeInMemoryOps<Record<string, unknown>>({})
|
|
46
|
+
const result = mutateItem(ops, 'missing', (item) => item)
|
|
47
|
+
|
|
48
|
+
assert.equal(result, null)
|
|
49
|
+
assert.equal(ops.saveCount, 0)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('passes full collection to the mutation function', () => {
|
|
53
|
+
const ops = makeInMemoryOps({ a: { v: 1 }, b: { v: 2 } })
|
|
54
|
+
let capturedAll: Record<string, unknown> | null = null
|
|
55
|
+
mutateItem(ops, 'a', (item, all) => {
|
|
56
|
+
capturedAll = all as Record<string, unknown>
|
|
57
|
+
return item
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
assert.ok(capturedAll)
|
|
61
|
+
assert.ok('a' in capturedAll!)
|
|
62
|
+
assert.ok('b' in capturedAll!)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('deleteItem (legacy path — no table)', () => {
|
|
67
|
+
it('deletes an existing item', () => {
|
|
68
|
+
const ops = makeInMemoryOps({ a: { v: 1 }, b: { v: 2 } })
|
|
69
|
+
const result = deleteItem(ops, 'a')
|
|
70
|
+
|
|
71
|
+
assert.equal(result, true)
|
|
72
|
+
assert.equal(ops.data.a, undefined)
|
|
73
|
+
assert.equal(ops.data.b.v, 2)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('returns false for missing item', () => {
|
|
77
|
+
const ops = makeInMemoryOps<Record<string, unknown>>({})
|
|
78
|
+
const result = deleteItem(ops, 'missing')
|
|
79
|
+
assert.equal(result, false)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('uses custom deleteFn when provided', () => {
|
|
83
|
+
let deletedId: string | null = null
|
|
84
|
+
const ops = makeInMemoryOps({ a: { v: 1 } })
|
|
85
|
+
ops.deleteFn = (id: string) => { deletedId = id }
|
|
86
|
+
|
|
87
|
+
const result = deleteItem(ops, 'a')
|
|
88
|
+
assert.equal(result, true)
|
|
89
|
+
assert.equal(deletedId, 'a')
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
})
|
|
@@ -1,22 +1,41 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { notify } from './ws-hub'
|
|
3
|
+
import { deleteStoredItem, patchStoredItem, type StorageCollection } from './storage'
|
|
3
4
|
|
|
4
5
|
export interface CollectionOps<T> {
|
|
5
6
|
load: () => Record<string, T>
|
|
6
7
|
save: (data: Record<string, T>) => void
|
|
7
8
|
deleteFn?: (id: string) => void
|
|
8
9
|
topic?: string
|
|
10
|
+
/** When set, mutateItem/deleteItem use row-level upsert/delete instead of save-all. */
|
|
11
|
+
table?: StorageCollection
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
/**
|
|
12
|
-
* Load → 404 check → mutate →
|
|
15
|
+
* Load → 404 check → mutate → upsert single row → notify.
|
|
13
16
|
* `fn` receives the item and the full collection, returns the updated item.
|
|
17
|
+
*
|
|
18
|
+
* When `ops.table` is set, uses an atomic read-modify-write transaction via
|
|
19
|
+
* patchStoredItem to prevent concurrent writers from losing each other's updates.
|
|
14
20
|
*/
|
|
15
21
|
export function mutateItem<T>(
|
|
16
22
|
ops: CollectionOps<T>,
|
|
17
23
|
id: string,
|
|
18
24
|
fn: (item: T, all: Record<string, T>) => T,
|
|
19
25
|
): T | null {
|
|
26
|
+
if (ops.table) {
|
|
27
|
+
// Atomic path: read + mutate + write inside a single SQLite transaction
|
|
28
|
+
const result = patchStoredItem<T>(ops.table, id, (current) => {
|
|
29
|
+
if (current === null) return null
|
|
30
|
+
// Load full collection for the fn callback (rare code paths need it)
|
|
31
|
+
const all = ops.load()
|
|
32
|
+
all[id] = current
|
|
33
|
+
return fn(current, all)
|
|
34
|
+
})
|
|
35
|
+
if (result !== null && ops.topic) notify(ops.topic)
|
|
36
|
+
return result
|
|
37
|
+
}
|
|
38
|
+
// Legacy path: load-all → mutate → save-all (no table set)
|
|
20
39
|
const all = ops.load()
|
|
21
40
|
if (!all[id]) return null
|
|
22
41
|
all[id] = fn(all[id], all)
|
|
@@ -26,8 +45,9 @@ export function mutateItem<T>(
|
|
|
26
45
|
}
|
|
27
46
|
|
|
28
47
|
/**
|
|
29
|
-
* Load → 404 check → delete → notify.
|
|
30
|
-
* Uses `ops.deleteFn` if provided,
|
|
48
|
+
* Load → 404 check → delete single row → notify.
|
|
49
|
+
* Uses `ops.deleteFn` if provided, then `ops.table` for row-level delete,
|
|
50
|
+
* otherwise inline `delete` + `save`.
|
|
31
51
|
*/
|
|
32
52
|
export function deleteItem<T>(
|
|
33
53
|
ops: CollectionOps<T>,
|
|
@@ -37,6 +57,8 @@ export function deleteItem<T>(
|
|
|
37
57
|
if (!all[id]) return false
|
|
38
58
|
if (ops.deleteFn) {
|
|
39
59
|
ops.deleteFn(id)
|
|
60
|
+
} else if (ops.table) {
|
|
61
|
+
deleteStoredItem(ops.table, id)
|
|
40
62
|
} else {
|
|
41
63
|
delete all[id]
|
|
42
64
|
ops.save(all)
|