pinecallDocs
Key concepts / State & visibility

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:

TypeScript
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;
}
Python
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.

Ruby
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 @state at all has every field as state. A class with any @state field 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.

Next steps#