@ottocode/server 0.1.173

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/package.json +42 -0
  2. package/src/events/bus.ts +43 -0
  3. package/src/events/types.ts +32 -0
  4. package/src/index.ts +281 -0
  5. package/src/openapi/helpers.ts +64 -0
  6. package/src/openapi/paths/ask.ts +70 -0
  7. package/src/openapi/paths/config.ts +218 -0
  8. package/src/openapi/paths/files.ts +72 -0
  9. package/src/openapi/paths/git.ts +457 -0
  10. package/src/openapi/paths/messages.ts +92 -0
  11. package/src/openapi/paths/sessions.ts +90 -0
  12. package/src/openapi/paths/setu.ts +154 -0
  13. package/src/openapi/paths/stream.ts +26 -0
  14. package/src/openapi/paths/terminals.ts +226 -0
  15. package/src/openapi/schemas.ts +345 -0
  16. package/src/openapi/spec.ts +49 -0
  17. package/src/presets.ts +85 -0
  18. package/src/routes/ask.ts +113 -0
  19. package/src/routes/auth.ts +592 -0
  20. package/src/routes/branch.ts +106 -0
  21. package/src/routes/config/agents.ts +44 -0
  22. package/src/routes/config/cwd.ts +21 -0
  23. package/src/routes/config/defaults.ts +45 -0
  24. package/src/routes/config/index.ts +16 -0
  25. package/src/routes/config/main.ts +73 -0
  26. package/src/routes/config/models.ts +139 -0
  27. package/src/routes/config/providers.ts +46 -0
  28. package/src/routes/config/utils.ts +120 -0
  29. package/src/routes/files.ts +218 -0
  30. package/src/routes/git/branch.ts +75 -0
  31. package/src/routes/git/commit.ts +209 -0
  32. package/src/routes/git/diff.ts +137 -0
  33. package/src/routes/git/index.ts +18 -0
  34. package/src/routes/git/push.ts +160 -0
  35. package/src/routes/git/schemas.ts +48 -0
  36. package/src/routes/git/staging.ts +208 -0
  37. package/src/routes/git/status.ts +83 -0
  38. package/src/routes/git/types.ts +31 -0
  39. package/src/routes/git/utils.ts +249 -0
  40. package/src/routes/openapi.ts +6 -0
  41. package/src/routes/research.ts +392 -0
  42. package/src/routes/root.ts +5 -0
  43. package/src/routes/session-approval.ts +63 -0
  44. package/src/routes/session-files.ts +387 -0
  45. package/src/routes/session-messages.ts +170 -0
  46. package/src/routes/session-stream.ts +61 -0
  47. package/src/routes/sessions.ts +814 -0
  48. package/src/routes/setu.ts +346 -0
  49. package/src/routes/terminals.ts +227 -0
  50. package/src/runtime/agent/registry.ts +351 -0
  51. package/src/runtime/agent/runner-reasoning.ts +108 -0
  52. package/src/runtime/agent/runner-setup.ts +257 -0
  53. package/src/runtime/agent/runner.ts +375 -0
  54. package/src/runtime/agent-registry.ts +6 -0
  55. package/src/runtime/ask/service.ts +369 -0
  56. package/src/runtime/context/environment.ts +202 -0
  57. package/src/runtime/debug/index.ts +117 -0
  58. package/src/runtime/debug/state.ts +140 -0
  59. package/src/runtime/errors/api-error.ts +192 -0
  60. package/src/runtime/errors/handling.ts +199 -0
  61. package/src/runtime/message/compaction-auto.ts +154 -0
  62. package/src/runtime/message/compaction-context.ts +101 -0
  63. package/src/runtime/message/compaction-detect.ts +26 -0
  64. package/src/runtime/message/compaction-limits.ts +37 -0
  65. package/src/runtime/message/compaction-mark.ts +111 -0
  66. package/src/runtime/message/compaction-prune.ts +75 -0
  67. package/src/runtime/message/compaction.ts +21 -0
  68. package/src/runtime/message/history-builder.ts +266 -0
  69. package/src/runtime/message/service.ts +468 -0
  70. package/src/runtime/message/tool-history-tracker.ts +204 -0
  71. package/src/runtime/prompt/builder.ts +167 -0
  72. package/src/runtime/provider/anthropic.ts +50 -0
  73. package/src/runtime/provider/copilot.ts +12 -0
  74. package/src/runtime/provider/google.ts +8 -0
  75. package/src/runtime/provider/index.ts +60 -0
  76. package/src/runtime/provider/moonshot.ts +8 -0
  77. package/src/runtime/provider/oauth-adapter.ts +237 -0
  78. package/src/runtime/provider/openai.ts +18 -0
  79. package/src/runtime/provider/opencode.ts +7 -0
  80. package/src/runtime/provider/openrouter.ts +7 -0
  81. package/src/runtime/provider/selection.ts +118 -0
  82. package/src/runtime/provider/setu.ts +126 -0
  83. package/src/runtime/provider/zai.ts +16 -0
  84. package/src/runtime/session/branch.ts +280 -0
  85. package/src/runtime/session/db-operations.ts +285 -0
  86. package/src/runtime/session/manager.ts +99 -0
  87. package/src/runtime/session/queue.ts +243 -0
  88. package/src/runtime/stream/abort-handler.ts +65 -0
  89. package/src/runtime/stream/error-handler.ts +371 -0
  90. package/src/runtime/stream/finish-handler.ts +101 -0
  91. package/src/runtime/stream/handlers.ts +5 -0
  92. package/src/runtime/stream/step-finish.ts +93 -0
  93. package/src/runtime/stream/types.ts +25 -0
  94. package/src/runtime/tools/approval.ts +180 -0
  95. package/src/runtime/tools/context.ts +83 -0
  96. package/src/runtime/tools/mapping.ts +154 -0
  97. package/src/runtime/tools/setup.ts +44 -0
  98. package/src/runtime/topup/manager.ts +110 -0
  99. package/src/runtime/utils/cwd.ts +69 -0
  100. package/src/runtime/utils/token.ts +35 -0
  101. package/src/tools/adapter.ts +634 -0
  102. package/src/tools/database/get-parent-session.ts +183 -0
  103. package/src/tools/database/get-session-context.ts +161 -0
  104. package/src/tools/database/index.ts +42 -0
  105. package/src/tools/database/present-session-links.ts +47 -0
  106. package/src/tools/database/query-messages.ts +160 -0
  107. package/src/tools/database/query-sessions.ts +126 -0
  108. package/src/tools/database/search-history.ts +135 -0
  109. package/src/types/sql-imports.d.ts +5 -0
  110. package/sst-env.d.ts +8 -0
  111. package/tsconfig.json +7 -0
@@ -0,0 +1,371 @@
1
+ import type { getDb } from '@ottocode/database';
2
+ import { messages, messageParts } from '@ottocode/database/schema';
3
+ import { eq } from 'drizzle-orm';
4
+ import { APICallError } from 'ai';
5
+ import { publish } from '../../events/bus.ts';
6
+ import { toErrorPayload } from '../errors/handling.ts';
7
+ import type { RunOpts } from '../session/queue.ts';
8
+ import type { ToolAdapterContext } from '../../tools/adapter.ts';
9
+ import { pruneSession, performAutoCompaction } from '../message/compaction.ts';
10
+ import { debugLog } from '../debug/index.ts';
11
+ import { enqueueAssistantRun } from '../session/queue.ts';
12
+ import { clearPendingTopup } from '../topup/manager.ts';
13
+
14
+ export function createErrorHandler(
15
+ opts: RunOpts,
16
+ db: Awaited<ReturnType<typeof getDb>>,
17
+ getStepIndex: () => number,
18
+ sharedCtx: ToolAdapterContext,
19
+ retryCallback?: (sessionId: string) => Promise<void>,
20
+ ) {
21
+ return async (err: unknown) => {
22
+ const errorPayload = toErrorPayload(err);
23
+ const isApiError = APICallError.isInstance(err);
24
+ const stepIndex = getStepIndex();
25
+
26
+ const errObj = err as Record<string, unknown>;
27
+ const nestedError = (errObj?.error as Record<string, unknown>)?.error as
28
+ | Record<string, unknown>
29
+ | undefined;
30
+ const causeError = errObj?.cause as Record<string, unknown> | undefined;
31
+
32
+ // Check for SETU_FIAT_SELECTED code specifically (not string matching)
33
+ const errorCode =
34
+ (errObj?.code as string) ??
35
+ ((errObj?.error as Record<string, unknown>)?.code as string) ??
36
+ ((
37
+ (errObj?.error as Record<string, unknown>)?.error as Record<
38
+ string,
39
+ unknown
40
+ >
41
+ )?.code as string) ??
42
+ ((errObj?.data as Record<string, unknown>)?.code as string) ??
43
+ ((errObj?.cause as Record<string, unknown>)?.code as string) ??
44
+ ((
45
+ (errObj?.cause as Record<string, unknown>)?.error as Record<
46
+ string,
47
+ unknown
48
+ >
49
+ )?.code as string) ??
50
+ (nestedError?.code as string) ??
51
+ (causeError?.code as string) ??
52
+ '';
53
+
54
+ // Also check error message for the exact fiat selection message
55
+ const errorMessage =
56
+ (errObj?.message as string) ??
57
+ ((errObj?.error as Record<string, unknown>)?.message as string) ??
58
+ ((
59
+ (errObj?.error as Record<string, unknown>)?.error as Record<
60
+ string,
61
+ unknown
62
+ >
63
+ )?.message as string) ??
64
+ ((errObj?.data as Record<string, unknown>)?.message as string) ??
65
+ ((errObj?.cause as Record<string, unknown>)?.message as string) ??
66
+ ((
67
+ (errObj?.cause as Record<string, unknown>)?.error as Record<
68
+ string,
69
+ unknown
70
+ >
71
+ )?.message as string) ??
72
+ (nestedError?.message as string) ??
73
+ (causeError?.message as string) ??
74
+ '';
75
+
76
+ const isFiatSelected = errorCode === 'SETU_FIAT_SELECTED';
77
+
78
+ // Handle fiat payment selected - this is not an error, just a signal to pause
79
+ if (isFiatSelected) {
80
+ debugLog('[stream-handlers] Fiat payment selected, pausing request');
81
+ clearPendingTopup(opts.sessionId);
82
+
83
+ // Add a helpful message part telling user to complete payment
84
+ const partId = crypto.randomUUID();
85
+ await db.insert(messageParts).values({
86
+ id: partId,
87
+ messageId: opts.assistantMessageId,
88
+ index: await sharedCtx.nextIndex(),
89
+ stepIndex: getStepIndex(),
90
+ type: 'error',
91
+ content: JSON.stringify({
92
+ message: 'Balance too low — Complete your top-up, then retry.',
93
+ type: 'balance_low',
94
+ errorType: 'balance_low',
95
+ isRetryable: true,
96
+ }),
97
+ agent: opts.agent,
98
+ provider: opts.provider,
99
+ model: opts.model,
100
+ startedAt: Date.now(),
101
+ completedAt: Date.now(),
102
+ });
103
+
104
+ // Mark the message as completed (not error, not pending)
105
+ await db
106
+ .update(messages)
107
+ .set({
108
+ status: 'complete',
109
+ completedAt: Date.now(),
110
+ error: null,
111
+ errorType: null,
112
+ errorDetails: null,
113
+ })
114
+ .where(eq(messages.id, opts.assistantMessageId));
115
+
116
+ // Emit the message part
117
+ publish({
118
+ type: 'message.part.delta',
119
+ sessionId: opts.sessionId,
120
+ payload: {
121
+ messageId: opts.assistantMessageId,
122
+ partId,
123
+ type: 'error',
124
+ content: JSON.stringify({
125
+ message: 'Balance too low — Complete your top-up, then retry.',
126
+ type: 'balance_low',
127
+ errorType: 'balance_low',
128
+ isRetryable: true,
129
+ }),
130
+ },
131
+ });
132
+
133
+ // Emit message completed
134
+ publish({
135
+ type: 'message.completed',
136
+ sessionId: opts.sessionId,
137
+ payload: {
138
+ id: opts.assistantMessageId,
139
+ fiatTopupRequired: true,
140
+ },
141
+ });
142
+
143
+ // Emit a special event so UI knows to show topup modal
144
+ publish({
145
+ type: 'setu.fiat.checkout_created',
146
+ sessionId: opts.sessionId,
147
+ payload: {
148
+ messageId: opts.assistantMessageId,
149
+ needsTopup: true,
150
+ },
151
+ });
152
+
153
+ return;
154
+ }
155
+
156
+ const errorType =
157
+ (errObj?.apiErrorType as string) ?? (nestedError?.type as string) ?? '';
158
+ const fullErrorStrLower = JSON.stringify(err).toLowerCase();
159
+
160
+ const isPromptTooLong =
161
+ fullErrorStrLower.includes('prompt is too long') ||
162
+ fullErrorStrLower.includes('maximum context length') ||
163
+ fullErrorStrLower.includes('too many tokens') ||
164
+ fullErrorStrLower.includes('context_length_exceeded') ||
165
+ fullErrorStrLower.includes('request too large') ||
166
+ fullErrorStrLower.includes('exceeds the model') ||
167
+ fullErrorStrLower.includes('exceeds the limit') ||
168
+ fullErrorStrLower.includes('prompt token count') ||
169
+ fullErrorStrLower.includes('context window') ||
170
+ fullErrorStrLower.includes('input is too long') ||
171
+ errorCode === 'context_length_exceeded' ||
172
+ errorType === 'invalid_request_error';
173
+
174
+ debugLog(
175
+ `[stream-handlers] isPromptTooLong: ${isPromptTooLong}, errorCode: ${errorCode}, errorType: ${errorType}`,
176
+ );
177
+
178
+ if (isPromptTooLong && !opts.isCompactCommand) {
179
+ debugLog(
180
+ '[stream-handlers] Prompt too long detected, auto-compacting...',
181
+ );
182
+
183
+ const retries = opts.compactionRetries ?? 0;
184
+ if (retries >= 2) {
185
+ debugLog(
186
+ '[stream-handlers] Compaction retry limit reached, surfacing error',
187
+ );
188
+ } else {
189
+ await db
190
+ .update(messages)
191
+ .set({ status: 'completed', completedAt: Date.now() })
192
+ .where(eq(messages.id, opts.assistantMessageId));
193
+
194
+ publish({
195
+ type: 'message.completed',
196
+ sessionId: opts.sessionId,
197
+ payload: {
198
+ id: opts.assistantMessageId,
199
+ autoCompacted: true,
200
+ },
201
+ });
202
+
203
+ const compactMessageId = crypto.randomUUID();
204
+ const compactMessageTime = Date.now();
205
+ await db.insert(messages).values({
206
+ id: compactMessageId,
207
+ sessionId: opts.sessionId,
208
+ role: 'assistant',
209
+ status: 'pending',
210
+ agent: opts.agent,
211
+ provider: opts.provider,
212
+ model: opts.model,
213
+ createdAt: compactMessageTime,
214
+ });
215
+
216
+ publish({
217
+ type: 'message.created',
218
+ sessionId: opts.sessionId,
219
+ payload: { id: compactMessageId, role: 'assistant' },
220
+ });
221
+
222
+ let compactionSucceeded = false;
223
+ try {
224
+ const publishWrapper = (event: {
225
+ type: string;
226
+ sessionId: string;
227
+ payload: Record<string, unknown>;
228
+ }) => {
229
+ publish(event as Parameters<typeof publish>[0]);
230
+ };
231
+ const compactResult = await performAutoCompaction(
232
+ db,
233
+ opts.sessionId,
234
+ compactMessageId,
235
+ publishWrapper,
236
+ opts.provider,
237
+ opts.model,
238
+ );
239
+ if (compactResult.success) {
240
+ debugLog(
241
+ `[stream-handlers] Auto-compaction succeeded: ${compactResult.summary?.slice(0, 100)}...`,
242
+ );
243
+ compactionSucceeded = true;
244
+ } else {
245
+ debugLog(
246
+ `[stream-handlers] Auto-compaction failed: ${compactResult.error}, falling back to prune`,
247
+ );
248
+ const pruneResult = await pruneSession(db, opts.sessionId);
249
+ debugLog(
250
+ `[stream-handlers] Fallback pruned ${pruneResult.pruned} parts, saved ~${pruneResult.saved} tokens`,
251
+ );
252
+ compactionSucceeded = pruneResult.pruned > 0;
253
+ }
254
+ } catch (compactErr) {
255
+ debugLog(
256
+ `[stream-handlers] Auto-compact error: ${compactErr instanceof Error ? compactErr.message : String(compactErr)}`,
257
+ );
258
+ }
259
+
260
+ await db
261
+ .update(messages)
262
+ .set({
263
+ status: compactionSucceeded ? 'completed' : 'error',
264
+ completedAt: Date.now(),
265
+ })
266
+ .where(eq(messages.id, compactMessageId));
267
+
268
+ publish({
269
+ type: 'message.completed',
270
+ sessionId: opts.sessionId,
271
+ payload: { id: compactMessageId, autoCompacted: true },
272
+ });
273
+
274
+ if (compactionSucceeded && retryCallback) {
275
+ debugLog('[stream-handlers] Triggering retry after compaction...');
276
+ const retryMessageId = crypto.randomUUID();
277
+ await db.insert(messages).values({
278
+ id: retryMessageId,
279
+ sessionId: opts.sessionId,
280
+ role: 'assistant',
281
+ status: 'pending',
282
+ agent: opts.agent,
283
+ provider: opts.provider,
284
+ model: opts.model,
285
+ createdAt: Date.now(),
286
+ });
287
+
288
+ publish({
289
+ type: 'message.created',
290
+ sessionId: opts.sessionId,
291
+ payload: { id: retryMessageId, role: 'assistant' },
292
+ });
293
+
294
+ enqueueAssistantRun(
295
+ {
296
+ ...opts,
297
+ assistantMessageId: retryMessageId,
298
+ compactionRetries: retries + 1,
299
+ },
300
+ retryCallback,
301
+ );
302
+ return;
303
+ }
304
+
305
+ if (compactionSucceeded) {
306
+ debugLog(
307
+ '[stream-handlers] No retryCallback provided, cannot auto-retry',
308
+ );
309
+ return;
310
+ }
311
+ }
312
+ }
313
+
314
+ const errorPartId = crypto.randomUUID();
315
+ const displayMessage =
316
+ isPromptTooLong && !opts.isCompactCommand
317
+ ? `${errorPayload.message}. Context auto-compacted - please retry your message.`
318
+ : errorPayload.message;
319
+ const errorPartType = isPromptTooLong
320
+ ? 'context_length_exceeded'
321
+ : errorPayload.type;
322
+ await db.insert(messageParts).values({
323
+ id: errorPartId,
324
+ messageId: opts.assistantMessageId,
325
+ index: await sharedCtx.nextIndex(),
326
+ stepIndex,
327
+ type: 'error',
328
+ content: JSON.stringify({
329
+ message: displayMessage,
330
+ type: errorPartType,
331
+ errorType: isPromptTooLong ? 'context_length_exceeded' : undefined,
332
+ details: errorPayload.details,
333
+ isAborted: false,
334
+ }),
335
+ agent: opts.agent,
336
+ provider: opts.provider,
337
+ model: opts.model,
338
+ startedAt: Date.now(),
339
+ completedAt: Date.now(),
340
+ });
341
+
342
+ await db
343
+ .update(messages)
344
+ .set({
345
+ status: 'error',
346
+ error: displayMessage,
347
+ errorType: errorPartType,
348
+ errorDetails: JSON.stringify({
349
+ ...errorPayload.details,
350
+ isApiError,
351
+ autoCompacted: isPromptTooLong && !opts.isCompactCommand,
352
+ }),
353
+ isAborted: false,
354
+ })
355
+ .where(eq(messages.id, opts.assistantMessageId));
356
+
357
+ publish({
358
+ type: 'error',
359
+ sessionId: opts.sessionId,
360
+ payload: {
361
+ messageId: opts.assistantMessageId,
362
+ partId: errorPartId,
363
+ error: displayMessage,
364
+ errorType: errorPartType,
365
+ details: errorPayload.details,
366
+ isAborted: false,
367
+ autoCompacted: isPromptTooLong && !opts.isCompactCommand,
368
+ },
369
+ });
370
+ };
371
+ }
@@ -0,0 +1,101 @@
1
+ import type { getDb } from '@ottocode/database';
2
+ import { messages, messageParts } from '@ottocode/database/schema';
3
+ import { eq } from 'drizzle-orm';
4
+ import { publish } from '../../events/bus.ts';
5
+ import { estimateModelCostUsd } from '@ottocode/sdk';
6
+ import type { RunOpts } from '../session/queue.ts';
7
+ import { markSessionCompacted } from '../message/compaction.ts';
8
+ import { debugLog } from '../debug/index.ts';
9
+ import type { FinishEvent } from './types.ts';
10
+ import {
11
+ normalizeUsage,
12
+ resolveUsageProvider,
13
+ } from '../session/db-operations.ts';
14
+
15
+ export function createFinishHandler(
16
+ opts: RunOpts,
17
+ db: Awaited<ReturnType<typeof getDb>>,
18
+ completeAssistantMessageFn: (
19
+ fin: FinishEvent,
20
+ opts: RunOpts,
21
+ db: Awaited<ReturnType<typeof getDb>>,
22
+ ) => Promise<void>,
23
+ ) {
24
+ return async (fin: FinishEvent) => {
25
+ try {
26
+ await completeAssistantMessageFn(fin, opts, db);
27
+ } catch {}
28
+
29
+ if (opts.isCompactCommand && fin.finishReason !== 'error') {
30
+ const assistantParts = await db
31
+ .select()
32
+ .from(messageParts)
33
+ .where(eq(messageParts.messageId, opts.assistantMessageId));
34
+ const hasTextContent = assistantParts.some(
35
+ (p) => p.type === 'text' && p.content && p.content !== '{"text":""}',
36
+ );
37
+
38
+ if (!hasTextContent) {
39
+ debugLog(
40
+ '[stream-handlers] /compact finished but no summary generated, skipping compaction marking',
41
+ );
42
+ } else {
43
+ try {
44
+ debugLog(
45
+ `[stream-handlers] /compact complete, marking session compacted`,
46
+ );
47
+ const result = await markSessionCompacted(
48
+ db,
49
+ opts.sessionId,
50
+ opts.assistantMessageId,
51
+ );
52
+ debugLog(
53
+ `[stream-handlers] Compacted ${result.compacted} parts, saved ~${result.saved} tokens`,
54
+ );
55
+ } catch (err) {
56
+ debugLog(
57
+ `[stream-handlers] Compaction failed: ${err instanceof Error ? err.message : String(err)}`,
58
+ );
59
+ }
60
+ }
61
+ }
62
+
63
+ const sessRows = await db
64
+ .select()
65
+ .from(messages)
66
+ .where(eq(messages.id, opts.assistantMessageId));
67
+
68
+ const usage = sessRows[0]
69
+ ? {
70
+ inputTokens: Number(sessRows[0].inputTokens ?? 0),
71
+ outputTokens: Number(sessRows[0].outputTokens ?? 0),
72
+ totalTokens: Number(sessRows[0].totalTokens ?? 0),
73
+ cachedInputTokens: Number(sessRows[0].cachedInputTokens ?? 0),
74
+ cacheCreationInputTokens: Number(
75
+ sessRows[0].cacheCreationInputTokens ?? 0,
76
+ ),
77
+ }
78
+ : fin.usage
79
+ ? normalizeUsage(
80
+ fin.usage,
81
+ undefined,
82
+ resolveUsageProvider(opts.provider, opts.model),
83
+ )
84
+ : undefined;
85
+
86
+ const costUsd = usage
87
+ ? estimateModelCostUsd(opts.provider, opts.model, usage)
88
+ : undefined;
89
+
90
+ publish({
91
+ type: 'message.completed',
92
+ sessionId: opts.sessionId,
93
+ payload: {
94
+ id: opts.assistantMessageId,
95
+ usage,
96
+ costUsd,
97
+ finishReason: fin.finishReason,
98
+ },
99
+ });
100
+ };
101
+ }
@@ -0,0 +1,5 @@
1
+ export { createStepFinishHandler } from './step-finish.ts';
2
+ export { createErrorHandler } from './error-handler.ts';
3
+ export { createAbortHandler } from './abort-handler.ts';
4
+ export { createFinishHandler } from './finish-handler.ts';
5
+ export type { StepFinishEvent, FinishEvent, AbortEvent } from './types.ts';
@@ -0,0 +1,93 @@
1
+ import type { getDb } from '@ottocode/database';
2
+ import { messageParts } from '@ottocode/database/schema';
3
+ import { eq } from 'drizzle-orm';
4
+ import { publish } from '../../events/bus.ts';
5
+ import type { RunOpts } from '../session/queue.ts';
6
+ import type { ToolAdapterContext } from '../../tools/adapter.ts';
7
+ import type { UsageData, ProviderMetadata } from '../session/db-operations.ts';
8
+ import type { StepFinishEvent } from './types.ts';
9
+
10
+ export function createStepFinishHandler(
11
+ opts: RunOpts,
12
+ db: Awaited<ReturnType<typeof getDb>>,
13
+ getStepIndex: () => number,
14
+ incrementStepIndex: () => number,
15
+ getCurrentPartId: () => string | null,
16
+ updateCurrentPartId: (id: string | null) => void,
17
+ updateAccumulated: (text: string) => void,
18
+ sharedCtx: ToolAdapterContext,
19
+ updateSessionTokensIncrementalFn: (
20
+ usage: UsageData,
21
+ providerOptions: ProviderMetadata | undefined,
22
+ opts: RunOpts,
23
+ db: Awaited<ReturnType<typeof getDb>>,
24
+ ) => Promise<void>,
25
+ updateMessageTokensIncrementalFn: (
26
+ usage: UsageData,
27
+ providerOptions: ProviderMetadata | undefined,
28
+ opts: RunOpts,
29
+ db: Awaited<ReturnType<typeof getDb>>,
30
+ ) => Promise<void>,
31
+ ) {
32
+ return async (step: StepFinishEvent) => {
33
+ const finishedAt = Date.now();
34
+ const currentPartId = getCurrentPartId();
35
+ const stepIndex = getStepIndex();
36
+
37
+ try {
38
+ if (currentPartId) {
39
+ await db
40
+ .update(messageParts)
41
+ .set({ completedAt: finishedAt })
42
+ .where(eq(messageParts.id, currentPartId));
43
+ }
44
+ } catch {}
45
+
46
+ if (step.usage) {
47
+ try {
48
+ await updateSessionTokensIncrementalFn(
49
+ step.usage,
50
+ step.providerMetadata,
51
+ opts,
52
+ db,
53
+ );
54
+ } catch {}
55
+
56
+ try {
57
+ await updateMessageTokensIncrementalFn(
58
+ step.usage,
59
+ step.providerMetadata,
60
+ opts,
61
+ db,
62
+ );
63
+ } catch {}
64
+ }
65
+
66
+ try {
67
+ publish({
68
+ type: 'finish-step',
69
+ sessionId: opts.sessionId,
70
+ payload: {
71
+ stepIndex,
72
+ usage: step.usage,
73
+ finishReason: step.finishReason,
74
+ response: step.response,
75
+ },
76
+ });
77
+ if (step.usage) {
78
+ publish({
79
+ type: 'usage',
80
+ sessionId: opts.sessionId,
81
+ payload: { stepIndex, ...step.usage },
82
+ });
83
+ }
84
+ } catch {}
85
+
86
+ try {
87
+ const newStepIndex = incrementStepIndex();
88
+ sharedCtx.stepIndex = newStepIndex;
89
+ updateCurrentPartId(null);
90
+ updateAccumulated('');
91
+ } catch {}
92
+ };
93
+ }
@@ -0,0 +1,25 @@
1
+ import type { UsageData, ProviderMetadata } from '../session/db-operations.ts';
2
+
3
+ export type StepFinishEvent = {
4
+ usage?: UsageData;
5
+ finishReason?: string;
6
+ response?: unknown;
7
+ providerMetadata?: ProviderMetadata;
8
+ };
9
+
10
+ export type FinishEvent = {
11
+ usage?: Pick<
12
+ UsageData,
13
+ | 'inputTokens'
14
+ | 'outputTokens'
15
+ | 'totalTokens'
16
+ | 'cachedInputTokens'
17
+ | 'cacheCreationInputTokens'
18
+ | 'reasoningTokens'
19
+ >;
20
+ finishReason?: string;
21
+ };
22
+
23
+ export type AbortEvent = {
24
+ steps: unknown[];
25
+ };