@swarmclawai/swarmclaw 0.7.7 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -14
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +23 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +46 -3
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +257 -38
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +48 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +45 -3
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +289 -34
- package/src/components/tasks/task-board.tsx +410 -25
- package/src/components/tasks/task-card.tsx +66 -8
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +33 -0
- package/src/lib/server/capability-router.ts +80 -19
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +378 -73
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +461 -137
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +84 -47
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +247 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +20 -11
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +3 -2
- package/src/lib/server/orchestrator.ts +2 -0
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +211 -6
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +409 -2
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +527 -68
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +83 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +366 -54
- package/src/lib/server/session-tools/context.ts +17 -3
- package/src/lib/server/session-tools/crud.ts +484 -84
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +102 -10
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +554 -75
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
- package/src/lib/server/session-tools/web.ts +621 -70
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +437 -2
- package/src/lib/server/stream-agent-chat.ts +957 -79
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +271 -0
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- package/src/types/index.ts +249 -14
|
@@ -7,6 +7,7 @@ import { getScheduleSignatureKey } from '@/lib/schedule-dedupe'
|
|
|
7
7
|
import { enqueueSystemEvent } from './system-events'
|
|
8
8
|
import { requestHeartbeatNow } from './heartbeat-wake'
|
|
9
9
|
import { processDueWatchJobs } from './watch-jobs'
|
|
10
|
+
import { isAgentDisabled } from './agent-availability'
|
|
10
11
|
|
|
11
12
|
const TICK_INTERVAL = 60_000 // 60 seconds
|
|
12
13
|
let intervalId: ReturnType<typeof setInterval> | null = null
|
|
@@ -32,6 +33,11 @@ interface SchedulerScheduleLike {
|
|
|
32
33
|
runNumber?: number
|
|
33
34
|
createdInSessionId?: string | null
|
|
34
35
|
createdByAgentId?: string | null
|
|
36
|
+
followupConnectorId?: string | null
|
|
37
|
+
followupChannelId?: string | null
|
|
38
|
+
followupThreadId?: string | null
|
|
39
|
+
followupSenderId?: string | null
|
|
40
|
+
followupSenderName?: string | null
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
export function startScheduler() {
|
|
@@ -123,6 +129,16 @@ async function tick() {
|
|
|
123
129
|
})
|
|
124
130
|
continue
|
|
125
131
|
}
|
|
132
|
+
if (isAgentDisabled(agent)) {
|
|
133
|
+
console.warn(`[scheduler] Skipping schedule "${schedule.name}" (${schedule.id}) because agent ${schedule.agentId} is disabled`)
|
|
134
|
+
advanceSchedule(schedule)
|
|
135
|
+
saveSchedules(schedules)
|
|
136
|
+
pushMainLoopEventToMainSessions({
|
|
137
|
+
type: 'schedule_skipped',
|
|
138
|
+
text: `Schedule skipped: "${schedule.name}" (${schedule.id}) — agent ${schedule.agentId} is disabled.`,
|
|
139
|
+
})
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
126
142
|
|
|
127
143
|
console.log(`[scheduler] Firing schedule "${schedule.name}" (${schedule.id})`)
|
|
128
144
|
schedule.lastRunAt = now
|
|
@@ -185,6 +201,11 @@ async function tick() {
|
|
|
185
201
|
sourceScheduleKey: scheduleSignature || null,
|
|
186
202
|
createdInSessionId: schedule.createdInSessionId || null,
|
|
187
203
|
createdByAgentId: schedule.createdByAgentId || null,
|
|
204
|
+
followupConnectorId: schedule.followupConnectorId || null,
|
|
205
|
+
followupChannelId: schedule.followupChannelId || null,
|
|
206
|
+
followupThreadId: schedule.followupThreadId || null,
|
|
207
|
+
followupSenderId: schedule.followupSenderId || null,
|
|
208
|
+
followupSenderName: schedule.followupSenderName || null,
|
|
188
209
|
runNumber: schedule.runNumber,
|
|
189
210
|
}
|
|
190
211
|
schedule.linkedTaskId = taskId
|
|
@@ -204,6 +225,13 @@ async function tick() {
|
|
|
204
225
|
if (schedule.createdInSessionId) {
|
|
205
226
|
enqueueSystemEvent(schedule.createdInSessionId, `Schedule triggered: ${schedule.name}`)
|
|
206
227
|
}
|
|
207
|
-
requestHeartbeatNow({
|
|
228
|
+
requestHeartbeatNow({
|
|
229
|
+
agentId: schedule.agentId,
|
|
230
|
+
eventId: `${schedule.id}:${schedule.runNumber}`,
|
|
231
|
+
reason: 'schedule',
|
|
232
|
+
source: `schedule:${schedule.id}`,
|
|
233
|
+
resumeMessage: `Schedule triggered: ${schedule.name}`,
|
|
234
|
+
detail: `Run #${schedule.runNumber} queued task ${taskId}.`,
|
|
235
|
+
})
|
|
208
236
|
}
|
|
209
237
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { buildSessionNoteMessage } from './session-note'
|
|
5
|
+
|
|
6
|
+
test('buildSessionNoteMessage defaults to assistant/system note metadata', () => {
|
|
7
|
+
const result = buildSessionNoteMessage({
|
|
8
|
+
text: 'Live test passed',
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
assert.ok(result)
|
|
12
|
+
assert.equal(result?.role, 'assistant')
|
|
13
|
+
assert.equal(result?.kind, 'system')
|
|
14
|
+
assert.equal(result?.text, 'Live test passed')
|
|
15
|
+
assert.equal(typeof result?.time, 'number')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('buildSessionNoteMessage trims text and preserves explicit role/kind', () => {
|
|
19
|
+
const result = buildSessionNoteMessage({
|
|
20
|
+
text: ' Visible smoke report ',
|
|
21
|
+
role: 'user',
|
|
22
|
+
kind: 'chat',
|
|
23
|
+
time: 123,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
assert.deepEqual(result, {
|
|
27
|
+
role: 'user',
|
|
28
|
+
kind: 'chat',
|
|
29
|
+
text: 'Visible smoke report',
|
|
30
|
+
time: 123,
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('buildSessionNoteMessage returns null for empty text', () => {
|
|
35
|
+
assert.equal(buildSessionNoteMessage({ text: ' ' }), null)
|
|
36
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Message, MessageToolEvent } from '@/types'
|
|
2
|
+
import { loadSessions, saveSessions } from './storage'
|
|
3
|
+
import { notify } from './ws-hub'
|
|
4
|
+
|
|
5
|
+
export interface SessionNoteInput {
|
|
6
|
+
sessionId: string
|
|
7
|
+
text: string
|
|
8
|
+
role?: Message['role']
|
|
9
|
+
kind?: Message['kind']
|
|
10
|
+
toolEvents?: MessageToolEvent[]
|
|
11
|
+
time?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildSessionNoteMessage(input: Omit<SessionNoteInput, 'sessionId'>): Message | null {
|
|
15
|
+
const trimmed = String(input.text || '').trim()
|
|
16
|
+
if (!trimmed) return null
|
|
17
|
+
return {
|
|
18
|
+
role: input.role || 'assistant',
|
|
19
|
+
kind: input.kind || 'system',
|
|
20
|
+
text: trimmed,
|
|
21
|
+
time: typeof input.time === 'number' && Number.isFinite(input.time) ? input.time : Date.now(),
|
|
22
|
+
...(Array.isArray(input.toolEvents) && input.toolEvents.length ? { toolEvents: input.toolEvents } : {}),
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function appendSessionNote(input: SessionNoteInput): Message | null {
|
|
27
|
+
const sessions = loadSessions()
|
|
28
|
+
const session = sessions[input.sessionId]
|
|
29
|
+
if (!session) return null
|
|
30
|
+
if (!Array.isArray(session.messages)) session.messages = []
|
|
31
|
+
|
|
32
|
+
const next = buildSessionNoteMessage(input)
|
|
33
|
+
if (!next) return null
|
|
34
|
+
|
|
35
|
+
session.messages.push(next)
|
|
36
|
+
session.lastActiveAt = next.time
|
|
37
|
+
sessions[input.sessionId] = session
|
|
38
|
+
saveSessions(sessions)
|
|
39
|
+
notify('sessions')
|
|
40
|
+
notify(`messages:${input.sessionId}`)
|
|
41
|
+
return next
|
|
42
|
+
}
|
|
@@ -7,6 +7,7 @@ import { log } from './logger'
|
|
|
7
7
|
import { isInternalHeartbeatRun } from './heartbeat-source'
|
|
8
8
|
import { cleanupSessionBrowser } from './session-tools/web'
|
|
9
9
|
import { cancelDelegationJobsForParentSession } from './delegation-jobs'
|
|
10
|
+
import { handleMainLoopRunResult } from './main-agent-loop'
|
|
10
11
|
|
|
11
12
|
export type SessionRunStatus = 'queued' | 'running' | 'completed' | 'failed' | 'cancelled'
|
|
12
13
|
export type SessionQueueMode = 'followup' | 'steer' | 'collect'
|
|
@@ -218,7 +219,10 @@ export function cancelAllHeartbeatRuns(reason = 'Heartbeat disabled globally'):
|
|
|
218
219
|
async function drainExecution(executionKey: string): Promise<void> {
|
|
219
220
|
if (state.runningByExecution.has(executionKey)) return
|
|
220
221
|
const q = queueForExecution(executionKey)
|
|
221
|
-
|
|
222
|
+
// Priority: user (non-heartbeat) runs go first. If a heartbeat is queued
|
|
223
|
+
// behind a user run, the user run takes priority.
|
|
224
|
+
const userIdx = q.findIndex(e => !isInternalHeartbeatRun(e.run.internal, e.run.source))
|
|
225
|
+
const next = userIdx >= 0 ? q.splice(userIdx, 1)[0] : q.shift()
|
|
222
226
|
if (!next) return
|
|
223
227
|
|
|
224
228
|
state.runningByExecution.set(executionKey, next)
|
|
@@ -276,6 +280,36 @@ async function drainExecution(executionKey: string): Promise<void> {
|
|
|
276
280
|
error: next.run.error || null,
|
|
277
281
|
durationMs: (next.run.endedAt || now()) - (next.run.startedAt || now()),
|
|
278
282
|
})
|
|
283
|
+
const followup = handleMainLoopRunResult({
|
|
284
|
+
sessionId: next.run.sessionId,
|
|
285
|
+
message: next.message,
|
|
286
|
+
internal: next.run.internal,
|
|
287
|
+
source: next.run.source,
|
|
288
|
+
resultText: result.text,
|
|
289
|
+
error: next.run.error,
|
|
290
|
+
toolEvents: result.toolEvents,
|
|
291
|
+
inputTokens: result.inputTokens,
|
|
292
|
+
outputTokens: result.outputTokens,
|
|
293
|
+
estimatedCost: result.estimatedCost,
|
|
294
|
+
})
|
|
295
|
+
if (followup) {
|
|
296
|
+
setTimeout(() => {
|
|
297
|
+
try {
|
|
298
|
+
enqueueSessionRun({
|
|
299
|
+
sessionId: next.run.sessionId,
|
|
300
|
+
message: followup.message,
|
|
301
|
+
internal: true,
|
|
302
|
+
source: 'main-loop-followup',
|
|
303
|
+
mode: 'followup',
|
|
304
|
+
dedupeKey: followup.dedupeKey,
|
|
305
|
+
})
|
|
306
|
+
} catch (err: unknown) {
|
|
307
|
+
log.warn('session-run', `Main loop follow-up enqueue failed for ${next.run.sessionId}`, {
|
|
308
|
+
error: err instanceof Error ? err.message : String(err),
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
}, Math.max(0, followup.delayMs || 0))
|
|
312
|
+
}
|
|
279
313
|
next.resolve(result)
|
|
280
314
|
} catch (err: any) {
|
|
281
315
|
const aborted = next.signalController.signal.aborted
|
|
@@ -385,6 +419,18 @@ export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSession
|
|
|
385
419
|
cancelPendingForSession(input.sessionId, 'Cancelled by steer mode')
|
|
386
420
|
}
|
|
387
421
|
|
|
422
|
+
// Heartbeat preemption: if a user chat arrives while a heartbeat is running,
|
|
423
|
+
// abort the heartbeat so the user doesn't wait. The heartbeat will retry
|
|
424
|
+
// on the next tick.
|
|
425
|
+
if (!internal && source === 'chat') {
|
|
426
|
+
const running = state.runningByExecution.get(executionKey)
|
|
427
|
+
if (running && isInternalHeartbeatRun(running.run.internal, running.run.source)) {
|
|
428
|
+
log.info('session-run', `Preempting heartbeat ${running.run.id} for user chat on ${input.sessionId}`)
|
|
429
|
+
abortSessionRuntime(running, 'Preempted by user chat')
|
|
430
|
+
state.runningByExecution.delete(executionKey)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
388
434
|
const running = state.runningByExecution.get(executionKey)
|
|
389
435
|
const q = queueForExecution(executionKey)
|
|
390
436
|
if (mode === 'collect' && !input.imagePath && !input.imageUrl && !input.attachedFiles?.length) {
|
|
@@ -475,15 +521,48 @@ export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSession
|
|
|
475
521
|
export function getSessionRunState(sessionId: string): {
|
|
476
522
|
runningRunId?: string
|
|
477
523
|
queueLength: number
|
|
524
|
+
} {
|
|
525
|
+
const summary = getSessionExecutionState(sessionId)
|
|
526
|
+
return {
|
|
527
|
+
runningRunId: summary.runningRunId,
|
|
528
|
+
queueLength: summary.queueLength,
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function getSessionExecutionState(sessionId: string): {
|
|
533
|
+
runningRunId?: string
|
|
534
|
+
queueLength: number
|
|
535
|
+
hasRunning: boolean
|
|
536
|
+
hasQueued: boolean
|
|
537
|
+
hasRunningHeartbeat: boolean
|
|
538
|
+
hasQueuedHeartbeat: boolean
|
|
539
|
+
hasRunningNonHeartbeat: boolean
|
|
540
|
+
hasQueuedNonHeartbeat: boolean
|
|
478
541
|
} {
|
|
479
542
|
const executionKey = executionKeyForSession(sessionId)
|
|
480
543
|
const running = state.runningByExecution.get(executionKey)
|
|
481
|
-
const
|
|
544
|
+
const runningMatchesSession = running?.run.sessionId === sessionId
|
|
545
|
+
const runningHeartbeat = Boolean(
|
|
546
|
+
runningMatchesSession
|
|
547
|
+
&& isInternalHeartbeatRun(running.run.internal, running.run.source),
|
|
548
|
+
)
|
|
549
|
+
const runningNonHeartbeat = Boolean(runningMatchesSession && !runningHeartbeat)
|
|
550
|
+
const queuedEntries = queueForExecution(executionKey).filter((entry) => entry.run.sessionId === sessionId)
|
|
551
|
+
const queuedHeartbeat = queuedEntries.filter((entry) =>
|
|
552
|
+
isInternalHeartbeatRun(entry.run.internal, entry.run.source),
|
|
553
|
+
).length
|
|
554
|
+
const queuedNonHeartbeat = queuedEntries.length - queuedHeartbeat
|
|
482
555
|
return {
|
|
483
|
-
runningRunId: (
|
|
556
|
+
runningRunId: (runningMatchesSession && running?.run.status === 'running')
|
|
484
557
|
? running.run.id
|
|
485
558
|
: undefined,
|
|
486
|
-
queueLength:
|
|
559
|
+
queueLength: queuedEntries.length,
|
|
560
|
+
hasRunning: Boolean(runningMatchesSession),
|
|
561
|
+
hasQueued: queuedEntries.length > 0,
|
|
562
|
+
hasRunningHeartbeat: runningHeartbeat,
|
|
563
|
+
hasQueuedHeartbeat: queuedHeartbeat > 0,
|
|
564
|
+
hasRunningNonHeartbeat: runningNonHeartbeat,
|
|
565
|
+
hasQueuedNonHeartbeat: queuedNonHeartbeat > 0,
|
|
487
566
|
}
|
|
488
567
|
}
|
|
489
568
|
|
|
@@ -6,6 +6,7 @@ import type { ToolBuildContext } from './context'
|
|
|
6
6
|
import type { Plugin, PluginHooks } from '@/types'
|
|
7
7
|
import { getPluginManager } from '../plugins'
|
|
8
8
|
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
9
|
+
import { normalizeCanvasContent, summarizeCanvasContent } from '@/lib/canvas-content'
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Core Canvas Execution Logic
|
|
@@ -14,6 +15,7 @@ async function executeCanvasAction(args: Record<string, unknown>, context: { ses
|
|
|
14
15
|
const normalized = normalizeToolInputArgs(args)
|
|
15
16
|
const action = normalized.action as string
|
|
16
17
|
const content = normalized.content as string | undefined
|
|
18
|
+
const document = normalized.document
|
|
17
19
|
try {
|
|
18
20
|
const sessionId = context.sessionId
|
|
19
21
|
if (!sessionId) return 'Error: no active session for canvas.'
|
|
@@ -23,13 +25,18 @@ async function executeCanvasAction(args: Record<string, unknown>, context: { ses
|
|
|
23
25
|
if (!session) return 'Error: session not found.'
|
|
24
26
|
|
|
25
27
|
if (action === 'present') {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
const nextContent = normalizeCanvasContent(document ?? content)
|
|
29
|
+
if (!nextContent) return 'Error: content or document is required for present action.'
|
|
30
|
+
;(session as Record<string, unknown>).canvasContent = nextContent
|
|
28
31
|
session.lastActiveAt = Date.now()
|
|
29
32
|
sessions[sessionId] = session
|
|
30
33
|
saveSessions(sessions)
|
|
31
34
|
notify(`canvas:${sessionId}`)
|
|
32
|
-
return JSON.stringify({
|
|
35
|
+
return JSON.stringify({
|
|
36
|
+
ok: true,
|
|
37
|
+
action: 'present',
|
|
38
|
+
...summarizeCanvasContent(nextContent),
|
|
39
|
+
})
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
if (action === 'hide') {
|
|
@@ -42,14 +49,8 @@ async function executeCanvasAction(args: Record<string, unknown>, context: { ses
|
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
if (action === 'snapshot') {
|
|
45
|
-
const current = (session as Record<string, unknown>).canvasContent
|
|
46
|
-
return JSON.stringify({
|
|
47
|
-
ok: true,
|
|
48
|
-
action: 'snapshot',
|
|
49
|
-
hasContent: !!current,
|
|
50
|
-
contentLength: typeof current === 'string' ? current.length : 0,
|
|
51
|
-
preview: typeof current === 'string' ? current.slice(0, 500) : null,
|
|
52
|
-
})
|
|
52
|
+
const current = normalizeCanvasContent((session as Record<string, unknown>).canvasContent)
|
|
53
|
+
return JSON.stringify({ ok: true, action: 'snapshot', ...summarizeCanvasContent(current) })
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
return `Unknown canvas action "${action}".`
|
|
@@ -73,7 +74,8 @@ const CanvasPlugin: Plugin = {
|
|
|
73
74
|
type: 'object',
|
|
74
75
|
properties: {
|
|
75
76
|
action: { type: 'string', enum: ['present', 'hide', 'snapshot'] },
|
|
76
|
-
content: { type: 'string' }
|
|
77
|
+
content: { type: 'string' },
|
|
78
|
+
document: { type: 'object', additionalProperties: true },
|
|
77
79
|
},
|
|
78
80
|
required: ['action']
|
|
79
81
|
},
|
|
@@ -0,0 +1,37 @@
|
|
|
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 { UPLOAD_DIR } from '../storage'
|
|
6
|
+
import { resolveConnectorMediaInput } from './connector'
|
|
7
|
+
|
|
8
|
+
describe('resolveConnectorMediaInput', () => {
|
|
9
|
+
it('resolves /api/uploads urls passed via mediaPath back to disk', () => {
|
|
10
|
+
const filename = `screenshot-test-${Date.now()}.png`
|
|
11
|
+
const uploadPath = path.join(UPLOAD_DIR, filename)
|
|
12
|
+
fs.mkdirSync(UPLOAD_DIR, { recursive: true })
|
|
13
|
+
fs.writeFileSync(uploadPath, 'png')
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const resolved = resolveConnectorMediaInput({
|
|
17
|
+
cwd: process.cwd(),
|
|
18
|
+
mediaPath: `/api/uploads/${filename}`,
|
|
19
|
+
})
|
|
20
|
+
assert.equal(resolved.error, undefined)
|
|
21
|
+
assert.equal(resolved.mediaPath, uploadPath)
|
|
22
|
+
} finally {
|
|
23
|
+
fs.rmSync(uploadPath, { force: true })
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('treats remote urls passed via mediaPath as sendable urls instead of local files', () => {
|
|
28
|
+
const resolved = resolveConnectorMediaInput({
|
|
29
|
+
cwd: process.cwd(),
|
|
30
|
+
mediaPath: 'https://example.com/report.pdf',
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
assert.equal(resolved.error, undefined)
|
|
34
|
+
assert.equal(resolved.mediaPath, undefined)
|
|
35
|
+
assert.equal(resolved.fileUrl, 'https://example.com/report.pdf')
|
|
36
|
+
})
|
|
37
|
+
})
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
CONNECTOR_MESSAGE_TOOL_ACTIONS,
|
|
6
|
+
CONNECTOR_MESSAGE_TOOL_PARAMETERS,
|
|
7
|
+
inferConnectorActionName,
|
|
8
|
+
normalizeConnectorActionInputAliases,
|
|
9
|
+
normalizeConnectorActionName,
|
|
10
|
+
} from './connector'
|
|
11
|
+
import { getPluginManager } from '../plugins'
|
|
12
|
+
import { buildSessionTools } from './index'
|
|
13
|
+
|
|
14
|
+
describe('connector_message_tool contract', () => {
|
|
15
|
+
it('exposes the connector actions and voice-note fields through the plugin schema', () => {
|
|
16
|
+
const entry = getPluginManager()
|
|
17
|
+
.getTools(['manage_connectors'])
|
|
18
|
+
.find((tool) => tool.tool.name === 'connector_message_tool')
|
|
19
|
+
|
|
20
|
+
assert.ok(entry, 'connector_message_tool should be registered for manage_connectors')
|
|
21
|
+
|
|
22
|
+
const props = (entry!.tool.parameters?.properties ?? {}) as Record<string, { type?: string; enum?: string[] }>
|
|
23
|
+
assert.deepEqual(props.action?.enum, [...CONNECTOR_MESSAGE_TOOL_ACTIONS])
|
|
24
|
+
assert.equal(props.approved?.type, 'boolean')
|
|
25
|
+
assert.equal(props.ptt?.type, 'boolean')
|
|
26
|
+
assert.equal(props.voiceText?.type, 'string')
|
|
27
|
+
assert.equal(props.recipientId?.type, 'string')
|
|
28
|
+
assert.equal(props.channel?.type, 'string')
|
|
29
|
+
assert.equal(Array.isArray(entry!.tool.parameters?.required), false)
|
|
30
|
+
assert.equal(Array.isArray((CONNECTOR_MESSAGE_TOOL_PARAMETERS as { required?: unknown }).required), false)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('normalizes legacy rich-message aliases to the current connector actions', () => {
|
|
34
|
+
assert.equal(normalizeConnectorActionName('message_react'), 'react')
|
|
35
|
+
assert.equal(normalizeConnectorActionName('message_edit'), 'edit')
|
|
36
|
+
assert.equal(normalizeConnectorActionName('message_delete'), 'delete')
|
|
37
|
+
assert.equal(normalizeConnectorActionName('message_pin'), 'pin')
|
|
38
|
+
assert.equal(normalizeConnectorActionName('send_voice_note'), 'send_voice_note')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('infers send-style actions from partial connector payloads', () => {
|
|
42
|
+
assert.equal(inferConnectorActionName({ voiceText: 'hello there' }), 'send_voice_note')
|
|
43
|
+
assert.equal(inferConnectorActionName({ followUpMessage: 'check back later', delaySec: 60 }), 'schedule_followup')
|
|
44
|
+
assert.equal(inferConnectorActionName({ message: 'plain text message' }), 'send')
|
|
45
|
+
assert.equal(inferConnectorActionName({}), null)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('normalizes connector and target aliases from model-generated delivery calls', () => {
|
|
49
|
+
const running = [{ id: 'd81cd63b', name: 'Main Whatsapp connection' }]
|
|
50
|
+
|
|
51
|
+
assert.deepEqual(
|
|
52
|
+
normalizeConnectorActionInputAliases({
|
|
53
|
+
action: 'send_voice_note',
|
|
54
|
+
channel: 'Main Whatsapp connection',
|
|
55
|
+
recipientId: '07958148127',
|
|
56
|
+
}, running),
|
|
57
|
+
{
|
|
58
|
+
action: 'send_voice_note',
|
|
59
|
+
channel: 'Main Whatsapp connection',
|
|
60
|
+
recipientId: '07958148127',
|
|
61
|
+
connectorId: 'd81cd63b',
|
|
62
|
+
to: '07958148127',
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
assert.deepEqual(
|
|
67
|
+
normalizeConnectorActionInputAliases({
|
|
68
|
+
action: 'send_voice_note',
|
|
69
|
+
id: 'd81cd63b',
|
|
70
|
+
target: '199900000001@lid',
|
|
71
|
+
}, running),
|
|
72
|
+
{
|
|
73
|
+
action: 'send_voice_note',
|
|
74
|
+
id: 'd81cd63b',
|
|
75
|
+
target: '199900000001@lid',
|
|
76
|
+
connectorId: 'd81cd63b',
|
|
77
|
+
to: '199900000001@lid',
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('treats raw id as messageId for message actions instead of as a target alias', () => {
|
|
83
|
+
assert.deepEqual(
|
|
84
|
+
normalizeConnectorActionInputAliases({
|
|
85
|
+
action: 'react',
|
|
86
|
+
id: 'msg-123',
|
|
87
|
+
emoji: '👍',
|
|
88
|
+
}, [{ id: 'conn-1', name: 'Primary connector' }]),
|
|
89
|
+
{
|
|
90
|
+
action: 'react',
|
|
91
|
+
id: 'msg-123',
|
|
92
|
+
emoji: '👍',
|
|
93
|
+
messageId: 'msg-123',
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('buildSessionTools exposes the native connector schema instead of the legacy passthrough bridge', async () => {
|
|
99
|
+
const built = await buildSessionTools(process.cwd(), ['manage_connectors'], {
|
|
100
|
+
sessionId: 'connector-native-schema-test',
|
|
101
|
+
agentId: 'default',
|
|
102
|
+
platformAssignScope: 'self',
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const connectorTool = built.tools.find((tool) => tool.name === 'connector_message_tool')
|
|
107
|
+
assert.ok(connectorTool, 'connector_message_tool should be available when manage_connectors is enabled')
|
|
108
|
+
|
|
109
|
+
const schema = (connectorTool as { schema?: { safeParse: (value: unknown) => { success: boolean } } }).schema
|
|
110
|
+
assert.ok(schema, 'connector_message_tool should expose a validation schema')
|
|
111
|
+
assert.equal(schema.safeParse({ action: 'send_voice_note', approved: true, ptt: true }).success, true)
|
|
112
|
+
assert.equal(schema.safeParse({ voiceText: 'hello', recipientId: '07958148127', channel: 'Main Whatsapp connection' }).success, true)
|
|
113
|
+
assert.equal(schema.safeParse({ action: 'message_react' }).success, true)
|
|
114
|
+
assert.equal(schema.safeParse({}).success, true)
|
|
115
|
+
assert.equal(schema.safeParse({ action: 'bogus_action' }).success, false)
|
|
116
|
+
} finally {
|
|
117
|
+
await built.cleanup()
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('loads connector_message_tool when a session only has the tool-level grant alias', async () => {
|
|
122
|
+
const built = await buildSessionTools(process.cwd(), ['connector_message_tool'], {
|
|
123
|
+
sessionId: 'connector-tool-alias-test',
|
|
124
|
+
agentId: 'default',
|
|
125
|
+
platformAssignScope: 'self',
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
assert.equal(
|
|
130
|
+
built.tools.some((tool) => tool.name === 'connector_message_tool'),
|
|
131
|
+
true,
|
|
132
|
+
'connector_message_tool should load from its persisted approval alias',
|
|
133
|
+
)
|
|
134
|
+
} finally {
|
|
135
|
+
await built.cleanup()
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
})
|