@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
|
@@ -18,14 +18,14 @@ import { getProvider } from '@/lib/providers'
|
|
|
18
18
|
import { estimateCost, checkAgentBudgetLimits } from './cost'
|
|
19
19
|
import { log } from './logger'
|
|
20
20
|
import { logExecution } from './execution-log'
|
|
21
|
-
import { streamAgentChat } from './stream-agent-chat'
|
|
21
|
+
import { buildToolDisciplineLines, streamAgentChat } from './stream-agent-chat'
|
|
22
22
|
import { runLinkUnderstanding } from './link-understanding'
|
|
23
23
|
import { buildSessionTools } from './session-tools'
|
|
24
24
|
import type { StructuredToolInterface } from '@langchain/core/tools'
|
|
25
25
|
import type { Session } from '@/types'
|
|
26
26
|
import { stripMainLoopMetaForPersistence } from './main-agent-loop'
|
|
27
27
|
import { getPluginManager } from './plugins'
|
|
28
|
-
import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
|
|
28
|
+
import { isLocalOpenClawEndpoint, normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
|
|
29
29
|
import { routeTaskIntent } from './capability-router'
|
|
30
30
|
import { notify } from './ws-hub'
|
|
31
31
|
import { applyResolvedRoute, resolvePrimaryAgentRoute } from './agent-runtime-config'
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
setCachedLlmResponse,
|
|
39
39
|
type LlmResponseCacheKeyInput,
|
|
40
40
|
} from './llm-response-cache'
|
|
41
|
+
import { genId } from '@/lib/id'
|
|
41
42
|
import type { Message, MessageToolEvent, SSEEvent, UsageRecord } from '@/types'
|
|
42
43
|
import { markProviderFailure, markProviderSuccess, rankDelegatesByHealth } from './provider-health'
|
|
43
44
|
import { isHeartbeatSource, isInternalHeartbeatRun } from './heartbeat-source'
|
|
@@ -46,14 +47,19 @@ import { buildIdentityContinuityContext, refreshSessionIdentityState } from './i
|
|
|
46
47
|
import { syncSessionArchiveMemory } from './session-archive-memory'
|
|
47
48
|
import { evaluateSessionFreshness, resetSessionRuntime, resolveSessionResetPolicy } from './session-reset-policy'
|
|
48
49
|
import { pruneStreamingAssistantArtifacts, upsertStreamingAssistantArtifact } from '@/lib/chat-streaming-state'
|
|
50
|
+
import { resolveActiveProjectContext } from './project-context'
|
|
51
|
+
import { shouldSuppressHiddenControlText, stripHiddenControlTokens } from './assistant-control'
|
|
52
|
+
import { buildToolEventAssistantSummary } from '@/lib/tool-event-summary'
|
|
53
|
+
import { buildAgentDisabledMessage, isAgentDisabled } from './agent-availability'
|
|
49
54
|
type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli'
|
|
50
55
|
|
|
51
56
|
/** Slice history from the most recent context-clear marker forward */
|
|
52
57
|
function applyContextClearBoundary(messages: Message[]): Message[] {
|
|
58
|
+
const filterModelHistory = (items: Message[]) => items.filter((message) => message.historyExcluded !== true)
|
|
53
59
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
54
|
-
if (messages[i].kind === 'context-clear') return messages.slice(i + 1)
|
|
60
|
+
if (messages[i].kind === 'context-clear') return filterModelHistory(messages.slice(i + 1))
|
|
55
61
|
}
|
|
56
|
-
return messages
|
|
62
|
+
return filterModelHistory(messages)
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
interface SessionWithTools {
|
|
@@ -119,6 +125,7 @@ export function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
|
|
|
119
125
|
previous
|
|
120
126
|
&& previous.name === (ev.toolName || 'unknown')
|
|
121
127
|
&& previous.input === (ev.toolInput || '')
|
|
128
|
+
&& previous.toolCallId === (ev.toolCallId || previous.toolCallId)
|
|
122
129
|
&& !previous.output
|
|
123
130
|
) {
|
|
124
131
|
return
|
|
@@ -126,11 +133,14 @@ export function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
|
|
|
126
133
|
bag.push({
|
|
127
134
|
name: ev.toolName || 'unknown',
|
|
128
135
|
input: ev.toolInput || '',
|
|
136
|
+
toolCallId: ev.toolCallId,
|
|
129
137
|
})
|
|
130
138
|
return
|
|
131
139
|
}
|
|
132
140
|
if (ev.t === 'tool_result') {
|
|
133
|
-
const idx =
|
|
141
|
+
const idx = ev.toolCallId
|
|
142
|
+
? bag.findLastIndex((e) => e.toolCallId === ev.toolCallId && !e.output)
|
|
143
|
+
: bag.findLastIndex((e) => e.name === (ev.toolName || 'unknown') && !e.output)
|
|
134
144
|
if (idx === -1) return
|
|
135
145
|
const output = ev.toolOutput || ''
|
|
136
146
|
bag[idx] = {
|
|
@@ -141,6 +151,25 @@ export function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
|
|
|
141
151
|
}
|
|
142
152
|
}
|
|
143
153
|
|
|
154
|
+
function escapeRegExp(value: string): string {
|
|
155
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function hasExplicitToolMention(message: string, toolName: string): boolean {
|
|
159
|
+
const escaped = escapeRegExp(toolName)
|
|
160
|
+
const negated = new RegExp(`\\b(?:do not|don't|dont|avoid|skip|without|never)\\s+(?:use\\s+|call\\s+|invoke\\s+)?(?:the\\s+)?\`?${escaped}\`?(?:\\s+tool)?\\b`, 'i')
|
|
161
|
+
if (negated.test(message)) return false
|
|
162
|
+
const boundary = new RegExp(`(^|[^a-z0-9_])\`?${escaped}\`?([^a-z0-9_]|$)`, 'i')
|
|
163
|
+
return boundary.test(message)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function hasExplicitGenericToolRequest(message: string, toolName: string): boolean {
|
|
167
|
+
const escaped = escapeRegExp(toolName)
|
|
168
|
+
const negated = new RegExp(`\\b(?:do not|don't|dont|avoid|skip|without|never)\\s+(?:use\\s+|call\\s+|invoke\\s+)?(?:the\\s+)?${escaped}(?:\\s+tool)?\\b`, 'i')
|
|
169
|
+
if (negated.test(message)) return false
|
|
170
|
+
return new RegExp(`(^|[\\s(])\`${escaped}\`([\\s).,!?]|$)|\\b${escaped}\\s+tool\\b|\\buse\\s+(?:the\\s+)?${escaped}\\b|\\bcall\\s+(?:the\\s+)?${escaped}\\b|\\binvoke\\s+(?:the\\s+)?${escaped}\\b`, 'i').test(message)
|
|
171
|
+
}
|
|
172
|
+
|
|
144
173
|
export function dedupeConsecutiveToolEvents(events: MessageToolEvent[]): MessageToolEvent[] {
|
|
145
174
|
const sameEvent = (left: MessageToolEvent, right: MessageToolEvent): boolean => (
|
|
146
175
|
left.name === right.name
|
|
@@ -177,6 +206,26 @@ export function dedupeConsecutiveToolEvents(events: MessageToolEvent[]): Message
|
|
|
177
206
|
return deduped
|
|
178
207
|
}
|
|
179
208
|
|
|
209
|
+
export function deriveTerminalRunError(params: {
|
|
210
|
+
errorMessage?: string
|
|
211
|
+
fullResponse: string
|
|
212
|
+
streamErrors: string[]
|
|
213
|
+
toolEvents: MessageToolEvent[]
|
|
214
|
+
internal: boolean
|
|
215
|
+
}): string | undefined {
|
|
216
|
+
if (params.errorMessage) return params.errorMessage
|
|
217
|
+
|
|
218
|
+
if (params.streamErrors.length > 0 && !params.fullResponse.trim()) {
|
|
219
|
+
return params.streamErrors[params.streamErrors.length - 1]
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!params.internal && !params.fullResponse.trim() && params.toolEvents.length === 0) {
|
|
223
|
+
return 'Run completed without any response text, tool calls, or explicit error details. Check the provider configuration and try again.'
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return undefined
|
|
227
|
+
}
|
|
228
|
+
|
|
180
229
|
function extractDelegateResponse(outputText: string): string | null {
|
|
181
230
|
try {
|
|
182
231
|
const parsed = JSON.parse(outputText) as Record<string, unknown>
|
|
@@ -191,6 +240,8 @@ function extractDelegateResponse(outputText: string): string | null {
|
|
|
191
240
|
const MANAGE_PLATFORM_RESOURCE_TO_TOOL: Record<string, string> = {
|
|
192
241
|
agent: 'manage_agents',
|
|
193
242
|
agents: 'manage_agents',
|
|
243
|
+
project: 'manage_projects',
|
|
244
|
+
projects: 'manage_projects',
|
|
194
245
|
task: 'manage_tasks',
|
|
195
246
|
tasks: 'manage_tasks',
|
|
196
247
|
schedule: 'manage_schedules',
|
|
@@ -355,13 +406,32 @@ function shouldReplaceRecentAssistantMessage(params: {
|
|
|
355
406
|
return prevTools === 0
|
|
356
407
|
}
|
|
357
408
|
|
|
409
|
+
function hasPersistableAssistantPayload(text: string, thinking: string, toolEvents: MessageToolEvent[]): boolean {
|
|
410
|
+
return text.trim().length > 0 || thinking.trim().length > 0 || toolEvents.length > 0
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function getPersistedAssistantText(text: string, toolEvents: MessageToolEvent[]): string {
|
|
414
|
+
const trimmed = text.trim()
|
|
415
|
+
if (trimmed) return trimmed
|
|
416
|
+
return buildToolEventAssistantSummary(toolEvents)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function getToolEventsSnapshotKey(toolEvents: MessageToolEvent[]): string {
|
|
420
|
+
return JSON.stringify(toolEvents.map((event) => [
|
|
421
|
+
event.name,
|
|
422
|
+
event.input,
|
|
423
|
+
event.output || '',
|
|
424
|
+
event.error === true,
|
|
425
|
+
event.toolCallId || '',
|
|
426
|
+
]))
|
|
427
|
+
}
|
|
428
|
+
|
|
358
429
|
export function pruneSuppressedHeartbeatStreamMessage(messages: Message[]): boolean {
|
|
359
430
|
return pruneStreamingAssistantArtifacts(messages)
|
|
360
431
|
}
|
|
361
432
|
|
|
362
433
|
export function requestedToolNamesFromMessage(message: string): string[] {
|
|
363
|
-
const
|
|
364
|
-
const candidates = [
|
|
434
|
+
const explicitCandidates = [
|
|
365
435
|
'delegate_to_claude_code',
|
|
366
436
|
'delegate_to_codex_cli',
|
|
367
437
|
'delegate_to_opencode_cli',
|
|
@@ -389,35 +459,106 @@ export function requestedToolNamesFromMessage(message: string): string[] {
|
|
|
389
459
|
'wallet_tool',
|
|
390
460
|
'http_request',
|
|
391
461
|
'send_file',
|
|
462
|
+
'sandbox_exec',
|
|
463
|
+
'sandbox_list_runtimes',
|
|
464
|
+
'schedule_wake',
|
|
465
|
+
'spawn_subagent',
|
|
466
|
+
'ask_human',
|
|
467
|
+
'context_status',
|
|
468
|
+
'context_summarize',
|
|
469
|
+
'openclaw_nodes',
|
|
470
|
+
'openclaw_workspace',
|
|
471
|
+
]
|
|
472
|
+
const genericCandidates = [
|
|
392
473
|
'browser',
|
|
393
474
|
'web',
|
|
394
475
|
'shell',
|
|
395
476
|
'files',
|
|
396
477
|
'edit_file',
|
|
397
|
-
'sandbox_exec',
|
|
398
|
-
'sandbox_list_runtimes',
|
|
399
478
|
'git',
|
|
400
479
|
'canvas',
|
|
401
|
-
'schedule_wake',
|
|
402
|
-
'spawn_subagent',
|
|
403
480
|
'mailbox',
|
|
404
|
-
'ask_human',
|
|
405
481
|
'document',
|
|
406
482
|
'extract',
|
|
407
483
|
'table',
|
|
408
484
|
'crawl',
|
|
409
|
-
'
|
|
410
|
-
'context_summarize',
|
|
411
|
-
'openclaw_nodes',
|
|
412
|
-
'openclaw_workspace',
|
|
485
|
+
'email',
|
|
413
486
|
]
|
|
414
|
-
const requested =
|
|
415
|
-
|
|
487
|
+
const requested = explicitCandidates.filter((name) => hasExplicitToolMention(message, name))
|
|
488
|
+
for (const name of genericCandidates) {
|
|
489
|
+
if (hasExplicitGenericToolRequest(message, name)) requested.push(name)
|
|
490
|
+
}
|
|
491
|
+
if (hasExplicitGenericToolRequest(message, 'delegate')) {
|
|
416
492
|
requested.push('delegate')
|
|
417
493
|
}
|
|
418
494
|
return Array.from(new Set(requested))
|
|
419
495
|
}
|
|
420
496
|
|
|
497
|
+
function parseToolJsonObject(raw: string): Record<string, unknown> | null {
|
|
498
|
+
const trimmed = raw.trim()
|
|
499
|
+
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) return null
|
|
500
|
+
try {
|
|
501
|
+
const parsed = JSON.parse(trimmed)
|
|
502
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
503
|
+
? parsed as Record<string, unknown>
|
|
504
|
+
: null
|
|
505
|
+
} catch {
|
|
506
|
+
return null
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function summarizeConnectorToolFailure(output: string): string {
|
|
511
|
+
const trimmed = output.trim()
|
|
512
|
+
const withoutPrefix = trimmed.replace(/^Error:\s*/i, '')
|
|
513
|
+
const parsed = parseToolJsonObject(withoutPrefix) || parseToolJsonObject(trimmed)
|
|
514
|
+
if (parsed) {
|
|
515
|
+
const detail = parsed.detail
|
|
516
|
+
if (detail && typeof detail === 'object' && !Array.isArray(detail)) {
|
|
517
|
+
const detailRecord = detail as Record<string, unknown>
|
|
518
|
+
const message = typeof detailRecord.message === 'string' ? detailRecord.message.trim() : ''
|
|
519
|
+
if (message) return message
|
|
520
|
+
const code = typeof detailRecord.code === 'string' ? detailRecord.code.trim() : ''
|
|
521
|
+
const status = typeof detailRecord.status === 'string' ? detailRecord.status.trim() : ''
|
|
522
|
+
if (code && status) return `${code}: ${status}`
|
|
523
|
+
if (code) return code
|
|
524
|
+
if (status) return status
|
|
525
|
+
}
|
|
526
|
+
const message = typeof parsed.message === 'string' ? parsed.message.trim() : ''
|
|
527
|
+
if (message) return message
|
|
528
|
+
const error = typeof parsed.error === 'string' ? parsed.error.trim() : ''
|
|
529
|
+
if (error) return error
|
|
530
|
+
}
|
|
531
|
+
return withoutPrefix.replace(/\s+/g, ' ').trim() || 'Connector delivery failed.'
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function connectorToolEventSucceeded(event: MessageToolEvent): boolean {
|
|
535
|
+
if (!event.output) return false
|
|
536
|
+
const parsed = parseToolJsonObject(event.output)
|
|
537
|
+
const status = typeof parsed?.status === 'string' ? parsed.status.trim().toLowerCase() : ''
|
|
538
|
+
return status === 'sent' || status === 'voice_sent' || status === 'scheduled'
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const POSITIVE_CONNECTOR_DELIVERY_RE = /\b(?:i(?:'ve| have)?(?: successfully)? sent|i sent|successfully sent|sent to your|voice note (?:has been|was) sent|message (?:has been|was) sent)\b/i
|
|
542
|
+
|
|
543
|
+
export function reconcileConnectorDeliveryText(text: string, events: MessageToolEvent[]): string {
|
|
544
|
+
const trimmed = text.trim()
|
|
545
|
+
if (!trimmed || !POSITIVE_CONNECTOR_DELIVERY_RE.test(trimmed)) return text
|
|
546
|
+
|
|
547
|
+
const connectorEvents = dedupeConsecutiveToolEvents(events).filter((event) => event.name === 'connector_message_tool')
|
|
548
|
+
if (connectorEvents.length === 0) return text
|
|
549
|
+
if (connectorEvents.some((event) => connectorToolEventSucceeded(event))) return text
|
|
550
|
+
|
|
551
|
+
const latestFailure = [...connectorEvents]
|
|
552
|
+
.reverse()
|
|
553
|
+
.find((event) => event.error === true && typeof event.output === 'string' && event.output.trim())
|
|
554
|
+
|
|
555
|
+
const failureSummary = latestFailure?.output
|
|
556
|
+
? summarizeConnectorToolFailure(latestFailure.output)
|
|
557
|
+
: 'I could not confirm that the connector actually sent anything.'
|
|
558
|
+
|
|
559
|
+
return `I couldn't send that through the configured connector. ${failureSummary}`.trim()
|
|
560
|
+
}
|
|
561
|
+
|
|
421
562
|
function parseKeyValueArgs(raw: string): Record<string, string> {
|
|
422
563
|
const out: Record<string, string> = {}
|
|
423
564
|
const regex = /([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*("([^"]*)"|'([^']*)'|[^\s,]+)/g
|
|
@@ -560,6 +701,17 @@ function hasToolEnabled(session: SessionWithTools, toolName: string): boolean {
|
|
|
560
701
|
return pluginIdMatches(session?.plugins || session?.tools || [], toolName)
|
|
561
702
|
}
|
|
562
703
|
|
|
704
|
+
export function hasDirectLocalCodingTools(session: SessionWithTools): boolean {
|
|
705
|
+
return [
|
|
706
|
+
'shell',
|
|
707
|
+
'execute_command',
|
|
708
|
+
'files',
|
|
709
|
+
'edit_file',
|
|
710
|
+
'openclaw_workspace',
|
|
711
|
+
'sandbox',
|
|
712
|
+
].some((toolName) => hasToolEnabled(session, toolName))
|
|
713
|
+
}
|
|
714
|
+
|
|
563
715
|
function enabledDelegationTools(session: SessionWithTools): DelegateTool[] {
|
|
564
716
|
const tools: DelegateTool[] = []
|
|
565
717
|
if (hasToolEnabled(session, 'claude_code') || hasToolEnabled(session, 'delegate')) tools.push('delegate_to_claude_code')
|
|
@@ -671,8 +823,39 @@ function syncSessionFromAgent(sessionId: string): void {
|
|
|
671
823
|
}
|
|
672
824
|
const isShortcutChat = session.shortcutForAgentId === agent.id || agent.threadSessionId === sessionId
|
|
673
825
|
if (isShortcutChat) {
|
|
826
|
+
const desiredPlugins = Array.isArray(agent.plugins) ? [...agent.plugins] : []
|
|
827
|
+
const currentPlugins = Array.isArray(session.plugins) ? [...session.plugins] : []
|
|
828
|
+
if (JSON.stringify(currentPlugins) !== JSON.stringify(desiredPlugins)) {
|
|
829
|
+
session.plugins = desiredPlugins
|
|
830
|
+
changed = true
|
|
831
|
+
}
|
|
674
832
|
if (session.shortcutForAgentId !== agent.id) { session.shortcutForAgentId = agent.id; changed = true }
|
|
675
833
|
if (session.name !== agent.name) { session.name = agent.name; changed = true }
|
|
834
|
+
const desiredHeartbeatEnabled = agent.heartbeatEnabled ?? false
|
|
835
|
+
if ((session.heartbeatEnabled ?? false) !== desiredHeartbeatEnabled) {
|
|
836
|
+
session.heartbeatEnabled = desiredHeartbeatEnabled
|
|
837
|
+
changed = true
|
|
838
|
+
}
|
|
839
|
+
const desiredHeartbeatIntervalSec = agent.heartbeatIntervalSec ?? null
|
|
840
|
+
if ((session.heartbeatIntervalSec ?? null) !== desiredHeartbeatIntervalSec) {
|
|
841
|
+
session.heartbeatIntervalSec = desiredHeartbeatIntervalSec
|
|
842
|
+
changed = true
|
|
843
|
+
}
|
|
844
|
+
const desiredMemoryScopeMode = agent.memoryScopeMode ?? null
|
|
845
|
+
if ((((session as unknown as Record<string, unknown>).memoryScopeMode as string | null | undefined) ?? null) !== desiredMemoryScopeMode) {
|
|
846
|
+
;(session as unknown as Record<string, unknown>).memoryScopeMode = desiredMemoryScopeMode
|
|
847
|
+
changed = true
|
|
848
|
+
}
|
|
849
|
+
const desiredMemoryTierPreference = agent.memoryTierPreference ?? null
|
|
850
|
+
if ((((session as unknown as Record<string, unknown>).memoryTierPreference as string | null | undefined) ?? null) !== desiredMemoryTierPreference) {
|
|
851
|
+
;(session as unknown as Record<string, unknown>).memoryTierPreference = desiredMemoryTierPreference
|
|
852
|
+
changed = true
|
|
853
|
+
}
|
|
854
|
+
const desiredProjectId = agent.projectId ?? null
|
|
855
|
+
if ((session.projectId ?? null) !== desiredProjectId) {
|
|
856
|
+
session.projectId = desiredProjectId
|
|
857
|
+
changed = true
|
|
858
|
+
}
|
|
676
859
|
}
|
|
677
860
|
|
|
678
861
|
if (changed) {
|
|
@@ -728,6 +911,15 @@ function buildAgentSystemPrompt(session: Session): string | undefined {
|
|
|
728
911
|
}
|
|
729
912
|
}
|
|
730
913
|
|
|
914
|
+
// 5b. Workspace context files (HEARTBEAT.md, IDENTITY.md, AGENTS.md, etc.)
|
|
915
|
+
try {
|
|
916
|
+
const { buildWorkspaceContext } = require('./workspace-context')
|
|
917
|
+
const wsCtx = buildWorkspaceContext({ cwd: session.cwd })
|
|
918
|
+
if (wsCtx.block) parts.push(wsCtx.block)
|
|
919
|
+
} catch {
|
|
920
|
+
// Workspace context is non-critical
|
|
921
|
+
}
|
|
922
|
+
|
|
731
923
|
// 6. Thinking & Output Format (OpenClaw Style)
|
|
732
924
|
const thinkingHint = [
|
|
733
925
|
'## Output Format',
|
|
@@ -737,6 +929,14 @@ function buildAgentSystemPrompt(session: Session): string | undefined {
|
|
|
737
929
|
]
|
|
738
930
|
parts.push(thinkingHint.join('\n'))
|
|
739
931
|
|
|
932
|
+
const enabledPlugins = Array.isArray(session.plugins) ? session.plugins : (Array.isArray(agent.plugins) ? agent.plugins : [])
|
|
933
|
+
const toolDisciplineLines = buildToolDisciplineLines(enabledPlugins)
|
|
934
|
+
if (toolDisciplineLines.length > 0) parts.push(['## Tool Discipline', ...toolDisciplineLines].join('\n'))
|
|
935
|
+
const operatingGuidance = getPluginManager().collectOperatingGuidance(enabledPlugins)
|
|
936
|
+
if (operatingGuidance.length > 0) parts.push(['## Tool Guidance', ...operatingGuidance].join('\n'))
|
|
937
|
+
const capabilityLines = getPluginManager().collectCapabilityDescriptions(enabledPlugins)
|
|
938
|
+
if (capabilityLines.length > 0) parts.push(['## Tool Capabilities', ...capabilityLines].join('\n'))
|
|
939
|
+
|
|
740
940
|
// 7. Heartbeat Guidance
|
|
741
941
|
parts.push([
|
|
742
942
|
'## Heartbeats',
|
|
@@ -826,8 +1026,34 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
826
1026
|
|
|
827
1027
|
const appSettings = loadSettings()
|
|
828
1028
|
const agentForSession = session.agentId ? loadAgents()[session.agentId] : null
|
|
1029
|
+
if (isAgentDisabled(agentForSession)) {
|
|
1030
|
+
const disabledError = buildAgentDisabledMessage(agentForSession, 'run chats')
|
|
1031
|
+
onEvent?.({ t: 'err', text: disabledError })
|
|
1032
|
+
|
|
1033
|
+
let persisted = false
|
|
1034
|
+
if (!internal) {
|
|
1035
|
+
session.messages.push({
|
|
1036
|
+
role: 'assistant',
|
|
1037
|
+
text: disabledError,
|
|
1038
|
+
time: Date.now(),
|
|
1039
|
+
})
|
|
1040
|
+
session.lastActiveAt = Date.now()
|
|
1041
|
+
saveSessions(sessions)
|
|
1042
|
+
persisted = true
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
return {
|
|
1046
|
+
runId,
|
|
1047
|
+
sessionId,
|
|
1048
|
+
text: disabledError,
|
|
1049
|
+
persisted,
|
|
1050
|
+
toolEvents: [],
|
|
1051
|
+
error: disabledError,
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
829
1054
|
const toolPolicy = resolveSessionToolPolicy(session.plugins, appSettings)
|
|
830
1055
|
const isHeartbeatRun = isInternalHeartbeatRun(internal, source)
|
|
1056
|
+
const isAutonomousInternalRun = internal && source !== 'chat'
|
|
831
1057
|
const isAutoRunNoHistory = isHeartbeatRun
|
|
832
1058
|
const heartbeatStatusOnly = false
|
|
833
1059
|
if (shouldApplySessionFreshnessReset(source)) {
|
|
@@ -847,6 +1073,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
847
1073
|
saveSessions(sessions)
|
|
848
1074
|
}
|
|
849
1075
|
}
|
|
1076
|
+
if (isAutonomousInternalRun) {
|
|
1077
|
+
try { syncSessionArchiveMemory(session, { agent: agentForSession }) } catch { /* archive sync is best-effort */ }
|
|
1078
|
+
}
|
|
850
1079
|
const pluginsForRun = heartbeatStatusOnly ? [] : toolPolicy.enabledPlugins
|
|
851
1080
|
let sessionForRun = pluginsForRun === session.plugins
|
|
852
1081
|
? session
|
|
@@ -1017,9 +1246,72 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1017
1246
|
|
|
1018
1247
|
let thinkingText = ''
|
|
1019
1248
|
let streamingPartialText = ''
|
|
1249
|
+
let lastPartialSaveAt = 0
|
|
1250
|
+
let lastPartialSnapshotKey = ''
|
|
1251
|
+
let partialSaveTimeout: ReturnType<typeof setTimeout> | null = null
|
|
1252
|
+
|
|
1253
|
+
const persistStreamingAssistantArtifact = () => {
|
|
1254
|
+
partialSaveTimeout = null
|
|
1255
|
+
const persistedToolEvents = toolEvents.length ? dedupeConsecutiveToolEvents([...toolEvents]) : []
|
|
1256
|
+
if (!hasPersistableAssistantPayload(streamingPartialText, thinkingText, persistedToolEvents)) return
|
|
1257
|
+
|
|
1258
|
+
const snapshotKey = JSON.stringify([
|
|
1259
|
+
streamingPartialText,
|
|
1260
|
+
thinkingText,
|
|
1261
|
+
getToolEventsSnapshotKey(persistedToolEvents),
|
|
1262
|
+
])
|
|
1263
|
+
if (snapshotKey === lastPartialSnapshotKey) return
|
|
1264
|
+
|
|
1265
|
+
lastPartialSnapshotKey = snapshotKey
|
|
1266
|
+
lastPartialSaveAt = Date.now()
|
|
1267
|
+
|
|
1268
|
+
try {
|
|
1269
|
+
const fresh = loadSessions()
|
|
1270
|
+
const current = fresh[sessionId]
|
|
1271
|
+
if (!current) return
|
|
1272
|
+
current.messages = Array.isArray(current.messages) ? current.messages : []
|
|
1273
|
+
const partialMsg: Message = {
|
|
1274
|
+
role: 'assistant',
|
|
1275
|
+
text: streamingPartialText,
|
|
1276
|
+
time: Date.now(),
|
|
1277
|
+
streaming: true,
|
|
1278
|
+
thinking: thinkingText || undefined,
|
|
1279
|
+
toolEvents: persistedToolEvents.length ? persistedToolEvents : undefined,
|
|
1280
|
+
}
|
|
1281
|
+
upsertStreamingAssistantArtifact(current.messages, partialMsg, {
|
|
1282
|
+
minIndex: runMessageStartIndex,
|
|
1283
|
+
minTime: runStartedAt,
|
|
1284
|
+
})
|
|
1285
|
+
fresh[sessionId] = current
|
|
1286
|
+
saveSessions(fresh)
|
|
1287
|
+
notify(`messages:${sessionId}`)
|
|
1288
|
+
} catch { /* partial save is best-effort */ }
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
const queuePartialAssistantPersist = (immediate = false) => {
|
|
1292
|
+
const now = Date.now()
|
|
1293
|
+
const minIntervalMs = 400
|
|
1294
|
+
if (immediate || now - lastPartialSaveAt >= minIntervalMs) {
|
|
1295
|
+
if (partialSaveTimeout) {
|
|
1296
|
+
clearTimeout(partialSaveTimeout)
|
|
1297
|
+
partialSaveTimeout = null
|
|
1298
|
+
}
|
|
1299
|
+
persistStreamingAssistantArtifact()
|
|
1300
|
+
return
|
|
1301
|
+
}
|
|
1302
|
+
if (partialSaveTimeout) return
|
|
1303
|
+
partialSaveTimeout = setTimeout(() => {
|
|
1304
|
+
persistStreamingAssistantArtifact()
|
|
1305
|
+
}, minIntervalMs - (now - lastPartialSaveAt))
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1020
1308
|
const emit = (ev: SSEEvent) => {
|
|
1309
|
+
let shouldPersistPartial = false
|
|
1310
|
+
let immediatePartialPersist = false
|
|
1021
1311
|
if (ev.t === 'd' && typeof ev.text === 'string') {
|
|
1022
1312
|
streamingPartialText += ev.text
|
|
1313
|
+
shouldPersistPartial = true
|
|
1314
|
+
immediatePartialPersist = streamingPartialText.length === ev.text.length
|
|
1023
1315
|
}
|
|
1024
1316
|
if (ev.t === 'err' && typeof ev.text === 'string') {
|
|
1025
1317
|
const trimmed = ev.text.trim()
|
|
@@ -1030,6 +1322,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1030
1322
|
}
|
|
1031
1323
|
if (ev.t === 'thinking' && ev.text) {
|
|
1032
1324
|
thinkingText += ev.text
|
|
1325
|
+
shouldPersistPartial = true
|
|
1033
1326
|
}
|
|
1034
1327
|
if (ev.t === 'md' && ev.text) {
|
|
1035
1328
|
try {
|
|
@@ -1043,36 +1336,18 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1043
1336
|
} catch { /* ignore non-JSON md events */ }
|
|
1044
1337
|
}
|
|
1045
1338
|
collectToolEvent(ev, toolEvents)
|
|
1339
|
+
if (ev.t === 'tool_call' || ev.t === 'tool_result') {
|
|
1340
|
+
shouldPersistPartial = true
|
|
1341
|
+
immediatePartialPersist = true
|
|
1342
|
+
}
|
|
1343
|
+
if (shouldPersistPartial) queuePartialAssistantPersist(immediatePartialPersist)
|
|
1046
1344
|
onEvent?.(ev)
|
|
1047
1345
|
}
|
|
1048
1346
|
|
|
1049
1347
|
// Periodic partial save so a browser refresh doesn't lose the in-flight response.
|
|
1050
|
-
|
|
1051
|
-
const PARTIAL_SAVE_INTERVAL_MS = 5000
|
|
1348
|
+
const PARTIAL_SAVE_INTERVAL_MS = 2000
|
|
1052
1349
|
const partialSaveTimer = setInterval(() => {
|
|
1053
|
-
|
|
1054
|
-
lastPartialSaveLen = streamingPartialText.length
|
|
1055
|
-
try {
|
|
1056
|
-
const fresh = loadSessions()
|
|
1057
|
-
const current = fresh[sessionId]
|
|
1058
|
-
if (!current) return
|
|
1059
|
-
current.messages = Array.isArray(current.messages) ? current.messages : []
|
|
1060
|
-
const partialMsg: Message = {
|
|
1061
|
-
role: 'assistant',
|
|
1062
|
-
text: streamingPartialText,
|
|
1063
|
-
time: Date.now(),
|
|
1064
|
-
streaming: true,
|
|
1065
|
-
toolEvents: toolEvents.length ? dedupeConsecutiveToolEvents([...toolEvents]) : undefined,
|
|
1066
|
-
}
|
|
1067
|
-
upsertStreamingAssistantArtifact(current.messages, partialMsg, {
|
|
1068
|
-
minIndex: runMessageStartIndex,
|
|
1069
|
-
minTime: runStartedAt,
|
|
1070
|
-
})
|
|
1071
|
-
fresh[sessionId] = current
|
|
1072
|
-
saveSessions(fresh)
|
|
1073
|
-
notify(`messages:${sessionId}`)
|
|
1074
|
-
} catch { /* partial save is best-effort */ }
|
|
1075
|
-
}
|
|
1350
|
+
persistStreamingAssistantArtifact()
|
|
1076
1351
|
}, PARTIAL_SAVE_INTERVAL_MS)
|
|
1077
1352
|
|
|
1078
1353
|
const parseAndEmit = (raw: string) => {
|
|
@@ -1105,7 +1380,10 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1105
1380
|
const responseCacheConfig = resolveLlmResponseCacheConfig(appSettings)
|
|
1106
1381
|
let responseCacheHit = false
|
|
1107
1382
|
let responseCacheInput: LlmResponseCacheKeyInput | null = null
|
|
1108
|
-
const
|
|
1383
|
+
const useLocalOpenClawNativeRuntime = providerType === 'openclaw' && isLocalOpenClawEndpoint(sessionForRun.apiEndpoint)
|
|
1384
|
+
const hasPlugins = !!(sessionForRun.plugins?.length || sessionForRun.tools?.length)
|
|
1385
|
+
&& !NON_LANGGRAPH_PROVIDER_IDS.has(providerType)
|
|
1386
|
+
&& !useLocalOpenClawNativeRuntime
|
|
1109
1387
|
|
|
1110
1388
|
let durationMs = 0
|
|
1111
1389
|
const startTs = Date.now()
|
|
@@ -1117,9 +1395,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1117
1395
|
? getSessionMessages(sessionId).slice(-6)
|
|
1118
1396
|
: undefined
|
|
1119
1397
|
|
|
1120
|
-
console.log(`[chat-execution] provider=${providerType}, hasPlugins=${hasPlugins}, imagePath=${imagePath || 'none'}, attachedFiles=${attachedFiles?.length || 0}, plugins=${(sessionForRun.plugins || sessionForRun.tools || []).length}`)
|
|
1398
|
+
console.log(`[chat-execution] provider=${providerType}, hasPlugins=${hasPlugins}, localOpenClawNative=${useLocalOpenClawNativeRuntime}, imagePath=${imagePath || 'none'}, attachedFiles=${attachedFiles?.length || 0}, plugins=${(sessionForRun.plugins || sessionForRun.tools || []).length}`)
|
|
1121
1399
|
if (hasPlugins) {
|
|
1122
|
-
|
|
1400
|
+
const result = await streamAgentChat({
|
|
1123
1401
|
session: sessionForRun,
|
|
1124
1402
|
message: effectiveMessage,
|
|
1125
1403
|
imagePath,
|
|
@@ -1129,7 +1407,8 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1129
1407
|
write: (raw) => parseAndEmit(raw),
|
|
1130
1408
|
history: heartbeatHistory ?? applyContextClearBoundary(getSessionMessages(sessionId)),
|
|
1131
1409
|
signal: abortController.signal,
|
|
1132
|
-
})
|
|
1410
|
+
})
|
|
1411
|
+
fullResponse = result.finalResponse || result.fullText
|
|
1133
1412
|
} else {
|
|
1134
1413
|
const directHistorySnapshot = isAutoRunNoHistory
|
|
1135
1414
|
? getSessionMessages(sessionId).slice(-6)
|
|
@@ -1201,6 +1480,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1201
1480
|
})
|
|
1202
1481
|
} finally {
|
|
1203
1482
|
clearInterval(partialSaveTimer)
|
|
1483
|
+
if (partialSaveTimeout) clearTimeout(partialSaveTimeout)
|
|
1204
1484
|
active.delete(sessionId)
|
|
1205
1485
|
if (signal) signal.removeEventListener('abort', abortFromOutside)
|
|
1206
1486
|
}
|
|
@@ -1261,12 +1541,18 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1261
1541
|
return false
|
|
1262
1542
|
}
|
|
1263
1543
|
const agent = session.agentId ? loadAgents()[session.agentId] : null
|
|
1544
|
+
const activeProjectContext = resolveActiveProjectContext(session)
|
|
1264
1545
|
const { tools, cleanup } = await buildSessionTools(session.cwd, sessionForRun.plugins || sessionForRun.tools || [], {
|
|
1265
1546
|
agentId: session.agentId || null,
|
|
1266
1547
|
sessionId,
|
|
1267
1548
|
platformAssignScope: agent?.platformAssignScope || 'self',
|
|
1268
1549
|
mcpServerIds: agent?.mcpServerIds,
|
|
1269
1550
|
mcpDisabledTools: agent?.mcpDisabledTools,
|
|
1551
|
+
projectId: activeProjectContext.projectId,
|
|
1552
|
+
projectRoot: activeProjectContext.projectRoot,
|
|
1553
|
+
projectName: activeProjectContext.project?.name || null,
|
|
1554
|
+
projectDescription: activeProjectContext.project?.description || null,
|
|
1555
|
+
memoryScopeMode: (((session as unknown as Record<string, unknown>).memoryScopeMode as string | null | undefined) ?? agent?.memoryScopeMode ?? null),
|
|
1270
1556
|
})
|
|
1271
1557
|
try {
|
|
1272
1558
|
const directTool = tools.find((t) => t?.name === toolName) as StructuredToolInterface | undefined
|
|
@@ -1277,10 +1563,11 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1277
1563
|
const selectedTool = directTool || tools.find((t) => t?.name === translated.toolName) as StructuredToolInterface | undefined
|
|
1278
1564
|
if (!selectedTool?.invoke) return false
|
|
1279
1565
|
const toolInput = JSON.stringify(translated.args)
|
|
1280
|
-
|
|
1566
|
+
const toolCallId = genId()
|
|
1567
|
+
emit({ t: 'tool_call', toolName, toolInput, toolCallId })
|
|
1281
1568
|
const toolOutput = await selectedTool.invoke(translated.args)
|
|
1282
1569
|
const outputText = typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput)
|
|
1283
|
-
emit({ t: 'tool_result', toolName, toolOutput: outputText })
|
|
1570
|
+
emit({ t: 'tool_result', toolName, toolOutput: outputText, toolCallId })
|
|
1284
1571
|
const delegateResponse = (
|
|
1285
1572
|
toolName === 'delegate'
|
|
1286
1573
|
|| toolName.startsWith('delegate_to_')
|
|
@@ -1334,6 +1621,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1334
1621
|
const shouldAutoDelegateCoding = (!internal && source === 'chat')
|
|
1335
1622
|
&& enabledDelegateTools.length > 0
|
|
1336
1623
|
&& !hasDelegationCall
|
|
1624
|
+
&& calledNames.size === 0
|
|
1625
|
+
&& !requestedToolNames.length
|
|
1626
|
+
&& !hasDirectLocalCodingTools(sessionForRun)
|
|
1337
1627
|
&& routingDecision?.intent === 'coding'
|
|
1338
1628
|
|
|
1339
1629
|
if (shouldAutoDelegateCoding) {
|
|
@@ -1422,10 +1712,28 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1422
1712
|
}
|
|
1423
1713
|
}
|
|
1424
1714
|
|
|
1425
|
-
|
|
1426
|
-
errorMessage
|
|
1715
|
+
const terminalError = deriveTerminalRunError({
|
|
1716
|
+
errorMessage,
|
|
1717
|
+
fullResponse: fullResponse || '',
|
|
1718
|
+
streamErrors,
|
|
1719
|
+
toolEvents,
|
|
1720
|
+
internal,
|
|
1721
|
+
})
|
|
1722
|
+
if (terminalError && terminalError !== errorMessage) {
|
|
1723
|
+
if (!errorMessage) {
|
|
1724
|
+
log.warn('chat-run', `Run ended without a visible response for session ${sessionId}`, {
|
|
1725
|
+
runId,
|
|
1726
|
+
source,
|
|
1727
|
+
internal,
|
|
1728
|
+
provider: providerType,
|
|
1729
|
+
messagePreview: effectiveMessage.slice(0, 200),
|
|
1730
|
+
inferredError: terminalError,
|
|
1731
|
+
})
|
|
1732
|
+
}
|
|
1733
|
+
errorMessage = terminalError
|
|
1427
1734
|
}
|
|
1428
1735
|
|
|
1736
|
+
const persistedToolEvents = dedupeConsecutiveToolEvents(toolEvents)
|
|
1429
1737
|
let finalText = (fullResponse || '').trim() || (!internal && errorMessage ? `Error: ${errorMessage}` : '')
|
|
1430
1738
|
if (pluginsForRun.length > 0 && finalText && !isHeartbeatRun) {
|
|
1431
1739
|
try {
|
|
@@ -1436,27 +1744,30 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1436
1744
|
)
|
|
1437
1745
|
} catch { /* outbound transforms are non-critical */ }
|
|
1438
1746
|
}
|
|
1747
|
+
finalText = reconcileConnectorDeliveryText(finalText, persistedToolEvents)
|
|
1439
1748
|
finalText = normalizeAssistantArtifactLinks(finalText, session.cwd)
|
|
1440
|
-
const
|
|
1441
|
-
const
|
|
1749
|
+
const rawTextForPersistence = stripMainLoopMetaForPersistence(finalText)
|
|
1750
|
+
const hiddenControlOnly = shouldSuppressHiddenControlText(rawTextForPersistence)
|
|
1751
|
+
const textForPersistence = stripHiddenControlTokens(rawTextForPersistence)
|
|
1752
|
+
const persistedText = getPersistedAssistantText(textForPersistence, persistedToolEvents)
|
|
1442
1753
|
|
|
1443
|
-
if (isHeartbeatRun &&
|
|
1444
|
-
const heartbeatStatus = extractHeartbeatStatus(
|
|
1754
|
+
if (isHeartbeatRun && rawTextForPersistence) {
|
|
1755
|
+
const heartbeatStatus = extractHeartbeatStatus(rawTextForPersistence)
|
|
1445
1756
|
if (heartbeatStatus) emit({ t: 'status', text: JSON.stringify(heartbeatStatus) })
|
|
1446
1757
|
}
|
|
1447
1758
|
|
|
1448
1759
|
// HEARTBEAT_OK suppression
|
|
1449
1760
|
const heartbeatConfig = input.heartbeatConfig
|
|
1450
1761
|
let heartbeatClassification: 'suppress' | 'strip' | 'keep' | null = null
|
|
1451
|
-
if (isHeartbeatRun &&
|
|
1452
|
-
heartbeatClassification = classifyHeartbeatResponse(
|
|
1762
|
+
if (isHeartbeatRun && rawTextForPersistence.length > 0) {
|
|
1763
|
+
heartbeatClassification = classifyHeartbeatResponse(rawTextForPersistence, heartbeatConfig?.ackMaxChars ?? 300, toolEvents.length > 0)
|
|
1453
1764
|
|
|
1454
1765
|
// Deduplication logic from OpenClaw (nagging prevention)
|
|
1455
1766
|
// If the model repeats itself exactly within 24h, suppress the heartbeat alert.
|
|
1456
1767
|
if (heartbeatClassification !== 'suppress' && !toolEvents.length) {
|
|
1457
1768
|
const prevText = session.lastHeartbeatText || ''
|
|
1458
1769
|
const prevSentAt = session.lastHeartbeatSentAt || 0
|
|
1459
|
-
const isDuplicate = prevText.trim() ===
|
|
1770
|
+
const isDuplicate = prevText.trim() === persistedText.trim()
|
|
1460
1771
|
&& (Date.now() - prevSentAt) < 24 * 60 * 60 * 1000
|
|
1461
1772
|
if (isDuplicate) {
|
|
1462
1773
|
heartbeatClassification = 'suppress'
|
|
@@ -1469,7 +1780,8 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1469
1780
|
notify(`heartbeat:agent:${session.agentId}`)
|
|
1470
1781
|
}
|
|
1471
1782
|
|
|
1472
|
-
const shouldPersistAssistant =
|
|
1783
|
+
const shouldPersistAssistant = !hiddenControlOnly
|
|
1784
|
+
&& hasPersistableAssistantPayload(persistedText, thinkingText, persistedToolEvents)
|
|
1473
1785
|
&& heartbeatClassification !== 'suppress'
|
|
1474
1786
|
|
|
1475
1787
|
const normalizeResumeId = (value: unknown): string | null =>
|
|
@@ -1480,16 +1792,14 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1480
1792
|
if (current) {
|
|
1481
1793
|
current.messages = Array.isArray(current.messages) ? current.messages : []
|
|
1482
1794
|
const currentAgent = current.agentId ? loadAgents()[current.agentId] : null
|
|
1483
|
-
|
|
1484
|
-
changed = pruneStreamingAssistantArtifacts(current.messages, {
|
|
1795
|
+
pruneStreamingAssistantArtifacts(current.messages, {
|
|
1485
1796
|
minIndex: runMessageStartIndex,
|
|
1486
1797
|
minTime: runStartedAt,
|
|
1487
|
-
})
|
|
1798
|
+
})
|
|
1488
1799
|
const persistField = (key: string, value: unknown) => {
|
|
1489
1800
|
const normalized = normalizeResumeId(value)
|
|
1490
1801
|
if ((current as Record<string, unknown>)[key] !== normalized) {
|
|
1491
1802
|
;(current as Record<string, unknown>)[key] = normalized
|
|
1492
|
-
changed = true
|
|
1493
1803
|
}
|
|
1494
1804
|
}
|
|
1495
1805
|
|
|
@@ -1508,18 +1818,15 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1508
1818
|
claudeCode: normalizeResumeId(sr.claudeCode ?? cr.claudeCode),
|
|
1509
1819
|
codex: normalizeResumeId(sr.codex ?? cr.codex),
|
|
1510
1820
|
opencode: normalizeResumeId(sr.opencode ?? cr.opencode),
|
|
1821
|
+
gemini: normalizeResumeId(sr.gemini ?? cr.gemini),
|
|
1511
1822
|
}
|
|
1512
1823
|
if (JSON.stringify(currentResume) !== JSON.stringify(nextResume)) {
|
|
1513
1824
|
current.delegateResumeIds = nextResume
|
|
1514
|
-
changed = true
|
|
1515
1825
|
}
|
|
1516
1826
|
}
|
|
1517
1827
|
|
|
1518
1828
|
if (shouldPersistAssistant) {
|
|
1519
1829
|
const persistedKind = isHeartbeatRun ? 'heartbeat' : 'chat'
|
|
1520
|
-
const persistedText = heartbeatClassification === 'strip'
|
|
1521
|
-
? textForPersistence.replace(/HEARTBEAT_OK/gi, '').trim()
|
|
1522
|
-
: textForPersistence
|
|
1523
1830
|
const nowTs = Date.now()
|
|
1524
1831
|
const nextAssistantMessage: Message = {
|
|
1525
1832
|
role: 'assistant',
|
|
@@ -1544,7 +1851,6 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1544
1851
|
current.lastHeartbeatText = persistedText
|
|
1545
1852
|
current.lastHeartbeatSentAt = nowTs
|
|
1546
1853
|
}
|
|
1547
|
-
changed = true
|
|
1548
1854
|
try {
|
|
1549
1855
|
await getPluginManager().runHook('onMessage', { session: current, message: nextAssistantMessage }, { enabledIds: pluginsForRun })
|
|
1550
1856
|
} catch { /* onMessage hooks are non-critical */ }
|
|
@@ -1603,7 +1909,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1603
1909
|
}
|
|
1604
1910
|
}
|
|
1605
1911
|
if (isHeartbeatRun && heartbeatClassification === 'suppress') {
|
|
1606
|
-
|
|
1912
|
+
pruneSuppressedHeartbeatStreamMessage(current.messages)
|
|
1607
1913
|
}
|
|
1608
1914
|
|
|
1609
1915
|
// Fire afterChatTurn hook for all enabled plugins (memory auto-save, logging, etc.)
|
|
@@ -1614,6 +1920,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1614
1920
|
response: textForPersistence,
|
|
1615
1921
|
source,
|
|
1616
1922
|
internal,
|
|
1923
|
+
toolEvents: persistedToolEvents,
|
|
1617
1924
|
}, { enabledIds: pluginsForRun })
|
|
1618
1925
|
} catch { /* afterChatTurn hooks are non-critical */ }
|
|
1619
1926
|
|
|
@@ -1623,10 +1930,8 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1623
1930
|
}
|
|
1624
1931
|
|
|
1625
1932
|
refreshSessionIdentityState(current, currentAgent)
|
|
1626
|
-
changed = true
|
|
1627
1933
|
try {
|
|
1628
|
-
|
|
1629
|
-
if (archiveSync.stored) changed = true
|
|
1934
|
+
syncSessionArchiveMemory(current, { agent: currentAgent })
|
|
1630
1935
|
} catch { /* archive sync is best-effort */ }
|
|
1631
1936
|
fresh[sessionId] = current
|
|
1632
1937
|
saveSessions(fresh)
|
|
@@ -1636,7 +1941,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1636
1941
|
return {
|
|
1637
1942
|
runId,
|
|
1638
1943
|
sessionId,
|
|
1639
|
-
text:
|
|
1944
|
+
text: hiddenControlOnly ? '' : textForPersistence,
|
|
1640
1945
|
persisted: shouldPersistAssistant,
|
|
1641
1946
|
toolEvents: persistedToolEvents,
|
|
1642
1947
|
error: errorMessage,
|