@cybernetyx1/atlasflow-runtime 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2762 @@
1
+ import {
2
+ createCwdSessionEnv,
3
+ makeFs,
4
+ virtualSandbox
5
+ } from "./chunk-4DU4GJ2X.js";
6
+ import {
7
+ inMemoryAdapter
8
+ } from "./chunk-M4JW76IL.js";
9
+ import {
10
+ createScopedSessionEnv,
11
+ mergeCommands
12
+ } from "./chunk-S7RZJMCF.js";
13
+ import {
14
+ getRegisteredApiKey,
15
+ resolveRegisteredModel
16
+ } from "./chunk-HO6QHSUS.js";
17
+
18
+ // src/agent.ts
19
+ function createAgent(initialize) {
20
+ return { __atlasflowAgent: true, initialize };
21
+ }
22
+ function defineAgent(definition) {
23
+ return typeof definition === "function" ? createAgent(definition) : createAgent(() => definition);
24
+ }
25
+ function isCreatedAgent(value) {
26
+ return typeof value === "object" && value !== null && value.__atlasflowAgent === true;
27
+ }
28
+ function defineAgentProfile(profile) {
29
+ return profile;
30
+ }
31
+ function applyProfile(config) {
32
+ const profile = config.profile;
33
+ if (!profile) return config;
34
+ return {
35
+ ...config,
36
+ model: config.model ?? profile.model,
37
+ instructions: config.instructions ?? profile.instructions,
38
+ tools: config.tools ?? profile.tools,
39
+ thinkingLevel: config.thinkingLevel ?? profile.thinkingLevel
40
+ };
41
+ }
42
+
43
+ // src/approval.ts
44
+ var ToolApprovalRequiredError = class extends Error {
45
+ constructor(runId, approval) {
46
+ super(`Tool "${approval.tool}" is waiting for approval (${approval.id}).`);
47
+ this.runId = runId;
48
+ this.approval = approval;
49
+ this.name = "ToolApprovalRequiredError";
50
+ }
51
+ runId;
52
+ approval;
53
+ code = "tool_approval_required";
54
+ partialTranscript;
55
+ };
56
+ function isToolApprovalRequiredError(value) {
57
+ return value instanceof ToolApprovalRequiredError || typeof value === "object" && value !== null && value.code === "tool_approval_required";
58
+ }
59
+ function attachToolApprovalPartialTranscript(value, partial) {
60
+ if (!isToolApprovalRequiredError(value)) return;
61
+ value.partialTranscript = partial;
62
+ }
63
+ function toolApprovalPartialTranscript(value) {
64
+ return isToolApprovalRequiredError(value) ? value.partialTranscript : void 0;
65
+ }
66
+ function normalizeToolApprovalPolicy(value) {
67
+ if (value === true) return { required: true };
68
+ if (value && value.required) return value;
69
+ return void 0;
70
+ }
71
+ function waitingToolApproval(record2) {
72
+ const state = toolApprovalState(record2.state);
73
+ if (!state || record2.status !== "waiting_approval") return void 0;
74
+ const pending = state.pending ? state.approvals[state.pending] : void 0;
75
+ if (pending?.status === "pending") return pending;
76
+ return Object.values(state.approvals).find((approval) => approval.status === "pending");
77
+ }
78
+ function decideToolApprovalState(record2, decision) {
79
+ const state = toolApprovalState(record2.state);
80
+ if (!state) throw new Error(`Run "${record2.runId}" is not waiting on a tool approval.`);
81
+ const approval = decision.approval ? state.approvals[decision.approval] : waitingToolApproval(record2);
82
+ if (!approval) throw new Error(`Run "${record2.runId}" has no pending tool approval.`);
83
+ if (approval.status !== "pending") throw new Error(`Approval "${approval.id}" is already ${approval.status}.`);
84
+ const decided = {
85
+ ...approval,
86
+ status: decision.approved ? "approved" : "rejected",
87
+ decidedAt: Date.now(),
88
+ resumeOnNextCall: decision.approved ? true : void 0,
89
+ note: decision.note
90
+ };
91
+ state.approvals[approval.id] = decided;
92
+ if (state.pending === approval.id) state.pending = void 0;
93
+ return { state, approval: decided };
94
+ }
95
+ function wrapToolApproval(tool, input) {
96
+ const policy = normalizeToolApprovalPolicy(tool.approval);
97
+ if (!policy) return tool;
98
+ return {
99
+ ...tool,
100
+ approval: policy,
101
+ execute: async (args, signal) => {
102
+ const record2 = await input.persistence.runs.get(input.runId);
103
+ if (!record2) throw new Error(`Run "${input.runId}" was not found while checking tool approval.`);
104
+ const next = await nextToolApproval(record2, tool, args, policy);
105
+ await input.persistence.runs.update(input.runId, { status: next.status, state: next.state });
106
+ if (next.action === "reject") throw new Error(`Tool "${tool.name}" was rejected${next.approval.note ? `: ${next.approval.note}` : "."}`);
107
+ if (next.action === "request") {
108
+ input.bus.emit({
109
+ type: "tool_approval_requested",
110
+ approval: next.approval.id,
111
+ tool: tool.name,
112
+ arguments: args,
113
+ runId: input.runId
114
+ });
115
+ throw new ToolApprovalRequiredError(input.runId, next.approval);
116
+ }
117
+ input.bus.emit({ type: "tool_approval_decided", approval: next.approval.id, tool: tool.name, approved: true, runId: input.runId });
118
+ return tool.execute(next.approval.arguments, signal);
119
+ }
120
+ };
121
+ }
122
+ async function nextToolApproval(record2, tool, args, policy) {
123
+ const state = ensureToolApprovalState(record2.state);
124
+ const canonicalCall = stableJson({ tool: tool.name, args });
125
+ const baseId = await toolApprovalBaseId(canonicalCall);
126
+ const existing = Object.values(state.approvals).filter((approval) => approvalMatchesCall(approval, tool.name, args, canonicalCall) && !approval.consumedAt).sort((a, b) => a.requestedAt - b.requestedAt)[0];
127
+ if (existing?.status === "approved") {
128
+ const consumed = { ...existing, consumedAt: Date.now(), resumeOnNextCall: void 0 };
129
+ state.approvals[existing.id] = consumed;
130
+ if (state.pending === existing.id) state.pending = void 0;
131
+ return { action: "execute", status: "running", state, approval: consumed };
132
+ }
133
+ if (existing?.status === "rejected") return { action: "reject", status: "failed", state, approval: existing };
134
+ if (existing?.status === "pending") {
135
+ state.pending = existing.id;
136
+ return { action: "request", status: "waiting_approval", state, approval: existing };
137
+ }
138
+ const resumable = Object.values(state.approvals).filter((approval) => approval.tool === tool.name && approval.status === "approved" && approval.resumeOnNextCall && !approval.consumedAt).sort((a, b) => (a.decidedAt ?? a.requestedAt) - (b.decidedAt ?? b.requestedAt))[0];
139
+ if (resumable) {
140
+ const consumed = { ...resumable, consumedAt: Date.now(), resumeOnNextCall: void 0 };
141
+ state.approvals[resumable.id] = consumed;
142
+ if (state.pending === resumable.id) state.pending = void 0;
143
+ return { action: "execute", status: "running", state, approval: consumed };
144
+ }
145
+ const ordinal = Object.values(state.approvals).filter((approval) => approvalMatchesCall(approval, tool.name, args, canonicalCall)).length + 1;
146
+ const requested = {
147
+ id: `${baseId}.${ordinal}`,
148
+ baseId,
149
+ tool: tool.name,
150
+ arguments: args,
151
+ reason: policy.reason,
152
+ status: "pending",
153
+ requestedAt: Date.now()
154
+ };
155
+ state.approvals[requested.id] = requested;
156
+ state.pending = requested.id;
157
+ return { action: "request", status: "waiting_approval", state, approval: requested };
158
+ }
159
+ function ensureToolApprovalState(value) {
160
+ const existing = toolApprovalState(value);
161
+ if (existing) return existing;
162
+ return { kind: "tool_approval", approvals: {} };
163
+ }
164
+ function toolApprovalState(value) {
165
+ if (!value || typeof value !== "object") return void 0;
166
+ const candidate = value;
167
+ if (candidate.kind !== "tool_approval" || !candidate.approvals || typeof candidate.approvals !== "object") return void 0;
168
+ return { kind: "tool_approval", approvals: { ...candidate.approvals }, pending: typeof candidate.pending === "string" ? candidate.pending : void 0 };
169
+ }
170
+ function approvalMatchesCall(approval, tool, args, canonicalCall = stableJson({ tool, args })) {
171
+ return approval.tool === tool && stableJson({ tool: approval.tool, args: approval.arguments }) === canonicalCall;
172
+ }
173
+ async function toolApprovalBaseId(canonicalCall) {
174
+ return `tap_sha256_${await sha256Hex(canonicalCall)}`;
175
+ }
176
+ function stableJson(value) {
177
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
178
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
179
+ const obj = value;
180
+ return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${stableJson(obj[key])}`).join(",")}}`;
181
+ }
182
+ async function sha256Hex(value) {
183
+ const cryptoApi = globalThis.crypto;
184
+ if (!cryptoApi?.subtle) throw new Error("Tool approval hashing requires Web Crypto SHA-256 support.");
185
+ const digest = await cryptoApi.subtle.digest("SHA-256", new TextEncoder().encode(value));
186
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
187
+ }
188
+
189
+ // src/builtin-tools.ts
190
+ var DEFAULT_MAX_OUTPUT = 5e4;
191
+ function builtinTools(env, options = {}) {
192
+ const cap = options.maxOutputChars ?? DEFAULT_MAX_OUTPUT;
193
+ const all = [
194
+ readTool(env, cap),
195
+ writeTool(env),
196
+ editTool(env),
197
+ bashTool(env, cap),
198
+ grepTool(env, cap),
199
+ globTool(env, cap)
200
+ ];
201
+ if (!options.include) return all;
202
+ const include = new Set(options.include);
203
+ return all.filter((t) => include.has(t.name));
204
+ }
205
+ var BUILTIN_TOOL_NAMES = ["read", "write", "edit", "bash", "grep", "glob", "task"];
206
+ function truncate(text, cap) {
207
+ if (text.length <= cap) return text;
208
+ return `${text.slice(0, cap)}
209
+ ... [output truncated at ${cap} characters]`;
210
+ }
211
+ function readTool(env, cap) {
212
+ return {
213
+ name: "read",
214
+ description: "Read a file from the workspace. Returns the file content (optionally a line range).",
215
+ parameters: {
216
+ type: "object",
217
+ properties: {
218
+ path: { type: "string", description: "File path (absolute, or relative to the workspace)." },
219
+ offset: { type: "number", description: "1-based line to start from (optional)." },
220
+ limit: { type: "number", description: "Max lines to return (optional)." }
221
+ },
222
+ required: ["path"]
223
+ },
224
+ execute: async (args) => {
225
+ const path = String(args.path);
226
+ const content = await env.readFile(path);
227
+ if (args.offset == null && args.limit == null) return truncate(content, cap);
228
+ const lines = content.split("\n");
229
+ const start = Math.max(0, (Number(args.offset) || 1) - 1);
230
+ const count = args.limit != null ? Number(args.limit) : lines.length - start;
231
+ return truncate(lines.slice(start, start + count).join("\n"), cap);
232
+ }
233
+ };
234
+ }
235
+ function writeTool(env) {
236
+ return {
237
+ name: "write",
238
+ description: "Write a file in the workspace, creating parent directories and overwriting any existing content.",
239
+ parameters: {
240
+ type: "object",
241
+ properties: {
242
+ path: { type: "string", description: "File path (absolute, or relative to the workspace)." },
243
+ content: { type: "string", description: "Full file content to write." }
244
+ },
245
+ required: ["path", "content"]
246
+ },
247
+ execute: async (args) => {
248
+ const path = String(args.path);
249
+ const content = String(args.content);
250
+ const dir = dirnameOf(env.resolvePath(path));
251
+ if (dir) await env.mkdir(dir, { recursive: true }).catch(() => {
252
+ });
253
+ await env.writeFile(path, content);
254
+ return `Wrote ${content.length} characters to ${path}`;
255
+ }
256
+ };
257
+ }
258
+ function editTool(env) {
259
+ return {
260
+ name: "edit",
261
+ description: "Replace an exact string in a file. The old string must match exactly and be unique unless replace_all is true.",
262
+ parameters: {
263
+ type: "object",
264
+ properties: {
265
+ path: { type: "string", description: "File path (absolute, or relative to the workspace)." },
266
+ old_string: { type: "string", description: "Exact text to find." },
267
+ new_string: { type: "string", description: "Replacement text." },
268
+ replace_all: { type: "boolean", description: "Replace every occurrence (default false)." }
269
+ },
270
+ required: ["path", "old_string", "new_string"]
271
+ },
272
+ execute: async (args) => {
273
+ const path = String(args.path);
274
+ const oldStr = String(args.old_string);
275
+ const newStr = String(args.new_string);
276
+ if (oldStr === newStr) throw new Error("old_string and new_string are identical");
277
+ if (oldStr === "") throw new Error("old_string must not be empty");
278
+ const content = await env.readFile(path);
279
+ const count = content.split(oldStr).length - 1;
280
+ if (count === 0) throw new Error(`old_string not found in ${path}`);
281
+ if (count > 1 && args.replace_all !== true)
282
+ throw new Error(`old_string appears ${count} times in ${path}; include more surrounding context to make it unique, or set replace_all`);
283
+ const updated = args.replace_all === true ? content.split(oldStr).join(newStr) : content.replace(oldStr, newStr);
284
+ await env.writeFile(path, updated);
285
+ return `Edited ${path} (${count} replacement${count === 1 ? "" : "s"})`;
286
+ }
287
+ };
288
+ }
289
+ function bashTool(env, cap) {
290
+ return {
291
+ name: "bash",
292
+ description: "Run a shell command in the workspace and return stdout, stderr, and the exit code. Some providers, including the default virtual sandbox, are lightweight and may not include python3, node, git, or system packages; if a command exits non-zero, treat it as failed and do not infer missing output.",
293
+ parameters: {
294
+ type: "object",
295
+ properties: {
296
+ command: { type: "string", description: "The shell command to run." },
297
+ timeout_ms: { type: "number", description: "Timeout in milliseconds (default 120000, max 600000)." },
298
+ cwd: { type: "string", description: "Working directory (optional)." }
299
+ },
300
+ required: ["command"]
301
+ },
302
+ execute: async (args, signal) => {
303
+ const timeoutMs = Math.min(Number(args.timeout_ms) || 12e4, 6e5);
304
+ const res = await env.exec(String(args.command), { timeoutMs, cwd: args.cwd ? String(args.cwd) : void 0, signal });
305
+ const parts = [];
306
+ if (res.exitCode !== 0) {
307
+ parts.push(`[command failed: exit code ${res.exitCode}] Do not infer or fabricate missing stdout; report the failure, stderr, and exit code.`);
308
+ }
309
+ if (res.stdout) parts.push(truncate(res.stdout, cap));
310
+ if (res.stderr) parts.push(`[stderr]
311
+ ${truncate(res.stderr, Math.min(cap, 1e4))}`);
312
+ parts.push(`[exit code: ${res.exitCode}]`);
313
+ return parts.join("\n");
314
+ }
315
+ };
316
+ }
317
+ function grepTool(env, cap) {
318
+ return {
319
+ name: "grep",
320
+ description: "Search file contents for a regular expression. Returns matching lines as path:line:text.",
321
+ parameters: {
322
+ type: "object",
323
+ properties: {
324
+ pattern: { type: "string", description: "Regular expression (POSIX extended)." },
325
+ path: { type: "string", description: "File or directory to search (default: workspace root)." },
326
+ ignore_case: { type: "boolean", description: "Case-insensitive search (default false)." }
327
+ },
328
+ required: ["pattern"]
329
+ },
330
+ execute: async (args, signal) => {
331
+ const target = args.path ? String(args.path) : ".";
332
+ const flags = `-rnE${args.ignore_case === true ? "i" : ""}`;
333
+ const cmd = `grep ${flags} --exclude-dir=node_modules --exclude-dir=.git -- ${shq(String(args.pattern))} ${shq(target)}`;
334
+ const res = await env.exec(cmd, { timeoutMs: 6e4, signal });
335
+ if (res.exitCode > 1) throw new Error(res.stderr || `grep failed with exit code ${res.exitCode}`);
336
+ return res.stdout ? truncate(res.stdout, cap) : "No matches found.";
337
+ }
338
+ };
339
+ }
340
+ function globTool(env, cap) {
341
+ return {
342
+ name: "glob",
343
+ description: 'Find files by glob pattern (e.g. "**/*.ts", "src/*.json"). Returns matching paths.',
344
+ parameters: {
345
+ type: "object",
346
+ properties: {
347
+ pattern: { type: "string", description: "Glob pattern. ** matches across directories." },
348
+ path: { type: "string", description: "Directory to search from (default: workspace root)." }
349
+ },
350
+ required: ["pattern"]
351
+ },
352
+ execute: async (args) => {
353
+ const base = args.path ? String(args.path) : ".";
354
+ const matcher = globToRegExp(String(args.pattern));
355
+ const found = [];
356
+ await walk(env, base, "", matcher, found, { visited: 0 });
357
+ return found.length ? truncate(found.sort().join("\n"), cap) : "No files matched.";
358
+ }
359
+ };
360
+ }
361
+ var WALK_LIMIT = 1e4;
362
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".atlasflow-build"]);
363
+ async function walk(env, base, rel, matcher, found, budget) {
364
+ if (budget.visited >= WALK_LIMIT || found.length >= 2e3) return;
365
+ const dir = rel ? joinPath(base, rel) : base;
366
+ let entries;
367
+ try {
368
+ entries = await env.readdir(dir);
369
+ } catch {
370
+ return;
371
+ }
372
+ for (const entry of entries) {
373
+ if (budget.visited++ >= WALK_LIMIT) return;
374
+ const childRel = rel ? `${rel}/${entry}` : entry;
375
+ let isDir = false;
376
+ try {
377
+ isDir = (await env.stat(joinPath(base, childRel))).isDirectory;
378
+ } catch {
379
+ continue;
380
+ }
381
+ if (isDir) {
382
+ if (!SKIP_DIRS.has(entry)) await walk(env, base, childRel, matcher, found, budget);
383
+ } else if (matcher.test(childRel)) {
384
+ found.push(childRel);
385
+ }
386
+ }
387
+ }
388
+ function joinPath(base, rel) {
389
+ return base === "." ? rel : `${base.replace(/\/$/, "")}/${rel}`;
390
+ }
391
+ function globToRegExp(pattern) {
392
+ let out = "";
393
+ for (let i = 0; i < pattern.length; i++) {
394
+ const ch = pattern[i];
395
+ if (ch === "*") {
396
+ if (pattern[i + 1] === "*") {
397
+ out += pattern[i + 2] === "/" ? "(?:.*/)?" : ".*";
398
+ i += pattern[i + 2] === "/" ? 2 : 1;
399
+ } else {
400
+ out += "[^/]*";
401
+ }
402
+ } else if (ch === "?") {
403
+ out += "[^/]";
404
+ } else if (ch && ".+^${}()|[]\\".includes(ch)) {
405
+ out += `\\${ch}`;
406
+ } else {
407
+ out += ch;
408
+ }
409
+ }
410
+ return new RegExp(`^${out}$`);
411
+ }
412
+ function dirnameOf(path) {
413
+ const idx = path.lastIndexOf("/");
414
+ if (idx <= 0) return null;
415
+ return path.slice(0, idx);
416
+ }
417
+ function shq(s) {
418
+ return `'${s.replace(/'/g, `'${String.fromCharCode(92)}''`)}'`;
419
+ }
420
+
421
+ // src/context.ts
422
+ function isWorkspaceSkill(skill) {
423
+ const candidate = skill;
424
+ return candidate.__atlasWorkspaceSkill === true && typeof candidate.directory === "string" && typeof candidate.skillMdPath === "string";
425
+ }
426
+ async function discoverSessionContext(env, definitionSkills = []) {
427
+ const instructionFiles = await readInstructionFiles(env, env.cwd);
428
+ const workspaceSkills = await discoverWorkspaceSkills(env, env.cwd);
429
+ const names = new Set(definitionSkills.map((skill) => skill.name));
430
+ for (const skill of workspaceSkills) {
431
+ if (names.has(skill.name)) throw new Error(`Skill name "${skill.name}" appears in both the agent definition and workspace discovery.`);
432
+ }
433
+ const instructions = composeDiscoveredInstructions(instructionFiles, workspaceSkills);
434
+ return { instructions, workspaceSkills };
435
+ }
436
+ async function loadWorkspaceSkill(env, skill) {
437
+ const content = await env.readFile(skill.skillMdPath);
438
+ const parsed = parseSkillMarkdown(content, { directoryName: lastPathPart(skill.directory), path: skill.skillMdPath });
439
+ return parsed;
440
+ }
441
+ function composeDiscoveredInstructions(instructionFiles, workspaceSkills) {
442
+ const parts = [];
443
+ if (instructionFiles.length) parts.push(instructionFiles.join("\n\n"));
444
+ if (workspaceSkills.length) {
445
+ parts.push(
446
+ [
447
+ "## Available Skills",
448
+ "",
449
+ "The following workspace skills provide specialized instructions for specific tasks. When a task matches a skill description, call the `activate_skill` tool with that skill name before proceeding so its full instructions are loaded.",
450
+ "",
451
+ ...workspaceSkills.map((skill) => `- **${skill.name}**${skill.description ? ` - ${skill.description}` : ""}`)
452
+ ].join("\n")
453
+ );
454
+ }
455
+ return parts.length ? parts.join("\n\n") : void 0;
456
+ }
457
+ async function readInstructionFiles(env, basePath) {
458
+ const out = [];
459
+ for (const filename of ["AGENTS.md", "CLAUDE.md"]) {
460
+ const filePath = joinPath2(basePath, filename);
461
+ if (await env.exists(filePath)) {
462
+ const content = (await env.readFile(filePath)).trim();
463
+ if (content) out.push(content);
464
+ }
465
+ }
466
+ return out;
467
+ }
468
+ async function discoverWorkspaceSkills(env, basePath) {
469
+ const skillsDir = joinPath2(basePath, ".agents/skills");
470
+ if (!await env.exists(skillsDir)) return [];
471
+ let entries;
472
+ try {
473
+ entries = await env.readdir(skillsDir);
474
+ } catch {
475
+ return [];
476
+ }
477
+ const skills = [];
478
+ for (const entry of entries.sort()) {
479
+ const skillDir = joinPath2(skillsDir, entry);
480
+ try {
481
+ const stat = await env.stat(skillDir);
482
+ if (!stat.isDirectory) continue;
483
+ } catch {
484
+ continue;
485
+ }
486
+ const skillMdPath = joinPath2(skillDir, "SKILL.md");
487
+ if (!await env.exists(skillMdPath)) continue;
488
+ try {
489
+ const parsed = parseSkillMarkdown(await env.readFile(skillMdPath), { directoryName: entry, path: skillMdPath });
490
+ skills.push({
491
+ __atlasWorkspaceSkill: true,
492
+ name: parsed.name,
493
+ description: parsed.description,
494
+ body: "",
495
+ allowedTools: parsed.allowedTools,
496
+ directory: skillDir,
497
+ skillMdPath
498
+ });
499
+ } catch (err) {
500
+ const detail = err instanceof Error ? err.message : String(err);
501
+ console.warn(`[atlasflow] Skipping invalid workspace skill "${entry}": ${detail}`);
502
+ }
503
+ }
504
+ return skills;
505
+ }
506
+ function parseSkillMarkdown(content, options) {
507
+ const match = content.replace(/^\uFEFF/, "").match(/^---\s*\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)([\s\S]*)$/);
508
+ if (!match) throw new Error(`Skill ${options.path} is missing YAML frontmatter with name and description.`);
509
+ const frontmatter = parseSimpleYamlMapping(match[1] ?? "", options.path);
510
+ const name = requireString(frontmatter.name, options.path, "name");
511
+ validateSkillName(name, options);
512
+ const description = requireString(frontmatter.description, options.path, "description");
513
+ if ([...description].length > 1024) throw new Error(`Skill ${options.path} description exceeds 1024 characters.`);
514
+ return {
515
+ name,
516
+ description,
517
+ body: (match[2] ?? "").trim(),
518
+ allowedTools: optionalString(frontmatter["allowed-tools"], options.path, "allowed-tools")?.split(/\s+/).filter(Boolean)
519
+ };
520
+ }
521
+ function parseSimpleYamlMapping(source, path) {
522
+ const out = {};
523
+ for (const rawLine of source.split(/\r?\n/)) {
524
+ const line = rawLine.trim();
525
+ if (!line || line.startsWith("#")) continue;
526
+ const idx = line.indexOf(":");
527
+ if (idx <= 0) throw new Error(`Skill ${path} frontmatter must be a simple YAML mapping.`);
528
+ const key = line.slice(0, idx).trim();
529
+ let value = line.slice(idx + 1).trim();
530
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
531
+ out[key] = value;
532
+ }
533
+ return out;
534
+ }
535
+ function requireString(value, path, field) {
536
+ if (typeof value !== "string" || value.trim() === "") throw new Error(`Skill ${path} must define frontmatter ${field} as a non-empty string.`);
537
+ return value.trim();
538
+ }
539
+ function optionalString(value, path, field) {
540
+ if (value == null) return void 0;
541
+ if (typeof value !== "string") throw new Error(`Skill ${path} frontmatter ${field} must be a string when provided.`);
542
+ const trimmed = value.trim();
543
+ return trimmed ? trimmed : void 0;
544
+ }
545
+ function validateSkillName(name, options) {
546
+ const normalized = name.normalize("NFKC");
547
+ if ([...normalized].length > 64) throw new Error(`Skill ${options.path} name must be at most 64 characters.`);
548
+ if (!/^[\p{L}\p{N}-]+$/u.test(normalized) || normalized !== normalized.toLowerCase()) {
549
+ throw new Error(`Skill ${options.path} name "${name}" must contain only lowercase letters, numbers, and hyphens.`);
550
+ }
551
+ if (normalized.startsWith("-") || normalized.endsWith("-") || normalized.includes("--")) {
552
+ throw new Error(`Skill ${options.path} name "${name}" must not start or end with a hyphen or contain consecutive hyphens.`);
553
+ }
554
+ if (normalized !== options.directoryName.normalize("NFKC")) {
555
+ throw new Error(`Skill ${options.path} declares name "${name}", but the directory is "${options.directoryName}".`);
556
+ }
557
+ }
558
+ function joinPath2(base, child) {
559
+ return `${base.replace(/\/$/, "")}/${child}`;
560
+ }
561
+ function lastPathPart(path) {
562
+ return path.replace(/\/+$/, "").split("/").pop() ?? path;
563
+ }
564
+
565
+ // src/rules.ts
566
+ var RuleViolation = class extends Error {
567
+ constructor(rule, message) {
568
+ super(message);
569
+ this.rule = rule;
570
+ this.name = "RuleViolation";
571
+ }
572
+ rule;
573
+ code = "rule_violation";
574
+ };
575
+ function matches(rule, toolName, subject) {
576
+ if (rule.tools && toolName !== void 0 && !rule.tools.includes(toolName)) return false;
577
+ if (rule.pattern !== void 0) {
578
+ try {
579
+ return new RegExp(rule.pattern, "i").test(subject);
580
+ } catch {
581
+ return false;
582
+ }
583
+ }
584
+ return true;
585
+ }
586
+ function makeRuleGuard(rules, hooks, bus, runId) {
587
+ const at = (point) => rules.filter((r) => r.enforce === point);
588
+ const fire = (rule, point) => {
589
+ bus.emit({ type: "rule_triggered", rule: rule.name, point, action: rule.action ?? "block", runId });
590
+ };
591
+ async function decide(decision, label) {
592
+ const d = await decision;
593
+ if (d && !d.allow) throw new RuleViolation(label, d.message);
594
+ }
595
+ return {
596
+ async checkPrompt(text) {
597
+ for (const rule of at("before_prompt")) {
598
+ if (!matches(rule, void 0, text)) continue;
599
+ fire(rule, "before_prompt");
600
+ if ((rule.action ?? "block") === "block") throw new RuleViolation(rule.name, rule.message);
601
+ }
602
+ await decide(hooks?.beforePrompt?.(text), "beforePrompt hook");
603
+ },
604
+ wrapTool(tool) {
605
+ return {
606
+ ...tool,
607
+ execute: async (args, signal) => {
608
+ const argsText = JSON.stringify(args);
609
+ for (const rule of at("before_tool")) {
610
+ if (!matches(rule, tool.name, argsText)) continue;
611
+ fire(rule, "before_tool");
612
+ if ((rule.action ?? "block") === "block") throw new RuleViolation(rule.name, rule.message);
613
+ }
614
+ await decide(hooks?.beforeTool?.(tool.name, args), "beforeTool hook");
615
+ const result = await tool.execute(args, signal);
616
+ for (const rule of at("after_tool")) {
617
+ if (!matches(rule, tool.name, result)) continue;
618
+ fire(rule, "after_tool");
619
+ if ((rule.action ?? "block") === "block") throw new RuleViolation(rule.name, rule.message);
620
+ }
621
+ await decide(hooks?.afterTool?.(tool.name, result), "afterTool hook");
622
+ return result;
623
+ }
624
+ };
625
+ }
626
+ };
627
+ }
628
+
629
+ // src/types.ts
630
+ function emptyUsage() {
631
+ return { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, totalTokens: 0, costTotal: 0 };
632
+ }
633
+ function addUsage(a, b) {
634
+ return {
635
+ inputTokens: a.inputTokens + b.inputTokens,
636
+ outputTokens: a.outputTokens + b.outputTokens,
637
+ cacheReadTokens: a.cacheReadTokens + b.cacheReadTokens,
638
+ cacheWriteTokens: a.cacheWriteTokens + b.cacheWriteTokens,
639
+ totalTokens: a.totalTokens + b.totalTokens,
640
+ costTotal: a.costTotal + b.costTotal
641
+ };
642
+ }
643
+
644
+ // src/engine-pi.ts
645
+ import {
646
+ getEnvApiKey,
647
+ getModel,
648
+ registerBuiltInApiProviders,
649
+ stream
650
+ } from "@earendil-works/pi-ai";
651
+ var providersReady = false;
652
+ function ensureProviders() {
653
+ if (!providersReady) {
654
+ registerBuiltInApiProviders();
655
+ providersReady = true;
656
+ }
657
+ }
658
+ var MAX_ITERATIONS = 50;
659
+ var PiAgentEngine = class {
660
+ async run(input, hooks) {
661
+ const { provider, modelId } = splitModel(input.model);
662
+ if (provider === "cloudflare") {
663
+ return runCloudflareAiBinding(input, hooks, modelId);
664
+ }
665
+ ensureProviders();
666
+ const override = input.providers?.[provider];
667
+ let model = resolveModel(provider, modelId, override);
668
+ model = applyProviderRuntimeSettings(model, override, input.env);
669
+ const registeredApiKey = getRegisteredApiKey(provider);
670
+ const apiKey = input.apiKey ?? override?.apiKey ?? registeredApiKey ?? envApiKeyFor(provider, input.env) ?? getEnvApiKey(provider);
671
+ const keyEnvNames = providerApiKeyEnvNames(provider);
672
+ if (!apiKey && requiresHostedProviderApiKey(provider, override, registeredApiKey)) {
673
+ const message2 = providerKeyMissingMessage(provider, input.model, keyEnvNames);
674
+ hooks.bus.emit({ type: "run_error", code: "provider_api_key_missing", message: message2, runId: hooks.runId, session: hooks.session });
675
+ throw new EngineError("provider_api_key_missing", message2);
676
+ }
677
+ const context = {
678
+ systemPrompt: input.instructions,
679
+ messages: input.messages.map((m) => toPiMessage(m, model)),
680
+ tools: input.tools.map(toPiTool)
681
+ };
682
+ const options = {
683
+ apiKey,
684
+ signal: input.signal,
685
+ sessionId: input.sessionId,
686
+ cacheRetention: input.cacheRetention ?? "short",
687
+ ...thinkingOptions(input.thinkingLevel)
688
+ };
689
+ const toolMap = new Map(input.tools.map((t) => [t.name, t]));
690
+ const appended = [];
691
+ let totalUsage = emptyUsage();
692
+ let finalText = "";
693
+ await resumePendingPiToolCalls(input.messages, toolMap, input.signal, hooks, context, appended, () => totalUsage);
694
+ for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
695
+ const turnId = `turn_${iteration}`;
696
+ hooks.bus.emit({ type: "turn_start", turnId, model: input.model, runId: hooks.runId, session: hooks.session });
697
+ let assistant;
698
+ try {
699
+ const handle = stream(model, context, options);
700
+ for await (const ev of handle) {
701
+ if (ev.type === "text_delta") {
702
+ hooks.bus.emit({ type: "text_delta", turnId, delta: ev.delta, runId: hooks.runId, session: hooks.session });
703
+ } else if (ev.type === "thinking_delta") {
704
+ hooks.bus.emit({ type: "thinking_delta", turnId, delta: ev.delta, runId: hooks.runId, session: hooks.session });
705
+ }
706
+ }
707
+ assistant = await handle.result();
708
+ } catch (err) {
709
+ const providerError = providerCredentialError(provider, input.model, keyEnvNames, err);
710
+ if (providerError) {
711
+ hooks.bus.emit({ type: "run_error", code: providerError.code, message: providerError.message, runId: hooks.runId, session: hooks.session });
712
+ throw providerError;
713
+ }
714
+ const message2 = err instanceof Error ? err.message : String(err);
715
+ hooks.bus.emit({ type: "run_error", code: "model_call_failed", message: message2, runId: hooks.runId, session: hooks.session });
716
+ throw new EngineError("model_call_failed", message2);
717
+ }
718
+ if (assistant.stopReason === "error") {
719
+ const message2 = assistant.errorMessage ?? "model returned an error";
720
+ const providerError = providerCredentialError(provider, input.model, keyEnvNames, message2);
721
+ if (providerError) {
722
+ hooks.bus.emit({ type: "run_error", code: providerError.code, message: providerError.message, runId: hooks.runId, session: hooks.session });
723
+ throw providerError;
724
+ }
725
+ hooks.bus.emit({ type: "run_error", code: "model_error", message: message2, runId: hooks.runId, session: hooks.session });
726
+ throw new EngineError("model_error", message2);
727
+ }
728
+ context.messages.push(assistant);
729
+ const ourAssistant = fromPiAssistant(assistant);
730
+ appended.push(ourAssistant);
731
+ totalUsage = addPiUsage(totalUsage, assistant.usage);
732
+ hooks.bus.emit({
733
+ type: "turn_end",
734
+ turnId,
735
+ usage: mapUsage(assistant.usage),
736
+ stopReason: assistant.stopReason,
737
+ runId: hooks.runId,
738
+ session: hooks.session
739
+ });
740
+ finalText = textOf(ourAssistant);
741
+ const toolCalls = assistant.content.filter((c) => c.type === "toolCall");
742
+ if (toolCalls.length === 0 || assistant.stopReason !== "toolUse") {
743
+ return { appended, text: finalText, usage: totalUsage };
744
+ }
745
+ for (const call of toolCalls) {
746
+ await runPiToolCall(call, toolMap, input.signal, hooks, context, appended, () => totalUsage);
747
+ }
748
+ }
749
+ const message = `agent loop exceeded ${MAX_ITERATIONS} iterations without finishing`;
750
+ hooks.bus.emit({ type: "run_error", code: "max_iterations", message, runId: hooks.runId, session: hooks.session });
751
+ throw new EngineError("max_iterations", message);
752
+ }
753
+ };
754
+ async function runCloudflareAiBinding(input, hooks, modelId) {
755
+ const ai = workersAiBinding(input.env);
756
+ if (!ai) {
757
+ const message2 = 'Cloudflare Workers AI binding unavailable. Models prefixed with "cloudflare/" require a Worker env.AI binding; use "cloudflare-workers-ai/" for HTTP/API-token access.';
758
+ hooks.bus.emit({ type: "run_error", code: "workers_ai_binding_unavailable", message: message2, runId: hooks.runId, session: hooks.session });
759
+ throw new EngineError("workers_ai_binding_unavailable", message2);
760
+ }
761
+ const toolMap = new Map(input.tools.map((t) => [t.name, t]));
762
+ const appended = [];
763
+ const messages = toWorkersAiMessages(input.instructions, input.messages);
764
+ let totalUsage = emptyUsage();
765
+ let finalText = "";
766
+ await resumePendingWorkersToolCalls(input.messages, toolMap, input.signal, hooks, messages, appended, () => totalUsage);
767
+ for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
768
+ const turnId = `turn_${iteration}`;
769
+ hooks.bus.emit({ type: "turn_start", turnId, model: input.model, runId: hooks.runId, session: hooks.session });
770
+ let parsed;
771
+ try {
772
+ const raw = await ai.run(
773
+ modelId,
774
+ {
775
+ messages,
776
+ ...input.tools.length ? { tools: input.tools.map(toWorkersAiTool), tool_choice: workersAiToolChoice(input.toolChoice, messages), parallel_tool_calls: true } : {}
777
+ },
778
+ { signal: input.signal }
779
+ );
780
+ parsed = await parseWorkersAiResponse(raw);
781
+ } catch (err) {
782
+ const message2 = err instanceof Error ? err.message : String(err);
783
+ hooks.bus.emit({ type: "run_error", code: "workers_ai_binding_failed", message: message2, runId: hooks.runId, session: hooks.session });
784
+ throw new EngineError("workers_ai_binding_failed", message2);
785
+ }
786
+ totalUsage = addUsage(totalUsage, parsed.usage);
787
+ if (parsed.text) hooks.bus.emit({ type: "text_delta", turnId, delta: parsed.text, runId: hooks.runId, session: hooks.session });
788
+ const assistant = {
789
+ role: "assistant",
790
+ content: [
791
+ ...parsed.text ? [{ type: "text", text: parsed.text }] : [],
792
+ ...parsed.toolCalls.map((call) => ({ type: "tool_call", id: call.id, name: call.name, arguments: call.arguments }))
793
+ ],
794
+ usage: parsed.usage,
795
+ stopReason: parsed.toolCalls.length ? "tool_use" : parsed.stopReason
796
+ };
797
+ appended.push(assistant);
798
+ messages.push(toWorkersAiAssistantMessage(parsed));
799
+ finalText = parsed.text;
800
+ hooks.bus.emit({
801
+ type: "turn_end",
802
+ turnId,
803
+ usage: parsed.usage,
804
+ stopReason: parsed.toolCalls.length ? "tool_use" : parsed.stopReason,
805
+ runId: hooks.runId,
806
+ session: hooks.session
807
+ });
808
+ if (!parsed.toolCalls.length) return { appended, text: finalText, usage: totalUsage };
809
+ for (const call of parsed.toolCalls) {
810
+ await runWorkersToolCall(call, toolMap, input.signal, hooks, messages, appended, () => totalUsage);
811
+ }
812
+ }
813
+ const message = `Workers AI binding agent loop exceeded ${MAX_ITERATIONS} iterations without finishing`;
814
+ hooks.bus.emit({ type: "run_error", code: "max_iterations", message, runId: hooks.runId, session: hooks.session });
815
+ throw new EngineError("max_iterations", message);
816
+ }
817
+ function envApiKeyFor(provider, env) {
818
+ if (!env) return void 0;
819
+ for (const name of providerApiKeyEnvNames(provider)) {
820
+ const value = env[name];
821
+ if (typeof value === "string" && value) return value;
822
+ }
823
+ return void 0;
824
+ }
825
+ function providerApiKeyEnvNames(provider) {
826
+ const names = {
827
+ anthropic: ["ANTHROPIC_API_KEY"],
828
+ openai: ["OPENAI_API_KEY"],
829
+ google: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
830
+ "cloudflare-workers-ai": ["CLOUDFLARE_API_KEY"],
831
+ "cloudflare-ai-gateway": ["CLOUDFLARE_API_KEY"],
832
+ groq: ["GROQ_API_KEY"],
833
+ openrouter: ["OPENROUTER_API_KEY"],
834
+ xai: ["XAI_API_KEY"],
835
+ mistral: ["MISTRAL_API_KEY"],
836
+ cerebras: ["CEREBRAS_API_KEY"]
837
+ };
838
+ return names[provider] ?? [`${provider.toUpperCase().replace(/-/g, "_")}_API_KEY`];
839
+ }
840
+ var HOSTED_PROVIDERS_REQUIRING_API_KEYS = /* @__PURE__ */ new Set([
841
+ "anthropic",
842
+ "openai",
843
+ "google",
844
+ "cloudflare-workers-ai",
845
+ "cloudflare-ai-gateway",
846
+ "groq",
847
+ "openrouter",
848
+ "xai",
849
+ "mistral",
850
+ "cerebras"
851
+ ]);
852
+ function requiresHostedProviderApiKey(provider, settings, registeredApiKey) {
853
+ if (registeredApiKey) return false;
854
+ if (!HOSTED_PROVIDERS_REQUIRING_API_KEYS.has(provider)) return false;
855
+ if (settings?.baseUrl || settings?.api || settings?.models) return false;
856
+ const headers = settings?.headers ?? {};
857
+ return !Object.keys(headers).some((key) => key.toLowerCase() === "authorization" || key.toLowerCase() === "x-api-key");
858
+ }
859
+ function providerKeyMissingMessage(provider, model, envNames) {
860
+ const envList = envNames.join(" or ");
861
+ return `Model provider "${provider}" requires ${envList} for model "${model}". Set ${envNames[0]} in the environment or pass providers["${provider}"].apiKey.`;
862
+ }
863
+ function providerCredentialError(provider, model, envNames, err) {
864
+ const message = err instanceof Error ? err.message : String(err);
865
+ if (!looksLikeProviderAuthError(message)) return void 0;
866
+ const envList = envNames.join(" or ");
867
+ return new EngineError(
868
+ "provider_api_key_rejected",
869
+ `Model provider "${provider}" rejected the API key for model "${model}". Check ${envList} or providers["${provider}"].apiKey.`
870
+ );
871
+ }
872
+ function looksLikeProviderAuthError(message) {
873
+ const normalized = message.toLowerCase();
874
+ return normalized.includes("401") || normalized.includes("unauthorized") || normalized.includes("authentication_error") || normalized.includes("invalid_api_key") || normalized.includes("invalid x-api-key") || normalized.includes("invalid api key");
875
+ }
876
+ var EngineError = class extends Error {
877
+ constructor(code, message) {
878
+ super(message);
879
+ this.code = code;
880
+ this.name = "EngineError";
881
+ }
882
+ code;
883
+ };
884
+ function thinkingOptions(level) {
885
+ if (!level || level === "off") return {};
886
+ const effort = level === "minimal" ? "low" : level;
887
+ return { thinkingEnabled: true, effort };
888
+ }
889
+ async function runTool(tool, call, signal) {
890
+ if (!tool) return { text: `Error: unknown tool "${call.name}"`, isError: true };
891
+ try {
892
+ return { text: await tool.execute(call.arguments, signal), isError: false };
893
+ } catch (err) {
894
+ if (isToolApprovalRequiredError(err)) throw err;
895
+ return { text: `Error: ${err instanceof Error ? err.message : String(err)}`, isError: true };
896
+ }
897
+ }
898
+ async function resumePendingPiToolCalls(messages, toolMap, signal, hooks, context, appended, usage) {
899
+ for (const call of pendingToolCalls(messages)) {
900
+ await runPiToolCall(call, toolMap, signal, hooks, context, appended, usage);
901
+ }
902
+ }
903
+ async function runPiToolCall(call, toolMap, signal, hooks, context, appended, usage) {
904
+ hooks.bus.emit({ type: "tool_call", tool: call.name, callId: call.id, arguments: call.arguments, runId: hooks.runId, session: hooks.session });
905
+ let ran;
906
+ try {
907
+ ran = await runTool(toolMap.get(call.name), call, signal);
908
+ } catch (err) {
909
+ attachToolApprovalPartialTranscript(err, { appended: [...appended], usage: usage() });
910
+ throw err;
911
+ }
912
+ const result = {
913
+ role: "toolResult",
914
+ toolCallId: call.id,
915
+ toolName: call.name,
916
+ content: [{ type: "text", text: ran.text }],
917
+ isError: ran.isError,
918
+ timestamp: Date.now()
919
+ };
920
+ context.messages.push(result);
921
+ appended.push({ role: "tool_result", toolCallId: call.id, toolName: call.name, content: [{ type: "text", text: ran.text }], isError: ran.isError });
922
+ hooks.bus.emit({ type: "tool_result", tool: call.name, callId: call.id, ok: !ran.isError, runId: hooks.runId, session: hooks.session });
923
+ }
924
+ async function resumePendingWorkersToolCalls(inputMessages, toolMap, signal, hooks, messages, appended, usage) {
925
+ for (const call of pendingToolCalls(inputMessages)) {
926
+ await runWorkersToolCall(call, toolMap, signal, hooks, messages, appended, usage);
927
+ }
928
+ }
929
+ async function runWorkersToolCall(call, toolMap, signal, hooks, messages, appended, usage) {
930
+ hooks.bus.emit({ type: "tool_call", tool: call.name, callId: call.id, arguments: call.arguments, runId: hooks.runId, session: hooks.session });
931
+ let ran;
932
+ try {
933
+ ran = await runTool(toolMap.get(call.name), call, signal);
934
+ } catch (err) {
935
+ attachToolApprovalPartialTranscript(err, { appended: [...appended], usage: usage() });
936
+ throw err;
937
+ }
938
+ const result = { role: "tool_result", toolCallId: call.id, toolName: call.name, content: [{ type: "text", text: ran.text }], isError: ran.isError };
939
+ appended.push(result);
940
+ messages.push({ role: "tool", tool_call_id: call.id, name: call.name, content: ran.text });
941
+ hooks.bus.emit({ type: "tool_result", tool: call.name, callId: call.id, ok: !ran.isError, runId: hooks.runId, session: hooks.session });
942
+ }
943
+ function pendingToolCalls(messages) {
944
+ for (let index = messages.length - 1; index >= 0; index--) {
945
+ const message = messages[index];
946
+ if (message.role === "tool_result") continue;
947
+ if (message.role !== "assistant") return [];
948
+ const calls = message.content.filter((part) => part.type === "tool_call").map((part) => ({ id: part.id, name: part.name, arguments: part.arguments }));
949
+ if (!calls.length) return [];
950
+ const resultIds = new Set(
951
+ messages.slice(index + 1).filter((candidate) => candidate.role === "tool_result").map((candidate) => candidate.toolCallId)
952
+ );
953
+ return calls.filter((call) => !resultIds.has(call.id));
954
+ }
955
+ return [];
956
+ }
957
+ function workersAiBinding(env) {
958
+ const ai = env?.AI;
959
+ return typeof ai === "object" && ai !== null && typeof ai.run === "function" ? ai : void 0;
960
+ }
961
+ function workersAiToolChoice(choice, messages) {
962
+ if (choice === "required") return messages.some((message) => message.role === "tool") ? "auto" : "required";
963
+ return choice ?? "auto";
964
+ }
965
+ function toWorkersAiMessages(instructions, messages) {
966
+ const out = [];
967
+ if (instructions) out.push({ role: "system", content: instructions });
968
+ for (const message of messages) {
969
+ if (message.role === "user") {
970
+ out.push({ role: "user", content: workersAiUserContent(message.content) });
971
+ } else if (message.role === "assistant") {
972
+ out.push(toWorkersAiAssistantMessage({
973
+ text: textOf(message),
974
+ toolCalls: message.content.filter((part) => part.type === "tool_call").map((part) => ({ id: part.id, name: part.name, arguments: part.arguments }))
975
+ }));
976
+ } else {
977
+ out.push({
978
+ role: "tool",
979
+ tool_call_id: message.toolCallId,
980
+ name: message.toolName,
981
+ content: message.content.filter((part) => part.type === "text").map((part) => part.text).join("\n")
982
+ });
983
+ }
984
+ }
985
+ return out;
986
+ }
987
+ function workersAiUserContent(content) {
988
+ const hasImage = content.some((part) => part.type === "image");
989
+ if (!hasImage) return content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
990
+ return content.map((part) => {
991
+ if (part.type === "text") return { type: "text", text: part.text };
992
+ return { type: "image_url", image_url: { url: `data:${part.mimeType};base64,${part.data}` } };
993
+ });
994
+ }
995
+ function toWorkersAiAssistantMessage(parsed) {
996
+ return {
997
+ role: "assistant",
998
+ content: parsed.text || "",
999
+ ...parsed.toolCalls.length ? { tool_calls: parsed.toolCalls.map(toWorkersAiToolCallWire) } : {}
1000
+ };
1001
+ }
1002
+ function toWorkersAiTool(tool) {
1003
+ return {
1004
+ type: "function",
1005
+ function: {
1006
+ name: tool.name,
1007
+ description: tool.description,
1008
+ parameters: tool.parameters,
1009
+ strict: false
1010
+ }
1011
+ };
1012
+ }
1013
+ function toWorkersAiToolCallWire(call) {
1014
+ return { id: call.id, type: "function", function: { name: call.name, arguments: JSON.stringify(call.arguments) } };
1015
+ }
1016
+ async function parseWorkersAiResponse(raw) {
1017
+ const response = raw instanceof Response ? await raw.json().catch(() => ({})) : raw;
1018
+ const body = objectValue(response) ?? {};
1019
+ const result = objectValue(body.result) ?? body;
1020
+ const choice = Array.isArray(result.choices) ? objectValue(result.choices[0]) : void 0;
1021
+ const message = objectValue(choice?.message) ?? objectValue(choice?.delta);
1022
+ const text = textFromWorkersAiContent(message?.content) ?? textFromWorkersAiContent(result.response) ?? textFromWorkersAiContent(result.text) ?? "";
1023
+ const toolCalls = parseWorkersAiToolCalls(message?.tool_calls ?? choice?.tool_calls ?? result.tool_calls);
1024
+ const usage = workersAiUsage(objectValue(result.usage) ?? objectValue(body.usage));
1025
+ const stopReason = workersAiStopReason(choice?.finish_reason);
1026
+ if (!text && toolCalls.length === 0) throw new Error(`Cloudflare AI binding returned no text or tool calls: ${safeJson(response)}`);
1027
+ return { text, toolCalls, usage, stopReason };
1028
+ }
1029
+ function objectValue(value) {
1030
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1031
+ }
1032
+ function textFromWorkersAiContent(value) {
1033
+ if (typeof value === "string") return value;
1034
+ if (!Array.isArray(value)) return void 0;
1035
+ return value.map((part) => {
1036
+ const obj = objectValue(part);
1037
+ return obj?.type === "text" && typeof obj.text === "string" ? obj.text : "";
1038
+ }).join("");
1039
+ }
1040
+ function parseWorkersAiToolCalls(value) {
1041
+ if (!Array.isArray(value)) return [];
1042
+ return value.flatMap((entry, index) => {
1043
+ const obj = objectValue(entry);
1044
+ if (!obj) return [];
1045
+ const fn = objectValue(obj.function) ?? obj;
1046
+ const name = typeof fn.name === "string" ? fn.name : void 0;
1047
+ if (!name) return [];
1048
+ return [
1049
+ {
1050
+ id: typeof obj.id === "string" && obj.id ? obj.id : `call_${index}`,
1051
+ name,
1052
+ arguments: parseToolArguments(fn.arguments ?? obj.arguments)
1053
+ }
1054
+ ];
1055
+ });
1056
+ }
1057
+ function parseToolArguments(value) {
1058
+ if (typeof value === "string") {
1059
+ try {
1060
+ const parsed = JSON.parse(value);
1061
+ return objectValue(parsed) ?? {};
1062
+ } catch {
1063
+ return {};
1064
+ }
1065
+ }
1066
+ return objectValue(value) ?? {};
1067
+ }
1068
+ function workersAiUsage(value) {
1069
+ const input = numberValue(value?.input_tokens) ?? numberValue(value?.prompt_tokens) ?? numberValue(value?.input) ?? 0;
1070
+ const output = numberValue(value?.output_tokens) ?? numberValue(value?.completion_tokens) ?? numberValue(value?.output) ?? 0;
1071
+ const cacheRead = numberValue(objectValue(value?.prompt_tokens_details)?.cached_tokens) ?? numberValue(value?.cache_read_tokens) ?? 0;
1072
+ const cacheWrite = numberValue(objectValue(value?.prompt_tokens_details)?.cache_write_tokens) ?? numberValue(value?.cache_write_tokens) ?? 0;
1073
+ return {
1074
+ inputTokens: input,
1075
+ outputTokens: output,
1076
+ cacheReadTokens: cacheRead,
1077
+ cacheWriteTokens: cacheWrite,
1078
+ totalTokens: numberValue(value?.total_tokens) ?? input + output + cacheRead + cacheWrite,
1079
+ costTotal: 0
1080
+ };
1081
+ }
1082
+ function numberValue(value) {
1083
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1084
+ }
1085
+ function workersAiStopReason(reason) {
1086
+ if (reason === "length") return "length";
1087
+ if (reason === "tool_calls" || reason === "function_call") return "tool_use";
1088
+ if (reason === "content_filter") return "error";
1089
+ return "stop";
1090
+ }
1091
+ function safeJson(value) {
1092
+ try {
1093
+ return JSON.stringify(value);
1094
+ } catch {
1095
+ return String(value);
1096
+ }
1097
+ }
1098
+ function splitModel(ref) {
1099
+ const slash = ref.indexOf("/");
1100
+ if (slash === -1) throw new EngineError("invalid_model", `Model must be "<provider>/<model-id>", got "${ref}"`);
1101
+ return { provider: ref.slice(0, slash), modelId: ref.slice(slash + 1) };
1102
+ }
1103
+ function resolveModel(provider, modelId, settings) {
1104
+ const registered = resolveRegisteredModel(provider, modelId);
1105
+ if (registered) return registered;
1106
+ const model = getModel(provider, modelId);
1107
+ if (model) return model;
1108
+ const custom = settings?.models?.[modelId];
1109
+ const api = custom?.api ?? settings?.api;
1110
+ if (!custom || !api) {
1111
+ throw new EngineError(
1112
+ "unknown_model",
1113
+ `Unknown model "${provider}/${modelId}". For custom providers, configure providers["${provider}"].models["${modelId}"] with an api.`
1114
+ );
1115
+ }
1116
+ return {
1117
+ id: modelId,
1118
+ name: custom.name ?? modelId,
1119
+ api,
1120
+ provider,
1121
+ baseUrl: custom.baseUrl ?? settings?.baseUrl ?? "",
1122
+ reasoning: custom.reasoning ?? false,
1123
+ thinkingLevelMap: custom.thinkingLevelMap,
1124
+ input: custom.input ?? ["text"],
1125
+ cost: {
1126
+ input: custom.cost?.input ?? 0,
1127
+ output: custom.cost?.output ?? 0,
1128
+ cacheRead: custom.cost?.cacheRead ?? 0,
1129
+ cacheWrite: custom.cost?.cacheWrite ?? 0
1130
+ },
1131
+ contextWindow: custom.contextWindow ?? 128e3,
1132
+ maxTokens: custom.maxTokens ?? 16384,
1133
+ headers: { ...settings?.headers ?? {}, ...custom.headers ?? {} },
1134
+ compat: custom.compat
1135
+ };
1136
+ }
1137
+ function applyProviderRuntimeSettings(model, settings, env) {
1138
+ const accountId = settings?.accountId ?? stringEnv(env, "CLOUDFLARE_ACCOUNT_ID");
1139
+ const baseUrl = expandProviderUrl(settings?.baseUrl ?? model.baseUrl, { CLOUDFLARE_ACCOUNT_ID: accountId });
1140
+ const headers = settings?.headers ? { ...model.headers, ...settings.headers } : model.headers;
1141
+ if (baseUrl === model.baseUrl && headers === model.headers) return model;
1142
+ return { ...model, baseUrl, headers };
1143
+ }
1144
+ function expandProviderUrl(url, values) {
1145
+ let out = url;
1146
+ for (const [key, value] of Object.entries(values)) {
1147
+ if (value) out = out.replaceAll(`{${key}}`, value);
1148
+ }
1149
+ return out;
1150
+ }
1151
+ function stringEnv(env, key) {
1152
+ const value = env?.[key];
1153
+ return typeof value === "string" && value ? value : void 0;
1154
+ }
1155
+ function toPiTool(tool) {
1156
+ return { name: tool.name, description: tool.description, parameters: tool.parameters };
1157
+ }
1158
+ function toPiMessage(message, model) {
1159
+ if (message.role === "user") {
1160
+ return {
1161
+ role: "user",
1162
+ content: message.content.map(
1163
+ (p) => p.type === "text" ? { type: "text", text: p.text } : { type: "image", data: p.data, mimeType: p.mimeType }
1164
+ ),
1165
+ timestamp: Date.now()
1166
+ };
1167
+ }
1168
+ if (message.role === "tool_result") {
1169
+ return {
1170
+ role: "toolResult",
1171
+ toolCallId: message.toolCallId,
1172
+ toolName: message.toolName,
1173
+ content: message.content.map(
1174
+ (p) => p.type === "text" ? { type: "text", text: p.text } : { type: "image", data: p.data, mimeType: p.mimeType }
1175
+ ),
1176
+ isError: message.isError ?? false,
1177
+ timestamp: Date.now()
1178
+ };
1179
+ }
1180
+ const content = message.content.map((p) => {
1181
+ if (p.type === "text") return { type: "text", text: p.text };
1182
+ if (p.type === "thinking") return { type: "thinking", thinking: p.thinking };
1183
+ return { type: "toolCall", id: p.id, name: p.name, arguments: p.arguments };
1184
+ });
1185
+ return {
1186
+ role: "assistant",
1187
+ content,
1188
+ api: model.api,
1189
+ provider: model.provider,
1190
+ model: model.id,
1191
+ usage: zeroPiUsage(),
1192
+ stopReason: toPiStopReason(message.stopReason),
1193
+ timestamp: Date.now()
1194
+ };
1195
+ }
1196
+ function fromPiAssistant(assistant) {
1197
+ return {
1198
+ role: "assistant",
1199
+ content: assistant.content.map((c) => {
1200
+ if (c.type === "text") return { type: "text", text: c.text };
1201
+ if (c.type === "thinking") return { type: "thinking", thinking: c.thinking };
1202
+ return { type: "tool_call", id: c.id, name: c.name, arguments: c.arguments };
1203
+ }),
1204
+ usage: mapUsage(assistant.usage),
1205
+ stopReason: fromPiStopReason(assistant.stopReason)
1206
+ };
1207
+ }
1208
+ function textOf(message) {
1209
+ return message.content.filter((c) => c.type === "text").map((c) => c.text).join("");
1210
+ }
1211
+ function mapUsage(u) {
1212
+ return {
1213
+ inputTokens: u.input,
1214
+ outputTokens: u.output,
1215
+ cacheReadTokens: u.cacheRead,
1216
+ cacheWriteTokens: u.cacheWrite,
1217
+ totalTokens: u.totalTokens,
1218
+ costTotal: u.cost.total
1219
+ };
1220
+ }
1221
+ function addPiUsage(acc, u) {
1222
+ return {
1223
+ inputTokens: acc.inputTokens + u.input,
1224
+ outputTokens: acc.outputTokens + u.output,
1225
+ cacheReadTokens: acc.cacheReadTokens + u.cacheRead,
1226
+ cacheWriteTokens: acc.cacheWriteTokens + u.cacheWrite,
1227
+ totalTokens: acc.totalTokens + u.totalTokens,
1228
+ costTotal: acc.costTotal + u.cost.total
1229
+ };
1230
+ }
1231
+ function zeroPiUsage() {
1232
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } };
1233
+ }
1234
+ function toPiStopReason(reason) {
1235
+ return reason === "tool_use" ? "toolUse" : reason;
1236
+ }
1237
+ function fromPiStopReason(reason) {
1238
+ return reason === "toolUse" ? "tool_use" : reason;
1239
+ }
1240
+
1241
+ // src/events.ts
1242
+ var EventBus = class {
1243
+ #subscribers = /* @__PURE__ */ new Set();
1244
+ #index;
1245
+ /**
1246
+ * `startIndex` seeds event numbering. Recovery re-invokes a run with the same
1247
+ * runId; seeding with the persisted event count keeps the per-run event log
1248
+ * append-only instead of colliding with (or overwriting) attempt one.
1249
+ */
1250
+ constructor(startIndex = 0) {
1251
+ this.#index = startIndex;
1252
+ }
1253
+ subscribe(fn) {
1254
+ this.#subscribers.add(fn);
1255
+ return () => this.#subscribers.delete(fn);
1256
+ }
1257
+ emit(event) {
1258
+ const full = { ...event, index: this.#index++, timestamp: event.timestamp ?? Date.now() };
1259
+ for (const fn of this.#subscribers) {
1260
+ try {
1261
+ fn(full);
1262
+ } catch {
1263
+ }
1264
+ }
1265
+ }
1266
+ };
1267
+ var globalSubscribers = /* @__PURE__ */ new Set();
1268
+ function observe(fn) {
1269
+ globalSubscribers.add(fn);
1270
+ return () => globalSubscribers.delete(fn);
1271
+ }
1272
+ function fanoutToGlobal(event) {
1273
+ for (const fn of globalSubscribers) {
1274
+ try {
1275
+ fn(event);
1276
+ } catch {
1277
+ }
1278
+ }
1279
+ }
1280
+
1281
+ // src/call.ts
1282
+ function makeCall(run, parentSignal) {
1283
+ const controller = new AbortController();
1284
+ let detach;
1285
+ if (parentSignal) {
1286
+ if (parentSignal.aborted) {
1287
+ controller.abort(parentSignal.reason);
1288
+ } else {
1289
+ const onAbort = () => controller.abort(parentSignal.reason);
1290
+ parentSignal.addEventListener("abort", onAbort, { once: true });
1291
+ detach = () => parentSignal.removeEventListener("abort", onAbort);
1292
+ }
1293
+ }
1294
+ const promise = run(controller.signal);
1295
+ promise.then(
1296
+ () => detach?.(),
1297
+ // Swallow here so an unawaited handle never becomes an unhandled rejection;
1298
+ // callers that await still receive the rejection through then() below.
1299
+ () => detach?.()
1300
+ );
1301
+ return {
1302
+ signal: controller.signal,
1303
+ abort: (reason) => controller.abort(reason),
1304
+ then: (onfulfilled, onrejected) => promise.then(onfulfilled, onrejected)
1305
+ };
1306
+ }
1307
+
1308
+ // src/tools.ts
1309
+ import { toJsonSchema } from "@valibot/to-json-schema";
1310
+ import * as v from "valibot";
1311
+ function defineTool(def) {
1312
+ return def;
1313
+ }
1314
+ function rawTool(tool) {
1315
+ return { ...tool, __atlasflowRawTool: true };
1316
+ }
1317
+ function isRawTool(tool) {
1318
+ return typeof tool === "object" && tool !== null && tool.__atlasflowRawTool === true;
1319
+ }
1320
+ function resolveTool(def) {
1321
+ return {
1322
+ name: def.name,
1323
+ description: def.description,
1324
+ parameters: toJsonSchema(def.parameters),
1325
+ approval: normalizeToolApprovalPolicy(def.approval),
1326
+ execute: async (args, signal) => {
1327
+ const parsed = v.parse(def.parameters, args);
1328
+ const result = await def.execute(parsed, signal);
1329
+ return typeof result === "string" ? result : JSON.stringify(result);
1330
+ }
1331
+ };
1332
+ }
1333
+ function resolveAnyTool(tool) {
1334
+ if (isRawTool(tool)) {
1335
+ return { name: tool.name, description: tool.description, parameters: tool.parameters, approval: tool.approval, execute: tool.execute };
1336
+ }
1337
+ return resolveTool(tool);
1338
+ }
1339
+
1340
+ // src/session.ts
1341
+ import { toJsonSchema as toJsonSchema2 } from "@valibot/to-json-schema";
1342
+ import * as v2 from "valibot";
1343
+ var operationCounter = 0;
1344
+ var SESSION_JOURNAL_KEY = "atlasflow:journal";
1345
+ var keyLocks = /* @__PURE__ */ new Map();
1346
+ async function withKeyLock(key, fn) {
1347
+ const prev = keyLocks.get(key) ?? Promise.resolve();
1348
+ const run = prev.then(fn, fn);
1349
+ const tail = run.catch(() => {
1350
+ });
1351
+ keyLocks.set(key, tail);
1352
+ void tail.then(() => {
1353
+ if (keyLocks.get(key) === tail) keyLocks.delete(key);
1354
+ });
1355
+ return run;
1356
+ }
1357
+ function estimateTokens(messages) {
1358
+ let chars = 0;
1359
+ for (const m of messages) chars += JSON.stringify(m.content).length;
1360
+ return Math.ceil(chars / 4);
1361
+ }
1362
+ var DEFAULT_COMPACTION = {
1363
+ thresholdTokens: 12e4,
1364
+ keepRecent: 10
1365
+ };
1366
+ var Session = class _Session {
1367
+ name;
1368
+ fs;
1369
+ #ctx;
1370
+ #key;
1371
+ constructor(name, ctx) {
1372
+ this.name = name;
1373
+ this.#ctx = ctx;
1374
+ this.#key = `${ctx.agentName}::${ctx.instanceId}::${name}`;
1375
+ this.fs = ctx.env ? makeFs(ctx.env) : noFs();
1376
+ }
1377
+ prompt(text, options) {
1378
+ return makeCall((signal) => this.#run(text, options ?? {}, "prompt", signal), options?.signal);
1379
+ }
1380
+ skill(ref, options) {
1381
+ const skill = typeof ref === "string" ? this.#ctx.skills.get(ref) : ref;
1382
+ if (!skill) throw new Error(`Unknown skill "${typeof ref === "string" ? ref : ref.name}"`);
1383
+ return makeCall(async (signal) => {
1384
+ if (!isWorkspaceSkill(skill)) return this.#run(skill.body, options ?? {}, "skill", signal);
1385
+ if (!this.#ctx.env) throw new Error(`Workspace skill "${skill.name}" requires a sandbox environment.`);
1386
+ const loaded = await loadWorkspaceSkill(this.#ctx.env, skill);
1387
+ return this.#run(loaded.body, options ?? {}, "skill", signal);
1388
+ }, options?.signal);
1389
+ }
1390
+ task(text, options) {
1391
+ return makeCall((signal) => {
1392
+ const sub = new _Session(`${this.name}/sub${++operationCounter}`, this.#ctx);
1393
+ return sub.#run(text, options ?? {}, "task", signal);
1394
+ }, options?.signal);
1395
+ }
1396
+ /** Branch this conversation: copy the current history into a new named session. */
1397
+ async fork(name) {
1398
+ const data = await this.#load();
1399
+ const sub = new _Session(name, this.#ctx);
1400
+ await this.#ctx.store.put(sub.#key, { ...structuredClone(data), name });
1401
+ return sub;
1402
+ }
1403
+ shell(command, options) {
1404
+ return makeCall(async (signal) => {
1405
+ if (!this.#ctx.env) throw new Error("No sandbox configured for this agent (sandbox: false).");
1406
+ const env = await createScopedSessionEnv(this.#ctx.env, mergeCommands(this.#ctx.commands, options?.commands));
1407
+ return env.exec(command, { cwd: options?.cwd, timeoutMs: options?.timeoutMs, signal });
1408
+ }, options?.signal);
1409
+ }
1410
+ async delete() {
1411
+ await this.#ctx.store.delete(this.#key);
1412
+ }
1413
+ /** Manually compact this session's history, keeping the most recent messages. */
1414
+ async compact(keepRecent = 4) {
1415
+ const model = this.#ctx.model;
1416
+ if (!model) return;
1417
+ const data = await this.#load();
1418
+ await this.#compact(data, model, keepRecent, void 0);
1419
+ await this.#ctx.store.put(this.#key, data);
1420
+ }
1421
+ async #maybeCompact(data, model, signal) {
1422
+ if (this.#ctx.compaction === false || !model) return;
1423
+ const cfg = this.#ctx.compaction ?? {};
1424
+ const overMessages = cfg.threshold != null && data.messages.length > cfg.threshold;
1425
+ const overTokens = estimateTokens(data.messages) > (cfg.thresholdTokens ?? DEFAULT_COMPACTION.thresholdTokens);
1426
+ if (!overMessages && !overTokens) return;
1427
+ await this.#compact(data, cfg.model ?? model, cfg.keepRecent ?? DEFAULT_COMPACTION.keepRecent, signal);
1428
+ await this.#ctx.store.put(this.#key, data);
1429
+ }
1430
+ /** Summarize all but the most recent `keepRecent` messages into one note. */
1431
+ async #compact(data, model, keepRecent, signal) {
1432
+ if (data.messages.length <= keepRecent) return;
1433
+ const older = data.messages.slice(0, data.messages.length - keepRecent);
1434
+ const recent = data.messages.slice(data.messages.length - keepRecent);
1435
+ const transcript = older.map(renderMessage).join("\n");
1436
+ const summary = await this.#ctx.engine.run(
1437
+ {
1438
+ model,
1439
+ instructions: "Summarize the following conversation concisely, preserving key facts, decisions, and open threads. Output only the summary.",
1440
+ messages: [{ role: "user", content: [{ type: "text", text: transcript }] }],
1441
+ tools: [],
1442
+ signal,
1443
+ apiKey: this.#ctx.apiKey,
1444
+ env: this.#ctx.envVars,
1445
+ providers: this.#ctx.providers
1446
+ },
1447
+ { bus: this.#ctx.bus, runId: this.#ctx.runId, session: this.name }
1448
+ );
1449
+ const before = estimateTokens(older);
1450
+ data.messages = [{ role: "user", content: [{ type: "text", text: `[Summary of earlier conversation]
1451
+ ${summary.text}` }] }, ...recent];
1452
+ data.updatedAt = Date.now();
1453
+ const freedTokens = Math.max(0, before - Math.ceil(summary.text.length / 4));
1454
+ this.#ctx.bus.emit({ type: "compaction", freedTokens, runId: this.#ctx.runId, session: this.name });
1455
+ }
1456
+ async #run(text, options, kind, signal) {
1457
+ const ctx = this.#ctx;
1458
+ const operationId = `op_${++operationCounter}`;
1459
+ ctx.bus.emit({ type: "operation_start", operationId, kind, runId: ctx.runId, session: this.name });
1460
+ try {
1461
+ const result = kind === "task" ? await this.#runInner(text, options, kind, signal, operationId) : await withKeyLock(this.#key, () => this.#runInner(text, options, kind, signal, operationId));
1462
+ ctx.bus.emit({ type: "operation_end", operationId, ok: true, runId: ctx.runId, session: this.name });
1463
+ return result;
1464
+ } catch (err) {
1465
+ ctx.bus.emit({ type: "operation_end", operationId, ok: false, runId: ctx.runId, session: this.name });
1466
+ throw err;
1467
+ }
1468
+ }
1469
+ async #runInner(text, options, kind, signal, operationId) {
1470
+ const ctx = this.#ctx;
1471
+ const model = options.model ?? ctx.model;
1472
+ if (!model) throw new Error("No model configured. Set `model` in the agent config or pass it per-call.");
1473
+ const data = kind === "task" ? { name: this.name, messages: [], metadata: {}, createdAt: Date.now(), updatedAt: Date.now() } : await this.#load();
1474
+ const replayEntry = kind === "task" ? void 0 : findReplayableJournalEntry(data, ctx.runId, kind, { includeApprovalParked: true });
1475
+ if (kind !== "task" && !replayEntry) await this.#maybeCompact(data, model, signal);
1476
+ const userParts = [{ type: "text", text }];
1477
+ for (const img of options.images ?? []) userParts.push({ type: "image", data: img.data, mimeType: img.mimeType });
1478
+ const userMessage = { role: "user", content: userParts };
1479
+ const callCommands = mergeCommands(ctx.commands, options.commands);
1480
+ if (callCommands.length && !ctx.env) throw new Error("Cannot use commands without a sandbox environment.");
1481
+ const callEnv = ctx.env ? await createScopedSessionEnv(ctx.env, callCommands) : void 0;
1482
+ const tools = [
1483
+ ...ctx.tools,
1484
+ ...callEnv && ctx.sandboxTools ? ctx.sandboxTools(callEnv) : [],
1485
+ ...(options.tools ?? []).map(resolveTool)
1486
+ ];
1487
+ const instructions = this.#composeInstructions(options.result);
1488
+ let journalOperationId = operationId;
1489
+ if (kind !== "task" && replayEntry) {
1490
+ journalOperationId = replayEntry.operationId;
1491
+ markJournalReplay(data, replayEntry.operationId);
1492
+ data.updatedAt = Date.now();
1493
+ await ctx.store.put(this.#key, data);
1494
+ } else if (kind !== "task") {
1495
+ const inputStart = data.messages.length;
1496
+ data.messages.push(userMessage);
1497
+ beginJournalEntry(data, {
1498
+ operationId,
1499
+ kind,
1500
+ status: "running",
1501
+ runId: ctx.runId,
1502
+ session: this.name,
1503
+ startedAt: Date.now(),
1504
+ inputStart,
1505
+ inputCount: 1,
1506
+ outputStart: data.messages.length,
1507
+ outputCount: 0
1508
+ });
1509
+ data.updatedAt = Date.now();
1510
+ await ctx.store.put(this.#key, data);
1511
+ }
1512
+ let result;
1513
+ try {
1514
+ result = await ctx.engine.run(
1515
+ {
1516
+ model,
1517
+ instructions,
1518
+ messages: kind === "task" ? [userMessage] : [...data.messages],
1519
+ tools,
1520
+ thinkingLevel: options.thinkingLevel ?? ctx.thinkingLevel,
1521
+ toolChoice: options.toolChoice ?? ctx.toolChoice,
1522
+ signal,
1523
+ apiKey: ctx.apiKey,
1524
+ env: ctx.envVars,
1525
+ providers: ctx.providers,
1526
+ sessionId: this.#key
1527
+ },
1528
+ { bus: ctx.bus, runId: ctx.runId, session: this.name }
1529
+ );
1530
+ if (kind !== "task") {
1531
+ appendJournalOutput(data, journalOperationId, result.appended, result.usage);
1532
+ data.updatedAt = Date.now();
1533
+ await ctx.store.put(this.#key, data);
1534
+ }
1535
+ let parsed;
1536
+ if (options.result) parsed = await this.#extractResult(options.result, result.text, instructions, model, signal);
1537
+ if (kind !== "task") {
1538
+ completeJournalEntry(data, journalOperationId);
1539
+ data.updatedAt = Date.now();
1540
+ await ctx.store.put(this.#key, data);
1541
+ }
1542
+ return { text: result.text, data: parsed, usage: result.usage };
1543
+ } catch (err) {
1544
+ if (kind !== "task") {
1545
+ if (isToolApprovalRequiredError(err)) {
1546
+ const partial = toolApprovalPartialTranscript(err);
1547
+ if (partial) appendJournalOutput(data, journalOperationId, partial.appended, partial.usage);
1548
+ parkJournalEntryForToolApproval(data, journalOperationId);
1549
+ } else {
1550
+ failJournalEntry(data, journalOperationId, err);
1551
+ }
1552
+ data.updatedAt = Date.now();
1553
+ await ctx.store.put(this.#key, data);
1554
+ }
1555
+ throw err;
1556
+ }
1557
+ }
1558
+ #composeInstructions(resultSchema) {
1559
+ const base = this.#ctx.instructions;
1560
+ if (!resultSchema) return base;
1561
+ const schema = JSON.stringify(toJsonSchema2(resultSchema));
1562
+ const directive = `When you have the final answer, respond with ONLY a single JSON object that conforms to this JSON Schema. No prose, no markdown fences:
1563
+ ${schema}`;
1564
+ return base ? `${base}
1565
+
1566
+ ${directive}` : directive;
1567
+ }
1568
+ async #extractResult(schema, text, instructions, model, signal) {
1569
+ const first = tryParse(schema, text);
1570
+ if (first.ok) return first.value;
1571
+ const repair = await this.#ctx.engine.run(
1572
+ {
1573
+ model,
1574
+ instructions,
1575
+ messages: [
1576
+ {
1577
+ role: "user",
1578
+ content: [{ type: "text", text: `Return ONLY the JSON object. Your previous reply was not valid JSON for the schema.
1579
+
1580
+ Previous reply:
1581
+ ${text}` }]
1582
+ }
1583
+ ],
1584
+ tools: [],
1585
+ signal,
1586
+ apiKey: this.#ctx.apiKey,
1587
+ env: this.#ctx.envVars,
1588
+ providers: this.#ctx.providers
1589
+ },
1590
+ { bus: this.#ctx.bus, runId: this.#ctx.runId, session: this.name }
1591
+ );
1592
+ const second = tryParse(schema, repair.text);
1593
+ if (second.ok) return second.value;
1594
+ throw new Error(`Model output did not match the result schema:
1595
+ ${second.error}`);
1596
+ }
1597
+ async #load() {
1598
+ const existing = await this.#ctx.store.get(this.#key);
1599
+ if (existing) {
1600
+ existing.metadata ??= {};
1601
+ return existing;
1602
+ }
1603
+ return { name: this.name, messages: [], metadata: {}, createdAt: Date.now(), updatedAt: Date.now() };
1604
+ }
1605
+ };
1606
+ function beginJournalEntry(data, entry) {
1607
+ const journal = journalEntries(data);
1608
+ journal.push(entry);
1609
+ }
1610
+ function findReplayableJournalEntry(data, runId, kind, options = {}) {
1611
+ if (!runId) return void 0;
1612
+ return journalEntries(data).filter(
1613
+ (entry) => entry.runId === runId && entry.kind === kind && entry.status === "running" && (entry.outputCount === 0 || options.includeApprovalParked && entry.resumeReason === "tool_approval")
1614
+ ).find((entry) => {
1615
+ const inputValid = entry.inputCount > 0 && entry.inputStart >= 0 && entry.inputStart + entry.inputCount <= data.messages.length;
1616
+ const outputValid = entry.outputStart >= 0 && entry.outputCount >= 0 && entry.outputStart + entry.outputCount <= data.messages.length;
1617
+ return inputValid && outputValid;
1618
+ });
1619
+ }
1620
+ function hasReplayableJournalEntry(data, runId, kind = "prompt") {
1621
+ return findReplayableJournalEntry(data, runId, kind) !== void 0;
1622
+ }
1623
+ function appendJournalOutput(data, operationId, appended, usage) {
1624
+ if (!appended.length) return;
1625
+ const entry = journalEntries(data).find((candidate) => candidate.operationId === operationId);
1626
+ const outputStart = entry && entry.outputCount > 0 ? entry.outputStart : data.messages.length;
1627
+ const outputCount = (entry?.outputCount ?? 0) + appended.length;
1628
+ const combinedUsage = entry?.usage ? addUsage(entry.usage, usage) : usage;
1629
+ data.messages.push(...appended);
1630
+ recordJournalOutput(data, operationId, { outputStart, outputCount, usage: combinedUsage });
1631
+ }
1632
+ function markJournalReplay(data, operationId) {
1633
+ const now = Date.now();
1634
+ const journal = journalEntries(data);
1635
+ const entry = journal.find((candidate) => candidate.operationId === operationId);
1636
+ if (!entry) return;
1637
+ entry.replayedAt = now;
1638
+ entry.replayCount = (entry.replayCount ?? 0) + 1;
1639
+ }
1640
+ function recordJournalOutput(data, operationId, patch) {
1641
+ updateJournalEntry(data, operationId, {
1642
+ outputStart: patch.outputStart,
1643
+ outputCount: patch.outputCount,
1644
+ usage: patch.usage
1645
+ });
1646
+ }
1647
+ function parkJournalEntryForToolApproval(data, operationId) {
1648
+ updateJournalEntry(data, operationId, { resumeReason: "tool_approval" });
1649
+ }
1650
+ function completeJournalEntry(data, operationId) {
1651
+ updateJournalEntry(data, operationId, {
1652
+ status: "completed",
1653
+ completedAt: Date.now(),
1654
+ error: void 0
1655
+ });
1656
+ }
1657
+ function failJournalEntry(data, operationId, err) {
1658
+ updateJournalEntry(data, operationId, {
1659
+ status: "failed",
1660
+ completedAt: Date.now(),
1661
+ error: { message: err instanceof Error ? err.message : String(err) }
1662
+ });
1663
+ }
1664
+ function reconcileSessionJournal(data, reconciliation) {
1665
+ let changed = false;
1666
+ for (const entry of journalEntries(data)) {
1667
+ if (entry.runId !== reconciliation.runId || entry.status !== "running") continue;
1668
+ entry.status = reconciliation.status;
1669
+ entry.completedAt = reconciliation.completedAt;
1670
+ entry.error = reconciliation.error;
1671
+ changed = true;
1672
+ }
1673
+ if (changed) data.updatedAt = Math.max(data.updatedAt, reconciliation.completedAt);
1674
+ return changed;
1675
+ }
1676
+ function updateJournalEntry(data, operationId, patch) {
1677
+ const journal = journalEntries(data);
1678
+ const index = journal.findIndex((entry) => entry.operationId === operationId);
1679
+ if (index === -1) return;
1680
+ journal[index] = { ...journal[index], ...patch };
1681
+ }
1682
+ function journalEntries(data) {
1683
+ const metadata = data.metadata ??= {};
1684
+ const existing = metadata[SESSION_JOURNAL_KEY];
1685
+ if (Array.isArray(existing)) return existing;
1686
+ const journal = [];
1687
+ metadata[SESSION_JOURNAL_KEY] = journal;
1688
+ return journal;
1689
+ }
1690
+ function tryParse(schema, text) {
1691
+ const cleaned = stripFences(text);
1692
+ let json;
1693
+ try {
1694
+ json = JSON.parse(cleaned);
1695
+ } catch {
1696
+ const extracted = extractJsonObject(cleaned);
1697
+ if (extracted === null) return { ok: false, error: "no JSON object found in output" };
1698
+ try {
1699
+ json = JSON.parse(extracted);
1700
+ } catch (e) {
1701
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
1702
+ }
1703
+ }
1704
+ const r = v2.safeParse(schema, json);
1705
+ if (r.success) return { ok: true, value: r.output };
1706
+ return { ok: false, error: r.issues.map((i) => i.message).join("; ") };
1707
+ }
1708
+ function stripFences(text) {
1709
+ const trimmed = text.trim();
1710
+ const fence = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/);
1711
+ return fence?.[1] ? fence[1].trim() : trimmed;
1712
+ }
1713
+ function extractJsonObject(text) {
1714
+ const start = text.indexOf("{");
1715
+ const end = text.lastIndexOf("}");
1716
+ if (start === -1 || end === -1 || end < start) return null;
1717
+ return text.slice(start, end + 1);
1718
+ }
1719
+ function renderMessage(m) {
1720
+ if (m.role === "user") return `User: ${m.content.map((p) => p.type === "text" ? p.text : "[image]").join("")}`;
1721
+ if (m.role === "assistant")
1722
+ return `Assistant: ${m.content.map((p) => p.type === "text" ? p.text : p.type === "tool_call" ? `[calls ${p.name}]` : "").join("")}`;
1723
+ return `Tool(${m.toolName}): ${m.content.map((p) => p.type === "text" ? p.text : "").join("")}`;
1724
+ }
1725
+ function noFs() {
1726
+ const fail = () => {
1727
+ throw new Error("No sandbox configured for this agent (sandbox: false).");
1728
+ };
1729
+ return { readFile: fail, readFileBuffer: fail, writeFile: fail, stat: fail, readdir: fail, exists: async () => false, mkdir: fail, rm: fail };
1730
+ }
1731
+
1732
+ // src/harness.ts
1733
+ var Harness = class {
1734
+ name;
1735
+ fs;
1736
+ #ctx;
1737
+ #sessions = /* @__PURE__ */ new Map();
1738
+ constructor(name, ctx) {
1739
+ this.name = name;
1740
+ this.#ctx = ctx;
1741
+ this.fs = ctx.env ? makeFs(ctx.env) : neverFs();
1742
+ }
1743
+ async session(name) {
1744
+ const key = name ?? this.#ctx.instanceId;
1745
+ let session = this.#sessions.get(key);
1746
+ if (!session) {
1747
+ session = new Session(key, this.#ctx);
1748
+ this.#sessions.set(key, session);
1749
+ }
1750
+ return session;
1751
+ }
1752
+ shell(command, options) {
1753
+ return makeCall(async (signal) => {
1754
+ if (!this.#ctx.env) throw new Error("No sandbox configured for this agent (sandbox: false).");
1755
+ const env = await createScopedSessionEnv(this.#ctx.env, mergeCommands(this.#ctx.commands, options?.commands));
1756
+ return env.exec(command, { cwd: options?.cwd, timeoutMs: options?.timeoutMs, signal });
1757
+ }, options?.signal);
1758
+ }
1759
+ };
1760
+ function neverFs() {
1761
+ const fail = () => {
1762
+ throw new Error("No sandbox configured for this agent (sandbox: false).");
1763
+ };
1764
+ return { readFile: fail, readFileBuffer: fail, writeFile: fail, stat: fail, readdir: fail, exists: async () => false, mkdir: fail, rm: fail };
1765
+ }
1766
+
1767
+ // src/runtime.ts
1768
+ var defaultEngine = new PiAgentEngine();
1769
+ var sharedPersistence = null;
1770
+ function getDefaultPersistence() {
1771
+ if (!sharedPersistence) sharedPersistence = inMemoryAdapter();
1772
+ return sharedPersistence;
1773
+ }
1774
+ var HEADLESS_DIRECTIVE = "You are running unattended. Work autonomously to complete the request \u2014 never ask the user questions or wait for confirmation.";
1775
+ var DEFAULT_RUN_LEASE_TTL_MS = 5 * 6e4;
1776
+ function genId(prefix) {
1777
+ const rand = Math.floor(Math.random() * 4294967295).toString(16).padStart(8, "0");
1778
+ return `${prefix}_${Date.now().toString(36)}${rand}`;
1779
+ }
1780
+ function sleep(ms) {
1781
+ return new Promise((r) => setTimeout(r, ms));
1782
+ }
1783
+ function backoffMs(attempt) {
1784
+ return Math.min(200 * 2 ** attempt, 5e3);
1785
+ }
1786
+ async function dispatchWorkflow(options) {
1787
+ const runId = options.runId ?? genId("run");
1788
+ const persistence = options.persistence ?? getDefaultPersistence();
1789
+ const owner = options.runLeaseOwner ?? genId("lease");
1790
+ const leaseTtlMs = options.runLeaseTtlMs ?? DEFAULT_RUN_LEASE_TTL_MS;
1791
+ if (options.runLeaseOwner) {
1792
+ const done2 = runDurably(options, runId, () => invokeAgent({ ...options, persistence, runId, runLeaseOwner: owner, runLeaseTtlMs: leaseTtlMs }).then(() => void 0)).then(
1793
+ () => void 0,
1794
+ () => void 0
1795
+ );
1796
+ return { runId, done: done2 };
1797
+ }
1798
+ const now = Date.now();
1799
+ const existing = await persistence.runs.get(runId);
1800
+ if (!existing) {
1801
+ await persistence.runs.create({
1802
+ runId,
1803
+ agent: options.agentName,
1804
+ instanceId: options.instanceId,
1805
+ status: "queued",
1806
+ startedAt: now,
1807
+ message: options.message,
1808
+ payload: options.payload,
1809
+ images: options.images
1810
+ });
1811
+ await appendQueuedEvent(persistence, { runId, agent: options.agentName, instanceId: options.instanceId, timestamp: now });
1812
+ } else if (existing.status !== "queued") {
1813
+ throw new Error(`run "${runId}" is already ${existing.status}`);
1814
+ }
1815
+ const done = runDurably(options, runId, () => executeQueuedRun(options, persistence, runId, owner, leaseTtlMs)).then(
1816
+ () => void 0,
1817
+ () => void 0
1818
+ // terminal failure is already recorded on the run record
1819
+ );
1820
+ return { runId, done };
1821
+ }
1822
+ function runDurably(options, runId, run) {
1823
+ if (!options.durability) return run();
1824
+ return Promise.resolve(
1825
+ options.durability(
1826
+ {
1827
+ runId,
1828
+ agentName: options.agentName,
1829
+ instanceId: options.instanceId,
1830
+ resume: options.runResume === true
1831
+ },
1832
+ run
1833
+ )
1834
+ );
1835
+ }
1836
+ async function executeQueuedRun(options, persistence, runId, owner, leaseTtlMs) {
1837
+ const claimed = await persistence.runs.claimQueued(runId, owner, Date.now(), leaseTtlMs);
1838
+ if (!claimed) return;
1839
+ await invokeAgent({ ...options, persistence, runId, runLeaseOwner: owner, runLeaseTtlMs: leaseTtlMs });
1840
+ }
1841
+ async function appendQueuedEvent(persistence, event) {
1842
+ const index = await persistence.runs.eventCount?.(event.runId) ?? (await persistence.runs.events(event.runId)).length;
1843
+ await persistence.runs.appendEvent(event.runId, {
1844
+ type: "run_queued",
1845
+ agent: event.agent,
1846
+ instanceId: event.instanceId,
1847
+ runId: event.runId,
1848
+ index,
1849
+ timestamp: event.timestamp
1850
+ });
1851
+ }
1852
+ async function recoverRuns(agents, opts) {
1853
+ const persistence = opts?.persistence ?? getDefaultPersistence();
1854
+ const leaseTtlMs = opts?.leaseTtlMs ?? DEFAULT_RUN_LEASE_TTL_MS;
1855
+ const agentRecovery = opts?.agentRecovery ?? "auto";
1856
+ const scopedRunIds = opts?.runIds ? new Set(opts.runIds) : void 0;
1857
+ if (opts?.forceLeaseExpiration && !scopedRunIds) {
1858
+ throw new Error("recoverRuns forceLeaseExpiration requires runIds");
1859
+ }
1860
+ const recovered = [];
1861
+ const queued = scopedRunIds ? await runsById(persistence, scopedRunIds, "queued") : await persistence.runs.list({ status: "queued" });
1862
+ for (const run of queued) {
1863
+ if (run.kind === "workflow") continue;
1864
+ const owner = genId("recovery");
1865
+ const claimed = await persistence.runs.claimQueued(run.runId, owner, Date.now(), leaseTtlMs);
1866
+ if (!claimed) continue;
1867
+ const agent = agents[run.agent];
1868
+ if (!agent) {
1869
+ await interruptRecoveredRun(persistence, run, owner, "unknown_agent", `agent "${run.agent}" is no longer registered`);
1870
+ recovered.push(run.runId);
1871
+ continue;
1872
+ }
1873
+ await dispatchWorkflow({
1874
+ agent,
1875
+ agentName: run.agent,
1876
+ instanceId: run.instanceId,
1877
+ message: run.message,
1878
+ payload: run.payload,
1879
+ images: run.images,
1880
+ persistence,
1881
+ engine: opts?.engine,
1882
+ env: opts?.env,
1883
+ runId: run.runId,
1884
+ runLeaseOwner: owner,
1885
+ runLeaseTtlMs: leaseTtlMs,
1886
+ durability: opts?.durability
1887
+ });
1888
+ recovered.push(run.runId);
1889
+ }
1890
+ const running = scopedRunIds ? await runsById(persistence, scopedRunIds, "running") : await persistence.runs.list({ status: "running" });
1891
+ for (const run of running) {
1892
+ if (run.kind === "workflow") continue;
1893
+ const owner = genId("recovery");
1894
+ if (opts?.forceLeaseExpiration) {
1895
+ await persistence.runs.update(run.runId, { leaseExpiresAt: 0 });
1896
+ }
1897
+ const claimed = await persistence.runs.claimLease(run.runId, owner, Date.now(), leaseTtlMs);
1898
+ if (!claimed) continue;
1899
+ const agent = agents[run.agent];
1900
+ if (!agent) {
1901
+ await interruptRecoveredRun(persistence, run, owner, "unknown_agent", `agent "${run.agent}" is no longer registered`);
1902
+ continue;
1903
+ }
1904
+ const shouldReplay = agentRecovery === "replay" || agentRecovery === "auto" && await canSafelyReplayAgentRun(persistence, run);
1905
+ if (!shouldReplay) {
1906
+ await interruptRecoveredRun(
1907
+ persistence,
1908
+ run,
1909
+ owner,
1910
+ "recovery_interrupted",
1911
+ "run was still marked running after its execution lease expired; it was not replayed to avoid duplicate side effects"
1912
+ );
1913
+ recovered.push(run.runId);
1914
+ continue;
1915
+ }
1916
+ await dispatchWorkflow({
1917
+ agent,
1918
+ agentName: run.agent,
1919
+ instanceId: run.instanceId,
1920
+ message: run.message,
1921
+ payload: run.payload,
1922
+ images: run.images,
1923
+ persistence,
1924
+ engine: opts?.engine,
1925
+ env: opts?.env,
1926
+ runId: run.runId,
1927
+ runLeaseOwner: owner,
1928
+ runLeaseTtlMs: leaseTtlMs,
1929
+ runResume: true,
1930
+ durability: opts?.durability
1931
+ });
1932
+ recovered.push(run.runId);
1933
+ }
1934
+ return recovered;
1935
+ }
1936
+ async function runsById(persistence, runIds, status) {
1937
+ const runs = [];
1938
+ for (const runId of runIds) {
1939
+ const run = await persistence.runs.get(runId);
1940
+ if (run?.status === status) runs.push(run);
1941
+ }
1942
+ return runs.sort((a, b) => b.startedAt - a.startedAt);
1943
+ }
1944
+ var SAFE_AUTO_REPLAY_EVENT_TYPES = /* @__PURE__ */ new Set(["run_queued", "run_start", "run_resume", "agent_start", "operation_start"]);
1945
+ async function canSafelyReplayAgentRun(persistence, run) {
1946
+ const sessionKey = `${run.agent}::${run.instanceId}::${run.instanceId}`;
1947
+ const session = await persistence.sessions.get(sessionKey);
1948
+ if (!session || !hasReplayableJournalEntry(session, run.runId, "prompt")) return false;
1949
+ const events = await persistence.runs.events(run.runId);
1950
+ return events.every((event) => SAFE_AUTO_REPLAY_EVENT_TYPES.has(event.type));
1951
+ }
1952
+ async function interruptRecoveredRun(persistence, run, leaseOwner, code, message) {
1953
+ const endedAt = Date.now();
1954
+ await persistence.runs.update(run.runId, { status: "interrupted", error: { code, message }, endedAt });
1955
+ await reconcileRecoveredRunJournal(persistence, run, { code, message, endedAt });
1956
+ const startIndex = await persistence.runs.eventCount?.(run.runId) ?? (await persistence.runs.events(run.runId)).length;
1957
+ await persistence.runs.appendEvent(run.runId, {
1958
+ type: "run_error",
1959
+ code,
1960
+ message,
1961
+ runId: run.runId,
1962
+ index: startIndex,
1963
+ timestamp: endedAt
1964
+ });
1965
+ await persistence.runs.appendEvent(run.runId, {
1966
+ type: "run_end",
1967
+ ok: false,
1968
+ runId: run.runId,
1969
+ index: startIndex + 1,
1970
+ timestamp: endedAt
1971
+ });
1972
+ await persistence.runs.releaseLease(run.runId, leaseOwner).catch(() => {
1973
+ });
1974
+ }
1975
+ async function reconcileRecoveredRunJournal(persistence, run, error) {
1976
+ const sessionKey = `${run.agent}::${run.instanceId}::${run.instanceId}`;
1977
+ const session = await persistence.sessions.get(sessionKey);
1978
+ if (!session) return;
1979
+ const changed = reconcileSessionJournal(session, {
1980
+ runId: run.runId,
1981
+ status: "interrupted",
1982
+ completedAt: error.endedAt,
1983
+ error: { code: error.code, message: error.message }
1984
+ });
1985
+ if (changed) await persistence.sessions.put(sessionKey, session);
1986
+ }
1987
+ function skillTool(skill, model, tools, sub) {
1988
+ const allowed = skill.allowedTools ? tools.filter((t) => skill.allowedTools.includes(t.name)) : tools;
1989
+ return {
1990
+ name: `skill_${skill.name}`,
1991
+ description: `Run the "${skill.name}" skill. ${skill.description}`,
1992
+ parameters: {
1993
+ type: "object",
1994
+ properties: { input: { type: "string", description: "Input or context to give the skill." } },
1995
+ required: ["input"]
1996
+ },
1997
+ execute: async (args, signal) => {
1998
+ if (!model) throw new Error(`Skill "${skill.name}" requires a model to be configured.`);
1999
+ const res = await sub.engine.run(
2000
+ {
2001
+ model,
2002
+ instructions: skillInstructions(skill),
2003
+ messages: [{ role: "user", content: [{ type: "text", text: String(args.input ?? "") }] }],
2004
+ tools: allowed,
2005
+ signal,
2006
+ apiKey: sub.apiKey,
2007
+ env: sub.env,
2008
+ providers: sub.providers
2009
+ },
2010
+ { bus: sub.bus, runId: sub.runId, session: `skill:${skill.name}` }
2011
+ );
2012
+ return res.text;
2013
+ }
2014
+ };
2015
+ }
2016
+ function skillInstructions(skill) {
2017
+ const files = Object.keys(skill.files ?? {});
2018
+ if (!files.length) return skill.body;
2019
+ const root = `.atlasflow/packaged-skills/${skill.name}`;
2020
+ return [
2021
+ skill.body,
2022
+ "",
2023
+ "Packaged skill resources are mounted in the sandbox. Read them when relevant:",
2024
+ ...files.map((file) => `- ${root}/${file}`)
2025
+ ].join("\n");
2026
+ }
2027
+ function activateSkillTool(env, workspaceSkills) {
2028
+ const byName = new Map(workspaceSkills.map((skill) => [skill.name, skill]));
2029
+ return {
2030
+ name: "activate_skill",
2031
+ description: "Load the full instructions for a workspace skill before using it.",
2032
+ parameters: {
2033
+ type: "object",
2034
+ properties: { name: { type: "string", description: "Workspace skill name from the Available Skills list." } },
2035
+ required: ["name"]
2036
+ },
2037
+ execute: async (args) => {
2038
+ const name = String(args.name ?? "");
2039
+ const skill = byName.get(name);
2040
+ if (!skill) throw new Error(`Unknown workspace skill "${name}".`);
2041
+ const loaded = await loadWorkspaceSkill(env, skill);
2042
+ return [`# Skill: ${loaded.name}`, "", loaded.description, "", loaded.body].join("\n");
2043
+ }
2044
+ };
2045
+ }
2046
+ function loadSkillInstructions(skills) {
2047
+ const items = [...skills];
2048
+ if (!items.length) return void 0;
2049
+ return [
2050
+ "## Available Skills",
2051
+ "",
2052
+ "The following skills provide specialized instructions. When a task matches a skill description, call the `load_skill` tool with that skill name before proceeding so its full instructions are loaded.",
2053
+ "",
2054
+ ...items.map((skill) => `- **${skill.name}**${skill.description ? ` - ${skill.description}` : ""}`)
2055
+ ].join("\n");
2056
+ }
2057
+ function loadSkillTool(env, skills) {
2058
+ return {
2059
+ name: "load_skill",
2060
+ description: "Load the full instructions for an available skill before using it.",
2061
+ parameters: {
2062
+ type: "object",
2063
+ properties: { name: { type: "string", description: "Skill name from the Available Skills list." } },
2064
+ required: ["name"]
2065
+ },
2066
+ execute: async (args) => {
2067
+ const name = String(args.name ?? "");
2068
+ const skill = skills.get(name);
2069
+ if (!skill) throw new Error(`Unknown skill "${name}".`);
2070
+ const loaded = isWorkspaceSkill(skill) ? await loadWorkspaceSkillForTool(env, skill) : skill;
2071
+ return [`# Skill: ${loaded.name}`, "", loaded.description, "", skillInstructions(loaded)].join("\n");
2072
+ }
2073
+ };
2074
+ }
2075
+ async function loadWorkspaceSkillForTool(env, skill) {
2076
+ if (!env) throw new Error(`Workspace skill "${skill.name}" requires a sandbox environment.`);
2077
+ return loadWorkspaceSkill(env, skill);
2078
+ }
2079
+ function taskTool(defaultModel, defaultInstructions, tools, subagents, sub) {
2080
+ const profiles = new Map(subagents.map((p) => [p.name, p]));
2081
+ const agentNames = [...profiles.keys()];
2082
+ return {
2083
+ name: "task",
2084
+ description: `Delegate a self-contained task to a sub-agent running in a fresh context. ` + (agentNames.length ? `Available agents: ${agentNames.join(", ")} (omit "agent" for a general-purpose one). ` : "") + "Returns the sub-agent's final answer.",
2085
+ parameters: {
2086
+ type: "object",
2087
+ properties: {
2088
+ prompt: { type: "string", description: "Complete, self-contained instructions for the sub-agent." },
2089
+ agent: { type: "string", description: agentNames.length ? `Subagent profile: one of ${agentNames.join(", ")}.` : "Unused." }
2090
+ },
2091
+ required: ["prompt"]
2092
+ },
2093
+ execute: async (args, signal) => {
2094
+ const profile = args.agent != null ? profiles.get(String(args.agent)) : void 0;
2095
+ if (args.agent != null && !profile) throw new Error(`Unknown subagent "${args.agent}". Available: ${agentNames.join(", ") || "(none)"}`);
2096
+ const model = profile?.model ?? defaultModel;
2097
+ if (!model) throw new Error("task requires a model to be configured.");
2098
+ const taskId = genId("task");
2099
+ sub.bus.emit({ type: "task_start", taskId, agent: profile?.name ?? "task", runId: sub.runId });
2100
+ try {
2101
+ const res = await sub.engine.run(
2102
+ {
2103
+ model,
2104
+ instructions: profile?.instructions ?? defaultInstructions,
2105
+ messages: [{ role: "user", content: [{ type: "text", text: String(args.prompt) }] }],
2106
+ tools: tools.filter((t) => t.name !== "task"),
2107
+ signal,
2108
+ apiKey: sub.apiKey,
2109
+ env: sub.env,
2110
+ providers: sub.providers
2111
+ },
2112
+ { bus: sub.bus, runId: sub.runId, session: `task:${taskId}` }
2113
+ );
2114
+ sub.bus.emit({ type: "task_end", taskId, ok: true, runId: sub.runId });
2115
+ return res.text;
2116
+ } catch (err) {
2117
+ sub.bus.emit({ type: "task_end", taskId, ok: false, runId: sub.runId });
2118
+ throw err;
2119
+ }
2120
+ }
2121
+ };
2122
+ }
2123
+ async function mountContextPacks(packs, sandboxEnv, bus, runId, options = {}) {
2124
+ const notes = [];
2125
+ const injectContent = options.injectContent !== false;
2126
+ for (const pack of packs) {
2127
+ const wantsSandbox = pack.mount === "sandbox";
2128
+ if (wantsSandbox && sandboxEnv) {
2129
+ const files = [];
2130
+ for (const item of pack.items) {
2131
+ const path = `packs/${pack.name}/${item.name}`;
2132
+ const dir = `packs/${pack.name}`;
2133
+ await sandboxEnv.mkdir(dir, { recursive: true }).catch(() => {
2134
+ });
2135
+ await sandboxEnv.writeFile(path, item.content);
2136
+ files.push(path);
2137
+ }
2138
+ notes.push(
2139
+ `## Context pack: ${pack.name}${pack.version ? ` (v${pack.version})` : ""}
2140
+ ${pack.description ?? ""}
2141
+ Mounted in your workspace \u2014 read these files when relevant:
2142
+ ${files.map((f) => `- ${f}`).join("\n")}`
2143
+ );
2144
+ continue;
2145
+ }
2146
+ if (wantsSandbox && !sandboxEnv) {
2147
+ bus.emit({
2148
+ type: "log",
2149
+ level: "warn",
2150
+ message: injectContent ? `context pack "${pack.name}" wants a sandbox mount but no sandbox is configured; injecting into context instead` : `context pack "${pack.name}" wants a sandbox mount but no sandbox is configured; keeping it available through load_context`,
2151
+ runId
2152
+ });
2153
+ }
2154
+ if (!injectContent) continue;
2155
+ const body = pack.items.map((i) => `### ${i.name}
2156
+ ${i.content}`).join("\n\n");
2157
+ notes.push(`## Context pack: ${pack.name}${pack.version ? ` (v${pack.version})` : ""}
2158
+ ${pack.description ?? ""}
2159
+ ${body}`);
2160
+ }
2161
+ return notes;
2162
+ }
2163
+ function loadContextInstructions(packs) {
2164
+ if (!packs.length) return void 0;
2165
+ return [
2166
+ "## Available Context",
2167
+ "",
2168
+ "The following context packs are available. When a task needs one of them, call the `load_context` tool with the pack name before relying on its details.",
2169
+ "",
2170
+ ...packs.map((pack) => {
2171
+ const items = pack.items.map((item) => item.name).join(", ");
2172
+ return `- **${pack.name}**${pack.description ? ` - ${pack.description}` : ""}${items ? ` (items: ${items})` : ""}`;
2173
+ })
2174
+ ].join("\n");
2175
+ }
2176
+ function loadContextTool(packs) {
2177
+ return {
2178
+ name: "load_context",
2179
+ description: "Load the full contents of an available context pack before using its details.",
2180
+ parameters: {
2181
+ type: "object",
2182
+ properties: {
2183
+ name: { type: "string", description: "Context pack name from the Available Context list." },
2184
+ item: { type: "string", description: "Optional item name within the context pack." }
2185
+ },
2186
+ required: ["name"]
2187
+ },
2188
+ execute: async (args) => {
2189
+ const name = String(args.name ?? "");
2190
+ const pack = packs.get(name);
2191
+ if (!pack) throw new Error(`Unknown context pack "${name}".`);
2192
+ const itemName = typeof args.item === "string" && args.item.trim() ? args.item : void 0;
2193
+ const items = itemName ? pack.items.filter((item) => item.name === itemName) : pack.items;
2194
+ if (itemName && !items.length) throw new Error(`Unknown item "${itemName}" in context pack "${name}".`);
2195
+ const body = items.map((item) => `### ${item.name}
2196
+ ${item.content}`).join("\n\n");
2197
+ return [`# Context pack: ${pack.name}${pack.version ? ` (v${pack.version})` : ""}`, "", pack.description ?? "", "", body].join("\n").trim();
2198
+ }
2199
+ };
2200
+ }
2201
+ async function mountPackagedSkillFiles(skills, sandboxEnv) {
2202
+ if (!sandboxEnv) return [];
2203
+ const notes = [];
2204
+ for (const skill of skills) {
2205
+ const files = Object.entries(skill.files ?? {});
2206
+ if (!files.length) continue;
2207
+ const root = `.atlasflow/packaged-skills/${skill.name}`;
2208
+ const mounted = [];
2209
+ for (const [relativePath, file] of files) {
2210
+ const target = `${root}/${relativePath}`;
2211
+ await sandboxEnv.writeFile(target, decodeBase64(file.content));
2212
+ mounted.push(target);
2213
+ }
2214
+ notes.push(`## Packaged skill resources: ${skill.name}
2215
+ Mounted in your workspace \u2014 read these files when relevant:
2216
+ ${mounted.map((file) => `- ${file}`).join("\n")}`);
2217
+ }
2218
+ return notes;
2219
+ }
2220
+ function decodeBase64(content) {
2221
+ if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(content, "base64"));
2222
+ const binary = atob(content);
2223
+ const out = new Uint8Array(binary.length);
2224
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
2225
+ return out;
2226
+ }
2227
+ async function invokeAgent(options) {
2228
+ const env = options.env ?? process.env;
2229
+ const engine = options.engine ?? defaultEngine;
2230
+ const persistence = options.persistence ?? getDefaultPersistence();
2231
+ const isRedispatch = options.runId != null;
2232
+ const runId = options.runId ?? genId("run");
2233
+ const leaseOwner = options.runLeaseOwner ?? genId("lease");
2234
+ const leaseTtlMs = options.runLeaseTtlMs ?? DEFAULT_RUN_LEASE_TTL_MS;
2235
+ let startIndex = 0;
2236
+ if (isRedispatch) {
2237
+ startIndex = await persistence.runs.eventCount?.(runId) ?? (await persistence.runs.events(runId)).length;
2238
+ }
2239
+ const bus = new EventBus(startIndex);
2240
+ bus.subscribe((e) => {
2241
+ fanoutToGlobal(e);
2242
+ options.onEvent?.(e);
2243
+ void persistence.runs.appendEvent(runId, e).catch(() => {
2244
+ });
2245
+ });
2246
+ const record2 = {
2247
+ runId,
2248
+ agent: options.agentName,
2249
+ instanceId: options.instanceId,
2250
+ status: "running",
2251
+ startedAt: Date.now(),
2252
+ message: options.message,
2253
+ payload: options.payload,
2254
+ images: options.images,
2255
+ leaseOwner,
2256
+ leaseAcquiredAt: Date.now(),
2257
+ leaseExpiresAt: Date.now() + leaseTtlMs
2258
+ };
2259
+ if (!await persistence.runs.get(runId)) await persistence.runs.create(record2);
2260
+ else if (!options.runLeaseOwner && !await persistence.runs.claimLease(runId, leaseOwner, Date.now(), leaseTtlMs)) {
2261
+ throw new Error(`run "${runId}" is already running under another live lease`);
2262
+ }
2263
+ bus.emit({ type: options.runResume ? "run_resume" : "run_start", agent: options.agentName, instanceId: options.instanceId, runId });
2264
+ bus.emit({ type: "agent_start", agent: options.agentName, runId });
2265
+ let sandboxEnv;
2266
+ let configCleanup;
2267
+ const leaseHeartbeat = setInterval(() => {
2268
+ void persistence.runs.heartbeatLease(runId, leaseOwner, Date.now(), leaseTtlMs).catch(() => {
2269
+ });
2270
+ }, Math.max(1e3, Math.floor(leaseTtlMs / 3)));
2271
+ leaseHeartbeat.unref?.();
2272
+ try {
2273
+ const rawConfig = await options.agent.initialize({ id: options.instanceId, env, payload: options.payload });
2274
+ const config = applyProfile(rawConfig);
2275
+ configCleanup = config.cleanup;
2276
+ const model = config.model === false ? void 0 : config.model;
2277
+ const guard = makeRuleGuard(config.rules ?? [], config.hooks, bus, runId);
2278
+ const wrapRuntimeTool = (tool) => guard.wrapTool(wrapToolApproval(tool, { persistence, runId, bus }));
2279
+ const tools = (config.tools ?? []).map(resolveAnyTool).map(wrapRuntimeTool);
2280
+ const authorToolNames = new Set(tools.map((t) => t.name));
2281
+ const declaredSkills = config.skills ?? [];
2282
+ const lazySkillLoading = config.skillLoading === "load";
2283
+ const skills = new Map(declaredSkills.map((s) => [s.name, s]));
2284
+ const contextPacks = config.contextPacks ?? [];
2285
+ const lazyContextLoading = config.contextLoading === "lazy";
2286
+ const contextPackMap = new Map(contextPacks.map((pack) => [pack.name, pack]));
2287
+ const sub = { engine, bus, runId, apiKey: options.apiKey, env, providers: config.providers };
2288
+ let sandboxTools;
2289
+ const sandboxFactory = config.sandbox === false ? void 0 : config.sandbox ?? virtualSandbox();
2290
+ if (sandboxFactory) {
2291
+ sandboxEnv = await sandboxFactory.createSessionEnv({ id: options.instanceId, env });
2292
+ if (config.cwd) sandboxEnv = createCwdSessionEnv(sandboxEnv, config.cwd);
2293
+ if (config.builtinTools !== false) {
2294
+ const include = Array.isArray(config.builtinTools) ? config.builtinTools : void 0;
2295
+ sandboxTools = (toolEnv) => builtinTools(toolEnv, { include }).filter((tool) => !authorToolNames.has(tool.name)).map(wrapRuntimeTool);
2296
+ }
2297
+ }
2298
+ const packNotes = await mountContextPacks(contextPacks, sandboxEnv, bus, runId, { injectContent: !lazyContextLoading });
2299
+ const skillFileNotes = await mountPackagedSkillFiles(declaredSkills, sandboxEnv);
2300
+ let discoveredInstructions;
2301
+ if (sandboxEnv) {
2302
+ const discovered = await discoverSessionContext(sandboxEnv, declaredSkills);
2303
+ discoveredInstructions = discovered.instructions;
2304
+ for (const skill of discovered.workspaceSkills) skills.set(skill.name, skill);
2305
+ if (discovered.workspaceSkills.length && !tools.some((t) => t.name === "activate_skill")) {
2306
+ tools.push(wrapRuntimeTool(activateSkillTool(sandboxEnv, discovered.workspaceSkills)));
2307
+ }
2308
+ }
2309
+ const lazySkillInstructions = lazySkillLoading ? loadSkillInstructions(skills.values()) : void 0;
2310
+ const lazyContextInstructions = lazyContextLoading ? loadContextInstructions(contextPacks) : void 0;
2311
+ const instructions = [options.baseInstructions, config.instructions, discoveredInstructions, lazySkillInstructions, lazyContextInstructions, ...packNotes, ...skillFileNotes, config.headless ? HEADLESS_DIRECTIVE : void 0].filter(Boolean).join("\n\n") || void 0;
2312
+ const agentCommandEnv = sandboxEnv ? await createScopedSessionEnv(sandboxEnv, config.commands ?? []) : void 0;
2313
+ const subcallTools = agentCommandEnv && sandboxTools ? [...tools, ...sandboxTools(agentCommandEnv)] : tools.slice();
2314
+ if (lazySkillLoading && skills.size && !tools.some((t) => t.name === "load_skill")) {
2315
+ tools.push(wrapRuntimeTool(loadSkillTool(sandboxEnv, skills)));
2316
+ }
2317
+ if (lazyContextLoading && contextPackMap.size && !tools.some((t) => t.name === "load_context")) {
2318
+ tools.push(wrapRuntimeTool(loadContextTool(contextPackMap)));
2319
+ }
2320
+ if (!lazySkillLoading) {
2321
+ for (const skill of skills.values()) {
2322
+ if (isWorkspaceSkill(skill)) continue;
2323
+ tools.push(wrapRuntimeTool(skillTool(skill, model, subcallTools, sub)));
2324
+ }
2325
+ }
2326
+ const personas = [...options.sharedPersonas ?? options.sharedSubagents ?? [], ...config.personas ?? config.subagents ?? []];
2327
+ const wantsTask = config.builtinTools === false ? false : !Array.isArray(config.builtinTools) || config.builtinTools.includes("task");
2328
+ if (model && wantsTask && !tools.some((t) => t.name === "task")) {
2329
+ const taskTools = agentCommandEnv && sandboxTools ? [...tools, ...sandboxTools(agentCommandEnv)] : tools.slice();
2330
+ tools.push(wrapRuntimeTool(taskTool(model, instructions, taskTools, personas, sub)));
2331
+ }
2332
+ const ctx = {
2333
+ engine,
2334
+ model,
2335
+ instructions,
2336
+ tools,
2337
+ skills,
2338
+ thinkingLevel: config.thinkingLevel,
2339
+ toolChoice: config.toolChoice,
2340
+ env: sandboxEnv,
2341
+ commands: config.commands,
2342
+ sandboxTools,
2343
+ store: persistence.sessions,
2344
+ bus,
2345
+ runId,
2346
+ agentName: options.agentName,
2347
+ instanceId: options.instanceId,
2348
+ apiKey: options.apiKey,
2349
+ envVars: env,
2350
+ providers: config.providers,
2351
+ compaction: config.compaction
2352
+ };
2353
+ const harness = new Harness(config.name ?? options.agentName, ctx);
2354
+ const session = await harness.session();
2355
+ const input = options.message ?? (options.payload != null ? JSON.stringify(options.payload) : "");
2356
+ const retry = config.durability?.retry ?? 0;
2357
+ const timeoutMs = config.durability?.timeoutMs;
2358
+ let response;
2359
+ let lastError;
2360
+ await guard.checkPrompt(input);
2361
+ for (let attempt = 0; attempt <= retry; attempt++) {
2362
+ if (options.signal?.aborted) throw options.signal.reason ?? new Error("invocation aborted");
2363
+ try {
2364
+ const signals = [];
2365
+ if (timeoutMs) signals.push(AbortSignal.timeout(timeoutMs));
2366
+ if (options.signal) signals.push(options.signal);
2367
+ const signal = signals.length ? AbortSignal.any(signals) : void 0;
2368
+ response = config.result ? await session.prompt(input, { result: config.result, signal, images: options.images }) : await session.prompt(input, { signal, images: options.images });
2369
+ lastError = void 0;
2370
+ break;
2371
+ } catch (err) {
2372
+ lastError = err;
2373
+ if (isToolApprovalRequiredError(err)) break;
2374
+ if (options.signal?.aborted) break;
2375
+ if (attempt < retry) {
2376
+ const message = err instanceof Error ? err.message : String(err);
2377
+ bus.emit({ type: "log", level: "warn", message: `attempt ${attempt + 1} failed: ${message}; retrying`, runId });
2378
+ await sleep(backoffMs(attempt));
2379
+ }
2380
+ }
2381
+ }
2382
+ if (!response) throw lastError ?? new Error("agent produced no response");
2383
+ await persistence.runs.update(runId, { status: "success", result: response.text, endedAt: Date.now() });
2384
+ bus.emit({ type: "agent_end", runId });
2385
+ bus.emit({ type: "run_end", ok: true, runId });
2386
+ return { runId, text: response.text, data: response.data, usage: response.usage };
2387
+ } catch (err) {
2388
+ if (isToolApprovalRequiredError(err)) {
2389
+ bus.emit({ type: "agent_end", runId });
2390
+ throw err;
2391
+ }
2392
+ const message = err instanceof Error ? err.message : String(err);
2393
+ const code = err.code ?? "agent_error";
2394
+ await persistence.runs.update(runId, { status: "failed", error: { code, message }, endedAt: Date.now() });
2395
+ bus.emit({ type: "run_error", code, message, runId });
2396
+ bus.emit({ type: "agent_end", runId });
2397
+ bus.emit({ type: "run_end", ok: false, runId });
2398
+ throw err;
2399
+ } finally {
2400
+ clearInterval(leaseHeartbeat);
2401
+ await persistence.runs.releaseLease(runId, leaseOwner).catch(() => {
2402
+ });
2403
+ await sandboxEnv?.dispose?.().catch(() => {
2404
+ });
2405
+ if (configCleanup) await Promise.resolve(configCleanup()).catch(() => {
2406
+ });
2407
+ }
2408
+ }
2409
+
2410
+ // src/workflow.ts
2411
+ import * as v3 from "valibot";
2412
+ var PromptStepSchema = v3.object({
2413
+ id: v3.pipe(v3.string(), v3.minLength(1)),
2414
+ kind: v3.literal("prompt"),
2415
+ /** May reference prior steps: {{steps.<id>.text}}. */
2416
+ prompt: v3.string(),
2417
+ /** Role slot or persona name executing this step (default: workflow default persona). */
2418
+ persona: v3.optional(v3.string()),
2419
+ model: v3.optional(v3.string())
2420
+ });
2421
+ var SkillStepSchema = v3.object({
2422
+ id: v3.pipe(v3.string(), v3.minLength(1)),
2423
+ kind: v3.literal("skill"),
2424
+ /** Name of a skill in the executing persona's loadout. */
2425
+ skill: v3.string(),
2426
+ /** Input text; may reference prior steps: {{steps.<id>.text}}. */
2427
+ input: v3.optional(v3.string()),
2428
+ persona: v3.optional(v3.string())
2429
+ });
2430
+ var GateStepSchema = v3.object({
2431
+ id: v3.pipe(v3.string(), v3.minLength(1)),
2432
+ kind: v3.literal("gate"),
2433
+ title: v3.string(),
2434
+ description: v3.optional(v3.string())
2435
+ });
2436
+ var WorkflowStepSchema = v3.variant("kind", [PromptStepSchema, SkillStepSchema, GateStepSchema]);
2437
+ var WorkflowDefSchema = v3.object({
2438
+ name: v3.pipe(v3.string(), v3.minLength(1)),
2439
+ description: v3.optional(v3.string()),
2440
+ steps: v3.pipe(v3.array(WorkflowStepSchema), v3.minLength(1)),
2441
+ /** Role slot → default persona name; rebindable at deploy. */
2442
+ roleSlots: v3.optional(v3.record(v3.string(), v3.string()))
2443
+ });
2444
+ function parseWorkflowDef(value) {
2445
+ const r = v3.safeParse(WorkflowDefSchema, value);
2446
+ if (!r.success) {
2447
+ throw new Error(`Invalid workflow definition: ${r.issues.map((i) => `${i.path?.map((p) => p.key).join(".") ?? ""} ${i.message}`).join("; ")}`);
2448
+ }
2449
+ return r.output;
2450
+ }
2451
+ function interpolate(template, state) {
2452
+ return template.replace(/\{\{\s*steps\.([\w-]+)\.text\s*\}\}/g, (_m, id) => state.results[id]?.text ?? "");
2453
+ }
2454
+ function resolvePersona(step, def, ctx) {
2455
+ const ref = step.persona ?? ctx.defaultPersona;
2456
+ if (!ref) return void 0;
2457
+ return ctx.bindings?.[ref] ?? def.roleSlots?.[ref] ?? ref;
2458
+ }
2459
+ async function startWorkflow(def, ctx = {}, opts) {
2460
+ parseWorkflowDef(def);
2461
+ const persistence = ctx.persistence ?? getDefaultPersistence();
2462
+ const runId = opts?.runId ?? genId("run");
2463
+ if (!await persistence.runs.get(runId)) {
2464
+ const seed = opts?.payload != null ? { payload: { text: typeof opts.payload === "string" ? opts.payload : JSON.stringify(opts.payload) } } : {};
2465
+ await persistence.runs.create({
2466
+ runId,
2467
+ agent: def.name,
2468
+ instanceId: runId,
2469
+ status: "running",
2470
+ startedAt: Date.now(),
2471
+ payload: opts?.payload,
2472
+ kind: "workflow",
2473
+ state: { cursor: 0, results: seed, gates: {} }
2474
+ });
2475
+ }
2476
+ return advanceWorkflow(def, ctx, runId);
2477
+ }
2478
+ async function advanceWorkflow(def, ctx, runId) {
2479
+ const persistence = ctx.persistence ?? getDefaultPersistence();
2480
+ const record2 = await persistence.runs.get(runId);
2481
+ if (!record2) throw new Error(`No workflow run "${runId}"`);
2482
+ const state = record2.state ?? { cursor: 0, results: {}, gates: {} };
2483
+ let lastText = "";
2484
+ for (let i = state.cursor; i < def.steps.length; i++) {
2485
+ const step = def.steps[i];
2486
+ if (step.kind === "gate") {
2487
+ const decision = state.gates[step.id];
2488
+ if (!decision) {
2489
+ state.cursor = i;
2490
+ await persistence.runs.update(runId, { status: "waiting_approval", state });
2491
+ return { runId, status: "waiting_approval", gate: { id: step.id, title: step.title } };
2492
+ }
2493
+ if (!decision.approved) {
2494
+ await persistence.runs.update(runId, { status: "failed", error: { code: "gate_rejected", message: `Gate "${step.title}" rejected${decision.note ? `: ${decision.note}` : ""}` }, endedAt: Date.now(), state });
2495
+ return { runId, status: "failed" };
2496
+ }
2497
+ state.cursor = i + 1;
2498
+ await persistence.runs.update(runId, { status: "running", state });
2499
+ continue;
2500
+ }
2501
+ try {
2502
+ lastText = await executeStep(step, def, ctx, state, runId);
2503
+ } catch (err) {
2504
+ const message = err instanceof Error ? err.message : String(err);
2505
+ await persistence.runs.update(runId, { status: "failed", error: { code: "step_failed", message: `step "${step.id}": ${message}` }, endedAt: Date.now(), state });
2506
+ return { runId, status: "failed" };
2507
+ }
2508
+ state.results[step.id] = { text: lastText };
2509
+ state.cursor = i + 1;
2510
+ await persistence.runs.update(runId, { state });
2511
+ }
2512
+ await persistence.runs.update(runId, { status: "success", result: lastText, endedAt: Date.now(), state });
2513
+ return { runId, status: "success", text: lastText };
2514
+ }
2515
+ async function executeStep(step, def, ctx, state, parentRunId) {
2516
+ const personaName = resolvePersona(step, def, ctx);
2517
+ const registered = personaName ? ctx.agents?.[personaName] : void 0;
2518
+ const profile = personaName ? ctx.profiles?.find((p) => p.name === personaName) : void 0;
2519
+ if (personaName && !registered && !profile) {
2520
+ throw new Error(`Unknown persona "${personaName}" (role slots: ${Object.keys(def.roleSlots ?? {}).join(", ") || "none"})`);
2521
+ }
2522
+ let message;
2523
+ let agent = registered;
2524
+ if (step.kind === "prompt") {
2525
+ message = interpolate(step.prompt, state);
2526
+ } else {
2527
+ const skill = ctx.skills?.find((s) => s.name === step.skill);
2528
+ if (!skill) throw new Error(`Unknown skill "${step.skill}"`);
2529
+ message = interpolate(step.input ?? "", state);
2530
+ if (registered) {
2531
+ const res3 = await invokeAgent({
2532
+ agent: registered,
2533
+ agentName: `${def.name}/${step.id}`,
2534
+ instanceId: `${parentRunId}/${step.id}`,
2535
+ message: `${skill.body}
2536
+
2537
+ --- INPUT ---
2538
+ ${message}`,
2539
+ persistence: ctx.persistence,
2540
+ engine: ctx.engine,
2541
+ env: ctx.env,
2542
+ onEvent: ctx.onEvent,
2543
+ baseInstructions: ctx.baseInstructions
2544
+ });
2545
+ return res3.text;
2546
+ }
2547
+ const model = profile?.model ?? ctx.defaultModel;
2548
+ if (!model) throw new Error(`Skill step "${step.id}" needs a model (set workflow defaultModel or bind a persona with one)`);
2549
+ const skillAgent = createAgent(() => ({ model, instructions: skill.body }));
2550
+ const res2 = await invokeAgent({
2551
+ agent: skillAgent,
2552
+ agentName: `${def.name}/${step.id}`,
2553
+ instanceId: `${parentRunId}/${step.id}`,
2554
+ message,
2555
+ persistence: ctx.persistence,
2556
+ engine: ctx.engine,
2557
+ env: ctx.env,
2558
+ onEvent: ctx.onEvent,
2559
+ baseInstructions: ctx.baseInstructions
2560
+ });
2561
+ return res2.text;
2562
+ }
2563
+ if (!agent) {
2564
+ const model = step.model ?? profile?.model ?? ctx.defaultModel;
2565
+ if (!model) throw new Error(`Step "${step.id}" needs a model (step.model, persona model, or workflow defaultModel)`);
2566
+ const instructions = profile?.instructions;
2567
+ agent = createAgent(() => ({ model, instructions }));
2568
+ }
2569
+ const res = await invokeAgent({
2570
+ agent,
2571
+ agentName: `${def.name}/${step.id}`,
2572
+ instanceId: `${parentRunId}/${step.id}`,
2573
+ message,
2574
+ persistence: ctx.persistence,
2575
+ engine: ctx.engine,
2576
+ env: ctx.env,
2577
+ onEvent: ctx.onEvent,
2578
+ baseInstructions: ctx.baseInstructions
2579
+ });
2580
+ return res.text;
2581
+ }
2582
+ async function approveGate(def, ctx, runId, decision) {
2583
+ const persistence = ctx.persistence ?? getDefaultPersistence();
2584
+ const record2 = await persistence.runs.get(runId);
2585
+ if (!record2) throw new Error(`No workflow run "${runId}"`);
2586
+ if (record2.status !== "waiting_approval") throw new Error(`Run "${runId}" is not waiting for approval (status: ${record2.status})`);
2587
+ const state = record2.state ?? { cursor: 0, results: {}, gates: {} };
2588
+ const step = def.steps[state.cursor];
2589
+ if (!step || step.kind !== "gate" || step.id !== decision.gate) {
2590
+ throw new Error(`Run "${runId}" is waiting on gate "${step && step.kind === "gate" ? step.id : "?"}", not "${decision.gate}"`);
2591
+ }
2592
+ state.gates[decision.gate] = { approved: decision.approved, note: decision.note, at: Date.now() };
2593
+ await persistence.runs.update(runId, { state });
2594
+ return advanceWorkflow(def, ctx, runId);
2595
+ }
2596
+ async function recoverWorkflows(defs, ctx = {}) {
2597
+ const persistence = ctx.persistence ?? getDefaultPersistence();
2598
+ const running = await persistence.runs.list({ status: "running" });
2599
+ const resumed = [];
2600
+ for (const record2 of running) {
2601
+ if (record2.kind !== "workflow") continue;
2602
+ const def = defs[record2.agent];
2603
+ if (!def) {
2604
+ await persistence.runs.update(record2.runId, { status: "interrupted", endedAt: Date.now() });
2605
+ continue;
2606
+ }
2607
+ void advanceWorkflow(def, { ...ctx, persistence }, record2.runId).catch(() => {
2608
+ });
2609
+ resumed.push(record2.runId);
2610
+ }
2611
+ return resumed;
2612
+ }
2613
+ function waitingGate(def, record2) {
2614
+ if (record2.status !== "waiting_approval") return void 0;
2615
+ const state = record2.state;
2616
+ const step = state ? def.steps[state.cursor] : void 0;
2617
+ return step?.kind === "gate" ? { id: step.id, title: step.title } : void 0;
2618
+ }
2619
+
2620
+ // src/channel.ts
2621
+ function defineChannel(channel) {
2622
+ return channel;
2623
+ }
2624
+ function isChannelDefinition(value) {
2625
+ return Boolean(value && typeof value === "object" && typeof value.handle === "function");
2626
+ }
2627
+ async function verifyGithubWebhookSignature(secret, body, header) {
2628
+ if (!secret || !header?.startsWith("sha256=")) return false;
2629
+ const expected = "sha256=" + await hmacSha256Hex(secret, body);
2630
+ return timingSafeEqual(expected, header);
2631
+ }
2632
+ async function verifySlackRequestSignature(secret, body, timestamp, signature, options = {}) {
2633
+ if (!secret || !timestamp || !signature?.startsWith("v0=")) return false;
2634
+ const now = options.now ?? Date.now();
2635
+ const toleranceMs = options.toleranceMs ?? 5 * 60 * 1e3;
2636
+ const ageMs = Math.abs(now - Number(timestamp) * 1e3);
2637
+ if (!Number.isFinite(ageMs) || ageMs > toleranceMs) return false;
2638
+ const expected = "v0=" + await hmacSha256Hex(secret, "v0:" + timestamp + ":" + body);
2639
+ return timingSafeEqual(expected, signature);
2640
+ }
2641
+ async function hmacSha256Hex(secret, body) {
2642
+ const encoder = new TextEncoder();
2643
+ const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
2644
+ const digest = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
2645
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
2646
+ }
2647
+ function timingSafeEqual(a, b) {
2648
+ if (a.length !== b.length) return false;
2649
+ let diff = 0;
2650
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
2651
+ return diff === 0;
2652
+ }
2653
+
2654
+ // src/cron.ts
2655
+ function cronMatches(expr, date) {
2656
+ const fields = expr.trim().split(/\s+/);
2657
+ if (fields.length !== 5) throw new Error(`Invalid cron "${expr}": expected 5 fields, got ${fields.length}`);
2658
+ const [minute, hour, dom, month, dow] = fields;
2659
+ const domMatch = fieldMatches(dom, date.getUTCDate(), 1, 31);
2660
+ const dowMatch = fieldMatches(dow, date.getUTCDay(), 0, 7, true);
2661
+ const dayMatch = dom !== "*" && dow !== "*" ? domMatch || dowMatch : domMatch && dowMatch;
2662
+ return fieldMatches(minute, date.getUTCMinutes(), 0, 59) && fieldMatches(hour, date.getUTCHours(), 0, 23) && dayMatch && fieldMatches(month, date.getUTCMonth() + 1, 1, 12);
2663
+ }
2664
+ function fieldMatches(field, value, min, max, isDow = false) {
2665
+ const values = isDow && value === 0 ? [0, 7] : [value];
2666
+ for (const part of field.split(",")) {
2667
+ const m = part.match(/^(\*|\d+(?:-\d+)?)(?:\/(\d+))?$/);
2668
+ if (!m) throw new Error(`Invalid cron field "${field}"`);
2669
+ const [, range, stepStr] = m;
2670
+ const step = stepStr ? Number(stepStr) : 1;
2671
+ if (step < 1) throw new Error(`Invalid cron step in "${field}"`);
2672
+ let lo = min;
2673
+ let hi = max;
2674
+ if (range !== "*") {
2675
+ const [a, b] = range.split("-").map(Number);
2676
+ lo = a;
2677
+ hi = b ?? (stepStr ? max : a);
2678
+ }
2679
+ for (const v4 of values) {
2680
+ if (v4 >= lo && v4 <= hi && (v4 - lo) % step === 0) return true;
2681
+ }
2682
+ }
2683
+ return false;
2684
+ }
2685
+ function startCronScheduler(jobs, opts) {
2686
+ if (jobs.length === 0) return () => {
2687
+ };
2688
+ let lastMinute = -1;
2689
+ const tick = () => {
2690
+ const now = /* @__PURE__ */ new Date();
2691
+ const minuteKey = Math.floor(now.getTime() / 6e4);
2692
+ if (minuteKey === lastMinute) return;
2693
+ lastMinute = minuteKey;
2694
+ for (const job of jobs) {
2695
+ try {
2696
+ if (cronMatches(job.cron, now)) {
2697
+ Promise.resolve(job.run()).catch((err) => opts?.onError?.(err, job));
2698
+ }
2699
+ } catch (err) {
2700
+ opts?.onError?.(err, job);
2701
+ }
2702
+ }
2703
+ };
2704
+ const timer = setInterval(tick, 15e3);
2705
+ timer.unref?.();
2706
+ return () => clearInterval(timer);
2707
+ }
2708
+
2709
+ export {
2710
+ createAgent,
2711
+ defineAgent,
2712
+ isCreatedAgent,
2713
+ defineAgentProfile,
2714
+ applyProfile,
2715
+ ToolApprovalRequiredError,
2716
+ isToolApprovalRequiredError,
2717
+ attachToolApprovalPartialTranscript,
2718
+ waitingToolApproval,
2719
+ decideToolApprovalState,
2720
+ builtinTools,
2721
+ BUILTIN_TOOL_NAMES,
2722
+ isWorkspaceSkill,
2723
+ discoverSessionContext,
2724
+ loadWorkspaceSkill,
2725
+ parseSkillMarkdown,
2726
+ RuleViolation,
2727
+ makeRuleGuard,
2728
+ emptyUsage,
2729
+ addUsage,
2730
+ PiAgentEngine,
2731
+ envApiKeyFor,
2732
+ EngineError,
2733
+ EventBus,
2734
+ observe,
2735
+ makeCall,
2736
+ defineTool,
2737
+ rawTool,
2738
+ isRawTool,
2739
+ resolveTool,
2740
+ resolveAnyTool,
2741
+ Session,
2742
+ Harness,
2743
+ defaultEngine,
2744
+ getDefaultPersistence,
2745
+ genId,
2746
+ dispatchWorkflow,
2747
+ recoverRuns,
2748
+ invokeAgent,
2749
+ WorkflowDefSchema,
2750
+ parseWorkflowDef,
2751
+ startWorkflow,
2752
+ advanceWorkflow,
2753
+ approveGate,
2754
+ recoverWorkflows,
2755
+ waitingGate,
2756
+ defineChannel,
2757
+ isChannelDefinition,
2758
+ verifyGithubWebhookSignature,
2759
+ verifySlackRequestSignature,
2760
+ cronMatches,
2761
+ startCronScheduler
2762
+ };