@swarmclawai/swarmclaw 1.2.1 → 1.2.3
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 +16 -85
- package/bin/server-cmd.js +64 -1
- package/package.json +2 -2
- package/skills/coding-agent/SKILL.md +111 -0
- package/skills/github/SKILL.md +140 -0
- package/skills/nano-banana-pro/SKILL.md +62 -0
- package/skills/nano-banana-pro/scripts/generate_image.py +235 -0
- package/skills/nano-pdf/SKILL.md +53 -0
- package/skills/openai-image-gen/SKILL.md +78 -0
- package/skills/openai-image-gen/scripts/gen.py +328 -0
- package/skills/resourceful-problem-solving/SKILL.md +49 -0
- package/skills/skill-creator/SKILL.md +147 -0
- package/skills/skill-creator/scripts/init_skill.py +378 -0
- package/skills/skill-creator/scripts/quick_validate.py +159 -0
- package/skills/summarize/SKILL.md +77 -0
- package/src/app/api/auth/route.ts +20 -5
- package/src/app/api/chats/[id]/devserver/route.ts +13 -19
- package/src/app/api/chats/[id]/messages/route.ts +13 -15
- package/src/app/api/chats/[id]/route.ts +9 -10
- package/src/app/api/chats/[id]/stop/route.ts +5 -7
- package/src/app/api/chats/messages-route.test.ts +8 -6
- package/src/app/api/chats/route.ts +9 -10
- package/src/app/api/ip/route.ts +2 -2
- package/src/app/api/preview-server/route.ts +1 -1
- package/src/app/api/projects/[id]/route.ts +7 -46
- package/src/cli/server-cmd.test.js +74 -0
- package/src/components/chat/chat-area.tsx +45 -23
- package/src/components/chat/message-bubble.test.ts +35 -0
- package/src/components/chat/message-bubble.tsx +19 -9
- package/src/components/chat/message-list.tsx +37 -3
- package/src/components/input/chat-input.tsx +34 -14
- package/src/components/openclaw/openclaw-deploy-panel.tsx +4 -0
- package/src/instrumentation.ts +1 -1
- package/src/lib/chat/assistant-render-id.ts +3 -0
- package/src/lib/chat/chat-streaming-state.test.ts +42 -3
- package/src/lib/chat/chat-streaming-state.ts +20 -8
- package/src/lib/chat/queued-message-queue.test.ts +23 -1
- package/src/lib/chat/queued-message-queue.ts +11 -2
- package/src/lib/providers/cli-utils.test.ts +124 -0
- package/src/lib/server/activity/activity-log.ts +21 -0
- package/src/lib/server/agents/agent-availability.test.ts +10 -5
- package/src/lib/server/agents/agent-cascade.ts +79 -59
- package/src/lib/server/agents/agent-registry.ts +3 -1
- package/src/lib/server/agents/agent-repository.ts +90 -0
- package/src/lib/server/agents/delegation-job-repository.ts +53 -0
- package/src/lib/server/agents/delegation-jobs.ts +11 -4
- package/src/lib/server/agents/guardian-checkpoint-repository.ts +35 -0
- package/src/lib/server/agents/guardian.ts +2 -2
- package/src/lib/server/agents/main-agent-loop.ts +10 -3
- package/src/lib/server/agents/main-loop-state-repository.ts +38 -0
- package/src/lib/server/agents/subagent-runtime.ts +9 -6
- package/src/lib/server/agents/subagent-swarm.ts +3 -2
- package/src/lib/server/agents/task-session.ts +3 -4
- package/src/lib/server/approvals/approval-repository.ts +30 -0
- package/src/lib/server/autonomy/supervisor-incident-repository.ts +42 -0
- package/src/lib/server/chat-execution/chat-execution-types.ts +38 -0
- package/src/lib/server/chat-execution/chat-execution-utils.ts +1 -1
- package/src/lib/server/chat-execution/chat-execution.ts +84 -1926
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +620 -0
- package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +221 -0
- package/src/lib/server/chat-execution/chat-turn-preflight.ts +133 -0
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +817 -0
- package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +296 -0
- package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +5 -5
- package/src/lib/server/chat-execution/message-classifier.test.ts +329 -0
- package/src/lib/server/chat-execution/post-stream-finalization.ts +1 -1
- package/src/lib/server/chat-execution/prompt-builder.ts +11 -0
- package/src/lib/server/chat-execution/prompt-sections.ts +5 -6
- package/src/lib/server/chat-execution/situational-awareness.ts +12 -7
- package/src/lib/server/chat-execution/stream-agent-chat.ts +16 -13
- package/src/lib/server/chatrooms/chatroom-repository.ts +32 -0
- package/src/lib/server/connectors/connector-repository.ts +58 -0
- package/src/lib/server/connectors/runtime-state.test.ts +117 -0
- package/src/lib/server/credentials/credential-repository.ts +7 -0
- package/src/lib/server/gateways/gateway-profile-repository.ts +4 -0
- package/src/lib/server/memory/memory-abstract.test.ts +59 -0
- package/src/lib/server/missions/mission-repository.ts +74 -0
- package/src/lib/server/missions/mission-service/actions.ts +6 -0
- package/src/lib/server/missions/mission-service/bindings.ts +9 -0
- package/src/lib/server/missions/mission-service/context.ts +4 -0
- package/src/lib/server/missions/mission-service/core.ts +2269 -0
- package/src/lib/server/missions/mission-service/queries.ts +12 -0
- package/src/lib/server/missions/mission-service/recovery.ts +5 -0
- package/src/lib/server/missions/mission-service/ticks.ts +9 -0
- package/src/lib/server/missions/mission-service.test.ts +9 -2
- package/src/lib/server/missions/mission-service.ts +6 -2266
- package/src/lib/server/openclaw/deploy.test.ts +42 -3
- package/src/lib/server/openclaw/deploy.ts +26 -12
- package/src/lib/server/persistence/repository-utils.ts +154 -0
- package/src/lib/server/persistence/storage-context.ts +51 -0
- package/src/lib/server/persistence/transaction.ts +1 -0
- package/src/lib/server/projects/project-repository.ts +36 -0
- package/src/lib/server/projects/project-service.ts +79 -0
- package/src/lib/server/protocols/protocol-normalization.test.ts +6 -4
- package/src/lib/server/runtime/alert-dispatch.ts +1 -1
- package/src/lib/server/runtime/daemon-policy.ts +1 -1
- package/src/lib/server/runtime/daemon-state/core.ts +1570 -0
- package/src/lib/server/runtime/daemon-state/health.ts +6 -0
- package/src/lib/server/runtime/daemon-state/policy.ts +7 -0
- package/src/lib/server/runtime/daemon-state/supervisor.ts +6 -0
- package/src/lib/server/runtime/daemon-state.test.ts +48 -0
- package/src/lib/server/runtime/daemon-state.ts +3 -1470
- package/src/lib/server/runtime/estop-repository.ts +4 -0
- package/src/lib/server/runtime/estop.ts +3 -1
- package/src/lib/server/runtime/heartbeat-service.test.ts +2 -2
- package/src/lib/server/runtime/heartbeat-service.ts +55 -34
- package/src/lib/server/runtime/heartbeat-wake.ts +6 -4
- package/src/lib/server/runtime/idle-window.ts +2 -2
- package/src/lib/server/runtime/network.ts +11 -0
- package/src/lib/server/runtime/orchestrator-events.ts +2 -2
- package/src/lib/server/runtime/queue/claims.ts +4 -0
- package/src/lib/server/runtime/queue/core.ts +2079 -0
- package/src/lib/server/runtime/queue/execution.ts +7 -0
- package/src/lib/server/runtime/queue/followups.ts +4 -0
- package/src/lib/server/runtime/queue/queries.ts +12 -0
- package/src/lib/server/runtime/queue/recovery.ts +7 -0
- package/src/lib/server/runtime/queue-recovery.test.ts +48 -13
- package/src/lib/server/runtime/queue-repository.ts +17 -0
- package/src/lib/server/runtime/queue.ts +5 -2061
- package/src/lib/server/runtime/run-ledger.ts +6 -5
- package/src/lib/server/runtime/run-repository.ts +73 -0
- package/src/lib/server/runtime/runtime-lock-repository.ts +8 -0
- package/src/lib/server/runtime/runtime-settings.ts +1 -1
- package/src/lib/server/runtime/runtime-state.ts +99 -0
- package/src/lib/server/runtime/scheduler.ts +4 -2
- package/src/lib/server/runtime/session-run-manager/cancellation.ts +157 -0
- package/src/lib/server/runtime/session-run-manager/drain.ts +246 -0
- package/src/lib/server/runtime/session-run-manager/enqueue.ts +287 -0
- package/src/lib/server/runtime/session-run-manager/queries.ts +117 -0
- package/src/lib/server/runtime/session-run-manager/recovery.ts +238 -0
- package/src/lib/server/runtime/session-run-manager/state.ts +441 -0
- package/src/lib/server/runtime/session-run-manager/types.ts +74 -0
- package/src/lib/server/runtime/session-run-manager.ts +72 -1377
- package/src/lib/server/runtime/watch-job-repository.ts +35 -0
- package/src/lib/server/runtime/watch-jobs.ts +3 -1
- package/src/lib/server/schedules/schedule-repository.ts +42 -0
- package/src/lib/server/sessions/session-repository.ts +85 -0
- package/src/lib/server/settings/settings-repository.ts +25 -0
- package/src/lib/server/skills/skill-discovery.test.ts +2 -2
- package/src/lib/server/skills/skill-discovery.ts +2 -2
- package/src/lib/server/skills/skill-repository.ts +14 -0
- package/src/lib/server/storage.ts +13 -24
- package/src/lib/server/tasks/task-repository.ts +54 -0
- package/src/lib/server/usage/usage-repository.ts +30 -0
- package/src/lib/server/webhooks/webhook-repository.ts +10 -0
- package/src/lib/strip-internal-metadata.test.ts +42 -41
- package/src/stores/use-chat-store.test.ts +54 -0
- package/src/stores/use-chat-store.ts +21 -5
- /package/{bundled-skills → skills}/google-workspace/SKILL.md +0 -0
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import os from 'os'
|
|
3
|
+
|
|
4
|
+
import { getProvider } from '@/lib/providers'
|
|
5
|
+
import type { Message, Session } from '@/types'
|
|
6
|
+
import {
|
|
7
|
+
decryptKey,
|
|
8
|
+
loadCredentials,
|
|
9
|
+
} from '@/lib/server/credentials/credential-repository'
|
|
10
|
+
import { getAgent } from '@/lib/server/agents/agent-repository'
|
|
11
|
+
import { getSession, saveSession } from '@/lib/server/sessions/session-repository'
|
|
12
|
+
import { loadSettings } from '@/lib/server/settings/settings-repository'
|
|
13
|
+
import { loadSkills } from '@/lib/server/skills/skill-repository'
|
|
14
|
+
import { resolveImagePath } from '@/lib/server/resolve-image'
|
|
15
|
+
import { resolveSessionToolPolicy } from '@/lib/server/tool-capability-policy'
|
|
16
|
+
import { listUniversalToolAccessExtensionIds } from '@/lib/server/universal-tool-access'
|
|
17
|
+
import {
|
|
18
|
+
buildAgentDisabledMessage,
|
|
19
|
+
isAgentDisabled,
|
|
20
|
+
} from '@/lib/server/agents/agent-availability'
|
|
21
|
+
import { buildCurrentDateTimePromptContext } from '@/lib/server/prompt-runtime-context'
|
|
22
|
+
import { buildWorkspaceContext } from '@/lib/server/workspace-context'
|
|
23
|
+
import {
|
|
24
|
+
buildRuntimeSkillPromptBlocks,
|
|
25
|
+
resolveRuntimeSkills,
|
|
26
|
+
} from '@/lib/server/skills/runtime-skill-resolver'
|
|
27
|
+
import {
|
|
28
|
+
applyResolvedRoute,
|
|
29
|
+
resolvePrimaryAgentRoute,
|
|
30
|
+
} from '@/lib/server/agents/agent-runtime-config'
|
|
31
|
+
import {
|
|
32
|
+
runCapabilityBeforeMessageWrite,
|
|
33
|
+
runCapabilityBeforeModelResolve,
|
|
34
|
+
runCapabilityHook,
|
|
35
|
+
runCapabilityToolResultPersist,
|
|
36
|
+
transformCapabilityText,
|
|
37
|
+
collectCapabilityDescriptions,
|
|
38
|
+
collectCapabilityOperatingGuidance,
|
|
39
|
+
} from '@/lib/server/native-capabilities'
|
|
40
|
+
import {
|
|
41
|
+
getEnabledCapabilityIds,
|
|
42
|
+
getEnabledCapabilitySelection,
|
|
43
|
+
splitCapabilityIds,
|
|
44
|
+
} from '@/lib/capability-selection'
|
|
45
|
+
import { normalizeProviderEndpoint, isLocalOpenClawEndpoint } from '@/lib/openclaw/openclaw-endpoint'
|
|
46
|
+
import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
|
|
47
|
+
import {
|
|
48
|
+
buildMissionContextBlock,
|
|
49
|
+
resolveMissionForTurn,
|
|
50
|
+
} from '@/lib/server/missions/mission-service'
|
|
51
|
+
import {
|
|
52
|
+
bridgeHumanReplyFromChat,
|
|
53
|
+
} from '@/lib/server/chatrooms/session-mailbox'
|
|
54
|
+
import { runLinkUnderstanding } from '@/lib/server/link-understanding'
|
|
55
|
+
import {
|
|
56
|
+
guardUntrustedText,
|
|
57
|
+
guardUntrustedToolEvents,
|
|
58
|
+
getUntrustedContentGuardMode,
|
|
59
|
+
} from '@/lib/server/untrusted-content'
|
|
60
|
+
import {
|
|
61
|
+
buildIdentityContinuityContext,
|
|
62
|
+
} from '@/lib/server/identity-continuity'
|
|
63
|
+
import {
|
|
64
|
+
resolveEffectiveSessionMemoryScopeMode,
|
|
65
|
+
} from '@/lib/server/memory/session-memory-scope'
|
|
66
|
+
import { syncSessionArchiveMemory } from '@/lib/server/memory/session-archive-memory'
|
|
67
|
+
import {
|
|
68
|
+
evaluateSessionFreshness,
|
|
69
|
+
resetSessionRuntime,
|
|
70
|
+
resolveSessionResetPolicy,
|
|
71
|
+
} from '@/lib/server/session-reset-policy'
|
|
72
|
+
import { checkAgentBudgetLimits } from '@/lib/server/cost'
|
|
73
|
+
import {
|
|
74
|
+
filterRuntimeCapabilityIds,
|
|
75
|
+
getTodaySpendUsd,
|
|
76
|
+
parseUsdLimit,
|
|
77
|
+
shouldApplySessionFreshnessReset,
|
|
78
|
+
shouldPersistInboundUserMessage,
|
|
79
|
+
} from '@/lib/server/chat-execution/chat-execution-utils'
|
|
80
|
+
import { loadEstopState } from '@/lib/server/runtime/estop'
|
|
81
|
+
import { buildToolSection, joinPromptSegments } from '@/lib/server/chat-execution/prompt-builder'
|
|
82
|
+
import { isDirectConnectorSession } from '@/lib/server/connectors/session-kind'
|
|
83
|
+
import type { ExecuteChatTurnInput } from '@/lib/server/chat-execution/chat-execution'
|
|
84
|
+
|
|
85
|
+
export function buildAgentRuntimeCapabilities(enabledExtensions: string[]): string[] {
|
|
86
|
+
const capabilities = ['heartbeats', 'autonomous_loop', 'multi_agent_chat']
|
|
87
|
+
if (enabledExtensions.length > 0) capabilities.unshift('tools')
|
|
88
|
+
return capabilities
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function buildNoToolsGuidance(): string[] {
|
|
92
|
+
return [
|
|
93
|
+
'## Tool Availability',
|
|
94
|
+
'No runtime tools are available in this chat after policy filtering.',
|
|
95
|
+
'Do not imply that a normal read-only action is waiting on user permission when the real blocker is missing tool access.',
|
|
96
|
+
'If browsing, web fetches, file edits, or other actions are unavailable, state that the capability is blocked by runtime policy in this session.',
|
|
97
|
+
'Only mention confirmation or approval when a real runtime tool explicitly returned that boundary for a concrete action.',
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function buildEnabledToolsAutonomyGuidance(): string[] {
|
|
102
|
+
return [
|
|
103
|
+
'## Tool Autonomy',
|
|
104
|
+
'Runtime tools are already available for normal use in this chat.',
|
|
105
|
+
'Do not request that a tool be enabled or switched on before using it.',
|
|
106
|
+
'Do not ask the user for permission before using enabled tools for ordinary read-only work, routine diagnostics, or reversible execution steps that are clearly part of the request.',
|
|
107
|
+
'If the user asks you to use an enabled tool or to perform a task that clearly maps to an enabled tool, attempt that tool path before asking the user to do the work manually.',
|
|
108
|
+
'If the task depends on current or external information and web tools are enabled, use them instead of answering from stale memory.',
|
|
109
|
+
'If the task asks for a file, report, dashboard, JSON, or other workspace artifact to be saved, use file-writing or shell tools to actually create it and mention the resulting path.',
|
|
110
|
+
'If the task asks you to inspect the local repository, runtime, or filesystem state, use shell or file tools instead of guessing.',
|
|
111
|
+
'Treat capability policy blocks and explicit platform feature gates as the real boundaries. Do not invent an approval queue when none exists.',
|
|
112
|
+
'When asked to create a file in a format you don\'t have a dedicated tool for (PDF, image, spreadsheet, etc.), check available skills first, then use shell tools to install and run a CLI tool that handles it.',
|
|
113
|
+
'If no skill or tool exists for a task, write a script and run it with shell tools. Install packages with pip/npm/brew as needed.',
|
|
114
|
+
'Never say "I can\'t do that" or "I don\'t have a tool for that" when shell tools are available. Attempt a code-based approach first. Only report inability after genuinely trying and failing.',
|
|
115
|
+
'When you solve a novel task with code or shell, consider using the extension_creator tool to save the solution as a reusable extension.',
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type PersistPhase = 'user' | 'system' | 'assistant_partial' | 'assistant_final' | 'heartbeat'
|
|
120
|
+
|
|
121
|
+
export async function applyMessageLifecycleHooks(params: {
|
|
122
|
+
session: Session
|
|
123
|
+
message: Message
|
|
124
|
+
enabledIds: string[]
|
|
125
|
+
phase: PersistPhase
|
|
126
|
+
runId?: string
|
|
127
|
+
isSynthetic?: boolean
|
|
128
|
+
}): Promise<Message | null> {
|
|
129
|
+
let currentMessage = params.message
|
|
130
|
+
const guardMode = getUntrustedContentGuardMode(loadSettings())
|
|
131
|
+
if (Array.isArray(currentMessage.toolEvents) && currentMessage.toolEvents.length > 0) {
|
|
132
|
+
currentMessage = {
|
|
133
|
+
...currentMessage,
|
|
134
|
+
toolEvents: guardUntrustedToolEvents({
|
|
135
|
+
toolEvents: currentMessage.toolEvents,
|
|
136
|
+
mode: guardMode,
|
|
137
|
+
}),
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const toolEvents = Array.isArray(currentMessage.toolEvents)
|
|
141
|
+
? currentMessage.toolEvents.filter((event) => typeof event.output === 'string' || event.error === true)
|
|
142
|
+
: []
|
|
143
|
+
|
|
144
|
+
for (const event of toolEvents) {
|
|
145
|
+
currentMessage = await runCapabilityToolResultPersist(
|
|
146
|
+
{
|
|
147
|
+
session: params.session,
|
|
148
|
+
message: currentMessage,
|
|
149
|
+
toolName: event.name,
|
|
150
|
+
toolCallId: event.toolCallId,
|
|
151
|
+
isSynthetic: params.isSynthetic,
|
|
152
|
+
},
|
|
153
|
+
{ enabledIds: params.enabledIds },
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const writeResult = await runCapabilityBeforeMessageWrite(
|
|
158
|
+
{
|
|
159
|
+
session: params.session,
|
|
160
|
+
message: currentMessage,
|
|
161
|
+
phase: params.phase,
|
|
162
|
+
runId: params.runId,
|
|
163
|
+
},
|
|
164
|
+
{ enabledIds: params.enabledIds },
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if (writeResult.block) return null
|
|
168
|
+
return writeResult.message
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
interface SessionWithCredentials {
|
|
172
|
+
credentialId?: string | null
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
interface ProviderApiKeyConfig {
|
|
176
|
+
requiresApiKey?: boolean
|
|
177
|
+
optionalApiKey?: boolean
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function joinSystemPromptBlocks(...blocks: Array<string | null | undefined>): string | undefined {
|
|
181
|
+
const joined = joinPromptSegments(...blocks)
|
|
182
|
+
return joined || undefined
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function syncSessionFromAgent(sessionId: string): void {
|
|
186
|
+
const session = getSession(sessionId)
|
|
187
|
+
if (!session?.agentId) return
|
|
188
|
+
const agent = getAgent(session.agentId)
|
|
189
|
+
if (!agent) return
|
|
190
|
+
|
|
191
|
+
let changed = false
|
|
192
|
+
const route = resolvePrimaryAgentRoute(agent, undefined, {
|
|
193
|
+
preferredGatewayTags: session.routePreferredGatewayTags || [],
|
|
194
|
+
preferredGatewayUseCase: session.routePreferredGatewayUseCase || null,
|
|
195
|
+
})
|
|
196
|
+
if (!session.provider && agent.provider) { session.provider = agent.provider; changed = true }
|
|
197
|
+
if ((session.model === undefined || session.model === null || session.model === '') && agent.model !== undefined) {
|
|
198
|
+
session.model = agent.model
|
|
199
|
+
changed = true
|
|
200
|
+
}
|
|
201
|
+
if (route) {
|
|
202
|
+
const resolved = applyResolvedRoute({ ...session }, route)
|
|
203
|
+
if (session.provider !== resolved.provider) { session.provider = resolved.provider; changed = true }
|
|
204
|
+
if (session.model !== resolved.model) { session.model = resolved.model; changed = true }
|
|
205
|
+
if ((session.credentialId || null) !== (resolved.credentialId || null)) {
|
|
206
|
+
session.credentialId = resolved.credentialId ?? null
|
|
207
|
+
changed = true
|
|
208
|
+
}
|
|
209
|
+
if (JSON.stringify(session.fallbackCredentialIds || []) !== JSON.stringify(resolved.fallbackCredentialIds || [])) {
|
|
210
|
+
session.fallbackCredentialIds = [...(resolved.fallbackCredentialIds || [])]
|
|
211
|
+
changed = true
|
|
212
|
+
}
|
|
213
|
+
if ((session.apiEndpoint || null) !== (resolved.apiEndpoint || null)) {
|
|
214
|
+
session.apiEndpoint = resolved.apiEndpoint ?? null
|
|
215
|
+
changed = true
|
|
216
|
+
}
|
|
217
|
+
if ((session.gatewayProfileId || null) !== (resolved.gatewayProfileId || null)) {
|
|
218
|
+
session.gatewayProfileId = resolved.gatewayProfileId ?? null
|
|
219
|
+
changed = true
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
if (session.credentialId === undefined && agent.credentialId !== undefined) {
|
|
223
|
+
session.credentialId = agent.credentialId ?? null
|
|
224
|
+
changed = true
|
|
225
|
+
}
|
|
226
|
+
if ((session.apiEndpoint === undefined || session.apiEndpoint === null) && agent.apiEndpoint !== undefined) {
|
|
227
|
+
const normalized = normalizeProviderEndpoint(agent.provider, agent.apiEndpoint ?? null)
|
|
228
|
+
if (normalized !== session.apiEndpoint) { session.apiEndpoint = normalized; changed = true }
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const agentSelection = getEnabledCapabilitySelection(agent)
|
|
232
|
+
if (!session.parentSessionId) {
|
|
233
|
+
const currentSelection = getEnabledCapabilitySelection(session)
|
|
234
|
+
if (
|
|
235
|
+
JSON.stringify(currentSelection.tools) !== JSON.stringify(agentSelection.tools)
|
|
236
|
+
|| JSON.stringify(currentSelection.extensions) !== JSON.stringify(agentSelection.extensions)
|
|
237
|
+
) {
|
|
238
|
+
session.tools = agentSelection.tools
|
|
239
|
+
session.extensions = agentSelection.extensions
|
|
240
|
+
changed = true
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const desiredMemoryScopeMode = resolveEffectiveSessionMemoryScopeMode(session, agent.memoryScopeMode ?? null)
|
|
244
|
+
if ((((session as unknown as Record<string, unknown>).memoryScopeMode as string | null | undefined) ?? null) !== desiredMemoryScopeMode) {
|
|
245
|
+
;(session as unknown as Record<string, unknown>).memoryScopeMode = desiredMemoryScopeMode
|
|
246
|
+
changed = true
|
|
247
|
+
}
|
|
248
|
+
const isShortcutChat = session.shortcutForAgentId === agent.id || agent.threadSessionId === sessionId
|
|
249
|
+
if (isShortcutChat) {
|
|
250
|
+
const desiredSelection = agentSelection
|
|
251
|
+
const currentShortcutSelection = getEnabledCapabilitySelection(session)
|
|
252
|
+
if (
|
|
253
|
+
JSON.stringify(currentShortcutSelection.tools) !== JSON.stringify(desiredSelection.tools)
|
|
254
|
+
|| JSON.stringify(currentShortcutSelection.extensions) !== JSON.stringify(desiredSelection.extensions)
|
|
255
|
+
) {
|
|
256
|
+
session.tools = desiredSelection.tools
|
|
257
|
+
session.extensions = desiredSelection.extensions
|
|
258
|
+
changed = true
|
|
259
|
+
}
|
|
260
|
+
if (session.shortcutForAgentId !== agent.id) { session.shortcutForAgentId = agent.id; changed = true }
|
|
261
|
+
if (session.name !== agent.name) { session.name = agent.name; changed = true }
|
|
262
|
+
const desiredHeartbeatEnabled = agent.heartbeatEnabled ?? false
|
|
263
|
+
if ((session.heartbeatEnabled ?? false) !== desiredHeartbeatEnabled) {
|
|
264
|
+
session.heartbeatEnabled = desiredHeartbeatEnabled
|
|
265
|
+
changed = true
|
|
266
|
+
}
|
|
267
|
+
const desiredHeartbeatIntervalSec = agent.heartbeatIntervalSec ?? null
|
|
268
|
+
if ((session.heartbeatIntervalSec ?? null) !== desiredHeartbeatIntervalSec) {
|
|
269
|
+
session.heartbeatIntervalSec = desiredHeartbeatIntervalSec
|
|
270
|
+
changed = true
|
|
271
|
+
}
|
|
272
|
+
const desiredMemoryTierPreference = agent.memoryTierPreference ?? null
|
|
273
|
+
if ((((session as unknown as Record<string, unknown>).memoryTierPreference as string | null | undefined) ?? null) !== desiredMemoryTierPreference) {
|
|
274
|
+
;(session as unknown as Record<string, unknown>).memoryTierPreference = desiredMemoryTierPreference
|
|
275
|
+
changed = true
|
|
276
|
+
}
|
|
277
|
+
const desiredProjectId = agent.projectId ?? null
|
|
278
|
+
if ((session.projectId ?? null) !== desiredProjectId) {
|
|
279
|
+
session.projectId = desiredProjectId
|
|
280
|
+
changed = true
|
|
281
|
+
}
|
|
282
|
+
const desiredOpenClawAgentId = agent.openclawAgentId ?? null
|
|
283
|
+
if ((session.openclawAgentId ?? null) !== desiredOpenClawAgentId) {
|
|
284
|
+
session.openclawAgentId = desiredOpenClawAgentId
|
|
285
|
+
changed = true
|
|
286
|
+
}
|
|
287
|
+
if (session.connectorContext) {
|
|
288
|
+
session.connectorContext = undefined
|
|
289
|
+
changed = true
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (changed) {
|
|
294
|
+
saveSession(sessionId, session)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function buildLightHeartbeatSystemPrompt(session: Session): string | undefined {
|
|
299
|
+
if (!session.agentId) return undefined
|
|
300
|
+
const agent = getAgent(session.agentId)
|
|
301
|
+
if (!agent) return undefined
|
|
302
|
+
|
|
303
|
+
const parts: string[] = []
|
|
304
|
+
parts.push(`## Identity\nName: ${agent.name}`)
|
|
305
|
+
if (agent.description) parts.push(`Description: ${agent.description}`)
|
|
306
|
+
parts.push(buildCurrentDateTimePromptContext())
|
|
307
|
+
if (agent.soul) parts.push(`## Soul\n${agent.soul.slice(0, 300)}`)
|
|
308
|
+
parts.push([
|
|
309
|
+
'## Heartbeats',
|
|
310
|
+
'You run on an autonomous heartbeat. If you receive a heartbeat poll and nothing needs attention, reply exactly: HEARTBEAT_OK',
|
|
311
|
+
].join('\n'))
|
|
312
|
+
return parts.join('\n\n')
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function buildAgentSystemPrompt(session: Session): string | undefined {
|
|
316
|
+
if (!session.agentId) return undefined
|
|
317
|
+
const agent = getAgent(session.agentId)
|
|
318
|
+
if (!agent) return undefined
|
|
319
|
+
|
|
320
|
+
const settings = loadSettings()
|
|
321
|
+
const allowSilentReplies = isDirectConnectorSession(session)
|
|
322
|
+
const parts: string[] = []
|
|
323
|
+
const enabledExtensions = listUniversalToolAccessExtensionIds(
|
|
324
|
+
getEnabledCapabilityIds(session).length > 0 ? getEnabledCapabilityIds(session) : getEnabledCapabilityIds(agent),
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
const identityLines = ['## My Identity']
|
|
328
|
+
identityLines.push(`Name: ${agent.name}`)
|
|
329
|
+
if (agent.emoji) identityLines.push(`Emoji: ${agent.emoji}`)
|
|
330
|
+
if (agent.creature) identityLines.push(`Creature: ${agent.creature}`)
|
|
331
|
+
if (agent.vibe) identityLines.push(`Vibe: ${agent.vibe}`)
|
|
332
|
+
if (agent.theme) identityLines.push(`Theme: ${agent.theme}`)
|
|
333
|
+
if (agent.description) identityLines.push(`Description: ${agent.description}`)
|
|
334
|
+
identityLines.push('I should always embody this identity. I am not just an "Assistant" — I am a persistent companion with my own soul and presence.')
|
|
335
|
+
parts.push(identityLines.join('\n'))
|
|
336
|
+
const continuityBlock = buildIdentityContinuityContext(session, agent)
|
|
337
|
+
if (continuityBlock) parts.push(continuityBlock)
|
|
338
|
+
|
|
339
|
+
const runtimeLines = [
|
|
340
|
+
'## Runtime',
|
|
341
|
+
`os=${process.platform} | host=${os.hostname()} | agent=${agent.id} | provider=${session.provider} | model=${session.model}`,
|
|
342
|
+
`capabilities=${buildAgentRuntimeCapabilities(enabledExtensions).join(',')}`,
|
|
343
|
+
'tool_access=universal',
|
|
344
|
+
]
|
|
345
|
+
parts.push(runtimeLines.join('\n'))
|
|
346
|
+
|
|
347
|
+
if (typeof settings.userPrompt === 'string' && settings.userPrompt.trim()) parts.push(`## User Instructions\n${settings.userPrompt}`)
|
|
348
|
+
parts.push(buildCurrentDateTimePromptContext())
|
|
349
|
+
|
|
350
|
+
if (agent.soul) parts.push(`## Soul\n${agent.soul}`)
|
|
351
|
+
if (agent.systemPrompt) parts.push(`## System Prompt\n${agent.systemPrompt}`)
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const runtimeSkills = resolveRuntimeSkills({
|
|
355
|
+
cwd: session.cwd,
|
|
356
|
+
enabledExtensions,
|
|
357
|
+
agentId: agent.id,
|
|
358
|
+
sessionId: session.id,
|
|
359
|
+
userId: session.user,
|
|
360
|
+
agentSkillIds: agent.skillIds || [],
|
|
361
|
+
storedSkills: loadSkills(),
|
|
362
|
+
selectedSkillId: session.skillRuntimeState?.selectedSkillId || null,
|
|
363
|
+
})
|
|
364
|
+
parts.push(...buildRuntimeSkillPromptBlocks(runtimeSkills))
|
|
365
|
+
} catch {
|
|
366
|
+
// Runtime skills are non-critical during prompt assembly.
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
const wsCtx = buildWorkspaceContext({ cwd: session.cwd })
|
|
371
|
+
if (wsCtx.block) parts.push(wsCtx.block)
|
|
372
|
+
} catch {
|
|
373
|
+
// Workspace context is non-critical.
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const thinkingHint = [
|
|
377
|
+
'## Output Format',
|
|
378
|
+
'If your model supports internal reasoning/thinking, put all internal analysis inside <think>...</think> tags.',
|
|
379
|
+
'Your final response to the user should be clear and concise.',
|
|
380
|
+
allowSilentReplies
|
|
381
|
+
? 'When you truly have nothing to say, respond with ONLY: NO_MESSAGE'
|
|
382
|
+
: 'For direct user chats, always send a visible reply. Never answer with NO_MESSAGE or HEARTBEAT_OK unless this is an explicit heartbeat poll.',
|
|
383
|
+
]
|
|
384
|
+
parts.push(thinkingHint.join('\n'))
|
|
385
|
+
|
|
386
|
+
if (enabledExtensions.length === 0) {
|
|
387
|
+
parts.push(buildNoToolsGuidance().join('\n'))
|
|
388
|
+
} else {
|
|
389
|
+
parts.push(buildEnabledToolsAutonomyGuidance().join('\n'))
|
|
390
|
+
}
|
|
391
|
+
const toolSectionLines = buildToolSection(enabledExtensions)
|
|
392
|
+
if (toolSectionLines.length > 0) parts.push(['## Tool Discipline', ...toolSectionLines].join('\n'))
|
|
393
|
+
const operatingGuidance = collectCapabilityOperatingGuidance(enabledExtensions)
|
|
394
|
+
if (operatingGuidance.length > 0) parts.push(['## Tool Guidance', ...operatingGuidance].join('\n'))
|
|
395
|
+
const capabilityLines = collectCapabilityDescriptions(enabledExtensions)
|
|
396
|
+
if (capabilityLines.length > 0) parts.push(['## Tool Capabilities', ...capabilityLines].join('\n'))
|
|
397
|
+
|
|
398
|
+
parts.push([
|
|
399
|
+
'## Heartbeats',
|
|
400
|
+
'You run on an autonomous heartbeat. If you receive a heartbeat poll and nothing needs attention, reply exactly: HEARTBEAT_OK',
|
|
401
|
+
].join('\n'))
|
|
402
|
+
|
|
403
|
+
return parts.join('\n\n')
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function resolveApiKeyForSession(session: SessionWithCredentials, provider: ProviderApiKeyConfig): string | null {
|
|
407
|
+
if (provider.requiresApiKey) {
|
|
408
|
+
if (!session.credentialId) throw new Error('No API key configured for this session')
|
|
409
|
+
const creds = loadCredentials()
|
|
410
|
+
const cred = creds[session.credentialId]
|
|
411
|
+
if (!cred) throw new Error('API key not found. Please add one in Settings.')
|
|
412
|
+
return decryptKey(cred.encryptedKey)
|
|
413
|
+
}
|
|
414
|
+
if (provider.optionalApiKey && session.credentialId) {
|
|
415
|
+
const creds = loadCredentials()
|
|
416
|
+
const cred = creds[session.credentialId]
|
|
417
|
+
if (cred) {
|
|
418
|
+
try { return decryptKey(cred.encryptedKey) } catch { return null }
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return null
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export interface PreparedBlockedChatTurn {
|
|
425
|
+
kind: 'blocked'
|
|
426
|
+
sessionId: string
|
|
427
|
+
session: Session
|
|
428
|
+
lifecycleRunId: string
|
|
429
|
+
blockedMessage: string
|
|
430
|
+
internal: boolean
|
|
431
|
+
runId?: string
|
|
432
|
+
syntheticEnabledIds: string[]
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export interface PreparedExecutableChatTurn {
|
|
436
|
+
kind: 'ready'
|
|
437
|
+
sessionId: string
|
|
438
|
+
message: string
|
|
439
|
+
internal: boolean
|
|
440
|
+
source: string
|
|
441
|
+
runId?: string
|
|
442
|
+
session: Session
|
|
443
|
+
sessionForRun: Session
|
|
444
|
+
appSettings: ReturnType<typeof loadSettings>
|
|
445
|
+
lifecycleRunId: string
|
|
446
|
+
agentForSession: ReturnType<typeof getAgent>
|
|
447
|
+
mission: Awaited<ReturnType<typeof resolveMissionForTurn>>
|
|
448
|
+
missionContextBlock?: string
|
|
449
|
+
extensionsForRun: string[]
|
|
450
|
+
effectiveMessage: string
|
|
451
|
+
providerType: string
|
|
452
|
+
provider: NonNullable<ReturnType<typeof getProvider>>
|
|
453
|
+
apiKey: string | null
|
|
454
|
+
hideAssistantTranscript: boolean
|
|
455
|
+
isHeartbeatRun: boolean
|
|
456
|
+
heartbeatLightContext: boolean
|
|
457
|
+
isAutoRunNoHistory: boolean
|
|
458
|
+
hasExtensions: boolean
|
|
459
|
+
systemPrompt?: string
|
|
460
|
+
resolvedImagePath?: string
|
|
461
|
+
runStartedAt: number
|
|
462
|
+
runMessageStartIndex: number
|
|
463
|
+
toolPolicy: ReturnType<typeof resolveSessionToolPolicy>
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export type PreparedChatTurn = PreparedBlockedChatTurn | PreparedExecutableChatTurn
|
|
467
|
+
|
|
468
|
+
export async function prepareChatTurn(input: ExecuteChatTurnInput): Promise<PreparedChatTurn> {
|
|
469
|
+
const estop = loadEstopState()
|
|
470
|
+
if (estop.level === 'all') {
|
|
471
|
+
throw new Error(estop.reason
|
|
472
|
+
? `Execution is blocked because all estop is engaged: ${estop.reason}`
|
|
473
|
+
: 'Execution is blocked because all estop is engaged.')
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const { message } = input
|
|
477
|
+
const {
|
|
478
|
+
sessionId,
|
|
479
|
+
imagePath,
|
|
480
|
+
imageUrl,
|
|
481
|
+
attachedFiles,
|
|
482
|
+
missionId: explicitMissionId,
|
|
483
|
+
internal = false,
|
|
484
|
+
runId,
|
|
485
|
+
source = 'chat',
|
|
486
|
+
onEvent,
|
|
487
|
+
} = input
|
|
488
|
+
|
|
489
|
+
const resolvedImagePath = resolveImagePath(imagePath, imageUrl) ?? undefined
|
|
490
|
+
|
|
491
|
+
syncSessionFromAgent(sessionId)
|
|
492
|
+
|
|
493
|
+
const session = getSession(sessionId)
|
|
494
|
+
if (!session) throw new Error(`Session not found: ${sessionId}`)
|
|
495
|
+
session.messages = Array.isArray(session.messages) ? session.messages : []
|
|
496
|
+
const runStartedAt = Date.now()
|
|
497
|
+
const runMessageStartIndex = session.messages.length
|
|
498
|
+
|
|
499
|
+
const appSettings = loadSettings()
|
|
500
|
+
const lifecycleRunId = runId || `${sessionId}:${runStartedAt}`
|
|
501
|
+
const agentForSession = session.agentId ? getAgent(session.agentId) : null
|
|
502
|
+
if (isAgentDisabled(agentForSession)) {
|
|
503
|
+
const blockedMessage = buildAgentDisabledMessage(agentForSession, 'run chats')
|
|
504
|
+
onEvent?.({ t: 'err', text: blockedMessage })
|
|
505
|
+
return {
|
|
506
|
+
kind: 'blocked',
|
|
507
|
+
sessionId,
|
|
508
|
+
session,
|
|
509
|
+
lifecycleRunId,
|
|
510
|
+
blockedMessage,
|
|
511
|
+
internal,
|
|
512
|
+
runId,
|
|
513
|
+
syntheticEnabledIds: getEnabledCapabilityIds(session),
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const runtimeCapabilityIds = filterRuntimeCapabilityIds(getEnabledCapabilityIds(session), {
|
|
518
|
+
delegationEnabled: agentForSession?.delegationEnabled === true,
|
|
519
|
+
})
|
|
520
|
+
const requestedCapabilityIds = runtimeCapabilityIds.length > 0
|
|
521
|
+
? listUniversalToolAccessExtensionIds(runtimeCapabilityIds)
|
|
522
|
+
: []
|
|
523
|
+
const toolPolicy = resolveSessionToolPolicy(requestedCapabilityIds, appSettings)
|
|
524
|
+
const isHeartbeatRun = input.internal === true && source === 'heartbeat'
|
|
525
|
+
const isAutonomousInternalRun = internal && source !== 'chat'
|
|
526
|
+
const heartbeatLightContext = isHeartbeatRun && !!input.heartbeatConfig?.lightContext
|
|
527
|
+
const isAutoRunNoHistory = isHeartbeatRun
|
|
528
|
+
|
|
529
|
+
if (shouldApplySessionFreshnessReset(source)) {
|
|
530
|
+
const freshness = evaluateSessionFreshness({
|
|
531
|
+
session,
|
|
532
|
+
policy: resolveSessionResetPolicy({
|
|
533
|
+
session,
|
|
534
|
+
agent: agentForSession,
|
|
535
|
+
settings: appSettings,
|
|
536
|
+
}),
|
|
537
|
+
})
|
|
538
|
+
if (!freshness.fresh) {
|
|
539
|
+
try { syncSessionArchiveMemory(session, { agent: agentForSession }) } catch { /* best-effort */ }
|
|
540
|
+
await runCapabilityHook(
|
|
541
|
+
'sessionEnd',
|
|
542
|
+
{
|
|
543
|
+
sessionId: session.id,
|
|
544
|
+
session,
|
|
545
|
+
messageCount: Array.isArray(session.messages) ? session.messages.length : 0,
|
|
546
|
+
durationMs: Date.now() - (session.createdAt || runStartedAt),
|
|
547
|
+
reason: freshness.reason || 'session_reset',
|
|
548
|
+
},
|
|
549
|
+
{ enabledIds: runtimeCapabilityIds },
|
|
550
|
+
)
|
|
551
|
+
resetSessionRuntime(session, freshness.reason || 'session_reset')
|
|
552
|
+
onEvent?.({ t: 'status', text: JSON.stringify({ sessionReset: freshness.reason || 'session_reset' }) })
|
|
553
|
+
saveSession(sessionId, session)
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (isAutonomousInternalRun) {
|
|
557
|
+
try { syncSessionArchiveMemory(session, { agent: agentForSession }) } catch { /* best-effort */ }
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const mission = await resolveMissionForTurn({
|
|
561
|
+
session,
|
|
562
|
+
message,
|
|
563
|
+
source,
|
|
564
|
+
internal,
|
|
565
|
+
runId: lifecycleRunId,
|
|
566
|
+
explicitMissionId: explicitMissionId || null,
|
|
567
|
+
})
|
|
568
|
+
if (mission?.id) {
|
|
569
|
+
session.missionId = mission.id
|
|
570
|
+
}
|
|
571
|
+
const extensionsForRun = toolPolicy.enabledExtensions
|
|
572
|
+
if (runMessageStartIndex === 0) {
|
|
573
|
+
await runCapabilityHook(
|
|
574
|
+
'sessionStart',
|
|
575
|
+
{
|
|
576
|
+
session,
|
|
577
|
+
resumedFrom: session.parentSessionId || null,
|
|
578
|
+
},
|
|
579
|
+
{ enabledIds: extensionsForRun },
|
|
580
|
+
)
|
|
581
|
+
}
|
|
582
|
+
const sessionForRunSelection = splitCapabilityIds(extensionsForRun)
|
|
583
|
+
let sessionForRun = JSON.stringify(runtimeCapabilityIds) === JSON.stringify(extensionsForRun)
|
|
584
|
+
? session
|
|
585
|
+
: { ...session, tools: sessionForRunSelection.tools, extensions: sessionForRunSelection.extensions }
|
|
586
|
+
if (mission?.id) {
|
|
587
|
+
sessionForRun = {
|
|
588
|
+
...sessionForRun,
|
|
589
|
+
missionId: mission.id,
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (agentForSession) {
|
|
593
|
+
const preferredRoute = resolvePrimaryAgentRoute(agentForSession, undefined, {
|
|
594
|
+
preferredGatewayTags: session.routePreferredGatewayTags || [],
|
|
595
|
+
preferredGatewayUseCase: session.routePreferredGatewayUseCase || null,
|
|
596
|
+
})
|
|
597
|
+
if (preferredRoute) {
|
|
598
|
+
sessionForRun = applyResolvedRoute({ ...sessionForRun }, preferredRoute)
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
let effectiveMessage = message
|
|
602
|
+
|
|
603
|
+
if (extensionsForRun.length > 0) {
|
|
604
|
+
try {
|
|
605
|
+
effectiveMessage = await transformCapabilityText(
|
|
606
|
+
'transformInboundMessage',
|
|
607
|
+
{ session: sessionForRun, text: message },
|
|
608
|
+
{ enabledIds: extensionsForRun },
|
|
609
|
+
)
|
|
610
|
+
} catch {
|
|
611
|
+
effectiveMessage = message
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (isHeartbeatRun && input.modelOverride) {
|
|
616
|
+
sessionForRun = { ...sessionForRun, model: input.modelOverride }
|
|
617
|
+
}
|
|
618
|
+
const missionContextBlock = buildMissionContextBlock(mission)
|
|
619
|
+
|
|
620
|
+
if (extensionsForRun.length > 0) {
|
|
621
|
+
const modelResolvePrompt = heartbeatLightContext
|
|
622
|
+
? (joinSystemPromptBlocks(buildLightHeartbeatSystemPrompt(sessionForRun), missionContextBlock) || '')
|
|
623
|
+
: (joinSystemPromptBlocks(buildAgentSystemPrompt(sessionForRun), missionContextBlock) || '')
|
|
624
|
+
const modelResolve = await runCapabilityBeforeModelResolve(
|
|
625
|
+
{
|
|
626
|
+
session: sessionForRun,
|
|
627
|
+
prompt: modelResolvePrompt,
|
|
628
|
+
message: effectiveMessage,
|
|
629
|
+
provider: sessionForRun.provider,
|
|
630
|
+
model: sessionForRun.model,
|
|
631
|
+
apiEndpoint: sessionForRun.apiEndpoint || null,
|
|
632
|
+
},
|
|
633
|
+
{ enabledIds: extensionsForRun },
|
|
634
|
+
)
|
|
635
|
+
if (modelResolve) {
|
|
636
|
+
sessionForRun = {
|
|
637
|
+
...sessionForRun,
|
|
638
|
+
provider: modelResolve.providerOverride ?? sessionForRun.provider,
|
|
639
|
+
model: modelResolve.modelOverride ?? sessionForRun.model,
|
|
640
|
+
...(modelResolve.apiEndpointOverride !== undefined ? { apiEndpoint: modelResolve.apiEndpointOverride } : {}),
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (toolPolicy.blockedExtensions.length > 0) {
|
|
646
|
+
const blockedSummary = toolPolicy.blockedExtensions
|
|
647
|
+
.map((entry) => `${entry.tool} (${entry.reason})`)
|
|
648
|
+
.join(', ')
|
|
649
|
+
onEvent?.({ t: 'err', text: `Capability policy blocked extensions for this run: ${blockedSummary}` })
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (session.agentId) {
|
|
653
|
+
const agent = getAgent(session.agentId)
|
|
654
|
+
if (agent) {
|
|
655
|
+
const budgetCheck = checkAgentBudgetLimits(agent)
|
|
656
|
+
const action = agent.budgetAction || 'warn'
|
|
657
|
+
|
|
658
|
+
if (budgetCheck.exceeded.length > 0) {
|
|
659
|
+
const blockedMessage = budgetCheck.exceeded.map((entry) => entry.message).join(' ')
|
|
660
|
+
if (action === 'block') {
|
|
661
|
+
onEvent?.({ t: 'err', text: blockedMessage })
|
|
662
|
+
return {
|
|
663
|
+
kind: 'blocked',
|
|
664
|
+
sessionId,
|
|
665
|
+
session,
|
|
666
|
+
lifecycleRunId,
|
|
667
|
+
blockedMessage,
|
|
668
|
+
internal,
|
|
669
|
+
runId,
|
|
670
|
+
syntheticEnabledIds: getEnabledCapabilityIds(session),
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
onEvent?.({ t: 'status', text: JSON.stringify({ budgetWarning: blockedMessage }) })
|
|
674
|
+
} else if (budgetCheck.warnings.length > 0) {
|
|
675
|
+
const warningText = budgetCheck.warnings.map((entry) => entry.message).join(' ')
|
|
676
|
+
onEvent?.({ t: 'status', text: JSON.stringify({ budgetWarning: warningText }) })
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const dailySpendLimitUsd = parseUsdLimit(appSettings.safetyMaxDailySpendUsd)
|
|
682
|
+
if (dailySpendLimitUsd !== null) {
|
|
683
|
+
const todaySpendUsd = getTodaySpendUsd()
|
|
684
|
+
if (todaySpendUsd >= dailySpendLimitUsd) {
|
|
685
|
+
const blockedMessage = `Safety budget reached: today's spend is $${todaySpendUsd.toFixed(4)} (limit $${dailySpendLimitUsd.toFixed(4)}). Increase safetyMaxDailySpendUsd to continue autonomous runs.`
|
|
686
|
+
onEvent?.({ t: 'err', text: blockedMessage })
|
|
687
|
+
return {
|
|
688
|
+
kind: 'blocked',
|
|
689
|
+
sessionId,
|
|
690
|
+
session,
|
|
691
|
+
lifecycleRunId,
|
|
692
|
+
blockedMessage,
|
|
693
|
+
internal,
|
|
694
|
+
runId,
|
|
695
|
+
syntheticEnabledIds: getEnabledCapabilityIds(session),
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const providerType = sessionForRun.provider || 'claude-cli'
|
|
701
|
+
const provider = getProvider(providerType)
|
|
702
|
+
if (!provider) throw new Error(`Unknown provider: ${providerType}`)
|
|
703
|
+
|
|
704
|
+
if (providerType === 'claude-cli' && !fs.existsSync(session.cwd)) {
|
|
705
|
+
throw new Error(`Directory not found: ${session.cwd}`)
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const apiKey = resolveApiKeyForSession(sessionForRun, provider)
|
|
709
|
+
const hideAssistantTranscript = internal && source === 'main-loop-followup'
|
|
710
|
+
|
|
711
|
+
const shouldPersistUserMessage = shouldPersistInboundUserMessage(internal, source)
|
|
712
|
+
if (shouldPersistUserMessage) {
|
|
713
|
+
const linkAnalysis = !internal ? await runLinkUnderstanding(message) : []
|
|
714
|
+
const guardedUserText = guardUntrustedText({
|
|
715
|
+
text: message,
|
|
716
|
+
source,
|
|
717
|
+
mode: getUntrustedContentGuardMode(appSettings),
|
|
718
|
+
trusted: (source === 'chat' && !internal) || internal,
|
|
719
|
+
}).text
|
|
720
|
+
const nextUserMessage = await applyMessageLifecycleHooks({
|
|
721
|
+
session,
|
|
722
|
+
message: {
|
|
723
|
+
role: 'user',
|
|
724
|
+
text: guardedUserText,
|
|
725
|
+
time: Date.now(),
|
|
726
|
+
imagePath: imagePath || undefined,
|
|
727
|
+
imageUrl: imageUrl || undefined,
|
|
728
|
+
attachedFiles: attachedFiles?.length ? attachedFiles : undefined,
|
|
729
|
+
replyToId: input.replyToId || undefined,
|
|
730
|
+
},
|
|
731
|
+
enabledIds: extensionsForRun,
|
|
732
|
+
phase: 'user',
|
|
733
|
+
runId: lifecycleRunId,
|
|
734
|
+
})
|
|
735
|
+
if (nextUserMessage) {
|
|
736
|
+
session.messages.push(nextUserMessage)
|
|
737
|
+
if (linkAnalysis.length > 0) {
|
|
738
|
+
const linkAnalysisMessage = await applyMessageLifecycleHooks({
|
|
739
|
+
session,
|
|
740
|
+
message: {
|
|
741
|
+
role: 'assistant',
|
|
742
|
+
kind: 'system',
|
|
743
|
+
text: `[Automated Link Analysis]\n${linkAnalysis.join('\n\n')}`,
|
|
744
|
+
time: Date.now(),
|
|
745
|
+
},
|
|
746
|
+
enabledIds: extensionsForRun,
|
|
747
|
+
phase: 'system',
|
|
748
|
+
runId: lifecycleRunId,
|
|
749
|
+
isSynthetic: true,
|
|
750
|
+
})
|
|
751
|
+
if (linkAnalysisMessage) {
|
|
752
|
+
session.messages.push(linkAnalysisMessage)
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
session.lastActiveAt = Date.now()
|
|
756
|
+
saveSession(sessionId, session)
|
|
757
|
+
if (!internal && source === 'chat') {
|
|
758
|
+
try {
|
|
759
|
+
bridgeHumanReplyFromChat({
|
|
760
|
+
sessionId,
|
|
761
|
+
payload: nextUserMessage.text,
|
|
762
|
+
})
|
|
763
|
+
} catch {
|
|
764
|
+
// Best-effort mailbox bridge only.
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if (!internal) {
|
|
768
|
+
try {
|
|
769
|
+
await runCapabilityHook('onMessage', { session, message: nextUserMessage }, { enabledIds: extensionsForRun })
|
|
770
|
+
} catch {
|
|
771
|
+
// onMessage hooks are non-critical.
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const useLocalOpenClawNativeRuntime = providerType === 'openclaw' && isLocalOpenClawEndpoint(sessionForRun.apiEndpoint)
|
|
778
|
+
const enabledSessionExtensions = getEnabledCapabilityIds(sessionForRun)
|
|
779
|
+
const hasExtensions = enabledSessionExtensions.length > 0
|
|
780
|
+
&& !NON_LANGGRAPH_PROVIDER_IDS.has(providerType)
|
|
781
|
+
&& !useLocalOpenClawNativeRuntime
|
|
782
|
+
|
|
783
|
+
const systemPrompt = heartbeatLightContext
|
|
784
|
+
? joinSystemPromptBlocks(buildLightHeartbeatSystemPrompt(sessionForRun), missionContextBlock)
|
|
785
|
+
: (hasExtensions ? undefined : joinSystemPromptBlocks(buildAgentSystemPrompt(sessionForRun), missionContextBlock))
|
|
786
|
+
|
|
787
|
+
return {
|
|
788
|
+
kind: 'ready',
|
|
789
|
+
sessionId,
|
|
790
|
+
message,
|
|
791
|
+
internal,
|
|
792
|
+
source,
|
|
793
|
+
runId,
|
|
794
|
+
session,
|
|
795
|
+
sessionForRun,
|
|
796
|
+
appSettings,
|
|
797
|
+
lifecycleRunId,
|
|
798
|
+
agentForSession,
|
|
799
|
+
mission,
|
|
800
|
+
missionContextBlock: missionContextBlock || undefined,
|
|
801
|
+
extensionsForRun,
|
|
802
|
+
effectiveMessage,
|
|
803
|
+
providerType,
|
|
804
|
+
provider,
|
|
805
|
+
apiKey,
|
|
806
|
+
hideAssistantTranscript,
|
|
807
|
+
isHeartbeatRun,
|
|
808
|
+
heartbeatLightContext,
|
|
809
|
+
isAutoRunNoHistory,
|
|
810
|
+
hasExtensions,
|
|
811
|
+
systemPrompt,
|
|
812
|
+
resolvedImagePath,
|
|
813
|
+
runStartedAt,
|
|
814
|
+
runMessageStartIndex,
|
|
815
|
+
toolPolicy,
|
|
816
|
+
}
|
|
817
|
+
}
|