Skip to content

Update dependency @flue/runtime to v2 - #423

Open
mend-for-github-com[bot] wants to merge 1 commit into
mainfrom
whitesource-remediate/flue-runtime-2.x
Open

mend-for-github-com[bot] wants to merge 1 commit into
mainfrom
whitesource-remediate/flue-runtime-2.x

Conversation

@mend-for-github-com

Copy link
Copy Markdown
Contributor

Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Type Update Change
@flue/runtime (source) dependencies major 1.0.0-beta.9 → 2.0.0

By merging this PR, the below vulnerabilities will be automatically resolved:

Severity CVSS Score Vulnerability
High High 7.5 CVE-2026-19534
High High 7.4 CVE-2026-84961
Medium Medium 6.5 CVE-2026-84933
Medium Medium 5.9 CVE-2026-18149
Medium Medium 5.9 CVE-2026-84890
Medium Medium 5.9 CVE-2026-85014
Medium Medium 5.9 CVE-2026-85024
Low Low 3.7 CVE-2026-18540
Low Low 3.7 CVE-2026-84947
Low Low 3.7 CVE-2026-85008

Release Notes

withastro/flue (@​flue/runtime)

v2.0.0

Compare Source

Breaking Changes
  • Flue is now a Vite plugin — flue dev and flue build are removed. Adopt Flue by adding flue() from the new @flue/vite package to vite.config.ts; vite dev and vite build own the deployable application. The Node target emits dist/server.mjs (run it with node dist/server.mjs; vite preview for Node is deferred). The Cloudflare target is the official @cloudflare/vite-plugin as an explicit sibling — plugins: [flue(), cloudflare({ config: flueWorkerConfig() })] — with the target auto-detected from cloudflare()'s presence; wrangler.jsonc stays user-owned, and Flue's contributions (the generated Worker entry, served as the virtual:flue/worker module, and the per-agent Durable Object bindings) are applied in memory by the flueWorkerConfig() customizer — nothing is generated into the project tree. flue.config.ts survives as the host-independent project config: defineConfig moves to @flue/runtime/config (the @flue/cli/config export is removed), the accepted fields are target, app, db, cloudflare, agents (a glob narrowing the 'use agent' scan), providers, and tracing, and the root/output fields and named vite export are dropped. See the migration guide for the full before/after.

  • File-based routing is removed — app.ts is the route map. The src/agents/*, src/workflows/*, and src/channels/* directory conventions no longer create routes or registrations. An agent module joins the application through the 'use agent' directive (the module's first statement); the build scans source files for the directive and registers every capitalized exported function as an agent, deriving each agent's durable storage identity from the exported function's name — or an agentName static pinning it — never from the file (on Cloudflare, one generated Flue<PascalName>Agent Durable Object class per exported agent function; renaming an unpinned function is the storage-identity change, expressed with wrangler-native renamed_classes, and colliding identities are a build error — renaming the file changes nothing). Every HTTP route is mounted explicitly in app.ts: app.route('/agents/triage', createAgentRouter(Triage)) for agents (createAgentRouter from @flue/runtime/routing), app.route('/channels/slack', slack.route()) for channels (hand-rolled channels mount via createChannelRouter). The router factory is pure and side-effect-free — registration comes from the scan, so dispatch-only agents need no mount, and the same agent can be mounted at two URLs. The agent module's named exports route (middleware), attachments, and description are removed along with the convention that read them: protect a mount with ordinary middleware layered in app.ts before it, and the attachment byte route is part of every agent router rather than opt-in. createDefaultFlueApp() and the runtime flue() router are removed; a missing app.ts is a build error.

  • Workflows are removed. defineWorkflow, invoke, run stores and run event streams, the /runs/:runId and POST /workflows/:name routes (including ?wait=result), listRuns/getRun, the SDK client.workflows/client.runs namespaces, the React workflow hooks, and the dev-console run UI are all deleted, with no compatibility stubs. Conversations are the only durable unit: a workflow becomes a program that drives an agent — flue run in a shell or CI step, the start()/init() JS API from Node, the Agent SDK over HTTP, or an external durable-workflow engine (Cloudflare Workflows, Inngest, Temporal) that checkpoints the dispatch receipt in one step and reads the settled reply in another. A deterministic code-first entrypoint (the old no-model run() body) has no direct replacement. The migration guide covers the conversion.

  • Agent authoring is rewritten: an agent is a plain exported function configured with hooks, and defineAgent is removed. A beta agent was a defineAgent(initializer) default export whose initializer returned a config object (model, instructions, tools, actions, skills, subagents, sandbox, …); now the capitalized exported function in a 'use agent' module is the agent. It runs fresh before every model call (a render), its return string is the instruction document, and everything else is declared by calling hooks during the render: useModel(model, { thinkingLevel?, compaction? }) (required, exactly once — the old model/thinkingLevel/compaction config fields), useInstruction(text), useTool(tool), useSkill(skill), useSubagent(subagent), useMcpConnection(definition), useSandbox(factory, { cwd? }), usePersistentState(name, default?) (durable per-instance state with React-style functional updates), useInitialData() (instance-creation data), useDelivery() (the delivered message in front of the model), useDataWriter(name, { schema }) (named client-facing data parts), useDispatchMessage() (an instance-bound dispatcher), and the lifecycle and response-boundary hooks useAgentStart(run), useAgentFinish(run) (whose ctx.append can send the model back to work in the same response), useResponseStart(fn), and useResponseFinish(fn) (response metadata). Because the render re-runs each turn, declarations may be conditional: a tool, skill, subagent, MCP connection, or sandbox that appears or disappears between renders takes effect at the next turn boundary and is narrated to the model as a structured delta signal. Custom hooks are ordinary use* functions — composition needs no registration. The contract the platform reads without running the function lives in statics on the function itself: agentName (the durable-identity override), initialData (a Valibot schema for creation data), and durability ({ maxAttempts, timeoutMs }, previously a config field). Deleted with the old model: defineAgent, createAgent, defineAgentProfile and AgentProfile, and the whole Actions surface (defineAction, the actions: [...] list, and their error types) — a model-invoked job is a useTool tool now, with harness: true for one that needs to prompt the harness. Subagents are declared values too: useSubagent(defineSubagent({ name, description, agent, model?, thinkingLevel? })) renders a delegate for the always-present task tool, GeneralSubagent is the exported general-purpose delegate, and a task call can only name a declared subagent. See the migration guide.

  • The tool run() context and the harness are reshaped. ToolContext is now { toolCallId, log, signal? } plus data — the schema-parsed arguments, renamed from input — when the tool declares an input schema, harness when it declares harness: true, and step when it declares durable: true; every tool gets log for structured progress events. The harness handed to harness: true tools and to the useAgentStart/useAgentFinish contexts flattens to { name, prompt(), compact(), sandbox }: session(), sessions, shell(), and fs are removed, and harness.sandbox is the live SessionEnv (exec, readFile, writeFile, stat, readdir, mkdir, rm, …), present only when the agent declared a sandbox. The six built-in tools (bash, read, write, edit, grep, glob) are composable per-tool factories (createBashTool(env), createReadTool(env), …) that a SandboxFactory's optional tools() can reassemble, extend, or replace wholesale.

  • Sandboxes are opt-in: an agent that declares no useSandbox() has no execution environment. The beta gave every agent an implicit in-memory virtual sandbox with full network access; now the six built-in shell/filesystem tools are added only when the render declares a sandbox, harness.sandbox and workspace-skill reads throw a clear declare-a-sandbox error otherwise, and workspace discovery contributes nothing to the prompt. Framework tools — task, activate_skill, packaged-skill reads — are environment-independent, so skills and subagents work sandbox-less. useSandbox(factory, { cwd? }) is the only way to attach an environment: local() (@flue/runtime/node) runs commands on the host, cloudflareSandbox(stub) (@flue/runtime/cloudflare) runs them in a Cloudflare Sandbox container, and the beta default's equivalent is useSandbox(bash(() => new Bash({ fs: new InMemoryFs() }))) with just-bash added to your own dependencies — the same you-import-the-library pattern as model providers. The declaration is submission-scoped and may be conditional: presence is re-read at every turn boundary, and a flip attaches or detaches the environment before the next model call, announced to the model as one full environment signal (new cwd, complete tool roster, a warning that the previous environment's files may be inaccessible). Not constructing the default environment also slims deploys that never opted in: the cloudflare example's main chunk drops from 5,481 kB to 3,482 kB.

  • Skill and markdown imports drop their import attributes — the specifier decides. Importing a path that resolves to a SKILL.md packages that skill directory at build time and yields a SkillReference for useSkill(...) (static imports only; frontmatter validated against the Agent Skills spec); importing any other .md file yields the markdown text as a plain string, which defineSkill({ name, description, instructions }) turns into a skill. The with { type: 'skill' } and with { type: 'markdown' } attribute forms are removed. defineSkill now validates and freezes instead of packaging — instructions is required, packaging happens lazily, and the runtime writes the frontmatter — and useSkill(...) accepts a SkillReference or a SkillDefinition directly. Manual skill invocation is gone: skills mount into the agent's disclosure catalog (or join it via workspace discovery under a sandbox's .agents/skills/), and the model activates them through the framework's activate_skill tool.

  • MCP servers are declared as connection definitions; connectMcpServer is removed. One flat McpConnectionDefinition ({ name, url, transport?, auth?, headers?, tools?, optional?, timeoutMs?, … }) drives the whole surface: useMcpConnection(definition) declares a server from the agent render — its tools mount as mcp__<server>__<tool>, read once per submission and cached per agent instance — defineMcpConnection(definition) is the validating, freezing typing helper for the exportable unit (spread it for per-mount overrides), and createMcpConnection(definition) is the imperative factory replacing the beta's connectMcpServer(name, options) for module-scope use on the Node target (McpServerConnection is renamed McpConnection). New in the definition: auth — a static bearer token or a function resolved freshly on every request, the seam for per-user and rotating credentials (a 401 retries once after the credential resolver is consulted); tools — an allowlist by the server's own tool names, where a name the server doesn't expose is a hard error; and optional: true — a server that fails to resolve mounts zero tools for that submission instead of failing it, announced to the model once as a resources signal naming each unavailable server and surfaced to observers as a warning event (failures are never cached, so the next submission retries). Underneath, the adapter runs on the v2 MCP SDK (@modelcontextprotocol/client, a direct dependency of @flue/runtime): structured tool output is validated by the client itself with a workerd-compatible validator, and existing v1 servers interoperate unchanged.

  • Model providers are Pi-native: registerProvider and registerApiProvider are removed, and the providers list is exhaustive. A custom or customized provider is a pi-ai Provider object registered with setProvider(provider) — built with createProvider(...) and the built-in factories from @earendil-works/pi-ai, which you depend on directly — replacing the beta's option-bag registerProvider(id, { ... }), registerApiProvider, and the deprecated pi-ai/compat layer under them (model capabilities such as reasoning, input, contextWindow, and cost are plain fields on the Model objects a provider declares). Model specifiers keep the 'provider-id/model-id' format. The new providers?: string[] config (in flue.config.ts or inline on the flue() plugin) selects which built-in providers exist: omitted, every pi built-in registers as before; set, only the listed providers — catalogs and protocol implementations included — are bundled and resolvable, and the narrowing is real bundle weight: examples/cloudflare with providers: ['anthropic'] deploys 5.8 MB instead of 9.6 MB (#​359). 'cloudflare' in the list selects the Workers AI binding provider — cloudflareBindingProvider({ binding, gateway? }) from @flue/runtime/cloudflare/workers-ai, auto-registered on Cloudflare builds when the list is omitted, with its wire-protocol implementations now loaded lazily so Durable Object cold starts stop paying for them — and naming it on the Node target is a config error pointing at the HTTP alternatives (cloudflare-workers-ai / cloudflare-ai-gateway). Programmatic runs get the same selection as start({ providers }), taking Provider objects.

  • Channel helper APIs are renamed instanceId/parseInstanceId. The value these helpers produce was always the agent instance id, so every channel package renames conversationKey → instanceId and parseConversationKey → parseInstanceId (zendesk's ticketKey/parseTicketKey included), with the Invalid*ConversationKeyError classes following as Invalid*InstanceIdError — a clean break, no aliases. parseInstanceId is also demoted from showcase to escape hatch: agents receive structured facts as creation data (useInitialData()) rather than parsing them back out of the id.

  • The SDK and React hooks address one conversation by URL. The framework no longer knows where agents live — app.ts is the map — so the client collapses to the resource it talks to: createFlueClient({ url: 'https://host/agents/triage/123456' }) (the agent's mount URL plus a caller-chosen conversation id) with send(), wait(), history(), observe(), abort(), and attachmentUrl(). The baseUrl + agent-name/id addressing, the client.agents namespace, and listAgents() are removed; a new conversation is the caller appending a fresh id to the mount URL. @flue/react's useFlueAgent({ url }) replaces useFlueAgent({ name, id }).

  • Assistant output projects as one conversation message per response. An assistant response spanning multiple model steps (text, tool calls, tool results, more text) previously projected one message per step; history snapshots and live updates now fold every assistant step of a submission into one message — parts accumulate in record order, usage sums across steps, identity fields stay the first step's — matching how the AI SDK ecosystem assembles one UIMessage per response. Clients that keyed logic on per-step assistant messages see fewer, larger messages; the chunk wire vocabulary is unchanged.

  • The @flue/dev-console TUI package is removed. The terminal chat console is not a direction Flue is investing in; local manual testing lives in the in-browser demo chat app (demo/ in the repository), which is the basis for a future flue dev experience.

  • flue run is rewritten as transport-free local execution, and the CLI slims to run/init/add/update/docs. flue run <path-to-agent-module> --message "..." compiles and runs one agent module under Node with no HTTP listener or Cloudflare emulation: product events stream to stderr, the final reply (or a --json envelope) prints to stdout, and the conversation id is printed so --id can continue the same conversation across invocations. Persistence honors the project's db.ts when configured, else a project-local cache database. Removed flags: --server, --header, --target, --root, --output, --config, and --input (superseded by --message); flue run <name> (a bare agent name) is replaced by the module path form. flue dev and flue build error with pointers to the Vite commands.

  • dispatch() takes a structured message, not an opaque input. AgentDispatchRequest.input: unknown is replaced by message: DeliveredMessage, the same unified shape a direct HTTP prompt uses internally: { kind: 'user', body: string, attachments?: DeliveredAttachment[] } for a real chat turn, or { kind: 'signal', type: string, body: string, attributes?: Record<string, string>, tagName?: string } for a structured event/webhook payload. body is always a string — JSON-stringify structured payloads yourself. dispatch() can now deliver a kind: 'user' message with image attachments, the same way a direct HTTP prompt does (attachments on kind: 'signal' are not supported). message is validated the same way as a direct prompt's body (a malformed message throws InvalidRequestError) instead of being forwarded unchecked. On the adapter surface, the submission input types collapse into one AgentSubmissionInput interface ({ kind: 'dispatch' | 'direct', submissionId, agent, id, message, acceptedAt }) — DispatchAgentSubmissionInput and DirectAgentSubmissionInput are removed, a dispatched submission's persisted input carries only submissionId (no separate dispatch-scoped id), and the persisted-attachment helpers exported from @flue/runtime/adapter are renamed: prepareDirectSubmission → prepareSubmissionAttachments, hydratePersistedDirectSubmission → hydratePersistedSubmissionAttachments, matchesPersistedDirectSubmission → matchesPersistedSubmissionAttachments. Every first-party channel example and blueprint is updated to the new shape, following one convention: body carries the message itself and structured metadata (sender identity, ids, titles) goes in attributes as flat strings.

  • dispatch() targets an agent definition, not a name string. The single signature is dispatch(agent, { id, message }), where agent is the default export of a 'use agent' module. The dispatch({ agent: '<name>', id, message }) named-string form is removed — a non-definition first argument throws InvalidRequestError — and the NamedAgentDispatchRequest type is no longer exported from @flue/runtime.

  • The direct agent HTTP wire body is a DeliveredMessage. POST /agents/:name/:id now accepts the same validated DeliveredMessage shape dispatch() admits — { "kind": "user", "body": "...", "attachments"?: [...] } for a chat turn (or a kind: 'signal' event) — replacing the { message, images? } wire body and the internal mapping layer behind it. @flue/sdk's AgentPromptOptions.message is now a DeliveredMessage (was string; the images option folds into attachments), and AgentPromptImage is renamed DeliveredAttachment in @flue/sdk and @flue/react. The DirectAgentPayload type is removed from @flue/runtime. flue run delivers its --message text as a kind: 'user' message.

  • Signal tagName must be a valid XML tag name. The optional tagName on a kind: 'signal' message is rendered as the signal's XML envelope in model context, so it is now validated (letters, digits, _, -, .; must not start with a digit, -, or ., and must be non-empty) and a malformed value throws InvalidRequestError at admission instead of injecting markup past the body/attribute escaping.

  • Direct agent prompts are fire-and-forget only. The ?wait=result synchronous mode on agent HTTP POSTs is removed; agent prompts always return a 202 admission. The in-process observer registry (createAgentSubmissionObserverRegistry, AgentSubmissionObserver, AgentSubmissionObserverRegistry), the DirectAttachedOptions/invokeDirectAttached/runDirectSyncMode admission path, and the result field on SubmissionSettledRecord, AgentConversationSettlement, AgentSubmissionSettledEvent, and the submission_settled FlueEvent variant are all removed. Callers that need the actual assistant reply should read it from the conversation transcript via the conversation client's history() or the live conversation stream.

  • The SDK's prompt() is removed. Use the conversation client's send() (fire-and-forget) plus wait() (completion-await, now Promise<void>) plus history() (to read the reply). AgentPromptResult and AgentPromptResponse types are removed from @flue/sdk.

  • The SDK's wait() no longer resolves with a result. It resolves void on completion and throws FlueExecutionError on failure or abort. A remote abort is now distinguishable from a real failure: FlueExecutionError.failure is 'aborted' for an aborted settlement (previously both classified as 'failed').

  • AttachedAgentEventCallback type removed from @flue/runtime. The onEvent callback parameter on admission no longer exists.

  • reconcileInterruptedSubmission return type simplified. Returns AgentSubmission | undefined (the replacement submission, or undefined) instead of the 5-variant ReconciliationResult discriminated union. Custom coordinator implementations that inspected .disposition should branch on truthiness instead.

  • The init() handle's dispatch() is enqueue-only; awaiting the reply is the new read(). handle.dispatch(request) now resolves at admission with the durable DispatchReceipt — the same contract as the top-level dispatch(), so the verb means one thing everywhere — instead of awaiting the settled reply, and it no longer takes an options argument. The settled reply moves to handle.read(receipt | submissionId, { onEvent?, signal? }), which resolves with the AgentReply (rejecting with AgentRunError on a failed or aborted settlement) and re-attaches from any process at any later time: settlement and reply are durable conversation records, so a receipt persisted across a crash — for example as a workflow step's durable result — is all a retry needs, and a submission that settled long ago resolves immediately. read()'s signal stops only the read (a local cancel; previously the dispatch signal requested a durable abort of the run) — the durable abort intent is the new explicit handle.abort(), which covers the running head and every queued submission. A read addressed to an instance that does not exist rejects with AgentInstanceNotFoundError on every target rather than waiting for work that can never settle. The AgentDispatchOptions type is renamed AgentReadOptions and belongs to read().

  • One id vocabulary end to end: dispatchId is renamed submissionId. DispatchReceipt.dispatchId (the field dispatch(), handle.dispatch(), and useDispatchMessage() resolve with) is now DispatchReceipt.submissionId; handle.read(receipt) and handle.read(submissionId) are unaffected, and a bare-string read target is still the receipt's id, just spelled submissionId. New dispatch submission ids are sub_-prefixed (previously dispatch_); ids remain opaque and old dispatch_-prefixed ids stay valid.

  • FlueEvent no longer has a dispatchId correlation field. submissionId alone identifies a submission's activity — dispatched and direct alike — on every event and on FlueExecutionContext; code that branched on dispatchId should read submissionId instead. See the Events Reference.

  • The flue.dispatch.id telemetry attribute is removed. @flue/opentelemetry and the native Cloudflare tracing adapter (createCloudflareTracing()) no longer emit it; flue.submission.id, already emitted alongside it, is the replacement.

  • Trace content is captured by default, and @flue/opentelemetry's content surface collapses to content?: false | { transform }. Both trace adapters — createOpenTelemetryInstrumentation() and the native Cloudflare createCloudflareTracing() — now emit conversation content (gen_ai.input/output.messages, system instructions, tool definitions/arguments/results) unless you pass content: false; installing an instrumentation with instrument(...) is the consent (on Cloudflare, enabling Workers Traces), and a transform is the policy hook. On @flue/opentelemetry this removes GenAIContentPolicy as a public type along with enabled, inline, externalContent (a side-effecting transform is the replacement — GenAIContentScope now carries traceId/spanId for correlation), limits (slice in the transform, or use the exported truncateContent(content, { maxBytes })), and the diagnostic callback. Exception message and stack now ship by default through the same gate (strip them with a transform on exception_message/exception_stacktrace, or disable content); the Cloudflare adapter continues to exclude them unconditionally. The flue.telemetry.content.*.truncated/.omitted marker attributes are gone: truncation is structural and in-band — payloads stay valid JSON under a 56 KiB per-span content budget (every content attribute a span carries shares one pool, with a reserve so response content always has room beside large prompts), with oldest messages dropped behind a role: "flue" sentinel message and oversized strings cut with a [flue:truncated, …] suffix (FLUE_TELEMETRY_EXTENSION_REVISION is now 4). @flue/opentelemetry requires a @flue/runtime version shipping the ./telemetry subpath.

  • GEN_AI_SCHEMA_URL is removed, and @flue/opentelemetry's tracer/meter no longer declare a schemaUrl. The URL was fabricated — the OpenTelemetry GenAI semantic-conventions repository has not published a schema URL — so exporting it and stamping it on the wire handed schema-aware tooling a dead link with defined semantics. The pinned upstream revision the gen_ai.* keys were read from remains exported as GEN_AI_SEMCONV_REVISION.

  • Persisted stores are stamped format_version 1, and stores written by 1.0.0-beta.x are rejected. Every persisted Flue store durably records the format version it was created with — a format_version key in the SQL backends' flue_meta table, the equivalent on the non-SQL adapters — and refuses to open anything else, throwing PersistedFormatVersionError (type: 'persisted_format_version_unsupported'). The persisted shapes changed incompatibly across the 2.0 line (one unified message input for both transports, the submission_id column naming, chunked attachment storage), so a beta-era store must be cleared; there is no migration. For adapter authors, the stamp contract is exported from @flue/runtime/adapter as FLUE_FORMAT_VERSION and assertSupportedFlueFormatVersion() (replacing FLUE_SCHEMA_VERSION and PersistedSchemaVersionError), and defineStoreContractTests exercises the stamping rules on every backend.

  • A tool's run() returns a result envelope — { output?, terminate? } — and bare non-string values are rejected. output is the tool's result value, validated against the declared output schema and serialized for the model exactly as before; terminate: true ends the agent's turn once the current tool batch settles — the same loop-ending contract the built-in finish/give_up tools use, so a custom tool can conclude a response without another model round-trip (the turn ends only when every result in the batch terminates, and a throwing tool never terminates). Returning a bare string remains shorthand for { output: <string> }, and returning nothing is still allowed for tools without an output schema; any other bare value (number, boolean, array, null) or an object with keys beyond output/terminate now throws an error telling you to wrap it — return { output: <value> } — which is what makes plain-object returns unambiguous. Termination is durable: the flag is recorded on the tool's canonical outcome (additive — no storage schema bump), so a crash between a terminal tool batch's commit and settlement now recovers to the live outcome — the submission settles without an extra model turn — closing the same recovery gap for turns ended by finish/give_up.

  • SessionEnv.exec now rejects promptly on abort — an un-cancellable sandbox command becomes a documented orphan. When options.signal aborts mid-command, the returned promise rejects with AbortError immediately instead of waiting for the remote command to settle. Previously, aborting agent work during a command on a signal-blind provider SDK (E2B, Daytona, Modal, and most others expose no mid-flight cancellation) blocked the entire abort path — the error tool result, the batch commit, the submission_aborted advisory, and the next queued prompt — until the command finished on its own. The guarantee is implemented once, in the runtime's wrappers (createSandboxSessionEnv and the bash() factory adapter), so SandboxApi itself is structurally unchanged and every adapter built on the wrappers gets the behavior for free; the first-party Cloudflare adapter's own local abort race is removed in favor of the shared one (its container death detector now handles liveness only). The still-running remote command is an explicit, named state — an orphaned command: its eventual settlement is consumed, never recorded (no conversation records, no tool events; a late SandboxDiedError from an orphan is swallowed into the settlement), and is observable through the new optional createSandboxSessionEnv(api, cwd, { onOrphanSettled }) callback with the exported OrphanedExecSettlement type. The abort-time error message — which reaches the model as the error tool result — now carries the suffix "The sandbox command could not be confirmed cancelled and may still be running.", and a subsequent command may run in the same sandbox while an orphan is still executing (whether the provider serializes or interleaves is provider-defined; this was already the reality on Cloudflare). Deliberately breaking in two ways: callers that relied on an abort rejection implying the remote command had finished lose that (it was already false on Cloudflare), and hand-rolled SessionEnv implementations built without the wrappers are now out of contract if they late-reject. The sandbox reference's "do not simulate mid-flight cancellation with Promise.race" guidance is withdrawn and inverted: the wrapper owns the abort race, and adapters must not add their own. No persisted-schema or record-shape changes — the abort path emits the same records, just without the wait.

  • AgentSubmissionStore gains a required method: settleQueuedSubmission(submissionId, outcome, error). Terminal settlement for a queued submission that can never be claimed — atomic and queued-gated (the first terminal state wins against a racing claim), no attempt is created, and no canonical settlement record exists for it (the same already-tolerated class as the SQL stores' malformed-row termination: the row's outcome is authoritative when no stream record exists). Custom AgentSubmissionStore implementations must add it; every first-party backend (the built-in SQLite stores plus @flue/libsql, @flue/postgres, @flue/mysql, @flue/mongodb, and @flue/redis) ships it in lockstep, and defineStoreContractTests now exercises it unconditionally.

New Features
  • Idempotent delivery: name a send with idempotencyKey and retries converge instead of duplicating turns. Every real inbound channel is at-least-once — Slack redelivers events, webhook providers retry, a browser resends after a lost response — and until now each redelivery admitted a brand-new submission. Both transports now accept an optional caller-chosen key (≤256 chars): dispatch(agent, { id, message, idempotencyKey }) (and the init() handle's dispatch), a reserved idempotencyKey body sibling beside initialData/uid on the direct POST /agents/:name/:id, and send({ message, idempotencyKey }) in @flue/sdk. Two admissions to the same agent instance with the same key are the same submission — delivered and answered at most once, every admission returning the same receipt, with replays marked deduplicated: true and echoing the original acceptedAt (direct replays re-attach from the stream origin offset). The key names the delivery, not the outcome: a submission that settled failed stays failed — retry with a fresh key to ask again. Reusing a key with a different payload rejects with the new structured 409, SubmissionConflictError (type: 'submission_conflict', the existing submission's id in meta.submissionId), identical on both targets — the Cloudflare internal dispatch conflict body is now structured JSON rehydrated Worker-side instead of bare text. No storage schema change and nothing new asked of persistence adapters: the key deterministically derives the submission id (sub_ik_ + SHA-256 over agent/instance/key, a frozen wire format), so the existing idempotent admission machinery does the rest, and custom stores get the feature for free. Channel packages stay stateless — pass the provider's redelivery-stable id through (idempotencyKey: payload.event_id for Slack); the channel READMEs and guide show the mapping.
  • New @flue/runtime/telemetry subpath: the backend-neutral GenAI content machinery shared by the native Cloudflare tracing adapter and @flue/opentelemetry — the role/parts message projection (inputMessages, outputMessages, systemInstructions, toolDefinitions, …), the content pipeline, the truncateContent(content, { maxBytes }) helper (same structural in-band truncation and [flue] sentinels as the built-in safety net, for tighter policy budgets inside a transform), the CONTENT_ATTR vocabulary, and the semconv revision constants. Dependency-free and Node-evaluable; the runtime stays free of @opentelemetry/api.
  • createCloudflareTracing() now captures conversation content into Workers Traces by default — input/output messages, system instructions, and tool definitions/arguments/results on the same gen_ai.* attributes the OTel adapter emits, so Flue conversations read directly in the Traces dashboard. content: false restores content-free spans; content: { transform } redacts or drops in code. Raw error messages and stacks never ship on this backend, caller-origin bash is traced as the same flue-owned flue.operation shell span @flue/opentelemetry emits — no gen_ai.* claims and no content, since a user's shell operation is not a model tool call — and all content projection runs only on sampled invocations.
  • Agents deployed to Cloudflare are traced with no wiring: enable Workers Traces and each response's trace carries the invoke_agent/chat/execute_tool spans, no instrument(...) line needed. Installing createCloudflareTracing(...) yourself customizes as before; tracing: false in flue.config.ts opts out.
  • Public conversation messages now expose typed purpose (user, assistant, dispatch, or advisory) and display (visible, hidden, or diagnostic), plus optional turnId grouping and a signal descriptor, so clients can distinguish public chat from internal, control, and advisory activity without parsing message text, timestamps, or ordering. The classification is applied identically across conversation history() snapshots and live updates, and @flue/sdk / @flue/react shapes are updated in lockstep (#​404).
  • vite preview now works on the Node target. vite build emits two entries — the self-starting dist/server.mjs and the non-listening dist/app.mjs application chunk it imports — and preview serves the built app.mjs natively (no Vite transformation), so what preview serves is exactly what node dist/server.mjs would serve, including production persistence defaults and the real process environment. Running preview without an artifact fails with "Run vite build first."
  • vite dev on the Node target now loads the project's .env file set (.env, .env.local, .env.<mode>, .env.<mode>.local) into the application's environment, with shell-exported values taking precedence — matching flue run, so model-provider keys work without a shell export. The built server still reads only the real environment, and Cloudflare .dev.vars/.env handling stays with the Cloudflare plugin.
  • The Node dev server's 503 "runtime unavailable" envelope now carries the underlying application load failure in its dev-only field, so a request tells you WHY the app failed to load without tailing the terminal, and load-failure stacks are remapped to authored sources (ssrFixStacktrace).
  • flue init is now interactive and scaffolds the full project skeleton for the new surface instead of only flue.config.ts: missing choices are prompted, with --target and --deploy answering them for scripted runs, and when the HTTP server setup is included it also writes a vite.config.ts with the flue() plugin (plus cloudflare() on the Cloudflare target), a starter src/app.ts route map, and — on the Cloudflare target — a wrangler.jsonc with nodejs_compat, observability.traces enabled, and commented migration guidance. --force still applies only to flue.config.*; the other files are created only when absent and never overwritten.
  • @flue/react's useFlueAgent() now exposes refresh(), so apps observing an agent conversation that may be created out-of-band (a server-side wakeup, queue worker, or webhook) can re-check on their own schedule instead of faking an empty history snapshot. Retry policy stays in userland (#​403).
  • Internally-logged error responses now carry a correlation ref. When the runtime renders a 500-class internal error (a non-FlueError, or a non-HTTP FlueError that escaped to a route), it mints an err_-prefixed ULID and stamps it in three places: the envelope's new error.ref field, a new flue-error-ref response header (which survives body-less HEAD responses and body-swallowing intermediaries), and the matching server-side log line's [flue] [err_…] prefix — so a caller reporting a production 500 can point operators at the exact log entry, and the ULID alone brackets the time window. Caller-mistake 4xx envelopes stay unlogged and ref-free ("ref iff logged"). When the failing request carried a W3C traceparent, it is logged beside the ref (never echoed on the response). @flue/sdk's FlueApiError exposes the ref as ref (envelope body first, header fallback) and includes it in the composed message — Flue API error 500 [internal_error] (ref err_…): … — so the ref lands in calling services' own logs with no extra code. The failed-submission settlement text now also directs callers to quote the submissionId, the async-path counterpart of the ref.
  • Queue activity and recovery failures are first-class runtime events. Three additions complete the observe() submission vocabulary (queued → running → settled): submission_queued is emitted immediately after durable admission on both targets (at-least-once — idempotent replays re-emit); submission_running is emitted from the attempt's own context on every attempt, carrying attemptCount/maxAttempts, so a recovery replacement re-announces itself with the incremented count; and submission_recovery surfaces every contained coordinator recovery failure — admission-side materialization, settlement finalization, interrupted-attempt reconciliation, attempt start/processing, and pass-wide failures — with operation/outcome discriminators, the same stackless error shape as submission_settled (the live observation adds the classified errorInfo with throw-site stack), and re-emission on every failed wake while the condition persists, closing the alerting hole where a submission stuck in a retry loop was visible only in stderr (each site's structured console.error remains). Deriving per-instance busy/idle by keying queued/running against settled on submissionId is now a documented stable pattern that converges across process restarts and Durable Object eviction; a delivery that joins a busy conversation emits queued + settled but never running. All three are live-only events like the rest of the stream — no conversation records, no schema change — and existing observers that branch on specific event.types are unaffected (exhaustive switch statements need a default arm). See the Events Reference.
  • start() and init(): the programmatic agent client. start({ agents, db?, env?, providers? }) (from @flue/runtime/node) assembles a transport-free Flue runtime in any Node process — agents passed as the functions themselves, an optional PersistenceAdapter (default: in-memory), and stop()/await using for drain-and-dispose — so scripts, cron jobs, and tests drive agents without an HTTP server. init(agent, { id?, uid? }) (from @flue/runtime, on both targets) returns the durable handle for one agent instance — an address, not a resource: it creates nothing and does no I/O, so it is safe at module scope — with dispatch() (enqueue-only, resolving at admission with the durable DispatchReceipt; a bare string is shorthand for a user message, { message, initialData?, idempotencyKey? } the full payload), read() (await the settled AgentReply, re-attachable from any process at any later time), and abort(). On Cloudflare the handle works inside cron callbacks, queue consumers, and Cloudflare Workflow steps — checkpoint the receipt as one step's durable result and read the reply in the next — the recommended shape for durable multi-step orchestration now that workflows live outside the framework.
  • durable: true tools get checkpointed steps. A tool declared durable: true receives ctx.step, and step.do(name, fn) runs fn once per name per tool call, durably recording the result before resolving. An interrupted durable: true call is re-executed on recovery with completed steps replaying their recorded values — the code between steps re-runs, memoized steps do not — instead of being settled with an unknown-outcome interrupted error like an ordinary tool. The guarantee is exactly-once-recorded, at-least-once-executed (a crash in the window between fn completing and the record landing re-runs that one step), so steps around external effects should be individually idempotent; step values must be JSON-serializable and small, names deterministic and unique within a call. A thrown error is not an interruption — a durable tool that throws settles as an ordinary tool error the model sees, with no retry-budget burn — and the flags compose: durable: true, harness: true receives both step and harness, with harness.prompt(...) wrapped in a step so recovery doesn't re-prompt.
  • Instance creation data and incarnation-conditional sends. An agent instance can be born with typed data: declare a Valibot schema as the agent's initialData static, read the parsed value with useInitialData(), and seed it from any sender — dispatch(agent, { id, message, initialData }), the init() handle's dispatch({ message, initialData }), a reserved initialData body sibling on the direct HTTP POST, send({ message, initialData }) in @flue/sdk, and flue run --data '<json>'. Validation runs synchronously at admission, before anything durable is admitted — a mismatched creating send rejects instead of queuing an unrunnable submission — and data sent to an already-existing instance is ignored. Every instance also gets a uid minted at its birth (inst_…, carried on every DispatchReceipt and SDK send result), and sends can be made conditional on it ETag-style with the uid option: a string continues only that incarnation (a missing instance or mismatched uid rejects at admission with AgentInstanceNotFoundError / 404, nothing durable), null creates only (an existing instance rejects with AgentInstanceExistsError / 409 carrying the existing uid in meta.uid), and omitted stays unconditional. The CLI speaks the same contract (flue run --uid <uid> / --new), and getAgentInstance(agent, id) looks up an instance's uid when no receipt is at hand.
  • A message that arrives while its conversation is busy can join the live response. Dispatches and direct HTTP prompts delivered mid-response no longer always wait in queue: at the next turn boundary the delivery joins the running response — the agent re-renders with the new input in front of the model, and the joined submission is answered by the response it joined (settlements expose the linkage as answeredBySubmissionId) — while a delivery that misses the live response runs as its own submission afterward, so nothing is lost either way.
  • In-process observe() now receives streaming tool-call argument deltas: the new toolcall_delta event carries { toolCallId, toolName, argumentTextDelta } while a model streams a tool call's JSON arguments, so a UI can preview an in-flight, schema-enforced tool call as it is written. Live-preview only — never persisted or replayed; the complete arguments still arrive with the canonical tool-call record (#​450).
  • The SDK client gains read(admission | submissionId, options?) — the HTTP counterpart of the handle's read(). It awaits the submission's settlement and resolves with its reply ({ text, data, metadata?, submissionId, uid? }), throwing FlueExecutionError on a failed or aborted settlement; the bare-id form follows the conversation from the stream origin, so a process that persisted just the admission re-attaches at any later time and an already-settled submission resolves immediately. wait() remains the cheaper outcome-only primitive. @flue/sdk also exports readSubmissionReply(conversation, submissionId), the pure projection read() uses internally — extract a reply from a history() snapshot or an observe() state with no extra fetch, including the coalesced reply when the send joined a busy response (replacing hand-picked messages.at(-1) reads that silently grab the wrong message on a busy conversation). The same projection backs the runtime's init().read(), so a reply read over HTTP and one read in-process agree.
Fixes & Other Changes
  • Updated @earendil-works/pi-ai and @earendil-works/pi-agent-core to 0.83.0.
  • Conversation reads and cold starts no longer replay an instance's whole record log, and resident conversation state no longer grows with lifetime streaming volume. Every read (history, updates, SSE connects, attachment resolution, in-process observers) is served from one shared per-instance fold advanced incrementally and shared with the writer — a dispatch after a read pays no second replay, and vice versa. The fold releases what it provably never reads again: per-chunk streaming-record bookkeeping at message completion (and at the settle barrier for interrupted streams), and a settled submission's attempt bookkeeping bodies, so the idempotency index scales with non-streaming records instead of delta volume. Cold loads seed from a durable fold checkpoint the writer cuts every 64 appends and fold only the suffix appended since — checkpoints are a discardable cache over the log, self-invalidated by format version, incarnation, and the stream head, never authoritative. Adapter surface: ConversationStreamStore gains optional putFoldCheckpoint/getFoldCheckpoint (the first optional store methods; implemented by every first-party adapter, additive tables/keys with no format-version event) — custom adapters without them stay fully functional and degrade to full replay with one warning per path. The idempotency contract relaxes exactly where the bookkeeping releases: replaying a streaming record after its message resolved now fails loudly instead of deduplicating silently; no legitimate path re-appends one (store-level producer sequencing dedupes batch retries).
  • @flue/opentelemetry now reserves the Stable exception.type attribute for the exception class name (RateLimitError, TypeError, …), per the OpenTelemetry exception conventions — previously it duplicated Flue's error taxonomy, which stays on error.type only. Applies to span exception events, span status, and gen_ai.client.operation.exception log records alike; when a failure carries neither a throw-site class name nor a (content-gated) message, no exception record is emitted and the span's error.type is the whole failure record.
  • Both trace backends now stamp gen_ai.agent.name on execute_tool spans (conditionally required by the GenAI semantic conventions when the owning agent is known), and the native Cloudflare adapter's chat spans gain server.address/server.port, gen_ai.conversation.compacted, and openai.api.type, closing the attribute gap with @flue/opentelemetry.
  • Terminalizing a durable agent submission (retry exhaustion, timeout, post-input interruption, or abort) now settles its conversation to a deterministic rest state instead of leaving dangling tool calls behind. Every tool call without a confirmed outcome gets an explicit interrupted-error tool_outcome and the batch is committed — recorded outcomes are preserved and nothing is re-executed or resumed — so a task tool call can no longer rest as "still running" forever in history projections, and the settled turn stays visible to future model context instead of being silently dropped. For a task call the marker includes the retained child conversation id; the child transcript itself is untouched. An interrupted in-progress assistant stream is likewise completed as aborted at terminalization. The terminal advisory now carries the interrupted-call list as structured attributes.interruptedTools (also on SubmissionRetryExhaustedError/SubmissionInterruptedError meta), so apps can settle their own run state without parsing text. Conversations already left dangling by earlier versions self-heal on their next prompt: a new submission settles abandoned trailing state before appending its input (#​419).
  • The local CORS defaults now apply to Cloudflare-target vite dev and to vite preview on both targets, not just Node vite dev, and are tightened to localhost: credentialed requests are allowed from localhost-family origins (Vite's own default origin set) instead of any reflected Origin, with the durable-stream coordination headers exposed — so separate-origin local clients (like the repository's demo chat app) can stream conversations from any local server shape, while an arbitrary website can no longer drive the local agent API through ambient credentials (#​483). Non-localhost clients need an explicit server.cors / preview.cors override — user-set values still win — and production servers keep CORS as an application concern.
  • Wrong-environment import failures now print the import chain. When a cloudflare:* module reaches a Node graph — under vite dev, vite build, or flue run — the diagnostic names the route to the problem (src/app.ts imports src/lib/platform.ts imports cloudflare:workers), not just the failing specifier, and adds the type-only escape hatch (import type is erased at build time). In vite dev the chain also lands in the 503 envelope's dev field.
  • Watcher event bursts (branch switch, format-on-save across files) now coalesce: the agent re-scan and Cloudflare input-regeneration queues run at most one in-flight pass plus one queued pass instead of one full pass per event, which also keeps dev-server shutdown from waiting behind a long queue.
  • @flue/vite hardening from a comparative review against SvelteKit's and TanStack Start's Vite plugins: resolve.dedupe for @flue/runtime/hono on every target, an explicit warning when user Vite config conflicts with values flue() enforces, a warning when multiple flue.config.* files coexist, stackless errors for expected user mistakes (the message is the diagnostic, without a framework stack burying it), hook-level transform filters (the import-attribute transform no longer parses every TS module in the graph), self-contained transform sourcemaps (sourcesContent), a file:line:column pointer on the missing-default-export diagnostic, and the Chrome DevTools well-known probe is answered with a 404 before it can reach a catch-all application route.
  • MCP tool connections no longer crash on Cloudflare Workers when a connected server advertises a tool outputSchema. JSON Schema validation now uses a codegen-free strategy compatible with the workerd runtime instead of runtime code generation (#​400).
  • Completed assistant messages now preserve their submissionId in conversation history() snapshots, so clients that group a user turn with its assistant answer by submission id keep that grouping after reload (#​402).
  • Tool-call duration is now durably recorded and surfaced as durationMs on the resolved dynamic-tool part in both history snapshots and live updates, instead of being available only on the ephemeral live event (#​407).
  • The Cloudflare extension's base and wrap callbacks are now typed against the concrete generated Durable Object constructor (GeneratedDurableObjectClass<TBase, TEnv>, a constructor producing TBase & DurableObject<TEnv>), replacing the unreleased generic type-preserving wrap signature. Every

[!IMPORTANT]
✂ PR body was truncated to here.

@mend-for-github-com mend-for-github-com Bot added the security fix Security fix generated by Mend label Oct 23, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

security fix Security fix generated by Mend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants