circuschief 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/packages/server/src/agents/AgentGateway.js +36 -3
- package/packages/server/src/agents/BaseAgent.js +15 -1
- package/packages/server/src/agents/LoggingAgentWrapper.js +4 -0
- package/packages/server/src/agents/adapters/ClaudeCodeAdapter.js +9 -6
- package/packages/server/src/agents/adapters/CodexAdapter.js +262 -14
- package/packages/server/src/agents/adapters/codexCliRunner.js +185 -0
- package/packages/server/src/agents/adapters/codexEventMapper.js +235 -0
- package/packages/server/src/agents/types.js +1 -0
- package/packages/server/src/agents/vcr/VCRAgentAdapter.js +8 -0
- package/packages/server/src/api/agents.js +27 -0
- package/packages/server/src/api/canvas.js +20 -0
- package/packages/server/src/api/index.js +2 -0
- package/packages/server/src/api/projects-session-helpers.js +25 -0
- package/packages/server/src/api/projects.js +8 -0
- package/packages/server/src/api/providers.js +1 -0
- package/packages/server/src/api/sessions-draft.js +1 -0
- package/packages/server/src/api/sessions-messages.js +6 -0
- package/packages/server/src/api/settings.js +52 -4
- package/packages/server/src/db/ConversationRepository.js +16 -3
- package/packages/server/src/db/ProjectDefaultsRepository.js +47 -37
- package/packages/server/src/db/ProviderRepository.js +62 -6
- package/packages/server/src/db/SessionRepository.js +74 -14
- package/packages/server/src/db/SettingsRepository.js +44 -16
- package/packages/server/src/db/conversation-helpers.js +1 -0
- package/packages/server/src/db/migrations/conversationsMigrations.js +4 -0
- package/packages/server/src/db/migrations/index.js +4 -0
- package/packages/server/src/db/migrations/miscMigrations.js +53 -3
- package/packages/server/src/db/migrations/sessionsMigrations.js +6 -1
- package/packages/server/src/db/session-helpers.js +8 -0
- package/packages/server/src/schema.sql +9 -0
- package/packages/server/src/services/agentCallLogger.js +1 -1
- package/packages/server/src/services/codexSpawnHelper.js +37 -0
- package/packages/server/src/services/commandButtonPrompts.js +48 -0
- package/packages/server/src/services/conversationContext.js +27 -0
- package/packages/server/src/services/draftSessionService.js +15 -2
- package/packages/server/src/services/kanbanTriggers.js +3 -0
- package/packages/server/src/services/providerTestService.js +115 -15
- package/packages/server/src/services/sessionAgentGuard.js +38 -0
- package/packages/server/src/services/sessionExecution.js +127 -33
- package/packages/server/src/services/sessionManager.js +45 -8
- package/packages/server/src/services/sessionPrompts.js +29 -0
- package/packages/server/src/services/sessionProvider.js +160 -41
- package/packages/server/src/services/streamEventCallbacks.js +72 -40
- package/packages/server/src/services/streamEventHandler.js +16 -2
- package/packages/server/src/services/streamUsageHandler.js +6 -0
- package/packages/server/src/services/summaryClaudeClient.js +37 -12
- package/packages/server/src/services/summaryModelClient.js +154 -0
- package/packages/server/src/services/summaryModelResolver.js +148 -0
- package/packages/server/src/services/summaryService.js +6 -4
- package/packages/server/src/services/templateTriggerService.js +2 -0
- package/packages/server/src/services/usageTracker.js +5 -1
- package/packages/server/src/services/visibleFinalErrorMessage.js +123 -0
- package/packages/shared/src/constants.js +1 -2
- package/packages/shared/src/contracts/projects.js +2 -0
- package/packages/shared/src/contracts/providers.js +24 -7
- package/packages/shared/src/contracts/sessions.js +1 -1
- package/packages/shared/src/index.js +1 -0
- package/packages/shared/src/types.js +28 -0
- package/packages/shared/src/utils.js +9 -17
- package/packages/web/dist/assets/ActiveSessionsView-UJsCILDL.js +1 -0
- package/packages/web/dist/assets/{AgentLogsView-DCF2WvP2.js → AgentLogsView-BGFPLjLa.js} +1 -1
- package/packages/web/dist/assets/ApiClient-B4YTtyY4.js +1 -0
- package/packages/web/dist/assets/{ArchiveConfirmModal-fgoEQhfq.js → ArchiveConfirmModal-OFaj_uX5.js} +1 -1
- package/packages/web/dist/assets/{CommandButtonDetailView-DAg07cDQ.js → CommandButtonDetailView-D8S258uP.js} +1 -1
- package/packages/web/dist/assets/EffortLevelSelector-C2378L8e.js +1 -0
- package/packages/web/dist/assets/{GeneralSettingsView-Cn9VI2du.js → GeneralSettingsView-DsHChEhv.js} +1 -1
- package/packages/web/dist/assets/{InputWithButton-BvboBGbz.js → InputWithButton-Ci15ox0a.js} +1 -1
- package/packages/web/dist/assets/{InterpolationHelp-0GoSBPgf.js → InterpolationHelp-CIkOSkWX.js} +1 -1
- package/packages/web/dist/assets/MarkdownEditor-5-bexzUT.js +2 -0
- package/packages/web/dist/assets/ModelSelector-BMpR0DPr.js +1 -0
- package/packages/web/dist/assets/{ModelSelector-DPPD-92R.css → ModelSelector-D8hbTRIt.css} +1 -1
- package/packages/web/dist/assets/{NewSessionView-C77YVqgY.js → NewSessionView-BCqtIgWH.js} +2 -2
- package/packages/web/dist/assets/{NewSessionView-D_Hi7M9g.css → NewSessionView-CUUdHkfv.css} +1 -1
- package/packages/web/dist/assets/ProjectEditView-D9sK0fdH.css +1 -0
- package/packages/web/dist/assets/ProjectEditView-RFaxHhAX.js +1 -0
- package/packages/web/dist/assets/{ProjectListView-CLwtuJ0J.js → ProjectListView-B9FuWESY.js} +1 -1
- package/packages/web/dist/assets/{ProjectNewView-CzDtVibO.js → ProjectNewView-D62jYlBL.js} +1 -1
- package/packages/web/dist/assets/ProvidersView-DDKMIQWZ.js +1 -0
- package/packages/web/dist/assets/ProvidersView-DE82G_5W.css +1 -0
- package/packages/web/dist/assets/QuickResponseSettings-CDm5vwP7.js +1 -0
- package/packages/web/dist/assets/{QuickResponsesPanel-DIBQFj0W.css → QuickResponsesPanel-BlFDvnZ2.css} +1 -1
- package/packages/web/dist/assets/{QuickResponsesPanel-CTXYjMF-.js → QuickResponsesPanel-DZ_Lre_l.js} +1 -1
- package/packages/web/dist/assets/{ResizableTextarea-Cw6aL4rp.js → ResizableTextarea-DiIOEGjN.js} +1 -1
- package/packages/web/dist/assets/ResizableTextarea-DsU3TVwF.css +1 -0
- package/packages/web/dist/assets/SessionCard-BMGC2HqI.css +1 -0
- package/packages/web/dist/assets/SessionCard-DmjnVYWn.js +1 -0
- package/packages/web/dist/assets/SessionDetailView-CL7nmfiB.js +36 -0
- package/packages/web/dist/assets/SessionDetailView-CupIkI7u.css +1 -0
- package/packages/web/dist/assets/SessionFormOptions-BpUALRKn.css +1 -0
- package/packages/web/dist/assets/SessionFormOptions-DYUISplS.js +1 -0
- package/packages/web/dist/assets/SessionListView-BcxGz4aC.js +1 -0
- package/packages/web/dist/assets/{SessionListView-DVhoZHN9.css → SessionListView-fHlQyecX.css} +1 -1
- package/packages/web/dist/assets/{SessionLogStream-DIndOyFR.js → SessionLogStream-DpUE6Xsh.js} +1 -1
- package/packages/web/dist/assets/{SettingsView-CmJ5JPd5.js → SettingsView-BC055tIA.js} +1 -1
- package/packages/web/dist/assets/SlashCommandWizard-DmTyNG9O.js +1 -0
- package/packages/web/dist/assets/SlashCommandWizard-Dn7sNaBd.css +1 -0
- package/packages/web/dist/assets/SummarySettingsView-BgnRCwlq.js +1 -0
- package/packages/web/dist/assets/SummarySettingsView-l2bxHmZZ.css +1 -0
- package/packages/web/dist/assets/TemplateDetailView-BlhOmLUX.js +1 -0
- package/packages/web/dist/assets/{commandButtons-D74TkPNU.js → commandButtons-D4RPpLiu.js} +1 -1
- package/packages/web/dist/assets/index-4rhEeO0B.js +1 -0
- package/packages/web/dist/assets/index-9vb2KaAd.js +1 -0
- package/packages/web/dist/assets/index-B0CvZXuN.js +7 -0
- package/packages/web/dist/assets/index-B6G18FqB.js +82 -0
- package/packages/web/dist/assets/{index-DMZZCi2u.js → index-BGwH4Cfn.js} +3 -3
- package/packages/web/dist/assets/index-BUhvkAdF.js +1 -0
- package/packages/web/dist/assets/index-BcnkUk2o.js +1 -0
- package/packages/web/dist/assets/{index-DQMHi05L.js → index-Bn5xdGFM.js} +2 -2
- package/packages/web/dist/assets/index-CNwkdB0T.js +1 -0
- package/packages/web/dist/assets/index-CfL84oGW.js +1 -0
- package/packages/web/dist/assets/index-CkmxO8Mm.js +1 -0
- package/packages/web/dist/assets/index-Cpy4-yv3.js +1 -0
- package/packages/web/dist/assets/index-CrAQJmoZ.js +1 -0
- package/packages/web/dist/assets/{index-gmCCsCQ1.css → index-Cs2nxhrT.css} +1 -1
- package/packages/web/dist/assets/index-D6Ky9vJe.js +3 -0
- package/packages/web/dist/assets/index-DfrE0gAC.js +1 -0
- package/packages/web/dist/assets/index-KwEyz0F3.js +1 -0
- package/packages/web/dist/assets/index-OfCywayk.js +1 -0
- package/packages/web/dist/assets/index-PDesaJc6.js +1 -0
- package/packages/web/dist/assets/index-uB6nhSvz.js +1 -0
- package/packages/web/dist/assets/{projects-D_C9dE9s.js → projects-BUiOGmmb.js} +1 -1
- package/packages/web/dist/assets/providers-Bh1ZiiJi.js +1 -0
- package/packages/web/dist/assets/sessions-DH1R-NhV.js +1 -0
- package/packages/web/dist/assets/settings-Z4AVVmkJ.js +1 -0
- package/packages/web/dist/index.html +2 -2
- package/packages/web/dist/assets/ActiveSessionsView-BafIafEu.js +0 -1
- package/packages/web/dist/assets/ApiClient-CcqJ-GAv.js +0 -1
- package/packages/web/dist/assets/EffortLevelSelector-xE3gidpq.js +0 -1
- package/packages/web/dist/assets/MarkdownEditor-HCKnwRye.js +0 -2
- package/packages/web/dist/assets/ModelSelector-B0RdlCHT.js +0 -1
- package/packages/web/dist/assets/ProjectEditView-BBHOsgBV.js +0 -1
- package/packages/web/dist/assets/ProjectEditView-CpeKj-_w.css +0 -1
- package/packages/web/dist/assets/ProvidersView-Eg93KbyC.js +0 -1
- package/packages/web/dist/assets/ProvidersView-uD8SKWpA.css +0 -1
- package/packages/web/dist/assets/QuickResponseSettings-BBHMapcA.js +0 -1
- package/packages/web/dist/assets/ResizableTextarea-B5nAA0RV.css +0 -1
- package/packages/web/dist/assets/SessionCard-CCapYVjy.js +0 -1
- package/packages/web/dist/assets/SessionCard-CcqIjL8q.css +0 -1
- package/packages/web/dist/assets/SessionDetailView-BL83oPiI.css +0 -1
- package/packages/web/dist/assets/SessionDetailView-CrZvMb3j.js +0 -36
- package/packages/web/dist/assets/SessionFormOptions-BuLlDF-7.css +0 -1
- package/packages/web/dist/assets/SessionFormOptions-Em7sQCGb.js +0 -1
- package/packages/web/dist/assets/SessionListView-3zdDtqhw.js +0 -1
- package/packages/web/dist/assets/SlashCommandWizard-BB30cSvo.css +0 -1
- package/packages/web/dist/assets/SlashCommandWizard-C_cSgF-P.js +0 -1
- package/packages/web/dist/assets/SummarySettingsView-DQM1n3bc.js +0 -1
- package/packages/web/dist/assets/SummarySettingsView-DcsmSVJI.css +0 -1
- package/packages/web/dist/assets/TemplateDetailView-B8clSBPk.js +0 -1
- package/packages/web/dist/assets/index-B5ocUoPf.js +0 -1
- package/packages/web/dist/assets/index-BELtFs3n.js +0 -1
- package/packages/web/dist/assets/index-BGAW2Nqa.js +0 -82
- package/packages/web/dist/assets/index-BsDR4w2c.js +0 -1
- package/packages/web/dist/assets/index-CVozYqQ-.js +0 -3
- package/packages/web/dist/assets/index-CefzeYRE.js +0 -1
- package/packages/web/dist/assets/index-CrLh8vw5.js +0 -1
- package/packages/web/dist/assets/index-DIvveuSK.js +0 -1
- package/packages/web/dist/assets/index-DPt6qBRK.js +0 -1
- package/packages/web/dist/assets/index-DYWZ8lD-.js +0 -1
- package/packages/web/dist/assets/index-DrlwE0Zo.js +0 -7
- package/packages/web/dist/assets/index-DuXChAe-.js +0 -1
- package/packages/web/dist/assets/index-Dz7jFUYU.js +0 -1
- package/packages/web/dist/assets/index-Gre8tUfC.js +0 -1
- package/packages/web/dist/assets/index-_Lv79l46.js +0 -1
- package/packages/web/dist/assets/index-f315nDFm.js +0 -1
- package/packages/web/dist/assets/index-rjbX81sm.js +0 -1
- package/packages/web/dist/assets/providers-BdvbPVdE.js +0 -1
- package/packages/web/dist/assets/sessions-Bs5FA6JZ.js +0 -1
- package/packages/web/dist/assets/settings-6Rw9xt-G.js +0 -1
|
@@ -3,71 +3,102 @@ import { broadcastToSession } from '../websocket.js';
|
|
|
3
3
|
import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
|
|
4
4
|
import * as summaryService from './summaryService.js';
|
|
5
5
|
import * as kanbanService from './kanbanService.js';
|
|
6
|
+
import { createVisibleFinalErrorMessage } from './visibleFinalErrorMessage.js';
|
|
6
7
|
import {
|
|
7
8
|
lastMessageIds,
|
|
8
9
|
activeSessions,
|
|
9
10
|
activeConversationIds,
|
|
11
|
+
finalErrorSessionIds,
|
|
10
12
|
associateAndBroadcastWorkLogs,
|
|
11
13
|
broadcastSessionStatus,
|
|
12
14
|
broadcastChangesUpdate,
|
|
13
15
|
} from './streamEventHandler.js';
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
|
-
*
|
|
17
|
-
* Encapsulates the duplicated completion block from runSession/continueSession/continueSessionWithExistingMessage
|
|
18
|
+
* Associate work logs with the last message and clean up tracking state.
|
|
18
19
|
* @param {string} sessionId
|
|
19
|
-
* @param {string} workingDirectory
|
|
20
|
-
* @param {{ handleTemplateTriggerIfNeeded?: Function, checkProactiveReschedule?: Function, handleAutoSendIfNeeded?: Function }} callbacks
|
|
21
20
|
*/
|
|
22
|
-
|
|
23
|
-
const { handleTemplateTriggerIfNeeded, checkProactiveReschedule: _checkProactiveReschedule, handleAutoSendIfNeeded } = callbacks;
|
|
24
|
-
// Associate work logs with the last message now that the turn is complete
|
|
21
|
+
function associateAndCleanupWorkLogs(sessionId) {
|
|
25
22
|
const lastMessageId = lastMessageIds.get(sessionId);
|
|
26
23
|
if (lastMessageId) {
|
|
27
24
|
associateAndBroadcastWorkLogs(sessionId, lastMessageId);
|
|
28
25
|
lastMessageIds.delete(sessionId);
|
|
29
26
|
}
|
|
27
|
+
}
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Handle post-turn activities for a session that's ready for follow-up.
|
|
31
|
+
* @param {string} sessionId
|
|
32
|
+
* @param {string} workingDirectory
|
|
33
|
+
* @param {{ checkProactiveReschedule?: Function, handleAutoSendIfNeeded?: Function, handleTemplateTriggerIfNeeded?: Function }} callbacks
|
|
34
|
+
* @returns {Promise<boolean>} True if rescheduled, false otherwise
|
|
35
|
+
*/
|
|
36
|
+
async function handleActiveSessionCompletion(sessionId, workingDirectory, callbacks) {
|
|
37
|
+
sessions.update(sessionId, { status: 'waiting', error: null });
|
|
38
|
+
broadcastSessionStatus(sessionId, 'waiting');
|
|
39
|
+
|
|
40
|
+
// Check if session should be proactively rescheduled based on token threshold
|
|
41
|
+
const { checkProactiveReschedule } = callbacks;
|
|
42
|
+
if (checkProactiveReschedule) {
|
|
43
|
+
const wasRescheduled = await checkProactiveReschedule(sessionId);
|
|
44
|
+
if (wasRescheduled) {
|
|
45
|
+
return true; // Session was rescheduled, don't continue with normal completion
|
|
43
46
|
}
|
|
47
|
+
}
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
// Extract PR URL immediately (lightweight, no API call)
|
|
50
|
+
summaryService.extractPrUrlIfNeeded(sessionId);
|
|
51
|
+
// Trigger debounced summary generation on turn completion (not complete yet)
|
|
52
|
+
summaryService.onSessionActivity(sessionId);
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
// Broadcast changes update when turn completes (real-time indicator)
|
|
55
|
+
const currentSession = sessions.getById(sessionId);
|
|
56
|
+
if (currentSession) {
|
|
57
|
+
await broadcastChangesUpdate(sessionId, currentSession.projectId, workingDirectory);
|
|
58
|
+
}
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
// Handle kanban lane movements based on targetLaneId
|
|
61
|
+
await kanbanService.handleTurnCompletion(sessionId);
|
|
58
62
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
// Auto-send queued prompt if enabled (runs BEFORE template trigger)
|
|
64
|
+
const { handleAutoSendIfNeeded, handleTemplateTriggerIfNeeded } = callbacks;
|
|
65
|
+
let autoSendFired = false;
|
|
66
|
+
if (handleAutoSendIfNeeded) {
|
|
67
|
+
autoSendFired = await handleAutoSendIfNeeded(sessionId);
|
|
68
|
+
}
|
|
64
69
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
+
// Only trigger next template if auto-send did NOT fire
|
|
71
|
+
// (if auto-send fired, template will trigger after that turn completes)
|
|
72
|
+
if (!autoSendFired && handleTemplateTriggerIfNeeded) {
|
|
73
|
+
await handleTemplateTriggerIfNeeded(sessionId);
|
|
70
74
|
}
|
|
75
|
+
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Handle post-turn completion logic (after stream loop ends successfully)
|
|
81
|
+
* Encapsulates the duplicated completion block from runSession/continueSession/continueSessionWithExistingMessage
|
|
82
|
+
* @param {string} sessionId
|
|
83
|
+
* @param {string} workingDirectory
|
|
84
|
+
* @param {{ handleTemplateTriggerIfNeeded?: Function, checkProactiveReschedule?: Function, handleAutoSendIfNeeded?: Function }} callbacks
|
|
85
|
+
*/
|
|
86
|
+
export async function handleTurnCompletion(sessionId, workingDirectory, callbacks = {}) {
|
|
87
|
+
// Associate work logs with the last message now that the turn is complete
|
|
88
|
+
associateAndCleanupWorkLogs(sessionId);
|
|
89
|
+
|
|
90
|
+
// Sessions with final errors should not transition to waiting
|
|
91
|
+
if (finalErrorSessionIds.has(sessionId)) {
|
|
92
|
+
finalErrorSessionIds.delete(sessionId);
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Session ready for follow-up - set to waiting instead of completed
|
|
97
|
+
const activeSession = activeSessions.get(sessionId);
|
|
98
|
+
if (activeSession && !activeSession.controller?.signal?.aborted) {
|
|
99
|
+
return handleActiveSessionCompletion(sessionId, workingDirectory, callbacks);
|
|
100
|
+
}
|
|
101
|
+
|
|
71
102
|
return false;
|
|
72
103
|
}
|
|
73
104
|
|
|
@@ -149,6 +180,7 @@ export async function handleSessionError(sessionId, error, options = {}) {
|
|
|
149
180
|
|
|
150
181
|
// Normal error handling (no reschedule or reschedule limits reached)
|
|
151
182
|
sessions.update(sessionId, { status: 'error', error: error.message });
|
|
183
|
+
createVisibleFinalErrorMessage(sessionId, error, activeConversationIds);
|
|
152
184
|
broadcastToSession(sessionId, WS_MESSAGE_TYPES.SESSION_ERROR, { sessionId, error: error.message });
|
|
153
185
|
|
|
154
186
|
// Optionally broadcast final conversation state (continueSession does this)
|
|
@@ -10,6 +10,10 @@ import {
|
|
|
10
10
|
handleTextDelta as _handleTextDelta,
|
|
11
11
|
handleResultUsage,
|
|
12
12
|
} from './streamUsageHandler.js';
|
|
13
|
+
import {
|
|
14
|
+
createVisibleFinalErrorMessage,
|
|
15
|
+
normalizeFinalErrorMessage,
|
|
16
|
+
} from './visibleFinalErrorMessage.js';
|
|
13
17
|
|
|
14
18
|
// ── Shared module-level state ──────────────────────────────────────────────
|
|
15
19
|
|
|
@@ -34,6 +38,9 @@ export const currentModels = new Map();
|
|
|
34
38
|
/** @type {Map<string, Set<string>>} Track tool_use IDs that have already been logged per session */
|
|
35
39
|
export const loggedToolUseIds = new Map();
|
|
36
40
|
|
|
41
|
+
/** @type {Set<string>} Track sessions that received a final result.error event */
|
|
42
|
+
export const finalErrorSessionIds = new Set();
|
|
43
|
+
|
|
37
44
|
// ── Helper functions ───────────────────────────────────────────────────────
|
|
38
45
|
|
|
39
46
|
/**
|
|
@@ -206,6 +213,9 @@ function handleAssistantTextContent(sessionId, textContent, toolUseBlocks) {
|
|
|
206
213
|
const currentModel = currentModels.get(sessionId) || null;
|
|
207
214
|
const message = messages.create(sessionId, 'assistant', textContent, { toolUse, conversationId, model: currentModel });
|
|
208
215
|
|
|
216
|
+
// Touch the session to update its updated_at timestamp so it sorts to the top
|
|
217
|
+
sessions.touch(sessionId);
|
|
218
|
+
|
|
209
219
|
// Associate pending work logs with this message immediately
|
|
210
220
|
// This ensures work logs are attached to the correct message, not just the last one
|
|
211
221
|
associateAndBroadcastWorkLogs(sessionId, message.id);
|
|
@@ -401,8 +411,11 @@ function handleResultEvent(sessionId, event) {
|
|
|
401
411
|
* @param {Object} event
|
|
402
412
|
*/
|
|
403
413
|
function handleResultError(sessionId, event) {
|
|
404
|
-
|
|
405
|
-
|
|
414
|
+
const errorMessage = normalizeFinalErrorMessage(event.error);
|
|
415
|
+
finalErrorSessionIds.add(sessionId);
|
|
416
|
+
sessions.update(sessionId, { status: 'error', error: errorMessage });
|
|
417
|
+
createVisibleFinalErrorMessage(sessionId, errorMessage, activeConversationIds);
|
|
418
|
+
broadcastToSession(sessionId, WS_MESSAGE_TYPES.SESSION_ERROR, { sessionId, error: errorMessage });
|
|
406
419
|
// Broadcast error status to project subscribers for session list updates
|
|
407
420
|
broadcastSessionStatus(sessionId, 'error');
|
|
408
421
|
// Extract PR URL before generating summary (PR may have been created before error)
|
|
@@ -478,6 +491,7 @@ export function cleanupSessionState(sessionId, includeConversationId = false) {
|
|
|
478
491
|
thinkingAccumulators.delete(sessionId);
|
|
479
492
|
currentModels.delete(sessionId);
|
|
480
493
|
loggedToolUseIds.delete(sessionId);
|
|
494
|
+
finalErrorSessionIds.delete(sessionId);
|
|
481
495
|
activeSessions.delete(sessionId);
|
|
482
496
|
if (includeConversationId) {
|
|
483
497
|
activeConversationIds.delete(sessionId);
|
|
@@ -74,6 +74,7 @@ export function handleTextDelta(sessionId, delta, textAccumulators) {
|
|
|
74
74
|
const turnData = currentTurnUsage.get(conversationId) || {
|
|
75
75
|
inputTokens: 0,
|
|
76
76
|
outputTokens: 0,
|
|
77
|
+
thinkingTokens: 0,
|
|
77
78
|
lastMessageOutput: 0,
|
|
78
79
|
cacheReadInputTokens: 0,
|
|
79
80
|
cacheCreationInputTokens: 0,
|
|
@@ -83,6 +84,7 @@ export function handleTextDelta(sessionId, delta, textAccumulators) {
|
|
|
83
84
|
const broadcastUsage = {
|
|
84
85
|
inputTokens: turnData.inputTokens,
|
|
85
86
|
outputTokens: turnData.outputTokens + Math.max(turnData.lastMessageOutput, newEstimate),
|
|
87
|
+
thinkingTokens: turnData.thinkingTokens,
|
|
86
88
|
cacheReadInputTokens: turnData.cacheReadInputTokens,
|
|
87
89
|
cacheCreationInputTokens: turnData.cacheCreationInputTokens,
|
|
88
90
|
};
|
|
@@ -123,6 +125,7 @@ export function extractTurnUsage(sessionId, event) {
|
|
|
123
125
|
return {
|
|
124
126
|
inputTokens: resolveTokenField(modelUsageEntry, event.usage, { camel: 'inputTokens', snake: 'input_tokens' }),
|
|
125
127
|
outputTokens: resolveTokenField(modelUsageEntry, event.usage, { camel: 'outputTokens', snake: 'output_tokens' }),
|
|
128
|
+
thinkingTokens: resolveTokenField(modelUsageEntry, event.usage, { camel: 'thinkingTokens', snake: 'thinking_tokens' }),
|
|
126
129
|
cacheReadInputTokens: resolveTokenField(modelUsageEntry, event.usage, { camel: 'cacheReadInputTokens', snake: 'cache_read_input_tokens' }),
|
|
127
130
|
cacheCreationInputTokens: resolveTokenField(modelUsageEntry, event.usage, { camel: 'cacheCreationInputTokens', snake: 'cache_creation_input_tokens' }),
|
|
128
131
|
webSearchRequests: modelUsageEntry?.webSearchRequests || 0,
|
|
@@ -142,6 +145,7 @@ export function buildCumulativeSessionUsage(sessionId, turnUsage) {
|
|
|
142
145
|
return {
|
|
143
146
|
inputTokens: (currentSession.inputTokens || 0) + turnUsage.inputTokens,
|
|
144
147
|
outputTokens: (currentSession.outputTokens || 0) + turnUsage.outputTokens,
|
|
148
|
+
thinkingTokens: (currentSession.thinkingTokens || 0) + turnUsage.thinkingTokens,
|
|
145
149
|
cacheReadInputTokens: (currentSession.cacheReadInputTokens || 0) + turnUsage.cacheReadInputTokens,
|
|
146
150
|
cacheCreationInputTokens: (currentSession.cacheCreationInputTokens || 0) + turnUsage.cacheCreationInputTokens,
|
|
147
151
|
webSearchRequests: (currentSession.webSearchRequests || 0) + turnUsage.webSearchRequests,
|
|
@@ -162,6 +166,7 @@ export function updateConversationUsage(conversationId, currentConversation, tur
|
|
|
162
166
|
const cumulativeConversationUsage = {
|
|
163
167
|
inputTokens: (currentConversation.inputTokens || 0) + turnUsage.inputTokens,
|
|
164
168
|
outputTokens: (currentConversation.outputTokens || 0) + turnUsage.outputTokens,
|
|
169
|
+
thinkingTokens: (currentConversation.thinkingTokens || 0) + turnUsage.thinkingTokens,
|
|
165
170
|
cacheReadInputTokens: (currentConversation.cacheReadInputTokens || 0) + turnUsage.cacheReadInputTokens,
|
|
166
171
|
cacheCreationInputTokens: (currentConversation.cacheCreationInputTokens || 0) + turnUsage.cacheCreationInputTokens,
|
|
167
172
|
webSearchRequests: (currentConversation.webSearchRequests || 0) + turnUsage.webSearchRequests,
|
|
@@ -197,6 +202,7 @@ export function handleResultUsage(sessionId, event) {
|
|
|
197
202
|
usage: updatedConversation ? {
|
|
198
203
|
inputTokens: updatedConversation.inputTokens,
|
|
199
204
|
outputTokens: updatedConversation.outputTokens,
|
|
205
|
+
thinkingTokens: updatedConversation.thinkingTokens,
|
|
200
206
|
cacheReadInputTokens: updatedConversation.cacheReadInputTokens,
|
|
201
207
|
cacheCreationInputTokens: updatedConversation.cacheCreationInputTokens,
|
|
202
208
|
webSearchRequests: updatedConversation.webSearchRequests,
|
|
@@ -49,11 +49,16 @@ function logResultUsage(callId, event) {
|
|
|
49
49
|
/**
|
|
50
50
|
* Build the query parameters for the Claude SDK call.
|
|
51
51
|
* @param {string} prompt - The prompt to send
|
|
52
|
-
* @param {{ systemPrompt?: string, jsonSchema?: Object }} options
|
|
52
|
+
* @param {{ systemPrompt?: string, jsonSchema?: Object, model?: string, env?: Object }} options
|
|
53
53
|
* @returns {Object} queryParams ready for the SDK query function
|
|
54
54
|
*/
|
|
55
55
|
function buildClaudeRequest(prompt, options) {
|
|
56
|
-
const {
|
|
56
|
+
const {
|
|
57
|
+
systemPrompt = null,
|
|
58
|
+
jsonSchema = null,
|
|
59
|
+
model = 'claude-haiku-4-5-20251001',
|
|
60
|
+
env = null,
|
|
61
|
+
} = options || {};
|
|
57
62
|
const schema = jsonSchema || SESSION_SUMMARY_SCHEMA;
|
|
58
63
|
|
|
59
64
|
return {
|
|
@@ -62,7 +67,8 @@ function buildClaudeRequest(prompt, options) {
|
|
|
62
67
|
cwd: process.cwd(),
|
|
63
68
|
permissionMode: 'bypassPermissions',
|
|
64
69
|
maxTurns: 1,
|
|
65
|
-
model
|
|
70
|
+
model,
|
|
71
|
+
...(env && { env }),
|
|
66
72
|
...(systemPrompt && { systemPrompt }),
|
|
67
73
|
outputFormat: {
|
|
68
74
|
type: 'json_schema',
|
|
@@ -108,6 +114,21 @@ async function handleClaudeResponse(eventStream, callId) {
|
|
|
108
114
|
return state.responseText;
|
|
109
115
|
}
|
|
110
116
|
|
|
117
|
+
function buildSummaryCallMetadata({ logMeta, model, providerId, selectionReason, promptLength }) {
|
|
118
|
+
return {
|
|
119
|
+
sessionId: logMeta.sessionId,
|
|
120
|
+
conversationId: logMeta.conversationId || null,
|
|
121
|
+
agentType: 'summary',
|
|
122
|
+
model,
|
|
123
|
+
callType: logMeta.callType,
|
|
124
|
+
promptLength,
|
|
125
|
+
metadata: {
|
|
126
|
+
...(providerId ? { providerId } : {}),
|
|
127
|
+
...(selectionReason ? { selectionReason } : {}),
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
111
132
|
export const SESSION_SUMMARY_SCHEMA = {
|
|
112
133
|
type: 'object',
|
|
113
134
|
properties: {
|
|
@@ -130,11 +151,16 @@ export const SESSION_SUMMARY_SCHEMA = {
|
|
|
130
151
|
* @param {string} prompt - The prompt to send
|
|
131
152
|
* @param {Array} recentMessages - Messages (for mock mode context)
|
|
132
153
|
* @param {string} sessionStatus - Session status (for mock mode context)
|
|
133
|
-
* @param {{ logMeta?: Object, systemPrompt?: string, jsonSchema?: Object }} options - Optional parameters
|
|
154
|
+
* @param {{ logMeta?: Object, systemPrompt?: string, jsonSchema?: Object, model?: string, env?: Object, providerId?: string|null, selectionReason?: string }} options - Optional parameters
|
|
134
155
|
* @returns {Promise<string>} The text response (JSON string)
|
|
135
156
|
*/
|
|
136
157
|
export async function callClaude(prompt, recentMessages, sessionStatus, options = {}) {
|
|
137
|
-
const {
|
|
158
|
+
const {
|
|
159
|
+
logMeta = null,
|
|
160
|
+
model = 'claude-haiku-4-5-20251001',
|
|
161
|
+
providerId = null,
|
|
162
|
+
selectionReason = null,
|
|
163
|
+
} = options || {};
|
|
138
164
|
// Build stable key for VCR cassette (session prompts are hardcoded strings in E2E tests)
|
|
139
165
|
let keyHint = null;
|
|
140
166
|
if (process.env.VCR_MODE && logMeta?.sessionId) {
|
|
@@ -151,14 +177,13 @@ export async function callClaude(prompt, recentMessages, sessionStatus, options
|
|
|
151
177
|
// Start logging if metadata provided
|
|
152
178
|
let callId = null;
|
|
153
179
|
if (logMeta) {
|
|
154
|
-
callId = agentCallLogger.startCall({
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
callType: logMeta.callType,
|
|
180
|
+
callId = agentCallLogger.startCall(buildSummaryCallMetadata({
|
|
181
|
+
logMeta,
|
|
182
|
+
model,
|
|
183
|
+
providerId,
|
|
184
|
+
selectionReason,
|
|
160
185
|
promptLength: prompt.length,
|
|
161
|
-
});
|
|
186
|
+
}));
|
|
162
187
|
}
|
|
163
188
|
|
|
164
189
|
try {
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import { callClaude, SESSION_SUMMARY_SCHEMA } from './summaryClaudeClient.js';
|
|
3
|
+
import { agentCallLogger } from './agentCallLogger.js';
|
|
4
|
+
import { buildProviderEnv } from './sessionProvider.js';
|
|
5
|
+
import { resolveSummaryModel } from './summaryModelResolver.js';
|
|
6
|
+
|
|
7
|
+
export { SESSION_SUMMARY_SCHEMA };
|
|
8
|
+
|
|
9
|
+
export async function callSummaryModel(prompt, recentMessages, sessionStatus, options = {}) {
|
|
10
|
+
const resolution = options.resolvedModel || resolveSummaryModel(options.summarySettings || {});
|
|
11
|
+
if (resolution.kind === 'openai') {
|
|
12
|
+
return callOpenAISummaryModel(prompt, resolution, options);
|
|
13
|
+
}
|
|
14
|
+
return callAnthropicSummaryModel({ prompt, recentMessages, sessionStatus, resolution, options });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function callAnthropicSummaryModel({ prompt, recentMessages, sessionStatus, resolution, options }) {
|
|
18
|
+
const providerEnv = resolution.provider && !resolution.provider.isBuiltIn
|
|
19
|
+
? { ...process.env, ...buildProviderEnv(resolution.provider) }
|
|
20
|
+
: null;
|
|
21
|
+
|
|
22
|
+
return callClaude(prompt, recentMessages, sessionStatus, {
|
|
23
|
+
...options,
|
|
24
|
+
model: resolution.model,
|
|
25
|
+
providerId: resolution.providerId,
|
|
26
|
+
selectionReason: resolution.selectionReason,
|
|
27
|
+
...(providerEnv ? { env: providerEnv } : {}),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function callOpenAISummaryModel(prompt, resolution, options) {
|
|
32
|
+
const { logMeta = null, systemPrompt = null, jsonSchema = null } = options || {};
|
|
33
|
+
const schema = jsonSchema || SESSION_SUMMARY_SCHEMA;
|
|
34
|
+
const provider = resolution.provider;
|
|
35
|
+
const client = createOpenAIClient(provider);
|
|
36
|
+
|
|
37
|
+
const callId = startOpenAISummaryLog(logMeta, resolution, prompt.length);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const response = await createOpenAIChatCompletion(client, {
|
|
41
|
+
model: resolution.model,
|
|
42
|
+
prompt,
|
|
43
|
+
systemPrompt,
|
|
44
|
+
schema,
|
|
45
|
+
structured: true,
|
|
46
|
+
}).catch(async (error) => {
|
|
47
|
+
if (!isStructuredOutputUnsupported(error)) throw error;
|
|
48
|
+
return createOpenAIChatCompletion(client, {
|
|
49
|
+
model: resolution.model,
|
|
50
|
+
prompt: withJsonOnlyInstruction(prompt, schema),
|
|
51
|
+
systemPrompt,
|
|
52
|
+
schema,
|
|
53
|
+
structured: false,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
logOpenAIUsage(callId, response.usage);
|
|
58
|
+
|
|
59
|
+
if (callId) {
|
|
60
|
+
agentCallLogger.completeCall(callId, { success: true });
|
|
61
|
+
}
|
|
62
|
+
return extractOpenAIContent(response);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (callId) {
|
|
65
|
+
agentCallLogger.completeCall(callId, { success: false, error });
|
|
66
|
+
}
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function startOpenAISummaryLog(logMeta, resolution, promptLength) {
|
|
72
|
+
if (!logMeta) return null;
|
|
73
|
+
return agentCallLogger.startCall({
|
|
74
|
+
sessionId: logMeta.sessionId,
|
|
75
|
+
conversationId: logMeta.conversationId || null,
|
|
76
|
+
agentType: 'summary',
|
|
77
|
+
model: resolution.model,
|
|
78
|
+
callType: logMeta.callType,
|
|
79
|
+
promptLength,
|
|
80
|
+
metadata: {
|
|
81
|
+
...(resolution.providerId ? { providerId: resolution.providerId } : {}),
|
|
82
|
+
...(resolution.selectionReason ? { selectionReason: resolution.selectionReason } : {}),
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function logOpenAIUsage(callId, usage) {
|
|
88
|
+
if (!callId || !usage) return;
|
|
89
|
+
agentCallLogger.updateUsage(callId, {
|
|
90
|
+
inputTokens: usage.prompt_tokens || 0,
|
|
91
|
+
outputTokens: usage.completion_tokens || 0,
|
|
92
|
+
thinkingTokens: 0,
|
|
93
|
+
cacheReadInputTokens: 0,
|
|
94
|
+
cacheCreationInputTokens: 0,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function createOpenAIClient(provider) {
|
|
99
|
+
const options = {
|
|
100
|
+
apiKey: provider?.authToken || process.env.OPENAI_API_KEY || 'missing',
|
|
101
|
+
};
|
|
102
|
+
if (provider?.baseUrl) options.baseURL = provider.baseUrl;
|
|
103
|
+
if (provider?.apiTimeoutMs) options.timeout = provider.apiTimeoutMs;
|
|
104
|
+
return new OpenAI(options);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function createOpenAIChatCompletion(client, { model, prompt, systemPrompt, schema, structured }) {
|
|
108
|
+
const messages = [];
|
|
109
|
+
if (systemPrompt) messages.push({ role: 'system', content: systemPrompt });
|
|
110
|
+
messages.push({ role: 'user', content: prompt });
|
|
111
|
+
|
|
112
|
+
const request = {
|
|
113
|
+
model,
|
|
114
|
+
messages,
|
|
115
|
+
temperature: 0,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (structured) {
|
|
119
|
+
request.response_format = {
|
|
120
|
+
type: 'json_schema',
|
|
121
|
+
json_schema: {
|
|
122
|
+
name: 'session_summary',
|
|
123
|
+
schema,
|
|
124
|
+
strict: false,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
} else {
|
|
128
|
+
request.response_format = { type: 'json_object' };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return client.chat.completions.create(request);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function extractOpenAIContent(response) {
|
|
135
|
+
const content = response?.choices?.[0]?.message?.content;
|
|
136
|
+
if (Array.isArray(content)) {
|
|
137
|
+
return content
|
|
138
|
+
.map((part) => part?.text || part?.content || '')
|
|
139
|
+
.join('');
|
|
140
|
+
}
|
|
141
|
+
return content || '';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isStructuredOutputUnsupported(error) {
|
|
145
|
+
const message = `${error?.message || ''} ${error?.code || ''} ${error?.type || ''}`.toLowerCase();
|
|
146
|
+
return [400, 404, 422].includes(error?.status)
|
|
147
|
+
|| message.includes('response_format')
|
|
148
|
+
|| message.includes('json_schema')
|
|
149
|
+
|| message.includes('unsupported');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function withJsonOnlyInstruction(prompt, schema) {
|
|
153
|
+
return `${prompt}\n\nReturn only a valid JSON object matching this JSON Schema:\n${JSON.stringify(schema)}`;
|
|
154
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { modelProviders, sessions } from '../database.js';
|
|
2
|
+
import { ACTIVITY_FIELDS_SQL } from '../db/session-helpers.js';
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_ANTHROPIC_SUMMARY_MODEL = 'claude-haiku-4-5-20251001';
|
|
5
|
+
export const DEFAULT_OPENAI_SUMMARY_MODEL = 'gpt-5.4-mini';
|
|
6
|
+
export const BUILT_IN_ANTHROPIC_PROVIDER_ID = 'anthropic-default';
|
|
7
|
+
export const BUILT_IN_OPENAI_PROVIDER_ID = 'openai-default';
|
|
8
|
+
|
|
9
|
+
export const CHEAPEST_SUMMARY_MODEL_BY_BUILT_IN_PROVIDER = Object.freeze({
|
|
10
|
+
[BUILT_IN_ANTHROPIC_PROVIDER_ID]: DEFAULT_ANTHROPIC_SUMMARY_MODEL,
|
|
11
|
+
[BUILT_IN_OPENAI_PROVIDER_ID]: DEFAULT_OPENAI_SUMMARY_MODEL,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const ANTHROPIC_TIER_NAMES = new Set(['sonnet', 'opus', 'haiku']);
|
|
15
|
+
|
|
16
|
+
export function isKnownBuiltInAnthropicModel(modelId) {
|
|
17
|
+
if (!modelId || typeof modelId !== 'string') return false;
|
|
18
|
+
if (ANTHROPIC_TIER_NAMES.has(modelId.toLowerCase())) return true;
|
|
19
|
+
if (modelId === DEFAULT_ANTHROPIC_SUMMARY_MODEL) return true;
|
|
20
|
+
|
|
21
|
+
const anthropicProvider = modelProviders.getById(BUILT_IN_ANTHROPIC_PROVIDER_ID);
|
|
22
|
+
return Boolean(
|
|
23
|
+
anthropicProvider?.models?.some((model) => model.modelId === modelId)
|
|
24
|
+
|| modelId.startsWith('claude-')
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function resolveSummaryModel(summarySettings = {}) {
|
|
29
|
+
const summaryModel = summarySettings?.summaryModel || '';
|
|
30
|
+
const summaryProviderId = summarySettings?.summaryProviderId || null;
|
|
31
|
+
|
|
32
|
+
if (summaryModel) {
|
|
33
|
+
return resolveExplicitSummaryModel(summaryModel, summaryProviderId);
|
|
34
|
+
}
|
|
35
|
+
if (summaryProviderId) {
|
|
36
|
+
throw new Error('summaryModel is required when summaryProviderId is set');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const recentFamily = findRecentBuiltInProviderFamily();
|
|
40
|
+
if (recentFamily === BUILT_IN_OPENAI_PROVIDER_ID) {
|
|
41
|
+
return {
|
|
42
|
+
model: DEFAULT_OPENAI_SUMMARY_MODEL,
|
|
43
|
+
provider: modelProviders.getById(BUILT_IN_OPENAI_PROVIDER_ID),
|
|
44
|
+
providerId: BUILT_IN_OPENAI_PROVIDER_ID,
|
|
45
|
+
kind: 'openai',
|
|
46
|
+
isDefault: true,
|
|
47
|
+
selectionReason: 'recent-built-in-provider',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (recentFamily === BUILT_IN_ANTHROPIC_PROVIDER_ID) {
|
|
52
|
+
return defaultAnthropicResolution('recent-built-in-provider');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return defaultAnthropicResolution('fallback');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveExplicitSummaryModel(summaryModel, summaryProviderId) {
|
|
59
|
+
if (!summaryProviderId) {
|
|
60
|
+
throw new Error('summaryProviderId is required when summaryModel is set');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const provider = modelProviders.getById(summaryProviderId);
|
|
64
|
+
if (!provider) {
|
|
65
|
+
throw new Error(`Summary provider not found: ${summaryProviderId}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const ownsModel = provider.models?.some((model) => model.modelId === summaryModel);
|
|
69
|
+
if (!ownsModel) {
|
|
70
|
+
throw new Error(`Summary provider ${summaryProviderId} does not own model ${summaryModel}`);
|
|
71
|
+
}
|
|
72
|
+
return providerResolution(summaryModel, provider, 'explicit');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function providerResolution(model, provider, selectionReason) {
|
|
76
|
+
const kind = provider.kind || 'anthropic';
|
|
77
|
+
return {
|
|
78
|
+
model,
|
|
79
|
+
provider,
|
|
80
|
+
providerId: provider.id,
|
|
81
|
+
kind,
|
|
82
|
+
isDefault: false,
|
|
83
|
+
selectionReason,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function defaultAnthropicResolution(selectionReason) {
|
|
88
|
+
return {
|
|
89
|
+
model: DEFAULT_ANTHROPIC_SUMMARY_MODEL,
|
|
90
|
+
provider: null,
|
|
91
|
+
providerId: null,
|
|
92
|
+
kind: 'anthropic',
|
|
93
|
+
isDefault: true,
|
|
94
|
+
selectionReason,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function findRecentBuiltInProviderFamily() {
|
|
99
|
+
for (const usage of getRecentSessionModelUsage()) {
|
|
100
|
+
const providerId = usage.providerId || null;
|
|
101
|
+
if (providerId) {
|
|
102
|
+
const provider = modelProviders.getById(providerId);
|
|
103
|
+
const family = builtInFamilyForProvider(provider);
|
|
104
|
+
if (family) return family;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const model = usage.model || null;
|
|
109
|
+
if (!model) continue;
|
|
110
|
+
|
|
111
|
+
const provider = modelProviders.getProviderByModelId(model);
|
|
112
|
+
const family = builtInFamilyForProvider(provider);
|
|
113
|
+
if (family) return family;
|
|
114
|
+
|
|
115
|
+
if (!provider && isKnownBuiltInAnthropicModel(model)) {
|
|
116
|
+
return BUILT_IN_ANTHROPIC_PROVIDER_ID;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function builtInFamilyForProvider(provider) {
|
|
123
|
+
if (!provider?.isBuiltIn) return null;
|
|
124
|
+
if (provider.id === BUILT_IN_OPENAI_PROVIDER_ID || provider.kind === 'openai') return BUILT_IN_OPENAI_PROVIDER_ID;
|
|
125
|
+
if (provider.id === BUILT_IN_ANTHROPIC_PROVIDER_ID || (provider.kind || 'anthropic') === 'anthropic') {
|
|
126
|
+
return BUILT_IN_ANTHROPIC_PROVIDER_ID;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function getRecentSessionModelUsage(limit = 50) {
|
|
132
|
+
const rows = sessions.db
|
|
133
|
+
.prepare(
|
|
134
|
+
`SELECT s.model, s.provider_id, ${ACTIVITY_FIELDS_SQL}
|
|
135
|
+
FROM sessions s
|
|
136
|
+
ORDER BY COALESCE(last_activity_at, s.updated_at, s.created_at) DESC,
|
|
137
|
+
s.updated_at DESC,
|
|
138
|
+
s.created_at DESC,
|
|
139
|
+
s.rowid DESC
|
|
140
|
+
LIMIT ?`
|
|
141
|
+
)
|
|
142
|
+
.all(limit);
|
|
143
|
+
|
|
144
|
+
return rows.map((row) => ({
|
|
145
|
+
model: row.model || null,
|
|
146
|
+
providerId: row.provider_id || null,
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { sessions, messages, sessionSummaries, projects, settings } from '../database.js';
|
|
15
15
|
import { createConcurrencyGuard } from './withConcurrencyGuard.js';
|
|
16
|
-
import {
|
|
16
|
+
import { callSummaryModel } from './summaryModelClient.js';
|
|
17
17
|
import {
|
|
18
18
|
MAX_MESSAGES,
|
|
19
19
|
MIN_MESSAGES_FOR_SUMMARY,
|
|
@@ -299,10 +299,11 @@ async function _doGenerateSummary(sessionId, retryCount = 0, force = false, user
|
|
|
299
299
|
// Build conversation context and prompt
|
|
300
300
|
const { prompt } = fetchConversationContext(sessionId, { existingSummary, recentMessages, session, globalSettings });
|
|
301
301
|
|
|
302
|
-
// Call
|
|
303
|
-
const responseText = await
|
|
302
|
+
// Call the configured summary model.
|
|
303
|
+
const responseText = await callSummaryModel(prompt, recentMessages, session.status, {
|
|
304
304
|
logMeta: { sessionId, callType: 'generateSessionSummary' },
|
|
305
305
|
systemPrompt: SUMMARY_SYSTEM_PROMPT,
|
|
306
|
+
summarySettings: globalSettings,
|
|
306
307
|
});
|
|
307
308
|
|
|
308
309
|
// Parse response and retry if needed
|
|
@@ -536,7 +537,8 @@ export {
|
|
|
536
537
|
SUMMARY_SYSTEM_PROMPT, formatMessages, buildIncrementalPrompt, parseSummaryResponse,
|
|
537
538
|
stripMarkdownCodeBlock as _stripMarkdownCodeBlock, trackMessageMetadata as _trackMessageMetadata,
|
|
538
539
|
};
|
|
539
|
-
export {
|
|
540
|
+
export { callSummaryModel };
|
|
541
|
+
export { callClaude } from './summaryClaudeClient.js';
|
|
540
542
|
export { parsePrUrl, validatePrUrl, extractPrUrlIfNeeded, enrichPrData as _enrichPrData };
|
|
541
543
|
export { getChildSessions, buildChildSessionContext, aggregateFilesModified };
|
|
542
544
|
|