@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 +32 -15
- package/dist/agent-BYG0nVbQ.mjs +432 -0
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +5 -2
- package/dist/cloudflare/index.d.mts +1 -1
- package/dist/cloudflare/index.mjs +2 -1
- package/dist/index.d.mts +2 -13
- package/dist/index.mjs +88 -72
- package/dist/internal.d.mts +15 -0
- package/dist/internal.mjs +6 -0
- package/dist/sandbox.d.mts +1 -1
- package/dist/sandbox.mjs +2 -1
- package/dist/{session-BD0MEuO3.mjs → session-BRLCNVG1.mjs} +38 -413
- package/dist/{types-xNvqlohs.d.mts → types-C8tsaK1j.d.mts} +18 -9
- package/package.json +5 -1
|
@@ -1,415 +1,9 @@
|
|
|
1
|
+
import { i as loadSkillByPath, n as createTools, r as discoverSessionContext, t as BUILTIN_TOOL_NAMES } from "./agent-BYG0nVbQ.mjs";
|
|
2
|
+
import { completeSimple, isContextOverflow } from "@mariozechner/pi-ai";
|
|
1
3
|
import { Agent } from "@mariozechner/pi-agent-core";
|
|
2
|
-
import { Type, completeSimple, isContextOverflow } from "@mariozechner/pi-ai";
|
|
3
4
|
import { toJsonSchema } from "@valibot/to-json-schema";
|
|
4
5
|
import * as v from "valibot";
|
|
5
6
|
|
|
6
|
-
//#region src/context.ts
|
|
7
|
-
/** Parse optional YAML frontmatter (--- delimited). Basic `key: value` only. */
|
|
8
|
-
function parseFrontmatterFile(content, defaultName) {
|
|
9
|
-
const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
10
|
-
if (!frontmatterMatch) return {
|
|
11
|
-
name: defaultName,
|
|
12
|
-
description: "",
|
|
13
|
-
body: content.trim(),
|
|
14
|
-
frontmatter: {}
|
|
15
|
-
};
|
|
16
|
-
const rawFrontmatter = frontmatterMatch[1] ?? "";
|
|
17
|
-
const body = frontmatterMatch[2] ?? "";
|
|
18
|
-
const frontmatter = {};
|
|
19
|
-
for (const line of rawFrontmatter.split("\n")) {
|
|
20
|
-
const match = line.match(/^(\w+):\s*(.+)$/);
|
|
21
|
-
if (match?.[1] && match[2]) frontmatter[match[1]] = match[2].trim();
|
|
22
|
-
}
|
|
23
|
-
return {
|
|
24
|
-
name: frontmatter.name || defaultName,
|
|
25
|
-
description: frontmatter.description || "",
|
|
26
|
-
body: body.trim(),
|
|
27
|
-
frontmatter
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
/** Read AGENTS.md (and CLAUDE.md if present) from a directory. Returns concatenated contents. */
|
|
31
|
-
async function readAgentsMd(env, basePath) {
|
|
32
|
-
const parts = [];
|
|
33
|
-
for (const filename of ["AGENTS.md", "CLAUDE.md"]) {
|
|
34
|
-
const filePath = basePath.endsWith("/") ? basePath + filename : `${basePath}/${filename}`;
|
|
35
|
-
if (await env.exists(filePath)) {
|
|
36
|
-
const content = await env.readFile(filePath);
|
|
37
|
-
parts.push(content.trim());
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return parts.join("\n\n");
|
|
41
|
-
}
|
|
42
|
-
/** Discover skills from .agents/skills/<name>/SKILL.md under basePath. */
|
|
43
|
-
async function discoverLocalSkills(env, basePath) {
|
|
44
|
-
const skillsDir = basePath.endsWith("/") ? `${basePath}.agents/skills` : `${basePath}/.agents/skills`;
|
|
45
|
-
if (!await env.exists(skillsDir)) return {};
|
|
46
|
-
const skills = {};
|
|
47
|
-
const entries = await env.readdir(skillsDir);
|
|
48
|
-
for (const entry of entries) {
|
|
49
|
-
const skillDir = `${skillsDir}/${entry}`;
|
|
50
|
-
try {
|
|
51
|
-
if (!(await env.stat(skillDir)).isDirectory) continue;
|
|
52
|
-
} catch {
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
const skillMdPath = `${skillDir}/SKILL.md`;
|
|
56
|
-
if (!await env.exists(skillMdPath)) continue;
|
|
57
|
-
const parsed = parseFrontmatterFile(await env.readFile(skillMdPath), entry);
|
|
58
|
-
skills[parsed.name] = {
|
|
59
|
-
name: parsed.name,
|
|
60
|
-
description: parsed.description,
|
|
61
|
-
instructions: parsed.body
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
return skills;
|
|
65
|
-
}
|
|
66
|
-
function composeSystemPrompt(agentsMd, skills, env) {
|
|
67
|
-
const parts = [];
|
|
68
|
-
if (agentsMd) parts.push(agentsMd);
|
|
69
|
-
const skillEntries = Object.values(skills);
|
|
70
|
-
if (skillEntries.length > 0) {
|
|
71
|
-
parts.push("", "## Available Skills", "");
|
|
72
|
-
for (const skill of skillEntries) {
|
|
73
|
-
const desc = skill.description ? ` - ${skill.description}` : "";
|
|
74
|
-
parts.push(`- **${skill.name}**${desc}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
if (env) {
|
|
78
|
-
const date = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
|
|
79
|
-
weekday: "short",
|
|
80
|
-
year: "numeric",
|
|
81
|
-
month: "short",
|
|
82
|
-
day: "numeric"
|
|
83
|
-
});
|
|
84
|
-
parts.push("", `Date: ${date}`);
|
|
85
|
-
parts.push(`Working directory: ${env.cwd}`);
|
|
86
|
-
if (env.directoryListing && env.directoryListing.length > 0) parts.push("", "Directory structure:", env.directoryListing.join("\n"));
|
|
87
|
-
}
|
|
88
|
-
return parts.join("\n");
|
|
89
|
-
}
|
|
90
|
-
/** Discover AGENTS.md, local skills, and directory listing from the session's cwd. */
|
|
91
|
-
async function discoverSessionContext(env) {
|
|
92
|
-
const cwd = env.cwd;
|
|
93
|
-
const agentsMd = await readAgentsMd(env, cwd);
|
|
94
|
-
const skills = await discoverLocalSkills(env, cwd);
|
|
95
|
-
let directoryListing;
|
|
96
|
-
try {
|
|
97
|
-
directoryListing = await env.readdir(cwd);
|
|
98
|
-
} catch {}
|
|
99
|
-
return {
|
|
100
|
-
systemPrompt: composeSystemPrompt(agentsMd, skills, {
|
|
101
|
-
cwd,
|
|
102
|
-
directoryListing
|
|
103
|
-
}),
|
|
104
|
-
skills
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
//#endregion
|
|
109
|
-
//#region src/agent.ts
|
|
110
|
-
const MAX_READ_LINES = 2e3;
|
|
111
|
-
const MAX_READ_BYTES = 50 * 1024;
|
|
112
|
-
const MAX_GREP_MATCHES = 100;
|
|
113
|
-
const MAX_GREP_LINE_LENGTH = 500;
|
|
114
|
-
const MAX_GLOB_RESULTS = 1e3;
|
|
115
|
-
const BUILTIN_TOOL_NAMES = new Set([
|
|
116
|
-
"read",
|
|
117
|
-
"write",
|
|
118
|
-
"edit",
|
|
119
|
-
"bash",
|
|
120
|
-
"grep",
|
|
121
|
-
"glob"
|
|
122
|
-
]);
|
|
123
|
-
function createTools(env) {
|
|
124
|
-
return [
|
|
125
|
-
createReadTool(env),
|
|
126
|
-
createWriteTool(env),
|
|
127
|
-
createEditTool(env),
|
|
128
|
-
createBashTool(env),
|
|
129
|
-
createGrepTool(env),
|
|
130
|
-
createGlobTool(env)
|
|
131
|
-
];
|
|
132
|
-
}
|
|
133
|
-
function createReadTool(env) {
|
|
134
|
-
return {
|
|
135
|
-
name: "read",
|
|
136
|
-
label: "Read File",
|
|
137
|
-
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.",
|
|
138
|
-
parameters: Type.Object({
|
|
139
|
-
path: Type.String({ description: "Path to the file to read" }),
|
|
140
|
-
offset: Type.Optional(Type.Number({ description: "Line number to start from (1-indexed)" })),
|
|
141
|
-
limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" }))
|
|
142
|
-
}),
|
|
143
|
-
async execute(_toolCallId, params, signal) {
|
|
144
|
-
throwIfAborted(signal);
|
|
145
|
-
try {
|
|
146
|
-
if ((await env.stat(params.path)).isDirectory) {
|
|
147
|
-
const entries = await env.readdir(params.path);
|
|
148
|
-
return {
|
|
149
|
-
content: [{
|
|
150
|
-
type: "text",
|
|
151
|
-
text: entries.join("\n") || "(empty directory)"
|
|
152
|
-
}],
|
|
153
|
-
details: {
|
|
154
|
-
path: params.path,
|
|
155
|
-
isDirectory: true,
|
|
156
|
-
entries: entries.length
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
} catch {}
|
|
161
|
-
const allLines = (await env.readFile(params.path)).split("\n");
|
|
162
|
-
const startLine = params.offset ? Math.max(0, params.offset - 1) : 0;
|
|
163
|
-
if (startLine >= allLines.length) throw new Error(`Offset ${params.offset} is beyond end of file (${allLines.length} lines total)`);
|
|
164
|
-
const endLine = params.limit ? startLine + params.limit : allLines.length;
|
|
165
|
-
const { text: truncatedText, wasTruncated } = truncateHead(allLines.slice(startLine, endLine), MAX_READ_LINES, MAX_READ_BYTES);
|
|
166
|
-
let output = truncatedText;
|
|
167
|
-
if (wasTruncated) {
|
|
168
|
-
const shownEnd = startLine + truncatedText.split("\n").length;
|
|
169
|
-
output += `\n\n[Showing lines ${startLine + 1}-${shownEnd} of ${allLines.length}. Use offset=${shownEnd + 1} to continue.]`;
|
|
170
|
-
}
|
|
171
|
-
return {
|
|
172
|
-
content: [{
|
|
173
|
-
type: "text",
|
|
174
|
-
text: output
|
|
175
|
-
}],
|
|
176
|
-
details: {
|
|
177
|
-
path: params.path,
|
|
178
|
-
lines: allLines.length
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
function createWriteTool(env) {
|
|
185
|
-
return {
|
|
186
|
-
name: "write",
|
|
187
|
-
label: "Write File",
|
|
188
|
-
description: "Write content to a file. Creates the file and parent directories if they do not exist.",
|
|
189
|
-
parameters: Type.Object({
|
|
190
|
-
path: Type.String({ description: "Path to the file to write" }),
|
|
191
|
-
content: Type.String({ description: "Content to write to the file" })
|
|
192
|
-
}),
|
|
193
|
-
async execute(_toolCallId, params, signal) {
|
|
194
|
-
throwIfAborted(signal);
|
|
195
|
-
const resolved = env.resolvePath(params.path);
|
|
196
|
-
const dir = resolved.replace(/\/[^/]*$/, "");
|
|
197
|
-
if (dir && dir !== resolved) await env.mkdir(dir, { recursive: true });
|
|
198
|
-
await env.writeFile(resolved, params.content);
|
|
199
|
-
return {
|
|
200
|
-
content: [{
|
|
201
|
-
type: "text",
|
|
202
|
-
text: `Successfully wrote ${params.content.length} bytes to ${params.path}`
|
|
203
|
-
}],
|
|
204
|
-
details: {
|
|
205
|
-
path: params.path,
|
|
206
|
-
size: params.content.length
|
|
207
|
-
}
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
function createEditTool(env) {
|
|
213
|
-
return {
|
|
214
|
-
name: "edit",
|
|
215
|
-
label: "Edit File",
|
|
216
|
-
description: "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.",
|
|
217
|
-
parameters: Type.Object({
|
|
218
|
-
path: Type.String({ description: "Path to the file to edit" }),
|
|
219
|
-
oldText: Type.String({ description: "Exact text to find (must be unique)" }),
|
|
220
|
-
newText: Type.String({ description: "Replacement text" }),
|
|
221
|
-
replaceAll: Type.Optional(Type.Boolean({ description: "Replace all occurrences" }))
|
|
222
|
-
}),
|
|
223
|
-
async execute(_toolCallId, params, signal) {
|
|
224
|
-
throwIfAborted(signal);
|
|
225
|
-
const content = await env.readFile(params.path);
|
|
226
|
-
if (params.replaceAll) {
|
|
227
|
-
const newContent = content.replaceAll(params.oldText, params.newText);
|
|
228
|
-
if (newContent === content) throw new Error(`Could not find the text in ${params.path}. No changes made.`);
|
|
229
|
-
await env.writeFile(params.path, newContent);
|
|
230
|
-
const count = content.split(params.oldText).length - 1;
|
|
231
|
-
return {
|
|
232
|
-
content: [{
|
|
233
|
-
type: "text",
|
|
234
|
-
text: `Replaced ${count} occurrences in ${params.path}`
|
|
235
|
-
}],
|
|
236
|
-
details: {
|
|
237
|
-
path: params.path,
|
|
238
|
-
replacements: count
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
const occurrences = countOccurrences(content, params.oldText);
|
|
243
|
-
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.`);
|
|
244
|
-
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.`);
|
|
245
|
-
const newContent = content.replace(params.oldText, params.newText);
|
|
246
|
-
await env.writeFile(params.path, newContent);
|
|
247
|
-
return {
|
|
248
|
-
content: [{
|
|
249
|
-
type: "text",
|
|
250
|
-
text: `Successfully edited ${params.path}`
|
|
251
|
-
}],
|
|
252
|
-
details: { path: params.path }
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
function createBashTool(env) {
|
|
258
|
-
return {
|
|
259
|
-
name: "bash",
|
|
260
|
-
label: "Run Command",
|
|
261
|
-
description: "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.",
|
|
262
|
-
parameters: Type.Object({
|
|
263
|
-
command: Type.String({ description: "Bash command to execute" }),
|
|
264
|
-
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds" }))
|
|
265
|
-
}),
|
|
266
|
-
async execute(_toolCallId, params, signal) {
|
|
267
|
-
throwIfAborted(signal);
|
|
268
|
-
return formatBashResult(await env.exec(params.command), params.command);
|
|
269
|
-
}
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
function formatBashResult(result, command) {
|
|
273
|
-
const { text: output } = truncateTail((result.stdout + (result.stderr ? "\n" + result.stderr : "")).trim(), MAX_READ_LINES, MAX_READ_BYTES);
|
|
274
|
-
if (result.exitCode !== 0) throw new Error(`${output}\n\nCommand exited with code ${result.exitCode}`);
|
|
275
|
-
return {
|
|
276
|
-
content: [{
|
|
277
|
-
type: "text",
|
|
278
|
-
text: output || "(no output)"
|
|
279
|
-
}],
|
|
280
|
-
details: {
|
|
281
|
-
command,
|
|
282
|
-
exitCode: result.exitCode
|
|
283
|
-
}
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
function createGrepTool(env) {
|
|
287
|
-
return {
|
|
288
|
-
name: "grep",
|
|
289
|
-
label: "Search Files",
|
|
290
|
-
description: "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.",
|
|
291
|
-
parameters: Type.Object({
|
|
292
|
-
pattern: Type.String({ description: "Search pattern (regex)" }),
|
|
293
|
-
path: Type.Optional(Type.String({ description: "Directory or file to search (default: .)" })),
|
|
294
|
-
include: Type.Optional(Type.String({ description: "Glob filter, e.g. \"*.ts\"" }))
|
|
295
|
-
}),
|
|
296
|
-
async execute(_toolCallId, params, signal) {
|
|
297
|
-
throwIfAborted(signal);
|
|
298
|
-
const searchPath = params.path || ".";
|
|
299
|
-
let cmd = `grep -rn "${escapeShellArg(params.pattern)}" ${escapeShellArg(searchPath)}`;
|
|
300
|
-
if (params.include) cmd = `grep -rn --include="${escapeShellArg(params.include)}" "${escapeShellArg(params.pattern)}" ${escapeShellArg(searchPath)}`;
|
|
301
|
-
const result = await env.exec(cmd);
|
|
302
|
-
if (result.exitCode === 1 && !result.stdout.trim()) return {
|
|
303
|
-
content: [{
|
|
304
|
-
type: "text",
|
|
305
|
-
text: "No matches found."
|
|
306
|
-
}],
|
|
307
|
-
details: { matchCount: 0 }
|
|
308
|
-
};
|
|
309
|
-
if (result.exitCode > 1) throw new Error(`grep failed: ${result.stderr}`);
|
|
310
|
-
const lines = result.stdout.trim().split("\n");
|
|
311
|
-
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");
|
|
312
|
-
if (lines.length > MAX_GREP_MATCHES) finalOutput += `\n\n[Showing ${MAX_GREP_MATCHES} of ${lines.length} matches. Narrow your search.]`;
|
|
313
|
-
return {
|
|
314
|
-
content: [{
|
|
315
|
-
type: "text",
|
|
316
|
-
text: finalOutput
|
|
317
|
-
}],
|
|
318
|
-
details: { matchCount: Math.min(lines.length, MAX_GREP_MATCHES) }
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
function createGlobTool(env) {
|
|
324
|
-
return {
|
|
325
|
-
name: "glob",
|
|
326
|
-
label: "Find Files",
|
|
327
|
-
description: "Find files by glob pattern. Returns matching file paths.",
|
|
328
|
-
parameters: Type.Object({
|
|
329
|
-
pattern: Type.String({ description: "Glob pattern, e.g. \"**/*.ts\"" }),
|
|
330
|
-
path: Type.Optional(Type.String({ description: "Directory to search in (default: .)" }))
|
|
331
|
-
}),
|
|
332
|
-
async execute(_toolCallId, params, signal) {
|
|
333
|
-
throwIfAborted(signal);
|
|
334
|
-
const cmd = `find ${escapeShellArg(params.path || ".")} -type f -name "${escapeShellArg(params.pattern)}" 2>/dev/null | head -${MAX_GLOB_RESULTS}`;
|
|
335
|
-
const result = await env.exec(cmd);
|
|
336
|
-
if (result.exitCode !== 0 && !result.stdout.trim()) return {
|
|
337
|
-
content: [{
|
|
338
|
-
type: "text",
|
|
339
|
-
text: "No files found matching pattern."
|
|
340
|
-
}],
|
|
341
|
-
details: { matchCount: 0 }
|
|
342
|
-
};
|
|
343
|
-
const paths = result.stdout.trim().split("\n").filter(Boolean);
|
|
344
|
-
if (paths.length === 0) return {
|
|
345
|
-
content: [{
|
|
346
|
-
type: "text",
|
|
347
|
-
text: "No files found matching pattern."
|
|
348
|
-
}],
|
|
349
|
-
details: { matchCount: 0 }
|
|
350
|
-
};
|
|
351
|
-
return {
|
|
352
|
-
content: [{
|
|
353
|
-
type: "text",
|
|
354
|
-
text: paths.join("\n")
|
|
355
|
-
}],
|
|
356
|
-
details: { matchCount: paths.length }
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
function throwIfAborted(signal) {
|
|
362
|
-
if (signal?.aborted) throw new Error("Operation aborted");
|
|
363
|
-
}
|
|
364
|
-
function countOccurrences(str, substr) {
|
|
365
|
-
let count = 0;
|
|
366
|
-
let pos = str.indexOf(substr, 0);
|
|
367
|
-
while (pos !== -1) {
|
|
368
|
-
count++;
|
|
369
|
-
pos = str.indexOf(substr, pos + substr.length);
|
|
370
|
-
}
|
|
371
|
-
return count;
|
|
372
|
-
}
|
|
373
|
-
function escapeShellArg(arg) {
|
|
374
|
-
return arg.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$").replace(/`/g, "\\`");
|
|
375
|
-
}
|
|
376
|
-
function truncateHead(lines, maxLines, maxBytes) {
|
|
377
|
-
let result = "";
|
|
378
|
-
let lineCount = 0;
|
|
379
|
-
let wasTruncated = false;
|
|
380
|
-
for (const line of lines) {
|
|
381
|
-
if (lineCount >= maxLines) {
|
|
382
|
-
wasTruncated = true;
|
|
383
|
-
break;
|
|
384
|
-
}
|
|
385
|
-
const next = lineCount === 0 ? line : "\n" + line;
|
|
386
|
-
if (result.length + next.length > maxBytes) {
|
|
387
|
-
wasTruncated = true;
|
|
388
|
-
break;
|
|
389
|
-
}
|
|
390
|
-
result += next;
|
|
391
|
-
lineCount++;
|
|
392
|
-
}
|
|
393
|
-
return {
|
|
394
|
-
text: result,
|
|
395
|
-
wasTruncated
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
function truncateTail(text, maxLines, maxBytes) {
|
|
399
|
-
const lines = text.split("\n");
|
|
400
|
-
if (lines.length <= maxLines && text.length <= maxBytes) return {
|
|
401
|
-
text,
|
|
402
|
-
wasTruncated: false
|
|
403
|
-
};
|
|
404
|
-
let result = lines.slice(-maxLines).join("\n");
|
|
405
|
-
if (result.length > maxBytes) result = result.slice(-maxBytes);
|
|
406
|
-
return {
|
|
407
|
-
text: result,
|
|
408
|
-
wasTruncated: true
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
//#endregion
|
|
413
7
|
//#region src/compaction.ts
|
|
414
8
|
const DEFAULT_COMPACTION_SETTINGS = {
|
|
415
9
|
enabled: true,
|
|
@@ -987,6 +581,7 @@ var Session = class Session {
|
|
|
987
581
|
});
|
|
988
582
|
}
|
|
989
583
|
async prompt(text, options) {
|
|
584
|
+
this.assertRoleExists(options?.role);
|
|
990
585
|
this.resolveModelForCall(options?.model, options?.role);
|
|
991
586
|
const promptWithRole = this.injectRoleInstructions(text, options?.role);
|
|
992
587
|
const schema = options?.result;
|
|
@@ -1007,8 +602,16 @@ var Session = class Session {
|
|
|
1007
602
|
}
|
|
1008
603
|
}
|
|
1009
604
|
async skill(name, options) {
|
|
1010
|
-
|
|
1011
|
-
|
|
605
|
+
this.assertRoleExists(options?.role);
|
|
606
|
+
let registeredSkill = this.config.skills[name];
|
|
607
|
+
if (!registeredSkill && (name.includes("/") || /\.(md|markdown)$/i.test(name))) {
|
|
608
|
+
const loaded = await loadSkillByPath(this.env, this.env.cwd, name);
|
|
609
|
+
if (loaded) registeredSkill = loaded;
|
|
610
|
+
}
|
|
611
|
+
if (!registeredSkill) {
|
|
612
|
+
const available = Object.keys(this.config.skills).join(", ") || "(none)";
|
|
613
|
+
throw new Error(`Skill "${name}" not registered. Available: ${available}. Skills can also be referenced by relative path under .agents/skills/ (e.g. "triage/reproduce.md").`);
|
|
614
|
+
}
|
|
1012
615
|
this.resolveModelForCall(options?.model, options?.role);
|
|
1013
616
|
const schema = options?.result;
|
|
1014
617
|
const skillPrompt = buildSkillPrompt(registeredSkill.instructions, options?.args, schema);
|
|
@@ -1046,6 +649,7 @@ var Session = class Session {
|
|
|
1046
649
|
}
|
|
1047
650
|
}
|
|
1048
651
|
async task(prompt, options) {
|
|
652
|
+
this.assertRoleExists(options?.role);
|
|
1049
653
|
if (!options?.workspace) throw new Error("[flue] task() requires a workspace option.");
|
|
1050
654
|
const taskCwd = options.workspace.startsWith("/") ? options.workspace : normalizePath(this.env.cwd + "/" + options.workspace);
|
|
1051
655
|
function taskResolvePath(p) {
|
|
@@ -1081,7 +685,7 @@ var Session = class Session {
|
|
|
1081
685
|
systemPrompt: localContext.systemPrompt,
|
|
1082
686
|
skills: localContext.skills,
|
|
1083
687
|
roles: this.config.roles,
|
|
1084
|
-
model: taskModel,
|
|
688
|
+
model: this.requireModel(taskModel, "this task() call"),
|
|
1085
689
|
resolveModel: this.config.resolveModel,
|
|
1086
690
|
compaction: this.config.compaction
|
|
1087
691
|
};
|
|
@@ -1113,7 +717,28 @@ var Session = class Session {
|
|
|
1113
717
|
let model = this.config.model;
|
|
1114
718
|
if (roleName && this.config.roles[roleName]?.model && this.config.resolveModel) model = this.config.resolveModel(this.config.roles[roleName].model);
|
|
1115
719
|
if (promptModel && this.config.resolveModel) model = this.config.resolveModel(promptModel);
|
|
1116
|
-
this.agent.state.model = model;
|
|
720
|
+
this.agent.state.model = this.requireModel(model, "this prompt()/skill()/task() call");
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Throws a clear, actionable error when no model is configured for a call.
|
|
724
|
+
* Use with the resolved model (post-precedence) to guarantee we never hand
|
|
725
|
+
* `undefined` to the underlying agent.
|
|
726
|
+
*/
|
|
727
|
+
requireModel(model, callSite) {
|
|
728
|
+
if (model) return model;
|
|
729
|
+
throw new Error(`[flue] No model configured for ${callSite}. Pass \`{ model: "provider/model-id" }\` to \`init()\` for a session-wide default, or to this prompt()/skill()/task() call for a one-off override.`);
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Throws a clear error when a caller references a role that isn't registered.
|
|
733
|
+
* Roles are loaded from `.flue/roles/` at build time. Called eagerly at the top
|
|
734
|
+
* of prompt()/skill()/task() so typos surface before any LLM work begins.
|
|
735
|
+
*/
|
|
736
|
+
assertRoleExists(roleName) {
|
|
737
|
+
if (!roleName) return;
|
|
738
|
+
if (this.config.roles[roleName]) return;
|
|
739
|
+
const available = Object.keys(this.config.roles);
|
|
740
|
+
const list = available.length > 0 ? available.join(", ") : "(none defined)";
|
|
741
|
+
throw new Error(`[flue] Role "${roleName}" not registered. Available roles: ${list}. Define roles as markdown files under \`.flue/roles/\`.`);
|
|
1117
742
|
}
|
|
1118
743
|
injectRoleInstructions(text, roleName) {
|
|
1119
744
|
if (!roleName) return text;
|
|
@@ -1297,4 +922,4 @@ function normalizePath(p) {
|
|
|
1297
922
|
}
|
|
1298
923
|
|
|
1299
924
|
//#endregion
|
|
1300
|
-
export {
|
|
925
|
+
export { Session as n, normalizePath as r, InMemorySessionStore as t };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
2
1
|
import { Model, TSchema } from "@mariozechner/pi-ai";
|
|
2
|
+
import { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
3
3
|
import * as v from "valibot";
|
|
4
4
|
|
|
5
5
|
//#region src/types.d.ts
|
|
@@ -107,8 +107,13 @@ interface AgentConfig {
|
|
|
107
107
|
/** Discovered at runtime from .agents/skills/ in the session's cwd. */
|
|
108
108
|
skills: Record<string, Skill>;
|
|
109
109
|
roles: Record<string, Role>;
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Session-wide default model. Undefined by default — the user must set it via
|
|
112
|
+
* `init({ model: "provider/model-id" })` or pass `{ model }` at each prompt/
|
|
113
|
+
* skill/task call site. Calls with no model resolved throw clearly at runtime.
|
|
114
|
+
*/
|
|
115
|
+
model: Model<any> | undefined;
|
|
116
|
+
/** Resolve a "provider/modelId" string to a Model instance. Throws on invalid input. */
|
|
112
117
|
resolveModel?: (modelString: string) => Model<any>;
|
|
113
118
|
compaction?: CompactionConfig;
|
|
114
119
|
}
|
|
@@ -121,7 +126,7 @@ interface FlueContext {
|
|
|
121
126
|
/** Create a session with sandbox + persistence. Can only be called once per request. */
|
|
122
127
|
init(options?: SessionInit): Promise<FlueSession>;
|
|
123
128
|
}
|
|
124
|
-
/**
|
|
129
|
+
/** All fields are optional — omitting gives platform defaults (empty sandbox, platform store, build-time model). */
|
|
125
130
|
interface SessionInit {
|
|
126
131
|
/**
|
|
127
132
|
* - `'empty'` (default): In-memory sandbox, no files, no host access.
|
|
@@ -132,6 +137,15 @@ interface SessionInit {
|
|
|
132
137
|
sandbox?: 'empty' | 'local' | SandboxFactory | BashLike;
|
|
133
138
|
/** Defaults to platform store (in-memory on Node, DO SQLite on Cloudflare). */
|
|
134
139
|
persist?: SessionStore;
|
|
140
|
+
/**
|
|
141
|
+
* Override the default model for this session. Applies to all prompt(), skill(),
|
|
142
|
+
* and task() calls unless overridden at the call site.
|
|
143
|
+
*
|
|
144
|
+
* Format: `'provider/modelId'` (e.g. `'anthropic/claude-opus-4-20250514'`).
|
|
145
|
+
*
|
|
146
|
+
* Precedence (highest wins): per-call `model` > role `model` > session `model` > build-time default.
|
|
147
|
+
*/
|
|
148
|
+
model?: string;
|
|
135
149
|
}
|
|
136
150
|
interface FlueSession {
|
|
137
151
|
prompt<S extends v.GenericSchema>(text: string, options: PromptOptions<S> & {
|
|
@@ -303,7 +317,6 @@ interface BuildContext {
|
|
|
303
317
|
roles: Record<string, Role>;
|
|
304
318
|
agentDir: string;
|
|
305
319
|
options: BuildOptions;
|
|
306
|
-
resolveSDKImport: (module: string) => string;
|
|
307
320
|
}
|
|
308
321
|
/** Controls the build output format for a target platform. */
|
|
309
322
|
interface BuildPlugin {
|
|
@@ -318,10 +331,6 @@ interface BuildOptions {
|
|
|
318
331
|
target?: 'node' | 'cloudflare';
|
|
319
332
|
/** Overrides `target` when provided. */
|
|
320
333
|
plugin?: BuildPlugin;
|
|
321
|
-
model?: {
|
|
322
|
-
provider: string;
|
|
323
|
-
modelId: string;
|
|
324
|
-
};
|
|
325
334
|
}
|
|
326
335
|
//#endregion
|
|
327
336
|
export { ShellOptions as C, TaskOptions as D, SkillOptions as E, ToolDef as O, SessionStore as S, Skill as T, Role as _, BuildOptions as a, SessionEnv as b, CommandDef as c, FlueContext as d, FlueEvent as f, PromptResponse as g, PromptOptions as h, BuildContext as i, CommandSupport as l, FlueSession as m, AgentInfo as n, BuildPlugin as o, FlueEventCallback as p, BashLike as r, Command as s, AgentConfig as t, FileStat as u, SandboxFactory as v, ShellResult as w, SessionInit as x, SessionData as y };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flue/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"exports": {
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
"types": "./dist/sandbox.d.mts",
|
|
17
17
|
"import": "./dist/sandbox.mjs"
|
|
18
18
|
},
|
|
19
|
+
"./internal": {
|
|
20
|
+
"types": "./dist/internal.d.mts",
|
|
21
|
+
"import": "./dist/internal.mjs"
|
|
22
|
+
},
|
|
19
23
|
"./cloudflare": {
|
|
20
24
|
"types": "./dist/cloudflare/index.d.mts",
|
|
21
25
|
"import": "./dist/cloudflare/index.mjs"
|