@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,255 @@
1
+ // AI Interaction Logging Utilities
2
+ // Formats AI interactions for debugging and analysis
3
+ //
4
+ // This logger uses the stored context from AIInteraction which is the
5
+ // SINGLE SOURCE OF TRUTH for what was sent to the model.
6
+ // The context is captured in ai-provider.ts using buildAIContext().
7
+
8
+ import { existsSync, mkdirSync, readdirSync, unlinkSync, writeFileSync } from 'fs';
9
+ import { join } from 'path';
10
+ import type { RuntimeState, AIInteraction, AILogMessage } from './types';
11
+
12
+ const LOG_DIR = '.ai-logs';
13
+ const MAX_LOGS = 20;
14
+
15
+ /**
16
+ * Format a single message for display.
17
+ */
18
+ function formatMessage(msg: AILogMessage): string {
19
+ const lines: string[] = [];
20
+
21
+ lines.push(`**[${msg.role}]**`);
22
+
23
+ // Content
24
+ if (msg.content) {
25
+ lines.push(msg.content);
26
+ }
27
+
28
+ // Tool calls (for assistant messages)
29
+ if (msg.toolCalls && msg.toolCalls.length > 0) {
30
+ lines.push('');
31
+ lines.push('*Tool calls:*');
32
+ for (const call of msg.toolCalls) {
33
+ lines.push(`- \`${call.toolName}(${JSON.stringify(call.args)})\``);
34
+ }
35
+ }
36
+
37
+ // Tool results (for user messages)
38
+ if (msg.toolResults && msg.toolResults.length > 0) {
39
+ lines.push('');
40
+ lines.push('*Tool results:*');
41
+ for (const result of msg.toolResults) {
42
+ if (result.error) {
43
+ lines.push(`- Error: ${result.error}`);
44
+ } else {
45
+ const resultStr = typeof result.result === 'string'
46
+ ? result.result
47
+ : JSON.stringify(result.result, null, 2);
48
+ lines.push(`- ${resultStr}`);
49
+ }
50
+ }
51
+ }
52
+
53
+ lines.push('');
54
+ return lines.join('\n');
55
+ }
56
+
57
+ /**
58
+ * Format tool calls that occurred during the interaction.
59
+ */
60
+ function formatToolCalls(toolCalls: AIInteraction['interactionToolCalls']): string {
61
+ if (!toolCalls || toolCalls.length === 0) {
62
+ return '';
63
+ }
64
+
65
+ const lines: string[] = [];
66
+ lines.push('### Tool Calls During Interaction');
67
+ lines.push('');
68
+
69
+ for (const call of toolCalls) {
70
+ lines.push(`- \`${call.toolName}(${JSON.stringify(call.args)})\``);
71
+ if (call.error) {
72
+ lines.push(` → Error: ${call.error}`);
73
+ } else if (call.result !== undefined) {
74
+ const resultStr = typeof call.result === 'string'
75
+ ? call.result
76
+ : JSON.stringify(call.result, null, 2);
77
+ lines.push(` → ${resultStr}`);
78
+ }
79
+ }
80
+
81
+ lines.push('');
82
+ return lines.join('\n');
83
+ }
84
+
85
+ /**
86
+ * Format a single AI interaction as markdown.
87
+ * Uses the stored context which is the single source of truth.
88
+ */
89
+ function formatInteraction(interaction: AIInteraction, index: number): string {
90
+ const lines: string[] = [];
91
+
92
+ // Header
93
+ lines.push(`## Interaction ${index + 1}`);
94
+ lines.push(`**Type:** ${interaction.type} | **Model:** ${interaction.model} | **Target:** ${interaction.targetType ?? 'text'}`);
95
+ lines.push('');
96
+
97
+ // Messages sent to model (from stored context - single source of truth)
98
+ // Note: The context is included in the user messages, so we don't need a separate section
99
+ lines.push('### Messages Sent to Model');
100
+ lines.push('');
101
+
102
+ for (const msg of interaction.messages) {
103
+ lines.push(formatMessage(msg));
104
+ }
105
+
106
+ // Tool calls that occurred during this interaction
107
+ if (interaction.interactionToolCalls && interaction.interactionToolCalls.length > 0) {
108
+ lines.push(formatToolCalls(interaction.interactionToolCalls));
109
+ }
110
+
111
+ // Response
112
+ lines.push('### Response');
113
+ lines.push('```');
114
+ if (typeof interaction.response === 'string') {
115
+ lines.push(interaction.response);
116
+ } else {
117
+ lines.push(JSON.stringify(interaction.response, null, 2));
118
+ }
119
+ lines.push('```');
120
+
121
+ // Metadata
122
+ if (interaction.usage || interaction.durationMs) {
123
+ lines.push('');
124
+
125
+ // Token usage
126
+ if (interaction.usage) {
127
+ const { inputTokens, outputTokens, cachedInputTokens, cacheCreationTokens, thinkingTokens } = interaction.usage;
128
+ let tokenStr = `**Tokens:** ${inputTokens} in`;
129
+ if (cachedInputTokens) {
130
+ tokenStr += ` (${cachedInputTokens} cached)`;
131
+ }
132
+ if (cacheCreationTokens) {
133
+ tokenStr += ` (${cacheCreationTokens} cache write)`;
134
+ }
135
+ tokenStr += ` / ${outputTokens} out`;
136
+ if (thinkingTokens) {
137
+ tokenStr += ` (${thinkingTokens} thinking)`;
138
+ }
139
+ lines.push(tokenStr);
140
+ }
141
+
142
+ // Duration
143
+ if (interaction.durationMs) {
144
+ lines.push(`**Duration:** ${interaction.durationMs}ms`);
145
+ }
146
+ }
147
+
148
+ return lines.join('\n');
149
+ }
150
+
151
+ /**
152
+ * Format all AI interactions as markdown.
153
+ */
154
+ export function formatAIInteractions(interactions: AIInteraction[]): string {
155
+ if (interactions.length === 0) {
156
+ return 'No AI interactions recorded.';
157
+ }
158
+
159
+ // Collect unique models used
160
+ const modelsUsed = new Map<string, { name: string; provider: string; url?: string; thinkingLevel?: string }>();
161
+ for (const interaction of interactions) {
162
+ if (interaction.modelDetails && !modelsUsed.has(interaction.model)) {
163
+ modelsUsed.set(interaction.model, interaction.modelDetails);
164
+ }
165
+ }
166
+
167
+ // Build header with model info
168
+ const headerLines = [
169
+ '# AI Interaction Log',
170
+ '',
171
+ `Total interactions: ${interactions.length}`,
172
+ ];
173
+
174
+ if (modelsUsed.size > 0) {
175
+ headerLines.push('');
176
+ headerLines.push('## Models Used');
177
+ for (const [varName, details] of modelsUsed) {
178
+ let modelLine = `- **${varName}**: \`${details.name}\` via ${details.provider}`;
179
+ if (details.url) {
180
+ modelLine += ` @ ${details.url}`;
181
+ }
182
+ if (details.thinkingLevel) {
183
+ modelLine += ` (thinking: ${details.thinkingLevel})`;
184
+ }
185
+ headerLines.push(modelLine);
186
+ }
187
+ }
188
+
189
+ headerLines.push('');
190
+ headerLines.push('---');
191
+ headerLines.push('');
192
+
193
+ const header = headerLines.join('\n');
194
+ const formatted = interactions.map((interaction, i) => formatInteraction(interaction, i));
195
+
196
+ return header + formatted.join('\n\n---\n\n');
197
+ }
198
+
199
+ /**
200
+ * Dump AI interactions to console in a readable format.
201
+ */
202
+ export function dumpAIInteractions(state: RuntimeState): void {
203
+ console.log('\n' + '='.repeat(60));
204
+ console.log('AI INTERACTION LOG');
205
+ console.log('='.repeat(60) + '\n');
206
+ console.log(formatAIInteractions(state.aiInteractions));
207
+ console.log('\n' + '='.repeat(60) + '\n');
208
+ }
209
+
210
+ /**
211
+ * Save AI interactions to a file in the log directory.
212
+ * Automatically rotates logs to keep only the last MAX_LOGS files.
213
+ */
214
+ export function saveAIInteractions(state: RuntimeState, projectRoot?: string): string | null {
215
+ if (state.aiInteractions.length === 0) {
216
+ return null;
217
+ }
218
+
219
+ const logDir = projectRoot ? join(projectRoot, LOG_DIR) : LOG_DIR;
220
+
221
+ // Ensure log directory exists
222
+ if (!existsSync(logDir)) {
223
+ mkdirSync(logDir, { recursive: true });
224
+ }
225
+
226
+ // Generate filename with timestamp
227
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
228
+ const filename = `ai-log-${timestamp}.md`;
229
+ const filepath = join(logDir, filename);
230
+
231
+ // Write the log
232
+ const content = formatAIInteractions(state.aiInteractions);
233
+ writeFileSync(filepath, content, 'utf-8');
234
+
235
+ // Rotate logs - keep only the last MAX_LOGS
236
+ rotateLogFiles(logDir);
237
+
238
+ return filepath;
239
+ }
240
+
241
+ /**
242
+ * Remove old log files to keep only the last MAX_LOGS.
243
+ */
244
+ function rotateLogFiles(logDir: string): void {
245
+ const files = readdirSync(logDir)
246
+ .filter(f => f.startsWith('ai-log-') && f.endsWith('.md'))
247
+ .sort()
248
+ .reverse(); // Newest first (ISO timestamp sorts correctly)
249
+
250
+ // Remove files beyond MAX_LOGS
251
+ const toDelete = files.slice(MAX_LOGS);
252
+ for (const file of toDelete) {
253
+ unlinkSync(join(logDir, file));
254
+ }
255
+ }
@@ -0,0 +1,347 @@
1
+ // Real AI Provider Implementation
2
+ // Uses the AI module to make actual API calls
3
+
4
+ import type { AIProvider, AIExecutionResult } from './index';
5
+ import type { RuntimeState, AILogMessage, PromptToolCall } from './types';
6
+ import type { VibeModelValue, TargetType, AIRequest, ModelConfig, AIProviderType } from './ai';
7
+ import type { VibeToolValue, ToolSchema } from './tools/types';
8
+ import { detectProvider, getProviderExecutor, buildAIRequest } from './ai';
9
+ import { withRetry } from './ai/retry';
10
+ import { executeWithTools, type ToolRoundResult } from './ai/tool-loop';
11
+ import { buildGlobalContext, formatContextForAI } from './context';
12
+ import { buildAIContext } from './ai/context';
13
+ import { buildVibeMessages, type VibeScopeParam } from './ai/formatters';
14
+ import {
15
+ getReturnTools,
16
+ shouldUseReturnTool,
17
+ isReturnToolCall,
18
+ buildReturnInstruction,
19
+ collectAndValidateFieldResults,
20
+ isFieldReturnResult,
21
+ RETURN_FIELD_TOOL,
22
+ } from './ai/return-tools';
23
+ import type { ExpectedField } from './types';
24
+
25
+ /**
26
+ * Get model value from runtime state by model name.
27
+ */
28
+ function getModelValue(state: RuntimeState, modelName: string): VibeModelValue | null {
29
+ // Search through all frames for the model
30
+ for (let i = state.callStack.length - 1; i >= 0; i--) {
31
+ const frame = state.callStack[i];
32
+ const variable = frame.locals[modelName];
33
+ if (variable?.value && isModelValue(variable.value)) {
34
+ return variable.value;
35
+ }
36
+ }
37
+ return null;
38
+ }
39
+
40
+ /**
41
+ * Type guard for model values.
42
+ */
43
+ function isModelValue(value: unknown): value is VibeModelValue {
44
+ return (
45
+ typeof value === 'object' &&
46
+ value !== null &&
47
+ '__vibeModel' in value &&
48
+ (value as VibeModelValue).__vibeModel === true
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Get target type from the pending variable declaration context.
54
+ * Returns null if not in a variable declaration or no type annotation.
55
+ */
56
+ function getTargetType(state: RuntimeState): TargetType {
57
+ // Look at the next instruction to see if we're assigning to a typed variable
58
+ const nextInstruction = state.instructionStack[0];
59
+ if (nextInstruction?.op === 'declare_var' && nextInstruction.type) {
60
+ const type = nextInstruction.type;
61
+ // Only return types that the AI module understands
62
+ if (['text', 'json', 'boolean', 'number'].includes(type) || type.endsWith('[]')) {
63
+ return type as TargetType;
64
+ }
65
+ }
66
+ return null;
67
+ }
68
+
69
+ /**
70
+ * Build model config from runtime model value.
71
+ */
72
+ function buildModelConfig(modelValue: VibeModelValue): ModelConfig {
73
+ if (!modelValue.name) {
74
+ throw new Error('Model name is required');
75
+ }
76
+ if (!modelValue.apiKey) {
77
+ throw new Error('API key is required');
78
+ }
79
+
80
+ const provider: AIProviderType =
81
+ (modelValue.provider as AIProviderType) ?? detectProvider(modelValue.url);
82
+
83
+ return {
84
+ name: modelValue.name,
85
+ apiKey: modelValue.apiKey,
86
+ url: modelValue.url,
87
+ provider,
88
+ maxRetriesOnError: modelValue.maxRetriesOnError ?? undefined,
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Create a real AI provider that uses actual API calls.
94
+ * The provider needs access to runtime state to get model configs.
95
+ */
96
+ export function createRealAIProvider(getState: () => RuntimeState): AIProvider {
97
+ return {
98
+ async execute(prompt: string): Promise<AIExecutionResult> {
99
+ const state = getState();
100
+
101
+ // Get model name from pendingAI or pendingCompress
102
+ let modelName: string;
103
+ let aiType: 'do' | 'vibe' | 'compress';
104
+ if (state.pendingAI) {
105
+ modelName = state.pendingAI.model;
106
+ aiType = state.pendingAI.type;
107
+ } else if (state.pendingCompress) {
108
+ modelName = state.pendingCompress.model;
109
+ aiType = 'compress';
110
+ } else {
111
+ throw new Error('No pending AI or compress request');
112
+ }
113
+ const modelValue = getModelValue(state, modelName);
114
+ if (!modelValue) {
115
+ throw new Error(`Model '${modelName}' not found in scope`);
116
+ }
117
+
118
+ // Determine target type from pending variable declaration
119
+ const targetType = getTargetType(state);
120
+
121
+ // Build model config
122
+ const model = buildModelConfig(modelValue);
123
+
124
+ // Get tools from model (empty array if no tools specified)
125
+ const modelTools: VibeToolValue[] = (modelValue.tools as VibeToolValue[]) ?? [];
126
+
127
+ // Always include return tools (keeps tool list consistent for caching)
128
+ const returnTools = getReturnTools();
129
+ const allTools = [...returnTools, ...modelTools];
130
+ const toolSchemas: ToolSchema[] = allTools.map((t) => t.schema);
131
+
132
+ // Build expected fields for return validation
133
+ // Priority: 1) pendingDestructuring (multi-value), 2) targetType (single-value)
134
+ let expectedFields: ExpectedField[] | null = null;
135
+ if (state.pendingDestructuring) {
136
+ // Multi-value destructuring: const {name: text, age: number} = do "..."
137
+ expectedFields = state.pendingDestructuring.map((f) => ({ name: f.name, type: f.type }));
138
+ } else if (shouldUseReturnTool(targetType)) {
139
+ // Single-value typed return: const x: number = do "..."
140
+ expectedFields = [{ name: 'value', type: targetType! }];
141
+ }
142
+
143
+ // Determine if this request should use tool-based return
144
+ const useToolReturn = expectedFields !== null && expectedFields.length > 0;
145
+ const returnToolName = useToolReturn ? RETURN_FIELD_TOOL : null;
146
+
147
+ // Append return instruction to prompt if we have expected fields
148
+ const finalPrompt = expectedFields
149
+ ? prompt + buildReturnInstruction(expectedFields)
150
+ : prompt;
151
+
152
+ // Build unified AI context (single source of truth)
153
+ // Use finalPrompt so the log shows what was actually sent
154
+ const aiContext = buildAIContext(
155
+ state,
156
+ model,
157
+ finalPrompt,
158
+ // For tool-based returns, pass null to disable structured output
159
+ useToolReturn ? null : targetType,
160
+ toolSchemas.length > 0 ? toolSchemas : undefined
161
+ );
162
+
163
+ // Build context from global context for the request
164
+ const context = buildGlobalContext(state);
165
+ const formattedContext = formatContextForAI(context);
166
+
167
+ // Build the request with tools
168
+ // For compress, treat as single-round 'do' type
169
+ const requestType = aiType === 'compress' ? 'do' : aiType;
170
+
171
+ const request: AIRequest = {
172
+ ...buildAIRequest(
173
+ model,
174
+ finalPrompt,
175
+ formattedContext.text,
176
+ requestType,
177
+ // For tool-based returns, pass null to disable structured output
178
+ useToolReturn ? null : targetType
179
+ ),
180
+ tools: toolSchemas.length > 0 ? toolSchemas : undefined,
181
+ };
182
+
183
+ // Get provider executor (provider is always defined after buildModelConfig)
184
+ const execute = getProviderExecutor(model.provider!);
185
+
186
+ // Execute with tool loop (handles multi-turn tool calling)
187
+ // 'do'/'compress' = single round (maxRounds: 1), 'vibe' = multi-turn (maxRounds: 10)
188
+ const maxRetries = modelValue.maxRetriesOnError ?? 3;
189
+ const isDo = aiType === 'do' || aiType === 'compress';
190
+ const { response, rounds, returnFieldResults, completedViaReturnTool } = await executeWithTools(
191
+ request,
192
+ allTools,
193
+ state.rootDir,
194
+ (req) => withRetry(() => execute(req), { maxRetries }),
195
+ {
196
+ maxRounds: isDo ? 1 : 10,
197
+ expectedReturnTool: returnToolName ?? undefined,
198
+ }
199
+ );
200
+
201
+ // Convert tool rounds to PromptToolCall format for logging
202
+ // Filter out return tools - they're internal and shouldn't appear in context
203
+ const interactionToolCalls: PromptToolCall[] = rounds.flatMap((round) =>
204
+ round.toolCalls
205
+ .filter((call) => !isReturnToolCall(call.toolName))
206
+ .map((call) => {
207
+ const result = round.results.find((r) => r.toolCallId === call.id);
208
+ return {
209
+ toolName: call.toolName,
210
+ args: call.args,
211
+ result: result?.result,
212
+ error: result?.error,
213
+ };
214
+ })
215
+ );
216
+
217
+ // Determine final value
218
+ let finalValue: unknown;
219
+ if (useToolReturn) {
220
+ if (completedViaReturnTool && returnFieldResults) {
221
+ // Filter and validate field return results
222
+ const fieldResults = returnFieldResults.filter(isFieldReturnResult);
223
+ if (fieldResults.length === 0) {
224
+ throw new Error('No valid field return results from AI');
225
+ }
226
+ // Validate and collect all fields
227
+ const validated = collectAndValidateFieldResults(fieldResults, expectedFields!);
228
+ // For single-value returns, extract the 'value' field
229
+ // For multi-value destructuring, keep the whole object
230
+ finalValue = state.pendingDestructuring ? validated : validated['value'];
231
+ } else {
232
+ // After max retries, AI still didn't call return tool
233
+ throw new Error(`AI failed to call ${returnToolName} after multiple attempts`);
234
+ }
235
+ } else {
236
+ // Check if model used return tool anyway (even when not expected)
237
+ // This can happen since return tools are always included for caching
238
+ if (completedViaReturnTool && returnFieldResults) {
239
+ const fieldResults = returnFieldResults.filter(isFieldReturnResult);
240
+ if (fieldResults.length > 0) {
241
+ // Model used return tool - extract value from it
242
+ // Take the first field result's value (typically 'value' field)
243
+ finalValue = fieldResults[0].value;
244
+ } else {
245
+ finalValue = response.parsedValue ?? response.content;
246
+ }
247
+ } else {
248
+ finalValue = response.parsedValue ?? response.content;
249
+ }
250
+ }
251
+
252
+ // Return the parsed value, usage, tool rounds, and context for logging
253
+ return {
254
+ value: finalValue,
255
+ usage: response.usage,
256
+ toolRounds: rounds.length > 0 ? rounds : undefined,
257
+ // Context for logging (single source of truth)
258
+ messages: aiContext.messages,
259
+ executionContext: aiContext.executionContext,
260
+ interactionToolCalls: interactionToolCalls.length > 0 ? interactionToolCalls : undefined,
261
+ };
262
+ },
263
+
264
+ async generateCode(prompt: string): Promise<AIExecutionResult> {
265
+ // For vibe expressions, generate Vibe code using scope parameters
266
+ const state = getState();
267
+ if (!state.pendingAI) {
268
+ throw new Error('No pending AI request');
269
+ }
270
+
271
+ const modelName = state.pendingAI.model;
272
+ if (modelName === 'default') {
273
+ throw new Error('Vibe expressions require a model to be specified');
274
+ }
275
+
276
+ const modelValue = getModelValue(state, modelName);
277
+ if (!modelValue) {
278
+ throw new Error(`Model '${modelName}' not found in scope`);
279
+ }
280
+
281
+ // Build model config
282
+ const model = buildModelConfig(modelValue);
283
+
284
+ // Get scope parameters for vibe code generation
285
+ const scopeParams: VibeScopeParam[] = state.pendingAI.vibeScopeParams ?? [];
286
+
287
+ // Build vibe-specific messages with specialized system prompt
288
+ const vibeMessages = buildVibeMessages(prompt, scopeParams);
289
+
290
+ // Build the request for code generation (no tools, no structured output)
291
+ const request: AIRequest = {
292
+ operationType: 'vibe',
293
+ prompt,
294
+ contextText: '', // Context is embedded in the vibe system prompt
295
+ targetType: null, // Raw text response expected
296
+ model,
297
+ // Override messages with vibe-specific format
298
+ };
299
+
300
+ // Get provider executor
301
+ const execute = getProviderExecutor(model.provider!);
302
+
303
+ // Execute directly without tool loop (vibe generates code, not tool calls)
304
+ const maxRetries = modelValue.maxRetriesOnError ?? 3;
305
+ const response = await withRetry(() => execute(request), { maxRetries });
306
+
307
+ // Log messages for debugging
308
+ const messages: AILogMessage[] = vibeMessages.map(m => ({
309
+ role: m.role,
310
+ content: m.content,
311
+ }));
312
+
313
+ return {
314
+ value: String(response.content),
315
+ usage: response.usage,
316
+ // Include vibe messages for logging
317
+ messages,
318
+ executionContext: [], // Vibe doesn't use execution context
319
+ };
320
+ },
321
+
322
+ async askUser(prompt: string): Promise<string> {
323
+ // For user input, we could integrate with readline or similar
324
+ // For now, throw to indicate this needs external handling
325
+ throw new Error(
326
+ 'User input not implemented. Use an external handler for awaiting_user state.'
327
+ );
328
+ },
329
+ };
330
+ }
331
+
332
+ /**
333
+ * A mock AI provider for testing (returns prompt as response).
334
+ */
335
+ export function createMockAIProvider(): AIProvider {
336
+ return {
337
+ async execute(prompt: string): Promise<AIExecutionResult> {
338
+ return { value: `[AI Response to: ${prompt}]` };
339
+ },
340
+ async generateCode(prompt: string): Promise<AIExecutionResult> {
341
+ return { value: `// Generated code for: ${prompt}` };
342
+ },
343
+ async askUser(prompt: string): Promise<string> {
344
+ return `[User input for: ${prompt}]`;
345
+ },
346
+ };
347
+ }