@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/agent",
3
- "version": "0.16.18",
3
+ "version": "0.18.0",
4
4
  "description": "A minimal, public domain AI CLI agent compatible with OpenCode's JSON interface. Bun-only runtime.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/cli/argv.ts CHANGED
@@ -4,30 +4,68 @@
4
4
  */
5
5
 
6
6
  /**
7
- * Extract model argument directly from process.argv
8
- * This is a safeguard against yargs caching issues (#192)
9
- * @returns The model argument from CLI or null if not found
7
+ * Extract a named argument directly from process.argv.
8
+ * Supports --name=value, --name value, and optional short aliases (-x=value, -x value).
9
+ * @returns The argument value from CLI or null if not found
10
10
  */
11
- export function getModelFromProcessArgv(): string | null {
11
+ function getArgFromProcessArgv(
12
+ longFlag: string,
13
+ shortFlag?: string
14
+ ): string | null {
12
15
  const args = process.argv;
16
+ const longPrefix = `--${longFlag}=`;
17
+ const shortPrefix = shortFlag ? `-${shortFlag}=` : null;
13
18
  for (let i = 0; i < args.length; i++) {
14
19
  const arg = args[i];
15
- // Handle --model=value format
16
- if (arg.startsWith('--model=')) {
17
- return arg.substring('--model='.length);
20
+ // Handle --flag=value format
21
+ if (arg.startsWith(longPrefix)) {
22
+ return arg.substring(longPrefix.length);
18
23
  }
19
- // Handle --model value format
20
- if (arg === '--model' && i + 1 < args.length) {
24
+ // Handle --flag value format
25
+ if (arg === `--${longFlag}` && i + 1 < args.length) {
21
26
  return args[i + 1];
22
27
  }
23
- // Handle -m=value format
24
- if (arg.startsWith('-m=')) {
25
- return arg.substring('-m='.length);
26
- }
27
- // Handle -m value format (but not if it looks like another flag)
28
- if (arg === '-m' && i + 1 < args.length && !args[i + 1].startsWith('-')) {
29
- return args[i + 1];
28
+ if (shortPrefix) {
29
+ // Handle -x=value format
30
+ if (arg.startsWith(shortPrefix)) {
31
+ return arg.substring(shortPrefix.length);
32
+ }
33
+ // Handle -x value format (but not if it looks like another flag)
34
+ if (
35
+ arg === `-${shortFlag}` &&
36
+ i + 1 < args.length &&
37
+ !args[i + 1].startsWith('-')
38
+ ) {
39
+ return args[i + 1];
40
+ }
30
41
  }
31
42
  }
32
43
  return null;
33
44
  }
45
+
46
+ /**
47
+ * Extract model argument directly from process.argv
48
+ * This is a safeguard against yargs caching issues (#192)
49
+ * @returns The model argument from CLI or null if not found
50
+ */
51
+ export function getModelFromProcessArgv(): string | null {
52
+ return getArgFromProcessArgv('model', 'm');
53
+ }
54
+
55
+ /**
56
+ * Extract --compaction-model argument directly from process.argv
57
+ * @returns The compaction model argument from CLI or null if not found
58
+ * @see https://github.com/link-assistant/agent/issues/219
59
+ */
60
+ export function getCompactionModelFromProcessArgv(): string | null {
61
+ return getArgFromProcessArgv('compaction-model');
62
+ }
63
+
64
+ /**
65
+ * Extract --compaction-safety-margin argument directly from process.argv
66
+ * @returns The compaction safety margin (%) from CLI or null if not found
67
+ * @see https://github.com/link-assistant/agent/issues/219
68
+ */
69
+ export function getCompactionSafetyMarginFromProcessArgv(): string | null {
70
+ return getArgFromProcessArgv('compaction-safety-margin');
71
+ }
@@ -193,7 +193,8 @@ export async function runContinuousServerMode(
193
193
  modelID,
194
194
  systemMessage,
195
195
  appendSystemMessage,
196
- jsonStandard
196
+ jsonStandard,
197
+ compactionModel
197
198
  ) {
198
199
  // Check both CLI flag and environment variable for compact JSON mode
199
200
  const compactJson = argv['compact-json'] === true || Flag.COMPACT_JSON();
@@ -286,6 +287,7 @@ export async function runContinuousServerMode(
286
287
  body: JSON.stringify({
287
288
  parts,
288
289
  model: { providerID, modelID },
290
+ compactionModel,
289
291
  system: systemMessage,
290
292
  appendSystem: appendSystemMessage,
291
293
  }),
@@ -443,7 +445,8 @@ export async function runContinuousDirectMode(
443
445
  modelID,
444
446
  systemMessage,
445
447
  appendSystemMessage,
446
- jsonStandard
448
+ jsonStandard,
449
+ compactionModel
447
450
  ) {
448
451
  // Check both CLI flag and environment variable for compact JSON mode
449
452
  const compactJson = argv['compact-json'] === true || Flag.COMPACT_JSON();
@@ -517,6 +520,7 @@ export async function runContinuousDirectMode(
517
520
  sessionID,
518
521
  parts,
519
522
  model: { providerID, modelID },
523
+ compactionModel,
520
524
  system: systemMessage,
521
525
  appendSystem: appendSystemMessage,
522
526
  }).catch((error) => {
@@ -13,3 +13,21 @@ export const DEFAULT_PROVIDER_ID = DEFAULT_MODEL.split('/')[0];
13
13
 
14
14
  /** Default model ID extracted from DEFAULT_MODEL. */
15
15
  export const DEFAULT_MODEL_ID = DEFAULT_MODEL.split('/').slice(1).join('/');
16
+
17
+ /**
18
+ * Default compaction model used when no `--compaction-model` CLI argument is provided.
19
+ * gpt-5-nano has a 400K context window, larger than most free base models (~200K),
20
+ * which allows compacting 100% of the base model's context without a safety margin.
21
+ * The special value "same" means use the same model as `--model`.
22
+ * @see https://github.com/link-assistant/agent/issues/219
23
+ */
24
+ export const DEFAULT_COMPACTION_MODEL = 'opencode/gpt-5-nano';
25
+
26
+ /**
27
+ * Default compaction safety margin as a percentage of usable context window.
28
+ * Applied only when the compaction model has a context window equal to or smaller
29
+ * than the base model. When the compaction model has a larger context, the margin
30
+ * is automatically set to 0 (allowing 100% context usage).
31
+ * @see https://github.com/link-assistant/agent/issues/219
32
+ */
33
+ export const DEFAULT_COMPACTION_SAFETY_MARGIN_PERCENT = 15;
@@ -1,6 +1,15 @@
1
- import { getModelFromProcessArgv } from './argv.ts';
1
+ import {
2
+ getModelFromProcessArgv,
3
+ getCompactionModelFromProcessArgv,
4
+ getCompactionSafetyMarginFromProcessArgv,
5
+ } from './argv.ts';
2
6
  import { Log } from '../util/log.ts';
3
- import { DEFAULT_PROVIDER_ID, DEFAULT_MODEL_ID } from './defaults.ts';
7
+ import {
8
+ DEFAULT_PROVIDER_ID,
9
+ DEFAULT_MODEL_ID,
10
+ DEFAULT_COMPACTION_MODEL,
11
+ DEFAULT_COMPACTION_SAFETY_MARGIN_PERCENT,
12
+ } from './defaults.ts';
4
13
 
5
14
  /**
6
15
  * Parse model config from argv. Supports "provider/model" or short "model" format.
@@ -101,6 +110,13 @@ export async function parseModelConfig(argv, outputError, outputStatus) {
101
110
  }));
102
111
  }
103
112
 
113
+ // Parse compaction model (#219)
114
+ const compactionModelResult = await parseCompactionModelConfig(
115
+ argv,
116
+ providerID,
117
+ modelID
118
+ );
119
+
104
120
  // Handle --use-existing-claude-oauth option
105
121
  // This reads OAuth credentials from ~/.claude/.credentials.json (Claude Code CLI)
106
122
  // For new authentication, use: agent auth login (select Anthropic > Claude Pro/Max)
@@ -144,5 +160,73 @@ export async function parseModelConfig(argv, outputError, outputStatus) {
144
160
  }
145
161
  }
146
162
 
147
- return { providerID, modelID };
163
+ return { providerID, modelID, compactionModel: compactionModelResult };
164
+ }
165
+
166
+ /**
167
+ * Parse compaction model config from argv.
168
+ * Resolves --compaction-model and --compaction-safety-margin CLI arguments.
169
+ * The special value "same" means use the base model for compaction.
170
+ * @see https://github.com/link-assistant/agent/issues/219
171
+ */
172
+ async function parseCompactionModelConfig(argv, baseProviderID, baseModelID) {
173
+ // Get compaction model from CLI (safeguard against yargs caching)
174
+ const cliCompactionModelArg = getCompactionModelFromProcessArgv();
175
+ const compactionModelArg =
176
+ cliCompactionModelArg ??
177
+ argv['compaction-model'] ??
178
+ DEFAULT_COMPACTION_MODEL;
179
+
180
+ // Get safety margin from CLI
181
+ const cliSafetyMarginArg = getCompactionSafetyMarginFromProcessArgv();
182
+ const compactionSafetyMarginPercent = cliSafetyMarginArg
183
+ ? parseInt(cliSafetyMarginArg, 10)
184
+ : (argv['compaction-safety-margin'] ??
185
+ DEFAULT_COMPACTION_SAFETY_MARGIN_PERCENT);
186
+
187
+ // Special "same" alias — use the base model for compaction
188
+ const useSameModel = compactionModelArg.toLowerCase() === 'same';
189
+
190
+ let compactionProviderID;
191
+ let compactionModelID;
192
+
193
+ if (useSameModel) {
194
+ compactionProviderID = baseProviderID;
195
+ compactionModelID = baseModelID;
196
+ Log.Default.info(() => ({
197
+ message:
198
+ 'compaction model set to "same" — using base model for compaction',
199
+ compactionProviderID,
200
+ compactionModelID,
201
+ }));
202
+ } else if (compactionModelArg.includes('/')) {
203
+ const parts = compactionModelArg.split('/');
204
+ compactionProviderID = parts[0];
205
+ compactionModelID = parts.slice(1).join('/');
206
+ Log.Default.info(() => ({
207
+ message: 'using explicit compaction model',
208
+ compactionProviderID,
209
+ compactionModelID,
210
+ }));
211
+ } else {
212
+ // Short name resolution
213
+ const { Provider } = await import('../provider/provider.ts');
214
+ const resolved =
215
+ await Provider.parseModelWithResolution(compactionModelArg);
216
+ compactionProviderID = resolved.providerID;
217
+ compactionModelID = resolved.modelID;
218
+ Log.Default.info(() => ({
219
+ message: 'resolved short compaction model name',
220
+ input: compactionModelArg,
221
+ compactionProviderID,
222
+ compactionModelID,
223
+ }));
224
+ }
225
+
226
+ return {
227
+ providerID: compactionProviderID,
228
+ modelID: compactionModelID,
229
+ useSameModel,
230
+ compactionSafetyMarginPercent,
231
+ };
148
232
  }
@@ -0,0 +1,163 @@
1
+ import {
2
+ DEFAULT_MODEL,
3
+ DEFAULT_COMPACTION_MODEL,
4
+ DEFAULT_COMPACTION_SAFETY_MARGIN_PERCENT,
5
+ } from './defaults.ts';
6
+
7
+ /**
8
+ * Yargs builder for the default `run` command options.
9
+ * Extracted from index.js to keep file size under 1000 lines.
10
+ */
11
+ export function buildRunOptions(yargs) {
12
+ return yargs
13
+ .option('model', {
14
+ type: 'string',
15
+ description: 'Model to use in format providerID/modelID',
16
+ default: DEFAULT_MODEL,
17
+ })
18
+ .option('json-standard', {
19
+ type: 'string',
20
+ description:
21
+ 'JSON output format standard: "opencode" (default) or "claude" (experimental)',
22
+ default: 'opencode',
23
+ choices: ['opencode', 'claude'],
24
+ })
25
+ .option('system-message', {
26
+ type: 'string',
27
+ description: 'Full override of the system message',
28
+ })
29
+ .option('system-message-file', {
30
+ type: 'string',
31
+ description: 'Full override of the system message from file',
32
+ })
33
+ .option('append-system-message', {
34
+ type: 'string',
35
+ description: 'Append to the default system message',
36
+ })
37
+ .option('append-system-message-file', {
38
+ type: 'string',
39
+ description: 'Append to the default system message from file',
40
+ })
41
+ .option('server', {
42
+ type: 'boolean',
43
+ description: 'Run in server mode (default)',
44
+ default: true,
45
+ })
46
+ .option('verbose', {
47
+ type: 'boolean',
48
+ description:
49
+ 'Enable verbose mode to debug API requests (shows system prompt, token counts, etc.)',
50
+ default: false,
51
+ })
52
+ .option('dry-run', {
53
+ type: 'boolean',
54
+ description:
55
+ 'Simulate operations without making actual API calls or package installations (useful for testing)',
56
+ default: false,
57
+ })
58
+ .option('use-existing-claude-oauth', {
59
+ type: 'boolean',
60
+ description:
61
+ 'Use existing Claude OAuth credentials from ~/.claude/.credentials.json (from Claude Code CLI)',
62
+ default: false,
63
+ })
64
+ .option('prompt', {
65
+ alias: 'p',
66
+ type: 'string',
67
+ description: 'Prompt message to send directly (bypasses stdin reading)',
68
+ })
69
+ .option('disable-stdin', {
70
+ type: 'boolean',
71
+ description:
72
+ 'Disable stdin streaming mode (requires --prompt or shows help)',
73
+ default: false,
74
+ })
75
+ .option('stdin-stream-timeout', {
76
+ type: 'number',
77
+ description:
78
+ 'Optional timeout in milliseconds for stdin reading (default: no timeout)',
79
+ })
80
+ .option('auto-merge-queued-messages', {
81
+ type: 'boolean',
82
+ description:
83
+ 'Enable auto-merging of rapidly arriving input lines into single messages (default: true)',
84
+ default: true,
85
+ })
86
+ .option('interactive', {
87
+ type: 'boolean',
88
+ description:
89
+ 'Enable interactive mode to accept manual input as plain text strings (default: true). Use --no-interactive to only accept JSON input.',
90
+ default: true,
91
+ })
92
+ .option('always-accept-stdin', {
93
+ type: 'boolean',
94
+ description:
95
+ 'Keep accepting stdin input even after the agent finishes work (default: true). Use --no-always-accept-stdin for single-message mode.',
96
+ default: true,
97
+ })
98
+ .option('compact-json', {
99
+ type: 'boolean',
100
+ description:
101
+ 'Output compact JSON (single line) instead of pretty-printed JSON (default: false). Useful for program-to-program communication.',
102
+ default: false,
103
+ })
104
+ .option('resume', {
105
+ alias: 'r',
106
+ type: 'string',
107
+ description:
108
+ 'Resume a specific session by ID. By default, forks the session with a new UUID. Use --no-fork to continue in the same session.',
109
+ })
110
+ .option('continue', {
111
+ alias: 'c',
112
+ type: 'boolean',
113
+ description:
114
+ 'Continue the most recent session. By default, forks the session with a new UUID. Use --no-fork to continue in the same session.',
115
+ default: false,
116
+ })
117
+ .option('no-fork', {
118
+ type: 'boolean',
119
+ description:
120
+ 'When used with --resume or --continue, continue in the same session without forking to a new UUID.',
121
+ default: false,
122
+ })
123
+ .option('generate-title', {
124
+ type: 'boolean',
125
+ description:
126
+ 'Generate session titles using AI (default: false). Disabling saves tokens and prevents rate limit issues.',
127
+ default: false,
128
+ })
129
+ .option('retry-timeout', {
130
+ type: 'number',
131
+ description:
132
+ 'Maximum total retry time in seconds for rate limit errors (default: 604800 = 7 days)',
133
+ })
134
+ .option('retry-on-rate-limits', {
135
+ type: 'boolean',
136
+ description:
137
+ '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.',
138
+ default: true,
139
+ })
140
+ .option('output-response-model', {
141
+ type: 'boolean',
142
+ description: 'Include model info in step_finish output',
143
+ default: true,
144
+ })
145
+ .option('summarize-session', {
146
+ type: 'boolean',
147
+ description:
148
+ 'Generate AI session summaries (default: true). Use --no-summarize-session to disable.',
149
+ default: true,
150
+ })
151
+ .option('compaction-model', {
152
+ type: 'string',
153
+ description:
154
+ 'Model to use for context compaction in format providerID/modelID. Use "same" to use the base model. Default: opencode/gpt-5-nano (free, 400K context).',
155
+ default: DEFAULT_COMPACTION_MODEL,
156
+ })
157
+ .option('compaction-safety-margin', {
158
+ type: 'number',
159
+ description:
160
+ 'Safety margin (%) of usable context window before triggering compaction. Only applies when the compaction model has equal or smaller context than the base model. Default: 15.',
161
+ default: DEFAULT_COMPACTION_SAFETY_MARGIN_PERCENT,
162
+ });
163
+ }
package/src/flag/flag.ts CHANGED
@@ -103,13 +103,19 @@ export namespace Flag {
103
103
  }
104
104
 
105
105
  // Session summarization configuration
106
- // When disabled, session summaries will not be generated
107
- // This saves tokens and prevents rate limit issues with free tier models
108
- // See: https://github.com/link-assistant/agent/issues/179
109
- export let SUMMARIZE_SESSION = truthyCompat(
110
- 'LINK_ASSISTANT_AGENT_SUMMARIZE_SESSION',
111
- 'AGENT_SUMMARIZE_SESSION'
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) {