@vibe-lang/runtime 0.2.6 → 0.2.7
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
CHANGED
package/src/parser/parse.ts
CHANGED
|
@@ -4,12 +4,34 @@ import { vibeAstVisitor } from './visitor';
|
|
|
4
4
|
import { setCurrentFile } from './visitor/helpers';
|
|
5
5
|
import { ParserError } from '../errors';
|
|
6
6
|
import type { Program } from '../ast';
|
|
7
|
+
import type { IRecognitionException } from 'chevrotain';
|
|
7
8
|
|
|
8
9
|
export interface ParseOptions {
|
|
9
10
|
/** File path to include in source locations (for error reporting) */
|
|
10
11
|
file?: string;
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Transform Chevrotain errors into user-friendly messages
|
|
16
|
+
*/
|
|
17
|
+
function improveErrorMessage(error: IRecognitionException): string {
|
|
18
|
+
const ruleStack = error.context?.ruleStack ?? [];
|
|
19
|
+
const previousToken = error.previousToken;
|
|
20
|
+
const message = error.message;
|
|
21
|
+
|
|
22
|
+
// Missing type annotation for function/tool parameter
|
|
23
|
+
// Detected: in "parameter" or "toolParameter" rule, expected Colon, previous token is Identifier
|
|
24
|
+
if (
|
|
25
|
+
(ruleStack.includes('parameter') || ruleStack.includes('toolParameter')) &&
|
|
26
|
+
message.includes('Colon') &&
|
|
27
|
+
previousToken?.tokenType?.name === 'Identifier'
|
|
28
|
+
) {
|
|
29
|
+
return `Missing type annotation for parameter '${previousToken.image}'`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return message;
|
|
33
|
+
}
|
|
34
|
+
|
|
13
35
|
/**
|
|
14
36
|
* Parse a Vibe source code string into an AST
|
|
15
37
|
*/
|
|
@@ -28,7 +50,7 @@ export function parse(source: string, options?: ParseOptions): Program {
|
|
|
28
50
|
if (vibeParser.errors.length > 0) {
|
|
29
51
|
const error = vibeParser.errors[0];
|
|
30
52
|
throw new ParserError(
|
|
31
|
-
error
|
|
53
|
+
improveErrorMessage(error),
|
|
32
54
|
error.token.image,
|
|
33
55
|
{ line: error.token.startLine ?? 1, column: error.token.startColumn ?? 1, file: options?.file },
|
|
34
56
|
source
|
|
@@ -58,6 +58,40 @@ function foo {
|
|
|
58
58
|
`)).toThrow();
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
+
test('function parameter missing type annotation', () => {
|
|
62
|
+
expect(() => parse(`function foo(x) { }`)).toThrow(
|
|
63
|
+
"Missing type annotation for parameter 'x'"
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('function first parameter missing type annotation', () => {
|
|
68
|
+
expect(() => parse(`function foo(x, y: text) { }`)).toThrow(
|
|
69
|
+
"Missing type annotation for parameter 'x'"
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('function second parameter missing type annotation', () => {
|
|
74
|
+
expect(() => parse(`function foo(x: text, y) { }`)).toThrow(
|
|
75
|
+
"Missing type annotation for parameter 'y'"
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('export function parameter missing type annotation', () => {
|
|
80
|
+
expect(() => parse(`export function foo(guesser, answerer) { }`)).toThrow(
|
|
81
|
+
"Missing type annotation for parameter 'guesser'"
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// tool declaration
|
|
87
|
+
// ============================================================================
|
|
88
|
+
|
|
89
|
+
test('tool parameter missing type annotation', () => {
|
|
90
|
+
expect(() => parse(`tool myTool(x) { ts() { return 1; } }`)).toThrow(
|
|
91
|
+
"Missing type annotation for parameter 'x'"
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
61
95
|
// ============================================================================
|
|
62
96
|
// if statement
|
|
63
97
|
// ============================================================================
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
2
|
import { parse } from '../../parser/parse';
|
|
3
3
|
import { Runtime, type AIProvider } from '../index';
|
|
4
|
+
import { createInitialState, resumeWithAIResponse } from '../state';
|
|
5
|
+
import { runUntilPause } from '../step';
|
|
4
6
|
|
|
5
7
|
describe('Runtime - Model Config Value Resolution', () => {
|
|
6
8
|
const mockProvider: AIProvider = {
|
|
@@ -335,4 +337,144 @@ let key = ts(testModel) {
|
|
|
335
337
|
await runtime.run();
|
|
336
338
|
expect(runtime.getValue('key')).toBe('fallback-key');
|
|
337
339
|
});
|
|
340
|
+
|
|
341
|
+
// ============================================================================
|
|
342
|
+
// Model as Function Parameter
|
|
343
|
+
// ============================================================================
|
|
344
|
+
|
|
345
|
+
test('function with model parameter - access model properties', async () => {
|
|
346
|
+
const runtime = createRuntime(`
|
|
347
|
+
model myModel = {
|
|
348
|
+
name: "gpt-4",
|
|
349
|
+
apiKey: "test-key",
|
|
350
|
+
provider: "openai"
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function getModelName(m: model): text {
|
|
354
|
+
return ts(m) { return m.name; }
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let name = getModelName(myModel)
|
|
358
|
+
`);
|
|
359
|
+
await runtime.run();
|
|
360
|
+
expect(runtime.getValue('name')).toBe('gpt-4');
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test('function with multiple model parameters', async () => {
|
|
364
|
+
const runtime = createRuntime(`
|
|
365
|
+
model guesserModel = {
|
|
366
|
+
name: "claude-sonnet-4-20250514",
|
|
367
|
+
apiKey: "key1",
|
|
368
|
+
provider: "anthropic"
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
model answererModel = {
|
|
372
|
+
name: "gpt-4",
|
|
373
|
+
apiKey: "key2",
|
|
374
|
+
provider: "openai"
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function getModelNames(guesser: model, answerer: model): text {
|
|
378
|
+
let g = ts(guesser) { return guesser.name; }
|
|
379
|
+
let a = ts(answerer) { return answerer.name; }
|
|
380
|
+
return g + " vs " + a
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let result = getModelNames(guesserModel, answererModel)
|
|
384
|
+
`);
|
|
385
|
+
await runtime.run();
|
|
386
|
+
expect(runtime.getValue('result')).toBe('claude-sonnet-4-20250514 vs gpt-4');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test('function with model and other parameters', async () => {
|
|
390
|
+
const runtime = createRuntime(`
|
|
391
|
+
model testModel = {
|
|
392
|
+
name: "test-model",
|
|
393
|
+
apiKey: "key",
|
|
394
|
+
provider: "test"
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function processWithModel(m: model, data: text): text {
|
|
398
|
+
let modelName = ts(m) { return m.name; }
|
|
399
|
+
return modelName + ": " + data
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let output = processWithModel(testModel, "hello")
|
|
403
|
+
`);
|
|
404
|
+
await runtime.run();
|
|
405
|
+
expect(runtime.getValue('output')).toBe('test-model: hello');
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test('function with model parameter used in do expression', () => {
|
|
409
|
+
const ast = parse(`
|
|
410
|
+
model myModel = {
|
|
411
|
+
name: "gpt-4",
|
|
412
|
+
apiKey: "key",
|
|
413
|
+
provider: "openai"
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function askQuestion(m: model, question: text): text {
|
|
417
|
+
return do question m default
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
let answer = askQuestion(myModel, "What is 2+2?")
|
|
421
|
+
`);
|
|
422
|
+
let state = createInitialState(ast);
|
|
423
|
+
state = runUntilPause(state);
|
|
424
|
+
|
|
425
|
+
// Should be waiting for AI response
|
|
426
|
+
expect(state.status).toBe('awaiting_ai');
|
|
427
|
+
expect(state.pendingAI?.prompt).toBe('What is 2+2?');
|
|
428
|
+
expect(state.pendingAI?.model).toBe('m'); // Model param name inside function
|
|
429
|
+
|
|
430
|
+
// Resume with mock response
|
|
431
|
+
state = resumeWithAIResponse(state, 'The answer is 4');
|
|
432
|
+
state = runUntilPause(state);
|
|
433
|
+
|
|
434
|
+
expect(state.status).toBe('completed');
|
|
435
|
+
expect(state.callStack[0].locals['answer']?.value).toBe('The answer is 4');
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test('function with two model parameters used in different do expressions', () => {
|
|
439
|
+
const ast = parse(`
|
|
440
|
+
model guesser = {
|
|
441
|
+
name: "claude-sonnet-4-20250514",
|
|
442
|
+
apiKey: "key1",
|
|
443
|
+
provider: "anthropic"
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
model answerer = {
|
|
447
|
+
name: "gpt-4",
|
|
448
|
+
apiKey: "key2",
|
|
449
|
+
provider: "openai"
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function playRound(g: model, a: model, category: text): text {
|
|
453
|
+
let question = do "Ask a question about {category}" g default
|
|
454
|
+
let answer = do "Answer: {question}" a default
|
|
455
|
+
return answer
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
let result = playRound(guesser, answerer, "animals")
|
|
459
|
+
`);
|
|
460
|
+
let state = createInitialState(ast);
|
|
461
|
+
state = runUntilPause(state);
|
|
462
|
+
|
|
463
|
+
// First AI call uses guesser model (param name 'g')
|
|
464
|
+
expect(state.status).toBe('awaiting_ai');
|
|
465
|
+
expect(state.pendingAI?.model).toBe('g');
|
|
466
|
+
|
|
467
|
+
state = resumeWithAIResponse(state, 'Is a penguin a bird?');
|
|
468
|
+
state = runUntilPause(state);
|
|
469
|
+
|
|
470
|
+
// Second AI call uses answerer model (param name 'a')
|
|
471
|
+
expect(state.status).toBe('awaiting_ai');
|
|
472
|
+
expect(state.pendingAI?.model).toBe('a');
|
|
473
|
+
|
|
474
|
+
state = resumeWithAIResponse(state, 'Yes, a penguin is a bird.');
|
|
475
|
+
state = runUntilPause(state);
|
|
476
|
+
|
|
477
|
+
expect(state.status).toBe('completed');
|
|
478
|
+
expect(state.callStack[0].locals['result']?.value).toBe('Yes, a penguin is a bird.');
|
|
479
|
+
});
|
|
338
480
|
});
|