Prompt tags & @render
The tags a view is written with, @render, and turning the channel's rules off.
In TypeScript a view is written in JSX, and it renders to plain text for the model, never to a page.
A handful of tags shape that text, and @render moves a long view out of the class. In Python and
Ruby the view is a template file beside the class — Jinja, or ERB — with no tags of its own:
the Python view, the Ruby view.
How the text is built#
| you write | the model reads |
|---|---|
<p>Ask for their name.</p> |
one paragraph. Line breaks and indentation inside it become single spaces |
| two siblings | two paragraphs, one blank line apart |
null, false, undefined |
nothing |
| a string or a number | itself |
<>…</> |
its children, with nothing added |
| any other lowercase tag | its children |
So {condition && <p>…</p>} is all the control flow a view needs. render() may also return a plain
string, for an agent with one sentence to say.
The tags#
Import them from @pinecall/agents:
| tag | renders as |
|---|---|
<Rules> |
its children one per line, inside <rules> … </rules> |
<Protocols> |
its children one per line, inside <protocols> … </protocols> |
<Rule> |
one line starting with - |
<Section title="…"> |
## title, a blank line, then its children as paragraphs |
<Example> |
its children inside <example> … </example>, so the model reads an example and not an instruction |
An empty <Rules>, <Protocols> or <Example> renders nothing at all.
import { Agent, Example, Rule, Rules, Section } from "@pinecall/agents";
override render() {
return (
<>
<Rules>
<Rule>Offer at most two slots.</Rule>
<Rule>Say the day before the time.</Rule>
</Rules>
{this.slots.length > 0 && (
<Section title="Free slots">
{this.slots.map((slot) => <p>{slot.id}: {slot.when} with {slot.professional}</p>)}
</Section>
)}
<Example>Tuesday at ten with Dr. Vidal, or Wednesday at four with Dr. Cabrera?</Example>
</>
);
}
With two slots on the table, the model reads:
<rules>
- Offer at most two slots.
- Say the day before the time.
</rules>
## Free slots
s1: Tuesday at 10:00 with Dr. Vidal
s2: Wednesday at 16:00 with Dr. Cabrera
<example>
Tuesday at ten with Dr. Vidal, or Wednesday at four with Dr. Cabrera?
</example>
A view that has grown past one screen holds two ideas. Give one of them a private method and call
it from the JSX: {this.slots.length > 0 && this.hoursOnTheTable()}. It is a method like any other,
and the state is still this.
@render: the view as a function#
@render(Prompt) uses a function beside the class as its view. The function's props are the
instance itself, so there is nothing to pass and nothing extra to type:
import { Agent, render, type Stages } from "@pinecall/agents";
/** The prompt as a function of the agent. */
const SupportPrompt = ({ stage, customer }: Support) => (
<>
{stage === "identify" && <p>Ask for the order number. Nothing else until you have it.</p>}
{customer && <p>You are talking to {customer.name}.</p>}
</>
);
@render(SupportPrompt)
export default class Support extends Agent {
stage: Stages<"identify" | "resolve"> = "identify";
customer?: Customer | undefined;
}
It is exactly render() { return SupportPrompt(this); }, and produces the same text.
- A getter destructures like a field.
- A method does not: pulled out of the instance, it loses its
this. Call it on the agent:(clinic: ClinicaNorte) => clinic.remembers("usual doctor") && <p>…</p>.
A class writes one spelling or the other. Both is refused when the class is defined:
Support declares both @render(SupportPrompt) and a render() method; two ways to answer one question — keep one
The framework's blocks#
The identity block's <rules> and <protocols> are the same for every agent, and a class cannot
replace them. The one block you can leave out is <channel>, for a class that writes its own
formatting rules:
export default class Support extends Agent {
channelRules = false;
}
this.call.medium is still there for a view that branches on voice or text.
The tags of the side panel (
Panel,Rows,Tableand the rest, from@pinecall/agents/panels) are not prompt tags. Used insiderender(), one throws:<Panel> belongs to a view, not to a prompt: render() writes text, view() draws a panel. The panel is The side panel.