@flue/sdk 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,14 @@
1
- # flue
2
-
3
1
  > **Experimental** — Flue is under active development. APIs may change.
2
+ >
3
+ > Looking for `v0.0.x`? [See here.](https://github.com/withastro/flue/tree/v0.0.x)
4
+
5
+ # Flue
6
+
7
+ Flue is **The Sandbox Agent Framework.** If you know how to use Claude Code (or OpenCode, Codex, Gemini, etc)... then you already know the basics of how to build agents with Flue.
4
8
 
5
- Agent framework where agents are directories compiled into deployable server artifacts.
9
+ A [Sandbox Agent](https://developers.openai.com/api/docs/guides/agents/sandboxes) pairs an **agent harness** (like Claude Code) with a secure, isolated container workspace. Sandbox Agents can edit files, write and execute code, spin up subagents, run terminal commands, and drive themselves autonomously to solve any given task. This pattern unlocks more powerful, intelligent agents that traditional AI frameworks wouldn't otherwise let you build.
10
+
11
+ Our take is that 1) any agent can be represented as a Sandbox Agent, and 2) any agent is _best_ represented as a Sandbox Agent. So we designed Flue to deliver on this vision.
6
12
 
7
13
  ## Packages
8
14
 
@@ -18,6 +24,8 @@ Agent framework where agents are directories compiled into deployable server art
18
24
 
19
25
  The simplest agent — no container, no tools, just a prompt and a typed result.
20
26
 
27
+ Unless you opt-in to initializing a full container sandbox, Flue will default to a virtual sandbox for every agent, powered by [just-bash](https://github.com/vercel-labs/just-bash). A virtual sandbox is going to be dramatically faster, cheaper, and more scalable than running a full container for every agent, which makes it perfect for building high-traffic/high-scale agents.
28
+
21
29
  ```ts
22
30
  // .flue/agents/hello-world.ts
23
31
  import type { FlueContext } from '@flue/sdk/client';
@@ -34,8 +42,8 @@ export default async function ({ init, payload, sessionId }: FlueContext) {
34
42
  const session = await init();
35
43
 
36
44
  // prompt() sends a message in the session, triggering action.
37
- // You can pass a schema to `result` to get typed, validated JSON back.
38
45
  const result = await session.prompt(`Translate this to ${payload.language}: "${payload.text}"`, {
46
+ // Pass a result schema to get typed, schema-validated data back from your agent.
39
47
  result: v.object({
40
48
  translation: v.string(),
41
49
  confidence: v.picklist(['low', 'medium', 'high']),
@@ -48,9 +56,9 @@ export default async function ({ init, payload, sessionId }: FlueContext) {
48
56
 
49
57
  ### Support Agent
50
58
 
51
- A support agent, also running in a virtual sandbox but now with an R2 bucket mounted as its file-system. The knowledge base is stored in R2 and mounted directly into the agent's filesystem — the agent searches it with its built-in tools (grep, glob, read).
59
+ A support agent can also run in a virtual sandbox, but we now add a file-system using an R2 bucket. The knowledge base is stored in R2 and mounted directly into the agent's filesystem — the agent searches it with its built-in tools (grep, glob, read). Skills are also defined in the bucket that help the agent perform its task.
52
60
 
53
- Session message history and file-system state are automatically persisted using Durable Objects (Cloudflare only). So you can revisit this session days, weeks, or years later and pick up where you left off automatically.
61
+ Because this agent is deployed to Cloudflare, message history and session state are automatically persisted for you. So you (or your customer) can revisit this support session days, weeks, or years later and pick up exactly where you left off.
54
62
 
55
63
  ```ts
56
64
  // .flue/agents/support.ts
@@ -72,18 +80,17 @@ export default async function ({ init, payload, env }: FlueContext) {
72
80
  relevant to this request, then write a helpful response.
73
81
 
74
82
  Customer: ${payload.message}`,
83
+ {
84
+ // Provide roles (aka subagents) to guide your agent. Defined in .flue/roles/
85
+ role: 'triager',
86
+ },
75
87
  );
76
88
  }
77
89
  ```
78
90
 
79
91
  ### Issue Triage (CI)
80
92
 
81
- A triage agent that runs whenever a new issue is opened (or commented on) on GitHub, running on GitHub Actions.
82
-
83
- Flue was designed to power CI workflows since day one. The `"local"` filesystem sandbox enables two things:
84
-
85
- 1. Mount the current directory to your virtual file system.
86
- 2. Connect privileged CLIs to your agent (`gh`, `glab`, `git`) without leaking sensitive keys and secrets.
93
+ A triage agent that runs in CI whenever an issue is opened on GitHub. The `"local"` sandbox mounts the host filesystem and lets you connect privileged CLIs (`gh`, `npm`, `git`) to the agent without leaking secrets.
87
94
 
88
95
  ```ts
89
96
  // .flue/agents/triage.ts
@@ -92,6 +99,8 @@ import { execFile } from 'node:child_process';
92
99
  import { promisify } from 'node:util';
93
100
  import * as v from 'valibot';
94
101
 
102
+ // Because we are running this in CI, we don't need to expose this as an HTTP endpoint.
103
+ // The CLI can run any agent from the command line, `flue run triage ...`
95
104
  export const triggers = {};
96
105
 
97
106
  // Connect privileged CLIs to your agent without leaking sensitive keys and secrets.
@@ -108,15 +117,23 @@ export default async function ({ init, payload }: FlueContext) {
108
117
  // 'local' mounts the host filesystem at /workspace — ideal for CI
109
118
  // where the repo is already checked out. Skills and AGENTS.md are
110
119
  // discovered automatically from the workspace directory.
111
- const session = await init({ sandbox: 'local' });
120
+ //
121
+ // `model` sets the default model for every prompt/skill call in this
122
+ // session. Override per-call with `{ model: '...' }` on prompt()/skill().
123
+ const session = await init({
124
+ sandbox: 'local',
125
+ model: 'anthropic/claude-opus-4-20250514',
126
+ });
112
127
 
128
+ // Skills can be referenced either by their frontmatter `name:` (shown below)
129
+ // or by a relative path under `.agents/skills/` — e.g.
130
+ // `session.skill('triage/reproduce.md', ...)`. Path references are handy for
131
+ // skill packs that group multiple stages under one directory.
113
132
  const result = await session.skill('triage', {
114
133
  // Pass arguments to any prompt or skill.
115
134
  args: { issueNumber: payload.issueNumber },
116
135
  // Grant access to `gh` and `npm` for the life of this skill.
117
136
  commands: [gh, npm],
118
- // Provide roles (aka subagents) to guide your agent. Defined in .flue/roles/
119
- role: 'triager',
120
137
  // Result schemas are great for being able to act/orchestrate
121
138
  // based on the result of your prompt or skill call.
122
139
  result: v.object({
@@ -0,0 +1,432 @@
1
+ import { Type } from "@mariozechner/pi-ai";
2
+
3
+ //#region src/context.ts
4
+ /** Parse optional YAML frontmatter (--- delimited). Basic `key: value` only. */
5
+ function parseFrontmatterFile(content, defaultName) {
6
+ const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
7
+ if (!frontmatterMatch) return {
8
+ name: defaultName,
9
+ description: "",
10
+ body: content.trim(),
11
+ frontmatter: {}
12
+ };
13
+ const rawFrontmatter = frontmatterMatch[1] ?? "";
14
+ const body = frontmatterMatch[2] ?? "";
15
+ const frontmatter = {};
16
+ for (const line of rawFrontmatter.split("\n")) {
17
+ const match = line.match(/^(\w+):\s*(.+)$/);
18
+ if (match?.[1] && match[2]) frontmatter[match[1]] = match[2].trim();
19
+ }
20
+ return {
21
+ name: frontmatter.name || defaultName,
22
+ description: frontmatter.description || "",
23
+ body: body.trim(),
24
+ frontmatter
25
+ };
26
+ }
27
+ /** Read AGENTS.md (and CLAUDE.md if present) from a directory. Returns concatenated contents. */
28
+ async function readAgentsMd(env, basePath) {
29
+ const parts = [];
30
+ for (const filename of ["AGENTS.md", "CLAUDE.md"]) {
31
+ const filePath = basePath.endsWith("/") ? basePath + filename : `${basePath}/${filename}`;
32
+ if (await env.exists(filePath)) {
33
+ const content = await env.readFile(filePath);
34
+ parts.push(content.trim());
35
+ }
36
+ }
37
+ return parts.join("\n\n");
38
+ }
39
+ /**
40
+ * Load a skill directly by relative path under `.agents/skills/`.
41
+ *
42
+ * The path is taken as-is — no extension is auto-appended. Callers reference
43
+ * the full filename, e.g. `'triage/reproduce.md'`. Returns `null` if the file
44
+ * doesn't exist.
45
+ *
46
+ * Used as a fallback by `session.skill()` when the requested name doesn't match
47
+ * a discovered skill's frontmatter `name:` field. Lets users organise skills as
48
+ * a pack of sibling markdown files under one directory (orchestration SKILL.md
49
+ * + stage files) without forcing each stage into its own `SKILL.md` subdirectory.
50
+ */
51
+ async function loadSkillByPath(env, basePath, relPath) {
52
+ const filePath = `${basePath.endsWith("/") ? `${basePath}.agents/skills` : `${basePath}/.agents/skills`}/${relPath}`;
53
+ if (!await env.exists(filePath)) return null;
54
+ const parsed = parseFrontmatterFile(await env.readFile(filePath), relPath.replace(/\.(md|markdown)$/i, ""));
55
+ return {
56
+ name: parsed.name,
57
+ description: parsed.description,
58
+ instructions: parsed.body
59
+ };
60
+ }
61
+ /** Discover skills from .agents/skills/<name>/SKILL.md under basePath. */
62
+ async function discoverLocalSkills(env, basePath) {
63
+ const skillsDir = basePath.endsWith("/") ? `${basePath}.agents/skills` : `${basePath}/.agents/skills`;
64
+ if (!await env.exists(skillsDir)) return {};
65
+ const skills = {};
66
+ const entries = await env.readdir(skillsDir);
67
+ for (const entry of entries) {
68
+ const skillDir = `${skillsDir}/${entry}`;
69
+ try {
70
+ if (!(await env.stat(skillDir)).isDirectory) continue;
71
+ } catch {
72
+ continue;
73
+ }
74
+ const skillMdPath = `${skillDir}/SKILL.md`;
75
+ if (!await env.exists(skillMdPath)) continue;
76
+ const parsed = parseFrontmatterFile(await env.readFile(skillMdPath), entry);
77
+ skills[parsed.name] = {
78
+ name: parsed.name,
79
+ description: parsed.description,
80
+ instructions: parsed.body
81
+ };
82
+ }
83
+ return skills;
84
+ }
85
+ function composeSystemPrompt(agentsMd, skills, env) {
86
+ const parts = [];
87
+ if (agentsMd) parts.push(agentsMd);
88
+ const skillEntries = Object.values(skills);
89
+ if (skillEntries.length > 0) {
90
+ parts.push("", "## Available Skills", "");
91
+ for (const skill of skillEntries) {
92
+ const desc = skill.description ? ` - ${skill.description}` : "";
93
+ parts.push(`- **${skill.name}**${desc}`);
94
+ }
95
+ }
96
+ if (env) {
97
+ const date = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
98
+ weekday: "short",
99
+ year: "numeric",
100
+ month: "short",
101
+ day: "numeric"
102
+ });
103
+ parts.push("", `Date: ${date}`);
104
+ parts.push(`Working directory: ${env.cwd}`);
105
+ if (env.directoryListing && env.directoryListing.length > 0) parts.push("", "Directory structure:", env.directoryListing.join("\n"));
106
+ }
107
+ return parts.join("\n");
108
+ }
109
+ /** Discover AGENTS.md, local skills, and directory listing from the session's cwd. */
110
+ async function discoverSessionContext(env) {
111
+ const cwd = env.cwd;
112
+ const agentsMd = await readAgentsMd(env, cwd);
113
+ const skills = await discoverLocalSkills(env, cwd);
114
+ let directoryListing;
115
+ try {
116
+ directoryListing = await env.readdir(cwd);
117
+ } catch {}
118
+ return {
119
+ systemPrompt: composeSystemPrompt(agentsMd, skills, {
120
+ cwd,
121
+ directoryListing
122
+ }),
123
+ skills
124
+ };
125
+ }
126
+
127
+ //#endregion
128
+ //#region src/agent.ts
129
+ const MAX_READ_LINES = 2e3;
130
+ const MAX_READ_BYTES = 50 * 1024;
131
+ const MAX_GREP_MATCHES = 100;
132
+ const MAX_GREP_LINE_LENGTH = 500;
133
+ const MAX_GLOB_RESULTS = 1e3;
134
+ const BUILTIN_TOOL_NAMES = new Set([
135
+ "read",
136
+ "write",
137
+ "edit",
138
+ "bash",
139
+ "grep",
140
+ "glob"
141
+ ]);
142
+ function createTools(env) {
143
+ return [
144
+ createReadTool(env),
145
+ createWriteTool(env),
146
+ createEditTool(env),
147
+ createBashTool(env),
148
+ createGrepTool(env),
149
+ createGlobTool(env)
150
+ ];
151
+ }
152
+ function createReadTool(env) {
153
+ return {
154
+ name: "read",
155
+ label: "Read File",
156
+ description: "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.",
157
+ parameters: Type.Object({
158
+ path: Type.String({ description: "Path to the file to read" }),
159
+ offset: Type.Optional(Type.Number({ description: "Line number to start from (1-indexed)" })),
160
+ limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" }))
161
+ }),
162
+ async execute(_toolCallId, params, signal) {
163
+ throwIfAborted(signal);
164
+ try {
165
+ if ((await env.stat(params.path)).isDirectory) {
166
+ const entries = await env.readdir(params.path);
167
+ return {
168
+ content: [{
169
+ type: "text",
170
+ text: entries.join("\n") || "(empty directory)"
171
+ }],
172
+ details: {
173
+ path: params.path,
174
+ isDirectory: true,
175
+ entries: entries.length
176
+ }
177
+ };
178
+ }
179
+ } catch {}
180
+ const allLines = (await env.readFile(params.path)).split("\n");
181
+ const startLine = params.offset ? Math.max(0, params.offset - 1) : 0;
182
+ if (startLine >= allLines.length) throw new Error(`Offset ${params.offset} is beyond end of file (${allLines.length} lines total)`);
183
+ const endLine = params.limit ? startLine + params.limit : allLines.length;
184
+ const { text: truncatedText, wasTruncated } = truncateHead(allLines.slice(startLine, endLine), MAX_READ_LINES, MAX_READ_BYTES);
185
+ let output = truncatedText;
186
+ if (wasTruncated) {
187
+ const shownEnd = startLine + truncatedText.split("\n").length;
188
+ output += `\n\n[Showing lines ${startLine + 1}-${shownEnd} of ${allLines.length}. Use offset=${shownEnd + 1} to continue.]`;
189
+ }
190
+ return {
191
+ content: [{
192
+ type: "text",
193
+ text: output
194
+ }],
195
+ details: {
196
+ path: params.path,
197
+ lines: allLines.length
198
+ }
199
+ };
200
+ }
201
+ };
202
+ }
203
+ function createWriteTool(env) {
204
+ return {
205
+ name: "write",
206
+ label: "Write File",
207
+ description: "Write content to a file. Creates the file and parent directories if they do not exist.",
208
+ parameters: Type.Object({
209
+ path: Type.String({ description: "Path to the file to write" }),
210
+ content: Type.String({ description: "Content to write to the file" })
211
+ }),
212
+ async execute(_toolCallId, params, signal) {
213
+ throwIfAborted(signal);
214
+ const resolved = env.resolvePath(params.path);
215
+ const dir = resolved.replace(/\/[^/]*$/, "");
216
+ if (dir && dir !== resolved) await env.mkdir(dir, { recursive: true });
217
+ await env.writeFile(resolved, params.content);
218
+ return {
219
+ content: [{
220
+ type: "text",
221
+ text: `Successfully wrote ${params.content.length} bytes to ${params.path}`
222
+ }],
223
+ details: {
224
+ path: params.path,
225
+ size: params.content.length
226
+ }
227
+ };
228
+ }
229
+ };
230
+ }
231
+ function createEditTool(env) {
232
+ return {
233
+ name: "edit",
234
+ label: "Edit File",
235
+ description: "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.",
236
+ parameters: Type.Object({
237
+ path: Type.String({ description: "Path to the file to edit" }),
238
+ oldText: Type.String({ description: "Exact text to find (must be unique)" }),
239
+ newText: Type.String({ description: "Replacement text" }),
240
+ replaceAll: Type.Optional(Type.Boolean({ description: "Replace all occurrences" }))
241
+ }),
242
+ async execute(_toolCallId, params, signal) {
243
+ throwIfAborted(signal);
244
+ const content = await env.readFile(params.path);
245
+ if (params.replaceAll) {
246
+ const newContent = content.replaceAll(params.oldText, params.newText);
247
+ if (newContent === content) throw new Error(`Could not find the text in ${params.path}. No changes made.`);
248
+ await env.writeFile(params.path, newContent);
249
+ const count = content.split(params.oldText).length - 1;
250
+ return {
251
+ content: [{
252
+ type: "text",
253
+ text: `Replaced ${count} occurrences in ${params.path}`
254
+ }],
255
+ details: {
256
+ path: params.path,
257
+ replacements: count
258
+ }
259
+ };
260
+ }
261
+ const occurrences = countOccurrences(content, params.oldText);
262
+ if (occurrences === 0) throw new Error(`Could not find the exact text in ${params.path}. Make sure your oldText matches exactly, including whitespace and indentation.`);
263
+ if (occurrences > 1) throw new Error(`Found ${occurrences} occurrences of the text in ${params.path}. Provide more surrounding context to make the match unique, or use replaceAll.`);
264
+ const newContent = content.replace(params.oldText, params.newText);
265
+ await env.writeFile(params.path, newContent);
266
+ return {
267
+ content: [{
268
+ type: "text",
269
+ text: `Successfully edited ${params.path}`
270
+ }],
271
+ details: { path: params.path }
272
+ };
273
+ }
274
+ };
275
+ }
276
+ function createBashTool(env) {
277
+ return {
278
+ name: "bash",
279
+ label: "Run Command",
280
+ description: "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.",
281
+ parameters: Type.Object({
282
+ command: Type.String({ description: "Bash command to execute" }),
283
+ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds" }))
284
+ }),
285
+ async execute(_toolCallId, params, signal) {
286
+ throwIfAborted(signal);
287
+ return formatBashResult(await env.exec(params.command), params.command);
288
+ }
289
+ };
290
+ }
291
+ function formatBashResult(result, command) {
292
+ const { text: output } = truncateTail((result.stdout + (result.stderr ? "\n" + result.stderr : "")).trim(), MAX_READ_LINES, MAX_READ_BYTES);
293
+ if (result.exitCode !== 0) throw new Error(`${output}\n\nCommand exited with code ${result.exitCode}`);
294
+ return {
295
+ content: [{
296
+ type: "text",
297
+ text: output || "(no output)"
298
+ }],
299
+ details: {
300
+ command,
301
+ exitCode: result.exitCode
302
+ }
303
+ };
304
+ }
305
+ function createGrepTool(env) {
306
+ return {
307
+ name: "grep",
308
+ label: "Search Files",
309
+ description: "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.",
310
+ parameters: Type.Object({
311
+ pattern: Type.String({ description: "Search pattern (regex)" }),
312
+ path: Type.Optional(Type.String({ description: "Directory or file to search (default: .)" })),
313
+ include: Type.Optional(Type.String({ description: "Glob filter, e.g. \"*.ts\"" }))
314
+ }),
315
+ async execute(_toolCallId, params, signal) {
316
+ throwIfAborted(signal);
317
+ const searchPath = params.path || ".";
318
+ let cmd = `grep -rn "${escapeShellArg(params.pattern)}" ${escapeShellArg(searchPath)}`;
319
+ if (params.include) cmd = `grep -rn --include="${escapeShellArg(params.include)}" "${escapeShellArg(params.pattern)}" ${escapeShellArg(searchPath)}`;
320
+ const result = await env.exec(cmd);
321
+ if (result.exitCode === 1 && !result.stdout.trim()) return {
322
+ content: [{
323
+ type: "text",
324
+ text: "No matches found."
325
+ }],
326
+ details: { matchCount: 0 }
327
+ };
328
+ if (result.exitCode > 1) throw new Error(`grep failed: ${result.stderr}`);
329
+ const lines = result.stdout.trim().split("\n");
330
+ let finalOutput = lines.slice(0, MAX_GREP_MATCHES).map((line) => line.length > MAX_GREP_LINE_LENGTH ? line.slice(0, MAX_GREP_LINE_LENGTH) + "..." : line).join("\n");
331
+ if (lines.length > MAX_GREP_MATCHES) finalOutput += `\n\n[Showing ${MAX_GREP_MATCHES} of ${lines.length} matches. Narrow your search.]`;
332
+ return {
333
+ content: [{
334
+ type: "text",
335
+ text: finalOutput
336
+ }],
337
+ details: { matchCount: Math.min(lines.length, MAX_GREP_MATCHES) }
338
+ };
339
+ }
340
+ };
341
+ }
342
+ function createGlobTool(env) {
343
+ return {
344
+ name: "glob",
345
+ label: "Find Files",
346
+ description: "Find files by glob pattern. Returns matching file paths.",
347
+ parameters: Type.Object({
348
+ pattern: Type.String({ description: "Glob pattern, e.g. \"**/*.ts\"" }),
349
+ path: Type.Optional(Type.String({ description: "Directory to search in (default: .)" }))
350
+ }),
351
+ async execute(_toolCallId, params, signal) {
352
+ throwIfAborted(signal);
353
+ const cmd = `find ${escapeShellArg(params.path || ".")} -type f -name "${escapeShellArg(params.pattern)}" 2>/dev/null | head -${MAX_GLOB_RESULTS}`;
354
+ const result = await env.exec(cmd);
355
+ if (result.exitCode !== 0 && !result.stdout.trim()) return {
356
+ content: [{
357
+ type: "text",
358
+ text: "No files found matching pattern."
359
+ }],
360
+ details: { matchCount: 0 }
361
+ };
362
+ const paths = result.stdout.trim().split("\n").filter(Boolean);
363
+ if (paths.length === 0) return {
364
+ content: [{
365
+ type: "text",
366
+ text: "No files found matching pattern."
367
+ }],
368
+ details: { matchCount: 0 }
369
+ };
370
+ return {
371
+ content: [{
372
+ type: "text",
373
+ text: paths.join("\n")
374
+ }],
375
+ details: { matchCount: paths.length }
376
+ };
377
+ }
378
+ };
379
+ }
380
+ function throwIfAborted(signal) {
381
+ if (signal?.aborted) throw new Error("Operation aborted");
382
+ }
383
+ function countOccurrences(str, substr) {
384
+ let count = 0;
385
+ let pos = str.indexOf(substr, 0);
386
+ while (pos !== -1) {
387
+ count++;
388
+ pos = str.indexOf(substr, pos + substr.length);
389
+ }
390
+ return count;
391
+ }
392
+ function escapeShellArg(arg) {
393
+ return arg.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$").replace(/`/g, "\\`");
394
+ }
395
+ function truncateHead(lines, maxLines, maxBytes) {
396
+ let result = "";
397
+ let lineCount = 0;
398
+ let wasTruncated = false;
399
+ for (const line of lines) {
400
+ if (lineCount >= maxLines) {
401
+ wasTruncated = true;
402
+ break;
403
+ }
404
+ const next = lineCount === 0 ? line : "\n" + line;
405
+ if (result.length + next.length > maxBytes) {
406
+ wasTruncated = true;
407
+ break;
408
+ }
409
+ result += next;
410
+ lineCount++;
411
+ }
412
+ return {
413
+ text: result,
414
+ wasTruncated
415
+ };
416
+ }
417
+ function truncateTail(text, maxLines, maxBytes) {
418
+ const lines = text.split("\n");
419
+ if (lines.length <= maxLines && text.length <= maxBytes) return {
420
+ text,
421
+ wasTruncated: false
422
+ };
423
+ let result = lines.slice(-maxLines).join("\n");
424
+ if (result.length > maxBytes) result = result.slice(-maxBytes);
425
+ return {
426
+ text: result,
427
+ wasTruncated: true
428
+ };
429
+ }
430
+
431
+ //#endregion
432
+ export { parseFrontmatterFile as a, loadSkillByPath as i, createTools as n, discoverSessionContext as r, BUILTIN_TOOL_NAMES as t };
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as ShellOptions, D as TaskOptions, E as SkillOptions, O as ToolDef, S as SessionStore, b as SessionEnv, d as FlueContext, f as FlueEvent, g as PromptResponse, h as PromptOptions, l as CommandSupport, m as FlueSession, p as FlueEventCallback, r as BashLike, s as Command, t as AgentConfig, u as FileStat, v as SandboxFactory, w as ShellResult, x as SessionInit, y as SessionData } from "./types-xNvqlohs.mjs";
1
+ import { C as ShellOptions, D as TaskOptions, E as SkillOptions, O as ToolDef, S as SessionStore, b as SessionEnv, d as FlueContext, f as FlueEvent, g as PromptResponse, h as PromptOptions, l as CommandSupport, m as FlueSession, p as FlueEventCallback, r as BashLike, s as Command, t as AgentConfig, u as FileStat, v as SandboxFactory, w as ShellResult, x as SessionInit, y as SessionData } from "./types-C8tsaK1j.mjs";
2
2
  import { Type } from "@mariozechner/pi-ai";
3
3
 
4
4
  //#region src/client.d.ts
package/dist/client.mjs CHANGED
@@ -1,4 +1,5 @@
1
- import { n as Session, o as discoverSessionContext } from "./session-BD0MEuO3.mjs";
1
+ import { r as discoverSessionContext } from "./agent-BYG0nVbQ.mjs";
2
+ import { n as Session } from "./session-BRLCNVG1.mjs";
2
3
  import { bashToSessionEnv } from "./sandbox.mjs";
3
4
  import { Type } from "@mariozechner/pi-ai";
4
5
 
@@ -24,10 +25,12 @@ function createFlueContext(config) {
24
25
  const store = options?.persist ?? config.defaultStore;
25
26
  const savedData = await store.load(config.sessionId);
26
27
  const localContext = await discoverSessionContext(env);
28
+ const sessionModel = options?.model && config.agentConfig.resolveModel ? config.agentConfig.resolveModel(options.model) : config.agentConfig.model;
27
29
  const sessionConfig = {
28
30
  ...config.agentConfig,
29
31
  systemPrompt: localContext.systemPrompt,
30
- skills: localContext.skills
32
+ skills: localContext.skills,
33
+ model: sessionModel
31
34
  };
32
35
  return new Session(config.sessionId, sessionConfig, env, store, savedData, currentEventCallback);
33
36
  },
@@ -1,4 +1,4 @@
1
- import { S as SessionStore, b as SessionEnv } from "../types-xNvqlohs.mjs";
1
+ import { S as SessionStore, b as SessionEnv } from "../types-C8tsaK1j.mjs";
2
2
 
3
3
  //#region src/cloudflare/virtual-sandbox.d.ts
4
4
  interface VirtualSandboxOptions {
@@ -1,4 +1,5 @@
1
- import "../session-BD0MEuO3.mjs";
1
+ import "../agent-BYG0nVbQ.mjs";
2
+ import "../session-BRLCNVG1.mjs";
2
3
  import { createSandboxSessionEnv } from "../sandbox.mjs";
3
4
  import { Workspace, WorkspaceFileSystem } from "@cloudflare/shell";
4
5
 
package/dist/index.d.mts CHANGED
@@ -1,7 +1,5 @@
1
- import { C as ShellOptions, D as TaskOptions, E as SkillOptions, O as ToolDef, S as SessionStore, T as Skill, _ as Role, a as BuildOptions, b as SessionEnv, c as CommandDef, d as FlueContext, f as FlueEvent, g as PromptResponse, h as PromptOptions, i as BuildContext, l as CommandSupport, m as FlueSession, n as AgentInfo, o as BuildPlugin, p as FlueEventCallback, r as BashLike, s as Command, t as AgentConfig, u as FileStat, v as SandboxFactory, w as ShellResult, x as SessionInit, y as SessionData } from "./types-xNvqlohs.mjs";
2
- import { FlueContextConfig, FlueContextInternal, createFlueContext } from "./client.mjs";
1
+ import { C as ShellOptions, D as TaskOptions, E as SkillOptions, O as ToolDef, S as SessionStore, T as Skill, _ as Role, a as BuildOptions, b as SessionEnv, c as CommandDef, d as FlueContext, f as FlueEvent, g as PromptResponse, h as PromptOptions, i as BuildContext, l as CommandSupport, m as FlueSession, n as AgentInfo, o as BuildPlugin, p as FlueEventCallback, r as BashLike, s as Command, t as AgentConfig, u as FileStat, v as SandboxFactory, w as ShellResult, x as SessionInit, y as SessionData } from "./types-C8tsaK1j.mjs";
3
2
  import { AgentTool } from "@mariozechner/pi-agent-core";
4
- import "valibot";
5
3
 
6
4
  //#region src/build.d.ts
7
5
  /**
@@ -10,17 +8,8 @@ import "valibot";
10
8
  */
11
9
  declare function build(options: BuildOptions): Promise<void>;
12
10
  //#endregion
13
- //#region src/session.d.ts
14
- /** In-memory session store. Sessions persist for the lifetime of the process. */
15
- declare class InMemorySessionStore implements SessionStore {
16
- private store;
17
- save(id: string, data: SessionData): Promise<void>;
18
- load(id: string): Promise<SessionData | null>;
19
- delete(id: string): Promise<void>;
20
- }
21
- //#endregion
22
11
  //#region src/agent.d.ts
23
12
  declare const BUILTIN_TOOL_NAMES: Set<string>;
24
13
  declare function createTools(env: SessionEnv): AgentTool<any>[];
25
14
  //#endregion
26
- export { type AgentConfig, type AgentInfo, BUILTIN_TOOL_NAMES, type BashLike, type BuildContext, type BuildOptions, type BuildPlugin, type Command, type CommandDef, type CommandSupport, type FileStat, type FlueContext, type FlueContextConfig, type FlueContextInternal, type FlueEvent, type FlueEventCallback, type FlueSession, InMemorySessionStore, type PromptOptions, type PromptResponse, type Role, type SandboxFactory, type SessionData, type SessionEnv, type SessionInit, type SessionStore, type ShellOptions, type ShellResult, type Skill, type SkillOptions, type TaskOptions, type ToolDef, build, createFlueContext, createTools };
15
+ export { type AgentConfig, type AgentInfo, BUILTIN_TOOL_NAMES, type BashLike, type BuildContext, type BuildOptions, type BuildPlugin, type Command, type CommandDef, type CommandSupport, type FileStat, type FlueContext, type FlueEvent, type FlueEventCallback, type FlueSession, type PromptOptions, type PromptResponse, type Role, type SandboxFactory, type SessionData, type SessionEnv, type SessionInit, type SessionStore, type ShellOptions, type ShellResult, type Skill, type SkillOptions, type TaskOptions, type ToolDef, build, createTools };