Browse documentation

Core

Durable threads

Create, stream, reconnect, resume input, cancel, and inspect a Flary run without keeping the browser request alive.

A durable thread is the unit of conversation and execution. It has a stable identity, a tenant binding, a workspace or project scope, an active mode, a provider lineage, and a resumable event cursor.

Flue owns the canonical transcript and execution stream in the thread Durable Object. Flary adds trusted context, run admission, normalized events, tool journaling, approvals, usage, and recovery metadata.

The request lifecycle

POST /runs
  → validate public input
  → resolve trusted tenant and agent context
  → admit one idempotent run
  → return run_id immediately

GET /runs/:runId/events
  → replay events after the supplied sequence
  → stream new events and heartbeats

browser disconnects
  → Flue continues the admitted work
  → client reconnects with Last-Event-ID

The client must treat the returned run ID and sequence as durable state. It must not create a second run only because an event stream disconnected.

Create and observe a run

import { createFlaryRunClient } from "flary/client";

const runs = createFlaryRunClient({
  baseUrl: "/v1/agents/support",
  headers: () => ({
    authorization: "Bearer " + getProductSessionToken(),
  }),
});

const run = await runs.create({
  requestId: crypto.randomUUID(),
  channelId: "ticket_42",
  input: {
    customer: { name: "Ada" },
    question: "How do I change my plan?",
  },
  idempotencyKey: "ticket_42:turn:7",
});

for await (const event of runs.observe(run.runId)) {
  renderEvent(event);
}

The client validates the request and every response with Flary’s Zod schemas. The host’s authentication layer decides whether the caller can use this agent and channel.

Reconnect after a disconnect

Persist the last sequence in the product UI or server session. A reconnect starts at that sequence:

let lastSequence = 0;

async function stream(runId: string) {
  for await (const event of runs.observe(runId, {
    afterSequence: lastSequence,
  })) {
    lastSequence = event.sequence;
    renderEvent(event);
  }
}

try {
  await stream(run.runId);
} catch {
  await new Promise((resolve) => setTimeout(resolve, 1_000));
  await stream(run.runId);
}

The HTTP API also accepts Last-Event-ID. The server replays only events after that cursor, then waits for new events. Event IDs and sequence numbers are monotonic for one run.

User input and cancellation

An agent can enter waiting state when it needs a decision, a missing value, or an approval. The host UI submits input to the run:

await runs.input(run.runId, {
  input: {
    answer: "Use the Pro plan",
  },
  idempotencyKey: "input:run_123:choice_1",
});

await runs.cancel(run.runId, {
  reason: "The customer closed the ticket",
  idempotencyKey: "cancel:run_123",
});

Use the dedicated thread client when the application uses thread bindings, mode changes, approvals, Recall, and user-input requests together. The run client remains useful for a narrow HTTP integration.

Idempotency and uncertain outcomes

Use a stable idempotency key for every submission and every state-changing tool call. A retry with the same key returns the admitted result instead of starting a second operation.

If the runtime stops after an external write starts but before the result is recorded, Flary records an uncertain outcome. It does not repeat the write. The host can inspect the journal, ask for confirmation, or run a provider specific reconciliation tool.

What survives a restart

State Source of truth
Visible messages and execution stream Flue thread Durable Object
Thread binding and active mode ID Flary thread SQLite
Run admission and idempotency D1 run repository
Replay projection and usage Flary normalized event projection
Large workspace blobs Tenant-scoped R2 object
Provider-private continuation metadata Encrypted host storage

JavaScript variables, open promises, and an HTTP connection are not durable. The runtime persists before it starts work that must survive eviction.

Thread scope

The host supplies a TrustedRunContext. It contains tenant, application, project, agent, revision, identity, roles, and scopes. The public JSON body does not contain these values.

{
  tenantId: "org_123",
  applicationId: "support",
  projectId: "help-center",
  agentId: "support",
  revisionId: "agent_revision_7",
  identity: { id: "user_42", kind: "user", version: "1" },
  roles: ["agent"],
  scopes: ["support:run"]
}

Resolve the context for every run and verify it against the immutable thread binding. A thread cannot change tenant, application, project, or identity scope by sending a different public request.