Vercel Logo

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:

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 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:

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?

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.

Why eve info shows zero skills

Run eve info and the skill count is 0, as expected. A dynamic skill doesn't exist until a session resolves it. The file contains a resolver that may produce a skill depending on the caller.

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 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?

supported.