@hoplogic/engine 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/dist/index.js ADDED
@@ -0,0 +1,639 @@
1
+ import { detectSerializationResidue, parseResult, repairValue, parseFullSpec, validateSpec } from '@hoplogic/spec';
2
+ import { z } from 'zod';
3
+
4
+ // src/execution-types.ts
5
+ function createEmptyStats() {
6
+ return {
7
+ total_steps: 0,
8
+ completed_steps: 0,
9
+ failed_steps: 0,
10
+ total_retries: 0,
11
+ llm_calls: 0,
12
+ verify_calls: 0
13
+ };
14
+ }
15
+ function formatVerifier(result, expectedKeys) {
16
+ if (result === null || result === void 0) {
17
+ return { passed: false, reason: "Result is null or undefined" };
18
+ }
19
+ if (expectedKeys && expectedKeys.length > 0) {
20
+ if (typeof result !== "object" || Array.isArray(result)) {
21
+ return { passed: false, reason: `Expected object with keys [${expectedKeys.join(", ")}], got ${Array.isArray(result) ? "array" : typeof result}` };
22
+ }
23
+ const obj = result;
24
+ const missing = expectedKeys.filter((k) => !(k in obj));
25
+ if (missing.length > 0) {
26
+ return { passed: false, reason: `Missing expected keys: ${missing.join(", ")}` };
27
+ }
28
+ }
29
+ if (typeof result === "object" && result !== null) {
30
+ const residue = detectSerializationResidue(result);
31
+ if (residue.length > 0) {
32
+ return { passed: false, reason: `Serialization residue detected at: ${residue.join(", ")}` };
33
+ }
34
+ }
35
+ return { passed: true, reason: "Format check passed" };
36
+ }
37
+ async function reverseVerify(verifier, task, context, result) {
38
+ return verifier.verify(task, context, result);
39
+ }
40
+ async function forwardCrossVerify(verifier, task, context, result) {
41
+ const results = await Promise.allSettled([
42
+ verifier.verify(task, context, result),
43
+ verifier.verify(task, context, result),
44
+ verifier.verify(task, context, result)
45
+ ]);
46
+ let passCount = 0;
47
+ let failCount = 0;
48
+ const reasons = [];
49
+ for (const r of results) {
50
+ if (r.status === "fulfilled") {
51
+ if (r.value.passed) passCount++;
52
+ else failCount++;
53
+ reasons.push(r.value.reason);
54
+ } else {
55
+ failCount++;
56
+ reasons.push(`Verification error: ${r.reason}`);
57
+ }
58
+ }
59
+ const passed = passCount >= 2;
60
+ return {
61
+ passed,
62
+ reason: passed ? `Cross-verification passed (${passCount}/3 agree)` : `Cross-verification failed (${failCount}/3 disagree): ${reasons.filter((_r, i) => {
63
+ const res = results[i];
64
+ return res?.status === "fulfilled" && !res.value.passed || res?.status === "rejected";
65
+ }).join("; ")}`
66
+ };
67
+ }
68
+ var TYPE_MAP = {
69
+ "str": z.string(),
70
+ "string": z.string(),
71
+ "int": z.number().int(),
72
+ "integer": z.number().int(),
73
+ "float": z.number(),
74
+ "number": z.number(),
75
+ "bool": z.boolean(),
76
+ "boolean": z.boolean(),
77
+ "list": z.array(z.unknown()),
78
+ "dict": z.record(z.unknown()),
79
+ "list[str]": z.array(z.string()),
80
+ "List[str]": z.array(z.string()),
81
+ "list[int]": z.array(z.number().int()),
82
+ "List[int]": z.array(z.number().int()),
83
+ "list[dict]": z.array(z.record(z.unknown())),
84
+ "List[dict]": z.array(z.record(z.unknown())),
85
+ "list[float]": z.array(z.number()),
86
+ "List[float]": z.array(z.number())
87
+ };
88
+ function resolveType(typeStr) {
89
+ return TYPE_MAP[typeStr.trim()] ?? z.string();
90
+ }
91
+ var TUPLE_RE = /^\(\s*(.+?)\s*,\s*"((?:[^"\\]|\\.)*)"\s*\)$/;
92
+ function splitTopLevel(s) {
93
+ const parts = [];
94
+ let current = "";
95
+ let depth = 0;
96
+ let inString = null;
97
+ for (let i = 0; i < s.length; i++) {
98
+ const ch = s[i];
99
+ if (inString) {
100
+ current += ch;
101
+ if (ch === inString && s[i - 1] !== "\\") {
102
+ inString = null;
103
+ }
104
+ continue;
105
+ }
106
+ if (ch === '"' || ch === "'") {
107
+ inString = ch;
108
+ current += ch;
109
+ continue;
110
+ }
111
+ if (ch === "{" || ch === "[" || ch === "(") {
112
+ depth++;
113
+ current += ch;
114
+ continue;
115
+ }
116
+ if (ch === "}" || ch === "]" || ch === ")") {
117
+ depth--;
118
+ current += ch;
119
+ continue;
120
+ }
121
+ if (ch === "," && depth === 0) {
122
+ parts.push(current.trim());
123
+ current = "";
124
+ continue;
125
+ }
126
+ current += ch;
127
+ }
128
+ if (current.trim()) {
129
+ parts.push(current.trim());
130
+ }
131
+ return parts;
132
+ }
133
+ function parseReturnFormat(formatStr) {
134
+ const trimmed = formatStr.trim();
135
+ if (!trimmed) return void 0;
136
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
137
+ const inner = trimmed.slice(1, -1).trim();
138
+ if (!inner) return {};
139
+ const parts = splitTopLevel(inner);
140
+ const result = {};
141
+ for (const part of parts) {
142
+ const colonIdx = part.indexOf(":");
143
+ if (colonIdx < 0) continue;
144
+ const key = part.slice(0, colonIdx).trim().replace(/^["']|["']$/g, "");
145
+ if (!key) continue;
146
+ const valStr = part.slice(colonIdx + 1).trim();
147
+ const tupleMatch = TUPLE_RE.exec(valStr);
148
+ if (tupleMatch) {
149
+ const innerType = resolveType(tupleMatch[1]);
150
+ const desc = tupleMatch[2].replace(/\\"/g, '"');
151
+ result[key] = { type: innerType, description: desc };
152
+ } else {
153
+ result[key] = { type: resolveType(valStr) };
154
+ }
155
+ }
156
+ return result;
157
+ }
158
+ return { result: { type: z.string(), description: trimmed } };
159
+ }
160
+ function createResponseFormat(formatStr) {
161
+ const fields = parseReturnFormat(formatStr);
162
+ let answerSchema;
163
+ if (!fields || Object.keys(fields).length === 0) {
164
+ answerSchema = z.string();
165
+ } else if (Object.keys(fields).length === 1 && "result" in fields) {
166
+ answerSchema = fields["result"].type;
167
+ } else {
168
+ const shape = {};
169
+ for (const [key, def] of Object.entries(fields)) {
170
+ shape[key] = def.type;
171
+ }
172
+ answerSchema = z.object(shape);
173
+ }
174
+ return z.object({
175
+ explanation: z.string(),
176
+ final_answer: answerSchema,
177
+ confidence: z.enum(["OK", "LACK_OF_INFO", "UNCERTAIN"]),
178
+ missing_info: z.record(z.object({
179
+ field: z.string(),
180
+ description: z.string(),
181
+ importance: z.string()
182
+ })).nullable().optional(),
183
+ suggestions: z.array(z.string()).nullable().optional()
184
+ });
185
+ }
186
+ function createDataSchema(formatStr) {
187
+ const fields = parseReturnFormat(formatStr);
188
+ if (!fields || Object.keys(fields).length === 0) {
189
+ return z.string();
190
+ }
191
+ if (Object.keys(fields).length === 1 && "result" in fields) {
192
+ return fields["result"].type;
193
+ }
194
+ const shape = {};
195
+ for (const [key, def] of Object.entries(fields)) {
196
+ shape[key] = def.type;
197
+ }
198
+ return z.object(shape);
199
+ }
200
+ var ExitSignal = class {
201
+ constructor(exitId, output) {
202
+ this.exitId = exitId;
203
+ this.output = output;
204
+ }
205
+ };
206
+ var LoopSignal = class {
207
+ constructor(action, targetId) {
208
+ this.action = action;
209
+ this.targetId = targetId;
210
+ }
211
+ };
212
+ var SpecExecutor = class {
213
+ llm;
214
+ verifier;
215
+ maxRetries;
216
+ constructor(options) {
217
+ this.llm = options.llm;
218
+ this.verifier = options.verifier;
219
+ this.maxRetries = options.maxRetries ?? 3;
220
+ }
221
+ /** Execute a StepInfo tree with given input variables */
222
+ async execute(steps, input) {
223
+ const stats = createEmptyStats();
224
+ const scope = new Map(Object.entries(input));
225
+ const stepResults = [];
226
+ let finalStatus = "ok";
227
+ let finalOutput = {};
228
+ try {
229
+ for (const step of steps) {
230
+ stats.total_steps++;
231
+ const result = await this.executeStep(step, scope, stats);
232
+ stepResults.push(result);
233
+ if (result.status !== "ok") {
234
+ finalStatus = result.status;
235
+ finalOutput = result.output;
236
+ break;
237
+ }
238
+ }
239
+ } catch (e) {
240
+ if (e instanceof ExitSignal) {
241
+ finalOutput = e.output;
242
+ } else {
243
+ throw e;
244
+ }
245
+ }
246
+ if (finalOutput === void 0 || typeof finalOutput === "object" && finalOutput !== null && Object.keys(finalOutput).length === 0) {
247
+ finalOutput = Object.fromEntries(scope);
248
+ }
249
+ return {
250
+ status: finalStatus,
251
+ output: finalOutput,
252
+ steps: stepResults,
253
+ stats
254
+ };
255
+ }
256
+ /** Execute a single step, dispatching by type */
257
+ async executeStep(step, scope, stats) {
258
+ switch (step.step_type) {
259
+ case "LLM":
260
+ return this.executeLLM(step, scope, stats);
261
+ case "code":
262
+ return this.executeCode(step, scope, stats);
263
+ case "loop":
264
+ return this.executeLoop(step, scope, stats);
265
+ case "branch":
266
+ return this.executeBranch(step, scope, stats);
267
+ case "flow":
268
+ return this.executeFlow(step, scope);
269
+ case "call":
270
+ return this.executeCall(step, scope, stats);
271
+ case "subtask":
272
+ return this.executeSubtask(step, scope, stats);
273
+ default:
274
+ return {
275
+ step_id: step.step_id,
276
+ status: "fail",
277
+ output: null,
278
+ retries: 0,
279
+ error: `Unknown step type: ${step.step_type}`
280
+ };
281
+ }
282
+ }
283
+ // ─── LLM step ───────────────────────────────────────────
284
+ async executeLLM(step, scope, stats) {
285
+ const prompt = this.buildPrompt(step, scope);
286
+ const options = {};
287
+ if (step.return_format) {
288
+ const fields = parseReturnFormat(step.return_format);
289
+ if (fields) {
290
+ options.systemPrompt = `Respond in JSON format with these fields: ${Object.keys(fields).join(", ")}`;
291
+ }
292
+ }
293
+ let lastError = "";
294
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
295
+ stats.llm_calls++;
296
+ const llmResult = await this.llm.query(prompt, options);
297
+ if (!llmResult.success) {
298
+ lastError = llmResult.content;
299
+ if (attempt < this.maxRetries) continue;
300
+ break;
301
+ }
302
+ const parsed = parseResult(llmResult.content);
303
+ const repaired = repairValue(parsed);
304
+ const expectedKeys = step.return_format ? Object.keys(parseReturnFormat(step.return_format) ?? {}) : void 0;
305
+ const fmtCheck = formatVerifier(repaired, expectedKeys?.length ? expectedKeys : void 0);
306
+ if (!fmtCheck.passed) {
307
+ lastError = fmtCheck.reason;
308
+ if (attempt < this.maxRetries) continue;
309
+ break;
310
+ }
311
+ if (this.verifier && step.verify_strategy !== "none") {
312
+ stats.verify_calls++;
313
+ const verifyResult = await reverseVerify(
314
+ this.verifier,
315
+ step.task || step.description,
316
+ JSON.stringify(Object.fromEntries(scope)),
317
+ JSON.stringify(repaired)
318
+ );
319
+ if (!verifyResult.passed) {
320
+ lastError = verifyResult.reason;
321
+ if (attempt < this.maxRetries) continue;
322
+ break;
323
+ }
324
+ }
325
+ this.storeOutputs(step, repaired, scope);
326
+ return {
327
+ step_id: step.step_id,
328
+ status: "ok",
329
+ output: repaired,
330
+ retries: attempt
331
+ };
332
+ }
333
+ return {
334
+ step_id: step.step_id,
335
+ status: "fail",
336
+ output: null,
337
+ retries: this.maxRetries,
338
+ error: lastError
339
+ };
340
+ }
341
+ // ─── Code step ──────────────────────────────────────────
342
+ executeCode(step, scope, stats) {
343
+ stats.total_steps;
344
+ for (const key of Object.keys(step.outputs)) {
345
+ if (!scope.has(key)) {
346
+ scope.set(key, null);
347
+ }
348
+ }
349
+ return Promise.resolve({
350
+ step_id: step.step_id,
351
+ status: "ok",
352
+ output: Object.fromEntries(
353
+ Object.keys(step.outputs).map((k) => [k, scope.get(k)])
354
+ ),
355
+ retries: 0
356
+ });
357
+ }
358
+ // ─── Loop step ──────────────────────────────────────────
359
+ async executeLoop(step, scope, stats) {
360
+ if (step.loop_mode === "for-each" && step.collection) {
361
+ const collection = scope.get(step.collection);
362
+ if (!Array.isArray(collection)) {
363
+ return {
364
+ step_id: step.step_id,
365
+ status: "fail",
366
+ output: null,
367
+ retries: 0,
368
+ error: `Loop collection "${step.collection}" is not an array`
369
+ };
370
+ }
371
+ const results = [];
372
+ for (const item of collection) {
373
+ if (step.element_var) {
374
+ scope.set(step.element_var, item);
375
+ }
376
+ try {
377
+ for (const child of step.children) {
378
+ stats.total_steps++;
379
+ const childResult = await this.executeStep(child, scope, stats);
380
+ if (childResult.status !== "ok") {
381
+ return { step_id: step.step_id, status: childResult.status, output: results, retries: 0, error: childResult.error };
382
+ }
383
+ }
384
+ results.push(item);
385
+ } catch (e) {
386
+ if (e instanceof LoopSignal) {
387
+ if (e.action === "break") break;
388
+ if (e.action === "continue") continue;
389
+ }
390
+ throw e;
391
+ }
392
+ }
393
+ this.storeOutputs(step, results, scope);
394
+ return { step_id: step.step_id, status: "ok", output: results, retries: 0 };
395
+ }
396
+ if (step.loop_mode === "while" && step.condition) {
397
+ const maxIter = step.max_iterations ?? 10;
398
+ let iteration = 0;
399
+ const results = [];
400
+ while (iteration < maxIter) {
401
+ iteration++;
402
+ try {
403
+ for (const child of step.children) {
404
+ stats.total_steps++;
405
+ const childResult = await this.executeStep(child, scope, stats);
406
+ if (childResult.status !== "ok") {
407
+ return { step_id: step.step_id, status: childResult.status, output: results, retries: 0, error: childResult.error };
408
+ }
409
+ }
410
+ results.push(iteration);
411
+ } catch (e) {
412
+ if (e instanceof LoopSignal) {
413
+ if (e.action === "break") break;
414
+ if (e.action === "continue") continue;
415
+ }
416
+ throw e;
417
+ }
418
+ }
419
+ this.storeOutputs(step, results, scope);
420
+ return { step_id: step.step_id, status: "ok", output: results, retries: 0 };
421
+ }
422
+ return {
423
+ step_id: step.step_id,
424
+ status: "fail",
425
+ output: null,
426
+ retries: 0,
427
+ error: "Loop step missing collection or condition"
428
+ };
429
+ }
430
+ // ─── Branch step ────────────────────────────────────────
431
+ async executeBranch(step, scope, stats) {
432
+ for (const child of step.children) {
433
+ stats.total_steps++;
434
+ const childResult = await this.executeStep(child, scope, stats);
435
+ if (childResult.status !== "ok") {
436
+ return { step_id: step.step_id, status: childResult.status, output: null, retries: 0, error: childResult.error };
437
+ }
438
+ }
439
+ return { step_id: step.step_id, status: "ok", output: null, retries: 0 };
440
+ }
441
+ // ─── Flow step ──────────────────────────────────────────
442
+ executeFlow(step, scope) {
443
+ if (step.flow_action === "exit") {
444
+ const output = Object.fromEntries(
445
+ Object.keys(step.outputs).map((k) => [k, scope.get(k)])
446
+ );
447
+ throw new ExitSignal(step.exit_id, output);
448
+ }
449
+ if (step.flow_action === "continue" || step.flow_action === "break") {
450
+ throw new LoopSignal(step.flow_action, step.exit_id);
451
+ }
452
+ return Promise.resolve({
453
+ step_id: step.step_id,
454
+ status: "ok",
455
+ output: null,
456
+ retries: 0
457
+ });
458
+ }
459
+ // ─── Call step ──────────────────────────────────────────
460
+ executeCall(step, scope, _stats) {
461
+ for (const key of Object.keys(step.outputs)) {
462
+ scope.set(key, `[call:${step.call_target}:${key}]`);
463
+ }
464
+ return Promise.resolve({
465
+ step_id: step.step_id,
466
+ status: "ok",
467
+ output: Object.fromEntries(
468
+ Object.keys(step.outputs).map((k) => [k, scope.get(k)])
469
+ ),
470
+ retries: 0
471
+ });
472
+ }
473
+ // ─── Subtask step ───────────────────────────────────────
474
+ async executeSubtask(step, scope, stats) {
475
+ if (step.expand_mode === "static" || !step.expand_mode) {
476
+ for (const child of step.children) {
477
+ stats.total_steps++;
478
+ const childResult = await this.executeStep(child, scope, stats);
479
+ if (childResult.status !== "ok") {
480
+ return { step_id: step.step_id, status: childResult.status, output: null, retries: 0, error: childResult.error };
481
+ }
482
+ }
483
+ return { step_id: step.step_id, status: "ok", output: null, retries: 0 };
484
+ }
485
+ for (const key of Object.keys(step.outputs)) {
486
+ scope.set(key, `[subtask:${step.expand_mode}:${key}]`);
487
+ }
488
+ return {
489
+ step_id: step.step_id,
490
+ status: "ok",
491
+ output: Object.fromEntries(
492
+ Object.keys(step.outputs).map((k) => [k, scope.get(k)])
493
+ ),
494
+ retries: 0
495
+ };
496
+ }
497
+ // ─── Helpers ────────────────────────────────────────────
498
+ buildPrompt(step, scope) {
499
+ const parts = [];
500
+ if (step.task) parts.push(`Task: ${step.task}`);
501
+ if (step.description) parts.push(`Description: ${step.description}`);
502
+ const inputs = Object.keys(step.inputs);
503
+ if (inputs.length > 0) {
504
+ parts.push("Context:");
505
+ for (const key of inputs) {
506
+ const val = scope.get(key);
507
+ parts.push(` ${key}: ${JSON.stringify(val)}`);
508
+ }
509
+ }
510
+ if (step.return_format) {
511
+ parts.push(`Output format: ${step.return_format}`);
512
+ }
513
+ return parts.join("\n");
514
+ }
515
+ storeOutputs(step, result, scope) {
516
+ const outputKeys = Object.keys(step.outputs);
517
+ if (outputKeys.length === 0) return;
518
+ if (typeof result === "object" && result !== null && !Array.isArray(result)) {
519
+ const obj = result;
520
+ for (const key of outputKeys) {
521
+ if (key in obj) {
522
+ scope.set(key, obj[key]);
523
+ }
524
+ }
525
+ } else if (outputKeys.length === 1) {
526
+ scope.set(outputKeys[0], result);
527
+ }
528
+ }
529
+ };
530
+ var HopJIT = class {
531
+ llm;
532
+ verifier;
533
+ maxRetries;
534
+ grammar;
535
+ // Resume state
536
+ _liveExecutor;
537
+ _liveSpec;
538
+ _liveValidationErrors = [];
539
+ _pendingNeedInput;
540
+ constructor(options) {
541
+ this.llm = options.llm;
542
+ this.verifier = options.verifier;
543
+ this.maxRetries = options.maxRetries ?? 3;
544
+ this.grammar = options.grammar;
545
+ }
546
+ /**
547
+ * Parse and execute a HopSpec markdown document.
548
+ *
549
+ * Flow: parse → validate → execute
550
+ * Returns a JITResult with execution output, stats, and validation info.
551
+ */
552
+ async runSpec(markdown, input, options) {
553
+ const parsed = parseFullSpec(markdown, this.grammar);
554
+ const steps = parsed.steps;
555
+ const sections = parsed.sections;
556
+ const mode = options?.mode ?? "jit";
557
+ let validationErrors = [];
558
+ if (!options?.skipValidation) {
559
+ validationErrors = validateSpec(steps, mode, sections, this.grammar);
560
+ }
561
+ this._liveSpec = steps;
562
+ this._liveValidationErrors = validationErrors;
563
+ if (steps.length === 0) {
564
+ this._liveExecutor = void 0;
565
+ return {
566
+ status: "fail",
567
+ output: null,
568
+ steps: [],
569
+ stats: { total_steps: 0, completed_steps: 0, failed_steps: 0, total_retries: 0, llm_calls: 0, verify_calls: 0 },
570
+ spec: steps,
571
+ validationErrors
572
+ };
573
+ }
574
+ const executorOpts = {
575
+ llm: this.llm,
576
+ verifier: this.verifier,
577
+ maxRetries: this.maxRetries
578
+ };
579
+ const executor = new SpecExecutor(executorOpts);
580
+ this._liveExecutor = executor;
581
+ const result = await executor.execute(steps, input);
582
+ this._liveExecutor = void 0;
583
+ this._pendingNeedInput = void 0;
584
+ return {
585
+ status: result.status,
586
+ output: result.output,
587
+ steps: result.steps,
588
+ stats: result.stats,
589
+ spec: steps,
590
+ validationErrors
591
+ };
592
+ }
593
+ /**
594
+ * Resume execution from a NEED_INPUT checkpoint.
595
+ * Injects feedback and continues from the interrupted step.
596
+ *
597
+ * @throws Error if no pending execution to resume
598
+ */
599
+ async resume(feedback) {
600
+ if (!this._liveExecutor || !this._liveSpec) {
601
+ throw new Error("No pending execution to resume");
602
+ }
603
+ const input = { _feedback: feedback };
604
+ const result = await this._liveExecutor.execute(this._liveSpec, input);
605
+ this._liveExecutor = void 0;
606
+ this._pendingNeedInput = void 0;
607
+ return {
608
+ status: result.status,
609
+ output: result.output,
610
+ steps: result.steps,
611
+ stats: result.stats,
612
+ spec: this._liveSpec,
613
+ validationErrors: this._liveValidationErrors
614
+ };
615
+ }
616
+ /** Cancel any pending execution and clear resume state. */
617
+ cancel() {
618
+ this._liveExecutor = void 0;
619
+ this._liveSpec = void 0;
620
+ this._liveValidationErrors = [];
621
+ this._pendingNeedInput = void 0;
622
+ }
623
+ /** Whether there is a pending execution that can be resumed. */
624
+ get hasPending() {
625
+ return this._liveExecutor !== void 0 && this._liveSpec !== void 0;
626
+ }
627
+ };
628
+
629
+ // src/llm-protocol.ts
630
+ function isSuccess(status) {
631
+ return status === "ok";
632
+ }
633
+ function isTerminal(status) {
634
+ return status === "ok" || status === "fail";
635
+ }
636
+
637
+ export { HopJIT, SpecExecutor, createDataSchema, createEmptyStats, createResponseFormat, formatVerifier, forwardCrossVerify, isSuccess, isTerminal, parseReturnFormat, reverseVerify };
638
+ //# sourceMappingURL=index.js.map
639
+ //# sourceMappingURL=index.js.map