Instructions and skills are templates over one documented context
Role instructions and skills could only say the same thing to every run, so guidance that depends
on the situation (who asked, whether there is a repository, who a role reports to) had to be
hardcoded into the opening prompt. We decided that they are Handlebars templates, rendered for each
run against one context: the organization, the role and its place in the org chart, the roles,
people (with their about), skills and projects, the task with its project, parent, creator,
requester and assignee, or the conversation with its person and subscriptions, and where the agent
works. The context is defined once, as a schema with a description on every field, in the template
module; the reference people and agents read is generated from it, and run data is built to it in
one place (run.opening.ts). That schema is the contract skills are written against, so it
changes with care: fields are added, not renamed.
Considered Options
- Keep growing the hardcoded prompt: rejected; every organization's way of working would have to be Mission Control's, and nothing could depend on who asked.
- A richer language (JavaScript, Liquid, Jinja-like): rejected; agents with
skills.managewrite templates too, and Handlebars runs no code of theirs and reads no object internals. - Lenient rendering, empty text for anything missing: rejected; a misspelled field would quietly vanish from a prompt. Templates render strictly instead, and a save is refused unless the template renders against sample data in which every field that may be empty is both empty and set, so a template that would fail for some run is caught when it is written.
- Partials (one skill including another): left out for now; they raise questions of order, cycles and access (a disabled or unattached skill) without a need yet.
Consequences
- The role instruction and ambient skills render when a session starts; a triggered skill renders
when the agent reads it with
get_skill. A continued session keeps what it was given, as before. - What an agent saw is in its transcript: the rendered opening prompt and every
get_skillresult. - Editing needs the template as written:
get_skillwithtemplate: truereturns it, and the management tools take and return templates unrendered. - Helper arguments are all read before a helper runs, so
anddoes not guard a field read from something empty; templates nest{{#if}}instead, and the reference says so. - A template that still fails at run start (data the samples did not foresee) fails the run with which template it was and why, rather than giving the agent a half-rendered prompt.
- Templates see no secrets, environment variables or tokens.
- A run works on a task or in a conversation (ADR 0011), so exactly one of
taskandconversationis set, andworkspaceis the conversation directory in a conversation. Makingtaskoptional is the one change that was not an addition: templates written while it was always set may read it unguarded. They keep rendering for tasks; the check on save renders against a conversation sample too, so such a template is caught when next edited. Until then a conversation with that role fails, saying which template and why in the conversation, rather than being given an instruction meant for a task. One instruction serves both, branching onconversationwhere they differ, rather than a role keeping a second instruction for conversations, which would repeat most of the first.
ADR 0034 adds agent conversations: neither task nor the human conversation is set there.
Validation also renders against that context. Templates must guard each subject explicitly,
using {{#if conversation}}…{{else if task}}…{{else}}…{{/if}} where they differ. A human
conversation's else branch cannot assume a task. Existing customized instructions remain
unchanged until their owner approves an edit; a failed live render names the incompatible template.