Skip to documentation
Documentation

flue

Connect a Flue 2.x application to Keiki's external harness.

Use @onkeiki/flue to run a Flue 2.x application as a Keiki external harness. Keiki keeps channel delivery, turn scheduling, completion, and trace history while Flue owns the agent runtime.

Install

Shell
npm install @onkeiki/flue @flue/runtime

Or run npx @onkeiki/cli init in your app to do this setup for you (see CLI).

The package supports Flue's Node and Cloudflare targets. Use a current Node release for local development. Cloudflare applications need nodejs_compat because the channel signing helper uses the Node-compatible crypto API.

Create the channel

Create a channel module with the Flue agent that should receive Keiki turns:

src/channels/keiki.ts
TypeScript
import { createKeikiChannel } from '@onkeiki/flue'
import { Assistant } from '../agents/assistant.ts'

export const channel = createKeikiChannel({
  agent: Assistant,
  signingSecret: process.env.KEIKI_SIGNING_SECRET,
})

Mount the returned router below your preferred path:

src/app.ts
TypeScript
app.route('/channels/keiki', channel.route())

Keiki then sends signed POST /channels/keiki/dispatch requests. The route returns 202 { "accepted": true } after Flue admits the delivery.

Configure the agent

Call useKeiki() from the Flue agent module:

TypeScript
import { useKeiki } from '@onkeiki/flue'

useKeiki()

The hook reads each delivered Keiki signal, mounts a turn-scoped MCP connection, requests typing, sends assistant messages, and settles the Keiki turn when the Flue submission finishes. It handles interim assistant text before a tool call as well as the final response.

Signing secret

Create the external-harness secret once:

text
POST /api/v1/developer/agents/:agentId/harness/signing-secret

Store the returned value in your secret manager as KEIKI_SIGNING_SECRET. Pass a string or a resolver instead when the secret is supplied by your runtime:

TypeScript
useKeiki({
  signingSecret: () => env.KEIKI_SIGNING_SECRET,
})

The channel route and the agent hook can run in different isolates. Flue signs a short-lived kts1 credential for each turn and each MCP or turn-API request; the credential is never placed in the signal body, attributes, initial data, or durable history.

For Cloudflare, bind the secret to the Worker and make it available to the Durable Object that runs the agent module. Pass the Durable Object's environment-backed resolver to createKeikiChannel() and useKeiki() rather than copying the secret into a message or instance ID.

For local workerd development, provide the secret in Wrangler's .dev.vars file; for deployed Workers, configure it as a Worker secret or binding. Keep .dev.vars out of version control. With nodejs_compat, the Worker and its Durable Objects can read their configured values through process.env.

Serving several agents

Use one resolver for the channel and the Flue hook:

TypeScript
const secrets: Record<string, string> = { agentA: secretA, agentB: secretB }

const channel = createKeikiChannel({
  agent: Assistant,
  signingSecret: ({ agentId }) => secrets[agentId],
})
useKeiki({ signingSecret: ({ agentId }) => secrets[agentId] })

Returning nothing rejects the push with 401 unknown_agent. The resolver runs before the signature is verified, so use a local map or cache rather than making a network call on every request. Flue calls it on every push and each signed turn API or MCP request.

Held-back turns and tools

Keiki can hold a turn back while still running it. The Flue agent receives the turn and can use mcp__keiki__* tools, but Keiki does not deliver the answer to the contact when the turn is held back.

The mounted MCP connection exposes Keiki's turn-scoped tools, including channel replies, files, reactions, and integrations. Its auth callback signs a fresh credential before every request, including retries.

Instructions and retry deduplication

Instructions are included in a <keiki_instructions> block by default:

TypeScript
createKeikiChannel({ agent: Assistant, instructions: false })

Set instructions: false to omit that block. Flue dispatches use the Keiki turn ID as Flue's native idempotency key, so a retried dispatch is admitted once while duplicate receipts still receive 202.

Mid-turn messages

With midTurnStrategy: 'append', an eligible push-mode turn dispatches each new batch to the same agent/conversation instance as a keiki.arrival signal. The hook keeps the active Keiki binding while Flue submissions for that instance are queued or running. Completion waits until the instance is idle, so a queued arrival can send its reply under the same Keiki turn and its sequence is included in consumedArrivals. Unconsumed batches, including arrivals rejected by an older adapter, return to Keiki's pending queue for a later turn. Relay-mode turns do not take arrivals.

An arrival submission that starts after the Keiki turn has reached its completion boundary does not attach to that turn or mark its sequence consumed. Its turn API calls can return 409 turn_closed; Keiki requeues the message, and a later turn answers it.

Cancellation

A cancellation request aborts the Flue instance addressed by the same agent and conversation identity. The platform records the resulting Keiki turn as cancelled; a late completion attempt receives 409 turn_closed.

v1 limits

  • Live token streaming is not bridged; completed assistant messages are sent.
  • Flue approval prompts and input requests are not bridged.
  • Relay mode is not shipped; use a push dispatch URL.