@link-assistant/agent 0.3.0 → 0.4.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/README.md CHANGED
@@ -181,6 +181,18 @@ See [MODELS.md](MODELS.md) for complete list of available models and pricing.
181
181
  See [docs/groq.md](docs/groq.md) for Groq provider documentation.
182
182
  See [docs/claude-oauth.md](docs/claude-oauth.md) for Claude OAuth provider documentation.
183
183
 
184
+ ### Direct Prompt Mode
185
+
186
+ Use `-p`/`--prompt` to send a prompt directly without reading from stdin:
187
+
188
+ ```bash
189
+ # Direct prompt (bypasses stdin)
190
+ agent -p "What is 2+2?"
191
+
192
+ # Useful in scripts
193
+ result=$(agent -p "Summarize: $(cat file.txt)")
194
+ ```
195
+
184
196
  ### CLI Options
185
197
 
186
198
  ```bash
@@ -197,6 +209,16 @@ Options:
197
209
  --system-message-file Full override of the system message from file
198
210
  --append-system-message Append to the default system message
199
211
  --append-system-message-file Append to the default system message from file
212
+
213
+ Stdin Mode Options:
214
+ -p, --prompt Direct prompt (bypasses stdin reading)
215
+ --disable-stdin Disable stdin streaming (requires --prompt)
216
+ --stdin-stream-timeout Timeout in ms for stdin reading (default: none)
217
+ --interactive Accept plain text input (default: true)
218
+ --no-interactive Only accept JSON input
219
+ --auto-merge-queued-messages Merge rapidly arriving lines (default: true)
220
+ --no-auto-merge-queued-messages Treat each line as separate message
221
+
200
222
  --help Show help
201
223
  --version Show version number
202
224
 
@@ -208,6 +230,8 @@ Commands:
208
230
  mcp MCP server commands
209
231
  ```
210
232
 
233
+ See [docs/stdin-mode.md](docs/stdin-mode.md) for comprehensive stdin mode documentation.
234
+
211
235
  ### JSON Output Standards
212
236
 
213
237
  The agent supports two JSON output format standards via the `--json-standard` option:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/agent",
3
- "version": "0.3.0",
3
+ "version": "0.4.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",
@@ -849,6 +849,8 @@ const GOOGLE_OAUTH_SCOPES = [
849
849
  'https://www.googleapis.com/auth/cloud-platform',
850
850
  'https://www.googleapis.com/auth/userinfo.email',
851
851
  'https://www.googleapis.com/auth/userinfo.profile',
852
+ 'https://www.googleapis.com/auth/generative-language.tuning',
853
+ 'https://www.googleapis.com/auth/generative-language.retriever',
852
854
  ];
853
855
 
854
856
  // Google OAuth endpoints
package/src/bun/index.ts CHANGED
@@ -5,10 +5,14 @@ import path from 'path';
5
5
  import { NamedError } from '../util/error';
6
6
  import { readableStreamToText } from 'bun';
7
7
  import { Flag } from '../flag/flag';
8
+ import { Lock } from '../util/lock';
8
9
 
9
10
  export namespace BunProc {
10
11
  const log = Log.create({ service: 'bun' });
11
12
 
13
+ // Lock key for serializing package installations to prevent race conditions
14
+ const INSTALL_LOCK_KEY = 'bun-install';
15
+
12
16
  export async function run(
13
17
  cmd: string[],
14
18
  options?: Bun.SpawnOptions.OptionsObject<any, any, any>
@@ -65,8 +69,38 @@ export namespace BunProc {
65
69
  })
66
70
  );
67
71
 
72
+ // Maximum number of retry attempts for cache-related errors
73
+ const MAX_RETRIES = 3;
74
+ // Delay between retries in milliseconds
75
+ const RETRY_DELAY_MS = 500;
76
+
77
+ /**
78
+ * Check if an error is related to Bun cache issues
79
+ */
80
+ function isCacheRelatedError(errorMsg: string): boolean {
81
+ return (
82
+ errorMsg.includes('failed copying files from cache') ||
83
+ errorMsg.includes('FileNotFound') ||
84
+ errorMsg.includes('ENOENT') ||
85
+ errorMsg.includes('EACCES') ||
86
+ errorMsg.includes('EBUSY')
87
+ );
88
+ }
89
+
90
+ /**
91
+ * Wait for a specified duration
92
+ */
93
+ function delay(ms: number): Promise<void> {
94
+ return new Promise((resolve) => setTimeout(resolve, ms));
95
+ }
96
+
68
97
  export async function install(pkg: string, version = 'latest') {
69
98
  const mod = path.join(Global.Path.cache, 'node_modules', pkg);
99
+
100
+ // Use a write lock to serialize all package installations
101
+ // This prevents race conditions when multiple packages are installed concurrently
102
+ using _ = await Lock.write(INSTALL_LOCK_KEY);
103
+
70
104
  const pkgjson = Bun.file(path.join(Global.Path.cache, 'package.json'));
71
105
  const parsed = await pkgjson.json().catch(async () => {
72
106
  const result = { dependencies: {} };
@@ -108,25 +142,78 @@ export namespace BunProc {
108
142
  version,
109
143
  });
110
144
 
111
- await BunProc.run(args, {
112
- cwd: Global.Path.cache,
113
- }).catch((e) => {
114
- log.error('package installation failed', {
145
+ // Retry logic for cache-related errors
146
+ let lastError: Error | undefined;
147
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
148
+ try {
149
+ await BunProc.run(args, {
150
+ cwd: Global.Path.cache,
151
+ });
152
+
153
+ log.info('package installed successfully', { pkg, version, attempt });
154
+ parsed.dependencies[pkg] = version;
155
+ await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2));
156
+ return mod;
157
+ } catch (e) {
158
+ const errorMsg = e instanceof Error ? e.message : String(e);
159
+ const isCacheError = isCacheRelatedError(errorMsg);
160
+
161
+ log.warn('package installation attempt failed', {
162
+ pkg,
163
+ version,
164
+ attempt,
165
+ maxRetries: MAX_RETRIES,
166
+ error: errorMsg,
167
+ isCacheError,
168
+ });
169
+
170
+ if (isCacheError && attempt < MAX_RETRIES) {
171
+ log.info('retrying installation after cache-related error', {
172
+ pkg,
173
+ version,
174
+ attempt,
175
+ nextAttempt: attempt + 1,
176
+ delayMs: RETRY_DELAY_MS,
177
+ });
178
+ await delay(RETRY_DELAY_MS);
179
+ lastError = e instanceof Error ? e : new Error(errorMsg);
180
+ continue;
181
+ }
182
+
183
+ // Non-cache error or final attempt - log and throw
184
+ log.error('package installation failed', {
185
+ pkg,
186
+ version,
187
+ error: errorMsg,
188
+ stack: e instanceof Error ? e.stack : undefined,
189
+ possibleCacheCorruption: isCacheError,
190
+ attempts: attempt,
191
+ });
192
+
193
+ // Provide helpful recovery instructions for cache-related errors
194
+ if (isCacheError) {
195
+ log.error(
196
+ 'Bun package cache may be corrupted. Try clearing the cache with: bun pm cache rm'
197
+ );
198
+ }
199
+
200
+ throw new InstallFailedError(
201
+ { pkg, version, details: errorMsg },
202
+ {
203
+ cause: e,
204
+ }
205
+ );
206
+ }
207
+ }
208
+
209
+ // This should not be reached, but handle it just in case
210
+ throw new InstallFailedError(
211
+ {
115
212
  pkg,
116
213
  version,
117
- error: e instanceof Error ? e.message : String(e),
118
- stack: e instanceof Error ? e.stack : undefined,
119
- });
120
- throw new InstallFailedError(
121
- { pkg, version, details: e instanceof Error ? e.message : String(e) },
122
- {
123
- cause: e,
124
- }
125
- );
126
- });
127
- log.info('package installed successfully', { pkg, version });
128
- parsed.dependencies[pkg] = version;
129
- await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2));
130
- return mod;
214
+ details: lastError?.message ?? 'Installation failed after all retries',
215
+ },
216
+ { cause: lastError }
217
+ );
131
218
  }
132
219
  }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Input queue for managing continuous stdin input.
3
+ * Supports queuing of multiple messages and auto-merging of rapidly arriving lines.
4
+ */
5
+ export class InputQueue {
6
+ constructor(options = {}) {
7
+ this.queue = [];
8
+ this.autoMerge = options.autoMerge !== false; // enabled by default
9
+ this.mergeDelayMs = options.mergeDelayMs || 50; // delay to wait for more lines
10
+ this.pendingLines = [];
11
+ this.mergeTimer = null;
12
+ this.onMessage = options.onMessage || (() => {});
13
+ this.interactive = options.interactive !== false; // enabled by default
14
+ }
15
+
16
+ /**
17
+ * Parse input and determine if it's JSON or plain text
18
+ * @param {string} input - Raw input string
19
+ * @returns {object} - Parsed message object
20
+ */
21
+ parseInput(input) {
22
+ const trimmed = input.trim();
23
+ if (!trimmed) {
24
+ return null;
25
+ }
26
+
27
+ try {
28
+ const parsed = JSON.parse(trimmed);
29
+ // If it has a message field, use it directly
30
+ if (typeof parsed === 'object' && parsed !== null) {
31
+ return parsed;
32
+ }
33
+ // Otherwise wrap it
34
+ return { message: JSON.stringify(parsed) };
35
+ } catch (_e) {
36
+ // Not JSON, treat as plain text message
37
+ return { message: trimmed };
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Add a line to the queue, potentially merging with pending lines
43
+ * @param {string} line - Input line
44
+ */
45
+ addLine(line) {
46
+ if (!this.interactive && !line.trim().startsWith('{')) {
47
+ // In non-interactive mode, only accept JSON
48
+ return;
49
+ }
50
+
51
+ if (this.autoMerge) {
52
+ // Add to pending lines and schedule merge
53
+ this.pendingLines.push(line);
54
+ this.scheduleMerge();
55
+ } else {
56
+ // No merging, queue immediately
57
+ const parsed = this.parseInput(line);
58
+ if (parsed) {
59
+ this.queue.push(parsed);
60
+ this.notifyMessage(parsed);
61
+ }
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Schedule a merge of pending lines after a short delay
67
+ */
68
+ scheduleMerge() {
69
+ if (this.mergeTimer) {
70
+ clearTimeout(this.mergeTimer);
71
+ }
72
+ this.mergeTimer = setTimeout(() => {
73
+ this.flushPendingLines();
74
+ }, this.mergeDelayMs);
75
+ }
76
+
77
+ /**
78
+ * Flush pending lines into a single merged message
79
+ */
80
+ flushPendingLines() {
81
+ if (this.pendingLines.length === 0) {
82
+ return;
83
+ }
84
+
85
+ const mergedText = this.pendingLines.join('\n');
86
+ this.pendingLines = [];
87
+ this.mergeTimer = null;
88
+
89
+ const parsed = this.parseInput(mergedText);
90
+ if (parsed) {
91
+ this.queue.push(parsed);
92
+ this.notifyMessage(parsed);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Immediately flush any pending lines (for shutdown)
98
+ */
99
+ flush() {
100
+ if (this.mergeTimer) {
101
+ clearTimeout(this.mergeTimer);
102
+ this.mergeTimer = null;
103
+ }
104
+ this.flushPendingLines();
105
+ }
106
+
107
+ /**
108
+ * Get next message from the queue
109
+ * @returns {object|null} - Next message or null if queue is empty
110
+ */
111
+ dequeue() {
112
+ return this.queue.shift() || null;
113
+ }
114
+
115
+ /**
116
+ * Check if queue has messages
117
+ * @returns {boolean}
118
+ */
119
+ hasMessages() {
120
+ return this.queue.length > 0;
121
+ }
122
+
123
+ /**
124
+ * Notify listener of new message
125
+ * @param {object} message - The message object
126
+ */
127
+ notifyMessage(message) {
128
+ if (this.onMessage) {
129
+ this.onMessage(message);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Get queue size
135
+ * @returns {number}
136
+ */
137
+ size() {
138
+ return this.queue.length;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Create a continuous stdin reader that queues input lines
144
+ * @param {object} options - Options for the reader
145
+ * @returns {object} - Reader with queue and control methods
146
+ */
147
+ // Note: This function is prepared for future continuous input mode
148
+ export function createContinuousStdinReader(options = {}) {
149
+ const inputQueue = new InputQueue(options);
150
+ let isRunning = true;
151
+ let lineBuffer = '';
152
+
153
+ const handleData = (chunk) => {
154
+ if (!isRunning) {
155
+ return;
156
+ }
157
+
158
+ lineBuffer += chunk.toString();
159
+ const lines = lineBuffer.split('\n');
160
+
161
+ // Keep the last incomplete line in buffer
162
+ lineBuffer = lines.pop() || '';
163
+
164
+ // Process complete lines
165
+ for (const line of lines) {
166
+ if (line.trim()) {
167
+ inputQueue.addLine(line);
168
+ }
169
+ }
170
+ };
171
+
172
+ const handleEnd = () => {
173
+ // Process any remaining data in buffer
174
+ if (lineBuffer.trim()) {
175
+ inputQueue.addLine(lineBuffer);
176
+ }
177
+ inputQueue.flush();
178
+ isRunning = false;
179
+ };
180
+
181
+ process.stdin.on('data', handleData);
182
+ process.stdin.on('end', handleEnd);
183
+ process.stdin.on('error', () => {
184
+ isRunning = false;
185
+ });
186
+
187
+ return {
188
+ queue: inputQueue,
189
+ stop: () => {
190
+ isRunning = false;
191
+ inputQueue.flush();
192
+ process.stdin.removeListener('data', handleData);
193
+ process.stdin.removeListener('end', handleEnd);
194
+ },
195
+ isRunning: () => isRunning,
196
+ };
197
+ }
package/src/index.js CHANGED
@@ -73,32 +73,71 @@ process.on('unhandledRejection', (reason, _promise) => {
73
73
  process.exit(1);
74
74
  });
75
75
 
76
- function readStdin() {
77
- return new Promise((resolve, reject) => {
76
+ /**
77
+ * Read stdin with optional timeout.
78
+ * @param {number|null} timeout - Timeout in milliseconds. If null, wait indefinitely until EOF.
79
+ * @returns {Promise<string>} - The stdin content
80
+ */
81
+ function readStdinWithTimeout(timeout = null) {
82
+ return new Promise((resolve) => {
78
83
  let data = '';
84
+ let hasData = false;
85
+ let timer = null;
86
+
87
+ const cleanup = () => {
88
+ if (timer) {
89
+ clearTimeout(timer);
90
+ }
91
+ process.stdin.removeListener('data', onData);
92
+ process.stdin.removeListener('end', onEnd);
93
+ process.stdin.removeListener('error', onError);
94
+ };
95
+
79
96
  const onData = (chunk) => {
97
+ hasData = true;
98
+ if (timer) {
99
+ clearTimeout(timer);
100
+ }
80
101
  data += chunk;
81
102
  };
103
+
82
104
  const onEnd = () => {
83
105
  cleanup();
84
106
  resolve(data);
85
107
  };
86
- const onError = (err) => {
108
+
109
+ const onError = () => {
87
110
  cleanup();
88
- reject(err);
89
- };
90
- const cleanup = () => {
91
- process.stdin.removeListener('data', onData);
92
- process.stdin.removeListener('end', onEnd);
93
- process.stdin.removeListener('error', onError);
111
+ resolve('');
94
112
  };
113
+
114
+ // Only set timeout if specified (not null)
115
+ if (timeout !== null) {
116
+ timer = setTimeout(() => {
117
+ if (!hasData) {
118
+ process.stdin.pause();
119
+ cleanup();
120
+ resolve('');
121
+ }
122
+ }, timeout);
123
+ }
124
+
95
125
  process.stdin.on('data', onData);
96
126
  process.stdin.on('end', onEnd);
97
127
  process.stdin.on('error', onError);
98
128
  });
99
129
  }
100
130
 
101
- async function runAgentMode(argv) {
131
+ /**
132
+ * Output JSON status message to stderr
133
+ * This prevents the status message from interfering with JSON output parsing
134
+ * @param {object} status - Status object to output
135
+ */
136
+ function outputStatus(status) {
137
+ console.error(JSON.stringify(status));
138
+ }
139
+
140
+ async function runAgentMode(argv, request) {
102
141
  // Note: verbose flag and logging are now initialized in middleware
103
142
  // See main() function for the middleware that sets up Flag and Log.init()
104
143
 
@@ -207,21 +246,6 @@ async function runAgentMode(argv) {
207
246
 
208
247
  // Logging is already initialized in middleware, no need to call Log.init() again
209
248
 
210
- // Read input from stdin
211
- const input = await readStdin();
212
- const trimmedInput = input.trim();
213
-
214
- // Try to parse as JSON, if it fails treat it as plain text message
215
- let request;
216
- try {
217
- request = JSON.parse(trimmedInput);
218
- } catch (_e) {
219
- // Not JSON, treat as plain text message
220
- request = {
221
- message: trimmedInput,
222
- };
223
- }
224
-
225
249
  // Wrap in Instance.provide for OpenCode infrastructure
226
250
  await Instance.provide({
227
251
  directory: process.cwd(),
@@ -533,7 +557,7 @@ async function runDirectMode(
533
557
  async function main() {
534
558
  try {
535
559
  // Parse command line arguments with subcommands
536
- const argv = await yargs(hideBin(process.argv))
560
+ const yargsInstance = yargs(hideBin(process.argv))
537
561
  .scriptName('agent')
538
562
  .usage('$0 [command] [options]')
539
563
  .version(pkg.version)
@@ -593,6 +617,34 @@ async function main() {
593
617
  'Use existing Claude OAuth credentials from ~/.claude/.credentials.json (from Claude Code CLI)',
594
618
  default: false,
595
619
  })
620
+ .option('prompt', {
621
+ alias: 'p',
622
+ type: 'string',
623
+ description: 'Prompt message to send directly (bypasses stdin reading)',
624
+ })
625
+ .option('disable-stdin', {
626
+ type: 'boolean',
627
+ description:
628
+ 'Disable stdin streaming mode (requires --prompt or shows help)',
629
+ default: false,
630
+ })
631
+ .option('stdin-stream-timeout', {
632
+ type: 'number',
633
+ description:
634
+ 'Optional timeout in milliseconds for stdin reading (default: no timeout)',
635
+ })
636
+ .option('auto-merge-queued-messages', {
637
+ type: 'boolean',
638
+ description:
639
+ 'Enable auto-merging of rapidly arriving input lines into single messages (default: true)',
640
+ default: true,
641
+ })
642
+ .option('interactive', {
643
+ type: 'boolean',
644
+ description:
645
+ 'Enable interactive mode to accept manual input as plain text strings (default: true). Use --no-interactive to only accept JSON input.',
646
+ default: true,
647
+ })
596
648
  // Initialize logging early for all CLI commands
597
649
  // This prevents debug output from appearing in CLI unless --verbose is used
598
650
  .middleware(async (argv) => {
@@ -641,15 +693,100 @@ async function main() {
641
693
  process.exit(1);
642
694
  }
643
695
  })
644
- .help().argv;
696
+ .help();
697
+
698
+ const argv = await yargsInstance.argv;
645
699
 
646
700
  // If a command was executed (like mcp), yargs handles it
647
701
  // Otherwise, check if we should run in agent mode (stdin piped)
648
702
  const commandExecuted = argv._ && argv._.length > 0;
649
703
 
650
704
  if (!commandExecuted) {
651
- // No command specified, run in default agent mode (stdin processing)
652
- await runAgentMode(argv);
705
+ // Check if --prompt flag was provided
706
+ if (argv.prompt) {
707
+ // Direct prompt mode - bypass stdin entirely
708
+ const request = { message: argv.prompt };
709
+ await runAgentMode(argv, request);
710
+ return;
711
+ }
712
+
713
+ // Check if --disable-stdin was set without --prompt
714
+ if (argv['disable-stdin']) {
715
+ // Output a helpful message suggesting to use --prompt
716
+ outputStatus({
717
+ type: 'error',
718
+ message:
719
+ 'No prompt provided. Use -p/--prompt to specify a message, or remove --disable-stdin to read from stdin.',
720
+ hint: 'Example: agent -p "Hello, how are you?"',
721
+ });
722
+ process.exit(1);
723
+ }
724
+
725
+ // Check if stdin is a TTY (interactive terminal)
726
+ // If it is, show help instead of waiting for input
727
+ if (process.stdin.isTTY) {
728
+ yargsInstance.showHelp();
729
+ process.exit(0);
730
+ }
731
+
732
+ // stdin is piped - enter stdin listening mode
733
+ // Output status message to inform user what's happening
734
+ const isInteractive = argv.interactive !== false;
735
+ const autoMerge = argv['auto-merge-queued-messages'] !== false;
736
+
737
+ outputStatus({
738
+ type: 'status',
739
+ mode: 'stdin-stream',
740
+ message:
741
+ 'Agent CLI in stdin listening mode. Accepts JSON and plain text input.',
742
+ hint: 'Press CTRL+C to exit. Use --help for options.',
743
+ acceptedFormats: isInteractive
744
+ ? ['JSON object with "message" field', 'Plain text']
745
+ : ['JSON object with "message" field'],
746
+ options: {
747
+ interactive: isInteractive,
748
+ autoMergeQueuedMessages: autoMerge,
749
+ },
750
+ });
751
+
752
+ // Read stdin with optional timeout
753
+ const timeout = argv['stdin-stream-timeout'] ?? null;
754
+ const input = await readStdinWithTimeout(timeout);
755
+ const trimmedInput = input.trim();
756
+
757
+ if (trimmedInput === '') {
758
+ outputStatus({
759
+ type: 'status',
760
+ message: 'No input received. Exiting.',
761
+ });
762
+ yargsInstance.showHelp();
763
+ process.exit(0);
764
+ }
765
+
766
+ // Try to parse as JSON, if it fails treat it as plain text message
767
+ let request;
768
+ try {
769
+ request = JSON.parse(trimmedInput);
770
+ } catch (_e) {
771
+ // Not JSON
772
+ if (!isInteractive) {
773
+ // In non-interactive mode, only accept JSON
774
+ outputStatus({
775
+ type: 'error',
776
+ message:
777
+ 'Invalid JSON input. In non-interactive mode (--no-interactive), only JSON input is accepted.',
778
+ hint: 'Use --interactive to accept plain text, or provide valid JSON: {"message": "your text"}',
779
+ });
780
+ process.exit(1);
781
+ }
782
+ // In interactive mode, treat as plain text message
783
+ request = {
784
+ message: trimmedInput,
785
+ };
786
+ }
787
+
788
+ // Run agent mode
789
+ await runAgentMode(argv, request);
653
790
  }
654
791
  } catch (error) {
655
792
  hasError = true;
@@ -905,6 +905,10 @@ export namespace Provider {
905
905
  if (opencodeProvider) {
906
906
  const [model] = sort(Object.values(opencodeProvider.info.models));
907
907
  if (model) {
908
+ log.info('using opencode provider as default', {
909
+ provider: opencodeProvider.info.id,
910
+ model: model.id,
911
+ });
908
912
  return {
909
913
  providerID: opencodeProvider.info.id,
910
914
  modelID: model.id,
@@ -912,7 +916,7 @@ export namespace Provider {
912
916
  }
913
917
  }
914
918
 
915
- // Fall back to any available provider
919
+ // Fall back to any available provider if opencode is not available
916
920
  const provider = providers.find(
917
921
  (p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id)
918
922
  );
@@ -290,10 +290,27 @@ export namespace SessionPrompt {
290
290
  history: msgs,
291
291
  });
292
292
 
293
- const model = await Provider.getModel(
294
- lastUser.model.providerID,
295
- lastUser.model.modelID
296
- );
293
+ let model;
294
+ try {
295
+ model = await Provider.getModel(
296
+ lastUser.model.providerID,
297
+ lastUser.model.modelID
298
+ );
299
+ } catch (error) {
300
+ log.warn(
301
+ 'Failed to initialize specified model, falling back to default model',
302
+ {
303
+ providerID: lastUser.model.providerID,
304
+ modelID: lastUser.model.modelID,
305
+ error: error instanceof Error ? error.message : String(error),
306
+ }
307
+ );
308
+ const defaultModel = await Provider.defaultModel();
309
+ model = await Provider.getModel(
310
+ defaultModel.providerID,
311
+ defaultModel.modelID
312
+ );
313
+ }
297
314
  const task = tasks.pop();
298
315
 
299
316
  // pending subtask