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