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
|
@@ -32,6 +32,7 @@ const SESSIONS_BASE_COLUMNS = `
|
|
|
32
32
|
parent_session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL,
|
|
33
33
|
input_tokens INTEGER DEFAULT 0,
|
|
34
34
|
output_tokens INTEGER DEFAULT 0,
|
|
35
|
+
thinking_tokens INTEGER DEFAULT 0,
|
|
35
36
|
cache_read_input_tokens INTEGER DEFAULT 0,
|
|
36
37
|
cache_creation_input_tokens INTEGER DEFAULT 0,
|
|
37
38
|
web_search_requests INTEGER DEFAULT 0,
|
|
@@ -59,7 +60,7 @@ const SESSIONS_ALL_COLUMNS = [
|
|
|
59
60
|
'id', 'project_id', 'name', 'status', 'mode', 'thinking_enabled',
|
|
60
61
|
'git_branch', 'git_worktree', 'pr_url', 'error', 'effort_level',
|
|
61
62
|
'cost_usd', 'claude_session_id', 'model', 'next_template_id',
|
|
62
|
-
'parent_session_id', 'input_tokens', 'output_tokens',
|
|
63
|
+
'parent_session_id', 'input_tokens', 'output_tokens', 'thinking_tokens',
|
|
63
64
|
'cache_read_input_tokens', 'cache_creation_input_tokens',
|
|
64
65
|
'web_search_requests', 'context_window', 'archived', 'starred',
|
|
65
66
|
'manually_named', 'scheduled_at', 'reschedule_delay_minutes',
|
|
@@ -211,6 +212,10 @@ export const sessionsMigrations = [
|
|
|
211
212
|
name: 'sessions-add-output_tokens',
|
|
212
213
|
up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'output_tokens', COL_INTEGER_DEFAULT_0); },
|
|
213
214
|
},
|
|
215
|
+
{
|
|
216
|
+
name: 'sessions-add-thinking_tokens',
|
|
217
|
+
up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'thinking_tokens', COL_INTEGER_DEFAULT_0); },
|
|
218
|
+
},
|
|
214
219
|
{
|
|
215
220
|
name: 'sessions-add-cache_read_input_tokens',
|
|
216
221
|
up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'cache_read_input_tokens', COL_INTEGER_DEFAULT_0); },
|
|
@@ -24,6 +24,7 @@ export function mapTokenUsage(row) {
|
|
|
24
24
|
return {
|
|
25
25
|
inputTokens: row.input_tokens || 0,
|
|
26
26
|
outputTokens: row.output_tokens || 0,
|
|
27
|
+
thinkingTokens: row.thinking_tokens || 0,
|
|
27
28
|
cacheReadInputTokens: row.cache_read_input_tokens || 0,
|
|
28
29
|
cacheCreationInputTokens: row.cache_creation_input_tokens || 0,
|
|
29
30
|
webSearchRequests: row.web_search_requests || 0,
|
|
@@ -58,7 +59,12 @@ const CONFIG_DEFAULTS = {
|
|
|
58
59
|
parentSessionId: null,
|
|
59
60
|
status: 'starting',
|
|
60
61
|
model: null,
|
|
62
|
+
providerId: null,
|
|
61
63
|
effortLevel: null,
|
|
64
|
+
// Agent runtime for the session: 'claude-code' (default) or 'codex'.
|
|
65
|
+
// Defaults to null so SessionRepository.create() can resolve it from the model.
|
|
66
|
+
// Explicit values from callers are preserved as-is.
|
|
67
|
+
agentType: null,
|
|
62
68
|
};
|
|
63
69
|
|
|
64
70
|
function buildConfig(src) {
|
|
@@ -96,6 +102,7 @@ export const DIRECT_FIELD_MAP = {
|
|
|
96
102
|
costUsd: 'cost_usd',
|
|
97
103
|
claudeSessionId: 'claude_session_id',
|
|
98
104
|
model: 'model',
|
|
105
|
+
providerId: 'provider_id',
|
|
99
106
|
nextTemplateId: 'next_template_id',
|
|
100
107
|
parentSessionId: 'parent_session_id',
|
|
101
108
|
scheduledAt: 'scheduled_at',
|
|
@@ -109,6 +116,7 @@ export const DIRECT_FIELD_MAP = {
|
|
|
109
116
|
effortLevel: 'effort_level',
|
|
110
117
|
targetLaneId: 'target_lane_id',
|
|
111
118
|
laneTriggerDepth: 'lane_trigger_depth',
|
|
119
|
+
agentType: 'agent_type',
|
|
112
120
|
};
|
|
113
121
|
|
|
114
122
|
/** camelCase -> snake_case column mapping for boolean fields (converted to 1/0) */
|
|
@@ -48,6 +48,13 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
48
48
|
claude_session_id TEXT,
|
|
49
49
|
next_template_id TEXT REFERENCES session_templates(id) ON DELETE SET NULL,
|
|
50
50
|
parent_session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL,
|
|
51
|
+
input_tokens INTEGER DEFAULT 0,
|
|
52
|
+
output_tokens INTEGER DEFAULT 0,
|
|
53
|
+
thinking_tokens INTEGER DEFAULT 0,
|
|
54
|
+
cache_read_input_tokens INTEGER DEFAULT 0,
|
|
55
|
+
cache_creation_input_tokens INTEGER DEFAULT 0,
|
|
56
|
+
web_search_requests INTEGER DEFAULT 0,
|
|
57
|
+
context_window INTEGER DEFAULT 200000,
|
|
51
58
|
created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
|
|
52
59
|
updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
53
60
|
);
|
|
@@ -64,6 +71,7 @@ CREATE TABLE IF NOT EXISTS conversations (
|
|
|
64
71
|
-- Token usage fields (per-conversation tracking)
|
|
65
72
|
input_tokens INTEGER DEFAULT 0,
|
|
66
73
|
output_tokens INTEGER DEFAULT 0,
|
|
74
|
+
thinking_tokens INTEGER DEFAULT 0,
|
|
67
75
|
cache_read_input_tokens INTEGER DEFAULT 0,
|
|
68
76
|
cache_creation_input_tokens INTEGER DEFAULT 0,
|
|
69
77
|
web_search_requests INTEGER DEFAULT 0,
|
|
@@ -233,6 +241,7 @@ CREATE TABLE IF NOT EXISTS providers (
|
|
|
233
241
|
api_timeout_ms INTEGER,
|
|
234
242
|
additional_env_vars TEXT,
|
|
235
243
|
is_built_in INTEGER NOT NULL DEFAULT 0,
|
|
244
|
+
kind TEXT NOT NULL DEFAULT 'anthropic' CHECK(kind IN ('anthropic','openai')),
|
|
236
245
|
created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
|
|
237
246
|
updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
238
247
|
);
|
|
@@ -20,7 +20,7 @@ export class AgentCallLogger {
|
|
|
20
20
|
const callId = nanoid();
|
|
21
21
|
|
|
22
22
|
// Build metadata object - only include keys with defined values
|
|
23
|
-
const metadata = {};
|
|
23
|
+
const metadata = { ...(meta.metadata || {}) };
|
|
24
24
|
if (meta.effortLevel !== undefined && meta.effortLevel !== null) {
|
|
25
25
|
metadata.effortLevel = meta.effortLevel;
|
|
26
26
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { createRobustEnv } from './nodeSpawnHelper.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create a custom spawn function for the Codex CLI.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors {@link createClaudeCodeSpawner} but keeps stderr piped (the Codex
|
|
8
|
+
* adapter surfaces stderr bytes as error events rather than silently ignoring
|
|
9
|
+
* them).
|
|
10
|
+
*
|
|
11
|
+
* As with the Claude helper:
|
|
12
|
+
* - The command 'node' is replaced with {@link process.execPath} so child
|
|
13
|
+
* processes use the same Node binary that's running the app (important
|
|
14
|
+
* for nvm/fnm/volta users).
|
|
15
|
+
* - `createRobustEnv` guarantees the Node bin directory is on PATH.
|
|
16
|
+
*
|
|
17
|
+
* @returns {Function} Spawn function of shape (options) => childProcess
|
|
18
|
+
*/
|
|
19
|
+
export function createCodexSpawner() {
|
|
20
|
+
return (options) => {
|
|
21
|
+
const { command, args, cwd, env, signal } = options;
|
|
22
|
+
|
|
23
|
+
// Replace 'node' with the absolute path to the current Node executable
|
|
24
|
+
const actualCommand = command === 'node' ? process.execPath : command;
|
|
25
|
+
|
|
26
|
+
// Ensure PATH includes the directory containing Node
|
|
27
|
+
const robustEnv = createRobustEnv(env);
|
|
28
|
+
|
|
29
|
+
return spawn(actualCommand, args, {
|
|
30
|
+
cwd,
|
|
31
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
32
|
+
signal,
|
|
33
|
+
env: robustEnv,
|
|
34
|
+
windowsHide: true,
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { commandButtons } from '../database.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build Command Button API instructions for system prompt if the project has command buttons.
|
|
5
|
+
* @param {string} apiUrl - Base API URL
|
|
6
|
+
* @param {string} sessionId - Current session ID
|
|
7
|
+
* @param {string} projectId - Current project ID
|
|
8
|
+
* @returns {string} Command button instructions or empty string if no buttons configured
|
|
9
|
+
*/
|
|
10
|
+
export function buildCommandButtonApiInstructions(apiUrl, sessionId, projectId) {
|
|
11
|
+
const buttons = commandButtons.getByProjectId(projectId);
|
|
12
|
+
if (!buttons || buttons.length === 0) {
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return `## Command Buttons API
|
|
17
|
+
|
|
18
|
+
This project has command buttons configured - reusable shell commands you can execute. Use the Bash tool to run these curl commands.
|
|
19
|
+
|
|
20
|
+
### List Available Buttons
|
|
21
|
+
\`\`\`bash
|
|
22
|
+
curl ${apiUrl}/api/projects/${projectId}/command-buttons
|
|
23
|
+
\`\`\`
|
|
24
|
+
|
|
25
|
+
### Run a Button
|
|
26
|
+
\`\`\`bash
|
|
27
|
+
curl -X POST ${apiUrl}/api/sessions/${sessionId}/command-buttons/<button_id>/run
|
|
28
|
+
\`\`\`
|
|
29
|
+
|
|
30
|
+
Response: { runId, buttonId, status: "running", output: "" }
|
|
31
|
+
|
|
32
|
+
### Check Run Status & Output
|
|
33
|
+
\`\`\`bash
|
|
34
|
+
curl ${apiUrl}/api/sessions/${sessionId}/command-buttons/runs/<run_id>
|
|
35
|
+
\`\`\`
|
|
36
|
+
|
|
37
|
+
Response: { runId, buttonId, status, exitCode, output, startedAt, completedAt }
|
|
38
|
+
|
|
39
|
+
### List All Runs for This Session
|
|
40
|
+
\`\`\`bash
|
|
41
|
+
curl ${apiUrl}/api/sessions/${sessionId}/command-buttons/runs
|
|
42
|
+
\`\`\`
|
|
43
|
+
|
|
44
|
+
### Kill a Running Command
|
|
45
|
+
\`\`\`bash
|
|
46
|
+
curl -X POST ${apiUrl}/api/sessions/${sessionId}/command-buttons/runs/<run_id>/kill
|
|
47
|
+
\`\`\``;
|
|
48
|
+
}
|
|
@@ -70,3 +70,30 @@ ${transcript}
|
|
|
70
70
|
|
|
71
71
|
`;
|
|
72
72
|
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Build context for a continuation where the adapter cannot resume.
|
|
76
|
+
* This is the generic case for any non-resumable adapter (Codex, future adapters).
|
|
77
|
+
* @param {string} conversationId
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
export function buildConversationContextForContinuation(conversationId) {
|
|
81
|
+
const conversationMessages = messages.getByConversationId(conversationId);
|
|
82
|
+
|
|
83
|
+
// Don't include the last user message (that's the current prompt)
|
|
84
|
+
const previousMessages = conversationMessages.slice(0, -1);
|
|
85
|
+
|
|
86
|
+
if (previousMessages.length === 0) {
|
|
87
|
+
return '';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const transcript = formatConversationHistory(previousMessages);
|
|
91
|
+
|
|
92
|
+
return `<conversation_history>
|
|
93
|
+
The following is the conversation history from this session so far. Continue naturally from where the conversation left off.
|
|
94
|
+
|
|
95
|
+
${transcript}
|
|
96
|
+
</conversation_history>
|
|
97
|
+
|
|
98
|
+
`;
|
|
99
|
+
}
|
|
@@ -2,6 +2,7 @@ import { sessions, messages, projects, conversations, attachments } from '../dat
|
|
|
2
2
|
import { broadcastToSession, broadcastToProject } from '../websocket.js';
|
|
3
3
|
import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
|
|
4
4
|
import * as slashCommandService from './slashCommandService.js';
|
|
5
|
+
import { resolveAgentTypeFromModel } from './sessionProvider.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Validates that a session is a draft (waiting status with no assistant messages).
|
|
@@ -111,6 +112,7 @@ function getOrCreateInitialMessage(session, options) {
|
|
|
111
112
|
* @param {object} options
|
|
112
113
|
* @param {string} [options.prompt] - Optional new prompt to use/override
|
|
113
114
|
* @param {string} [options.model] - Optional model override
|
|
115
|
+
* @param {string|null} [options.providerId] - Optional provider override
|
|
114
116
|
* @returns {Promise<object>} The updated session
|
|
115
117
|
*/
|
|
116
118
|
export async function startDraft(session, options = {}) {
|
|
@@ -125,6 +127,11 @@ export async function startDraft(session, options = {}) {
|
|
|
125
127
|
// Model to use for this session (optional - SDK will use default if not provided)
|
|
126
128
|
const model = options.model || session.pendingModel || session.model || null;
|
|
127
129
|
|
|
130
|
+
// Resolve the agent type from the selected model before launching.
|
|
131
|
+
// Draft sessions have no assistant messages yet, so the session is still
|
|
132
|
+
// choosing its initial runtime – the selected model determines agentType.
|
|
133
|
+
const agentType = model ? resolveAgentTypeFromModel(model) : session.agentType;
|
|
134
|
+
|
|
128
135
|
// Get or create the initial user message
|
|
129
136
|
const initialMessage = getOrCreateInitialMessage(session, options);
|
|
130
137
|
const finalPrompt = initialMessage.content;
|
|
@@ -132,8 +139,14 @@ export async function startDraft(session, options = {}) {
|
|
|
132
139
|
// Get session attachments for context
|
|
133
140
|
const sessionAttachments = attachments.getBySessionId(session.id);
|
|
134
141
|
|
|
135
|
-
// Update session status to starting
|
|
136
|
-
|
|
142
|
+
// Update session status to starting, clear pendingModel, and persist the
|
|
143
|
+
// resolved model + agentType BEFORE runSession() reads them from storage.
|
|
144
|
+
sessions.update(session.id, {
|
|
145
|
+
status: 'starting',
|
|
146
|
+
pendingModel: null,
|
|
147
|
+
...(model ? { model, agentType } : {}),
|
|
148
|
+
...(options.providerId !== undefined ? { providerId: options.providerId } : {}),
|
|
149
|
+
});
|
|
137
150
|
|
|
138
151
|
// Resolve skill/command invocations so skill body goes into system prompt
|
|
139
152
|
const resolved = await slashCommandService.resolvePromptSkillOrCommand(
|
|
@@ -9,6 +9,7 @@ import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
|
|
|
9
9
|
import { renderTemplatePrompt, getRootSession } from './templateTriggerService.js';
|
|
10
10
|
import { setupGitForSession } from './gitSessionSetup.js';
|
|
11
11
|
import { runSession } from './sessionManager.js';
|
|
12
|
+
import { resolveAgentTypeFromModel } from './sessionProvider.js';
|
|
12
13
|
|
|
13
14
|
// Maximum depth for recursive lane-entry template triggers
|
|
14
15
|
export const MAX_LANE_TRIGGER_DEPTH = 5;
|
|
@@ -147,6 +148,7 @@ async function buildChildSessionFromTemplate(template, session, lane, depth) {
|
|
|
147
148
|
gitBranch: settings.gitBranch,
|
|
148
149
|
status: 'starting',
|
|
149
150
|
model: settings.model,
|
|
151
|
+
agentType: resolveAgentTypeFromModel(settings.model),
|
|
150
152
|
});
|
|
151
153
|
|
|
152
154
|
// Configure session
|
|
@@ -240,6 +242,7 @@ async function buildChildSessionFromPrompt(lane, session, depth) {
|
|
|
240
242
|
const newSession = sessions.create(session.projectId, `Lane prompt (lane: ${lane.name})`, renderedPrompt, {
|
|
241
243
|
...settings,
|
|
242
244
|
status: 'starting',
|
|
245
|
+
agentType: resolveAgentTypeFromModel(settings.model),
|
|
243
246
|
});
|
|
244
247
|
|
|
245
248
|
// Configure session
|
|
@@ -1,35 +1,53 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
+
import OpenAI from 'openai';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
* Test a provider configuration by making a minimal API call
|
|
5
|
-
*
|
|
5
|
+
* Test a provider configuration by making a minimal API call.
|
|
6
|
+
* Branches on `kind`:
|
|
7
|
+
* - 'anthropic' → send a tiny `messages.create` via `@anthropic-ai/sdk`.
|
|
8
|
+
* - 'openai' → prefer `models.list()` via `openai`; fall back to a
|
|
9
|
+
* `chat.completions.create({ max_tokens: 1 })` if
|
|
10
|
+
* `models.list` is not supported (chat-only endpoints).
|
|
11
|
+
*
|
|
12
|
+
* Both branches return the same response shape:
|
|
13
|
+
* - Success: { success: true, message, details: { model, usage? } }
|
|
14
|
+
* - Failure: { success: false, message, details: { code, type } }
|
|
15
|
+
* This function never throws. Errors are mapped to the failure shape above.
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} config
|
|
18
|
+
* @param {'anthropic'|'openai'} [config.kind='anthropic'] - Provider kind
|
|
6
19
|
* @param {string} [config.baseUrl] - Base URL for the provider
|
|
7
20
|
* @param {string} [config.authToken] - Auth token for the provider
|
|
8
|
-
* @param {string} [config.defaultSonnetModel] -
|
|
21
|
+
* @param {string} [config.defaultSonnetModel] - For anthropic: model to test against
|
|
9
22
|
* @param {number} [config.apiTimeoutMs] - API timeout in milliseconds
|
|
10
23
|
* @returns {Promise<{success: boolean, message: string, details?: Object}>}
|
|
11
24
|
*/
|
|
12
25
|
export async function testProviderConnection(config) {
|
|
26
|
+
const { kind = 'anthropic' } = config || {};
|
|
27
|
+
if (kind === 'openai') {
|
|
28
|
+
return testOpenAIConnection(config);
|
|
29
|
+
}
|
|
30
|
+
return testAnthropicConnection(config);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Anthropic-kind connection test (unchanged from pre-kind behavior).
|
|
35
|
+
* @private
|
|
36
|
+
*/
|
|
37
|
+
async function testAnthropicConnection(config) {
|
|
13
38
|
const { baseUrl, authToken, defaultSonnetModel, apiTimeoutMs } = config;
|
|
14
39
|
|
|
15
40
|
try {
|
|
16
|
-
// Build client options
|
|
17
41
|
const clientOptions = {};
|
|
18
42
|
|
|
19
|
-
if (baseUrl)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (authToken) {
|
|
23
|
-
clientOptions.apiKey = authToken;
|
|
24
|
-
}
|
|
25
|
-
if (apiTimeoutMs) {
|
|
26
|
-
clientOptions.timeout = apiTimeoutMs;
|
|
27
|
-
}
|
|
43
|
+
if (baseUrl) clientOptions.baseURL = baseUrl;
|
|
44
|
+
if (authToken) clientOptions.apiKey = authToken;
|
|
45
|
+
if (apiTimeoutMs) clientOptions.timeout = apiTimeoutMs;
|
|
28
46
|
|
|
29
47
|
const client = new Anthropic(clientOptions);
|
|
30
48
|
|
|
31
|
-
// Use a minimal message to test connectivity
|
|
32
|
-
// This verifies: network, auth, and model availability
|
|
49
|
+
// Use a minimal message to test connectivity.
|
|
50
|
+
// This verifies: network, auth, and model availability.
|
|
33
51
|
const testModel = defaultSonnetModel || 'claude-sonnet-4-20250514';
|
|
34
52
|
|
|
35
53
|
const response = await client.messages.create({
|
|
@@ -58,6 +76,88 @@ export async function testProviderConnection(config) {
|
|
|
58
76
|
}
|
|
59
77
|
}
|
|
60
78
|
|
|
79
|
+
/**
|
|
80
|
+
* OpenAI-kind connection test. Tries `models.list()` first; if the endpoint
|
|
81
|
+
* does not implement that (common for chat-only proxies like LM Studio), falls
|
|
82
|
+
* back to a minimal `chat.completions.create({ max_tokens: 1 })`.
|
|
83
|
+
* @private
|
|
84
|
+
*/
|
|
85
|
+
async function testOpenAIConnection(config) {
|
|
86
|
+
try {
|
|
87
|
+
const client = createOpenAIClient(config);
|
|
88
|
+
return await testOpenAIClient(client, config);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return failureResponse(error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function createOpenAIClient(config) {
|
|
95
|
+
const { baseUrl, authToken, apiTimeoutMs } = config;
|
|
96
|
+
const clientOptions = { apiKey: authToken || 'missing' };
|
|
97
|
+
if (baseUrl) clientOptions.baseURL = baseUrl;
|
|
98
|
+
if (apiTimeoutMs) clientOptions.timeout = apiTimeoutMs;
|
|
99
|
+
return new OpenAI(clientOptions);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function testOpenAIClient(client, config) {
|
|
103
|
+
try {
|
|
104
|
+
return await testOpenAIModelsEndpoint(client, config);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error?.status !== 404) throw error;
|
|
107
|
+
return testOpenAIChatEndpoint(client, config);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function testOpenAIModelsEndpoint(client, config) {
|
|
112
|
+
const listResult = await client.models.list();
|
|
113
|
+
const first = pickFirstModel(listResult) || config.defaultSonnetModel || null;
|
|
114
|
+
return connectionSuccess(first ? { model: first } : {});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function testOpenAIChatEndpoint(client, config) {
|
|
118
|
+
const testModel = config.defaultSonnetModel || 'gpt-4o-mini';
|
|
119
|
+
const response = await client.chat.completions.create({
|
|
120
|
+
model: testModel,
|
|
121
|
+
max_tokens: 1,
|
|
122
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
123
|
+
});
|
|
124
|
+
return connectionSuccess({
|
|
125
|
+
model: response?.model || testModel,
|
|
126
|
+
...(response?.usage ? { usage: response.usage } : {}),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function connectionSuccess(details) {
|
|
131
|
+
return {
|
|
132
|
+
success: true,
|
|
133
|
+
message: 'Connection successful',
|
|
134
|
+
details,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function failureResponse(error) {
|
|
139
|
+
return {
|
|
140
|
+
success: false,
|
|
141
|
+
message: getErrorMessage(error),
|
|
142
|
+
details: { code: error.status || error.code, type: error.type || error.name },
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Extract a representative model ID from whatever shape `models.list()` returns.
|
|
148
|
+
* @private
|
|
149
|
+
*/
|
|
150
|
+
function pickFirstModel(listResult) {
|
|
151
|
+
if (!listResult) return null;
|
|
152
|
+
// Newer SDKs expose .data; older ones are plain arrays / async iterables.
|
|
153
|
+
const data = Array.isArray(listResult) ? listResult : listResult.data;
|
|
154
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
155
|
+
const entry = data[0];
|
|
156
|
+
return entry?.id || entry?.model || null;
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
61
161
|
/**
|
|
62
162
|
* Get a human-readable error message from an error object
|
|
63
163
|
* @param {Error} error - The error object
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { resolveAgentTypeFromModel } from './sessionProvider.js';
|
|
2
|
+
|
|
3
|
+
// Human-readable labels used in the cross-kind switch error message.
|
|
4
|
+
export const AGENT_TYPE_LABELS = Object.freeze({
|
|
5
|
+
'claude-code': 'Claude Code',
|
|
6
|
+
codex: 'Codex',
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} agentType
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
export function agentLabel(agentType) {
|
|
14
|
+
return AGENT_TYPE_LABELS[agentType] || agentType || 'unknown';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Cross-kind model switch guard. A session is bound to one agent type for its
|
|
19
|
+
* lifetime. If the caller selected a model that resolves to a different agent
|
|
20
|
+
* than the session's existing agentType, reject BEFORE dispatching
|
|
21
|
+
* continueSession or updating session.model — mixing Claude and Codex
|
|
22
|
+
* mid-conversation would produce a broken resume/context state. Same-kind
|
|
23
|
+
* model changes (sonnet↔opus, gpt-4o↔o1-mini) pass through.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} session - The session row (must include agentType + model).
|
|
26
|
+
* @param {string|null} requestedModel - Model ID from req.body.model, or null.
|
|
27
|
+
* @returns {{ error: string, message: string }|null} 400-body on block, or null to allow.
|
|
28
|
+
*/
|
|
29
|
+
export function checkCrossKindSwitch(session, requestedModel) {
|
|
30
|
+
const sessionAgentType = session.agentType || 'claude-code';
|
|
31
|
+
const effectiveModel = requestedModel || session.model;
|
|
32
|
+
const requestedAgentType = resolveAgentTypeFromModel(effectiveModel);
|
|
33
|
+
if (requestedAgentType === sessionAgentType) return null;
|
|
34
|
+
return {
|
|
35
|
+
error: 'CROSS_KIND_MODEL_SWITCH',
|
|
36
|
+
message: `Cannot switch agent kind mid-session (${agentLabel(sessionAgentType)} → ${agentLabel(requestedAgentType)})`,
|
|
37
|
+
};
|
|
38
|
+
}
|