@ottocode/server 0.1.226 → 0.1.228
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 +3 -3
- package/src/events/types.ts +2 -0
- package/src/hono-context.d.ts +7 -0
- package/src/index.ts +9 -1
- package/src/routes/ask.ts +5 -3
- package/src/routes/config/agents.ts +5 -3
- package/src/routes/config/defaults.ts +3 -3
- package/src/routes/config/main.ts +5 -3
- package/src/routes/config/models.ts +10 -6
- package/src/routes/config/providers.ts +8 -4
- package/src/routes/config/utils.ts +11 -4
- package/src/routes/terminals.ts +6 -4
- package/src/routes/tunnel.ts +1 -1
- package/src/runtime/agent/oauth-codex-continuation.ts +6 -1
- package/src/runtime/agent/runner.ts +32 -10
- package/src/runtime/ask/service.ts +5 -0
- package/src/runtime/errors/api-error.ts +29 -21
- package/src/runtime/message/service.ts +2 -2
- package/src/runtime/session/branch.ts +11 -1
- package/src/runtime/session/manager.ts +6 -1
- package/src/runtime/stream/step-finish.ts +3 -0
- package/src/tools/adapter.ts +4 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ottocode/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.228",
|
|
4
4
|
"description": "HTTP API server for ottocode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"typecheck": "tsc --noEmit"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@ottocode/sdk": "0.1.
|
|
53
|
-
"@ottocode/database": "0.1.
|
|
52
|
+
"@ottocode/sdk": "0.1.228",
|
|
53
|
+
"@ottocode/database": "0.1.228",
|
|
54
54
|
"drizzle-orm": "^0.44.5",
|
|
55
55
|
"hono": "^4.9.9",
|
|
56
56
|
"zod": "^4.3.6"
|
package/src/events/types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type OttoEventType =
|
|
2
2
|
| 'tool.approval.required'
|
|
3
|
+
| 'tool.approval.updated'
|
|
3
4
|
| 'tool.approval.resolved'
|
|
4
5
|
| 'setu.payment.required'
|
|
5
6
|
| 'setu.payment.signing'
|
|
@@ -11,6 +12,7 @@ export type OttoEventType =
|
|
|
11
12
|
| 'setu.fiat.checkout_created'
|
|
12
13
|
| 'setu.balance.updated'
|
|
13
14
|
| 'session.created'
|
|
15
|
+
| 'session.deleted'
|
|
14
16
|
| 'session.updated'
|
|
15
17
|
| 'message.created'
|
|
16
18
|
| 'message.updated'
|
package/src/index.ts
CHANGED
|
@@ -191,6 +191,7 @@ export type EmbeddedAppConfig = {
|
|
|
191
191
|
provider?: ProviderId;
|
|
192
192
|
model?: string;
|
|
193
193
|
agent?: string;
|
|
194
|
+
toolApproval?: 'auto' | 'dangerous' | 'all';
|
|
194
195
|
};
|
|
195
196
|
/** Additional CORS origins for proxies/Tailscale (e.g., ['https://myapp.ts.net', 'https://example.com']) */
|
|
196
197
|
corsOrigins?: string[];
|
|
@@ -202,7 +203,14 @@ export function createEmbeddedApp(config: EmbeddedAppConfig = {}) {
|
|
|
202
203
|
// Store injected config in Hono context for routes to access
|
|
203
204
|
// Config can be empty - routes will fall back to files/env
|
|
204
205
|
honoApp.use('*', async (c, next) => {
|
|
205
|
-
|
|
206
|
+
(
|
|
207
|
+
c as unknown as {
|
|
208
|
+
set: (
|
|
209
|
+
key: 'embeddedConfig',
|
|
210
|
+
value: EmbeddedAppConfig | undefined,
|
|
211
|
+
) => void;
|
|
212
|
+
}
|
|
213
|
+
).set('embeddedConfig', config);
|
|
206
214
|
await next();
|
|
207
215
|
});
|
|
208
216
|
|
package/src/routes/ask.ts
CHANGED
|
@@ -21,9 +21,11 @@ export function registerAskRoutes(app: Hono) {
|
|
|
21
21
|
return c.json({ error: 'Prompt is required.' }, 400);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const embeddedConfig =
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
const embeddedConfig = (
|
|
25
|
+
c as unknown as {
|
|
26
|
+
get: (key: 'embeddedConfig') => EmbeddedAppConfig | undefined;
|
|
27
|
+
}
|
|
28
|
+
).get('embeddedConfig');
|
|
27
29
|
|
|
28
30
|
// Hybrid fallback: Use embedded config if provided, otherwise fall back to files/env
|
|
29
31
|
let injectableConfig: InjectableConfig | undefined;
|
|
@@ -8,9 +8,11 @@ import { discoverAllAgents, getDefault } from './utils.ts';
|
|
|
8
8
|
export function registerAgentsRoute(app: Hono) {
|
|
9
9
|
app.get('/v1/config/agents', async (c) => {
|
|
10
10
|
try {
|
|
11
|
-
const embeddedConfig =
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const embeddedConfig = (
|
|
12
|
+
c as unknown as {
|
|
13
|
+
get: (key: 'embeddedConfig') => EmbeddedAppConfig | undefined;
|
|
14
|
+
}
|
|
15
|
+
).get('embeddedConfig');
|
|
14
16
|
|
|
15
17
|
if (embeddedConfig) {
|
|
16
18
|
const agents = embeddedConfig.agents
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
|
-
import { setConfig, loadConfig } from '@ottocode/sdk';
|
|
2
|
+
import { setConfig, loadConfig, type ProviderId } from '@ottocode/sdk';
|
|
3
3
|
import { logger } from '@ottocode/sdk';
|
|
4
4
|
import { serializeError } from '../../runtime/errors/api-error.ts';
|
|
5
5
|
|
|
@@ -21,7 +21,7 @@ export function registerDefaultsRoute(app: Hono) {
|
|
|
21
21
|
const scope = body.scope || 'global';
|
|
22
22
|
const updates: Partial<{
|
|
23
23
|
agent: string;
|
|
24
|
-
provider:
|
|
24
|
+
provider: ProviderId;
|
|
25
25
|
model: string;
|
|
26
26
|
toolApproval: 'auto' | 'dangerous' | 'all';
|
|
27
27
|
guidedMode: boolean;
|
|
@@ -30,7 +30,7 @@ export function registerDefaultsRoute(app: Hono) {
|
|
|
30
30
|
}> = {};
|
|
31
31
|
|
|
32
32
|
if (body.agent) updates.agent = body.agent;
|
|
33
|
-
if (body.provider) updates.provider = body.provider;
|
|
33
|
+
if (body.provider) updates.provider = body.provider as ProviderId;
|
|
34
34
|
if (body.model) updates.model = body.model;
|
|
35
35
|
if (body.toolApproval) updates.toolApproval = body.toolApproval;
|
|
36
36
|
if (body.guidedMode !== undefined) updates.guidedMode = body.guidedMode;
|
|
@@ -13,9 +13,11 @@ export function registerMainConfigRoute(app: Hono) {
|
|
|
13
13
|
app.get('/v1/config', async (c) => {
|
|
14
14
|
try {
|
|
15
15
|
const projectRoot = c.req.query('project') || process.cwd();
|
|
16
|
-
const embeddedConfig =
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const embeddedConfig = (
|
|
17
|
+
c as unknown as {
|
|
18
|
+
get: (key: 'embeddedConfig') => EmbeddedAppConfig | undefined;
|
|
19
|
+
}
|
|
20
|
+
).get('embeddedConfig');
|
|
19
21
|
|
|
20
22
|
const cfg = await loadConfig(projectRoot);
|
|
21
23
|
|
|
@@ -82,9 +82,11 @@ async function getAuthorizedCopilotModels(
|
|
|
82
82
|
export function registerModelsRoutes(app: Hono) {
|
|
83
83
|
app.get('/v1/config/providers/:provider/models', async (c) => {
|
|
84
84
|
try {
|
|
85
|
-
const embeddedConfig =
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
const embeddedConfig = (
|
|
86
|
+
c as unknown as {
|
|
87
|
+
get: (key: 'embeddedConfig') => EmbeddedAppConfig | undefined;
|
|
88
|
+
}
|
|
89
|
+
).get('embeddedConfig');
|
|
88
90
|
const provider = c.req.param('provider') as ProviderId;
|
|
89
91
|
|
|
90
92
|
const projectRoot = c.req.query('project') || process.cwd();
|
|
@@ -152,9 +154,11 @@ export function registerModelsRoutes(app: Hono) {
|
|
|
152
154
|
|
|
153
155
|
app.get('/v1/config/models', async (c) => {
|
|
154
156
|
try {
|
|
155
|
-
const embeddedConfig =
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
const embeddedConfig = (
|
|
158
|
+
c as unknown as {
|
|
159
|
+
get: (key: 'embeddedConfig') => EmbeddedAppConfig | undefined;
|
|
160
|
+
}
|
|
161
|
+
).get('embeddedConfig');
|
|
158
162
|
|
|
159
163
|
const projectRoot = c.req.query('project') || process.cwd();
|
|
160
164
|
const cfg = await loadConfig(projectRoot);
|
|
@@ -9,14 +9,18 @@ import { getAuthorizedProviders, getDefault } from './utils.ts';
|
|
|
9
9
|
export function registerProvidersRoute(app: Hono) {
|
|
10
10
|
app.get('/v1/config/providers', async (c) => {
|
|
11
11
|
try {
|
|
12
|
-
const embeddedConfig =
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const embeddedConfig = (
|
|
13
|
+
c as unknown as {
|
|
14
|
+
get: (key: 'embeddedConfig') => EmbeddedAppConfig | undefined;
|
|
15
|
+
}
|
|
16
|
+
).get('embeddedConfig');
|
|
15
17
|
|
|
16
18
|
if (embeddedConfig) {
|
|
17
19
|
const providers = embeddedConfig.auth
|
|
18
20
|
? (Object.keys(embeddedConfig.auth) as ProviderId[])
|
|
19
|
-
:
|
|
21
|
+
: embeddedConfig.provider
|
|
22
|
+
? [embeddedConfig.provider]
|
|
23
|
+
: [];
|
|
20
24
|
|
|
21
25
|
return c.json({
|
|
22
26
|
providers,
|
|
@@ -63,7 +63,8 @@ export async function getAuthTypeForProvider(
|
|
|
63
63
|
projectRoot: string,
|
|
64
64
|
): Promise<'api' | 'oauth' | 'wallet' | undefined> {
|
|
65
65
|
if (embeddedConfig?.auth?.[provider]) {
|
|
66
|
-
|
|
66
|
+
const embeddedAuth = embeddedConfig.auth[provider];
|
|
67
|
+
return 'type' in embeddedAuth ? embeddedAuth.type : 'api';
|
|
67
68
|
}
|
|
68
69
|
const auth = await getAuth(provider, projectRoot);
|
|
69
70
|
return auth?.type as 'api' | 'oauth' | 'wallet' | undefined;
|
|
@@ -83,7 +84,9 @@ export async function discoverAllAgents(
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
} catch (err) {
|
|
86
|
-
logger.debug('Failed to load agents.json',
|
|
87
|
+
logger.debug('Failed to load agents.json', {
|
|
88
|
+
error: err instanceof Error ? err.message : String(err),
|
|
89
|
+
});
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
try {
|
|
@@ -98,7 +101,9 @@ export async function discoverAllAgents(
|
|
|
98
101
|
}
|
|
99
102
|
}
|
|
100
103
|
} catch (err) {
|
|
101
|
-
logger.debug('Failed to read local agents directory',
|
|
104
|
+
logger.debug('Failed to read local agents directory', {
|
|
105
|
+
error: err instanceof Error ? err.message : String(err),
|
|
106
|
+
});
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
try {
|
|
@@ -113,7 +118,9 @@ export async function discoverAllAgents(
|
|
|
113
118
|
}
|
|
114
119
|
}
|
|
115
120
|
} catch (err) {
|
|
116
|
-
logger.debug('Failed to read global agents directory',
|
|
121
|
+
logger.debug('Failed to read global agents directory', {
|
|
122
|
+
error: err instanceof Error ? err.message : String(err),
|
|
123
|
+
});
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
return Array.from(agentSet).sort();
|
package/src/routes/terminals.ts
CHANGED
|
@@ -87,12 +87,14 @@ export function registerTerminalsRoutes(
|
|
|
87
87
|
return c.json({ error: 'Terminal not found' }, 404);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
const activeTerminal = terminal;
|
|
91
|
+
|
|
90
92
|
return streamSSE(c, async (stream) => {
|
|
91
93
|
logger.debug('SSE stream started for terminal', { id });
|
|
92
94
|
// Send historical buffer first (unless skipHistory is set)
|
|
93
95
|
const skipHistory = c.req.query('skipHistory') === 'true';
|
|
94
96
|
if (!skipHistory) {
|
|
95
|
-
const history =
|
|
97
|
+
const history = activeTerminal.read();
|
|
96
98
|
logger.debug('SSE sending terminal history', {
|
|
97
99
|
id,
|
|
98
100
|
lines: history.length,
|
|
@@ -120,8 +122,8 @@ export function registerTerminalsRoutes(
|
|
|
120
122
|
let finished = false;
|
|
121
123
|
|
|
122
124
|
function cleanup() {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
+
activeTerminal.removeDataListener(onData);
|
|
126
|
+
activeTerminal.removeExitListener(onExit);
|
|
125
127
|
c.req.raw.signal.removeEventListener('abort', onAbort);
|
|
126
128
|
}
|
|
127
129
|
|
|
@@ -145,7 +147,7 @@ export function registerTerminalsRoutes(
|
|
|
145
147
|
|
|
146
148
|
function onAbort() {
|
|
147
149
|
logger.debug('SSE client disconnected from terminal', {
|
|
148
|
-
id:
|
|
150
|
+
id: activeTerminal.id,
|
|
149
151
|
});
|
|
150
152
|
stream.close();
|
|
151
153
|
finish();
|
package/src/routes/tunnel.ts
CHANGED
|
@@ -59,7 +59,7 @@ export function registerTunnelRoutes(app: Hono) {
|
|
|
59
59
|
|
|
60
60
|
const url = await activeTunnel.start(port, (msg) => {
|
|
61
61
|
progressMessage = msg;
|
|
62
|
-
logger.debug('Tunnel progress
|
|
62
|
+
logger.debug('Tunnel progress', { message: msg });
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
tunnelUrl = url;
|
|
@@ -10,6 +10,7 @@ export type OauthCodexContinuationInput = {
|
|
|
10
10
|
firstToolSeen: boolean;
|
|
11
11
|
hasTrailingAssistantText: boolean;
|
|
12
12
|
endedWithToolActivity?: boolean;
|
|
13
|
+
lastToolName?: string;
|
|
13
14
|
droppedPseudoToolText: boolean;
|
|
14
15
|
lastAssistantText: string;
|
|
15
16
|
};
|
|
@@ -54,7 +55,7 @@ const MAX_UNCLEAN_EOF_RETRIES = 1;
|
|
|
54
55
|
|
|
55
56
|
function isUncleanEof(input: OauthCodexContinuationInput): boolean {
|
|
56
57
|
if (input.finishReason && input.finishReason !== 'unknown') return false;
|
|
57
|
-
if (input
|
|
58
|
+
if (isMissingAssistantSummary(input)) return true;
|
|
58
59
|
if (looksLikeIntermediateProgressText(input.lastAssistantText)) return true;
|
|
59
60
|
return false;
|
|
60
61
|
}
|
|
@@ -82,6 +83,10 @@ export function decideOauthCodexContinuation(
|
|
|
82
83
|
return { shouldContinue: true, reason: 'truncated' };
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
if (input.lastToolName === 'finish') {
|
|
87
|
+
return { shouldContinue: false };
|
|
88
|
+
}
|
|
89
|
+
|
|
85
90
|
if (input.endedWithToolActivity) {
|
|
86
91
|
return { shouldContinue: true, reason: 'ended-on-tool-activity' };
|
|
87
92
|
}
|
|
@@ -182,11 +182,23 @@ async function runAssistant(opts: RunOpts) {
|
|
|
182
182
|
let _finishObserved = false;
|
|
183
183
|
let _toolActivityObserved = false;
|
|
184
184
|
let _trailingAssistantTextAfterTool = false;
|
|
185
|
+
let _endedWithToolActivity = false;
|
|
186
|
+
let _lastToolName: string | undefined;
|
|
185
187
|
let _abortedByUser = false;
|
|
188
|
+
let titleGenerationTriggered = false;
|
|
186
189
|
const unsubscribeFinish = subscribe(opts.sessionId, (evt) => {
|
|
187
190
|
if (evt.type === 'tool.call' || evt.type === 'tool.result') {
|
|
188
191
|
_toolActivityObserved = true;
|
|
189
192
|
_trailingAssistantTextAfterTool = false;
|
|
193
|
+
_endedWithToolActivity = true;
|
|
194
|
+
try {
|
|
195
|
+
_lastToolName = (evt.payload as { name?: string } | undefined)?.name;
|
|
196
|
+
} catch {
|
|
197
|
+
_lastToolName = undefined;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (evt.type === 'tool.call') {
|
|
201
|
+
triggerTitleGenerationWhenReady();
|
|
190
202
|
}
|
|
191
203
|
if (evt.type !== 'tool.result') return;
|
|
192
204
|
try {
|
|
@@ -222,6 +234,22 @@ async function runAssistant(opts: RunOpts) {
|
|
|
222
234
|
stepIndex += 1;
|
|
223
235
|
return stepIndex;
|
|
224
236
|
};
|
|
237
|
+
const triggerTitleGenerationWhenReady = () => {
|
|
238
|
+
if (titleGenerationTriggered) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
titleGenerationTriggered = true;
|
|
243
|
+
if (!isFirstMessage) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
void triggerDeferredTitleGeneration({
|
|
248
|
+
cfg,
|
|
249
|
+
db,
|
|
250
|
+
sessionId: opts.sessionId,
|
|
251
|
+
});
|
|
252
|
+
};
|
|
225
253
|
|
|
226
254
|
const reasoningStates = new Map<string, ReasoningState>();
|
|
227
255
|
|
|
@@ -233,6 +261,7 @@ async function runAssistant(opts: RunOpts) {
|
|
|
233
261
|
getCurrentPartId,
|
|
234
262
|
updateCurrentPartId,
|
|
235
263
|
updateAccumulated,
|
|
264
|
+
triggerTitleGenerationWhenReady,
|
|
236
265
|
sharedCtx,
|
|
237
266
|
updateSessionTokensIncremental,
|
|
238
267
|
updateMessageTokensIncremental,
|
|
@@ -306,6 +335,7 @@ async function runAssistant(opts: RunOpts) {
|
|
|
306
335
|
(delta.trim().length > 0 && firstToolSeen())
|
|
307
336
|
) {
|
|
308
337
|
_trailingAssistantTextAfterTool = true;
|
|
338
|
+
_endedWithToolActivity = false;
|
|
309
339
|
}
|
|
310
340
|
|
|
311
341
|
if (!currentPartId && !accumulated.trim()) {
|
|
@@ -315,13 +345,6 @@ async function runAssistant(opts: RunOpts) {
|
|
|
315
345
|
if (!firstDeltaSeen) {
|
|
316
346
|
firstDeltaSeen = true;
|
|
317
347
|
streamStartTimer.end();
|
|
318
|
-
if (isFirstMessage) {
|
|
319
|
-
void triggerDeferredTitleGeneration({
|
|
320
|
-
cfg,
|
|
321
|
-
db,
|
|
322
|
-
sessionId: opts.sessionId,
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
348
|
}
|
|
326
349
|
|
|
327
350
|
if (!currentPartId) {
|
|
@@ -429,8 +452,6 @@ async function runAssistant(opts: RunOpts) {
|
|
|
429
452
|
|
|
430
453
|
const MAX_CONTINUATIONS = 6;
|
|
431
454
|
const continuationCount = opts.continuationCount ?? 0;
|
|
432
|
-
const endedWithToolActivity =
|
|
433
|
-
_toolActivityObserved && !_trailingAssistantTextAfterTool;
|
|
434
455
|
const continuationDecision = decideOauthCodexContinuation({
|
|
435
456
|
provider: opts.provider,
|
|
436
457
|
isOpenAIOAuth,
|
|
@@ -442,7 +463,8 @@ async function runAssistant(opts: RunOpts) {
|
|
|
442
463
|
rawFinishReason: streamRawFinishReason,
|
|
443
464
|
firstToolSeen: fs,
|
|
444
465
|
hasTrailingAssistantText: _trailingAssistantTextAfterTool,
|
|
445
|
-
endedWithToolActivity,
|
|
466
|
+
endedWithToolActivity: _endedWithToolActivity,
|
|
467
|
+
lastToolName: _lastToolName,
|
|
446
468
|
droppedPseudoToolText: oauthTextGuard?.dropped ?? false,
|
|
447
469
|
lastAssistantText: latestAssistantText,
|
|
448
470
|
});
|
|
@@ -127,7 +127,12 @@ async function processAskRequest(
|
|
|
127
127
|
google: { enabled: true },
|
|
128
128
|
openrouter: { enabled: true },
|
|
129
129
|
opencode: { enabled: true },
|
|
130
|
+
copilot: { enabled: true },
|
|
130
131
|
setu: { enabled: true },
|
|
132
|
+
zai: { enabled: true },
|
|
133
|
+
'zai-coding': { enabled: true },
|
|
134
|
+
moonshot: { enabled: true },
|
|
135
|
+
minimax: { enabled: true },
|
|
131
136
|
},
|
|
132
137
|
paths: {
|
|
133
138
|
dataDir: `${projectRoot}/.otto`,
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* across all API endpoints.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
|
8
9
|
import { isDebugEnabled } from '../debug/state.ts';
|
|
9
10
|
import { toErrorPayload } from './handling.ts';
|
|
10
11
|
|
|
@@ -16,7 +17,7 @@ export type APIErrorResponse = {
|
|
|
16
17
|
message: string;
|
|
17
18
|
type: string;
|
|
18
19
|
code?: string;
|
|
19
|
-
status?:
|
|
20
|
+
status?: ContentfulStatusCode;
|
|
20
21
|
details?: Record<string, unknown>;
|
|
21
22
|
stack?: string;
|
|
22
23
|
};
|
|
@@ -27,29 +28,33 @@ export type APIErrorResponse = {
|
|
|
27
28
|
*/
|
|
28
29
|
export class APIError extends Error {
|
|
29
30
|
public readonly code?: string;
|
|
30
|
-
public readonly status:
|
|
31
|
+
public readonly status: ContentfulStatusCode;
|
|
31
32
|
public readonly type: string;
|
|
32
33
|
public readonly details?: Record<string, unknown>;
|
|
33
34
|
|
|
34
35
|
constructor(
|
|
35
36
|
message: string,
|
|
36
|
-
options?:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
options?:
|
|
38
|
+
| ContentfulStatusCode
|
|
39
|
+
| {
|
|
40
|
+
code?: string;
|
|
41
|
+
status?: ContentfulStatusCode;
|
|
42
|
+
type?: string;
|
|
43
|
+
details?: Record<string, unknown>;
|
|
44
|
+
cause?: unknown;
|
|
45
|
+
},
|
|
43
46
|
) {
|
|
44
47
|
super(message);
|
|
45
48
|
this.name = 'APIError';
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
this.
|
|
49
|
-
this.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
const normalizedOptions =
|
|
50
|
+
typeof options === 'number' ? { status: options } : options;
|
|
51
|
+
this.code = normalizedOptions?.code;
|
|
52
|
+
this.status = normalizedOptions?.status ?? 500;
|
|
53
|
+
this.type = normalizedOptions?.type ?? 'api_error';
|
|
54
|
+
this.details = normalizedOptions?.details;
|
|
55
|
+
|
|
56
|
+
if (normalizedOptions?.cause) {
|
|
57
|
+
this.cause = normalizedOptions.cause;
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
// Maintain proper stack trace
|
|
@@ -72,7 +77,7 @@ export function serializeError(err: unknown): APIErrorResponse {
|
|
|
72
77
|
// Determine HTTP status code
|
|
73
78
|
// Default to 400 for generic errors (client errors)
|
|
74
79
|
// Only use 500 if explicitly set or for APIError instances without a status
|
|
75
|
-
let status = 400;
|
|
80
|
+
let status: ContentfulStatusCode = 400;
|
|
76
81
|
|
|
77
82
|
// Handle APIError instances first
|
|
78
83
|
if (err instanceof APIError) {
|
|
@@ -80,15 +85,16 @@ export function serializeError(err: unknown): APIErrorResponse {
|
|
|
80
85
|
} else if (err && typeof err === 'object') {
|
|
81
86
|
const errObj = err as Record<string, unknown>;
|
|
82
87
|
if (typeof errObj.status === 'number') {
|
|
83
|
-
status = errObj.status;
|
|
88
|
+
status = errObj.status as ContentfulStatusCode;
|
|
84
89
|
} else if (typeof errObj.statusCode === 'number') {
|
|
85
|
-
status = errObj.statusCode;
|
|
90
|
+
status = errObj.statusCode as ContentfulStatusCode;
|
|
86
91
|
} else if (
|
|
87
92
|
errObj.details &&
|
|
88
93
|
typeof errObj.details === 'object' &&
|
|
89
94
|
typeof (errObj.details as Record<string, unknown>).statusCode === 'number'
|
|
90
95
|
) {
|
|
91
|
-
status = (errObj.details as Record<string, unknown>)
|
|
96
|
+
status = (errObj.details as Record<string, unknown>)
|
|
97
|
+
.statusCode as ContentfulStatusCode;
|
|
92
98
|
}
|
|
93
99
|
}
|
|
94
100
|
|
|
@@ -130,7 +136,9 @@ export function serializeError(err: unknown): APIErrorResponse {
|
|
|
130
136
|
* @param err - The error to convert
|
|
131
137
|
* @returns Tuple of [APIErrorResponse, HTTP status code]
|
|
132
138
|
*/
|
|
133
|
-
export function createErrorResponse(
|
|
139
|
+
export function createErrorResponse(
|
|
140
|
+
err: unknown,
|
|
141
|
+
): [APIErrorResponse, ContentfulStatusCode] {
|
|
134
142
|
const response = serializeError(err);
|
|
135
143
|
return [response, response.error.status ?? 500];
|
|
136
144
|
}
|
|
@@ -284,7 +284,7 @@ async function generateSessionTitle(args: {
|
|
|
284
284
|
return;
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
const provider = sess.provider ?? cfg.defaults.provider;
|
|
287
|
+
const provider = (sess.provider ?? cfg.defaults.provider) as ProviderId;
|
|
288
288
|
const modelName = sess.model ?? cfg.defaults.model;
|
|
289
289
|
|
|
290
290
|
debugLog('[TITLE_GEN] Generating title for session');
|
|
@@ -365,7 +365,7 @@ Output ONLY the title, nothing else.`;
|
|
|
365
365
|
|
|
366
366
|
await db
|
|
367
367
|
.update(sessions)
|
|
368
|
-
.set({ title: sanitized,
|
|
368
|
+
.set({ title: sanitized, lastActiveAt: Date.now() })
|
|
369
369
|
.where(eq(sessions.id, sessionId));
|
|
370
370
|
|
|
371
371
|
debugLog(`[TITLE_GEN] Setting final title: "${sanitized}"`);
|
|
@@ -160,7 +160,14 @@ export async function createBranch({
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
const result: SessionRow = {
|
|
163
|
-
|
|
163
|
+
id: newSession.id,
|
|
164
|
+
title: newSession.title ?? null,
|
|
165
|
+
agent: newSession.agent,
|
|
166
|
+
provider: newSession.provider,
|
|
167
|
+
model: newSession.model,
|
|
168
|
+
projectPath: newSession.projectPath,
|
|
169
|
+
createdAt: newSession.createdAt,
|
|
170
|
+
lastActiveAt: newSession.lastActiveAt ?? null,
|
|
164
171
|
totalInputTokens: null,
|
|
165
172
|
totalOutputTokens: null,
|
|
166
173
|
totalCachedTokens: null,
|
|
@@ -171,6 +178,9 @@ export async function createBranch({
|
|
|
171
178
|
currentContextTokens: null,
|
|
172
179
|
contextSummary: null,
|
|
173
180
|
lastCompactedAt: null,
|
|
181
|
+
parentSessionId: newSession.parentSessionId ?? null,
|
|
182
|
+
branchPointMessageId: newSession.branchPointMessageId ?? null,
|
|
183
|
+
sessionType: newSession.sessionType ?? null,
|
|
174
184
|
};
|
|
175
185
|
|
|
176
186
|
publish({
|
|
@@ -39,7 +39,7 @@ export async function createSession({
|
|
|
39
39
|
await ensureProviderEnv(cfg, provider);
|
|
40
40
|
const id = crypto.randomUUID();
|
|
41
41
|
const now = Date.now();
|
|
42
|
-
const row = {
|
|
42
|
+
const row: SessionRow = {
|
|
43
43
|
id,
|
|
44
44
|
title: title ?? null,
|
|
45
45
|
agent,
|
|
@@ -56,6 +56,11 @@ export async function createSession({
|
|
|
56
56
|
totalToolTimeMs: null,
|
|
57
57
|
toolCountsJson: null,
|
|
58
58
|
currentContextTokens: null,
|
|
59
|
+
contextSummary: null,
|
|
60
|
+
lastCompactedAt: null,
|
|
61
|
+
parentSessionId: null,
|
|
62
|
+
branchPointMessageId: null,
|
|
63
|
+
sessionType: 'main',
|
|
59
64
|
};
|
|
60
65
|
await db.insert(sessions).values(row);
|
|
61
66
|
publish({ type: 'session.created', sessionId: id, payload: row });
|
|
@@ -16,6 +16,7 @@ export function createStepFinishHandler(
|
|
|
16
16
|
getCurrentPartId: () => string | null,
|
|
17
17
|
updateCurrentPartId: (id: string | null) => void,
|
|
18
18
|
updateAccumulated: (text: string) => void,
|
|
19
|
+
triggerTitleGenerationWhenReady: () => void,
|
|
19
20
|
sharedCtx: ToolAdapterContext,
|
|
20
21
|
updateSessionTokensIncrementalFn: (
|
|
21
22
|
usage: UsageData,
|
|
@@ -31,6 +32,8 @@ export function createStepFinishHandler(
|
|
|
31
32
|
) => Promise<void>,
|
|
32
33
|
) {
|
|
33
34
|
return async (step: StepFinishEvent) => {
|
|
35
|
+
triggerTitleGenerationWhenReady();
|
|
36
|
+
|
|
34
37
|
const finishedAt = Date.now();
|
|
35
38
|
const currentPartId = getCurrentPartId();
|
|
36
39
|
const stepIndex = getStepIndex();
|
package/src/tools/adapter.ts
CHANGED
|
@@ -130,12 +130,13 @@ export function adaptTools(
|
|
|
130
130
|
}: {
|
|
131
131
|
callId?: string;
|
|
132
132
|
startTs?: number;
|
|
133
|
-
stepIndexForEvent
|
|
133
|
+
stepIndexForEvent?: number;
|
|
134
134
|
args?: unknown;
|
|
135
135
|
},
|
|
136
136
|
) => {
|
|
137
137
|
const resultPartId = crypto.randomUUID();
|
|
138
138
|
const endTs = Date.now();
|
|
139
|
+
const effectiveStepIndex = stepIndexForEvent ?? ctx.stepIndex;
|
|
139
140
|
const dur =
|
|
140
141
|
typeof startTs === 'number' ? Math.max(0, endTs - startTs) : null;
|
|
141
142
|
|
|
@@ -160,7 +161,7 @@ export function adaptTools(
|
|
|
160
161
|
id: resultPartId,
|
|
161
162
|
messageId: ctx.messageId,
|
|
162
163
|
index,
|
|
163
|
-
stepIndex:
|
|
164
|
+
stepIndex: effectiveStepIndex,
|
|
164
165
|
type: 'tool_result',
|
|
165
166
|
content: JSON.stringify(contentObj),
|
|
166
167
|
agent: ctx.agent,
|
|
@@ -176,7 +177,7 @@ export function adaptTools(
|
|
|
176
177
|
publish({
|
|
177
178
|
type: 'tool.result',
|
|
178
179
|
sessionId: ctx.sessionId,
|
|
179
|
-
payload: { ...contentObj, stepIndex:
|
|
180
|
+
payload: { ...contentObj, stepIndex: effectiveStepIndex },
|
|
180
181
|
});
|
|
181
182
|
};
|
|
182
183
|
|