pinecallDocs
Key concepts / Prompt tags & @render

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.

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

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

TypeScript
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, Table and the rest, from @pinecall/agents/panels) are not prompt tags. Used inside render(), one throws: <Panel> belongs to a view, not to a prompt: render() writes text, view() draws a panel. The panel is The side panel.

Next steps#