circuschief 0.5.0 → 0.6.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.js +7 -0
- package/packages/server/src/api/providers.js +1 -0
- package/packages/server/src/api/sessions-messages.js +6 -0
- package/packages/server/src/db/ProviderRepository.js +62 -6
- package/packages/server/src/db/SessionRepository.js +67 -11
- package/packages/server/src/db/migrations/index.js +2 -0
- package/packages/server/src/db/migrations/miscMigrations.js +53 -3
- package/packages/server/src/db/session-helpers.js +5 -0
- package/packages/server/src/schema.sql +1 -0
- package/packages/server/src/services/codexSpawnHelper.js +37 -0
- package/packages/server/src/services/conversationContext.js +27 -0
- package/packages/server/src/services/draftSessionService.js +13 -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 +124 -32
- package/packages/server/src/services/sessionManager.js +45 -8
- package/packages/server/src/services/sessionPrompts.js +25 -0
- package/packages/server/src/services/sessionProvider.js +162 -41
- package/packages/server/src/services/streamEventHandler.js +3 -0
- package/packages/server/src/services/templateTriggerService.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/web/dist/assets/{ActiveSessionsView-BafIafEu.js → ActiveSessionsView-UCbQrF1b.js} +1 -1
- package/packages/web/dist/assets/{AgentLogsView-DCF2WvP2.js → AgentLogsView-Cdw4nmvd.js} +1 -1
- package/packages/web/dist/assets/ApiClient-CWbXWDUY.js +1 -0
- package/packages/web/dist/assets/{ArchiveConfirmModal-fgoEQhfq.js → ArchiveConfirmModal-J48eh3zw.js} +1 -1
- package/packages/web/dist/assets/{CommandButtonDetailView-DAg07cDQ.js → CommandButtonDetailView-DnFhJY5A.js} +1 -1
- package/packages/web/dist/assets/EffortLevelSelector-bXbPo4Zw.js +1 -0
- package/packages/web/dist/assets/{GeneralSettingsView-Cn9VI2du.js → GeneralSettingsView-CQkmdczf.js} +1 -1
- package/packages/web/dist/assets/{InputWithButton-BvboBGbz.js → InputWithButton-XyM3k6lN.js} +1 -1
- package/packages/web/dist/assets/{InterpolationHelp-0GoSBPgf.js → InterpolationHelp-PfYR3KJo.js} +1 -1
- package/packages/web/dist/assets/MarkdownEditor-P8F5kO-o.js +2 -0
- package/packages/web/dist/assets/{ModelSelector-DPPD-92R.css → ModelSelector-BZOT1Jc6.css} +1 -1
- package/packages/web/dist/assets/ModelSelector-CowKfGMP.js +1 -0
- package/packages/web/dist/assets/{NewSessionView-C77YVqgY.js → NewSessionView-DkjFLvHU.js} +1 -1
- package/packages/web/dist/assets/{ProjectEditView-BBHOsgBV.js → ProjectEditView-embVT7NC.js} +1 -1
- package/packages/web/dist/assets/{ProjectListView-CLwtuJ0J.js → ProjectListView-CuYMmd3O.js} +1 -1
- package/packages/web/dist/assets/{ProjectNewView-CzDtVibO.js → ProjectNewView-CNaA4Maf.js} +1 -1
- package/packages/web/dist/assets/ProvidersView-C7rydtOd.js +1 -0
- package/packages/web/dist/assets/ProvidersView-DE82G_5W.css +1 -0
- package/packages/web/dist/assets/{QuickResponseSettings-BBHMapcA.js → QuickResponseSettings-BTQEKhwJ.js} +1 -1
- package/packages/web/dist/assets/{QuickResponsesPanel-CTXYjMF-.js → QuickResponsesPanel-BqMYSHb0.js} +1 -1
- package/packages/web/dist/assets/ResizableTextarea-DsU3TVwF.css +1 -0
- package/packages/web/dist/assets/{ResizableTextarea-Cw6aL4rp.js → ResizableTextarea-wYF3K2RO.js} +1 -1
- package/packages/web/dist/assets/SessionCard-BMGC2HqI.css +1 -0
- package/packages/web/dist/assets/SessionCard-bLaQEWWX.js +1 -0
- package/packages/web/dist/assets/{SessionDetailView-BL83oPiI.css → SessionDetailView-Cv-xMzXp.css} +1 -1
- package/packages/web/dist/assets/{SessionDetailView-CrZvMb3j.js → SessionDetailView-CvQOUsW2.js} +27 -27
- package/packages/web/dist/assets/SessionFormOptions-3pzbgI2Q.js +1 -0
- package/packages/web/dist/assets/SessionFormOptions-DhhIkjIS.css +1 -0
- package/packages/web/dist/assets/SessionListView-Dranfb72.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-DTnDAF95.js} +1 -1
- package/packages/web/dist/assets/{SettingsView-CmJ5JPd5.js → SettingsView-DNLUSsHV.js} +1 -1
- package/packages/web/dist/assets/SlashCommandWizard-CRGFaO8t.js +1 -0
- package/packages/web/dist/assets/SlashCommandWizard-Dn7sNaBd.css +1 -0
- package/packages/web/dist/assets/{SummarySettingsView-DQM1n3bc.js → SummarySettingsView-C7G_suHp.js} +1 -1
- package/packages/web/dist/assets/{TemplateDetailView-B8clSBPk.js → TemplateDetailView-B78_DLMR.js} +1 -1
- package/packages/web/dist/assets/{commandButtons-D74TkPNU.js → commandButtons-Bbjf3fCt.js} +1 -1
- package/packages/web/dist/assets/{index-CefzeYRE.js → index--V7c-VZf.js} +1 -1
- package/packages/web/dist/assets/{index-BELtFs3n.js → index-8Q04yd7H.js} +1 -1
- package/packages/web/dist/assets/{index-rjbX81sm.js → index-B47XRBDH.js} +1 -1
- package/packages/web/dist/assets/index-BQL_L4gL.js +82 -0
- package/packages/web/dist/assets/{index-BsDR4w2c.js → index-BXbgZrhS.js} +1 -1
- package/packages/web/dist/assets/{index-DQMHi05L.js → index-Bs7Qf5D6.js} +1 -1
- package/packages/web/dist/assets/{index-Dz7jFUYU.js → index-CGhDVPen.js} +1 -1
- package/packages/web/dist/assets/{index-f315nDFm.js → index-CKcRO1A6.js} +1 -1
- package/packages/web/dist/assets/{index-Gre8tUfC.js → index-CTq-SLIW.js} +1 -1
- package/packages/web/dist/assets/{index-_Lv79l46.js → index-CYyos3iC.js} +1 -1
- package/packages/web/dist/assets/{index-DMZZCi2u.js → index-Cf6vdW-B.js} +3 -3
- package/packages/web/dist/assets/{index-DIvveuSK.js → index-CsCREAxF.js} +1 -1
- package/packages/web/dist/assets/{index-CVozYqQ-.js → index-DJTTk_8T.js} +1 -1
- package/packages/web/dist/assets/{index-DPt6qBRK.js → index-DPqUJ5JK.js} +1 -1
- package/packages/web/dist/assets/{index-B5ocUoPf.js → index-EwAe1dKg.js} +1 -1
- package/packages/web/dist/assets/{index-CrLh8vw5.js → index-JBA8axyA.js} +1 -1
- package/packages/web/dist/assets/{index-DrlwE0Zo.js → index-JkVHFtK5.js} +1 -1
- package/packages/web/dist/assets/{index-DuXChAe-.js → index-gMPUwT55.js} +1 -1
- package/packages/web/dist/assets/{index-DYWZ8lD-.js → index-wadc_0zT.js} +1 -1
- package/packages/web/dist/assets/{projects-D_C9dE9s.js → projects-CPt3AB7U.js} +1 -1
- package/packages/web/dist/assets/providers-ChfeMvUq.js +1 -0
- package/packages/web/dist/assets/sessions-CwPsJOb1.js +1 -0
- package/packages/web/dist/assets/{settings-6Rw9xt-G.js → settings-BOj6wq6t.js} +1 -1
- package/packages/web/dist/index.html +1 -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/ProvidersView-Eg93KbyC.js +0 -1
- package/packages/web/dist/assets/ProvidersView-uD8SKWpA.css +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/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/index-BGAW2Nqa.js +0 -82
- package/packages/web/dist/assets/providers-BdvbPVdE.js +0 -1
- package/packages/web/dist/assets/sessions-Bs5FA6JZ.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
|
|
@@ -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
|
|
|
@@ -243,6 +244,7 @@ function createSessionRow(projectId, config, nextTemplateId, initialStatus) {
|
|
|
243
244
|
status: initialStatus,
|
|
244
245
|
model: config.model,
|
|
245
246
|
effortLevel: config.effortLevel,
|
|
247
|
+
agentType: config.agentType,
|
|
246
248
|
});
|
|
247
249
|
|
|
248
250
|
const postCreateUpdate = {
|
|
@@ -298,6 +300,11 @@ router.post('/:id/sessions', uploadMiddleware('files', 10), handleUploadError, a
|
|
|
298
300
|
}
|
|
299
301
|
|
|
300
302
|
const { config, nextTemplateId } = prepared;
|
|
303
|
+
// Derive the agent type from the resolved model (after template overrides
|
|
304
|
+
// have been applied inside validateAndPrepareSessionConfig). Null/unknown
|
|
305
|
+
// model IDs fall back to 'claude-code'. This is the single source of truth
|
|
306
|
+
// for which adapter the session will use; sessions.create() persists it.
|
|
307
|
+
config.agentType = resolveAgentTypeFromModel(config.model);
|
|
301
308
|
const initialStatus = determineInitialStatus(config);
|
|
302
309
|
session = createSessionRow(req.params.id, config, nextTemplateId, initialStatus);
|
|
303
310
|
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,
|
|
@@ -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);
|
|
@@ -2,6 +2,22 @@ import { BaseRepository } from './BaseRepository.js';
|
|
|
2
2
|
import { databaseManager } from './DatabaseManager.js';
|
|
3
3
|
import { encrypt, decrypt } from '../services/encryption.js';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Valid values for `providers.kind`. Maps 1:1 to an agent adapter:
|
|
7
|
+
* - 'anthropic' → 'claude-code'
|
|
8
|
+
* - 'openai' → 'codex'
|
|
9
|
+
*/
|
|
10
|
+
export const PROVIDER_KINDS = Object.freeze(['anthropic', 'openai']);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Mapping from provider kind to the agent adapter that should drive sessions
|
|
14
|
+
* backed by that provider.
|
|
15
|
+
*/
|
|
16
|
+
export const AGENT_TYPE_BY_KIND = Object.freeze({
|
|
17
|
+
anthropic: 'claude-code',
|
|
18
|
+
openai: 'codex',
|
|
19
|
+
});
|
|
20
|
+
|
|
5
21
|
/**
|
|
6
22
|
* Provider repository class (replaces ModelProviderRepository).
|
|
7
23
|
*
|
|
@@ -11,6 +27,8 @@ import { encrypt, decrypt } from '../services/encryption.js';
|
|
|
11
27
|
* - No auto-sync logic (#syncDefaultModels removed)
|
|
12
28
|
* - Auth tokens are encrypted at rest (AES-256-GCM via encryption service)
|
|
13
29
|
* - `getProviderByModelId` includes models (needed for buildProviderEnv)
|
|
30
|
+
* - Providers carry a `kind` (`'anthropic'` | `'openai'`) that selects the
|
|
31
|
+
* agent adapter and env-var convention. `kind` is **immutable** after create.
|
|
14
32
|
*/
|
|
15
33
|
export class ProviderRepository extends BaseRepository {
|
|
16
34
|
constructor() {
|
|
@@ -27,6 +45,7 @@ export class ProviderRepository extends BaseRepository {
|
|
|
27
45
|
apiTimeoutMs: row.api_timeout_ms,
|
|
28
46
|
additionalEnvVars: row.additional_env_vars ? JSON.parse(row.additional_env_vars) : null,
|
|
29
47
|
isBuiltIn: row.is_built_in === 1,
|
|
48
|
+
kind: row.kind || 'anthropic',
|
|
30
49
|
createdAt: row.created_at,
|
|
31
50
|
updatedAt: row.updated_at,
|
|
32
51
|
};
|
|
@@ -64,12 +83,20 @@ export class ProviderRepository extends BaseRepository {
|
|
|
64
83
|
authToken = null,
|
|
65
84
|
apiTimeoutMs = null,
|
|
66
85
|
additionalEnvVars = null,
|
|
86
|
+
kind = 'anthropic',
|
|
67
87
|
} = data;
|
|
68
88
|
|
|
89
|
+
// Application-layer validation: give a clear error ahead of the DB CHECK.
|
|
90
|
+
if (!PROVIDER_KINDS.includes(kind)) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Invalid provider kind "${kind}". Must be one of: ${PROVIDER_KINDS.join(', ')}.`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
69
96
|
this.db
|
|
70
97
|
.prepare(
|
|
71
|
-
`INSERT INTO providers (id, name, base_url, auth_token, api_timeout_ms, additional_env_vars, created_at, updated_at)
|
|
72
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
98
|
+
`INSERT INTO providers (id, name, base_url, auth_token, api_timeout_ms, additional_env_vars, kind, created_at, updated_at)
|
|
99
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
73
100
|
)
|
|
74
101
|
.run(
|
|
75
102
|
id,
|
|
@@ -78,6 +105,7 @@ export class ProviderRepository extends BaseRepository {
|
|
|
78
105
|
encrypt(authToken),
|
|
79
106
|
apiTimeoutMs,
|
|
80
107
|
additionalEnvVars ? JSON.stringify(additionalEnvVars) : null,
|
|
108
|
+
kind,
|
|
81
109
|
now,
|
|
82
110
|
now
|
|
83
111
|
);
|
|
@@ -115,6 +143,15 @@ export class ProviderRepository extends BaseRepository {
|
|
|
115
143
|
* @returns {Object} Updated provider (with models array)
|
|
116
144
|
*/
|
|
117
145
|
update(id, data) {
|
|
146
|
+
// `kind` is immutable after create. Existing models + env wiring depend on it,
|
|
147
|
+
// so changing it in place would silently corrupt sessions already attached to
|
|
148
|
+
// this provider.
|
|
149
|
+
if (data && Object.prototype.hasOwnProperty.call(data, 'kind')) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
"Provider kind is immutable after create. Delete and recreate the provider to change kind."
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
118
155
|
const updates = [];
|
|
119
156
|
const values = [];
|
|
120
157
|
|
|
@@ -279,12 +316,15 @@ export class ProviderRepository extends BaseRepository {
|
|
|
279
316
|
return null;
|
|
280
317
|
}
|
|
281
318
|
|
|
282
|
-
//
|
|
319
|
+
// Prefer custom providers over built-ins for duplicate model IDs. This
|
|
320
|
+
// preserves user-managed OpenAI providers (alternate base URLs, keys, or
|
|
321
|
+
// env vars) even when official OpenAI models are also seeded built-ins.
|
|
283
322
|
const row = this.db
|
|
284
323
|
.prepare(
|
|
285
324
|
`SELECT p.id FROM providers p
|
|
286
325
|
JOIN provider_models pm ON p.id = pm.provider_id
|
|
287
|
-
WHERE pm.model_id =
|
|
326
|
+
WHERE pm.model_id = ?
|
|
327
|
+
ORDER BY p.is_built_in ASC, p.name ASC`
|
|
288
328
|
)
|
|
289
329
|
.get(modelId);
|
|
290
330
|
|
|
@@ -297,11 +337,27 @@ export class ProviderRepository extends BaseRepository {
|
|
|
297
337
|
const provider = this.getById(row.id);
|
|
298
338
|
if (!provider) return null;
|
|
299
339
|
|
|
300
|
-
// Built-in Anthropic provider falls through to SDK defaults
|
|
301
|
-
|
|
340
|
+
// Built-in **Anthropic** provider falls through to SDK defaults (keeps
|
|
341
|
+
// historical behavior of letting @anthropic-ai/claude-agent-sdk pick its
|
|
342
|
+
// own env). Built-in OpenAI (or any future non-Anthropic built-in) still
|
|
343
|
+
// needs its env vars to flow, so we return the provider object.
|
|
344
|
+
if (provider.isBuiltIn && provider.kind === 'anthropic') {
|
|
302
345
|
return null;
|
|
303
346
|
}
|
|
304
347
|
|
|
305
348
|
return provider;
|
|
306
349
|
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Resolve a provider's agent type from its id.
|
|
353
|
+
* @param {string|null|undefined} providerId
|
|
354
|
+
* @returns {string|null} 'claude-code' for anthropic-kind, 'codex' for openai-kind,
|
|
355
|
+
* or null if the provider is unknown.
|
|
356
|
+
*/
|
|
357
|
+
getAgentTypeForProvider(providerId) {
|
|
358
|
+
if (!providerId) return null;
|
|
359
|
+
const provider = this.getById(providerId);
|
|
360
|
+
if (!provider) return null;
|
|
361
|
+
return AGENT_TYPE_BY_KIND[provider.kind] || null;
|
|
362
|
+
}
|
|
307
363
|
}
|