@swarmclawai/swarmclaw 0.9.4 → 0.9.6
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 +8 -8
- package/package.json +1 -1
- package/src/app/api/settings/route.ts +1 -0
- package/src/app/api/wallets/[id]/send/route.ts +10 -4
- package/src/app/api/wallets/route.ts +5 -2
- package/src/app/settings/page.tsx +9 -0
- package/src/components/wallets/wallet-panel.tsx +12 -1
- package/src/components/wallets/wallet-section.tsx +4 -1
- package/src/lib/server/agents/main-agent-loop-advanced.test.ts +35 -0
- package/src/lib/server/agents/main-agent-loop.ts +12 -1
- package/src/lib/server/agents/subagent-runtime.test.ts +99 -16
- package/src/lib/server/agents/subagent-runtime.ts +115 -19
- package/src/lib/server/agents/subagent-swarm.ts +3 -3
- package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +112 -0
- package/src/lib/server/chat-execution/chat-execution.ts +357 -152
- package/src/lib/server/chat-execution/stream-agent-chat.test.ts +51 -0
- package/src/lib/server/chat-execution/stream-agent-chat.ts +201 -38
- package/src/lib/server/chat-execution/stream-continuation.ts +46 -0
- package/src/lib/server/connectors/contact-boundaries.ts +70 -8
- package/src/lib/server/connectors/manager.test.ts +129 -7
- package/src/lib/server/plugins.test.ts +263 -0
- package/src/lib/server/plugins.ts +406 -10
- package/src/lib/server/session-tools/context.ts +15 -1
- package/src/lib/server/session-tools/index.ts +42 -6
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +50 -0
- package/src/lib/server/session-tools/subagent.ts +3 -3
- package/src/lib/server/tool-loop-detection.test.ts +21 -0
- package/src/lib/server/tool-loop-detection.ts +79 -0
- package/src/lib/server/wallet/wallet-service.test.ts +25 -1
- package/src/lib/server/wallet/wallet-service.ts +13 -0
- package/src/types/index.ts +134 -1
- package/src/views/settings/section-wallets.tsx +35 -0
|
@@ -129,6 +129,49 @@ function resolveHeartbeatLastConnectorTarget(session: Session | null | undefined
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
type PersistPhase = 'user' | 'system' | 'assistant_partial' | 'assistant_final' | 'heartbeat'
|
|
133
|
+
|
|
134
|
+
async function applyMessageLifecycleHooks(params: {
|
|
135
|
+
session: Session
|
|
136
|
+
message: Message
|
|
137
|
+
enabledIds: string[]
|
|
138
|
+
phase: PersistPhase
|
|
139
|
+
runId?: string
|
|
140
|
+
isSynthetic?: boolean
|
|
141
|
+
}): Promise<Message | null> {
|
|
142
|
+
const pluginManager = getPluginManager()
|
|
143
|
+
let currentMessage = params.message
|
|
144
|
+
const toolEvents = Array.isArray(currentMessage.toolEvents)
|
|
145
|
+
? currentMessage.toolEvents.filter((event) => typeof event.output === 'string' || event.error === true)
|
|
146
|
+
: []
|
|
147
|
+
|
|
148
|
+
for (const event of toolEvents) {
|
|
149
|
+
currentMessage = await pluginManager.runToolResultPersist(
|
|
150
|
+
{
|
|
151
|
+
session: params.session,
|
|
152
|
+
message: currentMessage,
|
|
153
|
+
toolName: event.name,
|
|
154
|
+
toolCallId: event.toolCallId,
|
|
155
|
+
isSynthetic: params.isSynthetic,
|
|
156
|
+
},
|
|
157
|
+
{ enabledIds: params.enabledIds },
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const writeResult = await pluginManager.runBeforeMessageWrite(
|
|
162
|
+
{
|
|
163
|
+
session: params.session,
|
|
164
|
+
message: currentMessage,
|
|
165
|
+
phase: params.phase,
|
|
166
|
+
runId: params.runId,
|
|
167
|
+
},
|
|
168
|
+
{ enabledIds: params.enabledIds },
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if (writeResult.block) return null
|
|
172
|
+
return writeResult.message
|
|
173
|
+
}
|
|
174
|
+
|
|
132
175
|
interface SessionWithCredentials {
|
|
133
176
|
credentialId?: string | null
|
|
134
177
|
}
|
|
@@ -512,7 +555,7 @@ function buildAgentSystemPrompt(session: Session): string | undefined {
|
|
|
512
555
|
// 2. Runtime & Capabilities
|
|
513
556
|
const runtimeLines = [
|
|
514
557
|
'## Runtime',
|
|
515
|
-
`os=${process.platform} | host=${os.hostname()} | agent=${agent.id} | provider=${
|
|
558
|
+
`os=${process.platform} | host=${os.hostname()} | agent=${agent.id} | provider=${session.provider} | model=${session.model}`,
|
|
516
559
|
`capabilities=${buildAgentRuntimeCapabilities(enabledPlugins).join(',')}`,
|
|
517
560
|
'tool_access=universal',
|
|
518
561
|
]
|
|
@@ -627,6 +670,8 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
627
670
|
const runMessageStartIndex = session.messages.length
|
|
628
671
|
|
|
629
672
|
const appSettings = loadSettings()
|
|
673
|
+
const pluginManager = getPluginManager()
|
|
674
|
+
const lifecycleRunId = runId || `${sessionId}:${runStartedAt}`
|
|
630
675
|
const agentForSession = session.agentId ? loadAgents()[session.agentId] : null
|
|
631
676
|
if (isAgentDisabled(agentForSession)) {
|
|
632
677
|
const disabledError = buildAgentDisabledMessage(agentForSession, 'run chats')
|
|
@@ -634,14 +679,24 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
634
679
|
|
|
635
680
|
let persisted = false
|
|
636
681
|
if (!internal) {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
682
|
+
const disabledMessage = await applyMessageLifecycleHooks({
|
|
683
|
+
session,
|
|
684
|
+
message: {
|
|
685
|
+
role: 'assistant',
|
|
686
|
+
text: disabledError,
|
|
687
|
+
time: Date.now(),
|
|
688
|
+
},
|
|
689
|
+
enabledIds: Array.isArray(session.plugins) ? session.plugins : [],
|
|
690
|
+
phase: 'assistant_final',
|
|
691
|
+
runId: lifecycleRunId,
|
|
692
|
+
isSynthetic: true,
|
|
641
693
|
})
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
694
|
+
if (disabledMessage) {
|
|
695
|
+
session.messages.push(disabledMessage)
|
|
696
|
+
session.lastActiveAt = Date.now()
|
|
697
|
+
saveSessions(sessions)
|
|
698
|
+
persisted = true
|
|
699
|
+
}
|
|
645
700
|
}
|
|
646
701
|
|
|
647
702
|
return {
|
|
@@ -673,6 +728,19 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
673
728
|
})
|
|
674
729
|
if (!freshness.fresh) {
|
|
675
730
|
try { syncSessionArchiveMemory(session, { agent: agentForSession }) } catch { /* archive sync is best-effort */ }
|
|
731
|
+
await pluginManager.runHook(
|
|
732
|
+
'sessionEnd',
|
|
733
|
+
{
|
|
734
|
+
sessionId: session.id,
|
|
735
|
+
session,
|
|
736
|
+
messageCount: Array.isArray(session.messages) ? session.messages.length : 0,
|
|
737
|
+
durationMs: Date.now() - (session.createdAt || runStartedAt),
|
|
738
|
+
reason: freshness.reason || 'session_reset',
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
enabledIds: Array.isArray(session.plugins) ? session.plugins : [],
|
|
742
|
+
},
|
|
743
|
+
)
|
|
676
744
|
resetSessionRuntime(session, freshness.reason || 'session_reset')
|
|
677
745
|
onEvent?.({ t: 'status', text: JSON.stringify({ sessionReset: freshness.reason || 'session_reset' }) })
|
|
678
746
|
sessions[sessionId] = session
|
|
@@ -683,6 +751,16 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
683
751
|
try { syncSessionArchiveMemory(session, { agent: agentForSession }) } catch { /* archive sync is best-effort */ }
|
|
684
752
|
}
|
|
685
753
|
const pluginsForRun = heartbeatStatusOnly ? [] : toolPolicy.enabledPlugins
|
|
754
|
+
if (runMessageStartIndex === 0) {
|
|
755
|
+
await pluginManager.runHook(
|
|
756
|
+
'sessionStart',
|
|
757
|
+
{
|
|
758
|
+
session,
|
|
759
|
+
resumedFrom: session.parentSessionId || null,
|
|
760
|
+
},
|
|
761
|
+
{ enabledIds: pluginsForRun },
|
|
762
|
+
)
|
|
763
|
+
}
|
|
686
764
|
let sessionForRun = pluginsForRun === session.plugins
|
|
687
765
|
? session
|
|
688
766
|
: { ...session, plugins: pluginsForRun }
|
|
@@ -714,6 +792,31 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
714
792
|
sessionForRun = { ...sessionForRun, model: input.modelOverride }
|
|
715
793
|
}
|
|
716
794
|
|
|
795
|
+
if (pluginsForRun.length > 0) {
|
|
796
|
+
const modelResolvePrompt = heartbeatLightContext
|
|
797
|
+
? (buildLightHeartbeatSystemPrompt(sessionForRun) || '')
|
|
798
|
+
: (buildAgentSystemPrompt(sessionForRun) || '')
|
|
799
|
+
const modelResolve = await pluginManager.runBeforeModelResolve(
|
|
800
|
+
{
|
|
801
|
+
session: sessionForRun,
|
|
802
|
+
prompt: modelResolvePrompt,
|
|
803
|
+
message: effectiveMessage,
|
|
804
|
+
provider: sessionForRun.provider,
|
|
805
|
+
model: sessionForRun.model,
|
|
806
|
+
apiEndpoint: sessionForRun.apiEndpoint || null,
|
|
807
|
+
},
|
|
808
|
+
{ enabledIds: pluginsForRun },
|
|
809
|
+
)
|
|
810
|
+
if (modelResolve) {
|
|
811
|
+
sessionForRun = {
|
|
812
|
+
...sessionForRun,
|
|
813
|
+
provider: modelResolve.providerOverride ?? sessionForRun.provider,
|
|
814
|
+
model: modelResolve.modelOverride ?? sessionForRun.model,
|
|
815
|
+
...(modelResolve.apiEndpointOverride !== undefined ? { apiEndpoint: modelResolve.apiEndpointOverride } : {}),
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
717
820
|
if (!heartbeatStatusOnly && toolPolicy.blockedPlugins.length > 0) {
|
|
718
821
|
const blockedSummary = toolPolicy.blockedPlugins
|
|
719
822
|
.map((entry) => `${entry.tool} (${entry.reason})`)
|
|
@@ -736,14 +839,24 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
736
839
|
|
|
737
840
|
let persisted = false
|
|
738
841
|
if (!internal) {
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
842
|
+
const budgetMessage = await applyMessageLifecycleHooks({
|
|
843
|
+
session,
|
|
844
|
+
message: {
|
|
845
|
+
role: 'assistant',
|
|
846
|
+
text: budgetError,
|
|
847
|
+
time: Date.now(),
|
|
848
|
+
},
|
|
849
|
+
enabledIds: Array.isArray(session.plugins) ? session.plugins : [],
|
|
850
|
+
phase: 'assistant_final',
|
|
851
|
+
runId: lifecycleRunId,
|
|
852
|
+
isSynthetic: true,
|
|
743
853
|
})
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
854
|
+
if (budgetMessage) {
|
|
855
|
+
session.messages.push(budgetMessage)
|
|
856
|
+
session.lastActiveAt = Date.now()
|
|
857
|
+
saveSessions(sessions)
|
|
858
|
+
persisted = true
|
|
859
|
+
}
|
|
747
860
|
}
|
|
748
861
|
|
|
749
862
|
return {
|
|
@@ -773,14 +886,24 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
773
886
|
|
|
774
887
|
let persisted = false
|
|
775
888
|
if (!internal) {
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
889
|
+
const spendMessage = await applyMessageLifecycleHooks({
|
|
890
|
+
session,
|
|
891
|
+
message: {
|
|
892
|
+
role: 'assistant',
|
|
893
|
+
text: spendError,
|
|
894
|
+
time: Date.now(),
|
|
895
|
+
},
|
|
896
|
+
enabledIds: Array.isArray(session.plugins) ? session.plugins : [],
|
|
897
|
+
phase: 'assistant_final',
|
|
898
|
+
runId: lifecycleRunId,
|
|
899
|
+
isSynthetic: true,
|
|
780
900
|
})
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
901
|
+
if (spendMessage) {
|
|
902
|
+
session.messages.push(spendMessage)
|
|
903
|
+
session.lastActiveAt = Date.now()
|
|
904
|
+
saveSessions(sessions)
|
|
905
|
+
persisted = true
|
|
906
|
+
}
|
|
784
907
|
}
|
|
785
908
|
|
|
786
909
|
return {
|
|
@@ -821,30 +944,48 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
821
944
|
const shouldPersistUserMessage = shouldPersistInboundUserMessage(internal, source)
|
|
822
945
|
if (shouldPersistUserMessage) {
|
|
823
946
|
const linkAnalysis = !internal ? await runLinkUnderstanding(message) : []
|
|
824
|
-
const nextUserMessage
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
imageUrl: imageUrl || undefined,
|
|
830
|
-
attachedFiles: attachedFiles?.length ? attachedFiles : undefined,
|
|
831
|
-
replyToId: input.replyToId || undefined,
|
|
832
|
-
}
|
|
833
|
-
session.messages.push(nextUserMessage)
|
|
834
|
-
if (linkAnalysis.length > 0) {
|
|
835
|
-
session.messages.push({
|
|
836
|
-
role: 'assistant',
|
|
837
|
-
kind: 'system',
|
|
838
|
-
text: `[Automated Link Analysis]\n${linkAnalysis.join('\n\n')}`,
|
|
947
|
+
const nextUserMessage = await applyMessageLifecycleHooks({
|
|
948
|
+
session,
|
|
949
|
+
message: {
|
|
950
|
+
role: 'user',
|
|
951
|
+
text: message,
|
|
839
952
|
time: Date.now(),
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
953
|
+
imagePath: imagePath || undefined,
|
|
954
|
+
imageUrl: imageUrl || undefined,
|
|
955
|
+
attachedFiles: attachedFiles?.length ? attachedFiles : undefined,
|
|
956
|
+
replyToId: input.replyToId || undefined,
|
|
957
|
+
},
|
|
958
|
+
enabledIds: pluginsForRun,
|
|
959
|
+
phase: 'user',
|
|
960
|
+
runId: lifecycleRunId,
|
|
961
|
+
})
|
|
962
|
+
if (nextUserMessage) {
|
|
963
|
+
session.messages.push(nextUserMessage)
|
|
964
|
+
if (linkAnalysis.length > 0) {
|
|
965
|
+
const linkAnalysisMessage = await applyMessageLifecycleHooks({
|
|
966
|
+
session,
|
|
967
|
+
message: {
|
|
968
|
+
role: 'assistant',
|
|
969
|
+
kind: 'system',
|
|
970
|
+
text: `[Automated Link Analysis]\n${linkAnalysis.join('\n\n')}`,
|
|
971
|
+
time: Date.now(),
|
|
972
|
+
},
|
|
973
|
+
enabledIds: pluginsForRun,
|
|
974
|
+
phase: 'system',
|
|
975
|
+
runId: lifecycleRunId,
|
|
976
|
+
isSynthetic: true,
|
|
977
|
+
})
|
|
978
|
+
if (linkAnalysisMessage) {
|
|
979
|
+
session.messages.push(linkAnalysisMessage)
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
session.lastActiveAt = Date.now()
|
|
983
|
+
saveSessions(sessions)
|
|
984
|
+
if (!internal) {
|
|
985
|
+
try {
|
|
986
|
+
await pluginManager.runHook('onMessage', { session, message: nextUserMessage }, { enabledIds: pluginsForRun })
|
|
987
|
+
} catch { /* onMessage hooks are non-critical */ }
|
|
988
|
+
}
|
|
848
989
|
}
|
|
849
990
|
}
|
|
850
991
|
|
|
@@ -864,8 +1005,8 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
864
1005
|
// to avoid duplicating tool discipline, operating guidance, and capability sections.
|
|
865
1006
|
// lightContext mode uses a minimal prompt for both paths to reduce token cost.
|
|
866
1007
|
const systemPrompt = heartbeatLightContext
|
|
867
|
-
? buildLightHeartbeatSystemPrompt(
|
|
868
|
-
: (hasPlugins ? undefined : buildAgentSystemPrompt(
|
|
1008
|
+
? buildLightHeartbeatSystemPrompt(sessionForRun)
|
|
1009
|
+
: (hasPlugins ? undefined : buildAgentSystemPrompt(sessionForRun))
|
|
869
1010
|
const toolEvents: MessageToolEvent[] = []
|
|
870
1011
|
const streamErrors: string[] = []
|
|
871
1012
|
const accumulatedUsage = { inputTokens: 0, outputTokens: 0, estimatedCost: 0 }
|
|
@@ -876,6 +1017,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
876
1017
|
let lastPartialSnapshotKey = ''
|
|
877
1018
|
let partialSaveTimeout: ReturnType<typeof setTimeout> | null = null
|
|
878
1019
|
let partialPersistenceClosed = false
|
|
1020
|
+
let partialPersistChain: Promise<void> = Promise.resolve()
|
|
879
1021
|
|
|
880
1022
|
const stopPartialAssistantPersistence = () => {
|
|
881
1023
|
partialPersistenceClosed = true
|
|
@@ -885,35 +1027,41 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
885
1027
|
}
|
|
886
1028
|
}
|
|
887
1029
|
|
|
888
|
-
const persistStreamingAssistantArtifact = () => {
|
|
1030
|
+
const persistStreamingAssistantArtifact = async () => {
|
|
889
1031
|
partialSaveTimeout = null
|
|
890
1032
|
if (partialPersistenceClosed) return
|
|
891
1033
|
const persistedToolEvents = toolEvents.length ? dedupeConsecutiveToolEvents([...toolEvents]) : []
|
|
892
1034
|
if (!hasPersistableAssistantPayload(streamingPartialText, thinkingText, persistedToolEvents)) return
|
|
893
1035
|
|
|
894
|
-
const snapshotKey = JSON.stringify([
|
|
895
|
-
streamingPartialText,
|
|
896
|
-
thinkingText,
|
|
897
|
-
getToolEventsSnapshotKey(persistedToolEvents),
|
|
898
|
-
])
|
|
899
|
-
if (snapshotKey === lastPartialSnapshotKey) return
|
|
900
|
-
|
|
901
|
-
lastPartialSnapshotKey = snapshotKey
|
|
902
|
-
lastPartialSaveAt = Date.now()
|
|
903
|
-
|
|
904
1036
|
try {
|
|
905
1037
|
const fresh = loadSessions()
|
|
906
1038
|
const current = fresh[sessionId]
|
|
907
1039
|
if (!current) return
|
|
908
1040
|
current.messages = Array.isArray(current.messages) ? current.messages : []
|
|
909
|
-
const partialMsg
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1041
|
+
const partialMsg = await applyMessageLifecycleHooks({
|
|
1042
|
+
session: current,
|
|
1043
|
+
message: {
|
|
1044
|
+
role: 'assistant',
|
|
1045
|
+
text: streamingPartialText,
|
|
1046
|
+
time: Date.now(),
|
|
1047
|
+
streaming: true,
|
|
1048
|
+
thinking: thinkingText || undefined,
|
|
1049
|
+
toolEvents: persistedToolEvents.length ? persistedToolEvents : undefined,
|
|
1050
|
+
},
|
|
1051
|
+
enabledIds: pluginsForRun,
|
|
1052
|
+
phase: 'assistant_partial',
|
|
1053
|
+
runId: lifecycleRunId,
|
|
1054
|
+
isSynthetic: true,
|
|
1055
|
+
})
|
|
1056
|
+
if (!partialMsg) return
|
|
1057
|
+
const snapshotKey = JSON.stringify([
|
|
1058
|
+
partialMsg.text,
|
|
1059
|
+
partialMsg.thinking || '',
|
|
1060
|
+
getToolEventsSnapshotKey(partialMsg.toolEvents || []),
|
|
1061
|
+
])
|
|
1062
|
+
if (snapshotKey === lastPartialSnapshotKey) return
|
|
1063
|
+
lastPartialSnapshotKey = snapshotKey
|
|
1064
|
+
lastPartialSaveAt = Date.now()
|
|
917
1065
|
upsertStreamingAssistantArtifact(current.messages, partialMsg, {
|
|
918
1066
|
minIndex: runMessageStartIndex,
|
|
919
1067
|
minTime: runStartedAt,
|
|
@@ -924,6 +1072,14 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
924
1072
|
} catch { /* partial save is best-effort */ }
|
|
925
1073
|
}
|
|
926
1074
|
|
|
1075
|
+
const triggerPartialAssistantPersist = () => {
|
|
1076
|
+
partialPersistChain = partialPersistChain
|
|
1077
|
+
.catch(() => {})
|
|
1078
|
+
.then(async () => {
|
|
1079
|
+
await persistStreamingAssistantArtifact()
|
|
1080
|
+
})
|
|
1081
|
+
}
|
|
1082
|
+
|
|
927
1083
|
const queuePartialAssistantPersist = (immediate = false) => {
|
|
928
1084
|
if (partialPersistenceClosed) return
|
|
929
1085
|
const now = Date.now()
|
|
@@ -933,12 +1089,12 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
933
1089
|
clearTimeout(partialSaveTimeout)
|
|
934
1090
|
partialSaveTimeout = null
|
|
935
1091
|
}
|
|
936
|
-
|
|
1092
|
+
triggerPartialAssistantPersist()
|
|
937
1093
|
return
|
|
938
1094
|
}
|
|
939
1095
|
if (partialSaveTimeout) return
|
|
940
1096
|
partialSaveTimeout = setTimeout(() => {
|
|
941
|
-
|
|
1097
|
+
triggerPartialAssistantPersist()
|
|
942
1098
|
}, minIntervalMs - (now - lastPartialSaveAt))
|
|
943
1099
|
}
|
|
944
1100
|
|
|
@@ -1084,6 +1240,20 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1084
1240
|
})
|
|
1085
1241
|
emit({ t: 'd', text: cached.text })
|
|
1086
1242
|
} else {
|
|
1243
|
+
await pluginManager.runHook(
|
|
1244
|
+
'llmInput',
|
|
1245
|
+
{
|
|
1246
|
+
session: sessionForRun,
|
|
1247
|
+
runId: lifecycleRunId,
|
|
1248
|
+
provider: providerType,
|
|
1249
|
+
model: sessionForRun.model,
|
|
1250
|
+
systemPrompt,
|
|
1251
|
+
prompt: effectiveMessage,
|
|
1252
|
+
historyMessages: directHistorySnapshot,
|
|
1253
|
+
imagesCount: resolvedImagePath ? 1 : 0,
|
|
1254
|
+
},
|
|
1255
|
+
{ enabledIds: pluginsForRun },
|
|
1256
|
+
)
|
|
1087
1257
|
fullResponse = await provider.handler.streamChat({
|
|
1088
1258
|
session: sessionForRun,
|
|
1089
1259
|
message: effectiveMessage,
|
|
@@ -1101,6 +1271,26 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1101
1271
|
onUsage: (u) => { directUsage.inputTokens = u.inputTokens; directUsage.outputTokens = u.outputTokens; directUsage.received = true },
|
|
1102
1272
|
signal: abortController.signal,
|
|
1103
1273
|
})
|
|
1274
|
+
await pluginManager.runHook(
|
|
1275
|
+
'llmOutput',
|
|
1276
|
+
{
|
|
1277
|
+
session: sessionForRun,
|
|
1278
|
+
runId: lifecycleRunId,
|
|
1279
|
+
provider: providerType,
|
|
1280
|
+
model: sessionForRun.model,
|
|
1281
|
+
assistantTexts: fullResponse ? [fullResponse] : [],
|
|
1282
|
+
response: fullResponse,
|
|
1283
|
+
usage: directUsage.received
|
|
1284
|
+
? {
|
|
1285
|
+
input: directUsage.inputTokens,
|
|
1286
|
+
output: directUsage.outputTokens,
|
|
1287
|
+
total: directUsage.inputTokens + directUsage.outputTokens,
|
|
1288
|
+
estimatedCost: estimateCost(sessionForRun.model, directUsage.inputTokens, directUsage.outputTokens),
|
|
1289
|
+
}
|
|
1290
|
+
: undefined,
|
|
1291
|
+
},
|
|
1292
|
+
{ enabledIds: pluginsForRun },
|
|
1293
|
+
)
|
|
1104
1294
|
if (canUseResponseCache && responseCacheInput && fullResponse) {
|
|
1105
1295
|
setCachedLlmResponse(responseCacheInput, fullResponse, responseCacheConfig)
|
|
1106
1296
|
}
|
|
@@ -1127,6 +1317,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1127
1317
|
notify('sessions')
|
|
1128
1318
|
if (signal) signal.removeEventListener('abort', abortFromOutside)
|
|
1129
1319
|
}
|
|
1320
|
+
await partialPersistChain.catch(() => {})
|
|
1130
1321
|
|
|
1131
1322
|
if (!errorMessage) {
|
|
1132
1323
|
markProviderSuccess(providerType)
|
|
@@ -1228,6 +1419,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1228
1419
|
const hiddenControlOnly = shouldSuppressHiddenControlText(rawTextForPersistence)
|
|
1229
1420
|
const textForPersistence = stripHiddenControlTokens(rawTextForPersistence)
|
|
1230
1421
|
const persistedText = getPersistedAssistantText(textForPersistence, persistedToolEvents)
|
|
1422
|
+
let persistedResponseForHooks = textForPersistence
|
|
1231
1423
|
|
|
1232
1424
|
if (isHeartbeatRun && rawTextForPersistence) {
|
|
1233
1425
|
const heartbeatStatus = extractHeartbeatStatus(rawTextForPersistence)
|
|
@@ -1269,6 +1461,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1269
1461
|
|
|
1270
1462
|
const fresh = loadSessions()
|
|
1271
1463
|
const current = fresh[sessionId]
|
|
1464
|
+
let assistantPersisted = false
|
|
1272
1465
|
if (current) {
|
|
1273
1466
|
current.messages = Array.isArray(current.messages) ? current.messages : []
|
|
1274
1467
|
if (!isDirectConnectorSession(current) && current.connectorContext) {
|
|
@@ -1311,99 +1504,111 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1311
1504
|
if (shouldPersistAssistant) {
|
|
1312
1505
|
const persistedKind = isHeartbeatRun ? 'heartbeat' : 'chat'
|
|
1313
1506
|
const nowTs = Date.now()
|
|
1314
|
-
const nextAssistantMessage
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
current.messages
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
if (
|
|
1345
|
-
current.
|
|
1507
|
+
const nextAssistantMessage = await applyMessageLifecycleHooks({
|
|
1508
|
+
session: current,
|
|
1509
|
+
message: {
|
|
1510
|
+
role: 'assistant',
|
|
1511
|
+
text: persistedText,
|
|
1512
|
+
time: nowTs,
|
|
1513
|
+
thinking: thinkingText || undefined,
|
|
1514
|
+
toolEvents: persistedToolEvents.length ? persistedToolEvents : undefined,
|
|
1515
|
+
kind: persistedKind,
|
|
1516
|
+
},
|
|
1517
|
+
enabledIds: pluginsForRun,
|
|
1518
|
+
phase: isHeartbeatRun ? 'heartbeat' : 'assistant_final',
|
|
1519
|
+
runId: lifecycleRunId,
|
|
1520
|
+
})
|
|
1521
|
+
if (nextAssistantMessage) {
|
|
1522
|
+
const previous = current.messages.at(-1)
|
|
1523
|
+
const nextToolEvents = nextAssistantMessage.toolEvents || []
|
|
1524
|
+
const nextKind = nextAssistantMessage.kind || persistedKind
|
|
1525
|
+
if (previous?.streaming || shouldReplaceRecentAssistantMessage({
|
|
1526
|
+
previous,
|
|
1527
|
+
nextToolEvents,
|
|
1528
|
+
nextKind,
|
|
1529
|
+
now: nowTs,
|
|
1530
|
+
})) {
|
|
1531
|
+
current.messages[current.messages.length - 1] = nextAssistantMessage
|
|
1532
|
+
} else {
|
|
1533
|
+
current.messages.push(nextAssistantMessage)
|
|
1534
|
+
}
|
|
1535
|
+
assistantPersisted = true
|
|
1536
|
+
persistedResponseForHooks = nextAssistantMessage.text
|
|
1537
|
+
if (isHeartbeatRun) {
|
|
1538
|
+
current.lastHeartbeatText = nextAssistantMessage.text
|
|
1539
|
+
current.lastHeartbeatSentAt = nowTs
|
|
1346
1540
|
}
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
// Target routing for non-suppressed heartbeat alerts
|
|
1350
|
-
if (
|
|
1351
|
-
isHeartbeatRun
|
|
1352
|
-
&& shouldAutoRouteHeartbeatAlerts(heartbeatConfig)
|
|
1353
|
-
&& heartbeatConfig?.target
|
|
1354
|
-
&& heartbeatConfig.target !== 'none'
|
|
1355
|
-
) {
|
|
1356
1541
|
try {
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
channelId = lastTarget.channelId
|
|
1366
|
-
}
|
|
1367
|
-
} else if (heartbeatConfig.target.includes(':')) {
|
|
1368
|
-
const [cId, chId] = heartbeatConfig.target.split(':', 2)
|
|
1369
|
-
connectorId = cId
|
|
1370
|
-
channelId = chId
|
|
1371
|
-
} else {
|
|
1372
|
-
channelId = heartbeatConfig.target
|
|
1542
|
+
await pluginManager.runHook('onMessage', { session: current, message: nextAssistantMessage }, { enabledIds: pluginsForRun })
|
|
1543
|
+
} catch { /* onMessage hooks are non-critical */ }
|
|
1544
|
+
|
|
1545
|
+
// Conversation tone detection
|
|
1546
|
+
if (!internal) {
|
|
1547
|
+
const tone = estimateConversationTone(nextAssistantMessage.text)
|
|
1548
|
+
if (tone !== current.conversationTone) {
|
|
1549
|
+
current.conversationTone = tone
|
|
1373
1550
|
}
|
|
1374
|
-
if (channelId) {
|
|
1375
|
-
sendConnectorMessage({ connectorId, channelId, text: persistedText }).catch(() => {})
|
|
1376
|
-
}
|
|
1377
|
-
} catch {
|
|
1378
|
-
// Best effort — connector manager may not be loaded
|
|
1379
1551
|
}
|
|
1380
|
-
}
|
|
1381
1552
|
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
) {
|
|
1390
|
-
const recentInbound = current.connectorContext?.lastInboundAt
|
|
1391
|
-
&& (Date.now() - current.connectorContext.lastInboundAt) < 60_000
|
|
1392
|
-
const connectorId = typeof current.connectorContext?.connectorId === 'string'
|
|
1393
|
-
? current.connectorContext.connectorId.trim()
|
|
1394
|
-
: ''
|
|
1395
|
-
const channelId = typeof current.connectorContext?.channelId === 'string'
|
|
1396
|
-
? current.connectorContext.channelId.trim()
|
|
1397
|
-
: ''
|
|
1398
|
-
if (!recentInbound && channelId) {
|
|
1553
|
+
// Target routing for non-suppressed heartbeat alerts
|
|
1554
|
+
if (
|
|
1555
|
+
isHeartbeatRun
|
|
1556
|
+
&& shouldAutoRouteHeartbeatAlerts(heartbeatConfig)
|
|
1557
|
+
&& heartbeatConfig?.target
|
|
1558
|
+
&& heartbeatConfig.target !== 'none'
|
|
1559
|
+
) {
|
|
1399
1560
|
try {
|
|
1400
1561
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1401
|
-
const { sendConnectorMessage
|
|
1402
|
-
|
|
1562
|
+
const { sendConnectorMessage } = require('../connectors/manager')
|
|
1563
|
+
let connectorId: string | undefined
|
|
1564
|
+
let channelId: string | undefined
|
|
1565
|
+
if (heartbeatConfig.target === 'last') {
|
|
1566
|
+
const lastTarget = resolveHeartbeatLastConnectorTarget(current)
|
|
1567
|
+
if (lastTarget) {
|
|
1568
|
+
connectorId = lastTarget.connectorId
|
|
1569
|
+
channelId = lastTarget.channelId
|
|
1570
|
+
}
|
|
1571
|
+
} else if (heartbeatConfig.target.includes(':')) {
|
|
1572
|
+
const [cId, chId] = heartbeatConfig.target.split(':', 2)
|
|
1573
|
+
connectorId = cId
|
|
1574
|
+
channelId = chId
|
|
1575
|
+
} else {
|
|
1576
|
+
channelId = heartbeatConfig.target
|
|
1577
|
+
}
|
|
1578
|
+
if (channelId) {
|
|
1579
|
+
sendConnectorMessage({ connectorId, channelId, text: nextAssistantMessage.text }).catch(() => {})
|
|
1580
|
+
}
|
|
1403
1581
|
} catch {
|
|
1404
1582
|
// Best effort — connector manager may not be loaded
|
|
1405
1583
|
}
|
|
1406
1584
|
}
|
|
1585
|
+
|
|
1586
|
+
// Auto-discover connectors linked to this agent when no explicit target is set
|
|
1587
|
+
// Skip if a real inbound message was handled recently — the agent just responded to it
|
|
1588
|
+
if (
|
|
1589
|
+
isHeartbeatRun
|
|
1590
|
+
&& shouldAutoRouteHeartbeatAlerts(heartbeatConfig)
|
|
1591
|
+
&& !heartbeatConfig?.target
|
|
1592
|
+
&& isDirectConnectorSession(current)
|
|
1593
|
+
) {
|
|
1594
|
+
const recentInbound = current.connectorContext?.lastInboundAt
|
|
1595
|
+
&& (Date.now() - current.connectorContext.lastInboundAt) < 60_000
|
|
1596
|
+
const connectorId = typeof current.connectorContext?.connectorId === 'string'
|
|
1597
|
+
? current.connectorContext.connectorId.trim()
|
|
1598
|
+
: ''
|
|
1599
|
+
const channelId = typeof current.connectorContext?.channelId === 'string'
|
|
1600
|
+
? current.connectorContext.channelId.trim()
|
|
1601
|
+
: ''
|
|
1602
|
+
if (!recentInbound && channelId) {
|
|
1603
|
+
try {
|
|
1604
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1605
|
+
const { sendConnectorMessage: sendMsg } = require('../connectors/manager')
|
|
1606
|
+
sendMsg({ connectorId: connectorId || undefined, channelId, text: nextAssistantMessage.text }).catch(() => {})
|
|
1607
|
+
} catch {
|
|
1608
|
+
// Best effort — connector manager may not be loaded
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1407
1612
|
}
|
|
1408
1613
|
}
|
|
1409
1614
|
if (isHeartbeatRun && heartbeatClassification === 'suppress') {
|
|
@@ -1424,7 +1629,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1424
1629
|
await getPluginManager().runHook('afterChatTurn', {
|
|
1425
1630
|
session: current,
|
|
1426
1631
|
message,
|
|
1427
|
-
response:
|
|
1632
|
+
response: persistedResponseForHooks,
|
|
1428
1633
|
source,
|
|
1429
1634
|
internal,
|
|
1430
1635
|
toolEvents: persistedToolEvents,
|
|
@@ -1458,7 +1663,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1458
1663
|
runId,
|
|
1459
1664
|
sessionId,
|
|
1460
1665
|
text: hiddenControlOnly ? '' : textForPersistence,
|
|
1461
|
-
persisted:
|
|
1666
|
+
persisted: assistantPersisted,
|
|
1462
1667
|
toolEvents: persistedToolEvents,
|
|
1463
1668
|
error: errorMessage,
|
|
1464
1669
|
inputTokens: accumulatedUsage.inputTokens || undefined,
|