@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
|
@@ -4,30 +4,18 @@ import { HumanMessage, AIMessage } from '@langchain/core/messages'
|
|
|
4
4
|
import { buildSessionTools } from './session-tools'
|
|
5
5
|
import { buildChatModel } from './build-llm'
|
|
6
6
|
import { loadSettings, loadAgents, loadSkills, appendUsage } from './storage'
|
|
7
|
-
import { estimateCost } from './cost'
|
|
7
|
+
import { estimateCost, buildPluginDefinitionCosts } from './cost'
|
|
8
8
|
import { getPluginManager } from './plugins'
|
|
9
9
|
import { loadRuntimeSettings, getAgentLoopRecursionLimit } from './runtime-settings'
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
import { logExecution } from './execution-log'
|
|
12
12
|
import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
|
|
13
|
-
import {
|
|
14
|
-
import type { Session, Message, UsageRecord } from '@/types'
|
|
13
|
+
import { expandPluginIds } from './tool-aliases'
|
|
14
|
+
import type { Session, Message, UsageRecord, PluginInvocationRecord } from '@/types'
|
|
15
15
|
import { extractSuggestions } from './suggestions'
|
|
16
|
+
import { buildIdentityContinuityContext } from './identity-continuity'
|
|
16
17
|
|
|
17
18
|
/** Extract a breadcrumb title from notable tool completions (task/schedule/agent creation). */
|
|
18
|
-
function extractBreadcrumbTitle(toolName: string, input: unknown, output: string | undefined): string | null {
|
|
19
|
-
if (!input || typeof input !== 'object') return null
|
|
20
|
-
const inp = input as Record<string, unknown>
|
|
21
|
-
const action = typeof inp.action === 'string' ? inp.action : ''
|
|
22
|
-
if (toolName === 'manage_tasks') {
|
|
23
|
-
if (action === 'create') return `Created task: ${inp.title || 'Untitled'}`
|
|
24
|
-
if (output && /status.*completed|completed.*successfully/i.test(output)) return `Completed task: ${inp.title || inp.taskId || 'unknown'}`
|
|
25
|
-
}
|
|
26
|
-
if (toolName === 'manage_schedules' && action === 'create') return `Created schedule: ${inp.name || 'Untitled'}`
|
|
27
|
-
if (toolName === 'manage_agents' && action === 'create') return `Created agent: ${inp.name || 'Untitled'}`
|
|
28
|
-
return null
|
|
29
|
-
}
|
|
30
|
-
|
|
31
19
|
interface StreamAgentChatOpts {
|
|
32
20
|
session: Session
|
|
33
21
|
message: string
|
|
@@ -41,41 +29,17 @@ interface StreamAgentChatOpts {
|
|
|
41
29
|
signal?: AbortSignal
|
|
42
30
|
}
|
|
43
31
|
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
if (enabledTools.includes('edit_file')) lines.push('- I can make precise edits to files (`edit_file`) — surgical find-and-replace without rewriting the whole file.')
|
|
52
|
-
if (enabledTools.includes('web_search')) lines.push('- I can search the web (`web_search`) for research, fact-checking, and discovery.')
|
|
53
|
-
if (enabledTools.includes('web_fetch')) lines.push('- I can fetch and read web pages (`web_fetch`) to pull in real content for analysis.')
|
|
54
|
-
if (enabledTools.includes('browser')) lines.push('- I can control a browser (`browser`) — navigate sites, fill forms, take screenshots, interact with web apps.')
|
|
55
|
-
if (enabledTools.includes('claude_code')) lines.push('- I can hand off deep coding work to Claude Code (`delegate_to_claude_code`) for complex multi-file refactors and code generation. Resume IDs may come back via `[delegate_meta]`.')
|
|
56
|
-
if (enabledTools.includes('codex_cli')) lines.push('- I can hand off deep coding work to Codex (`delegate_to_codex_cli`) for complex multi-file refactors and code generation. Resume IDs may come back via `[delegate_meta]`.')
|
|
57
|
-
if (enabledTools.includes('opencode_cli')) lines.push('- I can hand off deep coding work to OpenCode (`delegate_to_opencode_cli`) for complex multi-file refactors and code generation. Resume IDs may come back via `[delegate_meta]`.')
|
|
58
|
-
if (enabledTools.includes('memory')) lines.push('- I have long-term memory (`memory_tool`) — I can remember things across conversations and recall them when needed.')
|
|
59
|
-
if (enabledTools.includes('sandbox')) lines.push('- I can run code in a sandbox (`sandbox_exec`) — JS/TS via Deno or Python, in an isolated environment. I get stdout, stderr, and any files created.')
|
|
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.')
|
|
61
|
-
if (enabledTools.includes('manage_tasks')) lines.push('- I can manage tasks (`manage_tasks`) — create plans, track progress, and stay organized over time.')
|
|
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.')
|
|
64
|
-
if (enabledTools.includes('manage_documents')) lines.push('- I can store and search documents (`manage_documents`) for long-term knowledge and reference.')
|
|
65
|
-
if (enabledTools.includes('manage_webhooks')) lines.push('- I can register webhooks (`manage_webhooks`) so external events can trigger my work automatically.')
|
|
66
|
-
if (enabledTools.includes('manage_skills')) lines.push('- I can manage reusable skills (`manage_skills`) — building blocks I can learn and apply.')
|
|
67
|
-
if (enabledTools.includes('manage_connectors')) lines.push('- I can manage messaging channels (`manage_connectors`) — WhatsApp, Telegram, Slack, Discord — and send proactive messages via `connector_message_tool`.')
|
|
68
|
-
if (enabledTools.includes('manage_sessions')) lines.push('- I can manage chat sessions (`manage_sessions`, `sessions_tool`, `whoami_tool`, `search_history_tool`) — check my identity, look up past conversations, message other sessions, and coordinate work.')
|
|
69
|
-
// Context tools are available to any session with tools (not just manage_sessions)
|
|
70
|
-
if (enabledTools.length > 0) {
|
|
32
|
+
function buildPluginCapabilityLines(enabledPlugins: string[], opts?: { platformAssignScope?: 'self' | 'all' }): string[] {
|
|
33
|
+
// Collect capability descriptions dynamically from plugins
|
|
34
|
+
const lines = getPluginManager().collectCapabilityDescriptions(enabledPlugins)
|
|
35
|
+
|
|
36
|
+
// Context tools are available to any session with plugins
|
|
37
|
+
if (enabledPlugins.length > 0) {
|
|
71
38
|
lines.push('- I can monitor my own context usage (`context_status`) and compact my conversation history (`context_summarize`) when I\'m running low on space.')
|
|
72
39
|
if (opts?.platformAssignScope === 'all') {
|
|
73
40
|
lines.push('- I can delegate tasks to other agents (`delegate_to_agent`) based on their strengths and availability.')
|
|
74
41
|
}
|
|
75
42
|
}
|
|
76
|
-
if (enabledTools.includes('manage_secrets')) lines.push('- I can store and retrieve encrypted secrets (`manage_secrets`) — API keys, credentials, tokens.')
|
|
77
|
-
if (enabledTools.includes('manage_chatrooms')) lines.push('- I can create and participate in chatrooms (`manage_chatrooms`) for multi-agent collaboration with @mention-based discussions.')
|
|
78
|
-
if (enabledTools.includes('wallet')) lines.push('- I have my own crypto wallet (`wallet_tool`) — I can check my balance, send SOL, and review my transaction history.')
|
|
79
43
|
return lines
|
|
80
44
|
}
|
|
81
45
|
|
|
@@ -96,24 +60,21 @@ const GOAL_DECOMPOSITION_BLOCK = [
|
|
|
96
60
|
'When you receive a broad, open-ended goal:',
|
|
97
61
|
'1. Break it into 3-7 concrete, sequentially-executable subtasks before taking action.',
|
|
98
62
|
'2. If manage_tasks is available, create a task for each subtask to track progress.',
|
|
99
|
-
'3.
|
|
63
|
+
'3. Present the plan as a short checklist or numbered list in plain language.',
|
|
100
64
|
'4. Execute the first subtask immediately — do not stop after planning.',
|
|
101
65
|
'5. After each subtask, update progress and move to the next.',
|
|
102
66
|
].join('\n')
|
|
103
67
|
|
|
104
68
|
function buildAgenticExecutionPolicy(opts: {
|
|
105
|
-
|
|
69
|
+
enabledPlugins: string[]
|
|
106
70
|
loopMode: 'bounded' | 'ongoing'
|
|
107
71
|
heartbeatPrompt: string
|
|
108
72
|
heartbeatIntervalSec: number
|
|
109
73
|
platformAssignScope?: 'self' | 'all'
|
|
110
74
|
userMessage?: string
|
|
111
|
-
hasExistingPlan?: boolean
|
|
112
75
|
}) {
|
|
113
|
-
const hasTooling = opts.
|
|
114
|
-
const
|
|
115
|
-
const has = (t: string) => opts.enabledTools.includes(t)
|
|
116
|
-
const hasDelegationTool = has('claude_code') || has('codex_cli') || has('opencode_cli')
|
|
76
|
+
const hasTooling = opts.enabledPlugins.length > 0
|
|
77
|
+
const pluginLines = buildPluginCapabilityLines(opts.enabledPlugins, { platformAssignScope: opts.platformAssignScope })
|
|
117
78
|
|
|
118
79
|
const parts: string[] = []
|
|
119
80
|
|
|
@@ -130,30 +91,9 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
130
91
|
: 'Loop: BOUNDED — execute multiple steps but finish within recursion budget.',
|
|
131
92
|
)
|
|
132
93
|
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
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.')
|
|
94
|
+
// Plugin-specific operating guidance (collected dynamically from plugins)
|
|
95
|
+
const guidanceLines = getPluginManager().collectOperatingGuidance(opts.enabledPlugins)
|
|
96
|
+
if (guidanceLines.length) parts.push(...guidanceLines)
|
|
157
97
|
|
|
158
98
|
// Response behavior
|
|
159
99
|
parts.push(
|
|
@@ -162,13 +102,15 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
162
102
|
'Always reply to: questions, tasks, emotional sharing, or when you have something useful to add.',
|
|
163
103
|
'Execute by default — only ask for confirmation on high-risk/irreversible actions. Do not end every response with a question.',
|
|
164
104
|
'Never repeat completed side effects. Verify state first.',
|
|
105
|
+
'If a tool returns an error or validation failure, do not claim the task succeeded. Retry with corrected arguments or explain the blocker plainly.',
|
|
106
|
+
'Delegation is optional, not a stopping condition. If one delegate backend is unavailable or unauthenticated, try another delegate backend or continue with your other tools.',
|
|
107
|
+
'Only mention files, screenshots, URLs, or download links that were actually returned by tools. Copy returned links exactly; do not rewrite them or prepend extra prefixes like "sandbox:".',
|
|
165
108
|
`Heartbeat: if message is "${opts.heartbeatPrompt}", reply "HEARTBEAT_OK" unless you have a progress update.`,
|
|
166
109
|
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
110
|
)
|
|
169
111
|
|
|
170
|
-
if (
|
|
171
|
-
if (opts.userMessage &&
|
|
112
|
+
if (pluginLines.length) parts.push('What I can do:\n' + pluginLines.join('\n'))
|
|
113
|
+
if (opts.userMessage && isBroadGoal(opts.userMessage)) parts.push(GOAL_DECOMPOSITION_BLOCK)
|
|
172
114
|
|
|
173
115
|
return parts.filter(Boolean).join('\n')
|
|
174
116
|
}
|
|
@@ -184,10 +126,10 @@ export interface StreamAgentChatResult {
|
|
|
184
126
|
export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<StreamAgentChatResult> {
|
|
185
127
|
const startTs = Date.now()
|
|
186
128
|
const { session, message, imagePath, attachedFiles, apiKey, systemPrompt, write, history, fallbackCredentialIds, signal } = opts
|
|
187
|
-
const
|
|
188
|
-
const hasShellCapability =
|
|
189
|
-
const
|
|
190
|
-
...
|
|
129
|
+
const rawPlugins = Array.isArray(session.plugins) ? session.plugins : []
|
|
130
|
+
const hasShellCapability = rawPlugins.some((toolId) => ['shell', 'execute_command'].includes(String(toolId)))
|
|
131
|
+
const sessionPlugins = expandPluginIds([
|
|
132
|
+
...rawPlugins,
|
|
191
133
|
...(hasShellCapability ? ['process'] : []),
|
|
192
134
|
])
|
|
193
135
|
|
|
@@ -196,7 +138,9 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
196
138
|
|
|
197
139
|
// Resolve agent's thinking level for provider-native params
|
|
198
140
|
let agentThinkingLevel: 'minimal' | 'low' | 'medium' | 'high' | undefined
|
|
199
|
-
if (session.
|
|
141
|
+
if (session.thinkingLevel) {
|
|
142
|
+
agentThinkingLevel = session.thinkingLevel
|
|
143
|
+
} else if (session.agentId) {
|
|
200
144
|
const agentsForThinking = loadAgents()
|
|
201
145
|
agentThinkingLevel = agentsForThinking[session.agentId]?.thinkingLevel
|
|
202
146
|
}
|
|
@@ -252,6 +196,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
252
196
|
if (agent?.description) identityLines.push(agent.description)
|
|
253
197
|
identityLines.push('I should always refer to myself by this name. I am not "Assistant" — I have my own name and identity.')
|
|
254
198
|
stateModifierParts.push(identityLines.join(' '))
|
|
199
|
+
const continuityBlock = buildIdentityContinuityContext(session, agent)
|
|
200
|
+
if (continuityBlock) stateModifierParts.push(continuityBlock)
|
|
255
201
|
if (agent?.soul) stateModifierParts.push(agent.soul)
|
|
256
202
|
if (agent?.systemPrompt) stateModifierParts.push(agent.systemPrompt)
|
|
257
203
|
if (agent?.skillIds?.length) {
|
|
@@ -279,109 +225,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
279
225
|
stateModifierParts.push(`## Reasoning Depth\n${thinkingGuidance[agentThinkingLevel]}`)
|
|
280
226
|
}
|
|
281
227
|
|
|
282
|
-
if ((session.tools || []).includes('memory') && session.agentId) {
|
|
283
|
-
try {
|
|
284
|
-
const memDb = getMemoryDb()
|
|
285
|
-
const memoryQuerySeed = [
|
|
286
|
-
message,
|
|
287
|
-
...history
|
|
288
|
-
.slice(-4)
|
|
289
|
-
.filter((h) => h.role === 'user')
|
|
290
|
-
.map((h) => h.text),
|
|
291
|
-
].join('\n')
|
|
292
|
-
|
|
293
|
-
const seen = new Set<string>()
|
|
294
|
-
const formatMemoryLine = (m: { category?: string; title?: string; content?: string; pinned?: boolean }) => {
|
|
295
|
-
const category = String(m.category || 'note')
|
|
296
|
-
const title = String(m.title || 'Untitled').replace(/\s+/g, ' ').trim()
|
|
297
|
-
const snippet = String(m.content || '').replace(/\s+/g, ' ').trim().slice(0, 220)
|
|
298
|
-
const pin = m.pinned ? ' [pinned]' : ''
|
|
299
|
-
return `- [${category}]${pin} ${title}: ${snippet}`
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Pinned memories always appear first
|
|
303
|
-
const pinned = memDb.listPinned(session.agentId, 5)
|
|
304
|
-
const pinnedLines = pinned
|
|
305
|
-
.filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
|
|
306
|
-
.map(formatMemoryLine)
|
|
307
|
-
|
|
308
|
-
// Reduce relevant slice by pinned count to keep total context bounded
|
|
309
|
-
const relevantSlice = Math.max(2, 6 - pinnedLines.length)
|
|
310
|
-
const relevantLookup = memDb.searchWithLinked(memoryQuerySeed, session.agentId, 1, 10, 14)
|
|
311
|
-
const relevant = relevantLookup.entries.slice(0, relevantSlice)
|
|
312
|
-
const recent = memDb.list(session.agentId, 12).slice(0, 6)
|
|
313
|
-
|
|
314
|
-
const relevantLines = relevant
|
|
315
|
-
.filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
|
|
316
|
-
.map(formatMemoryLine)
|
|
317
|
-
|
|
318
|
-
const recentLines = recent
|
|
319
|
-
.filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
|
|
320
|
-
.map(formatMemoryLine)
|
|
321
|
-
|
|
322
|
-
const memorySections: string[] = []
|
|
323
|
-
if (pinnedLines.length) {
|
|
324
|
-
memorySections.push(
|
|
325
|
-
['## Pinned Memories', 'Always-loaded memories marked as important.', ...pinnedLines].join('\n'),
|
|
326
|
-
)
|
|
327
|
-
}
|
|
328
|
-
if (relevantLines.length) {
|
|
329
|
-
memorySections.push(
|
|
330
|
-
['## Relevant Memory Hits', 'These memories were retrieved by relevance for the current objective.', ...relevantLines].join('\n'),
|
|
331
|
-
)
|
|
332
|
-
}
|
|
333
|
-
if (recentLines.length) {
|
|
334
|
-
memorySections.push(
|
|
335
|
-
['## Recent Memory Notes', 'Recent durable notes that may still apply.', ...recentLines].join('\n'),
|
|
336
|
-
)
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (memorySections.length) {
|
|
340
|
-
stateModifierParts.push(memorySections.join('\n\n'))
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Memory Policy — always injected when memory tool is available
|
|
344
|
-
stateModifierParts.push([
|
|
345
|
-
'## My Memory',
|
|
346
|
-
'I have long-term memory that persists across conversations. I use it naturally — I don\'t wait to be asked to remember things.',
|
|
347
|
-
'',
|
|
348
|
-
'**Things worth remembering:**',
|
|
349
|
-
'- What the user likes, dislikes, or has corrected me on',
|
|
350
|
-
'- Important decisions, outcomes, and lessons learned',
|
|
351
|
-
'- What I\'ve discovered about projects, codebases, or environments',
|
|
352
|
-
'- Problems I\'ve hit and how I solved them',
|
|
353
|
-
'- Who people are and how they relate to each other',
|
|
354
|
-
'- Configuration details and environment specifics that I\'ll need again',
|
|
355
|
-
'',
|
|
356
|
-
'**Not worth cluttering my memory with:**',
|
|
357
|
-
'- Throwaway acknowledgments or small talk',
|
|
358
|
-
'- Work-in-progress that\'ll change soon (use category "working" for scratch notes)',
|
|
359
|
-
'- Things already in my system prompt',
|
|
360
|
-
'- Something I\'ve already stored',
|
|
361
|
-
'',
|
|
362
|
-
'**Good habits:**',
|
|
363
|
-
'- Give memories clear titles ("User prefers dark mode" not "Note 1")',
|
|
364
|
-
'- Use categories: preference, fact, learning, project, identity, decision',
|
|
365
|
-
'- Check what I already know before storing something new',
|
|
366
|
-
'- When I learn something that corrects old knowledge, update or remove the old memory',
|
|
367
|
-
].join('\n'))
|
|
368
|
-
|
|
369
|
-
// Pre-compaction memory flush: nudge agent to save important context before it's lost
|
|
370
|
-
const msgCount = history.filter(m => m.role === 'user' || m.role === 'assistant').length
|
|
371
|
-
if (msgCount > 20) {
|
|
372
|
-
stateModifierParts.push([
|
|
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.',
|
|
376
|
-
].join('\n'))
|
|
377
|
-
}
|
|
378
|
-
} catch {
|
|
379
|
-
// If memory context fails to load, continue without blocking the run.
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
228
|
// Inject agent awareness (Phase 2: agents know about each other)
|
|
384
|
-
if ((session.
|
|
229
|
+
if ((session.plugins || []).length > 0 && session.agentId) {
|
|
385
230
|
try {
|
|
386
231
|
const { buildAgentAwarenessBlock } = await import('./agent-registry')
|
|
387
232
|
const awarenessBlock = buildAgentAwarenessBlock(session.agentId)
|
|
@@ -391,39 +236,17 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
391
236
|
}
|
|
392
237
|
}
|
|
393
238
|
|
|
394
|
-
//
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
if (wallet) {
|
|
401
|
-
const { getBalance, lamportsToSol } = await import('./solana')
|
|
402
|
-
let balanceSol = 0
|
|
403
|
-
try {
|
|
404
|
-
const lamports = await getBalance(wallet.publicKey)
|
|
405
|
-
balanceSol = lamportsToSol(lamports)
|
|
406
|
-
} catch { /* RPC failure — show 0 */ }
|
|
407
|
-
const perTxLimit = lamportsToSol(wallet.spendingLimitLamports ?? 100_000_000)
|
|
408
|
-
const dailyLimit = lamportsToSol(wallet.dailyLimitLamports ?? 1_000_000_000)
|
|
409
|
-
stateModifierParts.push([
|
|
410
|
-
'## Your Wallet',
|
|
411
|
-
`You own a ${wallet.chain} wallet — this is your wallet, your funds, your address. Speak about it in the first person ("my wallet", "my balance", "I have X SOL"). You can spend from it freely within your limits${wallet.requireApproval ? ', though the user needs to approve each send before it goes through' : ''}.`,
|
|
412
|
-
`- Address: ${wallet.publicKey}`,
|
|
413
|
-
`- Balance: ${balanceSol} SOL`,
|
|
414
|
-
`- Per-transaction limit: ${perTxLimit} SOL`,
|
|
415
|
-
`- Daily limit: ${dailyLimit} SOL`,
|
|
416
|
-
'Use the `wallet_tool` to check your balance, send SOL, or view your transaction history.',
|
|
417
|
-
].join('\n'))
|
|
418
|
-
}
|
|
419
|
-
} catch {
|
|
420
|
-
// Wallet context is non-critical
|
|
421
|
-
}
|
|
239
|
+
// Collect dynamic context from enabled plugins (wallet, memory, etc.)
|
|
240
|
+
try {
|
|
241
|
+
const pluginContextParts = await getPluginManager().collectAgentContext(session, sessionPlugins, message, history)
|
|
242
|
+
stateModifierParts.push(...pluginContextParts)
|
|
243
|
+
} catch {
|
|
244
|
+
// Plugin context injection is non-critical
|
|
422
245
|
}
|
|
423
246
|
|
|
424
247
|
// Tell the LLM about available plugins and their access status
|
|
425
248
|
{
|
|
426
|
-
const agentEnabledSet = new Set(
|
|
249
|
+
const agentEnabledSet = new Set(sessionPlugins)
|
|
427
250
|
const { getPluginManager } = await import('./plugins')
|
|
428
251
|
const allPlugins = getPluginManager().listPlugins()
|
|
429
252
|
const mcpDisabled = agentMcpDisabledTools ?? []
|
|
@@ -473,24 +296,20 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
473
296
|
)
|
|
474
297
|
}
|
|
475
298
|
|
|
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
|
-
|
|
479
299
|
stateModifierParts.push(
|
|
480
300
|
buildAgenticExecutionPolicy({
|
|
481
|
-
|
|
301
|
+
enabledPlugins: sessionPlugins,
|
|
482
302
|
loopMode: runtime.loopMode,
|
|
483
303
|
heartbeatPrompt,
|
|
484
304
|
heartbeatIntervalSec,
|
|
485
305
|
platformAssignScope: agentPlatformAssignScope,
|
|
486
306
|
userMessage: message,
|
|
487
|
-
hasExistingPlan,
|
|
488
307
|
}),
|
|
489
308
|
)
|
|
490
309
|
|
|
491
310
|
let stateModifier = stateModifierParts.join('\n\n')
|
|
492
311
|
|
|
493
|
-
const { tools, cleanup } = await buildSessionTools(session.cwd,
|
|
312
|
+
const { tools, cleanup, toolToPluginMap } = await buildSessionTools(session.cwd, sessionPlugins, {
|
|
494
313
|
agentId: session.agentId,
|
|
495
314
|
sessionId: session.id,
|
|
496
315
|
platformAssignScope: agentPlatformAssignScope,
|
|
@@ -582,12 +401,27 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
582
401
|
return parts
|
|
583
402
|
}
|
|
584
403
|
|
|
585
|
-
//
|
|
586
|
-
let
|
|
404
|
+
// Apply context-clear boundary: slice from most recent context-clear marker
|
|
405
|
+
let contextStart = 0
|
|
406
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
407
|
+
if (history[i].kind === 'context-clear') {
|
|
408
|
+
contextStart = i + 1
|
|
409
|
+
break
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const postClearHistory = history.slice(contextStart)
|
|
413
|
+
|
|
414
|
+
// Hard cap: only send the most recent 30 messages to the LLM
|
|
415
|
+
const recentHistory = postClearHistory.slice(-30)
|
|
416
|
+
|
|
417
|
+
// Auto-compaction: only trigger if the messages we'll actually send exceed context limits.
|
|
418
|
+
// The .slice(-30) hard cap already prevents context overflow for long conversations,
|
|
419
|
+
// so this only fires for sessions with very large individual messages.
|
|
420
|
+
let effectiveHistory = recentHistory
|
|
587
421
|
try {
|
|
588
422
|
const { shouldAutoCompact, llmCompact, estimateTokens } = await import('./context-manager')
|
|
589
423
|
const systemPromptTokens = estimateTokens(stateModifier)
|
|
590
|
-
if (shouldAutoCompact(
|
|
424
|
+
if (shouldAutoCompact(recentHistory, systemPromptTokens, session.provider, session.model)) {
|
|
591
425
|
const summarize = async (prompt: string): Promise<string> => {
|
|
592
426
|
const response = await llm.invoke([new HumanMessage(prompt)])
|
|
593
427
|
if (typeof response.content === 'string') return response.content
|
|
@@ -599,7 +433,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
599
433
|
return ''
|
|
600
434
|
}
|
|
601
435
|
const result = await llmCompact({
|
|
602
|
-
messages:
|
|
436
|
+
messages: recentHistory,
|
|
603
437
|
provider: session.provider,
|
|
604
438
|
model: session.model,
|
|
605
439
|
agentId: session.agentId || null,
|
|
@@ -608,12 +442,12 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
608
442
|
})
|
|
609
443
|
effectiveHistory = result.messages
|
|
610
444
|
console.log(
|
|
611
|
-
`[stream-agent-chat] Auto-compacted ${session.id}: ${
|
|
445
|
+
`[stream-agent-chat] Auto-compacted ${session.id}: ${recentHistory.length} → ${effectiveHistory.length} msgs` +
|
|
612
446
|
(result.summaryAdded ? ' (LLM summary)' : ' (sliding window fallback)'),
|
|
613
447
|
)
|
|
614
448
|
}
|
|
615
449
|
} catch {
|
|
616
|
-
// Context manager failure — continue with
|
|
450
|
+
// Context manager failure — continue with recent history
|
|
617
451
|
}
|
|
618
452
|
|
|
619
453
|
// Context degradation warning: prepend warning to system prompt when nearing limits
|
|
@@ -629,18 +463,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
629
463
|
// Warning failure is non-critical
|
|
630
464
|
}
|
|
631
465
|
|
|
632
|
-
// Apply context-clear boundary: slice from most recent context-clear marker
|
|
633
|
-
let contextStart = 0
|
|
634
|
-
for (let i = effectiveHistory.length - 1; i >= 0; i--) {
|
|
635
|
-
if (effectiveHistory[i].kind === 'context-clear') {
|
|
636
|
-
contextStart = i + 1
|
|
637
|
-
break
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
const postClearHistory = effectiveHistory.slice(contextStart)
|
|
641
|
-
|
|
642
466
|
const langchainMessages: Array<HumanMessage | AIMessage> = []
|
|
643
|
-
for (const m of
|
|
467
|
+
for (const m of effectiveHistory) {
|
|
644
468
|
if (m.role === 'user') {
|
|
645
469
|
langchainMessages.push(new HumanMessage({ content: await buildLangChainContent(m.text, m.imagePath, m.attachedFiles) }))
|
|
646
470
|
} else {
|
|
@@ -658,12 +482,13 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
658
482
|
let needsTextSeparator = false
|
|
659
483
|
let totalInputTokens = 0
|
|
660
484
|
let totalOutputTokens = 0
|
|
661
|
-
let lastToolInput: unknown = null
|
|
662
485
|
let accumulatedThinking = ''
|
|
486
|
+
const pluginInvocations: PluginInvocationRecord[] = []
|
|
487
|
+
let currentToolInputTokens = 0
|
|
663
488
|
|
|
664
489
|
// Plugin hooks: beforeAgentStart
|
|
665
490
|
const pluginMgr = getPluginManager()
|
|
666
|
-
await pluginMgr.runHook('beforeAgentStart', { session, message })
|
|
491
|
+
await pluginMgr.runHook('beforeAgentStart', { session, message }, { enabledIds: sessionPlugins })
|
|
667
492
|
|
|
668
493
|
const abortController = new AbortController()
|
|
669
494
|
const abortFromSignal = () => abortController.abort()
|
|
@@ -688,6 +513,9 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
688
513
|
const maxIterations = MAX_AUTO_CONTINUES + MAX_TRANSIENT_RETRIES
|
|
689
514
|
for (let iteration = 0; iteration <= maxIterations; iteration++) {
|
|
690
515
|
let shouldContinue: 'recursion' | 'transient' | false = false
|
|
516
|
+
let waitingForToolResult = false
|
|
517
|
+
let idleTimedOut = false
|
|
518
|
+
let idleTimer: ReturnType<typeof setTimeout> | null = null
|
|
691
519
|
|
|
692
520
|
// Fresh per-iteration controller so an internal LangGraph abort doesn't poison subsequent iterations.
|
|
693
521
|
// Linked to the parent so client disconnect / timeout still propagates.
|
|
@@ -696,7 +524,24 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
696
524
|
if (abortController.signal.aborted) iterationController.abort()
|
|
697
525
|
else abortController.signal.addEventListener('abort', onParentAbort)
|
|
698
526
|
|
|
527
|
+
const clearIdleWatchdog = () => {
|
|
528
|
+
if (idleTimer) {
|
|
529
|
+
clearTimeout(idleTimer)
|
|
530
|
+
idleTimer = null
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const armIdleWatchdog = () => {
|
|
535
|
+
clearIdleWatchdog()
|
|
536
|
+
if (waitingForToolResult || iterationController.signal.aborted) return
|
|
537
|
+
idleTimer = setTimeout(() => {
|
|
538
|
+
idleTimedOut = true
|
|
539
|
+
iterationController.abort()
|
|
540
|
+
}, 90_000)
|
|
541
|
+
}
|
|
542
|
+
|
|
699
543
|
try {
|
|
544
|
+
armIdleWatchdog()
|
|
700
545
|
const eventStream = agent.streamEvents(
|
|
701
546
|
{ messages: langchainMessages },
|
|
702
547
|
{ version: 'v2', recursionLimit, signal: iterationController.signal },
|
|
@@ -706,6 +551,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
706
551
|
const kind = event.event
|
|
707
552
|
|
|
708
553
|
if (kind === 'on_chat_model_stream') {
|
|
554
|
+
armIdleWatchdog()
|
|
709
555
|
const chunk = event.data?.chunk
|
|
710
556
|
if (chunk?.content) {
|
|
711
557
|
// content can be string or array of content blocks
|
|
@@ -745,6 +591,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
745
591
|
}
|
|
746
592
|
}
|
|
747
593
|
} else if (kind === 'on_llm_end') {
|
|
594
|
+
armIdleWatchdog()
|
|
748
595
|
// Track token usage from LLM responses — check all known LangChain event shapes
|
|
749
596
|
const output = event.data?.output
|
|
750
597
|
const usage = output?.llmOutput?.tokenUsage
|
|
@@ -757,15 +604,16 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
757
604
|
totalOutputTokens += usage.completionTokens || usage.output_tokens || usage.completion_tokens || 0
|
|
758
605
|
}
|
|
759
606
|
} else if (kind === 'on_tool_start') {
|
|
607
|
+
clearIdleWatchdog()
|
|
608
|
+
waitingForToolResult = true
|
|
760
609
|
hasToolCalls = true
|
|
761
610
|
needsTextSeparator = true
|
|
762
611
|
lastSegment = ''
|
|
763
612
|
const toolName = event.name || 'unknown'
|
|
764
613
|
const input = event.data?.input
|
|
765
|
-
|
|
766
|
-
// Plugin hooks: beforeToolExec
|
|
767
|
-
await pluginMgr.runHook('beforeToolExec', { toolName, input })
|
|
614
|
+
// Estimate input tokens for plugin invocation tracking
|
|
768
615
|
const inputStr = typeof input === 'string' ? input : JSON.stringify(input)
|
|
616
|
+
currentToolInputTokens = Math.ceil((inputStr?.length || 0) / 4)
|
|
769
617
|
logExecution(session.id, 'tool_call', `${toolName} invoked`, {
|
|
770
618
|
agentId: session.agentId,
|
|
771
619
|
detail: { toolName, input: inputStr?.slice(0, 4000) },
|
|
@@ -776,6 +624,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
776
624
|
toolInput: inputStr,
|
|
777
625
|
})}\n\n`)
|
|
778
626
|
} else if (kind === 'on_tool_end') {
|
|
627
|
+
waitingForToolResult = false
|
|
628
|
+
armIdleWatchdog()
|
|
779
629
|
const toolName = event.name || 'unknown'
|
|
780
630
|
const output = event.data?.output
|
|
781
631
|
const outputStr = typeof output === 'string'
|
|
@@ -783,25 +633,6 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
783
633
|
: output?.content
|
|
784
634
|
? String(output.content)
|
|
785
635
|
: 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
636
|
logExecution(session.id, 'tool_result', `${toolName} returned`, {
|
|
806
637
|
agentId: session.agentId,
|
|
807
638
|
detail: { toolName, output: outputStr?.slice(0, 4000), error: /^(Error:|error:)/i.test((outputStr || '').trim()) || undefined },
|
|
@@ -825,6 +656,16 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
825
656
|
})
|
|
826
657
|
}
|
|
827
658
|
}
|
|
659
|
+
// Track plugin invocation token estimates
|
|
660
|
+
const pluginId = toolToPluginMap[toolName] || '_unknown'
|
|
661
|
+
pluginInvocations.push({
|
|
662
|
+
pluginId,
|
|
663
|
+
toolName,
|
|
664
|
+
inputTokens: currentToolInputTokens,
|
|
665
|
+
outputTokens: Math.ceil((outputStr?.length || 0) / 4),
|
|
666
|
+
})
|
|
667
|
+
currentToolInputTokens = 0
|
|
668
|
+
|
|
828
669
|
write(`data: ${JSON.stringify({
|
|
829
670
|
t: 'tool_result',
|
|
830
671
|
toolName,
|
|
@@ -834,7 +675,9 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
834
675
|
}
|
|
835
676
|
} catch (innerErr: unknown) {
|
|
836
677
|
const errName = innerErr instanceof Error ? innerErr.constructor.name : ''
|
|
837
|
-
const errMsg =
|
|
678
|
+
const errMsg = idleTimedOut
|
|
679
|
+
? 'Model stream stalled without emitting text or tool results for 90 seconds.'
|
|
680
|
+
: innerErr instanceof Error ? innerErr.message : String(innerErr)
|
|
838
681
|
const errStack = innerErr instanceof Error ? innerErr.stack?.slice(0, 500) : undefined
|
|
839
682
|
|
|
840
683
|
// Classify the error:
|
|
@@ -842,9 +685,10 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
842
685
|
// 2. Transient abort/timeout — LLM API failure, not from client disconnect
|
|
843
686
|
const isRecursionError = errName === 'GraphRecursionError'
|
|
844
687
|
|| /recursion limit|maximum recursion/i.test(errMsg)
|
|
845
|
-
const isTransientAbort = !isRecursionError
|
|
688
|
+
const isTransientAbort = (!isRecursionError && idleTimedOut)
|
|
689
|
+
|| (!isRecursionError
|
|
846
690
|
&& /abort|timed?\s*out|ECONNRESET|ECONNREFUSED|socket hang up|network/i.test(errMsg)
|
|
847
|
-
&& !abortController.signal.aborted
|
|
691
|
+
&& !abortController.signal.aborted)
|
|
848
692
|
|
|
849
693
|
// Log diagnostic details for every error so we can trace root causes
|
|
850
694
|
console.error(`[stream-agent-chat] Error in streamEvents iteration=${iteration}`, {
|
|
@@ -875,6 +719,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
875
719
|
throw innerErr
|
|
876
720
|
}
|
|
877
721
|
} finally {
|
|
722
|
+
clearIdleWatchdog()
|
|
878
723
|
abortController.signal.removeEventListener('abort', onParentAbort)
|
|
879
724
|
}
|
|
880
725
|
|
|
@@ -931,6 +776,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
931
776
|
const totalTokens = totalInputTokens + totalOutputTokens
|
|
932
777
|
if (totalTokens > 0) {
|
|
933
778
|
const cost = estimateCost(session.model, totalInputTokens, totalOutputTokens)
|
|
779
|
+
const pluginDefinitionCosts = buildPluginDefinitionCosts(tools, toolToPluginMap)
|
|
934
780
|
const usageRecord: UsageRecord = {
|
|
935
781
|
sessionId: session.id,
|
|
936
782
|
messageIndex: history.length,
|
|
@@ -942,6 +788,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
942
788
|
estimatedCost: cost,
|
|
943
789
|
timestamp: Date.now(),
|
|
944
790
|
durationMs: Date.now() - startTs,
|
|
791
|
+
pluginDefinitionCosts,
|
|
792
|
+
pluginInvocations: pluginInvocations.length > 0 ? pluginInvocations : undefined,
|
|
945
793
|
}
|
|
946
794
|
appendUsage(session.id, usageRecord)
|
|
947
795
|
// Send usage metadata to client
|
|
@@ -952,7 +800,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
952
800
|
}
|
|
953
801
|
|
|
954
802
|
// Plugin hooks: afterAgentComplete
|
|
955
|
-
await pluginMgr.runHook('afterAgentComplete', { session, response: fullText })
|
|
803
|
+
await pluginMgr.runHook('afterAgentComplete', { session, response: fullText }, { enabledIds: sessionPlugins })
|
|
956
804
|
|
|
957
805
|
// OpenClaw auto-sync: push memory if enabled
|
|
958
806
|
try {
|