@agi-cli/server 0.1.58 → 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,10 +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
+ 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
+
11
26
  type StepFinishEvent = {
12
- usage?: { inputTokens?: number; outputTokens?: number };
27
+ usage?: UsageData;
13
28
  finishReason?: string;
14
29
  response?: unknown;
30
+ experimental_providerMetadata?: ProviderMetadata;
15
31
  };
16
32
 
17
33
  type FinishEvent = {
@@ -35,10 +51,22 @@ export function createStepFinishHandler(
35
51
  db: Awaited<ReturnType<typeof getDb>>,
36
52
  getCurrentPartId: () => string,
37
53
  getStepIndex: () => number,
38
- sharedCtx: ToolAdapterContext,
39
- updateCurrentPartId: (id: string) => void,
40
- updateAccumulated: (text: string) => void,
54
+ _sharedCtx: ToolAdapterContext,
55
+ _updateCurrentPartId: (id: string) => void,
56
+ _updateAccumulated: (text: string) => void,
41
57
  incrementStepIndex: () => number,
58
+ updateSessionTokensIncrementalFn: (
59
+ usage: UsageData,
60
+ providerMetadata: ProviderMetadata | undefined,
61
+ opts: RunOpts,
62
+ db: Awaited<ReturnType<typeof getDb>>,
63
+ ) => Promise<void>,
64
+ updateMessageTokensIncrementalFn: (
65
+ usage: UsageData,
66
+ providerMetadata: ProviderMetadata | undefined,
67
+ opts: RunOpts,
68
+ db: Awaited<ReturnType<typeof getDb>>,
69
+ ) => Promise<void>,
42
70
  ) {
43
71
  return async (step: StepFinishEvent) => {
44
72
  const finishedAt = Date.now();
@@ -50,116 +78,94 @@ export function createStepFinishHandler(
50
78
  .update(messageParts)
51
79
  .set({ completedAt: finishedAt })
52
80
  .where(eq(messageParts.id, currentPartId));
53
- } catch {}
81
+ } catch (err) {
82
+ console.error('[createStepFinishHandler] Failed to update part', err);
83
+ }
54
84
 
55
- try {
56
- publish({
57
- type: 'finish-step',
58
- sessionId: opts.sessionId,
59
- payload: {
60
- stepIndex,
61
- usage: step.usage,
62
- finishReason: step.finishReason,
63
- response: step.response,
64
- },
65
- });
66
- if (step.usage) {
67
- publish({
68
- type: 'usage',
69
- sessionId: opts.sessionId,
70
- payload: { stepIndex, ...step.usage },
71
- });
85
+ // Update tokens incrementally
86
+ if (step.usage) {
87
+ try {
88
+ await updateSessionTokensIncrementalFn(
89
+ step.usage,
90
+ step.experimental_providerMetadata,
91
+ opts,
92
+ db,
93
+ );
94
+ await updateMessageTokensIncrementalFn(
95
+ step.usage,
96
+ step.experimental_providerMetadata,
97
+ opts,
98
+ db,
99
+ );
100
+ } catch (err) {
101
+ console.error('[createStepFinishHandler] Token update failed', err);
72
102
  }
73
- } catch {}
103
+ }
74
104
 
75
- try {
76
- const newStepIndex = incrementStepIndex();
77
- const newPartId = crypto.randomUUID();
78
- const index = await sharedCtx.nextIndex();
79
- const nowTs = Date.now();
80
- await db.insert(messageParts).values({
81
- id: newPartId,
82
- messageId: opts.assistantMessageId,
83
- index,
84
- stepIndex: newStepIndex,
85
- type: 'text',
86
- content: JSON.stringify({ text: '' }),
87
- agent: opts.agent,
88
- provider: opts.provider,
89
- model: opts.model,
90
- startedAt: nowTs,
91
- });
92
- updateCurrentPartId(newPartId);
93
- sharedCtx.assistantPartId = newPartId;
94
- sharedCtx.stepIndex = newStepIndex;
95
- updateAccumulated('');
96
- } 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
+
115
+ incrementStepIndex();
97
116
  };
98
117
  }
99
118
 
100
119
  /**
101
- * Creates the onError handler for the stream
120
+ * Creates the onFinish handler for the stream
102
121
  */
103
- export function createErrorHandler(
122
+ export function createFinishHandler(
104
123
  opts: RunOpts,
105
124
  db: Awaited<ReturnType<typeof getDb>>,
106
- getStepIndex: () => number,
107
- 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,
108
132
  ) {
109
- return async (err: unknown) => {
110
- const errorPayload = toErrorPayload(err);
111
- const isApiError = APICallError.isInstance(err);
112
- const stepIndex = getStepIndex();
133
+ return async (fin: FinishEvent) => {
134
+ try {
135
+ await completeAssistantMessageFn(fin, opts, db);
113
136
 
114
- // Create error part for UI display
115
- const errorPartId = crypto.randomUUID();
116
- await db.insert(messageParts).values({
117
- id: errorPartId,
118
- messageId: opts.assistantMessageId,
119
- index: await sharedCtx.nextIndex(),
120
- stepIndex,
121
- type: 'error',
122
- content: JSON.stringify({
123
- message: errorPayload.message,
124
- type: errorPayload.type,
125
- details: errorPayload.details,
126
- isAborted: false,
127
- }),
128
- agent: opts.agent,
129
- provider: opts.provider,
130
- model: opts.model,
131
- startedAt: Date.now(),
132
- completedAt: Date.now(),
133
- });
137
+ const msgRows = await db
138
+ .select()
139
+ .from(messages)
140
+ .where(eq(messages.id, opts.assistantMessageId));
134
141
 
135
- // Update message status
136
- await db
137
- .update(messages)
138
- .set({
139
- status: 'error',
140
- error: errorPayload.message,
141
- errorType: errorPayload.type,
142
- errorDetails: JSON.stringify({
143
- ...errorPayload.details,
144
- isApiError,
145
- }),
146
- isAborted: false,
147
- })
148
- .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
+ }
149
152
 
150
- // Publish enhanced error event
151
- publish({
152
- type: 'error',
153
- sessionId: opts.sessionId,
154
- payload: {
153
+ publish('stream:finished', {
154
+ sessionId: opts.sessionId,
155
155
  messageId: opts.assistantMessageId,
156
- partId: errorPartId,
157
- error: errorPayload.message,
158
- errorType: errorPayload.type,
159
- details: errorPayload.details,
160
- isAborted: false,
161
- },
162
- });
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
+ }
163
169
  };
164
170
  }
165
171
 
@@ -169,107 +175,116 @@ export function createErrorHandler(
169
175
  export function createAbortHandler(
170
176
  opts: RunOpts,
171
177
  db: Awaited<ReturnType<typeof getDb>>,
172
- getStepIndex: () => number,
173
- sharedCtx: ToolAdapterContext,
178
+ _abortController: AbortController,
174
179
  ) {
175
- return async ({ steps }: AbortEvent) => {
176
- const stepIndex = getStepIndex();
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));
177
186
 
178
- // Create abort part for UI
179
- const abortPartId = crypto.randomUUID();
180
- await db.insert(messageParts).values({
181
- id: abortPartId,
182
- messageId: opts.assistantMessageId,
183
- index: await sharedCtx.nextIndex(),
184
- stepIndex,
185
- type: 'error',
186
- content: JSON.stringify({
187
- message: 'Generation stopped by user',
188
- type: 'abort',
189
- isAborted: true,
190
- stepsCompleted: steps.length,
191
- }),
192
- agent: opts.agent,
193
- provider: opts.provider,
194
- model: opts.model,
195
- startedAt: Date.now(),
196
- completedAt: Date.now(),
197
- });
187
+ publish('stream:aborted', {
188
+ sessionId: opts.sessionId,
189
+ messageId: opts.assistantMessageId,
190
+ assistantMessageId: opts.assistantMessageId,
191
+ });
192
+ } catch (err) {
193
+ console.error('[createAbortHandler] Error in onAbort', err);
194
+ }
195
+ };
196
+ }
198
197
 
199
- // Store abort info
200
- await db
201
- .update(messages)
202
- .set({
203
- status: 'error',
204
- error: 'Generation stopped by user',
205
- errorType: 'abort',
206
- errorDetails: JSON.stringify({
207
- stepsCompleted: steps.length,
208
- abortedAt: Date.now(),
209
- }),
210
- isAborted: true,
211
- })
212
- .where(eq(messages.id, opts.assistantMessageId));
198
+ /**
199
+ * Creates the error handler for the stream
200
+ */
201
+ export function createErrorHandler(
202
+ opts: RunOpts,
203
+ db: Awaited<ReturnType<typeof getDb>>,
204
+ ) {
205
+ return async (err: unknown) => {
206
+ console.error('[createErrorHandler] Stream error:', err);
213
207
 
214
- // Publish abort event
215
- publish({
216
- type: 'error',
217
- sessionId: opts.sessionId,
218
- payload: {
208
+ try {
209
+ let errorMessage = 'Unknown error';
210
+ let errorType = 'UNKNOWN_ERROR';
211
+ let errorStack: string | undefined;
212
+
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
+ }
224
+
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,
219
236
  messageId: opts.assistantMessageId,
220
- partId: abortPartId,
221
- error: 'Generation stopped by user',
222
- errorType: 'abort',
223
- isAborted: true,
224
- stepsCompleted: steps.length,
225
- },
226
- });
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
+ }
227
247
  };
228
248
  }
229
249
 
230
250
  /**
231
- * Creates the onFinish handler for the stream
251
+ * Creates the text delta handler for the stream
232
252
  */
233
- export function createFinishHandler(
253
+ export function createTextHandler(
234
254
  opts: RunOpts,
235
255
  db: Awaited<ReturnType<typeof getDb>>,
236
- ensureFinishToolCalled: () => Promise<void>,
237
- updateSessionTokensFn: (
238
- fin: FinishEvent,
239
- opts: RunOpts,
240
- db: Awaited<ReturnType<typeof getDb>>,
241
- ) => Promise<void>,
242
- completeAssistantMessageFn: (
243
- fin: FinishEvent,
244
- opts: RunOpts,
245
- db: Awaited<ReturnType<typeof getDb>>,
246
- ) => Promise<void>,
256
+ getCurrentPartId: () => string,
257
+ getStepIndex: () => number,
258
+ _updateCurrentPartId: (id: string) => void,
259
+ updateAccumulated: (text: string) => void,
260
+ getAccumulated: () => string,
247
261
  ) {
248
- return async (fin: FinishEvent) => {
249
- try {
250
- await ensureFinishToolCalled();
251
- } catch {}
262
+ return async (textDelta: string) => {
263
+ const currentPartId = getCurrentPartId();
264
+ const stepIndex = getStepIndex();
252
265
 
253
- try {
254
- await updateSessionTokensFn(fin, opts, db);
255
- } catch {}
266
+ // Accumulate the text
267
+ const accumulated = getAccumulated() + textDelta;
268
+ updateAccumulated(accumulated);
256
269
 
257
270
  try {
258
- await completeAssistantMessageFn(fin, opts, db);
259
- } catch {}
271
+ if (currentPartId) {
272
+ await db
273
+ .update(messageParts)
274
+ .set({ content: accumulated })
275
+ .where(eq(messageParts.id, currentPartId));
276
+ }
260
277
 
261
- const costUsd = fin.usage
262
- ? estimateModelCostUsd(opts.provider, opts.model, fin.usage)
263
- : undefined;
264
- publish({
265
- type: 'message.completed',
266
- sessionId: opts.sessionId,
267
- payload: {
268
- id: opts.assistantMessageId,
269
- usage: fin.usage,
270
- costUsd,
271
- finishReason: fin.finishReason,
272
- },
273
- });
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
+ }
274
289
  };
275
290
  }