btca-server 1.0.962 → 2.0.1

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 (43) hide show
  1. package/package.json +3 -3
  2. package/src/agent/agent.test.ts +31 -24
  3. package/src/agent/index.ts +8 -2
  4. package/src/agent/loop.ts +303 -346
  5. package/src/agent/service.ts +252 -233
  6. package/src/agent/types.ts +2 -2
  7. package/src/collections/index.ts +2 -1
  8. package/src/collections/service.ts +352 -345
  9. package/src/config/config.test.ts +3 -1
  10. package/src/config/index.ts +615 -727
  11. package/src/config/remote.ts +214 -369
  12. package/src/context/index.ts +6 -12
  13. package/src/context/transaction.ts +23 -30
  14. package/src/effect/errors.ts +45 -0
  15. package/src/effect/layers.ts +26 -0
  16. package/src/effect/runtime.ts +19 -0
  17. package/src/effect/services.ts +154 -0
  18. package/src/index.ts +291 -369
  19. package/src/metrics/index.ts +46 -46
  20. package/src/pricing/models-dev.ts +104 -106
  21. package/src/providers/auth.ts +159 -200
  22. package/src/providers/index.ts +19 -2
  23. package/src/providers/model.ts +115 -135
  24. package/src/providers/openai.ts +3 -3
  25. package/src/resources/impls/git.ts +123 -146
  26. package/src/resources/impls/npm.test.ts +16 -5
  27. package/src/resources/impls/npm.ts +66 -75
  28. package/src/resources/index.ts +6 -1
  29. package/src/resources/schema.ts +7 -6
  30. package/src/resources/service.test.ts +13 -12
  31. package/src/resources/service.ts +153 -112
  32. package/src/stream/index.ts +1 -1
  33. package/src/stream/service.test.ts +5 -5
  34. package/src/stream/service.ts +282 -293
  35. package/src/tools/glob.ts +126 -141
  36. package/src/tools/grep.ts +205 -210
  37. package/src/tools/index.ts +8 -4
  38. package/src/tools/list.ts +118 -140
  39. package/src/tools/read.ts +209 -235
  40. package/src/tools/virtual-sandbox.ts +91 -83
  41. package/src/validation/index.ts +18 -22
  42. package/src/vfs/virtual-fs.test.ts +37 -25
  43. package/src/vfs/virtual-fs.ts +218 -216
@@ -1,9 +1,8 @@
1
1
  import { stripUserQuestionFromStart, extractCoreQuestion } from '@btca/shared';
2
- import { Result } from 'better-result';
3
2
 
4
3
  import { getErrorHint, getErrorMessage, getErrorTag } from '../errors.ts';
5
- import { Metrics } from '../metrics/index.ts';
6
- import type { AgentLoop } from '../agent/loop.ts';
4
+ import { metricsError, metricsErrorInfo, metricsInfo } from '../metrics/index.ts';
5
+ import type { AgentEvent } from '../agent/loop.ts';
7
6
 
8
7
  import type {
9
8
  BtcaStreamDoneEvent,
@@ -36,318 +35,308 @@ const hasAnyDefined = (record: Record<string, unknown> | undefined) =>
36
35
  const costFor = (tokens: number | undefined, usdPerMTokens: number | undefined) =>
37
36
  tokens == null || usdPerMTokens == null ? undefined : (tokens / 1_000_000) * usdPerMTokens;
38
37
 
39
- export namespace StreamService {
40
- export const createSseStream = (args: {
41
- meta: BtcaStreamMetaEvent;
42
- eventStream: AsyncIterable<AgentLoop.AgentEvent>;
43
- question?: string; // Original question - used to filter echoed user message
44
- requestStartMs?: number;
45
- pricing?: {
46
- lookup: (args: { providerId: string; modelId: string; timeoutMs?: number }) => Promise<{
47
- source: 'models.dev';
48
- modelKey: string;
49
- ratesUsdPerMTokens: {
50
- input?: number;
51
- output?: number;
52
- reasoning?: number;
53
- cacheRead?: number;
54
- cacheWrite?: number;
55
- };
56
- } | null>;
57
- };
58
- pricingTimeoutMs?: number;
59
- }): ReadableStream<Uint8Array> => {
60
- const encoder = new TextEncoder();
61
-
62
- let closed = false;
63
-
64
- const emit = (
65
- controller: ReadableStreamDefaultController<Uint8Array>,
66
- event: BtcaStreamEvent
67
- ) => {
68
- if (closed) return;
69
- try {
70
- controller.enqueue(encoder.encode(toSse(event)));
71
- } catch {
72
- // If the client disconnects/cancels, the controller may already be closed.
73
- closed = true;
74
- }
75
- };
76
-
77
- // Track accumulated text and tool state
78
- let accumulatedText = '';
79
- let emittedText = '';
80
- let accumulatedReasoning = '';
81
- const toolsByCallId = new Map<string, Omit<BtcaStreamToolUpdatedEvent, 'type'>>();
82
- let textEvents = 0;
83
- let toolEvents = 0;
84
- let reasoningEvents = 0;
85
-
86
- const requestStartMs = args.requestStartMs ?? performance.now();
87
- let streamStartMs = requestStartMs;
88
-
89
- // Extract the core question for stripping echoed user message from final response
90
- const coreQuestion = extractCoreQuestion(args.question);
91
-
92
- return new ReadableStream<Uint8Array>({
93
- start(controller) {
94
- streamStartMs = performance.now();
95
- Metrics.info('stream.start', {
96
- collectionKey: args.meta.collection.key,
97
- resources: args.meta.resources,
98
- model: args.meta.model
99
- });
100
-
101
- emit(controller, args.meta);
102
-
103
- (async () => {
104
- const result = await Result.tryPromise(async () => {
105
- for await (const event of args.eventStream) {
106
- switch (event.type) {
107
- case 'text-delta': {
108
- textEvents += 1;
109
- accumulatedText += event.text;
110
-
111
- const nextText = stripUserQuestionFromStart(accumulatedText, coreQuestion);
112
- const delta = nextText.slice(emittedText.length);
113
- if (delta) {
114
- emittedText = nextText;
115
- const msg: BtcaStreamTextDeltaEvent = {
116
- type: 'text.delta',
117
- delta
118
- };
119
- emit(controller, msg);
120
- }
121
- break;
122
- }
38
+ export const createSseStream = (args: {
39
+ meta: BtcaStreamMetaEvent;
40
+ eventStream: AsyncIterable<AgentEvent>;
41
+ question?: string; // Original question - used to filter echoed user message
42
+ requestStartMs?: number;
43
+ pricing?: {
44
+ lookup: (args: { providerId: string; modelId: string; timeoutMs?: number }) => Promise<{
45
+ source: 'models.dev';
46
+ modelKey: string;
47
+ ratesUsdPerMTokens: {
48
+ input?: number;
49
+ output?: number;
50
+ reasoning?: number;
51
+ cacheRead?: number;
52
+ cacheWrite?: number;
53
+ };
54
+ } | null>;
55
+ };
56
+ pricingTimeoutMs?: number;
57
+ }): ReadableStream<Uint8Array> => {
58
+ const encoder = new TextEncoder();
59
+
60
+ let closed = false;
61
+
62
+ const emit = (
63
+ controller: ReadableStreamDefaultController<Uint8Array>,
64
+ event: BtcaStreamEvent
65
+ ) => {
66
+ if (closed) return;
67
+ try {
68
+ controller.enqueue(encoder.encode(toSse(event)));
69
+ } catch {
70
+ // If the client disconnects/cancels, the controller may already be closed.
71
+ closed = true;
72
+ }
73
+ };
123
74
 
124
- case 'reasoning-delta': {
125
- reasoningEvents += 1;
126
- accumulatedReasoning += event.text;
127
- const msg: BtcaStreamReasoningDeltaEvent = {
128
- type: 'reasoning.delta',
129
- delta: event.text
75
+ // Track accumulated text and tool state
76
+ let accumulatedText = '';
77
+ let emittedText = '';
78
+ let accumulatedReasoning = '';
79
+ const toolsByCallId = new Map<string, Omit<BtcaStreamToolUpdatedEvent, 'type'>>();
80
+ let textEvents = 0;
81
+ let toolEvents = 0;
82
+ let reasoningEvents = 0;
83
+
84
+ const requestStartMs = args.requestStartMs ?? performance.now();
85
+ let streamStartMs = requestStartMs;
86
+
87
+ // Extract the core question for stripping echoed user message from final response
88
+ const coreQuestion = extractCoreQuestion(args.question);
89
+
90
+ return new ReadableStream<Uint8Array>({
91
+ start(controller) {
92
+ streamStartMs = performance.now();
93
+ metricsInfo('stream.start', {
94
+ collectionKey: args.meta.collection.key,
95
+ resources: args.meta.resources,
96
+ model: args.meta.model
97
+ });
98
+
99
+ emit(controller, args.meta);
100
+
101
+ (async () => {
102
+ try {
103
+ for await (const event of args.eventStream) {
104
+ switch (event.type) {
105
+ case 'text-delta': {
106
+ textEvents += 1;
107
+ accumulatedText += event.text;
108
+
109
+ const nextText = stripUserQuestionFromStart(accumulatedText, coreQuestion);
110
+ const delta = nextText.slice(emittedText.length);
111
+ if (delta) {
112
+ emittedText = nextText;
113
+ const msg: BtcaStreamTextDeltaEvent = {
114
+ type: 'text.delta',
115
+ delta
130
116
  };
131
117
  emit(controller, msg);
132
- break;
133
118
  }
119
+ break;
120
+ }
134
121
 
135
- case 'tool-call': {
136
- toolEvents += 1;
137
- const callID = `tool-${toolEvents}`;
138
-
139
- // Store tool call info
140
- toolsByCallId.set(callID, {
141
- callID,
142
- tool: event.toolName,
143
- state: {
144
- status: 'running',
145
- input: event.input
146
- }
147
- });
148
-
149
- const update: BtcaStreamToolUpdatedEvent = {
150
- type: 'tool.updated',
151
- callID,
152
- tool: event.toolName,
153
- state: {
154
- status: 'running',
155
- input: event.input
156
- }
157
- };
158
- emit(controller, update);
159
- break;
160
- }
122
+ case 'reasoning-delta': {
123
+ reasoningEvents += 1;
124
+ accumulatedReasoning += event.text;
125
+ const msg: BtcaStreamReasoningDeltaEvent = {
126
+ type: 'reasoning.delta',
127
+ delta: event.text
128
+ };
129
+ emit(controller, msg);
130
+ break;
131
+ }
161
132
 
162
- case 'tool-result': {
163
- // Find the tool call and update its state
164
- for (const [callID, tool] of toolsByCallId) {
165
- if (tool.tool === event.toolName && tool.state?.status === 'running') {
166
- tool.state = {
167
- status: 'completed',
168
- input: tool.state.input,
169
- output: event.output
170
- };
171
-
172
- const update: BtcaStreamToolUpdatedEvent = {
173
- type: 'tool.updated',
174
- callID,
175
- tool: event.toolName,
176
- state: tool.state
177
- };
178
- emit(controller, update);
179
- break;
180
- }
133
+ case 'tool-call': {
134
+ toolEvents += 1;
135
+ const callID = `tool-${toolEvents}`;
136
+
137
+ // Store tool call info
138
+ toolsByCallId.set(callID, {
139
+ callID,
140
+ tool: event.toolName,
141
+ state: {
142
+ status: 'running',
143
+ input: event.input
144
+ }
145
+ });
146
+
147
+ const update: BtcaStreamToolUpdatedEvent = {
148
+ type: 'tool.updated',
149
+ callID,
150
+ tool: event.toolName,
151
+ state: {
152
+ status: 'running',
153
+ input: event.input
154
+ }
155
+ };
156
+ emit(controller, update);
157
+ break;
158
+ }
159
+
160
+ case 'tool-result': {
161
+ // Find the tool call and update its state
162
+ for (const [callID, tool] of toolsByCallId) {
163
+ if (tool.tool === event.toolName && tool.state?.status === 'running') {
164
+ tool.state = {
165
+ status: 'completed',
166
+ input: tool.state.input,
167
+ output: event.output
168
+ };
169
+
170
+ const update: BtcaStreamToolUpdatedEvent = {
171
+ type: 'tool.updated',
172
+ callID,
173
+ tool: event.toolName,
174
+ state: tool.state
175
+ };
176
+ emit(controller, update);
177
+ break;
181
178
  }
182
- break;
183
179
  }
180
+ break;
181
+ }
182
+
183
+ case 'finish': {
184
+ const finishedAtMs = performance.now();
185
+ const tools = Array.from(toolsByCallId.values());
184
186
 
185
- case 'finish': {
186
- const finishedAtMs = performance.now();
187
- const tools = Array.from(toolsByCallId.values());
187
+ // Strip the echoed user question from the final text
188
+ const finalText = stripUserQuestionFromStart(accumulatedText, coreQuestion);
189
+ emittedText = finalText;
188
190
 
189
- // Strip the echoed user question from the final text
190
- const finalText = stripUserQuestionFromStart(accumulatedText, coreQuestion);
191
- emittedText = finalText;
191
+ const usage = hasAnyDefined(event.usage as Record<string, unknown> | undefined)
192
+ ? {
193
+ inputTokens: event.usage?.inputTokens,
194
+ outputTokens: event.usage?.outputTokens,
195
+ reasoningTokens: event.usage?.reasoningTokens,
196
+ totalTokens: event.usage?.totalTokens
197
+ }
198
+ : undefined;
199
+
200
+ const totalMs = Math.max(0, finishedAtMs - requestStartMs);
201
+ const genMs = Math.max(0, finishedAtMs - streamStartMs);
192
202
 
193
- const usage = hasAnyDefined(event.usage as Record<string, unknown> | undefined)
203
+ const throughput =
204
+ genMs > 0 && usage
194
205
  ? {
195
- inputTokens: event.usage?.inputTokens,
196
- outputTokens: event.usage?.outputTokens,
197
- reasoningTokens: event.usage?.reasoningTokens,
198
- totalTokens: event.usage?.totalTokens
206
+ outputTokensPerSecond:
207
+ usage.outputTokens == null
208
+ ? undefined
209
+ : usage.outputTokens / (genMs / 1000),
210
+ totalTokensPerSecond:
211
+ usage.totalTokens == null ? undefined : usage.totalTokens / (genMs / 1000)
199
212
  }
200
213
  : undefined;
201
214
 
202
- const totalMs = Math.max(0, finishedAtMs - requestStartMs);
203
- const genMs = Math.max(0, finishedAtMs - streamStartMs);
204
-
205
- const throughput =
206
- genMs > 0 && usage
215
+ const pricingTimeoutMs = args.pricingTimeoutMs ?? 250;
216
+ const pricingLookup = args.pricing
217
+ ? withTimeout(
218
+ args.pricing.lookup({
219
+ providerId: args.meta.model.provider,
220
+ modelId: args.meta.model.model,
221
+ timeoutMs: pricingTimeoutMs
222
+ }),
223
+ pricingTimeoutMs
224
+ ).catch(() => null)
225
+ : Promise.resolve(null);
226
+
227
+ const pricingResult = await pricingLookup;
228
+
229
+ const pricing =
230
+ pricingResult && usage
231
+ ? (() => {
232
+ const rates = pricingResult.ratesUsdPerMTokens;
233
+ const input = costFor(usage.inputTokens, rates.input);
234
+ const output = costFor(usage.outputTokens, rates.output);
235
+ const reasoning = costFor(usage.reasoningTokens, rates.reasoning);
236
+ const hasAnyCostPart = input != null || output != null || reasoning != null;
237
+ const total = (input ?? 0) + (output ?? 0) + (reasoning ?? 0);
238
+
239
+ return {
240
+ source: 'models.dev' as const,
241
+ modelKey: pricingResult.modelKey,
242
+ ratesUsdPerMTokens: rates,
243
+ ...(hasAnyCostPart
244
+ ? {
245
+ costUsd: {
246
+ ...(input == null ? {} : { input }),
247
+ ...(output == null ? {} : { output }),
248
+ ...(reasoning == null ? {} : { reasoning }),
249
+ total
250
+ }
251
+ }
252
+ : {})
253
+ };
254
+ })()
255
+ : pricingResult
207
256
  ? {
208
- outputTokensPerSecond:
209
- usage.outputTokens == null
210
- ? undefined
211
- : usage.outputTokens / (genMs / 1000),
212
- totalTokensPerSecond:
213
- usage.totalTokens == null
214
- ? undefined
215
- : usage.totalTokens / (genMs / 1000)
257
+ source: 'models.dev' as const,
258
+ modelKey: pricingResult.modelKey,
259
+ ratesUsdPerMTokens: pricingResult.ratesUsdPerMTokens
216
260
  }
217
261
  : undefined;
218
262
 
219
- const pricingTimeoutMs = args.pricingTimeoutMs ?? 250;
220
- const pricingLookup = args.pricing
221
- ? withTimeout(
222
- args.pricing.lookup({
223
- providerId: args.meta.model.provider,
224
- modelId: args.meta.model.model,
225
- timeoutMs: pricingTimeoutMs
226
- }),
227
- pricingTimeoutMs
228
- ).catch(() => null)
229
- : Promise.resolve(null);
230
-
231
- const pricingResult = await pricingLookup;
232
-
233
- const pricing =
234
- pricingResult && usage
235
- ? (() => {
236
- const rates = pricingResult.ratesUsdPerMTokens;
237
- const input = costFor(usage.inputTokens, rates.input);
238
- const output = costFor(usage.outputTokens, rates.output);
239
- const reasoning = costFor(usage.reasoningTokens, rates.reasoning);
240
- const hasAnyCostPart =
241
- input != null || output != null || reasoning != null;
242
- const total = (input ?? 0) + (output ?? 0) + (reasoning ?? 0);
243
-
244
- return {
245
- source: 'models.dev' as const,
246
- modelKey: pricingResult.modelKey,
247
- ratesUsdPerMTokens: rates,
248
- ...(hasAnyCostPart
249
- ? {
250
- costUsd: {
251
- ...(input == null ? {} : { input }),
252
- ...(output == null ? {} : { output }),
253
- ...(reasoning == null ? {} : { reasoning }),
254
- total
255
- }
256
- }
257
- : {})
258
- };
259
- })()
260
- : pricingResult
261
- ? {
262
- source: 'models.dev' as const,
263
- modelKey: pricingResult.modelKey,
264
- ratesUsdPerMTokens: pricingResult.ratesUsdPerMTokens
265
- }
266
- : undefined;
267
-
268
- Metrics.info('stream.done', {
269
- collectionKey: args.meta.collection.key,
270
- textLength: finalText.length,
271
- reasoningLength: accumulatedReasoning.length,
272
- toolCount: tools.length,
273
- textEvents,
274
- toolEvents,
275
- reasoningEvents,
276
- finishReason: event.finishReason,
277
- totalMs,
278
- genMs,
279
- usage,
280
- pricingModelKey: pricingResult?.modelKey ?? null
281
- });
282
-
283
- const done: BtcaStreamDoneEvent = {
284
- type: 'done',
285
- text: finalText,
286
- reasoning: accumulatedReasoning,
287
- tools,
288
- ...(usage ? { usage } : {}),
289
- metrics: {
290
- timing: { totalMs, genMs },
291
- ...(throughput ? { throughput } : {}),
292
- ...(pricing ? { pricing } : {})
293
- }
294
- };
295
- emit(controller, done);
296
- break;
297
- }
298
-
299
- case 'error': {
300
- Metrics.error('stream.error', {
301
- collectionKey: args.meta.collection.key,
302
- error: Metrics.errorInfo(event.error)
303
- });
304
- const err: BtcaStreamErrorEvent = {
305
- type: 'error',
306
- tag: getErrorTag(event.error),
307
- message: getErrorMessage(event.error),
308
- hint: getErrorHint(event.error)
309
- };
310
- emit(controller, err);
311
- break;
312
- }
263
+ metricsInfo('stream.done', {
264
+ collectionKey: args.meta.collection.key,
265
+ textLength: finalText.length,
266
+ reasoningLength: accumulatedReasoning.length,
267
+ toolCount: tools.length,
268
+ textEvents,
269
+ toolEvents,
270
+ reasoningEvents,
271
+ finishReason: event.finishReason,
272
+ totalMs,
273
+ genMs,
274
+ usage,
275
+ pricingModelKey: pricingResult?.modelKey ?? null
276
+ });
277
+
278
+ const done: BtcaStreamDoneEvent = {
279
+ type: 'done',
280
+ text: finalText,
281
+ reasoning: accumulatedReasoning,
282
+ tools,
283
+ ...(usage ? { usage } : {}),
284
+ metrics: {
285
+ timing: { totalMs, genMs },
286
+ ...(throughput ? { throughput } : {}),
287
+ ...(pricing ? { pricing } : {})
288
+ }
289
+ };
290
+ emit(controller, done);
291
+ break;
313
292
  }
314
- }
315
- });
316
293
 
317
- result.match({
318
- ok: () => undefined,
319
- err: (cause) => {
320
- Metrics.error('stream.error', {
321
- collectionKey: args.meta.collection.key,
322
- error: Metrics.errorInfo(cause)
323
- });
324
- const err: BtcaStreamErrorEvent = {
325
- type: 'error',
326
- tag: getErrorTag(cause),
327
- message: getErrorMessage(cause),
328
- hint: getErrorHint(cause)
329
- };
330
- emit(controller, err);
294
+ case 'error': {
295
+ metricsError('stream.error', {
296
+ collectionKey: args.meta.collection.key,
297
+ error: metricsErrorInfo(event.error)
298
+ });
299
+ const err: BtcaStreamErrorEvent = {
300
+ type: 'error',
301
+ tag: getErrorTag(event.error),
302
+ message: getErrorMessage(event.error),
303
+ hint: getErrorHint(event.error)
304
+ };
305
+ emit(controller, err);
306
+ break;
307
+ }
331
308
  }
309
+ }
310
+ } catch (cause) {
311
+ metricsError('stream.error', {
312
+ collectionKey: args.meta.collection.key,
313
+ error: metricsErrorInfo(cause)
332
314
  });
333
-
334
- {
335
- Metrics.info('stream.closed', { collectionKey: args.meta.collection.key });
336
- if (!closed) {
337
- closed = true;
338
- try {
339
- controller.close();
340
- } catch {
341
- // Ignore double-close: cancellation/termination may have already closed the stream.
342
- }
315
+ const err: BtcaStreamErrorEvent = {
316
+ type: 'error',
317
+ tag: getErrorTag(cause),
318
+ message: getErrorMessage(cause),
319
+ hint: getErrorHint(cause)
320
+ };
321
+ emit(controller, err);
322
+ }
323
+
324
+ {
325
+ metricsInfo('stream.closed', { collectionKey: args.meta.collection.key });
326
+ if (!closed) {
327
+ closed = true;
328
+ try {
329
+ controller.close();
330
+ } catch {
331
+ // Ignore double-close: cancellation/termination may have already closed the stream.
343
332
  }
344
333
  }
345
- })();
346
- },
347
-
348
- cancel() {
349
- closed = true;
350
- }
351
- });
352
- };
353
- }
334
+ }
335
+ })();
336
+ },
337
+
338
+ cancel() {
339
+ closed = true;
340
+ }
341
+ });
342
+ };