@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.
- package/package.json +1 -1
- package/src/auth/claude-oauth.ts +5 -3
- package/src/auth/plugins.ts +56 -48
- package/src/cli/cmd/auth.ts +6 -3
- package/src/cli/continuous-mode.js +5 -1
- package/src/config/config.ts +5 -3
- package/src/file/ripgrep.ts +3 -1
- package/src/flag/flag.ts +13 -7
- package/src/index.js +19 -4
- package/src/provider/google-cloudcode.ts +4 -2
- package/src/provider/models.ts +3 -1
- package/src/provider/provider.ts +130 -51
- package/src/session/compaction.ts +88 -1
- package/src/session/message-v2.ts +24 -0
- package/src/session/processor.ts +18 -0
- package/src/session/summary.ts +121 -22
- package/src/tool/codesearch.ts +4 -1
- package/src/tool/webfetch.ts +4 -1
- package/src/tool/websearch.ts +4 -1
- package/src/util/verbose-fetch.ts +303 -0
package/src/provider/provider.ts
CHANGED
|
@@ -1201,23 +1201,60 @@ export namespace Provider {
|
|
|
1201
1201
|
sessionID: provider.id,
|
|
1202
1202
|
});
|
|
1203
1203
|
|
|
1204
|
-
//
|
|
1205
|
-
//
|
|
1206
|
-
//
|
|
1207
|
-
//
|
|
1208
|
-
//
|
|
1209
|
-
// See: https://github.com/link-assistant/agent/issues/
|
|
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
|
-
|
|
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
|
-
//
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
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
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
init.body
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
? {
|
|
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
|
-
|
|
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
|
});
|
package/src/session/processor.ts
CHANGED
|
@@ -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) {
|
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 }));
|