@swarmclawai/swarmclaw 1.2.5 → 1.2.8
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 +24 -17
- package/next.config.ts +1 -0
- package/package.json +3 -2
- package/scripts/easy-setup.mjs +1 -1
- package/scripts/postinstall.mjs +1 -1
- package/skills/swarmclaw.md +115 -0
- package/skills/tools/browser.md +131 -0
- package/skills/tools/execute.md +98 -0
- package/skills/tools/files.md +98 -0
- package/skills/tools/memory.md +104 -0
- package/skills/tools/platform.md +144 -0
- package/skills/tools/skills.md +83 -0
- package/src/app/api/chats/[id]/messages/route.ts +23 -19
- package/src/app/api/chats/messages-route.test.ts +105 -51
- package/src/app/api/mcp-servers/[id]/test/route.ts +3 -2
- package/src/app/api/openclaw/deploy/route.ts +2 -0
- package/src/app/api/setup/check-provider/route.ts +10 -2
- package/src/app/api/setup/doctor/route.ts +4 -4
- package/src/components/agents/agent-chat-list.tsx +23 -1
- package/src/components/agents/inspector-panel.tsx +165 -48
- package/src/components/chat/chat-area.tsx +38 -9
- package/src/components/chat/message-list.tsx +33 -19
- package/src/components/gateways/gateway-sheet.tsx +5 -2
- package/src/lib/agent-execute-defaults.test.ts +24 -0
- package/src/lib/agent-execute-defaults.ts +62 -0
- package/src/lib/chat/queued-message-queue.test.ts +134 -1
- package/src/lib/chat/queued-message-queue.ts +77 -2
- package/src/lib/providers/index.test.ts +108 -0
- package/src/lib/providers/index.ts +38 -15
- package/src/lib/server/agents/agent-service.ts +5 -0
- package/src/lib/server/builtin-extensions.ts +1 -0
- package/src/lib/server/chat-execution/chat-execution-advanced.test.ts +1 -1
- package/src/lib/server/chat-execution/chat-execution-tool-events.test.ts +1 -0
- package/src/lib/server/chat-execution/chat-execution-utils.ts +2 -2
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +79 -42
- package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -0
- package/src/lib/server/chat-execution/continuation-evaluator.ts +8 -0
- package/src/lib/server/chat-execution/memory-mutation-tools.ts +1 -1
- package/src/lib/server/chat-execution/message-classifier.ts +11 -1
- package/src/lib/server/chat-execution/prompt-builder.test.ts +28 -0
- package/src/lib/server/chat-execution/prompt-builder.ts +14 -1
- package/src/lib/server/chat-execution/prompt-mode.test.ts +24 -0
- package/src/lib/server/chat-execution/prompt-mode.ts +5 -1
- package/src/lib/server/chat-execution/stream-agent-chat.test.ts +6 -4
- package/src/lib/server/chat-execution/stream-agent-chat.ts +45 -16
- package/src/lib/server/chatrooms/chatroom-routing.test.ts +4 -0
- package/src/lib/server/connectors/discord.ts +2 -2
- package/src/lib/server/connectors/matrix.ts +3 -2
- package/src/lib/server/connectors/signal.ts +5 -4
- package/src/lib/server/connectors/slack.ts +10 -9
- package/src/lib/server/connectors/teams.ts +3 -2
- package/src/lib/server/connectors/telegram.ts +4 -4
- package/src/lib/server/connectors/whatsapp.ts +2 -2
- package/src/lib/server/daemon/controller.ts +7 -0
- package/src/lib/server/gateways/gateway-profile-service.ts +19 -1
- package/src/lib/server/messages/message-repository.test.ts +70 -0
- package/src/lib/server/messages/message-repository.ts +11 -6
- package/src/lib/server/openclaw/deploy.ts +32 -2
- package/src/lib/server/plugins-advanced.test.ts +1 -2
- package/src/lib/server/provider-health.ts +1 -1
- package/src/lib/server/runtime/process-manager.ts +13 -9
- package/src/lib/server/runtime/session-run-manager/queries.ts +15 -0
- package/src/lib/server/runtime/session-run-manager.test.ts +58 -0
- package/src/lib/server/sandbox/session-runtime.test.ts +18 -1
- package/src/lib/server/sandbox/session-runtime.ts +40 -28
- package/src/lib/server/session-tools/autonomy-tools.test.ts +7 -9
- package/src/lib/server/session-tools/context.ts +1 -1
- package/src/lib/server/session-tools/credential-env.ts +109 -0
- package/src/lib/server/session-tools/crud.ts +3 -3
- package/src/lib/server/session-tools/edit_file.ts +3 -2
- package/src/lib/server/session-tools/execute.test.ts +58 -0
- package/src/lib/server/session-tools/execute.ts +334 -0
- package/src/lib/server/session-tools/files-tool.ts +635 -0
- package/src/lib/server/session-tools/index.ts +14 -4
- package/src/lib/server/session-tools/memory-tool.ts +242 -0
- package/src/lib/server/session-tools/memory.ts +1 -1
- package/src/lib/server/session-tools/openclaw-nodes.ts +3 -2
- package/src/lib/server/session-tools/openclaw-workspace.ts +3 -2
- package/src/lib/server/session-tools/platform-tool.ts +617 -0
- package/src/lib/server/session-tools/session-info.ts +3 -2
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +3 -4
- package/src/lib/server/session-tools/shell.ts +7 -122
- package/src/lib/server/session-tools/skills-tool.ts +396 -0
- package/src/lib/server/session-tools/web.ts +2 -2
- package/src/lib/server/storage-normalization.ts +2 -0
- package/src/lib/server/tool-aliases.ts +2 -1
- package/src/lib/server/tool-capability-policy-advanced.test.ts +9 -2
- package/src/lib/server/tool-capability-policy.test.ts +2 -1
- package/src/lib/server/tool-capability-policy.ts +60 -33
- package/src/lib/server/tool-planning.ts +11 -0
- package/src/lib/setup-defaults.ts +5 -0
- package/src/lib/tool-definitions.ts +1 -0
- package/src/lib/validation/schemas.test.ts +16 -0
- package/src/lib/validation/schemas.ts +16 -0
- package/src/stores/use-chat-store.test.ts +231 -0
- package/src/stores/use-chat-store.ts +62 -13
- package/src/types/agent.ts +348 -0
- package/src/types/app-settings.ts +175 -0
- package/src/types/approval.ts +27 -0
- package/src/types/connector.ts +187 -0
- package/src/types/extension.ts +386 -0
- package/src/types/index.ts +16 -3555
- package/src/types/message.ts +57 -0
- package/src/types/misc.ts +739 -0
- package/src/types/mission.ts +185 -0
- package/src/types/protocol.ts +422 -0
- package/src/types/provider.ts +52 -0
- package/src/types/run.ts +183 -0
- package/src/types/schedule.ts +59 -0
- package/src/types/session.ts +265 -0
- package/src/types/skill.ts +157 -0
- package/src/types/task.ts +140 -0
- package/src/types/working-state.ts +211 -0
- package/src/views/settings/section-heartbeat.tsx +2 -2
- package/src/lib/server/session-tools/sandbox.ts +0 -281
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import { describe, it } from 'node:test'
|
|
3
3
|
import {
|
|
4
|
+
buildQueuedTranscriptMessages,
|
|
4
5
|
createOptimisticQueuedMessage,
|
|
5
6
|
clearQueuedMessagesForSession,
|
|
6
7
|
listQueuedMessagesForSession,
|
|
8
|
+
mergeQueuedTranscriptMessages,
|
|
7
9
|
removeQueuedMessageById,
|
|
8
10
|
replaceQueuedMessagesForSession,
|
|
9
11
|
snapshotToQueuedMessages,
|
|
@@ -82,12 +84,22 @@ describe('queued-message-queue', () => {
|
|
|
82
84
|
const queued = snapshotToQueuedMessages({
|
|
83
85
|
sessionId: 'session-a',
|
|
84
86
|
activeRunId: 'run-active',
|
|
87
|
+
activeTurn: {
|
|
88
|
+
runId: 'run-active',
|
|
89
|
+
sessionId: 'session-a',
|
|
90
|
+
text: 'sending now',
|
|
91
|
+
queuedAt: 4,
|
|
92
|
+
position: 0,
|
|
93
|
+
},
|
|
85
94
|
queueLength: 1,
|
|
86
95
|
items: [
|
|
87
96
|
{ runId: 'run-queued', sessionId: 'session-a', text: 'queued', queuedAt: 5, position: 1 },
|
|
88
97
|
],
|
|
89
98
|
})
|
|
90
|
-
assert.deepEqual(
|
|
99
|
+
assert.deepEqual(
|
|
100
|
+
queued.map((item) => [item.runId, item.sending === true]),
|
|
101
|
+
[['run-active', true], ['run-queued', false]],
|
|
102
|
+
)
|
|
91
103
|
})
|
|
92
104
|
|
|
93
105
|
it('preserves attachment and reply metadata from queue snapshots', () => {
|
|
@@ -123,6 +135,27 @@ describe('queued-message-queue', () => {
|
|
|
123
135
|
})
|
|
124
136
|
})
|
|
125
137
|
|
|
138
|
+
it('deduplicates an active turn when the snapshot also contains it in the queued items', () => {
|
|
139
|
+
const queued = snapshotToQueuedMessages({
|
|
140
|
+
sessionId: 'session-a',
|
|
141
|
+
activeRunId: 'run-active',
|
|
142
|
+
activeTurn: {
|
|
143
|
+
runId: 'run-active',
|
|
144
|
+
sessionId: 'session-a',
|
|
145
|
+
text: 'already running',
|
|
146
|
+
queuedAt: 6,
|
|
147
|
+
position: 0,
|
|
148
|
+
},
|
|
149
|
+
queueLength: 1,
|
|
150
|
+
items: [
|
|
151
|
+
{ runId: 'run-active', sessionId: 'session-a', text: 'already running', queuedAt: 6, position: 1 },
|
|
152
|
+
],
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
assert.deepEqual(queued.map((item) => item.runId), ['run-active'])
|
|
156
|
+
assert.equal(queued[0]?.sending, true)
|
|
157
|
+
})
|
|
158
|
+
|
|
126
159
|
it('sorts queued messages by position and queued time within a session', () => {
|
|
127
160
|
const unsorted: QueuedSessionMessage[] = [
|
|
128
161
|
{ runId: 'q4', sessionId: 'session-a', text: 'later', queuedAt: 9, position: 2 },
|
|
@@ -135,4 +168,104 @@ describe('queued-message-queue', () => {
|
|
|
135
168
|
['q5', 'q6', 'q4'],
|
|
136
169
|
)
|
|
137
170
|
})
|
|
171
|
+
|
|
172
|
+
it('builds transcript-ready user messages from sending queued turns', () => {
|
|
173
|
+
const transcript = buildQueuedTranscriptMessages([
|
|
174
|
+
{ runId: 'q1', sessionId: 'session-a', text: 'sending row', queuedAt: 20, position: 0, sending: true },
|
|
175
|
+
{ runId: 'q2', sessionId: 'session-a', text: 'pending row', queuedAt: 21, position: 1 },
|
|
176
|
+
{ runId: 'q3', sessionId: 'session-b', text: 'other session', queuedAt: 22, position: 0, sending: true },
|
|
177
|
+
], 'session-a')
|
|
178
|
+
|
|
179
|
+
assert.deepEqual(transcript, [
|
|
180
|
+
{
|
|
181
|
+
role: 'user',
|
|
182
|
+
text: 'sending row',
|
|
183
|
+
time: 20,
|
|
184
|
+
kind: 'chat',
|
|
185
|
+
clientRenderId: 'queued:q1',
|
|
186
|
+
imagePath: undefined,
|
|
187
|
+
imageUrl: undefined,
|
|
188
|
+
attachedFiles: undefined,
|
|
189
|
+
replyToId: undefined,
|
|
190
|
+
runId: 'q1',
|
|
191
|
+
},
|
|
192
|
+
])
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('merges sending queued turns into the transcript ahead of later assistant output', () => {
|
|
196
|
+
const merged = mergeQueuedTranscriptMessages([
|
|
197
|
+
{ role: 'assistant', text: 'Thinking...', time: 25, streaming: true, runId: 'run-active' },
|
|
198
|
+
], [
|
|
199
|
+
{ runId: 'run-active', sessionId: 'session-a', text: 'queued first', queuedAt: 20, position: 0, sending: true },
|
|
200
|
+
], 'session-a')
|
|
201
|
+
|
|
202
|
+
assert.deepEqual(merged.map((message) => [message.role, message.text, message.runId]), [
|
|
203
|
+
['user', 'queued first', 'run-active'],
|
|
204
|
+
['assistant', 'Thinking...', 'run-active'],
|
|
205
|
+
])
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('preserves existing sending items when replacing queue for a session', () => {
|
|
209
|
+
const queueWithSending: QueuedSessionMessage[] = [
|
|
210
|
+
{ runId: 'sending-1', sessionId: 'session-a', text: 'already sending', queuedAt: 1, position: 0, sending: true },
|
|
211
|
+
{ runId: 'q3', sessionId: 'session-a', text: 'queued', queuedAt: 2, position: 1 },
|
|
212
|
+
{ runId: 'q2', sessionId: 'session-b', text: 'other', queuedAt: 3, position: 1 },
|
|
213
|
+
]
|
|
214
|
+
const replaced = replaceQueuedMessagesForSession(queueWithSending, 'session-a', [
|
|
215
|
+
{ runId: 'q4', sessionId: 'session-a', text: 'new queued', queuedAt: 4, position: 1 },
|
|
216
|
+
], { activeRunId: null })
|
|
217
|
+
|
|
218
|
+
const forSession = listQueuedMessagesForSession(replaced, 'session-a')
|
|
219
|
+
assert.deepEqual(
|
|
220
|
+
forSession.map((item) => [item.runId, item.sending === true]),
|
|
221
|
+
[['sending-1', true], ['q4', false]],
|
|
222
|
+
)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('deduplicates sending items that appear in nextItems', () => {
|
|
226
|
+
const queueWithSending: QueuedSessionMessage[] = [
|
|
227
|
+
{ runId: 'run-active', sessionId: 'session-a', text: 'sending', queuedAt: 1, position: 0, sending: true },
|
|
228
|
+
]
|
|
229
|
+
const replaced = replaceQueuedMessagesForSession(queueWithSending, 'session-a', [
|
|
230
|
+
{ runId: 'run-active', sessionId: 'session-a', text: 'sending', queuedAt: 1, position: 0, sending: true },
|
|
231
|
+
{ runId: 'q5', sessionId: 'session-a', text: 'next', queuedAt: 2, position: 1 },
|
|
232
|
+
], { activeRunId: 'run-active' })
|
|
233
|
+
|
|
234
|
+
const forSession = listQueuedMessagesForSession(replaced, 'session-a')
|
|
235
|
+
assert.deepEqual(
|
|
236
|
+
forSession.map((item) => item.runId),
|
|
237
|
+
['run-active', 'q5'],
|
|
238
|
+
)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('inserts sending messages after last persisted message, not by timestamp', () => {
|
|
242
|
+
const merged = mergeQueuedTranscriptMessages([
|
|
243
|
+
{ role: 'user', text: 'First', time: 100 },
|
|
244
|
+
{ role: 'assistant', text: 'Reply', time: 200 },
|
|
245
|
+
{ role: 'user', text: 'Second', time: 300 },
|
|
246
|
+
{ role: 'assistant', text: 'Reply 2', time: 400 },
|
|
247
|
+
], [
|
|
248
|
+
// queuedAt is earlier than the last persisted message
|
|
249
|
+
{ runId: 'run-late', sessionId: 'session-a', text: 'queued early', queuedAt: 150, position: 0, sending: true },
|
|
250
|
+
], 'session-a')
|
|
251
|
+
|
|
252
|
+
// Should appear at the END, not spliced into the middle at time=150
|
|
253
|
+
assert.deepEqual(merged.map((msg) => msg.text), [
|
|
254
|
+
'First', 'Reply', 'Second', 'Reply 2', 'queued early',
|
|
255
|
+
])
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('skips a sending queued turn once the persisted user message is already present', () => {
|
|
259
|
+
const merged = mergeQueuedTranscriptMessages([
|
|
260
|
+
{ role: 'user', text: 'queued first', time: 20, runId: 'run-active' },
|
|
261
|
+
{ role: 'assistant', text: 'Thinking...', time: 25, streaming: true, runId: 'run-active' },
|
|
262
|
+
], [
|
|
263
|
+
{ runId: 'run-active', sessionId: 'session-a', text: 'queued first', queuedAt: 20, position: 0, sending: true },
|
|
264
|
+
], 'session-a')
|
|
265
|
+
|
|
266
|
+
assert.deepEqual(merged.map((message) => [message.role, message.text, message.runId]), [
|
|
267
|
+
['user', 'queued first', 'run-active'],
|
|
268
|
+
['assistant', 'Thinking...', 'run-active'],
|
|
269
|
+
])
|
|
270
|
+
})
|
|
138
271
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SessionQueueSnapshot, SessionQueuedTurn } from '@/types'
|
|
1
|
+
import type { Message, SessionQueueSnapshot, SessionQueuedTurn } from '@/types'
|
|
2
2
|
|
|
3
3
|
export interface QueuedSessionMessage extends SessionQueuedTurn {
|
|
4
4
|
optimistic?: boolean
|
|
@@ -38,7 +38,23 @@ export function createOptimisticQueuedMessage(
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export function snapshotToQueuedMessages(snapshot: SessionQueueSnapshot): QueuedSessionMessage[] {
|
|
41
|
-
|
|
41
|
+
const activeRunId = typeof snapshot.activeRunId === 'string' && snapshot.activeRunId.trim()
|
|
42
|
+
? snapshot.activeRunId
|
|
43
|
+
: null
|
|
44
|
+
const nextItems: QueuedSessionMessage[] = []
|
|
45
|
+
if (snapshot.activeTurn && activeRunId && snapshot.activeTurn.runId === activeRunId) {
|
|
46
|
+
nextItems.push({
|
|
47
|
+
...snapshot.activeTurn,
|
|
48
|
+
sending: true,
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
const seenRunIds = new Set(nextItems.map((item) => item.runId))
|
|
52
|
+
for (const item of snapshot.items) {
|
|
53
|
+
if (seenRunIds.has(item.runId)) continue
|
|
54
|
+
nextItems.push({ ...item })
|
|
55
|
+
seenRunIds.add(item.runId)
|
|
56
|
+
}
|
|
57
|
+
return nextItems
|
|
42
58
|
}
|
|
43
59
|
|
|
44
60
|
interface ReplaceQueuedMessagesOptions {
|
|
@@ -60,11 +76,17 @@ export function replaceQueuedMessagesForSession(
|
|
|
60
76
|
const activeRunId = typeof options.activeRunId === 'string' && options.activeRunId.trim()
|
|
61
77
|
? options.activeRunId
|
|
62
78
|
: null
|
|
79
|
+
// Preserve existing "sending" items not covered by the new snapshot —
|
|
80
|
+
// they'll be cleaned up later by setMessages or the timeout.
|
|
81
|
+
const existingSending = queue.filter((item) =>
|
|
82
|
+
item.sessionId === sessionId && item.sending && !nextRunIds.has(item.runId),
|
|
83
|
+
)
|
|
63
84
|
const consumed = previousForSession
|
|
64
85
|
.filter((item) => !item.optimistic && !nextRunIds.has(item.runId) && activeRunId === item.runId)
|
|
65
86
|
.map((item) => ({ ...item, sending: true }))
|
|
66
87
|
return [
|
|
67
88
|
...otherSessions,
|
|
89
|
+
...existingSending,
|
|
68
90
|
...consumed,
|
|
69
91
|
...nextItems,
|
|
70
92
|
]
|
|
@@ -80,6 +102,59 @@ export function listQueuedMessagesForSession(
|
|
|
80
102
|
.sort((left, right) => left.position - right.position || left.queuedAt - right.queuedAt)
|
|
81
103
|
}
|
|
82
104
|
|
|
105
|
+
export function buildQueuedTranscriptMessages(
|
|
106
|
+
queue: QueuedSessionMessage[],
|
|
107
|
+
sessionId: string | null | undefined,
|
|
108
|
+
): Message[] {
|
|
109
|
+
return listQueuedMessagesForSession(queue, sessionId)
|
|
110
|
+
.filter((item) => item.sending === true)
|
|
111
|
+
.map((item) => ({
|
|
112
|
+
role: 'user',
|
|
113
|
+
text: item.text,
|
|
114
|
+
time: item.queuedAt,
|
|
115
|
+
kind: 'chat',
|
|
116
|
+
clientRenderId: `queued:${item.runId}`,
|
|
117
|
+
imagePath: item.imagePath,
|
|
118
|
+
imageUrl: item.imageUrl,
|
|
119
|
+
attachedFiles: item.attachedFiles,
|
|
120
|
+
replyToId: item.replyToId,
|
|
121
|
+
runId: item.runId,
|
|
122
|
+
}))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function mergeQueuedTranscriptMessages(
|
|
126
|
+
messages: Message[],
|
|
127
|
+
queue: QueuedSessionMessage[],
|
|
128
|
+
sessionId: string | null | undefined,
|
|
129
|
+
): Message[] {
|
|
130
|
+
const queuedTranscript = buildQueuedTranscriptMessages(queue, sessionId)
|
|
131
|
+
if (queuedTranscript.length === 0) return messages
|
|
132
|
+
const merged = [...messages]
|
|
133
|
+
for (const queuedMessage of queuedTranscript) {
|
|
134
|
+
const queuedRunId = typeof queuedMessage.runId === 'string' && queuedMessage.runId.trim()
|
|
135
|
+
? queuedMessage.runId
|
|
136
|
+
: null
|
|
137
|
+
if (queuedRunId && merged.some((message) => message.role === 'user' && message.runId === queuedRunId)) {
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
// Place queued user message before its corresponding assistant response
|
|
141
|
+
// (same runId), otherwise append after the last persisted message.
|
|
142
|
+
const sameRunAssistantIndex = queuedRunId
|
|
143
|
+
? merged.findIndex((msg) => msg.role === 'assistant' && msg.runId === queuedRunId)
|
|
144
|
+
: -1
|
|
145
|
+
if (sameRunAssistantIndex >= 0) {
|
|
146
|
+
merged.splice(sameRunAssistantIndex, 0, queuedMessage)
|
|
147
|
+
} else {
|
|
148
|
+
const lastPersistedIndex = merged.findLastIndex(
|
|
149
|
+
(msg) => !msg.clientRenderId?.startsWith('queued:'),
|
|
150
|
+
)
|
|
151
|
+
const insertAt = lastPersistedIndex >= 0 ? lastPersistedIndex + 1 : merged.length
|
|
152
|
+
merged.splice(insertAt, 0, queuedMessage)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return merged
|
|
156
|
+
}
|
|
157
|
+
|
|
83
158
|
export function removeQueuedMessageById(
|
|
84
159
|
queue: QueuedSessionMessage[],
|
|
85
160
|
id: string,
|
|
@@ -76,3 +76,111 @@ test('builtin provider override records do not surface as custom providers', ()
|
|
|
76
76
|
|
|
77
77
|
assert.equal(output.openAiCount, 1)
|
|
78
78
|
})
|
|
79
|
+
|
|
80
|
+
test('custom provider resolution includes defaultEndpoint and optionalApiKey', () => {
|
|
81
|
+
const output = runWithTempDataDir<{
|
|
82
|
+
defaultEndpoint: string | null
|
|
83
|
+
optionalApiKey: boolean | null
|
|
84
|
+
requiresApiKey: boolean | null
|
|
85
|
+
}>(`
|
|
86
|
+
const storageModule = await import('@/lib/server/storage')
|
|
87
|
+
const storage = storageModule.default || storageModule
|
|
88
|
+
storage.saveProviderConfigs({
|
|
89
|
+
'custom-llama-server': {
|
|
90
|
+
id: 'custom-llama-server',
|
|
91
|
+
name: 'llama-server',
|
|
92
|
+
type: 'custom',
|
|
93
|
+
baseUrl: 'http://127.0.0.1:8080/v1',
|
|
94
|
+
models: ['my-model'],
|
|
95
|
+
requiresApiKey: false,
|
|
96
|
+
credentialId: null,
|
|
97
|
+
isEnabled: true,
|
|
98
|
+
createdAt: 1,
|
|
99
|
+
updatedAt: 1,
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const providersModule = await import('@/lib/providers/index')
|
|
104
|
+
const providers = providersModule.default || providersModule
|
|
105
|
+
const resolved = providers.getProvider('custom-llama-server')
|
|
106
|
+
|
|
107
|
+
console.log(JSON.stringify({
|
|
108
|
+
defaultEndpoint: resolved?.defaultEndpoint ?? null,
|
|
109
|
+
optionalApiKey: resolved?.optionalApiKey ?? null,
|
|
110
|
+
requiresApiKey: resolved?.requiresApiKey ?? null,
|
|
111
|
+
}))
|
|
112
|
+
`)
|
|
113
|
+
|
|
114
|
+
assert.equal(output.defaultEndpoint, 'http://127.0.0.1:8080/v1')
|
|
115
|
+
assert.equal(output.optionalApiKey, true)
|
|
116
|
+
assert.equal(output.requiresApiKey, false)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('custom provider with uuid-style ID resolves correctly', () => {
|
|
120
|
+
const output = runWithTempDataDir<{
|
|
121
|
+
resolvedName: string | null
|
|
122
|
+
hasHandler: boolean
|
|
123
|
+
}>(`
|
|
124
|
+
const storageModule = await import('@/lib/server/storage')
|
|
125
|
+
const storage = storageModule.default || storageModule
|
|
126
|
+
storage.saveProviderConfigs({
|
|
127
|
+
'custom-d20b934e': {
|
|
128
|
+
id: 'custom-d20b934e',
|
|
129
|
+
name: 'My llama-server',
|
|
130
|
+
type: 'custom',
|
|
131
|
+
baseUrl: 'http://127.0.0.1:8080/v1',
|
|
132
|
+
models: ['llama-3.1-8b'],
|
|
133
|
+
requiresApiKey: false,
|
|
134
|
+
credentialId: null,
|
|
135
|
+
isEnabled: true,
|
|
136
|
+
createdAt: 1,
|
|
137
|
+
updatedAt: 1,
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const providersModule = await import('@/lib/providers/index')
|
|
142
|
+
const providers = providersModule.default || providersModule
|
|
143
|
+
const resolved = providers.getProvider('custom-d20b934e')
|
|
144
|
+
|
|
145
|
+
console.log(JSON.stringify({
|
|
146
|
+
resolvedName: resolved?.name ?? null,
|
|
147
|
+
hasHandler: typeof resolved?.handler?.streamChat === 'function',
|
|
148
|
+
}))
|
|
149
|
+
`)
|
|
150
|
+
|
|
151
|
+
assert.equal(output.resolvedName, 'My llama-server')
|
|
152
|
+
assert.equal(output.hasHandler, true)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('disabled custom providers are not resolved by getProvider', () => {
|
|
156
|
+
const output = runWithTempDataDir<{
|
|
157
|
+
resolved: boolean
|
|
158
|
+
}>(`
|
|
159
|
+
const storageModule = await import('@/lib/server/storage')
|
|
160
|
+
const storage = storageModule.default || storageModule
|
|
161
|
+
storage.saveProviderConfigs({
|
|
162
|
+
'custom-disabled': {
|
|
163
|
+
id: 'custom-disabled',
|
|
164
|
+
name: 'Disabled Provider',
|
|
165
|
+
type: 'custom',
|
|
166
|
+
baseUrl: 'http://127.0.0.1:8080/v1',
|
|
167
|
+
models: ['test'],
|
|
168
|
+
requiresApiKey: false,
|
|
169
|
+
credentialId: null,
|
|
170
|
+
isEnabled: false,
|
|
171
|
+
createdAt: 1,
|
|
172
|
+
updatedAt: 1,
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const providersModule = await import('@/lib/providers/index')
|
|
177
|
+
const providers = providersModule.default || providersModule
|
|
178
|
+
const resolved = providers.getProvider('custom-disabled')
|
|
179
|
+
|
|
180
|
+
console.log(JSON.stringify({
|
|
181
|
+
resolved: resolved !== null,
|
|
182
|
+
}))
|
|
183
|
+
`)
|
|
184
|
+
|
|
185
|
+
assert.equal(output.resolved, false)
|
|
186
|
+
})
|
|
@@ -286,7 +286,8 @@ function getCustomProviders(): Record<string, CustomProviderConfig> {
|
|
|
286
286
|
return Object.fromEntries(
|
|
287
287
|
Object.entries(configs).filter(([, config]) => config?.type === 'custom'),
|
|
288
288
|
)
|
|
289
|
-
} catch {
|
|
289
|
+
} catch (err) {
|
|
290
|
+
log.warn(TAG, 'Failed to load custom providers from storage', errorMessage(err))
|
|
290
291
|
return {}
|
|
291
292
|
}
|
|
292
293
|
}
|
|
@@ -325,6 +326,7 @@ export function getProviderList(): ProviderInfo[] {
|
|
|
325
326
|
defaultModels: c.models,
|
|
326
327
|
supportsModelDiscovery: false,
|
|
327
328
|
requiresApiKey: c.requiresApiKey,
|
|
329
|
+
optionalApiKey: !c.requiresApiKey,
|
|
328
330
|
requiresEndpoint: false as boolean,
|
|
329
331
|
defaultEndpoint: c.baseUrl,
|
|
330
332
|
}))
|
|
@@ -348,26 +350,47 @@ export function getProviderList(): ProviderInfo[] {
|
|
|
348
350
|
return [...builtins, ...customs, ...extensionProviders]
|
|
349
351
|
}
|
|
350
352
|
|
|
353
|
+
function buildCustomProviderConfig(custom: CustomProviderConfig): BuiltinProviderConfig {
|
|
354
|
+
return {
|
|
355
|
+
id: custom.id as ProviderId,
|
|
356
|
+
name: custom.name,
|
|
357
|
+
models: custom.models,
|
|
358
|
+
requiresApiKey: custom.requiresApiKey,
|
|
359
|
+
optionalApiKey: !custom.requiresApiKey,
|
|
360
|
+
requiresEndpoint: false,
|
|
361
|
+
defaultEndpoint: custom.baseUrl,
|
|
362
|
+
handler: {
|
|
363
|
+
streamChat: async (opts) => {
|
|
364
|
+
const patchedSession = { ...opts.session, apiEndpoint: custom.baseUrl }
|
|
365
|
+
const { streamOpenAiChat } = await import('./openai')
|
|
366
|
+
return streamOpenAiChat({ ...opts, session: patchedSession })
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
351
372
|
export function getProvider(id: string): BuiltinProviderConfig | null {
|
|
352
373
|
if (PROVIDERS[id]) return PROVIDERS[id]
|
|
353
|
-
|
|
374
|
+
|
|
354
375
|
// Check custom providers
|
|
355
376
|
const customs = getCustomProviders()
|
|
356
377
|
const custom = customs[id]
|
|
357
378
|
if (custom?.isEnabled) {
|
|
358
|
-
return
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
379
|
+
return buildCustomProviderConfig(custom)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Fallback: direct single-item DB lookup for custom-* providers
|
|
383
|
+
if (id.startsWith('custom-') && !custom) {
|
|
384
|
+
try {
|
|
385
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
386
|
+
const { loadStoredItem } = require('@/lib/server/storage') as typeof import('@/lib/server/storage')
|
|
387
|
+
const directConfig = loadStoredItem('provider_configs', id) as CustomProviderConfig | null
|
|
388
|
+
if (directConfig?.type === 'custom' && directConfig.isEnabled) {
|
|
389
|
+
log.info(TAG, `Resolved custom provider '${id}' via direct DB lookup (batch load missed it)`)
|
|
390
|
+
return buildCustomProviderConfig(directConfig)
|
|
391
|
+
}
|
|
392
|
+
} catch (err) {
|
|
393
|
+
log.warn(TAG, `Direct DB lookup failed for provider '${id}'`, errorMessage(err))
|
|
371
394
|
}
|
|
372
395
|
}
|
|
373
396
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { genId } from '@/lib/id'
|
|
2
2
|
import { resolveAgentToolSelection } from '@/lib/agent-default-tools'
|
|
3
|
+
import { normalizeAgentExecuteConfig } from '@/lib/agent-execute-defaults'
|
|
3
4
|
import { normalizeAgentSandboxConfig } from '@/lib/agent-sandbox-defaults'
|
|
4
5
|
import { normalizeCapabilitySelection } from '@/lib/capability-selection'
|
|
5
6
|
import { normalizeProviderEndpoint } from '@/lib/openclaw/openclaw-endpoint'
|
|
@@ -180,6 +181,7 @@ export function createAgent(input: {
|
|
|
180
181
|
sessionDailyResetAt: typeof body.sessionDailyResetAt === 'string' ? body.sessionDailyResetAt : null,
|
|
181
182
|
sessionResetTimezone: typeof body.sessionResetTimezone === 'string' ? body.sessionResetTimezone : null,
|
|
182
183
|
sandboxConfig: normalizeAgentSandboxConfig(body.sandboxConfig),
|
|
184
|
+
executeConfig: body.executeConfig === null ? null : normalizeAgentExecuteConfig(body.executeConfig),
|
|
183
185
|
createdAt: now,
|
|
184
186
|
updatedAt: now,
|
|
185
187
|
}
|
|
@@ -225,6 +227,9 @@ export function updateAgent(agentId: string, body: Record<string, unknown>): Age
|
|
|
225
227
|
if (body.sandboxConfig !== undefined) {
|
|
226
228
|
agent.sandboxConfig = normalizeAgentSandboxConfig(body.sandboxConfig)
|
|
227
229
|
}
|
|
230
|
+
if (body.executeConfig !== undefined) {
|
|
231
|
+
agent.executeConfig = body.executeConfig === null ? null : normalizeAgentExecuteConfig(body.executeConfig)
|
|
232
|
+
}
|
|
228
233
|
if (
|
|
229
234
|
body.provider !== undefined
|
|
230
235
|
|| body.orchestratorEnabled !== undefined
|
|
@@ -470,7 +470,7 @@ describe('hasDirectLocalCodingTools', () => {
|
|
|
470
470
|
assert.equal(hasDirectLocalCodingTools({ tools: ['files'] }), true)
|
|
471
471
|
})
|
|
472
472
|
|
|
473
|
-
it('returns true for sandbox
|
|
473
|
+
it('returns true for legacy sandbox alias', () => {
|
|
474
474
|
assert.equal(hasDirectLocalCodingTools({ tools: ['sandbox'] }), true)
|
|
475
475
|
})
|
|
476
476
|
})
|
|
@@ -259,6 +259,7 @@ describe('hasDirectLocalCodingTools', () => {
|
|
|
259
259
|
it('treats shell and file tooling as local coding capability', () => {
|
|
260
260
|
assert.equal(hasDirectLocalCodingTools({ extensions: ['files'] }), true)
|
|
261
261
|
assert.equal(hasDirectLocalCodingTools({ extensions: ['shell'] }), true)
|
|
262
|
+
assert.equal(hasDirectLocalCodingTools({ extensions: ['execute'] }), true)
|
|
262
263
|
assert.equal(hasDirectLocalCodingTools({ extensions: ['edit_file'] }), true)
|
|
263
264
|
assert.equal(hasDirectLocalCodingTools({ extensions: ['delegate'] }), false)
|
|
264
265
|
})
|
|
@@ -324,8 +324,6 @@ export function requestedToolNamesFromMessage(message: string): string[] {
|
|
|
324
324
|
'wallet_tool',
|
|
325
325
|
'http_request',
|
|
326
326
|
'send_file',
|
|
327
|
-
'sandbox_exec',
|
|
328
|
-
'sandbox_list_runtimes',
|
|
329
327
|
'schedule_wake',
|
|
330
328
|
'spawn_subagent',
|
|
331
329
|
'ask_human',
|
|
@@ -338,6 +336,7 @@ export function requestedToolNamesFromMessage(message: string): string[] {
|
|
|
338
336
|
'browser',
|
|
339
337
|
'web',
|
|
340
338
|
'shell',
|
|
339
|
+
'execute',
|
|
341
340
|
'files',
|
|
342
341
|
'edit_file',
|
|
343
342
|
'canvas',
|
|
@@ -370,6 +369,7 @@ export function enabledDelegationTools(session: SessionWithTools): DelegateTool[
|
|
|
370
369
|
export function hasDirectLocalCodingTools(session: SessionWithTools): boolean {
|
|
371
370
|
return [
|
|
372
371
|
'shell',
|
|
372
|
+
'execute',
|
|
373
373
|
'execute_command',
|
|
374
374
|
'files',
|
|
375
375
|
'edit_file',
|