The prompt
The blocks the model reads, which are cached, and render(): the only part that changes between turns.
The model reads four blocks, in the same order on every turn, with the conversation between the
third and the fourth. Your class writes the docstring at the top and the last block, render().
The rest is Pinecall's.
How it works#
── identity (static) ── the class docstring · <rules> · <protocols> · <channel>
── knowledge (static) ── what the agent knows by heart, from its settings
── tools (static) ── every tool the class declares: its name and docstring
── history ── the conversation so far, and what each lookup returned
── view (dynamic) ── what render() says about this turn
The three static blocks come first and never read the state, so they are the same on every turn of a call and the model's provider can cache them. The history in between is the runtime's and grows with each turn. The view comes last, after the history, and is rendered again whenever the state changes.
render(): the state turned into what to do now.
identity#
Your class docstring, then three tagged sections the framework writes for every agent, in English:
| section | says |
|---|---|
<rules> |
invent nothing that did not come from a tool or the knowledge; one question per turn; answer in the language the caller speaks |
<protocols> |
to act, call a tool; before an irreversible action, read it back and wait for an explicit yes; offer a person when you cannot help |
<channel> |
how to write for this call (below) |
<channel> depends on the channel and on whether the call is spoken or written:
| the call | <channel> begins |
|---|---|
| a phone call, or a web call by voice | You are on a phone call. Everything you write is read aloud by a voice… |
| a web call in text | You are in a written chat on a website. Markdown is fine… |
| You are on WhatsApp. Use its formatting… |
A call never changes its channel, so this block stays cached all call long. A class that writes its
own formatting rules turns it off with channelRules = false.
The view#
In TypeScript, render() returns JSX that renders to text, with this as the state: the
fields, the getters, this.call and this.remembers(…). In Python and Ruby the view is a template
file beside the class, with the same things in scope.
override render() {
return (
<>
{this.stage === "identify" && <p>Greet the caller and ask for their name and phone.</p>}
{this.patient && <p>You are talking to {this.patient.name}, already on file.</p>}
{this.remembers("usual doctor") && <p>Offer their usual doctor's slots first.</p>}
</>
);
}
{% if stage == "identify" %}
Greet the caller and ask for their name and phone.
{% endif %}
{% if patient %}
You are talking to {{ patient.name }}, already on file.
{% endif %}
{% if remembers("usual doctor") %}
Offer their usual doctor's slots first.
{% endif %}
The view is a Jinja template beside the class, views/<slug>.jinja, with every state field in
scope by name and remembers("…").
<% if stage == :identify -%>
Greet the caller and ask for their name and phone.
<% end -%>
<% if patient -%>
You are talking to <%= patient.name %>, already on file.
<% end -%>
<% if remembers?("usual doctor") -%>
Offer their usual doctor's slots first.
<% end -%>
The view is an ERB template beside the class, views/<slug>.erb, with every state field in scope
by name and remembers?("…").
A class with no render() sends an empty view. Its prompt is its docstring and its tools.
The view is rendered again after every state change, when the caller's turn lands, when memory
recalls something, and when the call is claimed by a page. So this.call.history.last inside
render() is the turn being answered, not the one before it.
What memory and knowledge are not#
What memory recalls about a caller and what a knowledge base returns never enter the view. They reach the model as tool results in the history, where it can weigh them. Your view carries your authority; a fact from a document somebody else edited does not. See Prompt injection.
this.remembers("usual doctor") is true once memory has told this call something under those
words. It is a question about the call, not a way to put the fact into the prompt.
Print it#
No gateway, no key and no network:
pinecall prompt # the state a call opens in
pinecall prompt --state test/clinica-norte/goldens/identifica-al-paciente.json
pinecall prompt --channel web --medium text # the prompt a written chat gets
pinecall start --show-prompt # every agent's prompt, then exit
Each block is printed under its header, in the order it is sent, and beneath them the stage and the tools it shows.
Where a rule belongs#
The static blocks are read once and in general terms. The view is read in the turn it is about.
- A rule only in the docstring ("wait for an explicit yes before an irreversible action") does not tell the model that this tool is one. Say it in the view, in the stage where it applies.
- "The caller just named a slot" and "the caller just said yes" need opposite instructions. If the
view says only the first, the model reads the slot back forever. A field such as
proposedlets the view tell the two turns apart.
The state says where the conversation is. The view says what to do about it in this turn.
What it costs to get wrong#
After every render, Pinecall sends only the blocks whose text changed, and the tool list only when it changed. Rendering the same text twice costs nothing. A static block that changes mid-call breaks the cache for every turn after it.