@link-assistant/agent 0.27.0 → 0.27.1

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.
Files changed (54) hide show
  1. package/README.md +47 -5
  2. package/package.json +41 -42
  3. package/src/cli/argv.ts +1 -1
  4. package/src/cli/cmd/run.ts +7 -1
  5. package/src/cli/continuous-mode.js +38 -12
  6. package/src/cli/event-handler.js +3 -1
  7. package/src/cli/model-config.js +20 -4
  8. package/src/cli/run-options.js +144 -78
  9. package/src/config/config.ts +306 -105
  10. package/src/config/defaults.ts +13 -5
  11. package/src/config/file-config.ts +16 -0
  12. package/src/file/ripgrep.ts +38 -5
  13. package/src/file/time.ts +86 -9
  14. package/src/index.js +39 -33
  15. package/src/mcp/index.ts +370 -106
  16. package/src/permission/index.ts +22 -0
  17. package/src/project/instance.ts +5 -1
  18. package/src/project/project.ts +35 -2
  19. package/src/provider/free-tier-error.ts +1 -1
  20. package/src/provider/kilo-deprecation.ts +30 -0
  21. package/src/provider/kilo.ts +25 -7
  22. package/src/provider/provider.ts +53 -22
  23. package/src/provider/retry-fetch.ts +5 -0
  24. package/src/provider/transform.ts +139 -17
  25. package/src/session/compaction.ts +18 -2
  26. package/src/session/index.ts +30 -3
  27. package/src/session/message-v2.ts +318 -32
  28. package/src/session/processor.ts +274 -140
  29. package/src/session/prompt.ts +154 -128
  30. package/src/session/retry.ts +26 -13
  31. package/src/session/summary.ts +4 -1
  32. package/src/tool/bash.ts +232 -45
  33. package/src/tool/bash.txt +1 -1
  34. package/src/tool/codesearch.ts +15 -78
  35. package/src/tool/edit.ts +111 -30
  36. package/src/tool/glob.ts +2 -1
  37. package/src/tool/grep.ts +175 -57
  38. package/src/tool/ls.ts +2 -1
  39. package/src/tool/multiedit.ts +3 -1
  40. package/src/tool/read.ts +169 -41
  41. package/src/tool/task.ts +26 -18
  42. package/src/tool/tool.ts +4 -1
  43. package/src/tool/webfetch.ts +68 -56
  44. package/src/tool/webfetch.txt +4 -4
  45. package/src/tool/websearch.ts +20 -75
  46. package/src/tool/websearch.txt +2 -0
  47. package/src/tool/write.ts +23 -6
  48. package/src/util/bom.ts +32 -0
  49. package/src/util/exa-mcp.ts +82 -0
  50. package/src/util/legacy-error-names.ts +39 -0
  51. package/src/util/read-bounded.ts +32 -0
  52. package/src/util/sse-usage-extractor.ts +67 -13
  53. package/src/util/timeout.ts +3 -4
  54. package/src/util/verbose-fetch.ts +40 -1
@@ -18,6 +18,7 @@ import { Snapshot } from '../snapshot';
18
18
  import { fn } from '../util/fn';
19
19
  import { Storage } from '../storage/storage';
20
20
  import { isUnreachableNetworkError } from '../util/network-error';
21
+ import { STATUS_CODES } from 'node:http';
21
22
 
22
23
  export namespace MessageV2 {
23
24
  export const OutputLengthError = NamedError.create(
@@ -78,6 +79,96 @@ export namespace MessageV2 {
78
79
  );
79
80
  export type TimeoutError = z.infer<typeof TimeoutError.Schema>;
80
81
 
82
+ /**
83
+ * The provider asked us to wait (retry-after) longer than the configured
84
+ * retry timeout, so the request is not retried.
85
+ * See: https://github.com/link-assistant/agent/issues/157
86
+ */
87
+ export const RetryTimeoutExceededError = NamedError.create(
88
+ 'RetryTimeoutExceededError',
89
+ z.object({
90
+ message: z.string(),
91
+ isRetryable: z.literal(false),
92
+ retryAfterMs: z.number(),
93
+ maxTimeoutMs: z.number(),
94
+ })
95
+ );
96
+ export type RetryTimeoutExceededError = z.infer<
97
+ typeof RetryTimeoutExceededError.Schema
98
+ >;
99
+
100
+ /**
101
+ * The request exceeds the model's context window. Not retried as is: the
102
+ * session is compacted and the turn continues (upstream #14707).
103
+ */
104
+ export const ContextOverflowError = NamedError.create(
105
+ 'ContextOverflowError',
106
+ z.object({
107
+ message: z.string(),
108
+ responseBody: z.string().optional(),
109
+ })
110
+ );
111
+ export type ContextOverflowError = z.infer<
112
+ typeof ContextOverflowError.Schema
113
+ >;
114
+
115
+ // Provider wordings for "prompt exceeds the context window", from upstream
116
+ // packages/ai/src/provider-error.ts.
117
+ const OVERFLOW_PATTERNS = [
118
+ /prompt is too long/i,
119
+ /input is too long for (?:the )?(?:requested|selected) model/i,
120
+ /too large for the model'?s configured input limit/i,
121
+ /exceeded this model context window limit/i,
122
+ /exceeds the context window/i,
123
+ /exceeds (?:the )?(?:model'?s )?maximum context length(?: of [\d,]+ tokens?|\s*\([\d,]+\))/i,
124
+ /input token count.*exceeds the maximum/i,
125
+ /number of input tokens exceeds maximum length/i,
126
+ /tokens in request more than max tokens allowed/i,
127
+ /maximum prompt length is \d+/i,
128
+ /reduce the length of the messages/i,
129
+ /requested input length \d+ exceeds maximum input length/i,
130
+ /maximum context length is \d+ tokens/i,
131
+ /exceeds (?:the )?maximum allowed input length of [\d,]+ tokens?/i,
132
+ /input(?: \(\d+ tokens\))? is longer than the model'?s context length/i,
133
+ /exceeds the limit of \d+/i,
134
+ /exceeds the available context size/i,
135
+ /greater than the context length/i,
136
+ /`inputs` tokens \+ `max_new_tokens` must be <= \d+/i,
137
+ /context window exceeds limit/i,
138
+ /exceeded model token limit/i,
139
+ /context[_ ]length[_ ]exceeded/i,
140
+ /context length is only \d+ tokens/i,
141
+ /input length.*exceeds.*context length/i,
142
+ /prompt (?:too long|exceeds max length|超长)/i,
143
+ /too large for model with \d+ maximum context length/i,
144
+ /prompt has [\d,]+ tokens?, but the configured context size is [\d,]+ tokens?/i,
145
+ /model_context_window_exceeded/i,
146
+ /range of input length should be/i,
147
+ /too many tokens/i,
148
+ /token limit exceeded/i,
149
+ /request_too_large/i,
150
+ /request entity too large/i,
151
+ /payload too large/i,
152
+ /request too large/i,
153
+ ];
154
+ // Rate limits also talk about "too many tokens"; they are not overflow.
155
+ const OVERFLOW_EXCLUDE_PATTERNS = [
156
+ /^(throttling error|service unavailable):/i,
157
+ /rate limit/i,
158
+ /too many requests/i,
159
+ /max[_ ]tokens must be less than/i,
160
+ ];
161
+
162
+ export function isContextOverflow(message: string | undefined) {
163
+ if (!message) return false;
164
+ if (OVERFLOW_EXCLUDE_PATTERNS.some((pattern) => pattern.test(message)))
165
+ return false;
166
+ return (
167
+ OVERFLOW_PATTERNS.some((pattern) => pattern.test(message)) ||
168
+ /^4(?:00|13)\s*(status code)?\s*\(no body\)/i.test(message)
169
+ );
170
+ }
171
+
81
172
  const PartBase = z.object({
82
173
  id: z.string(),
83
174
  sessionID: z.string(),
@@ -458,6 +549,11 @@ export namespace MessageV2 {
458
549
  OutputLengthError.Schema,
459
550
  AbortedError.Schema,
460
551
  APIError.Schema,
552
+ ContextOverflowError.Schema,
553
+ // Retry-only errors are stored once their retries are used up.
554
+ SocketConnectionError.Schema,
555
+ TimeoutError.Schema,
556
+ RetryTimeoutExceededError.Schema,
461
557
  ])
462
558
  .optional(),
463
559
  parentID: z.string(),
@@ -669,11 +765,18 @@ export namespace MessageV2 {
669
765
  throw new Error('unknown message type');
670
766
  }
671
767
 
768
+ /**
769
+ * @param target the model the messages are sent to. Assistant turns from a
770
+ * different model drop their provider metadata and replay reasoning as text,
771
+ * because signatures and item ids are only valid for the model that produced
772
+ * them (upstream 021e42c0bb1).
773
+ */
672
774
  export async function toModelMessage(
673
775
  input: {
674
776
  info: Info;
675
777
  parts: Part[];
676
- }[]
778
+ }[],
779
+ target?: { providerID: string; modelID: string }
677
780
  ): Promise<ModelMessage[]> {
678
781
  const result: UIMessage[] = [];
679
782
 
@@ -728,12 +831,24 @@ export namespace MessageV2 {
728
831
  parts: [],
729
832
  };
730
833
  result.push(assistantMessage);
834
+ const differentModel =
835
+ target !== undefined &&
836
+ (target.providerID !== msg.info.providerID ||
837
+ target.modelID !== msg.info.modelID);
838
+ const metadata = <T>(value: T) => (differentModel ? undefined : value);
839
+ // Anthropic needs the empty separator between signed thinking blocks
840
+ // kept, so send ' ' rather than '' (upstream #21370).
841
+ const hasSignedReasoning = msg.parts.some(
842
+ (part) =>
843
+ part.type === 'reasoning' &&
844
+ part.metadata?.anthropic?.signature != null
845
+ );
731
846
  for (const part of msg.parts) {
732
847
  if (part.type === 'text')
733
848
  assistantMessage.parts.push({
734
849
  type: 'text',
735
- text: part.text,
736
- providerMetadata: part.metadata,
850
+ text: part.text === '' && hasSignedReasoning ? ' ' : part.text,
851
+ providerMetadata: metadata(part.metadata),
737
852
  });
738
853
  if (part.type === 'step-start')
739
854
  assistantMessage.parts.push({
@@ -772,7 +887,7 @@ export namespace MessageV2 {
772
887
  output: part.state.time.compacted
773
888
  ? '[Old tool result content cleared]'
774
889
  : part.state.output,
775
- callProviderMetadata: part.metadata,
890
+ callProviderMetadata: metadata(part.metadata),
776
891
  });
777
892
  }
778
893
  if (part.state.status === 'error')
@@ -782,10 +897,15 @@ export namespace MessageV2 {
782
897
  toolCallId: part.callID,
783
898
  input: part.state.input,
784
899
  errorText: part.state.error,
785
- callProviderMetadata: part.metadata,
900
+ callProviderMetadata: metadata(part.metadata),
786
901
  });
787
902
  }
788
903
  if (part.type === 'reasoning') {
904
+ if (differentModel) {
905
+ if (part.text.trim().length > 0)
906
+ assistantMessage.parts.push({ type: 'text', text: part.text });
907
+ continue;
908
+ }
789
909
  assistantMessage.parts.push({
790
910
  type: 'reasoning',
791
911
  text: part.text,
@@ -796,7 +916,13 @@ export namespace MessageV2 {
796
916
  }
797
917
  }
798
918
 
799
- return await convertToModelMessages(result);
919
+ // A message whose parts all convert to nothing would become
920
+ // `content: []`, which Anthropic and Bedrock reject (upstream #4811).
921
+ return await convertToModelMessages(
922
+ result.filter((msg) =>
923
+ msg.parts.some((part) => part.type !== 'step-start')
924
+ )
925
+ );
800
926
  }
801
927
 
802
928
  export const stream = fn(
@@ -861,6 +987,30 @@ export namespace MessageV2 {
861
987
  return result;
862
988
  }
863
989
 
990
+ // Stream-error codes whose retryability the HTTP status gets wrong
991
+ // (upstream #23069).
992
+ const NON_RETRYABLE_PROVIDER_CODES = new Set([
993
+ 'insufficient_quota',
994
+ 'usage_not_included',
995
+ 'invalid_prompt',
996
+ ]);
997
+ const RETRYABLE_PROVIDER_CODES = new Set([
998
+ 'server_error',
999
+ 'server_is_overloaded',
1000
+ 'overloaded_error',
1001
+ 'api_error',
1002
+ 'rate_limit_error',
1003
+ ]);
1004
+
1005
+ function parseJsonObject(input: unknown) {
1006
+ if (typeof input !== 'string') return;
1007
+ try {
1008
+ const parsed = JSON.parse(input);
1009
+ if (parsed && typeof parsed === 'object')
1010
+ return parsed as Record<string, any>;
1011
+ } catch {}
1012
+ }
1013
+
864
1014
  function normalizeProviderErrorObject(e: unknown) {
865
1015
  if (!e || typeof e !== 'object') return;
866
1016
 
@@ -869,21 +1019,36 @@ export namespace MessageV2 {
869
1019
  root['error'] && typeof root['error'] === 'object'
870
1020
  ? (root['error'] as Record<string, unknown>)
871
1021
  : root;
872
- const rawCode =
873
- candidate['code'] ??
874
- candidate['statusCode'] ??
875
- candidate['status'] ??
876
- root['code'] ??
877
- root['statusCode'] ??
878
- root['status'];
879
- const statusCode =
880
- typeof rawCode === 'number'
881
- ? rawCode
882
- : typeof rawCode === 'string' && /^\d+$/.test(rawCode)
883
- ? Number.parseInt(rawCode, 10)
884
- : undefined;
885
-
886
- if (statusCode === undefined) return;
1022
+ // A numeric status must win over a string `code` such as "server_error",
1023
+ // otherwise the object is not recognised at all (upstream #23069).
1024
+ const statusCode = [
1025
+ candidate['statusCode'],
1026
+ candidate['status'],
1027
+ root['statusCode'],
1028
+ root['status'],
1029
+ candidate['code'],
1030
+ root['code'],
1031
+ ]
1032
+ .map((value) =>
1033
+ typeof value === 'number'
1034
+ ? value
1035
+ : typeof value === 'string' && /^\d+$/.test(value)
1036
+ ? Number.parseInt(value, 10)
1037
+ : undefined
1038
+ )
1039
+ .find((value) => value !== undefined);
1040
+ const code = [
1041
+ candidate['code'],
1042
+ root['code'],
1043
+ candidate['type'],
1044
+ root['type'],
1045
+ ].find(
1046
+ (value): value is string =>
1047
+ typeof value === 'string' && value !== 'error' && !/^\d+$/.test(value)
1048
+ );
1049
+
1050
+ if (statusCode === undefined && (code === undefined || e instanceof Error))
1051
+ return;
887
1052
 
888
1053
  let responseBody: string | undefined;
889
1054
  try {
@@ -897,17 +1062,85 @@ export namespace MessageV2 {
897
1062
  ? candidate['message']
898
1063
  : typeof root['message'] === 'string'
899
1064
  ? root['message']
900
- : `Provider returned HTTP ${statusCode}`;
1065
+ : statusCode !== undefined
1066
+ ? `Provider returned HTTP ${statusCode}`
1067
+ : code!;
1068
+
1069
+ if (code === 'context_length_exceeded' || isContextOverflow(message)) {
1070
+ return { overflow: true as const, message, statusCode, responseBody };
1071
+ }
1072
+
1073
+ const sdkRetryable = [root['isRetryable'], candidate['isRetryable']].find(
1074
+ (value): value is boolean => typeof value === 'boolean'
1075
+ );
1076
+ const isRetryable =
1077
+ code && NON_RETRYABLE_PROVIDER_CODES.has(code)
1078
+ ? false
1079
+ : code && RETRYABLE_PROVIDER_CODES.has(code)
1080
+ ? true
1081
+ : (sdkRetryable ??
1082
+ (statusCode === 429 ||
1083
+ (statusCode !== undefined && statusCode >= 500)));
901
1084
 
902
1085
  return {
1086
+ overflow: false as const,
903
1087
  message,
904
1088
  statusCode,
905
- isRetryable:
906
- statusCode === 429 || (statusCode >= 500 && statusCode < 600),
1089
+ isRetryable,
907
1090
  responseBody,
908
1091
  };
909
1092
  }
910
1093
 
1094
+ // Adds the provider's explanation to a bare status text and keeps HTML
1095
+ // gateway pages out of the message (upstream #13530, #14096).
1096
+ function apiErrorMessage(e: APICallError) {
1097
+ const message = e.message;
1098
+ const body = e.responseBody;
1099
+ const statusText = e.statusCode ? STATUS_CODES[e.statusCode] : undefined;
1100
+ if (message === '') return (body || statusText || 'Unknown error').trim();
1101
+ if (!body || (statusText && message !== statusText)) return message;
1102
+
1103
+ const json = parseJsonObject(body);
1104
+ if (json) {
1105
+ const detail = [json.message, json.error?.message, json.error].find(
1106
+ (value): value is string => typeof value === 'string' && value !== ''
1107
+ );
1108
+ if (detail) return `${message}: ${detail}`.trim();
1109
+ }
1110
+
1111
+ if (/^\s*<!doctype|^\s*<html/i.test(body)) {
1112
+ if (e.statusCode === 401)
1113
+ return 'Unauthorized: request was blocked by a gateway or proxy. Your authentication token may be missing or expired - try `agent auth login` to re-authenticate.';
1114
+ if (e.statusCode === 403)
1115
+ return 'Forbidden: request was blocked by a gateway or proxy. You may not have permission to access this resource - check your account and provider settings.';
1116
+ return message;
1117
+ }
1118
+
1119
+ return `${message}: ${body}`.trim();
1120
+ }
1121
+
1122
+ // Plain errors that are really rate limits or overload (upstream retry.ts).
1123
+ function isRateLimitMessage(message: string) {
1124
+ const lower = message.toLowerCase();
1125
+ if (
1126
+ lower.includes('rate increased too quickly') ||
1127
+ lower.includes('rate limit') ||
1128
+ lower.includes('too many requests')
1129
+ )
1130
+ return true;
1131
+ const json = parseJsonObject(message);
1132
+ if (!json) return false;
1133
+ const code = typeof json.code === 'string' ? json.code.toLowerCase() : '';
1134
+ if (json.type === 'error' && json.error?.type === 'too_many_requests')
1135
+ return true;
1136
+ if (code.includes('exhausted') || code.includes('unavailable')) return true;
1137
+ return (
1138
+ json.type === 'error' &&
1139
+ typeof json.error?.code === 'string' &&
1140
+ json.error.code.includes('rate_limit')
1141
+ );
1142
+ }
1143
+
911
1144
  export function fromError(e: unknown, ctx: { providerID: string }) {
912
1145
  const providerError = APICallError.isInstance(e)
913
1146
  ? undefined
@@ -953,23 +1186,53 @@ export namespace MessageV2 {
953
1186
  },
954
1187
  { cause: e }
955
1188
  ).toObject();
956
- case APICallError.isInstance(e):
1189
+ case APICallError.isInstance(e): {
1190
+ const message = apiErrorMessage(e);
1191
+ const body = parseJsonObject(e.responseBody);
1192
+ if (
1193
+ isContextOverflow(message) ||
1194
+ e.statusCode === 413 ||
1195
+ body?.error?.code === 'context_length_exceeded'
1196
+ ) {
1197
+ return new MessageV2.ContextOverflowError(
1198
+ { message, responseBody: e.responseBody },
1199
+ { cause: e }
1200
+ ).toObject();
1201
+ }
1202
+ const status = e.statusCode;
957
1203
  return new MessageV2.APIError(
958
1204
  {
959
- message: e.message,
960
- statusCode: e.statusCode,
1205
+ message,
1206
+ statusCode: status,
961
1207
  // A refused connection / unresolvable host is not transient: the
962
1208
  // AI SDK marks it retryable, but retrying it for the 7-day global
963
- // budget makes the CLI hang forever (#290).
964
- isRetryable: isUnreachableNetworkError(e) ? false : e.isRetryable,
1209
+ // budget makes the CLI hang forever (#290). Any 5xx is transient
1210
+ // even when the SDK says otherwise (upstream #22508), and OpenAI
1211
+ // returns 404 for models that do exist (upstream #13579).
1212
+ isRetryable: isUnreachableNetworkError(e)
1213
+ ? false
1214
+ : e.isRetryable ||
1215
+ (status !== undefined && status >= 500) ||
1216
+ (ctx.providerID.startsWith('openai') && status === 404),
965
1217
  responseHeaders: e.responseHeaders,
966
1218
  responseBody: e.responseBody,
967
1219
  },
968
1220
  { cause: e }
969
1221
  ).toObject();
970
- case normalizeProviderErrorObject(e) !== undefined: {
971
- const providerError = normalizeProviderErrorObject(e)!;
972
- return new MessageV2.APIError(providerError, { cause: e }).toObject();
1222
+ }
1223
+ case providerError !== undefined: {
1224
+ const normalized = providerError!;
1225
+ if (normalized.overflow === true) {
1226
+ return new MessageV2.ContextOverflowError(
1227
+ {
1228
+ message: normalized.message,
1229
+ responseBody: normalized.responseBody,
1230
+ },
1231
+ { cause: e }
1232
+ ).toObject();
1233
+ }
1234
+ const { overflow: _, ...data } = normalized;
1235
+ return new MessageV2.APIError(data, { cause: e }).toObject();
973
1236
  }
974
1237
  case e instanceof Error: {
975
1238
  const message = e.message || e.toString();
@@ -996,6 +1259,29 @@ export namespace MessageV2 {
996
1259
  { cause: e }
997
1260
  ).toObject();
998
1261
  }
1262
+ // Bun throws ZlibError when a gzip body is cut off mid-transfer
1263
+ // (upstream #18064).
1264
+ if (
1265
+ e.name === 'ZlibError' ||
1266
+ String((e as { code?: unknown }).code ?? '').startsWith('Z_')
1267
+ ) {
1268
+ return new MessageV2.APIError(
1269
+ { message, isRetryable: true },
1270
+ { cause: e }
1271
+ ).toObject();
1272
+ }
1273
+ if (isContextOverflow(message)) {
1274
+ return new MessageV2.ContextOverflowError(
1275
+ { message },
1276
+ { cause: e }
1277
+ ).toObject();
1278
+ }
1279
+ if (isRateLimitMessage(message)) {
1280
+ return new MessageV2.APIError(
1281
+ { message, isRetryable: true },
1282
+ { cause: e }
1283
+ ).toObject();
1284
+ }
999
1285
  return new NamedError.Unknown(
1000
1286
  { message: e.toString() },
1001
1287
  { cause: e }