@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.
@@ -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: model.providerID,
521
- modelID: model.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(
@@ -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 (default)
84
- // See: https://github.com/link-assistant/agent/issues/179
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
- const small = await Provider.getSmallModel(assistantMsg.providerID);
96
- if (!small) return;
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: small.info.reasoning ? 1500 : 20,
159
+ maxOutputTokens: titleMaxTokens,
104
160
  providerOptions: ProviderTransform.providerOptions(
105
- small.npm,
106
- small.providerID,
161
+ model.npm,
162
+ model.providerID,
107
163
  {}
108
164
  ),
109
165
  messages: [
110
- ...SystemPrompt.title(small.providerID).map(
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: small.info.headers,
127
- model: small.language,
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: small.language,
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
- ${JSON.stringify(modelMessages)}
235
+ ${conversationContent}
159
236
  </conversation>
160
237
  `,
161
238
  },
162
239
  ],
163
- headers: small.info.headers,
164
- }).catch(() => {});
165
- if (result) summary = result.text;
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 = 2000
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: 4000) */
92
+ /** Maximum chars for response body preview (default: 200000) */
93
93
  responseBodyMaxChars?: number;
94
- /** Maximum chars for request body preview (default: 2000) */
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 = 4000,
117
- requestBodyMaxChars = 2000,
116
+ responseBodyMaxChars = 200000,
117
+ requestBodyMaxChars = 200000,
118
118
  } = options;
119
119
 
120
120
  return async (