@link-assistant/agent 0.22.7 → 0.22.9

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.22.7",
3
+ "version": "0.22.9",
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",
@@ -849,6 +849,48 @@ export namespace MessageV2 {
849
849
  return result;
850
850
  }
851
851
 
852
+ function normalizeProviderErrorObject(e: unknown) {
853
+ if (!e || typeof e !== 'object') return;
854
+
855
+ const root = e as Record<string, unknown>;
856
+ const candidate =
857
+ root['error'] && typeof root['error'] === 'object'
858
+ ? (root['error'] as Record<string, unknown>)
859
+ : root;
860
+ const rawCode =
861
+ candidate['code'] ?? candidate['statusCode'] ?? candidate['status'];
862
+ const statusCode =
863
+ typeof rawCode === 'number'
864
+ ? rawCode
865
+ : typeof rawCode === 'string' && /^\d+$/.test(rawCode)
866
+ ? Number.parseInt(rawCode, 10)
867
+ : undefined;
868
+
869
+ if (statusCode === undefined) return;
870
+
871
+ let responseBody: string | undefined;
872
+ try {
873
+ responseBody = JSON.stringify(e);
874
+ } catch {
875
+ responseBody = undefined;
876
+ }
877
+
878
+ const message =
879
+ typeof candidate['message'] === 'string'
880
+ ? candidate['message']
881
+ : typeof root['message'] === 'string'
882
+ ? root['message']
883
+ : `Provider returned HTTP ${statusCode}`;
884
+
885
+ return {
886
+ message,
887
+ statusCode,
888
+ isRetryable:
889
+ statusCode === 429 || (statusCode >= 500 && statusCode < 600),
890
+ responseBody,
891
+ };
892
+ }
893
+
852
894
  export function fromError(e: unknown, ctx: { providerID: string }) {
853
895
  switch (true) {
854
896
  case e instanceof DOMException && e.name === 'AbortError':
@@ -884,6 +926,10 @@ export namespace MessageV2 {
884
926
  },
885
927
  { cause: e }
886
928
  ).toObject();
929
+ case normalizeProviderErrorObject(e) !== undefined: {
930
+ const providerError = normalizeProviderErrorObject(e)!;
931
+ return new MessageV2.APIError(providerError, { cause: e }).toObject();
932
+ }
887
933
  case e instanceof Error: {
888
934
  const message = e.message || e.toString();
889
935
  // Detect Bun socket connection errors (known Bun issue with 10s idle timeout)
@@ -52,6 +52,40 @@ export namespace SessionProcessor {
52
52
  }
53
53
  }
54
54
 
55
+ /**
56
+ * Detect AI SDK/provider usage-shape crashes.
57
+ *
58
+ * Some provider error streams end before a valid finish-step usage object is
59
+ * available. Depending on the provider adapter, that can surface as a
60
+ * TypeError while reading token fields instead of the original provider
61
+ * error. Treat it as retryable provider failure rather than a generic crash.
62
+ *
63
+ * See: https://github.com/link-assistant/agent/issues/264
64
+ */
65
+ export function isUsageDataTypeError(error: unknown): boolean {
66
+ const message =
67
+ error instanceof Error
68
+ ? `${error.name}: ${error.message}`
69
+ : typeof error === 'string'
70
+ ? error
71
+ : '';
72
+
73
+ if (!(error instanceof TypeError) && !message.startsWith('TypeError:')) {
74
+ return false;
75
+ }
76
+
77
+ return [
78
+ 'input_tokens',
79
+ 'output_tokens',
80
+ 'usage.inputTokens',
81
+ 'usage.outputTokens',
82
+ 'inputTokens.total',
83
+ 'outputTokens.total',
84
+ "reading 'inputTokens'",
85
+ "reading 'outputTokens'",
86
+ ].some((pattern) => message.includes(pattern));
87
+ }
88
+
55
89
  export type Info = Awaited<ReturnType<typeof create>>;
56
90
  export type Result = Awaited<ReturnType<Info['process']>>;
57
91
 
@@ -542,37 +576,34 @@ export namespace SessionProcessor {
542
576
  } catch (e) {
543
577
  log.error(() => ({ message: 'process', error: e }));
544
578
 
545
- // Check for AI SDK usage-related TypeError (input_tokens undefined)
546
- // This happens when providers return incomplete usage data
547
- // See: https://github.com/link-assistant/agent/issues/152
548
- if (
549
- e instanceof TypeError &&
550
- (e.message.includes('input_tokens') ||
551
- e.message.includes('output_tokens') ||
552
- e.message.includes("reading 'input_tokens'") ||
553
- e.message.includes("reading 'output_tokens'"))
554
- ) {
579
+ let error = MessageV2.fromError(e, {
580
+ providerID: input.providerID,
581
+ });
582
+
583
+ // Check for AI SDK usage-related TypeErrors. These usually come
584
+ // from provider adapters after malformed/incomplete usage data, so
585
+ // retry them with normal API-error backoff instead of surfacing a
586
+ // misleading UnknownError.
587
+ if (isUsageDataTypeError(e)) {
555
588
  log.warn(() => ({
556
589
  message:
557
- 'Provider returned invalid usage data, continuing with zero usage',
558
- errorMessage: e.message,
590
+ 'provider returned invalid usage data; retrying as provider API error',
591
+ errorMessage: e instanceof Error ? e.message : String(e),
559
592
  providerID: input.providerID,
593
+ issue: 'https://github.com/link-assistant/agent/issues/264',
560
594
  }));
561
- // Set default token values to prevent crash
562
- input.assistantMessage.tokens = {
563
- input: 0,
564
- output: 0,
565
- reasoning: 0,
566
- cache: { read: 0, write: 0 },
567
- };
568
- // Continue processing instead of failing
569
- continue;
595
+ error = new MessageV2.APIError(
596
+ {
597
+ message:
598
+ e instanceof Error
599
+ ? e.message
600
+ : 'Provider returned invalid usage data',
601
+ isRetryable: true,
602
+ },
603
+ { cause: e }
604
+ ).toObject();
570
605
  }
571
606
 
572
- const error = MessageV2.fromError(e, {
573
- providerID: input.providerID,
574
- });
575
-
576
607
  // Check if error is retryable (APIError, SocketConnectionError, TimeoutError)
577
608
  const isRetryableAPIError =
578
609
  error?.name === 'APIError' && error.data.isRetryable;