@link-assistant/agent 0.16.18 → 0.18.0
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 +1 -1
- package/src/cli/argv.ts +54 -16
- package/src/cli/continuous-mode.js +6 -2
- package/src/cli/defaults.ts +18 -0
- package/src/cli/model-config.js +87 -3
- package/src/cli/run-options.js +163 -0
- package/src/flag/flag.ts +13 -7
- package/src/index.js +31 -150
- package/src/provider/provider.ts +21 -16
- package/src/session/compaction.ts +164 -5
- package/src/session/message-v2.ts +32 -0
- package/src/session/processor.ts +18 -0
- package/src/session/prompt.ts +45 -2
- package/src/session/summary.ts +121 -22
- package/src/util/verbose-fetch.ts +5 -5
package/src/session/prompt.ts
CHANGED
|
@@ -89,6 +89,14 @@ export namespace SessionPrompt {
|
|
|
89
89
|
modelID: z.string(),
|
|
90
90
|
})
|
|
91
91
|
.optional(),
|
|
92
|
+
compactionModel: z
|
|
93
|
+
.object({
|
|
94
|
+
providerID: z.string(),
|
|
95
|
+
modelID: z.string(),
|
|
96
|
+
useSameModel: z.boolean(),
|
|
97
|
+
compactionSafetyMarginPercent: z.number(),
|
|
98
|
+
})
|
|
99
|
+
.optional(),
|
|
92
100
|
agent: z.string().optional(),
|
|
93
101
|
noReply: z.boolean().optional(),
|
|
94
102
|
system: z.string().optional(),
|
|
@@ -396,6 +404,28 @@ export namespace SessionPrompt {
|
|
|
396
404
|
// Re-throw the error so it can be handled by the caller
|
|
397
405
|
throw error;
|
|
398
406
|
}
|
|
407
|
+
// Resolve compaction model context limit for overflow detection (#219)
|
|
408
|
+
let compactionModelContextLimit: number | undefined;
|
|
409
|
+
const compactionModelConfig = lastUser.compactionModel;
|
|
410
|
+
if (compactionModelConfig && !compactionModelConfig.useSameModel) {
|
|
411
|
+
try {
|
|
412
|
+
const compactionModelResolved = await Provider.getModel(
|
|
413
|
+
compactionModelConfig.providerID,
|
|
414
|
+
compactionModelConfig.modelID
|
|
415
|
+
);
|
|
416
|
+
compactionModelContextLimit =
|
|
417
|
+
compactionModelResolved.info?.limit?.context;
|
|
418
|
+
} catch {
|
|
419
|
+
// If compaction model can't be resolved, fall back to default safety margin
|
|
420
|
+
log.info(() => ({
|
|
421
|
+
message:
|
|
422
|
+
'could not resolve compaction model for context limit — using default safety margin',
|
|
423
|
+
compactionProviderID: compactionModelConfig.providerID,
|
|
424
|
+
compactionModelID: compactionModelConfig.modelID,
|
|
425
|
+
}));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
399
429
|
const task = tasks.pop();
|
|
400
430
|
|
|
401
431
|
// pending subtask
|
|
@@ -512,13 +542,23 @@ export namespace SessionPrompt {
|
|
|
512
542
|
|
|
513
543
|
// pending compaction
|
|
514
544
|
if (task?.type === 'compaction') {
|
|
545
|
+
// Use compaction model if configured, otherwise fall back to base model
|
|
546
|
+
const compactionModelConfig = lastUser.compactionModel;
|
|
547
|
+
const compactionProviderID =
|
|
548
|
+
compactionModelConfig && !compactionModelConfig.useSameModel
|
|
549
|
+
? compactionModelConfig.providerID
|
|
550
|
+
: model.providerID;
|
|
551
|
+
const compactionModelID =
|
|
552
|
+
compactionModelConfig && !compactionModelConfig.useSameModel
|
|
553
|
+
? compactionModelConfig.modelID
|
|
554
|
+
: model.modelID;
|
|
515
555
|
const result = await SessionCompaction.process({
|
|
516
556
|
messages: msgs,
|
|
517
557
|
parentID: lastUser.id,
|
|
518
558
|
abort,
|
|
519
559
|
model: {
|
|
520
|
-
providerID:
|
|
521
|
-
modelID:
|
|
560
|
+
providerID: compactionProviderID,
|
|
561
|
+
modelID: compactionModelID,
|
|
522
562
|
},
|
|
523
563
|
sessionID,
|
|
524
564
|
});
|
|
@@ -533,6 +573,8 @@ export namespace SessionPrompt {
|
|
|
533
573
|
SessionCompaction.isOverflow({
|
|
534
574
|
tokens: lastFinished.tokens,
|
|
535
575
|
model: model.info ?? { id: model.modelID },
|
|
576
|
+
compactionModel: lastUser.compactionModel,
|
|
577
|
+
compactionModelContextLimit,
|
|
536
578
|
})
|
|
537
579
|
) {
|
|
538
580
|
await SessionCompaction.create({
|
|
@@ -1053,6 +1095,7 @@ export namespace SessionPrompt {
|
|
|
1053
1095
|
model: input.model,
|
|
1054
1096
|
agent,
|
|
1055
1097
|
}),
|
|
1098
|
+
compactionModel: input.compactionModel,
|
|
1056
1099
|
};
|
|
1057
1100
|
|
|
1058
1101
|
const parts = await Promise.all(
|
package/src/session/summary.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { Instance } from '../project/instance';
|
|
|
14
14
|
import { Storage } from '../storage/storage';
|
|
15
15
|
import { Bus } from '../bus';
|
|
16
16
|
import { Flag } from '../flag/flag';
|
|
17
|
+
import { Token } from '../util/token';
|
|
17
18
|
|
|
18
19
|
export namespace SessionSummary {
|
|
19
20
|
const log = Log.create({ service: 'session.summary' });
|
|
@@ -80,34 +81,89 @@ export namespace SessionSummary {
|
|
|
80
81
|
};
|
|
81
82
|
await Session.updateMessage(userMsg);
|
|
82
83
|
|
|
83
|
-
// Skip AI-powered summarization if disabled
|
|
84
|
-
// See: https://github.com/link-assistant/agent/issues/
|
|
84
|
+
// Skip AI-powered summarization if disabled
|
|
85
|
+
// See: https://github.com/link-assistant/agent/issues/217
|
|
85
86
|
if (!Flag.SUMMARIZE_SESSION) {
|
|
86
87
|
log.info(() => ({
|
|
87
88
|
message: 'session summarization disabled',
|
|
88
|
-
hint: 'Enable with --summarize-session flag or AGENT_SUMMARIZE_SESSION=true',
|
|
89
|
+
hint: 'Enable with --summarize-session flag (enabled by default) or AGENT_SUMMARIZE_SESSION=true',
|
|
89
90
|
}));
|
|
90
91
|
return;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
const assistantMsg = messages.find((m) => m.info.role === 'assistant')!
|
|
94
95
|
.info as MessageV2.Assistant;
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
|
|
97
|
+
// Use the same model as the main session (--model) instead of a small model
|
|
98
|
+
// This ensures consistent behavior and uses the model the user explicitly requested
|
|
99
|
+
// See: https://github.com/link-assistant/agent/issues/217
|
|
100
|
+
log.info(() => ({
|
|
101
|
+
message: 'loading model for summarization',
|
|
102
|
+
providerID: assistantMsg.providerID,
|
|
103
|
+
modelID: assistantMsg.modelID,
|
|
104
|
+
hint: 'Using same model as --model (not a small model)',
|
|
105
|
+
}));
|
|
106
|
+
const model = await Provider.getModel(
|
|
107
|
+
assistantMsg.providerID,
|
|
108
|
+
assistantMsg.modelID
|
|
109
|
+
).catch(() => null);
|
|
110
|
+
if (!model) {
|
|
111
|
+
log.info(() => ({
|
|
112
|
+
message: 'could not load session model for summarization, skipping',
|
|
113
|
+
providerID: assistantMsg.providerID,
|
|
114
|
+
modelID: assistantMsg.modelID,
|
|
115
|
+
}));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (Flag.OPENCODE_VERBOSE) {
|
|
120
|
+
log.info(() => ({
|
|
121
|
+
message: 'summarization model loaded',
|
|
122
|
+
providerID: model.providerID,
|
|
123
|
+
modelID: model.modelID,
|
|
124
|
+
npm: model.npm,
|
|
125
|
+
contextLimit: model.info.limit.context,
|
|
126
|
+
outputLimit: model.info.limit.output,
|
|
127
|
+
reasoning: model.info.reasoning,
|
|
128
|
+
toolCall: model.info.tool_call,
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
97
131
|
|
|
98
132
|
const textPart = msgWithParts.parts.find(
|
|
99
133
|
(p) => p.type === 'text' && !p.synthetic
|
|
100
134
|
) as MessageV2.TextPart;
|
|
101
135
|
if (textPart && !userMsg.summary?.title) {
|
|
136
|
+
const titleMaxTokens = model.info.reasoning ? 1500 : 20;
|
|
137
|
+
const systemPrompts = SystemPrompt.title(model.providerID);
|
|
138
|
+
const userContent = `
|
|
139
|
+
The following is the text to summarize:
|
|
140
|
+
<text>
|
|
141
|
+
${textPart?.text ?? ''}
|
|
142
|
+
</text>
|
|
143
|
+
`;
|
|
144
|
+
|
|
145
|
+
if (Flag.OPENCODE_VERBOSE) {
|
|
146
|
+
log.info(() => ({
|
|
147
|
+
message: 'generating title via API',
|
|
148
|
+
providerID: model.providerID,
|
|
149
|
+
modelID: model.modelID,
|
|
150
|
+
maxOutputTokens: titleMaxTokens,
|
|
151
|
+
systemPromptCount: systemPrompts.length,
|
|
152
|
+
userContentLength: userContent.length,
|
|
153
|
+
userContentTokenEstimate: Token.estimate(userContent),
|
|
154
|
+
userContentPreview: userContent.substring(0, 500),
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
|
|
102
158
|
const result = await generateText({
|
|
103
|
-
maxOutputTokens:
|
|
159
|
+
maxOutputTokens: titleMaxTokens,
|
|
104
160
|
providerOptions: ProviderTransform.providerOptions(
|
|
105
|
-
|
|
106
|
-
|
|
161
|
+
model.npm,
|
|
162
|
+
model.providerID,
|
|
107
163
|
{}
|
|
108
164
|
),
|
|
109
165
|
messages: [
|
|
110
|
-
...
|
|
166
|
+
...systemPrompts.map(
|
|
111
167
|
(x): ModelMessage => ({
|
|
112
168
|
role: 'system',
|
|
113
169
|
content: x,
|
|
@@ -115,17 +171,22 @@ export namespace SessionSummary {
|
|
|
115
171
|
),
|
|
116
172
|
{
|
|
117
173
|
role: 'user' as const,
|
|
118
|
-
content:
|
|
119
|
-
The following is the text to summarize:
|
|
120
|
-
<text>
|
|
121
|
-
${textPart?.text ?? ''}
|
|
122
|
-
</text>
|
|
123
|
-
`,
|
|
174
|
+
content: userContent,
|
|
124
175
|
},
|
|
125
176
|
],
|
|
126
|
-
headers:
|
|
127
|
-
model:
|
|
177
|
+
headers: model.info.headers,
|
|
178
|
+
model: model.language,
|
|
128
179
|
});
|
|
180
|
+
|
|
181
|
+
if (Flag.OPENCODE_VERBOSE) {
|
|
182
|
+
log.info(() => ({
|
|
183
|
+
message: 'title API response received',
|
|
184
|
+
providerID: model.providerID,
|
|
185
|
+
modelID: model.modelID,
|
|
186
|
+
titleLength: result.text.length,
|
|
187
|
+
usage: result.usage,
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
129
190
|
log.info(() => ({ message: 'title', title: result.text }));
|
|
130
191
|
userMsg.summary.title = result.text;
|
|
131
192
|
await Session.updateMessage(userMsg);
|
|
@@ -146,8 +207,24 @@ export namespace SessionSummary {
|
|
|
146
207
|
if (!summary || diffs.length > 0) {
|
|
147
208
|
// Pre-convert messages to ModelMessage format (async in AI SDK 6.0+)
|
|
148
209
|
const modelMessages = await MessageV2.toModelMessage(messages);
|
|
210
|
+
const conversationContent = JSON.stringify(modelMessages);
|
|
211
|
+
|
|
212
|
+
if (Flag.OPENCODE_VERBOSE) {
|
|
213
|
+
log.info(() => ({
|
|
214
|
+
message: 'generating body summary via API',
|
|
215
|
+
providerID: model.providerID,
|
|
216
|
+
modelID: model.modelID,
|
|
217
|
+
maxOutputTokens: 100,
|
|
218
|
+
conversationLength: conversationContent.length,
|
|
219
|
+
conversationTokenEstimate: Token.estimate(conversationContent),
|
|
220
|
+
messageCount: modelMessages.length,
|
|
221
|
+
diffsCount: diffs.length,
|
|
222
|
+
hasPriorSummary: !!summary,
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
|
|
149
226
|
const result = await generateText({
|
|
150
|
-
model:
|
|
227
|
+
model: model.language,
|
|
151
228
|
maxOutputTokens: 100,
|
|
152
229
|
messages: [
|
|
153
230
|
{
|
|
@@ -155,14 +232,36 @@ export namespace SessionSummary {
|
|
|
155
232
|
content: `
|
|
156
233
|
Summarize the following conversation into 2 sentences MAX explaining what the assistant did and why. Do not explain the user's input. Do not speak in the third person about the assistant.
|
|
157
234
|
<conversation>
|
|
158
|
-
${
|
|
235
|
+
${conversationContent}
|
|
159
236
|
</conversation>
|
|
160
237
|
`,
|
|
161
238
|
},
|
|
162
239
|
],
|
|
163
|
-
headers:
|
|
164
|
-
}).catch(() => {
|
|
165
|
-
|
|
240
|
+
headers: model.info.headers,
|
|
241
|
+
}).catch((err) => {
|
|
242
|
+
if (Flag.OPENCODE_VERBOSE) {
|
|
243
|
+
log.warn(() => ({
|
|
244
|
+
message: 'body summary API call failed',
|
|
245
|
+
providerID: model.providerID,
|
|
246
|
+
modelID: model.modelID,
|
|
247
|
+
error: err instanceof Error ? err.message : String(err),
|
|
248
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
return undefined;
|
|
252
|
+
});
|
|
253
|
+
if (result) {
|
|
254
|
+
if (Flag.OPENCODE_VERBOSE) {
|
|
255
|
+
log.info(() => ({
|
|
256
|
+
message: 'body summary API response received',
|
|
257
|
+
providerID: model.providerID,
|
|
258
|
+
modelID: model.modelID,
|
|
259
|
+
summaryLength: result.text.length,
|
|
260
|
+
usage: result.usage,
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
summary = result.text;
|
|
264
|
+
}
|
|
166
265
|
}
|
|
167
266
|
userMsg.summary.body = summary;
|
|
168
267
|
log.info(() => ({ message: 'body', body: summary }));
|
|
@@ -64,7 +64,7 @@ export function sanitizeHeaders(
|
|
|
64
64
|
*/
|
|
65
65
|
export function bodyPreview(
|
|
66
66
|
body: BodyInit | null | undefined,
|
|
67
|
-
maxChars =
|
|
67
|
+
maxChars = 200000
|
|
68
68
|
): string | undefined {
|
|
69
69
|
if (!body) return undefined;
|
|
70
70
|
|
|
@@ -89,9 +89,9 @@ export function bodyPreview(
|
|
|
89
89
|
export interface VerboseFetchOptions {
|
|
90
90
|
/** Identifier for the caller (e.g. 'webfetch', 'auth-plugins', 'config') */
|
|
91
91
|
caller: string;
|
|
92
|
-
/** Maximum chars for response body preview (default:
|
|
92
|
+
/** Maximum chars for response body preview (default: 200000) */
|
|
93
93
|
responseBodyMaxChars?: number;
|
|
94
|
-
/** Maximum chars for request body preview (default:
|
|
94
|
+
/** Maximum chars for request body preview (default: 200000) */
|
|
95
95
|
requestBodyMaxChars?: number;
|
|
96
96
|
}
|
|
97
97
|
|
|
@@ -113,8 +113,8 @@ export function createVerboseFetch(
|
|
|
113
113
|
): typeof fetch {
|
|
114
114
|
const {
|
|
115
115
|
caller,
|
|
116
|
-
responseBodyMaxChars =
|
|
117
|
-
requestBodyMaxChars =
|
|
116
|
+
responseBodyMaxChars = 200000,
|
|
117
|
+
requestBodyMaxChars = 200000,
|
|
118
118
|
} = options;
|
|
119
119
|
|
|
120
120
|
return async (
|