@swarmclawai/swarmclaw 0.7.8 → 0.8.0
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 +12 -15
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +22 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +26 -1
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +73 -24
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +44 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +7 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +191 -95
- package/src/components/tasks/task-board.tsx +273 -2
- package/src/components/tasks/task-card.tsx +38 -9
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +11 -0
- package/src/lib/server/capability-router.ts +26 -1
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +353 -72
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +362 -63
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +1 -1
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +189 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +15 -10
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +2 -2
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +205 -5
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +262 -0
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +293 -61
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +52 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +348 -61
- package/src/lib/server/session-tools/context.ts +12 -3
- package/src/lib/server/session-tools/crud.ts +221 -10
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate.ts +64 -8
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +546 -79
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +162 -1
- package/src/lib/server/session-tools/web.ts +468 -64
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +419 -9
- package/src/lib/server/stream-agent-chat.ts +887 -83
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.ts +4 -2
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- package/src/types/index.ts +210 -14
|
@@ -8,11 +8,12 @@ import { loadSettings, loadAgents, loadSkills, appendUsage } from './storage'
|
|
|
8
8
|
import { estimateCost, buildPluginDefinitionCosts } from './cost'
|
|
9
9
|
import { getPluginManager } from './plugins'
|
|
10
10
|
import { loadRuntimeSettings, getAgentLoopRecursionLimit } from './runtime-settings'
|
|
11
|
+
import { buildSkillPromptText } from './skill-prompt-budget'
|
|
11
12
|
|
|
12
13
|
import { logExecution } from './execution-log'
|
|
13
14
|
import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
|
|
14
15
|
import { canonicalizePluginId, expandPluginIds } from './tool-aliases'
|
|
15
|
-
import type { Session, Message, UsageRecord, PluginInvocationRecord } from '@/types'
|
|
16
|
+
import type { Session, Message, UsageRecord, PluginInvocationRecord, MessageToolEvent } from '@/types'
|
|
16
17
|
import { extractSuggestions } from './suggestions'
|
|
17
18
|
import { buildIdentityContinuityContext } from './identity-continuity'
|
|
18
19
|
import { enqueueSystemEvent } from './system-events'
|
|
@@ -21,9 +22,10 @@ import {
|
|
|
21
22
|
getEnabledToolPlanningView,
|
|
22
23
|
getFirstToolForCapability,
|
|
23
24
|
getToolsForCapability,
|
|
24
|
-
matchToolCapabilitiesForMessage,
|
|
25
25
|
TOOL_CAPABILITY,
|
|
26
26
|
} from './tool-planning'
|
|
27
|
+
import { ToolLoopTracker } from './tool-loop-detection'
|
|
28
|
+
import type { LoopDetectionResult } from './tool-loop-detection'
|
|
27
29
|
|
|
28
30
|
/** Extract a breadcrumb title from notable tool completions (task/schedule/agent creation). */
|
|
29
31
|
interface StreamAgentChatOpts {
|
|
@@ -57,6 +59,8 @@ export function buildToolDisciplineLines(enabledPlugins: string[]): string[] {
|
|
|
57
59
|
const planning = getEnabledToolPlanningView(enabledPlugins)
|
|
58
60
|
const uniqueTools = planning.displayToolIds
|
|
59
61
|
if (uniqueTools.length === 0) return []
|
|
62
|
+
const walletTools = getToolsForCapability(enabledPlugins, TOOL_CAPABILITY.walletInspect)
|
|
63
|
+
const httpTools = getToolsForCapability(enabledPlugins, 'network.http')
|
|
60
64
|
|
|
61
65
|
const lines = [
|
|
62
66
|
`Enabled tools in this session: ${uniqueTools.map((toolId) => `\`${toolId}\``).join(', ')}.`,
|
|
@@ -82,6 +86,10 @@ export function buildToolDisciplineLines(enabledPlugins: string[]): string[] {
|
|
|
82
86
|
lines.push(`When a task asks for both research and screenshots, use ${researchLabel} first to identify the right source URLs, then use \`${browserCaptureTools[0]}\` to capture the relevant page.`)
|
|
83
87
|
}
|
|
84
88
|
|
|
89
|
+
if (researchSearchTools.length) {
|
|
90
|
+
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.`)
|
|
91
|
+
}
|
|
92
|
+
|
|
85
93
|
if (browserCaptureTools.length && deliveryMediaTools.length) {
|
|
86
94
|
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.`)
|
|
87
95
|
}
|
|
@@ -90,48 +98,85 @@ export function buildToolDisciplineLines(enabledPlugins: string[]): string[] {
|
|
|
90
98
|
lines.push(`If the user asks for a voice note and \`${deliveryVoiceTools[0]}\` is enabled, try it before saying voice notes are unsupported.`)
|
|
91
99
|
}
|
|
92
100
|
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (!normalized.trim()) return false
|
|
99
|
-
if (/```|package\.json|tsconfig|tsx?\b|jsx?\b|pytest|vitest|npm run|src\/|components\/|api\//.test(normalized)) return false
|
|
100
|
-
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
|
|
101
|
-
return isBroadGoal(text) && /(\.md\b|\.txt\b|copy|brief|proposal|plan|report|draft|document)/.test(normalized)
|
|
102
|
-
}
|
|
101
|
+
if (walletTools.length && (uniqueTools.includes('browser') || httpTools.length > 0)) {
|
|
102
|
+
lines.push(`For external wallet or trading workflows, inspect the available wallet first with \`${walletTools[0]}\` before browsing or calling third-party APIs.`)
|
|
103
|
+
lines.push('For dApps, exchanges, and wallet-connect flows, use a bounded loop: verify the wallet/tooling you control, attempt one concrete reversible step, then either execute the next real action or state the exact blocker. Do not keep browsing once the blocker is clear.')
|
|
104
|
+
lines.push('For swaps, purchases, and other live onchain tasks, do not shop across venues indefinitely. After a small number of failed API families, either use a direct onchain read path with the tools you have or state the blocker.')
|
|
105
|
+
}
|
|
103
106
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
if (uniqueTools.includes('browser')) {
|
|
108
|
+
lines.push('For browser form workflows, start with `read_page` or `extract_form_fields`, then prefer `fill_form` and `submit_form`. Only use raw `click`/`type`/`select` when you already have the exact target information from the current page.')
|
|
109
|
+
lines.push('When the task provides a literal URL or you are already on the correct page, keep working from that page state. Do not invent alternate domains, ports, or routes unless the current page explicitly links to them.')
|
|
110
|
+
}
|
|
108
111
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (toolName && !required.includes(toolName)) required.push(toolName)
|
|
112
|
+
if (uniqueTools.includes('ask_human')) {
|
|
113
|
+
lines.push('For human-loop tasks, use `ask_human` in order: `request_input` with a concrete question, `wait_for_reply` with the returned `correlationId`, then `list_mailbox` to read the `human_reply` payload. Use `ack_mailbox` with the reply envelope id once consumed, or omit `envelopeId` to ack the newest unread human reply. Do not loop on `status` without a `watchJobId` or `approvalId`.')
|
|
112
114
|
}
|
|
113
115
|
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
+
if (uniqueTools.includes('manage_schedules')) {
|
|
117
|
+
lines.push('Before creating a schedule, inspect existing schedules in this chat and reuse or update matching agent-created schedules instead of creating near-duplicates.')
|
|
118
|
+
lines.push('For one-off reminders, prefer `scheduleType: "once"`; reserve recurring schedules for work that truly needs to repeat.')
|
|
119
|
+
lines.push('When the user says stop, pause, cancel, or disable a reminder, list schedules first and pause or delete every matching schedule you created in this chat.')
|
|
116
120
|
}
|
|
117
121
|
|
|
118
|
-
if (
|
|
119
|
-
|
|
122
|
+
if (uniqueTools.includes('schedule_wake')) {
|
|
123
|
+
lines.push('For a one-off conversational reminder in the current chat, prefer `schedule_wake` over creating a recurring schedule.')
|
|
120
124
|
}
|
|
121
125
|
|
|
122
|
-
if (
|
|
123
|
-
|
|
126
|
+
if (uniqueTools.includes('manage_secrets')) {
|
|
127
|
+
lines.push('When a workflow reveals a password, app password, API key, recovery token, or other secret, store it with `manage_secrets` and do not echo the raw value in assistant text. Refer to the secret by name, service, or secret id instead.')
|
|
124
128
|
}
|
|
125
129
|
|
|
126
|
-
if (
|
|
127
|
-
|
|
130
|
+
if (uniqueTools.includes('delegate') && (uniqueTools.includes('shell') || uniqueTools.includes('files') || uniqueTools.includes('edit_file'))) {
|
|
131
|
+
lines.push('When local workspace tools like `shell`, `files`, or `edit_file` are already enabled, prefer using them directly for straightforward coding and verification. Use `delegate` when you need a specialist backend, a second implementation pass, or parallel work.')
|
|
128
132
|
}
|
|
129
133
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
134
|
+
return lines
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function looksLikeOpenEndedDeliverableTask(text: string): boolean {
|
|
138
|
+
const normalized = text.toLowerCase()
|
|
139
|
+
if (!normalized.trim()) return false
|
|
140
|
+
if (/```|package\.json|tsconfig|\btsx?\b|\bjsx?\b|pytest|vitest|npm run|src\/|components\/|api\//.test(normalized)) return false
|
|
141
|
+
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
|
|
142
|
+
// Explicit file-save instructions (e.g. "create X and save it to /tmp/foo.html")
|
|
143
|
+
if (
|
|
144
|
+
/\b(create|build|generate|make|write|produce)\b/.test(normalized)
|
|
145
|
+
&& /\b(save|write|output|export)\b[^.!?\n]{0,60}\b(to|as|in)\b[^.!?\n]{0,40}(\/|~\/|\.\/|\.[a-z]{2,5}\b)/.test(normalized)
|
|
146
|
+
) {
|
|
147
|
+
return true
|
|
148
|
+
}
|
|
149
|
+
if (
|
|
150
|
+
isBroadGoal(text)
|
|
151
|
+
&& /\b(create|build|generate|make|write|research|capture|take|start|produce)\b/.test(normalized)
|
|
152
|
+
&& /\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)
|
|
153
|
+
) {
|
|
154
|
+
return true
|
|
133
155
|
}
|
|
156
|
+
return isBroadGoal(text) && /(\.md\b|\.txt\b|\.html\b|\.json\b|copy|brief|proposal|plan|report|draft|document|dashboard)/.test(normalized)
|
|
157
|
+
}
|
|
134
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Returns tool names that the user explicitly referenced by name in their message.
|
|
161
|
+
*
|
|
162
|
+
* Previously this used regex-based capability matching (matchToolCapabilitiesForMessage)
|
|
163
|
+
* to infer required tools from keywords like "send", "search", "screenshot". This caused
|
|
164
|
+
* false positives ("sends an HTTP request" forced connector_message_tool, "create a file"
|
|
165
|
+
* forced delivery tools) and extra continuation loops.
|
|
166
|
+
*
|
|
167
|
+
* OpenClaw's approach: trust the LLM to select the right tools based on prompt engineering
|
|
168
|
+
* (tool discipline lines, skill adherence header, system prompt). No regex-based forced
|
|
169
|
+
* tool requirements. The deliverable/execution followthrough mechanisms handle cases where
|
|
170
|
+
* the agent stops early.
|
|
171
|
+
*
|
|
172
|
+
* We now only force tools when the user explicitly names them (ask_human, email) — these
|
|
173
|
+
* are cases where the LLM has a known tendency to skip the tool and respond in prose.
|
|
174
|
+
*/
|
|
175
|
+
export function getExplicitRequiredToolNames(userMessage: string, enabledPlugins: string[]): string[] {
|
|
176
|
+
const normalized = userMessage.toLowerCase()
|
|
177
|
+
const required: string[] = []
|
|
178
|
+
|
|
179
|
+
// Only force tools that the user explicitly names and the LLM tends to skip
|
|
135
180
|
if (enabledPlugins.includes('ask_human')
|
|
136
181
|
&& (/\bask_human\b/.test(normalized) || /ask the human/.test(normalized) || /request_input/.test(normalized))) {
|
|
137
182
|
required.push('ask_human')
|
|
@@ -153,6 +198,320 @@ const OPEN_ENDED_REVISION_BLOCK = [
|
|
|
153
198
|
'If `files` is available, use it with explicit actions and paths to inspect and revise the artifacts.',
|
|
154
199
|
].join('\n')
|
|
155
200
|
|
|
201
|
+
function looksLikeExternalWalletTask(text: string): boolean {
|
|
202
|
+
const normalized = text.toLowerCase()
|
|
203
|
+
if (!normalized.trim()) return false
|
|
204
|
+
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)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function looksLikeBoundedExternalExecutionTask(text: string): boolean {
|
|
208
|
+
const normalized = text.toLowerCase()
|
|
209
|
+
if (!looksLikeExternalWalletTask(text)) return false
|
|
210
|
+
return /\b(live|swap|trade|buy|purchase|sell|mint|claim|execute|transact|transaction|approve|broadcast)\b/.test(normalized)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function getEnabledDisplayTool(enabledPlugins: string[], canonicalPluginId: string): string | null {
|
|
214
|
+
return getEnabledToolPlanningView(enabledPlugins).displayToolIds.find((toolId) => toolId === canonicalPluginId) || null
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function buildExternalWalletExecutionBlock(enabledPlugins: string[]): string {
|
|
218
|
+
const hasExecutionContext = Boolean(
|
|
219
|
+
getFirstToolForCapability(enabledPlugins, TOOL_CAPABILITY.walletInspect)
|
|
220
|
+
|| getFirstToolForCapability(enabledPlugins, 'network.http')
|
|
221
|
+
|| getEnabledDisplayTool(enabledPlugins, 'browser')
|
|
222
|
+
|| getEnabledDisplayTool(enabledPlugins, 'manage_capabilities'),
|
|
223
|
+
)
|
|
224
|
+
if (!hasExecutionContext) return ''
|
|
225
|
+
const lines = [
|
|
226
|
+
'## External Service Execution',
|
|
227
|
+
'Define a stop condition before exploring: either complete one concrete reversible action, or identify the exact blocker with evidence.',
|
|
228
|
+
'A prose sentence saying approval is needed is not enough. When the next step is a wallet signature or transaction, trigger the actual wallet approval request through the tool.',
|
|
229
|
+
'After one or two discovery bursts, stop exploring and summarize the blocker if execution still depends on a missing capability such as injected wallet signing, external credentials, or unavailable approvals.',
|
|
230
|
+
'Do not mutate already confirmed identifiers unless newer tool evidence proves the earlier value was wrong.',
|
|
231
|
+
'Never claim success on a trading or dApp task unless you either completed the reversible step with tool evidence or clearly stated the final missing step.',
|
|
232
|
+
]
|
|
233
|
+
return lines.join('\n')
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function shouldForceExternalServiceSummary(params: {
|
|
237
|
+
userMessage: string
|
|
238
|
+
finalResponse: string
|
|
239
|
+
hasToolCalls: boolean
|
|
240
|
+
toolEventCount: number
|
|
241
|
+
}): boolean {
|
|
242
|
+
if (!looksLikeExternalWalletTask(params.userMessage)) return false
|
|
243
|
+
if (!params.hasToolCalls || params.toolEventCount === 0) return false
|
|
244
|
+
const trimmed = params.finalResponse.trim()
|
|
245
|
+
if (!trimmed) return true
|
|
246
|
+
if (/\b(blocker|blocked|cannot|can't|requires|need|missing|last reversible step|next step)\b/i.test(trimmed)) return false
|
|
247
|
+
if (trimmed.length >= 240 && !/(let me|i'll|i will|checking|verify|promising|look into|explore|access their interface)/i.test(trimmed)) return false
|
|
248
|
+
return /:$/.test(trimmed) || /(let me|i'll|i will|checking|verify|promising|look into|explore|access their interface)/i.test(trimmed) || trimmed.length < 240
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function resolveToolAction(input: unknown): string {
|
|
252
|
+
if (input && typeof input === 'object' && !Array.isArray(input)) {
|
|
253
|
+
const action = (input as Record<string, unknown>).action
|
|
254
|
+
return typeof action === 'string' ? action.trim().toLowerCase() : ''
|
|
255
|
+
}
|
|
256
|
+
if (typeof input !== 'string') return ''
|
|
257
|
+
const trimmed = input.trim()
|
|
258
|
+
if (!trimmed.startsWith('{')) return ''
|
|
259
|
+
try {
|
|
260
|
+
const parsed = JSON.parse(trimmed) as Record<string, unknown>
|
|
261
|
+
return typeof parsed.action === 'string' ? parsed.action.trim().toLowerCase() : ''
|
|
262
|
+
} catch {
|
|
263
|
+
return ''
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function shouldTerminateOnSuccessfulMemoryMutation(params: {
|
|
268
|
+
toolName: string
|
|
269
|
+
toolInput: unknown
|
|
270
|
+
toolOutput: string
|
|
271
|
+
}): boolean {
|
|
272
|
+
const canonicalToolName = canonicalizePluginId(params.toolName) || params.toolName
|
|
273
|
+
if (canonicalToolName !== 'memory') return false
|
|
274
|
+
const action = resolveToolAction(params.toolInput)
|
|
275
|
+
if (action !== 'store' && action !== 'update') return false
|
|
276
|
+
const output = extractSuggestions(params.toolOutput || '').clean.trim()
|
|
277
|
+
if (!output || /^error[:\s]/i.test(output)) return false
|
|
278
|
+
if (!/^(stored|updated) memory\b/i.test(output)) return false
|
|
279
|
+
return /no further memory lookup is needed unless the user asked you to verify/i.test(output)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function hasStateChangingWalletEvidence(toolEvents: MessageToolEvent[]): boolean {
|
|
283
|
+
return toolEvents.some((event) => {
|
|
284
|
+
const input = `${event.input || ''}\n${event.output || ''}`
|
|
285
|
+
return event.name === 'wallet_tool' && (
|
|
286
|
+
/"action":"send_transaction"/.test(input)
|
|
287
|
+
|| /"action":"send"/.test(input)
|
|
288
|
+
|| /"action":"sign_transaction"/.test(input)
|
|
289
|
+
|| /"type":"plugin_wallet_action_request"/.test(input)
|
|
290
|
+
|| /"type":"plugin_wallet_transfer_request"/.test(input)
|
|
291
|
+
|| /"status":"broadcast"/.test(input)
|
|
292
|
+
)
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function countExternalExecutionResearchSteps(toolEvents: MessageToolEvent[]): number {
|
|
297
|
+
return toolEvents.filter((event) => {
|
|
298
|
+
if (['http_request', 'web', 'web_search', 'web_fetch', 'browser'].includes(event.name)) return true
|
|
299
|
+
if (event.name !== 'wallet_tool') return false
|
|
300
|
+
return /"action":"(balance|address|transactions|call_contract|encode_contract_call)"/.test(event.input || '')
|
|
301
|
+
}).length
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function countDistinctExternalResearchHosts(toolEvents: MessageToolEvent[]): number {
|
|
305
|
+
const hosts = new Set<string>()
|
|
306
|
+
for (const event of toolEvents) {
|
|
307
|
+
const candidates = [event.input || '', event.output || '']
|
|
308
|
+
for (const candidate of candidates) {
|
|
309
|
+
const matches = candidate.match(/https?:\/\/[^"'\\\s)]+/g) || []
|
|
310
|
+
for (const match of matches) {
|
|
311
|
+
try {
|
|
312
|
+
hosts.add(new URL(match).host.toLowerCase())
|
|
313
|
+
} catch {
|
|
314
|
+
// Ignore malformed URLs in model/tool text.
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return hosts.size
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function getWalletApprovalBoundaryAction(output: string): string | null {
|
|
323
|
+
if (!output.includes('plugin_wallet_')) return null
|
|
324
|
+
if (/"type":"plugin_wallet_transfer_request"/.test(output)) return 'send'
|
|
325
|
+
const actionMatch = output.match(/"action":"([^"]+)"/)
|
|
326
|
+
const action = actionMatch?.[1] || ''
|
|
327
|
+
if (!action) return null
|
|
328
|
+
const readOnlyActions = new Set([
|
|
329
|
+
'balance',
|
|
330
|
+
'address',
|
|
331
|
+
'transactions',
|
|
332
|
+
'encode_contract_call',
|
|
333
|
+
'simulate_transaction',
|
|
334
|
+
])
|
|
335
|
+
return readOnlyActions.has(action) ? null : action
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function isWalletSimulationResult(toolName: string, output: string): boolean {
|
|
339
|
+
return toolName === 'wallet_tool' && /"status":"simulated"/.test(output)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function shouldForceExternalExecutionFollowthrough(params: {
|
|
343
|
+
userMessage: string
|
|
344
|
+
finalResponse: string
|
|
345
|
+
hasToolCalls: boolean
|
|
346
|
+
toolEvents: MessageToolEvent[]
|
|
347
|
+
}): boolean {
|
|
348
|
+
if (!looksLikeBoundedExternalExecutionTask(params.userMessage)) return false
|
|
349
|
+
if (!params.hasToolCalls || params.toolEvents.length < 4) return false
|
|
350
|
+
if (hasStateChangingWalletEvidence(params.toolEvents)) return false
|
|
351
|
+
const distinctHosts = countDistinctExternalResearchHosts(params.toolEvents)
|
|
352
|
+
const trimmed = params.finalResponse.trim()
|
|
353
|
+
if (!trimmed) return countExternalExecutionResearchSteps(params.toolEvents) >= 4 || distinctHosts >= 3
|
|
354
|
+
if (/\b(last reversible step|exact blocker|safest next action|blocked|cannot|can't|missing capability|no-key route unavailable)\b/i.test(trimmed)) {
|
|
355
|
+
return false
|
|
356
|
+
}
|
|
357
|
+
if (countExternalExecutionResearchSteps(params.toolEvents) < 4 && distinctHosts < 3) return false
|
|
358
|
+
return /(let me|i'll|i will|trying|research|query|check|look|promising|now let me|good -|good,)/i.test(trimmed) || trimmed.length < 500
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function looksLikeIncompleteDeliverableResponse(text: string): boolean {
|
|
362
|
+
const trimmed = text.trim()
|
|
363
|
+
if (!trimmed) return true
|
|
364
|
+
if (trimmed.endsWith(':') || trimmed.endsWith('...') || trimmed.endsWith('…')) return true
|
|
365
|
+
const lastChunk = trimmed.slice(-400).toLowerCase()
|
|
366
|
+
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)
|
|
367
|
+
|| /\b(?:i(?:'ll| will)|let me)\s+(?:now|next)?\s*(?:create|build|write|capture|take|start|finish|generate|continue)\b/.test(lastChunk)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function shouldForceDeliverableFollowthrough(params: {
|
|
371
|
+
userMessage: string
|
|
372
|
+
finalResponse: string
|
|
373
|
+
hasToolCalls: boolean
|
|
374
|
+
toolEvents: MessageToolEvent[]
|
|
375
|
+
}): boolean {
|
|
376
|
+
if (!looksLikeOpenEndedDeliverableTask(params.userMessage)) return false
|
|
377
|
+
if (!params.hasToolCalls || params.toolEvents.length === 0) return false
|
|
378
|
+
const trimmed = params.finalResponse.trim()
|
|
379
|
+
if (!trimmed) return params.toolEvents.length >= 2
|
|
380
|
+
if (
|
|
381
|
+
/\b(task complete|completed|finished|done|delivered|shared|sent|uploaded|attached)\b/i.test(trimmed)
|
|
382
|
+
&& /(?:\/api\/uploads\/|https?:\/\/|`[^`\n]+\.(?:md|pdf|png|jpe?g|webp|gif|html|txt|zip)`)/i.test(trimmed)
|
|
383
|
+
) {
|
|
384
|
+
return false
|
|
385
|
+
}
|
|
386
|
+
// If the user asked for file output but no file-write tool was used, force continuation
|
|
387
|
+
const userNormalized = params.userMessage.toLowerCase()
|
|
388
|
+
if (/\b(save|write|output)\b[^.!?\n]{0,60}\b(to|as)\b[^.!?\n]{0,40}(\/|~\/|\.[a-z]{2,5}\b)/.test(userNormalized)) {
|
|
389
|
+
const fileToolNames = ['write_file', 'edit_file', 'files', 'shell', 'execute_command']
|
|
390
|
+
const usedFileTools = params.toolEvents.some((e) => e.name && fileToolNames.includes(e.name))
|
|
391
|
+
if (!usedFileTools) return true
|
|
392
|
+
}
|
|
393
|
+
if (looksLikeIncompleteDeliverableResponse(trimmed)) return true
|
|
394
|
+
return trimmed.length < 120 && params.toolEvents.length >= 3
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function updateStreamedToolEvents(events: MessageToolEvent[], event: { type: 'call' | 'result'; name: string; input?: string; output?: string; toolCallId?: string }) {
|
|
398
|
+
if (event.type === 'call') {
|
|
399
|
+
events.push({
|
|
400
|
+
name: event.name,
|
|
401
|
+
input: event.input || '',
|
|
402
|
+
toolCallId: event.toolCallId,
|
|
403
|
+
})
|
|
404
|
+
return
|
|
405
|
+
}
|
|
406
|
+
const index = event.toolCallId
|
|
407
|
+
? events.findLastIndex((entry) => entry.toolCallId === event.toolCallId && !entry.output)
|
|
408
|
+
: events.findLastIndex((entry) => entry.name === event.name && !entry.output)
|
|
409
|
+
if (index === -1) return
|
|
410
|
+
events[index] = {
|
|
411
|
+
...events[index],
|
|
412
|
+
output: event.output || '',
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function renderToolEvidence(events: MessageToolEvent[]): string {
|
|
417
|
+
return events
|
|
418
|
+
.slice(-10)
|
|
419
|
+
.map((event, index) => [
|
|
420
|
+
`Tool ${index + 1}: ${event.name}`,
|
|
421
|
+
event.input ? `Input: ${event.input}` : '',
|
|
422
|
+
event.output ? `Output: ${event.output.slice(0, 1200)}` : '',
|
|
423
|
+
].filter(Boolean).join('\n'))
|
|
424
|
+
.join('\n\n')
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function buildForcedExternalServiceSummary(params: {
|
|
428
|
+
llm: { invoke: (messages: HumanMessage[]) => Promise<{ content: unknown }> }
|
|
429
|
+
userMessage: string
|
|
430
|
+
fullText: string
|
|
431
|
+
toolEvents: MessageToolEvent[]
|
|
432
|
+
}): Promise<string | null> {
|
|
433
|
+
const prompt = [
|
|
434
|
+
'You are finishing an interrupted external-service tool run.',
|
|
435
|
+
'Do not call tools. Do not continue browsing.',
|
|
436
|
+
'Based only on the objective, partial assistant text, and tool evidence below, produce a concise final status with exactly these headings:',
|
|
437
|
+
'Last reversible step',
|
|
438
|
+
'Exact blocker',
|
|
439
|
+
'Safest next action',
|
|
440
|
+
'',
|
|
441
|
+
`Objective:\n${params.userMessage}`,
|
|
442
|
+
'',
|
|
443
|
+
`Partial assistant text:\n${params.fullText || '(none)'}`,
|
|
444
|
+
'',
|
|
445
|
+
`Tool evidence:\n${renderToolEvidence(params.toolEvents) || '(none)'}`,
|
|
446
|
+
].join('\n')
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
const response = await Promise.race([
|
|
450
|
+
params.llm.invoke([new HumanMessage(prompt)]),
|
|
451
|
+
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('forced-summary-timeout')), 10_000)),
|
|
452
|
+
])
|
|
453
|
+
if (typeof response.content === 'string') return response.content.trim() || null
|
|
454
|
+
if (Array.isArray(response.content)) {
|
|
455
|
+
const text = response.content
|
|
456
|
+
.map((block: Record<string, unknown>) => (typeof block.text === 'string' ? block.text : ''))
|
|
457
|
+
.join('')
|
|
458
|
+
.trim()
|
|
459
|
+
return text || null
|
|
460
|
+
}
|
|
461
|
+
return null
|
|
462
|
+
} catch {
|
|
463
|
+
return null
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function buildExternalExecutionFollowthroughPrompt(params: {
|
|
468
|
+
userMessage: string
|
|
469
|
+
fullText: string
|
|
470
|
+
toolEvents: MessageToolEvent[]
|
|
471
|
+
}): string {
|
|
472
|
+
return [
|
|
473
|
+
'You are in a bounded external execution task and have already done enough research.',
|
|
474
|
+
'Do not restart broad discovery. Do not ask the user for another prompt.',
|
|
475
|
+
'Do not spend this continuation on more venue shopping. Use the already confirmed route unless one last fetch is strictly required to prepare execution.',
|
|
476
|
+
'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.',
|
|
477
|
+
'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.',
|
|
478
|
+
'Do not mutate already confirmed token addresses, router addresses, spender addresses, or network identifiers unless newer tool evidence proves the earlier value was wrong.',
|
|
479
|
+
'Within this continuation, do exactly one of the following:',
|
|
480
|
+
'1. Take the next concrete execution step now using the existing tools and stop at the first approval boundary for a state-changing action.',
|
|
481
|
+
'2. If no safe executable step exists with the current tools, state the exact blocker with evidence.',
|
|
482
|
+
'A successful continuation ends with one of these outcomes only: an approval request, a broadcast transaction, or a final blocker summary.',
|
|
483
|
+
'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.',
|
|
484
|
+
'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.',
|
|
485
|
+
'',
|
|
486
|
+
`Objective:\n${params.userMessage}`,
|
|
487
|
+
'',
|
|
488
|
+
`Current partial response:\n${params.fullText || '(none)'}`,
|
|
489
|
+
'',
|
|
490
|
+
`Recent tool evidence:\n${renderToolEvidence(params.toolEvents) || '(none)'}`,
|
|
491
|
+
].join('\n')
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function buildDeliverableFollowthroughPrompt(params: {
|
|
495
|
+
userMessage: string
|
|
496
|
+
fullText: string
|
|
497
|
+
toolEvents: MessageToolEvent[]
|
|
498
|
+
}): string {
|
|
499
|
+
return [
|
|
500
|
+
'You are in the middle of a multi-step deliverable and stopped after only a partial batch of work.',
|
|
501
|
+
'Continue from the existing workspace and artifacts. Do not restart from scratch and do not ask the user to restate the request.',
|
|
502
|
+
'Do not stop after one partial batch. Finish every requested deliverable that is still outstanding before concluding.',
|
|
503
|
+
'If a requested artifact cannot be produced, say exactly which artifact is missing, what blocked it, and what you already completed.',
|
|
504
|
+
'Use the existing files, screenshots, and generated outputs first. Inspect them if needed, then complete the remaining work.',
|
|
505
|
+
'End with a concise grouped completion summary that lists exact file paths, upload URLs, localhost URLs/ports, and screenshots you produced.',
|
|
506
|
+
'',
|
|
507
|
+
`Objective:\n${params.userMessage}`,
|
|
508
|
+
'',
|
|
509
|
+
`Current partial response:\n${params.fullText || '(none)'}`,
|
|
510
|
+
'',
|
|
511
|
+
`Recent tool evidence:\n${renderToolEvidence(params.toolEvents) || '(none)'}`,
|
|
512
|
+
].join('\n')
|
|
513
|
+
}
|
|
514
|
+
|
|
156
515
|
/** Detect whether a user message is a broad, high-level goal that benefits from decomposition. */
|
|
157
516
|
function isBroadGoal(text: string): boolean {
|
|
158
517
|
if (text.length < 50) return false
|
|
@@ -182,6 +541,8 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
182
541
|
heartbeatIntervalSec: number
|
|
183
542
|
platformAssignScope?: 'self' | 'all'
|
|
184
543
|
userMessage?: string
|
|
544
|
+
responseStyle?: 'concise' | 'normal' | 'detailed' | null
|
|
545
|
+
responseMaxChars?: number | null
|
|
185
546
|
}) {
|
|
186
547
|
const hasTooling = opts.enabledPlugins.length > 0
|
|
187
548
|
const pluginLines = buildPluginCapabilityLines(opts.enabledPlugins, { platformAssignScope: opts.platformAssignScope })
|
|
@@ -195,9 +556,12 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
195
556
|
hasTooling
|
|
196
557
|
? 'I take initiative — plan briefly, execute tools, evaluate, iterate until done. Never stop at advice when action is implied.'
|
|
197
558
|
: 'No tools enabled. Be explicit about what tool access is needed.',
|
|
198
|
-
'
|
|
559
|
+
'IMPORTANT: If information was already mentioned in THIS conversation, answer from context — do NOT call memory_tool or web search to look it up again. Only use memory_tool to recall info from PREVIOUS conversations not in the current thread.',
|
|
560
|
+
'If a skill applies to the task, follow its recommended approach first. Skill-specific commands are faster and more reliable than generic web search. Minimize tool calls — combine steps where possible.',
|
|
199
561
|
'If a task explicitly names an enabled tool, use that tool before declaring success. A prose request is not a substitute for `ask_human`, and browser work is not a substitute for `email` delivery.',
|
|
200
562
|
'When `ask_human` is enabled, collect required human input through the tool instead of asking for it only in plain assistant text.',
|
|
563
|
+
'Do not narrate routine tool calls. Just call the tool and report the outcome. Only narrate when the step is complex, sensitive, or the user needs to understand what is happening.',
|
|
564
|
+
'Do not repeat the same tool call with identical arguments. If a tool returns an error or empty result, try a different approach instead of retrying the same call.',
|
|
201
565
|
opts.loopMode === 'ongoing'
|
|
202
566
|
? 'Loop: ONGOING — keep iterating until done, blocked, or limits reached.'
|
|
203
567
|
: 'Loop: BOUNDED — execute multiple steps but finish within recursion budget.',
|
|
@@ -210,16 +574,15 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
210
574
|
// Response behavior
|
|
211
575
|
parts.push(
|
|
212
576
|
'## Response Rules',
|
|
213
|
-
'NO_MESSAGE: reply with exactly this
|
|
214
|
-
'
|
|
215
|
-
'
|
|
216
|
-
'
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
'Only mention files, screenshots, URLs, or download links that were actually returned by tools. Copy returned links exactly; do not rewrite them or prepend extra prefixes like "sandbox:".',
|
|
577
|
+
'NO_MESSAGE: reply with exactly this for pure acknowledgments (ok/thanks/bye/emoji).',
|
|
578
|
+
'Execute by default — only confirm on high-risk actions.',
|
|
579
|
+
'If a tool errors, retry or explain the blocker. Never claim success without evidence.',
|
|
580
|
+
'Keep responses concise. Bullet points over prose. After file operations, confirm the result briefly (path and status) without echoing the full file contents.',
|
|
581
|
+
opts.responseStyle === 'concise'
|
|
582
|
+
? `IMPORTANT: Be extremely concise.${opts.responseMaxChars ? ` Keep responses under ${opts.responseMaxChars} characters.` : ' Target under 500 characters.'} Lead with the answer, skip preamble.`
|
|
583
|
+
: opts.responseStyle === 'detailed'
|
|
584
|
+
? 'Provide thorough, detailed explanations when helpful.'
|
|
585
|
+
: '',
|
|
223
586
|
`Heartbeat: if message is "${opts.heartbeatPrompt}", reply "HEARTBEAT_OK" unless you have a progress update.`,
|
|
224
587
|
opts.heartbeatIntervalSec > 0 ? `Heartbeat cadence: ~${opts.heartbeatIntervalSec}s.` : '',
|
|
225
588
|
)
|
|
@@ -227,6 +590,10 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
227
590
|
if (toolDisciplineLines.length) parts.push('## Tool Discipline', ...toolDisciplineLines)
|
|
228
591
|
if (pluginLines.length) parts.push('What I can do:\n' + pluginLines.join('\n'))
|
|
229
592
|
if (opts.userMessage && isBroadGoal(opts.userMessage)) parts.push(GOAL_DECOMPOSITION_BLOCK)
|
|
593
|
+
if (opts.userMessage && looksLikeExternalWalletTask(opts.userMessage)) {
|
|
594
|
+
const externalExecutionBlock = buildExternalWalletExecutionBlock(opts.enabledPlugins)
|
|
595
|
+
if (externalExecutionBlock) parts.push(externalExecutionBlock)
|
|
596
|
+
}
|
|
230
597
|
if (opts.userMessage && looksLikeOpenEndedDeliverableTask(opts.userMessage) && opts.enabledPlugins.some((toolId) => toolId === 'files' || toolId === 'edit_file')) {
|
|
231
598
|
parts.push(OPEN_ENDED_REVISION_BLOCK)
|
|
232
599
|
}
|
|
@@ -242,6 +609,52 @@ export interface StreamAgentChatResult {
|
|
|
242
609
|
finalResponse: string
|
|
243
610
|
}
|
|
244
611
|
|
|
612
|
+
function resolveToolOnlyFinalResponse(toolEvents: MessageToolEvent[] | undefined): string {
|
|
613
|
+
const events = Array.isArray(toolEvents) ? toolEvents : []
|
|
614
|
+
for (let index = events.length - 1; index >= 0; index--) {
|
|
615
|
+
const event = events[index]
|
|
616
|
+
const output = typeof event?.output === 'string'
|
|
617
|
+
? extractSuggestions(event.output).clean.trim()
|
|
618
|
+
: ''
|
|
619
|
+
if (!output) continue
|
|
620
|
+
if (/^error[:\s]/i.test(output)) continue
|
|
621
|
+
if (output.startsWith('{') || output.startsWith('[')) continue
|
|
622
|
+
return output
|
|
623
|
+
}
|
|
624
|
+
return ''
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export function resolveFinalStreamResponseText(params: {
|
|
628
|
+
fullText: string
|
|
629
|
+
lastSegment: string
|
|
630
|
+
lastSettledSegment: string
|
|
631
|
+
hasToolCalls: boolean
|
|
632
|
+
toolEvents?: MessageToolEvent[]
|
|
633
|
+
}): string {
|
|
634
|
+
const fullText = params.fullText || ''
|
|
635
|
+
if (!params.hasToolCalls) return fullText
|
|
636
|
+
|
|
637
|
+
const candidates = [
|
|
638
|
+
extractSuggestions(params.lastSegment || '').clean.trim(),
|
|
639
|
+
extractSuggestions(params.lastSettledSegment || '').clean.trim(),
|
|
640
|
+
extractSuggestions(fullText).clean.trim(),
|
|
641
|
+
resolveToolOnlyFinalResponse(params.toolEvents),
|
|
642
|
+
]
|
|
643
|
+
|
|
644
|
+
return candidates.find((candidate) => candidate.length > 0) || ''
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
export function resolveContinuationAssistantText(params: {
|
|
648
|
+
iterationText: string
|
|
649
|
+
lastSegment: string
|
|
650
|
+
}): string {
|
|
651
|
+
const candidates = [
|
|
652
|
+
extractSuggestions(params.iterationText || '').clean.trim(),
|
|
653
|
+
extractSuggestions(params.lastSegment || '').clean.trim(),
|
|
654
|
+
]
|
|
655
|
+
return candidates.find((candidate) => candidate.length > 0) || ''
|
|
656
|
+
}
|
|
657
|
+
|
|
245
658
|
export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<StreamAgentChatResult> {
|
|
246
659
|
const startTs = Date.now()
|
|
247
660
|
const { session, message, imagePath, attachedFiles, apiKey, systemPrompt, write, history, fallbackCredentialIds, signal } = opts
|
|
@@ -305,6 +718,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
305
718
|
let agentMcpDisabledTools: string[] | undefined
|
|
306
719
|
let agentHeartbeatEnabled = false
|
|
307
720
|
let agentMemoryScopeMode: 'auto' | 'all' | 'global' | 'agent' | 'session' | 'project' | null = null
|
|
721
|
+
let agentResponseStyle: 'concise' | 'normal' | 'detailed' | null = null
|
|
722
|
+
let agentResponseMaxChars: number | null = null
|
|
308
723
|
const activeProjectContext = resolveActiveProjectContext(session)
|
|
309
724
|
if (session.agentId) {
|
|
310
725
|
const agents = loadAgents()
|
|
@@ -314,6 +729,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
314
729
|
agentMcpDisabledTools = agent?.mcpDisabledTools
|
|
315
730
|
agentHeartbeatEnabled = agent?.heartbeatEnabled === true
|
|
316
731
|
agentMemoryScopeMode = agent?.memoryScopeMode || null
|
|
732
|
+
agentResponseStyle = agent?.responseStyle || null
|
|
733
|
+
agentResponseMaxChars = agent?.responseMaxChars || null
|
|
317
734
|
if (!hasProvidedSystemPrompt) {
|
|
318
735
|
// Identity block — make sure the agent knows who it is
|
|
319
736
|
const identityLines = [`## My Identity`, `My name is ${agent?.name || 'Agent'}.`]
|
|
@@ -326,17 +743,25 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
326
743
|
if (agent?.systemPrompt) stateModifierParts.push(agent.systemPrompt)
|
|
327
744
|
if (agent?.skillIds?.length) {
|
|
328
745
|
const allSkills = loadSkills()
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (skill?.content) stateModifierParts.push(`## Skill: ${skill.name}\n${skill.content}`)
|
|
332
|
-
}
|
|
746
|
+
const skillPromptText = buildSkillPromptText(allSkills, agent.skillIds)
|
|
747
|
+
if (skillPromptText) stateModifierParts.push(skillPromptText)
|
|
333
748
|
}
|
|
749
|
+
|
|
750
|
+
// Auto-discover workspace/bundled skills not already in the DB
|
|
751
|
+
try {
|
|
752
|
+
const { discoverSkills } = await import('./skill-discovery')
|
|
753
|
+
const discovered = discoverSkills({ cwd: session.cwd })
|
|
754
|
+
if (discovered.length > 0) {
|
|
755
|
+
const discoveredBlock = discovered
|
|
756
|
+
.map(s => `- **${s.name}**: ${(s.description || '').slice(0, 120)}`)
|
|
757
|
+
.join('\n')
|
|
758
|
+
stateModifierParts.push(`## Available Skills\n${discoveredBlock}`)
|
|
759
|
+
}
|
|
760
|
+
} catch { /* non-critical */ }
|
|
334
761
|
}
|
|
335
762
|
}
|
|
336
763
|
|
|
337
|
-
|
|
338
|
-
stateModifierParts.push('I\'m here to get things done. I take action, use my tools, and focus on outcomes.')
|
|
339
|
-
}
|
|
764
|
+
// (conciseness and action-orientation are covered in the execution policy below)
|
|
340
765
|
|
|
341
766
|
// Thinking level guidance (applies to all providers via system prompt)
|
|
342
767
|
if (agentThinkingLevel) {
|
|
@@ -349,8 +774,21 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
349
774
|
stateModifierParts.push(`## Reasoning Depth\n${thinkingGuidance[agentThinkingLevel]}`)
|
|
350
775
|
}
|
|
351
776
|
|
|
352
|
-
// Inject
|
|
353
|
-
|
|
777
|
+
// Inject workspace context files only for agents with heartbeat enabled
|
|
778
|
+
// (these files provide goals and autonomous operating context)
|
|
779
|
+
if (!hasProvidedSystemPrompt && agentHeartbeatEnabled) {
|
|
780
|
+
try {
|
|
781
|
+
const { buildWorkspaceContext } = await import('./workspace-context')
|
|
782
|
+
const wsCtx = buildWorkspaceContext({ cwd: session.cwd })
|
|
783
|
+
if (wsCtx.block) stateModifierParts.push(wsCtx.block)
|
|
784
|
+
} catch {
|
|
785
|
+
// Workspace context is non-critical
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Inject agent awareness only if agent has delegation capabilities
|
|
790
|
+
const hasDelegation = sessionPlugins.some(p => p === 'delegate' || p === 'spawn_subagent')
|
|
791
|
+
if (hasDelegation && session.agentId) {
|
|
354
792
|
try {
|
|
355
793
|
const { buildAgentAwarenessBlock } = await import('./agent-registry')
|
|
356
794
|
const awarenessBlock = buildAgentAwarenessBlock(session.agentId)
|
|
@@ -427,21 +865,15 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
427
865
|
}
|
|
428
866
|
}
|
|
429
867
|
|
|
430
|
-
const
|
|
431
|
-
if (enabledButNoAccess.length > 0) {
|
|
432
|
-
parts.push(
|
|
433
|
-
`**Available but not assigned to me:** ${enabledButNoAccess.join(', ')}\n` +
|
|
434
|
-
'I can request access using `manage_capabilities` with action "request_access" or `request_tool_access`.',
|
|
435
|
-
)
|
|
436
|
-
}
|
|
868
|
+
const accessParts: string[] = []
|
|
437
869
|
if (globallyDisabled.length > 0) {
|
|
438
|
-
|
|
870
|
+
accessParts.push(`**Disabled site-wide:** ${globallyDisabled.join(', ')}`)
|
|
439
871
|
}
|
|
440
872
|
if (mcpDisabled.length > 0) {
|
|
441
|
-
|
|
873
|
+
accessParts.push(`**MCP tools not available:** ${mcpDisabled.join(', ')}`)
|
|
442
874
|
}
|
|
443
|
-
if (
|
|
444
|
-
stateModifierParts.push(`## Plugin Access\n${
|
|
875
|
+
if (accessParts.length > 0) {
|
|
876
|
+
stateModifierParts.push(`## Plugin Access\n${accessParts.join('\n')}`)
|
|
445
877
|
}
|
|
446
878
|
}
|
|
447
879
|
|
|
@@ -465,6 +897,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
465
897
|
heartbeatIntervalSec,
|
|
466
898
|
platformAssignScope: agentPlatformAssignScope,
|
|
467
899
|
userMessage: message,
|
|
900
|
+
responseStyle: agentResponseStyle,
|
|
901
|
+
responseMaxChars: agentResponseMaxChars,
|
|
468
902
|
}),
|
|
469
903
|
)
|
|
470
904
|
|
|
@@ -644,13 +1078,16 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
644
1078
|
|
|
645
1079
|
let fullText = ''
|
|
646
1080
|
let lastSegment = ''
|
|
1081
|
+
let lastSettledSegment = ''
|
|
647
1082
|
let hasToolCalls = false
|
|
648
1083
|
let needsTextSeparator = false
|
|
649
1084
|
let totalInputTokens = 0
|
|
650
1085
|
let totalOutputTokens = 0
|
|
651
1086
|
let accumulatedThinking = ''
|
|
652
1087
|
const pluginInvocations: PluginInvocationRecord[] = []
|
|
1088
|
+
const streamedToolEvents: MessageToolEvent[] = []
|
|
653
1089
|
let currentToolInputTokens = 0
|
|
1090
|
+
const boundedExternalExecutionTask = looksLikeBoundedExternalExecutionTask(message)
|
|
654
1091
|
|
|
655
1092
|
// Plugin hooks: beforeAgentStart
|
|
656
1093
|
const pluginMgr = getPluginManager()
|
|
@@ -673,20 +1110,49 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
673
1110
|
const MAX_AUTO_CONTINUES = 3
|
|
674
1111
|
const MAX_TRANSIENT_RETRIES = 2
|
|
675
1112
|
const MAX_REQUIRED_TOOL_CONTINUES = 2
|
|
1113
|
+
const MAX_EXECUTION_FOLLOWTHROUGHS = 1
|
|
1114
|
+
const MAX_DELIVERABLE_FOLLOWTHROUGHS = 2
|
|
1115
|
+
const MAX_TOOL_SUMMARY_RETRIES = 1
|
|
676
1116
|
let autoContinueCount = 0
|
|
677
1117
|
let transientRetryCount = 0
|
|
678
1118
|
let requiredToolContinueCount = 0
|
|
1119
|
+
let executionFollowthroughCount = 0
|
|
1120
|
+
let deliverableFollowthroughCount = 0
|
|
1121
|
+
let toolSummaryRetryCount = 0
|
|
679
1122
|
const explicitRequiredToolNames = getExplicitRequiredToolNames(message, sessionPlugins)
|
|
680
1123
|
const usedToolNames = new Set<string>()
|
|
1124
|
+
const loopTracker = new ToolLoopTracker()
|
|
1125
|
+
let loopDetectionTriggered: LoopDetectionResult | null = null
|
|
1126
|
+
let terminalToolResponse = ''
|
|
681
1127
|
|
|
682
1128
|
try {
|
|
683
|
-
|
|
1129
|
+
const maxIterations = MAX_AUTO_CONTINUES + MAX_TRANSIENT_RETRIES + MAX_REQUIRED_TOOL_CONTINUES + MAX_EXECUTION_FOLLOWTHROUGHS + MAX_DELIVERABLE_FOLLOWTHROUGHS + MAX_TOOL_SUMMARY_RETRIES
|
|
684
1130
|
for (let iteration = 0; iteration <= maxIterations; iteration++) {
|
|
685
|
-
let shouldContinue: 'recursion' | 'transient' | 'required_tool' | false = false
|
|
1131
|
+
let shouldContinue: 'recursion' | 'transient' | 'required_tool' | 'execution_followthrough' | 'deliverable_followthrough' | 'tool_summary' | false = false
|
|
686
1132
|
let requiredToolReminderNames: string[] = []
|
|
687
1133
|
let waitingForToolResult = false
|
|
688
1134
|
let idleTimedOut = false
|
|
1135
|
+
let reachedExecutionBoundary = false
|
|
1136
|
+
let executionFollowthroughReason: 'research_limit' | 'post_simulation' | null = null
|
|
689
1137
|
let idleTimer: ReturnType<typeof setTimeout> | null = null
|
|
1138
|
+
let iterationText = ''
|
|
1139
|
+
const iterationStartState: {
|
|
1140
|
+
fullText: string
|
|
1141
|
+
lastSegment: string
|
|
1142
|
+
lastSettledSegment: string
|
|
1143
|
+
needsTextSeparator: boolean
|
|
1144
|
+
accumulatedThinking: string
|
|
1145
|
+
hasToolCalls: boolean
|
|
1146
|
+
toolEventCount: number
|
|
1147
|
+
} = {
|
|
1148
|
+
fullText,
|
|
1149
|
+
lastSegment,
|
|
1150
|
+
lastSettledSegment,
|
|
1151
|
+
needsTextSeparator,
|
|
1152
|
+
accumulatedThinking,
|
|
1153
|
+
hasToolCalls,
|
|
1154
|
+
toolEventCount: streamedToolEvents.length,
|
|
1155
|
+
}
|
|
690
1156
|
|
|
691
1157
|
// Fresh per-iteration controller so an internal LangGraph abort doesn't poison subsequent iterations.
|
|
692
1158
|
// Linked to the parent so client disconnect / timeout still propagates.
|
|
@@ -711,6 +1177,13 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
711
1177
|
}, 90_000)
|
|
712
1178
|
}
|
|
713
1179
|
|
|
1180
|
+
// Dedup tracking: the tool() wrapper in session-tools/index.ts creates nested
|
|
1181
|
+
// tool invocations. LangGraph's streamEvents v2 emits on_tool_start/on_tool_end
|
|
1182
|
+
// at both the wrapper level and the inner invoke level. We track accepted run_ids
|
|
1183
|
+
// to suppress the duplicate nested events.
|
|
1184
|
+
const acceptedToolRunIds = new Set<string>()
|
|
1185
|
+
const seenToolInputKeys = new Set<string>()
|
|
1186
|
+
|
|
714
1187
|
try {
|
|
715
1188
|
armIdleWatchdog()
|
|
716
1189
|
const eventStream = agent.streamEvents(
|
|
@@ -739,10 +1212,12 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
739
1212
|
} else if (block.text) {
|
|
740
1213
|
if (needsTextSeparator && fullText.length > 0) {
|
|
741
1214
|
fullText += '\n\n'
|
|
1215
|
+
iterationText += '\n\n'
|
|
742
1216
|
write(`data: ${JSON.stringify({ t: 'd', text: '\n\n' })}\n\n`)
|
|
743
1217
|
needsTextSeparator = false
|
|
744
1218
|
}
|
|
745
1219
|
fullText += block.text
|
|
1220
|
+
iterationText += block.text
|
|
746
1221
|
lastSegment += block.text
|
|
747
1222
|
write(`data: ${JSON.stringify({ t: 'd', text: block.text })}\n\n`)
|
|
748
1223
|
}
|
|
@@ -752,10 +1227,12 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
752
1227
|
if (text) {
|
|
753
1228
|
if (needsTextSeparator && fullText.length > 0) {
|
|
754
1229
|
fullText += '\n\n'
|
|
1230
|
+
iterationText += '\n\n'
|
|
755
1231
|
write(`data: ${JSON.stringify({ t: 'd', text: '\n\n' })}\n\n`)
|
|
756
1232
|
needsTextSeparator = false
|
|
757
1233
|
}
|
|
758
1234
|
fullText += text
|
|
1235
|
+
iterationText += text
|
|
759
1236
|
lastSegment += text
|
|
760
1237
|
write(`data: ${JSON.stringify({ t: 'd', text })}\n\n`)
|
|
761
1238
|
}
|
|
@@ -775,16 +1252,36 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
775
1252
|
totalOutputTokens += usage.completionTokens || usage.output_tokens || usage.completion_tokens || 0
|
|
776
1253
|
}
|
|
777
1254
|
} else if (kind === 'on_tool_start') {
|
|
1255
|
+
const toolName = event.name || 'unknown'
|
|
1256
|
+
const input = event.data?.input
|
|
1257
|
+
const inputStr = typeof input === 'string' ? input : JSON.stringify(input)
|
|
1258
|
+
|
|
1259
|
+
// Dedup: skip nested duplicate from tool() wrapper in session-tools.
|
|
1260
|
+
// The wrapper creates a second on_tool_start with the same (name, input)
|
|
1261
|
+
// but a different run_id. We accept the first and reject the rest.
|
|
1262
|
+
const toolDedupeKey = `${toolName}::${inputStr}`
|
|
1263
|
+
if (seenToolInputKeys.has(toolDedupeKey)) {
|
|
1264
|
+
// Nested duplicate — don't emit SSE, don't log, don't track
|
|
1265
|
+
continue
|
|
1266
|
+
}
|
|
1267
|
+
seenToolInputKeys.add(toolDedupeKey)
|
|
1268
|
+
acceptedToolRunIds.add(event.run_id)
|
|
1269
|
+
|
|
778
1270
|
clearIdleWatchdog()
|
|
779
1271
|
waitingForToolResult = true
|
|
780
1272
|
hasToolCalls = true
|
|
781
1273
|
needsTextSeparator = true
|
|
1274
|
+
const settledSegment = extractSuggestions(lastSegment).clean.trim()
|
|
1275
|
+
if (settledSegment) lastSettledSegment = settledSegment
|
|
782
1276
|
lastSegment = ''
|
|
783
|
-
const toolName = event.name || 'unknown'
|
|
784
1277
|
usedToolNames.add(canonicalizePluginId(toolName) || toolName)
|
|
785
|
-
|
|
1278
|
+
// Shell-based HTTP (curl/wget/gh) satisfies research tool requirements —
|
|
1279
|
+
// don't force the agent to also use web_search when shell already fetched the data.
|
|
1280
|
+
if ((canonicalizePluginId(toolName) || toolName) === 'shell' && inputStr) {
|
|
1281
|
+
const cmdMatch = /curl|wget|http|gh\s+(issue|pr|api|repo|release|search|run)/.test(inputStr)
|
|
1282
|
+
if (cmdMatch) usedToolNames.add('web')
|
|
1283
|
+
}
|
|
786
1284
|
// Estimate input tokens for plugin invocation tracking
|
|
787
|
-
const inputStr = typeof input === 'string' ? input : JSON.stringify(input)
|
|
788
1285
|
currentToolInputTokens = Math.ceil((inputStr?.length || 0) / 4)
|
|
789
1286
|
logExecution(session.id, 'tool_call', `${toolName} invoked`, {
|
|
790
1287
|
agentId: session.agentId,
|
|
@@ -794,8 +1291,19 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
794
1291
|
t: 'tool_call',
|
|
795
1292
|
toolName,
|
|
796
1293
|
toolInput: inputStr,
|
|
1294
|
+
toolCallId: event.run_id,
|
|
797
1295
|
})}\n\n`)
|
|
1296
|
+
updateStreamedToolEvents(streamedToolEvents, {
|
|
1297
|
+
type: 'call',
|
|
1298
|
+
name: toolName,
|
|
1299
|
+
input: inputStr,
|
|
1300
|
+
toolCallId: event.run_id,
|
|
1301
|
+
})
|
|
798
1302
|
} else if (kind === 'on_tool_end') {
|
|
1303
|
+
// Dedup: skip on_tool_end for run_ids we didn't accept in on_tool_start
|
|
1304
|
+
if (!acceptedToolRunIds.has(event.run_id)) continue
|
|
1305
|
+
acceptedToolRunIds.delete(event.run_id)
|
|
1306
|
+
|
|
799
1307
|
waitingForToolResult = false
|
|
800
1308
|
armIdleWatchdog()
|
|
801
1309
|
const toolName = event.name || 'unknown'
|
|
@@ -838,11 +1346,89 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
838
1346
|
})
|
|
839
1347
|
currentToolInputTokens = 0
|
|
840
1348
|
|
|
1349
|
+
// --- Tool loop detection (modelled after OpenClaw) ---
|
|
1350
|
+
const loopResult = loopTracker.record(toolName, event.data?.input, output)
|
|
1351
|
+
if (loopResult) {
|
|
1352
|
+
logExecution(session.id, 'loop_detection', loopResult.message, {
|
|
1353
|
+
agentId: session.agentId,
|
|
1354
|
+
detail: { detector: loopResult.detector, severity: loopResult.severity, toolName },
|
|
1355
|
+
})
|
|
1356
|
+
if (loopResult.severity === 'critical') {
|
|
1357
|
+
loopDetectionTriggered = loopResult
|
|
1358
|
+
write(`data: ${JSON.stringify({ t: 'status', text: JSON.stringify({ loopDetection: loopResult.detector, severity: 'critical', message: loopResult.message }) })}\n\n`)
|
|
1359
|
+
break
|
|
1360
|
+
}
|
|
1361
|
+
if (loopResult.severity === 'warning') {
|
|
1362
|
+
write(`data: ${JSON.stringify({ t: 'status', text: JSON.stringify({ loopDetection: loopResult.detector, severity: 'warning', message: loopResult.message }) })}\n\n`)
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
841
1366
|
write(`data: ${JSON.stringify({
|
|
842
1367
|
t: 'tool_result',
|
|
843
1368
|
toolName,
|
|
844
1369
|
toolOutput: outputStr?.slice(0, 2000),
|
|
1370
|
+
toolCallId: event.run_id,
|
|
845
1371
|
})}\n\n`)
|
|
1372
|
+
updateStreamedToolEvents(streamedToolEvents, {
|
|
1373
|
+
type: 'result',
|
|
1374
|
+
name: toolName,
|
|
1375
|
+
output: outputStr,
|
|
1376
|
+
toolCallId: event.run_id,
|
|
1377
|
+
})
|
|
1378
|
+
if (shouldTerminateOnSuccessfulMemoryMutation({
|
|
1379
|
+
toolName,
|
|
1380
|
+
toolInput: event.data?.input,
|
|
1381
|
+
toolOutput: outputStr || '',
|
|
1382
|
+
})) {
|
|
1383
|
+
terminalToolResponse = extractSuggestions(outputStr || '').clean.trim()
|
|
1384
|
+
if (terminalToolResponse) {
|
|
1385
|
+
lastSegment = terminalToolResponse
|
|
1386
|
+
lastSettledSegment = terminalToolResponse
|
|
1387
|
+
}
|
|
1388
|
+
logExecution(session.id, 'decision', 'Successful memory write is terminal for this turn.', {
|
|
1389
|
+
agentId: session.agentId,
|
|
1390
|
+
detail: { toolName, action: resolveToolAction(event.data?.input) || null },
|
|
1391
|
+
})
|
|
1392
|
+
write(`data: ${JSON.stringify({
|
|
1393
|
+
t: 'status',
|
|
1394
|
+
text: JSON.stringify({ terminalToolResult: 'memory_write' }),
|
|
1395
|
+
})}\n\n`)
|
|
1396
|
+
break
|
|
1397
|
+
}
|
|
1398
|
+
if (boundedExternalExecutionTask && getWalletApprovalBoundaryAction(outputStr || '')) {
|
|
1399
|
+
reachedExecutionBoundary = true
|
|
1400
|
+
write(`data: ${JSON.stringify({
|
|
1401
|
+
t: 'status',
|
|
1402
|
+
text: JSON.stringify({ executionBoundary: 'wallet_approval' }),
|
|
1403
|
+
})}\n\n`)
|
|
1404
|
+
break
|
|
1405
|
+
}
|
|
1406
|
+
if (
|
|
1407
|
+
boundedExternalExecutionTask
|
|
1408
|
+
&& ['http_request', 'web', 'web_search', 'web_fetch', 'browser'].includes(toolName)
|
|
1409
|
+
&& !hasStateChangingWalletEvidence(streamedToolEvents)
|
|
1410
|
+
&& countExternalExecutionResearchSteps(streamedToolEvents) >= 5
|
|
1411
|
+
&& countDistinctExternalResearchHosts(streamedToolEvents) >= 3
|
|
1412
|
+
) {
|
|
1413
|
+
executionFollowthroughReason = 'research_limit'
|
|
1414
|
+
write(`data: ${JSON.stringify({
|
|
1415
|
+
t: 'status',
|
|
1416
|
+
text: JSON.stringify({ executionBoundary: 'research_limit' }),
|
|
1417
|
+
})}\n\n`)
|
|
1418
|
+
break
|
|
1419
|
+
}
|
|
1420
|
+
if (
|
|
1421
|
+
boundedExternalExecutionTask
|
|
1422
|
+
&& !hasStateChangingWalletEvidence(streamedToolEvents)
|
|
1423
|
+
&& isWalletSimulationResult(toolName, outputStr || '')
|
|
1424
|
+
) {
|
|
1425
|
+
executionFollowthroughReason = 'post_simulation'
|
|
1426
|
+
write(`data: ${JSON.stringify({
|
|
1427
|
+
t: 'status',
|
|
1428
|
+
text: JSON.stringify({ executionBoundary: 'post_simulation' }),
|
|
1429
|
+
})}\n\n`)
|
|
1430
|
+
break
|
|
1431
|
+
}
|
|
846
1432
|
}
|
|
847
1433
|
}
|
|
848
1434
|
} catch (innerErr: unknown) {
|
|
@@ -879,12 +1465,25 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
879
1465
|
})
|
|
880
1466
|
write(`data: ${JSON.stringify({ t: 'status', text: JSON.stringify({ autoContinue: autoContinueCount, maxContinues: MAX_AUTO_CONTINUES }) })}\n\n`)
|
|
881
1467
|
} else if (isTransientAbort && transientRetryCount < MAX_TRANSIENT_RETRIES && !abortController.signal.aborted) {
|
|
1468
|
+
// Reset client-side accumulated state — partial text/tool events from the
|
|
1469
|
+
// failed iteration can't be un-sent, so tell the client to clear them.
|
|
1470
|
+
const hadPartialOutput = iterationText.length > 0 || streamedToolEvents.length > iterationStartState.toolEventCount
|
|
1471
|
+
fullText = iterationStartState.fullText
|
|
1472
|
+
lastSegment = iterationStartState.lastSegment
|
|
1473
|
+
lastSettledSegment = iterationStartState.lastSettledSegment
|
|
1474
|
+
needsTextSeparator = iterationStartState.needsTextSeparator
|
|
1475
|
+
accumulatedThinking = iterationStartState.accumulatedThinking
|
|
1476
|
+
hasToolCalls = iterationStartState.hasToolCalls
|
|
1477
|
+
streamedToolEvents.length = iterationStartState.toolEventCount
|
|
882
1478
|
shouldContinue = 'transient'
|
|
883
1479
|
transientRetryCount++
|
|
884
1480
|
logExecution(session.id, 'decision', `Transient error, retrying (${transientRetryCount}/${MAX_TRANSIENT_RETRIES}): ${errMsg}`, {
|
|
885
1481
|
agentId: session.agentId,
|
|
886
|
-
detail: { errName, errMsg },
|
|
1482
|
+
detail: { errName, errMsg, hadPartialOutput },
|
|
887
1483
|
})
|
|
1484
|
+
if (hadPartialOutput) {
|
|
1485
|
+
write(`data: ${JSON.stringify({ t: 'reset', text: iterationStartState.fullText })}\n\n`)
|
|
1486
|
+
}
|
|
888
1487
|
write(`data: ${JSON.stringify({ t: 'status', text: JSON.stringify({ transientRetry: transientRetryCount, maxRetries: MAX_TRANSIENT_RETRIES, error: errMsg }) })}\n\n`)
|
|
889
1488
|
} else {
|
|
890
1489
|
// Non-retryable error or exhausted retries — rethrow to outer catch
|
|
@@ -895,8 +1494,38 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
895
1494
|
abortController.signal.removeEventListener('abort', onParentAbort)
|
|
896
1495
|
}
|
|
897
1496
|
|
|
1497
|
+
if (reachedExecutionBoundary) break
|
|
1498
|
+
|
|
1499
|
+
// Tool loop detection: critical severity stops the entire agent turn
|
|
1500
|
+
if (loopDetectionTriggered) {
|
|
1501
|
+
write(`data: ${JSON.stringify({ t: 'err', text: loopDetectionTriggered.message })}\n\n`)
|
|
1502
|
+
break
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
if (
|
|
1506
|
+
executionFollowthroughReason
|
|
1507
|
+
&& !shouldContinue
|
|
1508
|
+
&& executionFollowthroughCount < MAX_EXECUTION_FOLLOWTHROUGHS
|
|
1509
|
+
) {
|
|
1510
|
+
shouldContinue = 'execution_followthrough'
|
|
1511
|
+
executionFollowthroughCount++
|
|
1512
|
+
write(`data: ${JSON.stringify({
|
|
1513
|
+
t: 'status',
|
|
1514
|
+
text: JSON.stringify({
|
|
1515
|
+
externalExecutionFollowthrough: executionFollowthroughCount,
|
|
1516
|
+
maxFollowthroughs: MAX_EXECUTION_FOLLOWTHROUGHS,
|
|
1517
|
+
reason: executionFollowthroughReason,
|
|
1518
|
+
}),
|
|
1519
|
+
})}\n\n`)
|
|
1520
|
+
}
|
|
1521
|
+
|
|
898
1522
|
if (!shouldContinue && explicitRequiredToolNames.length > 0 && requiredToolContinueCount < MAX_REQUIRED_TOOL_CONTINUES) {
|
|
899
|
-
|
|
1523
|
+
// Canonicalize required tool names before comparing — tool planning uses
|
|
1524
|
+
// alias names (e.g. web_search) while LangGraph emits canonical names (e.g. web).
|
|
1525
|
+
requiredToolReminderNames = explicitRequiredToolNames.filter((toolName) => {
|
|
1526
|
+
const canonical = canonicalizePluginId(toolName) || toolName
|
|
1527
|
+
return !usedToolNames.has(toolName) && !usedToolNames.has(canonical)
|
|
1528
|
+
})
|
|
900
1529
|
if (requiredToolReminderNames.length > 0) {
|
|
901
1530
|
shouldContinue = 'required_tool'
|
|
902
1531
|
requiredToolContinueCount++
|
|
@@ -911,23 +1540,153 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
911
1540
|
}
|
|
912
1541
|
}
|
|
913
1542
|
|
|
1543
|
+
if (!shouldContinue
|
|
1544
|
+
&& executionFollowthroughCount < MAX_EXECUTION_FOLLOWTHROUGHS
|
|
1545
|
+
&& shouldForceExternalExecutionFollowthrough({
|
|
1546
|
+
userMessage: message,
|
|
1547
|
+
finalResponse: resolveFinalStreamResponseText({
|
|
1548
|
+
fullText,
|
|
1549
|
+
lastSegment,
|
|
1550
|
+
lastSettledSegment,
|
|
1551
|
+
hasToolCalls,
|
|
1552
|
+
toolEvents: streamedToolEvents,
|
|
1553
|
+
}),
|
|
1554
|
+
hasToolCalls,
|
|
1555
|
+
toolEvents: streamedToolEvents,
|
|
1556
|
+
})) {
|
|
1557
|
+
shouldContinue = 'execution_followthrough'
|
|
1558
|
+
executionFollowthroughCount++
|
|
1559
|
+
write(`data: ${JSON.stringify({
|
|
1560
|
+
t: 'status',
|
|
1561
|
+
text: JSON.stringify({
|
|
1562
|
+
externalExecutionFollowthrough: executionFollowthroughCount,
|
|
1563
|
+
maxFollowthroughs: MAX_EXECUTION_FOLLOWTHROUGHS,
|
|
1564
|
+
}),
|
|
1565
|
+
})}\n\n`)
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
if (!shouldContinue
|
|
1569
|
+
&& deliverableFollowthroughCount < MAX_DELIVERABLE_FOLLOWTHROUGHS
|
|
1570
|
+
&& shouldForceDeliverableFollowthrough({
|
|
1571
|
+
userMessage: message,
|
|
1572
|
+
finalResponse: resolveFinalStreamResponseText({
|
|
1573
|
+
fullText,
|
|
1574
|
+
lastSegment,
|
|
1575
|
+
lastSettledSegment,
|
|
1576
|
+
hasToolCalls,
|
|
1577
|
+
toolEvents: streamedToolEvents,
|
|
1578
|
+
}),
|
|
1579
|
+
hasToolCalls,
|
|
1580
|
+
toolEvents: streamedToolEvents,
|
|
1581
|
+
})) {
|
|
1582
|
+
shouldContinue = 'deliverable_followthrough'
|
|
1583
|
+
deliverableFollowthroughCount++
|
|
1584
|
+
write(`data: ${JSON.stringify({
|
|
1585
|
+
t: 'status',
|
|
1586
|
+
text: JSON.stringify({
|
|
1587
|
+
deliverableFollowthrough: deliverableFollowthroughCount,
|
|
1588
|
+
maxFollowthroughs: MAX_DELIVERABLE_FOLLOWTHROUGHS,
|
|
1589
|
+
}),
|
|
1590
|
+
})}\n\n`)
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// Generic fallback: tools were called but the model produced no text response.
|
|
1594
|
+
// This catches edge cases (e.g. after transient retry) where specialized
|
|
1595
|
+
// followthrough conditions don't match. Ask the LLM to summarize tool results.
|
|
1596
|
+
if (
|
|
1597
|
+
!shouldContinue
|
|
1598
|
+
&& hasToolCalls
|
|
1599
|
+
&& !fullText.trim()
|
|
1600
|
+
&& streamedToolEvents.length > 0
|
|
1601
|
+
&& toolSummaryRetryCount < MAX_TOOL_SUMMARY_RETRIES
|
|
1602
|
+
) {
|
|
1603
|
+
shouldContinue = 'tool_summary'
|
|
1604
|
+
toolSummaryRetryCount++
|
|
1605
|
+
logExecution(session.id, 'decision', `Tools called but no text generated — forcing summary continuation`, {
|
|
1606
|
+
agentId: session.agentId,
|
|
1607
|
+
detail: { toolEventCount: streamedToolEvents.length, toolSummaryRetryCount },
|
|
1608
|
+
})
|
|
1609
|
+
write(`data: ${JSON.stringify({
|
|
1610
|
+
t: 'status',
|
|
1611
|
+
text: JSON.stringify({ toolSummary: toolSummaryRetryCount, reason: 'empty_response_after_tools' }),
|
|
1612
|
+
})}\n\n`)
|
|
1613
|
+
}
|
|
1614
|
+
|
|
914
1615
|
if (!shouldContinue) break
|
|
915
1616
|
|
|
1617
|
+
const continuationAssistantText = resolveContinuationAssistantText({
|
|
1618
|
+
iterationText,
|
|
1619
|
+
lastSegment,
|
|
1620
|
+
})
|
|
1621
|
+
|
|
916
1622
|
if (shouldContinue === 'recursion') {
|
|
917
|
-
//
|
|
918
|
-
|
|
919
|
-
|
|
1623
|
+
// Continue with only the newly produced assistant text from this
|
|
1624
|
+
// iteration, not the cumulative full transcript, or the model tends to
|
|
1625
|
+
// restart from earlier paragraphs on later followthrough turns.
|
|
1626
|
+
if (continuationAssistantText) {
|
|
1627
|
+
langchainMessages.push(new AIMessage({ content: continuationAssistantText }))
|
|
920
1628
|
}
|
|
1629
|
+
const settledSegment = extractSuggestions(lastSegment).clean.trim()
|
|
1630
|
+
if (settledSegment) lastSettledSegment = settledSegment
|
|
921
1631
|
langchainMessages.push(new HumanMessage({ content: 'Continue where you left off. Complete the remaining steps of the objective.' }))
|
|
922
1632
|
lastSegment = ''
|
|
923
1633
|
} else if (shouldContinue === 'required_tool') {
|
|
924
|
-
if (
|
|
925
|
-
langchainMessages.push(new AIMessage({ content:
|
|
1634
|
+
if (continuationAssistantText) {
|
|
1635
|
+
langchainMessages.push(new AIMessage({ content: continuationAssistantText }))
|
|
926
1636
|
}
|
|
1637
|
+
const settledSegment = extractSuggestions(lastSegment).clean.trim()
|
|
1638
|
+
if (settledSegment) lastSettledSegment = settledSegment
|
|
927
1639
|
langchainMessages.push(new HumanMessage({
|
|
928
1640
|
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.`,
|
|
929
1641
|
}))
|
|
930
1642
|
lastSegment = ''
|
|
1643
|
+
} else if (shouldContinue === 'execution_followthrough') {
|
|
1644
|
+
if (continuationAssistantText) {
|
|
1645
|
+
langchainMessages.push(new AIMessage({ content: continuationAssistantText }))
|
|
1646
|
+
}
|
|
1647
|
+
const settledSegment = extractSuggestions(lastSegment).clean.trim()
|
|
1648
|
+
if (settledSegment) lastSettledSegment = settledSegment
|
|
1649
|
+
langchainMessages.push(new HumanMessage({
|
|
1650
|
+
content: buildExternalExecutionFollowthroughPrompt({
|
|
1651
|
+
userMessage: message,
|
|
1652
|
+
fullText,
|
|
1653
|
+
toolEvents: streamedToolEvents,
|
|
1654
|
+
}),
|
|
1655
|
+
}))
|
|
1656
|
+
lastSegment = ''
|
|
1657
|
+
} else if (shouldContinue === 'deliverable_followthrough') {
|
|
1658
|
+
if (continuationAssistantText) {
|
|
1659
|
+
langchainMessages.push(new AIMessage({ content: continuationAssistantText }))
|
|
1660
|
+
}
|
|
1661
|
+
const settledSegment = extractSuggestions(lastSegment).clean.trim()
|
|
1662
|
+
if (settledSegment) lastSettledSegment = settledSegment
|
|
1663
|
+
langchainMessages.push(new HumanMessage({
|
|
1664
|
+
content: buildDeliverableFollowthroughPrompt({
|
|
1665
|
+
userMessage: message,
|
|
1666
|
+
fullText,
|
|
1667
|
+
toolEvents: streamedToolEvents,
|
|
1668
|
+
}),
|
|
1669
|
+
}))
|
|
1670
|
+
lastSegment = ''
|
|
1671
|
+
} else if (shouldContinue === 'tool_summary') {
|
|
1672
|
+
// Model called tools but produced no text — prompt it to summarize the results.
|
|
1673
|
+
if (continuationAssistantText) {
|
|
1674
|
+
langchainMessages.push(new AIMessage({ content: continuationAssistantText }))
|
|
1675
|
+
}
|
|
1676
|
+
const toolSummaryLines = streamedToolEvents
|
|
1677
|
+
.filter((e) => e.output)
|
|
1678
|
+
.map((e) => `[${e.name}]: ${(e.output || '').slice(0, 500)}`)
|
|
1679
|
+
.slice(0, 6)
|
|
1680
|
+
langchainMessages.push(new HumanMessage({
|
|
1681
|
+
content: [
|
|
1682
|
+
'Your tool calls completed but you did not provide a response.',
|
|
1683
|
+
'Here are the tool results:',
|
|
1684
|
+
...toolSummaryLines,
|
|
1685
|
+
'',
|
|
1686
|
+
'Now answer the original question using these results. Be concise and direct.',
|
|
1687
|
+
].join('\n'),
|
|
1688
|
+
}))
|
|
1689
|
+
lastSegment = ''
|
|
931
1690
|
} else if (shouldContinue === 'transient') {
|
|
932
1691
|
// Short delay before retrying transient errors (API timeout, rate limit, etc.)
|
|
933
1692
|
await new Promise((r) => setTimeout(r, 2000 * transientRetryCount))
|
|
@@ -959,13 +1718,38 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
959
1718
|
|
|
960
1719
|
// Skip post-stream work if the client disconnected mid-stream
|
|
961
1720
|
if (signal?.aborted) {
|
|
1721
|
+
let finalResponse = resolveFinalStreamResponseText({
|
|
1722
|
+
fullText,
|
|
1723
|
+
lastSegment,
|
|
1724
|
+
lastSettledSegment,
|
|
1725
|
+
hasToolCalls,
|
|
1726
|
+
toolEvents: streamedToolEvents,
|
|
1727
|
+
})
|
|
1728
|
+
if (shouldForceExternalServiceSummary({
|
|
1729
|
+
userMessage: message,
|
|
1730
|
+
finalResponse,
|
|
1731
|
+
hasToolCalls,
|
|
1732
|
+
toolEventCount: streamedToolEvents.length,
|
|
1733
|
+
})) {
|
|
1734
|
+
const forcedSummary = await buildForcedExternalServiceSummary({
|
|
1735
|
+
llm,
|
|
1736
|
+
userMessage: message,
|
|
1737
|
+
fullText,
|
|
1738
|
+
toolEvents: streamedToolEvents,
|
|
1739
|
+
})
|
|
1740
|
+
if (forcedSummary) {
|
|
1741
|
+
fullText = fullText.trim() ? `${fullText.trim()}\n\n${forcedSummary}` : forcedSummary
|
|
1742
|
+
finalResponse = forcedSummary
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
962
1745
|
await cleanup()
|
|
963
|
-
return { fullText, finalResponse
|
|
1746
|
+
return { fullText, finalResponse }
|
|
964
1747
|
}
|
|
965
1748
|
|
|
966
1749
|
// Extract LLM-generated suggestions from the response and strip the tag
|
|
967
1750
|
const extracted = extractSuggestions(fullText)
|
|
968
1751
|
fullText = extracted.clean
|
|
1752
|
+
if (!fullText.trim() && terminalToolResponse) fullText = terminalToolResponse
|
|
969
1753
|
if (extracted.suggestions) {
|
|
970
1754
|
write(`data: ${JSON.stringify({ t: 'md', text: JSON.stringify({ suggestions: extracted.suggestions }) })}\n\n`)
|
|
971
1755
|
}
|
|
@@ -1008,6 +1792,35 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
1008
1792
|
})}\n\n`)
|
|
1009
1793
|
}
|
|
1010
1794
|
|
|
1795
|
+
// If tools were called, finalResponse is the text from the last LLM turn only.
|
|
1796
|
+
// Fall back to fullText if the last segment is empty (e.g. agent ended on a tool call
|
|
1797
|
+
// with no summary text).
|
|
1798
|
+
// Strip suggestions tag from lastSegment too (connector delivery)
|
|
1799
|
+
let finalResponse = resolveFinalStreamResponseText({
|
|
1800
|
+
fullText,
|
|
1801
|
+
lastSegment,
|
|
1802
|
+
lastSettledSegment,
|
|
1803
|
+
hasToolCalls,
|
|
1804
|
+
toolEvents: streamedToolEvents,
|
|
1805
|
+
})
|
|
1806
|
+
if (shouldForceExternalServiceSummary({
|
|
1807
|
+
userMessage: message,
|
|
1808
|
+
finalResponse,
|
|
1809
|
+
hasToolCalls,
|
|
1810
|
+
toolEventCount: streamedToolEvents.length,
|
|
1811
|
+
})) {
|
|
1812
|
+
const forcedSummary = await buildForcedExternalServiceSummary({
|
|
1813
|
+
llm,
|
|
1814
|
+
userMessage: message,
|
|
1815
|
+
fullText,
|
|
1816
|
+
toolEvents: streamedToolEvents,
|
|
1817
|
+
})
|
|
1818
|
+
if (forcedSummary) {
|
|
1819
|
+
fullText = fullText.trim() ? `${fullText.trim()}\n\n${forcedSummary}` : forcedSummary
|
|
1820
|
+
finalResponse = forcedSummary
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1011
1824
|
// Plugin hooks: afterAgentComplete
|
|
1012
1825
|
await pluginMgr.runHook('afterAgentComplete', { session, response: fullText }, { enabledIds: sessionPlugins })
|
|
1013
1826
|
|
|
@@ -1023,14 +1836,5 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
1023
1836
|
// Clean up browser and other session resources
|
|
1024
1837
|
await cleanup()
|
|
1025
1838
|
|
|
1026
|
-
// If tools were called, finalResponse is the text from the last LLM turn only.
|
|
1027
|
-
// Fall back to fullText if the last segment is empty (e.g. agent ended on a tool call
|
|
1028
|
-
// with no summary text).
|
|
1029
|
-
// Strip suggestions tag from lastSegment too (connector delivery)
|
|
1030
|
-
const cleanLastSegment = extractSuggestions(lastSegment).clean
|
|
1031
|
-
const finalResponse = hasToolCalls
|
|
1032
|
-
? (cleanLastSegment.trim() || fullText)
|
|
1033
|
-
: fullText
|
|
1034
|
-
|
|
1035
1839
|
return { fullText, finalResponse }
|
|
1036
1840
|
}
|