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.
Files changed (169) hide show
  1. package/package.json +2 -1
  2. package/packages/server/src/agents/AgentGateway.js +36 -3
  3. package/packages/server/src/agents/BaseAgent.js +15 -1
  4. package/packages/server/src/agents/LoggingAgentWrapper.js +4 -0
  5. package/packages/server/src/agents/adapters/ClaudeCodeAdapter.js +9 -6
  6. package/packages/server/src/agents/adapters/CodexAdapter.js +262 -14
  7. package/packages/server/src/agents/adapters/codexCliRunner.js +185 -0
  8. package/packages/server/src/agents/adapters/codexEventMapper.js +235 -0
  9. package/packages/server/src/agents/types.js +1 -0
  10. package/packages/server/src/agents/vcr/VCRAgentAdapter.js +8 -0
  11. package/packages/server/src/api/agents.js +27 -0
  12. package/packages/server/src/api/canvas.js +20 -0
  13. package/packages/server/src/api/index.js +2 -0
  14. package/packages/server/src/api/projects-session-helpers.js +25 -0
  15. package/packages/server/src/api/projects.js +8 -0
  16. package/packages/server/src/api/providers.js +1 -0
  17. package/packages/server/src/api/sessions-draft.js +1 -0
  18. package/packages/server/src/api/sessions-messages.js +6 -0
  19. package/packages/server/src/api/settings.js +52 -4
  20. package/packages/server/src/db/ConversationRepository.js +16 -3
  21. package/packages/server/src/db/ProjectDefaultsRepository.js +47 -37
  22. package/packages/server/src/db/ProviderRepository.js +62 -6
  23. package/packages/server/src/db/SessionRepository.js +74 -14
  24. package/packages/server/src/db/SettingsRepository.js +44 -16
  25. package/packages/server/src/db/conversation-helpers.js +1 -0
  26. package/packages/server/src/db/migrations/conversationsMigrations.js +4 -0
  27. package/packages/server/src/db/migrations/index.js +4 -0
  28. package/packages/server/src/db/migrations/miscMigrations.js +53 -3
  29. package/packages/server/src/db/migrations/sessionsMigrations.js +6 -1
  30. package/packages/server/src/db/session-helpers.js +8 -0
  31. package/packages/server/src/schema.sql +9 -0
  32. package/packages/server/src/services/agentCallLogger.js +1 -1
  33. package/packages/server/src/services/codexSpawnHelper.js +37 -0
  34. package/packages/server/src/services/commandButtonPrompts.js +48 -0
  35. package/packages/server/src/services/conversationContext.js +27 -0
  36. package/packages/server/src/services/draftSessionService.js +15 -2
  37. package/packages/server/src/services/kanbanTriggers.js +3 -0
  38. package/packages/server/src/services/providerTestService.js +115 -15
  39. package/packages/server/src/services/sessionAgentGuard.js +38 -0
  40. package/packages/server/src/services/sessionExecution.js +127 -33
  41. package/packages/server/src/services/sessionManager.js +45 -8
  42. package/packages/server/src/services/sessionPrompts.js +29 -0
  43. package/packages/server/src/services/sessionProvider.js +160 -41
  44. package/packages/server/src/services/streamEventCallbacks.js +72 -40
  45. package/packages/server/src/services/streamEventHandler.js +16 -2
  46. package/packages/server/src/services/streamUsageHandler.js +6 -0
  47. package/packages/server/src/services/summaryClaudeClient.js +37 -12
  48. package/packages/server/src/services/summaryModelClient.js +154 -0
  49. package/packages/server/src/services/summaryModelResolver.js +148 -0
  50. package/packages/server/src/services/summaryService.js +6 -4
  51. package/packages/server/src/services/templateTriggerService.js +2 -0
  52. package/packages/server/src/services/usageTracker.js +5 -1
  53. package/packages/server/src/services/visibleFinalErrorMessage.js +123 -0
  54. package/packages/shared/src/constants.js +1 -2
  55. package/packages/shared/src/contracts/projects.js +2 -0
  56. package/packages/shared/src/contracts/providers.js +24 -7
  57. package/packages/shared/src/contracts/sessions.js +1 -1
  58. package/packages/shared/src/index.js +1 -0
  59. package/packages/shared/src/types.js +28 -0
  60. package/packages/shared/src/utils.js +9 -17
  61. package/packages/web/dist/assets/ActiveSessionsView-UJsCILDL.js +1 -0
  62. package/packages/web/dist/assets/{AgentLogsView-DCF2WvP2.js → AgentLogsView-BGFPLjLa.js} +1 -1
  63. package/packages/web/dist/assets/ApiClient-B4YTtyY4.js +1 -0
  64. package/packages/web/dist/assets/{ArchiveConfirmModal-fgoEQhfq.js → ArchiveConfirmModal-OFaj_uX5.js} +1 -1
  65. package/packages/web/dist/assets/{CommandButtonDetailView-DAg07cDQ.js → CommandButtonDetailView-D8S258uP.js} +1 -1
  66. package/packages/web/dist/assets/EffortLevelSelector-C2378L8e.js +1 -0
  67. package/packages/web/dist/assets/{GeneralSettingsView-Cn9VI2du.js → GeneralSettingsView-DsHChEhv.js} +1 -1
  68. package/packages/web/dist/assets/{InputWithButton-BvboBGbz.js → InputWithButton-Ci15ox0a.js} +1 -1
  69. package/packages/web/dist/assets/{InterpolationHelp-0GoSBPgf.js → InterpolationHelp-CIkOSkWX.js} +1 -1
  70. package/packages/web/dist/assets/MarkdownEditor-5-bexzUT.js +2 -0
  71. package/packages/web/dist/assets/ModelSelector-BMpR0DPr.js +1 -0
  72. package/packages/web/dist/assets/{ModelSelector-DPPD-92R.css → ModelSelector-D8hbTRIt.css} +1 -1
  73. package/packages/web/dist/assets/{NewSessionView-C77YVqgY.js → NewSessionView-BCqtIgWH.js} +2 -2
  74. package/packages/web/dist/assets/{NewSessionView-D_Hi7M9g.css → NewSessionView-CUUdHkfv.css} +1 -1
  75. package/packages/web/dist/assets/ProjectEditView-D9sK0fdH.css +1 -0
  76. package/packages/web/dist/assets/ProjectEditView-RFaxHhAX.js +1 -0
  77. package/packages/web/dist/assets/{ProjectListView-CLwtuJ0J.js → ProjectListView-B9FuWESY.js} +1 -1
  78. package/packages/web/dist/assets/{ProjectNewView-CzDtVibO.js → ProjectNewView-D62jYlBL.js} +1 -1
  79. package/packages/web/dist/assets/ProvidersView-DDKMIQWZ.js +1 -0
  80. package/packages/web/dist/assets/ProvidersView-DE82G_5W.css +1 -0
  81. package/packages/web/dist/assets/QuickResponseSettings-CDm5vwP7.js +1 -0
  82. package/packages/web/dist/assets/{QuickResponsesPanel-DIBQFj0W.css → QuickResponsesPanel-BlFDvnZ2.css} +1 -1
  83. package/packages/web/dist/assets/{QuickResponsesPanel-CTXYjMF-.js → QuickResponsesPanel-DZ_Lre_l.js} +1 -1
  84. package/packages/web/dist/assets/{ResizableTextarea-Cw6aL4rp.js → ResizableTextarea-DiIOEGjN.js} +1 -1
  85. package/packages/web/dist/assets/ResizableTextarea-DsU3TVwF.css +1 -0
  86. package/packages/web/dist/assets/SessionCard-BMGC2HqI.css +1 -0
  87. package/packages/web/dist/assets/SessionCard-DmjnVYWn.js +1 -0
  88. package/packages/web/dist/assets/SessionDetailView-CL7nmfiB.js +36 -0
  89. package/packages/web/dist/assets/SessionDetailView-CupIkI7u.css +1 -0
  90. package/packages/web/dist/assets/SessionFormOptions-BpUALRKn.css +1 -0
  91. package/packages/web/dist/assets/SessionFormOptions-DYUISplS.js +1 -0
  92. package/packages/web/dist/assets/SessionListView-BcxGz4aC.js +1 -0
  93. package/packages/web/dist/assets/{SessionListView-DVhoZHN9.css → SessionListView-fHlQyecX.css} +1 -1
  94. package/packages/web/dist/assets/{SessionLogStream-DIndOyFR.js → SessionLogStream-DpUE6Xsh.js} +1 -1
  95. package/packages/web/dist/assets/{SettingsView-CmJ5JPd5.js → SettingsView-BC055tIA.js} +1 -1
  96. package/packages/web/dist/assets/SlashCommandWizard-DmTyNG9O.js +1 -0
  97. package/packages/web/dist/assets/SlashCommandWizard-Dn7sNaBd.css +1 -0
  98. package/packages/web/dist/assets/SummarySettingsView-BgnRCwlq.js +1 -0
  99. package/packages/web/dist/assets/SummarySettingsView-l2bxHmZZ.css +1 -0
  100. package/packages/web/dist/assets/TemplateDetailView-BlhOmLUX.js +1 -0
  101. package/packages/web/dist/assets/{commandButtons-D74TkPNU.js → commandButtons-D4RPpLiu.js} +1 -1
  102. package/packages/web/dist/assets/index-4rhEeO0B.js +1 -0
  103. package/packages/web/dist/assets/index-9vb2KaAd.js +1 -0
  104. package/packages/web/dist/assets/index-B0CvZXuN.js +7 -0
  105. package/packages/web/dist/assets/index-B6G18FqB.js +82 -0
  106. package/packages/web/dist/assets/{index-DMZZCi2u.js → index-BGwH4Cfn.js} +3 -3
  107. package/packages/web/dist/assets/index-BUhvkAdF.js +1 -0
  108. package/packages/web/dist/assets/index-BcnkUk2o.js +1 -0
  109. package/packages/web/dist/assets/{index-DQMHi05L.js → index-Bn5xdGFM.js} +2 -2
  110. package/packages/web/dist/assets/index-CNwkdB0T.js +1 -0
  111. package/packages/web/dist/assets/index-CfL84oGW.js +1 -0
  112. package/packages/web/dist/assets/index-CkmxO8Mm.js +1 -0
  113. package/packages/web/dist/assets/index-Cpy4-yv3.js +1 -0
  114. package/packages/web/dist/assets/index-CrAQJmoZ.js +1 -0
  115. package/packages/web/dist/assets/{index-gmCCsCQ1.css → index-Cs2nxhrT.css} +1 -1
  116. package/packages/web/dist/assets/index-D6Ky9vJe.js +3 -0
  117. package/packages/web/dist/assets/index-DfrE0gAC.js +1 -0
  118. package/packages/web/dist/assets/index-KwEyz0F3.js +1 -0
  119. package/packages/web/dist/assets/index-OfCywayk.js +1 -0
  120. package/packages/web/dist/assets/index-PDesaJc6.js +1 -0
  121. package/packages/web/dist/assets/index-uB6nhSvz.js +1 -0
  122. package/packages/web/dist/assets/{projects-D_C9dE9s.js → projects-BUiOGmmb.js} +1 -1
  123. package/packages/web/dist/assets/providers-Bh1ZiiJi.js +1 -0
  124. package/packages/web/dist/assets/sessions-DH1R-NhV.js +1 -0
  125. package/packages/web/dist/assets/settings-Z4AVVmkJ.js +1 -0
  126. package/packages/web/dist/index.html +2 -2
  127. package/packages/web/dist/assets/ActiveSessionsView-BafIafEu.js +0 -1
  128. package/packages/web/dist/assets/ApiClient-CcqJ-GAv.js +0 -1
  129. package/packages/web/dist/assets/EffortLevelSelector-xE3gidpq.js +0 -1
  130. package/packages/web/dist/assets/MarkdownEditor-HCKnwRye.js +0 -2
  131. package/packages/web/dist/assets/ModelSelector-B0RdlCHT.js +0 -1
  132. package/packages/web/dist/assets/ProjectEditView-BBHOsgBV.js +0 -1
  133. package/packages/web/dist/assets/ProjectEditView-CpeKj-_w.css +0 -1
  134. package/packages/web/dist/assets/ProvidersView-Eg93KbyC.js +0 -1
  135. package/packages/web/dist/assets/ProvidersView-uD8SKWpA.css +0 -1
  136. package/packages/web/dist/assets/QuickResponseSettings-BBHMapcA.js +0 -1
  137. package/packages/web/dist/assets/ResizableTextarea-B5nAA0RV.css +0 -1
  138. package/packages/web/dist/assets/SessionCard-CCapYVjy.js +0 -1
  139. package/packages/web/dist/assets/SessionCard-CcqIjL8q.css +0 -1
  140. package/packages/web/dist/assets/SessionDetailView-BL83oPiI.css +0 -1
  141. package/packages/web/dist/assets/SessionDetailView-CrZvMb3j.js +0 -36
  142. package/packages/web/dist/assets/SessionFormOptions-BuLlDF-7.css +0 -1
  143. package/packages/web/dist/assets/SessionFormOptions-Em7sQCGb.js +0 -1
  144. package/packages/web/dist/assets/SessionListView-3zdDtqhw.js +0 -1
  145. package/packages/web/dist/assets/SlashCommandWizard-BB30cSvo.css +0 -1
  146. package/packages/web/dist/assets/SlashCommandWizard-C_cSgF-P.js +0 -1
  147. package/packages/web/dist/assets/SummarySettingsView-DQM1n3bc.js +0 -1
  148. package/packages/web/dist/assets/SummarySettingsView-DcsmSVJI.css +0 -1
  149. package/packages/web/dist/assets/TemplateDetailView-B8clSBPk.js +0 -1
  150. package/packages/web/dist/assets/index-B5ocUoPf.js +0 -1
  151. package/packages/web/dist/assets/index-BELtFs3n.js +0 -1
  152. package/packages/web/dist/assets/index-BGAW2Nqa.js +0 -82
  153. package/packages/web/dist/assets/index-BsDR4w2c.js +0 -1
  154. package/packages/web/dist/assets/index-CVozYqQ-.js +0 -3
  155. package/packages/web/dist/assets/index-CefzeYRE.js +0 -1
  156. package/packages/web/dist/assets/index-CrLh8vw5.js +0 -1
  157. package/packages/web/dist/assets/index-DIvveuSK.js +0 -1
  158. package/packages/web/dist/assets/index-DPt6qBRK.js +0 -1
  159. package/packages/web/dist/assets/index-DYWZ8lD-.js +0 -1
  160. package/packages/web/dist/assets/index-DrlwE0Zo.js +0 -7
  161. package/packages/web/dist/assets/index-DuXChAe-.js +0 -1
  162. package/packages/web/dist/assets/index-Dz7jFUYU.js +0 -1
  163. package/packages/web/dist/assets/index-Gre8tUfC.js +0 -1
  164. package/packages/web/dist/assets/index-_Lv79l46.js +0 -1
  165. package/packages/web/dist/assets/index-f315nDFm.js +0 -1
  166. package/packages/web/dist/assets/index-rjbX81sm.js +0 -1
  167. package/packages/web/dist/assets/providers-BdvbPVdE.js +0 -1
  168. package/packages/web/dist/assets/sessions-Bs5FA6JZ.js +0 -1
  169. package/packages/web/dist/assets/settings-6Rw9xt-G.js +0 -1
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Codex event mapper.
3
+ *
4
+ * Translates the real Codex CLI JSON-line event schema (as emitted by
5
+ * `codex exec --json`) into the normalized SDK-shaped events that Circus
6
+ * Chief's stream event handler already understands for Claude Code:
7
+ *
8
+ * - {@code system(init)}
9
+ * - {@code stream_event(content_block_delta)}
10
+ * - {@code assistant}
11
+ * - {@code result(success, usage)}
12
+ *
13
+ * Real Codex event types (v0.124.0, confirmed via
14
+ * `codex app-server generate-json-schema`):
15
+ *
16
+ * - {@code thread.started} — { thread_id }
17
+ * - {@code turn.started} — no payload
18
+ * - {@code item.started} — { item: ThreadItem }
19
+ * - {@code item.completed} — { item: ThreadItem }
20
+ * - {@code turn.completed} — { usage: { input_tokens, cached_input_tokens, output_tokens } }
21
+ * - {@code turn.failed} — { error: { message } }
22
+ * - {@code error} — { message } (transient, treated as fatal)
23
+ *
24
+ * ThreadItem.type variants handled in v1:
25
+ * - {@code agent_message} — { id, text, ... } → emitted as text_delta + assistant
26
+ * - {@code command_execution} — { id, command, aggregated_output, exit_code, status } → tool_result
27
+ * - {@code file_change} — { id, changes: [{path, kind}] } → tool_result
28
+ * - {@code reasoning} — { id, text } or legacy { content: [{type, text}] } → tool_result
29
+ *
30
+ * All other variants (mcp_tool_call,
31
+ * dynamic_tool_call, collab_agent_tool_call, web_search, image_view, image_generation,
32
+ * plan, context_compaction, hook_prompt, entered_review_mode, exited_review_mode,
33
+ * user_message) are currently ignored.
34
+ *
35
+ * The mapper is stateful across calls so it can accumulate agent message
36
+ * text across multiple `item.completed` events within a turn and stash
37
+ * usage counters until the terminal `turn.completed`.
38
+ *
39
+ * Pure in-process — no I/O, no timers, no child processes.
40
+ *
41
+ * @param {Object} [options]
42
+ * @param {string} [options.model] - Optional model name to surface in the
43
+ * {@code system(init)} event. Codex's {@code thread.started} event does
44
+ * not carry the model, so the adapter must pass it in.
45
+ * @returns {{
46
+ * map: (codexEvent: Object) => Array<Object>,
47
+ * reset: () => void,
48
+ * finalize: () => Array<Object>
49
+ * }}
50
+ */
51
+ export function createCodexEventMapper({ model } = {}) {
52
+ const mapperState = new MapperState();
53
+ const warnedUnknownItemTypes = new Set();
54
+
55
+ const handlers = {
56
+ 'thread.started': (evt) => handleThreadStarted(evt, model),
57
+ 'turn.started': () => [],
58
+ 'item.started': () => [],
59
+ 'item.completed': (evt) => handleItemCompleted(evt, mapperState, warnedUnknownItemTypes),
60
+ 'turn.completed': (evt) => mapperState.onTurnCompleted(evt),
61
+ 'turn.failed': (evt) => handleTurnFailed(evt),
62
+ 'error': (evt) => handleError(evt),
63
+ };
64
+
65
+ function map(codexEvent) {
66
+ if (!codexEvent || typeof codexEvent !== 'object') return [];
67
+ const handler = handlers[codexEvent.type];
68
+ if (!handler) {
69
+ console.warn(`[codexEventMapper] Unknown Codex event type: "${codexEvent.type}"`);
70
+ return [];
71
+ }
72
+ return handler(codexEvent);
73
+ }
74
+
75
+ return {
76
+ map,
77
+ reset: () => mapperState.reset(),
78
+ finalize: () => mapperState.finalize(),
79
+ };
80
+ }
81
+
82
+ // --- Mapper state class ----------------------------------------------------
83
+
84
+ class MapperState {
85
+ constructor() {
86
+ this.reset();
87
+ }
88
+
89
+ reset() {
90
+ this.lastUsage = null;
91
+ this.terminated = false;
92
+ }
93
+
94
+ /**
95
+ * Called by the adapter when the underlying stream ends without an explicit
96
+ * {@code turn.completed}. Returns a terminal result event if one hasn't been
97
+ * emitted yet; otherwise an empty array.
98
+ */
99
+ finalize() {
100
+ if (this.terminated) return [];
101
+ this.terminated = true;
102
+ return [this.buildResultEvent()];
103
+ }
104
+
105
+ onTurnCompleted(evt) {
106
+ if (evt && evt.usage) {
107
+ this.lastUsage = {
108
+ input_tokens: evt.usage.input_tokens,
109
+ output_tokens: evt.usage.output_tokens,
110
+ };
111
+ }
112
+ this.terminated = true;
113
+ return [this.buildResultEvent()];
114
+ }
115
+
116
+ buildResultEvent() {
117
+ const usage = this.lastUsage || { input_tokens: 0, output_tokens: 0 };
118
+ return {
119
+ type: 'result',
120
+ subtype: 'success',
121
+ usage: {
122
+ input_tokens: usage.input_tokens || 0,
123
+ output_tokens: usage.output_tokens || 0,
124
+ },
125
+ };
126
+ }
127
+ }
128
+
129
+ // --- Pure event handlers ---------------------------------------------------
130
+
131
+ function handleThreadStarted(evt, model) {
132
+ const init = {
133
+ type: 'system',
134
+ subtype: 'init',
135
+ session_id: evt.thread_id,
136
+ };
137
+ if (model) init.model = model;
138
+ return [init];
139
+ }
140
+
141
+ function handleItemCompleted(evt, _state, warnedTypes) {
142
+ const item = evt.item;
143
+ if (!item || typeof item !== 'object') return [];
144
+
145
+ if (isAgentMessageItem(item)) {
146
+ const text = typeof item.text === 'string' ? item.text : '';
147
+ return [
148
+ {
149
+ type: 'stream_event',
150
+ event: {
151
+ type: 'content_block_delta',
152
+ delta: { type: 'text_delta', text },
153
+ },
154
+ },
155
+ {
156
+ type: 'assistant',
157
+ message: { content: [{ type: 'text', text }] },
158
+ },
159
+ ];
160
+ }
161
+
162
+ if (item.type === 'command_execution') {
163
+ return [mapCommandExecution(item)];
164
+ }
165
+
166
+ if (item.type === 'file_change') {
167
+ return [mapFileChange(item)];
168
+ }
169
+
170
+ if (item.type === 'reasoning') {
171
+ return [mapReasoning(item)];
172
+ }
173
+
174
+ // Unknown types — warn once per type
175
+ if (item.type && !warnedTypes.has(item.type)) {
176
+ warnedTypes.add(item.type);
177
+ console.warn(`[codexEventMapper] Ignoring unsupported item.type "${item.type}"`);
178
+ }
179
+ return [];
180
+ }
181
+
182
+ function isAgentMessageItem(item) {
183
+ return item.type === 'agent_message' || item.type === 'agentMessage';
184
+ }
185
+
186
+ function handleTurnFailed(evt) {
187
+ const message = evt?.error?.message || 'Codex turn failed';
188
+ throw new Error(message);
189
+ }
190
+
191
+ function handleError(evt) {
192
+ const message = evt?.message || 'Codex error';
193
+ throw new Error(message);
194
+ }
195
+
196
+ // --- Tool-use mapping helpers -----------------------------------------------
197
+
198
+ function mapCommandExecution(item) {
199
+ const cmd = item.command || '';
200
+ const parts = [`$ ${cmd}`];
201
+ if (item.exit_code !== undefined && item.exit_code !== 0) {
202
+ parts.push(`exit code: ${item.exit_code}`);
203
+ }
204
+ if (item.aggregated_output) parts.push(item.aggregated_output);
205
+ return {
206
+ type: 'tool_result',
207
+ tool_name: 'command_execution',
208
+ content: parts.join('\n'),
209
+ };
210
+ }
211
+
212
+ function mapFileChange(item) {
213
+ const changes = Array.isArray(item.changes)
214
+ ? item.changes.map((c) => `${c.kind || 'change'}: ${c.path || 'unknown'}`).join('\n')
215
+ : 'unknown file change';
216
+ return {
217
+ type: 'tool_result',
218
+ tool_name: 'file_change',
219
+ content: changes,
220
+ };
221
+ }
222
+
223
+ function mapReasoning(item) {
224
+ // v0.124.0 shape: plain `text` string (e.g. gpt-5.2)
225
+ // Legacy shape: `content` array of {type, text} objects
226
+ const text = item.text
227
+ || (Array.isArray(item.content)
228
+ ? item.content.map((c) => c.text || '').join('\n')
229
+ : '');
230
+ return {
231
+ type: 'tool_result',
232
+ tool_name: 'reasoning',
233
+ content: text,
234
+ };
235
+ }
@@ -24,6 +24,7 @@
24
24
  * @property {Object} env - Environment variables
25
25
  * @property {Function} spawnClaudeCodeProcess - Process spawner function
26
26
  * @property {string} [model] - Model to use
27
+ * @property {string|null} [effortLevel] - Reasoning effort override, or null/auto for provider default
27
28
  * @property {string} systemPrompt - System prompt string
28
29
  */
29
30
 
@@ -116,6 +116,14 @@ export class VCRAgentAdapter {
116
116
  return this.innerAgent.supportsResume?.() ?? false;
117
117
  }
118
118
 
119
+ /**
120
+ * Proxy conversation context need to inner agent
121
+ * @returns {boolean}
122
+ */
123
+ needsConversationContext() {
124
+ return this.innerAgent.needsConversationContext?.() ?? true;
125
+ }
126
+
119
127
  /**
120
128
  * Proxy capabilities to inner agent
121
129
  * @returns {object}
@@ -0,0 +1,27 @@
1
+ import { Router } from 'express';
2
+ import { agentGateway } from '../agents/AgentGateway.js';
3
+
4
+ const router = Router();
5
+
6
+ /**
7
+ * GET /api/agents
8
+ *
9
+ * Returns the capabilities of every registered agent adapter, sourced from the
10
+ * adapter's static `capabilities` field (no adapter instantiation).
11
+ *
12
+ * Response shape:
13
+ * [
14
+ * { agentType: 'claude-code', capabilities: { streaming, thinking, reasoningEffort, toolUse, resume } },
15
+ * { agentType: 'codex', capabilities: { streaming, thinking, reasoningEffort, toolUse, resume } },
16
+ * ]
17
+ */
18
+ router.get('/', (_req, res) => {
19
+ try {
20
+ const agents = agentGateway.getAllAgentCapabilities();
21
+ res.json(agents);
22
+ } catch (error) {
23
+ res.status(500).json({ error: error.message });
24
+ }
25
+ });
26
+
27
+ export default router;
@@ -279,6 +279,26 @@ router.get('/:id/canvas/file/:filename/content', (req, res) => {
279
279
  });
280
280
  });
281
281
 
282
+ // GET /api/sessions/:id/canvas/:itemId/content - Get one canvas item content inline
283
+ router.get('/:id/canvas/:itemId/content', (req, res) => {
284
+ const session = sessions.getById(req.params.id);
285
+ if (!session) return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
286
+
287
+ const item = canvasItems.getById(req.params.itemId);
288
+ if (!item) return res.status(404).json({ error: 'Canvas item not found' });
289
+ if (item.sessionId !== req.params.id) {
290
+ return res.status(400).json({ error: 'Canvas item does not belong to this session' });
291
+ }
292
+
293
+ res.json({
294
+ content: item.content ?? null,
295
+ data: item.data ?? null,
296
+ type: item.type,
297
+ mimeType: item.mimeType,
298
+ filename: item.filename,
299
+ });
300
+ });
301
+
282
302
  // GET /api/sessions/:id/canvas/file/:filename - Get canvas file by filename
283
303
  // Writes the file to /tmp and returns the file path for Claude's Read tool
284
304
  // Always returns the latest version
@@ -11,6 +11,7 @@ import providersRouter from './providers.js';
11
11
  import commandsRouter from './commands.js';
12
12
  import metricsRouter from './metrics.js';
13
13
  import kanbanRouter from './kanban.js';
14
+ import agentsRouter from './agents.js';
14
15
  import { getDbPath } from '../database.js';
15
16
  import { schedulerService } from '../services/schedulerService.js';
16
17
 
@@ -38,6 +39,7 @@ router.use('/git', gitRouter);
38
39
  router.use('/filesystem', filesystemRouter);
39
40
  router.use('/settings', settingsRouter);
40
41
  router.use('/providers', providersRouter);
42
+ router.use('/agents', agentsRouter);
41
43
  router.use('/commands', commandsRouter)
42
44
 
43
45
  // Canvas routes are nested under sessions
@@ -44,6 +44,26 @@ export function resolveDefault(explicit, projectDefault, systemDefault) {
44
44
  return systemDefault;
45
45
  }
46
46
 
47
+ /**
48
+ * Normalize provider IDs from JSON or multipart request fields.
49
+ * @param {*} value - Raw providerId value
50
+ * @returns {string|null|undefined}
51
+ */
52
+ export function normalizeProviderId(value) {
53
+ if (value === undefined) return undefined;
54
+ if (value === null || value === '') return null;
55
+ if (typeof value !== 'string') {
56
+ throw new TypeError('providerId must be a string or null');
57
+ }
58
+ return value;
59
+ }
60
+
61
+ function resolveProviderDefault(explicit, projectDefault, systemDefault) {
62
+ if (explicit !== undefined) return explicit;
63
+ if (projectDefault !== undefined && projectDefault !== null) return projectDefault;
64
+ return systemDefault ?? null;
65
+ }
66
+
47
67
  /**
48
68
  * Resolve thinkingEnabled with special boolean handling.
49
69
  * @param {object} body - Request body
@@ -114,11 +134,16 @@ export function prepareSessionConfig(body, projectDefs, systemDefaults) {
114
134
  effortLevel = null;
115
135
  }
116
136
 
137
+ const explicitProviderId = normalizeProviderId(body.providerId);
138
+ const projectProviderId = normalizeProviderId(projectDefs?.providerId);
139
+ const systemProviderId = normalizeProviderId(systemDefaults.providerId);
140
+
117
141
  return {
118
142
  prompt: body.prompt,
119
143
  name: body.name,
120
144
  mode: resolveDefault(body.mode, projectDefs?.mode, systemDefaults.mode),
121
145
  model: resolveDefault(body.model, projectDefs?.model, systemDefaults.model || null),
146
+ providerId: resolveProviderDefault(explicitProviderId, projectProviderId, systemProviderId),
122
147
  effortLevel,
123
148
  gitBranch: resolveDefault(body.gitBranch, projectDefs?.gitBranch, null),
124
149
  gitMode: resolveDefault(body.gitMode, projectDefs?.gitMode, null),
@@ -18,6 +18,7 @@ import {
18
18
  setupAndStartSession,
19
19
  } from './projects-session-helpers.js';
20
20
  import { validateGitSettings, buildRunsBySession } from './projects-helpers.js';
21
+ import { resolveAgentTypeFromModel } from '../services/sessionProvider.js';
21
22
  import { access, constants } from 'fs/promises';
22
23
  import { dirname, isAbsolute } from 'path';
23
24
 
@@ -242,7 +243,9 @@ function createSessionRow(projectId, config, nextTemplateId, initialStatus) {
242
243
  parentSessionId: config.parentSessionId,
243
244
  status: initialStatus,
244
245
  model: config.model,
246
+ providerId: config.providerId,
245
247
  effortLevel: config.effortLevel,
248
+ agentType: config.agentType,
246
249
  });
247
250
 
248
251
  const postCreateUpdate = {
@@ -298,6 +301,11 @@ router.post('/:id/sessions', uploadMiddleware('files', 10), handleUploadError, a
298
301
  }
299
302
 
300
303
  const { config, nextTemplateId } = prepared;
304
+ // Derive the agent type from the resolved model (after template overrides
305
+ // have been applied inside validateAndPrepareSessionConfig). Null/unknown
306
+ // model IDs fall back to 'claude-code'. This is the single source of truth
307
+ // for which adapter the session will use; sessions.create() persists it.
308
+ config.agentType = resolveAgentTypeFromModel(config.model);
301
309
  const initialStatus = determineInitialStatus(config);
302
310
  session = createSessionRow(req.params.id, config, nextTemplateId, initialStatus);
303
311
  return await startSessionOrFail(req, res, { session, config, project });
@@ -139,6 +139,7 @@ router.post('/:id/test', async (req, res) => {
139
139
  // Pick the sonnet-tiered model (if any) as the test model, falling back to any first model
140
140
  const sonnetModel = provider.models?.find((m) => m.tier === 'sonnet');
141
141
  const testConfig = {
142
+ kind: provider.kind || 'anthropic',
142
143
  baseUrl: provider.baseUrl,
143
144
  authToken: provider.authToken,
144
145
  defaultSonnetModel: sonnetModel?.modelId,
@@ -57,6 +57,7 @@ router.post('/:id/start', requireSession, async (req, res) => {
57
57
  const updatedSession = await startDraft(req.session_, {
58
58
  prompt: req.body.prompt,
59
59
  model: req.body.model,
60
+ providerId: req.body.providerId,
60
61
  });
61
62
 
62
63
  res.json({ success: true, session: updatedSession });
@@ -4,6 +4,7 @@ import { continueSession } from '../services/sessionManager.js';
4
4
  import { upload as _upload, handleUploadError } from '../middleware/upload.js';
5
5
  import { requireSession, requireSessionAndProject } from '../middleware/sessionLookup.js';
6
6
  import * as slashCommandService from '../services/slashCommandService.js';
7
+ import { checkCrossKindSwitch } from '../services/sessionAgentGuard.js';
7
8
 
8
9
  const router = Router();
9
10
 
@@ -61,6 +62,11 @@ router.post('/:id/message', _upload.array('files', 10), handleUploadError, requi
61
62
  return res.status(400).json({ error: 'Session is not waiting for input' });
62
63
  }
63
64
 
65
+ const crossKindError = checkCrossKindSwitch(req.session_, model);
66
+ if (crossKindError) {
67
+ return res.status(400).json(crossKindError);
68
+ }
69
+
64
70
  try {
65
71
  // Store file attachments if any - saves to disk in workingDirectory/.attachments
66
72
  const messageAttachments = attachments.createBatch(req.session_.id, null, files, req.workingDirectory);
@@ -1,8 +1,9 @@
1
1
  import { Router } from 'express';
2
- import { settings } from '../db/index.js';
2
+ import { modelProviders, settings } from '../db/index.js';
3
3
  import { DEFAULT_SESSION_TITLE_PROMPT } from '../services/summaryService.js';
4
4
 
5
5
  const router = Router();
6
+ const SUPPORTED_SUMMARY_PROVIDER_KINDS = new Set(['anthropic', 'openai']);
6
7
 
7
8
  /**
8
9
  * GET /api/settings/token-weights
@@ -93,19 +94,42 @@ router.get('/summary', (req, res) => {
93
94
  */
94
95
  router.put('/summary', (req, res) => {
95
96
  try {
96
- const { disableSessionSummaries, sessionTitlePrompt } = req.body;
97
+ const body = req.body || {};
98
+ const {
99
+ disableSessionSummaries,
100
+ sessionTitlePrompt,
101
+ summaryModel,
102
+ summaryProviderId,
103
+ } = body;
97
104
 
98
105
  // Validate that all required fields are present
99
106
  if (typeof disableSessionSummaries !== 'boolean' ||
100
- typeof sessionTitlePrompt !== 'string') {
107
+ typeof sessionTitlePrompt !== 'string' ||
108
+ !Object.prototype.hasOwnProperty.call(body, 'summaryModel') ||
109
+ !Object.prototype.hasOwnProperty.call(body, 'summaryProviderId')) {
101
110
  return res.status(400).json({
102
- error: 'Invalid summary settings. disableSessionSummaries must be a boolean, sessionTitlePrompt must be a string'
111
+ error: 'Invalid summary settings. disableSessionSummaries must be a boolean, sessionTitlePrompt must be a string, summaryModel must be present, and summaryProviderId must be present'
103
112
  });
104
113
  }
105
114
 
115
+ if (typeof summaryModel !== 'string') {
116
+ return res.status(400).json({ error: 'summaryModel must be a string' });
117
+ }
118
+
119
+ if (summaryProviderId !== null && typeof summaryProviderId !== 'string') {
120
+ return res.status(400).json({ error: 'summaryProviderId must be a string or null' });
121
+ }
122
+
123
+ const validationError = validateSummaryModelSelection(summaryModel, summaryProviderId);
124
+ if (validationError) {
125
+ return res.status(400).json({ error: validationError });
126
+ }
127
+
106
128
  const updatedSettings = settings.setSummarySettings({
107
129
  disableSessionSummaries,
108
130
  sessionTitlePrompt,
131
+ summaryModel,
132
+ summaryProviderId,
109
133
  });
110
134
 
111
135
  // Include the default prompt for UI display/editing
@@ -119,6 +143,30 @@ router.put('/summary', (req, res) => {
119
143
  }
120
144
  });
121
145
 
146
+ function validateSummaryModelSelection(summaryModel, summaryProviderId) {
147
+ if (!summaryModel) {
148
+ if (summaryProviderId !== null) {
149
+ return 'summaryProviderId must be null when summaryModel is empty';
150
+ }
151
+ return null;
152
+ }
153
+
154
+ if (!summaryProviderId) {
155
+ return 'summaryProviderId is required when summaryModel is set';
156
+ }
157
+
158
+ const provider = modelProviders.getById(summaryProviderId);
159
+ if (!provider) return `Unknown summary provider: ${summaryProviderId}`;
160
+ if (!SUPPORTED_SUMMARY_PROVIDER_KINDS.has(provider.kind || 'anthropic')) {
161
+ return `Unsupported summary provider kind: ${provider.kind}`;
162
+ }
163
+ const ownsModel = provider.models?.some((model) => model.modelId === summaryModel);
164
+ if (!ownsModel) {
165
+ return `Provider ${summaryProviderId} does not own summary model ${summaryModel}`;
166
+ }
167
+ return null;
168
+ }
169
+
122
170
  /**
123
171
  * DELETE /api/settings/summary
124
172
  * Reset summary settings to defaults
@@ -237,6 +237,7 @@ export class ConversationRepository extends BaseRepository {
237
237
  * @param {Object} usage - Usage data
238
238
  * @param {number} usage.inputTokens
239
239
  * @param {number} usage.outputTokens
240
+ * @param {number} usage.thinkingTokens
240
241
  * @param {number} usage.cacheReadInputTokens
241
242
  * @param {number} usage.cacheCreationInputTokens
242
243
  * @param {number} usage.webSearchRequests
@@ -255,6 +256,7 @@ export class ConversationRepository extends BaseRepository {
255
256
  `UPDATE conversations SET
256
257
  input_tokens = ?,
257
258
  output_tokens = ?,
259
+ thinking_tokens = ?,
258
260
  cache_read_input_tokens = ?,
259
261
  cache_creation_input_tokens = ?,
260
262
  web_search_requests = ?,
@@ -265,6 +267,7 @@ export class ConversationRepository extends BaseRepository {
265
267
  .run(
266
268
  usage.inputTokens,
267
269
  usage.outputTokens,
270
+ usage.thinkingTokens || 0,
268
271
  usage.cacheReadInputTokens,
269
272
  usage.cacheCreationInputTokens,
270
273
  usage.webSearchRequests,
@@ -290,9 +293,12 @@ export class ConversationRepository extends BaseRepository {
290
293
  const now = Date.now();
291
294
 
292
295
  this.db
293
- .prepare(
294
- `INSERT INTO conversations (id, session_id, name, summary, is_active, created_at, updated_at)
295
- VALUES (?, ?, ?, ?, ?, ?, ?)`
296
+ .prepare(
297
+ `INSERT INTO conversations (id, session_id, name, summary, is_active,
298
+ input_tokens, output_tokens, thinking_tokens,
299
+ cache_read_input_tokens, cache_creation_input_tokens,
300
+ web_search_requests, context_window, created_at, updated_at)
301
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
296
302
  )
297
303
  .run(
298
304
  id,
@@ -300,6 +306,13 @@ export class ConversationRepository extends BaseRepository {
300
306
  conv.name,
301
307
  conv.summary,
302
308
  conv.isActive ? 1 : 0,
309
+ conv.inputTokens,
310
+ conv.outputTokens,
311
+ conv.thinkingTokens,
312
+ conv.cacheReadInputTokens,
313
+ conv.cacheCreationInputTokens,
314
+ conv.webSearchRequests,
315
+ conv.contextWindow,
303
316
  now,
304
317
  now
305
318
  );