State & visibility
Which fields are the state a call opens in, and who may see each one.
The state is what the agent knows about this call: the patient it found, the slots it offered, the stage it is in. It is the instance's fields, and every change to it is recorded with who made it.
What is state?#
Every own field of the instance is state, and so is every getter. Pinecall keeps a copy of it on the call: the view is rendered from it, the console shows it beside the conversation, the call's log records each change, and a golden starts from it.
| on the class | state? |
|---|---|
a field: slots: Slot[] = [] |
yes |
a getter: get identified() { return !!this.patient } |
yes, read every time the state is copied |
a method: private agenda() |
no |
channelRules, phone, whatsapp, web |
no: these configure the class |
That is why a collaborator such as a calendar client belongs in a method and not in a getter: a getter ends up in the prompt's state, the log and every golden.
Who writes it#
Tools and hooks are the only writers. A write records its author:
| written in | author |
|---|---|
| a tool | the tool's name: findPatient |
onCall, onEnd |
hook:onCall, hook:onEnd |
onEvent for an event |
event:<name> |
Any other assignment throws UnauthoredWrite. Field initializers are not writes: they are the
state every call starts from.
The state a call opens in#
The class's own values#
A fresh instance per call, with its field initializers.
What onCall writes#
The hook runs before the model reads anything. See Hooks & events.
The state the call was opened with#
When whoever opened the call asked for a state (a golden's state, for example), those fields are
written over the rest. Then the first prompt is sent.
Who may see each field#
By default a field is tenant: your org sees it, and nobody else. Two other answers exist:
| visibility | means |
|---|---|
tenant |
the default. Your org's people and keys read it |
public |
also readable by a participant's browser in the call: a widget or your own page |
pii |
masked in the call's log when it is written |
Declare it with @state, field by field:
import { Agent, state, type Stages } from "@pinecall/agents";
export default class Support extends Agent {
@state stage: Stages<"identify" | "resolve"> = "identify";
@state({ pii: true }) customer?: Customer | undefined;
@state({ visibility: "public" }) total = 0;
}
from typing import Literal
from pinecall import Agent, state
class Support(Agent):
stage: Literal["identify", "resolve"] = "identify"
customer: dict | None = state(None, pii=True)
total: int = state(0, visibility="public")
@property
def identified(self) -> bool: # derived: in every snapshot, assigned by nobody
return self.customer is not None
Every annotated field is state. state(default, pii=True) or state(default, visibility="public")
says who may see one; a public @property is a derived field.
class Support < Pinecall::Agent
stage :identify, :resolve
state :customer, visibility: :pii
state :total, 0, visibility: :public
state(:identified) { !customer.nil? } # derived: read like any field, assigned by nobody
end
state declares a field, its initial value and its visibility; a block makes a derived field.
In TypeScript, @state({ pii: true }) is short for @state({ visibility: "pii" }). Writing both with
different answers is refused when the class is defined. A class that prefers a map writes
static visibility = { customer: "pii", total: "public" } as const; it wins over @state and says
only who may see a field, never whether it is state.
Decorating one field decides them all. A class with no
@stateat all has every field as state. A class with any@statefield means "these, and nothing else": an undecorated field is then scratch space, out of the prompt, the log, the console and goldens, and writable from anywhere. Getters stay state either way.
Optional fields#
A field a tool can empty again is declared patient?: Patient | undefined. The | undefined is
needed only if you turn on exactOptionalPropertyTypes; the project preset does not.