@swarmclawai/swarmclaw 0.2.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 +577 -0
- package/bin/server-cmd.js +359 -0
- package/bin/swarmclaw.js +29 -0
- package/bin/swarmclaw.mjs +1504 -0
- package/next.config.ts +33 -0
- package/package.json +112 -0
- package/postcss.config.mjs +7 -0
- package/public/branding/swarmclaw-org-avatar.png +0 -0
- package/public/branding/swarmclaw-org-avatar.svg +58 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/connectors.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/new-session-openclaw.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/schedules.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/agents/[id]/route.ts +30 -0
- package/src/app/api/agents/[id]/thread/route.ts +66 -0
- package/src/app/api/agents/generate/route.ts +42 -0
- package/src/app/api/agents/route.ts +33 -0
- package/src/app/api/auth/route.ts +25 -0
- package/src/app/api/claude-skills/route.ts +42 -0
- package/src/app/api/clawhub/install/route.ts +39 -0
- package/src/app/api/clawhub/search/route.ts +11 -0
- package/src/app/api/connectors/[id]/route.ts +79 -0
- package/src/app/api/connectors/route.ts +60 -0
- package/src/app/api/credentials/[id]/route.ts +14 -0
- package/src/app/api/credentials/route.ts +31 -0
- package/src/app/api/daemon/health-check/route.ts +11 -0
- package/src/app/api/daemon/route.ts +22 -0
- package/src/app/api/dirs/pick/route.ts +60 -0
- package/src/app/api/dirs/route.ts +29 -0
- package/src/app/api/documents/[id]/route.ts +47 -0
- package/src/app/api/documents/route.ts +93 -0
- package/src/app/api/files/serve/route.ts +69 -0
- package/src/app/api/generate/info/route.ts +12 -0
- package/src/app/api/generate/route.ts +106 -0
- package/src/app/api/ip/route.ts +6 -0
- package/src/app/api/knowledge/[id]/route.ts +61 -0
- package/src/app/api/knowledge/route.ts +48 -0
- package/src/app/api/knowledge/upload/route.ts +86 -0
- package/src/app/api/logs/route.ts +65 -0
- package/src/app/api/mcp-servers/[id]/route.ts +32 -0
- package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
- package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
- package/src/app/api/mcp-servers/route.ts +27 -0
- package/src/app/api/memory/[id]/route.ts +126 -0
- package/src/app/api/memory/maintenance/route.ts +63 -0
- package/src/app/api/memory/route.ts +111 -0
- package/src/app/api/memory-images/[filename]/route.ts +36 -0
- package/src/app/api/orchestrator/run/route.ts +43 -0
- package/src/app/api/plugins/install/route.ts +58 -0
- package/src/app/api/plugins/marketplace/route.ts +33 -0
- package/src/app/api/plugins/route.ts +21 -0
- package/src/app/api/preview-server/route.ts +339 -0
- package/src/app/api/providers/[id]/models/route.ts +29 -0
- package/src/app/api/providers/[id]/route.ts +34 -0
- package/src/app/api/providers/configs/route.ts +7 -0
- package/src/app/api/providers/ollama/route.ts +30 -0
- package/src/app/api/providers/openclaw/health/route.ts +23 -0
- package/src/app/api/providers/route.ts +28 -0
- package/src/app/api/runs/[id]/route.ts +9 -0
- package/src/app/api/runs/route.ts +13 -0
- package/src/app/api/schedules/[id]/route.ts +28 -0
- package/src/app/api/schedules/[id]/run/route.ts +104 -0
- package/src/app/api/schedules/route.ts +78 -0
- package/src/app/api/secrets/[id]/route.ts +29 -0
- package/src/app/api/secrets/route.ts +42 -0
- package/src/app/api/sessions/[id]/browser/route.ts +13 -0
- package/src/app/api/sessions/[id]/chat/route.ts +96 -0
- package/src/app/api/sessions/[id]/clear/route.ts +19 -0
- package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
- package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
- package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
- package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
- package/src/app/api/sessions/[id]/messages/route.ts +9 -0
- package/src/app/api/sessions/[id]/retry/route.ts +28 -0
- package/src/app/api/sessions/[id]/route.ts +103 -0
- package/src/app/api/sessions/[id]/stop/route.ts +13 -0
- package/src/app/api/sessions/heartbeat/route.ts +26 -0
- package/src/app/api/sessions/route.ts +85 -0
- package/src/app/api/settings/route.ts +58 -0
- package/src/app/api/setup/check-provider/route.ts +326 -0
- package/src/app/api/setup/doctor/route.ts +250 -0
- package/src/app/api/skills/[id]/route.ts +40 -0
- package/src/app/api/skills/import/route.ts +69 -0
- package/src/app/api/skills/route.ts +28 -0
- package/src/app/api/tasks/[id]/route.ts +102 -0
- package/src/app/api/tasks/route.ts +115 -0
- package/src/app/api/tts/route.ts +40 -0
- package/src/app/api/upload/route.ts +18 -0
- package/src/app/api/uploads/[filename]/route.ts +59 -0
- package/src/app/api/usage/route.ts +35 -0
- package/src/app/api/version/route.ts +81 -0
- package/src/app/api/version/update/route.ts +95 -0
- package/src/app/api/webhooks/[id]/history/route.ts +13 -0
- package/src/app/api/webhooks/[id]/route.ts +204 -0
- package/src/app/api/webhooks/route.ts +37 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +370 -0
- package/src/app/layout.tsx +52 -0
- package/src/app/page.tsx +172 -0
- package/src/cli/index.js +1232 -0
- package/src/cli/index.test.js +281 -0
- package/src/cli/index.ts +1158 -0
- package/src/cli/spec.js +284 -0
- package/src/components/agents/agent-card.tsx +219 -0
- package/src/components/agents/agent-chat-list.tsx +165 -0
- package/src/components/agents/agent-list.tsx +110 -0
- package/src/components/agents/agent-sheet.tsx +1220 -0
- package/src/components/auth/access-key-gate.tsx +248 -0
- package/src/components/auth/setup-wizard.tsx +940 -0
- package/src/components/auth/user-picker.tsx +88 -0
- package/src/components/chat/chat-area.tsx +406 -0
- package/src/components/chat/chat-header.tsx +491 -0
- package/src/components/chat/chat-tool-toggles.tsx +161 -0
- package/src/components/chat/code-block.tsx +146 -0
- package/src/components/chat/dev-server-bar.tsx +39 -0
- package/src/components/chat/message-bubble.tsx +486 -0
- package/src/components/chat/message-list.tsx +299 -0
- package/src/components/chat/session-debug-panel.tsx +196 -0
- package/src/components/chat/streaming-bubble.tsx +85 -0
- package/src/components/chat/thinking-indicator.tsx +26 -0
- package/src/components/chat/tool-call-bubble.tsx +438 -0
- package/src/components/chat/tool-request-banner.tsx +103 -0
- package/src/components/connectors/connector-list.tsx +196 -0
- package/src/components/connectors/connector-sheet.tsx +804 -0
- package/src/components/input/chat-input.tsx +235 -0
- package/src/components/knowledge/knowledge-list.tsx +206 -0
- package/src/components/knowledge/knowledge-sheet.tsx +316 -0
- package/src/components/layout/app-layout.tsx +1016 -0
- package/src/components/layout/daemon-indicator.tsx +56 -0
- package/src/components/layout/mobile-header.tsx +31 -0
- package/src/components/layout/network-banner.tsx +17 -0
- package/src/components/layout/update-banner.tsx +130 -0
- package/src/components/logs/log-list.tsx +358 -0
- package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
- package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
- package/src/components/memory/memory-card.tsx +63 -0
- package/src/components/memory/memory-detail.tsx +339 -0
- package/src/components/memory/memory-list.tsx +198 -0
- package/src/components/memory/memory-sheet.tsx +70 -0
- package/src/components/plugins/plugin-list.tsx +60 -0
- package/src/components/plugins/plugin-sheet.tsx +311 -0
- package/src/components/providers/provider-list.tsx +96 -0
- package/src/components/providers/provider-sheet.tsx +542 -0
- package/src/components/runs/run-list.tsx +231 -0
- package/src/components/schedules/schedule-card.tsx +63 -0
- package/src/components/schedules/schedule-list.tsx +76 -0
- package/src/components/schedules/schedule-sheet.tsx +336 -0
- package/src/components/secrets/secret-sheet.tsx +180 -0
- package/src/components/secrets/secrets-list.tsx +91 -0
- package/src/components/sessions/new-session-sheet.tsx +478 -0
- package/src/components/sessions/session-card.tsx +144 -0
- package/src/components/sessions/session-list.tsx +202 -0
- package/src/components/shared/ai-gen-block.tsx +77 -0
- package/src/components/shared/avatar.tsx +48 -0
- package/src/components/shared/bottom-sheet.tsx +30 -0
- package/src/components/shared/confirm-dialog.tsx +47 -0
- package/src/components/shared/connector-platform-icon.tsx +113 -0
- package/src/components/shared/dir-browser.tsx +285 -0
- package/src/components/shared/dropdown.tsx +55 -0
- package/src/components/shared/icon-button.tsx +25 -0
- package/src/components/shared/settings/plugin-manager.tsx +207 -0
- package/src/components/shared/settings/section-capability-policy.tsx +93 -0
- package/src/components/shared/settings/section-embedding.tsx +99 -0
- package/src/components/shared/settings/section-heartbeat.tsx +168 -0
- package/src/components/shared/settings/section-memory.tsx +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +108 -0
- package/src/components/shared/settings/section-providers.tsx +181 -0
- package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
- package/src/components/shared/settings/section-secrets.tsx +132 -0
- package/src/components/shared/settings/section-user-preferences.tsx +24 -0
- package/src/components/shared/settings/section-voice.tsx +53 -0
- package/src/components/shared/settings/settings-sheet.tsx +88 -0
- package/src/components/shared/settings/types.ts +7 -0
- package/src/components/shared/settings/utils.ts +13 -0
- package/src/components/shared/settings-sheet.tsx +1 -0
- package/src/components/shared/skeleton.tsx +19 -0
- package/src/components/shared/usage-badge.tsx +28 -0
- package/src/components/skills/clawhub-browser.tsx +225 -0
- package/src/components/skills/skill-list.tsx +70 -0
- package/src/components/skills/skill-sheet.tsx +254 -0
- package/src/components/tasks/task-board.tsx +96 -0
- package/src/components/tasks/task-card.tsx +179 -0
- package/src/components/tasks/task-column.tsx +73 -0
- package/src/components/tasks/task-list.tsx +118 -0
- package/src/components/tasks/task-sheet.tsx +415 -0
- package/src/components/ui/avatar.tsx +109 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/sonner.tsx +22 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +56 -0
- package/src/components/usage/usage-list.tsx +105 -0
- package/src/components/webhooks/webhook-list.tsx +166 -0
- package/src/components/webhooks/webhook-sheet.tsx +402 -0
- package/src/hooks/use-auto-resize.ts +20 -0
- package/src/hooks/use-media-query.ts +21 -0
- package/src/hooks/use-speech-recognition.ts +83 -0
- package/src/instrumentation.ts +8 -0
- package/src/lib/agents.ts +13 -0
- package/src/lib/api-client.ts +100 -0
- package/src/lib/chat.ts +60 -0
- package/src/lib/memory.ts +42 -0
- package/src/lib/openclaw-endpoint.test.ts +48 -0
- package/src/lib/openclaw-endpoint.ts +67 -0
- package/src/lib/provider-config.ts +13 -0
- package/src/lib/providers/anthropic.ts +135 -0
- package/src/lib/providers/claude-cli.ts +202 -0
- package/src/lib/providers/codex-cli.ts +260 -0
- package/src/lib/providers/index.ts +351 -0
- package/src/lib/providers/ollama.ts +131 -0
- package/src/lib/providers/openai.ts +164 -0
- package/src/lib/providers/openclaw.ts +330 -0
- package/src/lib/providers/opencode-cli.ts +164 -0
- package/src/lib/runtime-loop.ts +15 -0
- package/src/lib/schedule-dedupe.test.ts +84 -0
- package/src/lib/schedule-dedupe.ts +174 -0
- package/src/lib/schedule-name.ts +62 -0
- package/src/lib/schedules.ts +16 -0
- package/src/lib/server/agent-registry.ts +70 -0
- package/src/lib/server/api-routes.test.ts +362 -0
- package/src/lib/server/autonomy-contract.ts +200 -0
- package/src/lib/server/build-llm.ts +155 -0
- package/src/lib/server/capability-router.test.ts +21 -0
- package/src/lib/server/capability-router.ts +172 -0
- package/src/lib/server/chat-execution.ts +894 -0
- package/src/lib/server/clawhub-client.test.ts +161 -0
- package/src/lib/server/clawhub-client.ts +26 -0
- package/src/lib/server/connectors/connector-routing.test.ts +243 -0
- package/src/lib/server/connectors/discord.ts +116 -0
- package/src/lib/server/connectors/googlechat.ts +66 -0
- package/src/lib/server/connectors/manager.ts +559 -0
- package/src/lib/server/connectors/matrix.ts +78 -0
- package/src/lib/server/connectors/media.ts +149 -0
- package/src/lib/server/connectors/openclaw.test.ts +375 -0
- package/src/lib/server/connectors/openclaw.ts +1132 -0
- package/src/lib/server/connectors/signal.ts +183 -0
- package/src/lib/server/connectors/slack.ts +258 -0
- package/src/lib/server/connectors/teams.ts +94 -0
- package/src/lib/server/connectors/telegram.ts +221 -0
- package/src/lib/server/connectors/types.ts +62 -0
- package/src/lib/server/connectors/whatsapp.ts +349 -0
- package/src/lib/server/context-manager.ts +232 -0
- package/src/lib/server/cost.ts +31 -0
- package/src/lib/server/daemon-state.ts +354 -0
- package/src/lib/server/data-dir.ts +3 -0
- package/src/lib/server/embeddings.ts +111 -0
- package/src/lib/server/execution-log.ts +257 -0
- package/src/lib/server/gateway/protocol.test.ts +54 -0
- package/src/lib/server/gateway/protocol.ts +114 -0
- package/src/lib/server/heartbeat-service.ts +366 -0
- package/src/lib/server/knowledge-db.test.ts +441 -0
- package/src/lib/server/logger.ts +47 -0
- package/src/lib/server/main-agent-loop.ts +1017 -0
- package/src/lib/server/mcp-client.test.ts +342 -0
- package/src/lib/server/mcp-client.ts +130 -0
- package/src/lib/server/memory-db.ts +1078 -0
- package/src/lib/server/memory-graph.test.ts +153 -0
- package/src/lib/server/memory-graph.ts +138 -0
- package/src/lib/server/openclaw-health.ts +245 -0
- package/src/lib/server/orchestrator-lg.ts +431 -0
- package/src/lib/server/orchestrator.ts +364 -0
- package/src/lib/server/playwright-proxy.mjs +70 -0
- package/src/lib/server/plugins.ts +229 -0
- package/src/lib/server/process-manager.ts +327 -0
- package/src/lib/server/provider-health.ts +113 -0
- package/src/lib/server/queue.ts +859 -0
- package/src/lib/server/runtime-settings.ts +119 -0
- package/src/lib/server/scheduler.ts +196 -0
- package/src/lib/server/session-mailbox.ts +129 -0
- package/src/lib/server/session-run-manager.ts +512 -0
- package/src/lib/server/session-tools/connector.ts +124 -0
- package/src/lib/server/session-tools/context-mgmt.ts +103 -0
- package/src/lib/server/session-tools/context.ts +114 -0
- package/src/lib/server/session-tools/crud.ts +673 -0
- package/src/lib/server/session-tools/delegate.ts +708 -0
- package/src/lib/server/session-tools/file.ts +264 -0
- package/src/lib/server/session-tools/index.ts +164 -0
- package/src/lib/server/session-tools/memory.ts +230 -0
- package/src/lib/server/session-tools/session-info.ts +422 -0
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
- package/src/lib/server/session-tools/shell.ts +171 -0
- package/src/lib/server/session-tools/web.ts +408 -0
- package/src/lib/server/session-tools.ts +9 -0
- package/src/lib/server/skills-normalize.ts +130 -0
- package/src/lib/server/storage-mcp.test.ts +161 -0
- package/src/lib/server/storage.ts +670 -0
- package/src/lib/server/stream-agent-chat.ts +571 -0
- package/src/lib/server/task-reports.ts +122 -0
- package/src/lib/server/task-result.ts +161 -0
- package/src/lib/server/task-validation.test.ts +27 -0
- package/src/lib/server/task-validation.ts +90 -0
- package/src/lib/server/tool-capability-policy.test.ts +58 -0
- package/src/lib/server/tool-capability-policy.ts +262 -0
- package/src/lib/sessions.ts +68 -0
- package/src/lib/tasks.ts +20 -0
- package/src/lib/tts.ts +42 -0
- package/src/lib/upload.ts +10 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy.ts +43 -0
- package/src/stores/use-app-store.ts +468 -0
- package/src/stores/use-chat-store.ts +323 -0
- package/src/types/index.ts +621 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,894 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import {
|
|
3
|
+
loadSessions,
|
|
4
|
+
saveSessions,
|
|
5
|
+
loadCredentials,
|
|
6
|
+
decryptKey,
|
|
7
|
+
getSessionMessages,
|
|
8
|
+
loadAgents,
|
|
9
|
+
loadSkills,
|
|
10
|
+
loadSettings,
|
|
11
|
+
loadUsage,
|
|
12
|
+
active,
|
|
13
|
+
} from './storage'
|
|
14
|
+
import { getProvider } from '@/lib/providers'
|
|
15
|
+
import { log } from './logger'
|
|
16
|
+
import { logExecution } from './execution-log'
|
|
17
|
+
import { streamAgentChat } from './stream-agent-chat'
|
|
18
|
+
import { buildSessionTools } from './session-tools'
|
|
19
|
+
import { stripMainLoopMetaForPersistence } from './main-agent-loop'
|
|
20
|
+
import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
|
|
21
|
+
import { getMemoryDb } from './memory-db'
|
|
22
|
+
import { routeTaskIntent } from './capability-router'
|
|
23
|
+
import { resolveConcreteToolPolicyBlock, resolveSessionToolPolicy } from './tool-capability-policy'
|
|
24
|
+
import type { MessageToolEvent, SSEEvent } from '@/types'
|
|
25
|
+
import { markProviderFailure, markProviderSuccess, rankDelegatesByHealth } from './provider-health'
|
|
26
|
+
|
|
27
|
+
const CLI_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli'])
|
|
28
|
+
type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli'
|
|
29
|
+
|
|
30
|
+
interface SessionWithTools {
|
|
31
|
+
tools?: string[] | null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface SessionWithCredentials {
|
|
35
|
+
credentialId?: string | null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface ProviderApiKeyConfig {
|
|
39
|
+
requiresApiKey?: boolean
|
|
40
|
+
optionalApiKey?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ExecuteChatTurnInput {
|
|
44
|
+
sessionId: string
|
|
45
|
+
message: string
|
|
46
|
+
imagePath?: string
|
|
47
|
+
imageUrl?: string
|
|
48
|
+
internal?: boolean
|
|
49
|
+
source?: string
|
|
50
|
+
runId?: string
|
|
51
|
+
signal?: AbortSignal
|
|
52
|
+
onEvent?: (event: SSEEvent) => void
|
|
53
|
+
modelOverride?: string
|
|
54
|
+
heartbeatConfig?: { ackMaxChars: number; showOk: boolean; showAlerts: boolean; target: string | null }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ExecuteChatTurnResult {
|
|
58
|
+
runId?: string
|
|
59
|
+
sessionId: string
|
|
60
|
+
text: string
|
|
61
|
+
persisted: boolean
|
|
62
|
+
toolEvents: MessageToolEvent[]
|
|
63
|
+
error?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function extractEventJson(line: string): SSEEvent | null {
|
|
67
|
+
if (!line.startsWith('data: ')) return null
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(line.slice(6).trim()) as SSEEvent
|
|
70
|
+
} catch {
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
|
|
76
|
+
if (ev.t === 'tool_call') {
|
|
77
|
+
bag.push({
|
|
78
|
+
name: ev.toolName || 'unknown',
|
|
79
|
+
input: ev.toolInput || '',
|
|
80
|
+
})
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
if (ev.t === 'tool_result') {
|
|
84
|
+
const idx = bag.findLastIndex((e) => e.name === (ev.toolName || 'unknown') && !e.output)
|
|
85
|
+
if (idx === -1) return
|
|
86
|
+
const output = ev.toolOutput || ''
|
|
87
|
+
const isError = /^(Error:|error:)/i.test(output.trim())
|
|
88
|
+
|| output.includes('ECONNREFUSED')
|
|
89
|
+
|| output.includes('ETIMEDOUT')
|
|
90
|
+
|| output.includes('Error:')
|
|
91
|
+
bag[idx] = {
|
|
92
|
+
...bag[idx],
|
|
93
|
+
output,
|
|
94
|
+
error: isError || undefined,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function requestedToolNamesFromMessage(message: string): string[] {
|
|
100
|
+
const lower = message.toLowerCase()
|
|
101
|
+
const candidates = [
|
|
102
|
+
'delegate_to_claude_code',
|
|
103
|
+
'delegate_to_codex_cli',
|
|
104
|
+
'delegate_to_opencode_cli',
|
|
105
|
+
'connector_message_tool',
|
|
106
|
+
'sessions_tool',
|
|
107
|
+
'whoami_tool',
|
|
108
|
+
'search_history_tool',
|
|
109
|
+
'manage_agents',
|
|
110
|
+
'manage_tasks',
|
|
111
|
+
'manage_schedules',
|
|
112
|
+
'manage_documents',
|
|
113
|
+
'manage_webhooks',
|
|
114
|
+
'manage_skills',
|
|
115
|
+
'manage_connectors',
|
|
116
|
+
'manage_sessions',
|
|
117
|
+
'manage_secrets',
|
|
118
|
+
'memory_tool',
|
|
119
|
+
'browser',
|
|
120
|
+
'web_search',
|
|
121
|
+
'web_fetch',
|
|
122
|
+
'execute_command',
|
|
123
|
+
'read_file',
|
|
124
|
+
'write_file',
|
|
125
|
+
'list_files',
|
|
126
|
+
'copy_file',
|
|
127
|
+
'move_file',
|
|
128
|
+
'delete_file',
|
|
129
|
+
'edit_file',
|
|
130
|
+
'send_file',
|
|
131
|
+
'process_tool',
|
|
132
|
+
]
|
|
133
|
+
return candidates.filter((name) => lower.includes(name.toLowerCase()))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parseKeyValueArgs(raw: string): Record<string, string> {
|
|
137
|
+
const out: Record<string, string> = {}
|
|
138
|
+
const regex = /([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*("([^"]*)"|'([^']*)'|[^\s,]+)/g
|
|
139
|
+
let match: RegExpExecArray | null = null
|
|
140
|
+
while ((match = regex.exec(raw)) !== null) {
|
|
141
|
+
const key = match[1]
|
|
142
|
+
const value = match[3] ?? match[4] ?? match[2] ?? ''
|
|
143
|
+
out[key] = value.replace(/^['"]|['"]$/g, '').trim()
|
|
144
|
+
}
|
|
145
|
+
return out
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function extractConnectorMessageArgs(message: string): {
|
|
149
|
+
action: 'list_running' | 'list_targets' | 'send'
|
|
150
|
+
platform?: string
|
|
151
|
+
connectorId?: string
|
|
152
|
+
to?: string
|
|
153
|
+
message?: string
|
|
154
|
+
imageUrl?: string
|
|
155
|
+
fileUrl?: string
|
|
156
|
+
mediaPath?: string
|
|
157
|
+
mimeType?: string
|
|
158
|
+
fileName?: string
|
|
159
|
+
caption?: string
|
|
160
|
+
} | null {
|
|
161
|
+
if (!message.toLowerCase().includes('connector_message_tool')) return null
|
|
162
|
+
const parsed = parseKeyValueArgs(message)
|
|
163
|
+
|
|
164
|
+
let payload = parsed.message
|
|
165
|
+
if (!payload) {
|
|
166
|
+
const quoted = message.match(/message\s*=\s*("(.*?)"|'(.*?)')/i)
|
|
167
|
+
if (quoted) payload = (quoted[2] || quoted[3] || '').trim()
|
|
168
|
+
}
|
|
169
|
+
if (!payload) {
|
|
170
|
+
const raw = message.match(/message\s*=\s*([^\n]+)/i)
|
|
171
|
+
if (raw?.[1]) {
|
|
172
|
+
payload = raw[1]
|
|
173
|
+
.replace(/\b(Return|Output|Then|Respond)\b[\s\S]*$/i, '')
|
|
174
|
+
.trim()
|
|
175
|
+
.replace(/^['"]|['"]$/g, '')
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const actionRaw = (parsed.action || 'send').toLowerCase()
|
|
180
|
+
const action = actionRaw === 'list_running' || actionRaw === 'list_targets' || actionRaw === 'send'
|
|
181
|
+
? actionRaw
|
|
182
|
+
: 'send'
|
|
183
|
+
const args: {
|
|
184
|
+
action: 'list_running' | 'list_targets' | 'send'
|
|
185
|
+
platform?: string
|
|
186
|
+
connectorId?: string
|
|
187
|
+
to?: string
|
|
188
|
+
message?: string
|
|
189
|
+
imageUrl?: string
|
|
190
|
+
fileUrl?: string
|
|
191
|
+
mediaPath?: string
|
|
192
|
+
mimeType?: string
|
|
193
|
+
fileName?: string
|
|
194
|
+
caption?: string
|
|
195
|
+
} = { action }
|
|
196
|
+
const quoted = (key: string): string | undefined => {
|
|
197
|
+
const m = message.match(new RegExp(`${key}\\s*=\\s*(\"([^\"]*)\"|'([^']*)')`, 'i'))
|
|
198
|
+
return (m?.[2] || m?.[3] || '').trim() || undefined
|
|
199
|
+
}
|
|
200
|
+
if (parsed.platform) args.platform = parsed.platform
|
|
201
|
+
if (parsed.connectorId) args.connectorId = parsed.connectorId
|
|
202
|
+
if (parsed.to) args.to = parsed.to
|
|
203
|
+
if (payload) args.message = payload
|
|
204
|
+
args.imageUrl = parsed.imageUrl || quoted('imageUrl')
|
|
205
|
+
args.fileUrl = parsed.fileUrl || quoted('fileUrl')
|
|
206
|
+
args.mediaPath = parsed.mediaPath || quoted('mediaPath')
|
|
207
|
+
args.mimeType = parsed.mimeType || quoted('mimeType')
|
|
208
|
+
args.fileName = parsed.fileName || quoted('fileName')
|
|
209
|
+
args.caption = parsed.caption || quoted('caption')
|
|
210
|
+
return args
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function extractDelegationTask(message: string, toolName: string): string | null {
|
|
214
|
+
if (!message.toLowerCase().includes(toolName.toLowerCase())) return null
|
|
215
|
+
const patterns = [
|
|
216
|
+
/task\s+exactly\s*:\s*"([^"]+)"/i,
|
|
217
|
+
/task\s+exactly\s*:\s*'([^']+)'/i,
|
|
218
|
+
/task\s+exactly\s*:\s*([^\n]+?)(?:\.\s|$)/i,
|
|
219
|
+
/task\s*:\s*"([^"]+)"/i,
|
|
220
|
+
/task\s*:\s*'([^']+)'/i,
|
|
221
|
+
/task\s*:\s*([^\n]+?)(?:\.\s|$)/i,
|
|
222
|
+
]
|
|
223
|
+
for (const re of patterns) {
|
|
224
|
+
const m = message.match(re)
|
|
225
|
+
const task = (m?.[1] || '').trim()
|
|
226
|
+
if (task) return task
|
|
227
|
+
}
|
|
228
|
+
return null
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function hasToolEnabled(session: SessionWithTools, toolName: string): boolean {
|
|
232
|
+
return Array.isArray(session?.tools) && session.tools.includes(toolName)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function enabledDelegationTools(session: SessionWithTools): DelegateTool[] {
|
|
236
|
+
const tools: DelegateTool[] = []
|
|
237
|
+
if (hasToolEnabled(session, 'claude_code')) tools.push('delegate_to_claude_code')
|
|
238
|
+
if (hasToolEnabled(session, 'codex_cli')) tools.push('delegate_to_codex_cli')
|
|
239
|
+
if (hasToolEnabled(session, 'opencode_cli')) tools.push('delegate_to_opencode_cli')
|
|
240
|
+
return tools
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function parseUsdLimit(value: unknown): number | null {
|
|
244
|
+
const parsed = typeof value === 'number'
|
|
245
|
+
? value
|
|
246
|
+
: typeof value === 'string'
|
|
247
|
+
? Number.parseFloat(value)
|
|
248
|
+
: Number.NaN
|
|
249
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return null
|
|
250
|
+
return Math.max(0.01, Math.min(1_000_000, parsed))
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function getTodaySpendUsd(): number {
|
|
254
|
+
const usage = loadUsage()
|
|
255
|
+
const dayStart = new Date()
|
|
256
|
+
dayStart.setHours(0, 0, 0, 0)
|
|
257
|
+
const minTs = dayStart.getTime()
|
|
258
|
+
let total = 0
|
|
259
|
+
for (const records of Object.values(usage)) {
|
|
260
|
+
for (const record of records || []) {
|
|
261
|
+
const ts = typeof (record as any)?.timestamp === 'number' ? (record as any).timestamp : 0
|
|
262
|
+
if (ts < minTs) continue
|
|
263
|
+
const cost = typeof (record as any)?.estimatedCost === 'number' ? (record as any).estimatedCost : 0
|
|
264
|
+
if (Number.isFinite(cost) && cost > 0) total += cost
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return total
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function findFirstUrl(text: string): string | null {
|
|
271
|
+
const m = text.match(/https?:\/\/[^\s<>"')]+/i)
|
|
272
|
+
return m?.[0] || null
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function syncSessionFromAgent(sessionId: string): void {
|
|
276
|
+
const sessions = loadSessions()
|
|
277
|
+
const session = sessions[sessionId]
|
|
278
|
+
if (!session?.agentId) return
|
|
279
|
+
const agents = loadAgents()
|
|
280
|
+
const agent = agents[session.agentId]
|
|
281
|
+
if (!agent) return
|
|
282
|
+
|
|
283
|
+
let changed = false
|
|
284
|
+
if (agent.provider && agent.provider !== session.provider) { session.provider = agent.provider; changed = true }
|
|
285
|
+
if (agent.model !== undefined && agent.model !== session.model) { session.model = agent.model; changed = true }
|
|
286
|
+
if (agent.credentialId !== undefined && agent.credentialId !== session.credentialId) { session.credentialId = agent.credentialId ?? null; changed = true }
|
|
287
|
+
if (agent.apiEndpoint !== undefined) {
|
|
288
|
+
const normalized = normalizeProviderEndpoint(agent.provider, agent.apiEndpoint ?? null)
|
|
289
|
+
if (normalized !== session.apiEndpoint) { session.apiEndpoint = normalized; changed = true }
|
|
290
|
+
}
|
|
291
|
+
if (!Array.isArray(session.tools)) {
|
|
292
|
+
session.tools = Array.isArray(agent.tools) ? [...agent.tools] : []
|
|
293
|
+
changed = true
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (changed) {
|
|
297
|
+
sessions[sessionId] = session
|
|
298
|
+
saveSessions(sessions)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function buildAgentSystemPrompt(session: any): string | undefined {
|
|
303
|
+
if (!session.agentId) return undefined
|
|
304
|
+
const agents = loadAgents()
|
|
305
|
+
const agent = agents[session.agentId]
|
|
306
|
+
if (!agent?.systemPrompt && !agent?.soul) return undefined
|
|
307
|
+
|
|
308
|
+
const settings = loadSettings()
|
|
309
|
+
const parts: string[] = []
|
|
310
|
+
if (settings.userPrompt) parts.push(settings.userPrompt)
|
|
311
|
+
if (agent.soul) parts.push(agent.soul)
|
|
312
|
+
if (agent.systemPrompt) parts.push(agent.systemPrompt)
|
|
313
|
+
if (agent.skillIds?.length) {
|
|
314
|
+
const allSkills = loadSkills()
|
|
315
|
+
for (const skillId of agent.skillIds) {
|
|
316
|
+
const skill = allSkills[skillId]
|
|
317
|
+
if (skill?.content) parts.push(`## Skill: ${skill.name}\n${skill.content}`)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return parts.join('\n\n')
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function resolveApiKeyForSession(session: SessionWithCredentials, provider: ProviderApiKeyConfig): string | null {
|
|
324
|
+
if (provider.requiresApiKey) {
|
|
325
|
+
if (!session.credentialId) throw new Error('No API key configured for this session')
|
|
326
|
+
const creds = loadCredentials()
|
|
327
|
+
const cred = creds[session.credentialId]
|
|
328
|
+
if (!cred) throw new Error('API key not found. Please add one in Settings.')
|
|
329
|
+
return decryptKey(cred.encryptedKey)
|
|
330
|
+
}
|
|
331
|
+
if (provider.optionalApiKey && session.credentialId) {
|
|
332
|
+
const creds = loadCredentials()
|
|
333
|
+
const cred = creds[session.credentialId]
|
|
334
|
+
if (cred) {
|
|
335
|
+
try { return decryptKey(cred.encryptedKey) } catch { return null }
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return null
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function classifyHeartbeatResponse(text: string, ackMaxChars: number): 'suppress' | 'strip' | 'keep' {
|
|
342
|
+
const trimmed = text.trim()
|
|
343
|
+
if (trimmed === 'HEARTBEAT_OK') return 'suppress'
|
|
344
|
+
const stripped = trimmed.replace(/HEARTBEAT_OK/gi, '').trim()
|
|
345
|
+
if (!stripped) return 'suppress'
|
|
346
|
+
if (stripped.length <= ackMaxChars) return 'suppress'
|
|
347
|
+
return stripped.length < trimmed.length ? 'strip' : 'keep'
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const AUTO_MEMORY_MIN_INTERVAL_MS = 45 * 60 * 1000
|
|
351
|
+
|
|
352
|
+
function normalizeMemoryText(value: string): string {
|
|
353
|
+
return (value || '').replace(/\s+/g, ' ').trim()
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function shouldStoreAutoMemoryNote(opts: {
|
|
357
|
+
session: any
|
|
358
|
+
source: string
|
|
359
|
+
internal: boolean
|
|
360
|
+
message: string
|
|
361
|
+
response: string
|
|
362
|
+
now: number
|
|
363
|
+
}): boolean {
|
|
364
|
+
const { session, source, internal, message, response, now } = opts
|
|
365
|
+
if (internal) return false
|
|
366
|
+
if (source !== 'chat' && source !== 'connector') return false
|
|
367
|
+
if (!session?.agentId) return false
|
|
368
|
+
if (!Array.isArray(session.tools) || !session.tools.includes('memory')) return false
|
|
369
|
+
const msg = (message || '').trim()
|
|
370
|
+
const resp = (response || '').trim()
|
|
371
|
+
if (msg.length < 20 || resp.length < 40) return false
|
|
372
|
+
if (/^(ok|okay|cool|thanks|thx|got it|nice)[.! ]*$/i.test(msg)) return false
|
|
373
|
+
if (resp === 'HEARTBEAT_OK') return false
|
|
374
|
+
const last = typeof session.lastAutoMemoryAt === 'number' ? session.lastAutoMemoryAt : 0
|
|
375
|
+
if (last > 0 && now - last < AUTO_MEMORY_MIN_INTERVAL_MS) return false
|
|
376
|
+
return true
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function storeAutoMemoryNote(opts: {
|
|
380
|
+
session: any
|
|
381
|
+
message: string
|
|
382
|
+
response: string
|
|
383
|
+
source: string
|
|
384
|
+
now: number
|
|
385
|
+
}): string | null {
|
|
386
|
+
const { session, message, response, source, now } = opts
|
|
387
|
+
try {
|
|
388
|
+
const db = getMemoryDb()
|
|
389
|
+
const compactMessage = message.replace(/\s+/g, ' ').trim().slice(0, 220)
|
|
390
|
+
const compactResponse = response.replace(/\s+/g, ' ').trim().slice(0, 700)
|
|
391
|
+
const title = `[auto] ${compactMessage.slice(0, 90)}`
|
|
392
|
+
const content = [
|
|
393
|
+
`source: ${source}`,
|
|
394
|
+
`user_request: ${compactMessage}`,
|
|
395
|
+
`assistant_outcome: ${compactResponse}`,
|
|
396
|
+
].join('\n')
|
|
397
|
+
const latest = db.getLatestBySessionCategory?.(session.id, 'execution')
|
|
398
|
+
if (latest) {
|
|
399
|
+
const sameTitle = normalizeMemoryText(latest.title) === normalizeMemoryText(title)
|
|
400
|
+
const sameContent = normalizeMemoryText(latest.content) === normalizeMemoryText(content)
|
|
401
|
+
if (sameTitle && sameContent) {
|
|
402
|
+
session.lastAutoMemoryAt = now
|
|
403
|
+
return latest.id
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const created = db.add({
|
|
407
|
+
agentId: session.agentId,
|
|
408
|
+
sessionId: session.id,
|
|
409
|
+
category: 'execution',
|
|
410
|
+
title,
|
|
411
|
+
content,
|
|
412
|
+
} as any)
|
|
413
|
+
session.lastAutoMemoryAt = now
|
|
414
|
+
return created?.id || null
|
|
415
|
+
} catch {
|
|
416
|
+
return null
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promise<ExecuteChatTurnResult> {
|
|
421
|
+
const {
|
|
422
|
+
sessionId,
|
|
423
|
+
message,
|
|
424
|
+
imagePath,
|
|
425
|
+
imageUrl,
|
|
426
|
+
internal = false,
|
|
427
|
+
runId,
|
|
428
|
+
source = 'chat',
|
|
429
|
+
onEvent,
|
|
430
|
+
signal,
|
|
431
|
+
} = input
|
|
432
|
+
|
|
433
|
+
syncSessionFromAgent(sessionId)
|
|
434
|
+
|
|
435
|
+
const sessions = loadSessions()
|
|
436
|
+
const session = sessions[sessionId]
|
|
437
|
+
if (!session) throw new Error(`Session not found: ${sessionId}`)
|
|
438
|
+
|
|
439
|
+
const appSettings = loadSettings()
|
|
440
|
+
const toolPolicy = resolveSessionToolPolicy(session.tools, appSettings)
|
|
441
|
+
const isHeartbeatRun = internal && source === 'heartbeat'
|
|
442
|
+
const heartbeatStatus = session.mainLoopState?.status || 'idle'
|
|
443
|
+
const heartbeatStatusOnly = isHeartbeatRun
|
|
444
|
+
&& (session.name !== '__main__' || heartbeatStatus === 'ok' || heartbeatStatus === 'idle')
|
|
445
|
+
const toolsForRun = heartbeatStatusOnly ? [] : toolPolicy.enabledTools
|
|
446
|
+
let sessionForRun = toolsForRun === session.tools
|
|
447
|
+
? session
|
|
448
|
+
: { ...session, tools: toolsForRun }
|
|
449
|
+
|
|
450
|
+
// Apply model override for heartbeat runs (cheaper model)
|
|
451
|
+
if (isHeartbeatRun && input.modelOverride) {
|
|
452
|
+
sessionForRun = { ...sessionForRun, model: input.modelOverride }
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (!heartbeatStatusOnly && toolPolicy.blockedTools.length > 0) {
|
|
456
|
+
const blockedSummary = toolPolicy.blockedTools
|
|
457
|
+
.map((entry) => `${entry.tool} (${entry.reason})`)
|
|
458
|
+
.join(', ')
|
|
459
|
+
onEvent?.({ t: 'err', text: `Capability policy blocked tools for this run: ${blockedSummary}` })
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const dailySpendLimitUsd = parseUsdLimit(appSettings.safetyMaxDailySpendUsd)
|
|
463
|
+
if (dailySpendLimitUsd !== null) {
|
|
464
|
+
const todaySpendUsd = getTodaySpendUsd()
|
|
465
|
+
if (todaySpendUsd >= dailySpendLimitUsd) {
|
|
466
|
+
const spendError = `Safety budget reached: today's spend is $${todaySpendUsd.toFixed(4)} (limit $${dailySpendLimitUsd.toFixed(4)}). Increase safetyMaxDailySpendUsd to continue autonomous runs.`
|
|
467
|
+
onEvent?.({ t: 'err', text: spendError })
|
|
468
|
+
|
|
469
|
+
let persisted = false
|
|
470
|
+
if (!internal) {
|
|
471
|
+
session.messages.push({
|
|
472
|
+
role: 'assistant',
|
|
473
|
+
text: spendError,
|
|
474
|
+
time: Date.now(),
|
|
475
|
+
})
|
|
476
|
+
session.lastActiveAt = Date.now()
|
|
477
|
+
saveSessions(sessions)
|
|
478
|
+
persisted = true
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
runId,
|
|
483
|
+
sessionId,
|
|
484
|
+
text: spendError,
|
|
485
|
+
persisted,
|
|
486
|
+
toolEvents: [],
|
|
487
|
+
error: spendError,
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Log the trigger
|
|
493
|
+
logExecution(sessionId, 'trigger', `${source} message received`, {
|
|
494
|
+
runId,
|
|
495
|
+
agentId: session.agentId,
|
|
496
|
+
detail: {
|
|
497
|
+
source,
|
|
498
|
+
internal,
|
|
499
|
+
provider: session.provider,
|
|
500
|
+
model: session.model,
|
|
501
|
+
messagePreview: message.slice(0, 200),
|
|
502
|
+
hasImage: !!(imagePath || imageUrl),
|
|
503
|
+
},
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
const providerType = session.provider || 'claude-cli'
|
|
507
|
+
const provider = getProvider(providerType)
|
|
508
|
+
if (!provider) throw new Error(`Unknown provider: ${providerType}`)
|
|
509
|
+
|
|
510
|
+
if (providerType === 'claude-cli' && !fs.existsSync(session.cwd)) {
|
|
511
|
+
throw new Error(`Directory not found: ${session.cwd}`)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const apiKey = resolveApiKeyForSession(session, provider)
|
|
515
|
+
|
|
516
|
+
if (!internal) {
|
|
517
|
+
session.messages.push({
|
|
518
|
+
role: 'user',
|
|
519
|
+
text: message,
|
|
520
|
+
time: Date.now(),
|
|
521
|
+
imagePath: imagePath || undefined,
|
|
522
|
+
imageUrl: imageUrl || undefined,
|
|
523
|
+
})
|
|
524
|
+
session.lastActiveAt = Date.now()
|
|
525
|
+
saveSessions(sessions)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const systemPrompt = buildAgentSystemPrompt(session)
|
|
529
|
+
const toolEvents: MessageToolEvent[] = []
|
|
530
|
+
const streamErrors: string[] = []
|
|
531
|
+
|
|
532
|
+
const emit = (ev: SSEEvent) => {
|
|
533
|
+
if (ev.t === 'err' && typeof ev.text === 'string') {
|
|
534
|
+
const trimmed = ev.text.trim()
|
|
535
|
+
if (trimmed) {
|
|
536
|
+
streamErrors.push(trimmed)
|
|
537
|
+
if (streamErrors.length > 8) streamErrors.shift()
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
collectToolEvent(ev, toolEvents)
|
|
541
|
+
onEvent?.(ev)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const parseAndEmit = (raw: string) => {
|
|
545
|
+
const lines = raw.split('\n').filter(Boolean)
|
|
546
|
+
for (const line of lines) {
|
|
547
|
+
const ev = extractEventJson(line)
|
|
548
|
+
if (ev) emit(ev)
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
let fullResponse = ''
|
|
553
|
+
let errorMessage: string | undefined
|
|
554
|
+
|
|
555
|
+
const abortController = new AbortController()
|
|
556
|
+
const abortFromOutside = () => abortController.abort()
|
|
557
|
+
if (signal) {
|
|
558
|
+
if (signal.aborted) abortController.abort()
|
|
559
|
+
else signal.addEventListener('abort', abortFromOutside)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
active.set(sessionId, {
|
|
563
|
+
runId: runId || null,
|
|
564
|
+
source,
|
|
565
|
+
kill: () => abortController.abort(),
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
try {
|
|
569
|
+
const hasTools = !!sessionForRun.tools?.length && !CLI_PROVIDER_IDS.has(providerType)
|
|
570
|
+
fullResponse = hasTools
|
|
571
|
+
? (await streamAgentChat({
|
|
572
|
+
session: sessionForRun,
|
|
573
|
+
message,
|
|
574
|
+
imagePath,
|
|
575
|
+
apiKey,
|
|
576
|
+
systemPrompt,
|
|
577
|
+
write: (raw) => parseAndEmit(raw),
|
|
578
|
+
history: getSessionMessages(sessionId),
|
|
579
|
+
signal: abortController.signal,
|
|
580
|
+
})).fullText
|
|
581
|
+
: await provider.handler.streamChat({
|
|
582
|
+
session: sessionForRun,
|
|
583
|
+
message,
|
|
584
|
+
imagePath,
|
|
585
|
+
apiKey,
|
|
586
|
+
systemPrompt,
|
|
587
|
+
write: (raw: string) => parseAndEmit(raw),
|
|
588
|
+
active,
|
|
589
|
+
loadHistory: getSessionMessages,
|
|
590
|
+
})
|
|
591
|
+
} catch (err: any) {
|
|
592
|
+
errorMessage = err?.message || String(err)
|
|
593
|
+
const failureText = errorMessage || 'Run failed.'
|
|
594
|
+
markProviderFailure(providerType, failureText)
|
|
595
|
+
emit({ t: 'err', text: failureText })
|
|
596
|
+
log.error('chat-run', `Run failed for session ${sessionId}`, {
|
|
597
|
+
runId,
|
|
598
|
+
source,
|
|
599
|
+
internal,
|
|
600
|
+
error: failureText,
|
|
601
|
+
})
|
|
602
|
+
} finally {
|
|
603
|
+
active.delete(sessionId)
|
|
604
|
+
if (signal) signal.removeEventListener('abort', abortFromOutside)
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (!errorMessage) {
|
|
608
|
+
markProviderSuccess(providerType)
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const requestedToolNames = (!internal && source === 'chat')
|
|
612
|
+
? requestedToolNamesFromMessage(message)
|
|
613
|
+
: []
|
|
614
|
+
const routingDecision = (!internal && source === 'chat')
|
|
615
|
+
? routeTaskIntent(message, toolsForRun, appSettings)
|
|
616
|
+
: null
|
|
617
|
+
const calledNames = new Set((toolEvents || []).map((t) => t.name))
|
|
618
|
+
|
|
619
|
+
const invokeSessionTool = async (toolName: string, args: Record<string, unknown>, failurePrefix: string): Promise<boolean> => {
|
|
620
|
+
const blockedReason = resolveConcreteToolPolicyBlock(toolName, toolPolicy, appSettings)
|
|
621
|
+
if (blockedReason) {
|
|
622
|
+
emit({ t: 'err', text: `Capability policy blocked tool invocation "${toolName}": ${blockedReason}` })
|
|
623
|
+
return false
|
|
624
|
+
}
|
|
625
|
+
if (
|
|
626
|
+
appSettings.safetyRequireApprovalForOutbound === true
|
|
627
|
+
&& toolName === 'connector_message_tool'
|
|
628
|
+
&& source !== 'chat'
|
|
629
|
+
) {
|
|
630
|
+
emit({ t: 'err', text: 'Outbound connector messaging requires explicit user approval.' })
|
|
631
|
+
return false
|
|
632
|
+
}
|
|
633
|
+
const agent = session.agentId ? loadAgents()[session.agentId] : null
|
|
634
|
+
const { tools, cleanup } = await buildSessionTools(session.cwd, sessionForRun.tools || [], {
|
|
635
|
+
agentId: session.agentId || null,
|
|
636
|
+
sessionId,
|
|
637
|
+
platformAssignScope: agent?.platformAssignScope || 'self',
|
|
638
|
+
mcpServerIds: agent?.mcpServerIds,
|
|
639
|
+
mcpDisabledTools: agent?.mcpDisabledTools,
|
|
640
|
+
})
|
|
641
|
+
try {
|
|
642
|
+
const selectedTool = tools.find((t: any) => t?.name === toolName) as any
|
|
643
|
+
if (!selectedTool?.invoke) return false
|
|
644
|
+
const toolInput = JSON.stringify(args)
|
|
645
|
+
emit({ t: 'tool_call', toolName, toolInput })
|
|
646
|
+
const toolOutput = await selectedTool.invoke(args)
|
|
647
|
+
const outputText = typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput)
|
|
648
|
+
emit({ t: 'tool_result', toolName, toolOutput: outputText })
|
|
649
|
+
if (outputText?.trim()) fullResponse = outputText.trim()
|
|
650
|
+
calledNames.add(toolName)
|
|
651
|
+
return true
|
|
652
|
+
} catch (forceErr: any) {
|
|
653
|
+
emit({ t: 'err', text: `${failurePrefix}: ${forceErr?.message || String(forceErr)}` })
|
|
654
|
+
return false
|
|
655
|
+
} finally {
|
|
656
|
+
await cleanup()
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (requestedToolNames.includes('connector_message_tool') && !calledNames.has('connector_message_tool')) {
|
|
661
|
+
const forcedArgs = extractConnectorMessageArgs(message)
|
|
662
|
+
if (forcedArgs) {
|
|
663
|
+
await invokeSessionTool(
|
|
664
|
+
'connector_message_tool',
|
|
665
|
+
forcedArgs as unknown as Record<string, unknown>,
|
|
666
|
+
'Forced connector_message_tool invocation failed',
|
|
667
|
+
)
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const forcedDelegationTools: Array<'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli'> = [
|
|
672
|
+
'delegate_to_claude_code',
|
|
673
|
+
'delegate_to_codex_cli',
|
|
674
|
+
'delegate_to_opencode_cli',
|
|
675
|
+
]
|
|
676
|
+
for (const toolName of forcedDelegationTools) {
|
|
677
|
+
if (!requestedToolNames.includes(toolName)) continue
|
|
678
|
+
if (calledNames.has(toolName)) continue
|
|
679
|
+
const task = extractDelegationTask(message, toolName)
|
|
680
|
+
if (!task) continue
|
|
681
|
+
await invokeSessionTool(toolName, { task }, `Forced ${toolName} invocation failed`)
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const hasDelegationCall = forcedDelegationTools.some((toolName) => calledNames.has(toolName))
|
|
685
|
+
const enabledDelegateTools = enabledDelegationTools(sessionForRun)
|
|
686
|
+
const shouldAutoDelegateCoding = (!internal && source === 'chat')
|
|
687
|
+
&& enabledDelegateTools.length > 0
|
|
688
|
+
&& !hasDelegationCall
|
|
689
|
+
&& routingDecision?.intent === 'coding'
|
|
690
|
+
|
|
691
|
+
if (shouldAutoDelegateCoding) {
|
|
692
|
+
const baseDelegationOrder = routingDecision?.preferredDelegates?.length
|
|
693
|
+
? routingDecision.preferredDelegates
|
|
694
|
+
: forcedDelegationTools
|
|
695
|
+
const delegationOrder = rankDelegatesByHealth(baseDelegationOrder as DelegateTool[])
|
|
696
|
+
.filter((tool) => enabledDelegateTools.includes(tool))
|
|
697
|
+
for (const delegateTool of delegationOrder) {
|
|
698
|
+
const invoked = await invokeSessionTool(delegateTool, { task: message.trim() }, 'Auto-delegation failed')
|
|
699
|
+
if (invoked) break
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const shouldFailoverDelegate = (!internal && source === 'chat')
|
|
704
|
+
&& !!errorMessage
|
|
705
|
+
&& !(fullResponse || '').trim()
|
|
706
|
+
&& enabledDelegateTools.length > 0
|
|
707
|
+
&& !hasDelegationCall
|
|
708
|
+
&& (routingDecision?.intent === 'coding' || routingDecision?.intent === 'general')
|
|
709
|
+
if (shouldFailoverDelegate) {
|
|
710
|
+
const preferred = routingDecision?.preferredDelegates?.length
|
|
711
|
+
? routingDecision.preferredDelegates
|
|
712
|
+
: forcedDelegationTools
|
|
713
|
+
const fallbackOrder = rankDelegatesByHealth(preferred as DelegateTool[])
|
|
714
|
+
.filter((tool) => enabledDelegateTools.includes(tool))
|
|
715
|
+
for (const delegateTool of fallbackOrder) {
|
|
716
|
+
const invoked = await invokeSessionTool(
|
|
717
|
+
delegateTool,
|
|
718
|
+
{ task: message.trim() },
|
|
719
|
+
`Provider failover via ${delegateTool} failed`,
|
|
720
|
+
)
|
|
721
|
+
if (invoked) {
|
|
722
|
+
errorMessage = undefined
|
|
723
|
+
break
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const canAutoRouteWithTools = (!internal && source === 'chat')
|
|
729
|
+
&& !!routingDecision
|
|
730
|
+
&& calledNames.size === 0
|
|
731
|
+
&& requestedToolNames.length === 0
|
|
732
|
+
|
|
733
|
+
if (canAutoRouteWithTools && routingDecision?.intent === 'browsing' && routingDecision.primaryUrl && hasToolEnabled(sessionForRun, 'browser')) {
|
|
734
|
+
await invokeSessionTool(
|
|
735
|
+
'browser',
|
|
736
|
+
{ action: 'navigate', url: routingDecision.primaryUrl },
|
|
737
|
+
'Auto browser routing failed',
|
|
738
|
+
)
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (canAutoRouteWithTools && routingDecision?.intent === 'research') {
|
|
742
|
+
const routeUrl = routingDecision.primaryUrl || findFirstUrl(message)
|
|
743
|
+
if (routeUrl && hasToolEnabled(sessionForRun, 'web_fetch')) {
|
|
744
|
+
await invokeSessionTool('web_fetch', { url: routeUrl }, 'Auto web_fetch routing failed')
|
|
745
|
+
} else if (hasToolEnabled(sessionForRun, 'web_search')) {
|
|
746
|
+
await invokeSessionTool('web_search', { query: message.trim(), maxResults: 5 }, 'Auto web_search routing failed')
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (requestedToolNames.length > 0) {
|
|
751
|
+
const missed = requestedToolNames.filter((name) => !calledNames.has(name))
|
|
752
|
+
if (missed.length > 0) {
|
|
753
|
+
const notice = `Tool execution notice: requested tool(s) ${missed.join(', ')} were not actually invoked in this run.`
|
|
754
|
+
emit({ t: 'err', text: notice })
|
|
755
|
+
if (!fullResponse.includes('Tool execution notice:')) {
|
|
756
|
+
const trimmedResponse = (fullResponse || '').trim()
|
|
757
|
+
fullResponse = trimmedResponse
|
|
758
|
+
? `${trimmedResponse}\n\n${notice}`
|
|
759
|
+
: notice
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (!errorMessage && streamErrors.length > 0 && !(fullResponse || '').trim()) {
|
|
765
|
+
errorMessage = streamErrors[streamErrors.length - 1]
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const finalText = (fullResponse || '').trim() || (!internal && errorMessage ? `Error: ${errorMessage}` : '')
|
|
769
|
+
const textForPersistence = stripMainLoopMetaForPersistence(finalText, internal)
|
|
770
|
+
|
|
771
|
+
// HEARTBEAT_OK suppression
|
|
772
|
+
const heartbeatConfig = input.heartbeatConfig
|
|
773
|
+
let heartbeatClassification: 'suppress' | 'strip' | 'keep' | null = null
|
|
774
|
+
if (isHeartbeatRun && textForPersistence.length > 0) {
|
|
775
|
+
heartbeatClassification = classifyHeartbeatResponse(textForPersistence, heartbeatConfig?.ackMaxChars ?? 300)
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const shouldPersistAssistant = textForPersistence.length > 0
|
|
779
|
+
&& heartbeatClassification !== 'suppress'
|
|
780
|
+
|
|
781
|
+
const normalizeResumeId = (value: unknown): string | null =>
|
|
782
|
+
typeof value === 'string' && value.trim() ? value.trim() : null
|
|
783
|
+
|
|
784
|
+
const fresh = loadSessions()
|
|
785
|
+
const current = fresh[sessionId]
|
|
786
|
+
if (current) {
|
|
787
|
+
let changed = false
|
|
788
|
+
const persistField = (key: string, value: unknown) => {
|
|
789
|
+
const normalized = normalizeResumeId(value)
|
|
790
|
+
if ((current as any)[key] !== normalized) {
|
|
791
|
+
;(current as any)[key] = normalized
|
|
792
|
+
changed = true
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
persistField('claudeSessionId', session.claudeSessionId)
|
|
797
|
+
persistField('codexThreadId', session.codexThreadId)
|
|
798
|
+
persistField('opencodeSessionId', session.opencodeSessionId)
|
|
799
|
+
|
|
800
|
+
const sourceResume = session.delegateResumeIds
|
|
801
|
+
if (sourceResume && typeof sourceResume === 'object') {
|
|
802
|
+
const currentResume = (current.delegateResumeIds && typeof current.delegateResumeIds === 'object')
|
|
803
|
+
? current.delegateResumeIds
|
|
804
|
+
: {}
|
|
805
|
+
const nextResume = {
|
|
806
|
+
claudeCode: normalizeResumeId((sourceResume as any).claudeCode ?? (currentResume as any).claudeCode),
|
|
807
|
+
codex: normalizeResumeId((sourceResume as any).codex ?? (currentResume as any).codex),
|
|
808
|
+
opencode: normalizeResumeId((sourceResume as any).opencode ?? (currentResume as any).opencode),
|
|
809
|
+
}
|
|
810
|
+
if (JSON.stringify(currentResume) !== JSON.stringify(nextResume)) {
|
|
811
|
+
current.delegateResumeIds = nextResume
|
|
812
|
+
changed = true
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (shouldPersistAssistant) {
|
|
817
|
+
const persistedKind = internal && source !== 'session-awakening' ? 'heartbeat' : 'chat'
|
|
818
|
+
const persistedText = heartbeatClassification === 'strip'
|
|
819
|
+
? textForPersistence.replace(/HEARTBEAT_OK/gi, '').trim()
|
|
820
|
+
: textForPersistence
|
|
821
|
+
current.messages.push({
|
|
822
|
+
role: 'assistant',
|
|
823
|
+
text: persistedText,
|
|
824
|
+
time: Date.now(),
|
|
825
|
+
toolEvents: toolEvents.length ? toolEvents : undefined,
|
|
826
|
+
kind: persistedKind,
|
|
827
|
+
})
|
|
828
|
+
changed = true
|
|
829
|
+
|
|
830
|
+
// Target routing for non-suppressed heartbeat alerts
|
|
831
|
+
if (isHeartbeatRun && heartbeatConfig?.target && heartbeatConfig.target !== 'none' && heartbeatConfig.showAlerts !== false) {
|
|
832
|
+
try {
|
|
833
|
+
const { listRunningConnectors, sendConnectorMessage } = require('./connectors/manager')
|
|
834
|
+
let connectorId: string | undefined
|
|
835
|
+
let channelId: string | undefined
|
|
836
|
+
if (heartbeatConfig.target === 'last') {
|
|
837
|
+
const running = listRunningConnectors()
|
|
838
|
+
const first = running.find((c: any) => c.recentChannelId)
|
|
839
|
+
if (first) {
|
|
840
|
+
connectorId = first.id
|
|
841
|
+
channelId = first.recentChannelId
|
|
842
|
+
}
|
|
843
|
+
} else if (heartbeatConfig.target.includes(':')) {
|
|
844
|
+
const [cId, chId] = heartbeatConfig.target.split(':', 2)
|
|
845
|
+
connectorId = cId
|
|
846
|
+
channelId = chId
|
|
847
|
+
} else {
|
|
848
|
+
channelId = heartbeatConfig.target
|
|
849
|
+
}
|
|
850
|
+
if (channelId) {
|
|
851
|
+
sendConnectorMessage({ connectorId, channelId, text: persistedText }).catch(() => {})
|
|
852
|
+
}
|
|
853
|
+
} catch {
|
|
854
|
+
// Best effort — connector manager may not be loaded
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const autoMemoryEligible = shouldStoreAutoMemoryNote({
|
|
860
|
+
session: current,
|
|
861
|
+
source,
|
|
862
|
+
internal,
|
|
863
|
+
message,
|
|
864
|
+
response: textForPersistence,
|
|
865
|
+
now: Date.now(),
|
|
866
|
+
})
|
|
867
|
+
if (autoMemoryEligible) {
|
|
868
|
+
const storedId = storeAutoMemoryNote({
|
|
869
|
+
session: current,
|
|
870
|
+
message,
|
|
871
|
+
response: textForPersistence,
|
|
872
|
+
source,
|
|
873
|
+
now: Date.now(),
|
|
874
|
+
})
|
|
875
|
+
if (storedId) changed = true
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Don't extend idle timeout for heartbeat runs — only user-initiated activity counts
|
|
879
|
+
if (source !== 'heartbeat' && source !== 'main-loop-followup') {
|
|
880
|
+
current.lastActiveAt = Date.now()
|
|
881
|
+
}
|
|
882
|
+
fresh[sessionId] = current
|
|
883
|
+
saveSessions(fresh)
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return {
|
|
887
|
+
runId,
|
|
888
|
+
sessionId,
|
|
889
|
+
text: finalText,
|
|
890
|
+
persisted: shouldPersistAssistant,
|
|
891
|
+
toolEvents,
|
|
892
|
+
error: errorMessage,
|
|
893
|
+
}
|
|
894
|
+
}
|