@swarmclawai/swarmclaw 1.2.6 → 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/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/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,
|
|
@@ -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',
|
|
@@ -76,6 +76,7 @@ import {
|
|
|
76
76
|
import { checkAgentBudgetLimits } from '@/lib/server/cost'
|
|
77
77
|
import {
|
|
78
78
|
classifyMessage,
|
|
79
|
+
type MessageClassification,
|
|
79
80
|
toMessageSemanticsSummary,
|
|
80
81
|
} from '@/lib/server/chat-execution/message-classifier'
|
|
81
82
|
import {
|
|
@@ -87,6 +88,7 @@ import {
|
|
|
87
88
|
} from '@/lib/server/chat-execution/chat-execution-utils'
|
|
88
89
|
import { loadEstopState } from '@/lib/server/runtime/estop'
|
|
89
90
|
import { buildToolSection, joinPromptSegments } from '@/lib/server/chat-execution/prompt-builder'
|
|
91
|
+
import { resolvePromptMode, type PromptMode } from '@/lib/server/chat-execution/prompt-mode'
|
|
90
92
|
import { isDirectConnectorSession } from '@/lib/server/connectors/session-kind'
|
|
91
93
|
import type { ExecuteChatTurnInput } from '@/lib/server/chat-execution/chat-execution'
|
|
92
94
|
|
|
@@ -320,13 +322,17 @@ function buildLightHeartbeatSystemPrompt(session: Session): string | undefined {
|
|
|
320
322
|
return parts.join('\n\n')
|
|
321
323
|
}
|
|
322
324
|
|
|
323
|
-
function buildAgentSystemPrompt(
|
|
325
|
+
function buildAgentSystemPrompt(
|
|
326
|
+
session: Session,
|
|
327
|
+
options?: { lightweightDirectChat?: boolean },
|
|
328
|
+
): string | undefined {
|
|
324
329
|
if (!session.agentId) return undefined
|
|
325
330
|
const agent = getAgent(session.agentId)
|
|
326
331
|
if (!agent) return undefined
|
|
327
332
|
|
|
328
333
|
const settings = loadSettings()
|
|
329
334
|
const allowSilentReplies = isDirectConnectorSession(session)
|
|
335
|
+
const lightweightDirectChat = options?.lightweightDirectChat === true
|
|
330
336
|
const parts: string[] = []
|
|
331
337
|
const enabledExtensions = listUniversalToolAccessExtensionIds(
|
|
332
338
|
getEnabledCapabilityIds(session).length > 0 ? getEnabledCapabilityIds(session) : getEnabledCapabilityIds(agent),
|
|
@@ -341,7 +347,7 @@ function buildAgentSystemPrompt(session: Session): string | undefined {
|
|
|
341
347
|
if (agent.description) identityLines.push(`Description: ${agent.description}`)
|
|
342
348
|
identityLines.push('I should always embody this identity. I am not just an "Assistant" — I am a persistent companion with my own soul and presence.')
|
|
343
349
|
parts.push(identityLines.join('\n'))
|
|
344
|
-
const continuityBlock = buildIdentityContinuityContext(session, agent)
|
|
350
|
+
const continuityBlock = lightweightDirectChat ? null : buildIdentityContinuityContext(session, agent)
|
|
345
351
|
if (continuityBlock) parts.push(continuityBlock)
|
|
346
352
|
|
|
347
353
|
const runtimeLines = [
|
|
@@ -358,50 +364,57 @@ function buildAgentSystemPrompt(session: Session): string | undefined {
|
|
|
358
364
|
if (agent.soul) parts.push(`## Soul\n${agent.soul}`)
|
|
359
365
|
if (agent.systemPrompt) parts.push(`## System Prompt\n${agent.systemPrompt}`)
|
|
360
366
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
367
|
+
if (!lightweightDirectChat) {
|
|
368
|
+
try {
|
|
369
|
+
const runtimeSkills = resolveRuntimeSkills({
|
|
370
|
+
cwd: session.cwd,
|
|
371
|
+
enabledExtensions,
|
|
372
|
+
agentId: agent.id,
|
|
373
|
+
sessionId: session.id,
|
|
374
|
+
userId: session.user,
|
|
375
|
+
agentSkillIds: agent.skillIds || [],
|
|
376
|
+
storedSkills: loadSkills(),
|
|
377
|
+
selectedSkillId: session.skillRuntimeState?.selectedSkillId || null,
|
|
378
|
+
})
|
|
379
|
+
parts.push(...buildRuntimeSkillPromptBlocks(runtimeSkills))
|
|
380
|
+
} catch {
|
|
381
|
+
// Runtime skills are non-critical during prompt assembly.
|
|
382
|
+
}
|
|
376
383
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
384
|
+
try {
|
|
385
|
+
const wsCtx = buildWorkspaceContext({ cwd: session.cwd })
|
|
386
|
+
if (wsCtx.block) parts.push(wsCtx.block)
|
|
387
|
+
} catch {
|
|
388
|
+
// Workspace context is non-critical.
|
|
389
|
+
}
|
|
382
390
|
}
|
|
383
391
|
|
|
384
392
|
const thinkingHint = [
|
|
385
393
|
'## Output Format',
|
|
386
394
|
'If your model supports internal reasoning/thinking, put all internal analysis inside <think>...</think> tags.',
|
|
387
395
|
'Your final response to the user should be clear and concise.',
|
|
396
|
+
...(lightweightDirectChat
|
|
397
|
+
? ['This is a lightweight direct chat turn. Reply naturally in 1-3 short sentences. Do not delegate, plan, or narrate tools unless the user adds a concrete task that needs that escalation.']
|
|
398
|
+
: []),
|
|
388
399
|
allowSilentReplies
|
|
389
400
|
? 'When you truly have nothing to say, respond with ONLY: NO_MESSAGE'
|
|
390
401
|
: 'For direct user chats, always send a visible reply. Never answer with NO_MESSAGE or HEARTBEAT_OK unless this is an explicit heartbeat poll.',
|
|
391
402
|
]
|
|
392
403
|
parts.push(thinkingHint.join('\n'))
|
|
393
404
|
|
|
394
|
-
if (
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
405
|
+
if (!lightweightDirectChat) {
|
|
406
|
+
if (enabledExtensions.length === 0) {
|
|
407
|
+
parts.push(buildNoToolsGuidance().join('\n'))
|
|
408
|
+
} else {
|
|
409
|
+
parts.push(buildEnabledToolsAutonomyGuidance().join('\n'))
|
|
410
|
+
}
|
|
411
|
+
const toolSectionLines = buildToolSection(enabledExtensions)
|
|
412
|
+
if (toolSectionLines.length > 0) parts.push(['## Tool Discipline', ...toolSectionLines].join('\n'))
|
|
413
|
+
const operatingGuidance = collectCapabilityOperatingGuidance(enabledExtensions)
|
|
414
|
+
if (operatingGuidance.length > 0) parts.push(['## Tool Guidance', ...operatingGuidance].join('\n'))
|
|
415
|
+
const capabilityLines = collectCapabilityDescriptions(enabledExtensions)
|
|
416
|
+
if (capabilityLines.length > 0) parts.push(['## Tool Capabilities', ...capabilityLines].join('\n'))
|
|
398
417
|
}
|
|
399
|
-
const toolSectionLines = buildToolSection(enabledExtensions)
|
|
400
|
-
if (toolSectionLines.length > 0) parts.push(['## Tool Discipline', ...toolSectionLines].join('\n'))
|
|
401
|
-
const operatingGuidance = collectCapabilityOperatingGuidance(enabledExtensions)
|
|
402
|
-
if (operatingGuidance.length > 0) parts.push(['## Tool Guidance', ...operatingGuidance].join('\n'))
|
|
403
|
-
const capabilityLines = collectCapabilityDescriptions(enabledExtensions)
|
|
404
|
-
if (capabilityLines.length > 0) parts.push(['## Tool Capabilities', ...capabilityLines].join('\n'))
|
|
405
418
|
|
|
406
419
|
parts.push([
|
|
407
420
|
'## Heartbeats',
|
|
@@ -470,6 +483,8 @@ export interface PreparedExecutableChatTurn {
|
|
|
470
483
|
runStartedAt: number
|
|
471
484
|
runMessageStartIndex: number
|
|
472
485
|
toolPolicy: ReturnType<typeof resolveSessionToolPolicy>
|
|
486
|
+
classification: MessageClassification | null
|
|
487
|
+
promptMode: PromptMode
|
|
473
488
|
}
|
|
474
489
|
|
|
475
490
|
export type PreparedChatTurn = PreparedBlockedChatTurn | PreparedExecutableChatTurn
|
|
@@ -620,6 +635,24 @@ export async function prepareChatTurn(input: ExecuteChatTurnInput): Promise<Prep
|
|
|
620
635
|
}
|
|
621
636
|
}
|
|
622
637
|
|
|
638
|
+
const turnHistory = getMessages(sessionId)
|
|
639
|
+
const classification = !internal
|
|
640
|
+
? await classifyMessage({
|
|
641
|
+
sessionId,
|
|
642
|
+
agentId: session.agentId || null,
|
|
643
|
+
message,
|
|
644
|
+
history: turnHistory,
|
|
645
|
+
}).catch(() => null as MessageClassification | null)
|
|
646
|
+
: null
|
|
647
|
+
const lightweightDirectChat = classification?.isLightweightDirectChat === true
|
|
648
|
+
&& !internal
|
|
649
|
+
&& source === 'chat'
|
|
650
|
+
&& !isDirectConnectorSession(sessionForRun)
|
|
651
|
+
const promptMode = resolvePromptMode(sessionForRun, { preferMinimalPrompt: lightweightDirectChat })
|
|
652
|
+
if (lightweightDirectChat && sessionForRun.thinkingLevel !== 'minimal') {
|
|
653
|
+
sessionForRun = { ...sessionForRun, thinkingLevel: 'minimal' }
|
|
654
|
+
}
|
|
655
|
+
|
|
623
656
|
if (isHeartbeatRun && input.modelOverride) {
|
|
624
657
|
sessionForRun = { ...sessionForRun, model: input.modelOverride }
|
|
625
658
|
}
|
|
@@ -632,7 +665,10 @@ export async function prepareChatTurn(input: ExecuteChatTurnInput): Promise<Prep
|
|
|
632
665
|
if (extensionsForRun.length > 0) {
|
|
633
666
|
const modelResolvePrompt = heartbeatLightContext
|
|
634
667
|
? (joinSystemPromptBlocks(buildLightHeartbeatSystemPrompt(sessionForRun), executionBriefContextBlock) || '')
|
|
635
|
-
: (joinSystemPromptBlocks(
|
|
668
|
+
: (joinSystemPromptBlocks(
|
|
669
|
+
buildAgentSystemPrompt(sessionForRun, { lightweightDirectChat }),
|
|
670
|
+
executionBriefContextBlock,
|
|
671
|
+
) || '')
|
|
636
672
|
const modelResolve = await runCapabilityBeforeModelResolve(
|
|
637
673
|
{
|
|
638
674
|
session: sessionForRun,
|
|
@@ -724,14 +760,7 @@ export async function prepareChatTurn(input: ExecuteChatTurnInput): Promise<Prep
|
|
|
724
760
|
if (shouldPersistUserMessage) {
|
|
725
761
|
const [linkAnalysis, semantics] = await Promise.all([
|
|
726
762
|
!internal ? runLinkUnderstanding(message) : Promise.resolve([]),
|
|
727
|
-
|
|
728
|
-
sessionId,
|
|
729
|
-
agentId: session.agentId || null,
|
|
730
|
-
message,
|
|
731
|
-
history: getMessages(sessionId),
|
|
732
|
-
})
|
|
733
|
-
.then((classification) => toMessageSemanticsSummary(classification))
|
|
734
|
-
.catch(() => undefined),
|
|
763
|
+
Promise.resolve(toMessageSemanticsSummary(classification)),
|
|
735
764
|
])
|
|
736
765
|
const guardedUserText = guardUntrustedText({
|
|
737
766
|
text: message,
|
|
@@ -745,6 +774,7 @@ export async function prepareChatTurn(input: ExecuteChatTurnInput): Promise<Prep
|
|
|
745
774
|
role: 'user',
|
|
746
775
|
text: guardedUserText,
|
|
747
776
|
time: Date.now(),
|
|
777
|
+
runId: lifecycleRunId,
|
|
748
778
|
imagePath: imagePath || undefined,
|
|
749
779
|
imageUrl: imageUrl || undefined,
|
|
750
780
|
attachedFiles: attachedFiles?.length ? attachedFiles : undefined,
|
|
@@ -805,7 +835,12 @@ export async function prepareChatTurn(input: ExecuteChatTurnInput): Promise<Prep
|
|
|
805
835
|
|
|
806
836
|
const systemPrompt = heartbeatLightContext
|
|
807
837
|
? joinSystemPromptBlocks(buildLightHeartbeatSystemPrompt(sessionForRun), executionBriefContextBlock)
|
|
808
|
-
: (hasExtensions
|
|
838
|
+
: (hasExtensions
|
|
839
|
+
? undefined
|
|
840
|
+
: joinSystemPromptBlocks(
|
|
841
|
+
buildAgentSystemPrompt(sessionForRun, { lightweightDirectChat }),
|
|
842
|
+
executionBriefContextBlock,
|
|
843
|
+
))
|
|
809
844
|
|
|
810
845
|
return {
|
|
811
846
|
kind: 'ready',
|
|
@@ -837,5 +872,7 @@ export async function prepareChatTurn(input: ExecuteChatTurnInput): Promise<Prep
|
|
|
837
872
|
runStartedAt,
|
|
838
873
|
runMessageStartIndex,
|
|
839
874
|
toolPolicy,
|
|
875
|
+
classification,
|
|
876
|
+
promptMode,
|
|
840
877
|
}
|
|
841
878
|
}
|
|
@@ -76,6 +76,8 @@ export async function executePreparedChatTurn(params: {
|
|
|
76
76
|
isAutoRunNoHistory,
|
|
77
77
|
executionBrief,
|
|
78
78
|
executionBriefContextBlock,
|
|
79
|
+
classification,
|
|
80
|
+
promptMode,
|
|
79
81
|
} = prepared
|
|
80
82
|
|
|
81
83
|
const emit = partialPersistence.emit
|
|
@@ -151,6 +153,8 @@ export async function executePreparedChatTurn(params: {
|
|
|
151
153
|
history: heartbeatHistory ?? applyContextClearBoundary(getSessionMessages(sessionId)),
|
|
152
154
|
signal: abortController.signal,
|
|
153
155
|
source,
|
|
156
|
+
classification,
|
|
157
|
+
promptMode,
|
|
154
158
|
})
|
|
155
159
|
fullResponse = result.finalResponse || result.fullText
|
|
156
160
|
} else {
|
|
@@ -102,6 +102,13 @@ function checkUnfinishedToolCallsPending(ctx: ContinuationContext): Continuation
|
|
|
102
102
|
return null
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
function checkLightweightDirectChat(ctx: ContinuationContext): ContinuationDecision | null {
|
|
106
|
+
if (ctx.classification?.isLightweightDirectChat !== true) return null
|
|
107
|
+
if (!ctx.state.fullText.trim()) return null
|
|
108
|
+
if (ctx.state.hasToolCalls || ctx.state.streamedToolEvents.length > 0) return null
|
|
109
|
+
return { type: false, requiredToolReminderNames: [] }
|
|
110
|
+
}
|
|
111
|
+
|
|
105
112
|
function checkLoopDetection(ctx: ContinuationContext): ContinuationDecision | null {
|
|
106
113
|
const isToolFrequency = (ctx.state.loopDetectionTriggered?.detector === 'tool_frequency') || ctx.state.toolFrequencyBlocked
|
|
107
114
|
if (!ctx.state.loopDetectionTriggered && !isToolFrequency) return null
|
|
@@ -412,6 +419,7 @@ export function evaluateContinuation(ctx: ContinuationContext): ContinuationDeci
|
|
|
412
419
|
const checks = [
|
|
413
420
|
checkUnfinishedToolCallsPending,
|
|
414
421
|
checkLoopDetection,
|
|
422
|
+
checkLightweightDirectChat,
|
|
415
423
|
checkCoordinatorDelegation,
|
|
416
424
|
checkExecutionContinuation,
|
|
417
425
|
checkRequiredTools,
|
|
@@ -112,7 +112,7 @@ export function shouldTerminateOnSuccessfulMemoryMutation(params: {
|
|
|
112
112
|
: exactToolName === 'memory_update'
|
|
113
113
|
? 'update'
|
|
114
114
|
: resolveToolAction(params.toolInput)
|
|
115
|
-
if (action !== 'store' && action !== 'update') return false
|
|
115
|
+
if (action !== 'store' && action !== 'update' && action !== 'write') return false
|
|
116
116
|
const output = extractSuggestions(params.toolOutput || '').clean.trim()
|
|
117
117
|
if (!output || /^error[:\s]/i.test(output)) return false
|
|
118
118
|
if (!/^(stored|updated) memory\b/i.test(output)) return false
|