@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,319 @@
1
+ import { describe, it, expect } from 'bun:test';
2
+ import {
3
+ executeToolCalls,
4
+ executeWithTools,
5
+ requiresToolExecution,
6
+ } from '../tool-loop';
7
+ import type { AIToolCall, AIRequest, AIResponse } from '../types';
8
+ import type { VibeToolValue } from '../../tools/types';
9
+
10
+ // Test rootDir for tool context
11
+ const TEST_ROOT_DIR = process.cwd();
12
+
13
+ // Helper to create a simple tool
14
+ function createTool(name: string, executor: (args: Record<string, unknown>) => Promise<unknown>): VibeToolValue {
15
+ return {
16
+ __vibeTool: true,
17
+ name,
18
+ schema: {
19
+ name,
20
+ description: `Test tool: ${name}`,
21
+ parameters: [],
22
+ },
23
+ executor: async (args) => executor(args),
24
+ };
25
+ }
26
+
27
+ describe('executeToolCalls', () => {
28
+ it('should execute a single tool call successfully', async () => {
29
+ const tools = [
30
+ createTool('add', async (args) => (args.a as number) + (args.b as number)),
31
+ ];
32
+
33
+ const toolCalls: AIToolCall[] = [
34
+ { id: 'call_1', toolName: 'add', args: { a: 2, b: 3 } },
35
+ ];
36
+
37
+ const results = await executeToolCalls(toolCalls, tools, TEST_ROOT_DIR);
38
+
39
+ expect(results).toHaveLength(1);
40
+ expect(results[0]).toMatchObject({ toolCallId: 'call_1', result: 5 });
41
+ expect(typeof results[0].duration).toBe('number');
42
+ });
43
+
44
+ it('should execute multiple tool calls', async () => {
45
+ const tools = [
46
+ createTool('add', async (args) => (args.a as number) + (args.b as number)),
47
+ createTool('multiply', async (args) => (args.a as number) * (args.b as number)),
48
+ ];
49
+
50
+ const toolCalls: AIToolCall[] = [
51
+ { id: 'call_1', toolName: 'add', args: { a: 2, b: 3 } },
52
+ { id: 'call_2', toolName: 'multiply', args: { a: 4, b: 5 } },
53
+ ];
54
+
55
+ const results = await executeToolCalls(toolCalls, tools, TEST_ROOT_DIR);
56
+
57
+ expect(results).toHaveLength(2);
58
+ expect(results[0]).toMatchObject({ toolCallId: 'call_1', result: 5 });
59
+ expect(results[1]).toMatchObject({ toolCallId: 'call_2', result: 20 });
60
+ });
61
+
62
+ it('should return error for unknown tool', async () => {
63
+ const tools: VibeToolValue[] = [];
64
+
65
+ const toolCalls: AIToolCall[] = [
66
+ { id: 'call_1', toolName: 'unknown', args: {} },
67
+ ];
68
+
69
+ const results = await executeToolCalls(toolCalls, tools, TEST_ROOT_DIR);
70
+
71
+ expect(results).toHaveLength(1);
72
+ expect(results[0]).toMatchObject({
73
+ toolCallId: 'call_1',
74
+ error: "Tool 'unknown' not found",
75
+ });
76
+ expect(results[0].duration).toBe(0); // No execution time for unknown tool
77
+ });
78
+
79
+ it('should return error when tool throws', async () => {
80
+ const tools = [
81
+ createTool('failing', async () => {
82
+ throw new Error('Tool failed');
83
+ }),
84
+ ];
85
+
86
+ const toolCalls: AIToolCall[] = [
87
+ { id: 'call_1', toolName: 'failing', args: {} },
88
+ ];
89
+
90
+ const results = await executeToolCalls(toolCalls, tools, TEST_ROOT_DIR);
91
+
92
+ expect(results).toHaveLength(1);
93
+ expect(results[0]).toMatchObject({
94
+ toolCallId: 'call_1',
95
+ error: 'Tool failed',
96
+ });
97
+ expect(typeof results[0].duration).toBe('number');
98
+ });
99
+
100
+ it('should call onToolCall callback for each tool', async () => {
101
+ const tools = [
102
+ createTool('echo', async (args) => args.message),
103
+ ];
104
+
105
+ const toolCalls: AIToolCall[] = [
106
+ { id: 'call_1', toolName: 'echo', args: { message: 'hello' } },
107
+ ];
108
+
109
+ const callbacks: Array<{ call: AIToolCall; result: unknown; error?: string }> = [];
110
+
111
+ await executeToolCalls(toolCalls, tools, TEST_ROOT_DIR, (call, result, error) => {
112
+ callbacks.push({ call, result, error });
113
+ });
114
+
115
+ expect(callbacks).toHaveLength(1);
116
+ expect(callbacks[0].call).toEqual(toolCalls[0]);
117
+ expect(callbacks[0].result).toBe('hello');
118
+ expect(callbacks[0].error).toBeUndefined();
119
+ });
120
+ });
121
+
122
+ describe('executeWithTools', () => {
123
+ it('should return immediately if no tool calls', async () => {
124
+ const tools: VibeToolValue[] = [];
125
+
126
+ const request: AIRequest = {
127
+ operationType: 'do',
128
+ prompt: 'Hello',
129
+ contextText: '',
130
+ targetType: null,
131
+ model: { name: 'test', apiKey: 'key', url: null },
132
+ };
133
+
134
+ const mockResponse: AIResponse = {
135
+ content: 'Hello back!',
136
+ parsedValue: 'Hello back!',
137
+ };
138
+
139
+ const executeProvider = async () => mockResponse;
140
+
141
+ const { response, rounds } = await executeWithTools(
142
+ request,
143
+ tools,
144
+ TEST_ROOT_DIR,
145
+ executeProvider
146
+ );
147
+
148
+ expect(response).toEqual(mockResponse);
149
+ expect(rounds).toHaveLength(0);
150
+ });
151
+
152
+ it('should execute tool calls and make follow-up request', async () => {
153
+ const tools = [
154
+ createTool('getWeather', async () => ({ temp: 72, condition: 'sunny' })),
155
+ ];
156
+
157
+ const request: AIRequest = {
158
+ operationType: 'do',
159
+ prompt: "What's the weather?",
160
+ contextText: '',
161
+ targetType: null,
162
+ model: { name: 'test', apiKey: 'key', url: null },
163
+ };
164
+
165
+ // First response has tool call, second is final
166
+ let callCount = 0;
167
+ const executeProvider = async (): Promise<AIResponse> => {
168
+ callCount++;
169
+ if (callCount === 1) {
170
+ return {
171
+ content: '',
172
+ parsedValue: '',
173
+ toolCalls: [
174
+ { id: 'call_1', toolName: 'getWeather', args: {} },
175
+ ],
176
+ stopReason: 'tool_use',
177
+ };
178
+ }
179
+ return {
180
+ content: "It's 72°F and sunny!",
181
+ parsedValue: "It's 72°F and sunny!",
182
+ stopReason: 'end',
183
+ };
184
+ };
185
+
186
+ const { response, rounds } = await executeWithTools(
187
+ request,
188
+ tools,
189
+ TEST_ROOT_DIR,
190
+ executeProvider
191
+ );
192
+
193
+ expect(callCount).toBe(2);
194
+ expect(response.content).toBe("It's 72°F and sunny!");
195
+ expect(rounds).toHaveLength(1);
196
+ expect(rounds[0].toolCalls).toHaveLength(1);
197
+ expect(rounds[0].toolCalls[0].toolName).toBe('getWeather');
198
+ expect(rounds[0].results).toHaveLength(1);
199
+ expect(rounds[0].results[0].result).toEqual({ temp: 72, condition: 'sunny' });
200
+ });
201
+
202
+ it('should handle multiple rounds of tool calls', async () => {
203
+ const tools = [
204
+ createTool('step1', async () => 'result1'),
205
+ createTool('step2', async () => 'result2'),
206
+ ];
207
+
208
+ const request: AIRequest = {
209
+ operationType: 'do',
210
+ prompt: 'Do two steps',
211
+ contextText: '',
212
+ targetType: null,
213
+ model: { name: 'test', apiKey: 'key', url: null },
214
+ };
215
+
216
+ let callCount = 0;
217
+ const executeProvider = async (): Promise<AIResponse> => {
218
+ callCount++;
219
+ if (callCount === 1) {
220
+ return {
221
+ content: '',
222
+ parsedValue: '',
223
+ toolCalls: [{ id: 'call_1', toolName: 'step1', args: {} }],
224
+ stopReason: 'tool_use',
225
+ };
226
+ }
227
+ if (callCount === 2) {
228
+ return {
229
+ content: '',
230
+ parsedValue: '',
231
+ toolCalls: [{ id: 'call_2', toolName: 'step2', args: {} }],
232
+ stopReason: 'tool_use',
233
+ };
234
+ }
235
+ return {
236
+ content: 'Done!',
237
+ parsedValue: 'Done!',
238
+ stopReason: 'end',
239
+ };
240
+ };
241
+
242
+ const { response, rounds } = await executeWithTools(
243
+ request,
244
+ tools,
245
+ TEST_ROOT_DIR,
246
+ executeProvider
247
+ );
248
+
249
+ expect(callCount).toBe(3);
250
+ expect(response.content).toBe('Done!');
251
+ expect(rounds).toHaveLength(2);
252
+ expect(rounds[0].toolCalls[0].toolName).toBe('step1');
253
+ expect(rounds[1].toolCalls[0].toolName).toBe('step2');
254
+ });
255
+
256
+ it('should respect maxRounds limit', async () => {
257
+ const tools = [
258
+ createTool('infinite', async () => 'loop'),
259
+ ];
260
+
261
+ const request: AIRequest = {
262
+ operationType: 'do',
263
+ prompt: 'Loop forever',
264
+ contextText: '',
265
+ targetType: null,
266
+ model: { name: 'test', apiKey: 'key', url: null },
267
+ };
268
+
269
+ // Always returns tool call
270
+ const executeProvider = async (): Promise<AIResponse> => ({
271
+ content: '',
272
+ parsedValue: '',
273
+ toolCalls: [{ id: 'call_1', toolName: 'infinite', args: {} }],
274
+ stopReason: 'tool_use',
275
+ });
276
+
277
+ const { rounds } = await executeWithTools(
278
+ request,
279
+ tools,
280
+ TEST_ROOT_DIR,
281
+ executeProvider,
282
+ { maxRounds: 3 }
283
+ );
284
+
285
+ // Should stop at maxRounds
286
+ expect(rounds).toHaveLength(3);
287
+ });
288
+ });
289
+
290
+ describe('requiresToolExecution', () => {
291
+ it('should return true when response has tool calls', () => {
292
+ const response: AIResponse = {
293
+ content: '',
294
+ parsedValue: '',
295
+ toolCalls: [{ id: 'call_1', toolName: 'test', args: {} }],
296
+ };
297
+
298
+ expect(requiresToolExecution(response)).toBe(true);
299
+ });
300
+
301
+ it('should return false when response has no tool calls', () => {
302
+ const response: AIResponse = {
303
+ content: 'Hello',
304
+ parsedValue: 'Hello',
305
+ };
306
+
307
+ expect(requiresToolExecution(response)).toBe(false);
308
+ });
309
+
310
+ it('should return false when toolCalls is empty', () => {
311
+ const response: AIResponse = {
312
+ content: 'Hello',
313
+ parsedValue: 'Hello',
314
+ toolCalls: [],
315
+ };
316
+
317
+ expect(requiresToolExecution(response)).toBe(false);
318
+ });
319
+ });
@@ -0,0 +1,241 @@
1
+ import { describe, it, expect } from 'bun:test';
2
+ import {
3
+ toOpenAITool,
4
+ toOpenAITools,
5
+ toAnthropicTool,
6
+ toAnthropicTools,
7
+ toGoogleFunctionDeclaration,
8
+ toGoogleFunctionDeclarations,
9
+ } from '../tool-schema';
10
+ import type { ToolSchema } from '../../tools/types';
11
+
12
+ describe('OpenAI Tool Schema Conversion', () => {
13
+ it('should convert a simple tool schema', () => {
14
+ const schema: ToolSchema = {
15
+ name: 'get_weather',
16
+ description: 'Get weather for a location',
17
+ parameters: [
18
+ { name: 'location', type: { type: 'string' }, description: 'City name', required: true },
19
+ ],
20
+ };
21
+
22
+ const result = toOpenAITool(schema);
23
+
24
+ expect(result).toEqual({
25
+ type: 'function',
26
+ function: {
27
+ name: 'get_weather',
28
+ description: 'Get weather for a location',
29
+ parameters: {
30
+ type: 'object',
31
+ properties: {
32
+ location: {
33
+ type: 'string',
34
+ description: 'City name',
35
+ },
36
+ },
37
+ required: ['location'],
38
+ },
39
+ },
40
+ });
41
+ });
42
+
43
+ it('should convert multiple parameter types', () => {
44
+ const schema: ToolSchema = {
45
+ name: 'search',
46
+ parameters: [
47
+ { name: 'query', type: { type: 'string' }, required: true },
48
+ { name: 'limit', type: { type: 'number' }, required: false },
49
+ { name: 'exact', type: { type: 'boolean' }, required: false },
50
+ { name: 'filters', type: { type: 'object' }, required: false },
51
+ ],
52
+ };
53
+
54
+ const result = toOpenAITool(schema);
55
+
56
+ expect(result.function.parameters.properties).toEqual({
57
+ query: { type: 'string' },
58
+ limit: { type: 'number' },
59
+ exact: { type: 'boolean' },
60
+ filters: { type: 'object' },
61
+ });
62
+ expect(result.function.parameters.required).toEqual(['query']);
63
+ });
64
+
65
+ it('should convert multiple tools', () => {
66
+ const schemas: ToolSchema[] = [
67
+ { name: 'tool1', parameters: [] },
68
+ { name: 'tool2', parameters: [] },
69
+ ];
70
+
71
+ const result = toOpenAITools(schemas);
72
+
73
+ expect(result).toHaveLength(2);
74
+ expect(result[0].function.name).toBe('tool1');
75
+ expect(result[1].function.name).toBe('tool2');
76
+ });
77
+ });
78
+
79
+ describe('Anthropic Tool Schema Conversion', () => {
80
+ it('should convert a simple tool schema', () => {
81
+ const schema: ToolSchema = {
82
+ name: 'get_weather',
83
+ description: 'Get weather for a location',
84
+ parameters: [
85
+ { name: 'location', type: { type: 'string' }, description: 'City name', required: true },
86
+ ],
87
+ };
88
+
89
+ const result = toAnthropicTool(schema);
90
+
91
+ expect(result).toEqual({
92
+ name: 'get_weather',
93
+ description: 'Get weather for a location',
94
+ input_schema: {
95
+ type: 'object',
96
+ properties: {
97
+ location: {
98
+ type: 'string',
99
+ description: 'City name',
100
+ },
101
+ },
102
+ required: ['location'],
103
+ },
104
+ });
105
+ });
106
+
107
+ it('should convert multiple parameter types', () => {
108
+ const schema: ToolSchema = {
109
+ name: 'search',
110
+ parameters: [
111
+ { name: 'query', type: { type: 'string' }, required: true },
112
+ { name: 'limit', type: { type: 'number' }, required: false },
113
+ { name: 'exact', type: { type: 'boolean' }, required: false },
114
+ { name: 'filters', type: { type: 'object' }, required: false },
115
+ ],
116
+ };
117
+
118
+ const result = toAnthropicTool(schema);
119
+
120
+ expect(result.input_schema.properties).toEqual({
121
+ query: { type: 'string' },
122
+ limit: { type: 'number' },
123
+ exact: { type: 'boolean' },
124
+ filters: { type: 'object' },
125
+ });
126
+ expect(result.input_schema.required).toEqual(['query']);
127
+ });
128
+
129
+ it('should convert multiple tools', () => {
130
+ const schemas: ToolSchema[] = [
131
+ { name: 'tool1', parameters: [] },
132
+ { name: 'tool2', parameters: [] },
133
+ ];
134
+
135
+ const result = toAnthropicTools(schemas);
136
+
137
+ expect(result).toHaveLength(2);
138
+ expect(result[0].name).toBe('tool1');
139
+ expect(result[1].name).toBe('tool2');
140
+ });
141
+ });
142
+
143
+ describe('Google Tool Schema Conversion', () => {
144
+ it('should convert a simple tool schema', () => {
145
+ const schema: ToolSchema = {
146
+ name: 'get_weather',
147
+ description: 'Get weather for a location',
148
+ parameters: [
149
+ { name: 'location', type: { type: 'string' }, description: 'City name', required: true },
150
+ ],
151
+ };
152
+
153
+ const result = toGoogleFunctionDeclaration(schema);
154
+
155
+ expect(result).toEqual({
156
+ name: 'get_weather',
157
+ description: 'Get weather for a location',
158
+ parameters: {
159
+ type: 'object',
160
+ properties: {
161
+ location: {
162
+ type: 'string',
163
+ description: 'City name',
164
+ },
165
+ },
166
+ required: ['location'],
167
+ },
168
+ });
169
+ });
170
+
171
+ it('should convert multiple parameter types', () => {
172
+ const schema: ToolSchema = {
173
+ name: 'search',
174
+ parameters: [
175
+ { name: 'query', type: { type: 'string' }, required: true },
176
+ { name: 'limit', type: { type: 'number' }, required: false },
177
+ { name: 'exact', type: { type: 'boolean' }, required: false },
178
+ { name: 'filters', type: { type: 'object' }, required: false },
179
+ ],
180
+ };
181
+
182
+ const result = toGoogleFunctionDeclaration(schema);
183
+
184
+ expect(result.parameters.properties).toEqual({
185
+ query: { type: 'string' },
186
+ limit: { type: 'number' },
187
+ exact: { type: 'boolean' },
188
+ filters: { type: 'object' },
189
+ });
190
+ expect(result.parameters.required).toEqual(['query']);
191
+ });
192
+
193
+ it('should convert multiple tools', () => {
194
+ const schemas: ToolSchema[] = [
195
+ { name: 'tool1', parameters: [] },
196
+ { name: 'tool2', parameters: [] },
197
+ ];
198
+
199
+ const result = toGoogleFunctionDeclarations(schemas);
200
+
201
+ expect(result).toHaveLength(2);
202
+ expect(result[0].name).toBe('tool1');
203
+ expect(result[1].name).toBe('tool2');
204
+ });
205
+ });
206
+
207
+ describe('Parameter Type Mapping', () => {
208
+ it('should preserve all JSON Schema types', () => {
209
+ const schema: ToolSchema = {
210
+ name: 'test',
211
+ parameters: [
212
+ { name: 'text_param', type: { type: 'string' }, required: true },
213
+ { name: 'number_param', type: { type: 'number' }, required: true },
214
+ { name: 'boolean_param', type: { type: 'boolean' }, required: true },
215
+ { name: 'json_param', type: { type: 'object' }, required: true },
216
+ { name: 'text_array', type: { type: 'array', items: { type: 'string' } }, required: true },
217
+ { name: 'number_array', type: { type: 'array', items: { type: 'number' } }, required: true },
218
+ { name: 'json_array', type: { type: 'array', items: { type: 'object' } }, required: true },
219
+ ],
220
+ };
221
+
222
+ const result = toOpenAITool(schema);
223
+
224
+ expect(result.function.parameters.properties.text_param).toEqual({ type: 'string' });
225
+ expect(result.function.parameters.properties.number_param).toEqual({ type: 'number' });
226
+ expect(result.function.parameters.properties.boolean_param).toEqual({ type: 'boolean' });
227
+ expect(result.function.parameters.properties.json_param).toEqual({ type: 'object' });
228
+ expect(result.function.parameters.properties.text_array).toEqual({
229
+ type: 'array',
230
+ items: { type: 'string' },
231
+ });
232
+ expect(result.function.parameters.properties.number_array).toEqual({
233
+ type: 'array',
234
+ items: { type: 'number' },
235
+ });
236
+ expect(result.function.parameters.properties.json_array).toEqual({
237
+ type: 'array',
238
+ items: { type: 'object' },
239
+ });
240
+ });
241
+ });