A Playbook Per Tier
A good front-desk advisor keeps the explanation simple for a first-time customer. With a shop regular who works on their own bikes, they can discuss torque specs and part numbers. The person behind the counter is the same; the playbook changes with the customer.
We could try to cram all of that into instructions.md, but that gets messy: every caller would carry every tier's rules on every turn, and a walk-in would somehow know about the pro discount. What we want is a procedure that loads only for the caller it applies to, and only when it's relevant.
A skill is a Markdown procedure the model loads on demand. Ours needs to be chosen per caller, so we'll make it a dynamic skill that eve resolves for whoever is calling.
Outcome
The dispatcher loads a member or pro playbook based on the caller's tier, while a walk-in gets the plain desk, none the wiser about either.
Hands-on exercise
Build the dynamic skill. Create agent/skills/shop-playbook.ts. A dynamic capability is a function: eve runs it before every model call and it returns a capability, or null for none. Ours reads the current caller's tier and hands back the matching playbook as a skill:
import { defineDynamic, defineSkill } from "eve/skills";
const PLAYBOOKS: Record<string, { title: string; markdown: string }> = {
pro: {
title: "Pro / shop-mechanic playbook",
markdown:
"This caller is a pro mechanic. Talk torque specs and part numbers freely, " +
"skip the absolute basics, and recommend a Full Overhaul when the symptoms " +
"justify it. Reference /workspace/torque-specs.md for fastener values.",
},
member: {
title: "Member playbook",
markdown:
"This caller is a shop member. Mention the 10% labor discount on bookings, " +
"and offer a free loaner bike whenever a job will keep their bike overnight.",
},
};
export default defineDynamic((ctx) => {
const tier = ctx.session.auth.current?.attributes.tier;
const key = Array.isArray(tier) ? tier[0] : tier;
const playbook = typeof key === "string" ? PLAYBOOKS[key] : undefined;
if (!playbook) return null; // no tier → no playbook, just the standard desk
return defineSkill({
description:
`Use when serving a ${key}-tier customer. ` +
`Contains that tier's standing conventions.`,
content: `# ${playbook.title}\n\n${playbook.markdown}`,
});
});defineSkill takes the procedure as content, and eve turns it into the skill's SKILL.md. Because the function runs before every model call, the playbook always matches the caller eve has on record right now. Reading ctx.session.auth is cheap, so there's nothing to cache here. If a resolver ever fetches something slow, ctx.memo(ctx.session.id, fn) keeps the answer fixed for the session.
The pro playbook points at /workspace/torque-specs.md, a reference file the agent can open with its file tools when a pro asks for fastener values. Create it so there's something to read:
# Spoke & Mirror torque reference
Fastener values in newton-meters (Nm). When in doubt, start at the low end and check.
| Fastener | Torque (Nm) |
| ------------------ | ----------- |
| Disc rotor bolts | 6 |
| Stem faceplate | 5 |
| Seatpost clamp | 5 |
| Crank arm (2-bolt) | 12 |
| Cassette lockring | 40 |
| Pedal into crank | 35 |Anything under agent/sandbox/workspace/ is seeded into the agent's sandbox at /workspace/ when a session boots. Section 5 returns to the sandbox for deployment.
Give yourself a way to test it. Here's the catch: the skill reads tier from the caller's authenticated identity, and right now nothing sets one. In the TUI you'll always get the plain desk. So add a small testing door in agent/channels/eve.ts that stamps a tier from a header:
import { eveChannel } from "eve/channels/eve";
import { localDev, vercelOidc, type AuthFn } from "eve/channels/auth";
// TEMPORARY testing door: read a tier from a header so we can exercise the
// per-tier playbook locally. We replace this with a real auth policy in 4.3.
const demoTierAuth: AuthFn<Request> = async (request) => {
const tier = request.headers.get("x-shop-tier");
if (!tier) return null;
return {
attributes: { tier },
principalType: "user",
principalId: "demo-customer",
authenticator: "demo",
};
};
export default eveChannel({
auth: [demoTierAuth, vercelOidc(), localDev()],
});Try It
In the plain TUI, no tier is set, so it's the standard desk:
you › my rear shifting is sloppy, what do you suggest?
dispatcher › Sounds like the derailleur needs adjusting. A Basic Tune-Up ($65)
covers that. Want me to check openings?Now call over HTTP as a pro by sending the header. The first request returns an accepted session handle, so extract its ID and stream the answer:
sid=$(curl -s -X POST http://127.0.0.1:2000/eve/v1/session \
-H 'content-type: application/json' \
-H 'x-shop-tier: pro' \
-d '{"message":"rear shifting is sloppy, what do you suggest?"}' \
| sed -E 's/.*"sessionId":"([^"]+)".*/\1/')
curl -sN "http://127.0.0.1:2000/eve/v1/session/$sid/stream"The stream prints NDJSON commit lines. Pull the reply text out of the content.completed facts and it reads something like this:
dispatcher › Indexing's drifted, most likely. I'd check the B-tension and hanger
alignment before anything else. If the cassette's worn past spec it's a Full
Overhaul; torque the cassette lockring to spec (see the torque sheet). Want the
overhaul booked?The pro response includes torque specs, part-level detail, and an overhaul recommendation because the resolver loaded the pro playbook. Try x-shop-tier: member to hear about the labor discount and loaner bike instead.
Plain desk no matter what header you send? Two checks: the resolver must read ctx.session.auth.current?.attributes.tier (not the message), and your eve.ts must list demoTierAuth in the auth array. If eve info reports a discovery error, make sure shop-playbook.ts default-exports the defineDynamic(...) result and passes a function, not an object. A type error on markdown means the skill text needs to move to content.
Done-When
agent/skills/shop-playbook.tsdefault-exportsdefineDynamic((ctx) => ...), returning adefineSkill({ content, description })ornull.- With no tier, the resolver returns
nulland the agent runs the plain desk. - Sending
x-shop-tier: pro(ormember) over HTTP produces the matching playbook's behavior. - You can explain why the tier must come from auth, not from the user's message.
Solution
The full shop-playbook.ts and the demo eve.ts are both shown above. The shape worth remembering: defineDynamic takes a function that reads the session and returns a capability or null, and the same pattern also drives dynamic tools and instructions. eve calls it before every model call, so wrap anything slow in ctx.memo when the answer should hold for the whole session.
One loose end remains: this eve.ts trusts a header anyone could send. That is useful for a local test and unsafe for production. Section 4 adds a web dashboard and Slack, then replaces this file with an auth policy that derives tier from a verified caller.
Was this helpful?