@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/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Server } from './server/server.ts';
|
|
|
6
6
|
import { Instance } from './project/instance.ts';
|
|
7
7
|
import { Log } from './util/log.ts';
|
|
8
8
|
import { parseModelConfig } from './cli/model-config.js';
|
|
9
|
-
import {
|
|
9
|
+
import { buildRunOptions } from './cli/run-options.js';
|
|
10
10
|
// Bus is used via createBusEventSubscription in event-handler.js
|
|
11
11
|
import { Session } from './session/index.ts';
|
|
12
12
|
import { SessionPrompt } from './session/prompt.ts';
|
|
@@ -278,7 +278,7 @@ async function runAgentMode(argv, request) {
|
|
|
278
278
|
fn: async () => {
|
|
279
279
|
// Parse model config inside Instance.provide context
|
|
280
280
|
// This allows parseModelWithResolution to access the provider state
|
|
281
|
-
const { providerID, modelID } = await parseModelConfig(
|
|
281
|
+
const { providerID, modelID, compactionModel } = await parseModelConfig(
|
|
282
282
|
argv,
|
|
283
283
|
outputError,
|
|
284
284
|
outputStatus
|
|
@@ -293,7 +293,8 @@ async function runAgentMode(argv, request) {
|
|
|
293
293
|
modelID,
|
|
294
294
|
systemMessage,
|
|
295
295
|
appendSystemMessage,
|
|
296
|
-
jsonStandard
|
|
296
|
+
jsonStandard,
|
|
297
|
+
compactionModel
|
|
297
298
|
);
|
|
298
299
|
} else {
|
|
299
300
|
// DIRECT MODE: Run everything in single process
|
|
@@ -304,7 +305,8 @@ async function runAgentMode(argv, request) {
|
|
|
304
305
|
modelID,
|
|
305
306
|
systemMessage,
|
|
306
307
|
appendSystemMessage,
|
|
307
|
-
jsonStandard
|
|
308
|
+
jsonStandard,
|
|
309
|
+
compactionModel
|
|
308
310
|
);
|
|
309
311
|
}
|
|
310
312
|
},
|
|
@@ -363,7 +365,7 @@ async function runContinuousAgentMode(argv) {
|
|
|
363
365
|
fn: async () => {
|
|
364
366
|
// Parse model config inside Instance.provide context
|
|
365
367
|
// This allows parseModelWithResolution to access the provider state
|
|
366
|
-
const { providerID, modelID } = await parseModelConfig(
|
|
368
|
+
const { providerID, modelID, compactionModel } = await parseModelConfig(
|
|
367
369
|
argv,
|
|
368
370
|
outputError,
|
|
369
371
|
outputStatus
|
|
@@ -377,7 +379,8 @@ async function runContinuousAgentMode(argv) {
|
|
|
377
379
|
modelID,
|
|
378
380
|
systemMessage,
|
|
379
381
|
appendSystemMessage,
|
|
380
|
-
jsonStandard
|
|
382
|
+
jsonStandard,
|
|
383
|
+
compactionModel
|
|
381
384
|
);
|
|
382
385
|
} else {
|
|
383
386
|
// DIRECT MODE: Run everything in single process
|
|
@@ -387,7 +390,8 @@ async function runContinuousAgentMode(argv) {
|
|
|
387
390
|
modelID,
|
|
388
391
|
systemMessage,
|
|
389
392
|
appendSystemMessage,
|
|
390
|
-
jsonStandard
|
|
393
|
+
jsonStandard,
|
|
394
|
+
compactionModel
|
|
391
395
|
);
|
|
392
396
|
}
|
|
393
397
|
},
|
|
@@ -409,7 +413,8 @@ async function runServerMode(
|
|
|
409
413
|
modelID,
|
|
410
414
|
systemMessage,
|
|
411
415
|
appendSystemMessage,
|
|
412
|
-
jsonStandard
|
|
416
|
+
jsonStandard,
|
|
417
|
+
compactionModel
|
|
413
418
|
) {
|
|
414
419
|
const compactJson = argv['compact-json'] === true;
|
|
415
420
|
|
|
@@ -475,6 +480,7 @@ async function runServerMode(
|
|
|
475
480
|
providerID,
|
|
476
481
|
modelID,
|
|
477
482
|
},
|
|
483
|
+
compactionModel,
|
|
478
484
|
system: systemMessage,
|
|
479
485
|
appendSystem: appendSystemMessage,
|
|
480
486
|
}),
|
|
@@ -508,7 +514,8 @@ async function runDirectMode(
|
|
|
508
514
|
modelID,
|
|
509
515
|
systemMessage,
|
|
510
516
|
appendSystemMessage,
|
|
511
|
-
jsonStandard
|
|
517
|
+
jsonStandard,
|
|
518
|
+
compactionModel
|
|
512
519
|
) {
|
|
513
520
|
const compactJson = argv['compact-json'] === true;
|
|
514
521
|
|
|
@@ -558,6 +565,7 @@ async function runDirectMode(
|
|
|
558
565
|
providerID,
|
|
559
566
|
modelID,
|
|
560
567
|
},
|
|
568
|
+
compactionModel,
|
|
561
569
|
system: systemMessage,
|
|
562
570
|
appendSystem: appendSystemMessage,
|
|
563
571
|
}).catch((error) => {
|
|
@@ -596,146 +604,7 @@ async function main() {
|
|
|
596
604
|
.command({
|
|
597
605
|
command: '$0',
|
|
598
606
|
describe: 'Run agent in interactive or stdin mode (default)',
|
|
599
|
-
builder:
|
|
600
|
-
yargs
|
|
601
|
-
.option('model', {
|
|
602
|
-
type: 'string',
|
|
603
|
-
description: 'Model to use in format providerID/modelID',
|
|
604
|
-
default: DEFAULT_MODEL,
|
|
605
|
-
})
|
|
606
|
-
.option('json-standard', {
|
|
607
|
-
type: 'string',
|
|
608
|
-
description:
|
|
609
|
-
'JSON output format standard: "opencode" (default) or "claude" (experimental)',
|
|
610
|
-
default: 'opencode',
|
|
611
|
-
choices: ['opencode', 'claude'],
|
|
612
|
-
})
|
|
613
|
-
.option('system-message', {
|
|
614
|
-
type: 'string',
|
|
615
|
-
description: 'Full override of the system message',
|
|
616
|
-
})
|
|
617
|
-
.option('system-message-file', {
|
|
618
|
-
type: 'string',
|
|
619
|
-
description: 'Full override of the system message from file',
|
|
620
|
-
})
|
|
621
|
-
.option('append-system-message', {
|
|
622
|
-
type: 'string',
|
|
623
|
-
description: 'Append to the default system message',
|
|
624
|
-
})
|
|
625
|
-
.option('append-system-message-file', {
|
|
626
|
-
type: 'string',
|
|
627
|
-
description: 'Append to the default system message from file',
|
|
628
|
-
})
|
|
629
|
-
.option('server', {
|
|
630
|
-
type: 'boolean',
|
|
631
|
-
description: 'Run in server mode (default)',
|
|
632
|
-
default: true,
|
|
633
|
-
})
|
|
634
|
-
.option('verbose', {
|
|
635
|
-
type: 'boolean',
|
|
636
|
-
description:
|
|
637
|
-
'Enable verbose mode to debug API requests (shows system prompt, token counts, etc.)',
|
|
638
|
-
default: false,
|
|
639
|
-
})
|
|
640
|
-
.option('dry-run', {
|
|
641
|
-
type: 'boolean',
|
|
642
|
-
description:
|
|
643
|
-
'Simulate operations without making actual API calls or package installations (useful for testing)',
|
|
644
|
-
default: false,
|
|
645
|
-
})
|
|
646
|
-
.option('use-existing-claude-oauth', {
|
|
647
|
-
type: 'boolean',
|
|
648
|
-
description:
|
|
649
|
-
'Use existing Claude OAuth credentials from ~/.claude/.credentials.json (from Claude Code CLI)',
|
|
650
|
-
default: false,
|
|
651
|
-
})
|
|
652
|
-
.option('prompt', {
|
|
653
|
-
alias: 'p',
|
|
654
|
-
type: 'string',
|
|
655
|
-
description:
|
|
656
|
-
'Prompt message to send directly (bypasses stdin reading)',
|
|
657
|
-
})
|
|
658
|
-
.option('disable-stdin', {
|
|
659
|
-
type: 'boolean',
|
|
660
|
-
description:
|
|
661
|
-
'Disable stdin streaming mode (requires --prompt or shows help)',
|
|
662
|
-
default: false,
|
|
663
|
-
})
|
|
664
|
-
.option('stdin-stream-timeout', {
|
|
665
|
-
type: 'number',
|
|
666
|
-
description:
|
|
667
|
-
'Optional timeout in milliseconds for stdin reading (default: no timeout)',
|
|
668
|
-
})
|
|
669
|
-
.option('auto-merge-queued-messages', {
|
|
670
|
-
type: 'boolean',
|
|
671
|
-
description:
|
|
672
|
-
'Enable auto-merging of rapidly arriving input lines into single messages (default: true)',
|
|
673
|
-
default: true,
|
|
674
|
-
})
|
|
675
|
-
.option('interactive', {
|
|
676
|
-
type: 'boolean',
|
|
677
|
-
description:
|
|
678
|
-
'Enable interactive mode to accept manual input as plain text strings (default: true). Use --no-interactive to only accept JSON input.',
|
|
679
|
-
default: true,
|
|
680
|
-
})
|
|
681
|
-
.option('always-accept-stdin', {
|
|
682
|
-
type: 'boolean',
|
|
683
|
-
description:
|
|
684
|
-
'Keep accepting stdin input even after the agent finishes work (default: true). Use --no-always-accept-stdin for single-message mode.',
|
|
685
|
-
default: true,
|
|
686
|
-
})
|
|
687
|
-
.option('compact-json', {
|
|
688
|
-
type: 'boolean',
|
|
689
|
-
description:
|
|
690
|
-
'Output compact JSON (single line) instead of pretty-printed JSON (default: false). Useful for program-to-program communication.',
|
|
691
|
-
default: false,
|
|
692
|
-
})
|
|
693
|
-
.option('resume', {
|
|
694
|
-
alias: 'r',
|
|
695
|
-
type: 'string',
|
|
696
|
-
description:
|
|
697
|
-
'Resume a specific session by ID. By default, forks the session with a new UUID. Use --no-fork to continue in the same session.',
|
|
698
|
-
})
|
|
699
|
-
.option('continue', {
|
|
700
|
-
alias: 'c',
|
|
701
|
-
type: 'boolean',
|
|
702
|
-
description:
|
|
703
|
-
'Continue the most recent session. By default, forks the session with a new UUID. Use --no-fork to continue in the same session.',
|
|
704
|
-
default: false,
|
|
705
|
-
})
|
|
706
|
-
.option('no-fork', {
|
|
707
|
-
type: 'boolean',
|
|
708
|
-
description:
|
|
709
|
-
'When used with --resume or --continue, continue in the same session without forking to a new UUID.',
|
|
710
|
-
default: false,
|
|
711
|
-
})
|
|
712
|
-
.option('generate-title', {
|
|
713
|
-
type: 'boolean',
|
|
714
|
-
description:
|
|
715
|
-
'Generate session titles using AI (default: false). Disabling saves tokens and prevents rate limit issues.',
|
|
716
|
-
default: false,
|
|
717
|
-
})
|
|
718
|
-
.option('retry-timeout', {
|
|
719
|
-
type: 'number',
|
|
720
|
-
description:
|
|
721
|
-
'Maximum total retry time in seconds for rate limit errors (default: 604800 = 7 days)',
|
|
722
|
-
})
|
|
723
|
-
.option('retry-on-rate-limits', {
|
|
724
|
-
type: 'boolean',
|
|
725
|
-
description:
|
|
726
|
-
'Retry AI completions API requests when rate limited (HTTP 429). Use --no-retry-on-rate-limits in integration tests to fail fast instead of waiting.',
|
|
727
|
-
default: true,
|
|
728
|
-
})
|
|
729
|
-
.option('output-response-model', {
|
|
730
|
-
type: 'boolean',
|
|
731
|
-
description: 'Include model info in step_finish output',
|
|
732
|
-
default: true,
|
|
733
|
-
})
|
|
734
|
-
.option('summarize-session', {
|
|
735
|
-
type: 'boolean',
|
|
736
|
-
description: 'Generate AI session summaries',
|
|
737
|
-
default: false,
|
|
738
|
-
}),
|
|
607
|
+
builder: buildRunOptions,
|
|
739
608
|
handler: async (argv) => {
|
|
740
609
|
// Check both CLI flag and environment variable for compact JSON mode
|
|
741
610
|
const compactJson =
|
|
@@ -917,7 +786,10 @@ async function main() {
|
|
|
917
786
|
if (argv['output-response-model'] === false) {
|
|
918
787
|
Flag.setOutputResponseModel(false);
|
|
919
788
|
}
|
|
920
|
-
|
|
789
|
+
// summarize-session is enabled by default, only set if explicitly disabled
|
|
790
|
+
if (argv['summarize-session'] === false) {
|
|
791
|
+
Flag.setSummarizeSession(false);
|
|
792
|
+
} else {
|
|
921
793
|
Flag.setSummarizeSession(true);
|
|
922
794
|
}
|
|
923
795
|
// retry-on-rate-limits is enabled by default, only set if explicitly disabled
|
|
@@ -929,6 +801,15 @@ async function main() {
|
|
|
929
801
|
level: Flag.OPENCODE_VERBOSE ? 'DEBUG' : 'INFO',
|
|
930
802
|
compactJson: isCompact,
|
|
931
803
|
});
|
|
804
|
+
|
|
805
|
+
// Monkey-patch globalThis.fetch for raw HTTP logging in --verbose mode.
|
|
806
|
+
// Catches ALL HTTP calls regardless of AI SDK fetch passthrough. (#217)
|
|
807
|
+
if (!globalThis.__agentVerboseFetchInstalled) {
|
|
808
|
+
globalThis.fetch = createVerboseFetch(globalThis.fetch, {
|
|
809
|
+
caller: 'global',
|
|
810
|
+
});
|
|
811
|
+
globalThis.__agentVerboseFetchInstalled = true;
|
|
812
|
+
}
|
|
932
813
|
})
|
|
933
814
|
.fail((msg, err, yargs) => {
|
|
934
815
|
// 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,20 +28,149 @@ export namespace SessionCompaction {
|
|
|
28
28
|
),
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Default 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
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Compaction model configuration passed from CLI.
|
|
41
|
+
* @see https://github.com/link-assistant/agent/issues/219
|
|
42
|
+
*/
|
|
43
|
+
export interface CompactionModelConfig {
|
|
44
|
+
providerID: string;
|
|
45
|
+
modelID: string;
|
|
46
|
+
useSameModel: boolean;
|
|
47
|
+
compactionSafetyMarginPercent: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Compute the effective safety margin ratio.
|
|
52
|
+
*
|
|
53
|
+
* When the compaction model has a larger context window than the base model,
|
|
54
|
+
* the entire base model context can be used (ratio = 1.0, i.e. 0% margin),
|
|
55
|
+
* because the compaction model can ingest all of it.
|
|
56
|
+
*
|
|
57
|
+
* When the compaction model has equal or smaller context, the configured
|
|
58
|
+
* safety margin applies (default 15% → ratio 0.85).
|
|
59
|
+
*
|
|
60
|
+
* @see https://github.com/link-assistant/agent/issues/219
|
|
61
|
+
*/
|
|
62
|
+
export function computeSafetyMarginRatio(input: {
|
|
63
|
+
baseModelContextLimit: number;
|
|
64
|
+
compactionModel?: CompactionModelConfig;
|
|
65
|
+
compactionModelContextLimit?: number;
|
|
66
|
+
}): number {
|
|
67
|
+
const compactionModelConfig = input.compactionModel;
|
|
68
|
+
if (!compactionModelConfig) return OVERFLOW_SAFETY_MARGIN;
|
|
69
|
+
|
|
70
|
+
const compactionSafetyMarginPercent =
|
|
71
|
+
compactionModelConfig.compactionSafetyMarginPercent;
|
|
72
|
+
const configuredRatio = 1 - compactionSafetyMarginPercent / 100;
|
|
73
|
+
|
|
74
|
+
// When using the same model, always apply the configured safety margin
|
|
75
|
+
if (compactionModelConfig.useSameModel) return configuredRatio;
|
|
76
|
+
|
|
77
|
+
// When compaction model has a larger context, no safety margin needed
|
|
78
|
+
const compactionContextLimit = input.compactionModelContextLimit ?? 0;
|
|
79
|
+
if (
|
|
80
|
+
compactionContextLimit > 0 &&
|
|
81
|
+
compactionContextLimit > input.baseModelContextLimit
|
|
82
|
+
) {
|
|
83
|
+
log.info(() => ({
|
|
84
|
+
message:
|
|
85
|
+
'compaction model has larger context — using full base model context',
|
|
86
|
+
baseModelContextLimit: input.baseModelContextLimit,
|
|
87
|
+
compactionModelContextLimit: compactionContextLimit,
|
|
88
|
+
}));
|
|
89
|
+
return 1.0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return configuredRatio;
|
|
93
|
+
}
|
|
94
|
+
|
|
31
95
|
export function isOverflow(input: {
|
|
32
96
|
tokens: MessageV2.Assistant['tokens'];
|
|
33
97
|
model: ModelsDev.Model;
|
|
98
|
+
compactionModel?: CompactionModelConfig;
|
|
99
|
+
compactionModelContextLimit?: number;
|
|
34
100
|
}) {
|
|
35
101
|
if (Flag.OPENCODE_DISABLE_AUTOCOMPACT) return false;
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
102
|
+
const baseModelContextLimit = input.model.limit.context;
|
|
103
|
+
if (baseModelContextLimit === 0) return false;
|
|
38
104
|
const count =
|
|
39
105
|
input.tokens.input + input.tokens.cache.read + input.tokens.output;
|
|
40
|
-
const
|
|
106
|
+
const outputTokenLimit =
|
|
41
107
|
Math.min(input.model.limit.output, SessionPrompt.OUTPUT_TOKEN_MAX) ||
|
|
42
108
|
SessionPrompt.OUTPUT_TOKEN_MAX;
|
|
43
|
-
const
|
|
44
|
-
|
|
109
|
+
const usableContextWindow = baseModelContextLimit - outputTokenLimit;
|
|
110
|
+
const safetyMarginRatio = computeSafetyMarginRatio({
|
|
111
|
+
baseModelContextLimit,
|
|
112
|
+
compactionModel: input.compactionModel,
|
|
113
|
+
compactionModelContextLimit: input.compactionModelContextLimit,
|
|
114
|
+
});
|
|
115
|
+
const safeLimit = Math.floor(usableContextWindow * safetyMarginRatio);
|
|
116
|
+
const overflow = count > safeLimit;
|
|
117
|
+
log.info(() => ({
|
|
118
|
+
message: 'overflow check',
|
|
119
|
+
modelID: input.model.id,
|
|
120
|
+
contextLimit: baseModelContextLimit,
|
|
121
|
+
outputLimit: outputTokenLimit,
|
|
122
|
+
usableContextWindow,
|
|
123
|
+
safeLimit,
|
|
124
|
+
safetyMarginRatio,
|
|
125
|
+
compactionModelID: input.compactionModel?.modelID,
|
|
126
|
+
compactionModelContextLimit: input.compactionModelContextLimit,
|
|
127
|
+
currentTokens: count,
|
|
128
|
+
tokensBreakdown: {
|
|
129
|
+
input: input.tokens.input,
|
|
130
|
+
cacheRead: input.tokens.cache.read,
|
|
131
|
+
output: input.tokens.output,
|
|
132
|
+
},
|
|
133
|
+
overflow,
|
|
134
|
+
headroom: safeLimit - count,
|
|
135
|
+
}));
|
|
136
|
+
return overflow;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Compute context diagnostics for a given model and token usage.
|
|
141
|
+
* Used in step-finish parts to show context usage in JSON output.
|
|
142
|
+
* @see https://github.com/link-assistant/agent/issues/217
|
|
143
|
+
*/
|
|
144
|
+
export function contextDiagnostics(input: {
|
|
145
|
+
tokens: { input: number; output: number; cache: { read: number } };
|
|
146
|
+
model: ModelsDev.Model;
|
|
147
|
+
compactionModel?: CompactionModelConfig;
|
|
148
|
+
compactionModelContextLimit?: number;
|
|
149
|
+
}): MessageV2.ContextDiagnostics | undefined {
|
|
150
|
+
const contextLimit = input.model.limit.context;
|
|
151
|
+
if (contextLimit === 0) return undefined;
|
|
152
|
+
const outputLimit =
|
|
153
|
+
Math.min(input.model.limit.output, SessionPrompt.OUTPUT_TOKEN_MAX) ||
|
|
154
|
+
SessionPrompt.OUTPUT_TOKEN_MAX;
|
|
155
|
+
const usableContext = contextLimit - outputLimit;
|
|
156
|
+
const safetyMarginRatio = computeSafetyMarginRatio({
|
|
157
|
+
baseModelContextLimit: contextLimit,
|
|
158
|
+
compactionModel: input.compactionModel,
|
|
159
|
+
compactionModelContextLimit: input.compactionModelContextLimit,
|
|
160
|
+
});
|
|
161
|
+
const safeLimit = Math.floor(usableContext * safetyMarginRatio);
|
|
162
|
+
const currentTokens =
|
|
163
|
+
input.tokens.input + input.tokens.cache.read + input.tokens.output;
|
|
164
|
+
return {
|
|
165
|
+
contextLimit,
|
|
166
|
+
outputLimit,
|
|
167
|
+
usableContext,
|
|
168
|
+
safeLimit,
|
|
169
|
+
safetyMargin: safetyMarginRatio,
|
|
170
|
+
currentTokens,
|
|
171
|
+
headroom: safeLimit - currentTokens,
|
|
172
|
+
overflow: currentTokens > safeLimit,
|
|
173
|
+
};
|
|
45
174
|
}
|
|
46
175
|
|
|
47
176
|
export const PRUNE_MINIMUM = 20_000;
|
|
@@ -100,10 +229,27 @@ export namespace SessionCompaction {
|
|
|
100
229
|
};
|
|
101
230
|
abort: AbortSignal;
|
|
102
231
|
}) {
|
|
232
|
+
log.info(() => ({
|
|
233
|
+
message: 'compaction process starting',
|
|
234
|
+
providerID: input.model.providerID,
|
|
235
|
+
modelID: input.model.modelID,
|
|
236
|
+
messageCount: input.messages.length,
|
|
237
|
+
sessionID: input.sessionID,
|
|
238
|
+
}));
|
|
103
239
|
const model = await Provider.getModel(
|
|
104
240
|
input.model.providerID,
|
|
105
241
|
input.model.modelID
|
|
106
242
|
);
|
|
243
|
+
if (Flag.OPENCODE_VERBOSE) {
|
|
244
|
+
log.info(() => ({
|
|
245
|
+
message: 'compaction model loaded',
|
|
246
|
+
providerID: model.providerID,
|
|
247
|
+
modelID: model.modelID,
|
|
248
|
+
npm: model.npm,
|
|
249
|
+
contextLimit: model.info.limit.context,
|
|
250
|
+
outputLimit: model.info.limit.output,
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
107
253
|
const system = [...SystemPrompt.summarize(model.providerID)];
|
|
108
254
|
const msg = (await Session.updateMessage({
|
|
109
255
|
id: Identifier.ascending('message'),
|
|
@@ -156,6 +302,19 @@ export namespace SessionCompaction {
|
|
|
156
302
|
);
|
|
157
303
|
// Defensive check: ensure modelMessages is iterable (AI SDK 6.0.1 compatibility fix)
|
|
158
304
|
const safeModelMessages = Array.isArray(modelMessages) ? modelMessages : [];
|
|
305
|
+
|
|
306
|
+
if (Flag.OPENCODE_VERBOSE) {
|
|
307
|
+
log.info(() => ({
|
|
308
|
+
message: 'compaction streamText call',
|
|
309
|
+
providerID: model.providerID,
|
|
310
|
+
modelID: model.modelID,
|
|
311
|
+
systemPromptCount: system.length,
|
|
312
|
+
modelMessageCount: safeModelMessages.length,
|
|
313
|
+
filteredMessageCount: input.messages.length - safeModelMessages.length,
|
|
314
|
+
toolCall: model.info.tool_call,
|
|
315
|
+
}));
|
|
316
|
+
}
|
|
317
|
+
|
|
159
318
|
const result = await processor.process(() =>
|
|
160
319
|
streamText({
|
|
161
320
|
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
|
});
|
|
@@ -368,6 +392,14 @@ export namespace MessageV2 {
|
|
|
368
392
|
providerID: z.string(),
|
|
369
393
|
modelID: z.string(),
|
|
370
394
|
}),
|
|
395
|
+
compactionModel: z
|
|
396
|
+
.object({
|
|
397
|
+
providerID: z.string(),
|
|
398
|
+
modelID: z.string(),
|
|
399
|
+
useSameModel: z.boolean(),
|
|
400
|
+
compactionSafetyMarginPercent: z.number(),
|
|
401
|
+
})
|
|
402
|
+
.optional(),
|
|
371
403
|
system: z.string().optional(),
|
|
372
404
|
appendSystem: z.string().optional(),
|
|
373
405
|
tools: z.record(z.string(), z.boolean()).optional(),
|
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) {
|