@swarmclawai/swarmclaw 0.7.3 → 0.7.5
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 +47 -40
- package/bin/package-manager.js +157 -0
- package/bin/package-manager.test.js +90 -0
- package/bin/server-cmd.js +38 -7
- package/bin/swarmclaw.js +54 -4
- package/bin/update-cmd.js +48 -10
- package/bin/update-cmd.test.js +55 -0
- package/package.json +8 -3
- package/scripts/postinstall.mjs +26 -0
- package/src/app/api/agents/[id]/route.ts +17 -0
- package/src/app/api/agents/[id]/thread/route.ts +4 -87
- package/src/app/api/agents/route.ts +23 -1
- package/src/app/api/auth/route.ts +1 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +16 -5
- package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/route.ts +6 -0
- package/src/app/api/chats/[id]/route.ts +12 -0
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/route.ts +7 -1
- package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
- package/src/app/api/external-agents/[id]/route.ts +31 -0
- package/src/app/api/external-agents/register/route.ts +3 -0
- package/src/app/api/external-agents/route.ts +66 -0
- package/src/app/api/gateways/[id]/health/route.ts +28 -0
- package/src/app/api/gateways/[id]/route.ts +79 -0
- package/src/app/api/gateways/route.ts +57 -0
- package/src/app/api/openclaw/gateway/route.ts +10 -7
- package/src/app/api/openclaw/skills/route.ts +1 -1
- package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
- package/src/app/api/schedules/[id]/route.ts +38 -9
- package/src/app/api/schedules/route.ts +51 -28
- package/src/app/api/settings/route.ts +6 -10
- package/src/app/api/setup/doctor/route.ts +6 -4
- package/src/app/api/tasks/[id]/route.ts +2 -1
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/page.tsx +126 -15
- package/src/cli/binary.test.js +142 -0
- package/src/cli/index.js +34 -11
- package/src/cli/index.test.js +195 -0
- package/src/cli/index.ts +20 -4
- package/src/cli/server-cmd.test.js +59 -0
- package/src/cli/spec.js +20 -2
- package/src/components/agents/agent-sheet.tsx +249 -7
- package/src/components/agents/inspector-panel.tsx +3 -2
- package/src/components/agents/sandbox-env-panel.tsx +4 -1
- package/src/components/auth/setup-wizard.tsx +970 -275
- package/src/components/chat/chat-area.tsx +41 -14
- package/src/components/chat/chat-card.tsx +2 -1
- package/src/components/chat/chat-header.tsx +8 -13
- package/src/components/chat/chat-list.tsx +58 -20
- package/src/components/chat/message-list.tsx +142 -18
- package/src/components/chatrooms/chatroom-input.tsx +96 -33
- package/src/components/chatrooms/chatroom-list.tsx +141 -72
- package/src/components/chatrooms/chatroom-message.tsx +7 -6
- package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
- package/src/components/chatrooms/chatroom-view.tsx +157 -86
- package/src/components/chatrooms/reaction-picker.tsx +38 -33
- package/src/components/gateways/gateway-sheet.tsx +567 -0
- package/src/components/input/chat-input.tsx +135 -86
- package/src/components/layout/app-layout.tsx +2 -0
- package/src/components/memory/memory-browser.tsx +71 -6
- package/src/components/memory/memory-card.tsx +18 -0
- package/src/components/memory/memory-detail.tsx +58 -31
- package/src/components/memory/memory-sheet.tsx +32 -4
- package/src/components/projects/project-detail.tsx +7 -2
- package/src/components/providers/provider-list.tsx +158 -2
- package/src/components/providers/provider-sheet.tsx +81 -70
- package/src/components/shared/bottom-sheet.tsx +31 -15
- package/src/components/shared/confirm-dialog.tsx +45 -30
- package/src/components/shared/model-combobox.tsx +90 -8
- package/src/components/shared/settings/section-heartbeat.tsx +11 -6
- package/src/components/shared/settings/section-orchestrator.tsx +3 -0
- package/src/components/shared/settings/settings-page.tsx +5 -3
- package/src/components/tasks/approvals-panel.tsx +7 -1
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
- package/src/lib/heartbeat-defaults.ts +48 -0
- package/src/lib/memory-presentation.ts +59 -0
- package/src/lib/provider-model-discovery-client.ts +29 -0
- package/src/lib/providers/index.ts +12 -5
- package/src/lib/runtime-loop.ts +105 -3
- package/src/lib/safe-storage.ts +6 -1
- package/src/lib/server/agent-runtime-config.test.ts +141 -0
- package/src/lib/server/agent-runtime-config.ts +277 -0
- package/src/lib/server/agent-thread-session.test.ts +85 -0
- package/src/lib/server/agent-thread-session.ts +123 -0
- package/src/lib/server/approvals-auto-approve.test.ts +59 -0
- package/src/lib/server/build-llm.test.ts +13 -5
- package/src/lib/server/chat-execution-tool-events.test.ts +87 -2
- package/src/lib/server/chat-execution.ts +159 -71
- package/src/lib/server/chatroom-helpers.test.ts +7 -0
- package/src/lib/server/chatroom-helpers.ts +99 -6
- package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
- package/src/lib/server/connectors/manager.ts +89 -61
- package/src/lib/server/connectors/slack.ts +1 -1
- package/src/lib/server/daemon-state.ts +3 -2
- package/src/lib/server/data-dir.test.ts +56 -0
- package/src/lib/server/data-dir.ts +15 -9
- package/src/lib/server/eval/agent-regression.test.ts +47 -0
- package/src/lib/server/eval/agent-regression.ts +1742 -0
- package/src/lib/server/eval/runner.ts +11 -1
- package/src/lib/server/eval/store.ts +2 -1
- package/src/lib/server/heartbeat-service.ts +23 -8
- package/src/lib/server/heartbeat-wake.ts +6 -2
- package/src/lib/server/main-agent-loop.ts +13 -6
- package/src/lib/server/openclaw-exec-config.ts +4 -2
- package/src/lib/server/openclaw-gateway.ts +123 -36
- package/src/lib/server/orchestrator-lg.ts +1 -2
- package/src/lib/server/orchestrator.ts +3 -2
- package/src/lib/server/plugins.test.ts +9 -1
- package/src/lib/server/plugins.ts +12 -2
- package/src/lib/server/provider-model-discovery.ts +481 -0
- package/src/lib/server/queue.ts +1 -1
- package/src/lib/server/runtime-settings.test.ts +119 -0
- package/src/lib/server/runtime-settings.ts +12 -92
- package/src/lib/server/schedule-normalization.ts +187 -0
- package/src/lib/server/session-tools/autonomy-tools.test.ts +23 -0
- package/src/lib/server/session-tools/crud.ts +27 -3
- package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
- package/src/lib/server/session-tools/discovery.ts +18 -8
- package/src/lib/server/session-tools/file-normalize.test.ts +5 -0
- package/src/lib/server/session-tools/file.ts +8 -2
- package/src/lib/server/session-tools/http.ts +9 -3
- package/src/lib/server/session-tools/index.ts +31 -1
- package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
- package/src/lib/server/session-tools/monitor.ts +14 -7
- package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
- package/src/lib/server/session-tools/platform.ts +1 -1
- package/src/lib/server/session-tools/plugin-creator.ts +9 -2
- package/src/lib/server/session-tools/sandbox.ts +51 -92
- package/src/lib/server/session-tools/session-info.ts +22 -1
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +23 -0
- package/src/lib/server/session-tools/shell.ts +2 -2
- package/src/lib/server/session-tools/subagent.ts +3 -1
- package/src/lib/server/session-tools/web.ts +73 -30
- package/src/lib/server/storage.ts +29 -3
- package/src/lib/server/stream-agent-chat.test.ts +61 -0
- package/src/lib/server/stream-agent-chat.ts +139 -4
- package/src/lib/server/structured-extract.ts +1 -1
- package/src/lib/server/task-mention.ts +0 -1
- package/src/lib/server/tool-aliases.ts +37 -6
- package/src/lib/server/tool-capability-policy.ts +1 -1
- package/src/lib/setup-defaults.ts +352 -11
- package/src/lib/tool-definitions.ts +3 -4
- package/src/lib/validation/schemas.ts +55 -1
- package/src/stores/use-app-store.ts +43 -1
- package/src/stores/use-chatroom-store.ts +153 -26
- package/src/types/index.ts +189 -6
- package/src/app/api/chats/[id]/main-loop/route.ts +0 -13
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { describe, it } from 'node:test'
|
|
5
|
+
import { buildToolDisciplineLines, looksLikeOpenEndedDeliverableTask } from './stream-agent-chat'
|
|
6
|
+
|
|
7
|
+
const streamAgentChatSource = fs.readFileSync(path.join(path.dirname(new URL(import.meta.url).pathname), 'stream-agent-chat.ts'), 'utf-8')
|
|
8
|
+
|
|
9
|
+
describe('buildToolDisciplineLines', () => {
|
|
10
|
+
it('tells the agent to use direct platform tools when manage_platform is absent', () => {
|
|
11
|
+
const lines = buildToolDisciplineLines(['files', 'manage_schedules'])
|
|
12
|
+
|
|
13
|
+
assert.equal(lines[0], 'Enabled tools in this session: `files`, `manage_schedules`.')
|
|
14
|
+
assert.ok(lines.some((line) => line.includes('Do not substitute `manage_platform`')))
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('omits the manage_platform warning when the umbrella tool is enabled', () => {
|
|
18
|
+
const lines = buildToolDisciplineLines(['manage_platform', 'manage_schedules'])
|
|
19
|
+
|
|
20
|
+
assert.ok(lines.every((line) => !line.includes('Do not substitute `manage_platform`')))
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('includes concrete files-tool examples for revision work', () => {
|
|
24
|
+
const lines = buildToolDisciplineLines(['files'])
|
|
25
|
+
|
|
26
|
+
assert.ok(lines.some((line) => line.includes('{"action":"read","filePath":"path/to/file.md"}')))
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('warns browser tasks to use literal urls and the supported form schema', () => {
|
|
30
|
+
const lines = buildToolDisciplineLines(['browser', 'http_request', 'email', 'ask_human'])
|
|
31
|
+
|
|
32
|
+
assert.ok(lines.some((line) => line.includes('Do not invent placeholder URLs')))
|
|
33
|
+
assert.ok(lines.some((line) => line.includes('A shorthand `form` object keyed by input id/name also works')))
|
|
34
|
+
assert.ok(lines.some((line) => line.includes('Keep JSON request bodies as raw JSON strings')))
|
|
35
|
+
assert.ok(lines.some((line) => line.includes('{"action":"send","to":"user@example.com","subject":"...","body":"..."}')))
|
|
36
|
+
assert.ok(lines.some((line) => line.includes('do not guess or keep re-submitting blank forms')))
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('tells the agent that named enabled tools are completion requirements', () => {
|
|
40
|
+
assert.ok(streamAgentChatSource.includes('If a task explicitly names an enabled tool, use that tool before declaring success.'))
|
|
41
|
+
assert.ok(streamAgentChatSource.includes('collect required human input through the tool'))
|
|
42
|
+
assert.ok(streamAgentChatSource.includes('You have not yet completed the required explicit tool step(s):'))
|
|
43
|
+
assert.ok(streamAgentChatSource.includes('[Loop Budget Reached]'))
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('looksLikeOpenEndedDeliverableTask', () => {
|
|
48
|
+
it('detects open-ended deliverable prompts', () => {
|
|
49
|
+
assert.equal(
|
|
50
|
+
looksLikeOpenEndedDeliverableTask('Revise the landing copy and update the proposal draft with a stronger second pass.'),
|
|
51
|
+
true,
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('does not misclassify explicit coding tasks', () => {
|
|
56
|
+
assert.equal(
|
|
57
|
+
looksLikeOpenEndedDeliverableTask('Fix the React bug in src/components/chat/chat-area.tsx and run npm run build.'),
|
|
58
|
+
false,
|
|
59
|
+
)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import { createReactAgent } from '@langchain/langgraph/prebuilt'
|
|
3
3
|
import { HumanMessage, AIMessage } from '@langchain/core/messages'
|
|
4
|
+
import { DEFAULT_HEARTBEAT_INTERVAL_SEC } from '@/lib/heartbeat-defaults'
|
|
4
5
|
import { buildSessionTools } from './session-tools'
|
|
5
6
|
import { buildChatModel } from './build-llm'
|
|
6
7
|
import { loadSettings, loadAgents, loadSkills, appendUsage } from './storage'
|
|
@@ -14,6 +15,7 @@ import { expandPluginIds } from './tool-aliases'
|
|
|
14
15
|
import type { Session, Message, UsageRecord, PluginInvocationRecord } from '@/types'
|
|
15
16
|
import { extractSuggestions } from './suggestions'
|
|
16
17
|
import { buildIdentityContinuityContext } from './identity-continuity'
|
|
18
|
+
import { enqueueSystemEvent } from './system-events'
|
|
17
19
|
|
|
18
20
|
/** Extract a breadcrumb title from notable tool completions (task/schedule/agent creation). */
|
|
19
21
|
interface StreamAgentChatOpts {
|
|
@@ -43,6 +45,85 @@ function buildPluginCapabilityLines(enabledPlugins: string[], opts?: { platformA
|
|
|
43
45
|
return lines
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
export function buildToolDisciplineLines(enabledPlugins: string[]): string[] {
|
|
49
|
+
const uniqueTools = Array.from(new Set(enabledPlugins.filter(Boolean))).sort()
|
|
50
|
+
if (uniqueTools.length === 0) return []
|
|
51
|
+
|
|
52
|
+
const lines = [
|
|
53
|
+
`Enabled tools in this session: ${uniqueTools.map((toolId) => `\`${toolId}\``).join(', ')}.`,
|
|
54
|
+
'Only call tools from this enabled list or tools explicitly returned by the runtime.',
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
const directPlatformTools = uniqueTools.filter((toolId) => toolId.startsWith('manage_') && toolId !== 'manage_platform')
|
|
58
|
+
if (directPlatformTools.length > 0 && !uniqueTools.includes('manage_platform')) {
|
|
59
|
+
lines.push(`Use direct platform tools exactly as named (${directPlatformTools.map((toolId) => `\`${toolId}\``).join(', ')}). Do not substitute \`manage_platform\` unless it is explicitly enabled.`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (uniqueTools.includes('files')) {
|
|
63
|
+
lines.push('For `files`, include an explicit action whenever possible. Common patterns: `{"action":"list","dirPath":"."}`, `{"action":"read","filePath":"path/to/file.md"}`, and `{"action":"write","files":[{"path":"path/to/file.md","content":"..."}]}`.')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (uniqueTools.includes('shell')) {
|
|
67
|
+
lines.push('For `shell`, use `{"action":"execute","command":"..."}` for commands and `{"action":"status","processId":"..."}` or `{"action":"log","processId":"..."}` for long-lived processes.')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (uniqueTools.includes('web')) {
|
|
71
|
+
lines.push('For `web`, use `{"action":"search","query":"..."}` to research and `{"action":"fetch","url":"https://..."}` to read a specific page.')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (uniqueTools.includes('browser')) {
|
|
75
|
+
lines.push('For `browser`, when the task includes a literal URL, pass that exact URL string to `{"action":"navigate","url":"..."}`. Do not invent placeholder URLs like `[Your URL]`, `Example_URL`, or `MockMailPage_URL`.')
|
|
76
|
+
lines.push('For `browser` form work, prefer `{"action":"fill_form","fields":[{"element":"#email","value":"user@example.com"},{"element":"#password","value":"..."}]}`. A shorthand `form` object keyed by input id/name also works for simple forms.')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (uniqueTools.includes('http_request')) {
|
|
80
|
+
lines.push('For `http_request`, send exact literal URLs from the task or from prior tool results. Keep JSON request bodies as raw JSON strings.')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (uniqueTools.includes('email')) {
|
|
84
|
+
lines.push('For `email`, send mail with `{"action":"send","to":"user@example.com","subject":"...","body":"..."}`. If delivery depends on SMTP setup, check `{"action":"status"}` before claiming success.')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (uniqueTools.includes('ask_human')) {
|
|
88
|
+
lines.push('For `ask_human`, when a workflow needs a code, approval, or out-of-band value from a person, do not guess or keep re-submitting blank forms. Use `{"action":"request_input","question":"..."}` and, for durable pauses, `{"action":"wait_for_reply","correlationId":"..."}`.')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return lines
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function looksLikeOpenEndedDeliverableTask(text: string): boolean {
|
|
95
|
+
const normalized = text.toLowerCase()
|
|
96
|
+
if (!normalized.trim()) return false
|
|
97
|
+
if (/```|package\.json|tsconfig|tsx?\b|jsx?\b|pytest|vitest|npm run|src\/|components\/|api\//.test(normalized)) return false
|
|
98
|
+
if (/\b(revise|revision|iterate|iteration|draft|deliverable|deliverables|offer|brief|copy|proposal|landing|outreach|plan|strategy|report|memo|document|docs?)\b/.test(normalized)) return true
|
|
99
|
+
return isBroadGoal(text) && /(\.md\b|\.txt\b|copy|brief|proposal|plan|report|draft|document)/.test(normalized)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getExplicitRequiredToolNames(userMessage: string, enabledPlugins: string[]): string[] {
|
|
103
|
+
const normalized = userMessage.toLowerCase()
|
|
104
|
+
const required: string[] = []
|
|
105
|
+
|
|
106
|
+
if (enabledPlugins.includes('ask_human')
|
|
107
|
+
&& (/\bask_human\b/.test(normalized) || /ask the human/.test(normalized) || /request_input/.test(normalized))) {
|
|
108
|
+
required.push('ask_human')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (enabledPlugins.includes('email')
|
|
112
|
+
&& (/\bemail\b/.test(normalized) || /send a welcome email/.test(normalized) || /send an email/.test(normalized))) {
|
|
113
|
+
required.push('email')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return required
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const OPEN_ENDED_REVISION_BLOCK = [
|
|
120
|
+
'## Revision Loop',
|
|
121
|
+
'For open-ended deliverable work, do a real two-pass loop before declaring success: create the draft artifacts, critique them against the objective, then modify at least one artifact based on that critique.',
|
|
122
|
+
'A critique by itself does not count as iteration. Iteration requires an actual changed artifact.',
|
|
123
|
+
'When resuming in an existing workspace, inspect the current files first, then update them. Do not assume you lost access to the workspace without an explicit tool attempt.',
|
|
124
|
+
'If `files` is available, use it with explicit actions and paths to inspect and revise the artifacts.',
|
|
125
|
+
].join('\n')
|
|
126
|
+
|
|
46
127
|
/** Detect whether a user message is a broad, high-level goal that benefits from decomposition. */
|
|
47
128
|
function isBroadGoal(text: string): boolean {
|
|
48
129
|
if (text.length < 50) return false
|
|
@@ -75,6 +156,7 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
75
156
|
}) {
|
|
76
157
|
const hasTooling = opts.enabledPlugins.length > 0
|
|
77
158
|
const pluginLines = buildPluginCapabilityLines(opts.enabledPlugins, { platformAssignScope: opts.platformAssignScope })
|
|
159
|
+
const toolDisciplineLines = buildToolDisciplineLines(opts.enabledPlugins)
|
|
78
160
|
|
|
79
161
|
const parts: string[] = []
|
|
80
162
|
|
|
@@ -85,7 +167,8 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
85
167
|
? 'I take initiative — plan briefly, execute tools, evaluate, iterate until done. Never stop at advice when action is implied.'
|
|
86
168
|
: 'No tools enabled. Be explicit about what tool access is needed.',
|
|
87
169
|
'Follow through on stated intentions with tool calls. Never claim results without tool evidence.',
|
|
88
|
-
'If a
|
|
170
|
+
'If a task explicitly names an enabled tool, use that tool before declaring success. A prose request is not a substitute for `ask_human`, and browser work is not a substitute for `email` delivery.',
|
|
171
|
+
'When `ask_human` is enabled, collect required human input through the tool instead of asking for it only in plain assistant text.',
|
|
89
172
|
opts.loopMode === 'ongoing'
|
|
90
173
|
? 'Loop: ONGOING — keep iterating until done, blocked, or limits reached.'
|
|
91
174
|
: 'Loop: BOUNDED — execute multiple steps but finish within recursion budget.',
|
|
@@ -103,14 +186,21 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
103
186
|
'Execute by default — only ask for confirmation on high-risk/irreversible actions. Do not end every response with a question.',
|
|
104
187
|
'Never repeat completed side effects. Verify state first.',
|
|
105
188
|
'If a tool returns an error or validation failure, do not claim the task succeeded. Retry with corrected arguments or explain the blocker plainly.',
|
|
189
|
+
'Prefer the most specific tool you already have. Example: use `manage_schedules` for schedules and `manage_tasks` for tasks; treat `manage_platform` as a fallback umbrella only when a specific `manage_*` tool is unavailable.',
|
|
190
|
+
'For recurring, cron, interval, or follow-up automation requests, use `manage_schedules` directly when it is available.',
|
|
106
191
|
'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.',
|
|
192
|
+
'If a required tool is missing, request access by name with `manage_capabilities` action `request_access` (for example `shell` or `manage_schedules`).',
|
|
107
193
|
'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:".',
|
|
108
194
|
`Heartbeat: if message is "${opts.heartbeatPrompt}", reply "HEARTBEAT_OK" unless you have a progress update.`,
|
|
109
195
|
opts.heartbeatIntervalSec > 0 ? `Heartbeat cadence: ~${opts.heartbeatIntervalSec}s.` : '',
|
|
110
196
|
)
|
|
111
197
|
|
|
198
|
+
if (toolDisciplineLines.length) parts.push('## Tool Discipline', ...toolDisciplineLines)
|
|
112
199
|
if (pluginLines.length) parts.push('What I can do:\n' + pluginLines.join('\n'))
|
|
113
200
|
if (opts.userMessage && isBroadGoal(opts.userMessage)) parts.push(GOAL_DECOMPOSITION_BLOCK)
|
|
201
|
+
if (opts.userMessage && looksLikeOpenEndedDeliverableTask(opts.userMessage) && opts.enabledPlugins.some((toolId) => toolId === 'files' || toolId === 'edit_file')) {
|
|
202
|
+
parts.push(OPEN_ENDED_REVISION_BLOCK)
|
|
203
|
+
}
|
|
114
204
|
|
|
115
205
|
return parts.filter(Boolean).join('\n')
|
|
116
206
|
}
|
|
@@ -166,7 +256,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
166
256
|
: typeof raw === 'string'
|
|
167
257
|
? Number.parseInt(raw, 10)
|
|
168
258
|
: Number.NaN
|
|
169
|
-
if (!Number.isFinite(parsed)) return
|
|
259
|
+
if (!Number.isFinite(parsed)) return DEFAULT_HEARTBEAT_INTERVAL_SEC
|
|
170
260
|
return Math.max(0, Math.min(3600, Math.trunc(parsed)))
|
|
171
261
|
})()
|
|
172
262
|
|
|
@@ -184,12 +274,14 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
184
274
|
let agentPlatformAssignScope: 'self' | 'all' = 'self'
|
|
185
275
|
let agentMcpServerIds: string[] | undefined
|
|
186
276
|
let agentMcpDisabledTools: string[] | undefined
|
|
277
|
+
let agentHeartbeatEnabled = false
|
|
187
278
|
if (session.agentId) {
|
|
188
279
|
const agents = loadAgents()
|
|
189
280
|
const agent = agents[session.agentId]
|
|
190
281
|
agentPlatformAssignScope = agent?.platformAssignScope || 'self'
|
|
191
282
|
agentMcpServerIds = agent?.mcpServerIds
|
|
192
283
|
agentMcpDisabledTools = agent?.mcpDisabledTools
|
|
284
|
+
agentHeartbeatEnabled = agent?.heartbeatEnabled === true
|
|
193
285
|
if (!hasProvidedSystemPrompt) {
|
|
194
286
|
// Identity block — make sure the agent knows who it is
|
|
195
287
|
const identityLines = [`## My Identity`, `My name is ${agent?.name || 'Agent'}.`]
|
|
@@ -506,13 +598,18 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
506
598
|
|
|
507
599
|
const MAX_AUTO_CONTINUES = 3
|
|
508
600
|
const MAX_TRANSIENT_RETRIES = 2
|
|
601
|
+
const MAX_REQUIRED_TOOL_CONTINUES = 2
|
|
509
602
|
let autoContinueCount = 0
|
|
510
603
|
let transientRetryCount = 0
|
|
604
|
+
let requiredToolContinueCount = 0
|
|
605
|
+
const explicitRequiredToolNames = getExplicitRequiredToolNames(message, sessionPlugins)
|
|
606
|
+
const usedToolNames = new Set<string>()
|
|
511
607
|
|
|
512
608
|
try {
|
|
513
|
-
const maxIterations = MAX_AUTO_CONTINUES + MAX_TRANSIENT_RETRIES
|
|
609
|
+
const maxIterations = MAX_AUTO_CONTINUES + MAX_TRANSIENT_RETRIES + MAX_REQUIRED_TOOL_CONTINUES
|
|
514
610
|
for (let iteration = 0; iteration <= maxIterations; iteration++) {
|
|
515
|
-
let shouldContinue: 'recursion' | 'transient' | false = false
|
|
611
|
+
let shouldContinue: 'recursion' | 'transient' | 'required_tool' | false = false
|
|
612
|
+
let requiredToolReminderNames: string[] = []
|
|
516
613
|
let waitingForToolResult = false
|
|
517
614
|
let idleTimedOut = false
|
|
518
615
|
let idleTimer: ReturnType<typeof setTimeout> | null = null
|
|
@@ -610,6 +707,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
610
707
|
needsTextSeparator = true
|
|
611
708
|
lastSegment = ''
|
|
612
709
|
const toolName = event.name || 'unknown'
|
|
710
|
+
usedToolNames.add(toolName)
|
|
613
711
|
const input = event.data?.input
|
|
614
712
|
// Estimate input tokens for plugin invocation tracking
|
|
615
713
|
const inputStr = typeof input === 'string' ? input : JSON.stringify(input)
|
|
@@ -723,6 +821,22 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
723
821
|
abortController.signal.removeEventListener('abort', onParentAbort)
|
|
724
822
|
}
|
|
725
823
|
|
|
824
|
+
if (!shouldContinue && explicitRequiredToolNames.length > 0 && requiredToolContinueCount < MAX_REQUIRED_TOOL_CONTINUES) {
|
|
825
|
+
requiredToolReminderNames = explicitRequiredToolNames.filter((toolName) => !usedToolNames.has(toolName))
|
|
826
|
+
if (requiredToolReminderNames.length > 0) {
|
|
827
|
+
shouldContinue = 'required_tool'
|
|
828
|
+
requiredToolContinueCount++
|
|
829
|
+
write(`data: ${JSON.stringify({
|
|
830
|
+
t: 'status',
|
|
831
|
+
text: JSON.stringify({
|
|
832
|
+
requiredToolsPending: requiredToolReminderNames,
|
|
833
|
+
reminderCount: requiredToolContinueCount,
|
|
834
|
+
maxReminders: MAX_REQUIRED_TOOL_CONTINUES,
|
|
835
|
+
}),
|
|
836
|
+
})}\n\n`)
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
726
840
|
if (!shouldContinue) break
|
|
727
841
|
|
|
728
842
|
if (shouldContinue === 'recursion') {
|
|
@@ -732,6 +846,14 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
732
846
|
}
|
|
733
847
|
langchainMessages.push(new HumanMessage({ content: 'Continue where you left off. Complete the remaining steps of the objective.' }))
|
|
734
848
|
lastSegment = ''
|
|
849
|
+
} else if (shouldContinue === 'required_tool') {
|
|
850
|
+
if (fullText.trim()) {
|
|
851
|
+
langchainMessages.push(new AIMessage({ content: fullText }))
|
|
852
|
+
}
|
|
853
|
+
langchainMessages.push(new HumanMessage({
|
|
854
|
+
content: `You have not yet completed the required explicit tool step(s): ${requiredToolReminderNames.join(', ')}. Use those enabled tools now before declaring success. Do not replace ask_human with a plain-text request, and do not replace email delivery with browser work or prose.`,
|
|
855
|
+
}))
|
|
856
|
+
lastSegment = ''
|
|
735
857
|
} else if (shouldContinue === 'transient') {
|
|
736
858
|
// Short delay before retrying transient errors (API timeout, rate limit, etc.)
|
|
737
859
|
await new Promise((r) => setTimeout(r, 2000 * transientRetryCount))
|
|
@@ -741,6 +863,19 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
741
863
|
const errMsg = timedOut
|
|
742
864
|
? 'Ongoing loop stopped after reaching the configured runtime limit.'
|
|
743
865
|
: err instanceof Error ? err.message : String(err)
|
|
866
|
+
const heartbeatEligible = runtime.loopMode === 'ongoing' || session.heartbeatEnabled === true || agentHeartbeatEnabled
|
|
867
|
+
const budgetLimited = timedOut || /recursion limit|maximum recursion/i.test(errMsg)
|
|
868
|
+
if (heartbeatEligible && budgetLimited) {
|
|
869
|
+
enqueueSystemEvent(
|
|
870
|
+
session.id,
|
|
871
|
+
'[Loop Budget Reached] The previous autonomous run stopped after hitting its loop budget. On the next heartbeat, resume carefully from the current state, verify completed work before repeating it, and focus only on the remaining objective.',
|
|
872
|
+
'loop_budget_reached',
|
|
873
|
+
)
|
|
874
|
+
logExecution(session.id, 'decision', 'Queued a deferred resume cue for the next heartbeat after loop budget exhaustion.', {
|
|
875
|
+
agentId: session.agentId,
|
|
876
|
+
detail: { timedOut, heartbeatEligible },
|
|
877
|
+
})
|
|
878
|
+
}
|
|
744
879
|
logExecution(session.id, 'error', errMsg, { agentId: session.agentId, detail: { timedOut } })
|
|
745
880
|
write(`data: ${JSON.stringify({ t: 'err', text: errMsg })}\n\n`)
|
|
746
881
|
} finally {
|
|
@@ -43,7 +43,6 @@ export function parseMentionedAgentId(
|
|
|
43
43
|
agents: Record<string, Agent>,
|
|
44
44
|
): string | null {
|
|
45
45
|
const mentionRegex = /(?:^|[\s(])@([a-zA-Z0-9._-]+)/g
|
|
46
|
-
const agentList = Object.values(agents)
|
|
47
46
|
let match: RegExpExecArray | null
|
|
48
47
|
|
|
49
48
|
while ((match = mentionRegex.exec(description)) !== null) {
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
const PLUGIN_ALIAS_GROUPS: string[][] = [
|
|
2
|
-
['shell', 'execute_command', 'process_tool'
|
|
2
|
+
['shell', 'execute_command', 'process_tool'],
|
|
3
3
|
['files', 'read_file', 'write_file', 'list_files', 'copy_file', 'move_file', 'delete_file', 'send_file'],
|
|
4
4
|
['edit_file'],
|
|
5
5
|
['web', 'web_search', 'web_fetch'],
|
|
6
6
|
['browser', 'openclaw_browser'],
|
|
7
7
|
['delegate', 'claude_code', 'codex_cli', 'opencode_cli', 'gemini_cli', 'delegate_to_claude_code', 'delegate_to_codex_cli', 'delegate_to_opencode_cli', 'delegate_to_gemini_cli'],
|
|
8
|
-
['manage_platform'
|
|
8
|
+
['manage_platform'],
|
|
9
|
+
['manage_agents'],
|
|
10
|
+
['manage_tasks'],
|
|
11
|
+
['manage_schedules'],
|
|
12
|
+
['manage_skills'],
|
|
13
|
+
['manage_documents'],
|
|
14
|
+
['manage_webhooks'],
|
|
15
|
+
['manage_secrets'],
|
|
9
16
|
['manage_connectors', 'connectors', 'connector_message_tool'],
|
|
10
17
|
['manage_chatrooms', 'chatroom'],
|
|
11
18
|
['spawn_subagent', 'subagent', 'delegate_to_agent'],
|
|
@@ -32,6 +39,21 @@ const PLUGIN_ALIAS_GROUPS: string[][] = [
|
|
|
32
39
|
['crawl', 'site_crawler'],
|
|
33
40
|
]
|
|
34
41
|
|
|
42
|
+
const PLUGIN_IMPLICATIONS: Record<string, string[]> = {
|
|
43
|
+
shell: ['process'],
|
|
44
|
+
manage_platform: [
|
|
45
|
+
'manage_agents',
|
|
46
|
+
'manage_tasks',
|
|
47
|
+
'manage_schedules',
|
|
48
|
+
'manage_skills',
|
|
49
|
+
'manage_documents',
|
|
50
|
+
'manage_webhooks',
|
|
51
|
+
'manage_connectors',
|
|
52
|
+
'manage_sessions',
|
|
53
|
+
'manage_secrets',
|
|
54
|
+
],
|
|
55
|
+
}
|
|
56
|
+
|
|
35
57
|
const PLUGIN_CANONICAL_MAP = (() => {
|
|
36
58
|
const map = new Map<string, string>()
|
|
37
59
|
for (const group of PLUGIN_ALIAS_GROUPS) {
|
|
@@ -85,13 +107,22 @@ export function expandPluginIds(values: string[] | null | undefined): string[] {
|
|
|
85
107
|
while (queue.length > 0) {
|
|
86
108
|
const next = queue.shift()!
|
|
87
109
|
const normalized = normalizePluginId(next)
|
|
110
|
+
const canonical = canonicalizePluginId(next)
|
|
88
111
|
const aliases = PLUGIN_ALIAS_MAP.get(normalized)
|
|
89
|
-
const key = aliases ? normalized : next
|
|
112
|
+
const key = aliases ? normalized : (canonical || next)
|
|
90
113
|
if (expanded.has(key)) continue
|
|
91
114
|
expanded.add(key)
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
115
|
+
if (aliases) {
|
|
116
|
+
for (const alias of aliases) {
|
|
117
|
+
if (!expanded.has(alias)) queue.push(alias)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const implicationSources = [key, normalized, normalizePluginId(canonical)]
|
|
121
|
+
for (const source of implicationSources) {
|
|
122
|
+
if (!source) continue
|
|
123
|
+
for (const implied of PLUGIN_IMPLICATIONS[source] || []) {
|
|
124
|
+
if (!expanded.has(implied)) queue.push(implied)
|
|
125
|
+
}
|
|
95
126
|
}
|
|
96
127
|
}
|
|
97
128
|
|
|
@@ -56,7 +56,7 @@ const TOOL_DESCRIPTORS: Record<string, ToolDescriptor> = {
|
|
|
56
56
|
opencode_cli: { categories: ['delegation', 'execution'], concreteTools: ['delegate_to_opencode_cli'] },
|
|
57
57
|
gemini_cli: { categories: ['delegation', 'execution'], concreteTools: ['delegate_to_gemini_cli'] },
|
|
58
58
|
memory: { categories: ['memory'], concreteTools: ['memory', 'memory_tool', 'context_status', 'context_summarize'] },
|
|
59
|
-
sandbox: { categories: ['execution', 'filesystem'], concreteTools: ['sandbox', 'sandbox_exec', 'sandbox_list_runtimes'
|
|
59
|
+
sandbox: { categories: ['execution', 'filesystem'], concreteTools: ['sandbox', 'sandbox_exec', 'sandbox_list_runtimes'] },
|
|
60
60
|
git: { categories: ['execution', 'filesystem'], concreteTools: ['git'] },
|
|
61
61
|
http_request: { categories: ['network'], concreteTools: ['http_request'] },
|
|
62
62
|
canvas: { categories: ['filesystem'], concreteTools: ['canvas'] },
|