@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
|
@@ -1,115 +1,16 @@
|
|
|
1
|
-
import
|
|
2
|
-
import os from 'os'
|
|
1
|
+
import type { ExecuteChatTurnInput, ExecuteChatTurnResult } from './chat-execution-types'
|
|
3
2
|
import { perf } from '@/lib/server/runtime/perf'
|
|
3
|
+
import { markProviderSuccess } from '@/lib/server/provider-health'
|
|
4
|
+
import { executePreparedChatTurn } from '@/lib/server/chat-execution/chat-turn-stream-execution'
|
|
5
|
+
import { finalizeChatTurn } from '@/lib/server/chat-execution/chat-turn-finalization'
|
|
6
|
+
import { prepareChatTurn } from '@/lib/server/chat-execution/chat-turn-preparation'
|
|
4
7
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
loadCredentials,
|
|
8
|
-
decryptKey,
|
|
9
|
-
getSessionMessages,
|
|
10
|
-
loadAgents,
|
|
11
|
-
loadSkills,
|
|
12
|
-
loadSettings,
|
|
13
|
-
appendUsage,
|
|
14
|
-
active,
|
|
15
|
-
} from '@/lib/server/storage'
|
|
16
|
-
import { getProvider } from '@/lib/providers'
|
|
17
|
-
import { CONTEXT_OVERFLOW_RE } from '@/lib/providers/error-classification'
|
|
18
|
-
import { estimateCost, checkAgentBudgetLimits } from '@/lib/server/cost'
|
|
19
|
-
import { log } from '@/lib/server/logger'
|
|
20
|
-
import { logExecution } from '@/lib/server/execution-log'
|
|
21
|
-
import { streamAgentChat } from '@/lib/server/chat-execution/stream-agent-chat'
|
|
22
|
-
import { buildToolSection, joinPromptSegments } from '@/lib/server/chat-execution/prompt-builder'
|
|
23
|
-
import { pruneIncompleteToolEvents } from '@/lib/server/chat-execution/chat-streaming-utils'
|
|
24
|
-
import { runLinkUnderstanding } from '@/lib/server/link-understanding'
|
|
25
|
-
import type { Session } from '@/types'
|
|
26
|
-
import { stripMainLoopMetaForPersistence } from '@/lib/server/agents/main-agent-loop'
|
|
27
|
-
import { isLocalOpenClawEndpoint, normalizeProviderEndpoint } from '@/lib/openclaw/openclaw-endpoint'
|
|
28
|
-
import { notify } from '@/lib/server/ws-hub'
|
|
29
|
-
import { applyResolvedRoute, resolvePrimaryAgentRoute } from '@/lib/server/agents/agent-runtime-config'
|
|
30
|
-
import { resolveSessionToolPolicy } from '@/lib/server/tool-capability-policy'
|
|
31
|
-
import { buildCurrentDateTimePromptContext } from '@/lib/server/prompt-runtime-context'
|
|
32
|
-
import { buildWorkspaceContext } from '@/lib/server/workspace-context'
|
|
33
|
-
import { buildRuntimeSkillPromptBlocks, resolveRuntimeSkills } from '@/lib/server/skills/runtime-skill-resolver'
|
|
34
|
-
import { resolveImagePath } from '@/lib/server/resolve-image'
|
|
8
|
+
createPartialAssistantPersistence,
|
|
9
|
+
} from '@/lib/server/chat-execution/chat-turn-partial-persistence'
|
|
35
10
|
import {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
shouldAutoRouteHeartbeatAlerts,
|
|
40
|
-
shouldPersistInboundUserMessage,
|
|
41
|
-
translateRequestedToolInvocation,
|
|
42
|
-
normalizeAssistantArtifactLinks,
|
|
43
|
-
extractHeartbeatStatus,
|
|
44
|
-
shouldReplaceRecentAssistantMessage,
|
|
45
|
-
hasPersistableAssistantPayload,
|
|
46
|
-
getPersistedAssistantText,
|
|
47
|
-
getToolEventsSnapshotKey,
|
|
48
|
-
requestedToolNamesFromMessage,
|
|
49
|
-
shouldReplaceRecentConnectorFollowupMessage,
|
|
50
|
-
shouldSuppressRedundantConnectorDeliveryFollowup,
|
|
51
|
-
hasDirectLocalCodingTools,
|
|
52
|
-
parseUsdLimit,
|
|
53
|
-
getTodaySpendUsd,
|
|
54
|
-
classifyHeartbeatResponse,
|
|
55
|
-
estimateConversationTone,
|
|
56
|
-
pruneOldHeartbeatMessages,
|
|
57
|
-
} from '@/lib/server/chat-execution/chat-execution-utils'
|
|
58
|
-
import { reconcileConnectorDeliveryText } from '@/lib/server/chat-execution/chat-execution-connector-delivery'
|
|
59
|
-
import {
|
|
60
|
-
resolveRequestedToolPreflightResponse,
|
|
61
|
-
runExclusiveDirectMemoryPreflight,
|
|
62
|
-
runPostLlmToolRouting,
|
|
63
|
-
} from '@/lib/server/chat-execution/chat-turn-tool-routing'
|
|
64
|
-
import {
|
|
65
|
-
applyExactOutputContract,
|
|
66
|
-
classifyExactOutputContract,
|
|
67
|
-
type ExactOutputContract,
|
|
68
|
-
} from '@/lib/server/chat-execution/exact-output-contract'
|
|
69
|
-
import {
|
|
70
|
-
getCachedLlmResponse,
|
|
71
|
-
resolveLlmResponseCacheConfig,
|
|
72
|
-
setCachedLlmResponse,
|
|
73
|
-
type LlmResponseCacheKeyInput,
|
|
74
|
-
} from '@/lib/server/llm-response-cache'
|
|
75
|
-
import type { Message, MessageToolEvent, SSEEvent, UsageRecord } from '@/types'
|
|
76
|
-
import { markProviderFailure, markProviderSuccess } from '@/lib/server/provider-health'
|
|
77
|
-
import { isHeartbeatSource, isInternalHeartbeatRun } from '@/lib/server/runtime/heartbeat-source'
|
|
78
|
-
import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
|
|
79
|
-
import { buildIdentityContinuityContext, refreshSessionIdentityState } from '@/lib/server/identity-continuity'
|
|
80
|
-
import { resolveEffectiveSessionMemoryScopeMode } from '@/lib/server/memory/session-memory-scope'
|
|
81
|
-
import { syncSessionArchiveMemory } from '@/lib/server/memory/session-archive-memory'
|
|
82
|
-
import { evaluateSessionFreshness, resetSessionRuntime, resolveSessionResetPolicy } from '@/lib/server/session-reset-policy'
|
|
83
|
-
import { pruneStreamingAssistantArtifacts, upsertStreamingAssistantArtifact } from '@/lib/chat/chat-streaming-state'
|
|
84
|
-
import { shouldSuppressHiddenControlText, stripHiddenControlTokens } from '@/lib/server/agents/assistant-control'
|
|
85
|
-
import { buildAgentDisabledMessage, isAgentDisabled } from '@/lib/server/agents/agent-availability'
|
|
86
|
-
import { isDirectConnectorSession } from '@/lib/server/connectors/session-kind'
|
|
87
|
-
import { errorMessage as toErrorMessage } from '@/lib/shared-utils'
|
|
88
|
-
import { listUniversalToolAccessExtensionIds } from '@/lib/server/universal-tool-access'
|
|
89
|
-
import { bridgeHumanReplyFromChat } from '@/lib/server/chatrooms/session-mailbox'
|
|
90
|
-
import {
|
|
91
|
-
collectCapabilityDescriptions,
|
|
92
|
-
collectCapabilityOperatingGuidance,
|
|
93
|
-
runCapabilityBeforeMessageWrite,
|
|
94
|
-
runCapabilityBeforeModelResolve,
|
|
95
|
-
runCapabilityHook,
|
|
96
|
-
runCapabilityToolResultPersist,
|
|
97
|
-
transformCapabilityText,
|
|
98
|
-
} from '@/lib/server/native-capabilities'
|
|
99
|
-
import {
|
|
100
|
-
getEnabledCapabilityIds,
|
|
101
|
-
getEnabledCapabilitySelection,
|
|
102
|
-
splitCapabilityIds,
|
|
103
|
-
} from '@/lib/capability-selection'
|
|
104
|
-
import { guardUntrustedText, guardUntrustedToolEvents, getUntrustedContentGuardMode } from '@/lib/server/untrusted-content'
|
|
105
|
-
import { loadEstopState } from '@/lib/server/runtime/estop'
|
|
106
|
-
import {
|
|
107
|
-
applyMissionOutcomeForTurn,
|
|
108
|
-
buildMissionContextBlock,
|
|
109
|
-
resolveMissionForTurn,
|
|
110
|
-
} from '@/lib/server/missions/mission-service'
|
|
111
|
-
|
|
112
|
-
const TAG = 'chat-execution'
|
|
11
|
+
completeBlockedChatTurn,
|
|
12
|
+
runChatTurnPreflight,
|
|
13
|
+
} from '@/lib/server/chat-execution/chat-turn-preflight'
|
|
113
14
|
|
|
114
15
|
export {
|
|
115
16
|
shouldApplySessionFreshnessReset,
|
|
@@ -119,1851 +20,108 @@ export {
|
|
|
119
20
|
requestedToolNamesFromMessage,
|
|
120
21
|
filterRuntimeCapabilityIds,
|
|
121
22
|
hasDirectLocalCodingTools,
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function buildAgentRuntimeCapabilities(enabledExtensions: string[]): string[] {
|
|
126
|
-
const capabilities = ['heartbeats', 'autonomous_loop', 'multi_agent_chat']
|
|
127
|
-
if (enabledExtensions.length > 0) capabilities.unshift('tools')
|
|
128
|
-
return capabilities
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function buildNoToolsGuidance(): string[] {
|
|
132
|
-
return [
|
|
133
|
-
'## Tool Availability',
|
|
134
|
-
'No runtime tools are available in this chat after policy filtering.',
|
|
135
|
-
'Do not imply that a normal read-only action is waiting on user permission when the real blocker is missing tool access.',
|
|
136
|
-
'If browsing, web fetches, file edits, or other actions are unavailable, state that the capability is blocked by runtime policy in this session.',
|
|
137
|
-
'Only mention confirmation or approval when a real runtime tool explicitly returned that boundary for a concrete action.',
|
|
138
|
-
]
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function buildEnabledToolsAutonomyGuidance(): string[] {
|
|
142
|
-
return [
|
|
143
|
-
'## Tool Autonomy',
|
|
144
|
-
'Runtime tools are already available for normal use in this chat.',
|
|
145
|
-
'Do not request that a tool be enabled or switched on before using it.',
|
|
146
|
-
'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.',
|
|
147
|
-
'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.',
|
|
148
|
-
'If the task depends on current or external information and web tools are enabled, use them instead of answering from stale memory.',
|
|
149
|
-
'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.',
|
|
150
|
-
'If the task asks you to inspect the local repository, runtime, or filesystem state, use shell or file tools instead of guessing.',
|
|
151
|
-
'Treat capability policy blocks and explicit platform feature gates as the real boundaries. Do not invent an approval queue when none exists.',
|
|
152
|
-
]
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function resolveHeartbeatLastConnectorTarget(session: Session | null | undefined): {
|
|
156
|
-
connectorId?: string
|
|
157
|
-
channelId: string
|
|
158
|
-
} | null {
|
|
159
|
-
if (!isDirectConnectorSession(session)) return null
|
|
160
|
-
const connectorId = typeof session?.connectorContext?.connectorId === 'string'
|
|
161
|
-
? session.connectorContext.connectorId.trim()
|
|
162
|
-
: ''
|
|
163
|
-
const channelId = typeof session?.connectorContext?.channelId === 'string'
|
|
164
|
-
? session.connectorContext.channelId.trim()
|
|
165
|
-
: ''
|
|
166
|
-
if (!channelId) return null
|
|
167
|
-
return {
|
|
168
|
-
connectorId: connectorId || undefined,
|
|
169
|
-
channelId,
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
type PersistPhase = 'user' | 'system' | 'assistant_partial' | 'assistant_final' | 'heartbeat'
|
|
174
|
-
|
|
175
|
-
async function applyMessageLifecycleHooks(params: {
|
|
176
|
-
session: Session
|
|
177
|
-
message: Message
|
|
178
|
-
enabledIds: string[]
|
|
179
|
-
phase: PersistPhase
|
|
180
|
-
runId?: string
|
|
181
|
-
isSynthetic?: boolean
|
|
182
|
-
}): Promise<Message | null> {
|
|
183
|
-
let currentMessage = params.message
|
|
184
|
-
const guardMode = getUntrustedContentGuardMode(loadSettings())
|
|
185
|
-
if (Array.isArray(currentMessage.toolEvents) && currentMessage.toolEvents.length > 0) {
|
|
186
|
-
currentMessage = {
|
|
187
|
-
...currentMessage,
|
|
188
|
-
toolEvents: guardUntrustedToolEvents({
|
|
189
|
-
toolEvents: currentMessage.toolEvents,
|
|
190
|
-
mode: guardMode,
|
|
191
|
-
}),
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
const toolEvents = Array.isArray(currentMessage.toolEvents)
|
|
195
|
-
? currentMessage.toolEvents.filter((event) => typeof event.output === 'string' || event.error === true)
|
|
196
|
-
: []
|
|
197
|
-
|
|
198
|
-
for (const event of toolEvents) {
|
|
199
|
-
currentMessage = await runCapabilityToolResultPersist(
|
|
200
|
-
{
|
|
201
|
-
session: params.session,
|
|
202
|
-
message: currentMessage,
|
|
203
|
-
toolName: event.name,
|
|
204
|
-
toolCallId: event.toolCallId,
|
|
205
|
-
isSynthetic: params.isSynthetic,
|
|
206
|
-
},
|
|
207
|
-
{ enabledIds: params.enabledIds },
|
|
208
|
-
)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const writeResult = await runCapabilityBeforeMessageWrite(
|
|
212
|
-
{
|
|
213
|
-
session: params.session,
|
|
214
|
-
message: currentMessage,
|
|
215
|
-
phase: params.phase,
|
|
216
|
-
runId: params.runId,
|
|
217
|
-
},
|
|
218
|
-
{ enabledIds: params.enabledIds },
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
if (writeResult.block) return null
|
|
222
|
-
return writeResult.message
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
interface SessionWithCredentials {
|
|
226
|
-
credentialId?: string | null
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
interface ProviderApiKeyConfig {
|
|
230
|
-
requiresApiKey?: boolean
|
|
231
|
-
optionalApiKey?: boolean
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
export interface ExecuteChatTurnInput {
|
|
235
|
-
sessionId: string
|
|
236
|
-
message: string
|
|
237
|
-
missionId?: string | null
|
|
238
|
-
imagePath?: string
|
|
239
|
-
imageUrl?: string
|
|
240
|
-
attachedFiles?: string[]
|
|
241
|
-
internal?: boolean
|
|
242
|
-
source?: string
|
|
243
|
-
runId?: string
|
|
244
|
-
signal?: AbortSignal
|
|
245
|
-
onEvent?: (event: SSEEvent) => void
|
|
246
|
-
modelOverride?: string
|
|
247
|
-
heartbeatConfig?: {
|
|
248
|
-
ackMaxChars: number
|
|
249
|
-
showOk: boolean
|
|
250
|
-
showAlerts: boolean
|
|
251
|
-
target: string | null
|
|
252
|
-
lightContext?: boolean
|
|
253
|
-
deliveryMode?: 'default' | 'tool_only' | 'silent'
|
|
254
|
-
}
|
|
255
|
-
replyToId?: string
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
export interface ExecuteChatTurnResult {
|
|
259
|
-
runId?: string
|
|
260
|
-
sessionId: string
|
|
261
|
-
missionId?: string | null
|
|
262
|
-
text: string
|
|
263
|
-
persisted: boolean
|
|
264
|
-
toolEvents: MessageToolEvent[]
|
|
265
|
-
error?: string
|
|
266
|
-
inputTokens?: number
|
|
267
|
-
outputTokens?: number
|
|
268
|
-
estimatedCost?: number
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const EXACT_OUTPUT_CONTRACT_TIMEOUT_MS = 5_000
|
|
272
|
-
|
|
273
|
-
async function resolveExactOutputContractWithTimeout(params: {
|
|
274
|
-
sessionId: string
|
|
275
|
-
agentId?: string | null
|
|
276
|
-
userMessage: string
|
|
277
|
-
currentResponse: string
|
|
278
|
-
toolEvents: MessageToolEvent[]
|
|
279
|
-
internal: boolean
|
|
280
|
-
source: string
|
|
281
|
-
}): Promise<ExactOutputContract | null> {
|
|
282
|
-
if (params.internal || params.source !== 'chat') return null
|
|
283
|
-
if (params.toolEvents.length === 0) return null
|
|
284
|
-
// Skip expensive LLM classifier when no explicit exact-output markers appear
|
|
285
|
-
const { extractExplicitExactLiteral } = await import('@/lib/server/chat-execution/exact-output-contract')
|
|
286
|
-
if (!extractExplicitExactLiteral(params.userMessage)) return null
|
|
287
|
-
|
|
288
|
-
let timer: NodeJS.Timeout | null = null
|
|
289
|
-
try {
|
|
290
|
-
return await Promise.race<ExactOutputContract | null>([
|
|
291
|
-
classifyExactOutputContract({
|
|
292
|
-
sessionId: params.sessionId,
|
|
293
|
-
agentId: params.agentId || null,
|
|
294
|
-
userMessage: params.userMessage,
|
|
295
|
-
currentResponse: params.currentResponse,
|
|
296
|
-
toolEvents: params.toolEvents,
|
|
297
|
-
}).catch(() => null),
|
|
298
|
-
new Promise<null>((resolve) => {
|
|
299
|
-
timer = setTimeout(() => resolve(null), EXACT_OUTPUT_CONTRACT_TIMEOUT_MS)
|
|
300
|
-
}),
|
|
301
|
-
])
|
|
302
|
-
} finally {
|
|
303
|
-
if (timer) clearTimeout(timer)
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function extractEventJson(line: string): SSEEvent | null {
|
|
308
|
-
if (!line.startsWith('data: ')) return null
|
|
309
|
-
try {
|
|
310
|
-
return JSON.parse(line.slice(6).trim()) as SSEEvent
|
|
311
|
-
} catch {
|
|
312
|
-
return null
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function joinSystemPromptBlocks(...blocks: Array<string | null | undefined>): string | undefined {
|
|
317
|
-
const joined = joinPromptSegments(...blocks)
|
|
318
|
-
return joined || undefined
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
export function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
|
|
322
|
-
if (ev.t === 'tool_call') {
|
|
323
|
-
const previous = bag[bag.length - 1]
|
|
324
|
-
if (
|
|
325
|
-
previous
|
|
326
|
-
&& previous.name === (ev.toolName || 'unknown')
|
|
327
|
-
&& previous.input === (ev.toolInput || '')
|
|
328
|
-
&& previous.toolCallId === (ev.toolCallId || previous.toolCallId)
|
|
329
|
-
&& !previous.output
|
|
330
|
-
) {
|
|
331
|
-
return
|
|
332
|
-
}
|
|
333
|
-
bag.push({
|
|
334
|
-
name: ev.toolName || 'unknown',
|
|
335
|
-
input: ev.toolInput || '',
|
|
336
|
-
toolCallId: ev.toolCallId,
|
|
337
|
-
})
|
|
338
|
-
return
|
|
339
|
-
}
|
|
340
|
-
if (ev.t === 'tool_result') {
|
|
341
|
-
const idx = ev.toolCallId
|
|
342
|
-
? bag.findLastIndex((e) => e.toolCallId === ev.toolCallId && !e.output)
|
|
343
|
-
: bag.findLastIndex((e) => e.name === (ev.toolName || 'unknown') && !e.output)
|
|
344
|
-
if (idx === -1) return
|
|
345
|
-
const output = ev.toolOutput || ''
|
|
346
|
-
bag[idx] = {
|
|
347
|
-
...bag[idx],
|
|
348
|
-
output,
|
|
349
|
-
error: isLikelyToolErrorOutput(output) || undefined,
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
export function dedupeConsecutiveToolEvents(events: MessageToolEvent[]): MessageToolEvent[] {
|
|
355
|
-
const sameEvent = (left: MessageToolEvent, right: MessageToolEvent): boolean => (
|
|
356
|
-
left.name === right.name
|
|
357
|
-
&& left.input === right.input
|
|
358
|
-
&& (left.output || '') === (right.output || '')
|
|
359
|
-
&& (left.error === true) === (right.error === true)
|
|
360
|
-
)
|
|
361
|
-
const sameBlock = (startA: number, startB: number, size: number): boolean => {
|
|
362
|
-
for (let offset = 0; offset < size; offset += 1) {
|
|
363
|
-
if (!sameEvent(events[startA + offset], events[startB + offset])) return false
|
|
364
|
-
}
|
|
365
|
-
return true
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const deduped: MessageToolEvent[] = []
|
|
369
|
-
for (let index = 0; index < events.length;) {
|
|
370
|
-
const remaining = events.length - index
|
|
371
|
-
let collapsed = false
|
|
372
|
-
for (let blockSize = Math.floor(remaining / 2); blockSize >= 1; blockSize -= 1) {
|
|
373
|
-
if (!sameBlock(index, index + blockSize, blockSize)) continue
|
|
374
|
-
for (let offset = 0; offset < blockSize; offset += 1) deduped.push(events[index + offset])
|
|
375
|
-
const blockStart = index
|
|
376
|
-
index += blockSize
|
|
377
|
-
while (index + blockSize <= events.length && sameBlock(blockStart, index, blockSize)) {
|
|
378
|
-
index += blockSize
|
|
379
|
-
}
|
|
380
|
-
collapsed = true
|
|
381
|
-
break
|
|
382
|
-
}
|
|
383
|
-
if (collapsed) continue
|
|
384
|
-
deduped.push(events[index])
|
|
385
|
-
index += 1
|
|
386
|
-
}
|
|
387
|
-
return deduped
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
export function deriveTerminalRunError(params: {
|
|
391
|
-
errorMessage?: string
|
|
392
|
-
fullResponse: string
|
|
393
|
-
streamErrors: string[]
|
|
394
|
-
toolEvents: MessageToolEvent[]
|
|
395
|
-
internal: boolean
|
|
396
|
-
}): string | undefined {
|
|
397
|
-
if (params.errorMessage) return params.errorMessage
|
|
398
|
-
|
|
399
|
-
if (params.streamErrors.length > 0 && !params.fullResponse.trim()) {
|
|
400
|
-
return params.streamErrors[params.streamErrors.length - 1]
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (!params.internal && !params.fullResponse.trim() && params.toolEvents.length === 0) {
|
|
404
|
-
return 'Run completed without any response text, tool calls, or explicit error details. Check the provider configuration and try again.'
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
return undefined
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
export function shouldAppendMissedRequestedToolNotice(params: {
|
|
411
|
-
missedRequestedTools: string[]
|
|
412
|
-
fullResponse: string
|
|
413
|
-
errorMessage?: string
|
|
414
|
-
calledToolCount?: number
|
|
415
|
-
}): boolean {
|
|
416
|
-
if (!Array.isArray(params.missedRequestedTools) || params.missedRequestedTools.length === 0) return false
|
|
417
|
-
if (params.errorMessage) return false
|
|
418
|
-
if (params.fullResponse.includes('Tool execution notice:')) return false
|
|
419
|
-
if (!params.fullResponse.trim() && (params.calledToolCount || 0) === 0) return false
|
|
420
|
-
return true
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function shouldAutoDraftSkillSuggestion(params: {
|
|
424
|
-
assistantPersisted: boolean
|
|
425
|
-
internal: boolean
|
|
426
|
-
isHeartbeatRun: boolean
|
|
427
|
-
agentAutoDraftSetting: boolean
|
|
428
|
-
toolEventCount: number
|
|
429
|
-
messageCount: number
|
|
430
|
-
}): boolean {
|
|
431
|
-
if (!params.assistantPersisted) return false
|
|
432
|
-
if (params.internal || params.isHeartbeatRun) return false
|
|
433
|
-
if (!params.agentAutoDraftSetting) return false
|
|
434
|
-
if (params.toolEventCount === 0) return false
|
|
435
|
-
return params.messageCount >= 4
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
export function isLikelyToolErrorOutput(output: string): boolean {
|
|
439
|
-
const trimmed = String(output || '').trim()
|
|
440
|
-
if (!trimmed) return false
|
|
441
|
-
if (/^(Error(?::|\s*\(exit\b[^)]*\):?)|error:)/i.test(trimmed)) return true
|
|
442
|
-
if (/\b(MCP error|ECONNREFUSED|ETIMEDOUT|ERR_CONNECTION_REFUSED|ENOENT|EACCES)\b/i.test(trimmed)) return true
|
|
443
|
-
if (/\binvalid_type\b/i.test(trimmed) && /\b(issue|issues|expected|required|received|zod)\b/i.test(trimmed)) return true
|
|
444
|
-
try {
|
|
445
|
-
const parsed = JSON.parse(trimmed) as Record<string, unknown>
|
|
446
|
-
const status = typeof parsed.status === 'string' ? parsed.status.trim().toLowerCase() : ''
|
|
447
|
-
if (status === 'error' || status === 'failed') return true
|
|
448
|
-
if (typeof parsed.error === 'string' && parsed.error.trim()) return true
|
|
449
|
-
} catch {
|
|
450
|
-
// Ignore non-JSON tool output.
|
|
451
|
-
}
|
|
452
|
-
return false
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
export function pruneSuppressedHeartbeatStreamMessage(messages: Message[]): boolean {
|
|
456
|
-
return pruneStreamingAssistantArtifacts(messages)
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function syncSessionFromAgent(sessionId: string): void {
|
|
460
|
-
const sessions = loadSessions()
|
|
461
|
-
const session = sessions[sessionId]
|
|
462
|
-
if (!session?.agentId) return
|
|
463
|
-
const agents = loadAgents()
|
|
464
|
-
const agent = agents[session.agentId]
|
|
465
|
-
if (!agent) return
|
|
466
|
-
|
|
467
|
-
let changed = false
|
|
468
|
-
const route = resolvePrimaryAgentRoute(agent, undefined, {
|
|
469
|
-
preferredGatewayTags: session.routePreferredGatewayTags || [],
|
|
470
|
-
preferredGatewayUseCase: session.routePreferredGatewayUseCase || null,
|
|
471
|
-
})
|
|
472
|
-
if (!session.provider && agent.provider) { session.provider = agent.provider; changed = true }
|
|
473
|
-
if ((session.model === undefined || session.model === null || session.model === '') && agent.model !== undefined) {
|
|
474
|
-
session.model = agent.model
|
|
475
|
-
changed = true
|
|
476
|
-
}
|
|
477
|
-
if (route) {
|
|
478
|
-
const resolved = applyResolvedRoute({ ...session }, route)
|
|
479
|
-
if (session.provider !== resolved.provider) { session.provider = resolved.provider; changed = true }
|
|
480
|
-
if (session.model !== resolved.model) { session.model = resolved.model; changed = true }
|
|
481
|
-
if ((session.credentialId || null) !== (resolved.credentialId || null)) {
|
|
482
|
-
session.credentialId = resolved.credentialId ?? null
|
|
483
|
-
changed = true
|
|
484
|
-
}
|
|
485
|
-
if (JSON.stringify(session.fallbackCredentialIds || []) !== JSON.stringify(resolved.fallbackCredentialIds || [])) {
|
|
486
|
-
session.fallbackCredentialIds = [...(resolved.fallbackCredentialIds || [])]
|
|
487
|
-
changed = true
|
|
488
|
-
}
|
|
489
|
-
if ((session.apiEndpoint || null) !== (resolved.apiEndpoint || null)) {
|
|
490
|
-
session.apiEndpoint = resolved.apiEndpoint ?? null
|
|
491
|
-
changed = true
|
|
492
|
-
}
|
|
493
|
-
if ((session.gatewayProfileId || null) !== (resolved.gatewayProfileId || null)) {
|
|
494
|
-
session.gatewayProfileId = resolved.gatewayProfileId ?? null
|
|
495
|
-
changed = true
|
|
496
|
-
}
|
|
497
|
-
} else {
|
|
498
|
-
if (session.credentialId === undefined && agent.credentialId !== undefined) {
|
|
499
|
-
session.credentialId = agent.credentialId ?? null
|
|
500
|
-
changed = true
|
|
501
|
-
}
|
|
502
|
-
if ((session.apiEndpoint === undefined || session.apiEndpoint === null) && agent.apiEndpoint !== undefined) {
|
|
503
|
-
const normalized = normalizeProviderEndpoint(agent.provider, agent.apiEndpoint ?? null)
|
|
504
|
-
if (normalized !== session.apiEndpoint) { session.apiEndpoint = normalized; changed = true }
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
const agentSelection = getEnabledCapabilitySelection(agent)
|
|
508
|
-
// Subagent sessions have capabilities computed at spawn time (agent + parent merge).
|
|
509
|
-
// Don't overwrite them with just the agent's capabilities.
|
|
510
|
-
if (!session.parentSessionId) {
|
|
511
|
-
const currentSelection = getEnabledCapabilitySelection(session)
|
|
512
|
-
if (
|
|
513
|
-
JSON.stringify(currentSelection.tools) !== JSON.stringify(agentSelection.tools)
|
|
514
|
-
|| JSON.stringify(currentSelection.extensions) !== JSON.stringify(agentSelection.extensions)
|
|
515
|
-
) {
|
|
516
|
-
session.tools = agentSelection.tools
|
|
517
|
-
session.extensions = agentSelection.extensions
|
|
518
|
-
changed = true
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
const desiredMemoryScopeMode = resolveEffectiveSessionMemoryScopeMode(session, agent.memoryScopeMode ?? null)
|
|
522
|
-
if ((((session as unknown as Record<string, unknown>).memoryScopeMode as string | null | undefined) ?? null) !== desiredMemoryScopeMode) {
|
|
523
|
-
;(session as unknown as Record<string, unknown>).memoryScopeMode = desiredMemoryScopeMode
|
|
524
|
-
changed = true
|
|
525
|
-
}
|
|
526
|
-
const isShortcutChat = session.shortcutForAgentId === agent.id || agent.threadSessionId === sessionId
|
|
527
|
-
if (isShortcutChat) {
|
|
528
|
-
const desiredSelection = agentSelection
|
|
529
|
-
const currentShortcutSelection = getEnabledCapabilitySelection(session)
|
|
530
|
-
if (
|
|
531
|
-
JSON.stringify(currentShortcutSelection.tools) !== JSON.stringify(desiredSelection.tools)
|
|
532
|
-
|| JSON.stringify(currentShortcutSelection.extensions) !== JSON.stringify(desiredSelection.extensions)
|
|
533
|
-
) {
|
|
534
|
-
session.tools = desiredSelection.tools
|
|
535
|
-
session.extensions = desiredSelection.extensions
|
|
536
|
-
changed = true
|
|
537
|
-
}
|
|
538
|
-
if (session.shortcutForAgentId !== agent.id) { session.shortcutForAgentId = agent.id; changed = true }
|
|
539
|
-
if (session.name !== agent.name) { session.name = agent.name; changed = true }
|
|
540
|
-
const desiredHeartbeatEnabled = agent.heartbeatEnabled ?? false
|
|
541
|
-
if ((session.heartbeatEnabled ?? false) !== desiredHeartbeatEnabled) {
|
|
542
|
-
session.heartbeatEnabled = desiredHeartbeatEnabled
|
|
543
|
-
changed = true
|
|
544
|
-
}
|
|
545
|
-
const desiredHeartbeatIntervalSec = agent.heartbeatIntervalSec ?? null
|
|
546
|
-
if ((session.heartbeatIntervalSec ?? null) !== desiredHeartbeatIntervalSec) {
|
|
547
|
-
session.heartbeatIntervalSec = desiredHeartbeatIntervalSec
|
|
548
|
-
changed = true
|
|
549
|
-
}
|
|
550
|
-
const desiredMemoryTierPreference = agent.memoryTierPreference ?? null
|
|
551
|
-
if ((((session as unknown as Record<string, unknown>).memoryTierPreference as string | null | undefined) ?? null) !== desiredMemoryTierPreference) {
|
|
552
|
-
;(session as unknown as Record<string, unknown>).memoryTierPreference = desiredMemoryTierPreference
|
|
553
|
-
changed = true
|
|
554
|
-
}
|
|
555
|
-
const desiredProjectId = agent.projectId ?? null
|
|
556
|
-
if ((session.projectId ?? null) !== desiredProjectId) {
|
|
557
|
-
session.projectId = desiredProjectId
|
|
558
|
-
changed = true
|
|
559
|
-
}
|
|
560
|
-
const desiredOpenClawAgentId = agent.openclawAgentId ?? null
|
|
561
|
-
if ((session.openclawAgentId ?? null) !== desiredOpenClawAgentId) {
|
|
562
|
-
session.openclawAgentId = desiredOpenClawAgentId
|
|
563
|
-
changed = true
|
|
564
|
-
}
|
|
565
|
-
if (session.connectorContext) {
|
|
566
|
-
session.connectorContext = undefined
|
|
567
|
-
changed = true
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
if (changed) {
|
|
572
|
-
sessions[sessionId] = session
|
|
573
|
-
saveSessions(sessions)
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
/**
|
|
578
|
-
* Build a minimal system prompt for lightweight heartbeat context.
|
|
579
|
-
* Strips conversation history, skills, tool discipline, and workspace context.
|
|
580
|
-
* Keeps identity, datetime, and heartbeat guidance for correct routing.
|
|
581
|
-
*/
|
|
582
|
-
function buildLightHeartbeatSystemPrompt(session: Session): string | undefined {
|
|
583
|
-
if (!session.agentId) return undefined
|
|
584
|
-
const agents = loadAgents()
|
|
585
|
-
const agent = agents[session.agentId]
|
|
586
|
-
if (!agent) return undefined
|
|
587
|
-
|
|
588
|
-
const parts: string[] = []
|
|
589
|
-
parts.push(`## Identity\nName: ${agent.name}`)
|
|
590
|
-
if (agent.description) parts.push(`Description: ${agent.description}`)
|
|
591
|
-
parts.push(buildCurrentDateTimePromptContext())
|
|
592
|
-
if (agent.soul) parts.push(`## Soul\n${agent.soul.slice(0, 300)}`)
|
|
593
|
-
parts.push([
|
|
594
|
-
'## Heartbeats',
|
|
595
|
-
'You run on an autonomous heartbeat. If you receive a heartbeat poll and nothing needs attention, reply exactly: HEARTBEAT_OK',
|
|
596
|
-
].join('\n'))
|
|
597
|
-
return parts.join('\n\n')
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
function buildAgentSystemPrompt(session: Session): string | undefined {
|
|
601
|
-
if (!session.agentId) return undefined
|
|
602
|
-
const agents = loadAgents()
|
|
603
|
-
const agent = agents[session.agentId]
|
|
604
|
-
if (!agent) return undefined
|
|
605
|
-
|
|
606
|
-
const settings = loadSettings()
|
|
607
|
-
const allowSilentReplies = isDirectConnectorSession(session)
|
|
608
|
-
const parts: string[] = []
|
|
609
|
-
const enabledExtensions = listUniversalToolAccessExtensionIds(
|
|
610
|
-
getEnabledCapabilityIds(session).length > 0 ? getEnabledCapabilityIds(session) : getEnabledCapabilityIds(agent),
|
|
611
|
-
)
|
|
612
|
-
|
|
613
|
-
// 1. Identity & Persona
|
|
614
|
-
const identityLines = [`## My Identity`]
|
|
615
|
-
identityLines.push(`Name: ${agent.name}`)
|
|
616
|
-
if (agent.emoji) identityLines.push(`Emoji: ${agent.emoji}`)
|
|
617
|
-
if (agent.creature) identityLines.push(`Creature: ${agent.creature}`)
|
|
618
|
-
if (agent.vibe) identityLines.push(`Vibe: ${agent.vibe}`)
|
|
619
|
-
if (agent.theme) identityLines.push(`Theme: ${agent.theme}`)
|
|
620
|
-
if (agent.description) identityLines.push(`Description: ${agent.description}`)
|
|
621
|
-
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.')
|
|
622
|
-
parts.push(identityLines.join('\n'))
|
|
623
|
-
const continuityBlock = buildIdentityContinuityContext(session, agent)
|
|
624
|
-
if (continuityBlock) parts.push(continuityBlock)
|
|
625
|
-
|
|
626
|
-
// 2. Runtime & Capabilities
|
|
627
|
-
const runtimeLines = [
|
|
628
|
-
'## Runtime',
|
|
629
|
-
`os=${process.platform} | host=${os.hostname()} | agent=${agent.id} | provider=${session.provider} | model=${session.model}`,
|
|
630
|
-
`capabilities=${buildAgentRuntimeCapabilities(enabledExtensions).join(',')}`,
|
|
631
|
-
'tool_access=universal',
|
|
632
|
-
]
|
|
633
|
-
parts.push(runtimeLines.join('\n'))
|
|
634
|
-
|
|
635
|
-
// 3. User & DateTime Context
|
|
636
|
-
if (typeof settings.userPrompt === 'string' && settings.userPrompt.trim()) parts.push(`## User Instructions\n${settings.userPrompt}`)
|
|
637
|
-
parts.push(buildCurrentDateTimePromptContext())
|
|
638
|
-
|
|
639
|
-
// 4. Soul & Core Instructions
|
|
640
|
-
if (agent.soul) parts.push(`## Soul\n${agent.soul}`)
|
|
641
|
-
if (agent.systemPrompt) parts.push(`## System Prompt\n${agent.systemPrompt}`)
|
|
642
|
-
|
|
643
|
-
// 5. Skills (SwarmClaw Core)
|
|
644
|
-
try {
|
|
645
|
-
const runtimeSkills = resolveRuntimeSkills({
|
|
646
|
-
cwd: session.cwd,
|
|
647
|
-
enabledExtensions,
|
|
648
|
-
agentId: agent.id,
|
|
649
|
-
sessionId: session.id,
|
|
650
|
-
userId: session.user,
|
|
651
|
-
agentSkillIds: agent.skillIds || [],
|
|
652
|
-
storedSkills: loadSkills(),
|
|
653
|
-
selectedSkillId: session.skillRuntimeState?.selectedSkillId || null,
|
|
654
|
-
})
|
|
655
|
-
parts.push(...buildRuntimeSkillPromptBlocks(runtimeSkills))
|
|
656
|
-
} catch { /* non-critical */ }
|
|
657
|
-
|
|
658
|
-
// 5b. Workspace context files (HEARTBEAT.md, IDENTITY.md, AGENTS.md, etc.)
|
|
659
|
-
try {
|
|
660
|
-
const wsCtx = buildWorkspaceContext({ cwd: session.cwd })
|
|
661
|
-
if (wsCtx.block) parts.push(wsCtx.block)
|
|
662
|
-
} catch {
|
|
663
|
-
// Workspace context is non-critical
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// 6. Thinking & Output Format
|
|
667
|
-
const thinkingHint = [
|
|
668
|
-
'## Output Format',
|
|
669
|
-
'If your model supports internal reasoning/thinking, put all internal analysis inside <think>...</think> tags.',
|
|
670
|
-
'Your final response to the user should be clear and concise.',
|
|
671
|
-
allowSilentReplies
|
|
672
|
-
? 'When you truly have nothing to say, respond with ONLY: NO_MESSAGE'
|
|
673
|
-
: 'For direct user chats, always send a visible reply. Never answer with NO_MESSAGE or HEARTBEAT_OK unless this is an explicit heartbeat poll.',
|
|
674
|
-
]
|
|
675
|
-
parts.push(thinkingHint.join('\n'))
|
|
676
|
-
|
|
677
|
-
if (enabledExtensions.length === 0) {
|
|
678
|
-
parts.push(buildNoToolsGuidance().join('\n'))
|
|
679
|
-
} else {
|
|
680
|
-
parts.push(buildEnabledToolsAutonomyGuidance().join('\n'))
|
|
681
|
-
}
|
|
682
|
-
const toolSectionLines = buildToolSection(enabledExtensions)
|
|
683
|
-
if (toolSectionLines.length > 0) parts.push(['## Tool Discipline', ...toolSectionLines].join('\n'))
|
|
684
|
-
const operatingGuidance = collectCapabilityOperatingGuidance(enabledExtensions)
|
|
685
|
-
if (operatingGuidance.length > 0) parts.push(['## Tool Guidance', ...operatingGuidance].join('\n'))
|
|
686
|
-
const capabilityLines = collectCapabilityDescriptions(enabledExtensions)
|
|
687
|
-
if (capabilityLines.length > 0) parts.push(['## Tool Capabilities', ...capabilityLines].join('\n'))
|
|
23
|
+
} from '@/lib/server/chat-execution/chat-execution-utils'
|
|
688
24
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
'You run on an autonomous heartbeat. If you receive a heartbeat poll and nothing needs attention, reply exactly: HEARTBEAT_OK',
|
|
693
|
-
].join('\n'))
|
|
25
|
+
export {
|
|
26
|
+
reconcileConnectorDeliveryText,
|
|
27
|
+
} from '@/lib/server/chat-execution/chat-execution-connector-delivery'
|
|
694
28
|
|
|
695
|
-
|
|
696
|
-
|
|
29
|
+
export {
|
|
30
|
+
buildAgentRuntimeCapabilities,
|
|
31
|
+
buildEnabledToolsAutonomyGuidance,
|
|
32
|
+
buildNoToolsGuidance,
|
|
33
|
+
} from '@/lib/server/chat-execution/chat-turn-preparation'
|
|
697
34
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
const cred = creds[session.credentialId]
|
|
709
|
-
if (cred) {
|
|
710
|
-
try { return decryptKey(cred.encryptedKey) } catch { return null }
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
return null
|
|
714
|
-
}
|
|
35
|
+
export {
|
|
36
|
+
collectToolEvent,
|
|
37
|
+
dedupeConsecutiveToolEvents,
|
|
38
|
+
deriveTerminalRunError,
|
|
39
|
+
isLikelyToolErrorOutput,
|
|
40
|
+
} from '@/lib/server/chat-execution/chat-execution-tool-events'
|
|
41
|
+
export {
|
|
42
|
+
pruneSuppressedHeartbeatStreamMessage,
|
|
43
|
+
shouldAppendMissedRequestedToolNotice,
|
|
44
|
+
} from '@/lib/server/chat-execution/chat-turn-finalization'
|
|
715
45
|
|
|
46
|
+
export type { ExecuteChatTurnInput, ExecuteChatTurnResult } from './chat-execution-types'
|
|
716
47
|
|
|
717
48
|
export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promise<ExecuteChatTurnResult> {
|
|
718
|
-
const estop = loadEstopState()
|
|
719
|
-
if (estop.level === 'all') {
|
|
720
|
-
throw new Error(estop.reason
|
|
721
|
-
? `Execution is blocked because all estop is engaged: ${estop.reason}`
|
|
722
|
-
: 'Execution is blocked because all estop is engaged.')
|
|
723
|
-
}
|
|
724
|
-
const { message } = input
|
|
725
49
|
const {
|
|
726
50
|
sessionId,
|
|
727
|
-
imagePath,
|
|
728
|
-
imageUrl,
|
|
729
|
-
attachedFiles,
|
|
730
|
-
missionId: explicitMissionId,
|
|
731
|
-
internal = false,
|
|
732
|
-
runId,
|
|
733
51
|
source = 'chat',
|
|
734
|
-
onEvent,
|
|
735
|
-
signal,
|
|
736
52
|
} = input
|
|
737
|
-
|
|
738
|
-
// Resolve image path early: if the filesystem path is gone, fall back to
|
|
739
|
-
// the upload URL which resolveImagePath maps back to the uploads directory.
|
|
740
|
-
const resolvedImagePath = resolveImagePath(imagePath, imageUrl) ?? undefined
|
|
741
|
-
|
|
742
53
|
const endTurnPerf = perf.start('chat-execution', 'executeSessionChatTurn', { sessionId, source })
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
const appSettings = loadSettings()
|
|
754
|
-
const lifecycleRunId = runId || `${sessionId}:${runStartedAt}`
|
|
755
|
-
const agentForSession = session.agentId ? loadAgents()[session.agentId] : null
|
|
756
|
-
if (isAgentDisabled(agentForSession)) {
|
|
757
|
-
const disabledError = buildAgentDisabledMessage(agentForSession, 'run chats')
|
|
758
|
-
onEvent?.({ t: 'err', text: disabledError })
|
|
759
|
-
|
|
760
|
-
let persisted = false
|
|
761
|
-
if (!internal) {
|
|
762
|
-
const disabledMessage = await applyMessageLifecycleHooks({
|
|
763
|
-
session,
|
|
764
|
-
message: {
|
|
765
|
-
role: 'assistant',
|
|
766
|
-
text: disabledError,
|
|
767
|
-
time: Date.now(),
|
|
768
|
-
},
|
|
769
|
-
enabledIds: getEnabledCapabilityIds(session),
|
|
770
|
-
phase: 'assistant_final',
|
|
771
|
-
runId: lifecycleRunId,
|
|
772
|
-
isSynthetic: true,
|
|
773
|
-
})
|
|
774
|
-
if (disabledMessage) {
|
|
775
|
-
session.messages.push(disabledMessage)
|
|
776
|
-
session.lastActiveAt = Date.now()
|
|
777
|
-
saveSessions(sessions)
|
|
778
|
-
persisted = true
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
return {
|
|
783
|
-
runId,
|
|
784
|
-
sessionId,
|
|
785
|
-
text: disabledError,
|
|
786
|
-
persisted,
|
|
787
|
-
toolEvents: [],
|
|
788
|
-
error: disabledError,
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
const runtimeCapabilityIds = filterRuntimeCapabilityIds(getEnabledCapabilityIds(session), {
|
|
792
|
-
delegationEnabled: agentForSession?.delegationEnabled === true,
|
|
793
|
-
})
|
|
794
|
-
const toolPolicy = resolveSessionToolPolicy(listUniversalToolAccessExtensionIds(runtimeCapabilityIds), appSettings)
|
|
795
|
-
const isHeartbeatRun = isInternalHeartbeatRun(internal, source)
|
|
796
|
-
const isAutonomousInternalRun = internal && source !== 'chat'
|
|
797
|
-
const heartbeatLightContext = isHeartbeatRun && !!input.heartbeatConfig?.lightContext
|
|
798
|
-
const isAutoRunNoHistory = isHeartbeatRun
|
|
799
|
-
const heartbeatStatusOnly = false
|
|
800
|
-
if (shouldApplySessionFreshnessReset(source)) {
|
|
801
|
-
const freshness = evaluateSessionFreshness({
|
|
802
|
-
session,
|
|
803
|
-
policy: resolveSessionResetPolicy({
|
|
804
|
-
session,
|
|
805
|
-
agent: agentForSession,
|
|
806
|
-
settings: appSettings,
|
|
807
|
-
}),
|
|
808
|
-
})
|
|
809
|
-
if (!freshness.fresh) {
|
|
810
|
-
try { syncSessionArchiveMemory(session, { agent: agentForSession }) } catch { /* archive sync is best-effort */ }
|
|
811
|
-
await runCapabilityHook(
|
|
812
|
-
'sessionEnd',
|
|
813
|
-
{
|
|
814
|
-
sessionId: session.id,
|
|
815
|
-
session,
|
|
816
|
-
messageCount: Array.isArray(session.messages) ? session.messages.length : 0,
|
|
817
|
-
durationMs: Date.now() - (session.createdAt || runStartedAt),
|
|
818
|
-
reason: freshness.reason || 'session_reset',
|
|
819
|
-
},
|
|
820
|
-
{
|
|
821
|
-
enabledIds: runtimeCapabilityIds,
|
|
822
|
-
},
|
|
823
|
-
)
|
|
824
|
-
resetSessionRuntime(session, freshness.reason || 'session_reset')
|
|
825
|
-
onEvent?.({ t: 'status', text: JSON.stringify({ sessionReset: freshness.reason || 'session_reset' }) })
|
|
826
|
-
sessions[sessionId] = session
|
|
827
|
-
saveSessions(sessions)
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
if (isAutonomousInternalRun) {
|
|
831
|
-
try { syncSessionArchiveMemory(session, { agent: agentForSession }) } catch { /* archive sync is best-effort */ }
|
|
832
|
-
}
|
|
833
|
-
const mission = await resolveMissionForTurn({
|
|
834
|
-
session,
|
|
835
|
-
message,
|
|
836
|
-
source,
|
|
837
|
-
internal,
|
|
838
|
-
runId: lifecycleRunId,
|
|
839
|
-
explicitMissionId: explicitMissionId || null,
|
|
840
|
-
})
|
|
841
|
-
if (mission?.id) {
|
|
842
|
-
session.missionId = mission.id
|
|
843
|
-
}
|
|
844
|
-
const extensionsForRun = heartbeatStatusOnly ? [] : toolPolicy.enabledExtensions
|
|
845
|
-
if (runMessageStartIndex === 0) {
|
|
846
|
-
await runCapabilityHook(
|
|
847
|
-
'sessionStart',
|
|
848
|
-
{
|
|
849
|
-
session,
|
|
850
|
-
resumedFrom: session.parentSessionId || null,
|
|
851
|
-
},
|
|
852
|
-
{ enabledIds: extensionsForRun },
|
|
853
|
-
)
|
|
854
|
-
}
|
|
855
|
-
const sessionEnabledIds = runtimeCapabilityIds
|
|
856
|
-
const sessionForRunSelection = splitCapabilityIds(extensionsForRun)
|
|
857
|
-
let sessionForRun = JSON.stringify(sessionEnabledIds) === JSON.stringify(extensionsForRun)
|
|
858
|
-
? session
|
|
859
|
-
: { ...session, tools: sessionForRunSelection.tools, extensions: sessionForRunSelection.extensions }
|
|
860
|
-
if (mission?.id) {
|
|
861
|
-
sessionForRun = {
|
|
862
|
-
...sessionForRun,
|
|
863
|
-
missionId: mission.id,
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
if (agentForSession) {
|
|
867
|
-
const preferredRoute = resolvePrimaryAgentRoute(agentForSession, undefined, {
|
|
868
|
-
preferredGatewayTags: session.routePreferredGatewayTags || [],
|
|
869
|
-
preferredGatewayUseCase: session.routePreferredGatewayUseCase || null,
|
|
54
|
+
const preparedTurn = await prepareChatTurn(input)
|
|
55
|
+
if (preparedTurn.kind === 'blocked') {
|
|
56
|
+
const result = await completeBlockedChatTurn(preparedTurn)
|
|
57
|
+
endTurnPerf({
|
|
58
|
+
durationMs: 0,
|
|
59
|
+
toolEventCount: result.toolEvents.length,
|
|
60
|
+
inputTokens: result.inputTokens || 0,
|
|
61
|
+
outputTokens: result.outputTokens || 0,
|
|
62
|
+
error: !!result.error,
|
|
870
63
|
})
|
|
871
|
-
|
|
872
|
-
sessionForRun = applyResolvedRoute({ ...sessionForRun }, preferredRoute)
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
let effectiveMessage = message
|
|
876
|
-
|
|
877
|
-
if (extensionsForRun.length > 0) {
|
|
878
|
-
try {
|
|
879
|
-
effectiveMessage = await transformCapabilityText(
|
|
880
|
-
'transformInboundMessage',
|
|
881
|
-
{ session: sessionForRun, text: message },
|
|
882
|
-
{ enabledIds: extensionsForRun },
|
|
883
|
-
)
|
|
884
|
-
} catch {
|
|
885
|
-
effectiveMessage = message
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
// Apply model override for heartbeat runs (cheaper model)
|
|
890
|
-
if (isHeartbeatRun && input.modelOverride) {
|
|
891
|
-
sessionForRun = { ...sessionForRun, model: input.modelOverride }
|
|
892
|
-
}
|
|
893
|
-
const missionContextBlock = buildMissionContextBlock(mission)
|
|
894
|
-
|
|
895
|
-
if (extensionsForRun.length > 0) {
|
|
896
|
-
const modelResolvePrompt = heartbeatLightContext
|
|
897
|
-
? (joinSystemPromptBlocks(buildLightHeartbeatSystemPrompt(sessionForRun), missionContextBlock) || '')
|
|
898
|
-
: (joinSystemPromptBlocks(buildAgentSystemPrompt(sessionForRun), missionContextBlock) || '')
|
|
899
|
-
const modelResolve = await runCapabilityBeforeModelResolve(
|
|
900
|
-
{
|
|
901
|
-
session: sessionForRun,
|
|
902
|
-
prompt: modelResolvePrompt,
|
|
903
|
-
message: effectiveMessage,
|
|
904
|
-
provider: sessionForRun.provider,
|
|
905
|
-
model: sessionForRun.model,
|
|
906
|
-
apiEndpoint: sessionForRun.apiEndpoint || null,
|
|
907
|
-
},
|
|
908
|
-
{ enabledIds: extensionsForRun },
|
|
909
|
-
)
|
|
910
|
-
if (modelResolve) {
|
|
911
|
-
sessionForRun = {
|
|
912
|
-
...sessionForRun,
|
|
913
|
-
provider: modelResolve.providerOverride ?? sessionForRun.provider,
|
|
914
|
-
model: modelResolve.modelOverride ?? sessionForRun.model,
|
|
915
|
-
...(modelResolve.apiEndpointOverride !== undefined ? { apiEndpoint: modelResolve.apiEndpointOverride } : {}),
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
if (!heartbeatStatusOnly && toolPolicy.blockedExtensions.length > 0) {
|
|
921
|
-
const blockedSummary = toolPolicy.blockedExtensions
|
|
922
|
-
.map((entry) => `${entry.tool} (${entry.reason})`)
|
|
923
|
-
.join(', ')
|
|
924
|
-
onEvent?.({ t: 'err', text: `Capability policy blocked extensions for this run: ${blockedSummary}` })
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
// --- Agent spend-limit enforcement (hourly/daily/monthly) ---
|
|
928
|
-
if (session.agentId) {
|
|
929
|
-
const agentsMap = loadAgents()
|
|
930
|
-
const agent = agentsMap[session.agentId]
|
|
931
|
-
if (agent) {
|
|
932
|
-
const budgetCheck = checkAgentBudgetLimits(agent)
|
|
933
|
-
const action = agent.budgetAction || 'warn'
|
|
934
|
-
|
|
935
|
-
if (budgetCheck.exceeded.length > 0) {
|
|
936
|
-
const budgetError = budgetCheck.exceeded.map((entry) => entry.message).join(' ')
|
|
937
|
-
if (action === 'block') {
|
|
938
|
-
onEvent?.({ t: 'err', text: budgetError })
|
|
939
|
-
|
|
940
|
-
let persisted = false
|
|
941
|
-
if (!internal) {
|
|
942
|
-
const budgetMessage = await applyMessageLifecycleHooks({
|
|
943
|
-
session,
|
|
944
|
-
message: {
|
|
945
|
-
role: 'assistant',
|
|
946
|
-
text: budgetError,
|
|
947
|
-
time: Date.now(),
|
|
948
|
-
},
|
|
949
|
-
enabledIds: getEnabledCapabilityIds(session),
|
|
950
|
-
phase: 'assistant_final',
|
|
951
|
-
runId: lifecycleRunId,
|
|
952
|
-
isSynthetic: true,
|
|
953
|
-
})
|
|
954
|
-
if (budgetMessage) {
|
|
955
|
-
session.messages.push(budgetMessage)
|
|
956
|
-
session.lastActiveAt = Date.now()
|
|
957
|
-
saveSessions(sessions)
|
|
958
|
-
persisted = true
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
return {
|
|
963
|
-
runId,
|
|
964
|
-
sessionId,
|
|
965
|
-
text: budgetError,
|
|
966
|
-
persisted,
|
|
967
|
-
toolEvents: [],
|
|
968
|
-
error: budgetError,
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
// budgetAction === 'warn': emit a warning but continue
|
|
972
|
-
onEvent?.({ t: 'status', text: JSON.stringify({ budgetWarning: budgetError }) })
|
|
973
|
-
} else if (budgetCheck.warnings.length > 0) {
|
|
974
|
-
const warningText = budgetCheck.warnings.map((entry) => entry.message).join(' ')
|
|
975
|
-
onEvent?.({ t: 'status', text: JSON.stringify({ budgetWarning: warningText }) })
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
const dailySpendLimitUsd = parseUsdLimit(appSettings.safetyMaxDailySpendUsd)
|
|
981
|
-
if (dailySpendLimitUsd !== null) {
|
|
982
|
-
const todaySpendUsd = getTodaySpendUsd()
|
|
983
|
-
if (todaySpendUsd >= dailySpendLimitUsd) {
|
|
984
|
-
const spendError = `Safety budget reached: today's spend is $${todaySpendUsd.toFixed(4)} (limit $${dailySpendLimitUsd.toFixed(4)}). Increase safetyMaxDailySpendUsd to continue autonomous runs.`
|
|
985
|
-
onEvent?.({ t: 'err', text: spendError })
|
|
986
|
-
|
|
987
|
-
let persisted = false
|
|
988
|
-
if (!internal) {
|
|
989
|
-
const spendMessage = await applyMessageLifecycleHooks({
|
|
990
|
-
session,
|
|
991
|
-
message: {
|
|
992
|
-
role: 'assistant',
|
|
993
|
-
text: spendError,
|
|
994
|
-
time: Date.now(),
|
|
995
|
-
},
|
|
996
|
-
enabledIds: getEnabledCapabilityIds(session),
|
|
997
|
-
phase: 'assistant_final',
|
|
998
|
-
runId: lifecycleRunId,
|
|
999
|
-
isSynthetic: true,
|
|
1000
|
-
})
|
|
1001
|
-
if (spendMessage) {
|
|
1002
|
-
session.messages.push(spendMessage)
|
|
1003
|
-
session.lastActiveAt = Date.now()
|
|
1004
|
-
saveSessions(sessions)
|
|
1005
|
-
persisted = true
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
return {
|
|
1010
|
-
runId,
|
|
1011
|
-
sessionId,
|
|
1012
|
-
text: spendError,
|
|
1013
|
-
persisted,
|
|
1014
|
-
toolEvents: [],
|
|
1015
|
-
error: spendError,
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
// Log the trigger
|
|
1021
|
-
logExecution(sessionId, 'trigger', `${source} message received`, {
|
|
1022
|
-
runId,
|
|
1023
|
-
agentId: session.agentId,
|
|
1024
|
-
detail: {
|
|
1025
|
-
source,
|
|
1026
|
-
internal,
|
|
1027
|
-
provider: sessionForRun.provider,
|
|
1028
|
-
model: sessionForRun.model,
|
|
1029
|
-
messagePreview: effectiveMessage.slice(0, 200),
|
|
1030
|
-
hasImage: !!(imagePath || imageUrl),
|
|
1031
|
-
},
|
|
1032
|
-
})
|
|
1033
|
-
|
|
1034
|
-
const providerType = sessionForRun.provider || 'claude-cli'
|
|
1035
|
-
const provider = getProvider(providerType)
|
|
1036
|
-
if (!provider) throw new Error(`Unknown provider: ${providerType}`)
|
|
1037
|
-
|
|
1038
|
-
if (providerType === 'claude-cli' && !fs.existsSync(session.cwd)) {
|
|
1039
|
-
throw new Error(`Directory not found: ${session.cwd}`)
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
const apiKey = resolveApiKeyForSession(sessionForRun, provider)
|
|
1043
|
-
const hideAssistantTranscript = internal && source === 'main-loop-followup'
|
|
1044
|
-
|
|
1045
|
-
const shouldPersistUserMessage = shouldPersistInboundUserMessage(internal, source)
|
|
1046
|
-
if (shouldPersistUserMessage) {
|
|
1047
|
-
const linkAnalysis = !internal ? await runLinkUnderstanding(message) : []
|
|
1048
|
-
const guardedUserText = guardUntrustedText({
|
|
1049
|
-
text: message,
|
|
1050
|
-
source,
|
|
1051
|
-
mode: getUntrustedContentGuardMode(appSettings),
|
|
1052
|
-
trusted: (source === 'chat' && !internal) || internal,
|
|
1053
|
-
}).text
|
|
1054
|
-
const nextUserMessage = await applyMessageLifecycleHooks({
|
|
1055
|
-
session,
|
|
1056
|
-
message: {
|
|
1057
|
-
role: 'user',
|
|
1058
|
-
text: guardedUserText,
|
|
1059
|
-
time: Date.now(),
|
|
1060
|
-
imagePath: imagePath || undefined,
|
|
1061
|
-
imageUrl: imageUrl || undefined,
|
|
1062
|
-
attachedFiles: attachedFiles?.length ? attachedFiles : undefined,
|
|
1063
|
-
replyToId: input.replyToId || undefined,
|
|
1064
|
-
},
|
|
1065
|
-
enabledIds: extensionsForRun,
|
|
1066
|
-
phase: 'user',
|
|
1067
|
-
runId: lifecycleRunId,
|
|
1068
|
-
})
|
|
1069
|
-
if (nextUserMessage) {
|
|
1070
|
-
session.messages.push(nextUserMessage)
|
|
1071
|
-
if (linkAnalysis.length > 0) {
|
|
1072
|
-
const linkAnalysisMessage = await applyMessageLifecycleHooks({
|
|
1073
|
-
session,
|
|
1074
|
-
message: {
|
|
1075
|
-
role: 'assistant',
|
|
1076
|
-
kind: 'system',
|
|
1077
|
-
text: `[Automated Link Analysis]\n${linkAnalysis.join('\n\n')}`,
|
|
1078
|
-
time: Date.now(),
|
|
1079
|
-
},
|
|
1080
|
-
enabledIds: extensionsForRun,
|
|
1081
|
-
phase: 'system',
|
|
1082
|
-
runId: lifecycleRunId,
|
|
1083
|
-
isSynthetic: true,
|
|
1084
|
-
})
|
|
1085
|
-
if (linkAnalysisMessage) {
|
|
1086
|
-
session.messages.push(linkAnalysisMessage)
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
session.lastActiveAt = Date.now()
|
|
1090
|
-
saveSessions(sessions)
|
|
1091
|
-
if (!internal && source === 'chat') {
|
|
1092
|
-
try {
|
|
1093
|
-
bridgeHumanReplyFromChat({
|
|
1094
|
-
sessionId,
|
|
1095
|
-
payload: nextUserMessage.text,
|
|
1096
|
-
})
|
|
1097
|
-
} catch {
|
|
1098
|
-
// Best-effort bridge only — normal chat persistence must not fail on mailbox cleanup.
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
if (!internal) {
|
|
1102
|
-
try {
|
|
1103
|
-
await runCapabilityHook('onMessage', { session, message: nextUserMessage }, { enabledIds: extensionsForRun })
|
|
1104
|
-
} catch { /* onMessage hooks are non-critical */ }
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
// Determine extension/LangGraph path early so we can skip the redundant system prompt.
|
|
1110
|
-
// Dependencies: providerType (line 750), sessionForRun (line 625), isLocalOpenClawEndpoint (import).
|
|
1111
|
-
const useLocalOpenClawNativeRuntime = providerType === 'openclaw' && isLocalOpenClawEndpoint(sessionForRun.apiEndpoint)
|
|
1112
|
-
const enabledSessionExtensions = getEnabledCapabilityIds(sessionForRun)
|
|
1113
|
-
const hasExtensions = enabledSessionExtensions.length > 0
|
|
1114
|
-
&& !NON_LANGGRAPH_PROVIDER_IDS.has(providerType)
|
|
1115
|
-
&& !useLocalOpenClawNativeRuntime
|
|
1116
|
-
|
|
1117
|
-
// When using LangGraph (hasExtensions), streamAgentChatCore builds the full prompt
|
|
1118
|
-
// including identity, soul, skills, tool discipline, and execution policy.
|
|
1119
|
-
// Only build the standalone system prompt for the direct-provider (no LangGraph) path
|
|
1120
|
-
// to avoid duplicating tool discipline, operating guidance, and capability sections.
|
|
1121
|
-
// lightContext mode uses a minimal prompt for both paths to reduce token cost.
|
|
1122
|
-
const systemPrompt = heartbeatLightContext
|
|
1123
|
-
? joinSystemPromptBlocks(buildLightHeartbeatSystemPrompt(sessionForRun), missionContextBlock)
|
|
1124
|
-
: (hasExtensions ? undefined : joinSystemPromptBlocks(buildAgentSystemPrompt(sessionForRun), missionContextBlock))
|
|
1125
|
-
const toolEvents: MessageToolEvent[] = []
|
|
1126
|
-
const streamErrors: string[] = []
|
|
1127
|
-
const accumulatedUsage = { inputTokens: 0, outputTokens: 0, estimatedCost: 0 }
|
|
1128
|
-
|
|
1129
|
-
let thinkingText = ''
|
|
1130
|
-
let streamingPartialText = ''
|
|
1131
|
-
let lastPartialSaveAt = 0
|
|
1132
|
-
let lastPartialSnapshotKey = ''
|
|
1133
|
-
let partialSaveTimeout: ReturnType<typeof setTimeout> | null = null
|
|
1134
|
-
let partialPersistenceClosed = false
|
|
1135
|
-
let partialPersistChain: Promise<void> = Promise.resolve()
|
|
1136
|
-
|
|
1137
|
-
const stopPartialAssistantPersistence = () => {
|
|
1138
|
-
partialPersistenceClosed = true
|
|
1139
|
-
if (partialSaveTimeout) {
|
|
1140
|
-
clearTimeout(partialSaveTimeout)
|
|
1141
|
-
partialSaveTimeout = null
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
const persistStreamingAssistantArtifact = async () => {
|
|
1146
|
-
if (hideAssistantTranscript) return
|
|
1147
|
-
partialSaveTimeout = null
|
|
1148
|
-
if (partialPersistenceClosed) return
|
|
1149
|
-
const persistedToolEvents = toolEvents.length
|
|
1150
|
-
? dedupeConsecutiveToolEvents(pruneIncompleteToolEvents([...toolEvents]))
|
|
1151
|
-
: []
|
|
1152
|
-
if (!hasPersistableAssistantPayload(streamingPartialText, thinkingText, persistedToolEvents)) return
|
|
1153
|
-
|
|
1154
|
-
try {
|
|
1155
|
-
const fresh = loadSessions()
|
|
1156
|
-
const current = fresh[sessionId]
|
|
1157
|
-
if (!current) return
|
|
1158
|
-
current.messages = Array.isArray(current.messages) ? current.messages : []
|
|
1159
|
-
const partialMsg = await applyMessageLifecycleHooks({
|
|
1160
|
-
session: current,
|
|
1161
|
-
message: {
|
|
1162
|
-
role: 'assistant',
|
|
1163
|
-
text: streamingPartialText,
|
|
1164
|
-
time: Date.now(),
|
|
1165
|
-
streaming: true,
|
|
1166
|
-
runId: lifecycleRunId,
|
|
1167
|
-
thinking: thinkingText || undefined,
|
|
1168
|
-
toolEvents: persistedToolEvents.length ? persistedToolEvents : undefined,
|
|
1169
|
-
},
|
|
1170
|
-
enabledIds: extensionsForRun,
|
|
1171
|
-
phase: 'assistant_partial',
|
|
1172
|
-
runId: lifecycleRunId,
|
|
1173
|
-
isSynthetic: true,
|
|
1174
|
-
})
|
|
1175
|
-
if (!partialMsg) return
|
|
1176
|
-
const snapshotKey = JSON.stringify([
|
|
1177
|
-
partialMsg.text,
|
|
1178
|
-
partialMsg.thinking || '',
|
|
1179
|
-
getToolEventsSnapshotKey(partialMsg.toolEvents || []),
|
|
1180
|
-
])
|
|
1181
|
-
if (snapshotKey === lastPartialSnapshotKey) return
|
|
1182
|
-
lastPartialSnapshotKey = snapshotKey
|
|
1183
|
-
lastPartialSaveAt = Date.now()
|
|
1184
|
-
upsertStreamingAssistantArtifact(current.messages, partialMsg, {
|
|
1185
|
-
minIndex: runMessageStartIndex,
|
|
1186
|
-
minTime: runStartedAt,
|
|
1187
|
-
})
|
|
1188
|
-
fresh[sessionId] = current
|
|
1189
|
-
saveSessions(fresh)
|
|
1190
|
-
notify(`messages:${sessionId}`)
|
|
1191
|
-
} catch { /* partial save is best-effort */ }
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
const triggerPartialAssistantPersist = () => {
|
|
1195
|
-
partialPersistChain = partialPersistChain
|
|
1196
|
-
.catch(() => {})
|
|
1197
|
-
.then(async () => {
|
|
1198
|
-
await persistStreamingAssistantArtifact()
|
|
1199
|
-
})
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
const queuePartialAssistantPersist = (immediate = false) => {
|
|
1203
|
-
if (partialPersistenceClosed) return
|
|
1204
|
-
const now = Date.now()
|
|
1205
|
-
const minIntervalMs = 400
|
|
1206
|
-
if (immediate || now - lastPartialSaveAt >= minIntervalMs) {
|
|
1207
|
-
if (partialSaveTimeout) {
|
|
1208
|
-
clearTimeout(partialSaveTimeout)
|
|
1209
|
-
partialSaveTimeout = null
|
|
1210
|
-
}
|
|
1211
|
-
triggerPartialAssistantPersist()
|
|
1212
|
-
return
|
|
1213
|
-
}
|
|
1214
|
-
if (partialSaveTimeout) return
|
|
1215
|
-
partialSaveTimeout = setTimeout(() => {
|
|
1216
|
-
triggerPartialAssistantPersist()
|
|
1217
|
-
}, minIntervalMs - (now - lastPartialSaveAt))
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
const emit = (ev: SSEEvent) => {
|
|
1221
|
-
let shouldPersistPartial = false
|
|
1222
|
-
let immediatePartialPersist = false
|
|
1223
|
-
if (ev.t === 'reset') {
|
|
1224
|
-
// stream-agent-chat rolls back state after a transient error — reset
|
|
1225
|
-
// accumulated text/thinking/tools so the partial persist stays in sync.
|
|
1226
|
-
streamingPartialText = ev.text || ''
|
|
1227
|
-
thinkingText = ''
|
|
1228
|
-
toolEvents.length = 0
|
|
1229
|
-
shouldPersistPartial = true
|
|
1230
|
-
immediatePartialPersist = true
|
|
1231
|
-
}
|
|
1232
|
-
if (ev.t === 'd' && typeof ev.text === 'string') {
|
|
1233
|
-
streamingPartialText += ev.text
|
|
1234
|
-
shouldPersistPartial = true
|
|
1235
|
-
immediatePartialPersist = streamingPartialText.length === ev.text.length
|
|
1236
|
-
}
|
|
1237
|
-
if (ev.t === 'err' && typeof ev.text === 'string') {
|
|
1238
|
-
const trimmed = ev.text.trim()
|
|
1239
|
-
if (trimmed) {
|
|
1240
|
-
streamErrors.push(trimmed)
|
|
1241
|
-
if (streamErrors.length > 8) streamErrors.shift()
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
if (ev.t === 'thinking' && ev.text) {
|
|
1245
|
-
thinkingText += ev.text
|
|
1246
|
-
shouldPersistPartial = true
|
|
1247
|
-
}
|
|
1248
|
-
if (ev.t === 'md' && ev.text) {
|
|
1249
|
-
try {
|
|
1250
|
-
const mdPayload = JSON.parse(ev.text) as Record<string, unknown>
|
|
1251
|
-
const usage = mdPayload.usage as { inputTokens?: number; outputTokens?: number; estimatedCost?: number } | undefined
|
|
1252
|
-
if (usage) {
|
|
1253
|
-
if (typeof usage.inputTokens === 'number') accumulatedUsage.inputTokens += usage.inputTokens
|
|
1254
|
-
if (typeof usage.outputTokens === 'number') accumulatedUsage.outputTokens += usage.outputTokens
|
|
1255
|
-
if (typeof usage.estimatedCost === 'number') accumulatedUsage.estimatedCost += usage.estimatedCost
|
|
1256
|
-
}
|
|
1257
|
-
} catch { /* ignore non-JSON md events */ }
|
|
1258
|
-
}
|
|
1259
|
-
collectToolEvent(ev, toolEvents)
|
|
1260
|
-
if (ev.t === 'tool_call' || ev.t === 'tool_result') {
|
|
1261
|
-
shouldPersistPartial = true
|
|
1262
|
-
immediatePartialPersist = true
|
|
1263
|
-
}
|
|
1264
|
-
if (shouldPersistPartial) queuePartialAssistantPersist(immediatePartialPersist)
|
|
1265
|
-
onEvent?.(ev)
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
// Periodic partial save so a browser refresh doesn't lose the in-flight response.
|
|
1269
|
-
const PARTIAL_SAVE_INTERVAL_MS = 3500
|
|
1270
|
-
const partialSaveTimer = setInterval(() => {
|
|
1271
|
-
persistStreamingAssistantArtifact()
|
|
1272
|
-
}, PARTIAL_SAVE_INTERVAL_MS)
|
|
1273
|
-
|
|
1274
|
-
const parseAndEmit = (raw: string) => {
|
|
1275
|
-
const lines = raw.split('\n').filter(Boolean)
|
|
1276
|
-
for (const line of lines) {
|
|
1277
|
-
const ev = extractEventJson(line)
|
|
1278
|
-
if (ev) emit(ev)
|
|
1279
|
-
}
|
|
64
|
+
return result
|
|
1280
65
|
}
|
|
1281
66
|
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
const requestedToolPreflightResponse = resolveRequestedToolPreflightResponse({
|
|
1287
|
-
message,
|
|
1288
|
-
enabledExtensions: extensionsForRun,
|
|
1289
|
-
toolPolicy,
|
|
1290
|
-
appSettings,
|
|
1291
|
-
internal,
|
|
1292
|
-
source,
|
|
1293
|
-
session: sessionForRun,
|
|
67
|
+
const partialPersistence = createPartialAssistantPersistence({
|
|
68
|
+
prepared: preparedTurn,
|
|
69
|
+
onEvent: input.onEvent,
|
|
1294
70
|
})
|
|
1295
|
-
if (requestedToolPreflightResponse) {
|
|
1296
|
-
clearInterval(partialSaveTimer)
|
|
1297
|
-
stopPartialAssistantPersistence()
|
|
1298
|
-
emit({ t: 'd', text: requestedToolPreflightResponse })
|
|
1299
|
-
|
|
1300
|
-
let persisted = false
|
|
1301
|
-
if (!hideAssistantTranscript) {
|
|
1302
|
-
const nextAssistantMessage = await applyMessageLifecycleHooks({
|
|
1303
|
-
session,
|
|
1304
|
-
message: {
|
|
1305
|
-
role: 'assistant',
|
|
1306
|
-
text: requestedToolPreflightResponse,
|
|
1307
|
-
time: Date.now(),
|
|
1308
|
-
},
|
|
1309
|
-
enabledIds: extensionsForRun,
|
|
1310
|
-
phase: 'assistant_final',
|
|
1311
|
-
runId: lifecycleRunId,
|
|
1312
|
-
isSynthetic: true,
|
|
1313
|
-
})
|
|
1314
|
-
if (nextAssistantMessage) {
|
|
1315
|
-
session.messages.push(nextAssistantMessage)
|
|
1316
|
-
session.lastActiveAt = Date.now()
|
|
1317
|
-
saveSessions(sessions)
|
|
1318
|
-
notify(`messages:${sessionId}`)
|
|
1319
|
-
notify('sessions')
|
|
1320
|
-
persisted = true
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
return {
|
|
1325
|
-
runId,
|
|
1326
|
-
sessionId,
|
|
1327
|
-
text: requestedToolPreflightResponse,
|
|
1328
|
-
persisted,
|
|
1329
|
-
toolEvents: [],
|
|
1330
|
-
error: undefined,
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
71
|
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
let responseCacheHit = false
|
|
1339
|
-
let responseCacheInput: LlmResponseCacheKeyInput | null = null
|
|
1340
|
-
let durationMs = 0
|
|
1341
|
-
const startTs = Date.now()
|
|
1342
|
-
const endLlmPerf = perf.start('chat-execution', 'llm-round-trip', {
|
|
1343
|
-
sessionId,
|
|
1344
|
-
provider: providerType,
|
|
1345
|
-
hasExtensions,
|
|
1346
|
-
extensionCount: enabledSessionExtensions.length,
|
|
1347
|
-
})
|
|
1348
|
-
preflightToolRoutingResult = await runExclusiveDirectMemoryPreflight({
|
|
1349
|
-
session: sessionForRun,
|
|
1350
|
-
sessionId,
|
|
1351
|
-
message,
|
|
1352
|
-
effectiveMessage,
|
|
1353
|
-
enabledExtensions: extensionsForRun,
|
|
1354
|
-
toolPolicy,
|
|
1355
|
-
appSettings,
|
|
1356
|
-
internal,
|
|
1357
|
-
source,
|
|
1358
|
-
toolEvents,
|
|
1359
|
-
emit,
|
|
72
|
+
const preflight = await runChatTurnPreflight({
|
|
73
|
+
prepared: preparedTurn,
|
|
74
|
+
emit: partialPersistence.emit,
|
|
75
|
+
toolEvents: partialPersistence.getToolEvents(),
|
|
1360
76
|
})
|
|
1361
77
|
|
|
1362
|
-
if (
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
if (signal) {
|
|
1373
|
-
if (signal.aborted) abortController.abort()
|
|
1374
|
-
else signal.addEventListener('abort', abortFromOutside)
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
active.set(sessionId, {
|
|
1378
|
-
runId: runId || null,
|
|
1379
|
-
source,
|
|
1380
|
-
kill: () => abortController.abort(),
|
|
78
|
+
if (preflight?.terminalResult) {
|
|
79
|
+
if (preflight.terminalResult.text) input.onEvent?.({ t: 'd', text: preflight.terminalResult.text })
|
|
80
|
+
partialPersistence.stop()
|
|
81
|
+
await partialPersistence.awaitIdle()
|
|
82
|
+
endTurnPerf({
|
|
83
|
+
durationMs: 0,
|
|
84
|
+
toolEventCount: preflight.terminalResult.toolEvents.length,
|
|
85
|
+
inputTokens: preflight.terminalResult.inputTokens || 0,
|
|
86
|
+
outputTokens: preflight.terminalResult.outputTokens || 0,
|
|
87
|
+
error: !!preflight.terminalResult.error,
|
|
1381
88
|
})
|
|
1382
|
-
|
|
1383
|
-
try {
|
|
1384
|
-
// Heartbeat runs get a small tail of recent messages so the agent can see
|
|
1385
|
-
// prior findings and avoid repeating the same searches. Full history is
|
|
1386
|
-
// skipped to avoid blowing the context window on long-lived sessions.
|
|
1387
|
-
// lightContext mode skips history entirely for maximum token savings.
|
|
1388
|
-
const heartbeatHistory = isAutoRunNoHistory
|
|
1389
|
-
? (heartbeatLightContext ? [] : getSessionMessages(sessionId).slice(-6))
|
|
1390
|
-
: undefined
|
|
1391
|
-
|
|
1392
|
-
log.info(TAG, `provider=${providerType}, hasExtensions=${hasExtensions}, localOpenClawNative=${useLocalOpenClawNativeRuntime}, imagePath=${resolvedImagePath || 'none'}, attachedFiles=${attachedFiles?.length || 0}, extensions=${enabledSessionExtensions.length}`)
|
|
1393
|
-
if (hasExtensions) {
|
|
1394
|
-
const result = await streamAgentChat({
|
|
1395
|
-
session: sessionForRun,
|
|
1396
|
-
message: effectiveMessage,
|
|
1397
|
-
imagePath: resolvedImagePath,
|
|
1398
|
-
imageUrl,
|
|
1399
|
-
attachedFiles,
|
|
1400
|
-
apiKey,
|
|
1401
|
-
systemPrompt,
|
|
1402
|
-
extraSystemContext: missionContextBlock ? [missionContextBlock] : undefined,
|
|
1403
|
-
write: (raw) => parseAndEmit(raw),
|
|
1404
|
-
history: heartbeatHistory ?? applyContextClearBoundary(getSessionMessages(sessionId)),
|
|
1405
|
-
signal: abortController.signal,
|
|
1406
|
-
source,
|
|
1407
|
-
})
|
|
1408
|
-
fullResponse = result.finalResponse || result.fullText
|
|
1409
|
-
} else {
|
|
1410
|
-
let directHistorySnapshot = isAutoRunNoHistory
|
|
1411
|
-
? (heartbeatLightContext ? [] : getSessionMessages(sessionId).slice(-6))
|
|
1412
|
-
: applyContextClearBoundary(getSessionMessages(sessionId))
|
|
1413
|
-
responseCacheInput = {
|
|
1414
|
-
provider: providerType,
|
|
1415
|
-
model: sessionForRun.model,
|
|
1416
|
-
apiEndpoint: sessionForRun.apiEndpoint || '',
|
|
1417
|
-
systemPrompt,
|
|
1418
|
-
message: effectiveMessage,
|
|
1419
|
-
imagePath,
|
|
1420
|
-
imageUrl,
|
|
1421
|
-
attachedFiles,
|
|
1422
|
-
history: directHistorySnapshot,
|
|
1423
|
-
}
|
|
1424
|
-
const canUseResponseCache = !internal && responseCacheConfig.enabled
|
|
1425
|
-
const cached = canUseResponseCache
|
|
1426
|
-
? getCachedLlmResponse(responseCacheInput, responseCacheConfig)
|
|
1427
|
-
: null
|
|
1428
|
-
if (cached) {
|
|
1429
|
-
responseCacheHit = true
|
|
1430
|
-
fullResponse = cached.text
|
|
1431
|
-
emit({
|
|
1432
|
-
t: 'md',
|
|
1433
|
-
text: JSON.stringify({
|
|
1434
|
-
cache: {
|
|
1435
|
-
hit: true,
|
|
1436
|
-
ageMs: cached.ageMs,
|
|
1437
|
-
provider: cached.provider,
|
|
1438
|
-
model: cached.model,
|
|
1439
|
-
},
|
|
1440
|
-
}),
|
|
1441
|
-
})
|
|
1442
|
-
emit({ t: 'd', text: cached.text })
|
|
1443
|
-
} else {
|
|
1444
|
-
await runCapabilityHook(
|
|
1445
|
-
'llmInput',
|
|
1446
|
-
{
|
|
1447
|
-
session: sessionForRun,
|
|
1448
|
-
runId: lifecycleRunId,
|
|
1449
|
-
provider: providerType,
|
|
1450
|
-
model: sessionForRun.model,
|
|
1451
|
-
systemPrompt,
|
|
1452
|
-
prompt: effectiveMessage,
|
|
1453
|
-
historyMessages: directHistorySnapshot,
|
|
1454
|
-
imagesCount: resolvedImagePath ? 1 : 0,
|
|
1455
|
-
},
|
|
1456
|
-
{ enabledIds: extensionsForRun },
|
|
1457
|
-
)
|
|
1458
|
-
const doStreamChat = () => provider.handler.streamChat({
|
|
1459
|
-
session: sessionForRun,
|
|
1460
|
-
message: effectiveMessage,
|
|
1461
|
-
imagePath: resolvedImagePath,
|
|
1462
|
-
apiKey,
|
|
1463
|
-
systemPrompt,
|
|
1464
|
-
write: (raw: string) => parseAndEmit(raw),
|
|
1465
|
-
active,
|
|
1466
|
-
loadHistory: (sid: string) => {
|
|
1467
|
-
if (sid === sessionId) return directHistorySnapshot
|
|
1468
|
-
return isAutoRunNoHistory
|
|
1469
|
-
? getSessionMessages(sid).slice(-6)
|
|
1470
|
-
: applyContextClearBoundary(getSessionMessages(sid))
|
|
1471
|
-
},
|
|
1472
|
-
onUsage: (u) => { directUsage.inputTokens = u.inputTokens; directUsage.outputTokens = u.outputTokens; directUsage.received = true },
|
|
1473
|
-
signal: abortController.signal,
|
|
1474
|
-
})
|
|
1475
|
-
try {
|
|
1476
|
-
fullResponse = await doStreamChat()
|
|
1477
|
-
} catch (streamErr: unknown) {
|
|
1478
|
-
// On context overflow, reduce history and retry once
|
|
1479
|
-
const streamErrMsg = toErrorMessage(streamErr)
|
|
1480
|
-
const streamStatus = (streamErr as Record<string, unknown>)?.status
|
|
1481
|
-
if (typeof streamStatus === 'number' && streamStatus === 400 && CONTEXT_OVERFLOW_RE.test(streamErrMsg)) {
|
|
1482
|
-
log.warn('chat-run', `Context overflow in direct path, reducing history and retrying`, {
|
|
1483
|
-
sessionId, error: streamErrMsg, historyLen: directHistorySnapshot.length,
|
|
1484
|
-
})
|
|
1485
|
-
directHistorySnapshot = directHistorySnapshot.slice(-10)
|
|
1486
|
-
fullResponse = await doStreamChat()
|
|
1487
|
-
} else {
|
|
1488
|
-
throw streamErr
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
await runCapabilityHook(
|
|
1492
|
-
'llmOutput',
|
|
1493
|
-
{
|
|
1494
|
-
session: sessionForRun,
|
|
1495
|
-
runId: lifecycleRunId,
|
|
1496
|
-
provider: providerType,
|
|
1497
|
-
model: sessionForRun.model,
|
|
1498
|
-
assistantTexts: fullResponse ? [fullResponse] : [],
|
|
1499
|
-
response: fullResponse,
|
|
1500
|
-
usage: directUsage.received
|
|
1501
|
-
? {
|
|
1502
|
-
input: directUsage.inputTokens,
|
|
1503
|
-
output: directUsage.outputTokens,
|
|
1504
|
-
total: directUsage.inputTokens + directUsage.outputTokens,
|
|
1505
|
-
estimatedCost: estimateCost(sessionForRun.model, directUsage.inputTokens, directUsage.outputTokens),
|
|
1506
|
-
}
|
|
1507
|
-
: undefined,
|
|
1508
|
-
},
|
|
1509
|
-
{ enabledIds: extensionsForRun },
|
|
1510
|
-
)
|
|
1511
|
-
if (canUseResponseCache && responseCacheInput && fullResponse) {
|
|
1512
|
-
setCachedLlmResponse(responseCacheInput, fullResponse, responseCacheConfig)
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
durationMs = Date.now() - startTs
|
|
1517
|
-
endLlmPerf({ durationMs, cacheHit: responseCacheHit })
|
|
1518
|
-
} catch (err: unknown) {
|
|
1519
|
-
endLlmPerf({ error: true })
|
|
1520
|
-
errorMessage = toErrorMessage(err)
|
|
1521
|
-
const failureText = errorMessage || 'Run failed.'
|
|
1522
|
-
markProviderFailure(providerType, failureText, sessionForRun.credentialId)
|
|
1523
|
-
emit({ t: 'err', text: failureText })
|
|
1524
|
-
log.error('chat-run', `Run failed for session ${sessionId}`, {
|
|
1525
|
-
runId,
|
|
1526
|
-
source,
|
|
1527
|
-
internal,
|
|
1528
|
-
error: failureText,
|
|
1529
|
-
stack: err instanceof Error ? err.stack?.split('\n').slice(0, 6).join('\n') : undefined,
|
|
1530
|
-
})
|
|
1531
|
-
} finally {
|
|
1532
|
-
clearInterval(partialSaveTimer)
|
|
1533
|
-
stopPartialAssistantPersistence()
|
|
1534
|
-
active.delete(sessionId)
|
|
1535
|
-
notify('sessions')
|
|
1536
|
-
if (signal) signal.removeEventListener('abort', abortFromOutside)
|
|
1537
|
-
}
|
|
89
|
+
return preflight.terminalResult
|
|
1538
90
|
}
|
|
1539
|
-
await partialPersistChain.catch(() => {})
|
|
1540
|
-
|
|
1541
|
-
if (!errorMessage) {
|
|
1542
|
-
markProviderSuccess(providerType, sessionForRun.credentialId)
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
// Record usage for the direct (non-tools) streamChat path.
|
|
1546
|
-
// streamAgentChat already calls appendUsage internally for the tools path.
|
|
1547
|
-
if (!hasExtensions && fullResponse && !errorMessage && !responseCacheHit) {
|
|
1548
|
-
const inputTokens = directUsage.received ? directUsage.inputTokens : Math.ceil(message.length / 4)
|
|
1549
|
-
const outputTokens = directUsage.received ? directUsage.outputTokens : Math.ceil(fullResponse.length / 4)
|
|
1550
|
-
const totalTokens = inputTokens + outputTokens
|
|
1551
|
-
if (totalTokens > 0) {
|
|
1552
|
-
const cost = estimateCost(sessionForRun.model, inputTokens, outputTokens)
|
|
1553
|
-
const history = getSessionMessages(sessionId)
|
|
1554
|
-
const usageRecord: UsageRecord = {
|
|
1555
|
-
sessionId,
|
|
1556
|
-
messageIndex: history.length,
|
|
1557
|
-
model: sessionForRun.model,
|
|
1558
|
-
provider: providerType,
|
|
1559
|
-
inputTokens,
|
|
1560
|
-
outputTokens,
|
|
1561
|
-
totalTokens,
|
|
1562
|
-
estimatedCost: cost,
|
|
1563
|
-
timestamp: Date.now(),
|
|
1564
|
-
durationMs,
|
|
1565
|
-
agentId: sessionForRun.agentId || null,
|
|
1566
|
-
projectId: sessionForRun.projectId || null,
|
|
1567
|
-
}
|
|
1568
|
-
appendUsage(sessionId, usageRecord)
|
|
1569
|
-
emit({
|
|
1570
|
-
t: 'md',
|
|
1571
|
-
text: JSON.stringify({ usage: { inputTokens, outputTokens, totalTokens, estimatedCost: cost } }),
|
|
1572
|
-
})
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
const endPostProcessPerf = perf.start('chat-execution', 'post-process', { sessionId })
|
|
1577
|
-
const toolRoutingResult = preflightToolRoutingResult || await runPostLlmToolRouting({
|
|
1578
|
-
session: sessionForRun,
|
|
1579
|
-
sessionId,
|
|
1580
|
-
message,
|
|
1581
|
-
effectiveMessage,
|
|
1582
|
-
enabledExtensions: extensionsForRun,
|
|
1583
|
-
toolPolicy,
|
|
1584
|
-
appSettings,
|
|
1585
|
-
internal,
|
|
1586
|
-
source,
|
|
1587
|
-
toolEvents,
|
|
1588
|
-
emit,
|
|
1589
|
-
}, fullResponse, errorMessage)
|
|
1590
|
-
|
|
1591
|
-
fullResponse = toolRoutingResult.fullResponse
|
|
1592
|
-
errorMessage = toolRoutingResult.errorMessage
|
|
1593
91
|
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
})) {
|
|
1600
|
-
const notice = `Tool execution notice: requested tool(s) ${toolRoutingResult.missedRequestedTools.join(', ')} were not actually invoked in this run.`
|
|
1601
|
-
emit({ t: 'err', text: notice })
|
|
1602
|
-
const trimmedResponse = (fullResponse || '').trim()
|
|
1603
|
-
fullResponse = trimmedResponse
|
|
1604
|
-
? `${trimmedResponse}\n\n${notice}`
|
|
1605
|
-
: notice
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
const terminalError = deriveTerminalRunError({
|
|
1609
|
-
errorMessage,
|
|
1610
|
-
fullResponse: fullResponse || '',
|
|
1611
|
-
streamErrors,
|
|
1612
|
-
toolEvents,
|
|
1613
|
-
internal,
|
|
92
|
+
const streamResult = await executePreparedChatTurn({
|
|
93
|
+
input,
|
|
94
|
+
prepared: preparedTurn,
|
|
95
|
+
partialPersistence,
|
|
96
|
+
preflightToolRoutingResult: preflight?.directMemoryResult || null,
|
|
1614
97
|
})
|
|
1615
|
-
if (terminalError && terminalError !== errorMessage) {
|
|
1616
|
-
if (!errorMessage) {
|
|
1617
|
-
log.warn('chat-run', `Run ended without a visible response for session ${sessionId}`, {
|
|
1618
|
-
runId,
|
|
1619
|
-
source,
|
|
1620
|
-
internal,
|
|
1621
|
-
provider: providerType,
|
|
1622
|
-
messagePreview: effectiveMessage.slice(0, 200),
|
|
1623
|
-
inferredError: terminalError,
|
|
1624
|
-
})
|
|
1625
|
-
}
|
|
1626
|
-
errorMessage = terminalError
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
const persistedToolEvents = dedupeConsecutiveToolEvents(pruneIncompleteToolEvents(toolEvents))
|
|
1630
|
-
let finalText = (fullResponse || '').trim() || (!internal && errorMessage ? `Error: ${errorMessage}` : '')
|
|
1631
|
-
if (extensionsForRun.length > 0 && finalText && !isHeartbeatRun) {
|
|
1632
|
-
try {
|
|
1633
|
-
finalText = await transformCapabilityText(
|
|
1634
|
-
'transformOutboundMessage',
|
|
1635
|
-
{ session: sessionForRun, text: finalText },
|
|
1636
|
-
{ enabledIds: extensionsForRun },
|
|
1637
|
-
)
|
|
1638
|
-
} catch { /* outbound transforms are non-critical */ }
|
|
1639
|
-
}
|
|
1640
|
-
finalText = reconcileConnectorDeliveryText(finalText, persistedToolEvents)
|
|
1641
|
-
finalText = normalizeAssistantArtifactLinks(finalText, session.cwd)
|
|
1642
|
-
finalText = applyExactOutputContract({
|
|
1643
|
-
contract: await resolveExactOutputContractWithTimeout({
|
|
1644
|
-
sessionId,
|
|
1645
|
-
agentId: sessionForRun.agentId || null,
|
|
1646
|
-
userMessage: message,
|
|
1647
|
-
currentResponse: finalText,
|
|
1648
|
-
toolEvents: persistedToolEvents,
|
|
1649
|
-
internal,
|
|
1650
|
-
source,
|
|
1651
|
-
}),
|
|
1652
|
-
text: finalText,
|
|
1653
|
-
errorMessage,
|
|
1654
|
-
toolEvents: persistedToolEvents,
|
|
1655
|
-
})
|
|
1656
|
-
const rawTextForPersistence = stripMainLoopMetaForPersistence(finalText)
|
|
1657
|
-
const hiddenControlOnly = shouldSuppressHiddenControlText(rawTextForPersistence)
|
|
1658
|
-
const textForPersistence = stripHiddenControlTokens(rawTextForPersistence)
|
|
1659
|
-
const persistedText = getPersistedAssistantText(textForPersistence, persistedToolEvents)
|
|
1660
|
-
let persistedResponseForHooks = textForPersistence
|
|
1661
|
-
|
|
1662
|
-
if (isHeartbeatRun && rawTextForPersistence) {
|
|
1663
|
-
const heartbeatStatus = extractHeartbeatStatus(rawTextForPersistence)
|
|
1664
|
-
if (heartbeatStatus) emit({ t: 'status', text: JSON.stringify(heartbeatStatus) })
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
// HEARTBEAT_OK suppression
|
|
1668
|
-
const heartbeatConfig = input.heartbeatConfig
|
|
1669
|
-
let heartbeatClassification: 'suppress' | 'strip' | 'keep' | null = null
|
|
1670
|
-
if (isHeartbeatRun && rawTextForPersistence.length > 0) {
|
|
1671
|
-
heartbeatClassification = classifyHeartbeatResponse(rawTextForPersistence, heartbeatConfig?.ackMaxChars ?? 300, toolEvents.length > 0)
|
|
1672
98
|
|
|
1673
|
-
|
|
1674
|
-
// If the model repeats itself exactly within 24h, suppress the heartbeat alert.
|
|
1675
|
-
if (heartbeatClassification !== 'suppress' && !toolEvents.length) {
|
|
1676
|
-
const prevText = session.lastHeartbeatText || ''
|
|
1677
|
-
const prevSentAt = session.lastHeartbeatSentAt || 0
|
|
1678
|
-
const isDuplicate = prevText.trim() === persistedText.trim()
|
|
1679
|
-
&& (Date.now() - prevSentAt) < 24 * 60 * 60 * 1000
|
|
1680
|
-
if (isDuplicate) {
|
|
1681
|
-
heartbeatClassification = 'suppress'
|
|
1682
|
-
log.info('heartbeat', `Duplicate heartbeat suppressed for session ${sessionId} (same text within 24h)`)
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
}
|
|
99
|
+
await partialPersistence.awaitIdle()
|
|
1686
100
|
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
notify(`heartbeat:agent:${session.agentId}`)
|
|
101
|
+
if (!streamResult.errorMessage) {
|
|
102
|
+
markProviderSuccess(preparedTurn.providerType, preparedTurn.sessionForRun.credentialId)
|
|
1690
103
|
}
|
|
1691
104
|
|
|
1692
|
-
const
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
const fresh = loadSessions()
|
|
1705
|
-
const current = fresh[sessionId]
|
|
1706
|
-
let assistantPersisted = false
|
|
1707
|
-
if (current) {
|
|
1708
|
-
current.messages = Array.isArray(current.messages) ? current.messages : []
|
|
1709
|
-
if (!isDirectConnectorSession(current) && current.connectorContext) {
|
|
1710
|
-
current.connectorContext = undefined
|
|
1711
|
-
}
|
|
1712
|
-
const currentAgent = current.agentId ? loadAgents()[current.agentId] : null
|
|
1713
|
-
pruneStreamingAssistantArtifacts(current.messages, {
|
|
1714
|
-
minIndex: runMessageStartIndex,
|
|
1715
|
-
minTime: runStartedAt,
|
|
1716
|
-
})
|
|
1717
|
-
const persistField = (key: string, value: unknown) => {
|
|
1718
|
-
const normalized = normalizeResumeId(value)
|
|
1719
|
-
if ((current as unknown as Record<string, unknown>)[key] !== normalized) {
|
|
1720
|
-
;(current as unknown as Record<string, unknown>)[key] = normalized
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
|
-
persistField('claudeSessionId', session.claudeSessionId)
|
|
1725
|
-
persistField('codexThreadId', session.codexThreadId)
|
|
1726
|
-
persistField('opencodeSessionId', session.opencodeSessionId)
|
|
1727
|
-
|
|
1728
|
-
const sourceResume = session.delegateResumeIds
|
|
1729
|
-
if (sourceResume && typeof sourceResume === 'object') {
|
|
1730
|
-
const currentResume = (current.delegateResumeIds && typeof current.delegateResumeIds === 'object')
|
|
1731
|
-
? current.delegateResumeIds
|
|
1732
|
-
: {}
|
|
1733
|
-
const sr = sourceResume as Record<string, unknown>
|
|
1734
|
-
const cr = currentResume as Record<string, unknown>
|
|
1735
|
-
const nextResume = {
|
|
1736
|
-
claudeCode: normalizeResumeId(sr.claudeCode ?? cr.claudeCode),
|
|
1737
|
-
codex: normalizeResumeId(sr.codex ?? cr.codex),
|
|
1738
|
-
opencode: normalizeResumeId(sr.opencode ?? cr.opencode),
|
|
1739
|
-
gemini: normalizeResumeId(sr.gemini ?? cr.gemini),
|
|
1740
|
-
}
|
|
1741
|
-
if (JSON.stringify(currentResume) !== JSON.stringify(nextResume)) {
|
|
1742
|
-
current.delegateResumeIds = nextResume
|
|
1743
|
-
}
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
if (shouldPersistAssistant) {
|
|
1747
|
-
const persistedKind = isHeartbeatRun ? 'heartbeat' : 'chat'
|
|
1748
|
-
const nowTs = Date.now()
|
|
1749
|
-
const nextAssistantMessage = await applyMessageLifecycleHooks({
|
|
1750
|
-
session: current,
|
|
1751
|
-
message: {
|
|
1752
|
-
role: 'assistant',
|
|
1753
|
-
text: persistedText,
|
|
1754
|
-
time: nowTs,
|
|
1755
|
-
thinking: thinkingText || undefined,
|
|
1756
|
-
toolEvents: persistedToolEvents.length ? persistedToolEvents : undefined,
|
|
1757
|
-
kind: persistedKind,
|
|
1758
|
-
},
|
|
1759
|
-
enabledIds: extensionsForRun,
|
|
1760
|
-
phase: isHeartbeatRun ? 'heartbeat' : 'assistant_final',
|
|
1761
|
-
runId: lifecycleRunId,
|
|
1762
|
-
})
|
|
1763
|
-
if (nextAssistantMessage) {
|
|
1764
|
-
const previous = current.messages.at(-1)
|
|
1765
|
-
const nextToolEvents = nextAssistantMessage.toolEvents || []
|
|
1766
|
-
const nextKind = nextAssistantMessage.kind || persistedKind
|
|
1767
|
-
if (shouldSuppressRedundantConnectorDeliveryFollowup({
|
|
1768
|
-
previous,
|
|
1769
|
-
nextText: nextAssistantMessage.text,
|
|
1770
|
-
nextToolEvents,
|
|
1771
|
-
nextKind,
|
|
1772
|
-
now: nowTs,
|
|
1773
|
-
})) {
|
|
1774
|
-
persistedResponseForHooks = nextAssistantMessage.text
|
|
1775
|
-
} else if (previous?.runId === lifecycleRunId || shouldReplaceRecentAssistantMessage({
|
|
1776
|
-
previous,
|
|
1777
|
-
nextToolEvents,
|
|
1778
|
-
nextKind,
|
|
1779
|
-
now: nowTs,
|
|
1780
|
-
}) || shouldReplaceRecentConnectorFollowupMessage({
|
|
1781
|
-
previous,
|
|
1782
|
-
nextText: nextAssistantMessage.text,
|
|
1783
|
-
nextToolEvents,
|
|
1784
|
-
nextKind,
|
|
1785
|
-
now: nowTs,
|
|
1786
|
-
})) {
|
|
1787
|
-
current.messages[current.messages.length - 1] = nextAssistantMessage
|
|
1788
|
-
assistantPersisted = true
|
|
1789
|
-
} else {
|
|
1790
|
-
current.messages.push(nextAssistantMessage)
|
|
1791
|
-
assistantPersisted = true
|
|
1792
|
-
}
|
|
1793
|
-
persistedResponseForHooks = nextAssistantMessage.text
|
|
1794
|
-
if (assistantPersisted) {
|
|
1795
|
-
if (isHeartbeatRun) {
|
|
1796
|
-
current.lastHeartbeatText = nextAssistantMessage.text
|
|
1797
|
-
current.lastHeartbeatSentAt = nowTs
|
|
1798
|
-
}
|
|
1799
|
-
try {
|
|
1800
|
-
await runCapabilityHook('onMessage', { session: current, message: nextAssistantMessage }, { enabledIds: extensionsForRun })
|
|
1801
|
-
} catch { /* onMessage hooks are non-critical */ }
|
|
1802
|
-
|
|
1803
|
-
// Conversation tone detection
|
|
1804
|
-
if (!internal) {
|
|
1805
|
-
const tone = estimateConversationTone(nextAssistantMessage.text)
|
|
1806
|
-
if (tone !== current.conversationTone) {
|
|
1807
|
-
current.conversationTone = tone
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
// Target routing for non-suppressed heartbeat alerts
|
|
1813
|
-
if (
|
|
1814
|
-
assistantPersisted
|
|
1815
|
-
&&
|
|
1816
|
-
isHeartbeatRun
|
|
1817
|
-
&& shouldAutoRouteHeartbeatAlerts(heartbeatConfig)
|
|
1818
|
-
&& heartbeatConfig?.target
|
|
1819
|
-
&& heartbeatConfig.target !== 'none'
|
|
1820
|
-
) {
|
|
1821
|
-
try {
|
|
1822
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1823
|
-
const { sendConnectorMessage } = require('../connectors/manager')
|
|
1824
|
-
let connectorId: string | undefined
|
|
1825
|
-
let channelId: string | undefined
|
|
1826
|
-
if (heartbeatConfig.target === 'last') {
|
|
1827
|
-
const lastTarget = resolveHeartbeatLastConnectorTarget(current)
|
|
1828
|
-
if (lastTarget) {
|
|
1829
|
-
connectorId = lastTarget.connectorId
|
|
1830
|
-
channelId = lastTarget.channelId
|
|
1831
|
-
}
|
|
1832
|
-
} else if (heartbeatConfig.target.includes(':')) {
|
|
1833
|
-
const [cId, chId] = heartbeatConfig.target.split(':', 2)
|
|
1834
|
-
connectorId = cId
|
|
1835
|
-
channelId = chId
|
|
1836
|
-
} else {
|
|
1837
|
-
channelId = heartbeatConfig.target
|
|
1838
|
-
}
|
|
1839
|
-
if (channelId) {
|
|
1840
|
-
sendConnectorMessage({ connectorId, channelId, text: nextAssistantMessage.text }).catch((err: unknown) => { log.warn('connector', 'Heartbeat connector delivery failed', { connectorId, channelId, sessionId, error: typeof err === 'object' && err !== null && 'message' in err ? (err as Error).message : String(err) }) })
|
|
1841
|
-
}
|
|
1842
|
-
} catch {
|
|
1843
|
-
// Best effort — connector manager may not be loaded
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
// Auto-discover connectors linked to this agent when no explicit target is set
|
|
1848
|
-
// Skip if a real inbound message was handled recently — the agent just responded to it
|
|
1849
|
-
if (
|
|
1850
|
-
assistantPersisted
|
|
1851
|
-
&&
|
|
1852
|
-
isHeartbeatRun
|
|
1853
|
-
&& shouldAutoRouteHeartbeatAlerts(heartbeatConfig)
|
|
1854
|
-
&& !heartbeatConfig?.target
|
|
1855
|
-
&& isDirectConnectorSession(current)
|
|
1856
|
-
) {
|
|
1857
|
-
const recentInbound = current.connectorContext?.lastInboundAt
|
|
1858
|
-
&& (Date.now() - current.connectorContext.lastInboundAt) < 60_000
|
|
1859
|
-
const connectorId = typeof current.connectorContext?.connectorId === 'string'
|
|
1860
|
-
? current.connectorContext.connectorId.trim()
|
|
1861
|
-
: ''
|
|
1862
|
-
const channelId = typeof current.connectorContext?.channelId === 'string'
|
|
1863
|
-
? current.connectorContext.channelId.trim()
|
|
1864
|
-
: ''
|
|
1865
|
-
if (!recentInbound && channelId) {
|
|
1866
|
-
try {
|
|
1867
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1868
|
-
const { sendConnectorMessage: sendMsg } = require('../connectors/manager')
|
|
1869
|
-
sendMsg({ connectorId: connectorId || undefined, channelId, text: nextAssistantMessage.text }).catch((err: unknown) => { log.warn('connector', 'Auto-route connector delivery failed', { connectorId, channelId, sessionId, error: typeof err === 'object' && err !== null && 'message' in err ? (err as Error).message : String(err) }) })
|
|
1870
|
-
} catch {
|
|
1871
|
-
// Best effort — connector manager may not be loaded
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
}
|
|
1877
|
-
if (isHeartbeatRun && heartbeatClassification === 'suppress') {
|
|
1878
|
-
pruneSuppressedHeartbeatStreamMessage(current.messages)
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
|
-
// P1: Prune old heartbeat messages to prevent context bloat.
|
|
1882
|
-
// Long-running agents accumulate ~48 no-op messages/day; keep only the most recent 2.
|
|
1883
|
-
if (isHeartbeatRun) {
|
|
1884
|
-
const pruned = pruneOldHeartbeatMessages(current.messages)
|
|
1885
|
-
if (pruned > 0) {
|
|
1886
|
-
log.info('heartbeat', `Pruned ${pruned} old heartbeat message(s) from session ${sessionId}`)
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
// Fire afterChatTurn hook for all enabled extensions (memory auto-save, logging, etc.)
|
|
1891
|
-
try {
|
|
1892
|
-
await runCapabilityHook('afterChatTurn', {
|
|
1893
|
-
session: current,
|
|
1894
|
-
message,
|
|
1895
|
-
response: persistedResponseForHooks,
|
|
1896
|
-
source,
|
|
1897
|
-
internal,
|
|
1898
|
-
toolEvents: persistedToolEvents,
|
|
1899
|
-
}, { enabledIds: extensionsForRun })
|
|
1900
|
-
} catch { /* afterChatTurn hooks are non-critical */ }
|
|
1901
|
-
|
|
1902
|
-
// Don't extend idle timeout for heartbeat runs — only user-initiated activity counts
|
|
1903
|
-
if (!isHeartbeatSource(source)) {
|
|
1904
|
-
current.lastActiveAt = Date.now()
|
|
1905
|
-
}
|
|
1906
|
-
|
|
1907
|
-
refreshSessionIdentityState(current, currentAgent)
|
|
1908
|
-
let resolvedMissionId = mission?.id || current.missionId || null
|
|
1909
|
-
if (resolvedMissionId) {
|
|
1910
|
-
const updatedMission = await applyMissionOutcomeForTurn({
|
|
1911
|
-
session: current,
|
|
1912
|
-
missionId: resolvedMissionId,
|
|
1913
|
-
source,
|
|
1914
|
-
runId: lifecycleRunId,
|
|
1915
|
-
message,
|
|
1916
|
-
assistantText: hiddenControlOnly ? '' : textForPersistence,
|
|
1917
|
-
error: errorMessage || null,
|
|
1918
|
-
toolEvents: persistedToolEvents,
|
|
1919
|
-
})
|
|
1920
|
-
if (updatedMission?.id) {
|
|
1921
|
-
resolvedMissionId = updatedMission.id
|
|
1922
|
-
current.missionId = updatedMission.id
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
try {
|
|
1926
|
-
syncSessionArchiveMemory(current, { agent: currentAgent })
|
|
1927
|
-
} catch { /* archive sync is best-effort */ }
|
|
1928
|
-
fresh[sessionId] = current
|
|
1929
|
-
saveSessions(fresh)
|
|
1930
|
-
if (current.agentId && shouldAutoDraftSkillSuggestion({
|
|
1931
|
-
assistantPersisted,
|
|
1932
|
-
internal,
|
|
1933
|
-
isHeartbeatRun,
|
|
1934
|
-
agentAutoDraftSetting: currentAgent?.autoDraftSkillSuggestions === true,
|
|
1935
|
-
toolEventCount: persistedToolEvents.length,
|
|
1936
|
-
messageCount: current.messages.length,
|
|
1937
|
-
})) {
|
|
1938
|
-
try {
|
|
1939
|
-
const { createSkillSuggestionFromSession } = await import('@/lib/server/skills/skill-suggestions')
|
|
1940
|
-
await createSkillSuggestionFromSession(sessionId)
|
|
1941
|
-
} catch {
|
|
1942
|
-
// Reviewed skill drafting is best-effort.
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
notify(`messages:${sessionId}`)
|
|
1946
|
-
}
|
|
105
|
+
const result = await finalizeChatTurn({
|
|
106
|
+
input,
|
|
107
|
+
prepared: preparedTurn,
|
|
108
|
+
partialPersistence,
|
|
109
|
+
fullResponse: streamResult.fullResponse,
|
|
110
|
+
errorMessage: streamResult.errorMessage,
|
|
111
|
+
initialToolRoutingResult: streamResult.toolRoutingResult,
|
|
112
|
+
responseCacheHit: streamResult.responseCacheHit,
|
|
113
|
+
directUsage: streamResult.directUsage,
|
|
114
|
+
durationMs: streamResult.durationMs,
|
|
115
|
+
emit: partialPersistence.emit,
|
|
116
|
+
})
|
|
1947
117
|
|
|
1948
|
-
endPostProcessPerf({ toolEventCount: persistedToolEvents.length })
|
|
1949
118
|
endTurnPerf({
|
|
1950
|
-
durationMs,
|
|
1951
|
-
toolEventCount:
|
|
1952
|
-
inputTokens:
|
|
1953
|
-
outputTokens:
|
|
1954
|
-
error: !!
|
|
119
|
+
durationMs: streamResult.durationMs,
|
|
120
|
+
toolEventCount: result.toolEvents.length,
|
|
121
|
+
inputTokens: result.inputTokens || 0,
|
|
122
|
+
outputTokens: result.outputTokens || 0,
|
|
123
|
+
error: !!result.error,
|
|
1955
124
|
})
|
|
1956
125
|
|
|
1957
|
-
return
|
|
1958
|
-
runId,
|
|
1959
|
-
sessionId,
|
|
1960
|
-
missionId: mission?.id || null,
|
|
1961
|
-
text: hiddenControlOnly ? '' : textForPersistence,
|
|
1962
|
-
persisted: assistantPersisted,
|
|
1963
|
-
toolEvents: persistedToolEvents,
|
|
1964
|
-
error: errorMessage,
|
|
1965
|
-
inputTokens: accumulatedUsage.inputTokens || undefined,
|
|
1966
|
-
outputTokens: accumulatedUsage.outputTokens || undefined,
|
|
1967
|
-
estimatedCost: accumulatedUsage.estimatedCost || undefined,
|
|
1968
|
-
}
|
|
126
|
+
return result
|
|
1969
127
|
}
|