@swarmclawai/swarmclaw 0.7.7 → 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 -14
- 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 +23 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +46 -3
- package/src/app/api/connectors/route.ts +12 -8
- 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/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- 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/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- 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 +257 -38
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +48 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- 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/layout/app-layout.tsx +40 -23
- 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/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- 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 +45 -3
- package/src/components/shared/settings/section-voice.tsx +11 -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 +289 -34
- package/src/components/tasks/task-board.tsx +410 -25
- package/src/components/tasks/task-card.tsx +66 -8
- package/src/components/tasks/task-sheet.tsx +16 -4
- 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 +33 -0
- package/src/lib/server/capability-router.ts +80 -19
- 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 +378 -73
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +461 -137
- 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 +84 -47
- 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 +247 -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 +20 -11
- 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/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- 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 +3 -2
- package/src/lib/server/orchestrator.ts +2 -0
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +211 -6
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +409 -2
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +527 -68
- 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 +83 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +366 -54
- package/src/lib/server/session-tools/context.ts +17 -3
- package/src/lib/server/session-tools/crud.ts +484 -84
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +102 -10
- 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/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +554 -75
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- 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 +178 -0
- package/src/lib/server/session-tools/web.ts +621 -70
- 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 +437 -2
- package/src/lib/server/stream-agent-chat.ts +957 -79
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- 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.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +271 -0
- 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-definitions.ts +2 -1
- 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 +249 -14
|
@@ -8,14 +8,24 @@ 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
|
-
import { expandPluginIds } from './tool-aliases'
|
|
15
|
-
import type { Session, Message, UsageRecord, PluginInvocationRecord } from '@/types'
|
|
15
|
+
import { canonicalizePluginId, expandPluginIds } from './tool-aliases'
|
|
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'
|
|
20
|
+
import { resolveActiveProjectContext } from './project-context'
|
|
21
|
+
import {
|
|
22
|
+
getEnabledToolPlanningView,
|
|
23
|
+
getFirstToolForCapability,
|
|
24
|
+
getToolsForCapability,
|
|
25
|
+
TOOL_CAPABILITY,
|
|
26
|
+
} from './tool-planning'
|
|
27
|
+
import { ToolLoopTracker } from './tool-loop-detection'
|
|
28
|
+
import type { LoopDetectionResult } from './tool-loop-detection'
|
|
19
29
|
|
|
20
30
|
/** Extract a breadcrumb title from notable tool completions (task/schedule/agent creation). */
|
|
21
31
|
interface StreamAgentChatOpts {
|
|
@@ -46,8 +56,11 @@ function buildPluginCapabilityLines(enabledPlugins: string[], opts?: { platformA
|
|
|
46
56
|
}
|
|
47
57
|
|
|
48
58
|
export function buildToolDisciplineLines(enabledPlugins: string[]): string[] {
|
|
49
|
-
const
|
|
59
|
+
const planning = getEnabledToolPlanningView(enabledPlugins)
|
|
60
|
+
const uniqueTools = planning.displayToolIds
|
|
50
61
|
if (uniqueTools.length === 0) return []
|
|
62
|
+
const walletTools = getToolsForCapability(enabledPlugins, TOOL_CAPABILITY.walletInspect)
|
|
63
|
+
const httpTools = getToolsForCapability(enabledPlugins, 'network.http')
|
|
51
64
|
|
|
52
65
|
const lines = [
|
|
53
66
|
`Enabled tools in this session: ${uniqueTools.map((toolId) => `\`${toolId}\``).join(', ')}.`,
|
|
@@ -59,33 +72,63 @@ export function buildToolDisciplineLines(enabledPlugins: string[]): string[] {
|
|
|
59
72
|
lines.push(`Use direct platform tools exactly as named (${directPlatformTools.map((toolId) => `\`${toolId}\``).join(', ')}). Do not substitute \`manage_platform\` unless it is explicitly enabled.`)
|
|
60
73
|
}
|
|
61
74
|
|
|
62
|
-
|
|
63
|
-
|
|
75
|
+
lines.push(...planning.disciplineGuidance)
|
|
76
|
+
|
|
77
|
+
const researchSearchTools = getToolsForCapability(enabledPlugins, TOOL_CAPABILITY.researchSearch)
|
|
78
|
+
const researchFetchTools = getToolsForCapability(enabledPlugins, TOOL_CAPABILITY.researchFetch)
|
|
79
|
+
const browserCaptureTools = getToolsForCapability(enabledPlugins, TOOL_CAPABILITY.browserCapture)
|
|
80
|
+
const deliveryMediaTools = getToolsForCapability(enabledPlugins, TOOL_CAPABILITY.deliveryMedia)
|
|
81
|
+
const deliveryVoiceTools = getToolsForCapability(enabledPlugins, TOOL_CAPABILITY.deliveryVoiceNote)
|
|
82
|
+
|
|
83
|
+
if ((researchSearchTools.length || researchFetchTools.length) && browserCaptureTools.length) {
|
|
84
|
+
const researchLabel = [...researchSearchTools, ...researchFetchTools].map((toolName) => `\`${toolName}\``).join('/')
|
|
85
|
+
lines.push(`Research tools like ${researchLabel} gather sources and text, but they do not capture screenshots. Use \`${browserCaptureTools[0]}\` for screenshots or rendered page evidence.`)
|
|
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.`)
|
|
64
87
|
}
|
|
65
88
|
|
|
66
|
-
if (
|
|
67
|
-
lines.push(
|
|
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.`)
|
|
68
91
|
}
|
|
69
92
|
|
|
70
|
-
if (
|
|
71
|
-
lines.push(
|
|
93
|
+
if (browserCaptureTools.length && deliveryMediaTools.length) {
|
|
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.`)
|
|
72
95
|
}
|
|
73
96
|
|
|
74
|
-
if (
|
|
75
|
-
lines.push(
|
|
76
|
-
lines.push('For `browser` form work, prefer `{"action":"fill_form","fields":[{"element":"#email","value":"user@example.com"},{"element":"#password","value":"..."}]}`. A shorthand `form` object keyed by input id/name also works for simple forms.')
|
|
97
|
+
if (deliveryVoiceTools.length) {
|
|
98
|
+
lines.push(`If the user asks for a voice note and \`${deliveryVoiceTools[0]}\` is enabled, try it before saying voice notes are unsupported.`)
|
|
77
99
|
}
|
|
78
100
|
|
|
79
|
-
if (uniqueTools.includes('
|
|
80
|
-
lines.push(
|
|
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.')
|
|
81
105
|
}
|
|
82
106
|
|
|
83
|
-
if (uniqueTools.includes('
|
|
84
|
-
lines.push('For
|
|
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.')
|
|
85
110
|
}
|
|
86
111
|
|
|
87
112
|
if (uniqueTools.includes('ask_human')) {
|
|
88
|
-
lines.push('For `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`.')
|
|
114
|
+
}
|
|
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.')
|
|
120
|
+
}
|
|
121
|
+
|
|
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.')
|
|
124
|
+
}
|
|
125
|
+
|
|
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.')
|
|
128
|
+
}
|
|
129
|
+
|
|
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.')
|
|
89
132
|
}
|
|
90
133
|
|
|
91
134
|
return lines
|
|
@@ -94,15 +137,46 @@ export function buildToolDisciplineLines(enabledPlugins: string[]): string[] {
|
|
|
94
137
|
export function looksLikeOpenEndedDeliverableTask(text: string): boolean {
|
|
95
138
|
const normalized = text.toLowerCase()
|
|
96
139
|
if (!normalized.trim()) return false
|
|
97
|
-
if (/```|package\.json|tsconfig
|
|
140
|
+
if (/```|package\.json|tsconfig|\btsx?\b|\bjsx?\b|pytest|vitest|npm run|src\/|components\/|api\//.test(normalized)) return false
|
|
98
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
|
|
99
|
-
|
|
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
|
|
155
|
+
}
|
|
156
|
+
return isBroadGoal(text) && /(\.md\b|\.txt\b|\.html\b|\.json\b|copy|brief|proposal|plan|report|draft|document|dashboard)/.test(normalized)
|
|
100
157
|
}
|
|
101
158
|
|
|
102
|
-
|
|
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[] {
|
|
103
176
|
const normalized = userMessage.toLowerCase()
|
|
104
177
|
const required: string[] = []
|
|
105
178
|
|
|
179
|
+
// Only force tools that the user explicitly names and the LLM tends to skip
|
|
106
180
|
if (enabledPlugins.includes('ask_human')
|
|
107
181
|
&& (/\bask_human\b/.test(normalized) || /ask the human/.test(normalized) || /request_input/.test(normalized))) {
|
|
108
182
|
required.push('ask_human')
|
|
@@ -124,6 +198,320 @@ const OPEN_ENDED_REVISION_BLOCK = [
|
|
|
124
198
|
'If `files` is available, use it with explicit actions and paths to inspect and revise the artifacts.',
|
|
125
199
|
].join('\n')
|
|
126
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
|
+
|
|
127
515
|
/** Detect whether a user message is a broad, high-level goal that benefits from decomposition. */
|
|
128
516
|
function isBroadGoal(text: string): boolean {
|
|
129
517
|
if (text.length < 50) return false
|
|
@@ -140,10 +528,10 @@ const GOAL_DECOMPOSITION_BLOCK = [
|
|
|
140
528
|
'## Goal Decomposition',
|
|
141
529
|
'When you receive a broad, open-ended goal:',
|
|
142
530
|
'1. Break it into 3-7 concrete, sequentially-executable subtasks before taking action.',
|
|
143
|
-
'2. If manage_tasks is available,
|
|
144
|
-
'3. Present the plan as a short checklist or numbered list in plain language.',
|
|
145
|
-
'4. Execute the first subtask immediately — do not stop after planning.',
|
|
146
|
-
'5.
|
|
531
|
+
'2. If manage_tasks is available, use it only for durable tracking: multi-turn work, delegation, explicit backlog requests, or work you expect to resume later. Do not create a task for every micro-step.',
|
|
532
|
+
'3. Present the plan as a short checklist or numbered list in plain language. If durable tracking is unnecessary, keep it inline instead of creating tasks.',
|
|
533
|
+
'4. Execute the first substantive subtask immediately — do not stop after planning.',
|
|
534
|
+
'5. Update only the durable tasks you actually created; otherwise just continue executing and report progress plainly.',
|
|
147
535
|
].join('\n')
|
|
148
536
|
|
|
149
537
|
function buildAgenticExecutionPolicy(opts: {
|
|
@@ -153,6 +541,8 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
153
541
|
heartbeatIntervalSec: number
|
|
154
542
|
platformAssignScope?: 'self' | 'all'
|
|
155
543
|
userMessage?: string
|
|
544
|
+
responseStyle?: 'concise' | 'normal' | 'detailed' | null
|
|
545
|
+
responseMaxChars?: number | null
|
|
156
546
|
}) {
|
|
157
547
|
const hasTooling = opts.enabledPlugins.length > 0
|
|
158
548
|
const pluginLines = buildPluginCapabilityLines(opts.enabledPlugins, { platformAssignScope: opts.platformAssignScope })
|
|
@@ -166,9 +556,12 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
166
556
|
hasTooling
|
|
167
557
|
? 'I take initiative — plan briefly, execute tools, evaluate, iterate until done. Never stop at advice when action is implied.'
|
|
168
558
|
: 'No tools enabled. Be explicit about what tool access is needed.',
|
|
169
|
-
'
|
|
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.',
|
|
170
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.',
|
|
171
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.',
|
|
172
565
|
opts.loopMode === 'ongoing'
|
|
173
566
|
? 'Loop: ONGOING — keep iterating until done, blocked, or limits reached.'
|
|
174
567
|
: 'Loop: BOUNDED — execute multiple steps but finish within recursion budget.',
|
|
@@ -181,16 +574,15 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
181
574
|
// Response behavior
|
|
182
575
|
parts.push(
|
|
183
576
|
'## Response Rules',
|
|
184
|
-
'NO_MESSAGE: reply with exactly this
|
|
185
|
-
'
|
|
186
|
-
'
|
|
187
|
-
'
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
'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
|
+
: '',
|
|
194
586
|
`Heartbeat: if message is "${opts.heartbeatPrompt}", reply "HEARTBEAT_OK" unless you have a progress update.`,
|
|
195
587
|
opts.heartbeatIntervalSec > 0 ? `Heartbeat cadence: ~${opts.heartbeatIntervalSec}s.` : '',
|
|
196
588
|
)
|
|
@@ -198,6 +590,10 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
198
590
|
if (toolDisciplineLines.length) parts.push('## Tool Discipline', ...toolDisciplineLines)
|
|
199
591
|
if (pluginLines.length) parts.push('What I can do:\n' + pluginLines.join('\n'))
|
|
200
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
|
+
}
|
|
201
597
|
if (opts.userMessage && looksLikeOpenEndedDeliverableTask(opts.userMessage) && opts.enabledPlugins.some((toolId) => toolId === 'files' || toolId === 'edit_file')) {
|
|
202
598
|
parts.push(OPEN_ENDED_REVISION_BLOCK)
|
|
203
599
|
}
|
|
@@ -213,6 +609,52 @@ export interface StreamAgentChatResult {
|
|
|
213
609
|
finalResponse: string
|
|
214
610
|
}
|
|
215
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
|
+
|
|
216
658
|
export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<StreamAgentChatResult> {
|
|
217
659
|
const startTs = Date.now()
|
|
218
660
|
const { session, message, imagePath, attachedFiles, apiKey, systemPrompt, write, history, fallbackCredentialIds, signal } = opts
|
|
@@ -275,6 +717,10 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
275
717
|
let agentMcpServerIds: string[] | undefined
|
|
276
718
|
let agentMcpDisabledTools: string[] | undefined
|
|
277
719
|
let agentHeartbeatEnabled = false
|
|
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
|
|
723
|
+
const activeProjectContext = resolveActiveProjectContext(session)
|
|
278
724
|
if (session.agentId) {
|
|
279
725
|
const agents = loadAgents()
|
|
280
726
|
const agent = agents[session.agentId]
|
|
@@ -282,6 +728,9 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
282
728
|
agentMcpServerIds = agent?.mcpServerIds
|
|
283
729
|
agentMcpDisabledTools = agent?.mcpDisabledTools
|
|
284
730
|
agentHeartbeatEnabled = agent?.heartbeatEnabled === true
|
|
731
|
+
agentMemoryScopeMode = agent?.memoryScopeMode || null
|
|
732
|
+
agentResponseStyle = agent?.responseStyle || null
|
|
733
|
+
agentResponseMaxChars = agent?.responseMaxChars || null
|
|
285
734
|
if (!hasProvidedSystemPrompt) {
|
|
286
735
|
// Identity block — make sure the agent knows who it is
|
|
287
736
|
const identityLines = [`## My Identity`, `My name is ${agent?.name || 'Agent'}.`]
|
|
@@ -294,17 +743,25 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
294
743
|
if (agent?.systemPrompt) stateModifierParts.push(agent.systemPrompt)
|
|
295
744
|
if (agent?.skillIds?.length) {
|
|
296
745
|
const allSkills = loadSkills()
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if (skill?.content) stateModifierParts.push(`## Skill: ${skill.name}\n${skill.content}`)
|
|
300
|
-
}
|
|
746
|
+
const skillPromptText = buildSkillPromptText(allSkills, agent.skillIds)
|
|
747
|
+
if (skillPromptText) stateModifierParts.push(skillPromptText)
|
|
301
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 */ }
|
|
302
761
|
}
|
|
303
762
|
}
|
|
304
763
|
|
|
305
|
-
|
|
306
|
-
stateModifierParts.push('I\'m here to get things done. I take action, use my tools, and focus on outcomes.')
|
|
307
|
-
}
|
|
764
|
+
// (conciseness and action-orientation are covered in the execution policy below)
|
|
308
765
|
|
|
309
766
|
// Thinking level guidance (applies to all providers via system prompt)
|
|
310
767
|
if (agentThinkingLevel) {
|
|
@@ -317,8 +774,21 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
317
774
|
stateModifierParts.push(`## Reasoning Depth\n${thinkingGuidance[agentThinkingLevel]}`)
|
|
318
775
|
}
|
|
319
776
|
|
|
320
|
-
// Inject
|
|
321
|
-
|
|
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) {
|
|
322
792
|
try {
|
|
323
793
|
const { buildAgentAwarenessBlock } = await import('./agent-registry')
|
|
324
794
|
const awarenessBlock = buildAgentAwarenessBlock(session.agentId)
|
|
@@ -336,6 +806,43 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
336
806
|
// Plugin context injection is non-critical
|
|
337
807
|
}
|
|
338
808
|
|
|
809
|
+
if (!hasProvidedSystemPrompt && activeProjectContext.projectId) {
|
|
810
|
+
const projectLines = ['## Current Project']
|
|
811
|
+
if (activeProjectContext.project?.name) {
|
|
812
|
+
projectLines.push(`Active project: ${activeProjectContext.project.name}.`)
|
|
813
|
+
} else {
|
|
814
|
+
projectLines.push(`Active project ID: ${activeProjectContext.projectId}.`)
|
|
815
|
+
}
|
|
816
|
+
if (activeProjectContext.project?.description) {
|
|
817
|
+
projectLines.push(`Project description: ${activeProjectContext.project.description}`)
|
|
818
|
+
projectLines.push('Treat the project description above as authoritative context for who the project is for, what it is focused on, and which pilot priorities matter right now. If the user asks about the active project, answer from that description instead of saying the context is unavailable.')
|
|
819
|
+
}
|
|
820
|
+
if (activeProjectContext.objective) projectLines.push(`Project objective: ${activeProjectContext.objective}`)
|
|
821
|
+
if (activeProjectContext.audience) projectLines.push(`Who it is for: ${activeProjectContext.audience}`)
|
|
822
|
+
if (activeProjectContext.priorities.length > 0) projectLines.push(`Pilot priorities: ${activeProjectContext.priorities.join('; ')}`)
|
|
823
|
+
if (activeProjectContext.openObjectives.length > 0) projectLines.push(`Open objectives: ${activeProjectContext.openObjectives.join('; ')}`)
|
|
824
|
+
if (activeProjectContext.capabilityHints.length > 0) projectLines.push(`Suggested operating modes: ${activeProjectContext.capabilityHints.join('; ')}`)
|
|
825
|
+
if (activeProjectContext.credentialRequirements.length > 0) projectLines.push(`Credential and secret requirements: ${activeProjectContext.credentialRequirements.join('; ')}`)
|
|
826
|
+
if (activeProjectContext.successMetrics.length > 0) projectLines.push(`Success metrics: ${activeProjectContext.successMetrics.join('; ')}`)
|
|
827
|
+
if (activeProjectContext.heartbeatPrompt) projectLines.push(`Preferred heartbeat prompt: ${activeProjectContext.heartbeatPrompt}`)
|
|
828
|
+
if (activeProjectContext.heartbeatIntervalSec != null) projectLines.push(`Preferred heartbeat interval: ${activeProjectContext.heartbeatIntervalSec}s`)
|
|
829
|
+
if (activeProjectContext.resourceSummary) {
|
|
830
|
+
const summary = activeProjectContext.resourceSummary
|
|
831
|
+
const resourceBits = [
|
|
832
|
+
`open tasks ${summary.openTaskCount}`,
|
|
833
|
+
`active schedules ${summary.activeScheduleCount}`,
|
|
834
|
+
`project secrets ${summary.secretCount}`,
|
|
835
|
+
]
|
|
836
|
+
if (summary.topTaskTitles.length > 0) projectLines.push(`Top open tasks: ${summary.topTaskTitles.join('; ')}`)
|
|
837
|
+
if (summary.scheduleNames.length > 0) projectLines.push(`Active schedules: ${summary.scheduleNames.join('; ')}`)
|
|
838
|
+
if (summary.secretNames.length > 0) projectLines.push(`Known project secrets: ${summary.secretNames.join('; ')}`)
|
|
839
|
+
projectLines.push(`Project resource summary: ${resourceBits.join(', ')}.`)
|
|
840
|
+
}
|
|
841
|
+
if (activeProjectContext.projectRoot) projectLines.push(`Workspace root: ${activeProjectContext.projectRoot}`)
|
|
842
|
+
projectLines.push('When creating project tasks, schedules, secrets, memories, or deliverables for this work, default them to the active project unless the user redirects you.')
|
|
843
|
+
stateModifierParts.push(projectLines.join('\n'))
|
|
844
|
+
}
|
|
845
|
+
|
|
339
846
|
// Tell the LLM about available plugins and their access status
|
|
340
847
|
{
|
|
341
848
|
const agentEnabledSet = new Set(sessionPlugins)
|
|
@@ -358,21 +865,15 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
358
865
|
}
|
|
359
866
|
}
|
|
360
867
|
|
|
361
|
-
const
|
|
362
|
-
if (enabledButNoAccess.length > 0) {
|
|
363
|
-
parts.push(
|
|
364
|
-
`**Available but not assigned to me:** ${enabledButNoAccess.join(', ')}\n` +
|
|
365
|
-
'I can request access using `manage_capabilities` with action "request_access" or `request_tool_access`.',
|
|
366
|
-
)
|
|
367
|
-
}
|
|
868
|
+
const accessParts: string[] = []
|
|
368
869
|
if (globallyDisabled.length > 0) {
|
|
369
|
-
|
|
870
|
+
accessParts.push(`**Disabled site-wide:** ${globallyDisabled.join(', ')}`)
|
|
370
871
|
}
|
|
371
872
|
if (mcpDisabled.length > 0) {
|
|
372
|
-
|
|
873
|
+
accessParts.push(`**MCP tools not available:** ${mcpDisabled.join(', ')}`)
|
|
373
874
|
}
|
|
374
|
-
if (
|
|
375
|
-
stateModifierParts.push(`## Plugin Access\n${
|
|
875
|
+
if (accessParts.length > 0) {
|
|
876
|
+
stateModifierParts.push(`## Plugin Access\n${accessParts.join('\n')}`)
|
|
376
877
|
}
|
|
377
878
|
}
|
|
378
879
|
|
|
@@ -396,6 +897,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
396
897
|
heartbeatIntervalSec,
|
|
397
898
|
platformAssignScope: agentPlatformAssignScope,
|
|
398
899
|
userMessage: message,
|
|
900
|
+
responseStyle: agentResponseStyle,
|
|
901
|
+
responseMaxChars: agentResponseMaxChars,
|
|
399
902
|
}),
|
|
400
903
|
)
|
|
401
904
|
|
|
@@ -407,6 +910,11 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
407
910
|
platformAssignScope: agentPlatformAssignScope,
|
|
408
911
|
mcpServerIds: agentMcpServerIds,
|
|
409
912
|
mcpDisabledTools: agentMcpDisabledTools,
|
|
913
|
+
projectId: activeProjectContext.projectId,
|
|
914
|
+
projectRoot: activeProjectContext.projectRoot,
|
|
915
|
+
projectName: activeProjectContext.project?.name || null,
|
|
916
|
+
projectDescription: activeProjectContext.project?.description || null,
|
|
917
|
+
memoryScopeMode: agentMemoryScopeMode,
|
|
410
918
|
})
|
|
411
919
|
const agent = createReactAgent({ llm, tools, stateModifier })
|
|
412
920
|
const recursionLimit = getAgentLoopRecursionLimit(runtime)
|
|
@@ -570,13 +1078,16 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
570
1078
|
|
|
571
1079
|
let fullText = ''
|
|
572
1080
|
let lastSegment = ''
|
|
1081
|
+
let lastSettledSegment = ''
|
|
573
1082
|
let hasToolCalls = false
|
|
574
1083
|
let needsTextSeparator = false
|
|
575
1084
|
let totalInputTokens = 0
|
|
576
1085
|
let totalOutputTokens = 0
|
|
577
1086
|
let accumulatedThinking = ''
|
|
578
1087
|
const pluginInvocations: PluginInvocationRecord[] = []
|
|
1088
|
+
const streamedToolEvents: MessageToolEvent[] = []
|
|
579
1089
|
let currentToolInputTokens = 0
|
|
1090
|
+
const boundedExternalExecutionTask = looksLikeBoundedExternalExecutionTask(message)
|
|
580
1091
|
|
|
581
1092
|
// Plugin hooks: beforeAgentStart
|
|
582
1093
|
const pluginMgr = getPluginManager()
|
|
@@ -599,20 +1110,49 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
599
1110
|
const MAX_AUTO_CONTINUES = 3
|
|
600
1111
|
const MAX_TRANSIENT_RETRIES = 2
|
|
601
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
|
|
602
1116
|
let autoContinueCount = 0
|
|
603
1117
|
let transientRetryCount = 0
|
|
604
1118
|
let requiredToolContinueCount = 0
|
|
1119
|
+
let executionFollowthroughCount = 0
|
|
1120
|
+
let deliverableFollowthroughCount = 0
|
|
1121
|
+
let toolSummaryRetryCount = 0
|
|
605
1122
|
const explicitRequiredToolNames = getExplicitRequiredToolNames(message, sessionPlugins)
|
|
606
1123
|
const usedToolNames = new Set<string>()
|
|
1124
|
+
const loopTracker = new ToolLoopTracker()
|
|
1125
|
+
let loopDetectionTriggered: LoopDetectionResult | null = null
|
|
1126
|
+
let terminalToolResponse = ''
|
|
607
1127
|
|
|
608
1128
|
try {
|
|
609
|
-
|
|
1129
|
+
const maxIterations = MAX_AUTO_CONTINUES + MAX_TRANSIENT_RETRIES + MAX_REQUIRED_TOOL_CONTINUES + MAX_EXECUTION_FOLLOWTHROUGHS + MAX_DELIVERABLE_FOLLOWTHROUGHS + MAX_TOOL_SUMMARY_RETRIES
|
|
610
1130
|
for (let iteration = 0; iteration <= maxIterations; iteration++) {
|
|
611
|
-
let shouldContinue: 'recursion' | 'transient' | 'required_tool' | false = false
|
|
1131
|
+
let shouldContinue: 'recursion' | 'transient' | 'required_tool' | 'execution_followthrough' | 'deliverable_followthrough' | 'tool_summary' | false = false
|
|
612
1132
|
let requiredToolReminderNames: string[] = []
|
|
613
1133
|
let waitingForToolResult = false
|
|
614
1134
|
let idleTimedOut = false
|
|
1135
|
+
let reachedExecutionBoundary = false
|
|
1136
|
+
let executionFollowthroughReason: 'research_limit' | 'post_simulation' | null = null
|
|
615
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
|
+
}
|
|
616
1156
|
|
|
617
1157
|
// Fresh per-iteration controller so an internal LangGraph abort doesn't poison subsequent iterations.
|
|
618
1158
|
// Linked to the parent so client disconnect / timeout still propagates.
|
|
@@ -637,6 +1177,13 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
637
1177
|
}, 90_000)
|
|
638
1178
|
}
|
|
639
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
|
+
|
|
640
1187
|
try {
|
|
641
1188
|
armIdleWatchdog()
|
|
642
1189
|
const eventStream = agent.streamEvents(
|
|
@@ -665,10 +1212,12 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
665
1212
|
} else if (block.text) {
|
|
666
1213
|
if (needsTextSeparator && fullText.length > 0) {
|
|
667
1214
|
fullText += '\n\n'
|
|
1215
|
+
iterationText += '\n\n'
|
|
668
1216
|
write(`data: ${JSON.stringify({ t: 'd', text: '\n\n' })}\n\n`)
|
|
669
1217
|
needsTextSeparator = false
|
|
670
1218
|
}
|
|
671
1219
|
fullText += block.text
|
|
1220
|
+
iterationText += block.text
|
|
672
1221
|
lastSegment += block.text
|
|
673
1222
|
write(`data: ${JSON.stringify({ t: 'd', text: block.text })}\n\n`)
|
|
674
1223
|
}
|
|
@@ -678,10 +1227,12 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
678
1227
|
if (text) {
|
|
679
1228
|
if (needsTextSeparator && fullText.length > 0) {
|
|
680
1229
|
fullText += '\n\n'
|
|
1230
|
+
iterationText += '\n\n'
|
|
681
1231
|
write(`data: ${JSON.stringify({ t: 'd', text: '\n\n' })}\n\n`)
|
|
682
1232
|
needsTextSeparator = false
|
|
683
1233
|
}
|
|
684
1234
|
fullText += text
|
|
1235
|
+
iterationText += text
|
|
685
1236
|
lastSegment += text
|
|
686
1237
|
write(`data: ${JSON.stringify({ t: 'd', text })}\n\n`)
|
|
687
1238
|
}
|
|
@@ -701,16 +1252,36 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
701
1252
|
totalOutputTokens += usage.completionTokens || usage.output_tokens || usage.completion_tokens || 0
|
|
702
1253
|
}
|
|
703
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
|
+
|
|
704
1270
|
clearIdleWatchdog()
|
|
705
1271
|
waitingForToolResult = true
|
|
706
1272
|
hasToolCalls = true
|
|
707
1273
|
needsTextSeparator = true
|
|
1274
|
+
const settledSegment = extractSuggestions(lastSegment).clean.trim()
|
|
1275
|
+
if (settledSegment) lastSettledSegment = settledSegment
|
|
708
1276
|
lastSegment = ''
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
1277
|
+
usedToolNames.add(canonicalizePluginId(toolName) || toolName)
|
|
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
|
+
}
|
|
712
1284
|
// Estimate input tokens for plugin invocation tracking
|
|
713
|
-
const inputStr = typeof input === 'string' ? input : JSON.stringify(input)
|
|
714
1285
|
currentToolInputTokens = Math.ceil((inputStr?.length || 0) / 4)
|
|
715
1286
|
logExecution(session.id, 'tool_call', `${toolName} invoked`, {
|
|
716
1287
|
agentId: session.agentId,
|
|
@@ -720,8 +1291,19 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
720
1291
|
t: 'tool_call',
|
|
721
1292
|
toolName,
|
|
722
1293
|
toolInput: inputStr,
|
|
1294
|
+
toolCallId: event.run_id,
|
|
723
1295
|
})}\n\n`)
|
|
1296
|
+
updateStreamedToolEvents(streamedToolEvents, {
|
|
1297
|
+
type: 'call',
|
|
1298
|
+
name: toolName,
|
|
1299
|
+
input: inputStr,
|
|
1300
|
+
toolCallId: event.run_id,
|
|
1301
|
+
})
|
|
724
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
|
+
|
|
725
1307
|
waitingForToolResult = false
|
|
726
1308
|
armIdleWatchdog()
|
|
727
1309
|
const toolName = event.name || 'unknown'
|
|
@@ -764,11 +1346,89 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
764
1346
|
})
|
|
765
1347
|
currentToolInputTokens = 0
|
|
766
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
|
+
|
|
767
1366
|
write(`data: ${JSON.stringify({
|
|
768
1367
|
t: 'tool_result',
|
|
769
1368
|
toolName,
|
|
770
1369
|
toolOutput: outputStr?.slice(0, 2000),
|
|
1370
|
+
toolCallId: event.run_id,
|
|
771
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
|
+
}
|
|
772
1432
|
}
|
|
773
1433
|
}
|
|
774
1434
|
} catch (innerErr: unknown) {
|
|
@@ -805,12 +1465,25 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
805
1465
|
})
|
|
806
1466
|
write(`data: ${JSON.stringify({ t: 'status', text: JSON.stringify({ autoContinue: autoContinueCount, maxContinues: MAX_AUTO_CONTINUES }) })}\n\n`)
|
|
807
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
|
|
808
1478
|
shouldContinue = 'transient'
|
|
809
1479
|
transientRetryCount++
|
|
810
1480
|
logExecution(session.id, 'decision', `Transient error, retrying (${transientRetryCount}/${MAX_TRANSIENT_RETRIES}): ${errMsg}`, {
|
|
811
1481
|
agentId: session.agentId,
|
|
812
|
-
detail: { errName, errMsg },
|
|
1482
|
+
detail: { errName, errMsg, hadPartialOutput },
|
|
813
1483
|
})
|
|
1484
|
+
if (hadPartialOutput) {
|
|
1485
|
+
write(`data: ${JSON.stringify({ t: 'reset', text: iterationStartState.fullText })}\n\n`)
|
|
1486
|
+
}
|
|
814
1487
|
write(`data: ${JSON.stringify({ t: 'status', text: JSON.stringify({ transientRetry: transientRetryCount, maxRetries: MAX_TRANSIENT_RETRIES, error: errMsg }) })}\n\n`)
|
|
815
1488
|
} else {
|
|
816
1489
|
// Non-retryable error or exhausted retries — rethrow to outer catch
|
|
@@ -821,8 +1494,38 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
821
1494
|
abortController.signal.removeEventListener('abort', onParentAbort)
|
|
822
1495
|
}
|
|
823
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
|
+
|
|
824
1522
|
if (!shouldContinue && explicitRequiredToolNames.length > 0 && requiredToolContinueCount < MAX_REQUIRED_TOOL_CONTINUES) {
|
|
825
|
-
|
|
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
|
+
})
|
|
826
1529
|
if (requiredToolReminderNames.length > 0) {
|
|
827
1530
|
shouldContinue = 'required_tool'
|
|
828
1531
|
requiredToolContinueCount++
|
|
@@ -837,21 +1540,151 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
837
1540
|
}
|
|
838
1541
|
}
|
|
839
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
|
+
|
|
840
1615
|
if (!shouldContinue) break
|
|
841
1616
|
|
|
1617
|
+
const continuationAssistantText = resolveContinuationAssistantText({
|
|
1618
|
+
iterationText,
|
|
1619
|
+
lastSegment,
|
|
1620
|
+
})
|
|
1621
|
+
|
|
842
1622
|
if (shouldContinue === 'recursion') {
|
|
843
|
-
//
|
|
844
|
-
|
|
845
|
-
|
|
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 }))
|
|
846
1628
|
}
|
|
1629
|
+
const settledSegment = extractSuggestions(lastSegment).clean.trim()
|
|
1630
|
+
if (settledSegment) lastSettledSegment = settledSegment
|
|
847
1631
|
langchainMessages.push(new HumanMessage({ content: 'Continue where you left off. Complete the remaining steps of the objective.' }))
|
|
848
1632
|
lastSegment = ''
|
|
849
1633
|
} else if (shouldContinue === 'required_tool') {
|
|
850
|
-
if (
|
|
851
|
-
langchainMessages.push(new AIMessage({ content:
|
|
1634
|
+
if (continuationAssistantText) {
|
|
1635
|
+
langchainMessages.push(new AIMessage({ content: continuationAssistantText }))
|
|
1636
|
+
}
|
|
1637
|
+
const settledSegment = extractSuggestions(lastSegment).clean.trim()
|
|
1638
|
+
if (settledSegment) lastSettledSegment = settledSegment
|
|
1639
|
+
langchainMessages.push(new HumanMessage({
|
|
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.`,
|
|
1641
|
+
}))
|
|
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 }))
|
|
852
1675
|
}
|
|
1676
|
+
const toolSummaryLines = streamedToolEvents
|
|
1677
|
+
.filter((e) => e.output)
|
|
1678
|
+
.map((e) => `[${e.name}]: ${(e.output || '').slice(0, 500)}`)
|
|
1679
|
+
.slice(0, 6)
|
|
853
1680
|
langchainMessages.push(new HumanMessage({
|
|
854
|
-
content:
|
|
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'),
|
|
855
1688
|
}))
|
|
856
1689
|
lastSegment = ''
|
|
857
1690
|
} else if (shouldContinue === 'transient') {
|
|
@@ -885,13 +1718,38 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
885
1718
|
|
|
886
1719
|
// Skip post-stream work if the client disconnected mid-stream
|
|
887
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
|
+
}
|
|
888
1745
|
await cleanup()
|
|
889
|
-
return { fullText, finalResponse
|
|
1746
|
+
return { fullText, finalResponse }
|
|
890
1747
|
}
|
|
891
1748
|
|
|
892
1749
|
// Extract LLM-generated suggestions from the response and strip the tag
|
|
893
1750
|
const extracted = extractSuggestions(fullText)
|
|
894
1751
|
fullText = extracted.clean
|
|
1752
|
+
if (!fullText.trim() && terminalToolResponse) fullText = terminalToolResponse
|
|
895
1753
|
if (extracted.suggestions) {
|
|
896
1754
|
write(`data: ${JSON.stringify({ t: 'md', text: JSON.stringify({ suggestions: extracted.suggestions }) })}\n\n`)
|
|
897
1755
|
}
|
|
@@ -934,6 +1792,35 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
934
1792
|
})}\n\n`)
|
|
935
1793
|
}
|
|
936
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
|
+
|
|
937
1824
|
// Plugin hooks: afterAgentComplete
|
|
938
1825
|
await pluginMgr.runHook('afterAgentComplete', { session, response: fullText }, { enabledIds: sessionPlugins })
|
|
939
1826
|
|
|
@@ -949,14 +1836,5 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
949
1836
|
// Clean up browser and other session resources
|
|
950
1837
|
await cleanup()
|
|
951
1838
|
|
|
952
|
-
// If tools were called, finalResponse is the text from the last LLM turn only.
|
|
953
|
-
// Fall back to fullText if the last segment is empty (e.g. agent ended on a tool call
|
|
954
|
-
// with no summary text).
|
|
955
|
-
// Strip suggestions tag from lastSegment too (connector delivery)
|
|
956
|
-
const cleanLastSegment = extractSuggestions(lastSegment).clean
|
|
957
|
-
const finalResponse = hasToolCalls
|
|
958
|
-
? (cleanLastSegment.trim() || fullText)
|
|
959
|
-
: fullText
|
|
960
|
-
|
|
961
1839
|
return { fullText, finalResponse }
|
|
962
1840
|
}
|