@bxb1337/windsurf-fast-context 1.0.7 → 1.0.8

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
  });
@@ -59,9 +59,15 @@ class DevstralLanguageModel {
59
59
  const 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) {
@@ -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,
@@ -161,10 +169,13 @@ class DevstralLanguageModel {
161
169
  return;
162
170
  }
163
171
  closeTextSegment();
172
+ const unified = hasToolCalls ? 'tool-calls' : 'stop';
164
173
  safeEnqueue(controller, {
165
174
  type: 'finish',
166
- finishReason: 'stop',
167
- rawFinishReason: 'stop',
175
+ finishReason: {
176
+ unified,
177
+ raw: undefined,
178
+ },
168
179
  usage: emptyUsage(),
169
180
  });
170
181
  safeClose(controller);
@@ -178,8 +189,10 @@ class DevstralLanguageModel {
178
189
  });
179
190
  safeEnqueue(controller, {
180
191
  type: 'finish',
181
- finishReason: 'error',
182
- rawFinishReason: 'error',
192
+ finishReason: {
193
+ unified: 'error',
194
+ raw: undefined,
195
+ },
183
196
  usage: emptyUsage(),
184
197
  });
185
198
  }
@@ -253,31 +266,35 @@ function emptyUsage() {
253
266
  }
254
267
  function toV3Content(parts) {
255
268
  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
- };
269
+ if (part.type !== 'tool-call') {
270
+ return part;
263
271
  }
264
- return part;
272
+ const input = typeof part.input === 'string' ? part.input : JSON.stringify(part.input);
273
+ return {
274
+ type: 'tool-call',
275
+ toolCallId: part.toolCallId,
276
+ toolName: part.toolName,
277
+ input,
278
+ };
265
279
  });
266
280
  }
281
+ function isFunctionTool(tool) {
282
+ return tool.type === 'function';
283
+ }
267
284
  function buildGenerateRequest(input) {
268
285
  const request = new protobuf_js_1.ProtobufEncoder();
269
286
  request.writeMessage(1, buildMetadata(input.apiKey, input.jwt));
270
287
  for (const message of input.messages) {
271
288
  request.writeMessage(2, buildMessage(message));
272
289
  }
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]) => ({
290
+ const functionTools = input.tools?.filter(isFunctionTool) ?? [];
291
+ if (functionTools.length > 0) {
292
+ const toolsArray = functionTools.map((tool) => ({
276
293
  type: 'function',
277
294
  function: {
278
- name,
295
+ name: tool.name,
279
296
  description: tool.description ?? '',
280
- parameters: tool.parameters ?? {},
297
+ parameters: tool.inputSchema,
281
298
  },
282
299
  }));
283
300
  request.writeString(3, JSON.stringify(toolsArray));