@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
|
@@ -12,12 +12,14 @@ import { buildSkillPromptText } from './skill-prompt-budget'
|
|
|
12
12
|
|
|
13
13
|
import { logExecution } from './execution-log'
|
|
14
14
|
import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
|
|
15
|
-
import { canonicalizePluginId, expandPluginIds } from './tool-aliases'
|
|
15
|
+
import { canonicalizePluginId, expandPluginIds, pluginIdMatches } from './tool-aliases'
|
|
16
16
|
import type { Session, Message, UsageRecord, PluginInvocationRecord, MessageToolEvent } from '@/types'
|
|
17
17
|
import { extractSuggestions } from './suggestions'
|
|
18
18
|
import { buildIdentityContinuityContext } from './identity-continuity'
|
|
19
19
|
import { enqueueSystemEvent } from './system-events'
|
|
20
20
|
import { resolveActiveProjectContext } from './project-context'
|
|
21
|
+
import { resolveImagePath } from './resolve-image'
|
|
22
|
+
import { routeTaskIntent } from './capability-router'
|
|
21
23
|
import {
|
|
22
24
|
getEnabledToolPlanningView,
|
|
23
25
|
getFirstToolForCapability,
|
|
@@ -26,7 +28,54 @@ import {
|
|
|
26
28
|
} from './tool-planning'
|
|
27
29
|
import { ToolLoopTracker } from './tool-loop-detection'
|
|
28
30
|
import type { LoopDetectionResult } from './tool-loop-detection'
|
|
29
|
-
import { isCurrentThreadRecallRequest
|
|
31
|
+
import { isCurrentThreadRecallRequest } from './memory-policy'
|
|
32
|
+
import {
|
|
33
|
+
isBroadGoal,
|
|
34
|
+
looksLikeExternalWalletTask,
|
|
35
|
+
looksLikeBoundedExternalExecutionTask,
|
|
36
|
+
looksLikeOpenEndedDeliverableTask,
|
|
37
|
+
shouldForceExternalExecutionFollowthrough,
|
|
38
|
+
shouldForceDeliverableFollowthrough,
|
|
39
|
+
hasStateChangingWalletEvidence,
|
|
40
|
+
countExternalExecutionResearchSteps,
|
|
41
|
+
countDistinctExternalResearchHosts,
|
|
42
|
+
renderToolEvidence,
|
|
43
|
+
resolveFinalStreamResponseText,
|
|
44
|
+
resolveContinuationAssistantText,
|
|
45
|
+
buildContinuationPrompt,
|
|
46
|
+
} from './stream-continuation'
|
|
47
|
+
import type { ContinuationType } from './stream-continuation'
|
|
48
|
+
import { errorMessage, sleep } from '@/lib/shared-utils'
|
|
49
|
+
import { perf } from './perf'
|
|
50
|
+
import {
|
|
51
|
+
compactThreadRecallText,
|
|
52
|
+
getExplicitRequiredToolNames,
|
|
53
|
+
getWalletApprovalBoundaryAction,
|
|
54
|
+
isNarrowDirectMemoryWriteTurn,
|
|
55
|
+
isWalletSimulationResult,
|
|
56
|
+
resolveToolAction,
|
|
57
|
+
shouldAllowToolForDirectMemoryWrite,
|
|
58
|
+
shouldAllowToolForCurrentThreadRecall,
|
|
59
|
+
shouldForceExternalServiceSummary,
|
|
60
|
+
shouldTerminateOnSuccessfulMemoryMutation,
|
|
61
|
+
updateStreamedToolEvents,
|
|
62
|
+
} from './chat-streaming-utils'
|
|
63
|
+
|
|
64
|
+
// Re-export continuation functions so existing consumers don't need to change imports
|
|
65
|
+
export {
|
|
66
|
+
getExplicitRequiredToolNames,
|
|
67
|
+
isNarrowDirectMemoryWriteTurn,
|
|
68
|
+
isWalletSimulationResult,
|
|
69
|
+
looksLikeOpenEndedDeliverableTask,
|
|
70
|
+
shouldAllowToolForDirectMemoryWrite,
|
|
71
|
+
shouldAllowToolForCurrentThreadRecall,
|
|
72
|
+
shouldForceExternalExecutionFollowthrough,
|
|
73
|
+
shouldForceDeliverableFollowthrough,
|
|
74
|
+
shouldForceExternalServiceSummary,
|
|
75
|
+
shouldTerminateOnSuccessfulMemoryMutation,
|
|
76
|
+
resolveFinalStreamResponseText,
|
|
77
|
+
resolveContinuationAssistantText,
|
|
78
|
+
}
|
|
30
79
|
|
|
31
80
|
/** Extract a breadcrumb title from notable tool completions (task/schedule/agent creation). */
|
|
32
81
|
interface StreamAgentChatOpts {
|
|
@@ -66,6 +115,9 @@ export function buildToolDisciplineLines(enabledPlugins: string[]): string[] {
|
|
|
66
115
|
const lines = [
|
|
67
116
|
`Enabled tools in this session: ${uniqueTools.map((toolId) => `\`${toolId}\``).join(', ')}.`,
|
|
68
117
|
'Only call tools from this enabled list or tools explicitly returned by the runtime.',
|
|
118
|
+
'Treat enabled tools as available now. Do not ask the user for permission before routine use of an enabled tool.',
|
|
119
|
+
'If the request clearly maps to an enabled tool, try that tool before telling the user to do it themselves.',
|
|
120
|
+
'Only talk about approvals when a tool result explicitly returns an approval boundary for a concrete state-changing action.',
|
|
69
121
|
]
|
|
70
122
|
|
|
71
123
|
const directPlatformTools = uniqueTools.filter((toolId) => toolId.startsWith('manage_') && toolId !== 'manage_platform')
|
|
@@ -91,6 +143,16 @@ export function buildToolDisciplineLines(enabledPlugins: string[]): string[] {
|
|
|
91
143
|
lines.push(`For current events, live conflicts, or “keep watching for updates” requests, use \`${researchSearchTools[0]}\` before answering. Do not rely on memory or unstated background knowledge for fresh developments.`)
|
|
92
144
|
}
|
|
93
145
|
|
|
146
|
+
const alternateResearchTools = Array.from(new Set([
|
|
147
|
+
...(researchSearchTools.length || researchFetchTools.length ? [...researchSearchTools, ...researchFetchTools] : []),
|
|
148
|
+
...httpTools,
|
|
149
|
+
...(uniqueTools.includes('shell') ? ['shell'] : []),
|
|
150
|
+
...(uniqueTools.includes('browser') ? ['browser'] : []),
|
|
151
|
+
]))
|
|
152
|
+
if (alternateResearchTools.length >= 2) {
|
|
153
|
+
lines.push(`If one enabled research path is blocked by a site-specific error or access challenge, try one other enabled acquisition path (${alternateResearchTools.map((toolName) => `\`${toolName}\``).join(', ')}) before telling the user to fetch the data manually.`)
|
|
154
|
+
}
|
|
155
|
+
|
|
94
156
|
if (browserCaptureTools.length && deliveryMediaTools.length) {
|
|
95
157
|
lines.push(`When the user asks you to send screenshots or other media, capture the artifact first with \`${browserCaptureTools[0]}\`, then deliver that exact file or upload URL through \`${deliveryMediaTools[0]}\` instead of saying the capability is unavailable.`)
|
|
96
158
|
}
|
|
@@ -145,62 +207,6 @@ export function buildToolDisciplineLines(enabledPlugins: string[]): string[] {
|
|
|
145
207
|
return lines
|
|
146
208
|
}
|
|
147
209
|
|
|
148
|
-
export function looksLikeOpenEndedDeliverableTask(text: string): boolean {
|
|
149
|
-
const normalized = text.toLowerCase()
|
|
150
|
-
if (!normalized.trim()) return false
|
|
151
|
-
if (/```|package\.json|tsconfig|\btsx?\b|\bjsx?\b|pytest|vitest|npm run|src\/|components\/|api\//.test(normalized)) return false
|
|
152
|
-
if (/\b(revise|revision|iterate|iteration|draft|deliverable|deliverables|offer|brief|copy|proposal|landing|outreach|plan|strategy|report|memo|document|docs?)\b/.test(normalized)) return true
|
|
153
|
-
// Explicit file-save instructions (e.g. "create X and save it to /tmp/foo.html")
|
|
154
|
-
if (
|
|
155
|
-
/\b(create|build|generate|make|write|produce)\b/.test(normalized)
|
|
156
|
-
&& /\b(save|write|output|export)\b[^.!?\n]{0,60}\b(to|as|in)\b[^.!?\n]{0,40}(\/|~\/|\.\/|\.[a-z]{2,5}\b)/.test(normalized)
|
|
157
|
-
) {
|
|
158
|
-
return true
|
|
159
|
-
}
|
|
160
|
-
if (
|
|
161
|
-
isBroadGoal(text)
|
|
162
|
-
&& /\b(create|build|generate|make|write|research|capture|take|start|produce)\b/.test(normalized)
|
|
163
|
-
&& /\b(screenshot|screenshots|image|images|markdown|\.md\b|md\b|md files?|pdf|pdf files?|html|html\s+(?:page|file)|dashboard|site|sites|website|web page|webpage|dev server|dev servers|artifact|artifacts|topic|topics)\b/.test(normalized)
|
|
164
|
-
) {
|
|
165
|
-
return true
|
|
166
|
-
}
|
|
167
|
-
return isBroadGoal(text) && /(\.md\b|\.txt\b|\.html\b|\.json\b|copy|brief|proposal|plan|report|draft|document|dashboard)/.test(normalized)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Returns tool names that the user explicitly referenced by name in their message.
|
|
172
|
-
*
|
|
173
|
-
* Previously this used regex-based capability matching (matchToolCapabilitiesForMessage)
|
|
174
|
-
* to infer required tools from keywords like "send", "search", "screenshot". This caused
|
|
175
|
-
* false positives ("sends an HTTP request" forced connector_message_tool, "create a file"
|
|
176
|
-
* forced delivery tools) and extra continuation loops.
|
|
177
|
-
*
|
|
178
|
-
* OpenClaw's approach: trust the LLM to select the right tools based on prompt engineering
|
|
179
|
-
* (tool discipline lines, skill adherence header, system prompt). No regex-based forced
|
|
180
|
-
* tool requirements. The deliverable/execution followthrough mechanisms handle cases where
|
|
181
|
-
* the agent stops early.
|
|
182
|
-
*
|
|
183
|
-
* We now only force tools when the user explicitly names them (ask_human, email) — these
|
|
184
|
-
* are cases where the LLM has a known tendency to skip the tool and respond in prose.
|
|
185
|
-
*/
|
|
186
|
-
export function getExplicitRequiredToolNames(userMessage: string, enabledPlugins: string[]): string[] {
|
|
187
|
-
const normalized = userMessage.toLowerCase()
|
|
188
|
-
const required: string[] = []
|
|
189
|
-
|
|
190
|
-
// Only force tools that the user explicitly names and the LLM tends to skip
|
|
191
|
-
if (enabledPlugins.includes('ask_human')
|
|
192
|
-
&& (/\bask_human\b/.test(normalized) || /ask the human/.test(normalized) || /request_input/.test(normalized))) {
|
|
193
|
-
required.push('ask_human')
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (enabledPlugins.includes('email')
|
|
197
|
-
&& (/\bemail\b/.test(normalized) || /send a welcome email/.test(normalized) || /send an email/.test(normalized))) {
|
|
198
|
-
required.push('email')
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return required
|
|
202
|
-
}
|
|
203
|
-
|
|
204
210
|
const OPEN_ENDED_REVISION_BLOCK = [
|
|
205
211
|
'## Revision Loop',
|
|
206
212
|
'For open-ended deliverable work, do a real two-pass loop before declaring success: create the draft artifacts, critique them against the objective, then modify at least one artifact based on that critique.',
|
|
@@ -209,22 +215,23 @@ const OPEN_ENDED_REVISION_BLOCK = [
|
|
|
209
215
|
'If `files` is available, use it with explicit actions and paths to inspect and revise the artifacts.',
|
|
210
216
|
].join('\n')
|
|
211
217
|
|
|
212
|
-
function looksLikeExternalWalletTask(text: string): boolean {
|
|
213
|
-
const normalized = text.toLowerCase()
|
|
214
|
-
if (!normalized.trim()) return false
|
|
215
|
-
return /\b(wallet|wallet connect|walletconnect|trade|trading|exchange|dex|bridge|swap|deposit|withdraw|onchain|token|gas|hyperliquid|arbitrum|ethereum|solana|base|usdc|eth|sol)\b/.test(normalized)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function looksLikeBoundedExternalExecutionTask(text: string): boolean {
|
|
219
|
-
const normalized = text.toLowerCase()
|
|
220
|
-
if (!looksLikeExternalWalletTask(text)) return false
|
|
221
|
-
return /\b(live|swap|trade|buy|purchase|sell|mint|claim|execute|transact|transaction|approve|broadcast)\b/.test(normalized)
|
|
222
|
-
}
|
|
223
|
-
|
|
224
218
|
function getEnabledDisplayTool(enabledPlugins: string[], canonicalPluginId: string): string | null {
|
|
225
219
|
return getEnabledToolPlanningView(enabledPlugins).displayToolIds.find((toolId) => toolId === canonicalPluginId) || null
|
|
226
220
|
}
|
|
227
221
|
|
|
222
|
+
export function shouldForceAttachmentFollowthrough(params: {
|
|
223
|
+
userMessage: string
|
|
224
|
+
enabledPlugins: string[]
|
|
225
|
+
hasToolCalls: boolean
|
|
226
|
+
hasAttachmentContext: boolean
|
|
227
|
+
}): boolean {
|
|
228
|
+
if (!params.hasAttachmentContext) return false
|
|
229
|
+
if (params.hasToolCalls) return false
|
|
230
|
+
const decision = routeTaskIntent(params.userMessage, params.enabledPlugins, null)
|
|
231
|
+
if (decision.intent !== 'research' && decision.intent !== 'browsing') return false
|
|
232
|
+
return decision.preferredTools.some((toolName) => pluginIdMatches(params.enabledPlugins, toolName))
|
|
233
|
+
}
|
|
234
|
+
|
|
228
235
|
export function buildExternalWalletExecutionBlock(enabledPlugins: string[]): string {
|
|
229
236
|
const hasExecutionContext = Boolean(
|
|
230
237
|
getFirstToolForCapability(enabledPlugins, TOOL_CAPABILITY.walletInspect)
|
|
@@ -244,213 +251,6 @@ export function buildExternalWalletExecutionBlock(enabledPlugins: string[]): str
|
|
|
244
251
|
return lines.join('\n')
|
|
245
252
|
}
|
|
246
253
|
|
|
247
|
-
export function shouldForceExternalServiceSummary(params: {
|
|
248
|
-
userMessage: string
|
|
249
|
-
finalResponse: string
|
|
250
|
-
hasToolCalls: boolean
|
|
251
|
-
toolEventCount: number
|
|
252
|
-
}): boolean {
|
|
253
|
-
if (!looksLikeExternalWalletTask(params.userMessage)) return false
|
|
254
|
-
if (!params.hasToolCalls || params.toolEventCount === 0) return false
|
|
255
|
-
const trimmed = params.finalResponse.trim()
|
|
256
|
-
if (!trimmed) return true
|
|
257
|
-
if (/\b(blocker|blocked|cannot|can't|requires|need|missing|last reversible step|next step)\b/i.test(trimmed)) return false
|
|
258
|
-
if (trimmed.length >= 240 && !/(let me|i'll|i will|checking|verify|promising|look into|explore|access their interface)/i.test(trimmed)) return false
|
|
259
|
-
return /:$/.test(trimmed) || /(let me|i'll|i will|checking|verify|promising|look into|explore|access their interface)/i.test(trimmed) || trimmed.length < 240
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function resolveToolAction(input: unknown): string {
|
|
263
|
-
if (input && typeof input === 'object' && !Array.isArray(input)) {
|
|
264
|
-
const action = (input as Record<string, unknown>).action
|
|
265
|
-
return typeof action === 'string' ? action.trim().toLowerCase() : ''
|
|
266
|
-
}
|
|
267
|
-
if (typeof input !== 'string') return ''
|
|
268
|
-
const trimmed = input.trim()
|
|
269
|
-
if (!trimmed.startsWith('{')) return ''
|
|
270
|
-
try {
|
|
271
|
-
const parsed = JSON.parse(trimmed) as Record<string, unknown>
|
|
272
|
-
return typeof parsed.action === 'string' ? parsed.action.trim().toLowerCase() : ''
|
|
273
|
-
} catch {
|
|
274
|
-
return ''
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
export function shouldTerminateOnSuccessfulMemoryMutation(params: {
|
|
279
|
-
toolName: string
|
|
280
|
-
toolInput: unknown
|
|
281
|
-
toolOutput: string
|
|
282
|
-
}): boolean {
|
|
283
|
-
const canonicalToolName = canonicalizePluginId(params.toolName) || params.toolName
|
|
284
|
-
if (canonicalToolName !== 'memory') return false
|
|
285
|
-
const exactToolName = String(params.toolName || '').trim().toLowerCase()
|
|
286
|
-
const action = exactToolName === 'memory_store'
|
|
287
|
-
? 'store'
|
|
288
|
-
: exactToolName === 'memory_update'
|
|
289
|
-
? 'update'
|
|
290
|
-
: resolveToolAction(params.toolInput)
|
|
291
|
-
if (action !== 'store' && action !== 'update') return false
|
|
292
|
-
const output = extractSuggestions(params.toolOutput || '').clean.trim()
|
|
293
|
-
if (!output || /^error[:\s]/i.test(output)) return false
|
|
294
|
-
if (!/^(stored|updated) memory\b/i.test(output)) return false
|
|
295
|
-
return /no further memory lookup is needed unless the user asked you to verify/i.test(output)
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function hasStateChangingWalletEvidence(toolEvents: MessageToolEvent[]): boolean {
|
|
299
|
-
return toolEvents.some((event) => {
|
|
300
|
-
const input = `${event.input || ''}\n${event.output || ''}`
|
|
301
|
-
return event.name === 'wallet_tool' && (
|
|
302
|
-
/"action":"send_transaction"/.test(input)
|
|
303
|
-
|| /"action":"send"/.test(input)
|
|
304
|
-
|| /"action":"sign_transaction"/.test(input)
|
|
305
|
-
|| /"type":"plugin_wallet_action_request"/.test(input)
|
|
306
|
-
|| /"type":"plugin_wallet_transfer_request"/.test(input)
|
|
307
|
-
|| /"status":"broadcast"/.test(input)
|
|
308
|
-
)
|
|
309
|
-
})
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function countExternalExecutionResearchSteps(toolEvents: MessageToolEvent[]): number {
|
|
313
|
-
return toolEvents.filter((event) => {
|
|
314
|
-
if (['http_request', 'web', 'web_search', 'web_fetch', 'browser'].includes(event.name)) return true
|
|
315
|
-
if (event.name !== 'wallet_tool') return false
|
|
316
|
-
return /"action":"(balance|address|transactions|call_contract|encode_contract_call)"/.test(event.input || '')
|
|
317
|
-
}).length
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function countDistinctExternalResearchHosts(toolEvents: MessageToolEvent[]): number {
|
|
321
|
-
const hosts = new Set<string>()
|
|
322
|
-
for (const event of toolEvents) {
|
|
323
|
-
const candidates = [event.input || '', event.output || '']
|
|
324
|
-
for (const candidate of candidates) {
|
|
325
|
-
const matches = candidate.match(/https?:\/\/[^"'\\\s)]+/g) || []
|
|
326
|
-
for (const match of matches) {
|
|
327
|
-
try {
|
|
328
|
-
hosts.add(new URL(match).host.toLowerCase())
|
|
329
|
-
} catch {
|
|
330
|
-
// Ignore malformed URLs in model/tool text.
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
return hosts.size
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function getWalletApprovalBoundaryAction(output: string): string | null {
|
|
339
|
-
if (!output.includes('plugin_wallet_')) return null
|
|
340
|
-
if (/"type":"plugin_wallet_transfer_request"/.test(output)) return 'send'
|
|
341
|
-
const actionMatch = output.match(/"action":"([^"]+)"/)
|
|
342
|
-
const action = actionMatch?.[1] || ''
|
|
343
|
-
if (!action) return null
|
|
344
|
-
const readOnlyActions = new Set([
|
|
345
|
-
'balance',
|
|
346
|
-
'address',
|
|
347
|
-
'transactions',
|
|
348
|
-
'encode_contract_call',
|
|
349
|
-
'simulate_transaction',
|
|
350
|
-
])
|
|
351
|
-
return readOnlyActions.has(action) ? null : action
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
export function isWalletSimulationResult(toolName: string, output: string): boolean {
|
|
355
|
-
return toolName === 'wallet_tool' && /"status":"simulated"/.test(output)
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
export function shouldForceExternalExecutionFollowthrough(params: {
|
|
359
|
-
userMessage: string
|
|
360
|
-
finalResponse: string
|
|
361
|
-
hasToolCalls: boolean
|
|
362
|
-
toolEvents: MessageToolEvent[]
|
|
363
|
-
}): boolean {
|
|
364
|
-
if (!looksLikeBoundedExternalExecutionTask(params.userMessage)) return false
|
|
365
|
-
if (!params.hasToolCalls || params.toolEvents.length < 4) return false
|
|
366
|
-
if (hasStateChangingWalletEvidence(params.toolEvents)) return false
|
|
367
|
-
const distinctHosts = countDistinctExternalResearchHosts(params.toolEvents)
|
|
368
|
-
const trimmed = params.finalResponse.trim()
|
|
369
|
-
if (!trimmed) return countExternalExecutionResearchSteps(params.toolEvents) >= 4 || distinctHosts >= 3
|
|
370
|
-
if (/\b(last reversible step|exact blocker|safest next action|blocked|cannot|can't|missing capability|no-key route unavailable)\b/i.test(trimmed)) {
|
|
371
|
-
return false
|
|
372
|
-
}
|
|
373
|
-
if (countExternalExecutionResearchSteps(params.toolEvents) < 4 && distinctHosts < 3) return false
|
|
374
|
-
return /(let me|i'll|i will|trying|research|query|check|look|promising|now let me|good -|good,)/i.test(trimmed) || trimmed.length < 500
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function looksLikeIncompleteDeliverableResponse(text: string): boolean {
|
|
378
|
-
const trimmed = text.trim()
|
|
379
|
-
if (!trimmed) return true
|
|
380
|
-
if (trimmed.endsWith(':') || trimmed.endsWith('...') || trimmed.endsWith('…')) return true
|
|
381
|
-
const lastChunk = trimmed.slice(-400).toLowerCase()
|
|
382
|
-
return /\b(?:next|now|then|after that|moving on to|proceeding to)\b[^.!?\n]{0,120}\b(?:i(?:'ll| will)|create|build|write|capture|take|start|finish|generate)\b/.test(lastChunk)
|
|
383
|
-
|| /\b(?:i(?:'ll| will)|let me)\s+(?:now|next)?\s*(?:create|build|write|capture|take|start|finish|generate|continue)\b/.test(lastChunk)
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
export function shouldForceDeliverableFollowthrough(params: {
|
|
387
|
-
userMessage: string
|
|
388
|
-
finalResponse: string
|
|
389
|
-
hasToolCalls: boolean
|
|
390
|
-
toolEvents: MessageToolEvent[]
|
|
391
|
-
}): boolean {
|
|
392
|
-
if (!looksLikeOpenEndedDeliverableTask(params.userMessage)) return false
|
|
393
|
-
if (!params.hasToolCalls || params.toolEvents.length === 0) return false
|
|
394
|
-
const trimmed = params.finalResponse.trim()
|
|
395
|
-
if (!trimmed) return params.toolEvents.length >= 2
|
|
396
|
-
if (
|
|
397
|
-
/\b(task complete|completed|finished|done|delivered|shared|sent|uploaded|attached)\b/i.test(trimmed)
|
|
398
|
-
&& /(?:\/api\/uploads\/|https?:\/\/|`[^`\n]+\.(?:md|pdf|png|jpe?g|webp|gif|html|txt|zip)`)/i.test(trimmed)
|
|
399
|
-
) {
|
|
400
|
-
return false
|
|
401
|
-
}
|
|
402
|
-
// If the user asked for file output but no file-write tool was used, force continuation
|
|
403
|
-
const userNormalized = params.userMessage.toLowerCase()
|
|
404
|
-
if (/\b(save|write|output)\b[^.!?\n]{0,60}\b(to|as)\b[^.!?\n]{0,40}(\/|~\/|\.[a-z]{2,5}\b)/.test(userNormalized)) {
|
|
405
|
-
// Check if a file-writing tool was actually used (not just file-reading).
|
|
406
|
-
// The `files` tool with action: 'read' or 'list' doesn't count as writing.
|
|
407
|
-
const usedFileWriteTools = params.toolEvents.some((e) => {
|
|
408
|
-
if (!e.name) return false
|
|
409
|
-
if (['write_file', 'edit_file'].includes(e.name)) return true
|
|
410
|
-
if (e.name === 'shell' || e.name === 'execute_command') return true
|
|
411
|
-
if (e.name === 'files') {
|
|
412
|
-
// Only count as a write if the tool input specifies action: "write"
|
|
413
|
-
const input = e.input || ''
|
|
414
|
-
return /"action"\s*:\s*"write"/i.test(input)
|
|
415
|
-
}
|
|
416
|
-
return false
|
|
417
|
-
})
|
|
418
|
-
if (!usedFileWriteTools) return true
|
|
419
|
-
}
|
|
420
|
-
if (looksLikeIncompleteDeliverableResponse(trimmed)) return true
|
|
421
|
-
return trimmed.length < 120 && params.toolEvents.length >= 3
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function updateStreamedToolEvents(events: MessageToolEvent[], event: { type: 'call' | 'result'; name: string; input?: string; output?: string; toolCallId?: string }) {
|
|
425
|
-
if (event.type === 'call') {
|
|
426
|
-
events.push({
|
|
427
|
-
name: event.name,
|
|
428
|
-
input: event.input || '',
|
|
429
|
-
toolCallId: event.toolCallId,
|
|
430
|
-
})
|
|
431
|
-
return
|
|
432
|
-
}
|
|
433
|
-
const index = event.toolCallId
|
|
434
|
-
? events.findLastIndex((entry) => entry.toolCallId === event.toolCallId && !entry.output)
|
|
435
|
-
: events.findLastIndex((entry) => entry.name === event.name && !entry.output)
|
|
436
|
-
if (index === -1) return
|
|
437
|
-
events[index] = {
|
|
438
|
-
...events[index],
|
|
439
|
-
output: event.output || '',
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function renderToolEvidence(events: MessageToolEvent[]): string {
|
|
444
|
-
return events
|
|
445
|
-
.slice(-10)
|
|
446
|
-
.map((event, index) => [
|
|
447
|
-
`Tool ${index + 1}: ${event.name}`,
|
|
448
|
-
event.input ? `Input: ${event.input}` : '',
|
|
449
|
-
event.output ? `Output: ${event.output.slice(0, 1200)}` : '',
|
|
450
|
-
].filter(Boolean).join('\n'))
|
|
451
|
-
.join('\n\n')
|
|
452
|
-
}
|
|
453
|
-
|
|
454
254
|
async function buildForcedExternalServiceSummary(params: {
|
|
455
255
|
llm: { invoke: (messages: HumanMessage[]) => Promise<{ content: unknown }> }
|
|
456
256
|
userMessage: string
|
|
@@ -491,73 +291,6 @@ async function buildForcedExternalServiceSummary(params: {
|
|
|
491
291
|
}
|
|
492
292
|
}
|
|
493
293
|
|
|
494
|
-
function buildExternalExecutionFollowthroughPrompt(params: {
|
|
495
|
-
userMessage: string
|
|
496
|
-
fullText: string
|
|
497
|
-
toolEvents: MessageToolEvent[]
|
|
498
|
-
}): string {
|
|
499
|
-
return [
|
|
500
|
-
'You are in a bounded external execution task and have already done enough research.',
|
|
501
|
-
'Do not restart broad discovery. Do not ask the user for another prompt.',
|
|
502
|
-
'Do not spend this continuation on more venue shopping. Use the already confirmed route unless one last fetch is strictly required to prepare execution.',
|
|
503
|
-
'If several venue or aggregator APIs already failed, stop searching for more venues. Either use a direct onchain read path with the available wallet tools, or state the blocker.',
|
|
504
|
-
'A prose approval request does not count as completion. If the next step is a sign/send/approve action, call the real wallet tool action so the runtime can create the approval request.',
|
|
505
|
-
'Do not mutate already confirmed token addresses, router addresses, spender addresses, or network identifiers unless newer tool evidence proves the earlier value was wrong.',
|
|
506
|
-
'Within this continuation, do exactly one of the following:',
|
|
507
|
-
'1. Take the next concrete execution step now using the existing tools and stop at the first approval boundary for a state-changing action.',
|
|
508
|
-
'2. If no safe executable step exists with the current tools, state the exact blocker with evidence.',
|
|
509
|
-
'A successful continuation ends with one of these outcomes only: an approval request, a broadcast transaction, or a final blocker summary.',
|
|
510
|
-
'Prefer the route sources and facts already confirmed in the tool evidence below. Do not keep shopping for new venues unless the current options are clearly unusable.',
|
|
511
|
-
'If the tool evidence already includes enough information to prepare a contract call, approval, quote read, or transaction simulation, do that now instead of making another search or HTTP request.',
|
|
512
|
-
'',
|
|
513
|
-
`Objective:\n${params.userMessage}`,
|
|
514
|
-
'',
|
|
515
|
-
`Current partial response:\n${params.fullText || '(none)'}`,
|
|
516
|
-
'',
|
|
517
|
-
`Recent tool evidence:\n${renderToolEvidence(params.toolEvents) || '(none)'}`,
|
|
518
|
-
].join('\n')
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
function buildDeliverableFollowthroughPrompt(params: {
|
|
522
|
-
userMessage: string
|
|
523
|
-
fullText: string
|
|
524
|
-
toolEvents: MessageToolEvent[]
|
|
525
|
-
}): string {
|
|
526
|
-
const lines = [
|
|
527
|
-
'You are in the middle of a multi-step deliverable and stopped after only a partial batch of work.',
|
|
528
|
-
'Continue from the existing workspace and artifacts. Do not restart from scratch and do not ask the user to restate the request.',
|
|
529
|
-
'Do not stop after one partial batch. Finish every requested deliverable that is still outstanding before concluding.',
|
|
530
|
-
'If a requested artifact cannot be produced, say exactly which artifact is missing, what blocked it, and what you already completed.',
|
|
531
|
-
'Use the existing files, screenshots, and generated outputs first. Inspect them if needed, then complete the remaining work.',
|
|
532
|
-
'Preserve hard structural constraints from the original request: exact counts stay exact, required titled sections stay present, and source coverage gaps should be filled instead of skipped.',
|
|
533
|
-
'End with a concise grouped completion summary that lists exact file paths, upload URLs, localhost URLs/ports, and screenshots you produced.',
|
|
534
|
-
]
|
|
535
|
-
|
|
536
|
-
// If the user explicitly asked for file output, remind the model to use file tools
|
|
537
|
-
const userNormalized = params.userMessage.toLowerCase()
|
|
538
|
-
const fileOutputMatch = userNormalized.match(/\b(?:save|write|output|export)\b[^.!?\n]{0,80}\b(?:to|as|at|in)\b[^.!?\n]{0,60}(\/[^\s,'"]+|~\/[^\s,'"]+|\.\/[^\s,'"]+)/i)
|
|
539
|
-
if (fileOutputMatch) {
|
|
540
|
-
const fileToolNames = ['write_file', 'edit_file', 'files', 'shell', 'execute_command']
|
|
541
|
-
const usedFileTools = params.toolEvents.some((e) => e.name && fileToolNames.includes(e.name))
|
|
542
|
-
if (!usedFileTools) {
|
|
543
|
-
lines.push(
|
|
544
|
-
'',
|
|
545
|
-
`CRITICAL: The user asked you to save output to a file path (${fileOutputMatch[1] || 'see objective'}). You have NOT used any file-writing tool yet.`,
|
|
546
|
-
'You MUST use the `files` or `write_file` tool to write the content to the requested path. Do not just include the content in your text response — actually write the file.',
|
|
547
|
-
)
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
lines.push(
|
|
552
|
-
'',
|
|
553
|
-
`Objective:\n${params.userMessage}`,
|
|
554
|
-
'',
|
|
555
|
-
`Current partial response:\n${params.fullText || '(none)'}`,
|
|
556
|
-
'',
|
|
557
|
-
`Recent tool evidence:\n${renderToolEvidence(params.toolEvents) || '(none)'}`,
|
|
558
|
-
)
|
|
559
|
-
return lines.join('\n')
|
|
560
|
-
}
|
|
561
294
|
|
|
562
295
|
function buildExactStructureBlock(userMessage: string): string {
|
|
563
296
|
const exactBulletMatch = userMessage.match(/\bexactly\s+(\d+)\s+bullet points?\b/i)
|
|
@@ -571,18 +304,6 @@ function buildExactStructureBlock(userMessage: string): string {
|
|
|
571
304
|
].join('\n')
|
|
572
305
|
}
|
|
573
306
|
|
|
574
|
-
/** Detect whether a user message is a broad, high-level goal that benefits from decomposition. */
|
|
575
|
-
function isBroadGoal(text: string): boolean {
|
|
576
|
-
if (text.length < 50) return false
|
|
577
|
-
// Messages with code fences, file paths, or numbered steps are already structured
|
|
578
|
-
if (/```/.test(text)) return false
|
|
579
|
-
if (/\/(src|lib|app|pages|components|api)\//.test(text)) return false
|
|
580
|
-
if (/^\s*\d+[.)]\s/m.test(text)) return false
|
|
581
|
-
// Short direct questions aren't broad goals
|
|
582
|
-
if (text.length < 80 && text.endsWith('?')) return false
|
|
583
|
-
return true
|
|
584
|
-
}
|
|
585
|
-
|
|
586
307
|
const GOAL_DECOMPOSITION_BLOCK = [
|
|
587
308
|
'## Goal Decomposition',
|
|
588
309
|
'When you receive a broad, open-ended goal:',
|
|
@@ -602,6 +323,7 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
602
323
|
platformAssignScope?: 'self' | 'all'
|
|
603
324
|
userMessage?: string
|
|
604
325
|
history?: Message[]
|
|
326
|
+
hasAttachmentContext?: boolean
|
|
605
327
|
responseStyle?: 'concise' | 'normal' | 'detailed' | null
|
|
606
328
|
responseMaxChars?: number | null
|
|
607
329
|
}) {
|
|
@@ -642,6 +364,16 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
642
364
|
parts.push(buildDirectMemoryWriteBlock())
|
|
643
365
|
}
|
|
644
366
|
|
|
367
|
+
if (opts.hasAttachmentContext) {
|
|
368
|
+
parts.push(
|
|
369
|
+
'## Attachments',
|
|
370
|
+
'User attachments in this thread are part of the available context. Image attachments are directly visible to you, and readable files/PDFs are inlined when available.',
|
|
371
|
+
'If the user asks you to identify, read, transcribe, compare, or look something up from an attachment, inspect the attachment content first instead of claiming the image/file is unavailable.',
|
|
372
|
+
'When the task depends on details from an attachment plus outside lookup, extract the identifier from the attachment and then use the enabled tools to continue.',
|
|
373
|
+
'Do not claim you cannot use images, attachments, or external tools when those capabilities are available in this session. Only report a blocker after a real attempt or when the attachment is genuinely unreadable.',
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
|
|
645
377
|
// Plugin-specific operating guidance (collected dynamically from plugins)
|
|
646
378
|
const guidanceLines = getPluginManager().collectOperatingGuidance(opts.enabledPlugins)
|
|
647
379
|
if (guidanceLines.length) parts.push(...guidanceLines)
|
|
@@ -683,12 +415,6 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
683
415
|
return parts.filter(Boolean).join('\n')
|
|
684
416
|
}
|
|
685
417
|
|
|
686
|
-
function compactThreadRecallText(text: string, maxChars = 180): string {
|
|
687
|
-
const compact = extractSuggestions(text || '').clean.replace(/\s+/g, ' ').trim()
|
|
688
|
-
if (!compact) return ''
|
|
689
|
-
return compact.length > maxChars ? `${compact.slice(0, maxChars - 3)}...` : compact
|
|
690
|
-
}
|
|
691
|
-
|
|
692
418
|
function buildCurrentThreadRecallBlock(history: Message[]): string {
|
|
693
419
|
const recentUserFacts = history
|
|
694
420
|
.filter((entry) => entry.role === 'user' && typeof entry.text === 'string' && entry.text.trim())
|
|
@@ -735,41 +461,6 @@ function buildDirectMemoryWriteBlock(): string {
|
|
|
735
461
|
].join('\n')
|
|
736
462
|
}
|
|
737
463
|
|
|
738
|
-
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
|
|
739
|
-
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
|
|
740
|
-
|
|
741
|
-
export function isNarrowDirectMemoryWriteTurn(message: string): boolean {
|
|
742
|
-
const trimmed = String(message || '').trim()
|
|
743
|
-
if (!trimmed || !isDirectMemoryWriteRequest(trimmed)) return false
|
|
744
|
-
if (looksLikeOpenEndedDeliverableTask(trimmed)) return false
|
|
745
|
-
if (DIRECT_MEMORY_WRITE_EXTRA_ACTION_RE.test(trimmed) && !DIRECT_MEMORY_WRITE_CONFIRMATION_ONLY_RE.test(trimmed)) {
|
|
746
|
-
return false
|
|
747
|
-
}
|
|
748
|
-
return !isBroadGoal(trimmed) || DIRECT_MEMORY_WRITE_CONFIRMATION_ONLY_RE.test(trimmed) || !/[?]$/.test(trimmed)
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
const CURRENT_THREAD_RECALL_BLOCKED_TOOL_IDS = new Set([
|
|
752
|
-
'memory',
|
|
753
|
-
'manage_sessions',
|
|
754
|
-
'web',
|
|
755
|
-
'context_mgmt',
|
|
756
|
-
])
|
|
757
|
-
|
|
758
|
-
export function shouldAllowToolForCurrentThreadRecall(toolName: string): boolean {
|
|
759
|
-
const canonicalToolName = canonicalizePluginId(toolName) || toolName.trim().toLowerCase()
|
|
760
|
-
return !CURRENT_THREAD_RECALL_BLOCKED_TOOL_IDS.has(canonicalToolName)
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
const DIRECT_MEMORY_WRITE_ALLOWED_TOOL_IDS = new Set([
|
|
764
|
-
'memory_store',
|
|
765
|
-
'memory_update',
|
|
766
|
-
])
|
|
767
|
-
|
|
768
|
-
export function shouldAllowToolForDirectMemoryWrite(toolName: string): boolean {
|
|
769
|
-
const rawToolName = toolName.trim().toLowerCase()
|
|
770
|
-
return DIRECT_MEMORY_WRITE_ALLOWED_TOOL_IDS.has(rawToolName)
|
|
771
|
-
}
|
|
772
|
-
|
|
773
464
|
export interface StreamAgentChatResult {
|
|
774
465
|
/** All text accumulated across every LLM turn (for SSE / web UI history). */
|
|
775
466
|
fullText: string
|
|
@@ -778,53 +469,20 @@ export interface StreamAgentChatResult {
|
|
|
778
469
|
finalResponse: string
|
|
779
470
|
}
|
|
780
471
|
|
|
781
|
-
|
|
782
|
-
const events = Array.isArray(toolEvents) ? toolEvents : []
|
|
783
|
-
for (let index = events.length - 1; index >= 0; index--) {
|
|
784
|
-
const event = events[index]
|
|
785
|
-
const output = typeof event?.output === 'string'
|
|
786
|
-
? extractSuggestions(event.output).clean.trim()
|
|
787
|
-
: ''
|
|
788
|
-
if (!output) continue
|
|
789
|
-
if (/^error[:\s]/i.test(output)) continue
|
|
790
|
-
if (output.startsWith('{') || output.startsWith('[')) continue
|
|
791
|
-
return output
|
|
792
|
-
}
|
|
793
|
-
return ''
|
|
794
|
-
}
|
|
472
|
+
type StreamAgentChatHandler = (opts: StreamAgentChatOpts) => Promise<StreamAgentChatResult>
|
|
795
473
|
|
|
796
|
-
|
|
797
|
-
fullText: string
|
|
798
|
-
lastSegment: string
|
|
799
|
-
lastSettledSegment: string
|
|
800
|
-
hasToolCalls: boolean
|
|
801
|
-
toolEvents?: MessageToolEvent[]
|
|
802
|
-
}): string {
|
|
803
|
-
const fullText = params.fullText || ''
|
|
804
|
-
if (!params.hasToolCalls) return fullText
|
|
805
|
-
|
|
806
|
-
const candidates = [
|
|
807
|
-
extractSuggestions(params.lastSegment || '').clean.trim(),
|
|
808
|
-
extractSuggestions(params.lastSettledSegment || '').clean.trim(),
|
|
809
|
-
extractSuggestions(fullText).clean.trim(),
|
|
810
|
-
resolveToolOnlyFinalResponse(params.toolEvents),
|
|
811
|
-
]
|
|
474
|
+
let streamAgentChatOverride: StreamAgentChatHandler | null = null
|
|
812
475
|
|
|
813
|
-
|
|
476
|
+
export function setStreamAgentChatForTest(handler: StreamAgentChatHandler | null): void {
|
|
477
|
+
streamAgentChatOverride = handler
|
|
814
478
|
}
|
|
815
479
|
|
|
816
|
-
export function
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
}): string {
|
|
820
|
-
const candidates = [
|
|
821
|
-
extractSuggestions(params.iterationText || '').clean.trim(),
|
|
822
|
-
extractSuggestions(params.lastSegment || '').clean.trim(),
|
|
823
|
-
]
|
|
824
|
-
return candidates.find((candidate) => candidate.length > 0) || ''
|
|
480
|
+
export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<StreamAgentChatResult> {
|
|
481
|
+
if (streamAgentChatOverride) return streamAgentChatOverride(opts)
|
|
482
|
+
return streamAgentChatCore(opts)
|
|
825
483
|
}
|
|
826
484
|
|
|
827
|
-
|
|
485
|
+
async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAgentChatResult> {
|
|
828
486
|
const startTs = Date.now()
|
|
829
487
|
const { session, message, imagePath, attachedFiles, apiKey, systemPrompt, write, history, fallbackCredentialIds, signal } = opts
|
|
830
488
|
const rawPlugins = Array.isArray(session.plugins) ? session.plugins : []
|
|
@@ -875,6 +533,11 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
875
533
|
const hasProvidedSystemPrompt = typeof systemPrompt === 'string' && systemPrompt.trim().length > 0
|
|
876
534
|
const directMemoryWriteOnlyTurn = isNarrowDirectMemoryWriteTurn(message)
|
|
877
535
|
const currentThreadRecallRequest = !directMemoryWriteOnlyTurn && isCurrentThreadRecallRequest(message)
|
|
536
|
+
const hasAttachmentContext = Boolean(
|
|
537
|
+
imagePath
|
|
538
|
+
|| attachedFiles?.length
|
|
539
|
+
|| history.some((entry) => entry.imagePath || entry.imageUrl || (Array.isArray(entry.attachedFiles) && entry.attachedFiles.length > 0)),
|
|
540
|
+
)
|
|
878
541
|
|
|
879
542
|
if (hasProvidedSystemPrompt) {
|
|
880
543
|
stateModifierParts.push(systemPrompt!.trim())
|
|
@@ -1069,6 +732,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
1069
732
|
platformAssignScope: agentPlatformAssignScope,
|
|
1070
733
|
userMessage: message,
|
|
1071
734
|
history,
|
|
735
|
+
hasAttachmentContext,
|
|
1072
736
|
responseStyle: agentResponseStyle,
|
|
1073
737
|
responseMaxChars: agentResponseMaxChars,
|
|
1074
738
|
}),
|
|
@@ -1076,6 +740,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
1076
740
|
|
|
1077
741
|
let stateModifier = stateModifierParts.join('\n\n')
|
|
1078
742
|
|
|
743
|
+
const endToolBuildPerf = perf.start('stream-agent-chat', 'buildSessionTools', { sessionId: session.id })
|
|
1079
744
|
const { tools, cleanup, toolToPluginMap } = await buildSessionTools(session.cwd, sessionPlugins, {
|
|
1080
745
|
agentId: session.agentId,
|
|
1081
746
|
sessionId: session.id,
|
|
@@ -1088,6 +753,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
1088
753
|
projectDescription: activeProjectContext.project?.description || null,
|
|
1089
754
|
memoryScopeMode: agentMemoryScopeMode,
|
|
1090
755
|
})
|
|
756
|
+
endToolBuildPerf({ toolCount: tools.length })
|
|
1091
757
|
const toolsForTurn = currentThreadRecallRequest
|
|
1092
758
|
? tools.filter((tool) => {
|
|
1093
759
|
const toolName = typeof (tool as { name?: unknown }).name === 'string'
|
|
@@ -1253,7 +919,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
1253
919
|
const langchainMessages: Array<HumanMessage | AIMessage> = []
|
|
1254
920
|
for (const m of effectiveHistory) {
|
|
1255
921
|
if (m.role === 'user') {
|
|
1256
|
-
|
|
922
|
+
const resolvedImg = resolveImagePath(m.imagePath, m.imageUrl)
|
|
923
|
+
langchainMessages.push(new HumanMessage({ content: await buildLangChainContent(m.text, resolvedImg ?? undefined, m.attachedFiles) }))
|
|
1257
924
|
} else {
|
|
1258
925
|
langchainMessages.push(new AIMessage({ content: m.text }))
|
|
1259
926
|
}
|
|
@@ -1298,12 +965,14 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
1298
965
|
const MAX_TRANSIENT_RETRIES = 2
|
|
1299
966
|
const MAX_REQUIRED_TOOL_CONTINUES = 2
|
|
1300
967
|
const MAX_EXECUTION_FOLLOWTHROUGHS = 1
|
|
968
|
+
const MAX_ATTACHMENT_FOLLOWTHROUGHS = 1
|
|
1301
969
|
const MAX_DELIVERABLE_FOLLOWTHROUGHS = 2
|
|
1302
970
|
const MAX_TOOL_SUMMARY_RETRIES = 2
|
|
1303
971
|
let autoContinueCount = 0
|
|
1304
972
|
let transientRetryCount = 0
|
|
1305
973
|
let requiredToolContinueCount = 0
|
|
1306
974
|
let executionFollowthroughCount = 0
|
|
975
|
+
let attachmentFollowthroughCount = 0
|
|
1307
976
|
let deliverableFollowthroughCount = 0
|
|
1308
977
|
let toolSummaryRetryCount = 0
|
|
1309
978
|
const explicitRequiredToolNames = getExplicitRequiredToolNames(message, sessionPlugins)
|
|
@@ -1315,7 +984,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
1315
984
|
try {
|
|
1316
985
|
const maxIterations = MAX_AUTO_CONTINUES + MAX_TRANSIENT_RETRIES + MAX_REQUIRED_TOOL_CONTINUES + MAX_EXECUTION_FOLLOWTHROUGHS + MAX_DELIVERABLE_FOLLOWTHROUGHS + MAX_TOOL_SUMMARY_RETRIES
|
|
1317
986
|
for (let iteration = 0; iteration <= maxIterations; iteration++) {
|
|
1318
|
-
let shouldContinue:
|
|
987
|
+
let shouldContinue: ContinuationType = false
|
|
1319
988
|
let requiredToolReminderNames: string[] = []
|
|
1320
989
|
let waitingForToolResult = false
|
|
1321
990
|
let idleTimedOut = false
|
|
@@ -1370,6 +1039,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
1370
1039
|
// to suppress the duplicate nested events.
|
|
1371
1040
|
const acceptedToolRunIds = new Set<string>()
|
|
1372
1041
|
const seenToolInputKeys = new Set<string>()
|
|
1042
|
+
const toolPerfEnds = new Map<string, (extra?: Record<string, unknown>) => number>()
|
|
1373
1043
|
|
|
1374
1044
|
try {
|
|
1375
1045
|
armIdleWatchdog()
|
|
@@ -1453,6 +1123,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
1453
1123
|
}
|
|
1454
1124
|
seenToolInputKeys.add(toolDedupeKey)
|
|
1455
1125
|
acceptedToolRunIds.add(event.run_id)
|
|
1126
|
+
toolPerfEnds.set(event.run_id, perf.start('tool-call', toolName, { sessionId: session.id }))
|
|
1456
1127
|
|
|
1457
1128
|
clearIdleWatchdog()
|
|
1458
1129
|
waitingForToolResult = true
|
|
@@ -1490,6 +1161,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
1490
1161
|
// Dedup: skip on_tool_end for run_ids we didn't accept in on_tool_start
|
|
1491
1162
|
if (!acceptedToolRunIds.has(event.run_id)) continue
|
|
1492
1163
|
acceptedToolRunIds.delete(event.run_id)
|
|
1164
|
+
const endToolPerf = toolPerfEnds.get(event.run_id)
|
|
1165
|
+
toolPerfEnds.delete(event.run_id)
|
|
1493
1166
|
|
|
1494
1167
|
waitingForToolResult = false
|
|
1495
1168
|
armIdleWatchdog()
|
|
@@ -1550,6 +1223,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
1550
1223
|
}
|
|
1551
1224
|
}
|
|
1552
1225
|
|
|
1226
|
+
endToolPerf?.({ outputLen: outputStr?.length || 0 })
|
|
1553
1227
|
write(`data: ${JSON.stringify({
|
|
1554
1228
|
t: 'tool_result',
|
|
1555
1229
|
toolName,
|
|
@@ -1622,7 +1296,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
1622
1296
|
const errName = innerErr instanceof Error ? innerErr.constructor.name : ''
|
|
1623
1297
|
const errMsg = idleTimedOut
|
|
1624
1298
|
? 'Model stream stalled without emitting text or tool results for 90 seconds.'
|
|
1625
|
-
:
|
|
1299
|
+
: errorMessage(innerErr)
|
|
1626
1300
|
const errStack = innerErr instanceof Error ? innerErr.stack?.slice(0, 500) : undefined
|
|
1627
1301
|
|
|
1628
1302
|
// Classify the error:
|
|
@@ -1735,6 +1409,25 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
1735
1409
|
}
|
|
1736
1410
|
}
|
|
1737
1411
|
|
|
1412
|
+
if (!shouldContinue
|
|
1413
|
+
&& attachmentFollowthroughCount < MAX_ATTACHMENT_FOLLOWTHROUGHS
|
|
1414
|
+
&& shouldForceAttachmentFollowthrough({
|
|
1415
|
+
userMessage: message,
|
|
1416
|
+
enabledPlugins: sessionPlugins,
|
|
1417
|
+
hasToolCalls,
|
|
1418
|
+
hasAttachmentContext,
|
|
1419
|
+
})) {
|
|
1420
|
+
shouldContinue = 'attachment_followthrough'
|
|
1421
|
+
attachmentFollowthroughCount++
|
|
1422
|
+
write(`data: ${JSON.stringify({
|
|
1423
|
+
t: 'status',
|
|
1424
|
+
text: JSON.stringify({
|
|
1425
|
+
attachmentFollowthrough: attachmentFollowthroughCount,
|
|
1426
|
+
maxFollowthroughs: MAX_ATTACHMENT_FOLLOWTHROUGHS,
|
|
1427
|
+
}),
|
|
1428
|
+
})}\n\n`)
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1738
1431
|
if (!shouldContinue
|
|
1739
1432
|
&& executionFollowthroughCount < MAX_EXECUTION_FOLLOWTHROUGHS
|
|
1740
1433
|
&& shouldForceExternalExecutionFollowthrough({
|
|
@@ -1817,88 +1510,31 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
1817
1510
|
lastSegment,
|
|
1818
1511
|
})
|
|
1819
1512
|
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
langchainMessages.push(new HumanMessage({ content: 'Continue where you left off. Complete the remaining steps of the objective.' }))
|
|
1830
|
-
lastSegment = ''
|
|
1831
|
-
} else if (shouldContinue === 'required_tool') {
|
|
1832
|
-
if (continuationAssistantText) {
|
|
1833
|
-
langchainMessages.push(new AIMessage({ content: continuationAssistantText }))
|
|
1834
|
-
}
|
|
1835
|
-
const settledSegment = extractSuggestions(lastSegment).clean.trim()
|
|
1836
|
-
if (settledSegment) lastSettledSegment = settledSegment
|
|
1837
|
-
langchainMessages.push(new HumanMessage({
|
|
1838
|
-
content: `You have not yet completed the required explicit tool step(s): ${requiredToolReminderNames.join(', ')}. Use those enabled tools now before declaring success. Do not replace ask_human with a plain-text request, do not replace outbound delivery tools with prose, and do not replace screenshot requests with text-only summaries.`,
|
|
1839
|
-
}))
|
|
1840
|
-
lastSegment = ''
|
|
1841
|
-
} else if (shouldContinue === 'execution_followthrough') {
|
|
1842
|
-
if (continuationAssistantText) {
|
|
1843
|
-
langchainMessages.push(new AIMessage({ content: continuationAssistantText }))
|
|
1844
|
-
}
|
|
1845
|
-
const settledSegment = extractSuggestions(lastSegment).clean.trim()
|
|
1846
|
-
if (settledSegment) lastSettledSegment = settledSegment
|
|
1847
|
-
langchainMessages.push(new HumanMessage({
|
|
1848
|
-
content: buildExternalExecutionFollowthroughPrompt({
|
|
1849
|
-
userMessage: message,
|
|
1850
|
-
fullText,
|
|
1851
|
-
toolEvents: streamedToolEvents,
|
|
1852
|
-
}),
|
|
1853
|
-
}))
|
|
1854
|
-
lastSegment = ''
|
|
1855
|
-
} else if (shouldContinue === 'deliverable_followthrough') {
|
|
1513
|
+
const continuationPrompt = buildContinuationPrompt({
|
|
1514
|
+
type: shouldContinue,
|
|
1515
|
+
message,
|
|
1516
|
+
fullText,
|
|
1517
|
+
toolEvents: streamedToolEvents,
|
|
1518
|
+
requiredToolReminderNames,
|
|
1519
|
+
})
|
|
1520
|
+
|
|
1521
|
+
if (continuationPrompt) {
|
|
1856
1522
|
if (continuationAssistantText) {
|
|
1857
1523
|
langchainMessages.push(new AIMessage({ content: continuationAssistantText }))
|
|
1858
1524
|
}
|
|
1859
1525
|
const settledSegment = extractSuggestions(lastSegment).clean.trim()
|
|
1860
1526
|
if (settledSegment) lastSettledSegment = settledSegment
|
|
1861
|
-
langchainMessages.push(new HumanMessage({
|
|
1862
|
-
content: buildDeliverableFollowthroughPrompt({
|
|
1863
|
-
userMessage: message,
|
|
1864
|
-
fullText,
|
|
1865
|
-
toolEvents: streamedToolEvents,
|
|
1866
|
-
}),
|
|
1867
|
-
}))
|
|
1868
|
-
lastSegment = ''
|
|
1869
|
-
} else if (shouldContinue === 'tool_summary') {
|
|
1870
|
-
// Model called tools but produced no/trivial text — prompt it to synthesize results.
|
|
1871
|
-
if (continuationAssistantText) {
|
|
1872
|
-
langchainMessages.push(new AIMessage({ content: continuationAssistantText }))
|
|
1873
|
-
}
|
|
1874
|
-
const toolSummaryLines = streamedToolEvents
|
|
1875
|
-
.filter((e) => e.output)
|
|
1876
|
-
.map((e) => `[${e.name}]: ${(e.output || '').slice(0, 500)}`)
|
|
1877
|
-
.slice(0, 6)
|
|
1878
|
-
const preambleNote = fullText.trim()
|
|
1879
|
-
? `You started with "${fullText.trim().slice(0, 100)}..." but did not follow through with actual results.`
|
|
1880
|
-
: 'Your tool calls completed but you did not provide a response.'
|
|
1881
|
-
langchainMessages.push(new HumanMessage({
|
|
1882
|
-
content: [
|
|
1883
|
-
preambleNote,
|
|
1884
|
-
'Here are the tool results:',
|
|
1885
|
-
...toolSummaryLines,
|
|
1886
|
-
'',
|
|
1887
|
-
`Original request: ${message.slice(0, 500)}`,
|
|
1888
|
-
'',
|
|
1889
|
-
'Now answer the original request using these tool results. Be concise and direct. Present the findings clearly.',
|
|
1890
|
-
].join('\n'),
|
|
1891
|
-
}))
|
|
1527
|
+
langchainMessages.push(new HumanMessage({ content: continuationPrompt }))
|
|
1892
1528
|
lastSegment = ''
|
|
1893
1529
|
} else if (shouldContinue === 'transient') {
|
|
1894
1530
|
// Short delay before retrying transient errors (API timeout, rate limit, etc.)
|
|
1895
|
-
await
|
|
1531
|
+
await sleep(2000 * transientRetryCount)
|
|
1896
1532
|
}
|
|
1897
1533
|
}
|
|
1898
1534
|
} catch (err: unknown) {
|
|
1899
1535
|
const errMsg = timedOut
|
|
1900
1536
|
? 'Ongoing loop stopped after reaching the configured runtime limit.'
|
|
1901
|
-
:
|
|
1537
|
+
: errorMessage(err)
|
|
1902
1538
|
const heartbeatEligible = runtime.loopMode === 'ongoing' || session.heartbeatEnabled === true || agentHeartbeatEnabled
|
|
1903
1539
|
const budgetLimited = timedOut || /recursion limit|maximum recursion/i.test(errMsg)
|
|
1904
1540
|
if (heartbeatEligible && budgetLimited) {
|