@flue/sdk 0.3.4 → 0.3.6
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 +5 -3
- package/dist/{agent-BB4lwAd5.mjs → agent-BTB0809P.mjs} +1 -1
- package/dist/client.d.mts +2 -2
- package/dist/client.mjs +3 -3
- package/dist/cloudflare/index.d.mts +2 -2
- package/dist/cloudflare/index.mjs +5 -3
- package/dist/{command-helpers-DdAfbnom.d.mts → command-helpers-CXzopT_-.d.mts} +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +222 -92
- package/dist/internal.d.mts +264 -3
- package/dist/internal.mjs +420 -4
- package/dist/{mcp-DOgMtp8y.mjs → mcp-DTFRe9vh.mjs} +4 -3
- package/dist/{mcp-BVF-sOBZ.d.mts → mcp-EZy-Vb5M.d.mts} +1 -1
- package/dist/node/index.d.mts +2 -2
- package/dist/sandbox.d.mts +2 -1
- package/dist/sandbox.mjs +31 -5
- package/dist/{session-DukL3zwF.mjs → session-BaaSQTWS.mjs} +6 -4
- package/dist/{types-T8pE1xIS.d.mts → types-CItTrBsU.d.mts} +9 -4
- package/package.json +1 -1
package/dist/internal.mjs
CHANGED
|
@@ -1,10 +1,426 @@
|
|
|
1
|
-
import "./agent-
|
|
2
|
-
import { t as InMemorySessionStore } from "./session-
|
|
1
|
+
import "./agent-BTB0809P.mjs";
|
|
2
|
+
import { t as InMemorySessionStore } from "./session-BaaSQTWS.mjs";
|
|
3
3
|
import { bashFactoryToSessionEnv } from "./sandbox.mjs";
|
|
4
|
-
import "./mcp-
|
|
4
|
+
import "./mcp-DTFRe9vh.mjs";
|
|
5
5
|
import { createFlueContext } from "./client.mjs";
|
|
6
6
|
import { getModel } from "@mariozechner/pi-ai";
|
|
7
7
|
|
|
8
|
+
//#region src/errors.ts
|
|
9
|
+
/**
|
|
10
|
+
* Concrete error classes thrown by Flue.
|
|
11
|
+
*
|
|
12
|
+
* This file is the *vocabulary* of errors in Flue. Every error the framework
|
|
13
|
+
* throws has a class here. The framework scaffolding (base class, renderers,
|
|
14
|
+
* type guards, request-parsing helpers) lives in `error-utils.ts`.
|
|
15
|
+
*
|
|
16
|
+
* ──── Why this file exists ────────────────────────────────────────────────
|
|
17
|
+
*
|
|
18
|
+
* Concentrating every error in one file is deliberate. When all errors are
|
|
19
|
+
* visible together, it's easy to:
|
|
20
|
+
*
|
|
21
|
+
* - Keep message tone and detail level consistent across the codebase.
|
|
22
|
+
* - Notice duplicates ("oh, we already have an error for this case").
|
|
23
|
+
* - Establish norms by example — when adding a new error, look at the
|
|
24
|
+
* neighbors above and copy the pattern.
|
|
25
|
+
*
|
|
26
|
+
* Application code throughout the codebase should reach for one of these
|
|
27
|
+
* classes rather than constructing a `FlueError` ad hoc. If no existing class
|
|
28
|
+
* fits, add one here. That's the entire convention.
|
|
29
|
+
*
|
|
30
|
+
* ──── Two audiences: caller vs. developer ─────────────────────────────────
|
|
31
|
+
*
|
|
32
|
+
* The reader of an error message is one of two distinct audiences:
|
|
33
|
+
*
|
|
34
|
+
* - The *caller*: an HTTP client. Possibly third-party, possibly hostile,
|
|
35
|
+
* possibly an end user who shouldn't even know we're built on Flue.
|
|
36
|
+
* Sees `message` and `details` always.
|
|
37
|
+
*
|
|
38
|
+
* - The *developer*: the human running the service (`flue dev`, `flue run`,
|
|
39
|
+
* local debugging). Sees `dev` in addition, but only when the server is
|
|
40
|
+
* running in local/dev mode (gated by `FLUE_MODE=local`).
|
|
41
|
+
*
|
|
42
|
+
* Every error class must classify its prose by audience. The required-but-
|
|
43
|
+
* possibly-empty shape of both `details` and `dev` is the discipline:
|
|
44
|
+
* forgetting either field is a TypeScript error, and writing `''` is a
|
|
45
|
+
* deliberate "I have nothing for that audience" decision.
|
|
46
|
+
*
|
|
47
|
+
* Concretely:
|
|
48
|
+
*
|
|
49
|
+
* - `message` One sentence. Caller-safe. Always rendered.
|
|
50
|
+
* - `details` Longer caller-safe prose. About the request itself, the
|
|
51
|
+
* contract, what the caller can do to fix it. Always
|
|
52
|
+
* rendered. NEVER includes:
|
|
53
|
+
* - sibling/neighbor enumeration (leaks namespace)
|
|
54
|
+
* - filesystem paths or "agents/" / "skills/" / etc.
|
|
55
|
+
* (leaks framework internals)
|
|
56
|
+
* - source-code-level fix instructions ("add ... to your
|
|
57
|
+
* agent definition") (caller can't act on these)
|
|
58
|
+
* - build-time or runtime mechanics
|
|
59
|
+
* - `dev` Longer dev-audience prose. Available alternatives,
|
|
60
|
+
* filesystem layout, framework guidance, source-code-level
|
|
61
|
+
* fix instructions. Rendered ONLY when FLUE_MODE=local.
|
|
62
|
+
*
|
|
63
|
+
* When in doubt, put information in `dev`. The default is conservative.
|
|
64
|
+
*
|
|
65
|
+
* ──── Conventions for new error classes ───────────────────────────────────
|
|
66
|
+
*
|
|
67
|
+
* - Class name: PascalCase, suffixed with `Error`. E.g. `AgentNotFoundError`.
|
|
68
|
+
* - The class owns its `type` constant (snake_case). Set once in the
|
|
69
|
+
* subclass constructor, never passed by callers. Renaming the wire type
|
|
70
|
+
* is then a one-line change.
|
|
71
|
+
* - Constructor takes ONLY structured input data (the values used to build
|
|
72
|
+
* the message). The constructor assembles `message`, `details`, and
|
|
73
|
+
* `dev` from that data, so call sites never reinvent phrasing.
|
|
74
|
+
* - `details` and `dev` are both required strings. Pass `''` only when
|
|
75
|
+
* there's genuinely nothing more to say for that audience.
|
|
76
|
+
* - For HTTP errors, the class sets its own `status` (and `headers` where
|
|
77
|
+
* relevant). Callers do not pick HTTP status codes ad-hoc.
|
|
78
|
+
*
|
|
79
|
+
* Worked example (matches `AgentNotFoundError` below):
|
|
80
|
+
*
|
|
81
|
+
* new AgentNotFoundError({ name, available });
|
|
82
|
+
* // builds:
|
|
83
|
+
* // message: `Agent "foo" is not registered.`
|
|
84
|
+
* // details: `Verify the agent name is correct.`
|
|
85
|
+
* // dev: `Available agents: "echo", "greeter". Agents are
|
|
86
|
+
* // loaded from the workspace's "agents/" directory at
|
|
87
|
+
* // build time. ...`
|
|
88
|
+
*
|
|
89
|
+
* The wire response in production omits `dev`; in `flue dev` / `flue run`
|
|
90
|
+
* it includes `dev`. That separation is what lets the dev field be richly
|
|
91
|
+
* helpful without leaking namespace state to public callers.
|
|
92
|
+
*
|
|
93
|
+
* Counter-example to avoid:
|
|
94
|
+
*
|
|
95
|
+
* class AgentNotFoundError extends FlueHttpError {
|
|
96
|
+
* constructor(message: string) { // ✗ free-form
|
|
97
|
+
* super({ // ✗ wrong type
|
|
98
|
+
* type: 'agent_error',
|
|
99
|
+
* message,
|
|
100
|
+
* details: 'Available: "x", "y", "z"', // ✗ leaks names
|
|
101
|
+
* dev: '', // ✗ wasted channel
|
|
102
|
+
* status: 500, // ✗ wrong status
|
|
103
|
+
* });
|
|
104
|
+
* }
|
|
105
|
+
* }
|
|
106
|
+
*
|
|
107
|
+
* The structured-constructor pattern below is what prevents that drift.
|
|
108
|
+
*/
|
|
109
|
+
/**
|
|
110
|
+
* Format a list of items for inclusion in error details. Empty lists render
|
|
111
|
+
* as the supplied fallback (default `(none)`), so messages read naturally
|
|
112
|
+
* regardless of whether anything is registered.
|
|
113
|
+
*
|
|
114
|
+
* Module-private: only used by the concrete error subclasses below. Promote
|
|
115
|
+
* to `export` if/when a real cross-file caller appears.
|
|
116
|
+
*/
|
|
117
|
+
function formatList(items, fallback = "(none)") {
|
|
118
|
+
if (items.length === 0) return fallback;
|
|
119
|
+
return items.map((item) => `"${String(item)}"`).join(", ");
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Base class for every error Flue throws. Do not instantiate directly in
|
|
123
|
+
* application code — extend it via a subclass below. If a use case isn't
|
|
124
|
+
* covered, add a new subclass here rather than throwing a raw `FlueError`.
|
|
125
|
+
*/
|
|
126
|
+
var FlueError = class extends Error {
|
|
127
|
+
type;
|
|
128
|
+
details;
|
|
129
|
+
dev;
|
|
130
|
+
meta;
|
|
131
|
+
cause;
|
|
132
|
+
constructor(options) {
|
|
133
|
+
super(options.message);
|
|
134
|
+
this.name = "FlueError";
|
|
135
|
+
this.type = options.type;
|
|
136
|
+
this.details = options.details;
|
|
137
|
+
this.dev = options.dev;
|
|
138
|
+
this.meta = options.meta;
|
|
139
|
+
this.cause = options.cause;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* Base class for HTTP-layer errors. Adds `status` and optional `headers`.
|
|
144
|
+
* Subclasses set these in the `super({...})` call so the call site doesn't
|
|
145
|
+
* have to think about HTTP semantics.
|
|
146
|
+
*/
|
|
147
|
+
var FlueHttpError = class extends FlueError {
|
|
148
|
+
status;
|
|
149
|
+
headers;
|
|
150
|
+
constructor(options) {
|
|
151
|
+
super(options);
|
|
152
|
+
this.name = "FlueHttpError";
|
|
153
|
+
this.status = options.status;
|
|
154
|
+
this.headers = options.headers;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
var MethodNotAllowedError = class extends FlueHttpError {
|
|
158
|
+
constructor({ method, allowed }) {
|
|
159
|
+
super({
|
|
160
|
+
type: "method_not_allowed",
|
|
161
|
+
message: `HTTP method ${method} is not allowed on this endpoint.`,
|
|
162
|
+
details: `This endpoint accepts ${formatList(allowed)} only.`,
|
|
163
|
+
dev: "",
|
|
164
|
+
status: 405,
|
|
165
|
+
headers: { Allow: allowed.join(", ") }
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
var UnsupportedMediaTypeError = class extends FlueHttpError {
|
|
170
|
+
constructor({ received }) {
|
|
171
|
+
const detailLines = [];
|
|
172
|
+
if (received) detailLines.push(`Received Content-Type: "${received}".`);
|
|
173
|
+
else detailLines.push(`No Content-Type header was sent.`);
|
|
174
|
+
detailLines.push("Send the request body as JSON with the header \"Content-Type: application/json\", or omit the body entirely (and the Content-Type header) if the request doesn't have a payload.");
|
|
175
|
+
super({
|
|
176
|
+
type: "unsupported_media_type",
|
|
177
|
+
message: `Request body must be sent as application/json.`,
|
|
178
|
+
details: detailLines.join("\n"),
|
|
179
|
+
dev: "",
|
|
180
|
+
status: 415
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
var InvalidJsonError = class extends FlueHttpError {
|
|
185
|
+
constructor({ parseError }) {
|
|
186
|
+
super({
|
|
187
|
+
type: "invalid_json",
|
|
188
|
+
message: `Request body is not valid JSON.`,
|
|
189
|
+
details: `The JSON parser reported: ${parseError}\nVerify the body is well-formed JSON, or omit the body entirely if the request doesn't have a payload.`,
|
|
190
|
+
dev: "",
|
|
191
|
+
status: 400
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
var AgentNotFoundError = class extends FlueHttpError {
|
|
196
|
+
constructor({ name, available }) {
|
|
197
|
+
super({
|
|
198
|
+
type: "agent_not_found",
|
|
199
|
+
message: `Agent "${name}" is not registered.`,
|
|
200
|
+
details: `Verify the agent name is correct.`,
|
|
201
|
+
dev: `Available agents: ${formatList(available)}.\nAgents are loaded from the workspace's "agents/" directory at build time. Verify the agent file is present in the workspace being served.`,
|
|
202
|
+
status: 404
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
var AgentNotWebhookError = class extends FlueHttpError {
|
|
207
|
+
constructor({ name }) {
|
|
208
|
+
super({
|
|
209
|
+
type: "agent_not_webhook",
|
|
210
|
+
message: `Agent "${name}" is not web-accessible.`,
|
|
211
|
+
details: `This endpoint is not exposed over HTTP.`,
|
|
212
|
+
dev: "This agent has no webhook trigger configured. To expose it, add a webhook trigger to its definition (`triggers: { webhook: true }`). Trigger-less agents remain invokable via \"flue run\" in local mode.",
|
|
213
|
+
status: 404
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
var RouteNotFoundError = class extends FlueHttpError {
|
|
218
|
+
constructor({ method, path }) {
|
|
219
|
+
super({
|
|
220
|
+
type: "route_not_found",
|
|
221
|
+
message: `No route matches ${method} ${path}.`,
|
|
222
|
+
details: `Webhook agents are served at POST /agents/<name>/<id>.`,
|
|
223
|
+
dev: "",
|
|
224
|
+
status: 404
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
var InvalidRequestError = class extends FlueHttpError {
|
|
229
|
+
constructor({ reason }) {
|
|
230
|
+
super({
|
|
231
|
+
type: "invalid_request",
|
|
232
|
+
message: `Request is malformed.`,
|
|
233
|
+
details: reason,
|
|
234
|
+
dev: "",
|
|
235
|
+
status: 400
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region src/error-utils.ts
|
|
242
|
+
/**
|
|
243
|
+
* Error framework for Flue.
|
|
244
|
+
*
|
|
245
|
+
* This file holds the abstract scaffolding (renderers, helpers, request-
|
|
246
|
+
* parsing utilities, type guard, logger) that supports the concrete error
|
|
247
|
+
* subclasses defined in `errors.ts`. The two-file split is deliberate:
|
|
248
|
+
*
|
|
249
|
+
* - `errors.ts` is the *vocabulary*: the `FlueError` base class plus every
|
|
250
|
+
* concrete subclass Flue can throw. That's the file new contributors
|
|
251
|
+
* touch when they need to add a new error.
|
|
252
|
+
*
|
|
253
|
+
* - `error-utils.ts` (this file) is the *framework*: the renderers that
|
|
254
|
+
* turn errors into HTTP responses and SSE event data, the type guard,
|
|
255
|
+
* the logger, and the request-parsing utilities. This file rarely
|
|
256
|
+
* changes.
|
|
257
|
+
*
|
|
258
|
+
* Application code should NOT instantiate `FlueError` directly. Always reach
|
|
259
|
+
* for a subclass from `errors.ts`. If no existing subclass fits, add one
|
|
260
|
+
* there. This is what keeps message tone, detail level, and field naming
|
|
261
|
+
* consistent across the codebase.
|
|
262
|
+
*
|
|
263
|
+
* Wire envelope (HTTP body + SSE `data:` payload for error events):
|
|
264
|
+
*
|
|
265
|
+
* {
|
|
266
|
+
* "error": {
|
|
267
|
+
* "type": "...",
|
|
268
|
+
* "message": "...",
|
|
269
|
+
* "details": "...",
|
|
270
|
+
* "dev": "..." // present only in local/dev mode AND when non-empty
|
|
271
|
+
* }
|
|
272
|
+
* }
|
|
273
|
+
*
|
|
274
|
+
* Field rules:
|
|
275
|
+
* - `type`, `message`, `details` are always present on the wire.
|
|
276
|
+
* - `dev` is gated by `FLUE_MODE === 'local'` (set by `flue run` and
|
|
277
|
+
* `flue dev --target node`). Even in dev mode, `dev` is omitted when
|
|
278
|
+
* the error class set it to `''` — so its presence is not a reliable
|
|
279
|
+
* signal of mode by itself; clients should not depend on it that way.
|
|
280
|
+
* See `errors.ts` for the two-audience rationale.
|
|
281
|
+
* - `meta` is included on the wire only when an error subclass sets it
|
|
282
|
+
* (rare).
|
|
283
|
+
* - `cause` is never included on the wire (it's logged server-side only).
|
|
284
|
+
*/
|
|
285
|
+
function isFlueError(value) {
|
|
286
|
+
return value instanceof FlueError;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Structured error logger. Used by the HTTP and SSE renderers below to log
|
|
290
|
+
* unknown/wrapped errors before rendering a generic envelope.
|
|
291
|
+
*
|
|
292
|
+
* Module-private for now: when an external call site appears we can promote
|
|
293
|
+
* to `export` and decide the right shape for `warn`/`info` (FlueError
|
|
294
|
+
* subclasses with severity? plain strings? structured data?) — rather than
|
|
295
|
+
* committing to a shape now without any usage to validate it.
|
|
296
|
+
*/
|
|
297
|
+
function formatForLog(prefix, err) {
|
|
298
|
+
if (isFlueError(err)) {
|
|
299
|
+
const lines = [`${prefix} [${err.type}] ${err.message}`];
|
|
300
|
+
if (err.details) for (const line of err.details.split("\n")) lines.push(` ${line}`);
|
|
301
|
+
if (err.dev) for (const line of err.dev.split("\n")) lines.push(` ${line}`);
|
|
302
|
+
if (err.cause !== void 0) lines.push(` cause: ${err.cause instanceof Error ? err.cause.stack ?? err.cause.message : String(err.cause)}`);
|
|
303
|
+
return lines.join("\n");
|
|
304
|
+
}
|
|
305
|
+
if (err instanceof Error) return `${prefix} ${err.stack ?? err.message}`;
|
|
306
|
+
return `${prefix} ${String(err)}`;
|
|
307
|
+
}
|
|
308
|
+
const flueLog = { error(err) {
|
|
309
|
+
console.error(formatForLog("[flue]", err));
|
|
310
|
+
} };
|
|
311
|
+
/**
|
|
312
|
+
* Detect whether the server is running in local/dev mode. Gates whether the
|
|
313
|
+
* `dev` field is included on the wire — see the convention doc in `errors.ts`.
|
|
314
|
+
*
|
|
315
|
+
* Currently keyed off the `FLUE_MODE=local` env var, which is set by
|
|
316
|
+
* `flue run` and `flue dev --target node`. On Cloudflare workers there is
|
|
317
|
+
* no global `process` and no current "local mode" plumbing for the worker —
|
|
318
|
+
* so deployed CF and `flue dev --target cloudflare` both currently render
|
|
319
|
+
* the prod envelope. Threading a dev-mode signal through to the worker
|
|
320
|
+
* fetch handler is left as a follow-up.
|
|
321
|
+
*/
|
|
322
|
+
function isDevMode() {
|
|
323
|
+
return typeof process !== "undefined" && process.env?.FLUE_MODE === "local";
|
|
324
|
+
}
|
|
325
|
+
function envelope(err) {
|
|
326
|
+
const out = { error: {
|
|
327
|
+
type: err.type,
|
|
328
|
+
message: err.message,
|
|
329
|
+
details: err.details
|
|
330
|
+
} };
|
|
331
|
+
if (isDevMode() && err.dev) out.error.dev = err.dev;
|
|
332
|
+
if (err.meta) out.error.meta = err.meta;
|
|
333
|
+
return out;
|
|
334
|
+
}
|
|
335
|
+
const GENERIC_INTERNAL = { error: {
|
|
336
|
+
type: "internal_error",
|
|
337
|
+
message: "An internal error occurred.",
|
|
338
|
+
details: "The server encountered an unexpected error while handling this request."
|
|
339
|
+
} };
|
|
340
|
+
/**
|
|
341
|
+
* Render any thrown value into a `Response` with the canonical Flue error
|
|
342
|
+
* envelope. Unknown / non-Flue errors are logged in full and rendered as a
|
|
343
|
+
* generic 500 with no message leaked.
|
|
344
|
+
*/
|
|
345
|
+
function toHttpResponse(err) {
|
|
346
|
+
if (isFlueError(err)) {
|
|
347
|
+
const isHttp = err instanceof FlueHttpError;
|
|
348
|
+
const status = isHttp ? err.status : 500;
|
|
349
|
+
const headers = { "content-type": "application/json" };
|
|
350
|
+
if (isHttp && err.headers) Object.assign(headers, err.headers);
|
|
351
|
+
if (!isHttp) flueLog.error(err);
|
|
352
|
+
return new Response(JSON.stringify(envelope(err)), {
|
|
353
|
+
status,
|
|
354
|
+
headers
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
flueLog.error(err);
|
|
358
|
+
return new Response(JSON.stringify(GENERIC_INTERNAL), {
|
|
359
|
+
status: 500,
|
|
360
|
+
headers: { "content-type": "application/json" }
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Render any thrown value into a JSON string suitable for the `data:` line of
|
|
365
|
+
* an SSE `error` event. Same envelope as `toHttpResponse`. Unknown / non-Flue
|
|
366
|
+
* errors are logged and replaced with a generic envelope.
|
|
367
|
+
*/
|
|
368
|
+
function toSseData(err) {
|
|
369
|
+
if (isFlueError(err)) {
|
|
370
|
+
if (!(err instanceof FlueHttpError)) flueLog.error(err);
|
|
371
|
+
return JSON.stringify({
|
|
372
|
+
type: "error",
|
|
373
|
+
...envelope(err)
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
flueLog.error(err);
|
|
377
|
+
return JSON.stringify({
|
|
378
|
+
type: "error",
|
|
379
|
+
...GENERIC_INTERNAL
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Parse a request body as JSON. Returns `{}` for genuinely empty bodies
|
|
384
|
+
* (Content-Length: 0 or missing) so that webhook agents which don't accept
|
|
385
|
+
* a payload can be invoked without one.
|
|
386
|
+
*
|
|
387
|
+
* Throws `UnsupportedMediaTypeError` if a body is present without
|
|
388
|
+
* `application/json` content-type, and `InvalidJsonError` if the body is
|
|
389
|
+
* present but unparseable.
|
|
390
|
+
*/
|
|
391
|
+
async function parseJsonBody(request) {
|
|
392
|
+
const contentLengthHeader = request.headers.get("content-length");
|
|
393
|
+
const contentLength = contentLengthHeader === null ? null : Number(contentLengthHeader);
|
|
394
|
+
const contentType = request.headers.get("content-type");
|
|
395
|
+
if (contentLength === 0 || contentLengthHeader === null && contentType === null) return {};
|
|
396
|
+
if (!contentType || !contentType.toLowerCase().includes("application/json")) throw new UnsupportedMediaTypeError({ received: contentType });
|
|
397
|
+
let text;
|
|
398
|
+
try {
|
|
399
|
+
text = await request.text();
|
|
400
|
+
} catch (err) {
|
|
401
|
+
throw new InvalidJsonError({ parseError: err instanceof Error ? err.message : String(err) });
|
|
402
|
+
}
|
|
403
|
+
if (text.trim() === "") return {};
|
|
404
|
+
try {
|
|
405
|
+
return JSON.parse(text);
|
|
406
|
+
} catch (err) {
|
|
407
|
+
throw new InvalidJsonError({ parseError: err instanceof Error ? err.message : String(err) });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function validateAgentRequest(opts) {
|
|
411
|
+
if (opts.method !== "POST") throw new MethodNotAllowedError({
|
|
412
|
+
method: opts.method,
|
|
413
|
+
allowed: ["POST"]
|
|
414
|
+
});
|
|
415
|
+
if (opts.name.trim() === "" || opts.id.trim() === "") throw new InvalidRequestError({ reason: "Webhook URLs must have the shape /agents/<name>/<id> with non-empty segments." });
|
|
416
|
+
if (!opts.registeredAgents.includes(opts.name)) throw new AgentNotFoundError({
|
|
417
|
+
name: opts.name,
|
|
418
|
+
available: opts.registeredAgents
|
|
419
|
+
});
|
|
420
|
+
if (!opts.allowNonWebhook && !opts.webhookAgents.includes(opts.name)) throw new AgentNotWebhookError({ name: opts.name });
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
//#endregion
|
|
8
424
|
//#region src/internal.ts
|
|
9
425
|
/**
|
|
10
426
|
* Internal runtime helpers consumed by the generated server entry point.
|
|
@@ -36,4 +452,4 @@ function resolveModel(modelString) {
|
|
|
36
452
|
}
|
|
37
453
|
|
|
38
454
|
//#endregion
|
|
39
|
-
export { InMemorySessionStore, bashFactoryToSessionEnv, createFlueContext, resolveModel };
|
|
455
|
+
export { AgentNotFoundError, InMemorySessionStore, InvalidRequestError, MethodNotAllowedError, RouteNotFoundError, bashFactoryToSessionEnv, createFlueContext, parseJsonBody, resolveModel, toHttpResponse, toSseData, validateAgentRequest };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { r as discoverSessionContext } from "./agent-
|
|
2
|
-
import { a as assertRoleExists, n as Session, o as createScopedEnv, r as deleteSessionTree, s as mergeCommands } from "./session-
|
|
1
|
+
import { r as discoverSessionContext } from "./agent-BTB0809P.mjs";
|
|
2
|
+
import { a as assertRoleExists, n as Session, o as createScopedEnv, r as deleteSessionTree, s as mergeCommands } from "./session-BaaSQTWS.mjs";
|
|
3
3
|
import { createCwdSessionEnv } from "./sandbox.mjs";
|
|
4
4
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
5
5
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
@@ -31,7 +31,8 @@ var AgentClient = class {
|
|
|
31
31
|
const effectiveCommands = mergeCommands(this.agentCommands, options?.commands);
|
|
32
32
|
const result = await (await createScopedEnv(this.env, effectiveCommands)).exec(command, {
|
|
33
33
|
env: options?.env,
|
|
34
|
-
cwd: options?.cwd
|
|
34
|
+
cwd: options?.cwd,
|
|
35
|
+
timeout: options?.timeout
|
|
35
36
|
});
|
|
36
37
|
return {
|
|
37
38
|
stdout: result.stdout,
|
package/dist/node/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { l as Command } from "../types-
|
|
2
|
-
import { t as CommandExecutor } from "../command-helpers-
|
|
1
|
+
import { l as Command } from "../types-CItTrBsU.mjs";
|
|
2
|
+
import { t as CommandExecutor } from "../command-helpers-CXzopT_-.mjs";
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
4
|
|
|
5
5
|
//#region src/node/define-command.d.ts
|
package/dist/sandbox.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as SessionEnv, D as ShellResult, d as FileStat, i as BashFactory, u as CommandDef, x as SandboxFactory } from "./types-
|
|
1
|
+
import { C as SessionEnv, D as ShellResult, d as FileStat, i as BashFactory, u as CommandDef, x as SandboxFactory } from "./types-CItTrBsU.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/sandbox.d.ts
|
|
4
4
|
declare function createCwdSessionEnv(parentEnv: SessionEnv, cwd: string): SessionEnv;
|
|
@@ -21,6 +21,7 @@ interface SandboxApi {
|
|
|
21
21
|
exec(command: string, options?: {
|
|
22
22
|
cwd?: string;
|
|
23
23
|
env?: Record<string, string>;
|
|
24
|
+
timeout?: number;
|
|
24
25
|
}): Promise<ShellResult>;
|
|
25
26
|
}
|
|
26
27
|
/** Wrap a SandboxApi into SessionEnv. No just-bash, no intermediate filesystem layer. */
|
package/dist/sandbox.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import "./agent-
|
|
2
|
-
import { i as normalizePath, o as createScopedEnv } from "./session-
|
|
1
|
+
import "./agent-BTB0809P.mjs";
|
|
2
|
+
import { i as normalizePath, o as createScopedEnv } from "./session-BaaSQTWS.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/sandbox.ts
|
|
5
5
|
function createCwdSessionEnv(parentEnv, cwd) {
|
|
@@ -12,7 +12,8 @@ function createCwdSessionEnv(parentEnv, cwd) {
|
|
|
12
12
|
return {
|
|
13
13
|
exec: (cmd, opts) => parentEnv.exec(cmd, {
|
|
14
14
|
cwd: opts?.cwd ?? scopedCwd,
|
|
15
|
-
env: opts?.env
|
|
15
|
+
env: opts?.env,
|
|
16
|
+
timeout: opts?.timeout
|
|
16
17
|
}),
|
|
17
18
|
scope: async (options) => createCwdSessionEnv(await createScopedEnv(parentEnv, options?.commands ?? []), scopedCwd),
|
|
18
19
|
readFile: (p) => parentEnv.readFile(resolvePath(p)),
|
|
@@ -49,7 +50,31 @@ function createBashSessionEnv(bash, createScope) {
|
|
|
49
50
|
const cwd = bash.getCwd();
|
|
50
51
|
const resolve = (p) => p.startsWith("/") ? p : fs.resolvePath(cwd, p);
|
|
51
52
|
return {
|
|
52
|
-
exec: (cmd, opts) =>
|
|
53
|
+
exec: async (cmd, opts) => {
|
|
54
|
+
const exec = bash.exec;
|
|
55
|
+
const timeout = opts?.timeout;
|
|
56
|
+
let timeoutSignal;
|
|
57
|
+
let timer;
|
|
58
|
+
if (typeof timeout === "number") {
|
|
59
|
+
const controller = new AbortController();
|
|
60
|
+
timeoutSignal = controller.signal;
|
|
61
|
+
timer = setTimeout(() => controller.abort(), timeout * 1e3);
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const result = await exec.call(bash, cmd, opts ? {
|
|
65
|
+
cwd: opts.cwd,
|
|
66
|
+
env: opts.env,
|
|
67
|
+
signal: timeoutSignal
|
|
68
|
+
} : void 0);
|
|
69
|
+
if (timeoutSignal?.aborted) return {
|
|
70
|
+
...result,
|
|
71
|
+
stderr: result.stderr || `[flue] Command timed out after ${timeout} seconds.`
|
|
72
|
+
};
|
|
73
|
+
return result;
|
|
74
|
+
} finally {
|
|
75
|
+
if (timer) clearTimeout(timer);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
53
78
|
scope: (options) => createScope(options?.commands ?? []),
|
|
54
79
|
readFile: (p) => fs.readFile(resolve(p)),
|
|
55
80
|
readFileBuffer: (p) => fs.readFileBuffer(resolve(p)),
|
|
@@ -93,7 +118,8 @@ function createSandboxSessionEnv(api, cwd, cleanup) {
|
|
|
93
118
|
async exec(command, options) {
|
|
94
119
|
return api.exec(command, {
|
|
95
120
|
cwd: options?.cwd ?? cwd,
|
|
96
|
-
env: options?.env
|
|
121
|
+
env: options?.env,
|
|
122
|
+
timeout: options?.timeout
|
|
97
123
|
});
|
|
98
124
|
},
|
|
99
125
|
async readFile(path) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as loadSkillByPath, n as createTools, t as BUILTIN_TOOL_NAMES } from "./agent-
|
|
1
|
+
import { i as loadSkillByPath, n as createTools, t as BUILTIN_TOOL_NAMES } from "./agent-BTB0809P.mjs";
|
|
2
2
|
import { completeSimple, isContextOverflow } from "@mariozechner/pi-ai";
|
|
3
3
|
import { Agent } from "@mariozechner/pi-agent-core";
|
|
4
4
|
import { toJsonSchema } from "@valibot/to-json-schema";
|
|
@@ -491,7 +491,7 @@ function assertRoleExists(roles, roleName) {
|
|
|
491
491
|
if (roles[roleName]) return;
|
|
492
492
|
const available = Object.keys(roles);
|
|
493
493
|
const list = available.length > 0 ? available.join(", ") : "(none defined)";
|
|
494
|
-
throw new Error(`[flue] Role "${roleName}" not registered. Available roles: ${list}. Define roles as markdown files
|
|
494
|
+
throw new Error(`[flue] Role "${roleName}" not registered. Available roles: ${list}. Define roles as markdown files in \`roles/\` (or \`.flue/roles/\`).`);
|
|
495
495
|
}
|
|
496
496
|
function resolveEffectiveRole(options) {
|
|
497
497
|
const role = options.callRole ?? options.sessionRole ?? options.agentRole;
|
|
@@ -814,7 +814,8 @@ var Session = class {
|
|
|
814
814
|
}
|
|
815
815
|
if (!registeredSkill) {
|
|
816
816
|
const available = Object.keys(this.config.skills).join(", ") || "(none)";
|
|
817
|
-
|
|
817
|
+
const cwd = this.env.cwd;
|
|
818
|
+
throw new Error(`Skill "${name}" not registered. Available: ${available}.\n\nSkills are loaded at init() time from ${cwd}/.agents/skills/<name>/SKILL.md inside the session's sandbox. If you expected "${name}" to be there, make sure the file exists in your sandbox at that path before calling init() — the default empty sandbox starts with no files, so it has no skills unless you put them there.\n\nSkills can also be referenced by relative path under .agents/skills/ (e.g. "triage/reproduce.md").`);
|
|
818
819
|
}
|
|
819
820
|
const schema = options?.result;
|
|
820
821
|
const skillPrompt = buildSkillPrompt(registeredSkill.instructions, options?.args, schema);
|
|
@@ -845,7 +846,8 @@ var Session = class {
|
|
|
845
846
|
const effectiveCommands = mergeCommands(this.agentCommands, options?.commands);
|
|
846
847
|
const result = await (await createScopedEnv(this.env, effectiveCommands)).exec(command, {
|
|
847
848
|
env: options?.env,
|
|
848
|
-
cwd: options?.cwd
|
|
849
|
+
cwd: options?.cwd,
|
|
850
|
+
timeout: options?.timeout
|
|
849
851
|
});
|
|
850
852
|
const shellResult = {
|
|
851
853
|
stdout: result.stdout,
|
|
@@ -67,6 +67,7 @@ interface SessionEnv {
|
|
|
67
67
|
exec(command: string, options?: {
|
|
68
68
|
cwd?: string;
|
|
69
69
|
env?: Record<string, string>;
|
|
70
|
+
timeout?: number;
|
|
70
71
|
}): Promise<ShellResult>;
|
|
71
72
|
/** Create an operation-scoped environment, usually backed by a fresh Bash runtime. */
|
|
72
73
|
scope?(options?: {
|
|
@@ -119,12 +120,16 @@ interface AgentConfig {
|
|
|
119
120
|
resolveModel?: (modelString: string) => Model<any>;
|
|
120
121
|
compaction?: CompactionConfig;
|
|
121
122
|
}
|
|
122
|
-
/**
|
|
123
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Request context passed to agent handler functions. Pass type parameters
|
|
125
|
+
* to type `payload` and `env` (e.g. the `Env` interface generated by
|
|
126
|
+
* `wrangler types`). Compile-time only — no runtime validation of `payload`.
|
|
127
|
+
*/
|
|
128
|
+
interface FlueContext<TPayload = any, TEnv = Record<string, any>> {
|
|
124
129
|
readonly id: string;
|
|
125
|
-
readonly payload:
|
|
130
|
+
readonly payload: TPayload;
|
|
126
131
|
/** Platform env bindings (process.env on Node, Worker env on Cloudflare). */
|
|
127
|
-
readonly env:
|
|
132
|
+
readonly env: TEnv;
|
|
128
133
|
/** Initialize an agent runtime with sandbox + persistence. */
|
|
129
134
|
init(options?: AgentInit): Promise<FlueAgent>;
|
|
130
135
|
}
|