@bxb1337/windsurf-fast-context 1.0.7 → 1.0.9

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.
@@ -1,11 +1,25 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.convertPrompt = convertPrompt;
4
- function toContentString(value) {
5
- if (typeof value === 'string') {
6
- return value;
4
+ function toolOutputToString(output) {
5
+ switch (output.type) {
6
+ case 'text':
7
+ case 'error-text':
8
+ return output.value;
9
+ case 'json':
10
+ case 'error-json':
11
+ return JSON.stringify(output.value);
12
+ case 'execution-denied': {
13
+ const obj = { type: 'execution-denied' };
14
+ if (output.reason !== undefined)
15
+ obj.reason = output.reason;
16
+ return JSON.stringify(obj);
17
+ }
18
+ case 'content':
19
+ return JSON.stringify(output.value);
20
+ default:
21
+ return JSON.stringify(output);
7
22
  }
8
- return JSON.stringify(value);
9
23
  }
10
24
  function convertPrompt(prompt) {
11
25
  const messages = [];
@@ -42,13 +56,14 @@ function convertPrompt(prompt) {
42
56
  }
43
57
  continue;
44
58
  }
45
- // Tool result messages - use refCallId to reference the original tool call
46
59
  for (const part of message.content) {
60
+ if (part.type !== 'tool-result')
61
+ continue;
47
62
  messages.push({
48
63
  role: 4,
49
- content: toContentString(part.result),
64
+ content: toolOutputToString(part.output),
50
65
  metadata: {
51
- refCallId: part.toolCallId, // This links back to the tool call
66
+ refCallId: part.toolCallId,
52
67
  },
53
68
  });
54
69
  }
@@ -36,7 +36,7 @@ const prompt_converter_js_1 = require("./prompt-converter.js");
36
36
  },
37
37
  });
38
38
  });
39
- (0, vitest_1.it)('multi-turn preserves ordering across mixed roles', () => {
39
+ (0, vitest_1.it)('multi-turn preserves ordering across mixed roles (V3 output shape)', () => {
40
40
  const prompt = [
41
41
  { role: 'system', content: 'System instruction' },
42
42
  {
@@ -60,7 +60,7 @@ const prompt_converter_js_1 = require("./prompt-converter.js");
60
60
  type: 'tool-result',
61
61
  toolCallId: 'call_2',
62
62
  toolName: 'searchDocs',
63
- result: { hits: ['a.ts', 'b.ts'] },
63
+ output: { type: 'json', value: { hits: ['a.ts', 'b.ts'] } },
64
64
  },
65
65
  ],
66
66
  },
@@ -92,4 +92,119 @@ const prompt_converter_js_1 = require("./prompt-converter.js");
92
92
  ]);
93
93
  (0, vitest_1.expect)(prompt).toEqual(snapshot);
94
94
  });
95
+ (0, vitest_1.it)('tool-result with json output serializes value to JSON', () => {
96
+ const prompt = [
97
+ {
98
+ role: 'tool',
99
+ content: [
100
+ {
101
+ type: 'tool-result',
102
+ toolCallId: 'call_json',
103
+ toolName: 'searchTool',
104
+ output: { type: 'json', value: { files: ['x.ts', 'y.ts'], count: 2 } },
105
+ },
106
+ ],
107
+ },
108
+ ];
109
+ const result = (0, prompt_converter_js_1.convertPrompt)(prompt);
110
+ (0, vitest_1.expect)(result).toEqual([
111
+ {
112
+ role: 4,
113
+ content: '{"files":["x.ts","y.ts"],"count":2}',
114
+ metadata: { refCallId: 'call_json' },
115
+ },
116
+ ]);
117
+ });
118
+ (0, vitest_1.it)('tool-result with text output uses string directly', () => {
119
+ const prompt = [
120
+ {
121
+ role: 'tool',
122
+ content: [
123
+ {
124
+ type: 'tool-result',
125
+ toolCallId: 'call_text',
126
+ toolName: 'readFile',
127
+ output: { type: 'text', value: 'Operation completed successfully' },
128
+ },
129
+ ],
130
+ },
131
+ ];
132
+ const result = (0, prompt_converter_js_1.convertPrompt)(prompt);
133
+ (0, vitest_1.expect)(result).toEqual([
134
+ {
135
+ role: 4,
136
+ content: 'Operation completed successfully',
137
+ metadata: { refCallId: 'call_text' },
138
+ },
139
+ ]);
140
+ });
141
+ (0, vitest_1.it)('tool-result with error-text output serializes error message', () => {
142
+ const prompt = [
143
+ {
144
+ role: 'tool',
145
+ content: [
146
+ {
147
+ type: 'tool-result',
148
+ toolCallId: 'call_error',
149
+ toolName: 'executeCommand',
150
+ output: { type: 'error-text', value: 'Tool execution failed: timeout' },
151
+ },
152
+ ],
153
+ },
154
+ ];
155
+ const result = (0, prompt_converter_js_1.convertPrompt)(prompt);
156
+ (0, vitest_1.expect)(result).toEqual([
157
+ {
158
+ role: 4,
159
+ content: 'Tool execution failed: timeout',
160
+ metadata: { refCallId: 'call_error' },
161
+ },
162
+ ]);
163
+ });
164
+ (0, vitest_1.it)('tool-result with execution-denied output includes reason', () => {
165
+ const prompt = [
166
+ {
167
+ role: 'tool',
168
+ content: [
169
+ {
170
+ type: 'tool-result',
171
+ toolCallId: 'call_denied',
172
+ toolName: 'dangerousAction',
173
+ output: { type: 'execution-denied', reason: 'User rejected tool execution' },
174
+ },
175
+ ],
176
+ },
177
+ ];
178
+ const result = (0, prompt_converter_js_1.convertPrompt)(prompt);
179
+ (0, vitest_1.expect)(result).toEqual([
180
+ {
181
+ role: 4,
182
+ content: '{"type":"execution-denied","reason":"User rejected tool execution"}',
183
+ metadata: { refCallId: 'call_denied' },
184
+ },
185
+ ]);
186
+ });
187
+ (0, vitest_1.it)('tool-result with execution-denied output handles missing reason', () => {
188
+ const prompt = [
189
+ {
190
+ role: 'tool',
191
+ content: [
192
+ {
193
+ type: 'tool-result',
194
+ toolCallId: 'call_denied_no_reason',
195
+ toolName: 'someTool',
196
+ output: { type: 'execution-denied' },
197
+ },
198
+ ],
199
+ },
200
+ ];
201
+ const result = (0, prompt_converter_js_1.convertPrompt)(prompt);
202
+ (0, vitest_1.expect)(result).toEqual([
203
+ {
204
+ role: 4,
205
+ content: '{"type":"execution-denied"}',
206
+ metadata: { refCallId: 'call_denied_no_reason' },
207
+ },
208
+ ]);
209
+ });
95
210
  });
@@ -6,9 +6,6 @@ const protobuf_js_1 = require("../protocol/protobuf.js");
6
6
  const TOOL_CALL_PREFIX = '[TOOL_CALLS]';
7
7
  const ARGS_PREFIX = '[ARGS]';
8
8
  const STOP_TOKEN = '</s>';
9
- const EMPTY_TOOL_CALLS_PATTERN = /TOOL_CALLS\d*(?:<\/s>)?\s*(?:\{\s*\}\s*)+/g;
10
- // OpenAI-style TOOL_CALLS format: TOOL_CALLS{"type":"function","function":{"name":3,...}}{}...
11
- const OPENAI_TOOL_CALLS_PATTERN = /^TOOL_CALLS(\{[\s\S]*\}\s*)+\s*$/;
12
9
  function parseOpenAIToolCalls(responseText) {
13
10
  if (!responseText.startsWith('TOOL_CALLS')) {
14
11
  return null;
@@ -75,20 +72,6 @@ function pushText(parts, text) {
75
72
  parts.push({ type: 'text', text });
76
73
  }
77
74
  }
78
- function extractAnswerText(args) {
79
- if (typeof args === 'string') {
80
- return args;
81
- }
82
- if (args != null && typeof args === 'object') {
83
- if ('answer' in args && typeof args.answer === 'string') {
84
- return args.answer;
85
- }
86
- if ('text' in args && typeof args.text === 'string') {
87
- return args.text;
88
- }
89
- }
90
- return JSON.stringify(args);
91
- }
92
75
  function parseStringEnd(value, startIndex) {
93
76
  let index = startIndex + 1;
94
77
  let escaping = false;
@@ -255,7 +238,6 @@ function decodeResponseText(buffer) {
255
238
  }
256
239
  function convertResponse(buffer) {
257
240
  let responseText = decodeResponseText(buffer);
258
- responseText = responseText.replace(EMPTY_TOOL_CALLS_PATTERN, '');
259
241
  responseText = responseText.replace(STOP_TOKEN, '');
260
242
  const openaiToolCalls = parseOpenAIToolCalls(responseText);
261
243
  if (openaiToolCalls) {
@@ -286,18 +268,13 @@ function convertResponse(buffer) {
286
268
  cursor = malformedEnd;
287
269
  continue;
288
270
  }
289
- if (toolName === 'answer') {
290
- pushText(parts, extractAnswerText(parsedArgs.parsed));
291
- }
292
- else {
293
- toolCallCount += 1;
294
- parts.push({
295
- type: 'tool-call',
296
- toolCallId: `toolcall_${toolCallCount}`,
297
- toolName,
298
- input: parsedArgs.parsed,
299
- });
300
- }
271
+ toolCallCount += 1;
272
+ parts.push({
273
+ type: 'tool-call',
274
+ toolCallId: `toolcall_${toolCallCount}`,
275
+ toolName,
276
+ input: parsedArgs.parsed,
277
+ });
301
278
  cursor = parsedArgs.endIndex;
302
279
  }
303
280
  return parts;
@@ -21,7 +21,12 @@ const response_converter_js_1 = require("./response-converter.js");
21
21
  input: { query: 'prompt converter' },
22
22
  },
23
23
  { type: 'text', text: ' between ' },
24
- { type: 'text', text: 'final answer' },
24
+ {
25
+ type: 'tool-call',
26
+ toolCallId: 'toolcall_2',
27
+ toolName: 'answer',
28
+ input: { answer: 'final answer' },
29
+ },
25
30
  { type: 'text', text: ' after' },
26
31
  ]);
27
32
  });
@@ -48,7 +53,14 @@ const response_converter_js_1 = require("./response-converter.js");
48
53
  payload.writeVarint(1, 150);
49
54
  payload.writeString(2, '[TOOL_CALLS]answer[ARGS]{"answer":"final answer"}');
50
55
  const result = (0, response_converter_js_1.convertResponse)(payload.toBuffer());
51
- (0, vitest_1.expect)(result).toEqual([{ type: 'text', text: 'final answer' }]);
56
+ (0, vitest_1.expect)(result).toEqual([
57
+ {
58
+ type: 'tool-call',
59
+ toolCallId: 'toolcall_1',
60
+ toolName: 'answer',
61
+ input: { answer: 'final answer' },
62
+ },
63
+ ]);
52
64
  });
53
65
  (0, vitest_1.it)('protobuf payload - ignores metadata strings and keeps main text field', () => {
54
66
  const payload = new protobuf_js_1.ProtobufEncoder();
@@ -62,76 +74,11 @@ const response_converter_js_1 = require("./response-converter.js");
62
74
  const result = (0, response_converter_js_1.convertResponse)(compressed);
63
75
  (0, vitest_1.expect)(result).toEqual([{ type: 'text', text: 'hello from gzip' }]);
64
76
  });
65
- (0, vitest_1.it)('strips empty TOOL_CALLS markers with stop token', () => {
66
- const input = 'Hello world TOOL_CALLS0</s>{}';
67
- const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
68
- (0, vitest_1.expect)(result).toEqual([{ type: 'text', text: 'Hello world ' }]);
69
- });
70
- (0, vitest_1.it)('strips empty TOOL_CALLS markers without stop token', () => {
71
- const input = 'Hello world TOOL_CALLS1{}';
72
- const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
73
- (0, vitest_1.expect)(result).toEqual([{ type: 'text', text: 'Hello world ' }]);
74
- });
75
- (0, vitest_1.it)('strips empty TOOL_CALLS markers with whitespace', () => {
76
- const input = 'Text TOOL_CALLS2{ } more text';
77
- const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
78
- (0, vitest_1.expect)(result).toEqual([{ type: 'text', text: 'Text more text' }]);
79
- });
80
77
  (0, vitest_1.it)('strips standalone stop token', () => {
81
78
  const input = 'Hello world</s>';
82
79
  const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
83
80
  (0, vitest_1.expect)(result).toEqual([{ type: 'text', text: 'Hello world' }]);
84
81
  });
85
- (0, vitest_1.it)('handles TOOL_CALLS with number prefix before stop token', () => {
86
- const input = 'Text before TOOL_CALLS1</s>{} text after';
87
- const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
88
- (0, vitest_1.expect)(result).toEqual([{ type: 'text', text: 'Text before text after' }]);
89
- });
90
- (0, vitest_1.it)('strips TOOL_CALLS with double empty braces', () => {
91
- const input = 'Hello world TOOL_CALLS1{}{}';
92
- const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
93
- (0, vitest_1.expect)(result).toEqual([{ type: 'text', text: 'Hello world ' }]);
94
- });
95
- (0, vitest_1.it)('strips TOOL_CALLS with triple empty braces', () => {
96
- const input = 'Response TOOL_CALLS2{}{}{} end';
97
- const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
98
- (0, vitest_1.expect)(result).toEqual([{ type: 'text', text: 'Response end' }]);
99
- });
100
- (0, vitest_1.it)('strips TOOL_CALLS with stop token and multiple braces', () => {
101
- const input = 'Text TOOL_CALLS0</s>{}{} more text';
102
- const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
103
- (0, vitest_1.expect)(result).toEqual([{ type: 'text', text: 'Text more text' }]);
104
- });
105
- (0, vitest_1.it)('handles empty marker followed by real tool call', () => {
106
- const input = 'TOOL_CALLS0{} [TOOL_CALLS]searchDocs[ARGS]{"query":"test"}';
107
- const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
108
- (0, vitest_1.expect)(result).toEqual([
109
- {
110
- type: 'tool-call',
111
- toolCallId: 'toolcall_1',
112
- toolName: 'searchDocs',
113
- input: { query: 'test' },
114
- },
115
- ]);
116
- });
117
- (0, vitest_1.it)('handles real tool call followed by empty marker', () => {
118
- const input = '[TOOL_CALLS]searchDocs[ARGS]{"query":"test"} TOOL_CALLS1{} done';
119
- const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
120
- (0, vitest_1.expect)(result).toEqual([
121
- {
122
- type: 'tool-call',
123
- toolCallId: 'toolcall_1',
124
- toolName: 'searchDocs',
125
- input: { query: 'test' },
126
- },
127
- { type: 'text', text: ' done' },
128
- ]);
129
- });
130
- (0, vitest_1.it)('handles empty marker adjacent to real tool call', () => {
131
- const input = 'TOOL_CALLS0{}[TOOL_CALLS]answer[ARGS]{"answer":"result"}';
132
- const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
133
- (0, vitest_1.expect)(result).toEqual([{ type: 'text', text: 'result' }]);
134
- });
135
82
  (0, vitest_1.it)('parses OpenAI-style TOOL_CALLS with numeric IDs', () => {
136
83
  const input = 'TOOL_CALLS{"type":"function","function":{"name":3,"parameters":{"file_path":"/home/test","search_pattern":"binance"}}}{}';
137
84
  const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
@@ -186,4 +133,16 @@ const response_converter_js_1 = require("./response-converter.js");
186
133
  },
187
134
  ]);
188
135
  });
136
+ (0, vitest_1.it)('handles OpenAI-style with string name "answer" - emits as tool-call', () => {
137
+ const input = 'TOOL_CALLS{"type":"function","function":{"name":"answer","parameters":{"answer":"final answer"}}}{}';
138
+ const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
139
+ (0, vitest_1.expect)(result).toEqual([
140
+ {
141
+ type: 'tool-call',
142
+ toolCallId: 'toolcall_1',
143
+ toolName: 'answer',
144
+ input: { answer: 'final answer' },
145
+ },
146
+ ]);
147
+ });
189
148
  });
@@ -56,12 +56,18 @@ class DevstralLanguageModel {
56
56
  const requestFrame = (0, connect_frame_js_1.connectFrameEncode)(requestPayload);
57
57
  const headers = createConnectHeaders(this.headers);
58
58
  const responseFrame = await this.transport.postUnary(`${this.baseURL}${API_SERVICE_PATH}${DEVSTRAL_STREAM_PATH}`, requestFrame, headers);
59
- const responsePayloads = (0, connect_frame_js_1.connectFrameDecode)(responseFrame);
59
+ const { payloads: responsePayloads } = (0, connect_frame_js_1.connectFrameDecode)(responseFrame);
60
60
  const payloads = responsePayloads.length > 0 ? responsePayloads : [responseFrame];
61
61
  const content = payloads.flatMap((payload) => toV3Content((0, response_converter_js_1.convertResponse)(payload)));
62
+ const unified = content.some((part) => part.type === 'tool-call')
63
+ ? 'tool-calls'
64
+ : 'stop';
62
65
  return {
63
66
  content,
64
- finishReason: 'stop',
67
+ finishReason: {
68
+ unified,
69
+ raw: undefined,
70
+ },
65
71
  usage: emptyUsage(),
66
72
  warnings: [],
67
73
  };
@@ -89,6 +95,7 @@ class DevstralLanguageModel {
89
95
  options.abortSignal?.addEventListener('abort', abortHandler, { once: true });
90
96
  let textSegmentId = null;
91
97
  let textSegmentCounter = 0;
98
+ let hasToolCalls = false;
92
99
  let pending = Buffer.alloc(0);
93
100
  const closeTextSegment = () => {
94
101
  if (textSegmentId == null) {
@@ -103,7 +110,7 @@ class DevstralLanguageModel {
103
110
  try {
104
111
  safeEnqueue(controller, { type: 'stream-start', warnings: [] });
105
112
  safeEnqueue(controller, { type: 'response-metadata', modelId: this.modelId });
106
- while (!isAborted(options.abortSignal)) {
113
+ outerLoop: while (!isAborted(options.abortSignal)) {
107
114
  const next = await reader.read();
108
115
  if (next.done) {
109
116
  break;
@@ -138,6 +145,7 @@ class DevstralLanguageModel {
138
145
  continue;
139
146
  }
140
147
  closeTextSegment();
148
+ hasToolCalls = true;
141
149
  safeEnqueue(controller, {
142
150
  type: 'tool-input-start',
143
151
  id: part.toolCallId,
@@ -154,6 +162,9 @@ class DevstralLanguageModel {
154
162
  });
155
163
  safeEnqueue(controller, part);
156
164
  }
165
+ if (frameResult.isEndStream) {
166
+ break outerLoop;
167
+ }
157
168
  }
158
169
  }
159
170
  if (isAborted(options.abortSignal)) {
@@ -161,10 +172,13 @@ class DevstralLanguageModel {
161
172
  return;
162
173
  }
163
174
  closeTextSegment();
175
+ const unified = hasToolCalls ? 'tool-calls' : 'stop';
164
176
  safeEnqueue(controller, {
165
177
  type: 'finish',
166
- finishReason: 'stop',
167
- rawFinishReason: 'stop',
178
+ finishReason: {
179
+ unified,
180
+ raw: undefined,
181
+ },
168
182
  usage: emptyUsage(),
169
183
  });
170
184
  safeClose(controller);
@@ -178,8 +192,10 @@ class DevstralLanguageModel {
178
192
  });
179
193
  safeEnqueue(controller, {
180
194
  type: 'finish',
181
- finishReason: 'error',
182
- rawFinishReason: 'error',
195
+ finishReason: {
196
+ unified: 'error',
197
+ raw: undefined,
198
+ },
183
199
  usage: emptyUsage(),
184
200
  });
185
201
  }
@@ -206,10 +222,11 @@ function readNextConnectFrame(buffer) {
206
222
  return null;
207
223
  }
208
224
  const frame = buffer.subarray(0, frameLength);
209
- const decoded = (0, connect_frame_js_1.connectFrameDecode)(frame);
225
+ const { payloads, isEndStream } = (0, connect_frame_js_1.connectFrameDecode)(frame);
210
226
  return {
211
- payload: decoded[0] ?? Buffer.alloc(0),
227
+ payload: payloads[0] ?? Buffer.alloc(0),
212
228
  rest: buffer.subarray(frameLength),
229
+ isEndStream,
213
230
  };
214
231
  }
215
232
  function safeEnqueue(controller, part) {
@@ -253,31 +270,35 @@ function emptyUsage() {
253
270
  }
254
271
  function toV3Content(parts) {
255
272
  return parts.map((part) => {
256
- if (part.type === 'tool-call') {
257
- return {
258
- type: 'tool-call',
259
- toolCallId: part.toolCallId,
260
- toolName: part.toolName,
261
- input: JSON.stringify(part.input),
262
- };
273
+ if (part.type !== 'tool-call') {
274
+ return part;
263
275
  }
264
- return part;
276
+ const input = typeof part.input === 'string' ? part.input : JSON.stringify(part.input);
277
+ return {
278
+ type: 'tool-call',
279
+ toolCallId: part.toolCallId,
280
+ toolName: part.toolName,
281
+ input,
282
+ };
265
283
  });
266
284
  }
285
+ function isFunctionTool(tool) {
286
+ return tool.type === 'function';
287
+ }
267
288
  function buildGenerateRequest(input) {
268
289
  const request = new protobuf_js_1.ProtobufEncoder();
269
290
  request.writeMessage(1, buildMetadata(input.apiKey, input.jwt));
270
291
  for (const message of input.messages) {
271
292
  request.writeMessage(2, buildMessage(message));
272
293
  }
273
- // Tools are sent as a single JSON string at field 3 (not repeated messages)
274
- if (input.tools && Object.keys(input.tools).length > 0) {
275
- const toolsArray = Object.entries(input.tools).map(([name, tool]) => ({
294
+ const functionTools = input.tools?.filter(isFunctionTool) ?? [];
295
+ if (functionTools.length > 0) {
296
+ const toolsArray = functionTools.map((tool) => ({
276
297
  type: 'function',
277
298
  function: {
278
- name,
299
+ name: tool.name,
279
300
  description: tool.description ?? '',
280
- parameters: tool.parameters ?? {},
301
+ parameters: tool.inputSchema,
281
302
  },
282
303
  }));
283
304
  request.writeString(3, JSON.stringify(toolsArray));