@agi-cli/server 0.1.59 → 0.1.61

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.
@@ -8,17 +8,26 @@ import { toErrorPayload } from './error-handling.ts';
8
8
  import type { RunOpts } from './session-queue.ts';
9
9
  import type { ToolAdapterContext } from '../tools/adapter.ts';
10
10
 
11
- type StepFinishEvent = {
12
- usage?: {
13
- inputTokens?: number;
14
- outputTokens?: number;
15
- totalTokens?: number;
16
- cachedInputTokens?: number;
17
- reasoningTokens?: number;
11
+ interface ProviderMetadata {
12
+ openai?: {
13
+ cachedPromptTokens?: number;
18
14
  };
15
+ [key: string]: unknown;
16
+ }
17
+
18
+ interface UsageData {
19
+ inputTokens?: number;
20
+ outputTokens?: number;
21
+ totalTokens?: number;
22
+ cachedInputTokens?: number;
23
+ reasoningTokens?: number;
24
+ }
25
+
26
+ type StepFinishEvent = {
27
+ usage?: UsageData;
19
28
  finishReason?: string;
20
29
  response?: unknown;
21
- experimental_providerMetadata?: Record<string, any>;
30
+ experimental_providerMetadata?: ProviderMetadata;
22
31
  };
23
32
 
24
33
  type FinishEvent = {
@@ -42,19 +51,19 @@ export function createStepFinishHandler(
42
51
  db: Awaited<ReturnType<typeof getDb>>,
43
52
  getCurrentPartId: () => string,
44
53
  getStepIndex: () => number,
45
- sharedCtx: ToolAdapterContext,
46
- updateCurrentPartId: (id: string) => void,
47
- updateAccumulated: (text: string) => void,
54
+ _sharedCtx: ToolAdapterContext,
55
+ _updateCurrentPartId: (id: string) => void,
56
+ _updateAccumulated: (text: string) => void,
48
57
  incrementStepIndex: () => number,
49
58
  updateSessionTokensIncrementalFn: (
50
- usage: any,
51
- providerMetadata: Record<string, any> | undefined,
59
+ usage: UsageData,
60
+ providerMetadata: ProviderMetadata | undefined,
52
61
  opts: RunOpts,
53
62
  db: Awaited<ReturnType<typeof getDb>>,
54
63
  ) => Promise<void>,
55
64
  updateMessageTokensIncrementalFn: (
56
- usage: any,
57
- providerMetadata: Record<string, any> | undefined,
65
+ usage: UsageData,
66
+ providerMetadata: ProviderMetadata | undefined,
58
67
  opts: RunOpts,
59
68
  db: Awaited<ReturnType<typeof getDb>>,
60
69
  ) => Promise<void>,
@@ -69,9 +78,11 @@ export function createStepFinishHandler(
69
78
  .update(messageParts)
70
79
  .set({ completedAt: finishedAt })
71
80
  .where(eq(messageParts.id, currentPartId));
72
- } catch {}
81
+ } catch (err) {
82
+ console.error('[createStepFinishHandler] Failed to update part', err);
83
+ }
73
84
 
74
- // Update token counts incrementally after each step
85
+ // Update tokens incrementally
75
86
  if (step.usage) {
76
87
  try {
77
88
  await updateSessionTokensIncrementalFn(
@@ -80,126 +91,81 @@ export function createStepFinishHandler(
80
91
  opts,
81
92
  db,
82
93
  );
83
- } catch {}
84
-
85
- try {
86
94
  await updateMessageTokensIncrementalFn(
87
95
  step.usage,
88
96
  step.experimental_providerMetadata,
89
97
  opts,
90
98
  db,
91
99
  );
92
- } catch {}
100
+ } catch (err) {
101
+ console.error('[createStepFinishHandler] Token update failed', err);
102
+ }
93
103
  }
94
104
 
95
- try {
96
- publish({
97
- type: 'finish-step',
98
- sessionId: opts.sessionId,
99
- payload: {
100
- stepIndex,
101
- usage: step.usage,
102
- finishReason: step.finishReason,
103
- response: step.response,
104
- },
105
- });
106
- if (step.usage) {
107
- publish({
108
- type: 'usage',
109
- sessionId: opts.sessionId,
110
- payload: { stepIndex, ...step.usage },
111
- });
112
- }
113
- } catch {}
105
+ // Publish step-finished event
106
+ publish('stream:step-finished', {
107
+ sessionId: opts.sessionId,
108
+ messageId: opts.assistantMessageId,
109
+ assistantMessageId: opts.assistantMessageId,
110
+ stepIndex,
111
+ finishReason: step.finishReason,
112
+ usage: step.usage,
113
+ });
114
114
 
115
- try {
116
- const newStepIndex = incrementStepIndex();
117
- const newPartId = crypto.randomUUID();
118
- const index = await sharedCtx.nextIndex();
119
- const nowTs = Date.now();
120
- await db.insert(messageParts).values({
121
- id: newPartId,
122
- messageId: opts.assistantMessageId,
123
- index,
124
- stepIndex: newStepIndex,
125
- type: 'text',
126
- content: JSON.stringify({ text: '' }),
127
- agent: opts.agent,
128
- provider: opts.provider,
129
- model: opts.model,
130
- startedAt: nowTs,
131
- });
132
- updateCurrentPartId(newPartId);
133
- sharedCtx.assistantPartId = newPartId;
134
- sharedCtx.stepIndex = newStepIndex;
135
- updateAccumulated('');
136
- } catch {}
115
+ incrementStepIndex();
137
116
  };
138
117
  }
139
118
 
140
119
  /**
141
- * Creates the onError handler for the stream
120
+ * Creates the onFinish handler for the stream
142
121
  */
143
- export function createErrorHandler(
122
+ export function createFinishHandler(
144
123
  opts: RunOpts,
145
124
  db: Awaited<ReturnType<typeof getDb>>,
146
- getStepIndex: () => number,
147
- sharedCtx: ToolAdapterContext,
125
+ completeAssistantMessageFn: (
126
+ fin: FinishEvent,
127
+ opts: RunOpts,
128
+ db: Awaited<ReturnType<typeof getDb>>,
129
+ ) => Promise<void>,
130
+ _getAccumulated: () => string,
131
+ _abortController: AbortController,
148
132
  ) {
149
- return async (err: unknown) => {
150
- const errorPayload = toErrorPayload(err);
151
- const isApiError = APICallError.isInstance(err);
152
- const stepIndex = getStepIndex();
133
+ return async (fin: FinishEvent) => {
134
+ try {
135
+ await completeAssistantMessageFn(fin, opts, db);
153
136
 
154
- // Create error part for UI display
155
- const errorPartId = crypto.randomUUID();
156
- await db.insert(messageParts).values({
157
- id: errorPartId,
158
- messageId: opts.assistantMessageId,
159
- index: await sharedCtx.nextIndex(),
160
- stepIndex,
161
- type: 'error',
162
- content: JSON.stringify({
163
- message: errorPayload.message,
164
- type: errorPayload.type,
165
- details: errorPayload.details,
166
- isAborted: false,
167
- }),
168
- agent: opts.agent,
169
- provider: opts.provider,
170
- model: opts.model,
171
- startedAt: Date.now(),
172
- completedAt: Date.now(),
173
- });
137
+ const msgRows = await db
138
+ .select()
139
+ .from(messages)
140
+ .where(eq(messages.id, opts.assistantMessageId));
174
141
 
175
- // Update message status
176
- await db
177
- .update(messages)
178
- .set({
179
- status: 'error',
180
- error: errorPayload.message,
181
- errorType: errorPayload.type,
182
- errorDetails: JSON.stringify({
183
- ...errorPayload.details,
184
- isApiError,
185
- }),
186
- isAborted: false,
187
- })
188
- .where(eq(messages.id, opts.assistantMessageId));
142
+ let estimatedCost = 0;
143
+ if (msgRows.length > 0 && msgRows[0]) {
144
+ const msg = msgRows[0];
145
+ estimatedCost = estimateModelCostUsd(
146
+ opts.provider,
147
+ opts.model,
148
+ Number(msg.promptTokens ?? 0),
149
+ Number(msg.completionTokens ?? 0),
150
+ );
151
+ }
189
152
 
190
- // Publish enhanced error event
191
- publish({
192
- type: 'error',
193
- sessionId: opts.sessionId,
194
- payload: {
153
+ publish('stream:finished', {
154
+ sessionId: opts.sessionId,
195
155
  messageId: opts.assistantMessageId,
196
- partId: errorPartId,
197
- error: errorPayload.message,
198
- errorType: errorPayload.type,
199
- details: errorPayload.details,
200
- isAborted: false,
201
- },
202
- });
156
+ assistantMessageId: opts.assistantMessageId,
157
+ usage: fin.usage,
158
+ finishReason: fin.finishReason,
159
+ estimatedCost,
160
+ });
161
+ } catch (err) {
162
+ console.error('[createFinishHandler] Error in onFinish', err);
163
+ publish('stream:error', {
164
+ sessionId: opts.sessionId,
165
+ messageId: opts.assistantMessageId,
166
+ error: toErrorPayload(err),
167
+ });
168
+ }
203
169
  };
204
170
  }
205
171
 
@@ -209,116 +175,116 @@ export function createErrorHandler(
209
175
  export function createAbortHandler(
210
176
  opts: RunOpts,
211
177
  db: Awaited<ReturnType<typeof getDb>>,
212
- getStepIndex: () => number,
213
- sharedCtx: ToolAdapterContext,
178
+ _abortController: AbortController,
214
179
  ) {
215
- return async ({ steps }: AbortEvent) => {
216
- const stepIndex = getStepIndex();
217
-
218
- // Create abort part for UI
219
- const abortPartId = crypto.randomUUID();
220
- await db.insert(messageParts).values({
221
- id: abortPartId,
222
- messageId: opts.assistantMessageId,
223
- index: await sharedCtx.nextIndex(),
224
- stepIndex,
225
- type: 'error',
226
- content: JSON.stringify({
227
- message: 'Generation stopped by user',
228
- type: 'abort',
229
- isAborted: true,
230
- stepsCompleted: steps.length,
231
- }),
232
- agent: opts.agent,
233
- provider: opts.provider,
234
- model: opts.model,
235
- startedAt: Date.now(),
236
- completedAt: Date.now(),
237
- });
238
-
239
- // Store abort info
240
- await db
241
- .update(messages)
242
- .set({
243
- status: 'error',
244
- error: 'Generation stopped by user',
245
- errorType: 'abort',
246
- errorDetails: JSON.stringify({
247
- stepsCompleted: steps.length,
248
- abortedAt: Date.now(),
249
- }),
250
- isAborted: true,
251
- })
252
- .where(eq(messages.id, opts.assistantMessageId));
180
+ return async (_event: AbortEvent) => {
181
+ try {
182
+ await db
183
+ .update(messages)
184
+ .set({ status: 'aborted', finishedAt: new Date() })
185
+ .where(eq(messages.id, opts.assistantMessageId));
253
186
 
254
- // Publish abort event
255
- publish({
256
- type: 'error',
257
- sessionId: opts.sessionId,
258
- payload: {
187
+ publish('stream:aborted', {
188
+ sessionId: opts.sessionId,
259
189
  messageId: opts.assistantMessageId,
260
- partId: abortPartId,
261
- error: 'Generation stopped by user',
262
- errorType: 'abort',
263
- isAborted: true,
264
- stepsCompleted: steps.length,
265
- },
266
- });
190
+ assistantMessageId: opts.assistantMessageId,
191
+ });
192
+ } catch (err) {
193
+ console.error('[createAbortHandler] Error in onAbort', err);
194
+ }
267
195
  };
268
196
  }
269
197
 
270
198
  /**
271
- * Creates the onFinish handler for the stream
199
+ * Creates the error handler for the stream
272
200
  */
273
- export function createFinishHandler(
201
+ export function createErrorHandler(
274
202
  opts: RunOpts,
275
203
  db: Awaited<ReturnType<typeof getDb>>,
276
- ensureFinishToolCalled: () => Promise<void>,
277
- completeAssistantMessageFn: (
278
- fin: FinishEvent,
279
- opts: RunOpts,
280
- db: Awaited<ReturnType<typeof getDb>>,
281
- ) => Promise<void>,
282
204
  ) {
283
- return async (fin: FinishEvent) => {
205
+ return async (err: unknown) => {
206
+ console.error('[createErrorHandler] Stream error:', err);
207
+
284
208
  try {
285
- await ensureFinishToolCalled();
286
- } catch {}
209
+ let errorMessage = 'Unknown error';
210
+ let errorType = 'UNKNOWN_ERROR';
211
+ let errorStack: string | undefined;
287
212
 
288
- // Note: Token updates are handled incrementally in onStepFinish
289
- // Do NOT add fin.usage here as it would cause double-counting
213
+ if (err instanceof APICallError) {
214
+ errorMessage = err.message;
215
+ errorType = 'API_CALL_ERROR';
216
+ errorStack = err.stack;
217
+ } else if (err instanceof Error) {
218
+ errorMessage = err.message;
219
+ errorType = err.name || 'ERROR';
220
+ errorStack = err.stack;
221
+ } else if (typeof err === 'string') {
222
+ errorMessage = err;
223
+ }
290
224
 
291
- try {
292
- await completeAssistantMessageFn(fin, opts, db);
293
- } catch {}
225
+ await db
226
+ .update(messages)
227
+ .set({
228
+ status: 'error',
229
+ finishedAt: new Date(),
230
+ error: errorMessage,
231
+ })
232
+ .where(eq(messages.id, opts.assistantMessageId));
294
233
 
295
- // Use session totals from DB for accurate cost calculation
296
- const sessRows = await db
297
- .select()
298
- .from(messages)
299
- .where(eq(messages.id, opts.assistantMessageId));
234
+ publish('stream:error', {
235
+ sessionId: opts.sessionId,
236
+ messageId: opts.assistantMessageId,
237
+ assistantMessageId: opts.assistantMessageId,
238
+ error: {
239
+ message: errorMessage,
240
+ type: errorType,
241
+ stack: errorStack,
242
+ },
243
+ });
244
+ } catch (dbErr) {
245
+ console.error('[createErrorHandler] Failed to save error to DB', dbErr);
246
+ }
247
+ };
248
+ }
300
249
 
301
- const usage = sessRows[0]
302
- ? {
303
- inputTokens: Number(sessRows[0].promptTokens ?? 0),
304
- outputTokens: Number(sessRows[0].completionTokens ?? 0),
305
- totalTokens: Number(sessRows[0].totalTokens ?? 0),
306
- }
307
- : fin.usage;
250
+ /**
251
+ * Creates the text delta handler for the stream
252
+ */
253
+ export function createTextHandler(
254
+ opts: RunOpts,
255
+ db: Awaited<ReturnType<typeof getDb>>,
256
+ getCurrentPartId: () => string,
257
+ getStepIndex: () => number,
258
+ _updateCurrentPartId: (id: string) => void,
259
+ updateAccumulated: (text: string) => void,
260
+ getAccumulated: () => string,
261
+ ) {
262
+ return async (textDelta: string) => {
263
+ const currentPartId = getCurrentPartId();
264
+ const stepIndex = getStepIndex();
308
265
 
309
- const costUsd = usage
310
- ? estimateModelCostUsd(opts.provider, opts.model, usage)
311
- : undefined;
266
+ // Accumulate the text
267
+ const accumulated = getAccumulated() + textDelta;
268
+ updateAccumulated(accumulated);
312
269
 
313
- publish({
314
- type: 'message.completed',
315
- sessionId: opts.sessionId,
316
- payload: {
317
- id: opts.assistantMessageId,
318
- usage,
319
- costUsd,
320
- finishReason: fin.finishReason,
321
- },
322
- });
270
+ try {
271
+ if (currentPartId) {
272
+ await db
273
+ .update(messageParts)
274
+ .set({ content: accumulated })
275
+ .where(eq(messageParts.id, currentPartId));
276
+ }
277
+
278
+ publish('stream:text-delta', {
279
+ sessionId: opts.sessionId,
280
+ messageId: opts.assistantMessageId,
281
+ assistantMessageId: opts.assistantMessageId,
282
+ stepIndex,
283
+ textDelta,
284
+ fullText: accumulated,
285
+ });
286
+ } catch (err) {
287
+ console.error('[createTextHandler] Error updating text part', err);
288
+ }
323
289
  };
324
290
  }