@aigne/afs-agent 1.11.0-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +26 -0
- package/dist/index.d.mts +754 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +3667 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +62 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,3667 @@
|
|
|
1
|
+
import { ASH_EVENT_TYPES, makeNsLog } from "@aigne/afs";
|
|
2
|
+
import { validateToolCall } from "@aigne/afs-ash";
|
|
3
|
+
import { parseSkillFrontmatter } from "@aigne/afs-skills/format";
|
|
4
|
+
import { joinURL } from "ufo";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { AFSBaseProvider, Actions, Stat } from "@aigne/afs/provider";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { load } from "js-yaml";
|
|
11
|
+
|
|
12
|
+
//#region src/ash-generator.ts
|
|
13
|
+
/**
|
|
14
|
+
* Format a value as an ASH inline literal.
|
|
15
|
+
* Strings get quoted, numbers/booleans are bare.
|
|
16
|
+
*/
|
|
17
|
+
function formatValue(v) {
|
|
18
|
+
if (typeof v === "string") return JSON.stringify(v);
|
|
19
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
20
|
+
return JSON.stringify(v);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Format args as ASH inline object: { key: "val", num: 42 }
|
|
24
|
+
*/
|
|
25
|
+
function formatInlineArgs(args) {
|
|
26
|
+
const entries = Object.entries(args);
|
|
27
|
+
if (entries.length === 0) return "";
|
|
28
|
+
return ` { ${entries.map(([k, v]) => `${k}: ${formatValue(v)}`).join(", ")} }`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Generate a single pipeline statement for a tool call.
|
|
32
|
+
*/
|
|
33
|
+
function generateStatement(call) {
|
|
34
|
+
const resultPath = `/.results/${call.id}`;
|
|
35
|
+
if (call.op === "read" || call.op === "list") return `find ${call.path} | save ${resultPath}`;
|
|
36
|
+
const inlineArgs = formatInlineArgs(call.args);
|
|
37
|
+
return `action ${call.path}${inlineArgs} | save ${resultPath}`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Derive @caps declaration from the calls in this round.
|
|
41
|
+
*
|
|
42
|
+
* Capabilities:
|
|
43
|
+
* - read: for read/list ops (exact path)
|
|
44
|
+
* - exec: for exec ops (exact path)
|
|
45
|
+
* - write: always /.results/* (where save goes)
|
|
46
|
+
*/
|
|
47
|
+
function deriveCaps(calls) {
|
|
48
|
+
const parts = [];
|
|
49
|
+
for (const call of calls) if (call.op === "read" || call.op === "list") parts.push(`read ${call.path}`);
|
|
50
|
+
else if (call.op === "exec") parts.push(`exec ${call.path}`);
|
|
51
|
+
parts.push("write /.results/*");
|
|
52
|
+
return `@caps(${parts.join(" ")})`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Generate a complete ASH program from validated tool calls.
|
|
56
|
+
*
|
|
57
|
+
* Each call becomes a separate pipeline within a single job.
|
|
58
|
+
* The program is scoped with @caps derived from actual paths.
|
|
59
|
+
*/
|
|
60
|
+
function generateAsh(calls, round) {
|
|
61
|
+
const caps = deriveCaps(calls);
|
|
62
|
+
const statements = calls.map(generateStatement);
|
|
63
|
+
return `${caps}\njob round_${round} {\n ${statements.length > 0 ? statements.join("\n ") : "output \"no-op\""}\n}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/token-estimator.ts
|
|
68
|
+
/**
|
|
69
|
+
* Lightweight token estimation based on character types.
|
|
70
|
+
*
|
|
71
|
+
* Uses empirical heuristics for CJK characters, English words, and other characters
|
|
72
|
+
* to approximate token counts without requiring an external tokenizer.
|
|
73
|
+
*/
|
|
74
|
+
/** CJK Unified Ideographs + Extension A + Compatibility + Hiragana + Katakana */
|
|
75
|
+
const CJK_PATTERN = /[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff\u3040-\u309f\u30a0-\u30ff]/g;
|
|
76
|
+
/** Consecutive ASCII letter sequences (English words) */
|
|
77
|
+
const WORD_PATTERN = /[a-zA-Z]+/g;
|
|
78
|
+
/** CJK: ~1.5 characters per token */
|
|
79
|
+
const CJK_CHARS_PER_TOKEN = 1.5;
|
|
80
|
+
/** English: ~1 token per word (conservative estimate) */
|
|
81
|
+
const TOKENS_PER_WORD = 1;
|
|
82
|
+
/**
|
|
83
|
+
* Estimate token count for a text string.
|
|
84
|
+
*
|
|
85
|
+
* Uses character-type-aware heuristics:
|
|
86
|
+
* - CJK characters: ~1.5 chars per token
|
|
87
|
+
* - English words: ~1 token per word
|
|
88
|
+
* - Other characters (punctuation, digits, whitespace): ~1 char per token
|
|
89
|
+
*/
|
|
90
|
+
function estimateTokens(text) {
|
|
91
|
+
if (!text) return 0;
|
|
92
|
+
let tokens = 0;
|
|
93
|
+
let accountedChars = 0;
|
|
94
|
+
const cjkMatches = text.match(CJK_PATTERN);
|
|
95
|
+
if (cjkMatches) {
|
|
96
|
+
tokens += cjkMatches.length / CJK_CHARS_PER_TOKEN;
|
|
97
|
+
accountedChars += cjkMatches.length;
|
|
98
|
+
}
|
|
99
|
+
const wordMatches = text.match(WORD_PATTERN);
|
|
100
|
+
if (wordMatches) {
|
|
101
|
+
tokens += wordMatches.length * TOKENS_PER_WORD;
|
|
102
|
+
for (const w of wordMatches) accountedChars += w.length;
|
|
103
|
+
}
|
|
104
|
+
const remaining = text.length - accountedChars;
|
|
105
|
+
if (remaining > 0) tokens += remaining;
|
|
106
|
+
return Math.ceil(tokens);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Estimate total tokens for an array of messages (OpenAI format).
|
|
110
|
+
*
|
|
111
|
+
* Handles:
|
|
112
|
+
* - `content` (string) — text tokens, capped per message
|
|
113
|
+
* - `toolCalls` / `tool_calls` — function name + arguments + structure overhead
|
|
114
|
+
*
|
|
115
|
+
* @param singleMessageLimit - Max tokens counted per individual message content
|
|
116
|
+
* (prevents a single huge tool result from dominating the estimate)
|
|
117
|
+
*/
|
|
118
|
+
function estimateMessagesTokens(messages, singleMessageLimit = Number.POSITIVE_INFINITY) {
|
|
119
|
+
let total = 0;
|
|
120
|
+
for (const msg of messages) {
|
|
121
|
+
let msgTokens = 0;
|
|
122
|
+
const content = msg.content;
|
|
123
|
+
if (typeof content === "string") msgTokens += Math.min(estimateTokens(content), singleMessageLimit);
|
|
124
|
+
const toolCalls = msg.toolCalls ?? msg.tool_calls;
|
|
125
|
+
if (Array.isArray(toolCalls)) for (const tc of toolCalls) {
|
|
126
|
+
const fn = tc.function;
|
|
127
|
+
if (fn) {
|
|
128
|
+
msgTokens += estimateTokens(String(fn.name ?? ""));
|
|
129
|
+
const args = fn.arguments;
|
|
130
|
+
msgTokens += estimateTokens(typeof args === "string" ? args : JSON.stringify(args ?? {}));
|
|
131
|
+
msgTokens += 10;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
total += msgTokens;
|
|
135
|
+
}
|
|
136
|
+
return total;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/context.ts
|
|
141
|
+
const DEFAULT_MEMORY_MAX_TOKENS = 2e3;
|
|
142
|
+
/**
|
|
143
|
+
* Convert JSONL content (from archived sessions) into a readable conversation format.
|
|
144
|
+
* Non-JSONL content is returned as-is.
|
|
145
|
+
*/
|
|
146
|
+
function formatRecallContent(content) {
|
|
147
|
+
const lines = content.split("\n");
|
|
148
|
+
if (lines.length === 0) return content;
|
|
149
|
+
const formatted = [];
|
|
150
|
+
let hasJson = false;
|
|
151
|
+
for (const line of lines) {
|
|
152
|
+
const trimmed = line.trim();
|
|
153
|
+
if (!trimmed) continue;
|
|
154
|
+
try {
|
|
155
|
+
const msg = JSON.parse(trimmed);
|
|
156
|
+
if (msg.role && msg.content) {
|
|
157
|
+
formatted.push(`[${msg.role}]: ${msg.content}`);
|
|
158
|
+
hasJson = true;
|
|
159
|
+
} else formatted.push(trimmed);
|
|
160
|
+
} catch {
|
|
161
|
+
formatted.push(trimmed);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return hasJson && formatted.length > 0 ? formatted.join("\n") : content;
|
|
165
|
+
}
|
|
166
|
+
async function buildMemoryContext(afs, opts, task) {
|
|
167
|
+
let recallResult;
|
|
168
|
+
try {
|
|
169
|
+
recallResult = await afs.exec(opts.recallPath, {
|
|
170
|
+
text: task,
|
|
171
|
+
scope: opts.domain,
|
|
172
|
+
limit: 30
|
|
173
|
+
}, {});
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
if (!recallResult.success) return null;
|
|
178
|
+
const data = recallResult.data;
|
|
179
|
+
const layers = data?.layers;
|
|
180
|
+
if (!Array.isArray(layers) || layers.length === 0) return null;
|
|
181
|
+
const maxTokens = opts.maxTokens ?? DEFAULT_MEMORY_MAX_TOKENS;
|
|
182
|
+
const l3 = layers.find((l) => l.level === "L3");
|
|
183
|
+
const l2 = layers.find((l) => l.level === "L2");
|
|
184
|
+
const parts = [];
|
|
185
|
+
const sources = [];
|
|
186
|
+
let tokens = 0;
|
|
187
|
+
for (const layer of [l3, l2]) {
|
|
188
|
+
if (!layer?.entries) continue;
|
|
189
|
+
for (const entry of layer.entries) {
|
|
190
|
+
if (!entry.content) continue;
|
|
191
|
+
const readable = formatRecallContent(entry.content);
|
|
192
|
+
const t = estimateTokens(readable);
|
|
193
|
+
if (tokens + t > maxTokens) continue;
|
|
194
|
+
parts.push(readable);
|
|
195
|
+
sources.push(entry.path);
|
|
196
|
+
tokens += t;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (parts.length === 0) return null;
|
|
200
|
+
return {
|
|
201
|
+
text: parts.join("\n\n---\n\n"),
|
|
202
|
+
trace: {
|
|
203
|
+
tokens,
|
|
204
|
+
entries: parts.length,
|
|
205
|
+
sources
|
|
206
|
+
},
|
|
207
|
+
query: task,
|
|
208
|
+
available: data?.totalAvailable
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Assemble a messages array for LLM calls.
|
|
213
|
+
*
|
|
214
|
+
* Output order: [system, ...history, user]
|
|
215
|
+
*
|
|
216
|
+
* System message = system prompt + memory recall + directives.
|
|
217
|
+
* Without memory, output is identical to agent-run's current hand-assembled messages.
|
|
218
|
+
* Memory failures silently fall back to the no-memory path.
|
|
219
|
+
*/
|
|
220
|
+
async function buildContext(afs, opts) {
|
|
221
|
+
let memoryResult = null;
|
|
222
|
+
if (opts.memory) memoryResult = await buildMemoryContext(afs, opts.memory, opts.task);
|
|
223
|
+
const systemParts = [];
|
|
224
|
+
if (opts.system) systemParts.push(opts.system);
|
|
225
|
+
if (memoryResult) systemParts.push(`\n## Relevant memories\n${memoryResult.text}`);
|
|
226
|
+
if (opts.directives) {
|
|
227
|
+
for (const d of opts.directives) if (d) systemParts.push(d);
|
|
228
|
+
}
|
|
229
|
+
const systemContent = systemParts.join("\n\n");
|
|
230
|
+
const messages = [];
|
|
231
|
+
messages.push({
|
|
232
|
+
role: "system",
|
|
233
|
+
content: systemContent
|
|
234
|
+
});
|
|
235
|
+
if (opts.history) for (const msg of opts.history) messages.push(msg);
|
|
236
|
+
messages.push({
|
|
237
|
+
role: "user",
|
|
238
|
+
content: opts.task
|
|
239
|
+
});
|
|
240
|
+
const systemTokens = estimateTokens(systemContent);
|
|
241
|
+
const userTokens = estimateTokens(opts.task);
|
|
242
|
+
const historyTokens = opts.history ? estimateMessagesTokens(opts.history) : 0;
|
|
243
|
+
const historyCount = opts.history?.length ?? 0;
|
|
244
|
+
const manifest = {
|
|
245
|
+
memory: {
|
|
246
|
+
selected: memoryResult?.trace.sources ?? [],
|
|
247
|
+
query: opts.memory ? opts.task : void 0,
|
|
248
|
+
available: memoryResult?.available
|
|
249
|
+
},
|
|
250
|
+
history: {
|
|
251
|
+
keptTurns: historyCount,
|
|
252
|
+
truncatedTurns: 0,
|
|
253
|
+
compressed: false
|
|
254
|
+
},
|
|
255
|
+
tools: { included: [] }
|
|
256
|
+
};
|
|
257
|
+
return {
|
|
258
|
+
messages,
|
|
259
|
+
trace: {
|
|
260
|
+
system: { tokens: systemTokens },
|
|
261
|
+
memory: memoryResult?.trace ?? null,
|
|
262
|
+
history: {
|
|
263
|
+
tokens: historyTokens,
|
|
264
|
+
messages: historyCount
|
|
265
|
+
},
|
|
266
|
+
user: { tokens: userTokens },
|
|
267
|
+
total: systemTokens + historyTokens + userTokens,
|
|
268
|
+
manifest
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region src/context-compress.ts
|
|
275
|
+
const DEFAULT_CONTEXT_WINDOW$1 = 8e4;
|
|
276
|
+
/** Trigger compression when messages exceed this ratio of context window */
|
|
277
|
+
const TRIGGER_RATIO = .7;
|
|
278
|
+
/** Keep recent messages up to this ratio of context window */
|
|
279
|
+
const KEEP_RECENT_RATIO = .35;
|
|
280
|
+
/** Timeout for the compression LLM call (ms) */
|
|
281
|
+
const COMPRESS_TIMEOUT_MS = 6e4;
|
|
282
|
+
/** Max characters for the formatted summary input sent to LLM */
|
|
283
|
+
const MAX_SUMMARY_INPUT_CHARS = 3e4;
|
|
284
|
+
/** Per-tool-result truncation limit for summary formatting */
|
|
285
|
+
const TOOL_RESULT_TRUNCATE = 500;
|
|
286
|
+
/** System prompt for the summarizer LLM call */
|
|
287
|
+
const COMPRESS_SYSTEM_PROMPT = `You are a conversation compressor. Your task is to losslessly compress the provided conversation into a structured summary that preserves ALL important context for future conversation continuity.
|
|
288
|
+
|
|
289
|
+
You are NOT the assistant — do not respond to the user or continue the conversation. You are an external summarizer.
|
|
290
|
+
|
|
291
|
+
Output format — use this exact structure:
|
|
292
|
+
|
|
293
|
+
## User Profile
|
|
294
|
+
<Who the user is: name, role, expertise, preferences — if mentioned>
|
|
295
|
+
|
|
296
|
+
## Conversation Summary
|
|
297
|
+
<Chronological summary of what happened: topics discussed, questions asked, actions taken, tools called and their outcomes>
|
|
298
|
+
|
|
299
|
+
## Key Decisions & Facts
|
|
300
|
+
<Bullet list of specific decisions made, facts established, paths/URLs/IDs mentioned, configurations set>
|
|
301
|
+
|
|
302
|
+
## Current State
|
|
303
|
+
<Where things stand: what was accomplished, what is in progress, any errors or blockers encountered>
|
|
304
|
+
|
|
305
|
+
## Open Items
|
|
306
|
+
<Pending tasks, unanswered questions, follow-up items — if any>
|
|
307
|
+
|
|
308
|
+
Rules:
|
|
309
|
+
- Preserve specific details: names, file paths, IDs, error messages, config values
|
|
310
|
+
- Include tool call outcomes (what was called, what it returned in brief)
|
|
311
|
+
- Write in the same language the user used in the conversation
|
|
312
|
+
- Be thorough — it is better to include too much than to lose critical context
|
|
313
|
+
- If a section has no content, write "None" and move on`;
|
|
314
|
+
async function maybeCompress(messages, opts) {
|
|
315
|
+
const contextWindow = opts.contextWindow ?? DEFAULT_CONTEXT_WINDOW$1;
|
|
316
|
+
const triggerThreshold = Math.floor(contextWindow * TRIGGER_RATIO);
|
|
317
|
+
const keepRecentTokens = Math.floor(contextWindow * KEEP_RECENT_RATIO);
|
|
318
|
+
const totalTokens = estimateMessagesTokens(messages);
|
|
319
|
+
if (totalTokens <= triggerThreshold) {
|
|
320
|
+
opts.onProgress?.({
|
|
321
|
+
phase: "skipped",
|
|
322
|
+
beforeTokens: totalTokens,
|
|
323
|
+
contextWindow
|
|
324
|
+
});
|
|
325
|
+
return messages;
|
|
326
|
+
}
|
|
327
|
+
let systemMessage;
|
|
328
|
+
let contentStart = 0;
|
|
329
|
+
if (messages.length > 0 && messages[0].role === "system") {
|
|
330
|
+
systemMessage = messages[0];
|
|
331
|
+
contentStart = 1;
|
|
332
|
+
}
|
|
333
|
+
const singleMessageLimit = Math.floor(keepRecentTokens * .5);
|
|
334
|
+
let splitIndex = messages.length;
|
|
335
|
+
let recentTokens = 0;
|
|
336
|
+
for (let i = messages.length - 1; i >= contentStart; i--) {
|
|
337
|
+
const msgTokens = estimateMessagesTokens([messages[i]], singleMessageLimit);
|
|
338
|
+
if (recentTokens + msgTokens > keepRecentTokens) {
|
|
339
|
+
splitIndex = i + 1;
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
recentTokens += msgTokens;
|
|
343
|
+
splitIndex = i;
|
|
344
|
+
}
|
|
345
|
+
if (splitIndex <= contentStart) {
|
|
346
|
+
splitIndex = messages.length;
|
|
347
|
+
recentTokens = 0;
|
|
348
|
+
for (let i = messages.length - 1; i >= contentStart; i--) {
|
|
349
|
+
const msgTokens = estimateMessagesTokens([messages[i]]);
|
|
350
|
+
if (recentTokens + msgTokens > keepRecentTokens) {
|
|
351
|
+
splitIndex = i + 1;
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
recentTokens += msgTokens;
|
|
355
|
+
splitIndex = i;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
splitIndex = adjustSplitForMessageBoundary(messages, splitIndex, contentStart);
|
|
359
|
+
const pinIdx = opts.pinMessage && contentStart > 0 ? messages.indexOf(opts.pinMessage, contentStart) : opts.pinMessage ? messages.indexOf(opts.pinMessage) : -1;
|
|
360
|
+
const pinValid = pinIdx >= contentStart;
|
|
361
|
+
const pinInCompressZone = pinValid && !(pinValid && pinIdx >= splitIndex);
|
|
362
|
+
const pinnedMessage = pinValid ? messages[pinIdx] : void 0;
|
|
363
|
+
const toCompress = [];
|
|
364
|
+
for (let i = contentStart; i < splitIndex; i++) {
|
|
365
|
+
if (i === pinIdx) continue;
|
|
366
|
+
toCompress.push(messages[i]);
|
|
367
|
+
}
|
|
368
|
+
const toKeep = messages.slice(splitIndex);
|
|
369
|
+
if (toCompress.length === 0) return messages;
|
|
370
|
+
const baseProgress = {
|
|
371
|
+
beforeTokens: totalTokens,
|
|
372
|
+
contextWindow,
|
|
373
|
+
compressedMessages: toCompress.length,
|
|
374
|
+
keptMessages: toKeep.length
|
|
375
|
+
};
|
|
376
|
+
opts.onProgress?.({
|
|
377
|
+
phase: "triggered",
|
|
378
|
+
...baseProgress
|
|
379
|
+
});
|
|
380
|
+
try {
|
|
381
|
+
const formatted = formatMessagesForSummary(toCompress);
|
|
382
|
+
opts.onProgress?.({
|
|
383
|
+
phase: "summarizing",
|
|
384
|
+
...baseProgress
|
|
385
|
+
});
|
|
386
|
+
const compressResult = await Promise.race([opts.callLLM({ messages: [{
|
|
387
|
+
role: "system",
|
|
388
|
+
content: COMPRESS_SYSTEM_PROMPT
|
|
389
|
+
}, {
|
|
390
|
+
role: "user",
|
|
391
|
+
content: formatted
|
|
392
|
+
}] }), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("Compression LLM call timed out")), COMPRESS_TIMEOUT_MS))]);
|
|
393
|
+
if (!compressResult.success) return compressFallback(systemMessage, toKeep, toCompress, baseProgress, opts, "LLM call failed", pinInCompressZone ? pinnedMessage : void 0);
|
|
394
|
+
const summary = compressResult.data?.text;
|
|
395
|
+
if (typeof summary !== "string" || !summary.trim()) return compressFallback(systemMessage, toKeep, toCompress, baseProgress, opts, "Empty summary", pinInCompressZone ? pinnedMessage : void 0);
|
|
396
|
+
const result = [];
|
|
397
|
+
if (systemMessage) result.push(systemMessage);
|
|
398
|
+
if (pinInCompressZone && pinnedMessage) result.push(pinnedMessage);
|
|
399
|
+
result.push({
|
|
400
|
+
role: "user",
|
|
401
|
+
content: `[Previous conversation summary]\n${summary.trim()}`
|
|
402
|
+
});
|
|
403
|
+
result.push(...toKeep);
|
|
404
|
+
const afterTokens = estimateMessagesTokens(result);
|
|
405
|
+
opts.onProgress?.({
|
|
406
|
+
phase: "done",
|
|
407
|
+
...baseProgress,
|
|
408
|
+
afterTokens
|
|
409
|
+
});
|
|
410
|
+
if (opts.onCompressed) try {
|
|
411
|
+
await opts.onCompressed(result, toCompress);
|
|
412
|
+
} catch {}
|
|
413
|
+
return result;
|
|
414
|
+
} catch (err) {
|
|
415
|
+
return compressFallback(systemMessage, toKeep, toCompress, baseProgress, opts, err instanceof Error ? err.message : String(err), pinInCompressZone ? pinnedMessage : void 0);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Fallback when LLM-based compression fails (timeout, error, empty summary).
|
|
420
|
+
* Drops compressed messages and inserts a "[conversation history truncated]"
|
|
421
|
+
* placeholder so the agent can continue instead of looping.
|
|
422
|
+
*/
|
|
423
|
+
function compressFallback(systemMessage, toKeep, toCompress, baseProgress, opts, error, pinnedMessage) {
|
|
424
|
+
const result = [];
|
|
425
|
+
if (systemMessage) result.push(systemMessage);
|
|
426
|
+
if (pinnedMessage) result.push(pinnedMessage);
|
|
427
|
+
result.push({
|
|
428
|
+
role: "user",
|
|
429
|
+
content: `[Previous conversation history was truncated due to context limits. ${toCompress.length} older messages were removed.]`
|
|
430
|
+
});
|
|
431
|
+
result.push(...toKeep);
|
|
432
|
+
const afterTokens = estimateMessagesTokens(result);
|
|
433
|
+
opts.onProgress?.({
|
|
434
|
+
phase: "done",
|
|
435
|
+
...baseProgress,
|
|
436
|
+
afterTokens,
|
|
437
|
+
error: `Fallback truncation: ${error}`
|
|
438
|
+
});
|
|
439
|
+
if (opts.onCompressed) try {
|
|
440
|
+
opts.onCompressed(result, toCompress);
|
|
441
|
+
} catch {}
|
|
442
|
+
return result;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Adjust splitIndex so it doesn't break tool_call/tool_result groups.
|
|
446
|
+
*
|
|
447
|
+
* Rules:
|
|
448
|
+
* 1. Keep zone must not start with a tool message (orphaned result without parent assistant).
|
|
449
|
+
* → Walk backward to include the parent assistant+tool_calls.
|
|
450
|
+
* 2. Compress zone must not end with assistant+tool_calls whose results are in keep zone.
|
|
451
|
+
* → Move that assistant into keep zone.
|
|
452
|
+
*/
|
|
453
|
+
function adjustSplitForMessageBoundary(messages, splitIndex, contentStart) {
|
|
454
|
+
if (splitIndex <= contentStart || splitIndex >= messages.length) return splitIndex;
|
|
455
|
+
while (splitIndex > contentStart && messages[splitIndex]?.role === "tool") splitIndex--;
|
|
456
|
+
if (splitIndex > contentStart) {
|
|
457
|
+
const lastCompress = messages[splitIndex - 1];
|
|
458
|
+
const toolCalls = lastCompress?.toolCalls ?? lastCompress?.tool_calls;
|
|
459
|
+
if (Array.isArray(toolCalls) && toolCalls.length > 0) splitIndex--;
|
|
460
|
+
}
|
|
461
|
+
return splitIndex;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Format a single message into a readable line for the summarizer.
|
|
465
|
+
*/
|
|
466
|
+
function formatSingleMessage(msg) {
|
|
467
|
+
const role = String(msg.role ?? "unknown");
|
|
468
|
+
const content = String(msg.content ?? "");
|
|
469
|
+
const toolCalls = msg.toolCalls ?? msg.tool_calls;
|
|
470
|
+
if (Array.isArray(toolCalls) && toolCalls.length > 0) {
|
|
471
|
+
const callSummary = toolCalls.map((tc) => `${tc.function?.name ?? "unknown"}(...)`).join(", ");
|
|
472
|
+
return `[${role}] ${content.slice(0, 500)}\n Tool calls: ${callSummary}`;
|
|
473
|
+
}
|
|
474
|
+
if (role === "tool") return `[tool result] ${content.slice(0, TOOL_RESULT_TRUNCATE)}`;
|
|
475
|
+
return `[${role}] ${content.slice(0, 1e3)}`;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Format messages into a readable text block for the summarizer.
|
|
479
|
+
*
|
|
480
|
+
* Uses a two-end strategy: prioritize the earliest messages (conversation start,
|
|
481
|
+
* often contains user identity and goals) and the most recent messages (current
|
|
482
|
+
* context and latest actions). Middle messages are omitted if budget is exceeded.
|
|
483
|
+
*/
|
|
484
|
+
function formatMessagesForSummary(messages) {
|
|
485
|
+
const headBudget = Math.floor(MAX_SUMMARY_INPUT_CHARS * .4);
|
|
486
|
+
const tailBudget = Math.floor(MAX_SUMMARY_INPUT_CHARS * .6);
|
|
487
|
+
const headParts = [];
|
|
488
|
+
let headLen = 0;
|
|
489
|
+
let headEnd = 0;
|
|
490
|
+
for (let i = 0; i < messages.length; i++) {
|
|
491
|
+
const line = formatSingleMessage(messages[i]);
|
|
492
|
+
if (headLen + line.length + 2 > headBudget) break;
|
|
493
|
+
headParts.push(line);
|
|
494
|
+
headLen += line.length + 2;
|
|
495
|
+
headEnd = i + 1;
|
|
496
|
+
}
|
|
497
|
+
const tailParts = [];
|
|
498
|
+
let tailLen = 0;
|
|
499
|
+
let tailStart = messages.length;
|
|
500
|
+
for (let i = messages.length - 1; i >= headEnd; i--) {
|
|
501
|
+
const line = formatSingleMessage(messages[i]);
|
|
502
|
+
if (tailLen + line.length + 2 > tailBudget) break;
|
|
503
|
+
tailParts.unshift(line);
|
|
504
|
+
tailLen += line.length + 2;
|
|
505
|
+
tailStart = i;
|
|
506
|
+
}
|
|
507
|
+
const omitted = tailStart - headEnd;
|
|
508
|
+
if (omitted > 0) return [
|
|
509
|
+
...headParts,
|
|
510
|
+
`\n[... ${omitted} messages omitted ...]\n`,
|
|
511
|
+
...tailParts
|
|
512
|
+
].join("\n\n");
|
|
513
|
+
return [...headParts, ...tailParts].join("\n\n");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
//#endregion
|
|
517
|
+
//#region src/observability.ts
|
|
518
|
+
/**
|
|
519
|
+
* Agent-run observability helpers (Phase 0.5 jsonl).
|
|
520
|
+
*
|
|
521
|
+
* One run = one .jsonl file under `/instance/logs/agent-runs/{date}/`. Each line
|
|
522
|
+
* is one event with `type` discriminator + monotonic `seq` + ms `ts`. Writes go
|
|
523
|
+
* through the standard `callAFS("write", path, { content, mode: "append" })`
|
|
524
|
+
* interface so a future AFSLog provider can be mounted at `/instance/logs/`
|
|
525
|
+
* without touching application code.
|
|
526
|
+
*
|
|
527
|
+
* - `safeStringifyLine`: JSON.stringify with no indent, circular/BigInt/Date/
|
|
528
|
+
* Symbol/Function handling, plus per-line size cap via largest-field-first
|
|
529
|
+
* truncation.
|
|
530
|
+
* - `truncateField`: per-field truncation marker for over-budget strings + nested objects.
|
|
531
|
+
* - `stripInternalFields`: drops `_*` prefixed keys from recorded toolCall args
|
|
532
|
+
* (prevents `_scope_afs` / `_abort_signal` etc. from leaking into the log).
|
|
533
|
+
*/
|
|
534
|
+
const FIELD_LIMIT = 64 * 1024;
|
|
535
|
+
const EVENT_LIMIT = 256 * 1024;
|
|
536
|
+
function buildReplacer() {
|
|
537
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
538
|
+
return (_key, val) => {
|
|
539
|
+
if (typeof val === "bigint") return `${val.toString()}n`;
|
|
540
|
+
if (val instanceof Date) return val.toISOString();
|
|
541
|
+
if (typeof val === "function") return "[Function]";
|
|
542
|
+
if (typeof val === "symbol") return val.toString();
|
|
543
|
+
if (val && typeof val === "object") {
|
|
544
|
+
if (seen.has(val)) return "[Circular]";
|
|
545
|
+
seen.add(val);
|
|
546
|
+
}
|
|
547
|
+
return val;
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Stringify a single jsonl event into one line (no indent). Returns valid JSON
|
|
552
|
+
* even for `undefined` (which `JSON.stringify` would otherwise yield as
|
|
553
|
+
* `undefined` and break a `JSON.parse` round-trip).
|
|
554
|
+
*
|
|
555
|
+
* If the line exceeds `EVENT_LIMIT`, the largest fields are truncated until the
|
|
556
|
+
* line fits; truncated fields gain `_truncated: true` + `_originalSize` markers.
|
|
557
|
+
*/
|
|
558
|
+
function safeStringifyLine(value, maxEventSize = EVENT_LIMIT) {
|
|
559
|
+
if (value === void 0) return JSON.stringify({
|
|
560
|
+
_serialization_failed: true,
|
|
561
|
+
error: "input is undefined"
|
|
562
|
+
});
|
|
563
|
+
let json;
|
|
564
|
+
try {
|
|
565
|
+
json = JSON.stringify(value, buildReplacer());
|
|
566
|
+
} catch (err) {
|
|
567
|
+
return JSON.stringify({
|
|
568
|
+
_serialization_failed: true,
|
|
569
|
+
error: err instanceof Error ? err.message : String(err)
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
if (json.length > maxEventSize && value !== null && typeof value === "object" && !Array.isArray(value)) return truncateOversizedLine(value, maxEventSize);
|
|
573
|
+
return json;
|
|
574
|
+
}
|
|
575
|
+
function truncateOversizedLine(obj, maxEventSize) {
|
|
576
|
+
const truncated = { ...obj };
|
|
577
|
+
const fieldSizes = [];
|
|
578
|
+
for (const k of Object.keys(truncated)) {
|
|
579
|
+
let size;
|
|
580
|
+
try {
|
|
581
|
+
size = JSON.stringify(truncated[k], buildReplacer()).length;
|
|
582
|
+
} catch {
|
|
583
|
+
size = 0;
|
|
584
|
+
}
|
|
585
|
+
fieldSizes.push([k, size]);
|
|
586
|
+
}
|
|
587
|
+
fieldSizes.sort((a, b) => b[1] - a[1]);
|
|
588
|
+
for (const [k] of fieldSizes) {
|
|
589
|
+
truncated[k] = truncateField(truncated[k], FIELD_LIMIT);
|
|
590
|
+
let sized;
|
|
591
|
+
try {
|
|
592
|
+
sized = JSON.stringify(truncated, buildReplacer()).length;
|
|
593
|
+
} catch {
|
|
594
|
+
sized = maxEventSize + 1;
|
|
595
|
+
}
|
|
596
|
+
if (sized <= maxEventSize) break;
|
|
597
|
+
}
|
|
598
|
+
try {
|
|
599
|
+
return JSON.stringify(truncated, buildReplacer());
|
|
600
|
+
} catch (err) {
|
|
601
|
+
return JSON.stringify({
|
|
602
|
+
_serialization_failed: true,
|
|
603
|
+
error: err instanceof Error ? err.message : String(err)
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function truncateField(value, limit) {
|
|
608
|
+
if (value === null || value === void 0) return value;
|
|
609
|
+
if (typeof value === "string") {
|
|
610
|
+
if (value.length > limit) return {
|
|
611
|
+
_truncated: true,
|
|
612
|
+
_originalSize: value.length,
|
|
613
|
+
content: value.slice(0, limit)
|
|
614
|
+
};
|
|
615
|
+
return value;
|
|
616
|
+
}
|
|
617
|
+
let json;
|
|
618
|
+
try {
|
|
619
|
+
json = JSON.stringify(value, buildReplacer());
|
|
620
|
+
} catch {
|
|
621
|
+
return {
|
|
622
|
+
_truncated: true,
|
|
623
|
+
_serialization_failed: true
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
if (json.length > limit) return {
|
|
627
|
+
_truncated: true,
|
|
628
|
+
_originalSize: json.length,
|
|
629
|
+
content: json.slice(0, limit)
|
|
630
|
+
};
|
|
631
|
+
return value;
|
|
632
|
+
}
|
|
633
|
+
function stripInternalFields(args) {
|
|
634
|
+
const cleaned = {};
|
|
635
|
+
for (const [k, v] of Object.entries(args)) if (!k.startsWith("_")) cleaned[k] = v;
|
|
636
|
+
return cleaned;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
//#endregion
|
|
640
|
+
//#region src/tool-schema.ts
|
|
641
|
+
const OP_DEFINITIONS = {
|
|
642
|
+
read: {
|
|
643
|
+
name: "afs_read",
|
|
644
|
+
description: "Read RAW content at an AFS path. Use this ONLY when you already know the path is a data file and you want its bytes (e.g. /data/persona.md, a log). DO NOT use `afs_read` to learn how something works or discover capabilities — use `afs_explain` for that; `afs_explain` returns a curated guide (markdown with usage, warnings, required next steps) that raw reads omit, and skipping it causes agents to miss mandatory post-mount setup and write broken workarounds.\nTo read SEVERAL files (e.g. every child a list just returned), pass `entries` in ONE call and OMIT `path` — never loop single reads. Batch results are per-entry: check each results[i].success, partial success is normal (a missing file fails alone).",
|
|
645
|
+
extraProps: { entries: {
|
|
646
|
+
type: "array",
|
|
647
|
+
description: "Batch mode: array of {path} entries, up to 16 per call. When provided, top-level `path` is ignored. Chunk larger sets into multiple calls.",
|
|
648
|
+
items: {
|
|
649
|
+
type: "object",
|
|
650
|
+
properties: { path: {
|
|
651
|
+
type: "string",
|
|
652
|
+
description: "AFS path to read"
|
|
653
|
+
} },
|
|
654
|
+
required: ["path"]
|
|
655
|
+
}
|
|
656
|
+
} },
|
|
657
|
+
extraRequired: [],
|
|
658
|
+
pathOptional: true
|
|
659
|
+
},
|
|
660
|
+
list: {
|
|
661
|
+
name: "afs_list",
|
|
662
|
+
description: "List directory contents at an AFS path",
|
|
663
|
+
extraProps: {
|
|
664
|
+
depth: {
|
|
665
|
+
type: "number",
|
|
666
|
+
description: "Recursion depth (default 1)"
|
|
667
|
+
},
|
|
668
|
+
pattern: {
|
|
669
|
+
type: "string",
|
|
670
|
+
description: "Filter pattern, e.g. *.md"
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
extraRequired: []
|
|
674
|
+
},
|
|
675
|
+
exec: {
|
|
676
|
+
name: "afs_exec",
|
|
677
|
+
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.",
|
|
678
|
+
extraProps: { args: {
|
|
679
|
+
type: "object",
|
|
680
|
+
description: "Action arguments matching the action's inputSchema"
|
|
681
|
+
} },
|
|
682
|
+
extraRequired: []
|
|
683
|
+
},
|
|
684
|
+
search: {
|
|
685
|
+
name: "afs_search",
|
|
686
|
+
description: "Search for content within an AFS path",
|
|
687
|
+
extraProps: { query: {
|
|
688
|
+
type: "string",
|
|
689
|
+
description: "Search query"
|
|
690
|
+
} },
|
|
691
|
+
extraRequired: ["query"]
|
|
692
|
+
},
|
|
693
|
+
write: {
|
|
694
|
+
name: "afs_write",
|
|
695
|
+
description: "Write content to an AFS path. Eight modes cover most cases without read-modify-write loops:\n - replace (default) — overwrite path with `content`.\n - append / prepend — add `content` at end / start of existing.\n - patch — str_replace inside existing content (legacy).\n - create / update — fail if path exists / doesn't exist.\n - tree-merge — JSON-aware: merge `content` (a JSON-stringified object) into existing JSON, preserving sibling fields. Use this to flip a single field without re-writing the rest of the object.\n - tree-toggle — flip a boolean field named by `field`. No `content` needed.\n - tree-increment — add `by` (default 1) to a number field named by `field`. No `content` needed.\nProvide `path` for a single write. For multiple files, use batch mode: pass `entries` and OMIT the top-level `path` (it is ignored when `entries` is present) — never loop single writes. Batch is NOT a transaction: results are per-entry, check each results[i], partial success is normal.",
|
|
696
|
+
extraProps: {
|
|
697
|
+
content: {
|
|
698
|
+
type: "string",
|
|
699
|
+
description: "Content to write. For JSON files use a JSON-stringified object string. Ignored when `entries` is provided, and unused by tree-toggle / tree-increment."
|
|
700
|
+
},
|
|
701
|
+
mode: {
|
|
702
|
+
type: "string",
|
|
703
|
+
description: "Write mode. One of: replace (default), append, prepend, patch, create, update, tree-merge, tree-toggle, tree-increment."
|
|
704
|
+
},
|
|
705
|
+
field: {
|
|
706
|
+
type: "string",
|
|
707
|
+
description: "Field name on the JSON object — required for tree-toggle and tree-increment."
|
|
708
|
+
},
|
|
709
|
+
by: {
|
|
710
|
+
type: "number",
|
|
711
|
+
description: "Increment delta for tree-increment (default 1). Negative values decrement."
|
|
712
|
+
},
|
|
713
|
+
ifMatch: {
|
|
714
|
+
type: "string",
|
|
715
|
+
description: "Optimistic concurrency. Pass the opaque `version` returned by an earlier stat/read to fail the write (AFS_CONFLICT) if another writer changed the file in between."
|
|
716
|
+
},
|
|
717
|
+
entries: {
|
|
718
|
+
type: "array",
|
|
719
|
+
description: "Batch mode: array of {path, content, mode?} entries. When provided, top-level path/content/mode are ignored.",
|
|
720
|
+
items: {
|
|
721
|
+
type: "object",
|
|
722
|
+
properties: {
|
|
723
|
+
path: {
|
|
724
|
+
type: "string",
|
|
725
|
+
description: "AFS path to write to"
|
|
726
|
+
},
|
|
727
|
+
content: {
|
|
728
|
+
type: "string",
|
|
729
|
+
description: "Content to write"
|
|
730
|
+
},
|
|
731
|
+
mode: {
|
|
732
|
+
type: "string",
|
|
733
|
+
description: "Write mode (default: replace)"
|
|
734
|
+
}
|
|
735
|
+
},
|
|
736
|
+
required: ["path"]
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
},
|
|
740
|
+
extraRequired: [],
|
|
741
|
+
pathOptional: true
|
|
742
|
+
},
|
|
743
|
+
delete: {
|
|
744
|
+
name: "afs_delete",
|
|
745
|
+
description: "Delete an AFS path. Two modes: (1) Single-file delete: just `path`. (2) Batch delete on a directory: `path` is the directory + `where` is a predicate (a JSON-stringified object) matched against each child's content. Only matching children are deleted; the directory itself is kept. Use this for 'sweep' operations like clearing completed items in one call instead of list-then-loop.",
|
|
746
|
+
extraProps: {
|
|
747
|
+
where: {
|
|
748
|
+
type: "string",
|
|
749
|
+
description: "Optional. JSON-stringified predicate object. Each top-level key/value pair must match an entry's content for that entry to be deleted. Example: `\"{\\\"completed\\\":true}\"` deletes only entries whose JSON content has completed:true. Omit for single-file delete."
|
|
750
|
+
},
|
|
751
|
+
maxItems: {
|
|
752
|
+
type: "number",
|
|
753
|
+
description: "Optional cap on batch delete size (server enforces an upper bound). Use to limit blast radius when unsure."
|
|
754
|
+
}
|
|
755
|
+
},
|
|
756
|
+
extraRequired: []
|
|
757
|
+
},
|
|
758
|
+
stat: {
|
|
759
|
+
name: "afs_stat",
|
|
760
|
+
description: "Get metadata for an AFS path",
|
|
761
|
+
extraProps: {},
|
|
762
|
+
extraRequired: []
|
|
763
|
+
},
|
|
764
|
+
explain: {
|
|
765
|
+
name: "afs_explain",
|
|
766
|
+
description: "Get the curated guide for an AFS path. This is the DEFAULT first tool for discovery — always try this before `afs_read` or `afs_list` when investigating anything you don't already know how to use. Returns markdown: what the node is, how to invoke it, required fields, warnings, next steps. For providers in /registry/providers/*, the explain IS the mount guide — includes required post-mount setup (e.g. running a skill) that you MUST follow in full.",
|
|
767
|
+
extraProps: {},
|
|
768
|
+
extraRequired: []
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
/**
|
|
772
|
+
* Build OpenAI function-calling tools from ToolEntry declarations.
|
|
773
|
+
*
|
|
774
|
+
* Deduplicates operations across entries — one tool per unique op.
|
|
775
|
+
* Each tool's path description lists all allowed patterns for that op.
|
|
776
|
+
*
|
|
777
|
+
* When actionSchemas are provided (from startup discovery), generates
|
|
778
|
+
* per-action tools (afs_action_{name}) with explicit typed parameters
|
|
779
|
+
* so LLMs know exactly what fields each action requires.
|
|
780
|
+
*/
|
|
781
|
+
function buildToolSchema(tools, actionSchemas) {
|
|
782
|
+
const pathsByOp = /* @__PURE__ */ new Map();
|
|
783
|
+
const excludedActionsByOp = /* @__PURE__ */ new Map();
|
|
784
|
+
const maxDepthByOp = /* @__PURE__ */ new Map();
|
|
785
|
+
for (const entry of tools) for (const op of entry.ops) {
|
|
786
|
+
if (!pathsByOp.has(op)) pathsByOp.set(op, []);
|
|
787
|
+
const paths = pathsByOp.get(op);
|
|
788
|
+
if (!paths.includes(entry.path)) paths.push(entry.path);
|
|
789
|
+
if (entry.path.includes("**") && entry.maxDepth != null) {
|
|
790
|
+
if (!maxDepthByOp.has(op)) maxDepthByOp.set(op, /* @__PURE__ */ new Map());
|
|
791
|
+
maxDepthByOp.get(op).set(entry.path, entry.maxDepth);
|
|
792
|
+
}
|
|
793
|
+
if (op === "exec" && entry.exclude_actions?.length) {
|
|
794
|
+
if (!excludedActionsByOp.has(op)) excludedActionsByOp.set(op, []);
|
|
795
|
+
const excluded = excludedActionsByOp.get(op);
|
|
796
|
+
for (const a of entry.exclude_actions) if (!excluded.includes(a)) excluded.push(a);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
const result = [];
|
|
800
|
+
for (const [op, paths] of pathsByOp) {
|
|
801
|
+
const def = OP_DEFINITIONS[op];
|
|
802
|
+
if (!def) continue;
|
|
803
|
+
const depthMap = maxDepthByOp.get(op);
|
|
804
|
+
const hasDstar = paths.some((p) => p.includes("**"));
|
|
805
|
+
let pathDesc = `AFS path. Allowed: ${paths.join(", ")}.`;
|
|
806
|
+
if (hasDstar && depthMap) {
|
|
807
|
+
const depthNotes = [...depthMap.entries()].map(([p, d]) => `${p} (maxDepth ${d})`).join(", ");
|
|
808
|
+
pathDesc += ` ** matches multiple levels: ${depthNotes}.`;
|
|
809
|
+
}
|
|
810
|
+
pathDesc += " Note: * matches one level only (e.g. /a/* matches /a/b but NOT /a/b/c).";
|
|
811
|
+
const excluded = excludedActionsByOp.get(op);
|
|
812
|
+
if (excluded?.length) pathDesc += ` Excluded actions: ${excluded.join(", ")}.`;
|
|
813
|
+
let description = def.description;
|
|
814
|
+
let extraProps = def.extraProps;
|
|
815
|
+
let extraRequired = def.extraRequired;
|
|
816
|
+
if (op === "exec" && actionSchemas?.length) {
|
|
817
|
+
description = `Execute an action. Pass the action's parameters in args.
|
|
818
|
+
Available actions:\n${actionSchemas.map((a) => {
|
|
819
|
+
const fields = Object.entries(a.inputSchema?.properties ?? {}).map(([k, v]) => {
|
|
820
|
+
const req = a.inputSchema?.required?.includes(k) ? " (required)" : "";
|
|
821
|
+
let typeStr = v.type ?? "string";
|
|
822
|
+
if (typeStr === "object" && v.additionalProperties) typeStr = "object (map)";
|
|
823
|
+
if (typeStr === "array" && v.items) typeStr = "array";
|
|
824
|
+
return `${k}: ${typeStr}${req}`;
|
|
825
|
+
}).join(", ");
|
|
826
|
+
return `- ${a.pathPattern}: {${fields || "none"}} — ${a.description}`;
|
|
827
|
+
}).join("\n")}`;
|
|
828
|
+
extraRequired = ["args"];
|
|
829
|
+
const mergedProps = {};
|
|
830
|
+
for (const a of actionSchemas) if (a.inputSchema?.properties) for (const [k, v] of Object.entries(a.inputSchema.properties)) mergedProps[k] = {
|
|
831
|
+
...v,
|
|
832
|
+
type: v.type ?? "string"
|
|
833
|
+
};
|
|
834
|
+
extraProps = { args: {
|
|
835
|
+
type: ["object", "null"],
|
|
836
|
+
description: "Action arguments matching the action's inputSchema",
|
|
837
|
+
...Object.keys(mergedProps).length > 0 ? { properties: mergedProps } : {}
|
|
838
|
+
} };
|
|
839
|
+
}
|
|
840
|
+
result.push({
|
|
841
|
+
type: "function",
|
|
842
|
+
function: {
|
|
843
|
+
name: def.name,
|
|
844
|
+
description,
|
|
845
|
+
parameters: {
|
|
846
|
+
type: "object",
|
|
847
|
+
properties: {
|
|
848
|
+
path: {
|
|
849
|
+
type: "string",
|
|
850
|
+
description: pathDesc
|
|
851
|
+
},
|
|
852
|
+
...extraProps
|
|
853
|
+
},
|
|
854
|
+
required: def.pathOptional ? [...extraRequired] : ["path", ...extraRequired]
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
return result;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
//#endregion
|
|
863
|
+
//#region src/agent-run.ts
|
|
864
|
+
const log$1 = makeNsLog("provider:agent");
|
|
865
|
+
const DEFAULT_MAX_ROUNDS = 20;
|
|
866
|
+
const DEFAULT_ACTIONS_PER_ROUND = 10;
|
|
867
|
+
const RESULTS_PREFIX = "/.results/";
|
|
868
|
+
/** Circuit breaker: trip after N consecutive identical tool calls. */
|
|
869
|
+
const CIRCUIT_BREAKER_THRESHOLD = 3;
|
|
870
|
+
/**
|
|
871
|
+
* Default max output tokens per LLM call. Without this the gateway applies a
|
|
872
|
+
* conservative per-model default (~8k) — observed truncating a full article's
|
|
873
|
+
* `content` arg mid-sentence inside a cms-write tool call, after which the model
|
|
874
|
+
* re-emitted the same truncated call until the circuit breaker blocked recovery.
|
|
875
|
+
* We ask high so long content (article body + reasoning, or a whole doc page)
|
|
876
|
+
* has ample headroom; AI Device clamps this down to each routed model's real
|
|
877
|
+
* `maxOutputTokens` (see applyMaxOutputBudget in execution/forward.ts), so a
|
|
878
|
+
* model that supports less never receives an over-limit request. Only a default
|
|
879
|
+
* — a caller-supplied max_tokens always wins.
|
|
880
|
+
*/
|
|
881
|
+
const DEFAULT_MAX_OUTPUT_TOKENS = 32768;
|
|
882
|
+
/** LLM call retry defaults. Non-rate-limit retries keep the historical linear delay. */
|
|
883
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
884
|
+
const LLM_RETRY_DELAY_MS = 1e3;
|
|
885
|
+
const LLM_RETRY_MAX_MS = 5e3;
|
|
886
|
+
const LLM_RATE_LIMIT_RETRY_BASE_MS = 5e3;
|
|
887
|
+
const LLM_RATE_LIMIT_RETRY_MAX_MS = 3e4;
|
|
888
|
+
/** Timeout for a single LLM call (5 minutes). Prevents session lock from hanging forever. */
|
|
889
|
+
const LLM_CALL_TIMEOUT_MS = 300 * 1e3;
|
|
890
|
+
/** Default context window size when not provided by model metadata. */
|
|
891
|
+
const DEFAULT_CONTEXT_WINDOW = 8e4;
|
|
892
|
+
/** Maximum serialized size for chain array in progress events (bytes). */
|
|
893
|
+
const MAX_CHAIN_JSON_SIZE = 1024;
|
|
894
|
+
/**
|
|
895
|
+
* Compute sanitized depth and chain for progress event stamping.
|
|
896
|
+
*
|
|
897
|
+
* - Filters non-string entries from the chain
|
|
898
|
+
* - Strips path traversal characters
|
|
899
|
+
* - If no chain provided, builds [session] (or [] if no session)
|
|
900
|
+
* - Truncates chain if serialized JSON exceeds MAX_CHAIN_JSON_SIZE
|
|
901
|
+
* - depth = max(0, chain.length - 1)
|
|
902
|
+
*/
|
|
903
|
+
function computeProgressNesting(params) {
|
|
904
|
+
let chain;
|
|
905
|
+
if (params.chain && params.chain.length > 0) {
|
|
906
|
+
const filtered = params.chain.filter((s) => typeof s === "string");
|
|
907
|
+
if (filtered.length !== params.chain.length) log$1.warn("[agent-run] chain contains non-string elements, filtered out");
|
|
908
|
+
chain = filtered.map((s) => s.replace(/\.\.\//g, "").replace(/\.\.\\/g, ""));
|
|
909
|
+
} else if (params.session) {
|
|
910
|
+
if (params.parentSession && !params.chain) log$1.warn("[agent-run] parentSession set but chain missing, defaulting depth to 0");
|
|
911
|
+
chain = [params.session];
|
|
912
|
+
} else chain = [];
|
|
913
|
+
while (chain.length > 1 && JSON.stringify(chain).length > MAX_CHAIN_JSON_SIZE) chain = [chain[0], ...chain.slice(2)];
|
|
914
|
+
if (chain.length === 1 && JSON.stringify(chain).length > MAX_CHAIN_JSON_SIZE) chain = [chain[0].slice(0, MAX_CHAIN_JSON_SIZE - 10)];
|
|
915
|
+
return {
|
|
916
|
+
depth: Math.max(0, chain.length - 1),
|
|
917
|
+
chain
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
function isSubAgentRunPath(path) {
|
|
921
|
+
return path.endsWith("/.actions/run") || path.endsWith("/.actions/agent-run");
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Operations routed through ASH (deterministic sandbox).
|
|
925
|
+
*
|
|
926
|
+
* Currently empty: all ops go through direct AFS calls.
|
|
927
|
+
* agent-run's security comes from path-validator (tools declaration whitelist),
|
|
928
|
+
* not from ASH @caps. The ASH sandbox is valuable for user-written scripts
|
|
929
|
+
* (/.actions/run) but redundant for agent-run's auto-generated single-step execs.
|
|
930
|
+
*/
|
|
931
|
+
const ASH_OPS = /* @__PURE__ */ new Set([]);
|
|
932
|
+
const RATE_LIMIT_ERROR_RE = /\b429\b|RESOURCE_EXHAUSTED|Too Many Requests|rate[-\s]?limit|quota exceeded|quota has been exceeded/i;
|
|
933
|
+
function clampDelayMs(ms, maxMs) {
|
|
934
|
+
if (!Number.isFinite(ms) || ms <= 0) return 0;
|
|
935
|
+
return Math.min(Math.ceil(ms), maxMs);
|
|
936
|
+
}
|
|
937
|
+
function parseRetryAfterMs(error) {
|
|
938
|
+
const retryAfter = error.match(/retry[-_\s]?after["']?\s*[:=]?\s*["']?(\d+(?:\.\d+)?)\s*(ms|milliseconds?|s|sec|seconds?|m|minutes?)?/i);
|
|
939
|
+
const tryAgainIn = error.match(/try again (?:later,?\s*)?in\s+(\d+(?:\.\d+)?)\s*(ms|milliseconds?|s|sec|seconds?|m|minutes?)/i);
|
|
940
|
+
const match = retryAfter ?? tryAgainIn;
|
|
941
|
+
if (!match) return void 0;
|
|
942
|
+
const value = Number(match[1]);
|
|
943
|
+
if (!Number.isFinite(value) || value <= 0) return void 0;
|
|
944
|
+
const unit = match[2]?.toLowerCase();
|
|
945
|
+
if (unit?.startsWith("m") && unit !== "ms" && !unit.startsWith("millisecond")) return value * 6e4;
|
|
946
|
+
if (!unit || unit.startsWith("s")) return value * 1e3;
|
|
947
|
+
return value;
|
|
948
|
+
}
|
|
949
|
+
function computeLLMRetryDelayMs(error, failedAttempt) {
|
|
950
|
+
const attempt = Math.max(1, failedAttempt);
|
|
951
|
+
const retryAfterMs = parseRetryAfterMs(error);
|
|
952
|
+
if (retryAfterMs !== void 0) return clampDelayMs(retryAfterMs, LLM_RATE_LIMIT_RETRY_MAX_MS);
|
|
953
|
+
if (RATE_LIMIT_ERROR_RE.test(error)) return clampDelayMs(LLM_RATE_LIMIT_RETRY_BASE_MS * 2 ** (attempt - 1), LLM_RATE_LIMIT_RETRY_MAX_MS);
|
|
954
|
+
return clampDelayMs(LLM_RETRY_DELAY_MS * attempt, LLM_RETRY_MAX_MS);
|
|
955
|
+
}
|
|
956
|
+
function sleep(ms, signal) {
|
|
957
|
+
if (ms <= 0) return Promise.resolve();
|
|
958
|
+
if (signal?.aborted) return Promise.reject(/* @__PURE__ */ new Error("Cancelled"));
|
|
959
|
+
return new Promise((resolve, reject) => {
|
|
960
|
+
let timer;
|
|
961
|
+
const cleanup = () => {
|
|
962
|
+
clearTimeout(timer);
|
|
963
|
+
signal?.removeEventListener("abort", onAbort);
|
|
964
|
+
};
|
|
965
|
+
const onResolve = () => {
|
|
966
|
+
cleanup();
|
|
967
|
+
resolve();
|
|
968
|
+
};
|
|
969
|
+
const onAbort = () => {
|
|
970
|
+
cleanup();
|
|
971
|
+
reject(/* @__PURE__ */ new Error("Cancelled"));
|
|
972
|
+
};
|
|
973
|
+
timer = setTimeout(onResolve, ms);
|
|
974
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
/** Required arguments per operation (beyond path) */
|
|
978
|
+
const REQUIRED_ARGS = { search: ["query"] };
|
|
979
|
+
/** Map tool function name → AFS operation name */
|
|
980
|
+
function toolNameToOp(name) {
|
|
981
|
+
if (name.startsWith("afs_action_")) return "exec";
|
|
982
|
+
return {
|
|
983
|
+
afs_read: "read",
|
|
984
|
+
afs_list: "list",
|
|
985
|
+
afs_exec: "exec",
|
|
986
|
+
afs_search: "search",
|
|
987
|
+
afs_write: "write",
|
|
988
|
+
afs_delete: "delete",
|
|
989
|
+
afs_stat: "stat",
|
|
990
|
+
afs_explain: "explain"
|
|
991
|
+
}[name];
|
|
992
|
+
}
|
|
993
|
+
/** JSON.stringify with sorted keys for deterministic comparison of tool call args. */
|
|
994
|
+
function stableStringify(obj) {
|
|
995
|
+
return JSON.stringify(obj, Object.keys(obj).sort());
|
|
996
|
+
}
|
|
997
|
+
/** Truncate a result string for progress events. */
|
|
998
|
+
function truncateResult(s, max) {
|
|
999
|
+
if (!s) return void 0;
|
|
1000
|
+
return s.length > max ? `${s.slice(0, max)}...` : s;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Normalize a tool call from any format to our internal ToolCallInput.
|
|
1004
|
+
*
|
|
1005
|
+
* AigneHub returns OpenAI format:
|
|
1006
|
+
* { id, type: "function", function: { name, arguments } }
|
|
1007
|
+
* Some SDKs return Vercel AI SDK format:
|
|
1008
|
+
* { toolCallId, toolName, args }
|
|
1009
|
+
*/
|
|
1010
|
+
function normalizeToolCall(tc) {
|
|
1011
|
+
if (tc.toolCallId && tc.toolName) return {
|
|
1012
|
+
toolCallId: tc.toolCallId,
|
|
1013
|
+
toolName: tc.toolName,
|
|
1014
|
+
args: tc.args ?? {}
|
|
1015
|
+
};
|
|
1016
|
+
const fn = tc.function;
|
|
1017
|
+
const rawArgs = fn?.arguments;
|
|
1018
|
+
let args = {};
|
|
1019
|
+
if (typeof rawArgs === "string") try {
|
|
1020
|
+
args = JSON.parse(rawArgs);
|
|
1021
|
+
} catch {
|
|
1022
|
+
args = { _parseError: `Invalid JSON in function arguments: ${rawArgs.slice(0, 200)}` };
|
|
1023
|
+
}
|
|
1024
|
+
else if (rawArgs && typeof rawArgs === "object") args = rawArgs;
|
|
1025
|
+
return {
|
|
1026
|
+
toolCallId: tc.toolCallId ?? tc.id ?? "",
|
|
1027
|
+
toolName: tc.toolName ?? fn?.name ?? "",
|
|
1028
|
+
args
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Map AFS operation to a glyph a human scans faster than a word.
|
|
1033
|
+
* `afs_action_<name>` (per-action sugar) and any other non-standard tool
|
|
1034
|
+
* name collapse to the generic exec ⚡.
|
|
1035
|
+
*/
|
|
1036
|
+
const OP_EMOJI = {
|
|
1037
|
+
read: "📖",
|
|
1038
|
+
write: "✍️",
|
|
1039
|
+
edit: "✏️",
|
|
1040
|
+
list: "📋",
|
|
1041
|
+
stat: "ℹ️",
|
|
1042
|
+
explain: "💡",
|
|
1043
|
+
search: "🔎",
|
|
1044
|
+
exec: "⚡",
|
|
1045
|
+
delete: "🗑"
|
|
1046
|
+
};
|
|
1047
|
+
function toolNameToEmoji(toolName) {
|
|
1048
|
+
if (toolName.startsWith("afs_action_")) return OP_EMOJI.exec;
|
|
1049
|
+
return OP_EMOJI[toolName.replace(/^afs_/, "")] ?? "🔧";
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Render a round's tool calls + outcomes as a user-facing one-line-per-call
|
|
1053
|
+
* summary. Used by the onProgress payload so the progress surface (Telegram
|
|
1054
|
+
* live-progress, web UI tail, etc.) shows each AFS operation the agent ran
|
|
1055
|
+
* and whether it succeeded.
|
|
1056
|
+
*
|
|
1057
|
+
* Output per call: `<emoji> <path> <status>`, e.g.
|
|
1058
|
+
* 📖 /x/me ✓
|
|
1059
|
+
* 📋 /x/me/tweets ✓
|
|
1060
|
+
* ⚡ /x/.actions/search ✗
|
|
1061
|
+
*
|
|
1062
|
+
* Status glyphs:
|
|
1063
|
+
* ✓ — tool executed successfully
|
|
1064
|
+
* ✗ — tool errored (validation, runtime, permission)
|
|
1065
|
+
* ⊘ — circuit breaker tripped (same call repeated too many times)
|
|
1066
|
+
*
|
|
1067
|
+
* Falls back to `🔧` for tools outside the standard AFS op set.
|
|
1068
|
+
*/
|
|
1069
|
+
function summarizeToolCalls(roundCalls, traceEntries, errorResults) {
|
|
1070
|
+
const traceById = new Map(traceEntries.map((t) => [t.id, t]));
|
|
1071
|
+
const errorIds = new Set(errorResults.map((e) => e.id));
|
|
1072
|
+
return roundCalls.map((call) => {
|
|
1073
|
+
const emoji = toolNameToEmoji(call.toolName);
|
|
1074
|
+
const path = String(call.args.path ?? "");
|
|
1075
|
+
const trace = traceById.get(call.toolCallId);
|
|
1076
|
+
let status;
|
|
1077
|
+
if (errorIds.has(call.toolCallId)) status = "✗";
|
|
1078
|
+
else if (trace?.status === "circuit_breaker") status = "⊘";
|
|
1079
|
+
else if (trace?.status === "error") status = "✗";
|
|
1080
|
+
else status = "✓";
|
|
1081
|
+
return path ? `${emoji} ${path} ${status}` : `${emoji} ${status}`;
|
|
1082
|
+
}).join("\n");
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Fix path for per-action tool calls.
|
|
1086
|
+
*
|
|
1087
|
+
* LLMs often make mistakes with per-action tools:
|
|
1088
|
+
* 1. Put tool name in path: /.actions/afs_action_send → /.actions/send
|
|
1089
|
+
* 2. Drop mount prefix: /default/conversations/123 → /telegram/default/conversations/123
|
|
1090
|
+
*
|
|
1091
|
+
* Uses discovered pathPattern to auto-correct missing mount prefixes.
|
|
1092
|
+
* Mutates call.args.path in place.
|
|
1093
|
+
*/
|
|
1094
|
+
function fixPerActionPath(call, schemasByAction) {
|
|
1095
|
+
const path = call.args.path;
|
|
1096
|
+
if (!path) return;
|
|
1097
|
+
const realName = call.toolName.slice(11).replace(/_/g, "-");
|
|
1098
|
+
const marker = "/.actions/";
|
|
1099
|
+
const idx = path.lastIndexOf(marker);
|
|
1100
|
+
if (idx !== -1) {
|
|
1101
|
+
const currentAction = path.slice(idx + 10).split("/")[0];
|
|
1102
|
+
if (currentAction && currentAction !== realName) call.args.path = path.slice(0, idx) + marker + realName;
|
|
1103
|
+
}
|
|
1104
|
+
const schema = schemasByAction.get(realName);
|
|
1105
|
+
if (!schema?.pathPattern) return;
|
|
1106
|
+
const fixedPath = call.args.path;
|
|
1107
|
+
const starIdx = schema.pathPattern.indexOf("*");
|
|
1108
|
+
if (starIdx === -1) return;
|
|
1109
|
+
const expectedPrefix = schema.pathPattern.slice(0, starIdx);
|
|
1110
|
+
if (fixedPath.startsWith(expectedPrefix)) return;
|
|
1111
|
+
for (let i = 1; i < expectedPrefix.length; i++) if (expectedPrefix[i] === "/" && fixedPath.startsWith(expectedPrefix.slice(i))) {
|
|
1112
|
+
call.args.path = expectedPrefix.slice(0, i) + fixedPath;
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
/** Extract exec args from tool call: try nested `args` field first, fall back to all fields except `path`. */
|
|
1117
|
+
function extractExecArgs(callArgs) {
|
|
1118
|
+
if (callArgs.args && typeof callArgs.args === "object" && !Array.isArray(callArgs.args)) return callArgs.args;
|
|
1119
|
+
const { path: _, ...rest } = callArgs;
|
|
1120
|
+
return rest;
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Discover available actions and their inputSchemas at startup.
|
|
1124
|
+
*
|
|
1125
|
+
* Strategy (per exec-capable tool entry):
|
|
1126
|
+
* 1. Instance-probe: list basePath to find a child, then list its .actions/
|
|
1127
|
+
* 2. Capabilities fallback: walk up path hierarchy to find /.meta/.capabilities,
|
|
1128
|
+
* extract action schemas from the manifest's catalog entries
|
|
1129
|
+
*
|
|
1130
|
+
* Results are used to generate per-action tools with explicit typed parameters.
|
|
1131
|
+
* Best-effort: failures are silently ignored and the generic afs_exec fallback remains.
|
|
1132
|
+
*/
|
|
1133
|
+
async function discoverActionSchemas(tools, deps) {
|
|
1134
|
+
const schemas = [];
|
|
1135
|
+
const discoveredPaths = /* @__PURE__ */ new Set();
|
|
1136
|
+
const candidates = [];
|
|
1137
|
+
for (const tool of tools) {
|
|
1138
|
+
if (!tool.ops.includes("exec")) continue;
|
|
1139
|
+
const basePath = tool.path.replace(/\/?\*+$/, "");
|
|
1140
|
+
if (!basePath || discoveredPaths.has(basePath)) continue;
|
|
1141
|
+
discoveredPaths.add(basePath);
|
|
1142
|
+
candidates.push({
|
|
1143
|
+
tool,
|
|
1144
|
+
basePath
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
const discovered = await Promise.all(candidates.map(async ({ tool, basePath }) => {
|
|
1148
|
+
const toolSchemas = [];
|
|
1149
|
+
const toolActions = /* @__PURE__ */ new Set();
|
|
1150
|
+
try {
|
|
1151
|
+
if (basePath.endsWith("/.actions")) {
|
|
1152
|
+
await discoverFromActionsDir(tool, basePath, toolActions, toolSchemas, deps);
|
|
1153
|
+
return toolSchemas;
|
|
1154
|
+
}
|
|
1155
|
+
if (!await discoverViaInstanceProbe(tool, basePath, toolActions, toolSchemas, deps)) await discoverViaCapabilities(tool, basePath, toolActions, toolSchemas, deps);
|
|
1156
|
+
} catch {}
|
|
1157
|
+
return toolSchemas;
|
|
1158
|
+
}));
|
|
1159
|
+
const discoveredActions = /* @__PURE__ */ new Set();
|
|
1160
|
+
for (const toolSchemas of discovered) for (const schema of toolSchemas) {
|
|
1161
|
+
if (discoveredActions.has(schema.actionName)) continue;
|
|
1162
|
+
discoveredActions.add(schema.actionName);
|
|
1163
|
+
schemas.push(schema);
|
|
1164
|
+
}
|
|
1165
|
+
return schemas;
|
|
1166
|
+
}
|
|
1167
|
+
/** Fast path: tool path already ends with /.actions/* — list the directory directly. */
|
|
1168
|
+
async function discoverFromActionsDir(tool, actionsPath, discoveredActions, schemas, deps) {
|
|
1169
|
+
const result = await deps.callAFS("list", actionsPath);
|
|
1170
|
+
if (!result.success) return;
|
|
1171
|
+
const actions = extractEntries(result.data);
|
|
1172
|
+
for (const action of actions) {
|
|
1173
|
+
const name = extractActionNameFromEntry(action);
|
|
1174
|
+
if (!name || discoveredActions.has(name)) continue;
|
|
1175
|
+
discoveredActions.add(name);
|
|
1176
|
+
schemas.push({
|
|
1177
|
+
actionName: name,
|
|
1178
|
+
description: action.meta?.description || `Execute ${name}`,
|
|
1179
|
+
pathPattern: `${tool.path.replace(/\*+$/, name)}`,
|
|
1180
|
+
inputSchema: action.meta?.inputSchema
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
/** Strategy 1: list basePath, pick first child, list its .actions/ */
|
|
1185
|
+
async function discoverViaInstanceProbe(tool, basePath, discoveredActions, schemas, deps) {
|
|
1186
|
+
const listResult = await deps.callAFS("list", basePath);
|
|
1187
|
+
if (!listResult.success) return false;
|
|
1188
|
+
const entries = extractEntries(listResult.data);
|
|
1189
|
+
if (entries.length === 0) return false;
|
|
1190
|
+
const mcpTools = entries.filter((e) => e.meta?.kind === "mcp:tool" || e.meta?.kinds?.includes?.("mcp:tool"));
|
|
1191
|
+
if (mcpTools.length > 0) {
|
|
1192
|
+
let found$1 = false;
|
|
1193
|
+
for (const mcpTool of mcpTools) {
|
|
1194
|
+
const name = mcpTool.meta?.mcp?.name || extractActionNameFromEntry(mcpTool);
|
|
1195
|
+
if (!name || discoveredActions.has(name)) continue;
|
|
1196
|
+
discoveredActions.add(name);
|
|
1197
|
+
found$1 = true;
|
|
1198
|
+
const toolPath = mcpTool.path || `${basePath}/${name}`;
|
|
1199
|
+
schemas.push({
|
|
1200
|
+
actionName: name,
|
|
1201
|
+
description: mcpTool.meta?.description || `Execute ${name}`,
|
|
1202
|
+
pathPattern: toolPath,
|
|
1203
|
+
inputSchema: mcpTool.meta?.inputSchema
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
return found$1;
|
|
1207
|
+
}
|
|
1208
|
+
const firstChild = entries[0];
|
|
1209
|
+
const childPath = firstChild.path || firstChild.id;
|
|
1210
|
+
if (!childPath) return false;
|
|
1211
|
+
const actionsPath = `${childPath}/.actions`;
|
|
1212
|
+
const actionsResult = await deps.callAFS("list", actionsPath);
|
|
1213
|
+
if (!actionsResult.success) return false;
|
|
1214
|
+
const actions = extractEntries(actionsResult.data);
|
|
1215
|
+
let found = false;
|
|
1216
|
+
for (const action of actions) {
|
|
1217
|
+
const name = extractActionNameFromEntry(action);
|
|
1218
|
+
if (!name || discoveredActions.has(name)) continue;
|
|
1219
|
+
discoveredActions.add(name);
|
|
1220
|
+
found = true;
|
|
1221
|
+
schemas.push({
|
|
1222
|
+
actionName: name,
|
|
1223
|
+
description: action.meta?.description || `Execute ${name}`,
|
|
1224
|
+
pathPattern: `${tool.path}/.actions/${name}`,
|
|
1225
|
+
inputSchema: action.meta?.inputSchema
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
return found;
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Strategy 2: walk up path hierarchy to find a provider's /.meta/.capabilities,
|
|
1232
|
+
* then extract action schemas from the manifest's catalog + inputSchema.
|
|
1233
|
+
*
|
|
1234
|
+
* For a tool path like /telegram/default/conversations/*, tries:
|
|
1235
|
+
* /telegram/default/conversations/.meta/.capabilities
|
|
1236
|
+
* /telegram/default/.meta/.capabilities
|
|
1237
|
+
* /telegram/.meta/.capabilities
|
|
1238
|
+
*/
|
|
1239
|
+
async function discoverViaCapabilities(tool, basePath, discoveredActions, schemas, deps) {
|
|
1240
|
+
const segments = basePath.split("/").filter(Boolean);
|
|
1241
|
+
for (let depth = segments.length; depth >= 1; depth--) {
|
|
1242
|
+
const capsPath = `${`/${segments.slice(0, depth).join("/")}`}/.meta/.capabilities`;
|
|
1243
|
+
try {
|
|
1244
|
+
const result = await deps.callAFS("read", capsPath);
|
|
1245
|
+
if (!result.success) continue;
|
|
1246
|
+
const content = extractCapabilitiesContent(result.data);
|
|
1247
|
+
if (!content?.actions) continue;
|
|
1248
|
+
let found = false;
|
|
1249
|
+
for (const actionGroup of content.actions) {
|
|
1250
|
+
const catalog = actionGroup.catalog;
|
|
1251
|
+
if (!Array.isArray(catalog)) continue;
|
|
1252
|
+
for (const entry of catalog) {
|
|
1253
|
+
const name = entry.name;
|
|
1254
|
+
if (!name || discoveredActions.has(name)) continue;
|
|
1255
|
+
discoveredActions.add(name);
|
|
1256
|
+
found = true;
|
|
1257
|
+
schemas.push({
|
|
1258
|
+
actionName: name,
|
|
1259
|
+
description: entry.description || `Execute ${name}`,
|
|
1260
|
+
pathPattern: `${tool.path}/.actions/${name}`,
|
|
1261
|
+
inputSchema: entry.inputSchema
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
if (found) return true;
|
|
1266
|
+
} catch {}
|
|
1267
|
+
}
|
|
1268
|
+
return false;
|
|
1269
|
+
}
|
|
1270
|
+
/** Extract capabilities content from a read result (may be nested in data.content or data directly). */
|
|
1271
|
+
function extractCapabilitiesContent(data) {
|
|
1272
|
+
if (!data || typeof data !== "object") return null;
|
|
1273
|
+
const d = data;
|
|
1274
|
+
if (d.actions) return d;
|
|
1275
|
+
if (d.content && typeof d.content === "object" && d.content.actions) return d.content;
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
/** Extract entries array from callAFS response data (may be raw array or wrapped). */
|
|
1279
|
+
function extractEntries(data) {
|
|
1280
|
+
if (Array.isArray(data)) return data;
|
|
1281
|
+
if (data && typeof data === "object" && Array.isArray(data.data)) return data.data;
|
|
1282
|
+
return [];
|
|
1283
|
+
}
|
|
1284
|
+
/** Tolerant extraction of text content from a callAFS("read") result's data. */
|
|
1285
|
+
function extractReadContent(data) {
|
|
1286
|
+
if (typeof data === "string") return data;
|
|
1287
|
+
if (data && typeof data === "object") {
|
|
1288
|
+
const d = data;
|
|
1289
|
+
if (typeof d.content === "string") return d.content;
|
|
1290
|
+
if (typeof d.data === "string") return d.data;
|
|
1291
|
+
if (d.data && typeof d.data === "object" && typeof d.data.content === "string") return d.data.content;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
/** Parse `name` + `description` from a SKILL.md frontmatter block. Delegates to
|
|
1295
|
+
* the canonical parser in `@aigne/afs-skills/format` (single source of truth)
|
|
1296
|
+
* and preserves this module's `null`-when-empty contract for its callers. */
|
|
1297
|
+
function parseSkillFrontmatter$1(md) {
|
|
1298
|
+
const fm = parseSkillFrontmatter(md);
|
|
1299
|
+
if (Object.keys(fm).length === 0) return null;
|
|
1300
|
+
return {
|
|
1301
|
+
name: fm.name,
|
|
1302
|
+
description: fm.description
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
/**
|
|
1306
|
+
* Build a compact skill catalog from the agent's skill roots, read at startup
|
|
1307
|
+
* (server-side) so the LLM does NOT spend discovery rounds listing roots and
|
|
1308
|
+
* reading every SKILL.md just to learn what exists. Each skill's `name` +
|
|
1309
|
+
* `description` come from its SKILL.md frontmatter; the agent then reads the
|
|
1310
|
+
* full SKILL.md only for the one skill it decides to use.
|
|
1311
|
+
*
|
|
1312
|
+
* Bounded: scans at most 2 levels deep and caps the catalog size, so the
|
|
1313
|
+
* injected prompt stays small no matter how many skills are mounted. All AFS
|
|
1314
|
+
* access is best-effort — a failing root/skill is skipped, never fatal.
|
|
1315
|
+
*/
|
|
1316
|
+
async function buildSkillCatalog(skillPaths, deps) {
|
|
1317
|
+
if (!skillPaths || skillPaths.length === 0) return void 0;
|
|
1318
|
+
const MAX_SKILLS = 40;
|
|
1319
|
+
const DESC_MAX = 220;
|
|
1320
|
+
const entries = [];
|
|
1321
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1322
|
+
/** Try `<dir>/SKILL.md`; on success record the skill. Returns true if a
|
|
1323
|
+
* SKILL.md was found (so the caller does not descend into a leaf dir). */
|
|
1324
|
+
const tryLeaf = async (dirPath) => {
|
|
1325
|
+
const skillFile = `${dirPath}/SKILL.md`;
|
|
1326
|
+
let res;
|
|
1327
|
+
try {
|
|
1328
|
+
res = await deps.callAFS("read", skillFile);
|
|
1329
|
+
} catch {
|
|
1330
|
+
return false;
|
|
1331
|
+
}
|
|
1332
|
+
if (!res.success) return false;
|
|
1333
|
+
const md = extractReadContent(res.data);
|
|
1334
|
+
if (!md) return false;
|
|
1335
|
+
const fm = parseSkillFrontmatter$1(md);
|
|
1336
|
+
if (!fm?.name || seen.has(fm.name)) return !!fm?.name;
|
|
1337
|
+
seen.add(fm.name);
|
|
1338
|
+
let desc = (fm.description ?? "").replace(/\s+/g, " ").trim();
|
|
1339
|
+
if (desc.length > DESC_MAX) desc = `${desc.slice(0, DESC_MAX - 1)}…`;
|
|
1340
|
+
entries.push({
|
|
1341
|
+
name: fm.name,
|
|
1342
|
+
description: desc,
|
|
1343
|
+
path: skillFile
|
|
1344
|
+
});
|
|
1345
|
+
return true;
|
|
1346
|
+
};
|
|
1347
|
+
for (const root of skillPaths) {
|
|
1348
|
+
if (entries.length >= MAX_SKILLS) break;
|
|
1349
|
+
let listRes;
|
|
1350
|
+
try {
|
|
1351
|
+
listRes = await deps.callAFS("list", root);
|
|
1352
|
+
} catch {
|
|
1353
|
+
continue;
|
|
1354
|
+
}
|
|
1355
|
+
if (!listRes.success) continue;
|
|
1356
|
+
for (const child of extractEntries(listRes.data)) {
|
|
1357
|
+
if (entries.length >= MAX_SKILLS) break;
|
|
1358
|
+
const childPath = child.path || child.id;
|
|
1359
|
+
if (!childPath) continue;
|
|
1360
|
+
if (await tryLeaf(childPath)) continue;
|
|
1361
|
+
let groupRes;
|
|
1362
|
+
try {
|
|
1363
|
+
groupRes = await deps.callAFS("list", childPath);
|
|
1364
|
+
} catch {
|
|
1365
|
+
continue;
|
|
1366
|
+
}
|
|
1367
|
+
if (!groupRes.success) continue;
|
|
1368
|
+
for (const gc of extractEntries(groupRes.data)) {
|
|
1369
|
+
if (entries.length >= MAX_SKILLS) break;
|
|
1370
|
+
const gcPath = gc.path || gc.id;
|
|
1371
|
+
if (gcPath) await tryLeaf(gcPath);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
if (entries.length === 0) return void 0;
|
|
1376
|
+
return [
|
|
1377
|
+
"## Available Skills",
|
|
1378
|
+
"These skills are ready to use. When a user request matches a skill's purpose, `read` its SKILL.md for the full procedure and follow it exactly — do not improvise the workflow. If none fit, work directly with the AFS tools.",
|
|
1379
|
+
"",
|
|
1380
|
+
...entries.map((e) => `- **${e.name}** — ${e.description || "(no description)"} → read \`${e.path}\``)
|
|
1381
|
+
].join("\n");
|
|
1382
|
+
}
|
|
1383
|
+
/** Extract action name from an action entry (from path or id). */
|
|
1384
|
+
function extractActionNameFromEntry(entry) {
|
|
1385
|
+
if (entry.meta?.name) return entry.meta.name;
|
|
1386
|
+
const path = entry.path || entry.id || "";
|
|
1387
|
+
const idx = path.lastIndexOf("/.actions/");
|
|
1388
|
+
if (idx !== -1) return path.slice(idx + 10).split("/")[0];
|
|
1389
|
+
if (entry.id && !entry.id.includes("/")) return entry.id;
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Compute the per-tool-result character limit based on context window size.
|
|
1393
|
+
* Each tool result should occupy at most ~2% of the context window.
|
|
1394
|
+
*/
|
|
1395
|
+
function computeToolResultLimit(contextWindow) {
|
|
1396
|
+
const cw = contextWindow ?? DEFAULT_CONTEXT_WINDOW;
|
|
1397
|
+
return Math.max(16e3, Math.floor(cw * .05 * 3));
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Truncate a tool result string if it exceeds the given character limit.
|
|
1401
|
+
* Returns the original string unchanged if within limit.
|
|
1402
|
+
*/
|
|
1403
|
+
function truncateToolResult(content, limit) {
|
|
1404
|
+
if (content.length <= limit) return content;
|
|
1405
|
+
return `${content.slice(0, limit)}\n[... truncated ${content.length - limit} chars]`;
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Enrich tool result content with multimodal images when `_images` is present.
|
|
1409
|
+
* Returns the original string for normal results, or a multimodal content array
|
|
1410
|
+
* containing both text and image parts when images are detected.
|
|
1411
|
+
*/
|
|
1412
|
+
function enrichToolContent(resultStr) {
|
|
1413
|
+
try {
|
|
1414
|
+
const parsed = JSON.parse(resultStr);
|
|
1415
|
+
if (Array.isArray(parsed?._images) && parsed._images.length > 0) {
|
|
1416
|
+
const images = parsed._images.filter((img) => img && typeof img === "object" && img.type && (img.url || img.data || img.path));
|
|
1417
|
+
if (images.length > 0) return [{
|
|
1418
|
+
type: "text",
|
|
1419
|
+
text: resultStr
|
|
1420
|
+
}, ...images];
|
|
1421
|
+
}
|
|
1422
|
+
} catch {}
|
|
1423
|
+
return resultStr;
|
|
1424
|
+
}
|
|
1425
|
+
/** Normalize an in-memory message (AigneHub camelCase) to canonical JSONL format (OpenAI snake_case). */
|
|
1426
|
+
function normalizeForHistory(msg) {
|
|
1427
|
+
const role = msg.role;
|
|
1428
|
+
if (role === "system") return null;
|
|
1429
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1430
|
+
if (role === "user") return {
|
|
1431
|
+
role: "user",
|
|
1432
|
+
content: String(msg.content ?? ""),
|
|
1433
|
+
createdAt
|
|
1434
|
+
};
|
|
1435
|
+
if (role === "assistant") {
|
|
1436
|
+
const result = {
|
|
1437
|
+
role: "assistant",
|
|
1438
|
+
content: String(msg.content ?? ""),
|
|
1439
|
+
createdAt,
|
|
1440
|
+
...typeof msg.durationMs === "number" ? { durationMs: msg.durationMs } : {}
|
|
1441
|
+
};
|
|
1442
|
+
const rawToolCalls = msg.toolCalls ?? msg.tool_calls;
|
|
1443
|
+
if (rawToolCalls?.length) result.tool_calls = rawToolCalls.map((tc) => {
|
|
1444
|
+
const fn = tc.function;
|
|
1445
|
+
const rawArgs = fn?.arguments ?? tc.args;
|
|
1446
|
+
return {
|
|
1447
|
+
id: tc.id ?? tc.toolCallId ?? "",
|
|
1448
|
+
type: "function",
|
|
1449
|
+
function: {
|
|
1450
|
+
name: fn?.name ?? tc.toolName ?? "",
|
|
1451
|
+
arguments: typeof rawArgs === "string" ? rawArgs : JSON.stringify(rawArgs ?? {})
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
});
|
|
1455
|
+
return result;
|
|
1456
|
+
}
|
|
1457
|
+
if (role === "tool") {
|
|
1458
|
+
const content = Array.isArray(msg.content) ? msg.content : String(msg.content ?? "");
|
|
1459
|
+
return {
|
|
1460
|
+
role: "tool",
|
|
1461
|
+
tool_call_id: msg.tool_call_id ?? msg.toolCallId ?? "",
|
|
1462
|
+
content,
|
|
1463
|
+
createdAt,
|
|
1464
|
+
...typeof msg.durationMs === "number" ? { durationMs: msg.durationMs } : {}
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
return null;
|
|
1468
|
+
}
|
|
1469
|
+
async function runAgentLoop(params, deps) {
|
|
1470
|
+
const maxRounds = params.budget?.max_rounds ?? DEFAULT_MAX_ROUNDS;
|
|
1471
|
+
const actionsPerRound = params.budget?.actions_per_round ?? DEFAULT_ACTIONS_PER_ROUND;
|
|
1472
|
+
const tokenBudget = params.budget?.total_tokens ?? Number.POSITIVE_INFINITY;
|
|
1473
|
+
const nesting = computeProgressNesting(params);
|
|
1474
|
+
const emitProgress = (event) => {
|
|
1475
|
+
if (!params.onToolProgress) return;
|
|
1476
|
+
try {
|
|
1477
|
+
params.onToolProgress({
|
|
1478
|
+
...event,
|
|
1479
|
+
depth: nesting.depth,
|
|
1480
|
+
chain: [...nesting.chain]
|
|
1481
|
+
});
|
|
1482
|
+
} catch {}
|
|
1483
|
+
};
|
|
1484
|
+
/** Stamp seq + ts and forward to deps.emit. No-op when observability isn't
|
|
1485
|
+
* wired up (CLI direct call). Failures are swallowed — trace is best-effort.
|
|
1486
|
+
* `LogEventInput` (= LogEvent minus seq/ts, distributed) preserves each
|
|
1487
|
+
* variant's required keys at the call site. */
|
|
1488
|
+
const emitObs = async (event) => {
|
|
1489
|
+
if (!deps.emit || !deps.nextSeq) return;
|
|
1490
|
+
try {
|
|
1491
|
+
await deps.emit({
|
|
1492
|
+
...event,
|
|
1493
|
+
seq: deps.nextSeq(),
|
|
1494
|
+
ts: Date.now()
|
|
1495
|
+
});
|
|
1496
|
+
} catch {}
|
|
1497
|
+
};
|
|
1498
|
+
const afs = { exec: async (path, args, opts) => {
|
|
1499
|
+
return deps.callAFS("exec", path, {
|
|
1500
|
+
...args,
|
|
1501
|
+
...opts
|
|
1502
|
+
});
|
|
1503
|
+
} };
|
|
1504
|
+
const sessionTag = params.session ? params.session.split("/").filter(Boolean).pop() ?? params.session : Math.random().toString(16).slice(2, 10);
|
|
1505
|
+
const procId = await deps.callAFS("exec", "/proc/.actions/register", {
|
|
1506
|
+
type: "agent-run",
|
|
1507
|
+
did: `did:afs:ash-run:${sessionTag}`,
|
|
1508
|
+
parentId: params.execContext?.procId ?? params.parentProcId,
|
|
1509
|
+
budget: {
|
|
1510
|
+
tokens: params.budget?.total_tokens,
|
|
1511
|
+
calls: params.budget?.max_rounds
|
|
1512
|
+
}
|
|
1513
|
+
}).then((r) => (r?.data)?.procId).catch(() => void 0);
|
|
1514
|
+
const completeProc = (status) => {
|
|
1515
|
+
if (procId) deps.callAFS("exec", "/proc/.actions/complete", {
|
|
1516
|
+
id: procId,
|
|
1517
|
+
status
|
|
1518
|
+
}).catch(() => {});
|
|
1519
|
+
};
|
|
1520
|
+
if (params.memory?.toolsPath) params.tools.push({
|
|
1521
|
+
path: params.memory.toolsPath,
|
|
1522
|
+
ops: ["exec"]
|
|
1523
|
+
});
|
|
1524
|
+
if (params.memory?.domain && params.memory?.recallPath) {
|
|
1525
|
+
const mountRoot = params.memory.recallPath.replace(/\/\.actions\/recall\/?$/, "");
|
|
1526
|
+
if (mountRoot && mountRoot !== params.memory.recallPath) params.tools.push({
|
|
1527
|
+
path: joinURL(mountRoot, params.memory.domain),
|
|
1528
|
+
ops: ["list", "read"],
|
|
1529
|
+
maxDepth: 3
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
if (params.index?.scope && params.index?.queryPath) {
|
|
1533
|
+
const mountRoot = params.index.queryPath.replace(/\/\.actions\/query\/?$/, "");
|
|
1534
|
+
if (mountRoot && mountRoot !== params.index.queryPath) params.tools.push({
|
|
1535
|
+
path: joinURL(mountRoot, "domains", params.index.scope),
|
|
1536
|
+
ops: ["list", "read"],
|
|
1537
|
+
maxDepth: 2
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
const ASK_USER_PATH = "/dev/ui/.actions/ask";
|
|
1541
|
+
if (!params.tools.some((t) => t.path === ASK_USER_PATH)) params.tools.push({
|
|
1542
|
+
path: ASK_USER_PATH,
|
|
1543
|
+
ops: ["exec"]
|
|
1544
|
+
});
|
|
1545
|
+
const actionSchemas = await discoverActionSchemas(params.tools, deps);
|
|
1546
|
+
const schemasByAction = /* @__PURE__ */ new Map();
|
|
1547
|
+
for (const s of actionSchemas) schemasByAction.set(s.actionName, s);
|
|
1548
|
+
const toolSchema = buildToolSchema(params.tools, actionSchemas);
|
|
1549
|
+
let responseSchema;
|
|
1550
|
+
if (params.responseSchema) {
|
|
1551
|
+
if (params.responseSchema.startsWith("/")) try {
|
|
1552
|
+
const content = (await deps.callAFS("read", params.responseSchema))?.data?.content;
|
|
1553
|
+
if (typeof content === "string") responseSchema = JSON.parse(content);
|
|
1554
|
+
else if (content && typeof content === "object") responseSchema = content;
|
|
1555
|
+
} catch {}
|
|
1556
|
+
if (!responseSchema) try {
|
|
1557
|
+
const parsed = JSON.parse(params.responseSchema);
|
|
1558
|
+
if (parsed && typeof parsed === "object") responseSchema = parsed;
|
|
1559
|
+
} catch {}
|
|
1560
|
+
}
|
|
1561
|
+
let history = [];
|
|
1562
|
+
if (params.session && deps.loadHistory) try {
|
|
1563
|
+
history = await deps.loadHistory(params.session);
|
|
1564
|
+
} catch {}
|
|
1565
|
+
const AFS_HINT = [
|
|
1566
|
+
"## AFS Discovery Protocol",
|
|
1567
|
+
"Start with `read /.knowledge` — it returns a complete capability index of all providers, actions, and paths in ONE call.",
|
|
1568
|
+
"For deeper detail on a specific provider: `read /.knowledge/{provider-name}`.",
|
|
1569
|
+
"Only use `explain {path}` when you need the deepest detail about a specific path or action.",
|
|
1570
|
+
"Do NOT read source code from /modules/project/ to learn about providers — use `/.knowledge` or `explain` instead.",
|
|
1571
|
+
"",
|
|
1572
|
+
"## Skills",
|
|
1573
|
+
"Skills are regular AFS files, not a special tool. Two roots:",
|
|
1574
|
+
" - /blocklet/skills — this app's own skills (from the blocklet)",
|
|
1575
|
+
" - /.mounted-skills — each mounted provider's bundled skills",
|
|
1576
|
+
"",
|
|
1577
|
+
"When you need a capability:",
|
|
1578
|
+
" 1. `list` one of the skill roots to see what's available",
|
|
1579
|
+
" 2. `read <dir>/SKILL.md` (or `README.md`) for usage + params",
|
|
1580
|
+
" 3. `read <path>.ash` if you need exact `param` declarations",
|
|
1581
|
+
" 4. `exec <path>.ash { ...params, caps: [\"*\"] }` to run the skill",
|
|
1582
|
+
"",
|
|
1583
|
+
"A SKILL.md-bearing directory = one leaf skill. A DESCRIPTION.md-bearing directory = a group; descend into its children to find leaves."
|
|
1584
|
+
].join("\n");
|
|
1585
|
+
const STOP_DIRECTIVE = responseSchema ? `When you have completed the user's task, respond with your final answer as a JSON object. Do not make additional tool calls after you have gathered all needed information.\n\nResponse JSON Schema:\n${JSON.stringify(responseSchema, null, 2)}` : params.skipAfsHint ? "When you have completed the user's task, respond with a concise text summary. Do not make additional tool calls after the task is done." : `When you have completed the user's task, respond with a concise text summary. Do not make additional tool calls after the task is done.\n\n${AFS_HINT}`;
|
|
1586
|
+
const TIME_DIRECTIVE = `Current time: ${(/* @__PURE__ */ new Date()).toISOString()} (UTC). When writing timestamps in tool args (createdAt, updatedAt, etc.) use this — do not invent dates.`;
|
|
1587
|
+
const memoryConfig = params.memory ? {
|
|
1588
|
+
recallPath: params.memory.recallPath,
|
|
1589
|
+
maxTokens: params.memory.maxTokens,
|
|
1590
|
+
domain: params.memory.domain
|
|
1591
|
+
} : void 0;
|
|
1592
|
+
const skillCatalog = await buildSkillCatalog(params.skillPaths, deps);
|
|
1593
|
+
const messages = (await buildContext(afs, {
|
|
1594
|
+
task: params.task,
|
|
1595
|
+
system: params.system,
|
|
1596
|
+
history,
|
|
1597
|
+
memory: memoryConfig,
|
|
1598
|
+
directives: skillCatalog ? [
|
|
1599
|
+
TIME_DIRECTIVE,
|
|
1600
|
+
STOP_DIRECTIVE,
|
|
1601
|
+
skillCatalog
|
|
1602
|
+
] : [TIME_DIRECTIVE, STOP_DIRECTIVE]
|
|
1603
|
+
})).messages;
|
|
1604
|
+
const currentTaskMessage = messages[messages.length - 1];
|
|
1605
|
+
const contextWindow = params.budget?.context_window;
|
|
1606
|
+
const toolResultLimit = computeToolResultLimit(contextWindow);
|
|
1607
|
+
/** Normalize new messages, persist to working history (if session), and emit
|
|
1608
|
+
* one observability `message` event per normalized entry. emit fires
|
|
1609
|
+
* regardless of session — observability is independent from history.jsonl. */
|
|
1610
|
+
const appendNewMessages = async (newMsgs) => {
|
|
1611
|
+
const normalized = [];
|
|
1612
|
+
for (const msg of newMsgs) {
|
|
1613
|
+
const n = normalizeForHistory(msg);
|
|
1614
|
+
if (n) normalized.push(n);
|
|
1615
|
+
}
|
|
1616
|
+
if (normalized.length === 0) return;
|
|
1617
|
+
if (params.session) try {
|
|
1618
|
+
await deps.appendHistory?.(params.session, normalized);
|
|
1619
|
+
} catch {}
|
|
1620
|
+
for (const m of normalized) {
|
|
1621
|
+
const tc = m.tool_calls;
|
|
1622
|
+
const cid = m.tool_call_id;
|
|
1623
|
+
await emitObs({
|
|
1624
|
+
type: "message",
|
|
1625
|
+
role: m.role,
|
|
1626
|
+
content: m.content,
|
|
1627
|
+
...tc ? { toolCalls: tc } : {},
|
|
1628
|
+
...cid ? { callId: cid } : {}
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
};
|
|
1632
|
+
/** Latest compress progress, captured by `onProgress` and read by
|
|
1633
|
+
* `rewriteCompressed` (whose signature is fixed by maybeCompress's
|
|
1634
|
+
* `onCompressed` contract — only `compressed` + `archived` are passed). */
|
|
1635
|
+
let latestCompressInfo = {
|
|
1636
|
+
beforeTokens: 0,
|
|
1637
|
+
afterTokens: 0,
|
|
1638
|
+
round: 0
|
|
1639
|
+
};
|
|
1640
|
+
/** Archive original messages + rewrite working history + emit `compress` event. */
|
|
1641
|
+
const rewriteCompressed = async (compressed, archived) => {
|
|
1642
|
+
if (params.session) {
|
|
1643
|
+
if (deps.archiveHistory && archived.length > 0) {
|
|
1644
|
+
const archivedNorm = [];
|
|
1645
|
+
for (const msg of archived) {
|
|
1646
|
+
const n = normalizeForHistory(msg);
|
|
1647
|
+
if (n) archivedNorm.push(n);
|
|
1648
|
+
}
|
|
1649
|
+
try {
|
|
1650
|
+
await deps.archiveHistory(params.session, archivedNorm);
|
|
1651
|
+
} catch {}
|
|
1652
|
+
}
|
|
1653
|
+
if (deps.rewriteHistory) {
|
|
1654
|
+
const compressedNorm = [];
|
|
1655
|
+
for (const msg of compressed) {
|
|
1656
|
+
const n = normalizeForHistory(msg);
|
|
1657
|
+
if (n) compressedNorm.push(n);
|
|
1658
|
+
}
|
|
1659
|
+
await deps.rewriteHistory(params.session, compressedNorm);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
await emitObs({
|
|
1663
|
+
type: "compress",
|
|
1664
|
+
round: latestCompressInfo.round,
|
|
1665
|
+
compressed_messages: compressed,
|
|
1666
|
+
archived_count: archived.length,
|
|
1667
|
+
before_tokens: latestCompressInfo.beforeTokens,
|
|
1668
|
+
after_tokens: latestCompressInfo.afterTokens
|
|
1669
|
+
});
|
|
1670
|
+
};
|
|
1671
|
+
/** Archive conversation to memory provider (best-effort, called on completion). */
|
|
1672
|
+
const archiveToMemory = async (status) => {
|
|
1673
|
+
if (!params.memory?.archivePath || !params.session) return;
|
|
1674
|
+
if (status !== "completed") return;
|
|
1675
|
+
try {
|
|
1676
|
+
const archiveMessages = messages.filter((m) => {
|
|
1677
|
+
const role = m.role;
|
|
1678
|
+
if (role !== "user" && role !== "assistant") return false;
|
|
1679
|
+
const content = m.content;
|
|
1680
|
+
return content != null && String(content).trim().length > 0;
|
|
1681
|
+
}).map((m) => ({
|
|
1682
|
+
role: String(m.role),
|
|
1683
|
+
content: String(m.content)
|
|
1684
|
+
}));
|
|
1685
|
+
if (archiveMessages.length > 0) {
|
|
1686
|
+
const safeSessionId = params.session.replace(/^\/+/, "").replace(/\//g, "-");
|
|
1687
|
+
await deps.callAFS("exec", params.memory.archivePath, {
|
|
1688
|
+
sessionId: safeSessionId,
|
|
1689
|
+
messages: archiveMessages,
|
|
1690
|
+
domain: params.memory.domain
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
} catch (e) {
|
|
1694
|
+
log$1.error(`[agent-run] archiveToMemory error: ${e instanceof Error ? e.message : String(e)}`);
|
|
1695
|
+
}
|
|
1696
|
+
};
|
|
1697
|
+
await appendNewMessages([messages[messages.length - 1]]);
|
|
1698
|
+
const trace = [];
|
|
1699
|
+
let totalTokens = 0;
|
|
1700
|
+
let totalActions = 0;
|
|
1701
|
+
let subRuns = 0;
|
|
1702
|
+
let totalInputTokens = 0;
|
|
1703
|
+
let totalOutputTokens = 0;
|
|
1704
|
+
let totalCachedTokens = 0;
|
|
1705
|
+
let totalCost = 0;
|
|
1706
|
+
let resolvedModelName = "";
|
|
1707
|
+
const callHistory = /* @__PURE__ */ new Map();
|
|
1708
|
+
const totalsAccumulator = {
|
|
1709
|
+
rounds: 0,
|
|
1710
|
+
totalTokensIn: 0,
|
|
1711
|
+
totalTokensOut: 0,
|
|
1712
|
+
totalCachedTokens: 0,
|
|
1713
|
+
totalCost: 0,
|
|
1714
|
+
modelsUsed: /* @__PURE__ */ new Set(),
|
|
1715
|
+
hubsUsed: /* @__PURE__ */ new Set(),
|
|
1716
|
+
toolsUsed: /* @__PURE__ */ new Set()
|
|
1717
|
+
};
|
|
1718
|
+
/** Update totals + emit `turn` event after each LLM call. Hub is pulled from
|
|
1719
|
+
* `data.reason.topPick.hub` when AI Device returns a routing reason. */
|
|
1720
|
+
const emitTurn = async (turnRound, turnStartedAtMs, data, inputTokens, outputTokens, cost, cachedTokens, status = "ok", errorMessage = null) => {
|
|
1721
|
+
const hub = ((data?.reason)?.topPick)?.hub ?? null;
|
|
1722
|
+
const turnModel = data?.model ?? resolvedModelName ?? null;
|
|
1723
|
+
totalsAccumulator.rounds += 1;
|
|
1724
|
+
totalsAccumulator.totalTokensIn += inputTokens;
|
|
1725
|
+
totalsAccumulator.totalTokensOut += outputTokens;
|
|
1726
|
+
totalsAccumulator.totalCost += cost;
|
|
1727
|
+
totalsAccumulator.totalCachedTokens += cachedTokens;
|
|
1728
|
+
if (turnModel) totalsAccumulator.modelsUsed.add(turnModel);
|
|
1729
|
+
if (hub) totalsAccumulator.hubsUsed.add(hub);
|
|
1730
|
+
await emitObs({
|
|
1731
|
+
type: "turn",
|
|
1732
|
+
round: turnRound,
|
|
1733
|
+
model: turnModel,
|
|
1734
|
+
hub,
|
|
1735
|
+
tokensIn: inputTokens,
|
|
1736
|
+
tokensOut: outputTokens,
|
|
1737
|
+
tokensCached: cachedTokens,
|
|
1738
|
+
cost,
|
|
1739
|
+
durationMs: Date.now() - turnStartedAtMs,
|
|
1740
|
+
status,
|
|
1741
|
+
errorMessage
|
|
1742
|
+
});
|
|
1743
|
+
};
|
|
1744
|
+
/** Snapshot the totals accumulator for the AgentRunResult.totals field. */
|
|
1745
|
+
const totalsSnapshot = () => ({
|
|
1746
|
+
rounds: totalsAccumulator.rounds,
|
|
1747
|
+
totalTokensIn: totalsAccumulator.totalTokensIn,
|
|
1748
|
+
totalTokensOut: totalsAccumulator.totalTokensOut,
|
|
1749
|
+
totalCachedTokens: totalsAccumulator.totalCachedTokens,
|
|
1750
|
+
totalCost: totalsAccumulator.totalCost,
|
|
1751
|
+
modelsUsed: [...totalsAccumulator.modelsUsed],
|
|
1752
|
+
hubsUsed: [...totalsAccumulator.hubsUsed],
|
|
1753
|
+
toolsUsed: [...totalsAccumulator.toolsUsed]
|
|
1754
|
+
});
|
|
1755
|
+
for (let round = 1; round <= maxRounds; round++) {
|
|
1756
|
+
const roundStartedAtMs = Date.now();
|
|
1757
|
+
if (params.abortSignal?.aborted) {
|
|
1758
|
+
emitProgress({
|
|
1759
|
+
type: "summary",
|
|
1760
|
+
round: round - 1,
|
|
1761
|
+
model: resolvedModelName,
|
|
1762
|
+
tokens: {
|
|
1763
|
+
input: totalInputTokens,
|
|
1764
|
+
output: totalOutputTokens,
|
|
1765
|
+
cached: totalCachedTokens,
|
|
1766
|
+
cost: totalCost
|
|
1767
|
+
}
|
|
1768
|
+
});
|
|
1769
|
+
completeProc("done");
|
|
1770
|
+
return {
|
|
1771
|
+
status: "budget_exhausted",
|
|
1772
|
+
result: void 0,
|
|
1773
|
+
rounds: round - 1,
|
|
1774
|
+
total_actions: totalActions,
|
|
1775
|
+
total_tokens: totalTokens,
|
|
1776
|
+
sub_runs: subRuns,
|
|
1777
|
+
trace,
|
|
1778
|
+
totals: totalsSnapshot()
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1781
|
+
const compressed = await maybeCompress(messages, {
|
|
1782
|
+
contextWindow,
|
|
1783
|
+
pinMessage: currentTaskMessage,
|
|
1784
|
+
callLLM: deps.callLLM,
|
|
1785
|
+
onCompressed: rewriteCompressed,
|
|
1786
|
+
onProgress: (info) => {
|
|
1787
|
+
if (info.phase === "skipped") return;
|
|
1788
|
+
latestCompressInfo = {
|
|
1789
|
+
round,
|
|
1790
|
+
beforeTokens: info.beforeTokens,
|
|
1791
|
+
afterTokens: info.afterTokens ?? latestCompressInfo.afterTokens
|
|
1792
|
+
};
|
|
1793
|
+
if (params.onToolProgress) emitProgress({
|
|
1794
|
+
type: info.phase === "done" || info.phase === "failed" ? "compress_result" : "compress_start",
|
|
1795
|
+
round,
|
|
1796
|
+
compress: {
|
|
1797
|
+
phase: info.phase,
|
|
1798
|
+
beforeTokens: info.beforeTokens,
|
|
1799
|
+
contextWindow: info.contextWindow,
|
|
1800
|
+
compressedMessages: info.compressedMessages ?? 0,
|
|
1801
|
+
keptMessages: info.keptMessages ?? 0,
|
|
1802
|
+
afterTokens: info.afterTokens,
|
|
1803
|
+
error: info.error
|
|
1804
|
+
}
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
});
|
|
1808
|
+
if (compressed !== messages) {
|
|
1809
|
+
messages.length = 0;
|
|
1810
|
+
messages.push(...compressed);
|
|
1811
|
+
}
|
|
1812
|
+
const llmArgs = { ...params.modelArgs ?? {} };
|
|
1813
|
+
llmArgs.messages = [...messages];
|
|
1814
|
+
if (llmArgs.max_tokens == null && llmArgs.maxTokens == null) llmArgs.max_tokens = DEFAULT_MAX_OUTPUT_TOKENS;
|
|
1815
|
+
if (toolSchema.length > 0) {
|
|
1816
|
+
llmArgs.tools = toolSchema;
|
|
1817
|
+
llmArgs.toolChoice = params.toolChoice ?? "auto";
|
|
1818
|
+
}
|
|
1819
|
+
if (responseSchema) llmArgs.responseFormat = {
|
|
1820
|
+
type: "json_schema",
|
|
1821
|
+
jsonSchema: {
|
|
1822
|
+
name: "response",
|
|
1823
|
+
schema: responseSchema
|
|
1824
|
+
}
|
|
1825
|
+
};
|
|
1826
|
+
llmArgs.includeReason = true;
|
|
1827
|
+
const maxRetries = params.budget?.max_retries ?? DEFAULT_MAX_RETRIES;
|
|
1828
|
+
let llmResult;
|
|
1829
|
+
let lastLLMError = "";
|
|
1830
|
+
let hadStreamingDeltas = false;
|
|
1831
|
+
if (params.onToolProgress) llmArgs._onChunk = (chunk) => {
|
|
1832
|
+
if (chunk.thoughts) {
|
|
1833
|
+
hadStreamingDeltas = true;
|
|
1834
|
+
emitProgress({
|
|
1835
|
+
type: "thinking_delta",
|
|
1836
|
+
round,
|
|
1837
|
+
text: chunk.thoughts
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
if (chunk.text) {
|
|
1841
|
+
hadStreamingDeltas = true;
|
|
1842
|
+
emitProgress({
|
|
1843
|
+
type: "llm_delta",
|
|
1844
|
+
round,
|
|
1845
|
+
text: chunk.text
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
};
|
|
1849
|
+
emitProgress({
|
|
1850
|
+
type: "llm_start",
|
|
1851
|
+
round,
|
|
1852
|
+
model: resolvedModelName
|
|
1853
|
+
});
|
|
1854
|
+
const llmStartTime = Date.now();
|
|
1855
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
1856
|
+
try {
|
|
1857
|
+
llmResult = await Promise.race([deps.callLLM(llmArgs), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("LLM call timed out")), LLM_CALL_TIMEOUT_MS))]);
|
|
1858
|
+
if (llmResult.success) break;
|
|
1859
|
+
lastLLMError = llmResult.data?.error ?? llmResult.error?.message ?? "unknown error";
|
|
1860
|
+
} catch (err) {
|
|
1861
|
+
lastLLMError = err instanceof Error ? err.message : String(err);
|
|
1862
|
+
llmResult = void 0;
|
|
1863
|
+
}
|
|
1864
|
+
const hasNextAttempt = attempt < maxRetries - 1;
|
|
1865
|
+
const retryDelayMs = hasNextAttempt ? computeLLMRetryDelayMs(lastLLMError, attempt + 1) : void 0;
|
|
1866
|
+
emitProgress({
|
|
1867
|
+
type: "llm_result",
|
|
1868
|
+
round,
|
|
1869
|
+
model: resolvedModelName,
|
|
1870
|
+
attempt: attempt + 1,
|
|
1871
|
+
maxAttempts: maxRetries,
|
|
1872
|
+
llmError: lastLLMError,
|
|
1873
|
+
retryDelayMs
|
|
1874
|
+
});
|
|
1875
|
+
if (hasNextAttempt && retryDelayMs && retryDelayMs > 0) await (deps.sleep ?? sleep)(retryDelayMs, params.abortSignal);
|
|
1876
|
+
}
|
|
1877
|
+
if (!llmResult?.success) {
|
|
1878
|
+
trace.push({
|
|
1879
|
+
round,
|
|
1880
|
+
tool_calls: [],
|
|
1881
|
+
tokens: {
|
|
1882
|
+
input: 0,
|
|
1883
|
+
output: 0
|
|
1884
|
+
}
|
|
1885
|
+
});
|
|
1886
|
+
const diagMessages = llmArgs.messages || [];
|
|
1887
|
+
const requestSummary = {
|
|
1888
|
+
model: params.model,
|
|
1889
|
+
messageCount: diagMessages.length,
|
|
1890
|
+
hasTools: !!llmArgs.tools,
|
|
1891
|
+
toolCount: Array.isArray(llmArgs.tools) ? llmArgs.tools.length : 0
|
|
1892
|
+
};
|
|
1893
|
+
await emitTurn(round, roundStartedAtMs, void 0, 0, 0, 0, 0, "error", lastLLMError);
|
|
1894
|
+
completeProc("failed");
|
|
1895
|
+
return {
|
|
1896
|
+
status: "error",
|
|
1897
|
+
error: `LLM call failed after ${maxRetries} attempts: ${lastLLMError}`,
|
|
1898
|
+
detail: {
|
|
1899
|
+
request: requestSummary,
|
|
1900
|
+
lastResponse: llmResult ?? null
|
|
1901
|
+
},
|
|
1902
|
+
rounds: round,
|
|
1903
|
+
total_actions: totalActions,
|
|
1904
|
+
total_tokens: totalTokens,
|
|
1905
|
+
sub_runs: subRuns,
|
|
1906
|
+
trace,
|
|
1907
|
+
totals: totalsSnapshot()
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
const data = llmResult.data;
|
|
1911
|
+
const inputTokens = data.inputTokens ?? 0;
|
|
1912
|
+
const outputTokens = data.outputTokens ?? 0;
|
|
1913
|
+
totalTokens += inputTokens + outputTokens;
|
|
1914
|
+
if (procId) deps.callAFS("exec", "/proc/.actions/record-usage", {
|
|
1915
|
+
id: procId,
|
|
1916
|
+
tokens: inputTokens + outputTokens,
|
|
1917
|
+
calls: 1
|
|
1918
|
+
}).catch(() => {});
|
|
1919
|
+
if (data.model) resolvedModelName = data.model;
|
|
1920
|
+
const llmUsage = data.usage;
|
|
1921
|
+
totalInputTokens += inputTokens;
|
|
1922
|
+
totalOutputTokens += outputTokens;
|
|
1923
|
+
if (llmUsage?.cacheReadInputTokens) totalCachedTokens += llmUsage.cacheReadInputTokens;
|
|
1924
|
+
if (llmUsage?.aigneHubCredits) totalCost += llmUsage.aigneHubCredits;
|
|
1925
|
+
const text = data.text;
|
|
1926
|
+
const json = data.json;
|
|
1927
|
+
const thoughtsText = data.thoughts;
|
|
1928
|
+
const rawToolCalls = data.toolCalls;
|
|
1929
|
+
const finishReason = data.finishReason?.toLowerCase();
|
|
1930
|
+
if (finishReason === "length" || finishReason === "max_tokens") emitProgress({
|
|
1931
|
+
type: "warning",
|
|
1932
|
+
round,
|
|
1933
|
+
model: resolvedModelName,
|
|
1934
|
+
text: "Model output hit the token limit and was truncated — any content written this round may be incomplete. Write more concise content, or split a large file (one cms-write, then mode:append for the rest)."
|
|
1935
|
+
});
|
|
1936
|
+
if (thoughtsText && !hadStreamingDeltas) emitProgress({
|
|
1937
|
+
type: "thinking",
|
|
1938
|
+
round,
|
|
1939
|
+
text: thoughtsText,
|
|
1940
|
+
model: resolvedModelName
|
|
1941
|
+
});
|
|
1942
|
+
if (!rawToolCalls || rawToolCalls.length === 0) {
|
|
1943
|
+
trace.push({
|
|
1944
|
+
round,
|
|
1945
|
+
tool_calls: [],
|
|
1946
|
+
tokens: {
|
|
1947
|
+
input: inputTokens,
|
|
1948
|
+
output: outputTokens
|
|
1949
|
+
}
|
|
1950
|
+
});
|
|
1951
|
+
emitProgress({
|
|
1952
|
+
type: "summary",
|
|
1953
|
+
round,
|
|
1954
|
+
model: resolvedModelName,
|
|
1955
|
+
tokens: {
|
|
1956
|
+
input: totalInputTokens,
|
|
1957
|
+
output: totalOutputTokens,
|
|
1958
|
+
cached: totalCachedTokens,
|
|
1959
|
+
cost: totalCost
|
|
1960
|
+
}
|
|
1961
|
+
});
|
|
1962
|
+
await emitTurn(round, roundStartedAtMs, data, inputTokens, outputTokens, llmUsage?.aigneHubCredits ?? 0, llmUsage?.cacheReadInputTokens ?? 0);
|
|
1963
|
+
const assistantContent = json ? JSON.stringify(json) : text ?? "";
|
|
1964
|
+
const llmDurationMs$1 = Date.now() - llmStartTime;
|
|
1965
|
+
messages.push({
|
|
1966
|
+
role: "assistant",
|
|
1967
|
+
content: assistantContent,
|
|
1968
|
+
durationMs: llmDurationMs$1
|
|
1969
|
+
});
|
|
1970
|
+
await appendNewMessages([messages[messages.length - 1]]);
|
|
1971
|
+
let result = text ?? "";
|
|
1972
|
+
if (responseSchema) {
|
|
1973
|
+
if (json) result = json;
|
|
1974
|
+
else if (text) try {
|
|
1975
|
+
result = JSON.parse(text);
|
|
1976
|
+
} catch {
|
|
1977
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
1978
|
+
if (jsonMatch) try {
|
|
1979
|
+
result = JSON.parse(jsonMatch[0]);
|
|
1980
|
+
} catch {}
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
if (params.onProgress) {
|
|
1984
|
+
const prog = params.onProgress;
|
|
1985
|
+
const progressPath = typeof prog === "string" ? prog : prog.path;
|
|
1986
|
+
const progressArgs = typeof prog === "string" ? {} : Object.fromEntries(Object.entries(prog).filter(([k]) => k !== "path"));
|
|
1987
|
+
const trimmed = text?.trim() ?? "";
|
|
1988
|
+
const prefixedText = trimmed ? `🤖 ${trimmed}` : "";
|
|
1989
|
+
try {
|
|
1990
|
+
await deps.callAFS("exec", progressPath, {
|
|
1991
|
+
...progressArgs,
|
|
1992
|
+
text: prefixedText,
|
|
1993
|
+
message_text: prefixedText,
|
|
1994
|
+
tool_summary: ""
|
|
1995
|
+
});
|
|
1996
|
+
} catch {}
|
|
1997
|
+
}
|
|
1998
|
+
await archiveToMemory("completed");
|
|
1999
|
+
completeProc("done");
|
|
2000
|
+
return {
|
|
2001
|
+
status: "completed",
|
|
2002
|
+
result,
|
|
2003
|
+
rounds: round,
|
|
2004
|
+
total_actions: totalActions,
|
|
2005
|
+
total_tokens: totalTokens,
|
|
2006
|
+
sub_runs: subRuns,
|
|
2007
|
+
trace,
|
|
2008
|
+
totals: totalsSnapshot()
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
if (totalTokens > tokenBudget) {
|
|
2012
|
+
trace.push({
|
|
2013
|
+
round,
|
|
2014
|
+
tool_calls: [],
|
|
2015
|
+
tokens: {
|
|
2016
|
+
input: inputTokens,
|
|
2017
|
+
output: outputTokens
|
|
2018
|
+
}
|
|
2019
|
+
});
|
|
2020
|
+
emitProgress({
|
|
2021
|
+
type: "summary",
|
|
2022
|
+
round,
|
|
2023
|
+
model: resolvedModelName,
|
|
2024
|
+
tokens: {
|
|
2025
|
+
input: totalInputTokens,
|
|
2026
|
+
output: totalOutputTokens,
|
|
2027
|
+
cached: totalCachedTokens,
|
|
2028
|
+
cost: totalCost
|
|
2029
|
+
}
|
|
2030
|
+
});
|
|
2031
|
+
await emitTurn(round, roundStartedAtMs, data, inputTokens, outputTokens, llmUsage?.aigneHubCredits ?? 0, llmUsage?.cacheReadInputTokens ?? 0);
|
|
2032
|
+
completeProc("done");
|
|
2033
|
+
return {
|
|
2034
|
+
status: "budget_exhausted",
|
|
2035
|
+
result: text,
|
|
2036
|
+
rounds: round,
|
|
2037
|
+
total_actions: totalActions,
|
|
2038
|
+
total_tokens: totalTokens,
|
|
2039
|
+
sub_runs: subRuns,
|
|
2040
|
+
trace,
|
|
2041
|
+
totals: totalsSnapshot()
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
const toolCalls = rawToolCalls.map(normalizeToolCall);
|
|
2045
|
+
await emitTurn(round, roundStartedAtMs, data, inputTokens, outputTokens, llmUsage?.aigneHubCredits ?? 0, llmUsage?.cacheReadInputTokens ?? 0);
|
|
2046
|
+
const roundMsgStart = messages.length;
|
|
2047
|
+
const llmDurationMs = Date.now() - llmStartTime;
|
|
2048
|
+
messages.push({
|
|
2049
|
+
role: "assistant",
|
|
2050
|
+
content: text ?? "",
|
|
2051
|
+
toolCalls: rawToolCalls,
|
|
2052
|
+
durationMs: llmDurationMs
|
|
2053
|
+
});
|
|
2054
|
+
await appendNewMessages([messages[messages.length - 1]]);
|
|
2055
|
+
const roundCalls = toolCalls.slice(0, actionsPerRound);
|
|
2056
|
+
const droppedCalls = toolCalls.slice(actionsPerRound);
|
|
2057
|
+
const traceEntries = [];
|
|
2058
|
+
if (text?.trim()) emitProgress({
|
|
2059
|
+
type: "thinking",
|
|
2060
|
+
round,
|
|
2061
|
+
text,
|
|
2062
|
+
model: resolvedModelName
|
|
2063
|
+
});
|
|
2064
|
+
emitProgress({
|
|
2065
|
+
type: "tool_start",
|
|
2066
|
+
round,
|
|
2067
|
+
calls: roundCalls.map((c) => ({
|
|
2068
|
+
id: c.toolCallId,
|
|
2069
|
+
tool: c.toolName,
|
|
2070
|
+
path: String(c.args.path ?? ""),
|
|
2071
|
+
status: "pending",
|
|
2072
|
+
args: c.args
|
|
2073
|
+
}))
|
|
2074
|
+
});
|
|
2075
|
+
const toolsStartTime = Date.now();
|
|
2076
|
+
const skillResults = /* @__PURE__ */ new Map();
|
|
2077
|
+
const breakerResults = /* @__PURE__ */ new Map();
|
|
2078
|
+
const thisRoundKeys = /* @__PURE__ */ new Set();
|
|
2079
|
+
for (const call of roundCalls) {
|
|
2080
|
+
const key = `${call.toolName}:${stableStringify(call.args)}`;
|
|
2081
|
+
thisRoundKeys.add(key);
|
|
2082
|
+
if (String(call.args?.path ?? "") === ASK_USER_PATH) continue;
|
|
2083
|
+
const entry = callHistory.get(key);
|
|
2084
|
+
if (entry && entry.count >= CIRCUIT_BREAKER_THRESHOLD - 1) {
|
|
2085
|
+
breakerResults.set(call.toolCallId, entry.lastResult);
|
|
2086
|
+
traceEntries.push({
|
|
2087
|
+
id: call.toolCallId,
|
|
2088
|
+
name: call.toolName,
|
|
2089
|
+
path: String(call.args.path ?? ""),
|
|
2090
|
+
status: "circuit_breaker"
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
const validatedCalls = [];
|
|
2095
|
+
for (const call of roundCalls) {
|
|
2096
|
+
if (skillResults.has(call.toolCallId)) continue;
|
|
2097
|
+
if (breakerResults.has(call.toolCallId)) continue;
|
|
2098
|
+
const op = toolNameToOp(call.toolName);
|
|
2099
|
+
if (!op) {
|
|
2100
|
+
validatedCalls.push({
|
|
2101
|
+
call,
|
|
2102
|
+
op: "unknown",
|
|
2103
|
+
valid: false,
|
|
2104
|
+
reason: `Unknown tool: ${call.toolName}`
|
|
2105
|
+
});
|
|
2106
|
+
continue;
|
|
2107
|
+
}
|
|
2108
|
+
if (call.toolName.startsWith("afs_action_")) fixPerActionPath(call, schemasByAction);
|
|
2109
|
+
const path = call.args.path;
|
|
2110
|
+
const isBatchWrite = op === "write" && Array.isArray(call.args.entries) && call.args.entries.length > 0;
|
|
2111
|
+
const batchReadPaths = op === "read" && Array.isArray(call.args.entries) && call.args.entries.length > 0 ? call.args.entries.map((e) => e?.path).filter((p) => typeof p === "string") : null;
|
|
2112
|
+
if (!path && !isBatchWrite && !batchReadPaths) {
|
|
2113
|
+
validatedCalls.push({
|
|
2114
|
+
call,
|
|
2115
|
+
op,
|
|
2116
|
+
valid: false,
|
|
2117
|
+
reason: "Missing path argument"
|
|
2118
|
+
});
|
|
2119
|
+
continue;
|
|
2120
|
+
}
|
|
2121
|
+
if (path) {
|
|
2122
|
+
const validation = validateToolCall(path, op, params.tools);
|
|
2123
|
+
if (!validation.allowed) {
|
|
2124
|
+
validatedCalls.push({
|
|
2125
|
+
call,
|
|
2126
|
+
op,
|
|
2127
|
+
valid: false,
|
|
2128
|
+
reason: validation.reason
|
|
2129
|
+
});
|
|
2130
|
+
continue;
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
if (batchReadPaths) {
|
|
2134
|
+
const denied = batchReadPaths.map((p) => ({
|
|
2135
|
+
p,
|
|
2136
|
+
v: validateToolCall(p, op, params.tools)
|
|
2137
|
+
})).find(({ v }) => !v.allowed);
|
|
2138
|
+
if (denied) {
|
|
2139
|
+
validatedCalls.push({
|
|
2140
|
+
call,
|
|
2141
|
+
op,
|
|
2142
|
+
valid: false,
|
|
2143
|
+
reason: denied.v.reason
|
|
2144
|
+
});
|
|
2145
|
+
continue;
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
const missingArg = REQUIRED_ARGS[op]?.find((arg) => !(arg in call.args));
|
|
2149
|
+
if (missingArg) {
|
|
2150
|
+
validatedCalls.push({
|
|
2151
|
+
call,
|
|
2152
|
+
op,
|
|
2153
|
+
valid: false,
|
|
2154
|
+
reason: `Missing required argument '${missingArg}' for ${op}`
|
|
2155
|
+
});
|
|
2156
|
+
continue;
|
|
2157
|
+
}
|
|
2158
|
+
validatedCalls.push({
|
|
2159
|
+
call,
|
|
2160
|
+
op,
|
|
2161
|
+
valid: true
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2164
|
+
const ashCalls = [];
|
|
2165
|
+
const directCalls = [];
|
|
2166
|
+
const errorResults = [];
|
|
2167
|
+
for (const { call, op, valid, reason } of validatedCalls) {
|
|
2168
|
+
if (!valid) {
|
|
2169
|
+
errorResults.push({
|
|
2170
|
+
id: call.toolCallId,
|
|
2171
|
+
error: reason ?? "Validation failed"
|
|
2172
|
+
});
|
|
2173
|
+
traceEntries.push({
|
|
2174
|
+
id: call.toolCallId,
|
|
2175
|
+
name: call.toolName,
|
|
2176
|
+
path: String(call.args.path ?? ""),
|
|
2177
|
+
status: "error",
|
|
2178
|
+
error: reason
|
|
2179
|
+
});
|
|
2180
|
+
emitProgress({
|
|
2181
|
+
type: "tool_result",
|
|
2182
|
+
round,
|
|
2183
|
+
calls: [{
|
|
2184
|
+
id: call.toolCallId,
|
|
2185
|
+
tool: call.toolName,
|
|
2186
|
+
path: String(call.args.path ?? ""),
|
|
2187
|
+
status: "error",
|
|
2188
|
+
error: reason
|
|
2189
|
+
}]
|
|
2190
|
+
});
|
|
2191
|
+
continue;
|
|
2192
|
+
}
|
|
2193
|
+
const path = call.args.path;
|
|
2194
|
+
if (ASH_OPS.has(op)) ashCalls.push({
|
|
2195
|
+
id: call.toolCallId,
|
|
2196
|
+
op,
|
|
2197
|
+
path,
|
|
2198
|
+
args: extractExecArgs(call.args)
|
|
2199
|
+
});
|
|
2200
|
+
else directCalls.push({
|
|
2201
|
+
call,
|
|
2202
|
+
op
|
|
2203
|
+
});
|
|
2204
|
+
traceEntries.push({
|
|
2205
|
+
id: call.toolCallId,
|
|
2206
|
+
name: call.toolName,
|
|
2207
|
+
path,
|
|
2208
|
+
status: "ok"
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
const ashResults = {};
|
|
2212
|
+
if (ashCalls.length > 0) {
|
|
2213
|
+
const source = generateAsh(ashCalls, round);
|
|
2214
|
+
const ashResult = await deps.runAsh(source, {
|
|
2215
|
+
returnWrittenData: true,
|
|
2216
|
+
skipWritePrefix: RESULTS_PREFIX
|
|
2217
|
+
});
|
|
2218
|
+
if (ashResult.success && ashResult.data?.writtenData) {
|
|
2219
|
+
const writtenData = ashResult.data.writtenData;
|
|
2220
|
+
for (const call of ashCalls) {
|
|
2221
|
+
const data$1 = writtenData[`${RESULTS_PREFIX}${call.id}`];
|
|
2222
|
+
ashResults[call.id] = data$1 ? JSON.stringify(data$1) : "No result";
|
|
2223
|
+
}
|
|
2224
|
+
} else for (const call of ashCalls) ashResults[call.id] = `ASH execution error: ${ashResult.data?.error ?? "unknown"}`;
|
|
2225
|
+
emitProgress({
|
|
2226
|
+
type: "tool_result",
|
|
2227
|
+
round,
|
|
2228
|
+
calls: ashCalls.map((c) => ({
|
|
2229
|
+
id: c.id,
|
|
2230
|
+
tool: `afs_${c.op}`,
|
|
2231
|
+
path: c.path,
|
|
2232
|
+
status: ashResults[c.id]?.startsWith("ASH execution error") ? "error" : "ok",
|
|
2233
|
+
result: truncateResult(ashResults[c.id], 2e3)
|
|
2234
|
+
}))
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
const directResults = {};
|
|
2238
|
+
const executeDirectCall = async ({ call, op }) => {
|
|
2239
|
+
const path = call.args.path || "/";
|
|
2240
|
+
const directArgs = { ...call.args };
|
|
2241
|
+
delete directArgs.path;
|
|
2242
|
+
if (op === "exec" && directArgs.args && typeof directArgs.args === "object" && !Array.isArray(directArgs.args)) {
|
|
2243
|
+
if (!("task" in directArgs || "proc" in directArgs || "session" in directArgs || "system" in directArgs)) {
|
|
2244
|
+
const inner = directArgs.args;
|
|
2245
|
+
delete directArgs.args;
|
|
2246
|
+
for (const [k, v] of Object.entries(inner)) if (!(k in directArgs)) directArgs[k] = v;
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
if (op === "exec" && params.execContext) {
|
|
2250
|
+
const nested = directArgs.args;
|
|
2251
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
2252
|
+
const nestedObj = nested;
|
|
2253
|
+
const innerArgs = nestedObj.args;
|
|
2254
|
+
if (innerArgs && typeof innerArgs === "object" && !Array.isArray(innerArgs)) nestedObj.args = {
|
|
2255
|
+
...params.execContext,
|
|
2256
|
+
...innerArgs
|
|
2257
|
+
};
|
|
2258
|
+
else directArgs.args = {
|
|
2259
|
+
...params.execContext,
|
|
2260
|
+
...nestedObj
|
|
2261
|
+
};
|
|
2262
|
+
} else if (path.startsWith("/proc/messaging/tools/")) {
|
|
2263
|
+
for (const [k, v] of Object.entries(params.execContext)) if (!(k in directArgs)) directArgs[k] = v;
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
if (op === "exec" && procId && !directArgs.parentProcId) directArgs.parentProcId = procId;
|
|
2267
|
+
if (op === "exec" && isSubAgentRunPath(path)) {
|
|
2268
|
+
const parentRemaining = tokenBudget - totalTokens;
|
|
2269
|
+
if (parentRemaining < Number.POSITIVE_INFINITY) directArgs._parent_budget_remaining = Math.max(0, parentRemaining);
|
|
2270
|
+
if (params.abortSignal) directArgs._abort_signal = params.abortSignal;
|
|
2271
|
+
const parentSession = params.session ?? nesting.chain[nesting.chain.length - 1] ?? void 0;
|
|
2272
|
+
if (parentSession) directArgs._parent_session = parentSession;
|
|
2273
|
+
const childSession = typeof directArgs.session === "string" && directArgs.session ? directArgs.session : `${path}#${call.toolCallId}`;
|
|
2274
|
+
directArgs._chain = [...nesting.chain, childSession];
|
|
2275
|
+
}
|
|
2276
|
+
const toolStartedAt = Date.now();
|
|
2277
|
+
let recordResult = null;
|
|
2278
|
+
let recordStatus = "ok";
|
|
2279
|
+
let recordError = null;
|
|
2280
|
+
try {
|
|
2281
|
+
const result = await deps.callAFS(op, path, directArgs);
|
|
2282
|
+
const isOk = result.success;
|
|
2283
|
+
if (isOk && op === "exec" && path.endsWith("/.actions/run")) subRuns += 1;
|
|
2284
|
+
let dataForLLM = result.data;
|
|
2285
|
+
if (isOk && dataForLLM && typeof dataForLLM === "object" && "_meta" in dataForLLM) {
|
|
2286
|
+
const shaped = dataForLLM;
|
|
2287
|
+
if (shaped._meta) {
|
|
2288
|
+
emitProgress({
|
|
2289
|
+
type: "subAgentMetadata",
|
|
2290
|
+
round,
|
|
2291
|
+
calls: [{
|
|
2292
|
+
id: call.toolCallId,
|
|
2293
|
+
tool: call.toolName,
|
|
2294
|
+
path,
|
|
2295
|
+
status: "ok"
|
|
2296
|
+
}],
|
|
2297
|
+
subAgentMeta: shaped._meta
|
|
2298
|
+
});
|
|
2299
|
+
const childTokens = shaped._meta.tokens_used;
|
|
2300
|
+
if (typeof childTokens === "number" && childTokens > 0) totalTokens += childTokens;
|
|
2301
|
+
}
|
|
2302
|
+
dataForLLM = shaped.result;
|
|
2303
|
+
}
|
|
2304
|
+
if (isOk) {
|
|
2305
|
+
recordResult = dataForLLM;
|
|
2306
|
+
recordStatus = "ok";
|
|
2307
|
+
} else {
|
|
2308
|
+
recordStatus = "error";
|
|
2309
|
+
recordError = result.error?.message ?? result.data?.error ?? "failed";
|
|
2310
|
+
}
|
|
2311
|
+
let stringifiedResult;
|
|
2312
|
+
if (isOk) try {
|
|
2313
|
+
stringifiedResult = JSON.stringify(dataForLLM);
|
|
2314
|
+
} catch (stringErr) {
|
|
2315
|
+
const msg = stringErr instanceof Error ? stringErr.message : String(stringErr);
|
|
2316
|
+
stringifiedResult = `Error: ${msg}`;
|
|
2317
|
+
recordStatus = "error";
|
|
2318
|
+
recordError = msg;
|
|
2319
|
+
}
|
|
2320
|
+
else stringifiedResult = `Error: ${recordError ?? "failed"}`;
|
|
2321
|
+
directResults[call.toolCallId] = stringifiedResult;
|
|
2322
|
+
emitProgress({
|
|
2323
|
+
type: "tool_result",
|
|
2324
|
+
round,
|
|
2325
|
+
calls: [{
|
|
2326
|
+
id: call.toolCallId,
|
|
2327
|
+
tool: call.toolName,
|
|
2328
|
+
path,
|
|
2329
|
+
status: isOk ? "ok" : "error",
|
|
2330
|
+
...!isOk && { error: directResults[call.toolCallId] },
|
|
2331
|
+
result: truncateResult(directResults[call.toolCallId], 2e3)
|
|
2332
|
+
}]
|
|
2333
|
+
});
|
|
2334
|
+
} catch (err) {
|
|
2335
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2336
|
+
directResults[call.toolCallId] = `Error: ${msg}`;
|
|
2337
|
+
recordStatus = "error";
|
|
2338
|
+
recordError = msg;
|
|
2339
|
+
emitProgress({
|
|
2340
|
+
type: "tool_result",
|
|
2341
|
+
round,
|
|
2342
|
+
calls: [{
|
|
2343
|
+
id: call.toolCallId,
|
|
2344
|
+
tool: call.toolName,
|
|
2345
|
+
path,
|
|
2346
|
+
status: "error",
|
|
2347
|
+
error: msg,
|
|
2348
|
+
result: `Error: ${msg}`
|
|
2349
|
+
}]
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
totalsAccumulator.toolsUsed.add(call.toolName);
|
|
2353
|
+
await emitObs({
|
|
2354
|
+
type: "tool",
|
|
2355
|
+
round,
|
|
2356
|
+
callId: call.toolCallId,
|
|
2357
|
+
name: call.toolName,
|
|
2358
|
+
path,
|
|
2359
|
+
op,
|
|
2360
|
+
args: stripInternalFields(directArgs),
|
|
2361
|
+
result: truncateField(recordResult, FIELD_LIMIT),
|
|
2362
|
+
status: recordStatus,
|
|
2363
|
+
errorMessage: recordError,
|
|
2364
|
+
durationMs: Date.now() - toolStartedAt
|
|
2365
|
+
});
|
|
2366
|
+
};
|
|
2367
|
+
const isMutatingOp = (op) => op === "write" || op === "exec";
|
|
2368
|
+
let readBatch = [];
|
|
2369
|
+
const flushReadBatch = async () => {
|
|
2370
|
+
if (readBatch.length === 0) return;
|
|
2371
|
+
await Promise.all(readBatch.map((entry) => executeDirectCall(entry)));
|
|
2372
|
+
readBatch = [];
|
|
2373
|
+
};
|
|
2374
|
+
for (const entry of directCalls) {
|
|
2375
|
+
if (isMutatingOp(entry.op)) {
|
|
2376
|
+
await flushReadBatch();
|
|
2377
|
+
await executeDirectCall(entry);
|
|
2378
|
+
continue;
|
|
2379
|
+
}
|
|
2380
|
+
readBatch.push(entry);
|
|
2381
|
+
}
|
|
2382
|
+
await flushReadBatch();
|
|
2383
|
+
if (params.onProgress) {
|
|
2384
|
+
const prog = params.onProgress;
|
|
2385
|
+
const progressPath = typeof prog === "string" ? prog : prog.path;
|
|
2386
|
+
const progressArgs = typeof prog === "string" ? {} : Object.fromEntries(Object.entries(prog).filter(([k]) => k !== "path"));
|
|
2387
|
+
const toolSummary = summarizeToolCalls(roundCalls, traceEntries, errorResults);
|
|
2388
|
+
const trimmed = text?.trim() ?? "";
|
|
2389
|
+
const prefixedText = trimmed ? `🤖 ${trimmed}` : "";
|
|
2390
|
+
const enrichedText = prefixedText && toolSummary ? `${prefixedText}\n${toolSummary}` : prefixedText || toolSummary;
|
|
2391
|
+
await deps.callAFS("exec", progressPath, {
|
|
2392
|
+
...progressArgs,
|
|
2393
|
+
text: enrichedText,
|
|
2394
|
+
message_text: prefixedText,
|
|
2395
|
+
tool_summary: toolSummary
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
const updatedKeys = /* @__PURE__ */ new Set();
|
|
2399
|
+
for (const call of roundCalls) {
|
|
2400
|
+
if (breakerResults.has(call.toolCallId) || errorResults.find((e) => e.id === call.toolCallId)) continue;
|
|
2401
|
+
if (String(call.args?.path ?? "") === ASK_USER_PATH) continue;
|
|
2402
|
+
const key = `${call.toolName}:${stableStringify(call.args)}`;
|
|
2403
|
+
if (updatedKeys.has(key)) continue;
|
|
2404
|
+
updatedKeys.add(key);
|
|
2405
|
+
const result = ashResults[call.toolCallId] ?? directResults[call.toolCallId] ?? "";
|
|
2406
|
+
const prev = callHistory.get(key);
|
|
2407
|
+
callHistory.set(key, {
|
|
2408
|
+
count: (prev?.count ?? 0) + 1,
|
|
2409
|
+
lastResult: result
|
|
2410
|
+
});
|
|
2411
|
+
}
|
|
2412
|
+
for (const key of callHistory.keys()) if (!thisRoundKeys.has(key)) callHistory.delete(key);
|
|
2413
|
+
for (const call of roundCalls) {
|
|
2414
|
+
let content;
|
|
2415
|
+
if (skillResults.has(call.toolCallId)) content = skillResults.get(call.toolCallId);
|
|
2416
|
+
else if (breakerResults.has(call.toolCallId)) content = `[Circuit breaker: this exact call was repeated ${CIRCUIT_BREAKER_THRESHOLD} consecutive times. Cached result returned. Change your approach — do NOT re-send the identical call. If you are writing a large file and the content looks cut off, your output was likely truncated at the token limit: write more concise content, or split it (one cms-write for the first part, then mode:append for the rest).]\n${breakerResults.get(call.toolCallId)}`;
|
|
2417
|
+
else {
|
|
2418
|
+
const errorEntry = errorResults.find((e) => e.id === call.toolCallId);
|
|
2419
|
+
if (errorEntry) content = `Error: ${errorEntry.error}`;
|
|
2420
|
+
else {
|
|
2421
|
+
const resultStr = ashResults[call.toolCallId] ?? directResults[call.toolCallId] ?? void 0;
|
|
2422
|
+
content = resultStr !== void 0 ? enrichToolContent(truncateToolResult(resultStr, toolResultLimit)) : "Error: No result returned for this tool call";
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
if (typeof content === "string") content = truncateToolResult(content, toolResultLimit);
|
|
2426
|
+
messages.push({
|
|
2427
|
+
role: "tool",
|
|
2428
|
+
tool_call_id: call.toolCallId,
|
|
2429
|
+
content
|
|
2430
|
+
});
|
|
2431
|
+
}
|
|
2432
|
+
for (const call of droppedCalls) messages.push({
|
|
2433
|
+
role: "tool",
|
|
2434
|
+
tool_call_id: call.toolCallId,
|
|
2435
|
+
content: `Error: Tool call dropped — exceeded actions_per_round limit (${actionsPerRound}). Retry in next round.`
|
|
2436
|
+
});
|
|
2437
|
+
const toolsDurationMs = Date.now() - toolsStartTime;
|
|
2438
|
+
for (let i = roundMsgStart + 1; i < messages.length; i++) {
|
|
2439
|
+
const m = messages[i];
|
|
2440
|
+
if (m.role === "tool") m.durationMs = toolsDurationMs;
|
|
2441
|
+
}
|
|
2442
|
+
totalActions += roundCalls.length - errorResults.length - skillResults.size - breakerResults.size;
|
|
2443
|
+
trace.push({
|
|
2444
|
+
round,
|
|
2445
|
+
tool_calls: traceEntries,
|
|
2446
|
+
tokens: {
|
|
2447
|
+
input: inputTokens,
|
|
2448
|
+
output: outputTokens
|
|
2449
|
+
}
|
|
2450
|
+
});
|
|
2451
|
+
await appendNewMessages(messages.slice(roundMsgStart + 1));
|
|
2452
|
+
}
|
|
2453
|
+
emitProgress({
|
|
2454
|
+
type: "summary",
|
|
2455
|
+
round: maxRounds,
|
|
2456
|
+
model: resolvedModelName,
|
|
2457
|
+
tokens: {
|
|
2458
|
+
input: totalInputTokens,
|
|
2459
|
+
output: totalOutputTokens,
|
|
2460
|
+
cached: totalCachedTokens,
|
|
2461
|
+
cost: totalCost
|
|
2462
|
+
}
|
|
2463
|
+
});
|
|
2464
|
+
completeProc("done");
|
|
2465
|
+
return {
|
|
2466
|
+
status: "budget_exhausted",
|
|
2467
|
+
rounds: maxRounds,
|
|
2468
|
+
total_actions: totalActions,
|
|
2469
|
+
total_tokens: totalTokens,
|
|
2470
|
+
sub_runs: subRuns,
|
|
2471
|
+
trace,
|
|
2472
|
+
totals: totalsSnapshot()
|
|
2473
|
+
};
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
//#endregion
|
|
2477
|
+
//#region src/messages-log.ts
|
|
2478
|
+
/**
|
|
2479
|
+
* Append-only JSONL message log for inter-agent communication (L3).
|
|
2480
|
+
*
|
|
2481
|
+
* Provider internal: direct fs calls are allowed here (see CLAUDE.md).
|
|
2482
|
+
*/
|
|
2483
|
+
const MAX_BODY_BYTES = 64 * 1024;
|
|
2484
|
+
let globalSeq = 0;
|
|
2485
|
+
function appendMessage(filePath, from, to, body, kind = "message") {
|
|
2486
|
+
if (JSON.stringify(body).length > MAX_BODY_BYTES) throw new Error("PAYLOAD_TOO_LARGE: message body exceeds 64KB limit");
|
|
2487
|
+
if (!from || typeof from !== "string") throw new TypeError("from must be a non-empty string");
|
|
2488
|
+
if (!to || typeof to !== "string") throw new TypeError("to must be a non-empty string");
|
|
2489
|
+
const entry = {
|
|
2490
|
+
message_id: randomUUID(),
|
|
2491
|
+
from,
|
|
2492
|
+
to,
|
|
2493
|
+
kind,
|
|
2494
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2495
|
+
seq: ++globalSeq,
|
|
2496
|
+
body
|
|
2497
|
+
};
|
|
2498
|
+
const line = `${JSON.stringify(entry)}\n`;
|
|
2499
|
+
if (!existsSync(filePath)) writeFileSync(filePath, line);
|
|
2500
|
+
else appendFileSync(filePath, line);
|
|
2501
|
+
return entry;
|
|
2502
|
+
}
|
|
2503
|
+
function readMessages(filePath, options) {
|
|
2504
|
+
if (!existsSync(filePath)) return [];
|
|
2505
|
+
const raw = readFileSync(filePath, "utf8");
|
|
2506
|
+
if (!raw.trim()) return [];
|
|
2507
|
+
const entries = [];
|
|
2508
|
+
const lines = raw.split("\n");
|
|
2509
|
+
for (const line of lines) {
|
|
2510
|
+
const trimmed = line.trim();
|
|
2511
|
+
if (!trimmed) continue;
|
|
2512
|
+
try {
|
|
2513
|
+
const entry = JSON.parse(trimmed);
|
|
2514
|
+
entries.push(entry);
|
|
2515
|
+
} catch {}
|
|
2516
|
+
}
|
|
2517
|
+
let filtered = entries;
|
|
2518
|
+
if (options?.since) {
|
|
2519
|
+
const sinceDate = new Date(options.since);
|
|
2520
|
+
filtered = filtered.filter((e) => new Date(e.ts) > sinceDate);
|
|
2521
|
+
}
|
|
2522
|
+
filtered.sort((a, b) => {
|
|
2523
|
+
const tsDiff = new Date(a.ts).getTime() - new Date(b.ts).getTime();
|
|
2524
|
+
if (tsDiff !== 0) return tsDiff;
|
|
2525
|
+
return a.seq - b.seq;
|
|
2526
|
+
});
|
|
2527
|
+
if (options?.limit && options.limit > 0) filtered = filtered.slice(0, options.limit);
|
|
2528
|
+
return filtered;
|
|
2529
|
+
}
|
|
2530
|
+
function resetSeqCounter() {
|
|
2531
|
+
globalSeq = 0;
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
//#endregion
|
|
2535
|
+
//#region src/observability-routing.ts
|
|
2536
|
+
/**
|
|
2537
|
+
* Derive the owning persistence class from the trusted write context.
|
|
2538
|
+
* `agentRunOrigin` defaults to `interactive` when undefined.
|
|
2539
|
+
*
|
|
2540
|
+
* **An authenticated** caller (`callerAuthenticated: true`) ALWAYS routes to
|
|
2541
|
+
* `"user"` — their runs are owned by their persisted identity. An
|
|
2542
|
+
* unauthenticated caller — including an anonymous WS connection that the
|
|
2543
|
+
* Node daemon stamped with a synthetic guest DID — is treated as anonymous
|
|
2544
|
+
* and falls through to the `anonymousInteractivePersistence` policy so the
|
|
2545
|
+
* trace lands somewhere the observability explorer can actually scan
|
|
2546
|
+
* (`/blocklets/<id>/instance/logs/agent-runs/` or the host-inbox), rather
|
|
2547
|
+
* than the guest's per-DID user space which is by design private.
|
|
2548
|
+
*
|
|
2549
|
+
* `callerAuthenticated` defaults to `false` when `callerDid` is set but the
|
|
2550
|
+
* caller comes from a legacy path that doesn't carry an auth marker — that
|
|
2551
|
+
* was the pre-Phase 3c behavior, so legacy callers stay routed the same
|
|
2552
|
+
* way.
|
|
2553
|
+
*/
|
|
2554
|
+
function derivePersistenceClass(input) {
|
|
2555
|
+
if (input.callerDid && input.callerAuthenticated !== false) return "user";
|
|
2556
|
+
if (input.agentRunOrigin === "system") return "instance";
|
|
2557
|
+
if (input.anonymousInteractivePersistence === "instance") return "instance";
|
|
2558
|
+
return "ephemeral";
|
|
2559
|
+
}
|
|
2560
|
+
/** Build the relative-within-fragment jsonl path (`logs/agent-runs/<date>/<file>`). */
|
|
2561
|
+
function userFragmentRunPath(date, fileName) {
|
|
2562
|
+
return joinURL("/logs/agent-runs", date, fileName);
|
|
2563
|
+
}
|
|
2564
|
+
/**
|
|
2565
|
+
* Resolve the `TraceSink` for a run. Pure except for the injected
|
|
2566
|
+
* `spaceResolver` / `legacyProbe` async calls. Called once per run (the
|
|
2567
|
+
* resolved sink is reused for every emit).
|
|
2568
|
+
*/
|
|
2569
|
+
async function resolveTraceSink(input) {
|
|
2570
|
+
const pc = input.persistenceClass;
|
|
2571
|
+
if (pc === "ephemeral") return {
|
|
2572
|
+
kind: "none",
|
|
2573
|
+
persistenceClass: pc,
|
|
2574
|
+
reason: "ephemeral",
|
|
2575
|
+
dropped: false
|
|
2576
|
+
};
|
|
2577
|
+
if (pc === "user") {
|
|
2578
|
+
const missing = [];
|
|
2579
|
+
if (!input.callerDid) missing.push("callerDid");
|
|
2580
|
+
if (!input.instanceDid) missing.push("instanceDid");
|
|
2581
|
+
if (!input.spaceResolver) missing.push("spaceResolver");
|
|
2582
|
+
if (missing.length === 0) return {
|
|
2583
|
+
kind: "afs",
|
|
2584
|
+
persistenceClass: pc,
|
|
2585
|
+
afs: await (await input.spaceResolver.resolveUserSpace(input.callerDid)).getInstanceSpace({
|
|
2586
|
+
instanceDid: input.instanceDid,
|
|
2587
|
+
role: "user",
|
|
2588
|
+
accessMode: "readwrite"
|
|
2589
|
+
}),
|
|
2590
|
+
path: input.fragmentRunPath,
|
|
2591
|
+
reason: "user-direct-write"
|
|
2592
|
+
};
|
|
2593
|
+
const reason = `user-missing:${missing.join(",")} mode=${input.ownershipRoutingMode}`;
|
|
2594
|
+
if (input.ownershipRoutingMode === "strict-user" || input.ownershipRoutingMode === "strict-all") return {
|
|
2595
|
+
kind: "none",
|
|
2596
|
+
persistenceClass: pc,
|
|
2597
|
+
reason,
|
|
2598
|
+
dropped: true
|
|
2599
|
+
};
|
|
2600
|
+
return {
|
|
2601
|
+
kind: "legacy-current-afs",
|
|
2602
|
+
persistenceClass: pc,
|
|
2603
|
+
path: await input.legacyProbe(),
|
|
2604
|
+
reason
|
|
2605
|
+
};
|
|
2606
|
+
}
|
|
2607
|
+
if (input.ownershipRoutingMode === "strict-all" && !input.instanceDid) return {
|
|
2608
|
+
kind: "none",
|
|
2609
|
+
persistenceClass: pc,
|
|
2610
|
+
reason: `instance-missing:instanceDid mode=strict-all`,
|
|
2611
|
+
dropped: true
|
|
2612
|
+
};
|
|
2613
|
+
return {
|
|
2614
|
+
kind: "legacy-current-afs",
|
|
2615
|
+
persistenceClass: pc,
|
|
2616
|
+
path: await input.legacyProbe(),
|
|
2617
|
+
reason: "instance-legacy-probing"
|
|
2618
|
+
};
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
//#endregion
|
|
2622
|
+
//#region src/spawn-depth.ts
|
|
2623
|
+
const DEFAULT_MAX = 3;
|
|
2624
|
+
const HARD_CAP = 10;
|
|
2625
|
+
function checkSpawnDepth(chain, manifestMax) {
|
|
2626
|
+
if (!Array.isArray(chain)) throw new TypeError("chain must be an array");
|
|
2627
|
+
let max = DEFAULT_MAX;
|
|
2628
|
+
if (typeof manifestMax === "number" && manifestMax > 0) max = Math.min(Math.floor(manifestMax), HARD_CAP);
|
|
2629
|
+
const depth = chain.length;
|
|
2630
|
+
if (depth >= max) throw new Error(`MAX_SPAWN_DEPTH_EXCEEDED: depth ${depth} >= max ${max}`);
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
//#endregion
|
|
2634
|
+
//#region src/orchestration.ts
|
|
2635
|
+
/**
|
|
2636
|
+
* agent-run orchestration — the args→params→deps→runAgentLoop pipeline.
|
|
2637
|
+
*
|
|
2638
|
+
* Owns the execution flow that used to live as `executeAgentRunImpl` on
|
|
2639
|
+
* the AFSAsh class. Now exposed as a standalone function so AFSAgent
|
|
2640
|
+
* can call it directly without delegating to ash.
|
|
2641
|
+
*
|
|
2642
|
+
* What this layer does (between AFS exec args and `runAgentLoop`):
|
|
2643
|
+
*
|
|
2644
|
+
* 1. Strip the in-process `_*` args (scope_afs, on_tool_progress,
|
|
2645
|
+
* abort_signal, parent_session, chain) so cleanArgs is
|
|
2646
|
+
* serialization-safe.
|
|
2647
|
+
* 2. Validate task / model / tools shape.
|
|
2648
|
+
* 3. Resolve the model path (short name → /dev/ai/hubs/aignehub mount;
|
|
2649
|
+
* absolute path → normalized verbatim; chat-action leaf → kept as-is).
|
|
2650
|
+
* 4. Stat the model node to discover its context window (best-effort,
|
|
2651
|
+
* non-fatal).
|
|
2652
|
+
* 5. Append caller-provided structured `context` JSON to the task.
|
|
2653
|
+
* 6. Build the `AgentRunParams` + `AgentRunDeps` (callLLM, runAsh,
|
|
2654
|
+
* callAFS, history persistence) and invoke `runAgentLoop`.
|
|
2655
|
+
*
|
|
2656
|
+
* The `runAsh` dep delegates back to ash via AFS exec on the configured
|
|
2657
|
+
* mount path (default `/ash/.actions/run`) — agent-run does not own the
|
|
2658
|
+
* ash DSL execution. ash's run handler accepts `_return_written_data`
|
|
2659
|
+
* and `_skip_write_prefix` flags forwarded here so the orchestration
|
|
2660
|
+
* doesn't need to import AFSAsh internals.
|
|
2661
|
+
*/
|
|
2662
|
+
const log = makeNsLog("provider:agent");
|
|
2663
|
+
function normalizeAbsoluteAfsPath(path) {
|
|
2664
|
+
if (!path.startsWith("/")) return path;
|
|
2665
|
+
const segments = [];
|
|
2666
|
+
for (const segment of path.split("/")) {
|
|
2667
|
+
if (!segment || segment === ".") continue;
|
|
2668
|
+
if (segment === "..") segments.pop();
|
|
2669
|
+
else segments.push(segment);
|
|
2670
|
+
}
|
|
2671
|
+
return `/${segments.join("/")}`;
|
|
2672
|
+
}
|
|
2673
|
+
function hasUnsafePathSegments(path) {
|
|
2674
|
+
return path.split("/").some((segment) => segment === "." || segment === "..");
|
|
2675
|
+
}
|
|
2676
|
+
/**
|
|
2677
|
+
* Resolve a user-provided model identifier into an absolute AFS path.
|
|
2678
|
+
*
|
|
2679
|
+
* - `/foo/bar` → verbatim (after `..` / `.` normalization, rejected if
|
|
2680
|
+
* any path segment is `.` or `..`).
|
|
2681
|
+
* - `gpt-4o` → `/dev/ai/hubs/aignehub/models/gpt-4o`.
|
|
2682
|
+
*
|
|
2683
|
+
* The returned path may either point at a model NODE (e.g.
|
|
2684
|
+
* `/dev/ai/hubs/aignehub/models/gpt-4o`) or directly at a chat-action
|
|
2685
|
+
* leaf (e.g. `/dev/ai/.actions/chat`). The caller distinguishes via
|
|
2686
|
+
* regex on `/.actions/(chat|embed|image|video)$`.
|
|
2687
|
+
*/
|
|
2688
|
+
function resolveAgentModelPath(model) {
|
|
2689
|
+
if (!model) throw new Error("agent-run requires 'model' parameter as string");
|
|
2690
|
+
if (model.startsWith("/")) {
|
|
2691
|
+
if (hasUnsafePathSegments(model)) throw new Error("agent-run model path contains unsafe path segments");
|
|
2692
|
+
return normalizeAbsoluteAfsPath(model);
|
|
2693
|
+
}
|
|
2694
|
+
if (model.includes("//") || hasUnsafePathSegments(model)) throw new Error("agent-run model path contains unsafe path segments");
|
|
2695
|
+
return `/dev/ai/hubs/aignehub/models/${model}`;
|
|
2696
|
+
}
|
|
2697
|
+
/**
|
|
2698
|
+
* Normalize tool calls to AIGNE format: { id, type: "function", function: { name, arguments } }.
|
|
2699
|
+
*
|
|
2700
|
+
* Accepts Vercel AI SDK ({ toolCallId, toolName, args }),
|
|
2701
|
+
* OpenAI ({ id, type, function: { name, arguments: string } }),
|
|
2702
|
+
* or already-normalized AIGNE format.
|
|
2703
|
+
*/
|
|
2704
|
+
function normalizeToolCallsToAIGNE(toolCalls) {
|
|
2705
|
+
return toolCalls.map((tc) => {
|
|
2706
|
+
if (tc.toolName && !tc.function) return {
|
|
2707
|
+
id: tc.toolCallId ?? tc.id,
|
|
2708
|
+
type: "function",
|
|
2709
|
+
function: {
|
|
2710
|
+
name: tc.toolName,
|
|
2711
|
+
arguments: tc.args ?? {}
|
|
2712
|
+
}
|
|
2713
|
+
};
|
|
2714
|
+
const fn = tc.function;
|
|
2715
|
+
if (fn && typeof fn.arguments === "string") try {
|
|
2716
|
+
return {
|
|
2717
|
+
...tc,
|
|
2718
|
+
function: {
|
|
2719
|
+
...fn,
|
|
2720
|
+
arguments: JSON.parse(fn.arguments)
|
|
2721
|
+
}
|
|
2722
|
+
};
|
|
2723
|
+
} catch {
|
|
2724
|
+
return tc;
|
|
2725
|
+
}
|
|
2726
|
+
return tc;
|
|
2727
|
+
});
|
|
2728
|
+
}
|
|
2729
|
+
/**
|
|
2730
|
+
* The full args → result pipeline. Same contract as the legacy
|
|
2731
|
+
* `executeAgentRunImpl` method that used to live on AFSAsh, but as a
|
|
2732
|
+
* standalone function with explicit deps.
|
|
2733
|
+
*
|
|
2734
|
+
* Does NOT enqueue on a session queue — that's the caller's job (see
|
|
2735
|
+
* AFSAgent). Throws on validation failures (bad task / bad model)
|
|
2736
|
+
* and returns an `AFSExecResult` envelope on runtime failures.
|
|
2737
|
+
*/
|
|
2738
|
+
async function executeAgentRun(args, options = {}) {
|
|
2739
|
+
const ashMountPath = options.ashMountPath ?? "/ash";
|
|
2740
|
+
const runtimeAFSOverride = args._scope_afs;
|
|
2741
|
+
const onToolProgress = args._on_tool_progress;
|
|
2742
|
+
const abortSignal = args._abort_signal;
|
|
2743
|
+
const parentSession = typeof args._parent_session === "string" ? args._parent_session : void 0;
|
|
2744
|
+
const chain = Array.isArray(args._chain) ? args._chain.filter((value) => typeof value === "string") : void 0;
|
|
2745
|
+
const cleanArgs = { ...args };
|
|
2746
|
+
delete cleanArgs._scope_afs;
|
|
2747
|
+
delete cleanArgs._on_tool_progress;
|
|
2748
|
+
delete cleanArgs._abort_signal;
|
|
2749
|
+
delete cleanArgs._parent_session;
|
|
2750
|
+
delete cleanArgs._chain;
|
|
2751
|
+
delete cleanArgs._ui_session;
|
|
2752
|
+
const writePath = cleanArgs.write_to_path;
|
|
2753
|
+
delete cleanArgs.write_to_path;
|
|
2754
|
+
const context = cleanArgs.context;
|
|
2755
|
+
let task = cleanArgs.task;
|
|
2756
|
+
if (typeof task !== "string" || !task) if (context && typeof context === "object") task = JSON.stringify(context);
|
|
2757
|
+
else if (typeof context === "string" && context) task = context;
|
|
2758
|
+
else throw new Error("agent-run requires 'task' parameter as string");
|
|
2759
|
+
const model = cleanArgs.model;
|
|
2760
|
+
if (typeof model !== "string" || !model) throw new Error("agent-run requires 'model' parameter as string");
|
|
2761
|
+
const rawTools = cleanArgs.tools;
|
|
2762
|
+
const tools = rawTools === "none" || rawTools === void 0 ? [] : rawTools;
|
|
2763
|
+
if (!Array.isArray(tools)) throw new Error("agent-run 'tools' must be an array or 'none'");
|
|
2764
|
+
const globalAFS = options.globalAfs;
|
|
2765
|
+
if (!globalAFS && !runtimeAFSOverride) throw new Error("agent-run requires AFS root to be mounted");
|
|
2766
|
+
const afs = runtimeAFSOverride ?? globalAFS;
|
|
2767
|
+
const skillPaths = cleanArgs.skill_paths;
|
|
2768
|
+
const resolvedModel = resolveAgentModelPath(model);
|
|
2769
|
+
const isDirectActionPath = /\/\.actions\/(chat|embed|image|video)$/.test(resolvedModel);
|
|
2770
|
+
let contextWindow;
|
|
2771
|
+
try {
|
|
2772
|
+
if (afs.stat) {
|
|
2773
|
+
const statPath = isDirectActionPath ? resolvedModel.replace(/\/\.actions\/\w+$/, "") : resolvedModel;
|
|
2774
|
+
if (statPath && statPath !== "/dev/ai") {
|
|
2775
|
+
const cw = (await afs.stat(statPath)).data?.meta?.contextWindow;
|
|
2776
|
+
if (typeof cw === "number" && cw > 0) contextWindow = cw;
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
} catch {}
|
|
2780
|
+
let enrichedTask = task;
|
|
2781
|
+
if (cleanArgs.context != null) if (typeof cleanArgs.context === "string") enrichedTask += `\n\nMessage metadata:\n${cleanArgs.context}`;
|
|
2782
|
+
else {
|
|
2783
|
+
const metaStr = JSON.stringify(cleanArgs.context, null, 2);
|
|
2784
|
+
enrichedTask += `\n\nMessage metadata:\n\`\`\`json\n${metaStr}\n\`\`\``;
|
|
2785
|
+
}
|
|
2786
|
+
const systemPrompt = cleanArgs.system;
|
|
2787
|
+
const spawnChain = chain ?? (typeof cleanArgs.session === "string" ? [cleanArgs.session] : []);
|
|
2788
|
+
checkSpawnDepth(spawnChain);
|
|
2789
|
+
const userBudget = cleanArgs.budget ?? {};
|
|
2790
|
+
const params = {
|
|
2791
|
+
task: enrichedTask,
|
|
2792
|
+
model,
|
|
2793
|
+
tools,
|
|
2794
|
+
budget: {
|
|
2795
|
+
max_rounds: userBudget.max_rounds,
|
|
2796
|
+
actions_per_round: userBudget.actions_per_round,
|
|
2797
|
+
total_tokens: userBudget.total_tokens,
|
|
2798
|
+
context_window: contextWindow,
|
|
2799
|
+
max_retries: userBudget.max_retries
|
|
2800
|
+
},
|
|
2801
|
+
system: systemPrompt,
|
|
2802
|
+
session: cleanArgs.session,
|
|
2803
|
+
skillPaths,
|
|
2804
|
+
toolChoice: cleanArgs.tool_choice,
|
|
2805
|
+
modelArgs: cleanArgs.model_args !== void 0 ? cleanArgs.model_args : cleanArgs.modelArgs,
|
|
2806
|
+
onProgress: cleanArgs.on_progress,
|
|
2807
|
+
onToolProgress,
|
|
2808
|
+
responseSchema: cleanArgs.response_schema,
|
|
2809
|
+
execContext: cleanArgs.exec_context,
|
|
2810
|
+
parentSession,
|
|
2811
|
+
chain: spawnChain,
|
|
2812
|
+
memory: cleanArgs.memory,
|
|
2813
|
+
index: cleanArgs.index,
|
|
2814
|
+
skipAfsHint: cleanArgs.skip_afs_hint === true || cleanArgs.skipAfsHint === true || void 0,
|
|
2815
|
+
abortSignal,
|
|
2816
|
+
parentProcId: cleanArgs.parentProcId
|
|
2817
|
+
};
|
|
2818
|
+
let trustedAfsContext;
|
|
2819
|
+
/** Merge the trusted context into an AFS options object (no-op when empty). */
|
|
2820
|
+
const withCtx = (opts) => trustedAfsContext ? {
|
|
2821
|
+
...opts,
|
|
2822
|
+
context: trustedAfsContext
|
|
2823
|
+
} : opts;
|
|
2824
|
+
const deps = {
|
|
2825
|
+
callLLM: async (llmArgs) => {
|
|
2826
|
+
if (!afs.exec) return {
|
|
2827
|
+
success: false,
|
|
2828
|
+
data: { error: "exec not supported" }
|
|
2829
|
+
};
|
|
2830
|
+
const modelPath = isDirectActionPath ? resolvedModel : `${resolvedModel}/.actions/chat`;
|
|
2831
|
+
const onChunk = llmArgs._onChunk;
|
|
2832
|
+
const translated = { ...llmArgs };
|
|
2833
|
+
delete translated._onChunk;
|
|
2834
|
+
if (Array.isArray(translated.messages)) translated.messages = translated.messages.map((msg) => {
|
|
2835
|
+
const m = { ...msg };
|
|
2836
|
+
if (m.role === "assistant") m.role = "agent";
|
|
2837
|
+
if (m.role === "tool" && m.tool_call_id && !m.toolCallId) {
|
|
2838
|
+
m.toolCallId = m.tool_call_id;
|
|
2839
|
+
delete m.tool_call_id;
|
|
2840
|
+
}
|
|
2841
|
+
const rawTCs = m.toolCalls ?? m.tool_calls;
|
|
2842
|
+
if (Array.isArray(rawTCs)) {
|
|
2843
|
+
m.toolCalls = normalizeToolCallsToAIGNE(rawTCs);
|
|
2844
|
+
delete m.tool_calls;
|
|
2845
|
+
}
|
|
2846
|
+
return m;
|
|
2847
|
+
});
|
|
2848
|
+
try {
|
|
2849
|
+
const raw = await afs.exec(modelPath, translated, withCtx(onChunk ? { onChunk } : {}));
|
|
2850
|
+
if (raw.success && raw.data) {
|
|
2851
|
+
const inner = raw.data;
|
|
2852
|
+
const result$1 = inner.result;
|
|
2853
|
+
const src = result$1 ?? inner;
|
|
2854
|
+
const usage = src.usage ?? result$1;
|
|
2855
|
+
const toolCalls = src.toolCalls;
|
|
2856
|
+
raw.data = {
|
|
2857
|
+
text: src.text,
|
|
2858
|
+
json: src.json,
|
|
2859
|
+
thoughts: src.thoughts,
|
|
2860
|
+
toolCalls: toolCalls ? normalizeToolCallsToAIGNE(toolCalls) : void 0,
|
|
2861
|
+
inputTokens: usage?.inputTokens ?? inner.inputTokens,
|
|
2862
|
+
outputTokens: usage?.outputTokens ?? inner.outputTokens,
|
|
2863
|
+
model: src.model,
|
|
2864
|
+
usage: src.usage ?? usage,
|
|
2865
|
+
finishReason: src.finishReason ?? src.finish_reason ?? src.stopReason ?? src.stop_reason,
|
|
2866
|
+
reason: src.reason ?? inner.reason
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
return raw;
|
|
2870
|
+
} catch (err) {
|
|
2871
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2872
|
+
if (errMsg.includes("No module found")) return {
|
|
2873
|
+
success: false,
|
|
2874
|
+
data: { error: `Model provider not mounted: ${resolvedModel} — check if the provider started correctly (afs service restart)` }
|
|
2875
|
+
};
|
|
2876
|
+
return {
|
|
2877
|
+
success: false,
|
|
2878
|
+
data: { error: `LLM exec failed: ${errMsg}` }
|
|
2879
|
+
};
|
|
2880
|
+
}
|
|
2881
|
+
},
|
|
2882
|
+
runAsh: async (source, runArgs) => {
|
|
2883
|
+
if (!afs.exec) return {
|
|
2884
|
+
success: false,
|
|
2885
|
+
data: { error: "exec not supported" }
|
|
2886
|
+
};
|
|
2887
|
+
return afs.exec(`${ashMountPath}/.actions/run`, {
|
|
2888
|
+
source,
|
|
2889
|
+
...runArgs,
|
|
2890
|
+
_scope_afs: afs,
|
|
2891
|
+
_return_written_data: true,
|
|
2892
|
+
_skip_write_prefix: "/.results/"
|
|
2893
|
+
}, withCtx({}));
|
|
2894
|
+
},
|
|
2895
|
+
callAFS: async (op, path, opArgs) => {
|
|
2896
|
+
switch (op) {
|
|
2897
|
+
case "read": {
|
|
2898
|
+
const readEntries = opArgs?.entries;
|
|
2899
|
+
if (readEntries && readEntries.length > 0) {
|
|
2900
|
+
if (readEntries.length > 16) return {
|
|
2901
|
+
success: false,
|
|
2902
|
+
data: { error: `Too many entries (${readEntries.length} > 16) — split into chunks of 16` }
|
|
2903
|
+
};
|
|
2904
|
+
const paths = readEntries.map((e) => e.path);
|
|
2905
|
+
const batchEntries = paths.map((p) => ({ path: p }));
|
|
2906
|
+
const afsWithBatch = afs;
|
|
2907
|
+
if (typeof afsWithBatch.batchRead === "function") return {
|
|
2908
|
+
success: true,
|
|
2909
|
+
data: { batch: await afsWithBatch.batchRead(batchEntries, withCtx({})) }
|
|
2910
|
+
};
|
|
2911
|
+
const results = [];
|
|
2912
|
+
let succeeded = 0;
|
|
2913
|
+
let failed = 0;
|
|
2914
|
+
for (const p of paths) try {
|
|
2915
|
+
const r = await afs.read(p, withCtx({}));
|
|
2916
|
+
results.push({
|
|
2917
|
+
path: p,
|
|
2918
|
+
success: true,
|
|
2919
|
+
data: r.data
|
|
2920
|
+
});
|
|
2921
|
+
succeeded++;
|
|
2922
|
+
} catch (err) {
|
|
2923
|
+
results.push({
|
|
2924
|
+
path: p,
|
|
2925
|
+
success: false,
|
|
2926
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2927
|
+
});
|
|
2928
|
+
failed++;
|
|
2929
|
+
}
|
|
2930
|
+
return {
|
|
2931
|
+
success: true,
|
|
2932
|
+
data: { batch: {
|
|
2933
|
+
results,
|
|
2934
|
+
succeeded,
|
|
2935
|
+
failed
|
|
2936
|
+
} }
|
|
2937
|
+
};
|
|
2938
|
+
}
|
|
2939
|
+
return {
|
|
2940
|
+
success: true,
|
|
2941
|
+
data: (await afs.read(path, withCtx(opArgs ?? {}))).data
|
|
2942
|
+
};
|
|
2943
|
+
}
|
|
2944
|
+
case "list": return {
|
|
2945
|
+
success: true,
|
|
2946
|
+
data: (await afs.list(path, withCtx(opArgs ?? {}))).data
|
|
2947
|
+
};
|
|
2948
|
+
case "search":
|
|
2949
|
+
if (!afs.search) return {
|
|
2950
|
+
success: false,
|
|
2951
|
+
data: { error: "search not supported" }
|
|
2952
|
+
};
|
|
2953
|
+
return {
|
|
2954
|
+
success: true,
|
|
2955
|
+
data: { results: (await afs.search(path, opArgs?.query ?? "", withCtx(opArgs ?? {}))).data }
|
|
2956
|
+
};
|
|
2957
|
+
case "write": {
|
|
2958
|
+
if (!afs.write) return {
|
|
2959
|
+
success: false,
|
|
2960
|
+
data: { error: "write not supported" }
|
|
2961
|
+
};
|
|
2962
|
+
const entries = opArgs?.entries;
|
|
2963
|
+
if (entries && entries.length > 0) {
|
|
2964
|
+
const batchEntries = entries.map((e) => ({
|
|
2965
|
+
path: e.path,
|
|
2966
|
+
content: e.content !== void 0 ? { content: e.content } : void 0,
|
|
2967
|
+
mode: e.mode
|
|
2968
|
+
}));
|
|
2969
|
+
const afsWithBatch = afs;
|
|
2970
|
+
if (typeof afsWithBatch.batchWrite === "function") return {
|
|
2971
|
+
success: true,
|
|
2972
|
+
data: { batch: await afsWithBatch.batchWrite(batchEntries, withCtx({})) }
|
|
2973
|
+
};
|
|
2974
|
+
const results = [];
|
|
2975
|
+
let succeeded = 0;
|
|
2976
|
+
let failed = 0;
|
|
2977
|
+
for (const entry of batchEntries) try {
|
|
2978
|
+
const r = await afs.write(entry.path, entry.content ?? {}, withCtx(entry.mode !== void 0 ? { mode: entry.mode } : {}));
|
|
2979
|
+
results.push({
|
|
2980
|
+
path: entry.path,
|
|
2981
|
+
success: true,
|
|
2982
|
+
data: r.data
|
|
2983
|
+
});
|
|
2984
|
+
succeeded++;
|
|
2985
|
+
} catch (err) {
|
|
2986
|
+
results.push({
|
|
2987
|
+
path: entry.path,
|
|
2988
|
+
success: false,
|
|
2989
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2990
|
+
});
|
|
2991
|
+
failed++;
|
|
2992
|
+
}
|
|
2993
|
+
return {
|
|
2994
|
+
success: true,
|
|
2995
|
+
data: { batch: {
|
|
2996
|
+
results,
|
|
2997
|
+
succeeded,
|
|
2998
|
+
failed
|
|
2999
|
+
} }
|
|
3000
|
+
};
|
|
3001
|
+
}
|
|
3002
|
+
const writePayload = {};
|
|
3003
|
+
if (opArgs?.content !== void 0) writePayload.content = opArgs.content;
|
|
3004
|
+
const writeOpts = {};
|
|
3005
|
+
if (opArgs?.mode !== void 0) writeOpts.mode = opArgs.mode;
|
|
3006
|
+
if (opArgs?.field !== void 0) writeOpts.field = opArgs.field;
|
|
3007
|
+
if (opArgs?.by !== void 0) writeOpts.by = opArgs.by;
|
|
3008
|
+
if (opArgs?.ifMatch !== void 0) writeOpts.ifMatch = opArgs.ifMatch;
|
|
3009
|
+
return {
|
|
3010
|
+
success: true,
|
|
3011
|
+
data: { written: (await afs.write(path, writePayload, withCtx(writeOpts))).data }
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
case "delete": {
|
|
3015
|
+
if (!afs.delete) return {
|
|
3016
|
+
success: false,
|
|
3017
|
+
data: { error: "delete not supported" }
|
|
3018
|
+
};
|
|
3019
|
+
const deleteOpts = {};
|
|
3020
|
+
if (opArgs?.where !== void 0) {
|
|
3021
|
+
let where = opArgs.where;
|
|
3022
|
+
if (typeof where === "string") try {
|
|
3023
|
+
where = JSON.parse(where);
|
|
3024
|
+
} catch (e) {
|
|
3025
|
+
return {
|
|
3026
|
+
success: false,
|
|
3027
|
+
data: { error: `delete: 'where' is not valid JSON: ${e instanceof Error ? e.message : String(e)}` }
|
|
3028
|
+
};
|
|
3029
|
+
}
|
|
3030
|
+
deleteOpts.where = where;
|
|
3031
|
+
}
|
|
3032
|
+
if (typeof opArgs?.maxItems === "number") deleteOpts.maxItems = opArgs.maxItems;
|
|
3033
|
+
if (opArgs?.ifMatch !== void 0) deleteOpts.ifMatch = opArgs.ifMatch;
|
|
3034
|
+
return {
|
|
3035
|
+
success: true,
|
|
3036
|
+
data: { deleted: await afs.delete(path, withCtx(deleteOpts)) }
|
|
3037
|
+
};
|
|
3038
|
+
}
|
|
3039
|
+
case "stat":
|
|
3040
|
+
if (!afs.stat) return {
|
|
3041
|
+
success: false,
|
|
3042
|
+
data: { error: "stat not supported" }
|
|
3043
|
+
};
|
|
3044
|
+
return {
|
|
3045
|
+
success: true,
|
|
3046
|
+
data: { stat: (await afs.stat(path, withCtx({}))).data }
|
|
3047
|
+
};
|
|
3048
|
+
case "explain":
|
|
3049
|
+
if (!afs.explain) return {
|
|
3050
|
+
success: false,
|
|
3051
|
+
data: { error: "explain not supported" }
|
|
3052
|
+
};
|
|
3053
|
+
return {
|
|
3054
|
+
success: true,
|
|
3055
|
+
data: { content: (await afs.explain(path, withCtx({}))).content }
|
|
3056
|
+
};
|
|
3057
|
+
case "exec": {
|
|
3058
|
+
if (!afs.exec) return {
|
|
3059
|
+
success: false,
|
|
3060
|
+
data: { error: "exec not supported" }
|
|
3061
|
+
};
|
|
3062
|
+
const execArgs = opArgs?.args && typeof opArgs.args === "object" && !Array.isArray(opArgs.args) ? opArgs.args : opArgs ?? {};
|
|
3063
|
+
if (path.endsWith("/ash/.actions/run") || path.endsWith("/ash/.actions/run-job") || path.endsWith("/dev/agent/.actions/run")) execArgs._scope_afs = afs;
|
|
3064
|
+
try {
|
|
3065
|
+
return await afs.exec(path, execArgs, withCtx({}));
|
|
3066
|
+
} catch (execErr) {
|
|
3067
|
+
if (path.endsWith(".ash") && afs.read) {
|
|
3068
|
+
const readResult = await afs.read(path, withCtx({}));
|
|
3069
|
+
const source = String(readResult.data?.content ?? "");
|
|
3070
|
+
if (source.trim()) return afs.exec(`${ashMountPath}/.actions/run`, {
|
|
3071
|
+
source,
|
|
3072
|
+
...execArgs
|
|
3073
|
+
}, withCtx({}));
|
|
3074
|
+
}
|
|
3075
|
+
throw execErr;
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
default: return {
|
|
3079
|
+
success: false,
|
|
3080
|
+
data: { error: `Unsupported direct op: ${op}` }
|
|
3081
|
+
};
|
|
3082
|
+
}
|
|
3083
|
+
},
|
|
3084
|
+
loadHistory: async (sessionPath) => {
|
|
3085
|
+
const historyPath = `${sessionPath}/history.jsonl`;
|
|
3086
|
+
try {
|
|
3087
|
+
const result$1 = await afs.read(historyPath, withCtx({}));
|
|
3088
|
+
const content = String(result$1.data?.content ?? "");
|
|
3089
|
+
if (!content.trim()) return [];
|
|
3090
|
+
return content.trim().split("\n").flatMap((line) => {
|
|
3091
|
+
try {
|
|
3092
|
+
return [JSON.parse(line)];
|
|
3093
|
+
} catch {
|
|
3094
|
+
return [];
|
|
3095
|
+
}
|
|
3096
|
+
});
|
|
3097
|
+
} catch {
|
|
3098
|
+
return [];
|
|
3099
|
+
}
|
|
3100
|
+
},
|
|
3101
|
+
appendHistory: async (sessionPath, messages) => {
|
|
3102
|
+
const historyPath = `${sessionPath}/history.jsonl`;
|
|
3103
|
+
const newLines = `${messages.map((m) => JSON.stringify(m)).join("\n")}\n`;
|
|
3104
|
+
await afs.write(historyPath, { content: newLines }, withCtx({ mode: "append" }));
|
|
3105
|
+
},
|
|
3106
|
+
rewriteHistory: async (sessionPath, messages) => {
|
|
3107
|
+
const historyPath = `${sessionPath}/history.jsonl`;
|
|
3108
|
+
const content = `${messages.map((m) => JSON.stringify(m)).join("\n")}\n`;
|
|
3109
|
+
await afs.write(historyPath, { content }, withCtx({}));
|
|
3110
|
+
},
|
|
3111
|
+
archiveHistory: async (sessionPath, messages) => {
|
|
3112
|
+
const archivePath = `${sessionPath}/history-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.jsonl`;
|
|
3113
|
+
const content = `${messages.map((m) => JSON.stringify(m)).join("\n")}\n`;
|
|
3114
|
+
await afs.write(archivePath, { content }, withCtx({}));
|
|
3115
|
+
}
|
|
3116
|
+
};
|
|
3117
|
+
const runId = randomUUID();
|
|
3118
|
+
const shortId = runId.replace(/-/g, "").slice(0, 8);
|
|
3119
|
+
const startedAt = Date.now();
|
|
3120
|
+
const iso = new Date(startedAt).toISOString();
|
|
3121
|
+
const date = iso.slice(0, 10);
|
|
3122
|
+
const time = iso.slice(11, 19).replace(/:/g, "");
|
|
3123
|
+
const callerContextResolved = options.callerContext ?? {};
|
|
3124
|
+
const rawBlockletId = typeof callerContextResolved.blockletId === "string" ? callerContextResolved.blockletId : null;
|
|
3125
|
+
const blockletId = rawBlockletId !== null && rawBlockletId.length > 0 && !/[\\/]/.test(rawBlockletId) && !rawBlockletId.split("/").includes("..") && rawBlockletId !== "." && rawBlockletId !== ".." ? rawBlockletId : null;
|
|
3126
|
+
const runFileName = `${time}-${shortId}.jsonl`;
|
|
3127
|
+
const probeLegacyRunPath = async () => {
|
|
3128
|
+
const userInstanceRunPath = joinURL("/instance/logs/agent-runs", date, runFileName);
|
|
3129
|
+
const blockletRunPath = blockletId ? joinURL("/blocklets", blockletId, "instance/logs/agent-runs", date, runFileName) : null;
|
|
3130
|
+
const hostInboxRunPath = joinURL("/dev/observability/.host-inbox", date, runFileName);
|
|
3131
|
+
let runPath = userInstanceRunPath;
|
|
3132
|
+
if (blockletRunPath && blockletId) try {
|
|
3133
|
+
if ((await deps.callAFS("stat", joinURL("/blocklets", blockletId, "instance")))?.success) runPath = blockletRunPath;
|
|
3134
|
+
} catch {}
|
|
3135
|
+
if (runPath === userInstanceRunPath) try {
|
|
3136
|
+
if ((await deps.callAFS("stat", "/dev/observability/.host-inbox"))?.success) runPath = hostInboxRunPath;
|
|
3137
|
+
} catch {}
|
|
3138
|
+
return runPath;
|
|
3139
|
+
};
|
|
3140
|
+
const callerInfo = callerContextResolved.caller;
|
|
3141
|
+
const callerDid = (callerInfo && typeof callerInfo.did === "string" ? callerInfo.did : null) ?? (typeof callerContextResolved.userId === "string" ? callerContextResolved.userId : null);
|
|
3142
|
+
const callerRole = callerInfo?.role ?? callerInfo?.roles?.[0];
|
|
3143
|
+
const callerAuthenticated = Boolean(callerDid) && callerRole !== "guest";
|
|
3144
|
+
const instanceDid = typeof callerContextResolved.instanceDid === "string" ? callerContextResolved.instanceDid : null;
|
|
3145
|
+
const rawOrigin = callerContextResolved.agentRunOrigin;
|
|
3146
|
+
const agentRunOrigin = rawOrigin === "system" ? "system" : rawOrigin === "interactive" ? "interactive" : void 0;
|
|
3147
|
+
const obsRuntime = options.observabilityRuntime;
|
|
3148
|
+
const ownershipRoutingMode = obsRuntime?.ownershipRoutingMode ?? "observe-only";
|
|
3149
|
+
let sink;
|
|
3150
|
+
if (!obsRuntime) sink = {
|
|
3151
|
+
kind: "legacy-current-afs",
|
|
3152
|
+
persistenceClass: "instance",
|
|
3153
|
+
path: await probeLegacyRunPath(),
|
|
3154
|
+
reason: "no-observability-runtime"
|
|
3155
|
+
};
|
|
3156
|
+
else sink = await resolveTraceSink({
|
|
3157
|
+
persistenceClass: derivePersistenceClass({
|
|
3158
|
+
callerDid,
|
|
3159
|
+
callerAuthenticated,
|
|
3160
|
+
agentRunOrigin,
|
|
3161
|
+
anonymousInteractivePersistence: obsRuntime.anonymousInteractivePersistence
|
|
3162
|
+
}),
|
|
3163
|
+
callerDid,
|
|
3164
|
+
instanceDid,
|
|
3165
|
+
spaceResolver: obsRuntime.spaceResolver,
|
|
3166
|
+
ownershipRoutingMode,
|
|
3167
|
+
fragmentRunPath: userFragmentRunPath(date, runFileName),
|
|
3168
|
+
legacyProbe: probeLegacyRunPath
|
|
3169
|
+
});
|
|
3170
|
+
if (sink.kind === "none" && sink.dropped) log.warn(`[observability] drop mode=${ownershipRoutingMode} persistenceClass=${sink.persistenceClass} reason=${sink.reason}`);
|
|
3171
|
+
else if (sink.reason.includes("missing")) log.warn(`[observability] observe mode=${ownershipRoutingMode} persistenceClass=${sink.persistenceClass} reason=${sink.reason}`);
|
|
3172
|
+
const userDid = callerDid ?? (typeof callerContextResolved.userId === "string" ? callerContextResolved.userId : null);
|
|
3173
|
+
const sessionIdFromCtx = (typeof callerContextResolved.sessionId === "string" ? callerContextResolved.sessionId : null) ?? (typeof args._ui_session === "string" ? args._ui_session : null);
|
|
3174
|
+
{
|
|
3175
|
+
const ctx = {};
|
|
3176
|
+
if (callerInfo && typeof callerInfo.did === "string") ctx.caller = callerInfo;
|
|
3177
|
+
if (callerDid) ctx.userId = callerDid;
|
|
3178
|
+
if (instanceDid) ctx.instanceDid = instanceDid;
|
|
3179
|
+
if (agentRunOrigin) ctx.agentRunOrigin = agentRunOrigin;
|
|
3180
|
+
if (sessionIdFromCtx) ctx.sessionId = sessionIdFromCtx;
|
|
3181
|
+
if (rawBlockletId) ctx.blockletId = rawBlockletId;
|
|
3182
|
+
if (Object.keys(ctx).length > 0) trustedAfsContext = ctx;
|
|
3183
|
+
}
|
|
3184
|
+
const inputSnapshot = {
|
|
3185
|
+
task: params.task,
|
|
3186
|
+
model: params.model,
|
|
3187
|
+
tools: (params.tools ?? []).map((t) => ({
|
|
3188
|
+
path: t.path,
|
|
3189
|
+
ops: t.ops
|
|
3190
|
+
})),
|
|
3191
|
+
budget: userBudget,
|
|
3192
|
+
systemPrompt: params.system
|
|
3193
|
+
};
|
|
3194
|
+
let seq = 0;
|
|
3195
|
+
const nextSeq = () => seq++;
|
|
3196
|
+
const writeTrace = async (content) => {
|
|
3197
|
+
if (sink.kind === "none") return;
|
|
3198
|
+
if (sink.kind === "afs") {
|
|
3199
|
+
try {
|
|
3200
|
+
await sink.afs.write(sink.path, { content }, { mode: "replace" });
|
|
3201
|
+
} catch (err) {
|
|
3202
|
+
log.warn(`[observability] user-space emit threw: ${sink.path} — ${err instanceof Error ? err.message : String(err)}`);
|
|
3203
|
+
}
|
|
3204
|
+
return;
|
|
3205
|
+
}
|
|
3206
|
+
try {
|
|
3207
|
+
const result$1 = await deps.callAFS("write", sink.path, {
|
|
3208
|
+
content,
|
|
3209
|
+
mode: "replace"
|
|
3210
|
+
});
|
|
3211
|
+
if (!result$1.success) {
|
|
3212
|
+
const err = result$1.error?.message;
|
|
3213
|
+
log.warn(`[observability] emit failed: ${sink.path} — ${err ?? "unknown"}`);
|
|
3214
|
+
}
|
|
3215
|
+
} catch (err) {
|
|
3216
|
+
log.warn(`[observability] emit threw: ${sink.path} — ${err instanceof Error ? err.message : String(err)}`);
|
|
3217
|
+
}
|
|
3218
|
+
};
|
|
3219
|
+
const emittedLines = [];
|
|
3220
|
+
let emitChain = Promise.resolve();
|
|
3221
|
+
const emit = (event) => {
|
|
3222
|
+
const line = safeStringifyLine(event);
|
|
3223
|
+
emittedLines.push(line);
|
|
3224
|
+
const next = emitChain.then(() => writeTrace(`${emittedLines.join("\n")}\n`));
|
|
3225
|
+
emitChain = next.catch(() => {});
|
|
3226
|
+
return next;
|
|
3227
|
+
};
|
|
3228
|
+
deps.emit = emit;
|
|
3229
|
+
deps.nextSeq = nextSeq;
|
|
3230
|
+
await emit({
|
|
3231
|
+
type: "run.start",
|
|
3232
|
+
seq: nextSeq(),
|
|
3233
|
+
ts: startedAt,
|
|
3234
|
+
runId,
|
|
3235
|
+
blockletId: rawBlockletId,
|
|
3236
|
+
userDid,
|
|
3237
|
+
sessionId: sessionIdFromCtx,
|
|
3238
|
+
input: inputSnapshot,
|
|
3239
|
+
ownership: {
|
|
3240
|
+
persistenceClass: sink.persistenceClass,
|
|
3241
|
+
agentRunOrigin: agentRunOrigin ?? "interactive",
|
|
3242
|
+
ownershipRoutingMode,
|
|
3243
|
+
instanceDid,
|
|
3244
|
+
sinkKind: sink.kind,
|
|
3245
|
+
sinkReason: sink.reason
|
|
3246
|
+
}
|
|
3247
|
+
});
|
|
3248
|
+
const emitRunEnd = async (status, resultData, resultError, totalsSnap) => {
|
|
3249
|
+
const endedAt = Date.now();
|
|
3250
|
+
await emit({
|
|
3251
|
+
type: "run.end",
|
|
3252
|
+
seq: nextSeq(),
|
|
3253
|
+
ts: endedAt,
|
|
3254
|
+
runId,
|
|
3255
|
+
durationMs: endedAt - startedAt,
|
|
3256
|
+
status,
|
|
3257
|
+
result: {
|
|
3258
|
+
data: resultData,
|
|
3259
|
+
errorMessage: resultError
|
|
3260
|
+
},
|
|
3261
|
+
totals: totalsSnap ?? {
|
|
3262
|
+
rounds: 0,
|
|
3263
|
+
totalTokensIn: 0,
|
|
3264
|
+
totalTokensOut: 0,
|
|
3265
|
+
totalCost: 0,
|
|
3266
|
+
modelsUsed: [],
|
|
3267
|
+
hubsUsed: [],
|
|
3268
|
+
toolsUsed: []
|
|
3269
|
+
}
|
|
3270
|
+
});
|
|
3271
|
+
};
|
|
3272
|
+
let result;
|
|
3273
|
+
try {
|
|
3274
|
+
result = await runAgentLoop(params, deps);
|
|
3275
|
+
} catch (err) {
|
|
3276
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3277
|
+
await emitRunEnd("error", null, errMsg, void 0);
|
|
3278
|
+
return {
|
|
3279
|
+
success: false,
|
|
3280
|
+
data: {
|
|
3281
|
+
status: "error",
|
|
3282
|
+
error: `agent-run crashed: ${errMsg}`
|
|
3283
|
+
}
|
|
3284
|
+
};
|
|
3285
|
+
}
|
|
3286
|
+
await emitRunEnd(result.status === "completed" ? "ok" : result.status === "budget_exhausted" ? "budget_exhausted" : "error", result.result ?? null, result.error ?? null, result.totals);
|
|
3287
|
+
if (result.status === "budget_exhausted") return {
|
|
3288
|
+
success: false,
|
|
3289
|
+
error: {
|
|
3290
|
+
code: "BUDGET_EXHAUSTED",
|
|
3291
|
+
message: `Budget exhausted at round ${result.rounds}/${params.budget?.max_rounds ?? "?"} (${result.total_tokens} tokens). Increase budget.max_rounds or budget.total_tokens in agent.json to continue.`
|
|
3292
|
+
},
|
|
3293
|
+
data: result
|
|
3294
|
+
};
|
|
3295
|
+
if (writePath && result.status === "completed" && typeof result.result === "string") {
|
|
3296
|
+
if (!afs.write) return {
|
|
3297
|
+
success: false,
|
|
3298
|
+
error: {
|
|
3299
|
+
code: "WRITE_TO_PATH_FAILED",
|
|
3300
|
+
message: `write_to_path '${writePath}' failed: write not supported on this AFS`
|
|
3301
|
+
},
|
|
3302
|
+
data: result
|
|
3303
|
+
};
|
|
3304
|
+
const writeResult = result.result;
|
|
3305
|
+
try {
|
|
3306
|
+
await afs.write(writePath, { content: writeResult }, withCtx({ mode: "replace" }));
|
|
3307
|
+
} catch (writeErr) {
|
|
3308
|
+
return {
|
|
3309
|
+
success: false,
|
|
3310
|
+
error: {
|
|
3311
|
+
code: "WRITE_TO_PATH_FAILED",
|
|
3312
|
+
message: `write_to_path '${writePath}' failed: ${writeErr instanceof Error ? writeErr.message : String(writeErr)}`
|
|
3313
|
+
},
|
|
3314
|
+
data: result
|
|
3315
|
+
};
|
|
3316
|
+
}
|
|
3317
|
+
return {
|
|
3318
|
+
success: true,
|
|
3319
|
+
data: {
|
|
3320
|
+
path: writePath,
|
|
3321
|
+
byteCount: Buffer.byteLength(writeResult, "utf8"),
|
|
3322
|
+
summary: writeResult.slice(0, 200)
|
|
3323
|
+
}
|
|
3324
|
+
};
|
|
3325
|
+
}
|
|
3326
|
+
return {
|
|
3327
|
+
success: result.status === "completed",
|
|
3328
|
+
data: result
|
|
3329
|
+
};
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
//#endregion
|
|
3333
|
+
//#region src/session-queue.ts
|
|
3334
|
+
/**
|
|
3335
|
+
* Per-key Promise chain queue for serializing async operations.
|
|
3336
|
+
*
|
|
3337
|
+
* Used by ASHProvider to ensure same-session agent-run calls execute
|
|
3338
|
+
* sequentially, preventing history file race conditions.
|
|
3339
|
+
*/
|
|
3340
|
+
function deriveChildSession(parentSessionId) {
|
|
3341
|
+
if (typeof parentSessionId !== "string" || parentSessionId.length === 0) throw new TypeError("parentSessionId must be a non-empty string");
|
|
3342
|
+
if (parentSessionId.includes("..")) throw new Error("path traversal disallowed in session id");
|
|
3343
|
+
if (parentSessionId.includes("\0")) throw new Error("null bytes disallowed in session id");
|
|
3344
|
+
return `${parentSessionId}/sub/${randomUUID()}`;
|
|
3345
|
+
}
|
|
3346
|
+
const DEFAULT_MAX_DEPTH = 20;
|
|
3347
|
+
function createSessionQueue(maxDepth = DEFAULT_MAX_DEPTH) {
|
|
3348
|
+
const queues = /* @__PURE__ */ new Map();
|
|
3349
|
+
const depths = /* @__PURE__ */ new Map();
|
|
3350
|
+
return {
|
|
3351
|
+
queues,
|
|
3352
|
+
depths,
|
|
3353
|
+
async enqueue(session, fn) {
|
|
3354
|
+
if (!session) return fn();
|
|
3355
|
+
const depth = depths.get(session) ?? 0;
|
|
3356
|
+
if (depth >= maxDepth) throw new Error(`QUEUE_FULL: session queue full (max ${maxDepth})`);
|
|
3357
|
+
depths.set(session, depth + 1);
|
|
3358
|
+
const current = (queues.get(session) ?? Promise.resolve()).catch(() => {}).then(() => fn());
|
|
3359
|
+
queues.set(session, current);
|
|
3360
|
+
try {
|
|
3361
|
+
return await current;
|
|
3362
|
+
} finally {
|
|
3363
|
+
const d = (depths.get(session) ?? 1) - 1;
|
|
3364
|
+
if (d <= 0) depths.delete(session);
|
|
3365
|
+
else depths.set(session, d);
|
|
3366
|
+
if (queues.get(session) === current) queues.delete(session);
|
|
3367
|
+
}
|
|
3368
|
+
},
|
|
3369
|
+
destroy() {
|
|
3370
|
+
queues.clear();
|
|
3371
|
+
depths.clear();
|
|
3372
|
+
}
|
|
3373
|
+
};
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
//#endregion
|
|
3377
|
+
//#region \0@oxc-project+runtime@0.108.0/helpers/decorate.js
|
|
3378
|
+
function __decorate(decorators, target, key, desc) {
|
|
3379
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3380
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
3381
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
3382
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
//#endregion
|
|
3386
|
+
//#region src/provider.ts
|
|
3387
|
+
var AFSAgent = class AFSAgent extends AFSBaseProvider {
|
|
3388
|
+
static manifest() {
|
|
3389
|
+
return {
|
|
3390
|
+
name: "agent-run",
|
|
3391
|
+
description: "LLM agent runtime: multi-round inference + tool_call dispatch loop. Wraps a single agent manifest, model, and tool whitelist into a single-shot agentic execution.",
|
|
3392
|
+
uriTemplate: "agent-run://",
|
|
3393
|
+
category: "compute",
|
|
3394
|
+
schema: z.object({
|
|
3395
|
+
name: z.string().optional(),
|
|
3396
|
+
description: z.string().optional(),
|
|
3397
|
+
ashMountPath: z.string().optional(),
|
|
3398
|
+
maxSessionQueueDepth: z.number().int().positive().optional()
|
|
3399
|
+
}),
|
|
3400
|
+
tags: [
|
|
3401
|
+
"llm",
|
|
3402
|
+
"agent",
|
|
3403
|
+
"ai",
|
|
3404
|
+
"runtime"
|
|
3405
|
+
],
|
|
3406
|
+
capabilityTags: [
|
|
3407
|
+
"read-write",
|
|
3408
|
+
"auth:none",
|
|
3409
|
+
"local"
|
|
3410
|
+
],
|
|
3411
|
+
security: {
|
|
3412
|
+
riskLevel: "system",
|
|
3413
|
+
resourceAccess: ["process-spawn"],
|
|
3414
|
+
dataSensitivity: ["code"],
|
|
3415
|
+
notes: ["Executes LLM-driven multi-round agentic loops with tool dispatch. Uses @aigne/afs-ash's path-validator for tool whitelist enforcement and delegates ash DSL execution to the ash provider via AFS routing."]
|
|
3416
|
+
}
|
|
3417
|
+
};
|
|
3418
|
+
}
|
|
3419
|
+
static async load({ config } = {}) {
|
|
3420
|
+
return new AFSAgent(config);
|
|
3421
|
+
}
|
|
3422
|
+
name;
|
|
3423
|
+
description;
|
|
3424
|
+
accessMode = "readwrite";
|
|
3425
|
+
ashMountPath;
|
|
3426
|
+
sessionQueue;
|
|
3427
|
+
observabilityRuntime;
|
|
3428
|
+
afsRoot;
|
|
3429
|
+
constructor(options = {}) {
|
|
3430
|
+
super();
|
|
3431
|
+
this.name = options.name ?? "agent-run";
|
|
3432
|
+
this.description = options.description;
|
|
3433
|
+
this.ashMountPath = options.ashMountPath ?? "/ash";
|
|
3434
|
+
this.sessionQueue = createSessionQueue(options.maxSessionQueueDepth);
|
|
3435
|
+
this.observabilityRuntime = options.observabilityRuntime;
|
|
3436
|
+
}
|
|
3437
|
+
onMount(afs) {
|
|
3438
|
+
this.afsRoot = afs;
|
|
3439
|
+
}
|
|
3440
|
+
destroy() {
|
|
3441
|
+
this.sessionQueue.destroy();
|
|
3442
|
+
}
|
|
3443
|
+
async statRoot(_ctx) {
|
|
3444
|
+
return { data: {
|
|
3445
|
+
id: "agent",
|
|
3446
|
+
path: "/",
|
|
3447
|
+
meta: {
|
|
3448
|
+
kind: "afs:node",
|
|
3449
|
+
kinds: ["afs:node"],
|
|
3450
|
+
name: "agent",
|
|
3451
|
+
description: "Agentic run provider — exposes the `run` action",
|
|
3452
|
+
childrenCount: 0
|
|
3453
|
+
}
|
|
3454
|
+
} };
|
|
3455
|
+
}
|
|
3456
|
+
async statRunAction(_ctx) {
|
|
3457
|
+
return { data: {
|
|
3458
|
+
id: "run",
|
|
3459
|
+
path: "/.actions/run",
|
|
3460
|
+
meta: {
|
|
3461
|
+
kind: "afs:executable",
|
|
3462
|
+
kinds: ["afs:executable", "afs:node"],
|
|
3463
|
+
name: "run",
|
|
3464
|
+
description: "Run a single-task agentic loop"
|
|
3465
|
+
}
|
|
3466
|
+
} };
|
|
3467
|
+
}
|
|
3468
|
+
async listRootActions(_ctx) {
|
|
3469
|
+
return { data: [{
|
|
3470
|
+
id: "run",
|
|
3471
|
+
path: "/.actions/run",
|
|
3472
|
+
summary: "Run a single-task agentic loop",
|
|
3473
|
+
meta: {
|
|
3474
|
+
kind: "afs:executable",
|
|
3475
|
+
kinds: ["afs:executable", "afs:node"],
|
|
3476
|
+
name: "run",
|
|
3477
|
+
description: "Execute a multi-round agentic loop: LLM inference → tool_call validation → tool execution → result feedback. Stops when the LLM returns text or budget exhausts.",
|
|
3478
|
+
inputSchema: {
|
|
3479
|
+
type: "object",
|
|
3480
|
+
properties: {
|
|
3481
|
+
task: {
|
|
3482
|
+
type: "string",
|
|
3483
|
+
description: "The task for the agent to accomplish"
|
|
3484
|
+
},
|
|
3485
|
+
model: {
|
|
3486
|
+
type: "string",
|
|
3487
|
+
description: "AI model path, e.g. /dev/ai/.actions/chat"
|
|
3488
|
+
},
|
|
3489
|
+
tools: { description: "AFS path patterns the LLM may call. `none` or empty array → LLM-only mode (no tool calling)." },
|
|
3490
|
+
budget: {
|
|
3491
|
+
type: "object",
|
|
3492
|
+
description: "Budget caps: { max_rounds?, total_tokens? }"
|
|
3493
|
+
},
|
|
3494
|
+
session: {
|
|
3495
|
+
type: "string",
|
|
3496
|
+
description: "Session id for queue + history"
|
|
3497
|
+
},
|
|
3498
|
+
system: {
|
|
3499
|
+
type: "string",
|
|
3500
|
+
description: "System prompt prefix"
|
|
3501
|
+
},
|
|
3502
|
+
skill_paths: {
|
|
3503
|
+
type: "array",
|
|
3504
|
+
items: { type: "string" },
|
|
3505
|
+
description: "Paths to skill SKILL.md files for prompt augmentation"
|
|
3506
|
+
},
|
|
3507
|
+
write_to_path: {
|
|
3508
|
+
type: "string",
|
|
3509
|
+
description: "AFS path to write the completed result text. When set and the agent completes successfully with text output, the text is written to this path and the response returns { path, byteCount, summary } instead of the full result."
|
|
3510
|
+
}
|
|
3511
|
+
},
|
|
3512
|
+
required: ["task", "model"]
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
}] };
|
|
3516
|
+
}
|
|
3517
|
+
async execRootAction(ctx, args) {
|
|
3518
|
+
const actionName = ctx.params.action;
|
|
3519
|
+
if (actionName === "__debug-runtime") return this.execDebugRuntime();
|
|
3520
|
+
if (actionName !== "run") return {
|
|
3521
|
+
success: false,
|
|
3522
|
+
error: {
|
|
3523
|
+
code: "ACTION_NOT_FOUND",
|
|
3524
|
+
message: `Unknown action '${actionName}' on agent-run provider; only 'run' is supported`
|
|
3525
|
+
}
|
|
3526
|
+
};
|
|
3527
|
+
if (!this.afsRoot) return {
|
|
3528
|
+
success: false,
|
|
3529
|
+
error: {
|
|
3530
|
+
code: "AGENT_RUN_NOT_INITIALIZED",
|
|
3531
|
+
message: "AFSAgent has not been mounted yet — onMount has not fired"
|
|
3532
|
+
}
|
|
3533
|
+
};
|
|
3534
|
+
const session = args.session;
|
|
3535
|
+
let result;
|
|
3536
|
+
try {
|
|
3537
|
+
result = await this.sessionQueue.enqueue(session, () => executeAgentRun(args, {
|
|
3538
|
+
globalAfs: this.afsRoot,
|
|
3539
|
+
ashMountPath: this.ashMountPath,
|
|
3540
|
+
callerContext: ctx.context,
|
|
3541
|
+
observabilityRuntime: this.observabilityRuntime
|
|
3542
|
+
}));
|
|
3543
|
+
} catch (err) {
|
|
3544
|
+
if (err instanceof Error && err.message.startsWith("QUEUE_FULL")) return {
|
|
3545
|
+
success: false,
|
|
3546
|
+
error: {
|
|
3547
|
+
code: "QUEUE_FULL",
|
|
3548
|
+
message: err.message
|
|
3549
|
+
}
|
|
3550
|
+
};
|
|
3551
|
+
throw err;
|
|
3552
|
+
}
|
|
3553
|
+
this.emit({
|
|
3554
|
+
type: ASH_EVENT_TYPES.TASK_COMPLETE,
|
|
3555
|
+
path: "/",
|
|
3556
|
+
data: {
|
|
3557
|
+
success: result.success,
|
|
3558
|
+
errorCode: result.error?.code,
|
|
3559
|
+
model: typeof args.model === "string" ? args.model : void 0,
|
|
3560
|
+
durationMs: result.usage?.durationMs,
|
|
3561
|
+
callerBlockletId: ctx.context?.blockletId ?? void 0,
|
|
3562
|
+
session
|
|
3563
|
+
}
|
|
3564
|
+
});
|
|
3565
|
+
return result;
|
|
3566
|
+
}
|
|
3567
|
+
/**
|
|
3568
|
+
* Handler for the test-only `__debug-runtime` action (Phase 3b). Returns a
|
|
3569
|
+
* boolean/string projection of the injected `observabilityRuntime` so E2E
|
|
3570
|
+
* tests can assert the capability reached `/dev/agent` — but it deliberately
|
|
3571
|
+
* omits the `spaceResolver` handle itself (object capability must not leave
|
|
3572
|
+
* the host). Outside `AFS_TEST_MODE` it refuses with `AFS_PERMISSION` so
|
|
3573
|
+
* production never exposes runtime wiring.
|
|
3574
|
+
*/
|
|
3575
|
+
execDebugRuntime() {
|
|
3576
|
+
if (!process.env.AFS_TEST_MODE) return {
|
|
3577
|
+
success: false,
|
|
3578
|
+
error: {
|
|
3579
|
+
code: "AFS_PERMISSION",
|
|
3580
|
+
message: "__debug-runtime is only available under AFS_TEST_MODE"
|
|
3581
|
+
}
|
|
3582
|
+
};
|
|
3583
|
+
const rt = this.observabilityRuntime;
|
|
3584
|
+
return {
|
|
3585
|
+
success: true,
|
|
3586
|
+
data: { observabilityRuntime: {
|
|
3587
|
+
hasResolver: Boolean(rt?.spaceResolver),
|
|
3588
|
+
anonymousInteractivePersistence: rt?.anonymousInteractivePersistence ?? null,
|
|
3589
|
+
ownershipRoutingMode: rt?.ownershipRoutingMode ?? "observe-only"
|
|
3590
|
+
} }
|
|
3591
|
+
};
|
|
3592
|
+
}
|
|
3593
|
+
};
|
|
3594
|
+
__decorate([Stat("/")], AFSAgent.prototype, "statRoot", null);
|
|
3595
|
+
__decorate([Stat("/.actions/run")], AFSAgent.prototype, "statRunAction", null);
|
|
3596
|
+
__decorate([Actions("/")], AFSAgent.prototype, "listRootActions", null);
|
|
3597
|
+
__decorate([Actions.Exec("/", void 0, void 0, { effect: "write" })], AFSAgent.prototype, "execRootAction", null);
|
|
3598
|
+
|
|
3599
|
+
//#endregion
|
|
3600
|
+
//#region src/scratchpad.ts
|
|
3601
|
+
/**
|
|
3602
|
+
* Scratchpad provisioning — allocates per-run scratch directories under
|
|
3603
|
+
* a session-scoped memory path.
|
|
3604
|
+
*
|
|
3605
|
+
* Provider internal: direct fs calls are allowed here (see CLAUDE.md).
|
|
3606
|
+
*/
|
|
3607
|
+
function validatePathComponent(value, label) {
|
|
3608
|
+
if (typeof value !== "string" || value.length === 0) throw new TypeError(`${label} must be a non-empty string`);
|
|
3609
|
+
if (value.includes("..")) throw new Error(`path traversal disallowed in ${label}`);
|
|
3610
|
+
if (value.includes("\0")) throw new Error(`null byte disallowed in ${label}`);
|
|
3611
|
+
}
|
|
3612
|
+
function allocateScratch(memoryBasePath, session, runId) {
|
|
3613
|
+
validatePathComponent(session, "session");
|
|
3614
|
+
validatePathComponent(runId, "runId");
|
|
3615
|
+
const basePath = join(memoryBasePath, "runs", session, "subruns", runId);
|
|
3616
|
+
mkdirSync(basePath, { recursive: true });
|
|
3617
|
+
const layout = {
|
|
3618
|
+
basePath,
|
|
3619
|
+
stateYml: join(basePath, "state.yml"),
|
|
3620
|
+
messagesJsonl: join(basePath, "messages.jsonl"),
|
|
3621
|
+
eventsJsonl: join(basePath, "events.jsonl"),
|
|
3622
|
+
historyJsonl: join(basePath, "history.jsonl")
|
|
3623
|
+
};
|
|
3624
|
+
if (!existsSync(layout.stateYml)) writeFileSync(layout.stateYml, "");
|
|
3625
|
+
if (!existsSync(layout.messagesJsonl)) writeFileSync(layout.messagesJsonl, "");
|
|
3626
|
+
if (!existsSync(layout.eventsJsonl)) writeFileSync(layout.eventsJsonl, "");
|
|
3627
|
+
if (!existsSync(layout.historyJsonl)) writeFileSync(layout.historyJsonl, "");
|
|
3628
|
+
return layout;
|
|
3629
|
+
}
|
|
3630
|
+
function getStateProxy(layout) {
|
|
3631
|
+
const raw = readFileSync(layout.stateYml, "utf8").trim();
|
|
3632
|
+
if (!raw) return Object.freeze({});
|
|
3633
|
+
let parsed;
|
|
3634
|
+
try {
|
|
3635
|
+
parsed = load(raw);
|
|
3636
|
+
} catch {
|
|
3637
|
+
return Object.freeze({});
|
|
3638
|
+
}
|
|
3639
|
+
if (parsed === null || parsed === void 0 || typeof parsed !== "object") return Object.freeze({});
|
|
3640
|
+
return deepFreeze(parsed);
|
|
3641
|
+
}
|
|
3642
|
+
function deepFreeze(obj) {
|
|
3643
|
+
for (const value of Object.values(obj)) if (value && typeof value === "object" && !Object.isFrozen(value)) deepFreeze(value);
|
|
3644
|
+
return Object.freeze(obj);
|
|
3645
|
+
}
|
|
3646
|
+
function teardownSession(memoryBasePath, session, retention = "session") {
|
|
3647
|
+
if (retention === "forever") return;
|
|
3648
|
+
const sessionSubrunsDir = join(memoryBasePath, "runs", session, "subruns");
|
|
3649
|
+
if (!existsSync(sessionSubrunsDir)) return;
|
|
3650
|
+
if (retention === "session") {
|
|
3651
|
+
rmSync(sessionSubrunsDir, {
|
|
3652
|
+
recursive: true,
|
|
3653
|
+
force: true
|
|
3654
|
+
});
|
|
3655
|
+
const sessionDir = join(memoryBasePath, "runs", session);
|
|
3656
|
+
try {
|
|
3657
|
+
rmSync(sessionDir, {
|
|
3658
|
+
recursive: true,
|
|
3659
|
+
force: true
|
|
3660
|
+
});
|
|
3661
|
+
} catch {}
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
|
|
3665
|
+
//#endregion
|
|
3666
|
+
export { AFSAgent, allocateScratch, appendMessage, buildContext, buildToolSchema, checkSpawnDepth, computeLLMRetryDelayMs, createSessionQueue, deriveChildSession, estimateMessagesTokens, estimateTokens, executeAgentRun, generateAsh, getStateProxy, maybeCompress, normalizeToolCallsToAIGNE, readMessages, resetSeqCounter, resolveAgentModelPath, runAgentLoop, teardownSession };
|
|
3667
|
+
//# sourceMappingURL=index.mjs.map
|