Skip to documentation
Documentation

Alchemy

Declare Keiki agents and channels as Alchemy resources, next to the infrastructure the agent runs on.

@onkeiki/alchemy provides Alchemy v2 resources and Effect layers for Keiki. An agent declared in your stack is attached to channels by providing it to channel layers. alchemy deploy creates or updates them, and alchemy destroy removes them.

Terminal
bun add -d @onkeiki/alchemy alchemy effect @effect/platform-bun

The provider reads an org API key from KEIKI_API_KEY and, optionally, a base URL from KEIKI_BASE_URL.

Agent on Cloudflare

KeikiCloudflare.Agent puts the agent on the external harness. Keiki pushes each turn to <worker url>/keiki, where the Worker runs the turn with createKeikiHandler.

TypeScript
import * as  from '@alchemy.run/cloudflare'
import * as  from '@onkeiki/alchemy'
import * as  from '@onkeiki/alchemy/cloudflare'
import * as  from 'alchemy'
import * as  from 'effect/Effect'
import * as  from 'effect/Layer'

export const  = .Worker('Api', { : './src/worker.ts' })
export const  = .Agent('Support', { :  })
export const  = .IMessage('Line', { : 1 }).pipe(.())

export default .Stack(
  'Acme',
  {
    : .(.providers(), .providers()),
    : .state(),
  },
  .(function* () {
    const  = yield* .KeikiAgent
    return { : .id }
  }).(.(.(, ))),
)

Set path to serve turns from a route other than /keiki. The Worker URL is read when the stack deploys, so a Worker that moves updates the agent's dispatch URL on the next deploy.

Agents on other hosts

Keiki.Agent accepts any public HTTPS dispatchUrl, or inbox: true to queue turns for polling. A new host adapter only has to produce that URL, as KeikiCloudflare.Agent does. Keiki.HostedAgent instead has Keiki run the model loop itself:

TypeScript
const  = Keiki.Agent('Support', { : 'https://agent.acme.dev/keiki' })
const  = Keiki.HostedAgent('Concierge', {
  : 'anthropic/claude-sonnet-4.6',
  : 'You are the front desk for Acme.',
})

Each agent is a partner deployment. Its external_ref defaults to <stack>-<stage>-<id>, so every stage gets its own agent. Pass externalRef to adopt an existing deployment.

Channels

LayerChannelProps
IMessageimessageoptional lines (1–10)
Telegramtelegrambot_token
Slackslackbot_token, signing_secret, optional client_id, client_secret
WhatsAppwhatsappaccess_token, phone_number_id, app_secret, verify_token, optional waba_id
Emailagentmailregion, mode, optional inbox_id, username, domain
Apiapioptional passthrough
A2Aa2anone

Props follow the partner deployment channel spec. Pass ref to attach several channels of one type to the same agent (not supported for Email). Changing a channel's credentials replaces it, because a deploy keeps an existing channel's credentials. A replacement detaches the old channel before attaching the new one. Raising IMessage's lines buys the missing lines in place; lowering it releases none.

Destroying a channel calls the single-channel detach endpoint. An IMessage channel releases every line the agent holds, so declare one per agent. To keep a line, set Alchemy's retain removal policy on the resource. Destroying the agent deletes the deployment, its channels and its lines.

If an iMessage line needs payment, the channel's status is needs_payment and its checkoutUrl holds the checkout link. Deploy again after paying.

An Alchemy example deploys a Cloudflare Worker agent with an iMessage line:

Terminal
cd integrations/alchemy
OPENROUTER_API_KEY=... KEIKI_API_KEY=... bun run deploy:example
bun run destroy:example