btca-server 1.0.30 → 1.0.40

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": "btca-server",
3
- "version": "1.0.30",
3
+ "version": "1.0.40",
4
4
  "description": "BTCA server for answering questions about your codebase using OpenCode AI",
5
5
  "author": "Ben Davis",
6
6
  "license": "MIT",
@@ -49,6 +49,7 @@
49
49
  "prettier": "^3.7.4"
50
50
  },
51
51
  "dependencies": {
52
+ "@btca/shared": "workspace:*",
52
53
  "@opencode-ai/sdk": "^1.0.208",
53
54
  "hono": "^4.7.11",
54
55
  "zod": "^3.25.76"
@@ -95,7 +95,7 @@ export namespace Agent {
95
95
  const prompt = [
96
96
  'You are an expert internal agent whose job is to answer questions about the collection.',
97
97
  'You operate inside a collection directory.',
98
- 'Use the resources in this collection to answer the user\'s question.',
98
+ "Use the resources in this collection to answer the user's question.",
99
99
  args.agentInstructions
100
100
  ].join('\n');
101
101
 
package/src/index.ts CHANGED
@@ -62,7 +62,10 @@ const QuestionRequestSchema = z.object({
62
62
  question: z
63
63
  .string()
64
64
  .min(1, 'Question cannot be empty')
65
- .max(LIMITS.QUESTION_MAX, `Question too long (max ${LIMITS.QUESTION_MAX} chars)`),
65
+ .max(
66
+ LIMITS.QUESTION_MAX,
67
+ `Question too long (max ${LIMITS.QUESTION_MAX.toLocaleString()} chars). This includes conversation history - try starting a new thread or clearing the chat.`
68
+ ),
66
69
  resources: z
67
70
  .array(ResourceNameField)
68
71
  .max(
@@ -319,7 +322,11 @@ const createApp = (deps: {
319
322
  } satisfies BtcaStreamMetaEvent;
320
323
 
321
324
  Metrics.info('question.stream.start', { collectionKey });
322
- const stream = StreamService.createSseStream({ meta, eventStream });
325
+ const stream = StreamService.createSseStream({
326
+ meta,
327
+ eventStream,
328
+ question: decoded.question
329
+ });
323
330
 
324
331
  return new Response(stream, {
325
332
  headers: {
@@ -1,6 +1,11 @@
1
1
  import type { OcEvent } from '../agent/types.ts';
2
2
  import { getErrorMessage, getErrorTag } from '../errors.ts';
3
3
  import { Metrics } from '../metrics/index.ts';
4
+ import {
5
+ StreamingTagStripper,
6
+ extractCoreQuestion,
7
+ stripUserQuestionFromStart
8
+ } from '@btca/shared';
4
9
 
5
10
  import type {
6
11
  BtcaStreamDoneEvent,
@@ -41,6 +46,7 @@ export namespace StreamService {
41
46
  export const createSseStream = (args: {
42
47
  meta: BtcaStreamMetaEvent;
43
48
  eventStream: AsyncIterable<OcEvent>;
49
+ question?: string; // Original question - used to filter echoed user message
44
50
  }): ReadableStream<Uint8Array> => {
45
51
  const encoder = new TextEncoder();
46
52
 
@@ -52,6 +58,15 @@ export namespace StreamService {
52
58
  let textEvents = 0;
53
59
  let reasoningEvents = 0;
54
60
 
61
+ // Create streaming tag stripper for filtering history markers
62
+ const tagStripper = new StreamingTagStripper();
63
+
64
+ // Extract the core question for stripping echoed user message from final response
65
+ const coreQuestion = extractCoreQuestion(args.question);
66
+
67
+ // Track total emitted text for accurate final text reconstruction
68
+ let emittedText = '';
69
+
55
70
  const emit = (
56
71
  controller: ReadableStreamDefaultController<Uint8Array>,
57
72
  event: BtcaStreamEvent
@@ -73,17 +88,36 @@ export namespace StreamService {
73
88
  try {
74
89
  for await (const event of args.eventStream) {
75
90
  if (event.type === 'message.part.updated') {
76
- const part: any = (event.properties as any).part;
91
+ const props = event.properties as any;
92
+ const part: any = props?.part;
77
93
  if (!part || typeof part !== 'object') continue;
78
94
 
95
+ // Skip user messages - only stream assistant responses
96
+ const messageRole = props?.message?.role ?? props?.role;
97
+ if (messageRole === 'user') {
98
+ continue;
99
+ }
100
+
79
101
  if (part.type === 'text') {
80
102
  const partId = String(part.id);
81
103
  const nextText = String(part.text ?? '');
82
- const delta = updateAccumulator(text, partId, nextText);
83
- if (delta.length > 0) {
84
- textEvents += 1;
85
- const msg: BtcaStreamTextDeltaEvent = { type: 'text.delta', delta };
86
- emit(controller, msg);
104
+
105
+ // Get the raw delta from accumulator
106
+ const rawDelta = updateAccumulator(text, partId, nextText);
107
+
108
+ if (rawDelta.length > 0) {
109
+ // Filter through the streaming tag stripper
110
+ const cleanDelta = tagStripper.process(rawDelta);
111
+
112
+ if (cleanDelta.length > 0) {
113
+ textEvents += 1;
114
+ emittedText += cleanDelta;
115
+ const msg: BtcaStreamTextDeltaEvent = {
116
+ type: 'text.delta',
117
+ delta: cleanDelta
118
+ };
119
+ emit(controller, msg);
120
+ }
87
121
  }
88
122
  continue;
89
123
  }
@@ -120,18 +154,29 @@ export namespace StreamService {
120
154
 
121
155
  if (event.type === 'session.idle') {
122
156
  const tools = Array.from(toolsByCallId.values());
157
+
158
+ // Flush any remaining buffered content from the tag stripper
159
+ const flushed = tagStripper.flush();
160
+ if (flushed.length > 0) {
161
+ emittedText += flushed;
162
+ }
163
+
164
+ // Strip the echoed user question from the final text
165
+ let finalText = stripUserQuestionFromStart(emittedText, coreQuestion);
166
+
123
167
  Metrics.info('stream.done', {
124
168
  collectionKey: args.meta.collection.key,
125
- textLength: text.combined.length,
169
+ textLength: finalText.length,
126
170
  reasoningLength: reasoning.combined.length,
127
171
  toolCount: tools.length,
128
172
  toolUpdates,
129
173
  textEvents,
130
174
  reasoningEvents
131
175
  });
176
+
132
177
  const done: BtcaStreamDoneEvent = {
133
178
  type: 'done',
134
- text: text.combined,
179
+ text: finalText,
135
180
  reasoning: reasoning.combined,
136
181
  tools
137
182
  };
@@ -47,8 +47,8 @@ export const LIMITS = {
47
47
  NOTES_MAX: 500,
48
48
  /** Maximum length for search paths */
49
49
  SEARCH_PATH_MAX: 256,
50
- /** Maximum length for questions */
51
- QUESTION_MAX: 10_000,
50
+ /** Maximum length for questions (includes conversation history when formatted) */
51
+ QUESTION_MAX: 100_000,
52
52
  /** Maximum number of resources per request */
53
53
  MAX_RESOURCES_PER_REQUEST: 20
54
54
  } as const;