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 resolves at the start of each session.
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 resolver: it runs on a session event and returns a capability, or null for none. Ours runs on session.started, reads the 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({
events: {
"session.started": async (_event, ctx) => {
const tier = ctx.session.auth.current?.attributes.tier;
const key = Array.isArray(tier) ? tier[0] : tier;
const playbook = key ? 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.`,
markdown: `# ${playbook.title}\n\n${playbook.markdown}`,
});
},
},
});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, localDev(), vercelOidc()],
});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, and watch the tone change:
curl -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?"}'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.
Done-When
agent/skills/shop-playbook.tsdefault-exportsdefineDynamicresolving onsession.started.- 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 is a resolver keyed on a session event, and the same pattern also drives dynamic tools and instructions. Resolve on session.started for a per-session decision, then return a capability or null.
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?