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 +2 -1
- package/src/agent/service.ts +1 -1
- package/src/index.ts +9 -2
- package/src/stream/service.ts +53 -8
- package/src/validation/index.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "btca-server",
|
|
3
|
-
"version": "1.0.
|
|
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"
|
package/src/agent/service.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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({
|
|
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: {
|
package/src/stream/service.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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:
|
|
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:
|
|
179
|
+
text: finalText,
|
|
135
180
|
reasoning: reasoning.combined,
|
|
136
181
|
tools
|
|
137
182
|
};
|
package/src/validation/index.ts
CHANGED
|
@@ -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:
|
|
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;
|