@link-assistant/agent 0.8.4 → 0.8.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/agent",
3
- "version": "0.8.4",
3
+ "version": "0.8.6",
4
4
  "description": "A minimal, public domain AI CLI agent compatible with OpenCode's JSON interface. Bun-only runtime.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -413,6 +413,26 @@ export namespace Session {
413
413
  throw new Error(`Cannot convert ${value} to number`);
414
414
  }
415
415
 
416
+ // Handle objects with a 'total' field (e.g., { total: 8707, noCache: 6339, cacheRead: 2368 })
417
+ // Some AI providers return token counts as objects instead of plain numbers
418
+ // See: https://github.com/link-assistant/agent/issues/125
419
+ if (
420
+ typeof value === 'object' &&
421
+ value !== null &&
422
+ 'total' in value &&
423
+ typeof (value as { total: unknown }).total === 'number'
424
+ ) {
425
+ const result = (value as { total: number }).total;
426
+ if (Flag.OPENCODE_VERBOSE) {
427
+ log.debug(() => ({
428
+ message: 'toNumber extracted total from object',
429
+ context,
430
+ result,
431
+ }));
432
+ }
433
+ return result;
434
+ }
435
+
416
436
  // Try to convert to number
417
437
  const result = Number(value);
418
438
 
@@ -448,6 +468,90 @@ export namespace Session {
448
468
  }
449
469
  };
450
470
 
471
+ /**
472
+ * Safely converts a finishReason value to a string.
473
+ * Some AI providers return finishReason as an object instead of a string.
474
+ *
475
+ * For example, OpenCode provider on certain Bun versions may return:
476
+ * - { type: "stop" } instead of "stop"
477
+ * - { finishReason: "tool-calls" } instead of "tool-calls"
478
+ *
479
+ * This function handles these cases gracefully.
480
+ *
481
+ * @param value - The finishReason value (string, object, or undefined)
482
+ * @returns A string representing the finish reason, or 'unknown' if conversion fails
483
+ * @see https://github.com/link-assistant/agent/issues/125
484
+ */
485
+ export const toFinishReason = (value: unknown): string => {
486
+ // Log input data in verbose mode to help identify issues
487
+ if (Flag.OPENCODE_VERBOSE) {
488
+ log.debug(() => ({
489
+ message: 'toFinishReason input',
490
+ valueType: typeof value,
491
+ value:
492
+ typeof value === 'object' ? JSON.stringify(value) : String(value),
493
+ }));
494
+ }
495
+
496
+ // If it's already a string, return it
497
+ if (typeof value === 'string') {
498
+ return value;
499
+ }
500
+
501
+ // If it's undefined or null, return 'unknown'
502
+ if (value === undefined || value === null) {
503
+ return 'unknown';
504
+ }
505
+
506
+ // If it's an object, try to extract a meaningful string
507
+ if (typeof value === 'object') {
508
+ const obj = value as Record<string, unknown>;
509
+
510
+ // Try common field names that might contain the reason
511
+ if (typeof obj.type === 'string') {
512
+ if (Flag.OPENCODE_VERBOSE) {
513
+ log.debug(() => ({
514
+ message: 'toFinishReason extracted type from object',
515
+ result: obj.type,
516
+ }));
517
+ }
518
+ return obj.type;
519
+ }
520
+
521
+ if (typeof obj.finishReason === 'string') {
522
+ if (Flag.OPENCODE_VERBOSE) {
523
+ log.debug(() => ({
524
+ message: 'toFinishReason extracted finishReason from object',
525
+ result: obj.finishReason,
526
+ }));
527
+ }
528
+ return obj.finishReason;
529
+ }
530
+
531
+ if (typeof obj.reason === 'string') {
532
+ if (Flag.OPENCODE_VERBOSE) {
533
+ log.debug(() => ({
534
+ message: 'toFinishReason extracted reason from object',
535
+ result: obj.reason,
536
+ }));
537
+ }
538
+ return obj.reason;
539
+ }
540
+
541
+ // If we can't extract a specific field, return JSON representation
542
+ if (Flag.OPENCODE_VERBOSE) {
543
+ log.debug(() => ({
544
+ message: 'toFinishReason could not extract string, using JSON',
545
+ result: JSON.stringify(value),
546
+ }));
547
+ }
548
+ return JSON.stringify(value);
549
+ }
550
+
551
+ // For any other type, convert to string
552
+ return String(value);
553
+ };
554
+
451
555
  export const getUsage = fn(
452
556
  z.object({
453
557
  model: z.custom<ModelsDev.Model>(),
@@ -224,12 +224,17 @@ export namespace SessionProcessor {
224
224
  usage: value.usage,
225
225
  metadata: value.providerMetadata,
226
226
  });
227
- input.assistantMessage.finish = value.finishReason;
227
+ // Use toFinishReason to safely convert object/string finishReason to string
228
+ // See: https://github.com/link-assistant/agent/issues/125
229
+ const finishReason = Session.toFinishReason(
230
+ value.finishReason
231
+ );
232
+ input.assistantMessage.finish = finishReason;
228
233
  input.assistantMessage.cost += usage.cost;
229
234
  input.assistantMessage.tokens = usage.tokens;
230
235
  await Session.updatePart({
231
236
  id: Identifier.ascending('part'),
232
- reason: value.finishReason,
237
+ reason: finishReason,
233
238
  snapshot: await Snapshot.track(),
234
239
  messageID: input.assistantMessage.id,
235
240
  sessionID: input.assistantMessage.sessionID,
@@ -274,13 +279,19 @@ export namespace SessionProcessor {
274
279
 
275
280
  case 'text-delta':
276
281
  if (currentText) {
277
- currentText.text += value.text;
282
+ // Handle case where value.text might be an object instead of string
283
+ // See: https://github.com/link-assistant/agent/issues/125
284
+ const textDelta =
285
+ typeof value.text === 'string'
286
+ ? value.text
287
+ : String(value.text);
288
+ currentText.text += textDelta;
278
289
  if (value.providerMetadata)
279
290
  currentText.metadata = value.providerMetadata;
280
291
  if (currentText.text)
281
292
  await Session.updatePart({
282
293
  part: currentText,
283
- delta: value.text,
294
+ delta: textDelta,
284
295
  });
285
296
  }
286
297
  break;