@swarmclawai/swarmclaw 0.7.2 → 0.7.4
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 +116 -50
- package/bin/package-manager.js +157 -0
- package/bin/package-manager.test.js +90 -0
- package/bin/server-cmd.js +38 -7
- package/bin/swarmclaw.js +54 -4
- package/bin/update-cmd.js +48 -10
- package/bin/update-cmd.test.js +55 -0
- package/package.json +8 -3
- package/scripts/postinstall.mjs +26 -0
- package/src/app/api/agents/[id]/route.ts +43 -0
- package/src/app/api/agents/[id]/thread/route.ts +39 -8
- package/src/app/api/agents/route.ts +35 -2
- package/src/app/api/auth/route.ts +77 -8
- package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
- package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/route.ts +6 -0
- package/src/app/api/chats/[id]/browser/route.ts +5 -1
- package/src/app/api/chats/[id]/chat/route.ts +7 -3
- package/src/app/api/chats/[id]/messages/route.ts +19 -13
- package/src/app/api/chats/[id]/route.ts +30 -0
- package/src/app/api/chats/[id]/stop/route.ts +6 -1
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/route.ts +23 -1
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
- package/src/app/api/connectors/doctor/route.ts +13 -0
- package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
- package/src/app/api/external-agents/[id]/route.ts +31 -0
- package/src/app/api/external-agents/register/route.ts +3 -0
- package/src/app/api/external-agents/route.ts +66 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/gateways/[id]/health/route.ts +28 -0
- package/src/app/api/gateways/[id]/route.ts +79 -0
- package/src/app/api/gateways/route.ts +57 -0
- package/src/app/api/memory/maintenance/route.ts +11 -1
- package/src/app/api/openclaw/agent-files/route.ts +27 -4
- package/src/app/api/openclaw/gateway/route.ts +10 -7
- package/src/app/api/openclaw/skills/route.ts +12 -4
- package/src/app/api/plugins/dependencies/route.ts +24 -0
- package/src/app/api/plugins/install/route.ts +15 -92
- package/src/app/api/plugins/route.ts +3 -26
- package/src/app/api/plugins/settings/route.ts +17 -12
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
- package/src/app/api/schedules/[id]/route.ts +38 -9
- package/src/app/api/schedules/route.ts +51 -28
- package/src/app/api/settings/route.ts +55 -17
- package/src/app/api/setup/doctor/route.ts +6 -4
- package/src/app/api/tasks/[id]/route.ts +16 -6
- package/src/app/api/tasks/bulk/route.ts +3 -3
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +135 -17
- package/src/cli/binary.test.js +142 -0
- package/src/cli/index.js +38 -11
- package/src/cli/index.test.js +195 -0
- package/src/cli/index.ts +21 -12
- package/src/cli/server-cmd.test.js +59 -0
- package/src/cli/spec.js +20 -2
- package/src/components/agents/agent-card.tsx +15 -12
- package/src/components/agents/agent-chat-list.tsx +101 -1
- package/src/components/agents/agent-list.tsx +46 -9
- package/src/components/agents/agent-sheet.tsx +456 -23
- package/src/components/agents/inspector-panel.tsx +110 -49
- package/src/components/agents/sandbox-env-panel.tsx +4 -1
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +970 -275
- package/src/components/chat/chat-area.tsx +70 -27
- package/src/components/chat/chat-card.tsx +6 -21
- package/src/components/chat/chat-header.tsx +263 -366
- package/src/components/chat/chat-list.tsx +62 -26
- package/src/components/chat/checkpoint-timeline.tsx +1 -1
- package/src/components/chat/message-list.tsx +145 -19
- package/src/components/chatrooms/chatroom-input.tsx +96 -33
- package/src/components/chatrooms/chatroom-list.tsx +141 -72
- package/src/components/chatrooms/chatroom-message.tsx +7 -6
- package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
- package/src/components/chatrooms/chatroom-view.tsx +422 -209
- package/src/components/chatrooms/reaction-picker.tsx +38 -33
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +217 -0
- package/src/components/gateways/gateway-sheet.tsx +567 -0
- package/src/components/home/home-view.tsx +128 -4
- package/src/components/input/chat-input.tsx +135 -86
- package/src/components/layout/app-layout.tsx +385 -194
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/memory/memory-browser.tsx +71 -6
- package/src/components/memory/memory-card.tsx +18 -0
- package/src/components/memory/memory-detail.tsx +58 -31
- package/src/components/memory/memory-sheet.tsx +32 -4
- package/src/components/plugins/plugin-list.tsx +15 -3
- package/src/components/plugins/plugin-sheet.tsx +118 -9
- package/src/components/projects/project-detail.tsx +189 -1
- package/src/components/providers/provider-list.tsx +158 -2
- package/src/components/providers/provider-sheet.tsx +81 -70
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/bottom-sheet.tsx +31 -15
- package/src/components/shared/command-palette.tsx +111 -24
- package/src/components/shared/confirm-dialog.tsx +45 -30
- package/src/components/shared/model-combobox.tsx +90 -8
- package/src/components/shared/settings/plugin-manager.tsx +20 -4
- package/src/components/shared/settings/section-capability-policy.tsx +105 -0
- package/src/components/shared/settings/section-heartbeat.tsx +88 -6
- package/src/components/shared/settings/section-orchestrator.tsx +6 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
- package/src/components/shared/settings/section-secrets.tsx +6 -6
- package/src/components/shared/settings/section-user-preferences.tsx +1 -1
- package/src/components/shared/settings/section-voice.tsx +5 -1
- package/src/components/shared/settings/section-web-search.tsx +10 -2
- package/src/components/shared/settings/settings-page.tsx +248 -47
- package/src/components/tasks/approvals-panel.tsx +211 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/usage/metrics-dashboard.tsx +74 -1
- package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +7 -7
- package/src/lib/auth.ts +17 -0
- package/src/lib/chat-streaming-state.test.ts +108 -0
- package/src/lib/chat-streaming-state.ts +108 -0
- package/src/lib/heartbeat-defaults.ts +48 -0
- package/src/lib/memory-presentation.ts +59 -0
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/provider-model-discovery-client.ts +29 -0
- package/src/lib/providers/index.ts +12 -5
- package/src/lib/runtime-loop.ts +105 -3
- package/src/lib/safe-storage.ts +6 -1
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/agent-runtime-config.test.ts +141 -0
- package/src/lib/server/agent-runtime-config.ts +277 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +264 -0
- package/src/lib/server/approvals.ts +483 -75
- package/src/lib/server/autonomy-runtime.test.ts +341 -0
- package/src/lib/server/browser-state.test.ts +118 -0
- package/src/lib/server/browser-state.ts +123 -0
- package/src/lib/server/build-llm.test.ts +44 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
- package/src/lib/server/chat-execution.ts +402 -125
- package/src/lib/server/chatroom-health.test.ts +26 -0
- package/src/lib/server/chatroom-health.ts +2 -3
- package/src/lib/server/chatroom-helpers.test.ts +74 -2
- package/src/lib/server/chatroom-helpers.ts +144 -11
- package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
- package/src/lib/server/connectors/discord.ts +175 -11
- package/src/lib/server/connectors/doctor.test.ts +80 -0
- package/src/lib/server/connectors/doctor.ts +116 -0
- package/src/lib/server/connectors/manager.ts +994 -130
- package/src/lib/server/connectors/policy.test.ts +222 -0
- package/src/lib/server/connectors/policy.ts +452 -0
- package/src/lib/server/connectors/slack.ts +189 -10
- package/src/lib/server/connectors/telegram.ts +65 -15
- package/src/lib/server/connectors/thread-context.test.ts +44 -0
- package/src/lib/server/connectors/thread-context.ts +72 -0
- package/src/lib/server/connectors/types.ts +41 -11
- package/src/lib/server/daemon-state.ts +62 -3
- package/src/lib/server/data-dir.ts +13 -0
- package/src/lib/server/delegation-jobs.test.ts +140 -0
- package/src/lib/server/delegation-jobs.ts +248 -0
- package/src/lib/server/document-utils.test.ts +47 -0
- package/src/lib/server/document-utils.ts +397 -0
- package/src/lib/server/eval/agent-regression.test.ts +47 -0
- package/src/lib/server/eval/agent-regression.ts +1742 -0
- package/src/lib/server/eval/runner.ts +11 -1
- package/src/lib/server/eval/store.ts +2 -1
- package/src/lib/server/heartbeat-service.ts +23 -43
- package/src/lib/server/heartbeat-source.test.ts +22 -0
- package/src/lib/server/heartbeat-source.ts +7 -0
- package/src/lib/server/identity-continuity.test.ts +77 -0
- package/src/lib/server/identity-continuity.ts +127 -0
- package/src/lib/server/mailbox-utils.ts +347 -0
- package/src/lib/server/main-agent-loop.ts +31 -964
- package/src/lib/server/memory-db.ts +4 -6
- package/src/lib/server/memory-tiers.ts +40 -0
- package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
- package/src/lib/server/openclaw-agent-resolver.ts +128 -0
- package/src/lib/server/openclaw-exec-config.ts +6 -5
- package/src/lib/server/openclaw-gateway.ts +123 -36
- package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
- package/src/lib/server/openclaw-skills-normalize.ts +136 -0
- package/src/lib/server/openclaw-sync.ts +3 -2
- package/src/lib/server/orchestrator-lg.ts +18 -8
- package/src/lib/server/orchestrator.ts +5 -4
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +215 -0
- package/src/lib/server/plugins.ts +832 -69
- package/src/lib/server/provider-health.ts +33 -3
- package/src/lib/server/provider-model-discovery.ts +481 -0
- package/src/lib/server/queue.ts +4 -21
- package/src/lib/server/runtime-settings.test.ts +119 -0
- package/src/lib/server/runtime-settings.ts +12 -92
- package/src/lib/server/schedule-normalization.ts +187 -0
- package/src/lib/server/scheduler.ts +2 -0
- package/src/lib/server/session-archive-memory.test.ts +85 -0
- package/src/lib/server/session-archive-memory.ts +230 -0
- package/src/lib/server/session-mailbox.ts +8 -18
- package/src/lib/server/session-reset-policy.test.ts +99 -0
- package/src/lib/server/session-reset-policy.ts +311 -0
- package/src/lib/server/session-run-manager.ts +33 -80
- package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
- package/src/lib/server/session-tools/calendar.ts +2 -12
- package/src/lib/server/session-tools/connector.ts +109 -8
- package/src/lib/server/session-tools/context.ts +14 -2
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +96 -34
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +406 -20
- package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
- package/src/lib/server/session-tools/discovery.ts +40 -12
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/email.ts +1 -3
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +243 -24
- package/src/lib/server/session-tools/http.ts +9 -3
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +1 -3
- package/src/lib/server/session-tools/index.ts +87 -2
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
- package/src/lib/server/session-tools/memory.ts +35 -3
- package/src/lib/server/session-tools/monitor.ts +162 -12
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +142 -4
- package/src/lib/server/session-tools/plugin-creator.ts +95 -25
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +1 -3
- package/src/lib/server/session-tools/sandbox.ts +51 -92
- package/src/lib/server/session-tools/schedule.ts +20 -10
- package/src/lib/server/session-tools/session-info.ts +58 -4
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
- package/src/lib/server/session-tools/shell.ts +2 -2
- package/src/lib/server/session-tools/subagent.ts +195 -27
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +13 -10
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +947 -108
- package/src/lib/server/storage.ts +255 -10
- package/src/lib/server/stream-agent-chat.test.ts +61 -0
- package/src/lib/server/stream-agent-chat.ts +185 -25
- package/src/lib/server/structured-extract.test.ts +72 -0
- package/src/lib/server/structured-extract.ts +373 -0
- package/src/lib/server/task-mention.test.ts +16 -2
- package/src/lib/server/task-mention.ts +61 -11
- package/src/lib/server/tool-aliases.ts +80 -12
- package/src/lib/server/tool-capability-policy.ts +7 -1
- package/src/lib/server/tool-retry.ts +2 -0
- package/src/lib/server/watch-jobs.test.ts +173 -0
- package/src/lib/server/watch-jobs.ts +532 -0
- package/src/lib/server/ws-hub.ts +5 -3
- package/src/lib/setup-defaults.ts +352 -11
- package/src/lib/tool-definitions.ts +3 -4
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +62 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +43 -7
- package/src/stores/use-chat-store.ts +31 -2
- package/src/stores/use-chatroom-store.ts +153 -26
- package/src/types/index.ts +470 -44
- package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
- package/src/components/chat/new-chat-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -17
- package/src/lib/server/session-run-manager.test.ts +0 -26
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import os from 'os'
|
|
3
|
+
import path from 'path'
|
|
3
4
|
import {
|
|
4
5
|
loadSessions,
|
|
5
6
|
saveSessions,
|
|
@@ -27,6 +28,7 @@ import { getPluginManager } from './plugins'
|
|
|
27
28
|
import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
|
|
28
29
|
import { routeTaskIntent } from './capability-router'
|
|
29
30
|
import { notify } from './ws-hub'
|
|
31
|
+
import { applyResolvedRoute, resolvePrimaryAgentRoute } from './agent-runtime-config'
|
|
30
32
|
import { resolveConcreteToolPolicyBlock, resolveSessionToolPolicy } from './tool-capability-policy'
|
|
31
33
|
import { pluginIdMatches } from './tool-aliases'
|
|
32
34
|
import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
|
|
@@ -38,7 +40,12 @@ import {
|
|
|
38
40
|
} from './llm-response-cache'
|
|
39
41
|
import type { Message, MessageToolEvent, SSEEvent, UsageRecord } from '@/types'
|
|
40
42
|
import { markProviderFailure, markProviderSuccess, rankDelegatesByHealth } from './provider-health'
|
|
43
|
+
import { isHeartbeatSource, isInternalHeartbeatRun } from './heartbeat-source'
|
|
41
44
|
import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
|
|
45
|
+
import { buildIdentityContinuityContext, refreshSessionIdentityState } from './identity-continuity'
|
|
46
|
+
import { syncSessionArchiveMemory } from './session-archive-memory'
|
|
47
|
+
import { evaluateSessionFreshness, resetSessionRuntime, resolveSessionResetPolicy } from './session-reset-policy'
|
|
48
|
+
import { pruneStreamingAssistantArtifacts, upsertStreamingAssistantArtifact } from '@/lib/chat-streaming-state'
|
|
42
49
|
type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli'
|
|
43
50
|
|
|
44
51
|
/** Slice history from the most recent context-clear marker forward */
|
|
@@ -92,6 +99,10 @@ export interface ExecuteChatTurnResult {
|
|
|
92
99
|
estimatedCost?: number
|
|
93
100
|
}
|
|
94
101
|
|
|
102
|
+
export function shouldApplySessionFreshnessReset(source: string): boolean {
|
|
103
|
+
return source !== 'eval'
|
|
104
|
+
}
|
|
105
|
+
|
|
95
106
|
function extractEventJson(line: string): SSEEvent | null {
|
|
96
107
|
if (!line.startsWith('data: ')) return null
|
|
97
108
|
try {
|
|
@@ -101,8 +112,17 @@ function extractEventJson(line: string): SSEEvent | null {
|
|
|
101
112
|
}
|
|
102
113
|
}
|
|
103
114
|
|
|
104
|
-
function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
|
|
115
|
+
export function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
|
|
105
116
|
if (ev.t === 'tool_call') {
|
|
117
|
+
const previous = bag[bag.length - 1]
|
|
118
|
+
if (
|
|
119
|
+
previous
|
|
120
|
+
&& previous.name === (ev.toolName || 'unknown')
|
|
121
|
+
&& previous.input === (ev.toolInput || '')
|
|
122
|
+
&& !previous.output
|
|
123
|
+
) {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
106
126
|
bag.push({
|
|
107
127
|
name: ev.toolName || 'unknown',
|
|
108
128
|
input: ev.toolInput || '',
|
|
@@ -113,15 +133,210 @@ function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
|
|
|
113
133
|
const idx = bag.findLastIndex((e) => e.name === (ev.toolName || 'unknown') && !e.output)
|
|
114
134
|
if (idx === -1) return
|
|
115
135
|
const output = ev.toolOutput || ''
|
|
116
|
-
const isError = /^(Error:|error:)/i.test(output.trim())
|
|
117
|
-
|| output.includes('ECONNREFUSED')
|
|
118
|
-
|| output.includes('ETIMEDOUT')
|
|
119
|
-
|| output.includes('Error:')
|
|
120
136
|
bag[idx] = {
|
|
121
137
|
...bag[idx],
|
|
122
138
|
output,
|
|
123
|
-
error:
|
|
139
|
+
error: isLikelyToolErrorOutput(output) || undefined,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function dedupeConsecutiveToolEvents(events: MessageToolEvent[]): MessageToolEvent[] {
|
|
145
|
+
const sameEvent = (left: MessageToolEvent, right: MessageToolEvent): boolean => (
|
|
146
|
+
left.name === right.name
|
|
147
|
+
&& left.input === right.input
|
|
148
|
+
&& (left.output || '') === (right.output || '')
|
|
149
|
+
&& (left.error === true) === (right.error === true)
|
|
150
|
+
)
|
|
151
|
+
const sameBlock = (startA: number, startB: number, size: number): boolean => {
|
|
152
|
+
for (let offset = 0; offset < size; offset += 1) {
|
|
153
|
+
if (!sameEvent(events[startA + offset], events[startB + offset])) return false
|
|
154
|
+
}
|
|
155
|
+
return true
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const deduped: MessageToolEvent[] = []
|
|
159
|
+
for (let index = 0; index < events.length;) {
|
|
160
|
+
const remaining = events.length - index
|
|
161
|
+
let collapsed = false
|
|
162
|
+
for (let blockSize = Math.floor(remaining / 2); blockSize >= 1; blockSize -= 1) {
|
|
163
|
+
if (!sameBlock(index, index + blockSize, blockSize)) continue
|
|
164
|
+
for (let offset = 0; offset < blockSize; offset += 1) deduped.push(events[index + offset])
|
|
165
|
+
const blockStart = index
|
|
166
|
+
index += blockSize
|
|
167
|
+
while (index + blockSize <= events.length && sameBlock(blockStart, index, blockSize)) {
|
|
168
|
+
index += blockSize
|
|
169
|
+
}
|
|
170
|
+
collapsed = true
|
|
171
|
+
break
|
|
172
|
+
}
|
|
173
|
+
if (collapsed) continue
|
|
174
|
+
deduped.push(events[index])
|
|
175
|
+
index += 1
|
|
176
|
+
}
|
|
177
|
+
return deduped
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function extractDelegateResponse(outputText: string): string | null {
|
|
181
|
+
try {
|
|
182
|
+
const parsed = JSON.parse(outputText) as Record<string, unknown>
|
|
183
|
+
if (typeof parsed.response === 'string' && parsed.response.trim()) return parsed.response.trim()
|
|
184
|
+
if (typeof parsed.result === 'string' && parsed.result.trim()) return parsed.result.trim()
|
|
185
|
+
return null
|
|
186
|
+
} catch {
|
|
187
|
+
return null
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const MANAGE_PLATFORM_RESOURCE_TO_TOOL: Record<string, string> = {
|
|
192
|
+
agent: 'manage_agents',
|
|
193
|
+
agents: 'manage_agents',
|
|
194
|
+
task: 'manage_tasks',
|
|
195
|
+
tasks: 'manage_tasks',
|
|
196
|
+
schedule: 'manage_schedules',
|
|
197
|
+
schedules: 'manage_schedules',
|
|
198
|
+
skill: 'manage_skills',
|
|
199
|
+
skills: 'manage_skills',
|
|
200
|
+
document: 'manage_documents',
|
|
201
|
+
documents: 'manage_documents',
|
|
202
|
+
secret: 'manage_secrets',
|
|
203
|
+
secrets: 'manage_secrets',
|
|
204
|
+
connector: 'manage_connectors',
|
|
205
|
+
connectors: 'manage_connectors',
|
|
206
|
+
session: 'manage_sessions',
|
|
207
|
+
sessions: 'manage_sessions',
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function translateRequestedToolInvocation(
|
|
211
|
+
requestedName: string,
|
|
212
|
+
rawArgs: Record<string, unknown>,
|
|
213
|
+
messageFallback: string,
|
|
214
|
+
availableToolNames?: Iterable<string>,
|
|
215
|
+
): { toolName: string; args: Record<string, unknown> } {
|
|
216
|
+
const available = new Set(availableToolNames || [])
|
|
217
|
+
|
|
218
|
+
if (requestedName === 'web_search') {
|
|
219
|
+
return {
|
|
220
|
+
toolName: 'web',
|
|
221
|
+
args: {
|
|
222
|
+
action: 'search',
|
|
223
|
+
query: typeof rawArgs.query === 'string' ? rawArgs.query : messageFallback.trim(),
|
|
224
|
+
maxResults: typeof rawArgs.maxResults === 'number' ? rawArgs.maxResults : 5,
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (requestedName === 'web_fetch') {
|
|
229
|
+
return {
|
|
230
|
+
toolName: 'web',
|
|
231
|
+
args: {
|
|
232
|
+
action: 'fetch',
|
|
233
|
+
url: rawArgs.url,
|
|
234
|
+
},
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (requestedName === 'delegate_to_claude_code') {
|
|
238
|
+
return { toolName: 'delegate', args: { ...rawArgs, backend: 'claude' } }
|
|
239
|
+
}
|
|
240
|
+
if (requestedName === 'delegate_to_codex_cli') {
|
|
241
|
+
return { toolName: 'delegate', args: { ...rawArgs, backend: 'codex' } }
|
|
242
|
+
}
|
|
243
|
+
if (requestedName === 'delegate_to_opencode_cli') {
|
|
244
|
+
return { toolName: 'delegate', args: { ...rawArgs, backend: 'opencode' } }
|
|
245
|
+
}
|
|
246
|
+
if (requestedName === 'delegate_to_gemini_cli') {
|
|
247
|
+
return { toolName: 'delegate', args: { ...rawArgs, backend: 'gemini' } }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const managePrefix = 'manage_'
|
|
251
|
+
if (requestedName === 'manage_platform') {
|
|
252
|
+
const resource = typeof rawArgs.resource === 'string'
|
|
253
|
+
? rawArgs.resource.trim().toLowerCase()
|
|
254
|
+
: ''
|
|
255
|
+
const specificTool = MANAGE_PLATFORM_RESOURCE_TO_TOOL[resource]
|
|
256
|
+
if (specificTool && available.has(specificTool) && !available.has('manage_platform')) {
|
|
257
|
+
return { toolName: specificTool, args: rawArgs }
|
|
124
258
|
}
|
|
259
|
+
return { toolName: requestedName, args: rawArgs }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (requestedName.startsWith(managePrefix) && requestedName !== 'manage_platform') {
|
|
263
|
+
if (!available.has(requestedName) && available.has('manage_platform')) {
|
|
264
|
+
const resource = requestedName.slice(managePrefix.length)
|
|
265
|
+
if (resource) {
|
|
266
|
+
const { action, id, data, ...rest } = rawArgs
|
|
267
|
+
const nextArgs: Record<string, unknown> = { resource, ...rest }
|
|
268
|
+
if (action !== undefined) nextArgs.action = action
|
|
269
|
+
if (id !== undefined) nextArgs.id = id
|
|
270
|
+
if (data !== undefined) nextArgs.data = data
|
|
271
|
+
return {
|
|
272
|
+
toolName: 'manage_platform',
|
|
273
|
+
args: nextArgs,
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return { toolName: requestedName, args: rawArgs }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return { toolName: requestedName, args: rawArgs }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function isLikelyToolErrorOutput(output: string): boolean {
|
|
284
|
+
const trimmed = String(output || '').trim()
|
|
285
|
+
if (!trimmed) return false
|
|
286
|
+
if (/^(Error(?::|\s*\(exit\b[^)]*\):?)|error:)/i.test(trimmed)) return true
|
|
287
|
+
if (/\b(MCP error|ECONNREFUSED|ETIMEDOUT|ERR_CONNECTION_REFUSED|ENOENT|EACCES)\b/i.test(trimmed)) return true
|
|
288
|
+
if (/\binvalid_type\b/i.test(trimmed) && /\b(issue|issues|expected|required|received|zod)\b/i.test(trimmed)) return true
|
|
289
|
+
try {
|
|
290
|
+
const parsed = JSON.parse(trimmed) as Record<string, unknown>
|
|
291
|
+
const status = typeof parsed.status === 'string' ? parsed.status.trim().toLowerCase() : ''
|
|
292
|
+
if (status === 'error' || status === 'failed') return true
|
|
293
|
+
if (typeof parsed.error === 'string' && parsed.error.trim()) return true
|
|
294
|
+
} catch {
|
|
295
|
+
// Ignore non-JSON tool output.
|
|
296
|
+
}
|
|
297
|
+
return false
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function normalizeWorkspaceSandboxLinks(text: string, cwd: string): string {
|
|
301
|
+
return text.replace(/\[([^\]]+)\]\(sandbox:\/workspace\/([^)]+)\)/g, (raw, label: string, relativePath: string) => {
|
|
302
|
+
const normalized = String(relativePath || '').replace(/^\/+/, '')
|
|
303
|
+
if (!normalized) return raw
|
|
304
|
+
const resolvedCwd = path.resolve(cwd)
|
|
305
|
+
const resolved = path.resolve(resolvedCwd, normalized)
|
|
306
|
+
if (!resolved.startsWith(resolvedCwd)) return raw
|
|
307
|
+
if (!fs.existsSync(resolved)) return raw
|
|
308
|
+
return `[${label}](/api/files/serve?path=${encodeURIComponent(resolved)})`
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function normalizeAbsoluteFileMarkdownLinks(text: string): string {
|
|
313
|
+
return text.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (raw, label: string, target: string) => {
|
|
314
|
+
if (!path.isAbsolute(target)) return raw
|
|
315
|
+
const resolved = path.resolve(target)
|
|
316
|
+
if (!fs.existsSync(resolved)) return raw
|
|
317
|
+
return `[${label}](/api/files/serve?path=${encodeURIComponent(resolved)})`
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function normalizeAssistantArtifactLinks(text: string, cwd: string): string {
|
|
322
|
+
const uploadsNormalized = text.replace(/sandbox:\/api\/uploads\//g, '/api/uploads/')
|
|
323
|
+
const workspaceNormalized = normalizeWorkspaceSandboxLinks(uploadsNormalized, cwd)
|
|
324
|
+
return normalizeAbsoluteFileMarkdownLinks(workspaceNormalized)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function extractHeartbeatStatus(text: string): { goal?: string; status?: string; summary?: string; nextAction?: string } | null {
|
|
328
|
+
const match = text.match(/\[AGENT_HEARTBEAT_META\]\s*(\{[^\n]*\})/i)
|
|
329
|
+
if (!match) return null
|
|
330
|
+
try {
|
|
331
|
+
const meta = JSON.parse(match[1]) as Record<string, unknown>
|
|
332
|
+
const payload: { goal?: string; status?: string; summary?: string; nextAction?: string } = {}
|
|
333
|
+
if (typeof meta.goal === 'string' && meta.goal.trim()) payload.goal = meta.goal.trim()
|
|
334
|
+
if (typeof meta.status === 'string' && meta.status.trim()) payload.status = meta.status.trim()
|
|
335
|
+
if (typeof meta.summary === 'string' && meta.summary.trim()) payload.summary = meta.summary.trim()
|
|
336
|
+
if (typeof meta.next_action === 'string' && meta.next_action.trim()) payload.nextAction = meta.next_action.trim()
|
|
337
|
+
return Object.keys(payload).length > 0 ? payload : null
|
|
338
|
+
} catch {
|
|
339
|
+
return null
|
|
125
340
|
}
|
|
126
341
|
}
|
|
127
342
|
|
|
@@ -140,7 +355,11 @@ function shouldReplaceRecentAssistantMessage(params: {
|
|
|
140
355
|
return prevTools === 0
|
|
141
356
|
}
|
|
142
357
|
|
|
143
|
-
function
|
|
358
|
+
export function pruneSuppressedHeartbeatStreamMessage(messages: Message[]): boolean {
|
|
359
|
+
return pruneStreamingAssistantArtifacts(messages)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function requestedToolNamesFromMessage(message: string): string[] {
|
|
144
363
|
const lower = message.toLowerCase()
|
|
145
364
|
const candidates = [
|
|
146
365
|
'delegate_to_claude_code',
|
|
@@ -179,15 +398,24 @@ function requestedToolNamesFromMessage(message: string): string[] {
|
|
|
179
398
|
'sandbox_list_runtimes',
|
|
180
399
|
'git',
|
|
181
400
|
'canvas',
|
|
182
|
-
'delegate',
|
|
183
401
|
'schedule_wake',
|
|
184
402
|
'spawn_subagent',
|
|
403
|
+
'mailbox',
|
|
404
|
+
'ask_human',
|
|
405
|
+
'document',
|
|
406
|
+
'extract',
|
|
407
|
+
'table',
|
|
408
|
+
'crawl',
|
|
185
409
|
'context_status',
|
|
186
410
|
'context_summarize',
|
|
187
411
|
'openclaw_nodes',
|
|
188
412
|
'openclaw_workspace',
|
|
189
413
|
]
|
|
190
|
-
|
|
414
|
+
const requested = candidates.filter((name) => lower.includes(name.toLowerCase()))
|
|
415
|
+
if (/(^|[\s(])`delegate`([\s).,!?]|$)|\bdelegate tool\b|\buse delegate\b/.test(lower)) {
|
|
416
|
+
requested.push('delegate')
|
|
417
|
+
}
|
|
418
|
+
return Array.from(new Set(requested))
|
|
191
419
|
}
|
|
192
420
|
|
|
193
421
|
function parseKeyValueArgs(raw: string): Record<string, string> {
|
|
@@ -398,17 +626,51 @@ function syncSessionFromAgent(sessionId: string): void {
|
|
|
398
626
|
if (!agent) return
|
|
399
627
|
|
|
400
628
|
let changed = false
|
|
401
|
-
|
|
402
|
-
if (
|
|
403
|
-
if (
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
629
|
+
const route = resolvePrimaryAgentRoute(agent)
|
|
630
|
+
if (!session.provider && agent.provider) { session.provider = agent.provider; changed = true }
|
|
631
|
+
if ((session.model === undefined || session.model === null || session.model === '') && agent.model !== undefined) {
|
|
632
|
+
session.model = agent.model
|
|
633
|
+
changed = true
|
|
634
|
+
}
|
|
635
|
+
if (route) {
|
|
636
|
+
const resolved = applyResolvedRoute({ ...session }, route)
|
|
637
|
+
if (session.provider !== resolved.provider) { session.provider = resolved.provider; changed = true }
|
|
638
|
+
if (session.model !== resolved.model) { session.model = resolved.model; changed = true }
|
|
639
|
+
if ((session.credentialId || null) !== (resolved.credentialId || null)) {
|
|
640
|
+
session.credentialId = resolved.credentialId ?? null
|
|
641
|
+
changed = true
|
|
642
|
+
}
|
|
643
|
+
if (JSON.stringify(session.fallbackCredentialIds || []) !== JSON.stringify(resolved.fallbackCredentialIds || [])) {
|
|
644
|
+
session.fallbackCredentialIds = [...resolved.fallbackCredentialIds]
|
|
645
|
+
changed = true
|
|
646
|
+
}
|
|
647
|
+
if ((session.apiEndpoint || null) !== (resolved.apiEndpoint || null)) {
|
|
648
|
+
session.apiEndpoint = resolved.apiEndpoint ?? null
|
|
649
|
+
changed = true
|
|
650
|
+
}
|
|
651
|
+
if ((session.gatewayProfileId || null) !== (resolved.gatewayProfileId || null)) {
|
|
652
|
+
session.gatewayProfileId = resolved.gatewayProfileId ?? null
|
|
653
|
+
changed = true
|
|
654
|
+
}
|
|
655
|
+
} else {
|
|
656
|
+
if (session.credentialId === undefined && agent.credentialId !== undefined) {
|
|
657
|
+
session.credentialId = agent.credentialId ?? null
|
|
658
|
+
changed = true
|
|
659
|
+
}
|
|
660
|
+
if ((session.apiEndpoint === undefined || session.apiEndpoint === null) && agent.apiEndpoint !== undefined) {
|
|
661
|
+
const normalized = normalizeProviderEndpoint(agent.provider, agent.apiEndpoint ?? null)
|
|
662
|
+
if (normalized !== session.apiEndpoint) { session.apiEndpoint = normalized; changed = true }
|
|
663
|
+
}
|
|
407
664
|
}
|
|
408
665
|
if (!Array.isArray(session.plugins)) {
|
|
409
666
|
session.plugins = Array.isArray(agent.plugins) ? [...agent.plugins] : []
|
|
410
667
|
changed = true
|
|
411
668
|
}
|
|
669
|
+
const isShortcutChat = session.shortcutForAgentId === agent.id || agent.threadSessionId === sessionId
|
|
670
|
+
if (isShortcutChat) {
|
|
671
|
+
if (session.shortcutForAgentId !== agent.id) { session.shortcutForAgentId = agent.id; changed = true }
|
|
672
|
+
if (session.name !== agent.name) { session.name = agent.name; changed = true }
|
|
673
|
+
}
|
|
412
674
|
|
|
413
675
|
if (changed) {
|
|
414
676
|
sessions[sessionId] = session
|
|
@@ -435,6 +697,8 @@ function buildAgentSystemPrompt(session: Session): string | undefined {
|
|
|
435
697
|
if (agent.description) identityLines.push(`Description: ${agent.description}`)
|
|
436
698
|
identityLines.push('I should always embody this identity. I am not just an "Assistant" — I am a persistent companion with my own soul and presence.')
|
|
437
699
|
parts.push(identityLines.join('\n'))
|
|
700
|
+
const continuityBlock = buildIdentityContinuityContext(session, agent)
|
|
701
|
+
if (continuityBlock) parts.push(continuityBlock)
|
|
438
702
|
|
|
439
703
|
// 2. Runtime & Capabilities (OpenClaw Style)
|
|
440
704
|
const runtimeLines = [
|
|
@@ -553,20 +817,56 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
553
817
|
const sessions = loadSessions()
|
|
554
818
|
const session = sessions[sessionId]
|
|
555
819
|
if (!session) throw new Error(`Session not found: ${sessionId}`)
|
|
820
|
+
session.messages = Array.isArray(session.messages) ? session.messages : []
|
|
821
|
+
const runStartedAt = Date.now()
|
|
822
|
+
const runMessageStartIndex = session.messages.length
|
|
556
823
|
|
|
557
824
|
const appSettings = loadSettings()
|
|
825
|
+
const agentForSession = session.agentId ? loadAgents()[session.agentId] : null
|
|
558
826
|
const toolPolicy = resolveSessionToolPolicy(session.plugins, appSettings)
|
|
559
|
-
const isHeartbeatRun = internal
|
|
560
|
-
const isAutoRunNoHistory = isHeartbeatRun
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
827
|
+
const isHeartbeatRun = isInternalHeartbeatRun(internal, source)
|
|
828
|
+
const isAutoRunNoHistory = isHeartbeatRun
|
|
829
|
+
const heartbeatStatusOnly = false
|
|
830
|
+
if (shouldApplySessionFreshnessReset(source)) {
|
|
831
|
+
const freshness = evaluateSessionFreshness({
|
|
832
|
+
session,
|
|
833
|
+
policy: resolveSessionResetPolicy({
|
|
834
|
+
session,
|
|
835
|
+
agent: agentForSession,
|
|
836
|
+
settings: appSettings,
|
|
837
|
+
}),
|
|
838
|
+
})
|
|
839
|
+
if (!freshness.fresh) {
|
|
840
|
+
try { syncSessionArchiveMemory(session, { agent: agentForSession }) } catch { /* archive sync is best-effort */ }
|
|
841
|
+
resetSessionRuntime(session, freshness.reason || 'session_reset')
|
|
842
|
+
onEvent?.({ t: 'status', text: JSON.stringify({ sessionReset: freshness.reason || 'session_reset' }) })
|
|
843
|
+
sessions[sessionId] = session
|
|
844
|
+
saveSessions(sessions)
|
|
845
|
+
}
|
|
846
|
+
}
|
|
566
847
|
const pluginsForRun = heartbeatStatusOnly ? [] : toolPolicy.enabledPlugins
|
|
567
848
|
let sessionForRun = pluginsForRun === session.plugins
|
|
568
849
|
? session
|
|
569
850
|
: { ...session, plugins: pluginsForRun }
|
|
851
|
+
if (agentForSession) {
|
|
852
|
+
const preferredRoute = resolvePrimaryAgentRoute(agentForSession)
|
|
853
|
+
if (preferredRoute) {
|
|
854
|
+
sessionForRun = applyResolvedRoute({ ...sessionForRun }, preferredRoute)
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
let effectiveMessage = message
|
|
858
|
+
|
|
859
|
+
if (pluginsForRun.length > 0) {
|
|
860
|
+
try {
|
|
861
|
+
effectiveMessage = await getPluginManager().transformText(
|
|
862
|
+
'transformInboundMessage',
|
|
863
|
+
{ session: sessionForRun, text: message },
|
|
864
|
+
{ enabledIds: pluginsForRun },
|
|
865
|
+
)
|
|
866
|
+
} catch {
|
|
867
|
+
effectiveMessage = message
|
|
868
|
+
}
|
|
869
|
+
}
|
|
570
870
|
|
|
571
871
|
// Apply model override for heartbeat runs (cheaper model)
|
|
572
872
|
if (isHeartbeatRun && input.modelOverride) {
|
|
@@ -660,14 +960,14 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
660
960
|
detail: {
|
|
661
961
|
source,
|
|
662
962
|
internal,
|
|
663
|
-
provider:
|
|
664
|
-
model:
|
|
665
|
-
messagePreview:
|
|
963
|
+
provider: sessionForRun.provider,
|
|
964
|
+
model: sessionForRun.model,
|
|
965
|
+
messagePreview: effectiveMessage.slice(0, 200),
|
|
666
966
|
hasImage: !!(imagePath || imageUrl),
|
|
667
967
|
},
|
|
668
968
|
})
|
|
669
969
|
|
|
670
|
-
const providerType =
|
|
970
|
+
const providerType = sessionForRun.provider || 'claude-cli'
|
|
671
971
|
const provider = getProvider(providerType)
|
|
672
972
|
if (!provider) throw new Error(`Unknown provider: ${providerType}`)
|
|
673
973
|
|
|
@@ -675,11 +975,11 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
675
975
|
throw new Error(`Directory not found: ${session.cwd}`)
|
|
676
976
|
}
|
|
677
977
|
|
|
678
|
-
const apiKey = resolveApiKeyForSession(
|
|
978
|
+
const apiKey = resolveApiKeyForSession(sessionForRun, provider)
|
|
679
979
|
|
|
680
980
|
if (!internal) {
|
|
681
981
|
const linkAnalysis = await runLinkUnderstanding(message)
|
|
682
|
-
|
|
982
|
+
const nextUserMessage: Message = {
|
|
683
983
|
role: 'user',
|
|
684
984
|
text: message,
|
|
685
985
|
time: Date.now(),
|
|
@@ -687,7 +987,8 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
687
987
|
imageUrl: imageUrl || undefined,
|
|
688
988
|
attachedFiles: attachedFiles?.length ? attachedFiles : undefined,
|
|
689
989
|
replyToId: input.replyToId || undefined,
|
|
690
|
-
}
|
|
990
|
+
}
|
|
991
|
+
session.messages.push(nextUserMessage)
|
|
691
992
|
if (linkAnalysis.length > 0) {
|
|
692
993
|
session.messages.push({
|
|
693
994
|
role: 'assistant',
|
|
@@ -698,6 +999,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
698
999
|
}
|
|
699
1000
|
session.lastActiveAt = Date.now()
|
|
700
1001
|
saveSessions(sessions)
|
|
1002
|
+
try {
|
|
1003
|
+
await getPluginManager().runHook('onMessage', { session, message: nextUserMessage }, { enabledIds: pluginsForRun })
|
|
1004
|
+
} catch { /* onMessage hooks are non-critical */ }
|
|
701
1005
|
}
|
|
702
1006
|
|
|
703
1007
|
const systemPrompt = buildAgentSystemPrompt(session)
|
|
@@ -746,19 +1050,18 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
746
1050
|
const fresh = loadSessions()
|
|
747
1051
|
const current = fresh[sessionId]
|
|
748
1052
|
if (!current) return
|
|
1053
|
+
current.messages = Array.isArray(current.messages) ? current.messages : []
|
|
749
1054
|
const partialMsg: Message = {
|
|
750
1055
|
role: 'assistant',
|
|
751
1056
|
text: streamingPartialText,
|
|
752
1057
|
time: Date.now(),
|
|
753
1058
|
streaming: true,
|
|
754
|
-
toolEvents: toolEvents.length ? [...toolEvents] : undefined,
|
|
755
|
-
}
|
|
756
|
-
const lastMsg = current.messages.at(-1)
|
|
757
|
-
if (lastMsg?.streaming) {
|
|
758
|
-
current.messages[current.messages.length - 1] = partialMsg
|
|
759
|
-
} else {
|
|
760
|
-
current.messages.push(partialMsg)
|
|
1059
|
+
toolEvents: toolEvents.length ? dedupeConsecutiveToolEvents([...toolEvents]) : undefined,
|
|
761
1060
|
}
|
|
1061
|
+
upsertStreamingAssistantArtifact(current.messages, partialMsg, {
|
|
1062
|
+
minIndex: runMessageStartIndex,
|
|
1063
|
+
minTime: runStartedAt,
|
|
1064
|
+
})
|
|
762
1065
|
fresh[sessionId] = current
|
|
763
1066
|
saveSessions(fresh)
|
|
764
1067
|
notify(`messages:${sessionId}`)
|
|
@@ -812,7 +1115,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
812
1115
|
if (hasPlugins) {
|
|
813
1116
|
fullResponse = (await streamAgentChat({
|
|
814
1117
|
session: sessionForRun,
|
|
815
|
-
message:
|
|
1118
|
+
message: effectiveMessage,
|
|
816
1119
|
imagePath,
|
|
817
1120
|
attachedFiles,
|
|
818
1121
|
apiKey,
|
|
@@ -830,7 +1133,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
830
1133
|
model: sessionForRun.model,
|
|
831
1134
|
apiEndpoint: sessionForRun.apiEndpoint || '',
|
|
832
1135
|
systemPrompt,
|
|
833
|
-
message:
|
|
1136
|
+
message: effectiveMessage,
|
|
834
1137
|
imagePath,
|
|
835
1138
|
imageUrl,
|
|
836
1139
|
attachedFiles,
|
|
@@ -858,7 +1161,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
858
1161
|
} else {
|
|
859
1162
|
fullResponse = await provider.handler.streamChat({
|
|
860
1163
|
session: sessionForRun,
|
|
861
|
-
message:
|
|
1164
|
+
message: effectiveMessage,
|
|
862
1165
|
imagePath,
|
|
863
1166
|
apiKey,
|
|
864
1167
|
systemPrompt,
|
|
@@ -937,57 +1240,6 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
937
1240
|
: null
|
|
938
1241
|
const calledNames = new Set((toolEvents || []).map((t) => t.name))
|
|
939
1242
|
|
|
940
|
-
const translateToolInvocation = (
|
|
941
|
-
requestedName: string,
|
|
942
|
-
rawArgs: Record<string, unknown>,
|
|
943
|
-
): { toolName: string; args: Record<string, unknown> } => {
|
|
944
|
-
if (requestedName === 'web_search') {
|
|
945
|
-
return {
|
|
946
|
-
toolName: 'web',
|
|
947
|
-
args: {
|
|
948
|
-
action: 'search',
|
|
949
|
-
query: typeof rawArgs.query === 'string' ? rawArgs.query : message.trim(),
|
|
950
|
-
maxResults: typeof rawArgs.maxResults === 'number' ? rawArgs.maxResults : 5,
|
|
951
|
-
},
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
if (requestedName === 'web_fetch') {
|
|
955
|
-
return {
|
|
956
|
-
toolName: 'web',
|
|
957
|
-
args: {
|
|
958
|
-
action: 'fetch',
|
|
959
|
-
url: rawArgs.url,
|
|
960
|
-
},
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
if (requestedName === 'delegate_to_claude_code') {
|
|
964
|
-
return { toolName: 'delegate', args: { ...rawArgs, backend: 'claude' } }
|
|
965
|
-
}
|
|
966
|
-
if (requestedName === 'delegate_to_codex_cli') {
|
|
967
|
-
return { toolName: 'delegate', args: { ...rawArgs, backend: 'codex' } }
|
|
968
|
-
}
|
|
969
|
-
if (requestedName === 'delegate_to_opencode_cli') {
|
|
970
|
-
return { toolName: 'delegate', args: { ...rawArgs, backend: 'opencode' } }
|
|
971
|
-
}
|
|
972
|
-
if (requestedName === 'delegate_to_gemini_cli') {
|
|
973
|
-
return { toolName: 'delegate', args: { ...rawArgs, backend: 'gemini' } }
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
const managePrefix = 'manage_'
|
|
977
|
-
if (requestedName.startsWith(managePrefix) && requestedName !== 'manage_platform') {
|
|
978
|
-
const resource = requestedName.slice(managePrefix.length)
|
|
979
|
-
if (resource) {
|
|
980
|
-
const { action, id, data, ...rest } = rawArgs
|
|
981
|
-
return {
|
|
982
|
-
toolName: 'manage_platform',
|
|
983
|
-
args: { resource, action, id, data, ...rest },
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
return { toolName: requestedName, args: rawArgs }
|
|
989
|
-
}
|
|
990
|
-
|
|
991
1243
|
const invokeSessionTool = async (toolName: string, args: Record<string, unknown>, failurePrefix: string): Promise<boolean> => {
|
|
992
1244
|
const blockedReason = resolveConcreteToolPolicyBlock(toolName, toolPolicy, appSettings)
|
|
993
1245
|
if (blockedReason) {
|
|
@@ -1011,18 +1263,28 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1011
1263
|
mcpDisabledTools: agent?.mcpDisabledTools,
|
|
1012
1264
|
})
|
|
1013
1265
|
try {
|
|
1014
|
-
const
|
|
1015
|
-
const
|
|
1266
|
+
const directTool = tools.find((t) => t?.name === toolName) as StructuredToolInterface | undefined
|
|
1267
|
+
const availableToolNames = tools.map((candidate) => candidate?.name).filter(Boolean)
|
|
1268
|
+
const translated = directTool
|
|
1269
|
+
? { toolName, args }
|
|
1270
|
+
: translateRequestedToolInvocation(toolName, args, message, availableToolNames)
|
|
1271
|
+
const selectedTool = directTool || tools.find((t) => t?.name === translated.toolName) as StructuredToolInterface | undefined
|
|
1016
1272
|
if (!selectedTool?.invoke) return false
|
|
1017
1273
|
const toolInput = JSON.stringify(translated.args)
|
|
1018
1274
|
emit({ t: 'tool_call', toolName, toolInput })
|
|
1019
1275
|
const toolOutput = await selectedTool.invoke(translated.args)
|
|
1020
1276
|
const outputText = typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput)
|
|
1021
1277
|
emit({ t: 'tool_result', toolName, toolOutput: outputText })
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1278
|
+
const delegateResponse = (
|
|
1279
|
+
toolName === 'delegate'
|
|
1280
|
+
|| toolName.startsWith('delegate_to_')
|
|
1281
|
+
) ? extractDelegateResponse(outputText) : null
|
|
1282
|
+
if (delegateResponse) {
|
|
1283
|
+
fullResponse = delegateResponse
|
|
1284
|
+
} else if (!fullResponse.trim() && outputText?.trim()) {
|
|
1285
|
+
// Don't overwrite fullResponse with raw tool output — it's already captured
|
|
1286
|
+
// in toolEvents. Only set a brief notice when the LLM produced no text,
|
|
1287
|
+
// so the message bubble isn't empty.
|
|
1026
1288
|
const label = toolName.replace(/_/g, ' ')
|
|
1027
1289
|
fullResponse = `Used **${label}** — see tool output above for details.`
|
|
1028
1290
|
}
|
|
@@ -1075,7 +1337,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1075
1337
|
const delegationOrder = rankDelegatesByHealth(baseDelegationOrder as DelegateTool[])
|
|
1076
1338
|
.filter((tool) => enabledDelegateTools.includes(tool))
|
|
1077
1339
|
for (const delegateTool of delegationOrder) {
|
|
1078
|
-
const invoked = await invokeSessionTool(delegateTool, { task:
|
|
1340
|
+
const invoked = await invokeSessionTool(delegateTool, { task: effectiveMessage.trim() }, 'Auto-delegation failed')
|
|
1079
1341
|
if (invoked) break
|
|
1080
1342
|
}
|
|
1081
1343
|
}
|
|
@@ -1095,7 +1357,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1095
1357
|
for (const delegateTool of fallbackOrder) {
|
|
1096
1358
|
const invoked = await invokeSessionTool(
|
|
1097
1359
|
delegateTool,
|
|
1098
|
-
{ task:
|
|
1360
|
+
{ task: effectiveMessage.trim() },
|
|
1099
1361
|
`Provider failover via ${delegateTool} failed`,
|
|
1100
1362
|
)
|
|
1101
1363
|
if (invoked) {
|
|
@@ -1113,7 +1375,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1113
1375
|
if (canAutoRouteWithTools && routingDecision?.intent === 'browsing' && routingDecision.primaryUrl && hasToolEnabled(sessionForRun, 'browser')) {
|
|
1114
1376
|
await invokeSessionTool(
|
|
1115
1377
|
'browser',
|
|
1116
|
-
{ action: '
|
|
1378
|
+
{ action: 'read_page', url: routingDecision.primaryUrl },
|
|
1117
1379
|
'Auto browser routing failed',
|
|
1118
1380
|
)
|
|
1119
1381
|
}
|
|
@@ -1123,7 +1385,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1123
1385
|
if (routeUrl && hasToolEnabled(sessionForRun, 'web_fetch')) {
|
|
1124
1386
|
await invokeSessionTool('web_fetch', { url: routeUrl }, 'Auto web_fetch routing failed')
|
|
1125
1387
|
} else if (hasToolEnabled(sessionForRun, 'web_search')) {
|
|
1126
|
-
await invokeSessionTool('web_search', { query:
|
|
1388
|
+
await invokeSessionTool('web_search', { query: effectiveMessage.trim(), maxResults: 5 }, 'Auto web_search routing failed')
|
|
1127
1389
|
}
|
|
1128
1390
|
}
|
|
1129
1391
|
|
|
@@ -1158,27 +1420,23 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1158
1420
|
errorMessage = streamErrors[streamErrors.length - 1]
|
|
1159
1421
|
}
|
|
1160
1422
|
|
|
1161
|
-
|
|
1423
|
+
let finalText = (fullResponse || '').trim() || (!internal && errorMessage ? `Error: ${errorMessage}` : '')
|
|
1424
|
+
if (pluginsForRun.length > 0 && finalText && !isHeartbeatRun) {
|
|
1425
|
+
try {
|
|
1426
|
+
finalText = await getPluginManager().transformText(
|
|
1427
|
+
'transformOutboundMessage',
|
|
1428
|
+
{ session: sessionForRun, text: finalText },
|
|
1429
|
+
{ enabledIds: pluginsForRun },
|
|
1430
|
+
)
|
|
1431
|
+
} catch { /* outbound transforms are non-critical */ }
|
|
1432
|
+
}
|
|
1433
|
+
finalText = normalizeAssistantArtifactLinks(finalText, session.cwd)
|
|
1162
1434
|
const textForPersistence = stripMainLoopMetaForPersistence(finalText)
|
|
1435
|
+
const persistedToolEvents = dedupeConsecutiveToolEvents(toolEvents)
|
|
1163
1436
|
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
if (metaMatch) {
|
|
1168
|
-
try {
|
|
1169
|
-
const meta = JSON.parse(metaMatch[1])
|
|
1170
|
-
const statusPayload: Record<string, string | undefined> = {}
|
|
1171
|
-
if (meta.goal) statusPayload.goal = String(meta.goal)
|
|
1172
|
-
if (meta.status) statusPayload.status = String(meta.status)
|
|
1173
|
-
if (meta.summary) statusPayload.summary = String(meta.summary)
|
|
1174
|
-
if (meta.next_action) statusPayload.nextAction = String(meta.next_action)
|
|
1175
|
-
if (Object.keys(statusPayload).length > 0) {
|
|
1176
|
-
emit({ t: 'status', text: JSON.stringify(statusPayload) })
|
|
1177
|
-
}
|
|
1178
|
-
} catch {
|
|
1179
|
-
// ignore malformed meta JSON
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1437
|
+
if (isHeartbeatRun && finalText) {
|
|
1438
|
+
const heartbeatStatus = extractHeartbeatStatus(finalText)
|
|
1439
|
+
if (heartbeatStatus) emit({ t: 'status', text: JSON.stringify(heartbeatStatus) })
|
|
1182
1440
|
}
|
|
1183
1441
|
|
|
1184
1442
|
// HEARTBEAT_OK suppression
|
|
@@ -1214,7 +1472,13 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1214
1472
|
const fresh = loadSessions()
|
|
1215
1473
|
const current = fresh[sessionId]
|
|
1216
1474
|
if (current) {
|
|
1475
|
+
current.messages = Array.isArray(current.messages) ? current.messages : []
|
|
1476
|
+
const currentAgent = current.agentId ? loadAgents()[current.agentId] : null
|
|
1217
1477
|
let changed = false
|
|
1478
|
+
changed = pruneStreamingAssistantArtifacts(current.messages, {
|
|
1479
|
+
minIndex: runMessageStartIndex,
|
|
1480
|
+
minTime: runStartedAt,
|
|
1481
|
+
}) || changed
|
|
1218
1482
|
const persistField = (key: string, value: unknown) => {
|
|
1219
1483
|
const normalized = normalizeResumeId(value)
|
|
1220
1484
|
if ((current as Record<string, unknown>)[key] !== normalized) {
|
|
@@ -1246,7 +1510,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1246
1510
|
}
|
|
1247
1511
|
|
|
1248
1512
|
if (shouldPersistAssistant) {
|
|
1249
|
-
const persistedKind =
|
|
1513
|
+
const persistedKind = isHeartbeatRun ? 'heartbeat' : 'chat'
|
|
1250
1514
|
const persistedText = heartbeatClassification === 'strip'
|
|
1251
1515
|
? textForPersistence.replace(/HEARTBEAT_OK/gi, '').trim()
|
|
1252
1516
|
: textForPersistence
|
|
@@ -1256,13 +1520,13 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1256
1520
|
text: persistedText,
|
|
1257
1521
|
time: nowTs,
|
|
1258
1522
|
thinking: thinkingText || undefined,
|
|
1259
|
-
toolEvents:
|
|
1523
|
+
toolEvents: persistedToolEvents.length ? persistedToolEvents : undefined,
|
|
1260
1524
|
kind: persistedKind,
|
|
1261
1525
|
}
|
|
1262
1526
|
const previous = current.messages.at(-1)
|
|
1263
1527
|
if (previous?.streaming || shouldReplaceRecentAssistantMessage({
|
|
1264
1528
|
previous,
|
|
1265
|
-
nextToolEvents:
|
|
1529
|
+
nextToolEvents: persistedToolEvents,
|
|
1266
1530
|
nextKind: persistedKind,
|
|
1267
1531
|
now: nowTs,
|
|
1268
1532
|
})) {
|
|
@@ -1275,6 +1539,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1275
1539
|
current.lastHeartbeatSentAt = nowTs
|
|
1276
1540
|
}
|
|
1277
1541
|
changed = true
|
|
1542
|
+
try {
|
|
1543
|
+
await getPluginManager().runHook('onMessage', { session: current, message: nextAssistantMessage }, { enabledIds: pluginsForRun })
|
|
1544
|
+
} catch { /* onMessage hooks are non-critical */ }
|
|
1278
1545
|
|
|
1279
1546
|
// Conversation tone detection
|
|
1280
1547
|
if (!internal) {
|
|
@@ -1329,6 +1596,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1329
1596
|
}
|
|
1330
1597
|
}
|
|
1331
1598
|
}
|
|
1599
|
+
if (isHeartbeatRun && heartbeatClassification === 'suppress') {
|
|
1600
|
+
changed = pruneSuppressedHeartbeatStreamMessage(current.messages) || changed
|
|
1601
|
+
}
|
|
1332
1602
|
|
|
1333
1603
|
// Fire afterChatTurn hook for all enabled plugins (memory auto-save, logging, etc.)
|
|
1334
1604
|
try {
|
|
@@ -1338,13 +1608,20 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1338
1608
|
response: textForPersistence,
|
|
1339
1609
|
source,
|
|
1340
1610
|
internal,
|
|
1341
|
-
})
|
|
1611
|
+
}, { enabledIds: pluginsForRun })
|
|
1342
1612
|
} catch { /* afterChatTurn hooks are non-critical */ }
|
|
1343
1613
|
|
|
1344
1614
|
// Don't extend idle timeout for heartbeat runs — only user-initiated activity counts
|
|
1345
|
-
if (source
|
|
1615
|
+
if (!isHeartbeatSource(source)) {
|
|
1346
1616
|
current.lastActiveAt = Date.now()
|
|
1347
1617
|
}
|
|
1618
|
+
|
|
1619
|
+
refreshSessionIdentityState(current, currentAgent)
|
|
1620
|
+
changed = true
|
|
1621
|
+
try {
|
|
1622
|
+
const archiveSync = syncSessionArchiveMemory(current, { agent: currentAgent })
|
|
1623
|
+
if (archiveSync.stored) changed = true
|
|
1624
|
+
} catch { /* archive sync is best-effort */ }
|
|
1348
1625
|
fresh[sessionId] = current
|
|
1349
1626
|
saveSessions(fresh)
|
|
1350
1627
|
notify(`messages:${sessionId}`)
|
|
@@ -1355,7 +1632,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1355
1632
|
sessionId,
|
|
1356
1633
|
text: finalText,
|
|
1357
1634
|
persisted: shouldPersistAssistant,
|
|
1358
|
-
toolEvents,
|
|
1635
|
+
toolEvents: persistedToolEvents,
|
|
1359
1636
|
error: errorMessage,
|
|
1360
1637
|
inputTokens: accumulatedUsage.inputTokens || undefined,
|
|
1361
1638
|
outputTokens: accumulatedUsage.outputTokens || undefined,
|