@vibe-lang/runtime 0.2.5

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 (250) hide show
  1. package/package.json +46 -0
  2. package/src/ast/index.ts +375 -0
  3. package/src/ast.ts +2 -0
  4. package/src/debug/advanced-features.ts +482 -0
  5. package/src/debug/bun-inspector.ts +424 -0
  6. package/src/debug/handoff-manager.ts +283 -0
  7. package/src/debug/index.ts +150 -0
  8. package/src/debug/runner.ts +365 -0
  9. package/src/debug/server.ts +565 -0
  10. package/src/debug/stack-merger.ts +267 -0
  11. package/src/debug/state.ts +581 -0
  12. package/src/debug/test/advanced-features.test.ts +300 -0
  13. package/src/debug/test/e2e.test.ts +218 -0
  14. package/src/debug/test/handoff-manager.test.ts +256 -0
  15. package/src/debug/test/runner.test.ts +256 -0
  16. package/src/debug/test/stack-merger.test.ts +163 -0
  17. package/src/debug/test/state.test.ts +400 -0
  18. package/src/debug/test/ts-debug-integration.test.ts +374 -0
  19. package/src/debug/test/ts-import-tracker.test.ts +125 -0
  20. package/src/debug/test/ts-source-map.test.ts +169 -0
  21. package/src/debug/ts-import-tracker.ts +151 -0
  22. package/src/debug/ts-source-map.ts +171 -0
  23. package/src/errors/index.ts +124 -0
  24. package/src/index.ts +358 -0
  25. package/src/lexer/index.ts +348 -0
  26. package/src/lexer.ts +2 -0
  27. package/src/parser/index.ts +792 -0
  28. package/src/parser/parse.ts +45 -0
  29. package/src/parser/test/async.test.ts +248 -0
  30. package/src/parser/test/destructuring.test.ts +167 -0
  31. package/src/parser/test/do-expression.test.ts +486 -0
  32. package/src/parser/test/errors/do-expression.test.ts +95 -0
  33. package/src/parser/test/errors/error-locations.test.ts +230 -0
  34. package/src/parser/test/errors/invalid-expressions.test.ts +144 -0
  35. package/src/parser/test/errors/missing-tokens.test.ts +126 -0
  36. package/src/parser/test/errors/model-declaration.test.ts +185 -0
  37. package/src/parser/test/errors/nested-blocks.test.ts +226 -0
  38. package/src/parser/test/errors/unclosed-delimiters.test.ts +122 -0
  39. package/src/parser/test/errors/unexpected-tokens.test.ts +120 -0
  40. package/src/parser/test/import-export.test.ts +143 -0
  41. package/src/parser/test/literals.test.ts +404 -0
  42. package/src/parser/test/model-declaration.test.ts +161 -0
  43. package/src/parser/test/nested-blocks.test.ts +402 -0
  44. package/src/parser/test/parser.test.ts +743 -0
  45. package/src/parser/test/private.test.ts +136 -0
  46. package/src/parser/test/template-literal.test.ts +127 -0
  47. package/src/parser/test/tool-declaration.test.ts +302 -0
  48. package/src/parser/test/ts-block.test.ts +252 -0
  49. package/src/parser/test/type-annotations.test.ts +254 -0
  50. package/src/parser/visitor/helpers.ts +330 -0
  51. package/src/parser/visitor.ts +794 -0
  52. package/src/parser.ts +2 -0
  53. package/src/runtime/ai/cache-chunking.test.ts +69 -0
  54. package/src/runtime/ai/cache-chunking.ts +73 -0
  55. package/src/runtime/ai/client.ts +109 -0
  56. package/src/runtime/ai/context.ts +168 -0
  57. package/src/runtime/ai/formatters.ts +316 -0
  58. package/src/runtime/ai/index.ts +38 -0
  59. package/src/runtime/ai/language-ref.ts +38 -0
  60. package/src/runtime/ai/providers/anthropic.ts +253 -0
  61. package/src/runtime/ai/providers/google.ts +201 -0
  62. package/src/runtime/ai/providers/openai.ts +156 -0
  63. package/src/runtime/ai/retry.ts +100 -0
  64. package/src/runtime/ai/return-tools.ts +301 -0
  65. package/src/runtime/ai/test/client.test.ts +83 -0
  66. package/src/runtime/ai/test/formatters.test.ts +485 -0
  67. package/src/runtime/ai/test/retry.test.ts +137 -0
  68. package/src/runtime/ai/test/return-tools.test.ts +450 -0
  69. package/src/runtime/ai/test/tool-loop.test.ts +319 -0
  70. package/src/runtime/ai/test/tool-schema.test.ts +241 -0
  71. package/src/runtime/ai/tool-loop.ts +203 -0
  72. package/src/runtime/ai/tool-schema.ts +151 -0
  73. package/src/runtime/ai/types.ts +113 -0
  74. package/src/runtime/ai-logger.ts +255 -0
  75. package/src/runtime/ai-provider.ts +347 -0
  76. package/src/runtime/async/dependencies.ts +276 -0
  77. package/src/runtime/async/executor.ts +293 -0
  78. package/src/runtime/async/index.ts +43 -0
  79. package/src/runtime/async/scheduling.ts +163 -0
  80. package/src/runtime/async/test/dependencies.test.ts +284 -0
  81. package/src/runtime/async/test/executor.test.ts +388 -0
  82. package/src/runtime/context.ts +357 -0
  83. package/src/runtime/exec/ai.ts +139 -0
  84. package/src/runtime/exec/expressions.ts +475 -0
  85. package/src/runtime/exec/frames.ts +26 -0
  86. package/src/runtime/exec/functions.ts +305 -0
  87. package/src/runtime/exec/interpolation.ts +312 -0
  88. package/src/runtime/exec/statements.ts +604 -0
  89. package/src/runtime/exec/tools.ts +129 -0
  90. package/src/runtime/exec/typescript.ts +215 -0
  91. package/src/runtime/exec/variables.ts +279 -0
  92. package/src/runtime/index.ts +975 -0
  93. package/src/runtime/modules.ts +452 -0
  94. package/src/runtime/serialize.ts +103 -0
  95. package/src/runtime/state.ts +489 -0
  96. package/src/runtime/stdlib/core.ts +45 -0
  97. package/src/runtime/stdlib/directory.test.ts +156 -0
  98. package/src/runtime/stdlib/edit.test.ts +154 -0
  99. package/src/runtime/stdlib/fastEdit.test.ts +201 -0
  100. package/src/runtime/stdlib/glob.test.ts +106 -0
  101. package/src/runtime/stdlib/grep.test.ts +144 -0
  102. package/src/runtime/stdlib/index.ts +16 -0
  103. package/src/runtime/stdlib/readFile.test.ts +123 -0
  104. package/src/runtime/stdlib/tools/index.ts +707 -0
  105. package/src/runtime/stdlib/writeFile.test.ts +157 -0
  106. package/src/runtime/step.ts +969 -0
  107. package/src/runtime/test/ai-context.test.ts +1086 -0
  108. package/src/runtime/test/ai-result-object.test.ts +419 -0
  109. package/src/runtime/test/ai-tool-flow.test.ts +859 -0
  110. package/src/runtime/test/async-execution-order.test.ts +618 -0
  111. package/src/runtime/test/async-execution.test.ts +344 -0
  112. package/src/runtime/test/async-nested.test.ts +660 -0
  113. package/src/runtime/test/async-parallel-timing.test.ts +546 -0
  114. package/src/runtime/test/basic1.test.ts +154 -0
  115. package/src/runtime/test/binary-operators.test.ts +431 -0
  116. package/src/runtime/test/break-statement.test.ts +257 -0
  117. package/src/runtime/test/context-modes.test.ts +650 -0
  118. package/src/runtime/test/context.test.ts +466 -0
  119. package/src/runtime/test/core-functions.test.ts +228 -0
  120. package/src/runtime/test/e2e.test.ts +88 -0
  121. package/src/runtime/test/error-locations/error-locations.test.ts +80 -0
  122. package/src/runtime/test/error-locations/main-error.vibe +4 -0
  123. package/src/runtime/test/error-locations/main-import-error.vibe +3 -0
  124. package/src/runtime/test/error-locations/utils/helper.vibe +5 -0
  125. package/src/runtime/test/for-in.test.ts +312 -0
  126. package/src/runtime/test/helpers.ts +69 -0
  127. package/src/runtime/test/imports.test.ts +334 -0
  128. package/src/runtime/test/json-expressions.test.ts +232 -0
  129. package/src/runtime/test/literals.test.ts +372 -0
  130. package/src/runtime/test/logical-indexing.test.ts +478 -0
  131. package/src/runtime/test/member-methods.test.ts +324 -0
  132. package/src/runtime/test/model-config.test.ts +338 -0
  133. package/src/runtime/test/null-handling.test.ts +342 -0
  134. package/src/runtime/test/private-visibility.test.ts +332 -0
  135. package/src/runtime/test/runtime-state.test.ts +514 -0
  136. package/src/runtime/test/scoping.test.ts +370 -0
  137. package/src/runtime/test/string-interpolation.test.ts +354 -0
  138. package/src/runtime/test/template-literal.test.ts +181 -0
  139. package/src/runtime/test/tool-execution.test.ts +467 -0
  140. package/src/runtime/test/tool-schema-generation.test.ts +477 -0
  141. package/src/runtime/test/tostring.test.ts +210 -0
  142. package/src/runtime/test/ts-block.test.ts +594 -0
  143. package/src/runtime/test/ts-error-location.test.ts +231 -0
  144. package/src/runtime/test/types.test.ts +732 -0
  145. package/src/runtime/test/verbose-logger.test.ts +710 -0
  146. package/src/runtime/test/vibe-expression.test.ts +54 -0
  147. package/src/runtime/test/vibe-value-errors.test.ts +541 -0
  148. package/src/runtime/test/while.test.ts +232 -0
  149. package/src/runtime/tools/builtin.ts +30 -0
  150. package/src/runtime/tools/directory-tools.ts +70 -0
  151. package/src/runtime/tools/file-tools.ts +228 -0
  152. package/src/runtime/tools/index.ts +5 -0
  153. package/src/runtime/tools/registry.ts +48 -0
  154. package/src/runtime/tools/search-tools.ts +134 -0
  155. package/src/runtime/tools/security.ts +36 -0
  156. package/src/runtime/tools/system-tools.ts +312 -0
  157. package/src/runtime/tools/test/fixtures/base-types.ts +40 -0
  158. package/src/runtime/tools/test/fixtures/test-types.ts +132 -0
  159. package/src/runtime/tools/test/registry.test.ts +713 -0
  160. package/src/runtime/tools/test/security.test.ts +86 -0
  161. package/src/runtime/tools/test/system-tools.test.ts +679 -0
  162. package/src/runtime/tools/test/ts-schema.test.ts +357 -0
  163. package/src/runtime/tools/ts-schema.ts +341 -0
  164. package/src/runtime/tools/types.ts +89 -0
  165. package/src/runtime/tools/utility-tools.ts +198 -0
  166. package/src/runtime/ts-eval.ts +126 -0
  167. package/src/runtime/types.ts +797 -0
  168. package/src/runtime/validation.ts +160 -0
  169. package/src/runtime/verbose-logger.ts +459 -0
  170. package/src/runtime.ts +2 -0
  171. package/src/semantic/analyzer-context.ts +62 -0
  172. package/src/semantic/analyzer-validators.ts +575 -0
  173. package/src/semantic/analyzer-visitors.ts +534 -0
  174. package/src/semantic/analyzer.ts +83 -0
  175. package/src/semantic/index.ts +11 -0
  176. package/src/semantic/symbol-table.ts +58 -0
  177. package/src/semantic/test/async-validation.test.ts +301 -0
  178. package/src/semantic/test/compress-validation.test.ts +179 -0
  179. package/src/semantic/test/const-reassignment.test.ts +111 -0
  180. package/src/semantic/test/control-flow.test.ts +346 -0
  181. package/src/semantic/test/destructuring.test.ts +185 -0
  182. package/src/semantic/test/duplicate-declarations.test.ts +168 -0
  183. package/src/semantic/test/export-validation.test.ts +111 -0
  184. package/src/semantic/test/fixtures/math.ts +31 -0
  185. package/src/semantic/test/imports.test.ts +148 -0
  186. package/src/semantic/test/json-type.test.ts +68 -0
  187. package/src/semantic/test/literals.test.ts +127 -0
  188. package/src/semantic/test/model-validation.test.ts +179 -0
  189. package/src/semantic/test/prompt-validation.test.ts +343 -0
  190. package/src/semantic/test/scoping.test.ts +312 -0
  191. package/src/semantic/test/tool-validation.test.ts +306 -0
  192. package/src/semantic/test/ts-type-checking.test.ts +563 -0
  193. package/src/semantic/test/type-constraints.test.ts +111 -0
  194. package/src/semantic/test/type-inference.test.ts +87 -0
  195. package/src/semantic/test/type-validation.test.ts +552 -0
  196. package/src/semantic/test/undefined-variables.test.ts +163 -0
  197. package/src/semantic/ts-block-checker.ts +204 -0
  198. package/src/semantic/ts-signatures.ts +194 -0
  199. package/src/semantic/ts-types.ts +170 -0
  200. package/src/semantic/types.ts +58 -0
  201. package/tests/fixtures/conditional-logic.vibe +14 -0
  202. package/tests/fixtures/function-call.vibe +16 -0
  203. package/tests/fixtures/imports/cycle-detection/a.vibe +6 -0
  204. package/tests/fixtures/imports/cycle-detection/b.vibe +5 -0
  205. package/tests/fixtures/imports/cycle-detection/main.vibe +3 -0
  206. package/tests/fixtures/imports/module-isolation/main-b.vibe +8 -0
  207. package/tests/fixtures/imports/module-isolation/main.vibe +9 -0
  208. package/tests/fixtures/imports/module-isolation/moduleA.vibe +6 -0
  209. package/tests/fixtures/imports/module-isolation/moduleB.vibe +6 -0
  210. package/tests/fixtures/imports/nested-import/helper.vibe +6 -0
  211. package/tests/fixtures/imports/nested-import/main.vibe +3 -0
  212. package/tests/fixtures/imports/nested-import/utils.ts +3 -0
  213. package/tests/fixtures/imports/nested-isolation/file2.vibe +15 -0
  214. package/tests/fixtures/imports/nested-isolation/file3.vibe +10 -0
  215. package/tests/fixtures/imports/nested-isolation/main.vibe +21 -0
  216. package/tests/fixtures/imports/pure-cycle/a.vibe +5 -0
  217. package/tests/fixtures/imports/pure-cycle/b.vibe +5 -0
  218. package/tests/fixtures/imports/pure-cycle/main.vibe +3 -0
  219. package/tests/fixtures/imports/ts-boolean/checks.ts +14 -0
  220. package/tests/fixtures/imports/ts-boolean/main.vibe +10 -0
  221. package/tests/fixtures/imports/ts-boolean/type-mismatch.vibe +5 -0
  222. package/tests/fixtures/imports/ts-boolean/use-constant.vibe +18 -0
  223. package/tests/fixtures/imports/ts-error-handling/helpers.ts +42 -0
  224. package/tests/fixtures/imports/ts-error-handling/main.vibe +5 -0
  225. package/tests/fixtures/imports/ts-import/main.vibe +4 -0
  226. package/tests/fixtures/imports/ts-import/math.ts +9 -0
  227. package/tests/fixtures/imports/ts-variables/call-non-function.vibe +5 -0
  228. package/tests/fixtures/imports/ts-variables/data.ts +10 -0
  229. package/tests/fixtures/imports/ts-variables/import-json.vibe +5 -0
  230. package/tests/fixtures/imports/ts-variables/import-type-mismatch.vibe +5 -0
  231. package/tests/fixtures/imports/ts-variables/import-variable.vibe +5 -0
  232. package/tests/fixtures/imports/vibe-import/greet.vibe +5 -0
  233. package/tests/fixtures/imports/vibe-import/main.vibe +3 -0
  234. package/tests/fixtures/multiple-ai-calls.vibe +10 -0
  235. package/tests/fixtures/simple-greeting.vibe +6 -0
  236. package/tests/fixtures/template-literals.vibe +11 -0
  237. package/tests/integration/basic-ai/basic-ai.integration.test.ts +166 -0
  238. package/tests/integration/basic-ai/basic-ai.vibe +12 -0
  239. package/tests/integration/bug-fix/bug-fix.integration.test.ts +201 -0
  240. package/tests/integration/bug-fix/buggy-code.ts +22 -0
  241. package/tests/integration/bug-fix/fix-bug.vibe +21 -0
  242. package/tests/integration/compress/compress.integration.test.ts +206 -0
  243. package/tests/integration/destructuring/destructuring.integration.test.ts +92 -0
  244. package/tests/integration/hello-world-translator/hello-world-translator.integration.test.ts +61 -0
  245. package/tests/integration/line-annotator/context-modes.integration.test.ts +261 -0
  246. package/tests/integration/line-annotator/line-annotator.integration.test.ts +148 -0
  247. package/tests/integration/multi-feature/cumulative-sum.integration.test.ts +75 -0
  248. package/tests/integration/multi-feature/number-analyzer.integration.test.ts +191 -0
  249. package/tests/integration/multi-feature/number-analyzer.vibe +59 -0
  250. package/tests/integration/tool-calls/tool-calls.integration.test.ts +93 -0
@@ -0,0 +1,485 @@
1
+ // Formatter tests
2
+
3
+ import { describe, test, expect } from 'bun:test';
4
+ import {
5
+ buildSystemMessage,
6
+ buildContextMessage,
7
+ buildPromptMessage,
8
+ buildMessages,
9
+ buildToolSystemMessage,
10
+ extractTextContent,
11
+ extractUsage,
12
+ } from '../formatters';
13
+ import type { ToolSchema } from '../../tools/types';
14
+
15
+ describe('buildSystemMessage', () => {
16
+ test('returns a system message string', () => {
17
+ const message = buildSystemMessage();
18
+ expect(typeof message).toBe('string');
19
+ expect(message.length).toBeGreaterThan(0);
20
+ expect(message).toContain('Vibe');
21
+ });
22
+ });
23
+
24
+ describe('buildContextMessage', () => {
25
+ test('returns null for empty context', () => {
26
+ expect(buildContextMessage('')).toBeNull();
27
+ expect(buildContextMessage(' ')).toBeNull();
28
+ });
29
+
30
+ test('wraps context with header', () => {
31
+ const message = buildContextMessage('Variable x = 5');
32
+ expect(message).toContain('context');
33
+ expect(message).toContain('Variable x = 5');
34
+ });
35
+ });
36
+
37
+ describe('buildPromptMessage', () => {
38
+ test('returns prompt as-is', () => {
39
+ // Type instructions are now handled by return tools, not prompt modification
40
+ expect(buildPromptMessage('Hello')).toBe('Hello');
41
+ expect(buildPromptMessage('Calculate 2+2')).toBe('Calculate 2+2');
42
+ });
43
+ });
44
+
45
+ describe('buildToolSystemMessage', () => {
46
+ test('returns null for empty tools array', () => {
47
+ expect(buildToolSystemMessage([])).toBeNull();
48
+ });
49
+
50
+ test('formats single tool with description', () => {
51
+ const tools: ToolSchema[] = [
52
+ {
53
+ name: 'getWeather',
54
+ description: 'Get weather for a city',
55
+ parameters: [
56
+ { name: 'city', type: { type: 'string' }, required: true },
57
+ ],
58
+ },
59
+ ];
60
+
61
+ const message = buildToolSystemMessage(tools);
62
+
63
+ expect(message).toBe(
64
+ `You have access to the following tools:
65
+ - getWeather(city: string)
66
+ Get weather for a city
67
+
68
+ Call tools when needed to complete the task.`
69
+ );
70
+ });
71
+
72
+ test('full system message with multiple user-defined tools', () => {
73
+ // This represents what tools defined in Vibe would look like:
74
+ // tool add(a: number, b: number): number @description "Add two numbers" @param a "First number" @param b "Second number"
75
+ // tool multiply(x: number, y: number): number @description "Multiply two numbers"
76
+ // tool fetchData(url: text): json @description "Fetch JSON from URL" @param url "The URL to fetch"
77
+ const tools: ToolSchema[] = [
78
+ {
79
+ name: 'add',
80
+ description: 'Add two numbers',
81
+ parameters: [
82
+ { name: 'a', type: { type: 'number' }, description: 'First number', required: true },
83
+ { name: 'b', type: { type: 'number' }, description: 'Second number', required: true },
84
+ ],
85
+ returns: { type: 'number' },
86
+ },
87
+ {
88
+ name: 'multiply',
89
+ description: 'Multiply two numbers',
90
+ parameters: [
91
+ { name: 'x', type: { type: 'number' }, required: true },
92
+ { name: 'y', type: { type: 'number' }, required: true },
93
+ ],
94
+ returns: { type: 'number' },
95
+ },
96
+ {
97
+ name: 'fetchData',
98
+ description: 'Fetch JSON from URL',
99
+ parameters: [
100
+ { name: 'url', type: { type: 'string' }, description: 'The URL to fetch', required: true },
101
+ ],
102
+ returns: { type: 'object' },
103
+ },
104
+ ];
105
+
106
+ const message = buildToolSystemMessage(tools);
107
+
108
+ expect(message).toBe(
109
+ `You have access to the following tools:
110
+ - add(a: number, b: number)
111
+ Add two numbers
112
+ Parameters:
113
+ a: First number
114
+ b: Second number
115
+ - multiply(x: number, y: number)
116
+ Multiply two numbers
117
+ - fetchData(url: string)
118
+ Fetch JSON from URL
119
+ Parameters:
120
+ url: The URL to fetch
121
+
122
+ Call tools when needed to complete the task.`
123
+ );
124
+ });
125
+
126
+ test('full message array with tools, context, and prompt', () => {
127
+ const tools: ToolSchema[] = [
128
+ {
129
+ name: 'calculate',
130
+ description: 'Perform arithmetic',
131
+ parameters: [
132
+ { name: 'expression', type: { type: 'string' }, description: 'Math expression', required: true },
133
+ ],
134
+ },
135
+ {
136
+ name: 'storeResult',
137
+ description: 'Store a calculation result',
138
+ parameters: [
139
+ { name: 'key', type: { type: 'string' }, required: true },
140
+ { name: 'value', type: { type: 'number' }, required: true },
141
+ ],
142
+ },
143
+ ];
144
+
145
+ const contextText = `<entry> (current scope)
146
+ - x (number): 10
147
+ - y (number): 20`;
148
+
149
+ const messages = buildMessages(
150
+ 'Calculate x + y and store it',
151
+ contextText,
152
+ tools
153
+ );
154
+
155
+ // Verify complete message structure
156
+ expect(messages).toHaveLength(4);
157
+
158
+ // Message 0: Base system message
159
+ expect(messages[0]).toEqual({
160
+ role: 'system',
161
+ content: `You are an AI assistant integrated into the Vibe programming language runtime.
162
+ Your responses will be used programmatically in the execution flow.
163
+ Be concise and precise. Follow any type constraints exactly.
164
+ When context is provided, use it to inform your response.`,
165
+ });
166
+
167
+ // Message 1: Tool system message
168
+ expect(messages[1]).toEqual({
169
+ role: 'system',
170
+ content: `You have access to the following tools:
171
+ - calculate(expression: string)
172
+ Perform arithmetic
173
+ Parameters:
174
+ expression: Math expression
175
+ - storeResult(key: string, value: number)
176
+ Store a calculation result
177
+
178
+ Call tools when needed to complete the task.`,
179
+ });
180
+
181
+ // Message 2: Context message
182
+ expect(messages[2]).toEqual({
183
+ role: 'user',
184
+ content: `Here is the current program context:
185
+
186
+ <entry> (current scope)
187
+ - x (number): 10
188
+ - y (number): 20`,
189
+ });
190
+
191
+ // Message 3: Prompt
192
+ expect(messages[3]).toEqual({
193
+ role: 'user',
194
+ content: 'Calculate x + y and store it',
195
+ });
196
+ });
197
+
198
+ test('formats tool without description', () => {
199
+ const tools: ToolSchema[] = [
200
+ {
201
+ name: 'now',
202
+ parameters: [],
203
+ },
204
+ ];
205
+
206
+ const message = buildToolSystemMessage(tools);
207
+
208
+ expect(message).toBe(
209
+ `You have access to the following tools:
210
+ - now()
211
+
212
+ Call tools when needed to complete the task.`
213
+ );
214
+ });
215
+
216
+ test('formats multiple tools with various parameters', () => {
217
+ const tools: ToolSchema[] = [
218
+ {
219
+ name: 'add',
220
+ description: 'Add two numbers',
221
+ parameters: [
222
+ { name: 'a', type: { type: 'number' }, required: true },
223
+ { name: 'b', type: { type: 'number' }, required: true },
224
+ ],
225
+ },
226
+ {
227
+ name: 'greet',
228
+ description: 'Greet someone',
229
+ parameters: [
230
+ { name: 'name', type: { type: 'string' }, required: true },
231
+ ],
232
+ },
233
+ {
234
+ name: 'now',
235
+ description: 'Get current timestamp',
236
+ parameters: [],
237
+ },
238
+ ];
239
+
240
+ const message = buildToolSystemMessage(tools);
241
+
242
+ expect(message).toBe(
243
+ `You have access to the following tools:
244
+ - add(a: number, b: number)
245
+ Add two numbers
246
+ - greet(name: string)
247
+ Greet someone
248
+ - now()
249
+ Get current timestamp
250
+
251
+ Call tools when needed to complete the task.`
252
+ );
253
+ });
254
+
255
+ test('formats tool with many parameters', () => {
256
+ const tools: ToolSchema[] = [
257
+ {
258
+ name: 'sendEmail',
259
+ description: 'Send an email',
260
+ parameters: [
261
+ { name: 'to', type: { type: 'string' }, required: true },
262
+ { name: 'subject', type: { type: 'string' }, required: true },
263
+ { name: 'body', type: { type: 'string' }, required: true },
264
+ { name: 'cc', type: { type: 'string' }, required: false },
265
+ ],
266
+ },
267
+ ];
268
+
269
+ const message = buildToolSystemMessage(tools);
270
+
271
+ expect(message).toBe(
272
+ `You have access to the following tools:
273
+ - sendEmail(to: string, subject: string, body: string, cc: string)
274
+ Send an email
275
+
276
+ Call tools when needed to complete the task.`
277
+ );
278
+ });
279
+
280
+ test('formats tool with complex nested object parameter', () => {
281
+ const tools: ToolSchema[] = [
282
+ {
283
+ name: 'createOrder',
284
+ description: 'Create a new order',
285
+ parameters: [
286
+ {
287
+ name: 'customer',
288
+ type: {
289
+ type: 'object',
290
+ properties: {
291
+ name: { type: 'string' },
292
+ email: { type: 'string' },
293
+ address: {
294
+ type: 'object',
295
+ properties: {
296
+ street: { type: 'string' },
297
+ city: { type: 'string' },
298
+ zip: { type: 'string' },
299
+ },
300
+ },
301
+ },
302
+ },
303
+ description: 'Customer information',
304
+ required: true,
305
+ },
306
+ {
307
+ name: 'items',
308
+ type: {
309
+ type: 'array',
310
+ items: {
311
+ type: 'object',
312
+ properties: {
313
+ productId: { type: 'string' },
314
+ quantity: { type: 'number' },
315
+ price: { type: 'number' },
316
+ },
317
+ },
318
+ },
319
+ description: 'Order items',
320
+ required: true,
321
+ },
322
+ ],
323
+ },
324
+ ];
325
+
326
+ const message = buildToolSystemMessage(tools);
327
+
328
+ expect(message).toBe(
329
+ `You have access to the following tools:
330
+ - createOrder(customer: {name: string, email: string, address: {street: string, city: string, zip: string}}, items: {productId: string, quantity: number, price: number}[])
331
+ Create a new order
332
+ Parameters:
333
+ customer: Customer information
334
+ items: Order items
335
+
336
+ Call tools when needed to complete the task.`
337
+ );
338
+ });
339
+ });
340
+
341
+ describe('buildMessages', () => {
342
+ test('builds messages with system and prompt', () => {
343
+ const messages = buildMessages('Hello', '');
344
+
345
+ expect(messages).toHaveLength(2);
346
+ expect(messages[0].role).toBe('system');
347
+ expect(messages[1].role).toBe('user');
348
+ expect(messages[1].content).toBe('Hello');
349
+ });
350
+
351
+ test('includes context message when provided', () => {
352
+ const messages = buildMessages('Hello', 'x = 5');
353
+
354
+ expect(messages).toHaveLength(3);
355
+ expect(messages[0].role).toBe('system');
356
+ expect(messages[1].role).toBe('user');
357
+ expect(messages[1].content).toContain('x = 5');
358
+ expect(messages[2].role).toBe('user');
359
+ expect(messages[2].content).toBe('Hello');
360
+ });
361
+
362
+ test('includes tool system message when tools provided', () => {
363
+ const tools: ToolSchema[] = [
364
+ {
365
+ name: 'add',
366
+ description: 'Add two numbers',
367
+ parameters: [
368
+ { name: 'a', type: { type: 'number' }, required: true },
369
+ { name: 'b', type: { type: 'number' }, required: true },
370
+ ],
371
+ },
372
+ ];
373
+
374
+ const messages = buildMessages('Calculate 2+3', '', tools);
375
+
376
+ // Should have: system, tool system, prompt
377
+ expect(messages).toHaveLength(3);
378
+ expect(messages[0].role).toBe('system');
379
+ expect(messages[1].role).toBe('system');
380
+ expect(messages[1].content).toContain('You have access to the following tools:');
381
+ expect(messages[1].content).toContain('- add(a: number, b: number)');
382
+ expect(messages[1].content).toContain('Add two numbers');
383
+ expect(messages[2].role).toBe('user');
384
+ expect(messages[2].content).toBe('Calculate 2+3');
385
+ });
386
+
387
+ test('includes all message types: system, tools, context, prompt', () => {
388
+ const tools: ToolSchema[] = [
389
+ {
390
+ name: 'getWeather',
391
+ description: 'Get weather',
392
+ parameters: [{ name: 'city', type: { type: 'string' }, required: true }],
393
+ },
394
+ ];
395
+
396
+ const messages = buildMessages(
397
+ 'What is the weather?',
398
+ 'location = Seattle',
399
+ tools
400
+ );
401
+
402
+ // Should have: system, tool system, context, prompt
403
+ expect(messages).toHaveLength(4);
404
+ expect(messages[0].role).toBe('system');
405
+ expect(messages[0].content).toContain('Vibe');
406
+ expect(messages[1].role).toBe('system');
407
+ expect(messages[1].content).toContain('getWeather(city: string)');
408
+ expect(messages[1].content).toContain('Get weather');
409
+ expect(messages[2].role).toBe('user');
410
+ expect(messages[2].content).toContain('location = Seattle');
411
+ expect(messages[3].role).toBe('user');
412
+ expect(messages[3].content).toBe('What is the weather?');
413
+ });
414
+ });
415
+
416
+ describe('extractTextContent', () => {
417
+ test('extracts from Anthropic format', () => {
418
+ const response = {
419
+ content: [{ type: 'text', text: 'Hello from Claude' }],
420
+ };
421
+ expect(extractTextContent(response)).toBe('Hello from Claude');
422
+ });
423
+
424
+ test('extracts from OpenAI format', () => {
425
+ const response = {
426
+ choices: [{ message: { content: 'Hello from GPT' } }],
427
+ };
428
+ expect(extractTextContent(response)).toBe('Hello from GPT');
429
+ });
430
+
431
+ test('extracts from Google format', () => {
432
+ const response = {
433
+ candidates: [
434
+ {
435
+ content: {
436
+ parts: [{ text: 'Hello from Gemini' }],
437
+ },
438
+ },
439
+ ],
440
+ };
441
+ expect(extractTextContent(response)).toBe('Hello from Gemini');
442
+ });
443
+
444
+ test('throws for unknown format', () => {
445
+ expect(() => extractTextContent({})).toThrow();
446
+ expect(() => extractTextContent({ unknown: 'format' })).toThrow();
447
+ });
448
+ });
449
+
450
+ describe('extractUsage', () => {
451
+ test('extracts from Anthropic format', () => {
452
+ const response = {
453
+ usage: { input_tokens: 10, output_tokens: 20 },
454
+ };
455
+ expect(extractUsage(response)).toEqual({
456
+ inputTokens: 10,
457
+ outputTokens: 20,
458
+ });
459
+ });
460
+
461
+ test('extracts from OpenAI format', () => {
462
+ const response = {
463
+ usage: { prompt_tokens: 15, completion_tokens: 25 },
464
+ };
465
+ expect(extractUsage(response)).toEqual({
466
+ inputTokens: 15,
467
+ outputTokens: 25,
468
+ });
469
+ });
470
+
471
+ test('extracts from Google format', () => {
472
+ const response = {
473
+ usageMetadata: { promptTokenCount: 8, candidatesTokenCount: 12 },
474
+ };
475
+ expect(extractUsage(response)).toEqual({
476
+ inputTokens: 8,
477
+ outputTokens: 12,
478
+ });
479
+ });
480
+
481
+ test('returns undefined for missing usage', () => {
482
+ expect(extractUsage({})).toBeUndefined();
483
+ expect(extractUsage({ content: 'text' })).toBeUndefined();
484
+ });
485
+ });
@@ -0,0 +1,137 @@
1
+ // Retry logic tests
2
+
3
+ import { describe, test, expect, mock } from 'bun:test';
4
+ import {
5
+ isRetryableError,
6
+ calculateDelay,
7
+ withRetry,
8
+ createAIErrorFromResponse,
9
+ } from '../retry';
10
+ import { AIError } from '../types';
11
+
12
+ describe('isRetryableError', () => {
13
+ test('returns true for AIError with isRetryable flag', () => {
14
+ const error = new AIError('Rate limited', 429, true);
15
+ expect(isRetryableError(error)).toBe(true);
16
+ });
17
+
18
+ test('returns false for AIError without isRetryable flag', () => {
19
+ const error = new AIError('Bad request', 400, false);
20
+ expect(isRetryableError(error)).toBe(false);
21
+ });
22
+
23
+ test('returns true for network-related errors', () => {
24
+ expect(isRetryableError(new Error('network error'))).toBe(true);
25
+ expect(isRetryableError(new Error('ECONNRESET'))).toBe(true);
26
+ expect(isRetryableError(new Error('timeout'))).toBe(true);
27
+ expect(isRetryableError(new Error('socket hang up'))).toBe(true);
28
+ });
29
+
30
+ test('returns false for non-retryable errors', () => {
31
+ expect(isRetryableError(new Error('Unknown error'))).toBe(false);
32
+ expect(isRetryableError(new Error('Invalid API key'))).toBe(false);
33
+ });
34
+ });
35
+
36
+ describe('calculateDelay', () => {
37
+ test('returns delay with exponential growth', () => {
38
+ const delay0 = calculateDelay(0, 1000, 30000);
39
+ const delay1 = calculateDelay(1, 1000, 30000);
40
+ const delay2 = calculateDelay(2, 1000, 30000);
41
+
42
+ // Each should be roughly double the previous (with jitter)
43
+ // delay0 is base * 2^0 * jitter = 1000 * 1 * (0.5-1.0) = 500-1000
44
+ expect(delay0).toBeGreaterThanOrEqual(500);
45
+ expect(delay0).toBeLessThanOrEqual(1000);
46
+
47
+ // delay1 is base * 2^1 * jitter = 1000 * 2 * (0.5-1.0) = 1000-2000
48
+ expect(delay1).toBeGreaterThanOrEqual(1000);
49
+ expect(delay1).toBeLessThanOrEqual(2000);
50
+
51
+ // delay2 is base * 2^2 * jitter = 1000 * 4 * (0.5-1.0) = 2000-4000
52
+ expect(delay2).toBeGreaterThanOrEqual(2000);
53
+ expect(delay2).toBeLessThanOrEqual(4000);
54
+ });
55
+
56
+ test('caps delay at maxDelay', () => {
57
+ const delay = calculateDelay(10, 1000, 5000);
58
+ // At attempt 10, base delay would be 1000 * 2^10 = 1024000
59
+ // But capped at 5000 with jitter = 2500-5000
60
+ expect(delay).toBeGreaterThanOrEqual(2500);
61
+ expect(delay).toBeLessThanOrEqual(5000);
62
+ });
63
+ });
64
+
65
+ describe('createAIErrorFromResponse', () => {
66
+ test('creates retryable error for 429', () => {
67
+ const error = createAIErrorFromResponse(429, 'Rate limited');
68
+ expect(error.statusCode).toBe(429);
69
+ expect(error.isRetryable).toBe(true);
70
+ expect(error.message).toContain('Rate limited');
71
+ });
72
+
73
+ test('creates retryable error for 5xx', () => {
74
+ const error500 = createAIErrorFromResponse(500, 'Internal error');
75
+ expect(error500.isRetryable).toBe(true);
76
+
77
+ const error503 = createAIErrorFromResponse(503, 'Service unavailable');
78
+ expect(error503.isRetryable).toBe(true);
79
+ });
80
+
81
+ test('creates non-retryable error for 4xx (except 429)', () => {
82
+ const error400 = createAIErrorFromResponse(400, 'Bad request');
83
+ expect(error400.isRetryable).toBe(false);
84
+
85
+ const error401 = createAIErrorFromResponse(401, 'Unauthorized');
86
+ expect(error401.isRetryable).toBe(false);
87
+ });
88
+ });
89
+
90
+ describe('withRetry', () => {
91
+ test('returns result on first success', async () => {
92
+ const fn = mock().mockResolvedValue('success');
93
+ const result = await withRetry(fn, { maxRetries: 3 });
94
+
95
+ expect(result).toBe('success');
96
+ expect(fn).toHaveBeenCalledTimes(1);
97
+ });
98
+
99
+ test('retries on retryable error', async () => {
100
+ const fn = mock()
101
+ .mockRejectedValueOnce(new AIError('Rate limited', 429, true))
102
+ .mockResolvedValue('success');
103
+
104
+ const result = await withRetry(fn, { maxRetries: 3, baseDelayMs: 10 });
105
+
106
+ expect(result).toBe('success');
107
+ expect(fn).toHaveBeenCalledTimes(2);
108
+ });
109
+
110
+ test('does not retry on non-retryable error', async () => {
111
+ const fn = mock().mockRejectedValue(new AIError('Bad request', 400, false));
112
+
113
+ await expect(withRetry(fn, { maxRetries: 3 })).rejects.toThrow('Bad request');
114
+ expect(fn).toHaveBeenCalledTimes(1);
115
+ });
116
+
117
+ test('throws after max retries', async () => {
118
+ const fn = mock().mockRejectedValue(new AIError('Rate limited', 429, true));
119
+
120
+ await expect(
121
+ withRetry(fn, { maxRetries: 2, baseDelayMs: 10 })
122
+ ).rejects.toThrow('Rate limited');
123
+
124
+ expect(fn).toHaveBeenCalledTimes(3); // Initial + 2 retries
125
+ });
126
+
127
+ test('retries on network errors', async () => {
128
+ const fn = mock()
129
+ .mockRejectedValueOnce(new Error('network error'))
130
+ .mockResolvedValue('success');
131
+
132
+ const result = await withRetry(fn, { maxRetries: 3, baseDelayMs: 10 });
133
+
134
+ expect(result).toBe('success');
135
+ expect(fn).toHaveBeenCalledTimes(2);
136
+ });
137
+ });