@agi-cli/server 0.1.61 → 0.1.63

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,26 +8,17 @@ 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
- interface ProviderMetadata {
12
- openai?: {
13
- cachedPromptTokens?: number;
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
11
  type StepFinishEvent = {
27
- usage?: UsageData;
12
+ usage?: {
13
+ inputTokens?: number;
14
+ outputTokens?: number;
15
+ totalTokens?: number;
16
+ cachedInputTokens?: number;
17
+ reasoningTokens?: number;
18
+ };
28
19
  finishReason?: string;
29
20
  response?: unknown;
30
- experimental_providerMetadata?: ProviderMetadata;
21
+ experimental_providerMetadata?: Record<string, any>;
31
22
  };
32
23
 
33
24
  type FinishEvent = {
@@ -51,19 +42,19 @@ export function createStepFinishHandler(
51
42
  db: Awaited<ReturnType<typeof getDb>>,
52
43
  getCurrentPartId: () => string,
53
44
  getStepIndex: () => number,
54
- _sharedCtx: ToolAdapterContext,
55
- _updateCurrentPartId: (id: string) => void,
56
- _updateAccumulated: (text: string) => void,
45
+ sharedCtx: ToolAdapterContext,
46
+ updateCurrentPartId: (id: string) => void,
47
+ updateAccumulated: (text: string) => void,
57
48
  incrementStepIndex: () => number,
58
49
  updateSessionTokensIncrementalFn: (
59
- usage: UsageData,
60
- providerMetadata: ProviderMetadata | undefined,
50
+ usage: any,
51
+ providerMetadata: Record<string, any> | undefined,
61
52
  opts: RunOpts,
62
53
  db: Awaited<ReturnType<typeof getDb>>,
63
54
  ) => Promise<void>,
64
55
  updateMessageTokensIncrementalFn: (
65
- usage: UsageData,
66
- providerMetadata: ProviderMetadata | undefined,
56
+ usage: any,
57
+ providerMetadata: Record<string, any> | undefined,
67
58
  opts: RunOpts,
68
59
  db: Awaited<ReturnType<typeof getDb>>,
69
60
  ) => Promise<void>,
@@ -78,11 +69,9 @@ export function createStepFinishHandler(
78
69
  .update(messageParts)
79
70
  .set({ completedAt: finishedAt })
80
71
  .where(eq(messageParts.id, currentPartId));
81
- } catch (err) {
82
- console.error('[createStepFinishHandler] Failed to update part', err);
83
- }
72
+ } catch {}
84
73
 
85
- // Update tokens incrementally
74
+ // Update token counts incrementally after each step
86
75
  if (step.usage) {
87
76
  try {
88
77
  await updateSessionTokensIncrementalFn(
@@ -91,81 +80,126 @@ export function createStepFinishHandler(
91
80
  opts,
92
81
  db,
93
82
  );
83
+ } catch {}
84
+
85
+ try {
94
86
  await updateMessageTokensIncrementalFn(
95
87
  step.usage,
96
88
  step.experimental_providerMetadata,
97
89
  opts,
98
90
  db,
99
91
  );
100
- } catch (err) {
101
- console.error('[createStepFinishHandler] Token update failed', err);
102
- }
92
+ } catch {}
103
93
  }
104
94
 
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
- });
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 {}
114
114
 
115
- incrementStepIndex();
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 {}
116
137
  };
117
138
  }
118
139
 
119
140
  /**
120
- * Creates the onFinish handler for the stream
141
+ * Creates the onError handler for the stream
121
142
  */
122
- export function createFinishHandler(
143
+ export function createErrorHandler(
123
144
  opts: RunOpts,
124
145
  db: Awaited<ReturnType<typeof getDb>>,
125
- completeAssistantMessageFn: (
126
- fin: FinishEvent,
127
- opts: RunOpts,
128
- db: Awaited<ReturnType<typeof getDb>>,
129
- ) => Promise<void>,
130
- _getAccumulated: () => string,
131
- _abortController: AbortController,
146
+ getStepIndex: () => number,
147
+ sharedCtx: ToolAdapterContext,
132
148
  ) {
133
- return async (fin: FinishEvent) => {
134
- try {
135
- await completeAssistantMessageFn(fin, opts, db);
149
+ return async (err: unknown) => {
150
+ const errorPayload = toErrorPayload(err);
151
+ const isApiError = APICallError.isInstance(err);
152
+ const stepIndex = getStepIndex();
136
153
 
137
- const msgRows = await db
138
- .select()
139
- .from(messages)
140
- .where(eq(messages.id, opts.assistantMessageId));
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
+ });
141
174
 
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
- }
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));
152
189
 
153
- publish('stream:finished', {
154
- sessionId: opts.sessionId,
155
- messageId: opts.assistantMessageId,
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,
190
+ // Publish enhanced error event
191
+ publish({
192
+ type: 'error',
193
+ sessionId: opts.sessionId,
194
+ payload: {
165
195
  messageId: opts.assistantMessageId,
166
- error: toErrorPayload(err),
167
- });
168
- }
196
+ partId: errorPartId,
197
+ error: errorPayload.message,
198
+ errorType: errorPayload.type,
199
+ details: errorPayload.details,
200
+ isAborted: false,
201
+ },
202
+ });
169
203
  };
170
204
  }
171
205
 
@@ -175,116 +209,116 @@ export function createFinishHandler(
175
209
  export function createAbortHandler(
176
210
  opts: RunOpts,
177
211
  db: Awaited<ReturnType<typeof getDb>>,
178
- _abortController: AbortController,
212
+ getStepIndex: () => number,
213
+ sharedCtx: ToolAdapterContext,
179
214
  ) {
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));
215
+ return async ({ steps }: AbortEvent) => {
216
+ const stepIndex = getStepIndex();
186
217
 
187
- publish('stream:aborted', {
188
- sessionId: opts.sessionId,
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));
253
+
254
+ // Publish abort event
255
+ publish({
256
+ type: 'error',
257
+ sessionId: opts.sessionId,
258
+ payload: {
189
259
  messageId: opts.assistantMessageId,
190
- assistantMessageId: opts.assistantMessageId,
191
- });
192
- } catch (err) {
193
- console.error('[createAbortHandler] Error in onAbort', err);
194
- }
260
+ partId: abortPartId,
261
+ error: 'Generation stopped by user',
262
+ errorType: 'abort',
263
+ isAborted: true,
264
+ stepsCompleted: steps.length,
265
+ },
266
+ });
195
267
  };
196
268
  }
197
269
 
198
270
  /**
199
- * Creates the error handler for the stream
271
+ * Creates the onFinish handler for the stream
200
272
  */
201
- export function createErrorHandler(
273
+ export function createFinishHandler(
202
274
  opts: RunOpts,
203
275
  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>,
204
282
  ) {
205
- return async (err: unknown) => {
206
- console.error('[createErrorHandler] Stream error:', err);
207
-
283
+ return async (fin: FinishEvent) => {
208
284
  try {
209
- let errorMessage = 'Unknown error';
210
- let errorType = 'UNKNOWN_ERROR';
211
- let errorStack: string | undefined;
285
+ await ensureFinishToolCalled();
286
+ } catch {}
212
287
 
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
- }
288
+ // Note: Token updates are handled incrementally in onStepFinish
289
+ // Do NOT add fin.usage here as it would cause double-counting
224
290
 
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));
233
-
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
- }
291
+ try {
292
+ await completeAssistantMessageFn(fin, opts, db);
293
+ } catch {}
249
294
 
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();
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));
265
300
 
266
- // Accumulate the text
267
- const accumulated = getAccumulated() + textDelta;
268
- updateAccumulated(accumulated);
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;
269
308
 
270
- try {
271
- if (currentPartId) {
272
- await db
273
- .update(messageParts)
274
- .set({ content: accumulated })
275
- .where(eq(messageParts.id, currentPartId));
276
- }
309
+ const costUsd = usage
310
+ ? estimateModelCostUsd(opts.provider, opts.model, usage)
311
+ : undefined;
277
312
 
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
- }
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
+ });
289
323
  };
290
324
  }