@link-assistant/agent 0.16.17 → 0.17.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.
@@ -1201,23 +1201,60 @@ export namespace Provider {
1201
1201
  sessionID: provider.id,
1202
1202
  });
1203
1203
 
1204
- // Wrap fetch with verbose HTTP logging for debugging provider issues.
1205
- // IMPORTANT: The verbose check is done at call time (not SDK creation time)
1206
- // because the SDK is cached and Flag.OPENCODE_VERBOSE may change after creation.
1207
- // When verbose is disabled, the wrapper is a no-op passthrough with negligible overhead.
1208
- // See: https://github.com/link-assistant/agent/issues/200
1209
- // See: https://github.com/link-assistant/agent/issues/206
1204
+ // Verbose HTTP logging is handled by the global fetch monkey-patch
1205
+ // (installed in CLI middleware in index.js). The global patch catches ALL
1206
+ // HTTP calls reliably, regardless of how the AI SDK passes fetch internally.
1207
+ // This provider-level wrapper is kept as a fallback for environments where
1208
+ // the global patch may not be installed (e.g., programmatic use).
1209
+ // See: https://github.com/link-assistant/agent/issues/217
1210
+ // See: https://github.com/link-assistant/agent/issues/215
1210
1211
  {
1211
1212
  const innerFetch = options['fetch'];
1213
+ let verboseWrapperConfirmed = false;
1214
+ let httpCallCount = 0;
1215
+
1216
+ log.info('provider SDK fetch chain configured', {
1217
+ providerID: provider.id,
1218
+ pkg,
1219
+ globalVerboseFetchInstalled:
1220
+ !!globalThis.__agentVerboseFetchInstalled,
1221
+ verboseAtCreation: Flag.OPENCODE_VERBOSE,
1222
+ });
1223
+
1212
1224
  options['fetch'] = async (
1213
1225
  input: RequestInfo | URL,
1214
1226
  init?: RequestInit
1215
1227
  ): Promise<Response> => {
1216
- // Check verbose flag at call time — not at SDK creation time
1217
- if (!Flag.OPENCODE_VERBOSE) {
1228
+ // Check verbose flag at call time — not at SDK creation time.
1229
+ // When the global fetch monkey-patch is installed, it handles verbose
1230
+ // logging for all calls. The provider wrapper is a fallback for
1231
+ // environments without the global patch.
1232
+ // See: https://github.com/link-assistant/agent/issues/217
1233
+ if (
1234
+ !Flag.OPENCODE_VERBOSE ||
1235
+ globalThis.__agentVerboseFetchInstalled
1236
+ ) {
1218
1237
  return innerFetch(input, init);
1219
1238
  }
1220
1239
 
1240
+ httpCallCount++;
1241
+ const callNum = httpCallCount;
1242
+
1243
+ // Log a one-time confirmation that the verbose wrapper is active for this provider.
1244
+ // This diagnostic breadcrumb confirms the wrapper is in the fetch chain.
1245
+ // Also write to stderr as a redundant channel — stdout JSON may be filtered by wrappers.
1246
+ // See: https://github.com/link-assistant/agent/issues/215
1247
+ if (!verboseWrapperConfirmed) {
1248
+ verboseWrapperConfirmed = true;
1249
+ log.info('verbose HTTP logging active', {
1250
+ providerID: provider.id,
1251
+ });
1252
+ // Redundant stderr confirmation — visible even if stdout is piped/filtered
1253
+ process.stderr.write(
1254
+ `[verbose] HTTP logging active for provider: ${provider.id}\n`
1255
+ );
1256
+ }
1257
+
1221
1258
  const url =
1222
1259
  typeof input === 'string'
1223
1260
  ? input
@@ -1226,50 +1263,64 @@ export namespace Provider {
1226
1263
  : input.url;
1227
1264
  const method = init?.method ?? 'GET';
1228
1265
 
1229
- // Sanitize headers - mask authorization values
1230
- const sanitizedHeaders: Record<string, string> = {};
1231
- const rawHeaders = init?.headers;
1232
- if (rawHeaders) {
1233
- const entries =
1234
- rawHeaders instanceof Headers
1235
- ? Array.from(rawHeaders.entries())
1236
- : Array.isArray(rawHeaders)
1237
- ? rawHeaders
1238
- : Object.entries(rawHeaders);
1239
- for (const [key, value] of entries) {
1240
- const lower = key.toLowerCase();
1241
- if (
1242
- lower === 'authorization' ||
1243
- lower === 'x-api-key' ||
1244
- lower === 'api-key'
1245
- ) {
1246
- sanitizedHeaders[key] =
1247
- typeof value === 'string' && value.length > 8
1248
- ? value.slice(0, 4) + '...' + value.slice(-4)
1249
- : '[REDACTED]';
1250
- } else {
1251
- sanitizedHeaders[key] = String(value);
1266
+ // Wrap all verbose logging in try/catch so it never breaks the actual HTTP request.
1267
+ // If logging fails, the request must still proceed — logging is observability, not control flow.
1268
+ // See: https://github.com/link-assistant/agent/issues/215
1269
+ let sanitizedHeaders: Record<string, string> = {};
1270
+ let bodyPreview: string | undefined;
1271
+ try {
1272
+ // Sanitize headers - mask authorization values
1273
+ const rawHeaders = init?.headers;
1274
+ if (rawHeaders) {
1275
+ const entries =
1276
+ rawHeaders instanceof Headers
1277
+ ? Array.from(rawHeaders.entries())
1278
+ : Array.isArray(rawHeaders)
1279
+ ? rawHeaders
1280
+ : Object.entries(rawHeaders);
1281
+ for (const [key, value] of entries) {
1282
+ const lower = key.toLowerCase();
1283
+ if (
1284
+ lower === 'authorization' ||
1285
+ lower === 'x-api-key' ||
1286
+ lower === 'api-key'
1287
+ ) {
1288
+ sanitizedHeaders[key] =
1289
+ typeof value === 'string' && value.length > 8
1290
+ ? value.slice(0, 4) + '...' + value.slice(-4)
1291
+ : '[REDACTED]';
1292
+ } else {
1293
+ sanitizedHeaders[key] = String(value);
1294
+ }
1252
1295
  }
1253
1296
  }
1254
- }
1255
1297
 
1256
- // Log request body preview (truncated)
1257
- let bodyPreview: string | undefined;
1258
- if (init?.body) {
1259
- const bodyStr =
1260
- typeof init.body === 'string'
1261
- ? init.body
1262
- : init.body instanceof ArrayBuffer ||
1263
- init.body instanceof Uint8Array
1264
- ? `[binary ${(init.body as ArrayBuffer).byteLength ?? (init.body as Uint8Array).length} bytes]`
1265
- : undefined;
1266
- if (bodyStr && typeof bodyStr === 'string') {
1267
- bodyPreview =
1268
- bodyStr.length > 2000
1269
- ? bodyStr.slice(0, 2000) +
1270
- `... [truncated, total ${bodyStr.length} chars]`
1271
- : bodyStr;
1298
+ // Log request body preview (truncated)
1299
+ if (init?.body) {
1300
+ const bodyStr =
1301
+ typeof init.body === 'string'
1302
+ ? init.body
1303
+ : init.body instanceof ArrayBuffer ||
1304
+ init.body instanceof Uint8Array
1305
+ ? `[binary ${(init.body as ArrayBuffer).byteLength ?? (init.body as Uint8Array).length} bytes]`
1306
+ : undefined;
1307
+ if (bodyStr && typeof bodyStr === 'string') {
1308
+ bodyPreview =
1309
+ bodyStr.length > 200000
1310
+ ? bodyStr.slice(0, 200000) +
1311
+ `... [truncated, total ${bodyStr.length} chars]`
1312
+ : bodyStr;
1313
+ }
1272
1314
  }
1315
+ } catch (prepError) {
1316
+ // If header/body processing fails, log the error but continue with the request
1317
+ log.warn('verbose logging: failed to prepare request details', {
1318
+ providerID: provider.id,
1319
+ error:
1320
+ prepError instanceof Error
1321
+ ? prepError.message
1322
+ : String(prepError),
1323
+ });
1273
1324
  }
1274
1325
 
1275
1326
  // Use direct (non-lazy) logging for HTTP request/response to ensure output
@@ -1277,7 +1328,9 @@ export namespace Provider {
1277
1328
  // The verbose check is already done above, so lazy evaluation is not needed here.
1278
1329
  // See: https://github.com/link-assistant/agent/issues/211
1279
1330
  log.info('HTTP request', {
1331
+ caller: `provider/${provider.id}`,
1280
1332
  providerID: provider.id,
1333
+ callNum,
1281
1334
  method,
1282
1335
  url,
1283
1336
  headers: sanitizedHeaders,
@@ -1286,13 +1339,21 @@ export namespace Provider {
1286
1339
 
1287
1340
  const startMs = Date.now();
1288
1341
  try {
1289
- const response = await innerFetch(input, init);
1342
+ // Pass Bun-specific verbose:true to get detailed connection debugging
1343
+ // (prints request/response headers to stderr on socket errors).
1344
+ // This is a no-op on non-Bun runtimes.
1345
+ // See: https://bun.sh/docs/api/fetch
1346
+ // See: https://github.com/link-assistant/agent/issues/215
1347
+ const verboseInit = { ...init, verbose: true } as RequestInit;
1348
+ const response = await innerFetch(input, verboseInit);
1290
1349
  const durationMs = Date.now() - startMs;
1291
1350
 
1292
1351
  // Use direct (non-lazy) logging to ensure HTTP response details are captured
1293
1352
  // See: https://github.com/link-assistant/agent/issues/211
1294
1353
  log.info('HTTP response', {
1354
+ caller: `provider/${provider.id}`,
1295
1355
  providerID: provider.id,
1356
+ callNum,
1296
1357
  method,
1297
1358
  url,
1298
1359
  status: response.status,
@@ -1306,7 +1367,7 @@ export namespace Provider {
1306
1367
  // still receives the full stream while we asynchronously log a preview.
1307
1368
  // For non-streaming responses, buffer the body and reconstruct the Response.
1308
1369
  // See: https://github.com/link-assistant/agent/issues/204
1309
- const responseBodyMaxChars = 4000;
1370
+ const responseBodyMaxChars = 200000;
1310
1371
  const contentType = response.headers.get('content-type') ?? '';
1311
1372
  const isStreaming =
1312
1373
  contentType.includes('event-stream') ||
@@ -1342,7 +1403,9 @@ export namespace Provider {
1342
1403
  // Use direct (non-lazy) logging for stream body
1343
1404
  // See: https://github.com/link-assistant/agent/issues/211
1344
1405
  log.info('HTTP response body (stream)', {
1406
+ caller: `provider/${provider.id}`,
1345
1407
  providerID: provider.id,
1408
+ callNum,
1346
1409
  url,
1347
1410
  bodyPreview: truncated
1348
1411
  ? bodyPreview + `... [truncated]`
@@ -1370,7 +1433,9 @@ export namespace Provider {
1370
1433
  // Use direct (non-lazy) logging for non-streaming body
1371
1434
  // See: https://github.com/link-assistant/agent/issues/211
1372
1435
  log.info('HTTP response body', {
1436
+ caller: `provider/${provider.id}`,
1373
1437
  providerID: provider.id,
1438
+ callNum,
1374
1439
  url,
1375
1440
  bodyPreview,
1376
1441
  });
@@ -1386,15 +1451,29 @@ export namespace Provider {
1386
1451
  } catch (error) {
1387
1452
  const durationMs = Date.now() - startMs;
1388
1453
  // Use direct (non-lazy) logging for error path
1454
+ // Include stack trace and error cause for better debugging of socket errors
1389
1455
  // See: https://github.com/link-assistant/agent/issues/211
1456
+ // See: https://github.com/link-assistant/agent/issues/215
1390
1457
  log.error('HTTP request failed', {
1458
+ caller: `provider/${provider.id}`,
1391
1459
  providerID: provider.id,
1460
+ callNum,
1392
1461
  method,
1393
1462
  url,
1394
1463
  durationMs,
1395
1464
  error:
1396
1465
  error instanceof Error
1397
- ? { name: error.name, message: error.message }
1466
+ ? {
1467
+ name: error.name,
1468
+ message: error.message,
1469
+ stack: error.stack,
1470
+ cause:
1471
+ error.cause instanceof Error
1472
+ ? error.cause.message
1473
+ : error.cause
1474
+ ? String(error.cause)
1475
+ : undefined,
1476
+ }
1398
1477
  : String(error),
1399
1478
  });
1400
1479
  throw error;
@@ -28,6 +28,14 @@ export namespace SessionCompaction {
28
28
  ),
29
29
  };
30
30
 
31
+ /**
32
+ * Safety margin ratio for compaction trigger.
33
+ * We trigger compaction at 85% of usable context to avoid hitting hard limits.
34
+ * This means we stop 15% before (context - output) tokens.
35
+ * @see https://github.com/link-assistant/agent/issues/217
36
+ */
37
+ export const OVERFLOW_SAFETY_MARGIN = 0.85;
38
+
31
39
  export function isOverflow(input: {
32
40
  tokens: MessageV2.Assistant['tokens'];
33
41
  model: ModelsDev.Model;
@@ -41,7 +49,56 @@ export namespace SessionCompaction {
41
49
  Math.min(input.model.limit.output, SessionPrompt.OUTPUT_TOKEN_MAX) ||
42
50
  SessionPrompt.OUTPUT_TOKEN_MAX;
43
51
  const usable = context - output;
44
- return count > usable;
52
+ const safeLimit = Math.floor(usable * OVERFLOW_SAFETY_MARGIN);
53
+ const overflow = count > safeLimit;
54
+ log.info(() => ({
55
+ message: 'overflow check',
56
+ modelID: input.model.id,
57
+ contextLimit: context,
58
+ outputLimit: output,
59
+ usableContext: usable,
60
+ safeLimit,
61
+ safetyMargin: OVERFLOW_SAFETY_MARGIN,
62
+ currentTokens: count,
63
+ tokensBreakdown: {
64
+ input: input.tokens.input,
65
+ cacheRead: input.tokens.cache.read,
66
+ output: input.tokens.output,
67
+ },
68
+ overflow,
69
+ headroom: safeLimit - count,
70
+ }));
71
+ return overflow;
72
+ }
73
+
74
+ /**
75
+ * Compute context diagnostics for a given model and token usage.
76
+ * Used in step-finish parts to show context usage in JSON output.
77
+ * @see https://github.com/link-assistant/agent/issues/217
78
+ */
79
+ export function contextDiagnostics(input: {
80
+ tokens: { input: number; output: number; cache: { read: number } };
81
+ model: ModelsDev.Model;
82
+ }): MessageV2.ContextDiagnostics | undefined {
83
+ const contextLimit = input.model.limit.context;
84
+ if (contextLimit === 0) return undefined;
85
+ const outputLimit =
86
+ Math.min(input.model.limit.output, SessionPrompt.OUTPUT_TOKEN_MAX) ||
87
+ SessionPrompt.OUTPUT_TOKEN_MAX;
88
+ const usableContext = contextLimit - outputLimit;
89
+ const safeLimit = Math.floor(usableContext * OVERFLOW_SAFETY_MARGIN);
90
+ const currentTokens =
91
+ input.tokens.input + input.tokens.cache.read + input.tokens.output;
92
+ return {
93
+ contextLimit,
94
+ outputLimit,
95
+ usableContext,
96
+ safeLimit,
97
+ safetyMargin: OVERFLOW_SAFETY_MARGIN,
98
+ currentTokens,
99
+ headroom: safeLimit - currentTokens,
100
+ overflow: currentTokens > safeLimit,
101
+ };
45
102
  }
46
103
 
47
104
  export const PRUNE_MINIMUM = 20_000;
@@ -100,10 +157,27 @@ export namespace SessionCompaction {
100
157
  };
101
158
  abort: AbortSignal;
102
159
  }) {
160
+ log.info(() => ({
161
+ message: 'compaction process starting',
162
+ providerID: input.model.providerID,
163
+ modelID: input.model.modelID,
164
+ messageCount: input.messages.length,
165
+ sessionID: input.sessionID,
166
+ }));
103
167
  const model = await Provider.getModel(
104
168
  input.model.providerID,
105
169
  input.model.modelID
106
170
  );
171
+ if (Flag.OPENCODE_VERBOSE) {
172
+ log.info(() => ({
173
+ message: 'compaction model loaded',
174
+ providerID: model.providerID,
175
+ modelID: model.modelID,
176
+ npm: model.npm,
177
+ contextLimit: model.info.limit.context,
178
+ outputLimit: model.info.limit.output,
179
+ }));
180
+ }
107
181
  const system = [...SystemPrompt.summarize(model.providerID)];
108
182
  const msg = (await Session.updateMessage({
109
183
  id: Identifier.ascending('message'),
@@ -156,6 +230,19 @@ export namespace SessionCompaction {
156
230
  );
157
231
  // Defensive check: ensure modelMessages is iterable (AI SDK 6.0.1 compatibility fix)
158
232
  const safeModelMessages = Array.isArray(modelMessages) ? modelMessages : [];
233
+
234
+ if (Flag.OPENCODE_VERBOSE) {
235
+ log.info(() => ({
236
+ message: 'compaction streamText call',
237
+ providerID: model.providerID,
238
+ modelID: model.modelID,
239
+ systemPromptCount: system.length,
240
+ modelMessageCount: safeModelMessages.length,
241
+ filteredMessageCount: input.messages.length - safeModelMessages.length,
242
+ toolCall: model.info.tool_call,
243
+ }));
244
+ }
245
+
159
246
  const result = await processor.process(() =>
160
247
  streamText({
161
248
  onError(error) {
@@ -240,6 +240,27 @@ export namespace MessageV2 {
240
240
  });
241
241
  export type ModelInfo = z.infer<typeof ModelInfo>;
242
242
 
243
+ /**
244
+ * Context diagnostic info for step-finish parts.
245
+ * Shows model context limits and current usage to help debug compaction decisions.
246
+ * @see https://github.com/link-assistant/agent/issues/217
247
+ */
248
+ export const ContextDiagnostics = z
249
+ .object({
250
+ contextLimit: z.number(),
251
+ outputLimit: z.number(),
252
+ usableContext: z.number(),
253
+ safeLimit: z.number(),
254
+ safetyMargin: z.number(),
255
+ currentTokens: z.number(),
256
+ headroom: z.number(),
257
+ overflow: z.boolean(),
258
+ })
259
+ .meta({
260
+ ref: 'ContextDiagnostics',
261
+ });
262
+ export type ContextDiagnostics = z.infer<typeof ContextDiagnostics>;
263
+
243
264
  export const StepFinishPart = PartBase.extend({
244
265
  type: z.literal('step-finish'),
245
266
  reason: z.string(),
@@ -257,6 +278,9 @@ export namespace MessageV2 {
257
278
  // Model info included when --output-response-model is enabled
258
279
  // @see https://github.com/link-assistant/agent/issues/179
259
280
  model: ModelInfo.optional(),
281
+ // Context diagnostics for debugging compaction decisions
282
+ // @see https://github.com/link-assistant/agent/issues/217
283
+ context: ContextDiagnostics.optional(),
260
284
  }).meta({
261
285
  ref: 'StepFinishPart',
262
286
  });
@@ -17,6 +17,7 @@ import { Bus } from '../bus';
17
17
  import { SessionRetry } from './retry';
18
18
  import { SessionStatus } from './status';
19
19
  import { Flag } from '../flag/flag';
20
+ import { SessionCompaction } from './compaction';
20
21
 
21
22
  export namespace SessionProcessor {
22
23
  const DOOM_LOOP_THRESHOLD = 3;
@@ -366,6 +367,22 @@ export namespace SessionProcessor {
366
367
  }
367
368
  : undefined;
368
369
 
370
+ // Compute context diagnostics for JSON output
371
+ // @see https://github.com/link-assistant/agent/issues/217
372
+ const contextDiag = SessionCompaction.contextDiagnostics({
373
+ tokens: usage.tokens,
374
+ model: input.model,
375
+ });
376
+
377
+ if (Flag.OPENCODE_VERBOSE && contextDiag) {
378
+ log.info(() => ({
379
+ message: 'step-finish context diagnostics',
380
+ providerID: input.providerID,
381
+ modelID: input.model.id,
382
+ ...contextDiag,
383
+ }));
384
+ }
385
+
369
386
  await Session.updatePart({
370
387
  id: Identifier.ascending('part'),
371
388
  reason: finishReason,
@@ -376,6 +393,7 @@ export namespace SessionProcessor {
376
393
  tokens: usage.tokens,
377
394
  cost: usage.cost,
378
395
  model: modelInfo,
396
+ context: contextDiag,
379
397
  });
380
398
  await Session.updateMessage(input.assistantMessage);
381
399
  if (snapshot) {
@@ -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 }));