@orcalang/orca-lang 0.1.0
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/LICENSE +176 -0
- package/README.md +128 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +6 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/lock.d.ts +2 -0
- package/dist/auth/lock.d.ts.map +1 -0
- package/dist/auth/lock.js +59 -0
- package/dist/auth/lock.js.map +1 -0
- package/dist/auth/providers/anthropic.d.ts +14 -0
- package/dist/auth/providers/anthropic.d.ts.map +1 -0
- package/dist/auth/providers/anthropic.js +145 -0
- package/dist/auth/providers/anthropic.js.map +1 -0
- package/dist/auth/providers/index.d.ts +3 -0
- package/dist/auth/providers/index.d.ts.map +1 -0
- package/dist/auth/providers/index.js +3 -0
- package/dist/auth/providers/index.js.map +1 -0
- package/dist/auth/providers/minimax.d.ts +6 -0
- package/dist/auth/providers/minimax.d.ts.map +1 -0
- package/dist/auth/providers/minimax.js +65 -0
- package/dist/auth/providers/minimax.js.map +1 -0
- package/dist/auth/refresh.d.ts +8 -0
- package/dist/auth/refresh.d.ts.map +1 -0
- package/dist/auth/refresh.js +104 -0
- package/dist/auth/refresh.js.map +1 -0
- package/dist/auth/store.d.ts +11 -0
- package/dist/auth/store.d.ts.map +1 -0
- package/dist/auth/store.js +63 -0
- package/dist/auth/store.js.map +1 -0
- package/dist/auth/types.d.ts +51 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +2 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/compiler/mermaid.d.ts +3 -0
- package/dist/compiler/mermaid.d.ts.map +1 -0
- package/dist/compiler/mermaid.js +86 -0
- package/dist/compiler/mermaid.js.map +1 -0
- package/dist/compiler/xstate.d.ts +15 -0
- package/dist/compiler/xstate.d.ts.map +1 -0
- package/dist/compiler/xstate.js +542 -0
- package/dist/compiler/xstate.js.map +1 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +3 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/loader.d.ts +4 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +109 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/types.d.ts +13 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +8 -0
- package/dist/config/types.js.map +1 -0
- package/dist/generators/index.d.ts +5 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +5 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/registry.d.ts +12 -0
- package/dist/generators/registry.d.ts.map +1 -0
- package/dist/generators/registry.js +15 -0
- package/dist/generators/registry.js.map +1 -0
- package/dist/generators/typescript.d.ts +9 -0
- package/dist/generators/typescript.d.ts.map +1 -0
- package/dist/generators/typescript.js +55 -0
- package/dist/generators/typescript.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +630 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/anthropic.d.ts +14 -0
- package/dist/llm/anthropic.d.ts.map +1 -0
- package/dist/llm/anthropic.js +87 -0
- package/dist/llm/anthropic.js.map +1 -0
- package/dist/llm/grok.d.ts +13 -0
- package/dist/llm/grok.d.ts.map +1 -0
- package/dist/llm/grok.js +60 -0
- package/dist/llm/grok.js.map +1 -0
- package/dist/llm/index.d.ts +11 -0
- package/dist/llm/index.d.ts.map +1 -0
- package/dist/llm/index.js +23 -0
- package/dist/llm/index.js.map +1 -0
- package/dist/llm/ollama.d.ts +11 -0
- package/dist/llm/ollama.d.ts.map +1 -0
- package/dist/llm/ollama.js +51 -0
- package/dist/llm/ollama.js.map +1 -0
- package/dist/llm/openai.d.ts +13 -0
- package/dist/llm/openai.d.ts.map +1 -0
- package/dist/llm/openai.js +61 -0
- package/dist/llm/openai.js.map +1 -0
- package/dist/llm/provider.d.ts +32 -0
- package/dist/llm/provider.d.ts.map +1 -0
- package/dist/llm/provider.js +2 -0
- package/dist/llm/provider.js.map +1 -0
- package/dist/parser/ast-to-markdown.d.ts +3 -0
- package/dist/parser/ast-to-markdown.d.ts.map +1 -0
- package/dist/parser/ast-to-markdown.js +209 -0
- package/dist/parser/ast-to-markdown.js.map +1 -0
- package/dist/parser/ast.d.ts +183 -0
- package/dist/parser/ast.d.ts.map +1 -0
- package/dist/parser/ast.js +3 -0
- package/dist/parser/ast.js.map +1 -0
- package/dist/parser/markdown-parser.d.ts +8 -0
- package/dist/parser/markdown-parser.d.ts.map +1 -0
- package/dist/parser/markdown-parser.js +838 -0
- package/dist/parser/markdown-parser.js.map +1 -0
- package/dist/runtime/effects.d.ts +17 -0
- package/dist/runtime/effects.d.ts.map +1 -0
- package/dist/runtime/effects.js +28 -0
- package/dist/runtime/effects.js.map +1 -0
- package/dist/runtime/machine.d.ts +8 -0
- package/dist/runtime/machine.d.ts.map +1 -0
- package/dist/runtime/machine.js +158 -0
- package/dist/runtime/machine.js.map +1 -0
- package/dist/runtime/types.d.ts +37 -0
- package/dist/runtime/types.d.ts.map +1 -0
- package/dist/runtime/types.js +3 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/skills.d.ts +114 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +1103 -0
- package/dist/skills.js.map +1 -0
- package/dist/tools.d.ts +18 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +124 -0
- package/dist/tools.js.map +1 -0
- package/dist/verifier/completeness.d.ts +4 -0
- package/dist/verifier/completeness.d.ts.map +1 -0
- package/dist/verifier/completeness.js +82 -0
- package/dist/verifier/completeness.js.map +1 -0
- package/dist/verifier/determinism.d.ts +17 -0
- package/dist/verifier/determinism.d.ts.map +1 -0
- package/dist/verifier/determinism.js +301 -0
- package/dist/verifier/determinism.js.map +1 -0
- package/dist/verifier/properties.d.ts +6 -0
- package/dist/verifier/properties.d.ts.map +1 -0
- package/dist/verifier/properties.js +404 -0
- package/dist/verifier/properties.js.map +1 -0
- package/dist/verifier/structural.d.ts +50 -0
- package/dist/verifier/structural.d.ts.map +1 -0
- package/dist/verifier/structural.js +692 -0
- package/dist/verifier/structural.js.map +1 -0
- package/dist/verifier/types.d.ts +40 -0
- package/dist/verifier/types.d.ts.map +1 -0
- package/dist/verifier/types.js +2 -0
- package/dist/verifier/types.js.map +1 -0
- package/package.json +49 -0
- package/src/auth/index.ts +5 -0
- package/src/auth/lock.ts +71 -0
- package/src/auth/providers/anthropic.ts +192 -0
- package/src/auth/providers/index.ts +17 -0
- package/src/auth/providers/minimax.ts +100 -0
- package/src/auth/refresh.ts +138 -0
- package/src/auth/store.ts +75 -0
- package/src/auth/types.ts +62 -0
- package/src/compiler/mermaid.ts +109 -0
- package/src/compiler/xstate.ts +615 -0
- package/src/config/index.ts +2 -0
- package/src/config/loader.ts +122 -0
- package/src/config/types.ts +21 -0
- package/src/generators/index.ts +6 -0
- package/src/generators/registry.ts +27 -0
- package/src/generators/typescript.ts +67 -0
- package/src/index.ts +671 -0
- package/src/llm/anthropic.ts +102 -0
- package/src/llm/grok.ts +73 -0
- package/src/llm/index.ts +29 -0
- package/src/llm/ollama.ts +62 -0
- package/src/llm/openai.ts +74 -0
- package/src/llm/provider.ts +35 -0
- package/src/parser/ast-to-markdown.ts +220 -0
- package/src/parser/ast.ts +236 -0
- package/src/parser/markdown-parser.ts +844 -0
- package/src/runtime/effects.ts +48 -0
- package/src/runtime/machine.ts +201 -0
- package/src/runtime/types.ts +44 -0
- package/src/skills.ts +1339 -0
- package/src/tools.ts +144 -0
- package/src/verifier/completeness.ts +89 -0
- package/src/verifier/determinism.ts +328 -0
- package/src/verifier/properties.ts +507 -0
- package/src/verifier/structural.ts +803 -0
- package/src/verifier/types.ts +45 -0
package/dist/skills.js
ADDED
|
@@ -0,0 +1,1103 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { parseMarkdown } from './parser/markdown-parser.js';
|
|
3
|
+
import { checkStructural, analyzeFile } from './verifier/structural.js';
|
|
4
|
+
import { checkCompleteness } from './verifier/completeness.js';
|
|
5
|
+
import { checkDeterminism } from './verifier/determinism.js';
|
|
6
|
+
import { checkProperties } from './verifier/properties.js';
|
|
7
|
+
import { compileToXState } from './compiler/xstate.js';
|
|
8
|
+
import { compileToMermaid } from './compiler/mermaid.js';
|
|
9
|
+
import { loadConfig } from './config/index.js';
|
|
10
|
+
import { createProvider } from './llm/index.js';
|
|
11
|
+
import { getCodeGenerator } from './generators/index.js';
|
|
12
|
+
function resolveSource(input) {
|
|
13
|
+
if (input.source !== undefined)
|
|
14
|
+
return input.source;
|
|
15
|
+
if (input.file !== undefined)
|
|
16
|
+
return readFileSync(input.file, 'utf-8');
|
|
17
|
+
throw new Error('SkillInput requires either source or file');
|
|
18
|
+
}
|
|
19
|
+
function resolveLabel(input) {
|
|
20
|
+
return input.file ?? '<source>';
|
|
21
|
+
}
|
|
22
|
+
/** Parse an Orca machine definition (single-machine only). */
|
|
23
|
+
function parseSource(label, source) {
|
|
24
|
+
const { file } = parseMarkdown(source);
|
|
25
|
+
if (file.machines.length > 1) {
|
|
26
|
+
throw new Error(`${label} contains multiple machines.`);
|
|
27
|
+
}
|
|
28
|
+
return file.machines[0];
|
|
29
|
+
}
|
|
30
|
+
function collectStateNames(states) {
|
|
31
|
+
const names = [];
|
|
32
|
+
for (const s of states) {
|
|
33
|
+
names.push(s.name);
|
|
34
|
+
if (s.contains)
|
|
35
|
+
names.push(...collectStateNames(s.contains));
|
|
36
|
+
if (s.parallel) {
|
|
37
|
+
for (const region of s.parallel.regions) {
|
|
38
|
+
names.push(...collectStateNames(region.states));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return names;
|
|
43
|
+
}
|
|
44
|
+
function serializeType(type) {
|
|
45
|
+
switch (type.kind) {
|
|
46
|
+
case 'string': return 'string';
|
|
47
|
+
case 'int': return 'int';
|
|
48
|
+
case 'decimal': return 'decimal';
|
|
49
|
+
case 'bool': return 'bool';
|
|
50
|
+
case 'array': return `${type.elementType}[]`;
|
|
51
|
+
case 'map': return `Map<${type.keyType}, ${type.valueType}>`;
|
|
52
|
+
case 'optional': return `${type.innerType}?`;
|
|
53
|
+
case 'custom': return type.name;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function serializeGuardExpression(expr) {
|
|
57
|
+
switch (expr.kind) {
|
|
58
|
+
case 'true': return 'true';
|
|
59
|
+
case 'false': return 'false';
|
|
60
|
+
case 'not': return `!(${serializeGuardExpression(expr.expr)})`;
|
|
61
|
+
case 'and': return `(${serializeGuardExpression(expr.left)} && ${serializeGuardExpression(expr.right)})`;
|
|
62
|
+
case 'or': return `(${serializeGuardExpression(expr.left)} || ${serializeGuardExpression(expr.right)})`;
|
|
63
|
+
case 'compare': {
|
|
64
|
+
const opMap = { eq: '==', ne: '!=', lt: '<', gt: '>', le: '<=', ge: '>=' };
|
|
65
|
+
const leftPath = expr.left.path.join('.');
|
|
66
|
+
const left = leftPath.startsWith('ctx.') ? leftPath : `ctx.${leftPath}`;
|
|
67
|
+
const right = expr.right.type === 'string'
|
|
68
|
+
? `"${expr.right.value}"`
|
|
69
|
+
: String(expr.right.value);
|
|
70
|
+
return `${left} ${opMap[expr.op]} ${right}`;
|
|
71
|
+
}
|
|
72
|
+
case 'nullcheck': {
|
|
73
|
+
const rawPath = expr.expr.path.join('.');
|
|
74
|
+
const path = rawPath.startsWith('ctx.') ? rawPath : `ctx.${rawPath}`;
|
|
75
|
+
return expr.isNull ? `${path} == null` : `${path} != null`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function machineToParseResult(machine) {
|
|
80
|
+
return {
|
|
81
|
+
name: machine.name,
|
|
82
|
+
states: collectStateNames(machine.states),
|
|
83
|
+
events: machine.events.map(e => e.name),
|
|
84
|
+
transitions: machine.transitions.map(t => ({
|
|
85
|
+
source: t.source,
|
|
86
|
+
event: t.event,
|
|
87
|
+
guard: t.guard
|
|
88
|
+
? (t.guard.negated ? `!${t.guard.name}` : t.guard.name)
|
|
89
|
+
: undefined,
|
|
90
|
+
target: t.target,
|
|
91
|
+
action: t.action,
|
|
92
|
+
})),
|
|
93
|
+
guards: machine.guards.map(g => ({
|
|
94
|
+
name: g.name,
|
|
95
|
+
expression: serializeGuardExpression(g.expression),
|
|
96
|
+
})),
|
|
97
|
+
actions: machine.actions.map(a => ({
|
|
98
|
+
name: a.name,
|
|
99
|
+
hasEffect: a.hasEffect,
|
|
100
|
+
effectType: a.effectType,
|
|
101
|
+
})),
|
|
102
|
+
effects: machine.effects?.map(e => ({
|
|
103
|
+
name: e.name,
|
|
104
|
+
input: e.input,
|
|
105
|
+
output: e.output,
|
|
106
|
+
})),
|
|
107
|
+
context: machine.context.map(f => ({
|
|
108
|
+
name: f.name,
|
|
109
|
+
type: serializeType(f.type),
|
|
110
|
+
default: f.defaultValue,
|
|
111
|
+
})),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export function parseSkill(input) {
|
|
115
|
+
const source = resolveSource(input);
|
|
116
|
+
try {
|
|
117
|
+
const { file } = parseMarkdown(source);
|
|
118
|
+
const machines = file.machines.map(machineToParseResult);
|
|
119
|
+
return {
|
|
120
|
+
status: 'success',
|
|
121
|
+
machines,
|
|
122
|
+
machine: machines[0],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
return {
|
|
127
|
+
status: 'error',
|
|
128
|
+
error: err instanceof Error ? err.message : String(err),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// ── /verify-machine ───────────────────────────────────────────────────────────
|
|
133
|
+
export async function verifySkill(input) {
|
|
134
|
+
const source = resolveSource(input);
|
|
135
|
+
const label = resolveLabel(input);
|
|
136
|
+
let machine;
|
|
137
|
+
try {
|
|
138
|
+
machine = parseSource(label, source);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
// Parse error - return as verification error
|
|
142
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
143
|
+
return {
|
|
144
|
+
status: 'invalid',
|
|
145
|
+
machine: extractMachineNameFromSource(source) || 'unknown',
|
|
146
|
+
states: 0,
|
|
147
|
+
events: 0,
|
|
148
|
+
transitions: 0,
|
|
149
|
+
errors: [{
|
|
150
|
+
code: 'PARSE_ERROR',
|
|
151
|
+
message,
|
|
152
|
+
severity: 'error',
|
|
153
|
+
suggestion: 'Check Orca syntax - ensure proper formatting for .orca or .orca.md files',
|
|
154
|
+
}],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const structural = checkStructural(machine);
|
|
158
|
+
const completeness = checkCompleteness(machine);
|
|
159
|
+
const determinism = checkDeterminism(machine);
|
|
160
|
+
const properties = checkProperties(machine);
|
|
161
|
+
const mapError = (e) => ({
|
|
162
|
+
code: e.code,
|
|
163
|
+
message: e.message,
|
|
164
|
+
severity: e.severity,
|
|
165
|
+
location: e.location ? {
|
|
166
|
+
state: e.location.state,
|
|
167
|
+
event: e.location.event,
|
|
168
|
+
} : undefined,
|
|
169
|
+
suggestion: e.suggestion,
|
|
170
|
+
});
|
|
171
|
+
const allErrors = [
|
|
172
|
+
...structural.errors.map(mapError),
|
|
173
|
+
...completeness.errors.map(mapError),
|
|
174
|
+
...determinism.errors.map(mapError),
|
|
175
|
+
...properties.errors.map(mapError),
|
|
176
|
+
];
|
|
177
|
+
return {
|
|
178
|
+
status: allErrors.some(e => e.severity === 'error') ? 'invalid' : 'valid',
|
|
179
|
+
machine: machine.name,
|
|
180
|
+
states: machine.states.length,
|
|
181
|
+
events: machine.events.length,
|
|
182
|
+
transitions: machine.transitions.length,
|
|
183
|
+
errors: allErrors,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
export async function compileSkill(input, target) {
|
|
187
|
+
const source = resolveSource(input);
|
|
188
|
+
const machine = parseSource(resolveLabel(input), source);
|
|
189
|
+
// Run verification to get warnings
|
|
190
|
+
const structural = checkStructural(machine);
|
|
191
|
+
const completeness = checkCompleteness(machine);
|
|
192
|
+
const determinism = checkDeterminism(machine);
|
|
193
|
+
const warnings = [
|
|
194
|
+
...structural.errors.filter(e => e.severity === 'warning').map(e => ({
|
|
195
|
+
code: e.code,
|
|
196
|
+
message: e.message,
|
|
197
|
+
severity: e.severity,
|
|
198
|
+
})),
|
|
199
|
+
...completeness.errors.filter(e => e.severity === 'warning').map(e => ({
|
|
200
|
+
code: e.code,
|
|
201
|
+
message: e.message,
|
|
202
|
+
severity: e.severity,
|
|
203
|
+
})),
|
|
204
|
+
...determinism.errors.filter(e => e.severity === 'warning').map(e => ({
|
|
205
|
+
code: e.code,
|
|
206
|
+
message: e.message,
|
|
207
|
+
severity: e.severity,
|
|
208
|
+
})),
|
|
209
|
+
];
|
|
210
|
+
const output = target === 'xstate'
|
|
211
|
+
? compileToXState(machine)
|
|
212
|
+
: compileToMermaid(machine);
|
|
213
|
+
return {
|
|
214
|
+
status: 'success',
|
|
215
|
+
target,
|
|
216
|
+
output,
|
|
217
|
+
warnings,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
export async function generateActionsSkill(input, language = 'typescript', useLLM = false, configPath, generateTests = false) {
|
|
221
|
+
const source = resolveSource(input);
|
|
222
|
+
const machine = parseSource(resolveLabel(input), source);
|
|
223
|
+
const actions = machine.actions.map(action => ({
|
|
224
|
+
name: action.name,
|
|
225
|
+
signature: `${action.name}(${action.parameters.join(', ')}) -> ${action.returnType}${action.hasEffect ? ` + Effect<${action.effectType}>` : ''}`,
|
|
226
|
+
parameters: action.parameters,
|
|
227
|
+
returnType: action.returnType,
|
|
228
|
+
hasEffect: action.hasEffect,
|
|
229
|
+
effectType: action.effectType,
|
|
230
|
+
context_used: extractContextFields(machine, action.name),
|
|
231
|
+
}));
|
|
232
|
+
let scaffolds = {};
|
|
233
|
+
let tests = {};
|
|
234
|
+
if (useLLM) {
|
|
235
|
+
// Use LLM to generate action implementations
|
|
236
|
+
const config = loadConfig(configPath);
|
|
237
|
+
const provider = createProvider(config.provider, {
|
|
238
|
+
api_key: config.api_key,
|
|
239
|
+
base_url: config.base_url,
|
|
240
|
+
model: config.model,
|
|
241
|
+
max_tokens: config.max_tokens,
|
|
242
|
+
temperature: config.temperature,
|
|
243
|
+
});
|
|
244
|
+
scaffolds = await generateWithLLM(provider, actions, machine, language);
|
|
245
|
+
if (generateTests) {
|
|
246
|
+
tests = await generateUnitTests(provider, actions, machine, language);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
// Use template-based scaffold generation
|
|
251
|
+
for (const action of machine.actions) {
|
|
252
|
+
scaffolds[action.name] = generateActionScaffold(action, machine, language);
|
|
253
|
+
}
|
|
254
|
+
if (generateTests) {
|
|
255
|
+
tests = generateTemplateTests(actions, machine, language);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
status: 'success',
|
|
260
|
+
machine: machine.name,
|
|
261
|
+
actions,
|
|
262
|
+
scaffolds,
|
|
263
|
+
tests: Object.keys(tests).length > 0 ? tests : undefined,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
async function generateWithLLM(provider, actions, machine, language) {
|
|
267
|
+
const generator = getCodeGenerator(language);
|
|
268
|
+
const scaffolds = {};
|
|
269
|
+
const systemPrompt = `You are an expert ${language} developer specializing in state machine action implementations.
|
|
270
|
+
Given a machine definition and action signatures, generate complete action implementations.
|
|
271
|
+
Follow the type signatures exactly. Use the provided context fields.
|
|
272
|
+
If an action has an effect, return [newContext, effect] tuple.`;
|
|
273
|
+
for (const action of actions) {
|
|
274
|
+
const userPrompt = `Machine: ${machine.name}
|
|
275
|
+
Context fields: ${machine.context.map(f => `${f.name}: ${f.type || 'unknown'}`).join(', ')}
|
|
276
|
+
|
|
277
|
+
Action: ${action.signature}
|
|
278
|
+
Description: ${action.name}${action.hasEffect ? ` (effect type: ${action.effectType})` : ''}
|
|
279
|
+
|
|
280
|
+
Generate the implementation:`;
|
|
281
|
+
try {
|
|
282
|
+
const response = await provider.complete({
|
|
283
|
+
messages: [
|
|
284
|
+
{ role: 'system', content: systemPrompt },
|
|
285
|
+
{ role: 'user', content: userPrompt },
|
|
286
|
+
],
|
|
287
|
+
model: '',
|
|
288
|
+
max_tokens: 2048,
|
|
289
|
+
temperature: 0.5,
|
|
290
|
+
});
|
|
291
|
+
scaffolds[action.name] = response.content;
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
console.error(`LLM error for action ${action.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
295
|
+
// Fall back to scaffold on error
|
|
296
|
+
scaffolds[action.name] = generateActionScaffold(action, machine, language);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return scaffolds;
|
|
300
|
+
}
|
|
301
|
+
async function generateUnitTests(provider, actions, machine, language) {
|
|
302
|
+
const tests = {};
|
|
303
|
+
const systemPrompt = `You are an expert in ${language} testing. Generate unit tests for state machine actions.
|
|
304
|
+
Each test should verify the action's behavior with specific input contexts.
|
|
305
|
+
Use a testing framework appropriate for ${language}.`;
|
|
306
|
+
for (const action of actions) {
|
|
307
|
+
const contextFields = machine.context.map(f => `${f.name}: ${f.type || 'unknown'}`).join(', ');
|
|
308
|
+
const userPrompt = `Machine: ${machine.name}
|
|
309
|
+
Context type: { ${contextFields} }
|
|
310
|
+
Action: ${action.signature}
|
|
311
|
+
Context fields used: ${action.context_used.join(', ') || 'all fields'}
|
|
312
|
+
|
|
313
|
+
Generate unit tests that verify:
|
|
314
|
+
1. The action transforms context correctly
|
|
315
|
+
2. All context fields are preserved (if not modified)
|
|
316
|
+
3. Edge cases for the specific action
|
|
317
|
+
|
|
318
|
+
Format: Provide test code in a code fence.`;
|
|
319
|
+
try {
|
|
320
|
+
const response = await provider.complete({
|
|
321
|
+
messages: [
|
|
322
|
+
{ role: 'system', content: systemPrompt },
|
|
323
|
+
{ role: 'user', content: userPrompt },
|
|
324
|
+
],
|
|
325
|
+
model: '',
|
|
326
|
+
max_tokens: 2048,
|
|
327
|
+
temperature: 0.3,
|
|
328
|
+
});
|
|
329
|
+
tests[action.name] = response.content;
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
console.error(`Test generation error for action ${action.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
333
|
+
// Fall back to template tests on error
|
|
334
|
+
tests[action.name] = generateTemplateTestsForAction(action, machine);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return tests;
|
|
338
|
+
}
|
|
339
|
+
function generateTemplateTests(actions, machine, language = 'typescript') {
|
|
340
|
+
const tests = {};
|
|
341
|
+
for (const action of actions) {
|
|
342
|
+
tests[action.name] = generateTemplateTestsForAction(action, machine, language);
|
|
343
|
+
}
|
|
344
|
+
return tests;
|
|
345
|
+
}
|
|
346
|
+
function generateTemplateTestsForAction(action, machine, language = 'typescript') {
|
|
347
|
+
if (language === 'python') {
|
|
348
|
+
return generatePythonTestScaffold(action, machine);
|
|
349
|
+
}
|
|
350
|
+
if (language === 'go') {
|
|
351
|
+
return generateGoTestScaffold(action, machine);
|
|
352
|
+
}
|
|
353
|
+
return generateTypeScriptTestScaffold(action, machine);
|
|
354
|
+
}
|
|
355
|
+
function generateTypeScriptTestScaffold(action, machine) {
|
|
356
|
+
const ctxFields = machine.context.map(f => {
|
|
357
|
+
return ` ${f.name}: ${getDefaultValueForType(f.type, 'typescript')}`;
|
|
358
|
+
}).join(',\n');
|
|
359
|
+
const contextUsed = action.context_used.length > 0 ? action.context_used : machine.context.map(f => f.name);
|
|
360
|
+
if (action.hasEffect) {
|
|
361
|
+
return `// Tests for ${action.name}
|
|
362
|
+
describe('${action.name}', () => {
|
|
363
|
+
const defaultContext = {
|
|
364
|
+
${ctxFields}
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
it('should return updated context and emit effect', () => {
|
|
368
|
+
const ctx = { ...defaultContext };
|
|
369
|
+
const result = ${action.name}(ctx);
|
|
370
|
+
|
|
371
|
+
expect(result).toBeDefined();
|
|
372
|
+
expect(Array.isArray(result)).toBe(true);
|
|
373
|
+
expect(result[1]).toHaveProperty('type');
|
|
374
|
+
expect(result[1].type).toBe('${action.effectType}');
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('should preserve unmodified context fields', () => {
|
|
378
|
+
const ctx = { ...defaultContext };
|
|
379
|
+
const [newCtx] = ${action.name}(ctx);
|
|
380
|
+
|
|
381
|
+
${contextUsed.filter(f => !actionModifiesField(action, f)).map(f => `expect(newCtx.${f}).toBe(ctx.${f});`).join('\n ')}
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
`;
|
|
385
|
+
}
|
|
386
|
+
return `// Tests for ${action.name}
|
|
387
|
+
describe('${action.name}', () => {
|
|
388
|
+
const defaultContext = {
|
|
389
|
+
${ctxFields}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
it('should transform context correctly', () => {
|
|
393
|
+
const ctx = { ...defaultContext };
|
|
394
|
+
const result = ${action.name}(ctx);
|
|
395
|
+
|
|
396
|
+
expect(result).toBeDefined();
|
|
397
|
+
expect(typeof result).toBe('object');
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('should preserve unmodified context fields', () => {
|
|
401
|
+
const ctx = { ...defaultContext };
|
|
402
|
+
const result = ${action.name}(ctx);
|
|
403
|
+
|
|
404
|
+
${contextUsed.filter(f => !actionModifiesField(action, f)).map(f => `expect(result.${f}).toBe(ctx.${f});`).join('\n ')}
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
`;
|
|
408
|
+
}
|
|
409
|
+
function generatePythonTestScaffold(action, machine) {
|
|
410
|
+
const ctxFields = machine.context.map(f => {
|
|
411
|
+
return ` "${f.name}": ${getDefaultValueForType(f.type, 'python')}`;
|
|
412
|
+
}).join(',\n');
|
|
413
|
+
const contextUsed = action.context_used.length > 0 ? action.context_used : machine.context.map(f => f.name);
|
|
414
|
+
const preserved = contextUsed.filter(f => !actionModifiesField(action, f));
|
|
415
|
+
if (action.hasEffect) {
|
|
416
|
+
return `# Tests for ${action.name}
|
|
417
|
+
import pytest
|
|
418
|
+
from orca_runtime_python import Effect, EffectResult, EffectStatus
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def make_context():
|
|
422
|
+
return {
|
|
423
|
+
${ctxFields}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
@pytest.mark.asyncio
|
|
428
|
+
async def test_${action.name}_executes_effect():
|
|
429
|
+
effect = Effect(
|
|
430
|
+
type="${action.effectType}",
|
|
431
|
+
payload={"action": "${action.name}", "context": make_context(), "event": None},
|
|
432
|
+
)
|
|
433
|
+
result = await ${action.name}(effect)
|
|
434
|
+
assert result.status == EffectStatus.SUCCESS
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
@pytest.mark.asyncio
|
|
438
|
+
async def test_${action.name}_returns_effect_result():
|
|
439
|
+
effect = Effect(
|
|
440
|
+
type="${action.effectType}",
|
|
441
|
+
payload={"action": "${action.name}", "context": make_context(), "event": None},
|
|
442
|
+
)
|
|
443
|
+
result = await ${action.name}(effect)
|
|
444
|
+
assert isinstance(result, EffectResult)
|
|
445
|
+
`;
|
|
446
|
+
}
|
|
447
|
+
return `# Tests for ${action.name}
|
|
448
|
+
import pytest
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def make_context():
|
|
452
|
+
return {
|
|
453
|
+
${ctxFields}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
@pytest.mark.asyncio
|
|
458
|
+
async def test_${action.name}_transforms_context():
|
|
459
|
+
ctx = make_context()
|
|
460
|
+
result = await ${action.name}(ctx)
|
|
461
|
+
assert isinstance(result, dict)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
@pytest.mark.asyncio
|
|
465
|
+
async def test_${action.name}_preserves_fields():
|
|
466
|
+
ctx = make_context()
|
|
467
|
+
result = await ${action.name}(ctx)
|
|
468
|
+
${preserved.map(f => ` assert result["${f}"] == ctx["${f}"]`).join('\n')}
|
|
469
|
+
`;
|
|
470
|
+
}
|
|
471
|
+
function generateGoTestScaffold(action, machine) {
|
|
472
|
+
const fnName = toPascalCase(action.name);
|
|
473
|
+
const ctxFields = machine.context.map(f => {
|
|
474
|
+
return `\t\t"${f.name}": ${getDefaultValueForType(f.type, 'go')}`;
|
|
475
|
+
}).join(',\n');
|
|
476
|
+
const contextUsed = action.context_used.length > 0 ? action.context_used : machine.context.map(f => f.name);
|
|
477
|
+
const preserved = contextUsed.filter(f => !actionModifiesField(action, f));
|
|
478
|
+
if (action.hasEffect) {
|
|
479
|
+
return `// Tests for ${action.name}
|
|
480
|
+
package actions_test
|
|
481
|
+
|
|
482
|
+
import (
|
|
483
|
+
\t"testing"
|
|
484
|
+
\torca "github.com/jascal/orca-lang/packages/runtime-go/orca_runtime_go"
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
func Test${fnName}(t *testing.T) {
|
|
488
|
+
\tctx := orca.Context{
|
|
489
|
+
${ctxFields},
|
|
490
|
+
\t}
|
|
491
|
+
\teffect := orca.Effect{
|
|
492
|
+
\t\tType: "${action.effectType}",
|
|
493
|
+
\t\tPayload: map[string]any{"action": "${action.name}", "context": ctx},
|
|
494
|
+
\t}
|
|
495
|
+
\tresult := ${fnName}(effect)
|
|
496
|
+
\tif result.Status != orca.EffectStatusSuccess {
|
|
497
|
+
\t\tt.Errorf("expected success, got %v: %s", result.Status, result.Error)
|
|
498
|
+
\t}
|
|
499
|
+
}
|
|
500
|
+
`;
|
|
501
|
+
}
|
|
502
|
+
return `// Tests for ${action.name}
|
|
503
|
+
package actions_test
|
|
504
|
+
|
|
505
|
+
import (
|
|
506
|
+
\t"testing"
|
|
507
|
+
\torca "github.com/jascal/orca-lang/packages/runtime-go/orca_runtime_go"
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
func Test${fnName}(t *testing.T) {
|
|
511
|
+
\tctx := orca.Context{
|
|
512
|
+
${ctxFields},
|
|
513
|
+
\t}
|
|
514
|
+
\tevent := map[string]any{"type": "test"}
|
|
515
|
+
\tresult := ${fnName}(ctx, event)
|
|
516
|
+
\tif result == nil {
|
|
517
|
+
\t\tt.Fatal("${fnName} returned nil context")
|
|
518
|
+
\t}
|
|
519
|
+
${preserved.map(f => `\tif result["${f}"] != ctx["${f}"] {\n\t\tt.Errorf("${f}: got %v, want %v", result["${f}"], ctx["${f}"])\n\t}`).join('\n')}
|
|
520
|
+
}
|
|
521
|
+
`;
|
|
522
|
+
}
|
|
523
|
+
function actionModifiesField(action, fieldName) {
|
|
524
|
+
// Heuristic: if the action name suggests modification of a field, it likely modifies it
|
|
525
|
+
const modifiers = ['increment', 'decrement', 'set', 'update', 'add', 'remove', 'clear', 'reset', 'toggle'];
|
|
526
|
+
const name = action.name.toLowerCase();
|
|
527
|
+
for (const mod of modifiers) {
|
|
528
|
+
if (name.includes(mod) && name.includes(fieldName)) {
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
// Check if it's a setter-style action
|
|
533
|
+
if (name.startsWith('set_') && fieldName === name.replace('set_', '')) {
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
function getDefaultValueForType(type, language = 'typescript') {
|
|
539
|
+
if (!type) {
|
|
540
|
+
return language === 'python' ? '""' : language === 'go' ? '""' : "''";
|
|
541
|
+
}
|
|
542
|
+
if (typeof type === 'object' && 'kind' in type) {
|
|
543
|
+
if (language === 'python') {
|
|
544
|
+
switch (type.kind) {
|
|
545
|
+
case 'string': return '""';
|
|
546
|
+
case 'int': return '0';
|
|
547
|
+
case 'decimal': return '0.0';
|
|
548
|
+
case 'bool': return 'False';
|
|
549
|
+
case 'optional': return 'None';
|
|
550
|
+
case 'array': return '[]';
|
|
551
|
+
case 'map': return '{}';
|
|
552
|
+
case 'custom': return 'None';
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
else if (language === 'go') {
|
|
556
|
+
switch (type.kind) {
|
|
557
|
+
case 'string': return '""';
|
|
558
|
+
case 'int': return '0';
|
|
559
|
+
case 'decimal': return '0.0';
|
|
560
|
+
case 'bool': return 'false';
|
|
561
|
+
case 'optional': return 'nil';
|
|
562
|
+
case 'array': return 'nil';
|
|
563
|
+
case 'map': return 'nil';
|
|
564
|
+
case 'custom': return 'nil';
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
switch (type.kind) {
|
|
569
|
+
case 'string': return "''";
|
|
570
|
+
case 'int':
|
|
571
|
+
case 'decimal': return '0';
|
|
572
|
+
case 'bool': return 'false';
|
|
573
|
+
case 'optional': return 'null';
|
|
574
|
+
case 'array': return '[]';
|
|
575
|
+
case 'map': return '{}';
|
|
576
|
+
case 'custom': return 'null';
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return language === 'python' ? 'None' : language === 'go' ? 'nil' : 'null';
|
|
581
|
+
}
|
|
582
|
+
function extractContextFields(machine, actionName) {
|
|
583
|
+
const fields = [];
|
|
584
|
+
// Check transitions for context usage
|
|
585
|
+
for (const t of machine.transitions) {
|
|
586
|
+
if (t.action === actionName) {
|
|
587
|
+
// In a real implementation, this would analyze the guard expressions
|
|
588
|
+
// For now, just return all context fields
|
|
589
|
+
return machine.context.map(f => f.name);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
// Check on_entry/on_exit
|
|
593
|
+
for (const state of machine.states) {
|
|
594
|
+
if (state.onEntry === actionName || state.onExit === actionName) {
|
|
595
|
+
return machine.context.map(f => f.name);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return fields;
|
|
599
|
+
}
|
|
600
|
+
function toPascalCase(snake) {
|
|
601
|
+
return snake.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('');
|
|
602
|
+
}
|
|
603
|
+
function generateActionScaffold(action, machine, language) {
|
|
604
|
+
if (language === 'python') {
|
|
605
|
+
if (action.hasEffect) {
|
|
606
|
+
return `# Action: ${action.name}
|
|
607
|
+
# Effect: ${action.effectType}
|
|
608
|
+
# Register via: bus.register_effect_handler("${action.effectType}", ${action.name})
|
|
609
|
+
|
|
610
|
+
from typing import Any
|
|
611
|
+
from orca_runtime_python import Effect, EffectResult, EffectStatus
|
|
612
|
+
|
|
613
|
+
async def ${action.name}(effect: Effect) -> EffectResult:
|
|
614
|
+
# effect.payload contains {"action": "${action.name}", "context": {...}, "event": None}
|
|
615
|
+
ctx = effect.payload.get("context", {})
|
|
616
|
+
# TODO: Implement effect
|
|
617
|
+
return EffectResult(status=EffectStatus.SUCCESS, data={})
|
|
618
|
+
`;
|
|
619
|
+
}
|
|
620
|
+
return `# Action: ${action.name}
|
|
621
|
+
# Register via: machine.register_action("${action.name}", ${action.name})
|
|
622
|
+
|
|
623
|
+
from typing import Any
|
|
624
|
+
|
|
625
|
+
async def ${action.name}(ctx: dict[str, Any], event: Any = None) -> dict[str, Any]:
|
|
626
|
+
# TODO: Implement action
|
|
627
|
+
return dict(ctx)
|
|
628
|
+
`;
|
|
629
|
+
}
|
|
630
|
+
if (language === 'go') {
|
|
631
|
+
const fnName = toPascalCase(action.name);
|
|
632
|
+
if (action.hasEffect) {
|
|
633
|
+
return `// Action: ${action.name}
|
|
634
|
+
// Effect: ${action.effectType}
|
|
635
|
+
// Register via: bus.SetEffectHandler(DispatchEffects)
|
|
636
|
+
// Add a case for "${action.effectType}" in your effect dispatcher.
|
|
637
|
+
|
|
638
|
+
func ${fnName}(effect orca.Effect) orca.EffectResult {
|
|
639
|
+
\t// effect.Type == "${action.effectType}"
|
|
640
|
+
\t// effect.Payload contains {"action": "${action.name}", "context": map[string]any{...}}
|
|
641
|
+
\t// TODO: Implement effect
|
|
642
|
+
\treturn orca.EffectResult{Status: orca.EffectStatusSuccess, Data: map[string]any{}}
|
|
643
|
+
}
|
|
644
|
+
`;
|
|
645
|
+
}
|
|
646
|
+
return `// Action: ${action.name}
|
|
647
|
+
// Register via: machine.RegisterAction("${action.name}", ${fnName})
|
|
648
|
+
|
|
649
|
+
func ${fnName}(ctx orca.Context, event map[string]any) map[string]any {
|
|
650
|
+
\t// TODO: Implement action
|
|
651
|
+
\tresult := make(orca.Context)
|
|
652
|
+
\tfor k, v := range ctx {
|
|
653
|
+
\t\tresult[k] = v
|
|
654
|
+
\t}
|
|
655
|
+
\treturn result
|
|
656
|
+
}
|
|
657
|
+
`;
|
|
658
|
+
}
|
|
659
|
+
// TypeScript (default)
|
|
660
|
+
const params = action.parameters.map(p => {
|
|
661
|
+
if (p === 'ctx' || p === 'Context')
|
|
662
|
+
return 'ctx: Context';
|
|
663
|
+
if (p.startsWith('event:'))
|
|
664
|
+
return p;
|
|
665
|
+
return `ctx: Context, ${p}`;
|
|
666
|
+
}).join(', ');
|
|
667
|
+
if (action.hasEffect) {
|
|
668
|
+
return `// Action: ${action.name}
|
|
669
|
+
// Effect: ${action.effectType}
|
|
670
|
+
|
|
671
|
+
export function ${action.name}(${params}): [Context, Effect<${action.effectType}>] {
|
|
672
|
+
// TODO: Implement action
|
|
673
|
+
return [ctx, { type: '${action.effectType}', payload: {} }];
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Guard helper (if needed):
|
|
677
|
+
// export function ${action.name}_guard(ctx: Context): boolean {
|
|
678
|
+
// return true;
|
|
679
|
+
// }
|
|
680
|
+
`;
|
|
681
|
+
}
|
|
682
|
+
return `// Action: ${action.name}
|
|
683
|
+
|
|
684
|
+
export function ${action.name}(ctx: Context): Context {
|
|
685
|
+
// TODO: Implement action
|
|
686
|
+
return { ...ctx };
|
|
687
|
+
}
|
|
688
|
+
`;
|
|
689
|
+
}
|
|
690
|
+
export async function refineSkill(input, errors, configPath, maxIterations = 3) {
|
|
691
|
+
const config = loadConfig(configPath);
|
|
692
|
+
const provider = createProvider(config.provider, {
|
|
693
|
+
api_key: config.api_key,
|
|
694
|
+
base_url: config.base_url,
|
|
695
|
+
model: config.model,
|
|
696
|
+
max_tokens: config.max_tokens,
|
|
697
|
+
temperature: config.temperature,
|
|
698
|
+
});
|
|
699
|
+
const initialSource = resolveSource(input);
|
|
700
|
+
const initialMachine = parseSource(resolveLabel(input), initialSource);
|
|
701
|
+
const isMd = input.source !== undefined || (input.file !== undefined && (input.file.endsWith('.orca.md') || input.file.endsWith('.md')));
|
|
702
|
+
const systemPrompt = `You are an expert in Orca state machine language. Given verification errors, fix the machine definition.
|
|
703
|
+
Output ONLY the corrected Orca machine definition in ${isMd ? 'markdown (.orca.md)' : 'DSL (.orca)'} format, no explanations.`;
|
|
704
|
+
let currentSource = initialSource;
|
|
705
|
+
let currentErrors = errors;
|
|
706
|
+
let machineName = initialMachine.name;
|
|
707
|
+
let machineStates = initialMachine.states.map(s => s.name).join(', ');
|
|
708
|
+
let machineEvents = initialMachine.events.join(', ');
|
|
709
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
710
|
+
const errorList = currentErrors.map(e => `[${e.severity.toUpperCase()}] ${e.code}: ${e.message}`).join('\n');
|
|
711
|
+
const userPrompt = `Machine: ${machineName}
|
|
712
|
+
States: ${machineStates}
|
|
713
|
+
Events: ${machineEvents}
|
|
714
|
+
|
|
715
|
+
Verification Errors:
|
|
716
|
+
${errorList}
|
|
717
|
+
|
|
718
|
+
Machine Definition:
|
|
719
|
+
${currentSource}
|
|
720
|
+
|
|
721
|
+
Provide the corrected machine definition:`;
|
|
722
|
+
try {
|
|
723
|
+
const response = await provider.complete({
|
|
724
|
+
messages: [
|
|
725
|
+
{ role: 'system', content: systemPrompt },
|
|
726
|
+
{ role: 'user', content: userPrompt },
|
|
727
|
+
],
|
|
728
|
+
model: '',
|
|
729
|
+
max_tokens: 4096,
|
|
730
|
+
temperature: 0.3,
|
|
731
|
+
});
|
|
732
|
+
currentSource = stripCodeFence(response.content);
|
|
733
|
+
const verification = await verifySkill({ source: currentSource });
|
|
734
|
+
if (verification.status === 'valid') {
|
|
735
|
+
return {
|
|
736
|
+
status: 'success',
|
|
737
|
+
corrected: currentSource,
|
|
738
|
+
verification,
|
|
739
|
+
iterations: i + 1,
|
|
740
|
+
changes: [`Corrected after ${i + 1} iteration(s)`],
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
currentErrors = verification.errors.filter(e => e.severity === 'error');
|
|
744
|
+
// Update machine metadata for next iteration prompt
|
|
745
|
+
try {
|
|
746
|
+
const m = parseSource('<refined>', currentSource);
|
|
747
|
+
machineName = m.name;
|
|
748
|
+
machineStates = m.states.map(s => s.name).join(', ');
|
|
749
|
+
machineEvents = m.events.join(', ');
|
|
750
|
+
}
|
|
751
|
+
catch {
|
|
752
|
+
// parse error — keep previous metadata, errors already include PARSE_ERROR
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
catch (err) {
|
|
756
|
+
return {
|
|
757
|
+
status: 'error',
|
|
758
|
+
changes: [],
|
|
759
|
+
error: err instanceof Error ? err.message : String(err),
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
const finalVerification = await verifySkill({ source: currentSource });
|
|
764
|
+
return {
|
|
765
|
+
status: 'requires_refinement',
|
|
766
|
+
corrected: currentSource,
|
|
767
|
+
verification: finalVerification,
|
|
768
|
+
iterations: maxIterations,
|
|
769
|
+
changes: [`${maxIterations} iteration(s) attempted but errors remain`],
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
const ORCA_SYNTAX_REFERENCE = `Orca State Machine Markdown Syntax Reference (.orca.md):
|
|
773
|
+
|
|
774
|
+
The machine definition uses standard markdown: headings, tables, bullet lists, and blockquotes.
|
|
775
|
+
|
|
776
|
+
# machine MachineName
|
|
777
|
+
|
|
778
|
+
## context
|
|
779
|
+
|
|
780
|
+
| Field | Type | Default |
|
|
781
|
+
|--------|---------|---------|
|
|
782
|
+
| field1 | string | |
|
|
783
|
+
| field2 | int | 0 |
|
|
784
|
+
| field3 | string? | |
|
|
785
|
+
| field4 | bool | |
|
|
786
|
+
|
|
787
|
+
## events
|
|
788
|
+
|
|
789
|
+
- event1
|
|
790
|
+
- event2
|
|
791
|
+
- event3
|
|
792
|
+
|
|
793
|
+
## state idle [initial]
|
|
794
|
+
> Description of this state
|
|
795
|
+
- on_entry: action_name
|
|
796
|
+
- on_exit: action_name
|
|
797
|
+
- timeout: 5s -> target_state
|
|
798
|
+
- ignore: event1, event2
|
|
799
|
+
|
|
800
|
+
## state done [final]
|
|
801
|
+
> Terminal state
|
|
802
|
+
|
|
803
|
+
## transitions
|
|
804
|
+
|
|
805
|
+
| Source | Event | Guard | Target | Action |
|
|
806
|
+
|--------|--------|--------|--------|---------|
|
|
807
|
+
| idle | event1 | | active | action1 |
|
|
808
|
+
| active | event2 | guard1 | done | action2 |
|
|
809
|
+
| active | event2 | !guard1| idle | |
|
|
810
|
+
|
|
811
|
+
## guards
|
|
812
|
+
|
|
813
|
+
| Name | Expression |
|
|
814
|
+
|--------|---------------------------|
|
|
815
|
+
| guard1 | \`ctx.field2 > 10\` |
|
|
816
|
+
| guard2 | \`ctx.status == "active"\` |
|
|
817
|
+
|
|
818
|
+
NOTE: Guards support ONLY: comparisons (< > == != <= >=), null checks (== null, != null), boolean operators (and, or, not). NO method calls like .contains(), .includes(), etc.
|
|
819
|
+
|
|
820
|
+
## actions
|
|
821
|
+
|
|
822
|
+
| Name | Signature |
|
|
823
|
+
|---------|--------------------------------------------|
|
|
824
|
+
| action1 | \`(ctx) -> Context\` |
|
|
825
|
+
| action2 | \`(ctx, event) -> Context\` |
|
|
826
|
+
| action3 | \`(ctx) -> Context + Effect<EffectType>\` |
|
|
827
|
+
|
|
828
|
+
Key syntax notes:
|
|
829
|
+
- [initial] marks the initial state (exactly one required)
|
|
830
|
+
- [final] marks terminal states (zero or more allowed)
|
|
831
|
+
- Empty action column means "no action" (transition only changes state)
|
|
832
|
+
- Guard column uses guard name from guards table; prefix with ! to negate
|
|
833
|
+
- Guard expressions in backticks in the guards table
|
|
834
|
+
- Action signatures in backticks in the actions table
|
|
835
|
+
- Effect<T> in return type means the action emits an effect
|
|
836
|
+
- ctx.field accesses context fields (read-only in guards)
|
|
837
|
+
- timeout: 5s -> target means 5 second timeout transition
|
|
838
|
+
- Action BODIES are NOT written in Orca - only signatures in the actions table
|
|
839
|
+
- Transitions reference actions by name; actions are implemented separately
|
|
840
|
+
- States are headings (## for top-level, ### for nested children)
|
|
841
|
+
- State descriptions use blockquotes (> text)
|
|
842
|
+
- File extension should be .orca.md`;
|
|
843
|
+
export async function generateOrcaSkill(naturalLanguageSpec, configPath, maxIterations = 3) {
|
|
844
|
+
const config = loadConfig(configPath);
|
|
845
|
+
// Check if LLM is available
|
|
846
|
+
if (!config.api_key && !process.env.ANTHROPIC_API_KEY && !process.env.MINIMAX_API_KEY) {
|
|
847
|
+
return {
|
|
848
|
+
status: 'error',
|
|
849
|
+
error: 'No API key available. Set ANTHROPIC_API_KEY or MINIMAX_API_KEY in .env',
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
const provider = createProvider(config.provider, {
|
|
853
|
+
api_key: config.api_key,
|
|
854
|
+
base_url: config.base_url,
|
|
855
|
+
model: config.model,
|
|
856
|
+
max_tokens: config.max_tokens,
|
|
857
|
+
temperature: config.temperature,
|
|
858
|
+
});
|
|
859
|
+
const systemPrompt = `You are an expert in Orca state machine design. Generate Orca machine definitions in markdown (.orca.md) format from natural language descriptions.
|
|
860
|
+
|
|
861
|
+
${ORCA_SYNTAX_REFERENCE}
|
|
862
|
+
|
|
863
|
+
IMPORTANT - Guard Restrictions:
|
|
864
|
+
- Guards support ONLY: comparisons (< > == != <= >=), null checks, boolean operators
|
|
865
|
+
- NO method calls: do NOT use .contains(), .includes(), .length(), etc.
|
|
866
|
+
- For arrays, check .length > 0 or compare to null
|
|
867
|
+
- If you need complex logic, compute it in an action and store a boolean flag in context
|
|
868
|
+
|
|
869
|
+
IMPORTANT - Action Syntax:
|
|
870
|
+
- The actions table declares ONLY SIGNATURES (names and types), not implementations
|
|
871
|
+
- Write signatures in backticks: \`(ctx) -> Context\`
|
|
872
|
+
- NEVER write action bodies
|
|
873
|
+
- Transitions reference actions by name only
|
|
874
|
+
|
|
875
|
+
Important rules:
|
|
876
|
+
- Always include exactly ONE initial state marked with [initial]
|
|
877
|
+
- Final states should be marked with [final]
|
|
878
|
+
- Every (state, event) pair must have a transition OR the event must be ignored
|
|
879
|
+
- Use guards for conditional transitions
|
|
880
|
+
- Context should contain all data needed for guards and actions
|
|
881
|
+
- For effects (API calls, I/O), use Effect<T> return type in the signature
|
|
882
|
+
|
|
883
|
+
Output ONLY the Orca machine definition in .orca.md markdown format, wrapped in a code fence, with no additional text.`;
|
|
884
|
+
let currentOrca = '';
|
|
885
|
+
let lastErrors = [];
|
|
886
|
+
let iteration = 0;
|
|
887
|
+
while (iteration < maxIterations) {
|
|
888
|
+
const userPrompt = iteration === 0
|
|
889
|
+
? `Generate an Orca state machine for:\n${naturalLanguageSpec}`
|
|
890
|
+
: `The previous Orca machine had verification errors. Fix them:\n\nPrevious Orca:\n${currentOrca}\n\nVerification errors:\n${JSON.stringify(lastErrors, null, 2)}\n\nProvide the corrected Orca machine definition:`;
|
|
891
|
+
try {
|
|
892
|
+
const response = await provider.complete({
|
|
893
|
+
messages: [
|
|
894
|
+
{ role: 'system', content: systemPrompt },
|
|
895
|
+
{ role: 'user', content: userPrompt },
|
|
896
|
+
],
|
|
897
|
+
model: '',
|
|
898
|
+
max_tokens: 4096,
|
|
899
|
+
temperature: 0.5,
|
|
900
|
+
});
|
|
901
|
+
currentOrca = stripCodeFence(response.content);
|
|
902
|
+
const verification = await verifySkill({ source: currentOrca });
|
|
903
|
+
if (verification.status === 'valid') {
|
|
904
|
+
return {
|
|
905
|
+
status: 'success',
|
|
906
|
+
machine: verification.machine,
|
|
907
|
+
orca: currentOrca,
|
|
908
|
+
verification,
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
lastErrors = verification.errors;
|
|
912
|
+
iteration++;
|
|
913
|
+
}
|
|
914
|
+
catch (err) {
|
|
915
|
+
return {
|
|
916
|
+
status: 'error',
|
|
917
|
+
error: err instanceof Error ? err.message : String(err),
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
status: 'requires_refinement',
|
|
923
|
+
machine: extractMachineNameFromSource(currentOrca),
|
|
924
|
+
orca: currentOrca,
|
|
925
|
+
verification: await verifySkill({ source: currentOrca }).catch(() => ({
|
|
926
|
+
status: 'invalid',
|
|
927
|
+
machine: 'unknown',
|
|
928
|
+
states: 0,
|
|
929
|
+
events: 0,
|
|
930
|
+
transitions: 0,
|
|
931
|
+
errors: [],
|
|
932
|
+
})),
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
// ── /generate-orca-multi ─────────────────────────────────────────────────────
|
|
936
|
+
const MULTI_MACHINE_SYNTAX_ADDENDUM = `
|
|
937
|
+
Multi-machine files
|
|
938
|
+
-------------------
|
|
939
|
+
Separate machines with a line containing only three dashes (---).
|
|
940
|
+
|
|
941
|
+
A state can invoke another machine in the same file:
|
|
942
|
+
|
|
943
|
+
## state coordinatingState [initial]
|
|
944
|
+
- invoke: ChildMachineName
|
|
945
|
+
- on_done: nextState
|
|
946
|
+
- on_error: errorState
|
|
947
|
+
|
|
948
|
+
Rules:
|
|
949
|
+
- invoke: must name a machine defined in the same file
|
|
950
|
+
- on_done: fires when the invoked machine reaches a [final] state
|
|
951
|
+
- on_error: fires if the invoked machine cannot proceed
|
|
952
|
+
- Each state may invoke at most one child machine
|
|
953
|
+
- Circular invocations are not allowed (A invokes B invokes A)
|
|
954
|
+
- The coordinator machine should have a clear entry point and delegate work to child machines
|
|
955
|
+
- Child machines should have at least one [final] state so the coordinator's on_done can fire
|
|
956
|
+
`;
|
|
957
|
+
function verifyMultiSource(source) {
|
|
958
|
+
const { file } = parseMarkdown(source);
|
|
959
|
+
const allErrors = [];
|
|
960
|
+
// Cross-machine checks (cycle detection, unknown machines, input mappings)
|
|
961
|
+
const fileAnalysis = analyzeFile(file);
|
|
962
|
+
for (const e of [...fileAnalysis.errors, ...fileAnalysis.warnings]) {
|
|
963
|
+
allErrors.push({ code: e.code, message: e.message, severity: e.severity, suggestion: e.suggestion });
|
|
964
|
+
}
|
|
965
|
+
// Per-machine checks
|
|
966
|
+
for (const machine of file.machines) {
|
|
967
|
+
const structural = checkStructural(machine);
|
|
968
|
+
const completeness = checkCompleteness(machine);
|
|
969
|
+
const determinism = checkDeterminism(machine);
|
|
970
|
+
const properties = checkProperties(machine);
|
|
971
|
+
for (const e of [
|
|
972
|
+
...structural.errors,
|
|
973
|
+
...completeness.errors,
|
|
974
|
+
...determinism.errors,
|
|
975
|
+
...properties.errors,
|
|
976
|
+
]) {
|
|
977
|
+
allErrors.push({
|
|
978
|
+
code: e.code,
|
|
979
|
+
message: `[${machine.name}] ${e.message}`,
|
|
980
|
+
severity: e.severity,
|
|
981
|
+
location: e.location ? { state: e.location.state, event: e.location.event } : undefined,
|
|
982
|
+
suggestion: e.suggestion,
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return {
|
|
987
|
+
valid: !allErrors.some(e => e.severity === 'error'),
|
|
988
|
+
errors: allErrors,
|
|
989
|
+
machines: file.machines.map(m => m.name),
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
export async function generateOrcaMultiSkill(naturalLanguageSpec, configPath, maxIterations = 3) {
|
|
993
|
+
const config = loadConfig(configPath);
|
|
994
|
+
if (!config.api_key && !process.env.ANTHROPIC_API_KEY && !process.env.MINIMAX_API_KEY) {
|
|
995
|
+
return {
|
|
996
|
+
status: 'error',
|
|
997
|
+
error: 'No API key available. Set ANTHROPIC_API_KEY or MINIMAX_API_KEY in .env',
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
const provider = createProvider(config.provider, {
|
|
1001
|
+
api_key: config.api_key,
|
|
1002
|
+
base_url: config.base_url,
|
|
1003
|
+
model: config.model,
|
|
1004
|
+
max_tokens: config.max_tokens,
|
|
1005
|
+
temperature: config.temperature,
|
|
1006
|
+
});
|
|
1007
|
+
const systemPrompt = `You are an expert in Orca state machine design. Generate coordinated multi-machine Orca definitions in markdown (.orca.md) format from natural language descriptions.
|
|
1008
|
+
|
|
1009
|
+
${ORCA_SYNTAX_REFERENCE}
|
|
1010
|
+
${MULTI_MACHINE_SYNTAX_ADDENDUM}
|
|
1011
|
+
|
|
1012
|
+
IMPORTANT - Guard Restrictions:
|
|
1013
|
+
- Guards support ONLY: comparisons (< > == != <= >=), null checks, boolean operators
|
|
1014
|
+
- NO method calls: do NOT use .contains(), .includes(), .length(), etc.
|
|
1015
|
+
- If you need complex logic, compute it in an action and store a boolean flag in context
|
|
1016
|
+
|
|
1017
|
+
IMPORTANT - Action Syntax:
|
|
1018
|
+
- The actions table declares ONLY SIGNATURES (names and types), not implementations
|
|
1019
|
+
- Write signatures in backticks: \`(ctx) -> Context\`
|
|
1020
|
+
- NEVER write action bodies
|
|
1021
|
+
|
|
1022
|
+
Multi-machine design principles:
|
|
1023
|
+
- Design a clear coordinator machine that delegates to focused child machines
|
|
1024
|
+
- Each child machine should have a single responsibility and at least one [final] state
|
|
1025
|
+
- Use invoke: in coordinator states to call child machines
|
|
1026
|
+
- Separate machines with --- on its own line
|
|
1027
|
+
- Name machines clearly (e.g. OrderCoordinator, PaymentProcessor, NotificationSender)
|
|
1028
|
+
|
|
1029
|
+
Output ONLY the complete multi-machine Orca definition in .orca.md markdown format, wrapped in a code fence, with no additional text.`;
|
|
1030
|
+
let currentOrca = '';
|
|
1031
|
+
let lastErrors = [];
|
|
1032
|
+
let iteration = 0;
|
|
1033
|
+
while (iteration < maxIterations) {
|
|
1034
|
+
const userPrompt = iteration === 0
|
|
1035
|
+
? `Generate a multi-machine Orca state machine system for:\n${naturalLanguageSpec}\n\nDesign at least 2 coordinated machines separated by ---`
|
|
1036
|
+
: `The previous multi-machine Orca definition had verification errors. Fix them:\n\nPrevious Orca:\n${currentOrca}\n\nVerification errors:\n${JSON.stringify(lastErrors, null, 2)}\n\nProvide the corrected multi-machine Orca definition:`;
|
|
1037
|
+
try {
|
|
1038
|
+
const response = await provider.complete({
|
|
1039
|
+
messages: [
|
|
1040
|
+
{ role: 'system', content: systemPrompt },
|
|
1041
|
+
{ role: 'user', content: userPrompt },
|
|
1042
|
+
],
|
|
1043
|
+
model: '',
|
|
1044
|
+
max_tokens: 8192,
|
|
1045
|
+
temperature: 0.5,
|
|
1046
|
+
});
|
|
1047
|
+
currentOrca = stripCodeFence(response.content);
|
|
1048
|
+
let verification;
|
|
1049
|
+
try {
|
|
1050
|
+
verification = verifyMultiSource(currentOrca);
|
|
1051
|
+
}
|
|
1052
|
+
catch (parseErr) {
|
|
1053
|
+
lastErrors = [{
|
|
1054
|
+
code: 'PARSE_ERROR',
|
|
1055
|
+
message: parseErr instanceof Error ? parseErr.message : String(parseErr),
|
|
1056
|
+
severity: 'error',
|
|
1057
|
+
}];
|
|
1058
|
+
iteration++;
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
if (verification.valid) {
|
|
1062
|
+
return {
|
|
1063
|
+
status: 'success',
|
|
1064
|
+
machines: verification.machines,
|
|
1065
|
+
orca: currentOrca,
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
lastErrors = verification.errors.filter(e => e.severity === 'error');
|
|
1069
|
+
iteration++;
|
|
1070
|
+
}
|
|
1071
|
+
catch (err) {
|
|
1072
|
+
return {
|
|
1073
|
+
status: 'error',
|
|
1074
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
let finalMachines = [];
|
|
1079
|
+
try {
|
|
1080
|
+
const { file } = parseMarkdown(currentOrca);
|
|
1081
|
+
finalMachines = file.machines.map(m => m.name);
|
|
1082
|
+
}
|
|
1083
|
+
catch { /* ignore */ }
|
|
1084
|
+
return {
|
|
1085
|
+
status: 'requires_refinement',
|
|
1086
|
+
machines: finalMachines,
|
|
1087
|
+
orca: currentOrca,
|
|
1088
|
+
errors: lastErrors,
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
function stripCodeFence(code) {
|
|
1092
|
+
return code
|
|
1093
|
+
.replace(/^```(?:orca|markdown|md|orca\.md)?\n/, '')
|
|
1094
|
+
.replace(/^```\n/, '')
|
|
1095
|
+
.replace(/\n```$/, '')
|
|
1096
|
+
.trim();
|
|
1097
|
+
}
|
|
1098
|
+
function extractMachineNameFromSource(orca) {
|
|
1099
|
+
// Support both DSL format (machine Name) and markdown format (# machine Name)
|
|
1100
|
+
const match = orca.match(/^(?:#\s+)?machine\s+(\w+)/m);
|
|
1101
|
+
return match ? match[1] : 'Unknown';
|
|
1102
|
+
}
|
|
1103
|
+
//# sourceMappingURL=skills.js.map
|