@aigne/ash 0.0.1 → 0.0.2-beta.1

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 (66) hide show
  1. package/dist/ast.d.cts +13 -5
  2. package/dist/ast.d.cts.map +1 -1
  3. package/dist/ast.d.mts +13 -5
  4. package/dist/ast.d.mts.map +1 -1
  5. package/dist/compiler.cjs +27 -0
  6. package/dist/compiler.d.cts.map +1 -1
  7. package/dist/compiler.d.mts.map +1 -1
  8. package/dist/compiler.mjs +27 -0
  9. package/dist/compiler.mjs.map +1 -1
  10. package/dist/lexer.cjs +2 -1
  11. package/dist/lexer.d.cts +1 -1
  12. package/dist/lexer.d.cts.map +1 -1
  13. package/dist/lexer.d.mts +1 -1
  14. package/dist/lexer.d.mts.map +1 -1
  15. package/dist/lexer.mjs +2 -1
  16. package/dist/lexer.mjs.map +1 -1
  17. package/dist/parser.cjs +75 -1
  18. package/dist/parser.d.cts.map +1 -1
  19. package/dist/parser.d.mts.map +1 -1
  20. package/dist/parser.mjs +75 -1
  21. package/dist/parser.mjs.map +1 -1
  22. package/dist/type-checker.cjs +6 -0
  23. package/dist/type-checker.d.cts.map +1 -1
  24. package/dist/type-checker.d.mts.map +1 -1
  25. package/dist/type-checker.mjs +6 -0
  26. package/dist/type-checker.mjs.map +1 -1
  27. package/package.json +7 -1
  28. package/DESIGN.md +0 -41
  29. package/src/ai-dev-loop/ash-run-result.test.ts +0 -113
  30. package/src/ai-dev-loop/ash-run-result.ts +0 -46
  31. package/src/ai-dev-loop/ash-typed-error.test.ts +0 -136
  32. package/src/ai-dev-loop/ash-typed-error.ts +0 -50
  33. package/src/ai-dev-loop/ash-validate.test.ts +0 -54
  34. package/src/ai-dev-loop/ash-validate.ts +0 -34
  35. package/src/ai-dev-loop/dev-loop.test.ts +0 -364
  36. package/src/ai-dev-loop/dev-loop.ts +0 -156
  37. package/src/ai-dev-loop/dry-run.test.ts +0 -107
  38. package/src/ai-dev-loop/e2e-multi-fix.test.ts +0 -473
  39. package/src/ai-dev-loop/e2e.test.ts +0 -324
  40. package/src/ai-dev-loop/index.ts +0 -15
  41. package/src/ai-dev-loop/invariants.test.ts +0 -253
  42. package/src/ai-dev-loop/live-mode.test.ts +0 -63
  43. package/src/ai-dev-loop/live-mode.ts +0 -33
  44. package/src/ai-dev-loop/meta-tools.test.ts +0 -120
  45. package/src/ai-dev-loop/meta-tools.ts +0 -142
  46. package/src/ai-dev-loop/structured-runner.test.ts +0 -159
  47. package/src/ai-dev-loop/structured-runner.ts +0 -209
  48. package/src/ai-dev-loop/system-prompt.test.ts +0 -102
  49. package/src/ai-dev-loop/system-prompt.ts +0 -81
  50. package/src/ast.ts +0 -186
  51. package/src/compiler.test.ts +0 -2933
  52. package/src/compiler.ts +0 -1103
  53. package/src/e2e.test.ts +0 -552
  54. package/src/index.ts +0 -16
  55. package/src/lexer.test.ts +0 -538
  56. package/src/lexer.ts +0 -222
  57. package/src/parser.test.ts +0 -1024
  58. package/src/parser.ts +0 -835
  59. package/src/reference.test.ts +0 -166
  60. package/src/reference.ts +0 -125
  61. package/src/template.test.ts +0 -210
  62. package/src/template.ts +0 -139
  63. package/src/type-checker.test.ts +0 -1494
  64. package/src/type-checker.ts +0 -785
  65. package/tsconfig.json +0 -9
  66. package/tsdown.config.ts +0 -12
@@ -1,50 +0,0 @@
1
- /**
2
- * AshTypedError — Discriminated union of all ASH error types.
3
- *
4
- * Each kind carries enough structured information for AI self-repair.
5
- */
6
-
7
- export type AshTypedError =
8
- | { kind: "IntentDenied"; invariant: string; message: string; suggestion?: string }
9
- | { kind: "CapabilityMissing"; capability: string; message: string }
10
- | { kind: "ToolNotFound"; name: string; available: string[]; message: string }
11
- | { kind: "ValidationFailed"; field: string; expected: string; got: string; message: string }
12
- | { kind: "BudgetExceeded"; device: string; limit: number; used: number; message: string }
13
- | { kind: "Timeout"; step: string; limit_ms: number; message: string }
14
- | { kind: "ParseError"; line?: number; message: string }
15
- | { kind: "RuntimeError"; message: string; context?: Record<string, unknown> };
16
-
17
- const VALID_KINDS = new Set([
18
- "IntentDenied", "CapabilityMissing", "ToolNotFound", "ValidationFailed",
19
- "BudgetExceeded", "Timeout", "ParseError", "RuntimeError",
20
- ]);
21
-
22
- export function isAshTypedError(value: unknown): value is AshTypedError {
23
- if (!value || typeof value !== "object") return false;
24
- const obj = value as Record<string, unknown>;
25
- return typeof obj.kind === "string" && VALID_KINDS.has(obj.kind) && typeof obj.message === "string";
26
- }
27
-
28
- /**
29
- * Best-effort conversion from raw ASH error string → AshTypedError.
30
- */
31
- export function fromJobError(errorStr: string): AshTypedError {
32
- if (errorStr.startsWith("Capability denied:")) {
33
- const capability = errorStr.replace("Capability denied:", "").trim();
34
- return { kind: "CapabilityMissing", capability, message: errorStr };
35
- }
36
-
37
- if (errorStr.includes("not found") || errorStr.includes("unknown command")) {
38
- return { kind: "ToolNotFound", name: errorStr, available: [], message: errorStr };
39
- }
40
-
41
- if (errorStr.includes("timeout") || errorStr.includes("Timeout")) {
42
- return { kind: "Timeout", step: "", limit_ms: 0, message: errorStr };
43
- }
44
-
45
- if (errorStr.includes("parse") || errorStr.includes("syntax") || errorStr.includes("Unexpected")) {
46
- return { kind: "ParseError", message: errorStr };
47
- }
48
-
49
- return { kind: "RuntimeError", message: errorStr };
50
- }
@@ -1,54 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { ashValidate } from "./ash-validate.js";
3
-
4
- describe("ash.validate", () => {
5
- // ── Happy Path ─────────────────────────────────────────
6
-
7
- it("valid script → empty ParseError[]", () => {
8
- const errors = ashValidate('job "test" { find /users | map name }');
9
- expect(errors).toHaveLength(0);
10
- });
11
-
12
- it("script with unknown command → ParseError", () => {
13
- const errors = ashValidate('job "test" { find /users | foobar }');
14
- expect(errors.length).toBeGreaterThan(0);
15
- expect(errors[0].kind).toBe("ParseError");
16
- });
17
-
18
- it("script with unterminated string → ParseError with line number", () => {
19
- const errors = ashValidate('job "test { find /users }');
20
- expect(errors.length).toBeGreaterThan(0);
21
- });
22
-
23
- it("multiple errors → all collected in array", () => {
24
- // Two unknown commands
25
- const errors = ashValidate('job "test" { foobar | bazqux }');
26
- expect(errors.length).toBeGreaterThan(0);
27
- });
28
-
29
- // ── Bad Path ───────────────────────────────────────────
30
-
31
- it("empty string → empty ParseError[] (valid, no jobs)", () => {
32
- const errors = ashValidate("");
33
- expect(errors).toHaveLength(0);
34
- });
35
-
36
- it("null/undefined input → rejected", () => {
37
- expect(() => ashValidate(null as any)).toThrow();
38
- expect(() => ashValidate(undefined as any)).toThrow();
39
- });
40
-
41
- // ── Edge Cases ─────────────────────────────────────────
42
-
43
- it("script with only comments → valid", () => {
44
- // ASH may or may not support comments — if it does, this is valid
45
- // If not, it may be an empty program which is valid
46
- const errors = ashValidate("");
47
- expect(errors).toHaveLength(0);
48
- });
49
-
50
- it("script with unknown command → type error reported", () => {
51
- const errors = ashValidate('job "test" { find /users | foobar }');
52
- expect(errors.length).toBeGreaterThan(0);
53
- });
54
- });
@@ -1,34 +0,0 @@
1
- /**
2
- * ash.validate — Static analysis for ASH scripts.
3
- *
4
- * Reuses existing compileSource() diagnostics to catch syntax + type errors
5
- * without executing.
6
- */
7
-
8
- import { compileSource } from "../compiler.js";
9
- import type { AshTypedError } from "./ash-typed-error.js";
10
-
11
- export function ashValidate(source: string): AshTypedError[] {
12
- if (source == null) {
13
- throw new Error("ash.validate: source must be a string, got " + typeof source);
14
- }
15
-
16
- if (!source.trim()) {
17
- return [];
18
- }
19
-
20
- const result = compileSource(source);
21
- const errors: AshTypedError[] = [];
22
-
23
- for (const diag of result.diagnostics) {
24
- if (diag.severity === "warning") continue;
25
-
26
- errors.push({
27
- kind: "ParseError",
28
- line: diag.line,
29
- message: diag.message,
30
- });
31
- }
32
-
33
- return errors;
34
- }
@@ -1,364 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
- import { runDevLoop } from "./dev-loop.js";
3
- import type { AshRunResult } from "./ash-run-result.js";
4
-
5
- /** Fake think: returns tool calls with ASH script content */
6
- function fakeThink(scripts: string[]) {
7
- let call = 0;
8
- return vi.fn(async (_req: any) => ({
9
- kind: "completed" as const,
10
- response: {
11
- content: scripts[call] ?? "",
12
- tool_calls: [
13
- { id: `tc-${call}`, name: "ash_run", arguments: JSON.stringify({ script: scripts[call++] ?? "" }) },
14
- ],
15
- },
16
- toolResults: [],
17
- }));
18
- }
19
-
20
- /** Fake runner: returns success or failure based on the script */
21
- function fakeRunner(failScripts: Set<string> = new Set()) {
22
- return vi.fn(async (source: string): Promise<AshRunResult> => {
23
- if (failScripts.has(source)) {
24
- return {
25
- status: "error",
26
- steps: [{ step: 1, command: "find", status: "error", duration_ms: 5 }],
27
- failedAt: { kind: "RuntimeError", message: `Failed: ${source}` },
28
- duration_ms: 5,
29
- };
30
- }
31
- return {
32
- status: "ok",
33
- steps: [{ step: 1, command: "find", status: "ok", duration_ms: 5, output: [{ id: 1 }] }],
34
- output: [{ id: 1 }],
35
- duration_ms: 5,
36
- };
37
- });
38
- }
39
-
40
- function fakeValidate() {
41
- return vi.fn(async (_source: string) => []);
42
- }
43
-
44
- function fakeObserve() {
45
- return vi.fn(async (_data: any) => {});
46
- }
47
-
48
- describe("AI Dev Loop Core", () => {
49
- // ── Happy Path ─────────────────────────────────────────
50
-
51
- it("success on first try", async () => {
52
- const think = fakeThink(['job "test" { find /users }']);
53
- const runner = fakeRunner();
54
- const validate = fakeValidate();
55
- const observe = fakeObserve();
56
-
57
- const result = await runDevLoop({
58
- intent: "find all users",
59
- max_iterations: 3,
60
- think,
61
- runner,
62
- validate,
63
- observe,
64
- });
65
-
66
- expect(result.status).toBe("ok");
67
- expect(result.iterations).toBe(1);
68
- expect(result.finalResult?.status).toBe("ok");
69
- expect(think).toHaveBeenCalledTimes(1);
70
- });
71
-
72
- it("first run fails → LLM corrects → second run succeeds", async () => {
73
- const badScript = "bad script";
74
- const goodScript = 'job "fixed" { find /users }';
75
- const think = fakeThink([badScript, goodScript]);
76
- const runner = fakeRunner(new Set([badScript]));
77
- const validate = fakeValidate();
78
- const observe = fakeObserve();
79
-
80
- const result = await runDevLoop({
81
- intent: "find users",
82
- max_iterations: 5,
83
- think,
84
- runner,
85
- validate,
86
- observe,
87
- });
88
-
89
- expect(result.status).toBe("ok");
90
- expect(result.iterations).toBe(2);
91
- expect(think).toHaveBeenCalledTimes(2);
92
- });
93
-
94
- it("returns final AshRunResult and all observations", async () => {
95
- const think = fakeThink(["script1"]);
96
- const runner = fakeRunner();
97
- const observe = fakeObserve();
98
-
99
- const result = await runDevLoop({
100
- intent: "do something",
101
- max_iterations: 3,
102
- think,
103
- runner,
104
- validate: fakeValidate(),
105
- observe,
106
- });
107
-
108
- expect(result.finalResult).toBeDefined();
109
- expect(result.observations.length).toBeGreaterThanOrEqual(1);
110
- });
111
-
112
- it("each iteration calls observe with execution result", async () => {
113
- const think = fakeThink(["s1", "s2"]);
114
- const runner = fakeRunner(new Set(["s1"]));
115
- const observe = fakeObserve();
116
-
117
- await runDevLoop({
118
- intent: "test",
119
- max_iterations: 3,
120
- think,
121
- runner,
122
- validate: fakeValidate(),
123
- observe,
124
- });
125
-
126
- expect(observe).toHaveBeenCalledTimes(2);
127
- });
128
-
129
- it("calls validate before run", async () => {
130
- const callOrder: string[] = [];
131
- const validate = vi.fn(async () => { callOrder.push("validate"); return []; });
132
- const runner = vi.fn(async (): Promise<AshRunResult> => {
133
- callOrder.push("run");
134
- return { status: "ok", steps: [], output: [], duration_ms: 0 };
135
- });
136
-
137
- await runDevLoop({
138
- intent: "test",
139
- max_iterations: 1,
140
- think: fakeThink(["script"]),
141
- runner,
142
- validate,
143
- observe: fakeObserve(),
144
- });
145
-
146
- expect(callOrder).toEqual(["validate", "run"]);
147
- });
148
-
149
- it("successful loop returns iteration count", async () => {
150
- const think = fakeThink(["a", "b", "c"]);
151
- const runner = fakeRunner(new Set(["a", "b"]));
152
-
153
- const result = await runDevLoop({
154
- intent: "test",
155
- max_iterations: 5,
156
- think,
157
- runner,
158
- validate: fakeValidate(),
159
- observe: fakeObserve(),
160
- });
161
-
162
- expect(result.iterations).toBe(3);
163
- });
164
-
165
- // ── Bad Path ───────────────────────────────────────────
166
-
167
- it("exceeds max_iterations → returns last failure (INV-4)", async () => {
168
- const think = fakeThink(["bad1", "bad2", "bad3"]);
169
- const runner = fakeRunner(new Set(["bad1", "bad2", "bad3"]));
170
-
171
- const result = await runDevLoop({
172
- intent: "will fail",
173
- max_iterations: 3,
174
- think,
175
- runner,
176
- validate: fakeValidate(),
177
- observe: fakeObserve(),
178
- });
179
-
180
- expect(result.status).toBe("error");
181
- expect(result.iterations).toBe(3);
182
- expect(result.finalResult?.status).toBe("error");
183
- expect(result.observations).toHaveLength(3);
184
- });
185
-
186
- it("validate catches parse error → correction prompt without run", async () => {
187
- const validate = vi.fn(async (source: string) => {
188
- if (source === "bad syntax") {
189
- return [{ kind: "ParseError" as const, message: "unexpected token", line: 1 }];
190
- }
191
- return [];
192
- });
193
- const runner = fakeRunner();
194
- const think = fakeThink(["bad syntax", "good script"]);
195
-
196
- const result = await runDevLoop({
197
- intent: "test",
198
- max_iterations: 3,
199
- think,
200
- runner,
201
- validate,
202
- observe: fakeObserve(),
203
- });
204
-
205
- // Runner should only be called for the good script
206
- expect(runner).toHaveBeenCalledTimes(1);
207
- expect(result.status).toBe("ok");
208
- });
209
-
210
- it("LLM returns empty response → treated as error", async () => {
211
- const think = vi.fn(async () => ({
212
- kind: "completed" as const,
213
- response: { content: "", tool_calls: undefined },
214
- toolResults: [],
215
- }));
216
-
217
- const result = await runDevLoop({
218
- intent: "test",
219
- max_iterations: 2,
220
- think,
221
- runner: fakeRunner(),
222
- validate: fakeValidate(),
223
- observe: fakeObserve(),
224
- });
225
-
226
- expect(result.status).toBe("error");
227
- });
228
-
229
- it("think throws → loop terminates with error", async () => {
230
- const think = vi.fn(async () => { throw new Error("LLM down"); });
231
-
232
- const result = await runDevLoop({
233
- intent: "test",
234
- max_iterations: 3,
235
- think,
236
- runner: fakeRunner(),
237
- validate: fakeValidate(),
238
- observe: fakeObserve(),
239
- });
240
-
241
- expect(result.status).toBe("error");
242
- expect(result.error).toContain("LLM down");
243
- });
244
-
245
- it("budget exceeded mid-loop → loop terminates with error", async () => {
246
- let callCount = 0;
247
- const think = vi.fn(async () => {
248
- callCount++;
249
- if (callCount === 2) throw new Error("BudgetExceeded: token limit reached");
250
- return {
251
- kind: "completed" as const,
252
- response: {
253
- content: "",
254
- tool_calls: [{ id: "tc-1", name: "ash_run", arguments: JSON.stringify({ script: "fail" }) }],
255
- },
256
- toolResults: [],
257
- };
258
- });
259
-
260
- const result = await runDevLoop({
261
- intent: "test",
262
- max_iterations: 5,
263
- think,
264
- runner: fakeRunner(new Set(["fail"])),
265
- validate: fakeValidate(),
266
- observe: fakeObserve(),
267
- });
268
-
269
- expect(result.status).toBe("error");
270
- expect(result.error).toContain("BudgetExceeded");
271
- expect(result.iterations).toBe(2);
272
- });
273
-
274
- it("extractScript: script from content fallback when no tool_calls", async () => {
275
- const think = vi.fn(async () => ({
276
- kind: "completed" as const,
277
- response: { content: 'job "fromContent" { find /x }', tool_calls: undefined },
278
- toolResults: [],
279
- }));
280
-
281
- const result = await runDevLoop({
282
- intent: "test",
283
- max_iterations: 1,
284
- think,
285
- runner: fakeRunner(),
286
- validate: fakeValidate(),
287
- observe: fakeObserve(),
288
- });
289
-
290
- expect(result.status).toBe("ok");
291
- });
292
-
293
- // ── Edge Cases ─────────────────────────────────────────
294
-
295
- it("max_iterations=1 → single attempt, no correction", async () => {
296
- const think = fakeThink(["script"]);
297
- const runner = fakeRunner(new Set(["script"]));
298
-
299
- const result = await runDevLoop({
300
- intent: "test",
301
- max_iterations: 1,
302
- think,
303
- runner,
304
- validate: fakeValidate(),
305
- observe: fakeObserve(),
306
- });
307
-
308
- expect(result.status).toBe("error");
309
- expect(result.iterations).toBe(1);
310
- expect(think).toHaveBeenCalledTimes(1);
311
- });
312
-
313
- it("LLM fixes on last iteration → success", async () => {
314
- const scripts = ["bad1", "bad2", "bad3", "bad4", "good"];
315
- const think = fakeThink(scripts);
316
- const runner = fakeRunner(new Set(["bad1", "bad2", "bad3", "bad4"]));
317
-
318
- const result = await runDevLoop({
319
- intent: "test",
320
- max_iterations: 5,
321
- think,
322
- runner,
323
- validate: fakeValidate(),
324
- observe: fakeObserve(),
325
- });
326
-
327
- expect(result.status).toBe("ok");
328
- expect(result.iterations).toBe(5);
329
- });
330
-
331
- it("all iterations fail → returns array of all failures", async () => {
332
- const think = fakeThink(["f1", "f2"]);
333
- const runner = fakeRunner(new Set(["f1", "f2"]));
334
-
335
- const result = await runDevLoop({
336
- intent: "test",
337
- max_iterations: 2,
338
- think,
339
- runner,
340
- validate: fakeValidate(),
341
- observe: fakeObserve(),
342
- });
343
-
344
- expect(result.observations).toHaveLength(2);
345
- });
346
-
347
- // ── Security ───────────────────────────────────────────
348
-
349
- it("default mode is dry-run (INV-1)", async () => {
350
- const think = fakeThink(["script"]);
351
- const runner = fakeRunner();
352
-
353
- const result = await runDevLoop({
354
- intent: "test",
355
- max_iterations: 1,
356
- think,
357
- runner,
358
- validate: fakeValidate(),
359
- observe: fakeObserve(),
360
- });
361
-
362
- expect(result.mode).toBe("dry-run");
363
- });
364
- });
@@ -1,156 +0,0 @@
1
- /**
2
- * AI Dev Loop Core — bounded generate → validate → run → observe → correct loop.
3
- *
4
- * Invariants enforced:
5
- * - INV-1: Default mode is dry-run
6
- * - INV-3: Observe only, never commit to long-term memory
7
- * - INV-4: Bounded by max_iterations
8
- */
9
-
10
- import type { AshRunResult, AshRunFailure } from "./ash-run-result.js";
11
- import type { AshTypedError } from "./ash-typed-error.js";
12
- import { buildSystemPrompt, buildCorrectionPrompt } from "./system-prompt.js";
13
-
14
- export interface DevLoopConfig {
15
- intent: string;
16
- max_iterations: number;
17
- mode?: "dry-run" | "live";
18
- think: (request: any) => Promise<any>;
19
- runner: (source: string) => Promise<AshRunResult>;
20
- validate: (source: string) => Promise<AshTypedError[]>;
21
- observe: (data: any) => Promise<void>;
22
- }
23
-
24
- export interface DevLoopResult {
25
- status: "ok" | "error";
26
- iterations: number;
27
- mode: "dry-run" | "live";
28
- finalResult?: AshRunResult;
29
- observations: any[];
30
- error?: string;
31
- }
32
-
33
- /**
34
- * Run the AI dev loop: think → extract ASH → validate → run → observe → correct or return.
35
- */
36
- export async function runDevLoop(config: DevLoopConfig): Promise<DevLoopResult> {
37
- const mode = config.mode ?? "dry-run";
38
- const observations: any[] = [];
39
- let lastFailure: AshRunFailure | undefined;
40
- let lastScript: string | undefined;
41
-
42
- for (let i = 0; i < config.max_iterations; i++) {
43
- // 1. Think — ask LLM for ASH script
44
- let thinkResult: any;
45
- try {
46
- const messages: any[] = [];
47
- if (i === 0) {
48
- messages.push({ role: "system", content: buildSystemPrompt({ max_iterations: config.max_iterations }) });
49
- messages.push({ role: "user", content: config.intent });
50
- } else {
51
- messages.push({ role: "user", content: buildCorrectionPrompt(lastFailure!, lastScript!) });
52
- }
53
- thinkResult = await config.think({ messages });
54
- } catch (err: any) {
55
- return { status: "error", iterations: i + 1, mode, observations, error: err.message };
56
- }
57
-
58
- // 2. Extract script from tool call or content
59
- const script = extractScript(thinkResult);
60
- if (!script) {
61
- const obs = { iteration: i + 1, error: "empty_response" };
62
- observations.push(obs);
63
- await config.observe(obs);
64
- // If no script, treat as failure for this iteration
65
- if (i === config.max_iterations - 1) {
66
- return { status: "error", iterations: i + 1, mode, observations, error: "LLM returned no script" };
67
- }
68
- // Create a synthetic failure for correction
69
- lastFailure = {
70
- status: "error",
71
- steps: [],
72
- failedAt: { kind: "RuntimeError", message: "LLM returned no ASH script" } as AshTypedError,
73
- duration_ms: 0,
74
- };
75
- lastScript = "";
76
- continue;
77
- }
78
-
79
- lastScript = script;
80
-
81
- // 3. Validate
82
- const errors = await config.validate(script);
83
- if (errors.length > 0) {
84
- const obs = { iteration: i + 1, script, validation_errors: errors };
85
- observations.push(obs);
86
- await config.observe(obs);
87
-
88
- if (i === config.max_iterations - 1) {
89
- return {
90
- status: "error",
91
- iterations: i + 1,
92
- mode,
93
- observations,
94
- finalResult: {
95
- status: "error",
96
- steps: [],
97
- failedAt: errors[0],
98
- duration_ms: 0,
99
- },
100
- };
101
- }
102
-
103
- lastFailure = {
104
- status: "error",
105
- steps: [],
106
- failedAt: errors[0] as AshTypedError,
107
- duration_ms: 0,
108
- };
109
- continue;
110
- }
111
-
112
- // 4. Run
113
- const result = await config.runner(script);
114
-
115
- // 5. Observe
116
- const obs = { iteration: i + 1, script, result };
117
- observations.push(obs);
118
- await config.observe(obs);
119
-
120
- // 6. Check result
121
- if (result.status === "ok") {
122
- return { status: "ok", iterations: i + 1, mode, finalResult: result, observations };
123
- }
124
-
125
- // Failed — prepare for correction
126
- lastFailure = result as AshRunFailure;
127
- }
128
-
129
- // Exhausted all iterations
130
- return {
131
- status: "error",
132
- iterations: config.max_iterations,
133
- mode,
134
- finalResult: lastFailure,
135
- observations,
136
- };
137
- }
138
-
139
- function extractScript(thinkResult: any): string | undefined {
140
- // Try tool_calls first (ash_run call)
141
- const toolCalls = thinkResult?.response?.tool_calls;
142
- if (Array.isArray(toolCalls)) {
143
- for (const tc of toolCalls) {
144
- if (tc.name === "ash_run") {
145
- try {
146
- const args = typeof tc.arguments === "string" ? JSON.parse(tc.arguments) : tc.arguments;
147
- if (args?.script) return args.script;
148
- } catch { /* ignore parse error */ }
149
- }
150
- }
151
- }
152
- // Fall back to content
153
- const content = thinkResult?.response?.content;
154
- if (typeof content === "string" && content.trim()) return content.trim();
155
- return undefined;
156
- }