@aigne/afs-ash 1.11.0-beta.12

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.
@@ -0,0 +1,873 @@
1
+ //#region src/ash-generator.ts
2
+ /**
3
+ * Format a value as an ASH inline literal.
4
+ * Strings get quoted, numbers/booleans are bare.
5
+ */
6
+ function formatValue(v) {
7
+ if (typeof v === "string") return JSON.stringify(v);
8
+ if (typeof v === "number" || typeof v === "boolean") return String(v);
9
+ return JSON.stringify(v);
10
+ }
11
+ /**
12
+ * Format args as ASH inline object: { key: "val", num: 42 }
13
+ */
14
+ function formatInlineArgs(args) {
15
+ const entries = Object.entries(args);
16
+ if (entries.length === 0) return "";
17
+ return ` { ${entries.map(([k, v]) => `${k}: ${formatValue(v)}`).join(", ")} }`;
18
+ }
19
+ /**
20
+ * Generate a single pipeline statement for a tool call.
21
+ */
22
+ function generateStatement(call) {
23
+ const resultPath = `/.results/${call.id}`;
24
+ if (call.op === "read" || call.op === "list") return `find ${call.path} | save ${resultPath}`;
25
+ const inlineArgs = formatInlineArgs(call.args);
26
+ return `action ${call.path}${inlineArgs} | save ${resultPath}`;
27
+ }
28
+ /**
29
+ * Derive @caps declaration from the calls in this round.
30
+ *
31
+ * Capabilities:
32
+ * - read: for read/list ops (exact path)
33
+ * - exec: for exec ops (exact path)
34
+ * - write: always /.results/* (where save goes)
35
+ */
36
+ function deriveCaps(calls) {
37
+ const parts = [];
38
+ for (const call of calls) if (call.op === "read" || call.op === "list") parts.push(`read ${call.path}`);
39
+ else if (call.op === "exec") parts.push(`exec ${call.path}`);
40
+ parts.push("write /.results/*");
41
+ return `@caps(${parts.join(" ")})`;
42
+ }
43
+ /**
44
+ * Generate a complete ASH program from validated tool calls.
45
+ *
46
+ * Each call becomes a separate pipeline within a single job.
47
+ * The program is scoped with @caps derived from actual paths.
48
+ */
49
+ function generateAsh(calls, round) {
50
+ const caps = deriveCaps(calls);
51
+ const statements = calls.map(generateStatement);
52
+ return `${caps}\njob round_${round} {\n ${statements.length > 0 ? statements.join("\n ") : "output \"no-op\""}\n}`;
53
+ }
54
+
55
+ //#endregion
56
+ //#region src/path-validator.ts
57
+ /**
58
+ * Match an actual path against a declaration pattern.
59
+ *
60
+ * Supports:
61
+ * - Exact: "/a/b" matches "/a/b"
62
+ * - Single-level `*`: "/a/*" matches "/a/b" but NOT "/a/b/c"
63
+ * - Multi-level `**`: "/a/**" matches "/a/b/c" up to `maxDepth` levels from `**` position
64
+ *
65
+ * @param maxDepth Maximum levels `**` can consume. Defaults to 0 (no expansion).
66
+ */
67
+ function matchPath(actual, pattern, maxDepth = 0) {
68
+ if (actual === pattern) return true;
69
+ const patternParts = pattern.split("/");
70
+ const actualParts = actual.split("/");
71
+ const dstarIdx = patternParts.indexOf("**");
72
+ if (dstarIdx === -1) return matchSingleLevel(actualParts, patternParts);
73
+ const prefix = patternParts.slice(0, dstarIdx);
74
+ const suffix = patternParts.slice(dstarIdx + 1);
75
+ if (actualParts.length < prefix.length + suffix.length) return false;
76
+ for (let i = 0; i < prefix.length; i++) {
77
+ if (prefix[i] === "*") continue;
78
+ if (prefix[i] !== actualParts[i]) return false;
79
+ }
80
+ const suffixStart = actualParts.length - suffix.length;
81
+ if (suffixStart < prefix.length) return false;
82
+ for (let i = 0; i < suffix.length; i++) {
83
+ if (suffix[i] === "*") continue;
84
+ if (suffix[i] !== actualParts[suffixStart + i]) return false;
85
+ }
86
+ const dstarLevels = actualParts.length - prefix.length - suffix.length;
87
+ return dstarLevels >= 0 && dstarLevels <= maxDepth;
88
+ }
89
+ /** Single-level * matching (no **). */
90
+ function matchSingleLevel(actualParts, patternParts) {
91
+ if (!patternParts.some((p) => p === "*")) return false;
92
+ if (actualParts.length === patternParts.length) {
93
+ for (let i = 0; i < patternParts.length; i++) {
94
+ if (patternParts[i] === "*") continue;
95
+ if (patternParts[i] !== actualParts[i]) return false;
96
+ }
97
+ return true;
98
+ }
99
+ if (actualParts.length < patternParts.length) {
100
+ for (let i = actualParts.length; i < patternParts.length; i++) if (patternParts[i] !== "*") return false;
101
+ for (let i = 0; i < actualParts.length; i++) if (patternParts[i] !== "*" && patternParts[i] !== actualParts[i]) return false;
102
+ return true;
103
+ }
104
+ return false;
105
+ }
106
+ /**
107
+ * Extract the action name from an exec path.
108
+ * E.g., "/modules/github/issues/42/.actions/close-issue" → "close-issue"
109
+ */
110
+ function extractActionName(path) {
111
+ const idx = path.lastIndexOf("/.actions/");
112
+ if (idx === -1) return void 0;
113
+ return path.slice(idx + 10).split("/")[0];
114
+ }
115
+ /**
116
+ * Validate a tool_call's path and operation against the tools declaration.
117
+ */
118
+ function validateToolCall(path, op, tools) {
119
+ let basePath;
120
+ if (op === "exec") {
121
+ const idx = path.indexOf("/.actions/");
122
+ if (idx !== -1) basePath = path.slice(0, idx);
123
+ }
124
+ let pathMatched = false;
125
+ for (const entry of tools) {
126
+ const md = entry.maxDepth ?? 0;
127
+ if (!matchPath(path, entry.path, md) && !(basePath && matchPath(basePath, entry.path, md))) continue;
128
+ pathMatched = true;
129
+ if (!entry.ops.includes(op)) continue;
130
+ if (op === "exec" && entry.exclude_actions?.length) {
131
+ const actionName = extractActionName(path);
132
+ if (actionName && entry.exclude_actions.includes(actionName)) return {
133
+ allowed: false,
134
+ reason: `Action '${actionName}' is excluded on '${entry.path}'`
135
+ };
136
+ }
137
+ return { allowed: true };
138
+ }
139
+ if (pathMatched) return {
140
+ allowed: false,
141
+ reason: `Operation '${op}' not allowed on '${path}'`
142
+ };
143
+ return {
144
+ allowed: false,
145
+ reason: `Path '${path}' not in tools declaration`
146
+ };
147
+ }
148
+
149
+ //#endregion
150
+ //#region src/tool-schema.ts
151
+ const OP_DEFINITIONS = {
152
+ read: {
153
+ name: "afs_read",
154
+ description: "Read content at an AFS path",
155
+ extraProps: {},
156
+ extraRequired: []
157
+ },
158
+ list: {
159
+ name: "afs_list",
160
+ description: "List directory contents at an AFS path",
161
+ extraProps: {
162
+ depth: {
163
+ type: "number",
164
+ description: "Recursion depth (default 1)"
165
+ },
166
+ pattern: {
167
+ type: "string",
168
+ description: "Filter pattern, e.g. *.md"
169
+ }
170
+ },
171
+ extraRequired: []
172
+ },
173
+ exec: {
174
+ name: "afs_exec",
175
+ description: "Execute an action at an AFS path. IMPORTANT: Before calling, use afs_list on the parent path's .actions/ to discover available actions and their inputSchema, then pass the required fields in args.",
176
+ extraProps: { args: {
177
+ type: "object",
178
+ description: "Action arguments matching the action's inputSchema"
179
+ } },
180
+ extraRequired: []
181
+ },
182
+ search: {
183
+ name: "afs_search",
184
+ description: "Search for content within an AFS path",
185
+ extraProps: { query: {
186
+ type: "string",
187
+ description: "Search query"
188
+ } },
189
+ extraRequired: ["query"]
190
+ },
191
+ write: {
192
+ name: "afs_write",
193
+ description: "Write content to an AFS path",
194
+ extraProps: { content: {
195
+ type: "string",
196
+ description: "Content to write"
197
+ } },
198
+ extraRequired: ["content"]
199
+ },
200
+ stat: {
201
+ name: "afs_stat",
202
+ description: "Get metadata for an AFS path",
203
+ extraProps: {},
204
+ extraRequired: []
205
+ },
206
+ explain: {
207
+ name: "afs_explain",
208
+ description: "Get a human-readable explanation for an AFS path",
209
+ extraProps: {},
210
+ extraRequired: []
211
+ }
212
+ };
213
+ /**
214
+ * Build OpenAI function-calling tools from ToolEntry declarations.
215
+ *
216
+ * Deduplicates operations across entries — one tool per unique op.
217
+ * Each tool's path description lists all allowed patterns for that op.
218
+ *
219
+ * When actionSchemas are provided (from startup discovery), generates
220
+ * per-action tools (afs_action_{name}) with explicit typed parameters
221
+ * so LLMs know exactly what fields each action requires.
222
+ */
223
+ function buildToolSchema(tools, actionSchemas) {
224
+ const pathsByOp = /* @__PURE__ */ new Map();
225
+ const excludedActionsByOp = /* @__PURE__ */ new Map();
226
+ const maxDepthByOp = /* @__PURE__ */ new Map();
227
+ for (const entry of tools) for (const op of entry.ops) {
228
+ if (!pathsByOp.has(op)) pathsByOp.set(op, []);
229
+ const paths = pathsByOp.get(op);
230
+ if (!paths.includes(entry.path)) paths.push(entry.path);
231
+ if (entry.path.includes("**") && entry.maxDepth != null) {
232
+ if (!maxDepthByOp.has(op)) maxDepthByOp.set(op, /* @__PURE__ */ new Map());
233
+ maxDepthByOp.get(op).set(entry.path, entry.maxDepth);
234
+ }
235
+ if (op === "exec" && entry.exclude_actions?.length) {
236
+ if (!excludedActionsByOp.has(op)) excludedActionsByOp.set(op, []);
237
+ const excluded = excludedActionsByOp.get(op);
238
+ for (const a of entry.exclude_actions) if (!excluded.includes(a)) excluded.push(a);
239
+ }
240
+ }
241
+ const result = [];
242
+ for (const [op, paths] of pathsByOp) {
243
+ const def = OP_DEFINITIONS[op];
244
+ if (!def) continue;
245
+ const depthMap = maxDepthByOp.get(op);
246
+ const hasDstar = paths.some((p) => p.includes("**"));
247
+ let pathDesc = `AFS path. Allowed: ${paths.join(", ")}.`;
248
+ if (hasDstar && depthMap) {
249
+ const depthNotes = [...depthMap.entries()].map(([p, d]) => `${p} (maxDepth ${d})`).join(", ");
250
+ pathDesc += ` ** matches multiple levels: ${depthNotes}.`;
251
+ }
252
+ pathDesc += " Note: * matches one level only (e.g. /a/* matches /a/b but NOT /a/b/c).";
253
+ const excluded = excludedActionsByOp.get(op);
254
+ if (excluded?.length) pathDesc += ` Excluded actions: ${excluded.join(", ")}.`;
255
+ let description = def.description;
256
+ let extraProps = def.extraProps;
257
+ let extraRequired = def.extraRequired;
258
+ if (op === "exec" && actionSchemas?.length) {
259
+ description = `Execute an action. Pass the action's parameters in args.
260
+ Available actions:\n${actionSchemas.map((a) => {
261
+ const fields = Object.entries(a.inputSchema?.properties ?? {}).map(([k, v]) => {
262
+ const req = a.inputSchema?.required?.includes(k) ? " (required)" : "";
263
+ return `${k}: ${v.type ?? "string"}${req}`;
264
+ }).join(", ");
265
+ return `- ${a.pathPattern}: {${fields || "none"}} — ${a.description}`;
266
+ }).join("\n")}`;
267
+ extraRequired = ["args"];
268
+ const mergedProps = {};
269
+ for (const a of actionSchemas) if (a.inputSchema?.properties) for (const [k, v] of Object.entries(a.inputSchema.properties)) mergedProps[k] = {
270
+ type: v.type ?? "string",
271
+ ...v.description ? { description: v.description } : {}
272
+ };
273
+ extraProps = { args: {
274
+ type: "object",
275
+ description: "Action arguments matching the action's inputSchema",
276
+ ...Object.keys(mergedProps).length > 0 ? { properties: mergedProps } : {}
277
+ } };
278
+ }
279
+ result.push({
280
+ type: "function",
281
+ function: {
282
+ name: def.name,
283
+ description,
284
+ parameters: {
285
+ type: "object",
286
+ properties: {
287
+ path: {
288
+ type: "string",
289
+ description: pathDesc
290
+ },
291
+ ...extraProps
292
+ },
293
+ required: ["path", ...extraRequired]
294
+ }
295
+ }
296
+ });
297
+ }
298
+ return result;
299
+ }
300
+
301
+ //#endregion
302
+ //#region src/agent-run.ts
303
+ const DEFAULT_MAX_ROUNDS = 20;
304
+ const DEFAULT_ACTIONS_PER_ROUND = 10;
305
+ const RESULTS_PREFIX = "/.results/";
306
+ /**
307
+ * Operations routed through ASH (deterministic sandbox).
308
+ *
309
+ * Currently empty: all ops go through direct AFS calls.
310
+ * agent-run's security comes from path-validator (tools declaration whitelist),
311
+ * not from ASH @caps. The ASH sandbox is valuable for user-written scripts
312
+ * (/.actions/run) but redundant for agent-run's auto-generated single-step execs.
313
+ */
314
+ const ASH_OPS = /* @__PURE__ */ new Set([]);
315
+ /** Required arguments per operation (beyond path) */
316
+ const REQUIRED_ARGS = {
317
+ search: ["query"],
318
+ write: ["content"]
319
+ };
320
+ /** Map tool function name → AFS operation name */
321
+ function toolNameToOp(name) {
322
+ if (name.startsWith("afs_action_")) return "exec";
323
+ return {
324
+ afs_read: "read",
325
+ afs_list: "list",
326
+ afs_exec: "exec",
327
+ afs_search: "search",
328
+ afs_write: "write",
329
+ afs_stat: "stat",
330
+ afs_explain: "explain"
331
+ }[name];
332
+ }
333
+ /**
334
+ * Normalize a tool call from any format to our internal ToolCallInput.
335
+ *
336
+ * AigneHub returns OpenAI format:
337
+ * { id, type: "function", function: { name, arguments } }
338
+ * Some SDKs return Vercel AI SDK format:
339
+ * { toolCallId, toolName, args }
340
+ */
341
+ function normalizeToolCall(tc) {
342
+ if (tc.toolCallId && tc.toolName) return {
343
+ toolCallId: tc.toolCallId,
344
+ toolName: tc.toolName,
345
+ args: tc.args ?? {}
346
+ };
347
+ const fn = tc.function;
348
+ const rawArgs = fn?.arguments;
349
+ let args = {};
350
+ if (typeof rawArgs === "string") try {
351
+ args = JSON.parse(rawArgs);
352
+ } catch {
353
+ args = { _parseError: `Invalid JSON in function arguments: ${rawArgs.slice(0, 200)}` };
354
+ }
355
+ else if (rawArgs && typeof rawArgs === "object") args = rawArgs;
356
+ return {
357
+ toolCallId: tc.toolCallId ?? tc.id ?? "",
358
+ toolName: tc.toolName ?? fn?.name ?? "",
359
+ args
360
+ };
361
+ }
362
+ /**
363
+ * Fix path for per-action tool calls.
364
+ *
365
+ * LLMs often make mistakes with per-action tools:
366
+ * 1. Put tool name in path: /.actions/afs_action_send → /.actions/send
367
+ * 2. Drop mount prefix: /default/conversations/123 → /telegram/default/conversations/123
368
+ *
369
+ * Uses discovered pathPattern to auto-correct missing mount prefixes.
370
+ * Mutates call.args.path in place.
371
+ */
372
+ function fixPerActionPath(call, schemasByAction) {
373
+ const path = call.args.path;
374
+ if (!path) return;
375
+ const realName = call.toolName.slice(11).replace(/_/g, "-");
376
+ const marker = "/.actions/";
377
+ const idx = path.lastIndexOf(marker);
378
+ if (idx !== -1) {
379
+ const currentAction = path.slice(idx + 10).split("/")[0];
380
+ if (currentAction && currentAction !== realName) call.args.path = path.slice(0, idx) + marker + realName;
381
+ }
382
+ const schema = schemasByAction.get(realName);
383
+ if (!schema?.pathPattern) return;
384
+ const fixedPath = call.args.path;
385
+ const starIdx = schema.pathPattern.indexOf("*");
386
+ if (starIdx === -1) return;
387
+ const expectedPrefix = schema.pathPattern.slice(0, starIdx);
388
+ if (fixedPath.startsWith(expectedPrefix)) return;
389
+ for (let i = 1; i < expectedPrefix.length; i++) if (expectedPrefix[i] === "/" && fixedPath.startsWith(expectedPrefix.slice(i))) {
390
+ call.args.path = expectedPrefix.slice(0, i) + fixedPath;
391
+ return;
392
+ }
393
+ }
394
+ /** Extract exec args from tool call: try nested `args` field first, fall back to all fields except `path`. */
395
+ function extractExecArgs(callArgs) {
396
+ if (callArgs.args && typeof callArgs.args === "object" && !Array.isArray(callArgs.args)) return callArgs.args;
397
+ const { path: _, ...rest } = callArgs;
398
+ return rest;
399
+ }
400
+ /**
401
+ * Discover available actions and their inputSchemas at startup.
402
+ *
403
+ * Strategy (per exec-capable tool entry):
404
+ * 1. Instance-probe: list basePath to find a child, then list its .actions/
405
+ * 2. Capabilities fallback: walk up path hierarchy to find /.meta/.capabilities,
406
+ * extract action schemas from the manifest's catalog entries
407
+ *
408
+ * Results are used to generate per-action tools with explicit typed parameters.
409
+ * Best-effort: failures are silently ignored and the generic afs_exec fallback remains.
410
+ */
411
+ async function discoverActionSchemas(tools, deps) {
412
+ const schemas = [];
413
+ const discoveredPaths = /* @__PURE__ */ new Set();
414
+ const discoveredActions = /* @__PURE__ */ new Set();
415
+ for (const tool of tools) {
416
+ if (!tool.ops.includes("exec")) continue;
417
+ const basePath = tool.path.replace(/\/?\*+$/, "");
418
+ if (!basePath || discoveredPaths.has(basePath)) continue;
419
+ discoveredPaths.add(basePath);
420
+ try {
421
+ if (basePath.endsWith("/.actions")) {
422
+ await discoverFromActionsDir(tool, basePath, discoveredActions, schemas, deps);
423
+ continue;
424
+ }
425
+ if (!await discoverViaInstanceProbe(tool, basePath, discoveredActions, schemas, deps)) await discoverViaCapabilities(tool, basePath, discoveredActions, schemas, deps);
426
+ } catch {}
427
+ }
428
+ return schemas;
429
+ }
430
+ /** Fast path: tool path already ends with /.actions/* — list the directory directly. */
431
+ async function discoverFromActionsDir(tool, actionsPath, discoveredActions, schemas, deps) {
432
+ const result = await deps.callAFS("list", actionsPath);
433
+ if (!result.success) return;
434
+ const actions = extractEntries(result.data);
435
+ for (const action of actions) {
436
+ const name = extractActionNameFromEntry(action);
437
+ if (!name || discoveredActions.has(name)) continue;
438
+ discoveredActions.add(name);
439
+ schemas.push({
440
+ actionName: name,
441
+ description: action.meta?.description || `Execute ${name}`,
442
+ pathPattern: `${tool.path.replace(/\*+$/, name)}`,
443
+ inputSchema: action.meta?.inputSchema
444
+ });
445
+ }
446
+ }
447
+ /** Strategy 1: list basePath, pick first child, list its .actions/ */
448
+ async function discoverViaInstanceProbe(tool, basePath, discoveredActions, schemas, deps) {
449
+ const listResult = await deps.callAFS("list", basePath);
450
+ if (!listResult.success) return false;
451
+ const entries = extractEntries(listResult.data);
452
+ if (entries.length === 0) return false;
453
+ const mcpTools = entries.filter((e) => e.meta?.kind === "mcp:tool" || e.meta?.kinds?.includes?.("mcp:tool"));
454
+ if (mcpTools.length > 0) {
455
+ let found$1 = false;
456
+ for (const mcpTool of mcpTools) {
457
+ const name = mcpTool.meta?.mcp?.name || extractActionNameFromEntry(mcpTool);
458
+ if (!name || discoveredActions.has(name)) continue;
459
+ discoveredActions.add(name);
460
+ found$1 = true;
461
+ const toolPath = mcpTool.path || `${basePath}/${name}`;
462
+ schemas.push({
463
+ actionName: name,
464
+ description: mcpTool.meta?.description || `Execute ${name}`,
465
+ pathPattern: toolPath,
466
+ inputSchema: mcpTool.meta?.inputSchema
467
+ });
468
+ }
469
+ return found$1;
470
+ }
471
+ const firstChild = entries[0];
472
+ const childPath = firstChild.path || firstChild.id;
473
+ if (!childPath) return false;
474
+ const actionsPath = `${childPath}/.actions`;
475
+ const actionsResult = await deps.callAFS("list", actionsPath);
476
+ if (!actionsResult.success) return false;
477
+ const actions = extractEntries(actionsResult.data);
478
+ let found = false;
479
+ for (const action of actions) {
480
+ const name = extractActionNameFromEntry(action);
481
+ if (!name || discoveredActions.has(name)) continue;
482
+ discoveredActions.add(name);
483
+ found = true;
484
+ schemas.push({
485
+ actionName: name,
486
+ description: action.meta?.description || `Execute ${name}`,
487
+ pathPattern: `${tool.path}/.actions/${name}`,
488
+ inputSchema: action.meta?.inputSchema
489
+ });
490
+ }
491
+ return found;
492
+ }
493
+ /**
494
+ * Strategy 2: walk up path hierarchy to find a provider's /.meta/.capabilities,
495
+ * then extract action schemas from the manifest's catalog + inputSchema.
496
+ *
497
+ * For a tool path like /telegram/default/conversations/*, tries:
498
+ * /telegram/default/conversations/.meta/.capabilities
499
+ * /telegram/default/.meta/.capabilities
500
+ * /telegram/.meta/.capabilities
501
+ */
502
+ async function discoverViaCapabilities(tool, basePath, discoveredActions, schemas, deps) {
503
+ const segments = basePath.split("/").filter(Boolean);
504
+ for (let depth = segments.length; depth >= 1; depth--) {
505
+ const capsPath = `${`/${segments.slice(0, depth).join("/")}`}/.meta/.capabilities`;
506
+ try {
507
+ const result = await deps.callAFS("read", capsPath);
508
+ if (!result.success) continue;
509
+ const content = extractCapabilitiesContent(result.data);
510
+ if (!content?.actions) continue;
511
+ let found = false;
512
+ for (const actionGroup of content.actions) {
513
+ const catalog = actionGroup.catalog;
514
+ if (!Array.isArray(catalog)) continue;
515
+ for (const entry of catalog) {
516
+ const name = entry.name;
517
+ if (!name || discoveredActions.has(name)) continue;
518
+ discoveredActions.add(name);
519
+ found = true;
520
+ schemas.push({
521
+ actionName: name,
522
+ description: entry.description || `Execute ${name}`,
523
+ pathPattern: `${tool.path}/.actions/${name}`,
524
+ inputSchema: entry.inputSchema
525
+ });
526
+ }
527
+ }
528
+ if (found) return true;
529
+ } catch {}
530
+ }
531
+ return false;
532
+ }
533
+ /** Extract capabilities content from a read result (may be nested in data.content or data directly). */
534
+ function extractCapabilitiesContent(data) {
535
+ if (!data || typeof data !== "object") return null;
536
+ const d = data;
537
+ if (d.actions) return d;
538
+ if (d.content && typeof d.content === "object" && d.content.actions) return d.content;
539
+ return null;
540
+ }
541
+ /** Extract entries array from callAFS response data (may be raw array or wrapped). */
542
+ function extractEntries(data) {
543
+ if (Array.isArray(data)) return data;
544
+ if (data && typeof data === "object" && Array.isArray(data.data)) return data.data;
545
+ return [];
546
+ }
547
+ /** Extract action name from an action entry (from path or id). */
548
+ function extractActionNameFromEntry(entry) {
549
+ if (entry.meta?.name) return entry.meta.name;
550
+ const path = entry.path || entry.id || "";
551
+ const idx = path.lastIndexOf("/.actions/");
552
+ if (idx !== -1) return path.slice(idx + 10).split("/")[0];
553
+ if (entry.id && !entry.id.includes("/")) return entry.id;
554
+ }
555
+ /** Normalize an in-memory message (AigneHub camelCase) to canonical JSONL format (OpenAI snake_case). */
556
+ function normalizeForHistory(msg) {
557
+ const role = msg.role;
558
+ if (role === "system") return null;
559
+ if (role === "user") return {
560
+ role: "user",
561
+ content: String(msg.content ?? "")
562
+ };
563
+ if (role === "assistant") {
564
+ const result = {
565
+ role: "assistant",
566
+ content: String(msg.content ?? "")
567
+ };
568
+ const rawToolCalls = msg.toolCalls ?? msg.tool_calls;
569
+ if (rawToolCalls?.length) result.tool_calls = rawToolCalls.map((tc) => {
570
+ const fn = tc.function;
571
+ const rawArgs = fn?.arguments ?? tc.args;
572
+ return {
573
+ id: tc.id ?? tc.toolCallId ?? "",
574
+ type: "function",
575
+ function: {
576
+ name: fn?.name ?? tc.toolName ?? "",
577
+ arguments: typeof rawArgs === "string" ? rawArgs : JSON.stringify(rawArgs ?? {})
578
+ }
579
+ };
580
+ });
581
+ return result;
582
+ }
583
+ if (role === "tool") return {
584
+ role: "tool",
585
+ tool_call_id: msg.tool_call_id ?? msg.toolCallId ?? "",
586
+ content: String(msg.content ?? "")
587
+ };
588
+ return null;
589
+ }
590
+ async function runAgentLoop(params, deps) {
591
+ const maxRounds = params.budget?.max_rounds ?? DEFAULT_MAX_ROUNDS;
592
+ const actionsPerRound = params.budget?.actions_per_round ?? DEFAULT_ACTIONS_PER_ROUND;
593
+ const tokenBudget = params.budget?.total_tokens ?? Number.POSITIVE_INFINITY;
594
+ const actionSchemas = await discoverActionSchemas(params.tools, deps);
595
+ const schemasByAction = /* @__PURE__ */ new Map();
596
+ for (const s of actionSchemas) schemasByAction.set(s.actionName, s);
597
+ const toolSchema = buildToolSchema(params.tools, actionSchemas);
598
+ let history = [];
599
+ if (params.session && deps.loadHistory) try {
600
+ history = await deps.loadHistory(params.session);
601
+ } catch {}
602
+ const messages = [];
603
+ if (params.system) messages.push({
604
+ role: "system",
605
+ content: params.system
606
+ });
607
+ for (const msg of history) messages.push({ ...msg });
608
+ messages.push({
609
+ role: "user",
610
+ content: params.task
611
+ });
612
+ const saveSession = async () => {
613
+ if (!params.session || !deps.saveHistory) return;
614
+ try {
615
+ const toSave = [];
616
+ for (const msg of messages) {
617
+ const normalized = normalizeForHistory(msg);
618
+ if (normalized) toSave.push(normalized);
619
+ }
620
+ await deps.saveHistory(params.session, toSave);
621
+ } catch {}
622
+ };
623
+ const trace = [];
624
+ let totalTokens = 0;
625
+ let totalActions = 0;
626
+ for (let round = 1; round <= maxRounds; round++) {
627
+ const llmArgs = { messages: [...messages] };
628
+ if (toolSchema.length > 0) {
629
+ llmArgs.tools = toolSchema;
630
+ llmArgs.toolChoice = "auto";
631
+ }
632
+ const llmResult = await deps.callLLM(llmArgs);
633
+ if (!llmResult.success) {
634
+ const errMsg = llmResult.data?.error ?? llmResult.error?.message ?? "unknown error";
635
+ trace.push({
636
+ round,
637
+ tool_calls: [],
638
+ tokens: {
639
+ input: 0,
640
+ output: 0
641
+ }
642
+ });
643
+ await saveSession();
644
+ return {
645
+ status: "error",
646
+ error: `LLM call failed: ${errMsg}`,
647
+ rounds: round,
648
+ total_actions: totalActions,
649
+ total_tokens: totalTokens,
650
+ trace
651
+ };
652
+ }
653
+ const data = llmResult.data;
654
+ const inputTokens = data.inputTokens ?? 0;
655
+ const outputTokens = data.outputTokens ?? 0;
656
+ totalTokens += inputTokens + outputTokens;
657
+ const text = data.text;
658
+ const rawToolCalls = data.toolCalls;
659
+ if (!rawToolCalls || rawToolCalls.length === 0) {
660
+ trace.push({
661
+ round,
662
+ tool_calls: [],
663
+ tokens: {
664
+ input: inputTokens,
665
+ output: outputTokens
666
+ }
667
+ });
668
+ messages.push({
669
+ role: "assistant",
670
+ content: text ?? ""
671
+ });
672
+ await saveSession();
673
+ return {
674
+ status: "completed",
675
+ result: text ?? "",
676
+ rounds: round,
677
+ total_actions: totalActions,
678
+ total_tokens: totalTokens,
679
+ trace
680
+ };
681
+ }
682
+ if (totalTokens > tokenBudget) {
683
+ trace.push({
684
+ round,
685
+ tool_calls: [],
686
+ tokens: {
687
+ input: inputTokens,
688
+ output: outputTokens
689
+ }
690
+ });
691
+ await saveSession();
692
+ return {
693
+ status: "budget_exhausted",
694
+ result: text,
695
+ rounds: round,
696
+ total_actions: totalActions,
697
+ total_tokens: totalTokens,
698
+ trace
699
+ };
700
+ }
701
+ const toolCalls = rawToolCalls.map(normalizeToolCall);
702
+ messages.push({
703
+ role: "assistant",
704
+ content: text ?? "",
705
+ toolCalls: rawToolCalls
706
+ });
707
+ const roundCalls = toolCalls.slice(0, actionsPerRound);
708
+ const droppedCalls = toolCalls.slice(actionsPerRound);
709
+ const traceEntries = [];
710
+ const validatedCalls = [];
711
+ for (const call of roundCalls) {
712
+ const op = toolNameToOp(call.toolName);
713
+ if (!op) {
714
+ validatedCalls.push({
715
+ call,
716
+ op: "unknown",
717
+ valid: false,
718
+ reason: `Unknown tool: ${call.toolName}`
719
+ });
720
+ continue;
721
+ }
722
+ if (call.toolName.startsWith("afs_action_")) fixPerActionPath(call, schemasByAction);
723
+ const path = call.args.path;
724
+ if (!path) {
725
+ validatedCalls.push({
726
+ call,
727
+ op,
728
+ valid: false,
729
+ reason: "Missing path argument"
730
+ });
731
+ continue;
732
+ }
733
+ const validation = validateToolCall(path, op, params.tools);
734
+ if (!validation.allowed) {
735
+ validatedCalls.push({
736
+ call,
737
+ op,
738
+ valid: false,
739
+ reason: validation.reason
740
+ });
741
+ continue;
742
+ }
743
+ const missingArg = REQUIRED_ARGS[op]?.find((arg) => !(arg in call.args));
744
+ if (missingArg) {
745
+ validatedCalls.push({
746
+ call,
747
+ op,
748
+ valid: false,
749
+ reason: `Missing required argument '${missingArg}' for ${op}`
750
+ });
751
+ continue;
752
+ }
753
+ validatedCalls.push({
754
+ call,
755
+ op,
756
+ valid: true
757
+ });
758
+ }
759
+ const ashCalls = [];
760
+ const directCalls = [];
761
+ const errorResults = [];
762
+ for (const { call, op, valid, reason } of validatedCalls) {
763
+ if (!valid) {
764
+ errorResults.push({
765
+ id: call.toolCallId,
766
+ error: reason ?? "Validation failed"
767
+ });
768
+ traceEntries.push({
769
+ id: call.toolCallId,
770
+ name: call.toolName,
771
+ path: String(call.args.path ?? ""),
772
+ status: "error",
773
+ error: reason
774
+ });
775
+ continue;
776
+ }
777
+ const path = call.args.path;
778
+ if (ASH_OPS.has(op)) ashCalls.push({
779
+ id: call.toolCallId,
780
+ op,
781
+ path,
782
+ args: extractExecArgs(call.args)
783
+ });
784
+ else directCalls.push({
785
+ call,
786
+ op
787
+ });
788
+ traceEntries.push({
789
+ id: call.toolCallId,
790
+ name: call.toolName,
791
+ path,
792
+ status: "ok"
793
+ });
794
+ }
795
+ const ashResults = {};
796
+ if (ashCalls.length > 0) {
797
+ const source = generateAsh(ashCalls, round);
798
+ const ashResult = await deps.runAsh(source, {
799
+ returnWrittenData: true,
800
+ skipWritePrefix: RESULTS_PREFIX
801
+ });
802
+ if (ashResult.success && ashResult.data?.writtenData) {
803
+ const writtenData = ashResult.data.writtenData;
804
+ for (const call of ashCalls) {
805
+ const data$1 = writtenData[`${RESULTS_PREFIX}${call.id}`];
806
+ ashResults[call.id] = data$1 ? JSON.stringify(data$1) : "No result";
807
+ }
808
+ } else for (const call of ashCalls) ashResults[call.id] = `ASH execution error: ${ashResult.data?.error ?? "unknown"}`;
809
+ }
810
+ const directResults = {};
811
+ for (const { call, op } of directCalls) {
812
+ const path = call.args.path;
813
+ const directArgs = { ...call.args };
814
+ delete directArgs.path;
815
+ try {
816
+ const result = await deps.callAFS(op, path, directArgs);
817
+ directResults[call.toolCallId] = result.success ? JSON.stringify(result.data) : `Error: ${result.data?.error ?? "failed"}`;
818
+ } catch (err) {
819
+ const msg = err instanceof Error ? err.message : String(err);
820
+ directResults[call.toolCallId] = `Error: ${msg}`;
821
+ }
822
+ }
823
+ for (const call of roundCalls) {
824
+ const errorEntry = errorResults.find((e) => e.id === call.toolCallId);
825
+ if (errorEntry) messages.push({
826
+ role: "tool",
827
+ tool_call_id: call.toolCallId,
828
+ content: `Error: ${errorEntry.error}`
829
+ });
830
+ else if (ashResults[call.toolCallId] !== void 0) messages.push({
831
+ role: "tool",
832
+ tool_call_id: call.toolCallId,
833
+ content: ashResults[call.toolCallId]
834
+ });
835
+ else if (directResults[call.toolCallId] !== void 0) messages.push({
836
+ role: "tool",
837
+ tool_call_id: call.toolCallId,
838
+ content: directResults[call.toolCallId]
839
+ });
840
+ else messages.push({
841
+ role: "tool",
842
+ tool_call_id: call.toolCallId,
843
+ content: "Error: No result returned for this tool call"
844
+ });
845
+ }
846
+ for (const call of droppedCalls) messages.push({
847
+ role: "tool",
848
+ tool_call_id: call.toolCallId,
849
+ content: `Error: Tool call dropped — exceeded actions_per_round limit (${actionsPerRound}). Retry in next round.`
850
+ });
851
+ totalActions += roundCalls.length - errorResults.length;
852
+ trace.push({
853
+ round,
854
+ tool_calls: traceEntries,
855
+ tokens: {
856
+ input: inputTokens,
857
+ output: outputTokens
858
+ }
859
+ });
860
+ }
861
+ await saveSession();
862
+ return {
863
+ status: "budget_exhausted",
864
+ rounds: maxRounds,
865
+ total_actions: totalActions,
866
+ total_tokens: totalTokens,
867
+ trace
868
+ };
869
+ }
870
+
871
+ //#endregion
872
+ export { runAgentLoop };
873
+ //# sourceMappingURL=agent-run-CXJQ0jPV.mjs.map