Vercel Logo

A Playbook Per Tier

Walk into a good shop as a first-timer and the front-desk advisor keeps it simple: here's what's wrong, here's the fix, here's the cost. Walk in as a shop regular who works on their own bikes and the conversation changes, they'll talk torque specs and part numbers and skip the hand-holding. Same shop, same person behind the counter, different playbook depending on who's asking.

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.

That's a skill: a markdown procedure the model pulls in on demand instead of carrying always. And we want it chosen per caller, so it's a dynamic skill, resolved at the start of each session from who that caller is.

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:

agent/skills/shop-playbook.ts
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:

agent/sandbox/workspace/torque-specs.md
# 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 you put under agent/sandbox/workspace/ is seeded into the agent's sandbox at /workspace/ when a session boots. That's a first taste of the sandbox; more in Section 5.

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:

agent/channels/eve.ts
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()],
});
Keep `localDev()` in the chain

The eve channel fails closed: if no entry in the auth array admits a request, the route returns Authorization is required for this route. localDev() is the entry that admits the local TUI. If you merge demoTierAuth into your existing eve.ts by hand instead of replacing the file, don't drop localDev(), or the TUI will lock you out on the very next message. demoTierAuth only matches requests that carry an x-shop-tier header, which the TUI never sends.

The tier comes from auth, not from the chat

This is the whole point of keying on ctx.session.auth. A walk-in can't talk their way into the pro discount by saying "I'm a pro", the tier is a claim the door stamps, not text the model reads. The demo door fakes that claim from a header so you can test. In 4.3 you'll swap it for auth that earns the claim honestly.

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?

Same question, completely different desk: torque specs, part-level talk, the overhaul recommendation, all from the pro playbook the resolver loaded because the caller's tier was pro. Try x-shop-tier: member and you'll hear about the labor discount and a loaner bike instead.

Why eve info shows zero skills

Run eve info and the skill count is 0. That's correct: a dynamic skill doesn't exist until a session resolves it. There's no static skill on disk to advertise, only a resolver that might produce one, depending on who's calling. The capability is conditional by design.

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.ts default-exports defineDynamic resolving on session.started.
  • With no tier, the resolver returns null and the agent runs the plain desk.
  • Sending x-shop-tier: pro (or member) 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 resolver pattern drives dynamic tools and instructions too, not just skills. Resolve on session.started for a per-session decision, return a capability or null.

One loose end: that eve.ts is a stand-in. It trusts a header anyone could send, which is fine on your laptop and dangerous in production. We've kept it deliberately crude so the focus stayed on the skill. In Section 4, we put real doors on the agent, a web dashboard, Slack, and a proper auth policy, and in 4.3, this exact file grows up into one that stamps tier from a caller who actually proved who they are.

Was this helpful?

supported.