Skip to content

feat: the Barista, a @yielded/agent agent with a Kitchen container tool - #17

Open
sam-goodwin wants to merge 8 commits into
chore/alchemy-beta-83from
feat/barista-agent
Open

sam-goodwin wants to merge 8 commits into
chore/alchemy-beta-83from
feat/barista-agent

Conversation

@sam-goodwin

Copy link
Copy Markdown
Collaborator

Stacked on #16 (Alchemy beta.83).

The Room's replies now come from the Barista, a @yielded/agent agent on Workers AI. Its menu tool calls the Kitchen, an Effect container that serves tools over Alchemy's schemaless RPC.

Kitchen: tools served over container RPC

// kitchen/index.ts
export class Kitchen extends Cloudflare.Container<
  Kitchen,
  { menu: () => Effect.Effect<ReadonlyArray<MenuItem>> }
>()("Kitchen") {}

// kitchen/main.ts: the container's process, provided to the stacks
export default Kitchen.make(
  { main: import.meta.url, image: "oven/bun:latest" },
  Effect.succeed({ menu: () => Effect.succeed(MENU) }),
);

The tool calls the Kitchen; the schema lives at the model boundary

// room/menu.ts
export const Menu = Tool.make("menu", {
  description: "Today's café menu: every drink and pastry with its price in US dollars.",
  success: Schema.Array(Schema.Struct({ name: Schema.String, price: Schema.Number })),
});

export const menu = (kitchen: Kitchen) =>
  Effect.gen(function* () {
    const runtime = yield* Effect.context<RuntimeContext>();
    const start = kitchen.start({ enableInternet: false });
    return Effect.fn("Kitchen.menu")(
      function* () {
        yield* start;
        return yield* kitchen.menu();
      },
      Effect.orDie,
      Effect.provideContext(runtime),
    );
  });

The Barista

// room/barista.ts
const BaristaAgent = Agent.make("barista", {
  input: Schema.String,
  output: Output.text(Schema.String),
  instructions: "You are a friendly barista in a group chat at a café. … call the menu tool …",
  toolkit: Toolkit.make(Menu),
  policy: { maxTurns: 4, maxToolCalls: 2, maxDuration: "60 seconds" },
});

export class Barista extends Context.Service<
  Barista,
  { readonly reply: (conversation: string) => Stream.Stream<string, BaristaError> }
>()("Barista") {
  static readonly layer = (kitchen: Kitchen, model: Layer.Layer<LanguageModel | ProviderName | ModelName, never, RuntimeContext>) => …
}

Barista is a service so the library's internal types don't leak into the Room's declaration emit.

Room

+    const kitchen = yield* Kitchen;
     const ai = yield* Cloudflare.Workers.AI();
+    const model = Model.make("workers-ai", MODEL, ai.model({ model: MODEL, parameters: { maxTokens: 512 } }));
 …
-        Layer.provide(languageModel),
+        Layer.provide(Barista.layer(kitchen, model)),
 // room/reply.ts
-    yield* model.streamText({ prompt }).pipe(
-      Stream.filter((part) => part.type === "text-delta"),
-      Stream.map((part) => part.delta),
+    yield* barista.reply(transcript(conversation)).pipe(

Alchemy patch (to upstream)

patches/alchemy@2.0.0-beta.83.patch fixes the Workers AI LanguageModel for agents:

 const tryParseJsonArgs = (raw: unknown): unknown => {
+  // Workers AI sends `""` as the arguments of a call to a parameterless tool.
+  if (raw === "" || raw == null) return {};
     outputTokens: {
       total: completionTokens,
-      text: 0,
-      reasoning: 0,
+      text: Math.max(0, completionTokens - reasoningTokens),
+      reasoning: reasoningTokens,
     },

Dependencies

-  effect: 4.0.0
+  effect: 4.0.5          # @yielded/agent needs ^4.0.3 (and @effect/* 4.0.5 together)
+  "@yielded/agent": 0.1.0-beta.183
+peerDependencyRules:
+  allowedVersions:
+    effect: "4"          # Foldkit pins its effect peer to 4.0.0

alchemy dev now needs Docker for the Kitchen.

Tests

// backend/test/api.test.ts and frontend/test/chat.test.ts
"How much is a flat white?" → the assistant's reply contains "4.50" (only the Kitchen's menu knows it)
ALCHEMY_DEV=1 pnpm --filter @cafe/backend test      # 7/7
pnpm --filter @cafe/backend test                    # 7/7 (live, 3 runs)
ALCHEMY_DEV=1 pnpm test:e2e / pnpm test:e2e         # 6/6, 6/6

🤖 Generated with Claude Code

sam-goodwin and others added 3 commits October 15, 2026 12:21
@yielded/agent needs effect ^4.0.3; Foldkit pins its effect peer to 4.0.0
exactly, so peerDependencyRules allows any effect 4. @effect/* and @yielded/*
join the minimumReleaseAge exemptions alongside effect.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Patches alchemy@2.0.0-beta.83's Workers AI LanguageModel (to upstream):
- a call to a parameterless tool arrives with arguments "", which was passed
  through as a string; decode it as {}.
- output usage reported text: 0, reasoning: 0 alongside the real total, which
  @yielded/agent rejects as contradictory; report text = total - reasoning.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Room's replies now come from a @yielded/agent agent on Workers AI. Its
menu tool calls the Kitchen, an Effect container serving the menu over
Alchemy's RPC, bound and started by the Room.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@sam-goodwin
sam-goodwin added this pull request to stack #18 October 15, 2026 19:45
sam-goodwin and others added 5 commits October 15, 2026 12:51
The demo deploys and runs the Kitchen container, whose image builds with
Docker; macOS runners can't run it. tcut renders through Chrome on Linux.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…quest

On a fresh deploy the Kitchen can fail to start at first, and the menu tool
turned that into a defect that 500'd sendPrompt. Retry start with backoff,
and report any reply defect as ResponseFailed with its cause.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Right after a deploy, Cloudflare can have no container instance for the
Durable Object yet ("There is no container instance that can be provided to
this Durable Object, try again later"): start succeeds but the container
exits before its port opens. Retry start + menu (safe to repeat) for ~45s,
and give the barista's run 90 seconds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CI saw a bare 500 from /health just after a fresh deploy with the Kitchen.
Catch defects too and include the cause, and show the body in the test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A fresh deploy now builds and pushes the Kitchen's container image, which
pushed beforeAll past vitest's 120s hook timeout on CI.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Preview deployed

App: https://cafe-web-pr-17-e4ohbuapg62bvdhh.testing-2b2.workers.dev

Built from commit 0780050.


This comment updates automatically with each push.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant