@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.
- package/package.json +46 -0
- package/src/ast/index.ts +375 -0
- package/src/ast.ts +2 -0
- package/src/debug/advanced-features.ts +482 -0
- package/src/debug/bun-inspector.ts +424 -0
- package/src/debug/handoff-manager.ts +283 -0
- package/src/debug/index.ts +150 -0
- package/src/debug/runner.ts +365 -0
- package/src/debug/server.ts +565 -0
- package/src/debug/stack-merger.ts +267 -0
- package/src/debug/state.ts +581 -0
- package/src/debug/test/advanced-features.test.ts +300 -0
- package/src/debug/test/e2e.test.ts +218 -0
- package/src/debug/test/handoff-manager.test.ts +256 -0
- package/src/debug/test/runner.test.ts +256 -0
- package/src/debug/test/stack-merger.test.ts +163 -0
- package/src/debug/test/state.test.ts +400 -0
- package/src/debug/test/ts-debug-integration.test.ts +374 -0
- package/src/debug/test/ts-import-tracker.test.ts +125 -0
- package/src/debug/test/ts-source-map.test.ts +169 -0
- package/src/debug/ts-import-tracker.ts +151 -0
- package/src/debug/ts-source-map.ts +171 -0
- package/src/errors/index.ts +124 -0
- package/src/index.ts +358 -0
- package/src/lexer/index.ts +348 -0
- package/src/lexer.ts +2 -0
- package/src/parser/index.ts +792 -0
- package/src/parser/parse.ts +45 -0
- package/src/parser/test/async.test.ts +248 -0
- package/src/parser/test/destructuring.test.ts +167 -0
- package/src/parser/test/do-expression.test.ts +486 -0
- package/src/parser/test/errors/do-expression.test.ts +95 -0
- package/src/parser/test/errors/error-locations.test.ts +230 -0
- package/src/parser/test/errors/invalid-expressions.test.ts +144 -0
- package/src/parser/test/errors/missing-tokens.test.ts +126 -0
- package/src/parser/test/errors/model-declaration.test.ts +185 -0
- package/src/parser/test/errors/nested-blocks.test.ts +226 -0
- package/src/parser/test/errors/unclosed-delimiters.test.ts +122 -0
- package/src/parser/test/errors/unexpected-tokens.test.ts +120 -0
- package/src/parser/test/import-export.test.ts +143 -0
- package/src/parser/test/literals.test.ts +404 -0
- package/src/parser/test/model-declaration.test.ts +161 -0
- package/src/parser/test/nested-blocks.test.ts +402 -0
- package/src/parser/test/parser.test.ts +743 -0
- package/src/parser/test/private.test.ts +136 -0
- package/src/parser/test/template-literal.test.ts +127 -0
- package/src/parser/test/tool-declaration.test.ts +302 -0
- package/src/parser/test/ts-block.test.ts +252 -0
- package/src/parser/test/type-annotations.test.ts +254 -0
- package/src/parser/visitor/helpers.ts +330 -0
- package/src/parser/visitor.ts +794 -0
- package/src/parser.ts +2 -0
- package/src/runtime/ai/cache-chunking.test.ts +69 -0
- package/src/runtime/ai/cache-chunking.ts +73 -0
- package/src/runtime/ai/client.ts +109 -0
- package/src/runtime/ai/context.ts +168 -0
- package/src/runtime/ai/formatters.ts +316 -0
- package/src/runtime/ai/index.ts +38 -0
- package/src/runtime/ai/language-ref.ts +38 -0
- package/src/runtime/ai/providers/anthropic.ts +253 -0
- package/src/runtime/ai/providers/google.ts +201 -0
- package/src/runtime/ai/providers/openai.ts +156 -0
- package/src/runtime/ai/retry.ts +100 -0
- package/src/runtime/ai/return-tools.ts +301 -0
- package/src/runtime/ai/test/client.test.ts +83 -0
- package/src/runtime/ai/test/formatters.test.ts +485 -0
- package/src/runtime/ai/test/retry.test.ts +137 -0
- package/src/runtime/ai/test/return-tools.test.ts +450 -0
- package/src/runtime/ai/test/tool-loop.test.ts +319 -0
- package/src/runtime/ai/test/tool-schema.test.ts +241 -0
- package/src/runtime/ai/tool-loop.ts +203 -0
- package/src/runtime/ai/tool-schema.ts +151 -0
- package/src/runtime/ai/types.ts +113 -0
- package/src/runtime/ai-logger.ts +255 -0
- package/src/runtime/ai-provider.ts +347 -0
- package/src/runtime/async/dependencies.ts +276 -0
- package/src/runtime/async/executor.ts +293 -0
- package/src/runtime/async/index.ts +43 -0
- package/src/runtime/async/scheduling.ts +163 -0
- package/src/runtime/async/test/dependencies.test.ts +284 -0
- package/src/runtime/async/test/executor.test.ts +388 -0
- package/src/runtime/context.ts +357 -0
- package/src/runtime/exec/ai.ts +139 -0
- package/src/runtime/exec/expressions.ts +475 -0
- package/src/runtime/exec/frames.ts +26 -0
- package/src/runtime/exec/functions.ts +305 -0
- package/src/runtime/exec/interpolation.ts +312 -0
- package/src/runtime/exec/statements.ts +604 -0
- package/src/runtime/exec/tools.ts +129 -0
- package/src/runtime/exec/typescript.ts +215 -0
- package/src/runtime/exec/variables.ts +279 -0
- package/src/runtime/index.ts +975 -0
- package/src/runtime/modules.ts +452 -0
- package/src/runtime/serialize.ts +103 -0
- package/src/runtime/state.ts +489 -0
- package/src/runtime/stdlib/core.ts +45 -0
- package/src/runtime/stdlib/directory.test.ts +156 -0
- package/src/runtime/stdlib/edit.test.ts +154 -0
- package/src/runtime/stdlib/fastEdit.test.ts +201 -0
- package/src/runtime/stdlib/glob.test.ts +106 -0
- package/src/runtime/stdlib/grep.test.ts +144 -0
- package/src/runtime/stdlib/index.ts +16 -0
- package/src/runtime/stdlib/readFile.test.ts +123 -0
- package/src/runtime/stdlib/tools/index.ts +707 -0
- package/src/runtime/stdlib/writeFile.test.ts +157 -0
- package/src/runtime/step.ts +969 -0
- package/src/runtime/test/ai-context.test.ts +1086 -0
- package/src/runtime/test/ai-result-object.test.ts +419 -0
- package/src/runtime/test/ai-tool-flow.test.ts +859 -0
- package/src/runtime/test/async-execution-order.test.ts +618 -0
- package/src/runtime/test/async-execution.test.ts +344 -0
- package/src/runtime/test/async-nested.test.ts +660 -0
- package/src/runtime/test/async-parallel-timing.test.ts +546 -0
- package/src/runtime/test/basic1.test.ts +154 -0
- package/src/runtime/test/binary-operators.test.ts +431 -0
- package/src/runtime/test/break-statement.test.ts +257 -0
- package/src/runtime/test/context-modes.test.ts +650 -0
- package/src/runtime/test/context.test.ts +466 -0
- package/src/runtime/test/core-functions.test.ts +228 -0
- package/src/runtime/test/e2e.test.ts +88 -0
- package/src/runtime/test/error-locations/error-locations.test.ts +80 -0
- package/src/runtime/test/error-locations/main-error.vibe +4 -0
- package/src/runtime/test/error-locations/main-import-error.vibe +3 -0
- package/src/runtime/test/error-locations/utils/helper.vibe +5 -0
- package/src/runtime/test/for-in.test.ts +312 -0
- package/src/runtime/test/helpers.ts +69 -0
- package/src/runtime/test/imports.test.ts +334 -0
- package/src/runtime/test/json-expressions.test.ts +232 -0
- package/src/runtime/test/literals.test.ts +372 -0
- package/src/runtime/test/logical-indexing.test.ts +478 -0
- package/src/runtime/test/member-methods.test.ts +324 -0
- package/src/runtime/test/model-config.test.ts +338 -0
- package/src/runtime/test/null-handling.test.ts +342 -0
- package/src/runtime/test/private-visibility.test.ts +332 -0
- package/src/runtime/test/runtime-state.test.ts +514 -0
- package/src/runtime/test/scoping.test.ts +370 -0
- package/src/runtime/test/string-interpolation.test.ts +354 -0
- package/src/runtime/test/template-literal.test.ts +181 -0
- package/src/runtime/test/tool-execution.test.ts +467 -0
- package/src/runtime/test/tool-schema-generation.test.ts +477 -0
- package/src/runtime/test/tostring.test.ts +210 -0
- package/src/runtime/test/ts-block.test.ts +594 -0
- package/src/runtime/test/ts-error-location.test.ts +231 -0
- package/src/runtime/test/types.test.ts +732 -0
- package/src/runtime/test/verbose-logger.test.ts +710 -0
- package/src/runtime/test/vibe-expression.test.ts +54 -0
- package/src/runtime/test/vibe-value-errors.test.ts +541 -0
- package/src/runtime/test/while.test.ts +232 -0
- package/src/runtime/tools/builtin.ts +30 -0
- package/src/runtime/tools/directory-tools.ts +70 -0
- package/src/runtime/tools/file-tools.ts +228 -0
- package/src/runtime/tools/index.ts +5 -0
- package/src/runtime/tools/registry.ts +48 -0
- package/src/runtime/tools/search-tools.ts +134 -0
- package/src/runtime/tools/security.ts +36 -0
- package/src/runtime/tools/system-tools.ts +312 -0
- package/src/runtime/tools/test/fixtures/base-types.ts +40 -0
- package/src/runtime/tools/test/fixtures/test-types.ts +132 -0
- package/src/runtime/tools/test/registry.test.ts +713 -0
- package/src/runtime/tools/test/security.test.ts +86 -0
- package/src/runtime/tools/test/system-tools.test.ts +679 -0
- package/src/runtime/tools/test/ts-schema.test.ts +357 -0
- package/src/runtime/tools/ts-schema.ts +341 -0
- package/src/runtime/tools/types.ts +89 -0
- package/src/runtime/tools/utility-tools.ts +198 -0
- package/src/runtime/ts-eval.ts +126 -0
- package/src/runtime/types.ts +797 -0
- package/src/runtime/validation.ts +160 -0
- package/src/runtime/verbose-logger.ts +459 -0
- package/src/runtime.ts +2 -0
- package/src/semantic/analyzer-context.ts +62 -0
- package/src/semantic/analyzer-validators.ts +575 -0
- package/src/semantic/analyzer-visitors.ts +534 -0
- package/src/semantic/analyzer.ts +83 -0
- package/src/semantic/index.ts +11 -0
- package/src/semantic/symbol-table.ts +58 -0
- package/src/semantic/test/async-validation.test.ts +301 -0
- package/src/semantic/test/compress-validation.test.ts +179 -0
- package/src/semantic/test/const-reassignment.test.ts +111 -0
- package/src/semantic/test/control-flow.test.ts +346 -0
- package/src/semantic/test/destructuring.test.ts +185 -0
- package/src/semantic/test/duplicate-declarations.test.ts +168 -0
- package/src/semantic/test/export-validation.test.ts +111 -0
- package/src/semantic/test/fixtures/math.ts +31 -0
- package/src/semantic/test/imports.test.ts +148 -0
- package/src/semantic/test/json-type.test.ts +68 -0
- package/src/semantic/test/literals.test.ts +127 -0
- package/src/semantic/test/model-validation.test.ts +179 -0
- package/src/semantic/test/prompt-validation.test.ts +343 -0
- package/src/semantic/test/scoping.test.ts +312 -0
- package/src/semantic/test/tool-validation.test.ts +306 -0
- package/src/semantic/test/ts-type-checking.test.ts +563 -0
- package/src/semantic/test/type-constraints.test.ts +111 -0
- package/src/semantic/test/type-inference.test.ts +87 -0
- package/src/semantic/test/type-validation.test.ts +552 -0
- package/src/semantic/test/undefined-variables.test.ts +163 -0
- package/src/semantic/ts-block-checker.ts +204 -0
- package/src/semantic/ts-signatures.ts +194 -0
- package/src/semantic/ts-types.ts +170 -0
- package/src/semantic/types.ts +58 -0
- package/tests/fixtures/conditional-logic.vibe +14 -0
- package/tests/fixtures/function-call.vibe +16 -0
- package/tests/fixtures/imports/cycle-detection/a.vibe +6 -0
- package/tests/fixtures/imports/cycle-detection/b.vibe +5 -0
- package/tests/fixtures/imports/cycle-detection/main.vibe +3 -0
- package/tests/fixtures/imports/module-isolation/main-b.vibe +8 -0
- package/tests/fixtures/imports/module-isolation/main.vibe +9 -0
- package/tests/fixtures/imports/module-isolation/moduleA.vibe +6 -0
- package/tests/fixtures/imports/module-isolation/moduleB.vibe +6 -0
- package/tests/fixtures/imports/nested-import/helper.vibe +6 -0
- package/tests/fixtures/imports/nested-import/main.vibe +3 -0
- package/tests/fixtures/imports/nested-import/utils.ts +3 -0
- package/tests/fixtures/imports/nested-isolation/file2.vibe +15 -0
- package/tests/fixtures/imports/nested-isolation/file3.vibe +10 -0
- package/tests/fixtures/imports/nested-isolation/main.vibe +21 -0
- package/tests/fixtures/imports/pure-cycle/a.vibe +5 -0
- package/tests/fixtures/imports/pure-cycle/b.vibe +5 -0
- package/tests/fixtures/imports/pure-cycle/main.vibe +3 -0
- package/tests/fixtures/imports/ts-boolean/checks.ts +14 -0
- package/tests/fixtures/imports/ts-boolean/main.vibe +10 -0
- package/tests/fixtures/imports/ts-boolean/type-mismatch.vibe +5 -0
- package/tests/fixtures/imports/ts-boolean/use-constant.vibe +18 -0
- package/tests/fixtures/imports/ts-error-handling/helpers.ts +42 -0
- package/tests/fixtures/imports/ts-error-handling/main.vibe +5 -0
- package/tests/fixtures/imports/ts-import/main.vibe +4 -0
- package/tests/fixtures/imports/ts-import/math.ts +9 -0
- package/tests/fixtures/imports/ts-variables/call-non-function.vibe +5 -0
- package/tests/fixtures/imports/ts-variables/data.ts +10 -0
- package/tests/fixtures/imports/ts-variables/import-json.vibe +5 -0
- package/tests/fixtures/imports/ts-variables/import-type-mismatch.vibe +5 -0
- package/tests/fixtures/imports/ts-variables/import-variable.vibe +5 -0
- package/tests/fixtures/imports/vibe-import/greet.vibe +5 -0
- package/tests/fixtures/imports/vibe-import/main.vibe +3 -0
- package/tests/fixtures/multiple-ai-calls.vibe +10 -0
- package/tests/fixtures/simple-greeting.vibe +6 -0
- package/tests/fixtures/template-literals.vibe +11 -0
- package/tests/integration/basic-ai/basic-ai.integration.test.ts +166 -0
- package/tests/integration/basic-ai/basic-ai.vibe +12 -0
- package/tests/integration/bug-fix/bug-fix.integration.test.ts +201 -0
- package/tests/integration/bug-fix/buggy-code.ts +22 -0
- package/tests/integration/bug-fix/fix-bug.vibe +21 -0
- package/tests/integration/compress/compress.integration.test.ts +206 -0
- package/tests/integration/destructuring/destructuring.integration.test.ts +92 -0
- package/tests/integration/hello-world-translator/hello-world-translator.integration.test.ts +61 -0
- package/tests/integration/line-annotator/context-modes.integration.test.ts +261 -0
- package/tests/integration/line-annotator/line-annotator.integration.test.ts +148 -0
- package/tests/integration/multi-feature/cumulative-sum.integration.test.ts +75 -0
- package/tests/integration/multi-feature/number-analyzer.integration.test.ts +191 -0
- package/tests/integration/multi-feature/number-analyzer.vibe +59 -0
- 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
|
+
}
|