@swarmclawai/swarmclaw 0.6.7 → 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 +82 -39
- 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 +19 -5
- package/src/app/api/approvals/route.ts +22 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/eval/run/route.ts +37 -0
- package/src/app/api/eval/scenarios/route.ts +24 -0
- package/src/app/api/eval/suite/route.ts +29 -0
- 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/graph/route.ts +46 -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/sessions/[id]/checkpoints/route.ts +31 -0
- package/src/app/api/sessions/[id]/restore/route.ts +36 -0
- package/src/app/api/settings/route.ts +62 -0
- package/src/app/api/setup/doctor/route.ts +22 -5
- package/src/app/api/souls/[id]/route.ts +65 -0
- package/src/app/api/souls/route.ts +70 -0
- package/src/app/api/tasks/[id]/approve/route.ts +4 -3
- package/src/app/api/tasks/[id]/route.ts +16 -3
- package/src/app/api/tasks/route.ts +10 -2
- package/src/app/api/usage/route.ts +9 -2
- package/src/app/globals.css +27 -0
- package/src/app/page.tsx +10 -5
- package/src/cli/index.js +37 -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 +112 -34
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/agents/soul-library-picker.tsx +84 -13
- package/src/components/auth/access-key-gate.tsx +63 -54
- package/src/components/auth/user-picker.tsx +37 -32
- package/src/components/chat/activity-moment.tsx +2 -0
- 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/checkpoint-timeline.tsx +112 -0
- 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 +46 -4
- package/src/components/chat/session-approval-card.tsx +80 -0
- package/src/components/chat/session-debug-panel.tsx +106 -84
- package/src/components/chat/streaming-bubble.tsx +6 -5
- package/src/components/chat/task-approval-card.tsx +78 -0
- package/src/components/chat/thinking-indicator.tsx +48 -12
- package/src/components/chat/tool-call-bubble.tsx +3 -0
- 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 +37 -7
- package/src/components/home/home-view.tsx +54 -24
- 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 +87 -19
- 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-browser.tsx +73 -45
- package/src/components/memory/memory-graph-view.tsx +203 -0
- package/src/components/memory/memory-list.tsx +20 -13
- package/src/components/plugins/plugin-list.tsx +214 -60
- 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 +28 -9
- 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/hint-tip.tsx +31 -0
- 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 +149 -4
- 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 +224 -0
- 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 +72 -48
- 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 +319 -74
- package/src/lib/server/chatroom-helpers.ts +63 -5
- package/src/lib/server/chatroom-orchestration.ts +74 -0
- package/src/lib/server/clawhub-client.ts +82 -6
- package/src/lib/server/connectors/manager.ts +27 -1
- package/src/lib/server/context-manager.ts +132 -50
- package/src/lib/server/cost.test.ts +73 -0
- package/src/lib/server/cost.ts +165 -34
- package/src/lib/server/daemon-state.ts +112 -1
- package/src/lib/server/data-dir.ts +18 -1
- package/src/lib/server/eval/runner.ts +126 -0
- package/src/lib/server/eval/scenarios.ts +218 -0
- package/src/lib/server/eval/scorer.ts +96 -0
- package/src/lib/server/eval/store.ts +37 -0
- package/src/lib/server/eval/types.ts +48 -0
- package/src/lib/server/execution-log.ts +12 -8
- package/src/lib/server/guardian.ts +34 -0
- package/src/lib/server/heartbeat-service.ts +53 -1
- package/src/lib/server/integrity-monitor.ts +208 -0
- package/src/lib/server/langgraph-checkpoint.ts +10 -0
- package/src/lib/server/link-understanding.ts +55 -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 +115 -16
- 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 +193 -19
- package/src/lib/server/memory-retrieval.test.ts +56 -0
- package/src/lib/server/mmr.ts +73 -0
- package/src/lib/server/orchestrator-lg.ts +7 -1
- package/src/lib/server/orchestrator.ts +4 -3
- package/src/lib/server/plugins.ts +662 -132
- package/src/lib/server/process-manager.ts +18 -0
- package/src/lib/server/query-expansion.ts +57 -0
- package/src/lib/server/queue.ts +280 -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 +32 -2
- 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 +95 -33
- package/src/lib/server/session-tools/index.ts +217 -138
- package/src/lib/server/session-tools/memory.ts +154 -239
- 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 +78 -0
- 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 +181 -327
- package/src/lib/server/storage.ts +36 -0
- package/src/lib/server/stream-agent-chat.ts +348 -242
- 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 +24 -5
- package/src/lib/server/tool-retry.ts +62 -0
- package/src/lib/server/transcript-repair.ts +72 -0
- package/src/lib/setup-defaults.ts +1 -0
- package/src/lib/tasks.ts +7 -1
- package/src/lib/tool-definitions.ts +24 -23
- package/src/lib/validation/schemas.ts +13 -0
- package/src/lib/view-routes.ts +2 -23
- package/src/stores/use-app-store.ts +23 -1
- package/src/types/index.ts +155 -10
|
@@ -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
|
|
|
@@ -59,6 +60,7 @@ function buildToolCapabilityLines(enabledTools: string[], opts?: { platformAssig
|
|
|
59
60
|
if (enabledTools.includes('manage_agents')) lines.push('- I can create and configure other agents (`manage_agents`) — spin up specialists when a task calls for it.')
|
|
60
61
|
if (enabledTools.includes('manage_tasks')) lines.push('- I can manage tasks (`manage_tasks`) — create plans, track progress, and stay organized over time.')
|
|
61
62
|
if (enabledTools.includes('manage_schedules')) lines.push('- I can set up schedules (`manage_schedules`) for recurring work or future follow-ups.')
|
|
63
|
+
if (enabledTools.includes('schedule_wake')) lines.push('- I can set a conversational timer (`schedule_wake`) to remind myself to check back on something later in this chat.')
|
|
62
64
|
if (enabledTools.includes('manage_documents')) lines.push('- I can store and search documents (`manage_documents`) for long-term knowledge and reference.')
|
|
63
65
|
if (enabledTools.includes('manage_webhooks')) lines.push('- I can register webhooks (`manage_webhooks`) so external events can trigger my work automatically.')
|
|
64
66
|
if (enabledTools.includes('manage_skills')) lines.push('- I can manage reusable skills (`manage_skills`) — building blocks I can learn and apply.')
|
|
@@ -77,122 +79,98 @@ function buildToolCapabilityLines(enabledTools: string[], opts?: { platformAssig
|
|
|
77
79
|
return lines
|
|
78
80
|
}
|
|
79
81
|
|
|
82
|
+
/** Detect whether a user message is a broad, high-level goal that benefits from decomposition. */
|
|
83
|
+
function isBroadGoal(text: string): boolean {
|
|
84
|
+
if (text.length < 50) return false
|
|
85
|
+
// Messages with code fences, file paths, or numbered steps are already structured
|
|
86
|
+
if (/```/.test(text)) return false
|
|
87
|
+
if (/\/(src|lib|app|pages|components|api)\//.test(text)) return false
|
|
88
|
+
if (/^\s*\d+[.)]\s/m.test(text)) return false
|
|
89
|
+
// Short direct questions aren't broad goals
|
|
90
|
+
if (text.length < 80 && text.endsWith('?')) return false
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const GOAL_DECOMPOSITION_BLOCK = [
|
|
95
|
+
'## Goal Decomposition',
|
|
96
|
+
'When you receive a broad, open-ended goal:',
|
|
97
|
+
'1. Break it into 3-7 concrete, sequentially-executable subtasks before taking action.',
|
|
98
|
+
'2. If manage_tasks is available, create a task for each subtask to track progress.',
|
|
99
|
+
'3. Output your plan in a [MAIN_LOOP_PLAN] JSON line: {"steps":["step1","step2",...],"current_step":"step1"}',
|
|
100
|
+
'4. Execute the first subtask immediately — do not stop after planning.',
|
|
101
|
+
'5. After each subtask, update progress and move to the next.',
|
|
102
|
+
].join('\n')
|
|
103
|
+
|
|
80
104
|
function buildAgenticExecutionPolicy(opts: {
|
|
81
105
|
enabledTools: string[]
|
|
82
106
|
loopMode: 'bounded' | 'ongoing'
|
|
83
107
|
heartbeatPrompt: string
|
|
84
108
|
heartbeatIntervalSec: number
|
|
85
109
|
platformAssignScope?: 'self' | 'all'
|
|
110
|
+
userMessage?: string
|
|
111
|
+
hasExistingPlan?: boolean
|
|
86
112
|
}) {
|
|
87
113
|
const hasTooling = opts.enabledTools.length > 0
|
|
88
114
|
const toolLines = buildToolCapabilityLines(opts.enabledTools, { platformAssignScope: opts.platformAssignScope })
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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(
|
|
96
122
|
'## How I Work',
|
|
97
|
-
'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.',
|
|
98
123
|
hasTooling
|
|
99
|
-
? '
|
|
100
|
-
: '
|
|
101
|
-
'
|
|
102
|
-
'
|
|
103
|
-
'If you state an intention to do research/build/execute, immediately follow through with tool calls in the same run.',
|
|
104
|
-
'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.',
|
|
105
|
-
'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.',
|
|
106
|
-
'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.',
|
|
107
128
|
opts.loopMode === 'ongoing'
|
|
108
|
-
? 'Loop
|
|
109
|
-
: 'Loop
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
: '
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
? 'If `execute_command` fails due workdir/path traversal, retry without a workdir override or use a safe relative path under the current session cwd.'
|
|
154
|
-
: '',
|
|
155
|
-
hasDelegationTool
|
|
156
|
-
? `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".`
|
|
157
|
-
: '',
|
|
158
|
-
opts.enabledTools.includes('memory')
|
|
159
|
-
? '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.'
|
|
160
|
-
: '',
|
|
161
|
-
opts.enabledTools.includes('memory')
|
|
162
|
-
? 'The platform preloads relevant memory context each turn. Use memory_tool for deeper lookup, explicit recall requests, and durable storage.'
|
|
163
|
-
: '',
|
|
164
|
-
opts.enabledTools.includes('memory')
|
|
165
|
-
? '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.'
|
|
166
|
-
: '',
|
|
167
|
-
'## Knowing When Not to Reply',
|
|
168
|
-
'Real conversations have natural pauses. Not every message needs a response — sometimes the most human thing is comfortable silence.',
|
|
169
|
-
'Reply with exactly "NO_MESSAGE" (nothing else) to suppress outbound delivery when replying would feel unnatural.',
|
|
170
|
-
'Think about what a thoughtful friend would do:',
|
|
171
|
-
'- "okay" / "alright" / "cool" / "got it" / "sounds good" → they\'re just acknowledging, not expecting a reply back',
|
|
172
|
-
'- "thanks" / "thx" / "ty" after you\'ve helped → the conversation is wrapping up naturally',
|
|
173
|
-
'- thumbs up, emoji reactions, read receipts → these are closers, not openers',
|
|
174
|
-
'- "night" / "ttyl" / "bye" / "gotta go" → they\'re leaving, let them go',
|
|
175
|
-
'- "haha" / "lol" / "lmao" → they appreciated something, no follow-up needed',
|
|
176
|
-
'- forwarded content or status updates with no question → they\'re sharing, not asking',
|
|
177
|
-
'Always reply when:',
|
|
178
|
-
'- There is a question, even an implied one ("I wonder if...")',
|
|
179
|
-
'- They give you a task or instruction',
|
|
180
|
-
'- They share something emotional or personal — silence here feels cold',
|
|
181
|
-
'- They say "thanks" with a follow-up context ("thanks, what about X?") or in a tone that expects "you\'re welcome"',
|
|
182
|
-
'- You have something genuinely useful to add',
|
|
183
|
-
'The test: if you saw this message from a friend, would you feel compelled to type something back? If not, NO_MESSAGE.',
|
|
184
|
-
'Ask for confirmation only for high-risk or irreversible actions. For normal low-risk research/build steps, proceed autonomously.',
|
|
185
|
-
'Default behavior is execution, not interrogation: do not ask exploratory clarification questions when a safe next action exists.',
|
|
186
|
-
'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.',
|
|
187
|
-
'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.',
|
|
188
|
-
'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.',
|
|
189
|
-
'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.',
|
|
190
|
-
`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.`,
|
|
191
|
-
opts.heartbeatIntervalSec > 0
|
|
192
|
-
? `Expected heartbeat cadence is roughly every ${opts.heartbeatIntervalSec} seconds while ongoing work is active.`
|
|
193
|
-
: '',
|
|
194
|
-
toolLines.length ? 'What I can do:\n' + toolLines.join('\n') : '',
|
|
195
|
-
].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')
|
|
196
174
|
}
|
|
197
175
|
|
|
198
176
|
export interface StreamAgentChatResult {
|
|
@@ -204,11 +182,14 @@ export interface StreamAgentChatResult {
|
|
|
204
182
|
}
|
|
205
183
|
|
|
206
184
|
export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<StreamAgentChatResult> {
|
|
185
|
+
const startTs = Date.now()
|
|
207
186
|
const { session, message, imagePath, attachedFiles, apiKey, systemPrompt, write, history, fallbackCredentialIds, signal } = opts
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
+
])
|
|
212
193
|
|
|
213
194
|
// fallbackCredentialIds is intentionally accepted for compatibility with caller signatures.
|
|
214
195
|
void fallbackCredentialIds
|
|
@@ -385,14 +366,13 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
385
366
|
'- When I learn something that corrects old knowledge, update or remove the old memory',
|
|
386
367
|
].join('\n'))
|
|
387
368
|
|
|
388
|
-
// Pre-compaction memory flush: nudge agent to
|
|
369
|
+
// Pre-compaction memory flush: nudge agent to save important context before it's lost
|
|
389
370
|
const msgCount = history.filter(m => m.role === 'user' || m.role === 'assistant').length
|
|
390
371
|
if (msgCount > 20) {
|
|
391
372
|
stateModifierParts.push([
|
|
392
|
-
'##
|
|
393
|
-
'This conversation is getting long and I might lose older context soon.
|
|
394
|
-
'important I\'ve learned, decided, or discovered to
|
|
395
|
-
'in future conversations. Only what matters, not every detail. If there\'s nothing worth saving, carry on.',
|
|
373
|
+
'## Reflection & Consolidation Reminder',
|
|
374
|
+
'This conversation is getting long and I might lose older context soon.',
|
|
375
|
+
'Save anything important I\'ve learned, decided, or discovered to memory now. Only what matters, not every detail.',
|
|
396
376
|
].join('\n'))
|
|
397
377
|
}
|
|
398
378
|
} catch {
|
|
@@ -441,26 +421,44 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
441
421
|
}
|
|
442
422
|
}
|
|
443
423
|
|
|
444
|
-
// Tell the LLM about
|
|
424
|
+
// Tell the LLM about available plugins and their access status
|
|
445
425
|
{
|
|
446
|
-
const
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
'web_search', 'web_fetch', 'browser', 'memory',
|
|
450
|
-
'claude_code', 'codex_cli', 'opencode_cli',
|
|
451
|
-
'sandbox', 'create_document', 'create_spreadsheet', 'http_request', 'git', 'wallet',
|
|
452
|
-
'manage_agents', 'manage_tasks', 'manage_schedules', 'manage_skills',
|
|
453
|
-
'manage_documents', 'manage_webhooks', 'manage_connectors', 'manage_sessions', 'manage_secrets',
|
|
454
|
-
]
|
|
455
|
-
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()
|
|
456
429
|
const mcpDisabled = agentMcpDisabledTools ?? []
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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`.',
|
|
462
451
|
)
|
|
463
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
|
+
}
|
|
464
462
|
}
|
|
465
463
|
|
|
466
464
|
if (settings.suggestionsEnabled === true) {
|
|
@@ -475,6 +473,9 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
475
473
|
)
|
|
476
474
|
}
|
|
477
475
|
|
|
476
|
+
// Check for existing plan in mainLoopState to skip decomposition injection
|
|
477
|
+
const hasExistingPlan = Array.isArray(session.mainLoopState?.planSteps) && session.mainLoopState.planSteps.length > 0
|
|
478
|
+
|
|
478
479
|
stateModifierParts.push(
|
|
479
480
|
buildAgenticExecutionPolicy({
|
|
480
481
|
enabledTools: sessionToolsWithImplicitProcess,
|
|
@@ -482,10 +483,12 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
482
483
|
heartbeatPrompt,
|
|
483
484
|
heartbeatIntervalSec,
|
|
484
485
|
platformAssignScope: agentPlatformAssignScope,
|
|
486
|
+
userMessage: message,
|
|
487
|
+
hasExistingPlan,
|
|
485
488
|
}),
|
|
486
489
|
)
|
|
487
490
|
|
|
488
|
-
|
|
491
|
+
let stateModifier = stateModifierParts.join('\n\n')
|
|
489
492
|
|
|
490
493
|
const { tools, cleanup } = await buildSessionTools(session.cwd, sessionToolsWithImplicitProcess, {
|
|
491
494
|
agentId: session.agentId,
|
|
@@ -613,6 +616,19 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
613
616
|
// Context manager failure — continue with full history
|
|
614
617
|
}
|
|
615
618
|
|
|
619
|
+
// Context degradation warning: prepend warning to system prompt when nearing limits
|
|
620
|
+
try {
|
|
621
|
+
const { getContextDegradationWarning, estimateTokens: estTokens } = await import('./context-manager')
|
|
622
|
+
const sysTokens = estTokens(stateModifier)
|
|
623
|
+
const warning = getContextDegradationWarning(effectiveHistory, sysTokens, session.provider, session.model)
|
|
624
|
+
if (warning) {
|
|
625
|
+
stateModifierParts.unshift(warning)
|
|
626
|
+
stateModifier = stateModifierParts.join('\n\n')
|
|
627
|
+
}
|
|
628
|
+
} catch {
|
|
629
|
+
// Warning failure is non-critical
|
|
630
|
+
}
|
|
631
|
+
|
|
616
632
|
// Apply context-clear boundary: slice from most recent context-clear marker
|
|
617
633
|
let contextStart = 0
|
|
618
634
|
for (let i = effectiveHistory.length - 1; i >= 0; i--) {
|
|
@@ -624,7 +640,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
624
640
|
const postClearHistory = effectiveHistory.slice(contextStart)
|
|
625
641
|
|
|
626
642
|
const langchainMessages: Array<HumanMessage | AIMessage> = []
|
|
627
|
-
for (const m of postClearHistory.slice(-
|
|
643
|
+
for (const m of postClearHistory.slice(-30)) {
|
|
628
644
|
if (m.role === 'user') {
|
|
629
645
|
langchainMessages.push(new HumanMessage({ content: await buildLangChainContent(m.text, m.imagePath, m.attachedFiles) }))
|
|
630
646
|
} else {
|
|
@@ -639,6 +655,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
639
655
|
let fullText = ''
|
|
640
656
|
let lastSegment = ''
|
|
641
657
|
let hasToolCalls = false
|
|
658
|
+
let needsTextSeparator = false
|
|
642
659
|
let totalInputTokens = 0
|
|
643
660
|
let totalOutputTokens = 0
|
|
644
661
|
let lastToolInput: unknown = null
|
|
@@ -662,135 +679,223 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
662
679
|
}, runtime.ongoingLoopMaxRuntimeMs)
|
|
663
680
|
: null
|
|
664
681
|
|
|
682
|
+
const MAX_AUTO_CONTINUES = 3
|
|
683
|
+
const MAX_TRANSIENT_RETRIES = 2
|
|
684
|
+
let autoContinueCount = 0
|
|
685
|
+
let transientRetryCount = 0
|
|
686
|
+
|
|
665
687
|
try {
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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)
|
|
670
698
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
+
}
|
|
692
745
|
}
|
|
693
746
|
}
|
|
694
|
-
} else {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
|
700
758
|
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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'}`, {
|
|
750
814
|
agentId: session.agentId,
|
|
751
|
-
|
|
752
|
-
category: 'breadcrumb',
|
|
753
|
-
title: breadcrumbTitle,
|
|
754
|
-
content: '',
|
|
815
|
+
detail: { toolName, filePath: inputObj?.filePath, sourcePath: inputObj?.sourcePath, destinationPath: inputObj?.destinationPath, success: !/^Error/i.test((outputStr || '').trim()) },
|
|
755
816
|
})
|
|
756
817
|
}
|
|
757
|
-
|
|
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
|
+
}
|
|
758
834
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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,
|
|
763
855
|
})
|
|
764
|
-
|
|
765
|
-
if (
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
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})`, {
|
|
861
|
+
agentId: session.agentId,
|
|
862
|
+
detail: { errName, errMsg },
|
|
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}`, {
|
|
769
869
|
agentId: session.agentId,
|
|
770
|
-
detail: {
|
|
870
|
+
detail: { errName, errMsg },
|
|
771
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
|
|
772
876
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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 }))
|
|
782
887
|
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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))
|
|
788
893
|
}
|
|
789
894
|
}
|
|
790
|
-
} catch (err:
|
|
895
|
+
} catch (err: unknown) {
|
|
791
896
|
const errMsg = timedOut
|
|
792
897
|
? 'Ongoing loop stopped after reaching the configured runtime limit.'
|
|
793
|
-
: err.message
|
|
898
|
+
: err instanceof Error ? err.message : String(err)
|
|
794
899
|
logExecution(session.id, 'error', errMsg, { agentId: session.agentId, detail: { timedOut } })
|
|
795
900
|
write(`data: ${JSON.stringify({ t: 'err', text: errMsg })}\n\n`)
|
|
796
901
|
} finally {
|
|
@@ -836,6 +941,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
836
941
|
totalTokens,
|
|
837
942
|
estimatedCost: cost,
|
|
838
943
|
timestamp: Date.now(),
|
|
944
|
+
durationMs: Date.now() - startTs,
|
|
839
945
|
}
|
|
840
946
|
appendUsage(session.id, usageRecord)
|
|
841
947
|
// Send usage metadata to client
|