@link-assistant/agent 0.26.15 → 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.
- package/README.md +49 -7
- package/package.json +51 -42
- package/src/agent/agent.ts +12 -7
- package/src/auth/azure-login.ts +241 -0
- package/src/auth/plugins.ts +9 -0
- package/src/auth/provider-logins.ts +804 -0
- package/src/cli/argv.ts +1 -1
- package/src/cli/cmd/auth.ts +4 -0
- package/src/cli/cmd/models.ts +68 -13
- package/src/cli/cmd/run.ts +7 -1
- package/src/cli/continuous-mode.js +38 -12
- package/src/cli/event-handler.js +3 -1
- package/src/cli/model-config.js +27 -8
- package/src/cli/run-options.js +144 -78
- package/src/config/config.ts +306 -105
- package/src/config/defaults.ts +13 -5
- package/src/config/file-config.ts +16 -0
- package/src/file/ripgrep.ts +38 -5
- package/src/file/time.ts +86 -9
- package/src/index.js +42 -33
- package/src/mcp/index.ts +370 -106
- package/src/permission/index.ts +22 -0
- package/src/project/instance.ts +5 -1
- package/src/project/project.ts +35 -2
- package/src/provider/env-template.ts +115 -0
- package/src/provider/extra-providers.ts +347 -0
- package/src/provider/free-tier-error.ts +1 -1
- package/src/provider/kilo-deprecation.ts +30 -0
- package/src/provider/kilo.ts +44 -7
- package/src/provider/model-discovery.ts +690 -0
- package/src/provider/models.ts +11 -1
- package/src/provider/parity-loaders.ts +595 -0
- package/src/provider/provider.ts +309 -203
- package/src/provider/retry-fetch.ts +5 -0
- package/src/provider/transform.ts +168 -17
- package/src/session/compaction.ts +18 -2
- package/src/session/index.ts +30 -3
- package/src/session/message-v2.ts +318 -32
- package/src/session/processor.ts +274 -140
- package/src/session/prompt.ts +154 -128
- package/src/session/retry.ts +26 -13
- package/src/session/summary.ts +4 -1
- package/src/tool/bash.ts +232 -45
- package/src/tool/bash.txt +1 -1
- package/src/tool/codesearch.ts +15 -78
- package/src/tool/edit.ts +111 -30
- package/src/tool/glob.ts +2 -1
- package/src/tool/grep.ts +175 -57
- package/src/tool/ls.ts +2 -1
- package/src/tool/multiedit.ts +3 -1
- package/src/tool/read.ts +169 -41
- package/src/tool/task.ts +26 -18
- package/src/tool/tool.ts +4 -1
- package/src/tool/webfetch.ts +68 -56
- package/src/tool/webfetch.txt +4 -4
- package/src/tool/websearch.ts +20 -75
- package/src/tool/websearch.txt +2 -0
- package/src/tool/write.ts +23 -6
- package/src/util/bom.ts +32 -0
- package/src/util/exa-mcp.ts +82 -0
- package/src/util/legacy-error-names.ts +39 -0
- package/src/util/read-bounded.ts +32 -0
- package/src/util/sse-usage-extractor.ts +67 -13
- package/src/util/timeout.ts +3 -4
- package/src/util/verbose-fetch.ts +40 -1
|
@@ -461,6 +461,9 @@ export namespace RetryFetch {
|
|
|
461
461
|
}));
|
|
462
462
|
|
|
463
463
|
await sleep(delay, init?.signal ?? undefined);
|
|
464
|
+
// The failed response is dropped: release its body and connection
|
|
465
|
+
// now instead of waiting for GC (issue #335). Not awaited.
|
|
466
|
+
response.body?.cancel().catch(() => {});
|
|
464
467
|
continue;
|
|
465
468
|
}
|
|
466
469
|
|
|
@@ -554,6 +557,8 @@ export namespace RetryFetch {
|
|
|
554
557
|
|
|
555
558
|
try {
|
|
556
559
|
await sleep(delay, isolatedSignal);
|
|
560
|
+
// Retrying: release the dropped 429 body (issue #335).
|
|
561
|
+
response.body?.cancel().catch(() => {});
|
|
557
562
|
} catch (sleepError) {
|
|
558
563
|
// Check if the original request was aborted (user cancellation)
|
|
559
564
|
// In that case, we should stop retrying
|
|
@@ -1,8 +1,97 @@
|
|
|
1
|
-
import type { ModelMessage } from 'ai';
|
|
2
|
-
import { unique } from 'remeda';
|
|
1
|
+
import type { ModelMessage, ToolResultPart } from 'ai';
|
|
2
|
+
import { mergeDeep, unique } from 'remeda';
|
|
3
3
|
import type { JSONSchema } from 'zod/v4/core';
|
|
4
4
|
|
|
5
5
|
export namespace ProviderTransform {
|
|
6
|
+
/**
|
|
7
|
+
* Replace lone UTF-16 surrogates, which strict JSON parsers reject with a
|
|
8
|
+
* 400 on every later turn (upstream #25934).
|
|
9
|
+
*/
|
|
10
|
+
export function sanitizeSurrogates(content: string) {
|
|
11
|
+
return content.replace(
|
|
12
|
+
/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g,
|
|
13
|
+
'�'
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sanitizeToolResult(part: ToolResultPart) {
|
|
18
|
+
const output = part.output;
|
|
19
|
+
if (output.type === 'text' || output.type === 'error-text') {
|
|
20
|
+
return {
|
|
21
|
+
...part,
|
|
22
|
+
output: { ...output, value: sanitizeSurrogates(output.value) },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (output.type === 'content') {
|
|
26
|
+
return {
|
|
27
|
+
...part,
|
|
28
|
+
output: {
|
|
29
|
+
...output,
|
|
30
|
+
value: output.value.map((item) =>
|
|
31
|
+
item.type === 'text'
|
|
32
|
+
? { ...item, text: sanitizeSurrogates(item.text) }
|
|
33
|
+
: item
|
|
34
|
+
),
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return part;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sanitizeMessages(msgs: ModelMessage[]): ModelMessage[] {
|
|
42
|
+
return msgs.map((msg) => {
|
|
43
|
+
if (typeof msg.content === 'string') {
|
|
44
|
+
return {
|
|
45
|
+
...msg,
|
|
46
|
+
content: sanitizeSurrogates(msg.content),
|
|
47
|
+
} as typeof msg;
|
|
48
|
+
}
|
|
49
|
+
if (!Array.isArray(msg.content)) return msg;
|
|
50
|
+
return {
|
|
51
|
+
...msg,
|
|
52
|
+
content: msg.content.map((part: any) => {
|
|
53
|
+
if (part.type === 'text' || part.type === 'reasoning') {
|
|
54
|
+
return { ...part, text: sanitizeSurrogates(part.text) };
|
|
55
|
+
}
|
|
56
|
+
if (part.type === 'tool-result') return sanitizeToolResult(part);
|
|
57
|
+
return part;
|
|
58
|
+
}),
|
|
59
|
+
} as typeof msg;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Anthropic and Bedrock reject empty text blocks; keep reasoning that is
|
|
65
|
+
* signed or redacted (upstream #14586, #26276).
|
|
66
|
+
*/
|
|
67
|
+
function dropEmptyParts(msgs: ModelMessage[], npm?: string) {
|
|
68
|
+
if (npm !== '@ai-sdk/anthropic' && npm !== '@ai-sdk/amazon-bedrock') {
|
|
69
|
+
return msgs;
|
|
70
|
+
}
|
|
71
|
+
const ns = npm === '@ai-sdk/anthropic' ? 'anthropic' : 'bedrock';
|
|
72
|
+
return msgs
|
|
73
|
+
.map((msg) => {
|
|
74
|
+
if (typeof msg.content === 'string') {
|
|
75
|
+
return msg.content === '' ? undefined : msg;
|
|
76
|
+
}
|
|
77
|
+
if (!Array.isArray(msg.content)) return msg;
|
|
78
|
+
const filtered = (msg.content as any[]).filter((part) => {
|
|
79
|
+
if (part.type === 'text') return part.text !== '';
|
|
80
|
+
if (part.type === 'reasoning') {
|
|
81
|
+
return (
|
|
82
|
+
part.text.trim().length > 0 ||
|
|
83
|
+
part.providerOptions?.[ns]?.signature != null ||
|
|
84
|
+
part.providerOptions?.[ns]?.redactedData != null
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
});
|
|
89
|
+
if (filtered.length === 0) return undefined;
|
|
90
|
+
return { ...msg, content: filtered } as typeof msg;
|
|
91
|
+
})
|
|
92
|
+
.filter((msg): msg is ModelMessage => msg !== undefined);
|
|
93
|
+
}
|
|
94
|
+
|
|
6
95
|
function normalizeMessages(
|
|
7
96
|
msgs: ModelMessage[],
|
|
8
97
|
providerID: string,
|
|
@@ -84,7 +173,8 @@ export namespace ProviderTransform {
|
|
|
84
173
|
|
|
85
174
|
function applyCaching(
|
|
86
175
|
msgs: ModelMessage[],
|
|
87
|
-
providerID: string
|
|
176
|
+
providerID: string,
|
|
177
|
+
npm?: string
|
|
88
178
|
): ModelMessage[] {
|
|
89
179
|
const system = msgs.filter((msg) => msg.role === 'system').slice(0, 2);
|
|
90
180
|
const final = msgs.filter((msg) => msg.role !== 'system').slice(-2);
|
|
@@ -96,35 +186,43 @@ export namespace ProviderTransform {
|
|
|
96
186
|
openrouter: {
|
|
97
187
|
cache_control: { type: 'ephemeral' },
|
|
98
188
|
},
|
|
189
|
+
// Bedrock only accepts the 'default' cache point (upstream #11664).
|
|
99
190
|
bedrock: {
|
|
100
|
-
cachePoint: { type: '
|
|
191
|
+
cachePoint: { type: 'default' },
|
|
101
192
|
},
|
|
102
193
|
openaiCompatible: {
|
|
103
194
|
cache_control: { type: 'ephemeral' },
|
|
104
195
|
},
|
|
105
196
|
};
|
|
106
197
|
|
|
198
|
+
// Bedrock reads the cache point from the message (upstream #18959).
|
|
199
|
+
const useMessageLevelOptions =
|
|
200
|
+
providerID === 'anthropic' ||
|
|
201
|
+
providerID.includes('bedrock') ||
|
|
202
|
+
npm === '@ai-sdk/amazon-bedrock';
|
|
203
|
+
|
|
107
204
|
for (const msg of unique([...system, ...final])) {
|
|
108
205
|
const shouldUseContentOptions =
|
|
109
|
-
|
|
206
|
+
!useMessageLevelOptions &&
|
|
110
207
|
Array.isArray(msg.content) &&
|
|
111
208
|
msg.content.length > 0;
|
|
112
209
|
|
|
210
|
+
// Deep-merge so signatures already in providerOptions survive (upstream #10323).
|
|
113
211
|
if (shouldUseContentOptions) {
|
|
114
212
|
const lastContent = msg.content[msg.content.length - 1];
|
|
115
213
|
if (lastContent && typeof lastContent === 'object') {
|
|
116
|
-
lastContent.providerOptions =
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
214
|
+
lastContent.providerOptions = mergeDeep(
|
|
215
|
+
lastContent.providerOptions ?? {},
|
|
216
|
+
providerOptions
|
|
217
|
+
) as typeof lastContent.providerOptions;
|
|
120
218
|
continue;
|
|
121
219
|
}
|
|
122
220
|
}
|
|
123
221
|
|
|
124
|
-
msg.providerOptions =
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
222
|
+
msg.providerOptions = mergeDeep(
|
|
223
|
+
msg.providerOptions ?? {},
|
|
224
|
+
providerOptions
|
|
225
|
+
) as typeof msg.providerOptions;
|
|
128
226
|
}
|
|
129
227
|
|
|
130
228
|
return msgs;
|
|
@@ -133,15 +231,18 @@ export namespace ProviderTransform {
|
|
|
133
231
|
export function message(
|
|
134
232
|
msgs: ModelMessage[],
|
|
135
233
|
providerID: string,
|
|
136
|
-
modelID: string
|
|
234
|
+
modelID: string,
|
|
235
|
+
npm?: string
|
|
137
236
|
) {
|
|
237
|
+
msgs = sanitizeMessages(msgs);
|
|
238
|
+
msgs = dropEmptyParts(msgs, npm);
|
|
138
239
|
msgs = normalizeMessages(msgs, providerID, modelID);
|
|
139
240
|
if (
|
|
140
241
|
providerID === 'anthropic' ||
|
|
141
242
|
modelID.includes('anthropic') ||
|
|
142
243
|
modelID.includes('claude')
|
|
143
244
|
) {
|
|
144
|
-
msgs = applyCaching(msgs, providerID);
|
|
245
|
+
msgs = applyCaching(msgs, providerID, npm);
|
|
145
246
|
}
|
|
146
247
|
|
|
147
248
|
return msgs;
|
|
@@ -159,6 +260,27 @@ export namespace ProviderTransform {
|
|
|
159
260
|
return undefined;
|
|
160
261
|
}
|
|
161
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Catalogue providers whose chat-completions endpoint accepts an unscoped
|
|
265
|
+
* `prompt_cache_key`, as audited by OpenCode
|
|
266
|
+
* (packages/core/src/plugin/provider/prompt-cache-key.ts).
|
|
267
|
+
* @see https://github.com/link-assistant/agent/issues/334
|
|
268
|
+
*/
|
|
269
|
+
export const PROMPT_CACHE_KEY_PROVIDERS = new Set([
|
|
270
|
+
'abliteration-ai',
|
|
271
|
+
'aihubmix',
|
|
272
|
+
'anyapi',
|
|
273
|
+
'auriko',
|
|
274
|
+
'edenai',
|
|
275
|
+
'jalapeno',
|
|
276
|
+
'llama',
|
|
277
|
+
'morph',
|
|
278
|
+
'snowflake-cortex',
|
|
279
|
+
'tencent-tokenhub',
|
|
280
|
+
'upstage',
|
|
281
|
+
'venice',
|
|
282
|
+
]);
|
|
283
|
+
|
|
162
284
|
export function options(
|
|
163
285
|
providerID: string,
|
|
164
286
|
modelID: string,
|
|
@@ -167,6 +289,14 @@ export namespace ProviderTransform {
|
|
|
167
289
|
): Record<string, any> | undefined {
|
|
168
290
|
const result: Record<string, any> = {};
|
|
169
291
|
|
|
292
|
+
if (
|
|
293
|
+
npm === '@ai-sdk/openai-compatible' &&
|
|
294
|
+
PROMPT_CACHE_KEY_PROVIDERS.has(providerID)
|
|
295
|
+
) {
|
|
296
|
+
// Unknown openai-compatible options are sent verbatim in the body.
|
|
297
|
+
result['prompt_cache_key'] = sessionID;
|
|
298
|
+
}
|
|
299
|
+
|
|
170
300
|
if (providerID === 'openai') {
|
|
171
301
|
result['promptCacheKey'] = sessionID;
|
|
172
302
|
}
|
|
@@ -251,11 +381,32 @@ export namespace ProviderTransform {
|
|
|
251
381
|
return standardLimit;
|
|
252
382
|
}
|
|
253
383
|
|
|
384
|
+
/**
|
|
385
|
+
* Moonshot expands $ref before validating and rejects sibling keywords, and
|
|
386
|
+
* it does not accept tuple-style `items` arrays (upstream #24730).
|
|
387
|
+
*/
|
|
388
|
+
function sanitizeMoonshot(node: unknown): unknown {
|
|
389
|
+
if (node === null || typeof node !== 'object') return node;
|
|
390
|
+
if (Array.isArray(node)) return node.map(sanitizeMoonshot);
|
|
391
|
+
if ('$ref' in node && typeof node.$ref === 'string') {
|
|
392
|
+
return { $ref: node.$ref };
|
|
393
|
+
}
|
|
394
|
+
const result: Record<string, unknown> = Object.fromEntries(
|
|
395
|
+
Object.entries(node).map(([key, value]) => [key, sanitizeMoonshot(value)])
|
|
396
|
+
);
|
|
397
|
+
if (Array.isArray(result.items)) result.items = result.items[0] ?? {};
|
|
398
|
+
return result;
|
|
399
|
+
}
|
|
400
|
+
|
|
254
401
|
export function schema(
|
|
255
|
-
|
|
256
|
-
|
|
402
|
+
providerID: string,
|
|
403
|
+
modelID: string,
|
|
257
404
|
schema: JSONSchema.BaseSchema
|
|
258
405
|
) {
|
|
406
|
+
if (providerID === 'moonshotai' || modelID.toLowerCase().includes('kimi')) {
|
|
407
|
+
schema = sanitizeMoonshot(schema) as JSONSchema.BaseSchema;
|
|
408
|
+
}
|
|
409
|
+
|
|
259
410
|
/*
|
|
260
411
|
if (["openai", "azure"].includes(providerID)) {
|
|
261
412
|
if (schema.type === "object" && schema.properties) {
|
|
@@ -347,7 +347,8 @@ export namespace SessionCompaction {
|
|
|
347
347
|
}
|
|
348
348
|
|
|
349
349
|
return false;
|
|
350
|
-
})
|
|
350
|
+
}),
|
|
351
|
+
{ providerID: model.providerID, modelID: model.modelID }
|
|
351
352
|
);
|
|
352
353
|
// Defensive check: ensure modelMessages is iterable (AI SDK 6.0.1 compatibility fix)
|
|
353
354
|
const safeModelMessages = Array.isArray(modelMessages) ? modelMessages : [];
|
|
@@ -410,7 +411,8 @@ export namespace SessionCompaction {
|
|
|
410
411
|
args.params.prompt = ProviderTransform.message(
|
|
411
412
|
args.params.prompt,
|
|
412
413
|
model.providerID,
|
|
413
|
-
model.modelID
|
|
414
|
+
model.modelID,
|
|
415
|
+
model.npm
|
|
414
416
|
);
|
|
415
417
|
}
|
|
416
418
|
return args.params;
|
|
@@ -420,6 +422,20 @@ export namespace SessionCompaction {
|
|
|
420
422
|
}),
|
|
421
423
|
})
|
|
422
424
|
);
|
|
425
|
+
// The summary request itself overflowed: compacting again would send the
|
|
426
|
+
// same history, so stop with a clear error (upstream #14707).
|
|
427
|
+
if (result === 'compact') {
|
|
428
|
+
processor.message.error = new MessageV2.ContextOverflowError({
|
|
429
|
+
message:
|
|
430
|
+
'Conversation history too large to compact - exceeds model context limit',
|
|
431
|
+
}).toObject();
|
|
432
|
+
await Session.updateMessage(processor.message);
|
|
433
|
+
Bus.publish(Session.Event.Error, {
|
|
434
|
+
sessionID: input.sessionID,
|
|
435
|
+
error: processor.message.error,
|
|
436
|
+
});
|
|
437
|
+
return 'stop';
|
|
438
|
+
}
|
|
423
439
|
if (result === 'continue') {
|
|
424
440
|
const continueMsg = await Session.updateMessage({
|
|
425
441
|
id: Identifier.ascending('message'),
|
package/src/session/index.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Decimal } from 'decimal.js';
|
|
2
|
+
import { FileTime } from '../file/time';
|
|
2
3
|
import z from 'zod';
|
|
3
4
|
import { type LanguageModelUsage, type ProviderMetadata } from 'ai';
|
|
4
5
|
import { Bus } from '../bus';
|
|
5
6
|
import { Config } from '../config/file-config';
|
|
6
|
-
import { isVerbose } from '../config/config';
|
|
7
|
+
import { config, isVerbose } from '../config/config';
|
|
7
8
|
import { Identifier } from '../id/id';
|
|
8
9
|
import type { ModelsDev } from '../provider/models';
|
|
9
10
|
import { Storage } from '../storage/storage';
|
|
@@ -328,6 +329,7 @@ export namespace Session {
|
|
|
328
329
|
await Storage.remove(msg);
|
|
329
330
|
}
|
|
330
331
|
await Storage.remove(['session', project.id, sessionID]);
|
|
332
|
+
FileTime.clear(sessionID);
|
|
331
333
|
Bus.publish(Event.Deleted, {
|
|
332
334
|
info: session,
|
|
333
335
|
});
|
|
@@ -757,11 +759,36 @@ export namespace Session {
|
|
|
757
759
|
)
|
|
758
760
|
)
|
|
759
761
|
: 0;
|
|
760
|
-
|
|
762
|
+
// AI SDK 7 reports reasoning under outputTokenDetails.
|
|
763
|
+
const detailReasoning = safeNum(
|
|
764
|
+
toNumber(
|
|
765
|
+
(
|
|
766
|
+
effectiveUsage as {
|
|
767
|
+
outputTokenDetails?: { reasoningTokens?: unknown };
|
|
768
|
+
}
|
|
769
|
+
).outputTokenDetails?.reasoningTokens,
|
|
770
|
+
'outputTokenDetails.reasoningTokens'
|
|
771
|
+
)
|
|
772
|
+
);
|
|
773
|
+
const reasoningTokens =
|
|
774
|
+
topLevelReasoningTokens || nestedReasoning || detailReasoning;
|
|
775
|
+
|
|
776
|
+
// outputTokens already includes reasoning, which is billed separately
|
|
777
|
+
// below, so take it out to avoid charging it twice (upstream #21047).
|
|
778
|
+
// Providers that report more reasoning than output count it apart.
|
|
779
|
+
const rawOutputTokens = safeNum(
|
|
780
|
+
toNumber(effectiveUsage.outputTokens, 'outputTokens')
|
|
781
|
+
);
|
|
782
|
+
// --legacy-token-usage / LINK_ASSISTANT_AGENT_LEGACY_TOKEN_USAGE keeps
|
|
783
|
+
// the raw figure (reasoning counted in output, as before).
|
|
784
|
+
const outputTokens =
|
|
785
|
+
!config.legacyTokenUsage && reasoningTokens <= rawOutputTokens
|
|
786
|
+
? rawOutputTokens - reasoningTokens
|
|
787
|
+
: rawOutputTokens;
|
|
761
788
|
|
|
762
789
|
const tokens = {
|
|
763
790
|
input: Math.max(0, adjustedInputTokens), // Ensure non-negative
|
|
764
|
-
output:
|
|
791
|
+
output: outputTokens,
|
|
765
792
|
reasoning: reasoningTokens,
|
|
766
793
|
cache: {
|
|
767
794
|
write: cacheWriteTokens,
|