@swarmclawai/swarmclaw 0.6.8 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -45
- package/next.config.ts +31 -6
- package/package.json +3 -2
- package/src/app/api/agents/[id]/thread/route.ts +1 -0
- package/src/app/api/agents/route.ts +18 -5
- package/src/app/api/approvals/route.ts +22 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
- package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
- package/src/app/api/memory/route.ts +36 -5
- package/src/app/api/notifications/route.ts +3 -0
- package/src/app/api/plugins/install/route.ts +57 -5
- package/src/app/api/plugins/marketplace/route.ts +73 -22
- package/src/app/api/plugins/route.ts +61 -1
- package/src/app/api/plugins/ui/route.ts +34 -0
- package/src/app/api/settings/route.ts +62 -0
- package/src/app/api/setup/doctor/route.ts +22 -5
- package/src/app/api/tasks/[id]/approve/route.ts +4 -3
- package/src/app/api/tasks/[id]/route.ts +11 -3
- package/src/app/api/tasks/route.ts +8 -2
- package/src/app/globals.css +27 -0
- package/src/app/page.tsx +10 -5
- package/src/cli/index.js +13 -0
- package/src/components/activity/activity-feed.tsx +9 -2
- package/src/components/agents/agent-avatar.tsx +5 -1
- package/src/components/agents/agent-card.tsx +55 -9
- package/src/components/agents/agent-sheet.tsx +86 -29
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/auth/access-key-gate.tsx +63 -54
- package/src/components/auth/user-picker.tsx +37 -32
- package/src/components/chat/chat-area.tsx +11 -0
- package/src/components/chat/chat-header.tsx +69 -25
- package/src/components/chat/chat-tool-toggles.tsx +2 -2
- package/src/components/chat/code-block.tsx +3 -1
- package/src/components/chat/exec-approval-card.tsx +8 -1
- package/src/components/chat/message-bubble.tsx +164 -4
- package/src/components/chat/message-list.tsx +30 -4
- package/src/components/chat/session-approval-card.tsx +80 -0
- package/src/components/chat/streaming-bubble.tsx +6 -5
- package/src/components/chat/thinking-indicator.tsx +48 -12
- package/src/components/chat/tool-request-banner.tsx +39 -20
- package/src/components/chatrooms/chatroom-list.tsx +11 -4
- package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
- package/src/components/connectors/connector-list.tsx +33 -11
- package/src/components/connectors/connector-sheet.tsx +29 -6
- package/src/components/home/home-view.tsx +20 -14
- package/src/components/input/chat-input.tsx +22 -1
- package/src/components/knowledge/knowledge-list.tsx +17 -18
- package/src/components/knowledge/knowledge-sheet.tsx +9 -5
- package/src/components/layout/app-layout.tsx +73 -21
- package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
- package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
- package/src/components/memory/memory-list.tsx +20 -13
- package/src/components/plugins/plugin-list.tsx +213 -59
- package/src/components/plugins/plugin-sheet.tsx +119 -24
- package/src/components/projects/project-list.tsx +17 -9
- package/src/components/providers/provider-list.tsx +21 -6
- package/src/components/providers/provider-sheet.tsx +42 -25
- package/src/components/runs/run-list.tsx +17 -13
- package/src/components/schedules/schedule-card.tsx +10 -3
- package/src/components/schedules/schedule-list.tsx +2 -2
- package/src/components/schedules/schedule-sheet.tsx +19 -7
- package/src/components/secrets/secret-sheet.tsx +7 -2
- package/src/components/secrets/secrets-list.tsx +18 -5
- package/src/components/sessions/new-session-sheet.tsx +183 -376
- package/src/components/sessions/session-card.tsx +10 -2
- package/src/components/settings/gateway-connection-panel.tsx +9 -8
- package/src/components/shared/command-palette.tsx +13 -5
- package/src/components/shared/empty-state.tsx +20 -8
- package/src/components/shared/notification-center.tsx +134 -86
- package/src/components/shared/profile-sheet.tsx +4 -0
- package/src/components/shared/settings/plugin-manager.tsx +360 -135
- package/src/components/shared/settings/section-capability-policy.tsx +3 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +144 -0
- package/src/components/skills/clawhub-browser.tsx +1 -0
- package/src/components/skills/skill-list.tsx +31 -12
- package/src/components/skills/skill-sheet.tsx +20 -7
- package/src/components/tasks/approvals-panel.tsx +170 -66
- package/src/components/tasks/task-board.tsx +20 -12
- package/src/components/tasks/task-card.tsx +21 -7
- package/src/components/tasks/task-column.tsx +4 -3
- package/src/components/tasks/task-list.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +130 -1
- package/src/components/ui/dialog.tsx +1 -0
- package/src/components/ui/sheet.tsx +1 -0
- package/src/components/usage/metrics-dashboard.tsx +66 -64
- package/src/components/wallets/wallet-panel.tsx +65 -41
- package/src/components/wallets/wallet-section.tsx +9 -3
- package/src/components/webhooks/webhook-list.tsx +21 -12
- package/src/components/webhooks/webhook-sheet.tsx +13 -3
- package/src/lib/approval-display.test.ts +45 -0
- package/src/lib/approval-display.ts +62 -0
- package/src/lib/clipboard.ts +38 -0
- package/src/lib/memory.ts +8 -0
- package/src/lib/providers/claude-cli.ts +5 -3
- package/src/lib/providers/index.ts +67 -21
- package/src/lib/runtime-loop.ts +3 -2
- package/src/lib/server/approvals.ts +150 -0
- package/src/lib/server/chat-execution.ts +223 -62
- package/src/lib/server/clawhub-client.ts +82 -6
- package/src/lib/server/connectors/manager.ts +27 -1
- package/src/lib/server/cost.test.ts +73 -0
- package/src/lib/server/cost.ts +165 -34
- package/src/lib/server/daemon-state.ts +42 -0
- package/src/lib/server/data-dir.ts +18 -1
- package/src/lib/server/integrity-monitor.ts +208 -0
- package/src/lib/server/llm-response-cache.test.ts +102 -0
- package/src/lib/server/llm-response-cache.ts +227 -0
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/main-session.ts +6 -3
- package/src/lib/server/mcp-conformance.test.ts +18 -0
- package/src/lib/server/mcp-conformance.ts +233 -0
- package/src/lib/server/memory-db.ts +180 -17
- package/src/lib/server/memory-retrieval.test.ts +56 -0
- package/src/lib/server/orchestrator-lg.ts +4 -1
- package/src/lib/server/orchestrator.ts +4 -3
- package/src/lib/server/plugins.ts +650 -142
- package/src/lib/server/process-manager.ts +18 -0
- package/src/lib/server/queue.ts +253 -11
- package/src/lib/server/runtime-settings.ts +9 -0
- package/src/lib/server/session-run-manager.test.ts +23 -0
- package/src/lib/server/session-run-manager.ts +11 -1
- package/src/lib/server/session-tools/canvas.ts +85 -50
- package/src/lib/server/session-tools/chatroom.ts +130 -127
- package/src/lib/server/session-tools/connector.ts +233 -454
- package/src/lib/server/session-tools/context-mgmt.ts +87 -105
- package/src/lib/server/session-tools/crud.ts +84 -7
- package/src/lib/server/session-tools/delegate.ts +351 -752
- package/src/lib/server/session-tools/discovery.ts +198 -0
- package/src/lib/server/session-tools/edit_file.ts +82 -0
- package/src/lib/server/session-tools/file-send.test.ts +39 -0
- package/src/lib/server/session-tools/file.ts +257 -425
- package/src/lib/server/session-tools/git.ts +87 -47
- package/src/lib/server/session-tools/http.ts +85 -33
- package/src/lib/server/session-tools/index.ts +205 -160
- package/src/lib/server/session-tools/memory.ts +152 -265
- package/src/lib/server/session-tools/monitor.ts +126 -0
- package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
- package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
- package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
- package/src/lib/server/session-tools/platform.ts +86 -0
- package/src/lib/server/session-tools/plugin-creator.ts +239 -0
- package/src/lib/server/session-tools/sample-ui.ts +97 -0
- package/src/lib/server/session-tools/sandbox.ts +175 -148
- package/src/lib/server/session-tools/schedule.ts +66 -31
- package/src/lib/server/session-tools/session-info.ts +104 -410
- package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
- package/src/lib/server/session-tools/shell.ts +171 -143
- package/src/lib/server/session-tools/subagent.ts +77 -77
- package/src/lib/server/session-tools/wallet.ts +182 -106
- package/src/lib/server/session-tools/web.ts +179 -349
- package/src/lib/server/storage.ts +24 -0
- package/src/lib/server/stream-agent-chat.ts +301 -244
- package/src/lib/server/task-quality-gate.test.ts +44 -0
- package/src/lib/server/task-quality-gate.ts +67 -0
- package/src/lib/server/task-validation.test.ts +78 -0
- package/src/lib/server/task-validation.ts +67 -2
- package/src/lib/server/tool-aliases.ts +68 -0
- package/src/lib/server/tool-capability-policy.ts +23 -5
- package/src/lib/tasks.ts +7 -1
- package/src/lib/tool-definitions.ts +23 -23
- package/src/lib/validation/schemas.ts +12 -0
- package/src/lib/view-routes.ts +2 -24
- package/src/stores/use-app-store.ts +23 -1
- package/src/types/index.ts +121 -7
|
@@ -10,6 +10,7 @@ import { loadRuntimeSettings, getAgentLoopRecursionLimit } from './runtime-setti
|
|
|
10
10
|
import { getMemoryDb } from './memory-db'
|
|
11
11
|
import { logExecution } from './execution-log'
|
|
12
12
|
import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
|
|
13
|
+
import { expandToolIds } from './tool-aliases'
|
|
13
14
|
import type { Session, Message, UsageRecord } from '@/types'
|
|
14
15
|
import { extractSuggestions } from './suggestions'
|
|
15
16
|
|
|
@@ -111,115 +112,65 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
111
112
|
}) {
|
|
112
113
|
const hasTooling = opts.enabledTools.length > 0
|
|
113
114
|
const toolLines = buildToolCapabilityLines(opts.enabledTools, { platformAssignScope: opts.platformAssignScope })
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
const has = (t: string) => opts.enabledTools.includes(t)
|
|
116
|
+
const hasDelegationTool = has('claude_code') || has('codex_cli') || has('opencode_cli')
|
|
117
|
+
|
|
118
|
+
const parts: string[] = []
|
|
119
|
+
|
|
120
|
+
// Core execution philosophy
|
|
121
|
+
parts.push(
|
|
121
122
|
'## How I Work',
|
|
122
|
-
'I take initiative. When there\'s work to do, I do it — I use my tools to research, build, and make real progress rather than just talking about it.',
|
|
123
123
|
hasTooling
|
|
124
|
-
? '
|
|
125
|
-
: '
|
|
126
|
-
'
|
|
127
|
-
'
|
|
128
|
-
'If you state an intention to do research/build/execute, immediately follow through with tool calls in the same run.',
|
|
129
|
-
'Never claim completed research/build results without tool evidence. If a tool fails or returns empty results, say that clearly and retry with another approach.',
|
|
130
|
-
'If the user names a tool explicitly (for example "call connector_message_tool"), you must actually invoke that tool instead of simulating or paraphrasing its result.',
|
|
131
|
-
'Before finalizing: verify key claims with concrete outputs from tools whenever tools are available.',
|
|
124
|
+
? 'I take initiative — plan briefly, execute tools, evaluate, iterate until done. Never stop at advice when action is implied.'
|
|
125
|
+
: 'No tools enabled. Be explicit about what tool access is needed.',
|
|
126
|
+
'Follow through on stated intentions with tool calls. Never claim results without tool evidence.',
|
|
127
|
+
'If a tool is named explicitly, invoke it. Short progress updates between steps.',
|
|
132
128
|
opts.loopMode === 'ongoing'
|
|
133
|
-
? 'Loop
|
|
134
|
-
: 'Loop
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
: '
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
? 'If `execute_command` fails due workdir/path traversal, retry without a workdir override or use a safe relative path under the current session cwd.'
|
|
179
|
-
: '',
|
|
180
|
-
hasDelegationTool
|
|
181
|
-
? `Only use CLI delegation (${delegationOrder.join(' -> ')}) for tasks that need deep code understanding across multiple files: large refactors, complex debugging, multi-file code generation, or test suites. Never delegate when the user says "run", "start", "serve", "execute", or "test it locally".`
|
|
182
|
-
: '',
|
|
183
|
-
opts.enabledTools.includes('memory')
|
|
184
|
-
? 'Memory is active and required for long-horizon work: before major tasks, run memory_tool search/list for relevant prior work; after each meaningful step, store concise reusable notes (what changed, where it lives, constraints, next step). Treat memory as shared context plus your own agent notes, not as user-owned personal profile data.'
|
|
185
|
-
: '',
|
|
186
|
-
opts.enabledTools.includes('memory')
|
|
187
|
-
? 'The platform preloads relevant memory context each turn. Use memory_tool for deeper lookup, explicit recall requests, and durable storage.'
|
|
188
|
-
: '',
|
|
189
|
-
opts.enabledTools.includes('memory')
|
|
190
|
-
? 'If the user gives an open goal (e.g. "go make money"), do not keep re-asking broad clarifying questions. Form a hypothesis, execute a concrete step, then adapt using memory + evidence.'
|
|
191
|
-
: '',
|
|
192
|
-
'## Knowing When Not to Reply',
|
|
193
|
-
'Real conversations have natural pauses. Not every message needs a response — sometimes the most human thing is comfortable silence.',
|
|
194
|
-
'Reply with exactly "NO_MESSAGE" (nothing else) to suppress outbound delivery when replying would feel unnatural.',
|
|
195
|
-
'Think about what a thoughtful friend would do:',
|
|
196
|
-
'- "okay" / "alright" / "cool" / "got it" / "sounds good" → they\'re just acknowledging, not expecting a reply back',
|
|
197
|
-
'- "thanks" / "thx" / "ty" after you\'ve helped → the conversation is wrapping up naturally',
|
|
198
|
-
'- thumbs up, emoji reactions, read receipts → these are closers, not openers',
|
|
199
|
-
'- "night" / "ttyl" / "bye" / "gotta go" → they\'re leaving, let them go',
|
|
200
|
-
'- "haha" / "lol" / "lmao" → they appreciated something, no follow-up needed',
|
|
201
|
-
'- forwarded content or status updates with no question → they\'re sharing, not asking',
|
|
202
|
-
'Always reply when:',
|
|
203
|
-
'- There is a question, even an implied one ("I wonder if...")',
|
|
204
|
-
'- They give you a task or instruction',
|
|
205
|
-
'- They share something emotional or personal — silence here feels cold',
|
|
206
|
-
'- They say "thanks" with a follow-up context ("thanks, what about X?") or in a tone that expects "you\'re welcome"',
|
|
207
|
-
'- You have something genuinely useful to add',
|
|
208
|
-
'The test: if you saw this message from a friend, would you feel compelled to type something back? If not, NO_MESSAGE.',
|
|
209
|
-
'Ask for confirmation only for high-risk or irreversible actions. For normal low-risk research/build steps, proceed autonomously.',
|
|
210
|
-
'Default behavior is execution, not interrogation: do not ask exploratory clarification questions when a safe next action exists.',
|
|
211
|
-
'Do not end every response with a question. Use declarative completion statements by default, and only ask a question when a concrete missing detail blocks the next action.',
|
|
212
|
-
'Do not pause for a "continue" confirmation after the user has already asked you to execute a goal. Keep moving until blocked by permissions, missing credentials, or hard tool failures.',
|
|
213
|
-
'Never repeat one-time side effects that are already complete (for example creating the same schedule/task again). Verify state first, then either continue execution or reply HEARTBEAT_OK.',
|
|
214
|
-
'For main-loop tick messages that begin with "SWARM_MAIN_MISSION_TICK" or "SWARM_MAIN_AUTO_FOLLOWUP", follow that response contract exactly and include one valid [MAIN_LOOP_META] JSON line when you are not returning HEARTBEAT_OK.',
|
|
215
|
-
`Heartbeat protocol: if the user message is exactly "${opts.heartbeatPrompt}", reply exactly "HEARTBEAT_OK" when there is nothing important to report; otherwise reply with a concise progress update and immediate next step.`,
|
|
216
|
-
opts.heartbeatIntervalSec > 0
|
|
217
|
-
? `Expected heartbeat cadence is roughly every ${opts.heartbeatIntervalSec} seconds while ongoing work is active.`
|
|
218
|
-
: '',
|
|
219
|
-
toolLines.length ? 'What I can do:\n' + toolLines.join('\n') : '',
|
|
220
|
-
// Inject goal decomposition instructions for broad goals without existing plans
|
|
221
|
-
(opts.userMessage && !opts.hasExistingPlan && isBroadGoal(opts.userMessage)) ? GOAL_DECOMPOSITION_BLOCK : '',
|
|
222
|
-
].filter(Boolean).join('\n')
|
|
129
|
+
? 'Loop: ONGOING — keep iterating until done, blocked, or limits reached.'
|
|
130
|
+
: 'Loop: BOUNDED — execute multiple steps but finish within recursion budget.',
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
// Tool-specific guidance (consolidated)
|
|
134
|
+
if (has('shell')) {
|
|
135
|
+
parts.push(
|
|
136
|
+
'Shell: use `execute_command` for servers, installs, scripts, git. Use `background=true` for long-lived processes.',
|
|
137
|
+
'Verify servers with `process_tool` status/log and liveness probes before claiming success.',
|
|
138
|
+
'Resolve IPs/URLs via shell — never use placeholders. Retry path errors without workdir override.',
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
if (hasDelegationTool) {
|
|
142
|
+
parts.push(
|
|
143
|
+
'CRITICAL: `execute_command` (not delegation) for running servers, installs, scripts. Delegation sessions end and kill processes.',
|
|
144
|
+
'Delegate only for deep multi-file code work: refactors, debugging, generation, test suites.',
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
if (has('memory')) {
|
|
148
|
+
parts.push(
|
|
149
|
+
'Memory: search before major tasks, store concise notes after meaningful steps. Platform preloads context each turn.',
|
|
150
|
+
'For open goals, form a hypothesis and execute — do not keep re-asking broad questions.',
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
if (has('manage_tasks')) parts.push('Create/update tasks for long-lived goals to track progress.')
|
|
154
|
+
if (has('manage_schedules')) parts.push('Use schedules for follow-ups. Check existing schedules before creating new ones.')
|
|
155
|
+
if (has('manage_connectors')) parts.push('Connectors: proactive outreach for significant events only. Keep messages concise, no duplicates.')
|
|
156
|
+
if (has('manage_sessions')) parts.push('Inspect existing chats before creating duplicates.')
|
|
157
|
+
|
|
158
|
+
// Response behavior
|
|
159
|
+
parts.push(
|
|
160
|
+
'## Response Rules',
|
|
161
|
+
'NO_MESSAGE: reply with exactly this to suppress delivery for pure acknowledgments (ok/thanks/bye/emoji/lol).',
|
|
162
|
+
'Always reply to: questions, tasks, emotional sharing, or when you have something useful to add.',
|
|
163
|
+
'Execute by default — only ask for confirmation on high-risk/irreversible actions. Do not end every response with a question.',
|
|
164
|
+
'Never repeat completed side effects. Verify state first.',
|
|
165
|
+
`Heartbeat: if message is "${opts.heartbeatPrompt}", reply "HEARTBEAT_OK" unless you have a progress update.`,
|
|
166
|
+
opts.heartbeatIntervalSec > 0 ? `Heartbeat cadence: ~${opts.heartbeatIntervalSec}s.` : '',
|
|
167
|
+
'For SWARM_MAIN_MISSION_TICK / SWARM_MAIN_AUTO_FOLLOWUP messages, follow the response contract and include [MAIN_LOOP_META] JSON.',
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if (toolLines.length) parts.push('What I can do:\n' + toolLines.join('\n'))
|
|
171
|
+
if (opts.userMessage && !opts.hasExistingPlan && isBroadGoal(opts.userMessage)) parts.push(GOAL_DECOMPOSITION_BLOCK)
|
|
172
|
+
|
|
173
|
+
return parts.filter(Boolean).join('\n')
|
|
223
174
|
}
|
|
224
175
|
|
|
225
176
|
export interface StreamAgentChatResult {
|
|
@@ -233,10 +184,12 @@ export interface StreamAgentChatResult {
|
|
|
233
184
|
export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<StreamAgentChatResult> {
|
|
234
185
|
const startTs = Date.now()
|
|
235
186
|
const { session, message, imagePath, attachedFiles, apiKey, systemPrompt, write, history, fallbackCredentialIds, signal } = opts
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
187
|
+
const rawTools = Array.isArray(session.tools) ? session.tools : []
|
|
188
|
+
const hasShellCapability = rawTools.some((toolId) => ['shell', 'execute_command'].includes(String(toolId)))
|
|
189
|
+
const sessionToolsWithImplicitProcess = expandToolIds([
|
|
190
|
+
...rawTools,
|
|
191
|
+
...(hasShellCapability ? ['process'] : []),
|
|
192
|
+
])
|
|
240
193
|
|
|
241
194
|
// fallbackCredentialIds is intentionally accepted for compatibility with caller signatures.
|
|
242
195
|
void fallbackCredentialIds
|
|
@@ -413,17 +366,14 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
413
366
|
'- When I learn something that corrects old knowledge, update or remove the old memory',
|
|
414
367
|
].join('\n'))
|
|
415
368
|
|
|
416
|
-
// Pre-compaction memory flush
|
|
369
|
+
// Pre-compaction memory flush: nudge agent to save important context before it's lost
|
|
417
370
|
const msgCount = history.filter(m => m.role === 'user' || m.role === 'assistant').length
|
|
418
371
|
if (msgCount > 20) {
|
|
419
|
-
const canEditSelf = (session.tools || []).includes('manage_agents')
|
|
420
372
|
stateModifierParts.push([
|
|
421
373
|
'## Reflection & Consolidation Reminder',
|
|
422
374
|
'This conversation is getting long and I might lose older context soon.',
|
|
423
|
-
'
|
|
424
|
-
|
|
425
|
-
'If there\'s nothing worth saving or updating, carry on.',
|
|
426
|
-
].filter(Boolean).join('\n'))
|
|
375
|
+
'Save anything important I\'ve learned, decided, or discovered to memory now. Only what matters, not every detail.',
|
|
376
|
+
].join('\n'))
|
|
427
377
|
}
|
|
428
378
|
} catch {
|
|
429
379
|
// If memory context fails to load, continue without blocking the run.
|
|
@@ -471,26 +421,44 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
471
421
|
}
|
|
472
422
|
}
|
|
473
423
|
|
|
474
|
-
// Tell the LLM about
|
|
424
|
+
// Tell the LLM about available plugins and their access status
|
|
475
425
|
{
|
|
476
|
-
const
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
'web_search', 'web_fetch', 'browser', 'memory',
|
|
480
|
-
'claude_code', 'codex_cli', 'opencode_cli',
|
|
481
|
-
'sandbox', 'create_document', 'create_spreadsheet', 'http_request', 'git', 'wallet',
|
|
482
|
-
'manage_agents', 'manage_tasks', 'manage_schedules', 'schedule_wake', 'manage_skills',
|
|
483
|
-
'manage_documents', 'manage_webhooks', 'manage_connectors', 'manage_sessions', 'manage_secrets',
|
|
484
|
-
]
|
|
485
|
-
const disabled = allToolIds.filter((t) => !enabledSet.has(t))
|
|
426
|
+
const agentEnabledSet = new Set(sessionToolsWithImplicitProcess)
|
|
427
|
+
const { getPluginManager } = await import('./plugins')
|
|
428
|
+
const allPlugins = getPluginManager().listPlugins()
|
|
486
429
|
const mcpDisabled = agentMcpDisabledTools ?? []
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
430
|
+
|
|
431
|
+
// Categorize plugins
|
|
432
|
+
const globallyDisabled: string[] = [] // Disabled site-wide by admin
|
|
433
|
+
const enabledButNoAccess: string[] = [] // Enabled globally but agent doesn't have access
|
|
434
|
+
const agentHasAccess: string[] = [] // Agent can use these
|
|
435
|
+
|
|
436
|
+
for (const p of allPlugins) {
|
|
437
|
+
if (!p.enabled) {
|
|
438
|
+
globallyDisabled.push(`${p.name} (${p.filename})`)
|
|
439
|
+
} else if (!agentEnabledSet.has(p.filename)) {
|
|
440
|
+
enabledButNoAccess.push(`${p.name} (${p.filename})`)
|
|
441
|
+
} else {
|
|
442
|
+
agentHasAccess.push(p.filename)
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const parts: string[] = []
|
|
447
|
+
if (enabledButNoAccess.length > 0) {
|
|
448
|
+
parts.push(
|
|
449
|
+
`**Available but not assigned to me:** ${enabledButNoAccess.join(', ')}\n` +
|
|
450
|
+
'I can request access using `manage_capabilities` with action "request_access" or `request_tool_access`.',
|
|
492
451
|
)
|
|
493
452
|
}
|
|
453
|
+
if (globallyDisabled.length > 0) {
|
|
454
|
+
parts.push(`**Disabled site-wide:** ${globallyDisabled.join(', ')} — ask the user to enable these in Settings > Plugins first.`)
|
|
455
|
+
}
|
|
456
|
+
if (mcpDisabled.length > 0) {
|
|
457
|
+
parts.push(`**MCP tools not available:** ${mcpDisabled.join(', ')}`)
|
|
458
|
+
}
|
|
459
|
+
if (parts.length > 0) {
|
|
460
|
+
stateModifierParts.push(`## Plugin Access\n${parts.join('\n')}`)
|
|
461
|
+
}
|
|
494
462
|
}
|
|
495
463
|
|
|
496
464
|
if (settings.suggestionsEnabled === true) {
|
|
@@ -672,7 +640,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
672
640
|
const postClearHistory = effectiveHistory.slice(contextStart)
|
|
673
641
|
|
|
674
642
|
const langchainMessages: Array<HumanMessage | AIMessage> = []
|
|
675
|
-
for (const m of postClearHistory.slice(-
|
|
643
|
+
for (const m of postClearHistory.slice(-30)) {
|
|
676
644
|
if (m.role === 'user') {
|
|
677
645
|
langchainMessages.push(new HumanMessage({ content: await buildLangChainContent(m.text, m.imagePath, m.attachedFiles) }))
|
|
678
646
|
} else {
|
|
@@ -687,6 +655,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
687
655
|
let fullText = ''
|
|
688
656
|
let lastSegment = ''
|
|
689
657
|
let hasToolCalls = false
|
|
658
|
+
let needsTextSeparator = false
|
|
690
659
|
let totalInputTokens = 0
|
|
691
660
|
let totalOutputTokens = 0
|
|
692
661
|
let lastToolInput: unknown = null
|
|
@@ -710,135 +679,223 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
710
679
|
}, runtime.ongoingLoopMaxRuntimeMs)
|
|
711
680
|
: null
|
|
712
681
|
|
|
682
|
+
const MAX_AUTO_CONTINUES = 3
|
|
683
|
+
const MAX_TRANSIENT_RETRIES = 2
|
|
684
|
+
let autoContinueCount = 0
|
|
685
|
+
let transientRetryCount = 0
|
|
686
|
+
|
|
713
687
|
try {
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
688
|
+
const maxIterations = MAX_AUTO_CONTINUES + MAX_TRANSIENT_RETRIES
|
|
689
|
+
for (let iteration = 0; iteration <= maxIterations; iteration++) {
|
|
690
|
+
let shouldContinue: 'recursion' | 'transient' | false = false
|
|
691
|
+
|
|
692
|
+
// Fresh per-iteration controller so an internal LangGraph abort doesn't poison subsequent iterations.
|
|
693
|
+
// Linked to the parent so client disconnect / timeout still propagates.
|
|
694
|
+
const iterationController = new AbortController()
|
|
695
|
+
const onParentAbort = () => iterationController.abort()
|
|
696
|
+
if (abortController.signal.aborted) iterationController.abort()
|
|
697
|
+
else abortController.signal.addEventListener('abort', onParentAbort)
|
|
718
698
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
699
|
+
try {
|
|
700
|
+
const eventStream = agent.streamEvents(
|
|
701
|
+
{ messages: langchainMessages },
|
|
702
|
+
{ version: 'v2', recursionLimit, signal: iterationController.signal },
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
for await (const event of eventStream) {
|
|
706
|
+
const kind = event.event
|
|
707
|
+
|
|
708
|
+
if (kind === 'on_chat_model_stream') {
|
|
709
|
+
const chunk = event.data?.chunk
|
|
710
|
+
if (chunk?.content) {
|
|
711
|
+
// content can be string or array of content blocks
|
|
712
|
+
if (Array.isArray(chunk.content)) {
|
|
713
|
+
for (const block of chunk.content) {
|
|
714
|
+
// Anthropic extended thinking blocks
|
|
715
|
+
if (block.type === 'thinking' && block.thinking) {
|
|
716
|
+
accumulatedThinking += block.thinking
|
|
717
|
+
write(`data: ${JSON.stringify({ t: 'thinking', text: block.thinking })}\n\n`)
|
|
718
|
+
// OpenClaw [[thinking]] prefix convention
|
|
719
|
+
} else if (typeof block.text === 'string' && block.text.startsWith('[[thinking]]')) {
|
|
720
|
+
accumulatedThinking += block.text.slice(12)
|
|
721
|
+
write(`data: ${JSON.stringify({ t: 'thinking', text: block.text.slice(12) })}\n\n`)
|
|
722
|
+
} else if (block.text) {
|
|
723
|
+
if (needsTextSeparator && fullText.length > 0) {
|
|
724
|
+
fullText += '\n\n'
|
|
725
|
+
write(`data: ${JSON.stringify({ t: 'd', text: '\n\n' })}\n\n`)
|
|
726
|
+
needsTextSeparator = false
|
|
727
|
+
}
|
|
728
|
+
fullText += block.text
|
|
729
|
+
lastSegment += block.text
|
|
730
|
+
write(`data: ${JSON.stringify({ t: 'd', text: block.text })}\n\n`)
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
} else {
|
|
734
|
+
const text = typeof chunk.content === 'string' ? chunk.content : ''
|
|
735
|
+
if (text) {
|
|
736
|
+
if (needsTextSeparator && fullText.length > 0) {
|
|
737
|
+
fullText += '\n\n'
|
|
738
|
+
write(`data: ${JSON.stringify({ t: 'd', text: '\n\n' })}\n\n`)
|
|
739
|
+
needsTextSeparator = false
|
|
740
|
+
}
|
|
741
|
+
fullText += text
|
|
742
|
+
lastSegment += text
|
|
743
|
+
write(`data: ${JSON.stringify({ t: 'd', text })}\n\n`)
|
|
744
|
+
}
|
|
740
745
|
}
|
|
741
746
|
}
|
|
742
|
-
} else {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
747
|
+
} else if (kind === 'on_llm_end') {
|
|
748
|
+
// Track token usage from LLM responses — check all known LangChain event shapes
|
|
749
|
+
const output = event.data?.output
|
|
750
|
+
const usage = output?.llmOutput?.tokenUsage
|
|
751
|
+
|| output?.llmOutput?.usage
|
|
752
|
+
|| output?.usage_metadata
|
|
753
|
+
|| output?.response_metadata?.usage
|
|
754
|
+
|| output?.response_metadata?.tokenUsage
|
|
755
|
+
if (usage) {
|
|
756
|
+
totalInputTokens += usage.promptTokens || usage.input_tokens || usage.prompt_tokens || 0
|
|
757
|
+
totalOutputTokens += usage.completionTokens || usage.output_tokens || usage.completion_tokens || 0
|
|
748
758
|
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
759
|
+
} else if (kind === 'on_tool_start') {
|
|
760
|
+
hasToolCalls = true
|
|
761
|
+
needsTextSeparator = true
|
|
762
|
+
lastSegment = ''
|
|
763
|
+
const toolName = event.name || 'unknown'
|
|
764
|
+
const input = event.data?.input
|
|
765
|
+
lastToolInput = input
|
|
766
|
+
// Plugin hooks: beforeToolExec
|
|
767
|
+
await pluginMgr.runHook('beforeToolExec', { toolName, input })
|
|
768
|
+
const inputStr = typeof input === 'string' ? input : JSON.stringify(input)
|
|
769
|
+
logExecution(session.id, 'tool_call', `${toolName} invoked`, {
|
|
770
|
+
agentId: session.agentId,
|
|
771
|
+
detail: { toolName, input: inputStr?.slice(0, 4000) },
|
|
772
|
+
})
|
|
773
|
+
write(`data: ${JSON.stringify({
|
|
774
|
+
t: 'tool_call',
|
|
775
|
+
toolName,
|
|
776
|
+
toolInput: inputStr,
|
|
777
|
+
})}\n\n`)
|
|
778
|
+
} else if (kind === 'on_tool_end') {
|
|
779
|
+
const toolName = event.name || 'unknown'
|
|
780
|
+
const output = event.data?.output
|
|
781
|
+
const outputStr = typeof output === 'string'
|
|
782
|
+
? output
|
|
783
|
+
: output?.content
|
|
784
|
+
? String(output.content)
|
|
785
|
+
: JSON.stringify(output)
|
|
786
|
+
// Plugin hooks: afterToolExec
|
|
787
|
+
await pluginMgr.runHook('afterToolExec', { toolName, input: null, output: outputStr })
|
|
788
|
+
// Event-driven memory breadcrumbs
|
|
789
|
+
if (session.agentId && (session.tools || []).includes('memory')) {
|
|
790
|
+
try {
|
|
791
|
+
const breadcrumbTitle = extractBreadcrumbTitle(toolName, lastToolInput, outputStr)
|
|
792
|
+
if (breadcrumbTitle) {
|
|
793
|
+
const memDb = getMemoryDb()
|
|
794
|
+
memDb.add({
|
|
795
|
+
agentId: session.agentId,
|
|
796
|
+
sessionId: session.id,
|
|
797
|
+
category: 'breadcrumb',
|
|
798
|
+
title: breadcrumbTitle,
|
|
799
|
+
content: '',
|
|
800
|
+
})
|
|
801
|
+
}
|
|
802
|
+
} catch { /* breadcrumbs are best-effort */ }
|
|
803
|
+
}
|
|
804
|
+
lastToolInput = null
|
|
805
|
+
logExecution(session.id, 'tool_result', `${toolName} returned`, {
|
|
806
|
+
agentId: session.agentId,
|
|
807
|
+
detail: { toolName, output: outputStr?.slice(0, 4000), error: /^(Error:|error:)/i.test((outputStr || '').trim()) || undefined },
|
|
808
|
+
})
|
|
809
|
+
// Enriched file_op logging for file-mutating tools
|
|
810
|
+
if (['write_file', 'edit_file', 'copy_file', 'move_file', 'delete_file'].includes(toolName)) {
|
|
811
|
+
const inputData = event.data?.input
|
|
812
|
+
const inputObj = typeof inputData === 'object' ? inputData : {}
|
|
813
|
+
logExecution(session.id, 'file_op', `${toolName}: ${inputObj?.filePath || inputObj?.sourcePath || 'unknown'}`, {
|
|
798
814
|
agentId: session.agentId,
|
|
799
|
-
|
|
800
|
-
category: 'breadcrumb',
|
|
801
|
-
title: breadcrumbTitle,
|
|
802
|
-
content: '',
|
|
815
|
+
detail: { toolName, filePath: inputObj?.filePath, sourcePath: inputObj?.sourcePath, destinationPath: inputObj?.destinationPath, success: !/^Error/i.test((outputStr || '').trim()) },
|
|
803
816
|
})
|
|
804
817
|
}
|
|
805
|
-
|
|
818
|
+
// Enriched commit logging for git operations
|
|
819
|
+
if (toolName === 'execute_command' && outputStr) {
|
|
820
|
+
const commitMatch = outputStr.match(/\[[\w/-]+\s+([a-f0-9]{7,40})\]/)
|
|
821
|
+
if (commitMatch) {
|
|
822
|
+
logExecution(session.id, 'commit', `git commit ${commitMatch[1]}`, {
|
|
823
|
+
agentId: session.agentId,
|
|
824
|
+
detail: { commitId: commitMatch[1], outputPreview: outputStr.slice(0, 500) },
|
|
825
|
+
})
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
write(`data: ${JSON.stringify({
|
|
829
|
+
t: 'tool_result',
|
|
830
|
+
toolName,
|
|
831
|
+
toolOutput: outputStr?.slice(0, 2000),
|
|
832
|
+
})}\n\n`)
|
|
833
|
+
}
|
|
806
834
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
835
|
+
} catch (innerErr: unknown) {
|
|
836
|
+
const errName = innerErr instanceof Error ? innerErr.constructor.name : ''
|
|
837
|
+
const errMsg = innerErr instanceof Error ? innerErr.message : String(innerErr)
|
|
838
|
+
const errStack = innerErr instanceof Error ? innerErr.stack?.slice(0, 500) : undefined
|
|
839
|
+
|
|
840
|
+
// Classify the error:
|
|
841
|
+
// 1. GraphRecursionError — explicit or wrapped as abort (LangGraph aborts internally on limit)
|
|
842
|
+
// 2. Transient abort/timeout — LLM API failure, not from client disconnect
|
|
843
|
+
const isRecursionError = errName === 'GraphRecursionError'
|
|
844
|
+
|| /recursion limit|maximum recursion/i.test(errMsg)
|
|
845
|
+
const isTransientAbort = !isRecursionError
|
|
846
|
+
&& /abort|timed?\s*out|ECONNRESET|ECONNREFUSED|socket hang up|network/i.test(errMsg)
|
|
847
|
+
&& !abortController.signal.aborted
|
|
848
|
+
|
|
849
|
+
// Log diagnostic details for every error so we can trace root causes
|
|
850
|
+
console.error(`[stream-agent-chat] Error in streamEvents iteration=${iteration}`, {
|
|
851
|
+
errName, errMsg, errStack,
|
|
852
|
+
isRecursionError, isTransientAbort,
|
|
853
|
+
hasToolCalls, fullTextLen: fullText.length,
|
|
854
|
+
parentAborted: abortController.signal.aborted,
|
|
811
855
|
})
|
|
812
|
-
|
|
813
|
-
if (
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
logExecution(session.id, '
|
|
856
|
+
|
|
857
|
+
if (isRecursionError && autoContinueCount < MAX_AUTO_CONTINUES && !abortController.signal.aborted) {
|
|
858
|
+
shouldContinue = 'recursion'
|
|
859
|
+
autoContinueCount++
|
|
860
|
+
logExecution(session.id, 'decision', `Recursion limit hit, auto-continuing (${autoContinueCount}/${MAX_AUTO_CONTINUES})`, {
|
|
817
861
|
agentId: session.agentId,
|
|
818
|
-
detail: {
|
|
862
|
+
detail: { errName, errMsg },
|
|
819
863
|
})
|
|
864
|
+
write(`data: ${JSON.stringify({ t: 'status', text: JSON.stringify({ autoContinue: autoContinueCount, maxContinues: MAX_AUTO_CONTINUES }) })}\n\n`)
|
|
865
|
+
} else if (isTransientAbort && transientRetryCount < MAX_TRANSIENT_RETRIES && !abortController.signal.aborted) {
|
|
866
|
+
shouldContinue = 'transient'
|
|
867
|
+
transientRetryCount++
|
|
868
|
+
logExecution(session.id, 'decision', `Transient error, retrying (${transientRetryCount}/${MAX_TRANSIENT_RETRIES}): ${errMsg}`, {
|
|
869
|
+
agentId: session.agentId,
|
|
870
|
+
detail: { errName, errMsg },
|
|
871
|
+
})
|
|
872
|
+
write(`data: ${JSON.stringify({ t: 'status', text: JSON.stringify({ transientRetry: transientRetryCount, maxRetries: MAX_TRANSIENT_RETRIES, error: errMsg }) })}\n\n`)
|
|
873
|
+
} else {
|
|
874
|
+
// Non-retryable error or exhausted retries — rethrow to outer catch
|
|
875
|
+
throw innerErr
|
|
820
876
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
877
|
+
} finally {
|
|
878
|
+
abortController.signal.removeEventListener('abort', onParentAbort)
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (!shouldContinue) break
|
|
882
|
+
|
|
883
|
+
if (shouldContinue === 'recursion') {
|
|
884
|
+
// Append accumulated text and a continue prompt
|
|
885
|
+
if (fullText.trim()) {
|
|
886
|
+
langchainMessages.push(new AIMessage({ content: fullText }))
|
|
830
887
|
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
888
|
+
langchainMessages.push(new HumanMessage({ content: 'Continue where you left off. Complete the remaining steps of the objective.' }))
|
|
889
|
+
lastSegment = ''
|
|
890
|
+
} else if (shouldContinue === 'transient') {
|
|
891
|
+
// Short delay before retrying transient errors (API timeout, rate limit, etc.)
|
|
892
|
+
await new Promise((r) => setTimeout(r, 2000 * transientRetryCount))
|
|
836
893
|
}
|
|
837
894
|
}
|
|
838
|
-
} catch (err:
|
|
895
|
+
} catch (err: unknown) {
|
|
839
896
|
const errMsg = timedOut
|
|
840
897
|
? 'Ongoing loop stopped after reaching the configured runtime limit.'
|
|
841
|
-
: err.message
|
|
898
|
+
: err instanceof Error ? err.message : String(err)
|
|
842
899
|
logExecution(session.id, 'error', errMsg, { agentId: session.agentId, detail: { timedOut } })
|
|
843
900
|
write(`data: ${JSON.stringify({ t: 'err', text: errMsg })}\n\n`)
|
|
844
901
|
} finally {
|