btca-server 1.0.63 → 1.0.71

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.
Files changed (42) hide show
  1. package/README.md +4 -1
  2. package/package.json +4 -2
  3. package/src/agent/agent.test.ts +114 -16
  4. package/src/agent/loop.ts +14 -11
  5. package/src/agent/service.ts +117 -86
  6. package/src/collections/index.ts +0 -0
  7. package/src/collections/service.ts +187 -57
  8. package/src/collections/types.ts +1 -0
  9. package/src/collections/virtual-metadata.ts +32 -0
  10. package/src/config/config.test.ts +0 -0
  11. package/src/config/index.ts +195 -127
  12. package/src/config/remote.ts +132 -79
  13. package/src/context/index.ts +0 -0
  14. package/src/context/transaction.ts +20 -15
  15. package/src/errors.ts +0 -0
  16. package/src/index.ts +29 -15
  17. package/src/metrics/index.ts +18 -13
  18. package/src/providers/auth.ts +38 -11
  19. package/src/providers/model.ts +3 -1
  20. package/src/providers/openrouter.ts +39 -0
  21. package/src/providers/registry.ts +2 -0
  22. package/src/resources/helpers.ts +0 -0
  23. package/src/resources/impls/git.test.ts +0 -0
  24. package/src/resources/impls/git.ts +160 -117
  25. package/src/resources/index.ts +0 -0
  26. package/src/resources/schema.ts +24 -27
  27. package/src/resources/service.ts +0 -0
  28. package/src/resources/types.ts +0 -0
  29. package/src/stream/index.ts +0 -0
  30. package/src/stream/service.ts +23 -14
  31. package/src/tools/context.ts +4 -0
  32. package/src/tools/glob.ts +72 -45
  33. package/src/tools/grep.ts +136 -57
  34. package/src/tools/index.ts +0 -2
  35. package/src/tools/list.ts +34 -53
  36. package/src/tools/read.ts +46 -32
  37. package/src/tools/virtual-sandbox.ts +103 -0
  38. package/src/validation/index.ts +12 -12
  39. package/src/vfs/virtual-fs.test.ts +107 -0
  40. package/src/vfs/virtual-fs.ts +273 -0
  41. package/src/tools/ripgrep.ts +0 -348
  42. package/src/tools/sandbox.ts +0 -164
package/README.md CHANGED
@@ -140,7 +140,6 @@ Example config.toml:
140
140
  provider = "anthropic"
141
141
  model = "claude-3-7-sonnet-20250219"
142
142
  resourcesDirectory = "~/.btca/resources"
143
- collectionsDirectory = "~/.btca/collections"
144
143
 
145
144
  [[resources]]
146
145
  type = "local"
@@ -158,6 +157,10 @@ branch = "main"
158
157
 
159
158
  - `PORT`: Server port (default: 8080)
160
159
  - `OPENCODE_API_KEY`: OpenCode AI API key (required)
160
+ - `OPENROUTER_API_KEY`: OpenRouter API key (required when provider is `openrouter`)
161
+ - `OPENROUTER_BASE_URL`: Override OpenRouter base URL (optional)
162
+ - `OPENROUTER_HTTP_REFERER`: Optional OpenRouter header for rankings
163
+ - `OPENROUTER_X_TITLE`: Optional OpenRouter header for rankings
161
164
 
162
165
  ## TypeScript Types
163
166
 
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "btca-server",
3
- "version": "1.0.63",
3
+ "version": "1.0.71",
4
4
  "description": "BTCA server for answering questions about your codebase using OpenCode AI",
5
5
  "author": "Ben Davis",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/bmdavis419/better-context",
9
+ "url": "git+https://github.com/davis7dotsh/better-context.git",
10
10
  "directory": "apps/server"
11
11
  },
12
12
  "keywords": [
@@ -69,7 +69,9 @@
69
69
  "@btca/shared": "workspace:*",
70
70
  "@opencode-ai/sdk": "^1.1.28",
71
71
  "ai": "^6.0.49",
72
+ "better-result": "^2.6.0",
72
73
  "hono": "^4.7.11",
74
+ "just-bash": "^2.7.0",
73
75
  "opencode-ai": "^1.1.36",
74
76
  "zod": "^3.25.76"
75
77
  }
@@ -6,6 +6,11 @@ import os from 'node:os';
6
6
  import { Agent } from './service.ts';
7
7
  import { Config } from '../config/index.ts';
8
8
  import type { CollectionResult } from '../collections/types.ts';
9
+ import {
10
+ getVirtualCollectionMetadata,
11
+ setVirtualCollectionMetadata
12
+ } from '../collections/virtual-metadata.ts';
13
+ import { VirtualFs } from '../vfs/virtual-fs.ts';
9
14
 
10
15
  describe('Agent', () => {
11
16
  let testDir: string;
@@ -41,21 +46,22 @@ describe('Agent', () => {
41
46
  // Run with: BTCA_RUN_INTEGRATION_TESTS=1 bun test
42
47
  describe.skipIf(!process.env.BTCA_RUN_INTEGRATION_TESTS)('Agent.ask (integration)', () => {
43
48
  it('asks a question and receives an answer', async () => {
44
- // Create a simple collection directory with a test file
45
- const collectionPath = path.join(testDir, 'test-collection');
46
- await fs.mkdir(collectionPath, { recursive: true });
47
- await fs.writeFile(
48
- path.join(collectionPath, 'README.md'),
49
- '# Test Documentation\n\nThis is a test file. The answer to life is 42.'
50
- );
51
-
52
49
  process.chdir(testDir);
53
50
  const config = await Config.load();
54
51
  const agent = Agent.create(config);
55
52
 
53
+ const vfsId = VirtualFs.create();
54
+ await VirtualFs.mkdir('/', { recursive: true }, vfsId);
55
+ await VirtualFs.writeFile(
56
+ '/README.md',
57
+ '# Test Documentation\n\nThis is a test file. The answer to life is 42.',
58
+ vfsId
59
+ );
60
+
56
61
  const collection: CollectionResult = {
57
- path: collectionPath,
58
- agentInstructions: 'This is a test collection with a README file.'
62
+ path: '/',
63
+ agentInstructions: 'This is a test collection with a README file.',
64
+ vfsId
59
65
  };
60
66
 
61
67
  const result = await agent.ask({
@@ -75,17 +81,18 @@ describe('Agent', () => {
75
81
  }, 60000);
76
82
 
77
83
  it('handles askStream and receives events', async () => {
78
- const collectionPath = path.join(testDir, 'stream-collection');
79
- await fs.mkdir(collectionPath, { recursive: true });
80
- await fs.writeFile(path.join(collectionPath, 'data.txt'), 'The capital of France is Paris.');
81
-
82
84
  process.chdir(testDir);
83
85
  const config = await Config.load();
84
86
  const agent = Agent.create(config);
85
87
 
88
+ const vfsId = VirtualFs.create();
89
+ await VirtualFs.mkdir('/', { recursive: true }, vfsId);
90
+ await VirtualFs.writeFile('/data.txt', 'The capital of France is Paris.', vfsId);
91
+
86
92
  const collection: CollectionResult = {
87
- path: collectionPath,
88
- agentInstructions: 'Simple test collection.'
93
+ path: '/',
94
+ agentInstructions: 'Simple test collection.',
95
+ vfsId
89
96
  };
90
97
 
91
98
  const { stream, model } = await agent.askStream({
@@ -107,5 +114,96 @@ describe('Agent', () => {
107
114
  const textEvents = events.filter((e) => e.type === 'text-delta');
108
115
  expect(textEvents.length).toBeGreaterThan(0);
109
116
  }, 60000);
117
+
118
+ it('cleans up virtual collections after ask', async () => {
119
+ process.chdir(testDir);
120
+ const config = await Config.load();
121
+ const agent = Agent.create(config);
122
+
123
+ const vfsId = VirtualFs.create();
124
+ await VirtualFs.mkdir('/', { recursive: true }, vfsId);
125
+ await VirtualFs.mkdir('/docs', { recursive: true }, vfsId);
126
+ await VirtualFs.writeFile('/docs/README.md', 'Virtual README\nThe answer is 42.', vfsId);
127
+
128
+ setVirtualCollectionMetadata({
129
+ vfsId,
130
+ collectionKey: 'virtual-test',
131
+ createdAt: new Date().toISOString(),
132
+ resources: [
133
+ {
134
+ name: 'docs',
135
+ fsName: 'docs',
136
+ type: 'local',
137
+ path: '/docs',
138
+ repoSubPaths: [],
139
+ loadedAt: new Date().toISOString()
140
+ }
141
+ ]
142
+ });
143
+
144
+ const collection: CollectionResult = {
145
+ path: '/',
146
+ agentInstructions: 'This is a virtual collection with a README file.',
147
+ vfsId
148
+ };
149
+
150
+ const result = await agent.ask({
151
+ collection,
152
+ question: 'What number is the answer according to the README?'
153
+ });
154
+
155
+ expect(result).toBeDefined();
156
+ expect(VirtualFs.has(vfsId)).toBe(false);
157
+ expect(getVirtualCollectionMetadata(vfsId)).toBeUndefined();
158
+ }, 60000);
159
+
160
+ it('cleans up virtual collections after askStream', async () => {
161
+ process.chdir(testDir);
162
+ const config = await Config.load();
163
+ const agent = Agent.create(config);
164
+
165
+ const vfsId = VirtualFs.create();
166
+ await VirtualFs.mkdir('/', { recursive: true }, vfsId);
167
+ await VirtualFs.mkdir('/docs', { recursive: true }, vfsId);
168
+ await VirtualFs.writeFile(
169
+ '/docs/README.md',
170
+ 'Virtual README\nThe capital of France is Paris.',
171
+ vfsId
172
+ );
173
+
174
+ setVirtualCollectionMetadata({
175
+ vfsId,
176
+ collectionKey: 'virtual-stream-test',
177
+ createdAt: new Date().toISOString(),
178
+ resources: [
179
+ {
180
+ name: 'docs',
181
+ fsName: 'docs',
182
+ type: 'local',
183
+ path: '/docs',
184
+ repoSubPaths: [],
185
+ loadedAt: new Date().toISOString()
186
+ }
187
+ ]
188
+ });
189
+
190
+ const collection: CollectionResult = {
191
+ path: '/',
192
+ agentInstructions: 'This is a virtual collection with a README file.',
193
+ vfsId
194
+ };
195
+
196
+ const { stream } = await agent.askStream({
197
+ collection,
198
+ question: 'What is the capital of France according to the README?'
199
+ });
200
+
201
+ for await (const _event of stream) {
202
+ // drain stream to trigger cleanup
203
+ }
204
+
205
+ expect(VirtualFs.has(vfsId)).toBe(false);
206
+ expect(getVirtualCollectionMetadata(vfsId)).toBeUndefined();
207
+ }, 60000);
110
208
  });
111
209
  });
package/src/agent/loop.ts CHANGED
@@ -25,6 +25,7 @@ export namespace AgentLoop {
25
25
  providerId: string;
26
26
  modelId: string;
27
27
  collectionPath: string;
28
+ vfsId?: string;
28
29
  agentInstructions: string;
29
30
  question: string;
30
31
  maxSteps?: number;
@@ -65,13 +66,13 @@ export namespace AgentLoop {
65
66
  /**
66
67
  * Create the tools for the agent
67
68
  */
68
- function createTools(basePath: string) {
69
+ function createTools(basePath: string, vfsId?: string) {
69
70
  return {
70
71
  read: tool({
71
72
  description: 'Read the contents of a file. Returns the file contents with line numbers.',
72
73
  inputSchema: ReadTool.Parameters,
73
74
  execute: async (params: ReadTool.ParametersType) => {
74
- const result = await ReadTool.execute(params, { basePath });
75
+ const result = await ReadTool.execute(params, { basePath, vfsId });
75
76
  return result.output;
76
77
  }
77
78
  }),
@@ -81,7 +82,7 @@ export namespace AgentLoop {
81
82
  'Search for a regex pattern in file contents. Returns matching lines with file paths and line numbers.',
82
83
  inputSchema: GrepTool.Parameters,
83
84
  execute: async (params: GrepTool.ParametersType) => {
84
- const result = await GrepTool.execute(params, { basePath });
85
+ const result = await GrepTool.execute(params, { basePath, vfsId });
85
86
  return result.output;
86
87
  }
87
88
  }),
@@ -91,7 +92,7 @@ export namespace AgentLoop {
91
92
  'Find files matching a glob pattern (e.g. "**/*.ts", "src/**/*.js"). Returns a list of matching file paths sorted by modification time.',
92
93
  inputSchema: GlobTool.Parameters,
93
94
  execute: async (params: GlobTool.ParametersType) => {
94
- const result = await GlobTool.execute(params, { basePath });
95
+ const result = await GlobTool.execute(params, { basePath, vfsId });
95
96
  return result.output;
96
97
  }
97
98
  }),
@@ -101,7 +102,7 @@ export namespace AgentLoop {
101
102
  'List the contents of a directory. Returns files and subdirectories with their types.',
102
103
  inputSchema: ListTool.Parameters,
103
104
  execute: async (params: ListTool.ParametersType) => {
104
- const result = await ListTool.execute(params, { basePath });
105
+ const result = await ListTool.execute(params, { basePath, vfsId });
105
106
  return result.output;
106
107
  }
107
108
  })
@@ -111,8 +112,8 @@ export namespace AgentLoop {
111
112
  /**
112
113
  * Get initial context by listing the collection directory
113
114
  */
114
- async function getInitialContext(collectionPath: string): Promise<string> {
115
- const result = await ListTool.execute({ path: '.' }, { basePath: collectionPath });
115
+ async function getInitialContext(collectionPath: string, vfsId?: string) {
116
+ const result = await ListTool.execute({ path: '.' }, { basePath: collectionPath, vfsId });
116
117
  return `Collection contents:\n${result.output}`;
117
118
  }
118
119
 
@@ -124,6 +125,7 @@ export namespace AgentLoop {
124
125
  providerId,
125
126
  modelId,
126
127
  collectionPath,
128
+ vfsId,
127
129
  agentInstructions,
128
130
  question,
129
131
  maxSteps = 40
@@ -133,7 +135,7 @@ export namespace AgentLoop {
133
135
  const model = await Model.getModel(providerId, modelId);
134
136
 
135
137
  // Get initial context
136
- const initialContext = await getInitialContext(collectionPath);
138
+ const initialContext = await getInitialContext(collectionPath, vfsId);
137
139
 
138
140
  // Build messages
139
141
  const messages: ModelMessage[] = [
@@ -144,7 +146,7 @@ export namespace AgentLoop {
144
146
  ];
145
147
 
146
148
  // Create tools
147
- const tools = createTools(collectionPath);
149
+ const tools = createTools(collectionPath, vfsId);
148
150
 
149
151
  // Collect events
150
152
  const events: AgentEvent[] = [];
@@ -218,6 +220,7 @@ export namespace AgentLoop {
218
220
  providerId,
219
221
  modelId,
220
222
  collectionPath,
223
+ vfsId,
221
224
  agentInstructions,
222
225
  question,
223
226
  maxSteps = 40
@@ -227,7 +230,7 @@ export namespace AgentLoop {
227
230
  const model = await Model.getModel(providerId, modelId);
228
231
 
229
232
  // Get initial context
230
- const initialContext = await getInitialContext(collectionPath);
233
+ const initialContext = await getInitialContext(collectionPath, vfsId);
231
234
 
232
235
  // Build messages
233
236
  const messages: ModelMessage[] = [
@@ -238,7 +241,7 @@ export namespace AgentLoop {
238
241
  ];
239
242
 
240
243
  // Create tools
241
- const tools = createTools(collectionPath);
244
+ const tools = createTools(collectionPath, vfsId);
242
245
 
243
246
  // Run streamText with tool execution
244
247
  const result = streamText({
@@ -8,12 +8,15 @@ import {
8
8
  type Config as OpenCodeConfig,
9
9
  type OpencodeClient
10
10
  } from '@opencode-ai/sdk';
11
+ import { Result } from 'better-result';
11
12
 
12
13
  import { Config } from '../config/index.ts';
13
14
  import { CommonHints, type TaggedErrorOptions } from '../errors.ts';
14
15
  import { Metrics } from '../metrics/index.ts';
15
16
  import { Auth, getSupportedProviders } from '../providers/index.ts';
16
17
  import type { CollectionResult } from '../collections/types.ts';
18
+ import { clearVirtualCollectionMetadata } from '../collections/virtual-metadata.ts';
19
+ import { VirtualFs } from '../vfs/virtual-fs.ts';
17
20
  import type { AgentResult, TrackedInstance, InstanceInfo } from './types.ts';
18
21
  import { AgentLoop } from './loop.ts';
19
22
 
@@ -110,10 +113,14 @@ export namespace Agent {
110
113
  super(`Provider "${args.providerId}" is not connected`);
111
114
  this.providerId = args.providerId;
112
115
  this.connectedProviders = args.connectedProviders;
116
+ const baseHint =
117
+ args.providerId === 'openrouter'
118
+ ? 'Set OPENROUTER_API_KEY to authenticate OpenRouter.'
119
+ : CommonHints.RUN_AUTH;
113
120
  if (args.connectedProviders.length > 0) {
114
- this.hint = `${CommonHints.RUN_AUTH} Connected providers: ${args.connectedProviders.join(', ')}.`;
121
+ this.hint = `${baseHint} Connected providers: ${args.connectedProviders.join(', ')}.`;
115
122
  } else {
116
- this.hint = `${CommonHints.RUN_AUTH} No providers are currently connected.`;
123
+ this.hint = `${baseHint} No providers are currently connected.`;
117
124
  }
118
125
  }
119
126
  }
@@ -226,11 +233,11 @@ export namespace Agent {
226
233
  server: { close(): void; url: string };
227
234
  baseUrl: string;
228
235
  }> => {
229
- const maxAttempts = 10;
230
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
231
- const port = Math.floor(Math.random() * 3000) + 3000;
232
- const created = await createOpencode({ port, config: args.ocConfig }).catch(
233
- (err: unknown) => {
236
+ const tryCreateOpencode = async (port: number) => {
237
+ const result = await Result.tryPromise(() => createOpencode({ port, config: args.ocConfig }));
238
+ return result.match({
239
+ ok: (created) => created,
240
+ err: (err) => {
234
241
  const error = err as { cause?: Error };
235
242
  if (error?.cause instanceof Error && error.cause.stack?.includes('port')) return null;
236
243
  throw new AgentError({
@@ -239,7 +246,13 @@ export namespace Agent {
239
246
  cause: err
240
247
  });
241
248
  }
242
- );
249
+ });
250
+ };
251
+
252
+ const maxAttempts = 10;
253
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
254
+ const port = Math.floor(Math.random() * 3000) + 3000;
255
+ const created = await tryCreateOpencode(port);
243
256
 
244
257
  if (created) {
245
258
  const baseUrl = `http://localhost:${port}`;
@@ -272,10 +285,17 @@ export namespace Agent {
272
285
  questionLength: question.length
273
286
  });
274
287
 
288
+ const cleanup = () => {
289
+ if (!collection.vfsId) return;
290
+ VirtualFs.dispose(collection.vfsId);
291
+ clearVirtualCollectionMetadata(collection.vfsId);
292
+ };
293
+
275
294
  // Validate provider is authenticated
276
295
  const isAuthed = await Auth.isAuthenticated(config.provider);
277
296
  if (!isAuthed && config.provider !== 'opencode') {
278
297
  const authenticated = await Auth.getAuthenticatedProviders();
298
+ cleanup();
279
299
  throw new ProviderNotConnectedError({
280
300
  providerId: config.provider,
281
301
  connectedProviders: authenticated
@@ -283,13 +303,23 @@ export namespace Agent {
283
303
  }
284
304
 
285
305
  // Create a generator that wraps the AgentLoop stream
286
- const eventGenerator = AgentLoop.stream({
287
- providerId: config.provider,
288
- modelId: config.model,
289
- collectionPath: collection.path,
290
- agentInstructions: collection.agentInstructions,
291
- question
292
- });
306
+ const eventGenerator = (async function* () {
307
+ try {
308
+ const stream = AgentLoop.stream({
309
+ providerId: config.provider,
310
+ modelId: config.model,
311
+ collectionPath: collection.path,
312
+ vfsId: collection.vfsId,
313
+ agentInstructions: collection.agentInstructions,
314
+ question
315
+ });
316
+ for await (const event of stream) {
317
+ yield event;
318
+ }
319
+ } finally {
320
+ cleanup();
321
+ }
322
+ })();
293
323
 
294
324
  return {
295
325
  stream: eventGenerator,
@@ -307,45 +337,60 @@ export namespace Agent {
307
337
  questionLength: question.length
308
338
  });
309
339
 
340
+ const cleanup = () => {
341
+ if (!collection.vfsId) return;
342
+ VirtualFs.dispose(collection.vfsId);
343
+ clearVirtualCollectionMetadata(collection.vfsId);
344
+ };
345
+
310
346
  // Validate provider is authenticated
311
347
  const isAuthed = await Auth.isAuthenticated(config.provider);
312
348
  if (!isAuthed && config.provider !== 'opencode') {
313
349
  const authenticated = await Auth.getAuthenticatedProviders();
350
+ cleanup();
314
351
  throw new ProviderNotConnectedError({
315
352
  providerId: config.provider,
316
353
  connectedProviders: authenticated
317
354
  });
318
355
  }
319
356
 
320
- try {
321
- const result = await AgentLoop.run({
357
+ const runResult = await Result.tryPromise(() =>
358
+ AgentLoop.run({
322
359
  providerId: config.provider,
323
360
  modelId: config.model,
324
361
  collectionPath: collection.path,
362
+ vfsId: collection.vfsId,
325
363
  agentInstructions: collection.agentInstructions,
326
364
  question
327
- });
365
+ })
366
+ );
328
367
 
329
- Metrics.info('agent.ask.complete', {
330
- provider: config.provider,
331
- model: config.model,
332
- answerLength: result.answer.length,
333
- eventCount: result.events.length
334
- });
368
+ cleanup();
335
369
 
336
- return {
337
- answer: result.answer,
338
- model: result.model,
339
- events: result.events
340
- };
341
- } catch (error) {
342
- Metrics.error('agent.ask.error', { error: Metrics.errorInfo(error) });
343
- throw new AgentError({
344
- message: 'Failed to get response from AI',
345
- hint: 'This may be a temporary issue. Try running the command again.',
346
- cause: error
347
- });
348
- }
370
+ return runResult.match({
371
+ ok: (result) => {
372
+ Metrics.info('agent.ask.complete', {
373
+ provider: config.provider,
374
+ model: config.model,
375
+ answerLength: result.answer.length,
376
+ eventCount: result.events.length
377
+ });
378
+
379
+ return {
380
+ answer: result.answer,
381
+ model: result.model,
382
+ events: result.events
383
+ };
384
+ },
385
+ err: (error) => {
386
+ Metrics.error('agent.ask.error', { error: Metrics.errorInfo(error) });
387
+ throw new AgentError({
388
+ message: 'Failed to get response from AI',
389
+ hint: 'This may be a temporary issue. Try running the command again.',
390
+ cause: error
391
+ });
392
+ }
393
+ });
349
394
  };
350
395
 
351
396
  /**
@@ -353,31 +398,11 @@ export namespace Agent {
353
398
  * This still spawns a full OpenCode instance for clients that need it
354
399
  */
355
400
  const getOpencodeInstance: Service['getOpencodeInstance'] = async ({ collection }) => {
356
- const ocConfig = buildOpenCodeConfig({
357
- agentInstructions: collection.agentInstructions,
358
- providerId: config.provider,
359
- providerTimeoutMs: config.providerTimeoutMs
360
- });
361
- const { server, baseUrl } = await createOpencodeInstance({
362
- collectionPath: collection.path,
363
- ocConfig
364
- });
365
-
366
- // Register the instance for lifecycle management
367
- const instanceId = generateInstanceId();
368
- registerInstance(instanceId, server, collection.path);
369
-
370
- Metrics.info('agent.oc.instance.ready', {
371
- baseUrl,
372
- collectionPath: collection.path,
373
- instanceId
401
+ throw new AgentError({
402
+ message: 'OpenCode instance not available',
403
+ hint: 'BTCA uses virtual collections only. Use the btca ask/stream APIs instead.',
404
+ cause: new Error('Virtual collections are not compatible with filesystem-based OpenCode')
374
405
  });
375
-
376
- return {
377
- url: baseUrl,
378
- model: { provider: config.provider, model: config.model },
379
- instanceId
380
- };
381
406
  };
382
407
 
383
408
  /**
@@ -413,20 +438,23 @@ export namespace Agent {
413
438
  return { closed: false };
414
439
  }
415
440
 
416
- try {
417
- instance.server.close();
418
- unregisterInstance(instanceId);
419
- Metrics.info('agent.instance.closed', { instanceId });
420
- return { closed: true };
421
- } catch (cause) {
422
- Metrics.error('agent.instance.close.err', {
423
- instanceId,
424
- error: Metrics.errorInfo(cause)
425
- });
426
- // Still remove from registry even if close failed
427
- unregisterInstance(instanceId);
428
- return { closed: true };
429
- }
441
+ const closeResult = Result.try(() => instance.server.close());
442
+ return closeResult.match({
443
+ ok: () => {
444
+ unregisterInstance(instanceId);
445
+ Metrics.info('agent.instance.closed', { instanceId });
446
+ return { closed: true };
447
+ },
448
+ err: (cause) => {
449
+ Metrics.error('agent.instance.close.err', {
450
+ instanceId,
451
+ error: Metrics.errorInfo(cause)
452
+ });
453
+ // Still remove from registry even if close failed
454
+ unregisterInstance(instanceId);
455
+ return { closed: true };
456
+ }
457
+ });
430
458
  };
431
459
 
432
460
  /**
@@ -450,17 +478,20 @@ export namespace Agent {
450
478
  let closed = 0;
451
479
 
452
480
  for (const instance of instances) {
453
- try {
454
- instance.server.close();
455
- closed++;
456
- } catch (cause) {
457
- Metrics.error('agent.instance.close.err', {
458
- instanceId: instance.id,
459
- error: Metrics.errorInfo(cause)
460
- });
461
- // Count as closed even if there was an error
462
- closed++;
463
- }
481
+ const closeResult = Result.try(() => instance.server.close());
482
+ closeResult.match({
483
+ ok: () => {
484
+ closed++;
485
+ },
486
+ err: (cause) => {
487
+ Metrics.error('agent.instance.close.err', {
488
+ instanceId: instance.id,
489
+ error: Metrics.errorInfo(cause)
490
+ });
491
+ // Count as closed even if there was an error
492
+ closed++;
493
+ }
494
+ });
464
495
  }
465
496
 
466
497
  instanceRegistry.clear();
File without changes