@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
|
@@ -3,19 +3,28 @@ import { loadApprovals, upsertApproval, loadSessions, saveSessions, loadSettings
|
|
|
3
3
|
import type { ApprovalRequest, ApprovalCategory, Message } from '@/types'
|
|
4
4
|
import { notify } from './ws-hub'
|
|
5
5
|
import { log } from './logger'
|
|
6
|
+
import { requestHeartbeatNow } from './heartbeat-wake'
|
|
7
|
+
import { enqueueSystemEvent } from './system-events'
|
|
8
|
+
import { enqueueSessionRun } from './session-run-manager'
|
|
9
|
+
import { buildApprovalMatchKey, buildApprovalMatchKeyFromRequest } from './approval-match'
|
|
10
|
+
import { getPluginManager } from './plugins'
|
|
11
|
+
import { addAllowedSender } from './connectors/pairing'
|
|
6
12
|
|
|
7
13
|
const AUTO_APPROVABLE_CATEGORIES: ApprovalCategory[] = [
|
|
8
14
|
'tool_access',
|
|
9
15
|
'wallet_transfer',
|
|
16
|
+
'wallet_action',
|
|
10
17
|
'plugin_scaffold',
|
|
11
18
|
'plugin_install',
|
|
12
19
|
'task_tool',
|
|
13
20
|
'human_loop',
|
|
21
|
+
'connector_sender',
|
|
14
22
|
]
|
|
15
23
|
const DEFAULT_APPROVAL_CONNECTOR_NOTIFY_DELAY_SEC = 300
|
|
16
24
|
const MIN_APPROVAL_CONNECTOR_NOTIFY_DELAY_SEC = 30
|
|
17
25
|
const MAX_APPROVAL_CONNECTOR_NOTIFY_DELAY_SEC = 86_400
|
|
18
26
|
const APPROVAL_CONNECTOR_NOTIFY_RETRY_COOLDOWN_MS = 10 * 60 * 1000
|
|
27
|
+
const RECENT_APPROVED_APPROVAL_REUSE_WINDOW_MS = 10 * 60 * 1000
|
|
19
28
|
|
|
20
29
|
interface RunningConnectorSummary {
|
|
21
30
|
id: string
|
|
@@ -37,6 +46,53 @@ function trimToString(value: unknown): string {
|
|
|
37
46
|
return typeof value === 'string' ? value.trim() : ''
|
|
38
47
|
}
|
|
39
48
|
|
|
49
|
+
function normalizePluginList(value: unknown): string[] {
|
|
50
|
+
if (!Array.isArray(value)) return []
|
|
51
|
+
return value
|
|
52
|
+
.map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getApprovalTargetPlugins(request: ApprovalRequest): string[] {
|
|
57
|
+
return [
|
|
58
|
+
trimToString(request.data.pluginId),
|
|
59
|
+
trimToString(request.data.toolId),
|
|
60
|
+
trimToString(request.data.toolName),
|
|
61
|
+
].filter(Boolean)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getEnabledPluginsForApproval(request: ApprovalRequest): string[] {
|
|
65
|
+
const sessions = loadSessions()
|
|
66
|
+
const agents = loadAgents()
|
|
67
|
+
const sessionPlugins = request.sessionId ? normalizePluginList(sessions[request.sessionId]?.plugins) : []
|
|
68
|
+
const agentPlugins = request.agentId ? normalizePluginList(agents[request.agentId]?.plugins) : []
|
|
69
|
+
const targetPlugins = getApprovalTargetPlugins(request)
|
|
70
|
+
return Array.from(new Set([...sessionPlugins, ...agentPlugins, ...targetPlugins]))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getApprovalGuidance(
|
|
74
|
+
request: ApprovalRequest,
|
|
75
|
+
phase: 'request' | 'resume' | 'connector_reminder',
|
|
76
|
+
approved?: boolean,
|
|
77
|
+
): string[] {
|
|
78
|
+
const enabledPlugins = getEnabledPluginsForApproval(request)
|
|
79
|
+
if (enabledPlugins.length === 0) return []
|
|
80
|
+
return getPluginManager().collectApprovalGuidance(enabledPlugins, {
|
|
81
|
+
approval: request,
|
|
82
|
+
phase,
|
|
83
|
+
approved,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function appendGuidanceToLines(lines: string[], guidance: string[], label = 'Plugin guidance:'): string[] {
|
|
88
|
+
if (guidance.length === 0) return lines
|
|
89
|
+
lines.push(label)
|
|
90
|
+
for (const line of guidance) {
|
|
91
|
+
lines.push(`- ${line}`)
|
|
92
|
+
}
|
|
93
|
+
return lines
|
|
94
|
+
}
|
|
95
|
+
|
|
40
96
|
function clampApprovalConnectorNotifyDelaySec(value: unknown): number {
|
|
41
97
|
const parsed = typeof value === 'number'
|
|
42
98
|
? value
|
|
@@ -64,6 +120,39 @@ function approvalsAreDisabled(): boolean {
|
|
|
64
120
|
return loadSettings().approvalsEnabled === false
|
|
65
121
|
}
|
|
66
122
|
|
|
123
|
+
function canReuseApprovedDecision(category: ApprovalCategory, approval: ApprovalRequest, now: number): boolean {
|
|
124
|
+
if (category === 'tool_access') return true
|
|
125
|
+
return (now - approval.updatedAt) <= RECENT_APPROVED_APPROVAL_REUSE_WINDOW_MS
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function findReusableApproval(params: {
|
|
129
|
+
category: ApprovalCategory
|
|
130
|
+
data: Record<string, unknown>
|
|
131
|
+
agentId?: string | null
|
|
132
|
+
sessionId?: string | null
|
|
133
|
+
taskId?: string | null
|
|
134
|
+
}): ApprovalRequest | null {
|
|
135
|
+
const targetKey = buildApprovalMatchKey(params)
|
|
136
|
+
if (!targetKey) return null
|
|
137
|
+
|
|
138
|
+
const approvals = loadApprovals() as Record<string, ApprovalRequest>
|
|
139
|
+
const now = Date.now()
|
|
140
|
+
let recentApproved: ApprovalRequest | null = null
|
|
141
|
+
|
|
142
|
+
for (const approval of Object.values(approvals)) {
|
|
143
|
+
if (approval.category !== params.category) continue
|
|
144
|
+
if (buildApprovalMatchKeyFromRequest(approval) !== targetKey) continue
|
|
145
|
+
if (approval.status === 'pending') return approval
|
|
146
|
+
if (approval.status !== 'approved') continue
|
|
147
|
+
if (!canReuseApprovedDecision(params.category, approval, now)) continue
|
|
148
|
+
if (!recentApproved || approval.updatedAt > recentApproved.updatedAt) {
|
|
149
|
+
recentApproved = approval
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return recentApproved
|
|
154
|
+
}
|
|
155
|
+
|
|
67
156
|
function getMessageSourceConnectorTarget(
|
|
68
157
|
message: Record<string, unknown> | null | undefined,
|
|
69
158
|
runningById: Map<string, RunningConnectorSummary>,
|
|
@@ -161,10 +250,12 @@ function buildApprovalConnectorReminderText(request: ApprovalRequest): string {
|
|
|
161
250
|
if (description) lines.push(`Details: ${description.slice(0, 500)}`)
|
|
162
251
|
lines.push(`Pending for about ${ageMin} minute${ageMin === 1 ? '' : 's'}.`)
|
|
163
252
|
lines.push('Open the Approvals panel to approve or reject it.')
|
|
253
|
+
appendGuidanceToLines(lines, getApprovalGuidance(request, 'connector_reminder'), 'Agent guidance:')
|
|
164
254
|
return lines.join('\n')
|
|
165
255
|
}
|
|
166
256
|
|
|
167
257
|
function buildApprovalChatMessage(request: ApprovalRequest): string {
|
|
258
|
+
const guidance = getApprovalGuidance(request, 'request')
|
|
168
259
|
const targetId = getApprovalTargetId(request.data)
|
|
169
260
|
switch (request.category) {
|
|
170
261
|
case 'tool_access':
|
|
@@ -175,6 +266,7 @@ function buildApprovalChatMessage(request: ApprovalRequest): string {
|
|
|
175
266
|
toolId: targetId || '',
|
|
176
267
|
reason: trimToString(request.description),
|
|
177
268
|
message: `Plugin access request sent to user for "${targetId || 'requested tool'}". Once granted, I'll automatically continue.`,
|
|
269
|
+
guidance: guidance.length > 0 ? guidance : undefined,
|
|
178
270
|
})
|
|
179
271
|
case 'plugin_scaffold':
|
|
180
272
|
return JSON.stringify({
|
|
@@ -182,6 +274,7 @@ function buildApprovalChatMessage(request: ApprovalRequest): string {
|
|
|
182
274
|
approvalId: request.id,
|
|
183
275
|
filename: trimToString(request.data.filename),
|
|
184
276
|
message: `I've submitted a request to create plugin "${trimToString(request.data.filename) || 'plugin.js'}". The user needs to approve it via the Approvals page or the approval card in chat. Once approved, the plugin file will be written automatically — no need to call this tool again.`,
|
|
277
|
+
guidance: guidance.length > 0 ? guidance : undefined,
|
|
185
278
|
})
|
|
186
279
|
case 'plugin_install':
|
|
187
280
|
return JSON.stringify({
|
|
@@ -191,6 +284,7 @@ function buildApprovalChatMessage(request: ApprovalRequest): string {
|
|
|
191
284
|
pluginId: trimToString(request.data.pluginId),
|
|
192
285
|
reason: trimToString(request.description),
|
|
193
286
|
message: `I'm requesting to install a new plugin${trimToString(request.data.url) ? ` from ${trimToString(request.data.url)}` : ''}. This will add new capabilities to the platform.`,
|
|
287
|
+
guidance: guidance.length > 0 ? guidance : undefined,
|
|
194
288
|
})
|
|
195
289
|
case 'wallet_transfer':
|
|
196
290
|
return JSON.stringify({
|
|
@@ -200,7 +294,25 @@ function buildApprovalChatMessage(request: ApprovalRequest): string {
|
|
|
200
294
|
toAddress: trimToString(request.data.toAddress),
|
|
201
295
|
memo: trimToString(request.data.memo),
|
|
202
296
|
message: `I'm requesting to send ${request.data.amountSol ?? 'funds'} to ${trimToString(request.data.toAddress) || 'the specified address'}. Please approve this transaction.`,
|
|
297
|
+
guidance: guidance.length > 0 ? guidance : undefined,
|
|
298
|
+
})
|
|
299
|
+
case 'wallet_action':
|
|
300
|
+
return JSON.stringify({
|
|
301
|
+
type: 'plugin_wallet_action_request',
|
|
302
|
+
approvalId: request.id,
|
|
303
|
+
action: trimToString(request.data.action),
|
|
304
|
+
chain: trimToString(request.data.chain),
|
|
305
|
+
network: trimToString(request.data.network),
|
|
306
|
+
summary: trimToString(request.data.summary),
|
|
307
|
+
message: trimToString(request.description) || `I'm requesting approval for wallet action "${trimToString(request.data.action) || request.title}".`,
|
|
308
|
+
guidance: guidance.length > 0 ? guidance : undefined,
|
|
203
309
|
})
|
|
310
|
+
case 'connector_sender':
|
|
311
|
+
return [
|
|
312
|
+
`[Approval requested] ${request.title}`,
|
|
313
|
+
trimToString(request.description) || `Allow ${trimToString(request.data.senderName) || trimToString(request.data.senderId) || 'this sender'} on ${trimToString(request.data.connectorName) || 'the connector'}.`,
|
|
314
|
+
'Approve or reject this sender in the chat approval card or the Approvals panel.',
|
|
315
|
+
].filter(Boolean).join('\n')
|
|
204
316
|
default: {
|
|
205
317
|
const lines = [
|
|
206
318
|
`[Approval requested] ${request.title}`,
|
|
@@ -208,11 +320,92 @@ function buildApprovalChatMessage(request: ApprovalRequest): string {
|
|
|
208
320
|
const description = trimToString(request.description)
|
|
209
321
|
if (description) lines.push(`Details: ${description}`)
|
|
210
322
|
lines.push('Approve or reject this request in the chat approval card or the Approvals panel.')
|
|
323
|
+
appendGuidanceToLines(lines, guidance)
|
|
211
324
|
return lines.join('\n')
|
|
212
325
|
}
|
|
213
326
|
}
|
|
214
327
|
}
|
|
215
328
|
|
|
329
|
+
function buildApprovalDecisionResumeText(request: ApprovalRequest, approved: boolean): string {
|
|
330
|
+
const statusLabel = approved ? 'approved' : 'rejected'
|
|
331
|
+
const lines = [`[Approval ${statusLabel}] ${request.title}`]
|
|
332
|
+
const description = trimToString(request.description)
|
|
333
|
+
if (description) lines.push(`Details: ${description}`)
|
|
334
|
+
lines.push(`Approval id: ${request.id}`)
|
|
335
|
+
lines.push(approved
|
|
336
|
+
? 'Continue the work that was blocked on this approval.'
|
|
337
|
+
: 'The requested action was rejected. Adjust the plan and continue safely.')
|
|
338
|
+
return lines.join('\n')
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function buildApprovalDecisionResumeMessage(request: ApprovalRequest, approved: boolean): string {
|
|
342
|
+
const guidance = getApprovalGuidance(request, 'resume', approved)
|
|
343
|
+
const lines = [
|
|
344
|
+
'APPROVAL_DECISION_EVENT',
|
|
345
|
+
`Approval id: ${request.id}`,
|
|
346
|
+
`Category: ${request.category}`,
|
|
347
|
+
`Status: ${approved ? 'approved' : 'rejected'}`,
|
|
348
|
+
`Title: ${request.title}`,
|
|
349
|
+
]
|
|
350
|
+
const description = trimToString(request.description)
|
|
351
|
+
if (description) lines.push(`Details: ${description}`)
|
|
352
|
+
|
|
353
|
+
const action = trimToString(request.data.action)
|
|
354
|
+
const chain = trimToString(request.data.chain)
|
|
355
|
+
const network = trimToString(request.data.network)
|
|
356
|
+
const summary = trimToString(request.data.summary)
|
|
357
|
+
if (action) lines.push(`Action: ${action}`)
|
|
358
|
+
if (chain) lines.push(`Chain: ${chain}`)
|
|
359
|
+
if (network) lines.push(`Network: ${network}`)
|
|
360
|
+
if (summary) lines.push(`Summary: ${summary}`)
|
|
361
|
+
if (request.category === 'wallet_action' || request.category === 'wallet_transfer') {
|
|
362
|
+
lines.push(`Approved payload: ${JSON.stringify(request.data)}`)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (approved) {
|
|
366
|
+
lines.push('Resume the exact work that was blocked on this approval now.')
|
|
367
|
+
lines.push('Use this exact approvalId for the matching blocked tool action if the tool requires one.')
|
|
368
|
+
lines.push('If the exact blocked action still applies, execute it before doing more research or recomputing alternatives.')
|
|
369
|
+
lines.push('If tool evidence proves the approved action can no longer be executed as approved, request a fresh approval for the new exact action instead of reusing the old approvalId.')
|
|
370
|
+
} else {
|
|
371
|
+
lines.push('The requested action was rejected. Adjust the plan safely and continue without retrying the rejected action.')
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
appendGuidanceToLines(lines, guidance)
|
|
375
|
+
return lines.join('\n')
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function wakeForApprovalDecision(request: ApprovalRequest, approved: boolean): void {
|
|
379
|
+
if (request.category === 'connector_sender') return
|
|
380
|
+
const reason = approved ? 'approval-approved' : 'approval-rejected'
|
|
381
|
+
if (request.sessionId) {
|
|
382
|
+
enqueueSystemEvent(
|
|
383
|
+
request.sessionId,
|
|
384
|
+
buildApprovalDecisionResumeText(request, approved),
|
|
385
|
+
`approval:${request.id}:${approved ? 'approved' : 'rejected'}`,
|
|
386
|
+
)
|
|
387
|
+
enqueueSessionRun({
|
|
388
|
+
sessionId: request.sessionId,
|
|
389
|
+
message: buildApprovalDecisionResumeMessage(request, approved),
|
|
390
|
+
internal: true,
|
|
391
|
+
source: 'approval-decision',
|
|
392
|
+
mode: 'collect',
|
|
393
|
+
dedupeKey: `approval-decision:${request.id}`,
|
|
394
|
+
})
|
|
395
|
+
return
|
|
396
|
+
}
|
|
397
|
+
if (request.agentId) {
|
|
398
|
+
requestHeartbeatNow({
|
|
399
|
+
agentId: request.agentId,
|
|
400
|
+
eventId: `approval:${request.id}:${approved ? 'approved' : 'rejected'}`,
|
|
401
|
+
reason,
|
|
402
|
+
source: `approval:${request.category}`,
|
|
403
|
+
resumeMessage: buildApprovalDecisionResumeText(request, approved),
|
|
404
|
+
detail: request.title || request.category,
|
|
405
|
+
})
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
216
409
|
function pushApprovalRequestMessage(request: ApprovalRequest): void {
|
|
217
410
|
const sessionId = trimToString(request.sessionId)
|
|
218
411
|
if (!sessionId) return
|
|
@@ -232,6 +425,7 @@ function pushApprovalRequestMessage(request: ApprovalRequest): void {
|
|
|
232
425
|
text,
|
|
233
426
|
time: Date.now(),
|
|
234
427
|
kind: 'system',
|
|
428
|
+
...(request.category === 'connector_sender' ? { historyExcluded: true } : {}),
|
|
235
429
|
})
|
|
236
430
|
session.lastActiveAt = Date.now()
|
|
237
431
|
sessions[sessionId] = session
|
|
@@ -407,6 +601,14 @@ async function applyApprovedSideEffects(request: ApprovalRequest): Promise<void>
|
|
|
407
601
|
}
|
|
408
602
|
}
|
|
409
603
|
}
|
|
604
|
+
|
|
605
|
+
if (request.category === 'connector_sender') {
|
|
606
|
+
const connectorId = trimToString(request.data.connectorId)
|
|
607
|
+
const senderId = trimToString(request.data.senderId)
|
|
608
|
+
if (connectorId && senderId) {
|
|
609
|
+
addAllowedSender(connectorId, senderId)
|
|
610
|
+
}
|
|
611
|
+
}
|
|
410
612
|
}
|
|
411
613
|
|
|
412
614
|
async function persistApprovalDecision(request: ApprovalRequest, approved: boolean): Promise<ApprovalRequest> {
|
|
@@ -444,6 +646,14 @@ export async function requestApprovalMaybeAutoApprove(params: {
|
|
|
444
646
|
sessionId?: string | null
|
|
445
647
|
taskId?: string | null
|
|
446
648
|
}): Promise<ApprovalRequest> {
|
|
649
|
+
const reusable = findReusableApproval(params)
|
|
650
|
+
if (reusable) {
|
|
651
|
+
if (reusable.status === 'pending' && (approvalsAreDisabled() || isApprovalCategoryAutoApproved(reusable.category))) {
|
|
652
|
+
return persistApprovalDecision(reusable, true)
|
|
653
|
+
}
|
|
654
|
+
return reusable
|
|
655
|
+
}
|
|
656
|
+
|
|
447
657
|
const request = requestApproval(params)
|
|
448
658
|
if (!approvalsAreDisabled() && !isApprovalCategoryAutoApproved(request.category)) {
|
|
449
659
|
pushApprovalRequestMessage(request)
|
|
@@ -457,7 +667,10 @@ export async function submitDecision(id: string, approved: boolean): Promise<voi
|
|
|
457
667
|
const approvals = loadApprovals() as Record<string, ApprovalRequest>
|
|
458
668
|
const request = approvals[id]
|
|
459
669
|
if (!request) throw new Error('Approval request not found')
|
|
460
|
-
|
|
670
|
+
if (request.status === (approved ? 'approved' : 'rejected')) return
|
|
671
|
+
if (request.status !== 'pending') return
|
|
672
|
+
const updated = await persistApprovalDecision(request, approved)
|
|
673
|
+
wakeForApprovalDecision(updated, approved)
|
|
461
674
|
}
|
|
462
675
|
|
|
463
676
|
export function listPendingApprovalsNeedingConnectorNotification(params?: {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
import { shouldSuppressHiddenControlText, stripHiddenControlTokens } from './assistant-control'
|
|
4
|
+
|
|
5
|
+
describe('assistant-control', () => {
|
|
6
|
+
it('suppresses pure hidden control replies', () => {
|
|
7
|
+
assert.equal(shouldSuppressHiddenControlText('NO_MESSAGE'), true)
|
|
8
|
+
assert.equal(shouldSuppressHiddenControlText(' HEARTBEAT_OK '), true)
|
|
9
|
+
assert.equal(stripHiddenControlTokens('NO_MESSAGE'), '')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('strips leaked control prefixes without suppressing real content', () => {
|
|
13
|
+
assert.equal(
|
|
14
|
+
stripHiddenControlTokens('NO_MESSAGEIt seems there was an error earlier on.'),
|
|
15
|
+
'It seems there was an error earlier on.',
|
|
16
|
+
)
|
|
17
|
+
assert.equal(
|
|
18
|
+
shouldSuppressHiddenControlText('NO_MESSAGEIt seems there was an error earlier on.'),
|
|
19
|
+
false,
|
|
20
|
+
)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('removes standalone control-token lines from mixed content', () => {
|
|
24
|
+
assert.equal(
|
|
25
|
+
stripHiddenControlTokens('Working on it.\nNO_MESSAGE\nI found the issue.'),
|
|
26
|
+
'Working on it.\nI found the issue.',
|
|
27
|
+
)
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const CONTROL_TOKEN_NAMES = ['NO_MESSAGE', 'HEARTBEAT_OK'] as const
|
|
2
|
+
const CONTROL_TOKEN_PREFIX_RE = /^\s*(?:NO_MESSAGE|HEARTBEAT_OK)(?:(?=[\s.,:;!?()[\]{}"'`-]|$)|(?=[A-Z]))\s*/i
|
|
3
|
+
const CONTROL_TOKEN_LINE_RE = /(^|\n)\s*(?:NO_MESSAGE|HEARTBEAT_OK)\s*(\n|$)/gi
|
|
4
|
+
|
|
5
|
+
export function stripHiddenControlTokens(text: string): string {
|
|
6
|
+
let cleaned = String(text || '')
|
|
7
|
+
let previous = ''
|
|
8
|
+
|
|
9
|
+
while (cleaned !== previous) {
|
|
10
|
+
previous = cleaned
|
|
11
|
+
cleaned = cleaned.replace(CONTROL_TOKEN_PREFIX_RE, '')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
cleaned = cleaned.replace(CONTROL_TOKEN_LINE_RE, '$1')
|
|
15
|
+
return cleaned.replace(/\n{3,}/g, '\n\n').trim()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function shouldSuppressHiddenControlText(text: string): boolean {
|
|
19
|
+
const raw = String(text || '').trim()
|
|
20
|
+
if (!raw) return false
|
|
21
|
+
if (!CONTROL_TOKEN_NAMES.some((token) => raw.toUpperCase().includes(token))) return false
|
|
22
|
+
return stripHiddenControlTokens(raw).length === 0
|
|
23
|
+
}
|
|
@@ -41,4 +41,83 @@ describe('buildChatModel', () => {
|
|
|
41
41
|
assert.equal(model.caller?.maxRetries, OPENAI_COMPAT_MODEL_MAX_RETRIES)
|
|
42
42
|
assert.deepEqual(model.clientConfig?.defaultHeaders, { 'Content-Type': 'text/plain' })
|
|
43
43
|
})
|
|
44
|
+
|
|
45
|
+
it('routes glm-5:cloud to Ollama Cloud and strips the transport suffix', () => {
|
|
46
|
+
const originalKey = process.env.OLLAMA_API_KEY
|
|
47
|
+
process.env.OLLAMA_API_KEY = 'ollama-cloud-test-key'
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const llm = buildChatModel({
|
|
51
|
+
provider: 'ollama',
|
|
52
|
+
model: 'glm-5:cloud',
|
|
53
|
+
apiKey: null,
|
|
54
|
+
})
|
|
55
|
+
const model = llm as ChatOpenAiInternals & {
|
|
56
|
+
model?: string
|
|
57
|
+
clientConfig?: { baseURL?: string }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
assert.equal(llm instanceof ChatOpenAI, true)
|
|
61
|
+
assert.equal(model.model, 'glm-5')
|
|
62
|
+
assert.equal(model.clientConfig?.baseURL, 'https://ollama.com/v1')
|
|
63
|
+
assert.equal(model.timeout, OPENAI_COMPAT_MODEL_TIMEOUT_MS)
|
|
64
|
+
assert.equal(model.caller?.maxRetries, OPENAI_COMPAT_MODEL_MAX_RETRIES)
|
|
65
|
+
} finally {
|
|
66
|
+
if (originalKey === undefined) delete process.env.OLLAMA_API_KEY
|
|
67
|
+
else process.env.OLLAMA_API_KEY = originalKey
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('keeps glm-5:cloud on the local Ollama endpoint when no cloud key is available', () => {
|
|
72
|
+
const originalKey = process.env.OLLAMA_API_KEY
|
|
73
|
+
delete process.env.OLLAMA_API_KEY
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const llm = buildChatModel({
|
|
77
|
+
provider: 'ollama',
|
|
78
|
+
model: 'glm-5:cloud',
|
|
79
|
+
apiKey: null,
|
|
80
|
+
})
|
|
81
|
+
const model = llm as ChatOpenAiInternals & {
|
|
82
|
+
model?: string
|
|
83
|
+
clientConfig?: { baseURL?: string }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
assert.equal(llm instanceof ChatOpenAI, true)
|
|
87
|
+
assert.equal(model.model, 'glm-5:cloud')
|
|
88
|
+
assert.equal(model.clientConfig?.baseURL, 'http://localhost:11434/v1')
|
|
89
|
+
assert.equal(model.timeout, OPENAI_COMPAT_MODEL_TIMEOUT_MS)
|
|
90
|
+
assert.equal(model.caller?.maxRetries, OPENAI_COMPAT_MODEL_MAX_RETRIES)
|
|
91
|
+
} finally {
|
|
92
|
+
if (originalKey === undefined) delete process.env.OLLAMA_API_KEY
|
|
93
|
+
else process.env.OLLAMA_API_KEY = originalKey
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('keeps an explicit local Ollama endpoint even when a cloud key exists', () => {
|
|
98
|
+
const originalKey = process.env.OLLAMA_API_KEY
|
|
99
|
+
process.env.OLLAMA_API_KEY = 'ollama-cloud-test-key'
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const llm = buildChatModel({
|
|
103
|
+
provider: 'ollama',
|
|
104
|
+
model: 'glm-5:cloud',
|
|
105
|
+
apiKey: null,
|
|
106
|
+
apiEndpoint: 'http://localhost:11434',
|
|
107
|
+
})
|
|
108
|
+
const model = llm as ChatOpenAiInternals & {
|
|
109
|
+
model?: string
|
|
110
|
+
clientConfig?: { baseURL?: string }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
assert.equal(llm instanceof ChatOpenAI, true)
|
|
114
|
+
assert.equal(model.model, 'glm-5:cloud')
|
|
115
|
+
assert.equal(model.clientConfig?.baseURL, 'http://localhost:11434/v1')
|
|
116
|
+
assert.equal(model.timeout, OPENAI_COMPAT_MODEL_TIMEOUT_MS)
|
|
117
|
+
assert.equal(model.caller?.maxRetries, OPENAI_COMPAT_MODEL_MAX_RETRIES)
|
|
118
|
+
} finally {
|
|
119
|
+
if (originalKey === undefined) delete process.env.OLLAMA_API_KEY
|
|
120
|
+
else process.env.OLLAMA_API_KEY = originalKey
|
|
121
|
+
}
|
|
122
|
+
})
|
|
44
123
|
})
|
|
@@ -4,12 +4,18 @@ import { loadCredentials, decryptKey, loadAgents, loadSettings } from './storage
|
|
|
4
4
|
import { getProviderList } from '../providers'
|
|
5
5
|
import { normalizeOpenClawEndpoint } from '../openclaw-endpoint'
|
|
6
6
|
import { NON_LANGGRAPH_PROVIDER_IDS } from '../provider-sets'
|
|
7
|
+
import { resolveOllamaRuntimeConfig } from './ollama-runtime'
|
|
7
8
|
|
|
8
9
|
const OLLAMA_CLOUD_URL = 'https://ollama.com/v1'
|
|
9
10
|
const OLLAMA_LOCAL_URL = 'http://localhost:11434/v1'
|
|
10
11
|
export const OPENAI_COMPAT_MODEL_TIMEOUT_MS = 180_000
|
|
11
12
|
export const OPENAI_COMPAT_MODEL_MAX_RETRIES = 0
|
|
12
13
|
|
|
14
|
+
function toOpenAiCompatibleBaseUrl(endpoint: string | null | undefined, fallback: string): string {
|
|
15
|
+
const normalized = (endpoint || fallback).replace(/\/+$/, '')
|
|
16
|
+
return normalized.endsWith('/v1') ? normalized : `${normalized}/v1`
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
/**
|
|
14
20
|
* Build a LangChain chat model from provider config.
|
|
15
21
|
* Uses the provider registry for endpoint defaults — no hardcoded provider list.
|
|
@@ -46,12 +52,16 @@ export function buildChatModel(opts: {
|
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
if (provider === 'ollama') {
|
|
49
|
-
const
|
|
55
|
+
const runtime = resolveOllamaRuntimeConfig({ model, apiKey, apiEndpoint })
|
|
56
|
+
if (runtime.useCloud && !runtime.apiKey) {
|
|
57
|
+
throw new Error('Ollama Cloud model requires an API key. Set OLLAMA_API_KEY or attach an Ollama credential.')
|
|
58
|
+
}
|
|
59
|
+
const baseURL = runtime.useCloud
|
|
50
60
|
? OLLAMA_CLOUD_URL
|
|
51
|
-
: (endpoint
|
|
61
|
+
: toOpenAiCompatibleBaseUrl(runtime.endpoint, OLLAMA_LOCAL_URL)
|
|
52
62
|
return new ChatOpenAI({
|
|
53
|
-
model: model || 'qwen3.5',
|
|
54
|
-
apiKey: apiKey || 'ollama',
|
|
63
|
+
model: runtime.model || 'qwen3.5',
|
|
64
|
+
apiKey: runtime.useCloud ? runtime.apiKey || undefined : 'ollama',
|
|
55
65
|
timeout: OPENAI_COMPAT_MODEL_TIMEOUT_MS,
|
|
56
66
|
maxRetries: OPENAI_COMPAT_MODEL_MAX_RETRIES,
|
|
57
67
|
configuration: { baseURL },
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { normalizeCanvasDocument } from './canvas-content'
|
|
5
|
+
|
|
6
|
+
describe('normalizeCanvasDocument', () => {
|
|
7
|
+
it('filters invalid metric rows and keeps valid metrics blocks', () => {
|
|
8
|
+
const result = normalizeCanvasDocument({
|
|
9
|
+
title: 'Smoke',
|
|
10
|
+
blocks: [
|
|
11
|
+
{
|
|
12
|
+
type: 'metrics',
|
|
13
|
+
items: [
|
|
14
|
+
{ label: 'Healthy', value: 12, tone: 'positive' },
|
|
15
|
+
{ label: '', value: 'skip-me' },
|
|
16
|
+
{ value: 'missing-label' },
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
assert.ok(result)
|
|
23
|
+
assert.equal(result?.blocks.length, 1)
|
|
24
|
+
assert.deepEqual(result?.blocks[0], {
|
|
25
|
+
type: 'metrics',
|
|
26
|
+
items: [
|
|
27
|
+
{ label: 'Healthy', value: '12', tone: 'positive', detail: undefined },
|
|
28
|
+
],
|
|
29
|
+
title: undefined,
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
})
|
|
@@ -19,3 +19,36 @@ test('routeTaskIntent keeps coding prompts prioritized over memory keywords', ()
|
|
|
19
19
|
)
|
|
20
20
|
assert.equal(decision.intent, 'coding')
|
|
21
21
|
})
|
|
22
|
+
|
|
23
|
+
test('routeTaskIntent keeps hybrid research-plus-media prompts in research intent', () => {
|
|
24
|
+
const decision = routeTaskIntent(
|
|
25
|
+
'Can you tell me more if there is any news related to the US-Iran war, and can you send me some screenshots and give me a summary and maybe send me a voice note about it?',
|
|
26
|
+
['web_search', 'web_fetch', 'browser', 'manage_connectors'],
|
|
27
|
+
null,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
assert.equal(decision.intent, 'research')
|
|
31
|
+
assert.deepEqual(decision.preferredTools, ['web_search', 'web_fetch', 'browser', 'connector_message_tool'])
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('routeTaskIntent treats direct voice-note delivery as outreach', () => {
|
|
35
|
+
const decision = routeTaskIntent(
|
|
36
|
+
'Send me a voice note over WhatsApp summarizing what changed.',
|
|
37
|
+
['manage_connectors'],
|
|
38
|
+
null,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
assert.equal(decision.intent, 'outreach')
|
|
42
|
+
assert.deepEqual(decision.preferredTools, ['connector_message_tool'])
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('routeTaskIntent treats keep-watching update requests as research even without explicit news keywords', () => {
|
|
46
|
+
const decision = routeTaskIntent(
|
|
47
|
+
'Tell me about the Iran war, keep watching for meaningful updates, and avoid duplicate reminders.',
|
|
48
|
+
['web_search', 'manage_schedules'],
|
|
49
|
+
null,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
assert.equal(decision.intent, 'research')
|
|
53
|
+
assert.deepEqual(decision.preferredTools, ['web_search', 'web_fetch'])
|
|
54
|
+
})
|