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