@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,859 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { parse } from '../../parser/parse';
3
+ import { Runtime, type AIProvider, type AIExecutionResult } from '../index';
4
+ import { executeWithTools, type ToolRoundResult } from '../ai/tool-loop';
5
+ import type { AIRequest, AIResponse } from '../ai/types';
6
+ import type { VibeToolValue, ToolSchema } from '../tools/types';
7
+ import { formatContextForAI, buildLocalContext } from '../context';
8
+
9
+ /**
10
+ * Tool calling flow tests with mocked AI responses.
11
+ *
12
+ * These tests verify the tool calling flow works correctly when an AI
13
+ * response includes tool calls. The AI provider is mocked, but real tools
14
+ * are registered and executed.
15
+ *
16
+ * The flow being tested:
17
+ * 1. `do` command is executed
18
+ * 2. Mocked AI provider returns tool calls
19
+ * 3. Registered tools are actually executed
20
+ * 4. Tool results are passed back to AI (mocked followup)
21
+ * 5. Final response is returned to variable
22
+ * 6. Context shows tool call history
23
+ *
24
+ * For tests that call real AI APIs, see tests/integration/
25
+ */
26
+
27
+ // Track tool executions for verification
28
+ interface ToolExecution {
29
+ name: string;
30
+ args: Record<string, unknown>;
31
+ result: unknown;
32
+ }
33
+
34
+ /**
35
+ * Create an AI provider that simulates tool calling by using executeWithTools
36
+ * with a controllable mock backend.
37
+ */
38
+ function createToolCallingAIProvider(
39
+ mockResponses: AIResponse[],
40
+ toolExecutions: ToolExecution[],
41
+ testTools: VibeToolValue[]
42
+ ): AIProvider {
43
+ let callIndex = 0;
44
+
45
+ // Wrap tool executors to track executions
46
+ const trackedTools: VibeToolValue[] = testTools.map(tool => ({
47
+ ...tool,
48
+ executor: async (args: Record<string, unknown>) => {
49
+ const result = await tool.executor(args, { rootDir: process.cwd() });
50
+ toolExecutions.push({ name: tool.name, args, result });
51
+ return result;
52
+ },
53
+ }));
54
+
55
+ return {
56
+ async execute(prompt: string): Promise<AIExecutionResult> {
57
+ // Mock provider that returns responses in sequence
58
+ const mockProviderExecutor = async (request: AIRequest): Promise<AIResponse> => {
59
+ const response = mockResponses[callIndex] ?? mockResponses[mockResponses.length - 1];
60
+ callIndex++;
61
+ return response;
62
+ };
63
+
64
+ // Use executeWithTools to actually execute tools
65
+ const { response, rounds } = await executeWithTools(
66
+ {
67
+ prompt,
68
+ model: { name: 'mock', apiKey: 'mock', url: null },
69
+ operationType: 'do',
70
+ contextText: '',
71
+ targetType: null,
72
+ },
73
+ trackedTools,
74
+ process.cwd(), // Test rootDir for path sandboxing
75
+ mockProviderExecutor,
76
+ { maxRounds: 10 }
77
+ );
78
+
79
+ return {
80
+ value: response.parsedValue ?? response.content,
81
+ usage: response.usage,
82
+ toolRounds: rounds.length > 0 ? rounds : undefined,
83
+ };
84
+ },
85
+
86
+ async generateCode(prompt: string): Promise<AIExecutionResult> {
87
+ return { value: `// Generated for: ${prompt}` };
88
+ },
89
+
90
+ async askUser(prompt: string): Promise<string> {
91
+ throw new Error('User input not implemented in test');
92
+ },
93
+ };
94
+ }
95
+
96
+ // Simple test tools that don't have side effects
97
+ function createTestTools(): VibeToolValue[] {
98
+ return [
99
+ {
100
+ __vibeTool: true,
101
+ name: 'add',
102
+ schema: {
103
+ name: 'add',
104
+ description: 'Add two numbers',
105
+ parameters: [
106
+ { name: 'a', type: { type: 'number' }, required: true },
107
+ { name: 'b', type: { type: 'number' }, required: true },
108
+ ],
109
+ },
110
+ executor: async (args) => (args.a as number) + (args.b as number),
111
+ },
112
+ {
113
+ __vibeTool: true,
114
+ name: 'multiply',
115
+ schema: {
116
+ name: 'multiply',
117
+ description: 'Multiply two numbers',
118
+ parameters: [
119
+ { name: 'a', type: { type: 'number' }, required: true },
120
+ { name: 'b', type: { type: 'number' }, required: true },
121
+ ],
122
+ },
123
+ executor: async (args) => (args.a as number) * (args.b as number),
124
+ },
125
+ {
126
+ __vibeTool: true,
127
+ name: 'getWeather',
128
+ schema: {
129
+ name: 'getWeather',
130
+ description: 'Get weather for a city',
131
+ parameters: [
132
+ { name: 'city', type: { type: 'string' }, required: true },
133
+ ],
134
+ },
135
+ executor: async (args) => {
136
+ const city = args.city as string;
137
+ // Return mock weather data based on city
138
+ const weatherData: Record<string, { temp: number; condition: string }> = {
139
+ 'Seattle': { temp: 55, condition: 'rainy' },
140
+ 'San Francisco': { temp: 68, condition: 'sunny' },
141
+ 'New York': { temp: 45, condition: 'cloudy' },
142
+ };
143
+ return weatherData[city] ?? { temp: 70, condition: 'unknown' };
144
+ },
145
+ },
146
+ ];
147
+ }
148
+
149
+ describe('AI Tool Calling Flow', () => {
150
+ test('single tool call is executed and result returned to AI', async () => {
151
+ const ast = parse(`
152
+ model m = { name: "test", apiKey: "key", url: "http://test" }
153
+ let result: text = vibe "Calculate 5 + 3" m default
154
+ `);
155
+
156
+ const toolExecutions: ToolExecution[] = [];
157
+ const testTools = createTestTools();
158
+
159
+ // Mock responses: first returns tool call, second returns final answer
160
+ const mockResponses: AIResponse[] = [
161
+ {
162
+ content: '',
163
+ parsedValue: '',
164
+ toolCalls: [
165
+ { id: 'call_1', toolName: 'add', args: { a: 5, b: 3 } },
166
+ ],
167
+ stopReason: 'tool_use',
168
+ },
169
+ {
170
+ content: 'The result of 5 + 3 is 8',
171
+ parsedValue: 'The result of 5 + 3 is 8',
172
+ stopReason: 'end',
173
+ },
174
+ ];
175
+
176
+ const aiProvider = createToolCallingAIProvider(mockResponses, toolExecutions, testTools);
177
+ const runtime = new Runtime(ast, aiProvider);
178
+ const result = await runtime.run();
179
+
180
+ // Verify tool was actually executed
181
+ expect(toolExecutions).toHaveLength(1);
182
+ expect(toolExecutions[0]).toEqual({
183
+ name: 'add',
184
+ args: { a: 5, b: 3 },
185
+ result: 8,
186
+ });
187
+
188
+ // Verify final result
189
+ expect(runtime.getValue('result')).toBe('The result of 5 + 3 is 8');
190
+ });
191
+
192
+ test('multiple tool calls in single response are all executed', async () => {
193
+ const ast = parse(`
194
+ model m = { name: "test", apiKey: "key", url: "http://test" }
195
+ let weather: text = vibe "What's the weather in Seattle and SF?" m default
196
+ `);
197
+
198
+ const toolExecutions: ToolExecution[] = [];
199
+ const testTools = createTestTools();
200
+
201
+ // Mock responses: first returns two tool calls, second returns final answer
202
+ const mockResponses: AIResponse[] = [
203
+ {
204
+ content: '',
205
+ parsedValue: '',
206
+ toolCalls: [
207
+ { id: 'call_1', toolName: 'getWeather', args: { city: 'Seattle' } },
208
+ { id: 'call_2', toolName: 'getWeather', args: { city: 'San Francisco' } },
209
+ ],
210
+ stopReason: 'tool_use',
211
+ },
212
+ {
213
+ content: 'Seattle is 55°F and rainy, San Francisco is 68°F and sunny.',
214
+ parsedValue: 'Seattle is 55°F and rainy, San Francisco is 68°F and sunny.',
215
+ stopReason: 'end',
216
+ },
217
+ ];
218
+
219
+ const aiProvider = createToolCallingAIProvider(mockResponses, toolExecutions, testTools);
220
+ const runtime = new Runtime(ast, aiProvider);
221
+ await runtime.run();
222
+
223
+ // Verify both tools were executed
224
+ expect(toolExecutions).toHaveLength(2);
225
+ expect(toolExecutions[0]).toEqual({
226
+ name: 'getWeather',
227
+ args: { city: 'Seattle' },
228
+ result: { temp: 55, condition: 'rainy' },
229
+ });
230
+ expect(toolExecutions[1]).toEqual({
231
+ name: 'getWeather',
232
+ args: { city: 'San Francisco' },
233
+ result: { temp: 68, condition: 'sunny' },
234
+ });
235
+
236
+ // Verify final result
237
+ expect(runtime.getValue('weather')).toBe('Seattle is 55°F and rainy, San Francisco is 68°F and sunny.');
238
+ });
239
+
240
+ test('multiple rounds of tool calls are executed sequentially', async () => {
241
+ const ast = parse(`
242
+ model m = { name: "test", apiKey: "key", url: "http://test" }
243
+ let result: text = vibe "Calculate (2+3) * 4" m default
244
+ `);
245
+
246
+ const toolExecutions: ToolExecution[] = [];
247
+ const testTools = createTestTools();
248
+
249
+ // Mock responses: first round adds, second round multiplies, third is final
250
+ const mockResponses: AIResponse[] = [
251
+ {
252
+ content: '',
253
+ parsedValue: '',
254
+ toolCalls: [
255
+ { id: 'call_1', toolName: 'add', args: { a: 2, b: 3 } },
256
+ ],
257
+ stopReason: 'tool_use',
258
+ },
259
+ {
260
+ content: '',
261
+ parsedValue: '',
262
+ toolCalls: [
263
+ { id: 'call_2', toolName: 'multiply', args: { a: 5, b: 4 } },
264
+ ],
265
+ stopReason: 'tool_use',
266
+ },
267
+ {
268
+ content: 'The result of (2+3) * 4 is 20',
269
+ parsedValue: 'The result of (2+3) * 4 is 20',
270
+ stopReason: 'end',
271
+ },
272
+ ];
273
+
274
+ const aiProvider = createToolCallingAIProvider(mockResponses, toolExecutions, testTools);
275
+ const runtime = new Runtime(ast, aiProvider);
276
+ await runtime.run();
277
+
278
+ // Verify tools were executed in order
279
+ expect(toolExecutions).toHaveLength(2);
280
+ expect(toolExecutions[0]).toEqual({
281
+ name: 'add',
282
+ args: { a: 2, b: 3 },
283
+ result: 5,
284
+ });
285
+ expect(toolExecutions[1]).toEqual({
286
+ name: 'multiply',
287
+ args: { a: 5, b: 4 },
288
+ result: 20,
289
+ });
290
+
291
+ // Verify final result
292
+ expect(runtime.getValue('result')).toBe('The result of (2+3) * 4 is 20');
293
+
294
+ // Verify formatted context shows all tool calls in order
295
+ const state = runtime.getState();
296
+ const context = buildLocalContext(state);
297
+ const formatted = formatContextForAI(context, { includeInstructions: false });
298
+
299
+ expect(formatted.text).toBe(
300
+ ` <entry> (current scope)
301
+ --> vibe: "Calculate (2+3) * 4"
302
+ [tool] add({"a":2,"b":3})
303
+ [result] 5
304
+ [tool] multiply({"a":5,"b":4})
305
+ [result] 20
306
+ <-- result (text): The result of (2+3) * 4 is 20`
307
+ );
308
+ });
309
+
310
+ test('tool calls appear in context for subsequent AI calls', async () => {
311
+ const ast = parse(`
312
+ model m = { name: "test", apiKey: "key", url: "http://test" }
313
+ let first: text = vibe "What's 2 + 2?" m default
314
+ let second: text = vibe "What was the previous result?" m default
315
+ `);
316
+
317
+ const toolExecutions: ToolExecution[] = [];
318
+ const testTools = createTestTools();
319
+
320
+ // We need to track if context includes tool calls from first do
321
+ let secondCallContext = '';
322
+
323
+ const mockResponses: AIResponse[] = [
324
+ // First do call - returns tool call
325
+ {
326
+ content: '',
327
+ parsedValue: '',
328
+ toolCalls: [
329
+ { id: 'call_1', toolName: 'add', args: { a: 2, b: 2 } },
330
+ ],
331
+ stopReason: 'tool_use',
332
+ },
333
+ // First do call - final response
334
+ {
335
+ content: 'The result is 4',
336
+ parsedValue: 'The result is 4',
337
+ stopReason: 'end',
338
+ },
339
+ // Second do call - no tool calls
340
+ {
341
+ content: 'The previous result was 4',
342
+ parsedValue: 'The previous result was 4',
343
+ stopReason: 'end',
344
+ },
345
+ ];
346
+
347
+ const aiProvider = createToolCallingAIProvider(mockResponses, toolExecutions, testTools);
348
+ const runtime = new Runtime(ast, aiProvider);
349
+ await runtime.run();
350
+
351
+ // Verify tool was executed
352
+ expect(toolExecutions).toHaveLength(1);
353
+ expect(toolExecutions[0].result).toBe(4);
354
+
355
+ // Verify both results assigned correctly
356
+ expect(runtime.getValue('first')).toBe('The result is 4');
357
+ expect(runtime.getValue('second')).toBe('The previous result was 4');
358
+
359
+ // Get state and check that tool calls are embedded in prompt entries
360
+ const state = runtime.getState();
361
+ const frame = state.callStack[state.callStack.length - 1];
362
+
363
+ // Find prompt entries that have toolCalls
364
+ const promptEntries = frame.orderedEntries.filter(e => e.kind === 'prompt');
365
+ expect(promptEntries).toHaveLength(2); // first and second do calls
366
+
367
+ // First prompt should have the tool call embedded
368
+ const firstPrompt = promptEntries[0];
369
+ expect(firstPrompt.kind).toBe('prompt');
370
+ if (firstPrompt.kind === 'prompt') {
371
+ expect(firstPrompt.toolCalls).toHaveLength(1);
372
+ expect(firstPrompt.toolCalls![0]).toEqual({
373
+ toolName: 'add',
374
+ args: { a: 2, b: 2 },
375
+ result: 4,
376
+ });
377
+ }
378
+ });
379
+
380
+ test('AI call with no tool calls works normally', async () => {
381
+ const ast = parse(`
382
+ model m = { name: "test", apiKey: "key", url: "http://test" }
383
+ let greeting: text = vibe "Say hello" m default
384
+ `);
385
+
386
+ const toolExecutions: ToolExecution[] = [];
387
+ const testTools = createTestTools();
388
+
389
+ // Mock response with no tool calls
390
+ const mockResponses: AIResponse[] = [
391
+ {
392
+ content: 'Hello, world!',
393
+ parsedValue: 'Hello, world!',
394
+ stopReason: 'end',
395
+ },
396
+ ];
397
+
398
+ const aiProvider = createToolCallingAIProvider(mockResponses, toolExecutions, testTools);
399
+ const runtime = new Runtime(ast, aiProvider);
400
+ await runtime.run();
401
+
402
+ // No tools should be executed
403
+ expect(toolExecutions).toHaveLength(0);
404
+
405
+ // Verify result
406
+ expect(runtime.getValue('greeting')).toBe('Hello, world!');
407
+ });
408
+
409
+ test('tool call errors are captured and passed to AI', async () => {
410
+ const ast = parse(`
411
+ model m = { name: "test", apiKey: "key", url: "http://test" }
412
+ let result: text = vibe "Try to do something that fails" m default
413
+ `);
414
+
415
+ const toolExecutions: ToolExecution[] = [];
416
+
417
+ // Create a tool that throws an error
418
+ const testTools: VibeToolValue[] = [
419
+ {
420
+ __vibeTool: true,
421
+ name: 'failingTool',
422
+ schema: {
423
+ name: 'failingTool',
424
+ description: 'A tool that always fails',
425
+ parameters: [],
426
+ },
427
+ executor: async () => {
428
+ throw new Error('Tool execution failed!');
429
+ },
430
+ },
431
+ ];
432
+
433
+ // Mock responses: first returns tool call, AI handles error, then final response
434
+ const mockResponses: AIResponse[] = [
435
+ {
436
+ content: '',
437
+ parsedValue: '',
438
+ toolCalls: [
439
+ { id: 'call_1', toolName: 'failingTool', args: {} },
440
+ ],
441
+ stopReason: 'tool_use',
442
+ },
443
+ {
444
+ content: 'I tried to use a tool but it failed. Let me handle that gracefully.',
445
+ parsedValue: 'I tried to use a tool but it failed. Let me handle that gracefully.',
446
+ stopReason: 'end',
447
+ },
448
+ ];
449
+
450
+ const aiProvider = createToolCallingAIProvider(mockResponses, toolExecutions, testTools);
451
+ const runtime = new Runtime(ast, aiProvider);
452
+ await runtime.run();
453
+
454
+ // Tool execution was attempted (even though it failed)
455
+ // Note: Our tracking wrapper won't capture failed executions, but the flow should complete
456
+ expect(runtime.getValue('result')).toBe('I tried to use a tool but it failed. Let me handle that gracefully.');
457
+ });
458
+ });
459
+
460
+ describe('AI Tool Calling - Do Command (Single Round)', () => {
461
+ test('do command with tool calls shows tool history in context for second do call', async () => {
462
+ // This test verifies that when using `do`:
463
+ // 1. First `do` call triggers tool execution (single round)
464
+ // 2. Second `do` call can see the tool calls and results from the first `do` in its context
465
+ const ast = parse(`
466
+ model m = { name: "test", apiKey: "key", url: "http://test" }
467
+ let first: text = do "Calculate 5 + 3 and 2 * 4" m default
468
+ let second: text = do "What were the previous calculations?" m default
469
+ `);
470
+
471
+ const toolExecutions: ToolExecution[] = [];
472
+ const testTools = createTestTools();
473
+
474
+ // Mock responses:
475
+ // First do call - returns two tool calls, then final response (single round for do)
476
+ // Second do call - just returns a response (no tools)
477
+ const mockResponses: AIResponse[] = [
478
+ // First do: AI requests two tool calls
479
+ {
480
+ content: '',
481
+ parsedValue: '',
482
+ toolCalls: [
483
+ { id: 'call_1', toolName: 'add', args: { a: 5, b: 3 } },
484
+ { id: 'call_2', toolName: 'multiply', args: { a: 2, b: 4 } },
485
+ ],
486
+ stopReason: 'tool_use',
487
+ },
488
+ // First do: Final response after tools execute
489
+ {
490
+ content: '5 + 3 = 8 and 2 * 4 = 8',
491
+ parsedValue: '5 + 3 = 8 and 2 * 4 = 8',
492
+ stopReason: 'end',
493
+ },
494
+ // Second do: Response referencing previous context
495
+ {
496
+ content: 'Previously: add(5,3)=8 and multiply(2,4)=8',
497
+ parsedValue: 'Previously: add(5,3)=8 and multiply(2,4)=8',
498
+ stopReason: 'end',
499
+ },
500
+ ];
501
+
502
+ const aiProvider = createToolCallingAIProvider(mockResponses, toolExecutions, testTools);
503
+ const runtime = new Runtime(ast, aiProvider);
504
+ await runtime.run();
505
+
506
+ // Verify both tools were executed in the first do call
507
+ expect(toolExecutions).toHaveLength(2);
508
+ expect(toolExecutions[0]).toEqual({
509
+ name: 'add',
510
+ args: { a: 5, b: 3 },
511
+ result: 8,
512
+ });
513
+ expect(toolExecutions[1]).toEqual({
514
+ name: 'multiply',
515
+ args: { a: 2, b: 4 },
516
+ result: 8,
517
+ });
518
+
519
+ // Verify both results assigned correctly
520
+ expect(runtime.getValue('first')).toBe('5 + 3 = 8 and 2 * 4 = 8');
521
+ expect(runtime.getValue('second')).toBe('Previously: add(5,3)=8 and multiply(2,4)=8');
522
+
523
+ // Get state and verify context structure
524
+ const state = runtime.getState();
525
+ const frame = state.callStack[state.callStack.length - 1];
526
+
527
+ // Find prompt entries
528
+ const promptEntries = frame.orderedEntries.filter(e => e.kind === 'prompt');
529
+ expect(promptEntries).toHaveLength(2); // first and second do calls
530
+
531
+ // First prompt should have the two tool calls embedded
532
+ const firstPrompt = promptEntries[0];
533
+ expect(firstPrompt.kind).toBe('prompt');
534
+ if (firstPrompt.kind === 'prompt') {
535
+ expect(firstPrompt.aiType).toBe('do');
536
+ expect(firstPrompt.toolCalls).toHaveLength(2);
537
+ expect(firstPrompt.toolCalls![0]).toEqual({
538
+ toolName: 'add',
539
+ args: { a: 5, b: 3 },
540
+ result: 8,
541
+ });
542
+ expect(firstPrompt.toolCalls![1]).toEqual({
543
+ toolName: 'multiply',
544
+ args: { a: 2, b: 4 },
545
+ result: 8,
546
+ });
547
+ expect(firstPrompt.response).toBe('5 + 3 = 8 and 2 * 4 = 8');
548
+ }
549
+
550
+ // Second prompt should have no tool calls
551
+ const secondPrompt = promptEntries[1];
552
+ if (secondPrompt.kind === 'prompt') {
553
+ expect(secondPrompt.aiType).toBe('do');
554
+ expect(secondPrompt.toolCalls).toBeUndefined();
555
+ expect(secondPrompt.response).toBe('Previously: add(5,3)=8 and multiply(2,4)=8');
556
+ }
557
+
558
+ // Verify formatted context shows the complete history
559
+ const context = buildLocalContext(state);
560
+ const formatted = formatContextForAI(context, { includeInstructions: false });
561
+
562
+ expect(formatted.text).toBe(
563
+ ` <entry> (current scope)
564
+ --> do: "Calculate 5 + 3 and 2 * 4"
565
+ [tool] add({"a":5,"b":3})
566
+ [result] 8
567
+ [tool] multiply({"a":2,"b":4})
568
+ [result] 8
569
+ <-- first (text): 5 + 3 = 8 and 2 * 4 = 8
570
+ --> do: "What were the previous calculations?"
571
+ <-- second (text): Previously: add(5,3)=8 and multiply(2,4)=8`
572
+ );
573
+ });
574
+ });
575
+
576
+ describe('AI Tool Calling - Formatted Context Output', () => {
577
+ test('formatted context shows tool calls and results', async () => {
578
+ const ast = parse(`
579
+ model m = { name: "test", apiKey: "key", url: "http://test" }
580
+ let result: text = vibe "Calculate 5 + 3" m default
581
+ `);
582
+
583
+ const toolExecutions: ToolExecution[] = [];
584
+ const testTools = createTestTools();
585
+
586
+ const mockResponses: AIResponse[] = [
587
+ {
588
+ content: '',
589
+ parsedValue: '',
590
+ toolCalls: [
591
+ { id: 'call_1', toolName: 'add', args: { a: 5, b: 3 } },
592
+ ],
593
+ stopReason: 'tool_use',
594
+ },
595
+ {
596
+ content: 'The answer is 8',
597
+ parsedValue: 'The answer is 8',
598
+ stopReason: 'end',
599
+ },
600
+ ];
601
+
602
+ const aiProvider = createToolCallingAIProvider(mockResponses, toolExecutions, testTools);
603
+ const runtime = new Runtime(ast, aiProvider);
604
+ await runtime.run();
605
+
606
+ // Get the formatted context
607
+ const state = runtime.getState();
608
+ const context = buildLocalContext(state);
609
+ const formatted = formatContextForAI(context, { includeInstructions: false });
610
+
611
+ // Verify formatted output shows: AI call → tool calls → response (via variable)
612
+ expect(formatted.text).toBe(
613
+ ` <entry> (current scope)
614
+ --> vibe: "Calculate 5 + 3"
615
+ [tool] add({"a":5,"b":3})
616
+ [result] 8
617
+ <-- result (text): The answer is 8`
618
+ );
619
+ });
620
+
621
+ test('formatted context shows multiple tool calls in sequence', async () => {
622
+ const ast = parse(`
623
+ model m = { name: "test", apiKey: "key", url: "http://test" }
624
+ let weather: text = vibe "Weather in Seattle and NYC?" m default
625
+ `);
626
+
627
+ const toolExecutions: ToolExecution[] = [];
628
+ const testTools = createTestTools();
629
+
630
+ const mockResponses: AIResponse[] = [
631
+ {
632
+ content: '',
633
+ parsedValue: '',
634
+ toolCalls: [
635
+ { id: 'call_1', toolName: 'getWeather', args: { city: 'Seattle' } },
636
+ { id: 'call_2', toolName: 'getWeather', args: { city: 'New York' } },
637
+ ],
638
+ stopReason: 'tool_use',
639
+ },
640
+ {
641
+ content: 'Seattle: 55F rainy, NYC: 45F cloudy',
642
+ parsedValue: 'Seattle: 55F rainy, NYC: 45F cloudy',
643
+ stopReason: 'end',
644
+ },
645
+ ];
646
+
647
+ const aiProvider = createToolCallingAIProvider(mockResponses, toolExecutions, testTools);
648
+ const runtime = new Runtime(ast, aiProvider);
649
+ await runtime.run();
650
+
651
+ const state = runtime.getState();
652
+ const context = buildLocalContext(state);
653
+ const formatted = formatContextForAI(context, { includeInstructions: false });
654
+
655
+ expect(formatted.text).toBe(
656
+ ` <entry> (current scope)
657
+ --> vibe: "Weather in Seattle and NYC?"
658
+ [tool] getWeather({"city":"Seattle"})
659
+ [result] {"temp":55,"condition":"rainy"}
660
+ [tool] getWeather({"city":"New York"})
661
+ [result] {"temp":45,"condition":"cloudy"}
662
+ <-- weather (text): Seattle: 55F rainy, NYC: 45F cloudy`
663
+ );
664
+ });
665
+
666
+ test('formatted context shows tool call error', async () => {
667
+ const ast = parse(`
668
+ model m = { name: "test", apiKey: "key", url: "http://test" }
669
+ let result: text = vibe "Try the failing tool" m default
670
+ `);
671
+
672
+ const toolExecutions: ToolExecution[] = [];
673
+ const failingTools: VibeToolValue[] = [
674
+ {
675
+ __vibeTool: true,
676
+ name: 'riskyOperation',
677
+ schema: {
678
+ name: 'riskyOperation',
679
+ description: 'A risky operation',
680
+ parameters: [],
681
+ },
682
+ executor: async () => {
683
+ throw new Error('Operation failed: insufficient permissions');
684
+ },
685
+ },
686
+ ];
687
+
688
+ const mockResponses: AIResponse[] = [
689
+ {
690
+ content: '',
691
+ parsedValue: '',
692
+ toolCalls: [
693
+ { id: 'call_1', toolName: 'riskyOperation', args: {} },
694
+ ],
695
+ stopReason: 'tool_use',
696
+ },
697
+ {
698
+ content: 'The operation failed due to permissions',
699
+ parsedValue: 'The operation failed due to permissions',
700
+ stopReason: 'end',
701
+ },
702
+ ];
703
+
704
+ const aiProvider = createToolCallingAIProvider(mockResponses, toolExecutions, failingTools);
705
+ const runtime = new Runtime(ast, aiProvider);
706
+ await runtime.run();
707
+
708
+ const state = runtime.getState();
709
+ const context = buildLocalContext(state);
710
+ const formatted = formatContextForAI(context, { includeInstructions: false });
711
+
712
+ expect(formatted.text).toBe(
713
+ ` <entry> (current scope)
714
+ --> vibe: "Try the failing tool"
715
+ [tool] riskyOperation({})
716
+ [error] Operation failed: insufficient permissions
717
+ <-- result (text): The operation failed due to permissions`
718
+ );
719
+ });
720
+ });
721
+
722
+ describe('AI Tool Calling - Context Modes (forget/verbose)', () => {
723
+ test('tool calls inside loop with forget mode are removed from context', async () => {
724
+ // Tool calls inside a loop with 'forget' should not appear in context after loop exits
725
+ const ast = parse(`
726
+ model m = { name: "test", apiKey: "key", url: "http://test" }
727
+ let sum = 0
728
+ for i in [1, 2] {
729
+ let partial: number = vibe "Add {i} to running total" m default
730
+ sum = sum + partial
731
+ } forget
732
+ let final: text = vibe "What is the final sum?" m default
733
+ `);
734
+
735
+ const toolExecutions: ToolExecution[] = [];
736
+ const testTools = createTestTools();
737
+
738
+ // Mock responses for: loop iter 1 (with tool), loop iter 2 (with tool), final do
739
+ const mockResponses: AIResponse[] = [
740
+ // First iteration - AI calls add tool
741
+ {
742
+ content: '',
743
+ parsedValue: '',
744
+ toolCalls: [{ id: 'call_1', toolName: 'add', args: { a: 0, b: 1 } }],
745
+ stopReason: 'tool_use',
746
+ },
747
+ { content: '1', parsedValue: 1, stopReason: 'end' },
748
+ // Second iteration - AI calls add tool
749
+ {
750
+ content: '',
751
+ parsedValue: '',
752
+ toolCalls: [{ id: 'call_2', toolName: 'add', args: { a: 1, b: 2 } }],
753
+ stopReason: 'tool_use',
754
+ },
755
+ { content: '3', parsedValue: 3, stopReason: 'end' },
756
+ // Final do call - no tools
757
+ { content: 'The final sum is 3', parsedValue: 'The final sum is 3', stopReason: 'end' },
758
+ ];
759
+
760
+ const aiProvider = createToolCallingAIProvider(mockResponses, toolExecutions, testTools);
761
+ const runtime = new Runtime(ast, aiProvider);
762
+ await runtime.run();
763
+
764
+ // Verify tools were executed during the loop
765
+ expect(toolExecutions).toHaveLength(2);
766
+
767
+ // Get context after loop with forget - tool calls should NOT appear
768
+ const state = runtime.getState();
769
+ const context = buildLocalContext(state);
770
+ const formatted = formatContextForAI(context, { includeInstructions: false });
771
+
772
+ // With 'forget', the loop's tool calls, iterations, and scope markers are removed
773
+ // Only variables declared outside the loop and what happens after remain
774
+ expect(formatted.text).toBe(
775
+ ` <entry> (current scope)
776
+ - sum (number): 0
777
+ --> vibe: "What is the final sum?"
778
+ <-- final (text): The final sum is 3`
779
+ );
780
+ });
781
+
782
+ test('tool calls inside loop with verbose mode are preserved in context', async () => {
783
+ // Tool calls inside a loop with 'verbose' should appear in context
784
+ const ast = parse(`
785
+ model m = { name: "test", apiKey: "key", url: "http://test" }
786
+ let sum = 0
787
+ for i in [1, 2] {
788
+ let partial: number = vibe "Add {i}" m default
789
+ sum = sum + partial
790
+ } verbose
791
+ let final: text = vibe "What is sum?" m default
792
+ `);
793
+
794
+ const toolExecutions: ToolExecution[] = [];
795
+ const testTools = createTestTools();
796
+
797
+ const mockResponses: AIResponse[] = [
798
+ // First iteration with tool
799
+ {
800
+ content: '',
801
+ parsedValue: '',
802
+ toolCalls: [{ id: 'call_1', toolName: 'add', args: { a: 0, b: 1 } }],
803
+ stopReason: 'tool_use',
804
+ },
805
+ { content: '1', parsedValue: 1, stopReason: 'end' },
806
+ // Second iteration with tool
807
+ {
808
+ content: '',
809
+ parsedValue: '',
810
+ toolCalls: [{ id: 'call_2', toolName: 'add', args: { a: 1, b: 2 } }],
811
+ stopReason: 'tool_use',
812
+ },
813
+ { content: '3', parsedValue: 3, stopReason: 'end' },
814
+ // Final do
815
+ { content: 'Sum is 3', parsedValue: 'Sum is 3', stopReason: 'end' },
816
+ ];
817
+
818
+ const aiProvider = createToolCallingAIProvider(mockResponses, toolExecutions, testTools);
819
+ const runtime = new Runtime(ast, aiProvider);
820
+ await runtime.run();
821
+
822
+ // Verify tools were executed
823
+ expect(toolExecutions).toHaveLength(2);
824
+
825
+ // Get context - with verbose, tool calls should be preserved
826
+ const state = runtime.getState();
827
+ const context = buildLocalContext(state);
828
+ const formatted = formatContextForAI(context, { includeInstructions: false });
829
+
830
+ // With 'verbose', the loop preserves all history including tool calls and scope markers
831
+ // Note: sum = 0 + 1 = 1, then sum = 1 + 3 = 4
832
+ // Order: AI call → tool calls → response (via variable assignment)
833
+ // With unified interpolation, {i} is left as reference in prompt strings
834
+ expect(formatted.text).toBe(
835
+ ` <entry> (current scope)
836
+ - sum (number): 0
837
+ ==> for i
838
+ - i (number): 1
839
+ --> vibe: "Add {i}"
840
+ [tool] add({"a":0,"b":1})
841
+ [result] 1
842
+ <-- partial (number): 1
843
+ - sum (number): 1
844
+ - i (number): 2
845
+ --> vibe: "Add {i}"
846
+ [tool] add({"a":1,"b":2})
847
+ [result] 3
848
+ <-- partial (number): 3
849
+ - sum (number): 4
850
+ <== for i
851
+ --> vibe: "What is sum?"
852
+ <-- final (text): Sum is 3`
853
+ );
854
+ });
855
+ });
856
+
857
+ // Note: Function context modes (verbose/forget) were removed.
858
+ // Functions always forget their internal context on exit, like traditional callstacks.
859
+ // If you need data visible outside a function, return it and assign to a variable.