@link-assistant/agent 0.16.18 → 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/flag/flag.ts +13 -7
- package/src/index.js +16 -3
- package/src/provider/provider.ts +21 -16
- 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/util/verbose-fetch.ts +5 -5
package/package.json
CHANGED
package/src/flag/flag.ts
CHANGED
|
@@ -103,13 +103,19 @@ export namespace Flag {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
// Session summarization configuration
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
// See: https://github.com/link-assistant/agent/issues/
|
|
109
|
-
export let SUMMARIZE_SESSION =
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
106
|
+
// Enabled by default - generates AI-powered session summaries using the same model
|
|
107
|
+
// Can be disabled with --no-summarize-session or AGENT_SUMMARIZE_SESSION=false
|
|
108
|
+
// See: https://github.com/link-assistant/agent/issues/217
|
|
109
|
+
export let SUMMARIZE_SESSION = (() => {
|
|
110
|
+
const value = (
|
|
111
|
+
getEnv(
|
|
112
|
+
'LINK_ASSISTANT_AGENT_SUMMARIZE_SESSION',
|
|
113
|
+
'AGENT_SUMMARIZE_SESSION'
|
|
114
|
+
) ?? ''
|
|
115
|
+
).toLowerCase();
|
|
116
|
+
if (value === 'false' || value === '0') return false;
|
|
117
|
+
return true; // Default to true
|
|
118
|
+
})();
|
|
113
119
|
|
|
114
120
|
// Allow setting summarize-session mode programmatically (e.g., from CLI --summarize-session flag)
|
|
115
121
|
export function setSummarizeSession(value: boolean) {
|
package/src/index.js
CHANGED
|
@@ -733,8 +733,9 @@ async function main() {
|
|
|
733
733
|
})
|
|
734
734
|
.option('summarize-session', {
|
|
735
735
|
type: 'boolean',
|
|
736
|
-
description:
|
|
737
|
-
|
|
736
|
+
description:
|
|
737
|
+
'Generate AI session summaries (default: true). Use --no-summarize-session to disable.',
|
|
738
|
+
default: true,
|
|
738
739
|
}),
|
|
739
740
|
handler: async (argv) => {
|
|
740
741
|
// Check both CLI flag and environment variable for compact JSON mode
|
|
@@ -917,7 +918,10 @@ async function main() {
|
|
|
917
918
|
if (argv['output-response-model'] === false) {
|
|
918
919
|
Flag.setOutputResponseModel(false);
|
|
919
920
|
}
|
|
920
|
-
|
|
921
|
+
// summarize-session is enabled by default, only set if explicitly disabled
|
|
922
|
+
if (argv['summarize-session'] === false) {
|
|
923
|
+
Flag.setSummarizeSession(false);
|
|
924
|
+
} else {
|
|
921
925
|
Flag.setSummarizeSession(true);
|
|
922
926
|
}
|
|
923
927
|
// retry-on-rate-limits is enabled by default, only set if explicitly disabled
|
|
@@ -929,6 +933,15 @@ async function main() {
|
|
|
929
933
|
level: Flag.OPENCODE_VERBOSE ? 'DEBUG' : 'INFO',
|
|
930
934
|
compactJson: isCompact,
|
|
931
935
|
});
|
|
936
|
+
|
|
937
|
+
// Monkey-patch globalThis.fetch for raw HTTP logging in --verbose mode.
|
|
938
|
+
// Catches ALL HTTP calls regardless of AI SDK fetch passthrough. (#217)
|
|
939
|
+
if (!globalThis.__agentVerboseFetchInstalled) {
|
|
940
|
+
globalThis.fetch = createVerboseFetch(globalThis.fetch, {
|
|
941
|
+
caller: 'global',
|
|
942
|
+
});
|
|
943
|
+
globalThis.__agentVerboseFetchInstalled = true;
|
|
944
|
+
}
|
|
932
945
|
})
|
|
933
946
|
.fail((msg, err, yargs) => {
|
|
934
947
|
// Handle errors from command handlers
|
package/src/provider/provider.ts
CHANGED
|
@@ -1201,25 +1201,23 @@ 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
1210
|
// See: https://github.com/link-assistant/agent/issues/215
|
|
1211
1211
|
{
|
|
1212
1212
|
const innerFetch = options['fetch'];
|
|
1213
1213
|
let verboseWrapperConfirmed = false;
|
|
1214
1214
|
let httpCallCount = 0;
|
|
1215
1215
|
|
|
1216
|
-
|
|
1217
|
-
// This runs once per provider SDK creation (not per request).
|
|
1218
|
-
// If verbose is off at creation time, the per-request check still applies.
|
|
1219
|
-
// See: https://github.com/link-assistant/agent/issues/215
|
|
1220
|
-
log.info('verbose HTTP fetch wrapper installed', {
|
|
1216
|
+
log.info('provider SDK fetch chain configured', {
|
|
1221
1217
|
providerID: provider.id,
|
|
1222
1218
|
pkg,
|
|
1219
|
+
globalVerboseFetchInstalled:
|
|
1220
|
+
!!globalThis.__agentVerboseFetchInstalled,
|
|
1223
1221
|
verboseAtCreation: Flag.OPENCODE_VERBOSE,
|
|
1224
1222
|
});
|
|
1225
1223
|
|
|
@@ -1227,8 +1225,15 @@ export namespace Provider {
|
|
|
1227
1225
|
input: RequestInfo | URL,
|
|
1228
1226
|
init?: RequestInit
|
|
1229
1227
|
): Promise<Response> => {
|
|
1230
|
-
// Check verbose flag at call time — not at SDK creation time
|
|
1231
|
-
|
|
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
|
+
) {
|
|
1232
1237
|
return innerFetch(input, init);
|
|
1233
1238
|
}
|
|
1234
1239
|
|
|
@@ -1301,8 +1306,8 @@ export namespace Provider {
|
|
|
1301
1306
|
: undefined;
|
|
1302
1307
|
if (bodyStr && typeof bodyStr === 'string') {
|
|
1303
1308
|
bodyPreview =
|
|
1304
|
-
bodyStr.length >
|
|
1305
|
-
? bodyStr.slice(0,
|
|
1309
|
+
bodyStr.length > 200000
|
|
1310
|
+
? bodyStr.slice(0, 200000) +
|
|
1306
1311
|
`... [truncated, total ${bodyStr.length} chars]`
|
|
1307
1312
|
: bodyStr;
|
|
1308
1313
|
}
|
|
@@ -1362,7 +1367,7 @@ export namespace Provider {
|
|
|
1362
1367
|
// still receives the full stream while we asynchronously log a preview.
|
|
1363
1368
|
// For non-streaming responses, buffer the body and reconstruct the Response.
|
|
1364
1369
|
// See: https://github.com/link-assistant/agent/issues/204
|
|
1365
|
-
const responseBodyMaxChars =
|
|
1370
|
+
const responseBodyMaxChars = 200000;
|
|
1366
1371
|
const contentType = response.headers.get('content-type') ?? '';
|
|
1367
1372
|
const isStreaming =
|
|
1368
1373
|
contentType.includes('event-stream') ||
|
|
@@ -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 }));
|
|
@@ -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 (
|