@swarmclawai/swarmclaw 0.7.1 → 0.7.3
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 +155 -150
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +37 -9
- package/src/app/api/agents/route.ts +13 -2
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
- package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
- package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
- package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
- package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
- package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
- package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
- package/src/app/api/{sessions → chats}/route.ts +21 -7
- 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/files/open/route.ts +16 -14
- 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/skills/route.ts +11 -3
- 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 +6 -26
- package/src/app/api/plugins/settings/route.ts +40 -0
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/settings/route.ts +49 -7
- package/src/app/api/tasks/[id]/route.ts +15 -6
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/usage/route.ts +30 -0
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +39 -33
- package/src/cli/index.ts +43 -49
- package/src/cli/spec.js +29 -27
- package/src/components/agents/agent-card.tsx +16 -13
- package/src/components/agents/agent-chat-list.tsx +104 -4
- package/src/components/agents/agent-list.tsx +54 -22
- package/src/components/agents/agent-sheet.tsx +209 -18
- package/src/components/agents/cron-job-form.tsx +3 -3
- package/src/components/agents/inspector-panel.tsx +110 -50
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +5 -38
- package/src/components/chat/chat-area.tsx +39 -27
- package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
- package/src/components/chat/chat-header.tsx +299 -314
- package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
- package/src/components/chat/chat-tool-toggles.tsx +26 -17
- package/src/components/chat/checkpoint-timeline.tsx +4 -4
- package/src/components/chat/message-bubble.tsx +4 -1
- package/src/components/chat/message-list.tsx +5 -3
- package/src/components/chat/session-debug-panel.tsx +1 -1
- package/src/components/chat/tool-request-banner.tsx +3 -3
- package/src/components/chatrooms/agent-hover-card.tsx +3 -3
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
- package/src/components/chatrooms/chatroom-view.tsx +347 -205
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +218 -1
- package/src/components/home/home-view.tsx +129 -5
- package/src/components/layout/app-layout.tsx +392 -182
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/plugins/plugin-list.tsx +487 -254
- package/src/components/plugins/plugin-sheet.tsx +236 -13
- package/src/components/projects/project-detail.tsx +183 -0
- package/src/components/settings/gateway-connection-panel.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -25
- 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 +78 -1
- package/src/components/shared/settings/section-orchestrator.tsx +3 -3
- package/src/components/shared/settings/section-providers.tsx +1 -1
- 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 +244 -56
- package/src/components/tasks/approvals-panel.tsx +205 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/usage/metrics-dashboard.tsx +147 -1
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +8 -8
- 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/chat.ts +1 -1
- package/src/lib/{sessions.ts → chats.ts} +28 -18
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/providers/claude-cli.ts +1 -1
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +205 -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 +36 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/capability-router.ts +10 -8
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
- package/src/lib/server/chat-execution.ts +285 -165
- 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 +67 -2
- package/src/lib/server/chatroom-helpers.ts +48 -8
- 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 +948 -112
- 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 +188 -9
- 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/cost.ts +34 -1
- package/src/lib/server/daemon-state.ts +61 -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/heartbeat-service.ts +14 -40
- 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 +28 -1103
- 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 +5 -6
- 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 +20 -9
- package/src/lib/server/orchestrator.ts +7 -7
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +927 -66
- package/src/lib/server/provider-health.ts +38 -6
- package/src/lib/server/queue.ts +13 -28
- 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 -82
- package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
- package/src/lib/server/session-tools/calendar.ts +366 -0
- package/src/lib/server/session-tools/canvas.ts +1 -1
- package/src/lib/server/session-tools/chatroom.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +114 -10
- package/src/lib/server/session-tools/context.ts +21 -5
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +74 -28
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +497 -24
- package/src/lib/server/session-tools/discovery.ts +24 -6
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/edit_file.ts +4 -2
- package/src/lib/server/session-tools/email.ts +320 -0
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +241 -25
- package/src/lib/server/session-tools/git.ts +1 -1
- package/src/lib/server/session-tools/http.ts +1 -1
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +380 -0
- package/src/lib/server/session-tools/index.ts +130 -50
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +172 -3
- package/src/lib/server/session-tools/monitor.ts +151 -8
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
- package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +148 -7
- package/src/lib/server/session-tools/plugin-creator.ts +89 -26
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +301 -0
- package/src/lib/server/session-tools/sample-ui.ts +1 -1
- package/src/lib/server/session-tools/sandbox.ts +4 -2
- package/src/lib/server/session-tools/schedule.ts +24 -12
- package/src/lib/server/session-tools/session-info.ts +43 -7
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/shell.ts +5 -2
- package/src/lib/server/session-tools/subagent.ts +194 -28
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +42 -12
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +926 -91
- package/src/lib/server/storage.ts +255 -16
- package/src/lib/server/stream-agent-chat.ts +116 -268
- 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 -10
- package/src/lib/server/tool-aliases.ts +66 -18
- package/src/lib/server/tool-capability-policy.test.ts +9 -9
- package/src/lib/server/tool-capability-policy.ts +38 -27
- 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/tool-definitions.ts +4 -0
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +10 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +5 -11
- package/src/stores/use-chat-store.ts +38 -9
- package/src/types/index.ts +352 -47
- package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
- package/src/components/sessions/new-session-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -24
- package/src/lib/server/session-run-manager.test.ts +0 -23
- /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
|
@@ -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,
|
|
@@ -23,12 +24,12 @@ import { buildSessionTools } from './session-tools'
|
|
|
23
24
|
import type { StructuredToolInterface } from '@langchain/core/tools'
|
|
24
25
|
import type { Session } from '@/types'
|
|
25
26
|
import { stripMainLoopMetaForPersistence } from './main-agent-loop'
|
|
27
|
+
import { getPluginManager } from './plugins'
|
|
26
28
|
import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
|
|
27
|
-
import { getMemoryDb } from './memory-db'
|
|
28
29
|
import { routeTaskIntent } from './capability-router'
|
|
29
30
|
import { notify } from './ws-hub'
|
|
30
31
|
import { resolveConcreteToolPolicyBlock, resolveSessionToolPolicy } from './tool-capability-policy'
|
|
31
|
-
import {
|
|
32
|
+
import { pluginIdMatches } from './tool-aliases'
|
|
32
33
|
import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
|
|
33
34
|
import {
|
|
34
35
|
getCachedLlmResponse,
|
|
@@ -38,8 +39,13 @@ import {
|
|
|
38
39
|
} from './llm-response-cache'
|
|
39
40
|
import type { Message, MessageToolEvent, SSEEvent, UsageRecord } from '@/types'
|
|
40
41
|
import { markProviderFailure, markProviderSuccess, rankDelegatesByHealth } from './provider-health'
|
|
42
|
+
import { isHeartbeatSource, isInternalHeartbeatRun } from './heartbeat-source'
|
|
41
43
|
import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
|
|
42
|
-
|
|
44
|
+
import { buildIdentityContinuityContext, refreshSessionIdentityState } from './identity-continuity'
|
|
45
|
+
import { syncSessionArchiveMemory } from './session-archive-memory'
|
|
46
|
+
import { evaluateSessionFreshness, resetSessionRuntime, resolveSessionResetPolicy } from './session-reset-policy'
|
|
47
|
+
import { pruneStreamingAssistantArtifacts, upsertStreamingAssistantArtifact } from '@/lib/chat-streaming-state'
|
|
48
|
+
type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli'
|
|
43
49
|
|
|
44
50
|
/** Slice history from the most recent context-clear marker forward */
|
|
45
51
|
function applyContextClearBoundary(messages: Message[]): Message[] {
|
|
@@ -50,6 +56,8 @@ function applyContextClearBoundary(messages: Message[]): Message[] {
|
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
interface SessionWithTools {
|
|
59
|
+
plugins?: string[] | null
|
|
60
|
+
/** @deprecated Use plugins */
|
|
53
61
|
tools?: string[] | null
|
|
54
62
|
}
|
|
55
63
|
|
|
@@ -90,6 +98,10 @@ export interface ExecuteChatTurnResult {
|
|
|
90
98
|
estimatedCost?: number
|
|
91
99
|
}
|
|
92
100
|
|
|
101
|
+
export function shouldApplySessionFreshnessReset(source: string): boolean {
|
|
102
|
+
return source !== 'eval'
|
|
103
|
+
}
|
|
104
|
+
|
|
93
105
|
function extractEventJson(line: string): SSEEvent | null {
|
|
94
106
|
if (!line.startsWith('data: ')) return null
|
|
95
107
|
try {
|
|
@@ -99,8 +111,17 @@ function extractEventJson(line: string): SSEEvent | null {
|
|
|
99
111
|
}
|
|
100
112
|
}
|
|
101
113
|
|
|
102
|
-
function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
|
|
114
|
+
export function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
|
|
103
115
|
if (ev.t === 'tool_call') {
|
|
116
|
+
const previous = bag[bag.length - 1]
|
|
117
|
+
if (
|
|
118
|
+
previous
|
|
119
|
+
&& previous.name === (ev.toolName || 'unknown')
|
|
120
|
+
&& previous.input === (ev.toolInput || '')
|
|
121
|
+
&& !previous.output
|
|
122
|
+
) {
|
|
123
|
+
return
|
|
124
|
+
}
|
|
104
125
|
bag.push({
|
|
105
126
|
name: ev.toolName || 'unknown',
|
|
106
127
|
input: ev.toolInput || '',
|
|
@@ -123,6 +144,96 @@ function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
|
|
|
123
144
|
}
|
|
124
145
|
}
|
|
125
146
|
|
|
147
|
+
export function dedupeConsecutiveToolEvents(events: MessageToolEvent[]): MessageToolEvent[] {
|
|
148
|
+
const sameEvent = (left: MessageToolEvent, right: MessageToolEvent): boolean => (
|
|
149
|
+
left.name === right.name
|
|
150
|
+
&& left.input === right.input
|
|
151
|
+
&& (left.output || '') === (right.output || '')
|
|
152
|
+
&& (left.error === true) === (right.error === true)
|
|
153
|
+
)
|
|
154
|
+
const sameBlock = (startA: number, startB: number, size: number): boolean => {
|
|
155
|
+
for (let offset = 0; offset < size; offset += 1) {
|
|
156
|
+
if (!sameEvent(events[startA + offset], events[startB + offset])) return false
|
|
157
|
+
}
|
|
158
|
+
return true
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const deduped: MessageToolEvent[] = []
|
|
162
|
+
for (let index = 0; index < events.length;) {
|
|
163
|
+
const remaining = events.length - index
|
|
164
|
+
let collapsed = false
|
|
165
|
+
for (let blockSize = Math.floor(remaining / 2); blockSize >= 1; blockSize -= 1) {
|
|
166
|
+
if (!sameBlock(index, index + blockSize, blockSize)) continue
|
|
167
|
+
for (let offset = 0; offset < blockSize; offset += 1) deduped.push(events[index + offset])
|
|
168
|
+
const blockStart = index
|
|
169
|
+
index += blockSize
|
|
170
|
+
while (index + blockSize <= events.length && sameBlock(blockStart, index, blockSize)) {
|
|
171
|
+
index += blockSize
|
|
172
|
+
}
|
|
173
|
+
collapsed = true
|
|
174
|
+
break
|
|
175
|
+
}
|
|
176
|
+
if (collapsed) continue
|
|
177
|
+
deduped.push(events[index])
|
|
178
|
+
index += 1
|
|
179
|
+
}
|
|
180
|
+
return deduped
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function extractDelegateResponse(outputText: string): string | null {
|
|
184
|
+
try {
|
|
185
|
+
const parsed = JSON.parse(outputText) as Record<string, unknown>
|
|
186
|
+
if (typeof parsed.response === 'string' && parsed.response.trim()) return parsed.response.trim()
|
|
187
|
+
if (typeof parsed.result === 'string' && parsed.result.trim()) return parsed.result.trim()
|
|
188
|
+
return null
|
|
189
|
+
} catch {
|
|
190
|
+
return null
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function normalizeWorkspaceSandboxLinks(text: string, cwd: string): string {
|
|
195
|
+
return text.replace(/sandbox:\/workspace\/([^\s)"'\]`]+)/g, (raw, relativePath: string) => {
|
|
196
|
+
const normalized = String(relativePath || '').replace(/^\/+/, '')
|
|
197
|
+
if (!normalized) return raw
|
|
198
|
+
const resolvedCwd = path.resolve(cwd)
|
|
199
|
+
const resolved = path.resolve(resolvedCwd, normalized)
|
|
200
|
+
if (!resolved.startsWith(resolvedCwd)) return raw
|
|
201
|
+
if (!fs.existsSync(resolved)) return raw
|
|
202
|
+
return `/api/files/serve?path=${encodeURIComponent(resolved)}`
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function normalizeAbsoluteFileMarkdownLinks(text: string): string {
|
|
207
|
+
return text.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (raw, label: string, target: string) => {
|
|
208
|
+
if (!path.isAbsolute(target)) return raw
|
|
209
|
+
const resolved = path.resolve(target)
|
|
210
|
+
if (!fs.existsSync(resolved)) return raw
|
|
211
|
+
return `[${label}](/api/files/serve?path=${encodeURIComponent(resolved)})`
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function normalizeAssistantArtifactLinks(text: string, cwd: string): string {
|
|
216
|
+
const uploadsNormalized = text.replace(/sandbox:\/api\/uploads\//g, '/api/uploads/')
|
|
217
|
+
const workspaceNormalized = normalizeWorkspaceSandboxLinks(uploadsNormalized, cwd)
|
|
218
|
+
return normalizeAbsoluteFileMarkdownLinks(workspaceNormalized)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function extractHeartbeatStatus(text: string): { goal?: string; status?: string; summary?: string; nextAction?: string } | null {
|
|
222
|
+
const match = text.match(/\[AGENT_HEARTBEAT_META\]\s*(\{[^\n]*\})/i)
|
|
223
|
+
if (!match) return null
|
|
224
|
+
try {
|
|
225
|
+
const meta = JSON.parse(match[1]) as Record<string, unknown>
|
|
226
|
+
const payload: { goal?: string; status?: string; summary?: string; nextAction?: string } = {}
|
|
227
|
+
if (typeof meta.goal === 'string' && meta.goal.trim()) payload.goal = meta.goal.trim()
|
|
228
|
+
if (typeof meta.status === 'string' && meta.status.trim()) payload.status = meta.status.trim()
|
|
229
|
+
if (typeof meta.summary === 'string' && meta.summary.trim()) payload.summary = meta.summary.trim()
|
|
230
|
+
if (typeof meta.next_action === 'string' && meta.next_action.trim()) payload.nextAction = meta.next_action.trim()
|
|
231
|
+
return Object.keys(payload).length > 0 ? payload : null
|
|
232
|
+
} catch {
|
|
233
|
+
return null
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
126
237
|
function shouldReplaceRecentAssistantMessage(params: {
|
|
127
238
|
previous: Message | null | undefined
|
|
128
239
|
nextToolEvents: MessageToolEvent[]
|
|
@@ -138,12 +249,17 @@ function shouldReplaceRecentAssistantMessage(params: {
|
|
|
138
249
|
return prevTools === 0
|
|
139
250
|
}
|
|
140
251
|
|
|
141
|
-
function
|
|
252
|
+
export function pruneSuppressedHeartbeatStreamMessage(messages: Message[]): boolean {
|
|
253
|
+
return pruneStreamingAssistantArtifacts(messages)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function requestedToolNamesFromMessage(message: string): string[] {
|
|
142
257
|
const lower = message.toLowerCase()
|
|
143
258
|
const candidates = [
|
|
144
259
|
'delegate_to_claude_code',
|
|
145
260
|
'delegate_to_codex_cli',
|
|
146
261
|
'delegate_to_opencode_cli',
|
|
262
|
+
'delegate_to_gemini_cli',
|
|
147
263
|
'connector_message_tool',
|
|
148
264
|
'sessions_tool',
|
|
149
265
|
'whoami_tool',
|
|
@@ -176,15 +292,24 @@ function requestedToolNamesFromMessage(message: string): string[] {
|
|
|
176
292
|
'sandbox_list_runtimes',
|
|
177
293
|
'git',
|
|
178
294
|
'canvas',
|
|
179
|
-
'delegate',
|
|
180
295
|
'schedule_wake',
|
|
181
296
|
'spawn_subagent',
|
|
297
|
+
'mailbox',
|
|
298
|
+
'ask_human',
|
|
299
|
+
'document',
|
|
300
|
+
'extract',
|
|
301
|
+
'table',
|
|
302
|
+
'crawl',
|
|
182
303
|
'context_status',
|
|
183
304
|
'context_summarize',
|
|
184
305
|
'openclaw_nodes',
|
|
185
306
|
'openclaw_workspace',
|
|
186
307
|
]
|
|
187
|
-
|
|
308
|
+
const requested = candidates.filter((name) => lower.includes(name.toLowerCase()))
|
|
309
|
+
if (/(^|[\s(])`delegate`([\s).,!?]|$)|\bdelegate tool\b|\buse delegate\b/.test(lower)) {
|
|
310
|
+
requested.push('delegate')
|
|
311
|
+
}
|
|
312
|
+
return Array.from(new Set(requested))
|
|
188
313
|
}
|
|
189
314
|
|
|
190
315
|
function parseKeyValueArgs(raw: string): Record<string, string> {
|
|
@@ -326,7 +451,7 @@ function extractDelegationTask(message: string, toolName: string): string | null
|
|
|
326
451
|
}
|
|
327
452
|
|
|
328
453
|
function hasToolEnabled(session: SessionWithTools, toolName: string): boolean {
|
|
329
|
-
return
|
|
454
|
+
return pluginIdMatches(session?.plugins || session?.tools || [], toolName)
|
|
330
455
|
}
|
|
331
456
|
|
|
332
457
|
function enabledDelegationTools(session: SessionWithTools): DelegateTool[] {
|
|
@@ -334,6 +459,7 @@ function enabledDelegationTools(session: SessionWithTools): DelegateTool[] {
|
|
|
334
459
|
if (hasToolEnabled(session, 'claude_code') || hasToolEnabled(session, 'delegate')) tools.push('delegate_to_claude_code')
|
|
335
460
|
if (hasToolEnabled(session, 'codex_cli')) tools.push('delegate_to_codex_cli')
|
|
336
461
|
if (hasToolEnabled(session, 'opencode_cli')) tools.push('delegate_to_opencode_cli')
|
|
462
|
+
if (hasToolEnabled(session, 'gemini_cli')) tools.push('delegate_to_gemini_cli')
|
|
337
463
|
return tools
|
|
338
464
|
}
|
|
339
465
|
|
|
@@ -394,17 +520,28 @@ function syncSessionFromAgent(sessionId: string): void {
|
|
|
394
520
|
if (!agent) return
|
|
395
521
|
|
|
396
522
|
let changed = false
|
|
397
|
-
if (
|
|
398
|
-
if (
|
|
399
|
-
|
|
400
|
-
|
|
523
|
+
if (!session.provider && agent.provider) { session.provider = agent.provider; changed = true }
|
|
524
|
+
if ((session.model === undefined || session.model === null || session.model === '') && agent.model !== undefined) {
|
|
525
|
+
session.model = agent.model
|
|
526
|
+
changed = true
|
|
527
|
+
}
|
|
528
|
+
if (session.credentialId === undefined && agent.credentialId !== undefined) {
|
|
529
|
+
session.credentialId = agent.credentialId ?? null
|
|
530
|
+
changed = true
|
|
531
|
+
}
|
|
532
|
+
if ((session.apiEndpoint === undefined || session.apiEndpoint === null) && agent.apiEndpoint !== undefined) {
|
|
401
533
|
const normalized = normalizeProviderEndpoint(agent.provider, agent.apiEndpoint ?? null)
|
|
402
534
|
if (normalized !== session.apiEndpoint) { session.apiEndpoint = normalized; changed = true }
|
|
403
535
|
}
|
|
404
|
-
if (!Array.isArray(session.
|
|
405
|
-
session.
|
|
536
|
+
if (!Array.isArray(session.plugins)) {
|
|
537
|
+
session.plugins = Array.isArray(agent.plugins) ? [...agent.plugins] : []
|
|
406
538
|
changed = true
|
|
407
539
|
}
|
|
540
|
+
const isShortcutChat = session.shortcutForAgentId === agent.id || agent.threadSessionId === sessionId
|
|
541
|
+
if (isShortcutChat) {
|
|
542
|
+
if (session.shortcutForAgentId !== agent.id) { session.shortcutForAgentId = agent.id; changed = true }
|
|
543
|
+
if (session.name !== agent.name) { session.name = agent.name; changed = true }
|
|
544
|
+
}
|
|
408
545
|
|
|
409
546
|
if (changed) {
|
|
410
547
|
sessions[sessionId] = session
|
|
@@ -431,6 +568,8 @@ function buildAgentSystemPrompt(session: Session): string | undefined {
|
|
|
431
568
|
if (agent.description) identityLines.push(`Description: ${agent.description}`)
|
|
432
569
|
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.')
|
|
433
570
|
parts.push(identityLines.join('\n'))
|
|
571
|
+
const continuityBlock = buildIdentityContinuityContext(session, agent)
|
|
572
|
+
if (continuityBlock) parts.push(continuityBlock)
|
|
434
573
|
|
|
435
574
|
// 2. Runtime & Capabilities (OpenClaw Style)
|
|
436
575
|
const runtimeLines = [
|
|
@@ -529,75 +668,6 @@ function estimateConversationTone(text: string): string {
|
|
|
529
668
|
return 'neutral'
|
|
530
669
|
}
|
|
531
670
|
|
|
532
|
-
const AUTO_MEMORY_MIN_INTERVAL_MS = 45 * 60 * 1000
|
|
533
|
-
|
|
534
|
-
function normalizeMemoryText(value: string): string {
|
|
535
|
-
return (value || '').replace(/\s+/g, ' ').trim()
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
function shouldStoreAutoMemoryNote(opts: {
|
|
539
|
-
session: Session
|
|
540
|
-
source: string
|
|
541
|
-
internal: boolean
|
|
542
|
-
message: string
|
|
543
|
-
response: string
|
|
544
|
-
now: number
|
|
545
|
-
}): boolean {
|
|
546
|
-
const { session, source, internal, message, response, now } = opts
|
|
547
|
-
if (internal) return false
|
|
548
|
-
if (source !== 'chat' && source !== 'connector') return false
|
|
549
|
-
if (!session?.agentId) return false
|
|
550
|
-
if (!Array.isArray(session.tools) || !session.tools.includes('memory')) return false
|
|
551
|
-
const msg = (message || '').trim()
|
|
552
|
-
const resp = (response || '').trim()
|
|
553
|
-
if (msg.length < 20 || resp.length < 40) return false
|
|
554
|
-
if (/^(ok|okay|cool|thanks|thx|got it|nice)[.! ]*$/i.test(msg)) return false
|
|
555
|
-
if (resp === 'HEARTBEAT_OK') return false
|
|
556
|
-
const last = typeof session.lastAutoMemoryAt === 'number' ? session.lastAutoMemoryAt : 0
|
|
557
|
-
if (last > 0 && now - last < AUTO_MEMORY_MIN_INTERVAL_MS) return false
|
|
558
|
-
return true
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
function storeAutoMemoryNote(opts: {
|
|
562
|
-
session: Session
|
|
563
|
-
message: string
|
|
564
|
-
response: string
|
|
565
|
-
source: string
|
|
566
|
-
now: number
|
|
567
|
-
}): string | null {
|
|
568
|
-
const { session, message, response, source, now } = opts
|
|
569
|
-
try {
|
|
570
|
-
const db = getMemoryDb()
|
|
571
|
-
const compactMessage = message.replace(/\s+/g, ' ').trim().slice(0, 220)
|
|
572
|
-
const compactResponse = response.replace(/\s+/g, ' ').trim().slice(0, 700)
|
|
573
|
-
const title = `[auto] ${compactMessage.slice(0, 90)}`
|
|
574
|
-
const content = [
|
|
575
|
-
`source: ${source}`,
|
|
576
|
-
`user_request: ${compactMessage}`,
|
|
577
|
-
`assistant_outcome: ${compactResponse}`,
|
|
578
|
-
].join('\n')
|
|
579
|
-
const latest = db.getLatestBySessionCategory?.(session.id, 'execution')
|
|
580
|
-
if (latest) {
|
|
581
|
-
const sameTitle = normalizeMemoryText(latest.title) === normalizeMemoryText(title)
|
|
582
|
-
const sameContent = normalizeMemoryText(latest.content) === normalizeMemoryText(content)
|
|
583
|
-
if (sameTitle && sameContent) {
|
|
584
|
-
session.lastAutoMemoryAt = now
|
|
585
|
-
return latest.id
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
const created = db.add({
|
|
589
|
-
agentId: session.agentId as string,
|
|
590
|
-
sessionId: session.id as string,
|
|
591
|
-
category: 'execution',
|
|
592
|
-
title,
|
|
593
|
-
content,
|
|
594
|
-
})
|
|
595
|
-
session.lastAutoMemoryAt = now
|
|
596
|
-
return created?.id || null
|
|
597
|
-
} catch {
|
|
598
|
-
return null
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
671
|
|
|
602
672
|
export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promise<ExecuteChatTurnResult> {
|
|
603
673
|
const { message } = input
|
|
@@ -618,31 +688,61 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
618
688
|
const sessions = loadSessions()
|
|
619
689
|
const session = sessions[sessionId]
|
|
620
690
|
if (!session) throw new Error(`Session not found: ${sessionId}`)
|
|
691
|
+
session.messages = Array.isArray(session.messages) ? session.messages : []
|
|
692
|
+
const runStartedAt = Date.now()
|
|
693
|
+
const runMessageStartIndex = session.messages.length
|
|
621
694
|
|
|
622
695
|
const appSettings = loadSettings()
|
|
623
|
-
const
|
|
624
|
-
const
|
|
625
|
-
const
|
|
626
|
-
const
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
696
|
+
const agentForSession = session.agentId ? loadAgents()[session.agentId] : null
|
|
697
|
+
const toolPolicy = resolveSessionToolPolicy(session.plugins, appSettings)
|
|
698
|
+
const isHeartbeatRun = isInternalHeartbeatRun(internal, source)
|
|
699
|
+
const isAutoRunNoHistory = isHeartbeatRun
|
|
700
|
+
const heartbeatStatusOnly = false
|
|
701
|
+
if (shouldApplySessionFreshnessReset(source)) {
|
|
702
|
+
const freshness = evaluateSessionFreshness({
|
|
703
|
+
session,
|
|
704
|
+
policy: resolveSessionResetPolicy({
|
|
705
|
+
session,
|
|
706
|
+
agent: agentForSession,
|
|
707
|
+
settings: appSettings,
|
|
708
|
+
}),
|
|
709
|
+
})
|
|
710
|
+
if (!freshness.fresh) {
|
|
711
|
+
try { syncSessionArchiveMemory(session, { agent: agentForSession }) } catch { /* archive sync is best-effort */ }
|
|
712
|
+
resetSessionRuntime(session, freshness.reason || 'session_reset')
|
|
713
|
+
onEvent?.({ t: 'status', text: JSON.stringify({ sessionReset: freshness.reason || 'session_reset' }) })
|
|
714
|
+
sessions[sessionId] = session
|
|
715
|
+
saveSessions(sessions)
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
const pluginsForRun = heartbeatStatusOnly ? [] : toolPolicy.enabledPlugins
|
|
719
|
+
let sessionForRun = pluginsForRun === session.plugins
|
|
633
720
|
? session
|
|
634
|
-
: { ...session,
|
|
721
|
+
: { ...session, plugins: pluginsForRun }
|
|
722
|
+
let effectiveMessage = message
|
|
723
|
+
|
|
724
|
+
if (pluginsForRun.length > 0) {
|
|
725
|
+
try {
|
|
726
|
+
effectiveMessage = await getPluginManager().transformText(
|
|
727
|
+
'transformInboundMessage',
|
|
728
|
+
{ session: sessionForRun, text: message },
|
|
729
|
+
{ enabledIds: pluginsForRun },
|
|
730
|
+
)
|
|
731
|
+
} catch {
|
|
732
|
+
effectiveMessage = message
|
|
733
|
+
}
|
|
734
|
+
}
|
|
635
735
|
|
|
636
736
|
// Apply model override for heartbeat runs (cheaper model)
|
|
637
737
|
if (isHeartbeatRun && input.modelOverride) {
|
|
638
738
|
sessionForRun = { ...sessionForRun, model: input.modelOverride }
|
|
639
739
|
}
|
|
640
740
|
|
|
641
|
-
if (!heartbeatStatusOnly && toolPolicy.
|
|
642
|
-
const blockedSummary = toolPolicy.
|
|
741
|
+
if (!heartbeatStatusOnly && toolPolicy.blockedPlugins.length > 0) {
|
|
742
|
+
const blockedSummary = toolPolicy.blockedPlugins
|
|
643
743
|
.map((entry) => `${entry.tool} (${entry.reason})`)
|
|
644
744
|
.join(', ')
|
|
645
|
-
onEvent?.({ t: 'err', text: `Capability policy blocked
|
|
745
|
+
onEvent?.({ t: 'err', text: `Capability policy blocked plugins for this run: ${blockedSummary}` })
|
|
646
746
|
}
|
|
647
747
|
|
|
648
748
|
// --- Agent spend-limit enforcement (hourly/daily/monthly) ---
|
|
@@ -727,7 +827,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
727
827
|
internal,
|
|
728
828
|
provider: session.provider,
|
|
729
829
|
model: session.model,
|
|
730
|
-
messagePreview:
|
|
830
|
+
messagePreview: effectiveMessage.slice(0, 200),
|
|
731
831
|
hasImage: !!(imagePath || imageUrl),
|
|
732
832
|
},
|
|
733
833
|
})
|
|
@@ -744,7 +844,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
744
844
|
|
|
745
845
|
if (!internal) {
|
|
746
846
|
const linkAnalysis = await runLinkUnderstanding(message)
|
|
747
|
-
|
|
847
|
+
const nextUserMessage: Message = {
|
|
748
848
|
role: 'user',
|
|
749
849
|
text: message,
|
|
750
850
|
time: Date.now(),
|
|
@@ -752,7 +852,8 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
752
852
|
imageUrl: imageUrl || undefined,
|
|
753
853
|
attachedFiles: attachedFiles?.length ? attachedFiles : undefined,
|
|
754
854
|
replyToId: input.replyToId || undefined,
|
|
755
|
-
}
|
|
855
|
+
}
|
|
856
|
+
session.messages.push(nextUserMessage)
|
|
756
857
|
if (linkAnalysis.length > 0) {
|
|
757
858
|
session.messages.push({
|
|
758
859
|
role: 'assistant',
|
|
@@ -763,6 +864,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
763
864
|
}
|
|
764
865
|
session.lastActiveAt = Date.now()
|
|
765
866
|
saveSessions(sessions)
|
|
867
|
+
try {
|
|
868
|
+
await getPluginManager().runHook('onMessage', { session, message: nextUserMessage }, { enabledIds: pluginsForRun })
|
|
869
|
+
} catch { /* onMessage hooks are non-critical */ }
|
|
766
870
|
}
|
|
767
871
|
|
|
768
872
|
const systemPrompt = buildAgentSystemPrompt(session)
|
|
@@ -811,19 +915,18 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
811
915
|
const fresh = loadSessions()
|
|
812
916
|
const current = fresh[sessionId]
|
|
813
917
|
if (!current) return
|
|
918
|
+
current.messages = Array.isArray(current.messages) ? current.messages : []
|
|
814
919
|
const partialMsg: Message = {
|
|
815
920
|
role: 'assistant',
|
|
816
921
|
text: streamingPartialText,
|
|
817
922
|
time: Date.now(),
|
|
818
923
|
streaming: true,
|
|
819
|
-
toolEvents: toolEvents.length ? [...toolEvents] : undefined,
|
|
820
|
-
}
|
|
821
|
-
const lastMsg = current.messages.at(-1)
|
|
822
|
-
if (lastMsg?.streaming) {
|
|
823
|
-
current.messages[current.messages.length - 1] = partialMsg
|
|
824
|
-
} else {
|
|
825
|
-
current.messages.push(partialMsg)
|
|
924
|
+
toolEvents: toolEvents.length ? dedupeConsecutiveToolEvents([...toolEvents]) : undefined,
|
|
826
925
|
}
|
|
926
|
+
upsertStreamingAssistantArtifact(current.messages, partialMsg, {
|
|
927
|
+
minIndex: runMessageStartIndex,
|
|
928
|
+
minTime: runStartedAt,
|
|
929
|
+
})
|
|
827
930
|
fresh[sessionId] = current
|
|
828
931
|
saveSessions(fresh)
|
|
829
932
|
notify(`messages:${sessionId}`)
|
|
@@ -861,7 +964,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
861
964
|
const responseCacheConfig = resolveLlmResponseCacheConfig(appSettings)
|
|
862
965
|
let responseCacheHit = false
|
|
863
966
|
let responseCacheInput: LlmResponseCacheKeyInput | null = null
|
|
864
|
-
const
|
|
967
|
+
const hasPlugins = !!(sessionForRun.plugins?.length || sessionForRun.tools?.length) && !NON_LANGGRAPH_PROVIDER_IDS.has(providerType)
|
|
865
968
|
|
|
866
969
|
let durationMs = 0
|
|
867
970
|
const startTs = Date.now()
|
|
@@ -873,11 +976,11 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
873
976
|
? getSessionMessages(sessionId).slice(-6)
|
|
874
977
|
: undefined
|
|
875
978
|
|
|
876
|
-
console.log(`[chat-execution] provider=${providerType},
|
|
877
|
-
if (
|
|
979
|
+
console.log(`[chat-execution] provider=${providerType}, hasPlugins=${hasPlugins}, imagePath=${imagePath || 'none'}, attachedFiles=${attachedFiles?.length || 0}, plugins=${(sessionForRun.plugins || sessionForRun.tools || []).length}`)
|
|
980
|
+
if (hasPlugins) {
|
|
878
981
|
fullResponse = (await streamAgentChat({
|
|
879
982
|
session: sessionForRun,
|
|
880
|
-
message:
|
|
983
|
+
message: effectiveMessage,
|
|
881
984
|
imagePath,
|
|
882
985
|
attachedFiles,
|
|
883
986
|
apiKey,
|
|
@@ -895,7 +998,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
895
998
|
model: sessionForRun.model,
|
|
896
999
|
apiEndpoint: sessionForRun.apiEndpoint || '',
|
|
897
1000
|
systemPrompt,
|
|
898
|
-
message:
|
|
1001
|
+
message: effectiveMessage,
|
|
899
1002
|
imagePath,
|
|
900
1003
|
imageUrl,
|
|
901
1004
|
attachedFiles,
|
|
@@ -923,7 +1026,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
923
1026
|
} else {
|
|
924
1027
|
fullResponse = await provider.handler.streamChat({
|
|
925
1028
|
session: sessionForRun,
|
|
926
|
-
message:
|
|
1029
|
+
message: effectiveMessage,
|
|
927
1030
|
imagePath,
|
|
928
1031
|
apiKey,
|
|
929
1032
|
systemPrompt,
|
|
@@ -967,7 +1070,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
967
1070
|
|
|
968
1071
|
// Record usage for the direct (non-tools) streamChat path.
|
|
969
1072
|
// streamAgentChat already calls appendUsage internally for the tools path.
|
|
970
|
-
if (!
|
|
1073
|
+
if (!hasPlugins && fullResponse && !errorMessage && !responseCacheHit) {
|
|
971
1074
|
const inputTokens = directUsage.received ? directUsage.inputTokens : Math.ceil(message.length / 4)
|
|
972
1075
|
const outputTokens = directUsage.received ? directUsage.outputTokens : Math.ceil(fullResponse.length / 4)
|
|
973
1076
|
const totalTokens = inputTokens + outputTokens
|
|
@@ -998,7 +1101,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
998
1101
|
? requestedToolNamesFromMessage(message)
|
|
999
1102
|
: []
|
|
1000
1103
|
const routingDecision = (!internal && source === 'chat')
|
|
1001
|
-
? routeTaskIntent(message,
|
|
1104
|
+
? routeTaskIntent(message, pluginsForRun, appSettings)
|
|
1002
1105
|
: null
|
|
1003
1106
|
const calledNames = new Set((toolEvents || []).map((t) => t.name))
|
|
1004
1107
|
|
|
@@ -1034,6 +1137,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1034
1137
|
if (requestedName === 'delegate_to_opencode_cli') {
|
|
1035
1138
|
return { toolName: 'delegate', args: { ...rawArgs, backend: 'opencode' } }
|
|
1036
1139
|
}
|
|
1140
|
+
if (requestedName === 'delegate_to_gemini_cli') {
|
|
1141
|
+
return { toolName: 'delegate', args: { ...rawArgs, backend: 'gemini' } }
|
|
1142
|
+
}
|
|
1037
1143
|
|
|
1038
1144
|
const managePrefix = 'manage_'
|
|
1039
1145
|
if (requestedName.startsWith(managePrefix) && requestedName !== 'manage_platform') {
|
|
@@ -1065,7 +1171,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1065
1171
|
return false
|
|
1066
1172
|
}
|
|
1067
1173
|
const agent = session.agentId ? loadAgents()[session.agentId] : null
|
|
1068
|
-
const { tools, cleanup } = await buildSessionTools(session.cwd, sessionForRun.tools || [], {
|
|
1174
|
+
const { tools, cleanup } = await buildSessionTools(session.cwd, sessionForRun.plugins || sessionForRun.tools || [], {
|
|
1069
1175
|
agentId: session.agentId || null,
|
|
1070
1176
|
sessionId,
|
|
1071
1177
|
platformAssignScope: agent?.platformAssignScope || 'self',
|
|
@@ -1081,10 +1187,16 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1081
1187
|
const toolOutput = await selectedTool.invoke(translated.args)
|
|
1082
1188
|
const outputText = typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput)
|
|
1083
1189
|
emit({ t: 'tool_result', toolName, toolOutput: outputText })
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1190
|
+
const delegateResponse = (
|
|
1191
|
+
toolName === 'delegate'
|
|
1192
|
+
|| toolName.startsWith('delegate_to_')
|
|
1193
|
+
) ? extractDelegateResponse(outputText) : null
|
|
1194
|
+
if (delegateResponse) {
|
|
1195
|
+
fullResponse = delegateResponse
|
|
1196
|
+
} else if (!fullResponse.trim() && outputText?.trim()) {
|
|
1197
|
+
// Don't overwrite fullResponse with raw tool output — it's already captured
|
|
1198
|
+
// in toolEvents. Only set a brief notice when the LLM produced no text,
|
|
1199
|
+
// so the message bubble isn't empty.
|
|
1088
1200
|
const label = toolName.replace(/_/g, ' ')
|
|
1089
1201
|
fullResponse = `Used **${label}** — see tool output above for details.`
|
|
1090
1202
|
}
|
|
@@ -1109,10 +1221,11 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1109
1221
|
}
|
|
1110
1222
|
}
|
|
1111
1223
|
|
|
1112
|
-
const forcedDelegationTools:
|
|
1224
|
+
const forcedDelegationTools: DelegateTool[] = [
|
|
1113
1225
|
'delegate_to_claude_code',
|
|
1114
1226
|
'delegate_to_codex_cli',
|
|
1115
1227
|
'delegate_to_opencode_cli',
|
|
1228
|
+
'delegate_to_gemini_cli',
|
|
1116
1229
|
]
|
|
1117
1230
|
for (const toolName of forcedDelegationTools) {
|
|
1118
1231
|
if (!requestedToolNames.includes(toolName)) continue
|
|
@@ -1136,7 +1249,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1136
1249
|
const delegationOrder = rankDelegatesByHealth(baseDelegationOrder as DelegateTool[])
|
|
1137
1250
|
.filter((tool) => enabledDelegateTools.includes(tool))
|
|
1138
1251
|
for (const delegateTool of delegationOrder) {
|
|
1139
|
-
const invoked = await invokeSessionTool(delegateTool, { task:
|
|
1252
|
+
const invoked = await invokeSessionTool(delegateTool, { task: effectiveMessage.trim() }, 'Auto-delegation failed')
|
|
1140
1253
|
if (invoked) break
|
|
1141
1254
|
}
|
|
1142
1255
|
}
|
|
@@ -1156,7 +1269,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1156
1269
|
for (const delegateTool of fallbackOrder) {
|
|
1157
1270
|
const invoked = await invokeSessionTool(
|
|
1158
1271
|
delegateTool,
|
|
1159
|
-
{ task:
|
|
1272
|
+
{ task: effectiveMessage.trim() },
|
|
1160
1273
|
`Provider failover via ${delegateTool} failed`,
|
|
1161
1274
|
)
|
|
1162
1275
|
if (invoked) {
|
|
@@ -1174,7 +1287,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1174
1287
|
if (canAutoRouteWithTools && routingDecision?.intent === 'browsing' && routingDecision.primaryUrl && hasToolEnabled(sessionForRun, 'browser')) {
|
|
1175
1288
|
await invokeSessionTool(
|
|
1176
1289
|
'browser',
|
|
1177
|
-
{ action: '
|
|
1290
|
+
{ action: 'read_page', url: routingDecision.primaryUrl },
|
|
1178
1291
|
'Auto browser routing failed',
|
|
1179
1292
|
)
|
|
1180
1293
|
}
|
|
@@ -1184,7 +1297,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1184
1297
|
if (routeUrl && hasToolEnabled(sessionForRun, 'web_fetch')) {
|
|
1185
1298
|
await invokeSessionTool('web_fetch', { url: routeUrl }, 'Auto web_fetch routing failed')
|
|
1186
1299
|
} else if (hasToolEnabled(sessionForRun, 'web_search')) {
|
|
1187
|
-
await invokeSessionTool('web_search', { query:
|
|
1300
|
+
await invokeSessionTool('web_search', { query: effectiveMessage.trim(), maxResults: 5 }, 'Auto web_search routing failed')
|
|
1188
1301
|
}
|
|
1189
1302
|
}
|
|
1190
1303
|
|
|
@@ -1219,27 +1332,23 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1219
1332
|
errorMessage = streamErrors[streamErrors.length - 1]
|
|
1220
1333
|
}
|
|
1221
1334
|
|
|
1222
|
-
|
|
1223
|
-
|
|
1335
|
+
let finalText = (fullResponse || '').trim() || (!internal && errorMessage ? `Error: ${errorMessage}` : '')
|
|
1336
|
+
if (pluginsForRun.length > 0 && finalText && !isHeartbeatRun) {
|
|
1337
|
+
try {
|
|
1338
|
+
finalText = await getPluginManager().transformText(
|
|
1339
|
+
'transformOutboundMessage',
|
|
1340
|
+
{ session: sessionForRun, text: finalText },
|
|
1341
|
+
{ enabledIds: pluginsForRun },
|
|
1342
|
+
)
|
|
1343
|
+
} catch { /* outbound transforms are non-critical */ }
|
|
1344
|
+
}
|
|
1345
|
+
finalText = normalizeAssistantArtifactLinks(finalText, session.cwd)
|
|
1346
|
+
const textForPersistence = stripMainLoopMetaForPersistence(finalText)
|
|
1347
|
+
const persistedToolEvents = dedupeConsecutiveToolEvents(toolEvents)
|
|
1224
1348
|
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
if (metaMatch) {
|
|
1229
|
-
try {
|
|
1230
|
-
const meta = JSON.parse(metaMatch[1])
|
|
1231
|
-
const statusPayload: Record<string, string | undefined> = {}
|
|
1232
|
-
if (meta.goal) statusPayload.goal = String(meta.goal)
|
|
1233
|
-
if (meta.status) statusPayload.status = String(meta.status)
|
|
1234
|
-
if (meta.summary) statusPayload.summary = String(meta.summary)
|
|
1235
|
-
if (meta.next_action) statusPayload.nextAction = String(meta.next_action)
|
|
1236
|
-
if (Object.keys(statusPayload).length > 0) {
|
|
1237
|
-
emit({ t: 'status', text: JSON.stringify(statusPayload) })
|
|
1238
|
-
}
|
|
1239
|
-
} catch {
|
|
1240
|
-
// ignore malformed meta JSON
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1349
|
+
if (isHeartbeatRun && finalText) {
|
|
1350
|
+
const heartbeatStatus = extractHeartbeatStatus(finalText)
|
|
1351
|
+
if (heartbeatStatus) emit({ t: 'status', text: JSON.stringify(heartbeatStatus) })
|
|
1243
1352
|
}
|
|
1244
1353
|
|
|
1245
1354
|
// HEARTBEAT_OK suppression
|
|
@@ -1275,7 +1384,13 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1275
1384
|
const fresh = loadSessions()
|
|
1276
1385
|
const current = fresh[sessionId]
|
|
1277
1386
|
if (current) {
|
|
1387
|
+
current.messages = Array.isArray(current.messages) ? current.messages : []
|
|
1388
|
+
const currentAgent = current.agentId ? loadAgents()[current.agentId] : null
|
|
1278
1389
|
let changed = false
|
|
1390
|
+
changed = pruneStreamingAssistantArtifacts(current.messages, {
|
|
1391
|
+
minIndex: runMessageStartIndex,
|
|
1392
|
+
minTime: runStartedAt,
|
|
1393
|
+
}) || changed
|
|
1279
1394
|
const persistField = (key: string, value: unknown) => {
|
|
1280
1395
|
const normalized = normalizeResumeId(value)
|
|
1281
1396
|
if ((current as Record<string, unknown>)[key] !== normalized) {
|
|
@@ -1307,7 +1422,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1307
1422
|
}
|
|
1308
1423
|
|
|
1309
1424
|
if (shouldPersistAssistant) {
|
|
1310
|
-
const persistedKind =
|
|
1425
|
+
const persistedKind = isHeartbeatRun ? 'heartbeat' : 'chat'
|
|
1311
1426
|
const persistedText = heartbeatClassification === 'strip'
|
|
1312
1427
|
? textForPersistence.replace(/HEARTBEAT_OK/gi, '').trim()
|
|
1313
1428
|
: textForPersistence
|
|
@@ -1317,13 +1432,13 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1317
1432
|
text: persistedText,
|
|
1318
1433
|
time: nowTs,
|
|
1319
1434
|
thinking: thinkingText || undefined,
|
|
1320
|
-
toolEvents:
|
|
1435
|
+
toolEvents: persistedToolEvents.length ? persistedToolEvents : undefined,
|
|
1321
1436
|
kind: persistedKind,
|
|
1322
1437
|
}
|
|
1323
1438
|
const previous = current.messages.at(-1)
|
|
1324
1439
|
if (previous?.streaming || shouldReplaceRecentAssistantMessage({
|
|
1325
1440
|
previous,
|
|
1326
|
-
nextToolEvents:
|
|
1441
|
+
nextToolEvents: persistedToolEvents,
|
|
1327
1442
|
nextKind: persistedKind,
|
|
1328
1443
|
now: nowTs,
|
|
1329
1444
|
})) {
|
|
@@ -1336,6 +1451,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1336
1451
|
current.lastHeartbeatSentAt = nowTs
|
|
1337
1452
|
}
|
|
1338
1453
|
changed = true
|
|
1454
|
+
try {
|
|
1455
|
+
await getPluginManager().runHook('onMessage', { session: current, message: nextAssistantMessage }, { enabledIds: pluginsForRun })
|
|
1456
|
+
} catch { /* onMessage hooks are non-critical */ }
|
|
1339
1457
|
|
|
1340
1458
|
// Conversation tone detection
|
|
1341
1459
|
if (!internal) {
|
|
@@ -1390,30 +1508,32 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1390
1508
|
}
|
|
1391
1509
|
}
|
|
1392
1510
|
}
|
|
1511
|
+
if (isHeartbeatRun && heartbeatClassification === 'suppress') {
|
|
1512
|
+
changed = pruneSuppressedHeartbeatStreamMessage(current.messages) || changed
|
|
1513
|
+
}
|
|
1393
1514
|
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
internal,
|
|
1398
|
-
message: message,
|
|
1399
|
-
response: textForPersistence,
|
|
1400
|
-
now: Date.now(),
|
|
1401
|
-
})
|
|
1402
|
-
if (autoMemoryEligible) {
|
|
1403
|
-
const storedId = storeAutoMemoryNote({
|
|
1515
|
+
// Fire afterChatTurn hook for all enabled plugins (memory auto-save, logging, etc.)
|
|
1516
|
+
try {
|
|
1517
|
+
await getPluginManager().runHook('afterChatTurn', {
|
|
1404
1518
|
session: current,
|
|
1405
|
-
message
|
|
1519
|
+
message,
|
|
1406
1520
|
response: textForPersistence,
|
|
1407
1521
|
source,
|
|
1408
|
-
|
|
1409
|
-
})
|
|
1410
|
-
|
|
1411
|
-
}
|
|
1522
|
+
internal,
|
|
1523
|
+
}, { enabledIds: pluginsForRun })
|
|
1524
|
+
} catch { /* afterChatTurn hooks are non-critical */ }
|
|
1412
1525
|
|
|
1413
1526
|
// Don't extend idle timeout for heartbeat runs — only user-initiated activity counts
|
|
1414
|
-
if (source
|
|
1527
|
+
if (!isHeartbeatSource(source)) {
|
|
1415
1528
|
current.lastActiveAt = Date.now()
|
|
1416
1529
|
}
|
|
1530
|
+
|
|
1531
|
+
refreshSessionIdentityState(current, currentAgent)
|
|
1532
|
+
changed = true
|
|
1533
|
+
try {
|
|
1534
|
+
const archiveSync = syncSessionArchiveMemory(current, { agent: currentAgent })
|
|
1535
|
+
if (archiveSync.stored) changed = true
|
|
1536
|
+
} catch { /* archive sync is best-effort */ }
|
|
1417
1537
|
fresh[sessionId] = current
|
|
1418
1538
|
saveSessions(fresh)
|
|
1419
1539
|
notify(`messages:${sessionId}`)
|
|
@@ -1424,7 +1544,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1424
1544
|
sessionId,
|
|
1425
1545
|
text: finalText,
|
|
1426
1546
|
persisted: shouldPersistAssistant,
|
|
1427
|
-
toolEvents,
|
|
1547
|
+
toolEvents: persistedToolEvents,
|
|
1428
1548
|
error: errorMessage,
|
|
1429
1549
|
inputTokens: accumulatedUsage.inputTokens || undefined,
|
|
1430
1550
|
outputTokens: accumulatedUsage.outputTokens || undefined,
|