Skip to content

Agent API Reference

Core agent creation, invocation, identity, conversations, and the full reasoning engine — every public class across promptise.agent, promptise.engine, and promptise.conversations.

Building Agents

build_agent

promptise.agent.build_agent(*, servers=None, model, instructions=None, identity=None, trace_tools=False, cross_agents=None, include_broadcast=False, max_delegation_depth=DEFAULT_MAX_DELEGATION_DEPTH, delegation_timeout=None, sandbox=None, observer=None, observer_agent_id=None, observe=None, memory=None, memory_auto_store=False, memory_max_results=5, memory_min_score=0.0, memory_timeout=5.0, extra_tools=None, flow=None, conversation_store=None, conversation_max_messages=0, optimize_tools=None, guardrails=None, cache=None, approval=None, events=None, on_tool_progress=None, max_invocation_time=0, adaptive=None, context_engine=None, context_compaction=None, agent_pattern=None, pattern=None, graph_blocks=None, node_pool=None, max_agent_iterations=25, tool_summary='off', code_action=None, expose_resources=False, expose_prompts=False) async

Build an MCP-first agent and return a :class:PromptiseAgent.

Discovers tools from the configured MCP servers, converts them into LangChain tools, and builds an agent graph. The result is always a :class:PromptiseAgent with observability and memory as opt-in capabilities.

Parameters:

Name Type Description Default
servers Mapping[str, ServerSpec] | None

Mapping of server name to spec (HTTP/SSE recommended). Optional: leave it out (or pass {}) for an agent with no MCP tools, e.g. one that only uses extra_tools or just the model and its instructions.

None
model ModelLike

REQUIRED. A LangChain chat model instance, a provider id string such as "openai:gpt-5-mini" or "azure:my-deployment" (see :mod:promptise.models for every provider and alias), or a Runnable.

required
instructions str | Any | None

Optional system prompt. Defaults to the built-in DEFAULT_SYSTEM_PROMPT.

None
identity AgentIdentity | None

Optional :class:~promptise.identity.AgentIdentity giving the agent a traceable identity. When provided, every tool call and LLM turn the agent records is attributed to the agent's identifier — its agent_id handle, or, for an IdP-backed identity with no handle, the IdP subject — unless observer_agent_id is set explicitly. A verifiable identity is also presented to HTTP/SSE MCP servers that have no bearer of their own, so the server can authenticate and attribute the calling agent. The credential is requested for each server's audience on every request, so it is renewed before it expires (the session is reopened with the new one) and refreshed once if a server answers 401; a call the server still rejects fails with :class:~promptise.mcp.client.MCPConnectionRejectedError instead of hanging. If the identity cannot supply a credential (the IdP is unreachable), the request is not sent: it fails with :class:~promptise.mcp.client.MCPCredentialError rather than going out unauthenticated. The identity is exposed as :attr:PromptiseAgent.identity.

None
trace_tools bool

Print each tool invocation and result to stdout. Covers MCP tools, cross-agent tools, sandbox tools and extra_tools, and calls answered from an earlier result (ContextCompaction(reuse_results=...)), marked as reused.

False
cross_agents Mapping[str, CrossAgent] | None

Optional mapping of peer name → CrossAgent. Each peer is exposed as an ask_agent_<name> tool.

None
include_broadcast bool

Also add a broadcast_to_agents tool that asks several peers the same question in parallel. Off by default. A peer whose ask_agent_<name> tool needs approval is not reachable through it (unless broadcast_to_agents needs approval too).

False
max_delegation_depth int

Most nested delegations one request may make (this agent → peer → peer …). A deeper call, or a call back into a peer that is already working on the request, is refused with a :class:~promptise.cross_agent.DelegationError the model sees as the tool's error. Default 3.

DEFAULT_MAX_DELEGATION_DEPTH
delegation_timeout float | None

Seconds to wait for a peer's answer when its :class:CrossAgent sets no timeout of its own. None (default) = no limit beyond the peer's max_invocation_time.

None
memory Any | None

Optional :class:~promptise.memory.MemoryProvider. When provided, the agent automatically searches memory before each invocation and injects relevant context as a SystemMessage.

None
memory_auto_store bool

When True and memory is provided, automatically store each exchange in long-term memory after invocation. Defaults to False.

False
memory_max_results int

Maximum memories injected per invocation (the limit passed to the provider's search). Defaults to 5.

5
memory_min_score float

Drop search results scoring below this relevance (0.0--1.0). Defaults to 0.0 (keep all).

0.0
memory_timeout float

Seconds to wait for the memory search before answering without memory context, and for an auto-store write before returning. A write that takes longer is not cancelled: it finishes in the background, its outcome is logged, and :meth:PromptiseAgent.shutdown waits up to 30 seconds (or memory_timeout, if longer) for it. Defaults to 5.0. Raise it when the provider is slow to start (a ChromaProvider loads its embedding model on first use).

5.0
sandbox bool | dict[str, Any] | None

Optional sandbox configuration (True, a dict of :class:~promptise.sandbox.SandboxConfig fields, a SandboxConfig, or None). Unknown keys raise. The network defaults to "none". When the sandbox cannot be started (no docker package, Docker not running, network="restricted" not enforceable) build_agent raises instead of building an agent without it. agent_pattern="code-action" enables a sandbox automatically.

None
observer Any | None

Optional :class:ObservabilityCollector to reuse.

None
observer_agent_id str | None

Agent identifier for tool-event recording. Also the agent_id of every event the agent emits (see events).

None
observe bool | Any | None

Plug-and-play observability. Can be: - True: Enable with defaults (STANDARD level, HTML report). Writes an HTML report file to ./reports when the agent shuts down; pass an :class:ObservabilityConfig with other transporters or output_dir to change that. - :class:ObservabilityConfig: Full configuration. A config with level=ObserveLevel.OFF records nothing. - A dict of :class:ObservabilityConfig fields (as in a .superagent file's observability: section). - None/False: Disabled (default).

None
extra_tools list[BaseTool] | None

Optional additional :class:BaseTool instances to include alongside MCP-discovered tools. Used by the runtime for meta-tools (open mode) and custom agent-created tools.

None
expose_resources bool

Also give the agent the MCP servers' resources: adds a read_resource tool (its description lists the available resources and URI templates) and a list_resources tool. Nothing is added when no server serves resources.

False
expose_prompts bool

Also give the agent the MCP servers' prompts: adds a get_prompt tool (its description lists the prompts and their arguments) that returns the rendered prompt for the agent to follow. A generated tool is named mcp_<name> when an MCP server already has a tool with its name. For a server that receives caller tokens (HTTPServerSpec.forwards_caller_token), both send the invoking caller's bearer token, exactly like MCP tool calls; otherwise they use the credentials configured for the server.

False
flow Any | None

Optional :class:~promptise.prompts.flows.ConversationFlow instance, subclass, or zero-argument factory. The system prompt then evolves across turns with the flow's phase and active blocks. Each conversation gets its own flow: per session_id (see :meth:PromptiseAgent.chat), else per :class:CallerContext; an instance is deep-copied as a template. Calls with neither get a throwaway flow built from the messages passed in. The flow's prompt comes first, and instructions and the tool list are appended to it in the same system message; with no instructions, the default system prompt is not added.

None
conversation_store Any | None

Optional :class:~promptise.conversations.ConversationStore. When provided, the agent's :meth:~PromptiseAgent.chat method automatically loads and persists conversation history.

None
conversation_max_messages int

Maximum messages to keep per session when using the conversation store. 0 = unlimited. Oldest messages are dropped when the limit is reached.

0
context_engine Any | None

Optional :class:~promptise.ContextEngine. Its layers (including your add_layer content) are assembled with token budgeting on every call, and what the budget leaves after the instructions and tool definitions bounds the tool loop through context compaction. Its window is taken from the chat model's profile metadata unless you passed model_context_window.

None
context_compaction bool | int | Any | None

How long tool loops are compacted (:class:~promptise.engine.ContextCompaction). None or True: the defaults (compact after 6 tool results). False: never compact; the model always sees the full transcript. An int: compact after that many tool results. Applies to nodes with context_scope="auto" (the default ReAct pattern) and the threshold-free settings of "ledger" nodes; a node's own compaction= wins. Compaction never skips a tool call: a repeated call runs the tool, unless its tool is opted in with ContextCompaction(reuse_results=...).

None
approval Any | None

Human-in-the-loop approval. An :class:~promptise.approval.ApprovalPolicy gates the agent's own tool calls that match its patterns. Whatever is given — a policy, an :class:~promptise.approval.ApprovalHandler, or a callable — also answers server-side approval gates: MCP servers that ask the client to confirm a gated call through MCP elicitation (ElicitationApprover, MCPcast's default) reach this handler instead of being declined. A bare handler does only the latter. Without approval the agent declares no elicitation support and such servers deny the call. See :func:~promptise.approval.approval_elicitation_callback for the mapping and its fail-closed rules.

None
guardrails Any | None

Optional input/output scanner — a :class:~promptise.guardrails.PromptiseSecurityScanner or any object with check_input / check_output. True uses :meth:PromptiseSecurityScanner.default (call warmup() on your own scanner to load its model at startup instead of on the first message). When the scanner has scan_tool_results=True, every tool is wrapped so its result is scanned before the model reads it.

None
max_agent_iterations int

The most node executions one run may make (the engine's max_iterations). With the default ReAct graph each execution of the reason node is one model call plus the tool calls it asks for, so N allows at most N model calls; the reason node's own limit of 15 applies when N is higher. Other patterns count each node execution (planner, tools, reflection...). A run that reaches the limit after the model asked for tools, before it saw their results, raises :class:~promptise.engine.GraphIterationLimitError. Defaults to 25.

25
tool_summary str

Whether the agent's system prompt also lists the offered tools in an "Available tools" block, on top of the tool definitions the provider receives: "off" (default), "names" (one line of names) or "full" (parameters and the first 120 characters of each description; the behaviour before 1.3.4: 25 to 40 tokens a tool). Nodes of a custom graph that set PromptNode(tool_summary=...) keep theirs.

'off'
code_action Any | None

Options for agent_pattern="code-action": a :class:~promptise.engine.CodeActionConfig or a dict with exec_timeout (seconds the program may run, default 120), max_repairs (default 1) and max_tool_calls (default 50). Raises ValueError with any other pattern.

None
events Any | None

Optional :class:~promptise.events.EventNotifier. The agent emits invocation.*, tool.error, tool.slow, tool.progress (one per progress notification an MCP tool sends), guardrail.*, approval.* and cache.* events to it, whether or not observe is on, and stops it (delivering what is queued) in :meth:~PromptiseAgent.shutdown.

None
on_tool_progress Callable[[str, float, float | None, str | None], Any] | None

Optional callback for progress notifications MCP servers send while a tool call runs (ProgressReporter on a Promptise server), called as (tool_name, progress, total, message); sync or async. Progress is requested from servers only when this, events or trace_tools is set.

None
adaptive Any | None

Adaptive strategy (learning from failed tool calls): True, an :class:~promptise.strategy.AdaptiveStrategyConfig or a dict of its fields. Requires memory. Independent of observe. Failures and lessons are partitioned per caller by default (scope="per_user"); the manager is available as :attr:PromptiseAgent.adaptive_strategy.

None
agent_pattern str | Any | None

The reasoning graph: a :class:~promptise.engine.PromptGraph, or a pattern name from promptise.agent.AGENT_PATTERNS — "react" (the default), "managed", "code-action", "verify", "peoatr", "research", "autonomous", "deliberate" or "debate". Nodes of a custom graph created with inject_tools=True receive the discovered tools. An unknown name raises ValueError.

None
pattern str | Any | None

Deprecated alias for agent_pattern.

None

Returns:

Name Type Description
A PromptiseAgent
PromptiseAgent

Use agent.get_stats(), agent.generate_report(path), and

PromptiseAgent

await agent.shutdown() for observability and cleanup.

PromptiseAgent

promptise.agent.PromptiseAgent

The unified Promptise agent.

Always returned by :func:build_agent. Observability and memory are opt-in capabilities activated by constructor parameters — disabled features no-op or return sensible defaults, so callers never need to check what type they got back.

.. code-block:: python

# Simple — no observe, no memory
agent = await build_agent(servers=..., model="openai:gpt-5-mini")
result = await agent.ainvoke({"messages": [...]})
await agent.shutdown()

# With observability
agent = await build_agent(..., observe=True)
result = await agent.ainvoke({"messages": [...]})
stats = agent.get_stats()
agent.generate_report("report.html")
await agent.shutdown()

# With memory
agent = await build_agent(..., memory=InMemoryProvider())
result = await agent.ainvoke({"messages": [...]})  # auto-injects context
await agent.shutdown()

Attributes:

Name Type Description
collector

The :class:ObservabilityCollector holding recorded events, or None when observability is not enabled.

provider

The :class:~promptise.memory.MemoryProvider instance, or None when memory is not enabled.

ainvoke(input, config=None, *, caller=None, session_id=None, **kwargs) async

Invoke the agent asynchronously.

Parameters:

Name Type Description Default
input Any

LangGraph-style input dict with messages.

required
config dict[str, Any] | None

LangGraph config dict (callbacks, etc.).

None
session_id str | None

Optional conversation ID. With a conversation flow, each session (per caller) keeps its own flow state. :meth:chat passes its session_id here.

None
caller CallerContext | None

Optional :class:CallerContext with per-request identity. When provided, user_id is used for conversation ownership and the full context is available to guardrails and observability. When omitted, the ambient CallerContext (if one is already active in this task) is inherited, so cross-agent delegation keeps the original human principal: a peer invoked inside another agent's request scopes its cache, memory, guardrails, and conversations to the original user.

None

When memory is enabled, relevant context is searched and injected as a SystemMessage before the inner graph runs. When observability is enabled, a callback handler is attached to capture every LLM turn, tool call, and token count.

astream(input, config=None, *, caller=None, **kwargs) async

Stream the agent's run, one chunk per step of its graph.

Each chunk is {"messages": [...]}: the conversation once a node of the agent's graph has finished (LangGraph's stream_mode="values"). The last chunk is what :meth:ainvoke returns. Input guardrails run first (a violation raises), an answer is checked by the output guardrails before its chunk is yielded, and memory and observability work as in :meth:ainvoke. The semantic cache is not consulted.

For token-by-token output and tool activity, use :meth:astream_with_tools.

Parameters:

Name Type Description Default
input Any

Agent input (same format as ainvoke).

required
config dict[str, Any] | None

LangChain config dict (callbacks, etc.).

None
caller CallerContext | None

Optional :class:CallerContext for per-request identity. When omitted, the ambient caller is inherited, as in :meth:ainvoke.

None

astream_with_tools(input, config=None, *, caller=None, include_arguments=True, tool_display_names=None, **kwargs) async

Stream agent execution with tool visibility.

Yields structured :class:StreamEvent objects that show the complete agent reasoning process: tool calls, their results, LLM tokens, and the final response.

Parameters:

Name Type Description Default
input Any

Agent input (same format as ainvoke).

required
config dict[str, Any] | None

LangChain config dict.

None
caller CallerContext | None

Per-request identity for multi-user. When omitted, the ambient caller is inherited, as in :meth:ainvoke.

None
include_arguments bool

Include tool arguments in events.

True
tool_display_names dict[str, str] | None

Custom display names for tools.

None

Yields:

Type Description
AsyncIterator[Any]
AsyncIterator[Any]
AsyncIterator[Any]
async for event in agent.astream_with_tools(input, caller=caller):
    if event.type == "tool_start":
        print(f"🔧 {event.tool_display_name}...")
    elif event.type == "token":
        print(event.text, end="", flush=True)

invoke(input, config=None, *, caller=None, session_id=None, **kwargs)

Invoke the agent synchronously.

Memory injection requires async I/O. When a running event loop is detected (e.g. inside Jupyter), memory injection is skipped for the sync path — use :meth:ainvoke instead.

chat(message, *, session_id, user_id=None, caller=None, metadata=None, system_prompt=None) async

Send a message and get a response, with automatic session persistence.

This is the high-level API for building chat applications. The conversation store (if configured) handles loading history, persisting new messages, and session lifecycle automatically.

If no conversation store is configured, this still works — it just has no history beyond the current call.

Ownership enforcement: When user_id (or caller.user_id) is provided and the session already exists, the store checks that the session belongs to that user. If it belongs to a different user, :class:~promptise.conversations.SessionAccessDenied is raised.

Parameters:

Name Type Description Default
message str

The user's message text.

required
session_id str

Unique identifier for this conversation session. Use :func:~promptise.conversations.generate_session_id to create cryptographically secure, non-enumerable IDs.

required
user_id str | None

Optional user identifier. Shorthand for caller=CallerContext(user_id=...): it scopes session ownership and the rest of the invocation (per-user memory, cache, guardrails, observability). If both user_id and caller are provided, caller.user_id takes precedence; a caller without a user_id gets this one.

None
caller CallerContext | None

Optional :class:CallerContext with per-request identity. Carries user_id, bearer_token, roles, scopes, and metadata through the entire invocation.

None
metadata dict[str, Any] | None

Optional metadata to attach to the user message (e.g. source, IP, device).

None
system_prompt str | None

Optional per-call system prompt override.

None

Returns:

Type Description
str

The assistant's response text.

Raises:

Type Description
SessionAccessDenied

If user_id is provided and the session belongs to a different user.

RuntimeError

If the conversation store fails while the session's ownership is being checked. chat() fails closed here: it will not answer in a session whose owner it could not verify. Failures to load or save the history are logged and the call still answers.

from promptise.conversations import generate_session_id

agent = await build_agent(..., conversation_store=store)
sid = generate_session_id()
reply = await agent.chat("Hello!", session_id=sid, user_id="user-42")
reply = await agent.chat("What did I say?", session_id=sid, user_id="user-42")

get_session(session_id, *, user_id=None) async

Get session metadata.

Parameters:

Name Type Description Default
session_id str

The session to retrieve.

required
user_id str | None

When provided, verifies the session belongs to this user.

None

Returns:

Type Description
Any

Raises:

Type Description
RuntimeError

If no conversation store is configured.

SessionAccessDenied

If user_id is provided and the session belongs to a different user.

list_sessions(*, user_id=None, limit=50, offset=0) async

List conversation sessions from the configured store.

Parameters:

Name Type Description Default
user_id str | None

Filter by user. None returns all sessions.

None
limit int

Maximum sessions to return.

50
offset int

Pagination offset.

0

Returns:

Type Description
list[Any]

List of :class:~promptise.conversations.SessionInfo objects.

Raises:

Type Description
RuntimeError

If no conversation store is configured.

delete_session(session_id, *, user_id=None) async

Delete a conversation session and all its messages.

Parameters:

Name Type Description Default
session_id str

The session to delete.

required
user_id str | None

When provided, verifies the session belongs to this user before deleting.

None

Returns:

Type Description
bool

True if the session existed and was deleted.

Raises:

Type Description
RuntimeError

If no conversation store is configured.

SessionAccessDenied

If user_id is provided and the session belongs to a different user.

update_session(session_id, *, calling_user_id=None, user_id=..., title=..., metadata=...) async

Update session metadata (title, user_id, custom metadata).

Only provided fields are updated — omitted fields are left unchanged.

Parameters:

Name Type Description Default
session_id str

The session to update.

required
calling_user_id str | None

The user making this request. When provided, verifies ownership before applying changes.

None
user_id str | None

New user_id to assign (for ownership transfer).

...
title str | None

New title.

...
metadata dict[str, Any] | None

New metadata dict.

...

Raises:

Type Description
RuntimeError

If no conversation store is configured or the store does not support update_session.

SessionAccessDenied

If calling_user_id is provided and the session belongs to a different user.

shutdown() async

Flush transporters, close MCP connections, and release resources.

Always safe to call — no-ops for features that are not enabled. Call this when the agent is no longer needed.

get_stats()

Return aggregate observability statistics.

Returns an empty dict when observability is not enabled.

generate_report(path, title='Agent Observability Report')

Write an interactive HTML report of the recorded events to path.

The report is written to exactly path (missing parent directories are created; an existing file is overwritten).

Parameters:

Name Type Description Default
path str | Path

File to write, e.g. "report.html".

required
title str

Page title and heading of the report.

'Agent Observability Report'

Returns:

Type Description
str

The path the report was written to, as a string.

Raises:

Type Description
RuntimeError

When observability is not enabled.

OSError

When the file cannot be written.

CallerContext

promptise.agent.CallerContext dataclass

Identity and metadata for the caller of an agent invocation.

Pass this to ainvoke() or chat() to carry per-request identity through the entire invocation — guardrails, conversation ownership, observability, and the MCP servers the agent calls.

Attributes:

Name Type Description
user_id str | None

Unique user identifier. Used for conversation session ownership and observability correlation.

tenant_id str | None

Organisation / tenant the caller belongs to. When set, it becomes part of :attr:isolation_key, so every per-user isolation surface (semantic cache, memory search, conversation ownership) is scoped per tenant — two tenants with the same user_id can never see each other's data.

bearer_token str | None

JWT or OAuth token for the caller. Sent as Authorization: Bearer <token> on every tool call to an HTTP/SSE MCP server during this invocation, over a session opened for this token — never shared with another caller — so the server authenticates the user, not the credential the agent was built with. A server opts out with HTTPServerSpec(forward_caller_token=False); a server with an API key keeps it unless it sets forward_caller_token=True. stdio servers have no request headers and never receive it. Tool discovery at build time still uses the server spec's credentials.

roles set[str]

Caller's roles (e.g. {"admin", "analyst"}). Available for custom guardrail rules and logging.

scopes set[str]

OAuth scopes (e.g. {"read", "write"}).

metadata dict[str, Any]

Arbitrary key-value metadata (IP, user-agent, etc.).

caller = CallerContext(
    user_id="user-42",
    tenant_id="acme",
    bearer_token="eyJhbGciOiJIUzI1NiIs...",
    roles={"analyst"},
)
result = await agent.ainvoke(input, caller=caller)
reply = await agent.chat("Hello", session_id=sid, caller=caller)

isolation_key property

The identity key used for every per-user isolation surface.

"{tenant_id}::{user_id}" when a tenant is set, otherwise the plain user_id. This single derivation is what the semantic cache, memory scoping, and conversation ownership all key on, so tenant isolation is a structural invariant rather than a convention. None when user_id is unset.

get_current_caller

promptise.agent.get_current_caller()

Return the :class:CallerContext for the current invocation.

Safe to call from guardrails, context providers, observability handlers, or any code running inside an ainvoke() / chat() call. Returns None outside of an invocation.


Reasoning Graph Engine

PromptGraphEngine

promptise.engine.execution.PromptGraphEngine

Adaptive graph traversal engine.

Traverses a PromptGraph by executing nodes and following edges. The graph is copied per invocation so concurrent calls are safe and runtime mutations don't affect the original graph.

Parameters:

Name Type Description Default
graph PromptGraph

The graph to traverse.

required
model BaseChatModel

LangChain BaseChatModel for LLM calls.

required
max_iterations int

Maximum node executions per run: N allows exactly N (in the ReAct graph, N model calls).

50
max_node_iterations int

Engine-wide ceiling on how many times a single node can execute in one run (each tool-loop round counts). A node's own max_iterations applies when it is lower. When a node has used its budget, the engine does not run it again: it follows the node's "error" transition, else an __error__ node, else ends the run. A run that ends this way (or on max_iterations) in the middle of a tool loop raises :class:GraphIterationLimitError: the model never answered.

25
hooks list[Any] | None

List of hook instances for interception.

None
allow_self_modification bool

Allow the LLM to modify the graph via structured output _graph_action fields.

True
max_mutations_per_run int

Cap on graph mutations per run.

10
tools list[Any] | None

Tools for nodes with inject_tools=True (build_agent passes the MCP tools it discovered). Tools set on the graph's nodes are added to them.

None
compaction Any | None

:class:~promptise.engine.compaction.ContextCompaction settings for nodes that compact (context_scope="auto" / "ledger") and set none of their own.

None
tool_summary str | None

The "Available tools" block that PromptNode nodes with tools add to their system prompt when they set no tool_summary of their own: "off" (None, the default), "names" or "full". The model receives the tool definitions either way.

None
last_report property

The execution report from the last run (ainvoke() or a stream).

ainvoke(input, config=None, **kwargs) async

Run the graph to completion.

Returns {"messages": [...]} matching the LangGraph contract.

astream(input, config=None, **kwargs) async

Run the graph, yielding the conversation after every node.

Each chunk is {"messages": [...]} -- the full message list once a node has finished (LangGraph's stream_mode="values"). The last chunk equals what :meth:ainvoke returns. Nodes run exactly as in :meth:ainvoke: one model call per step, tool calls in parallel. A run that ends on a failed node raises :class:GraphExecutionError after its chunks.

astream_events(input, *, config=None, version='v2', **kwargs) async

Stream execution events matching LangGraph v2 format.

Yields event dicts consumed by PromptiseAgent.astream_with_tools():

  • on_chat_model_stream -- one per model chunk; data["chunk"] is the AIMessageChunk.
  • on_tool_start / on_tool_end / on_tool_error -- one start and one end per tool call, sharing a run_id. on_tool_end carries data["status"] ("success" or "error" when the tool reported an error, e.g. an MCP ToolError) and data["duration_ms"]; on_tool_error means the tool raised.
  • on_node_start, on_node_end, on_node_error (engine-specific).

Nodes stream through :meth:BaseNode.stream and run exactly once: the events are a view of the same execution :meth:ainvoke does.

GraphExecutionError

promptise.engine.execution.GraphExecutionError

Bases: RuntimeError

A graph run ended on a node failure the graph did not recover from.

Raised by :meth:PromptGraphEngine.ainvoke and, after their output, by :meth:PromptGraphEngine.astream and :meth:PromptGraphEngine.astream_events. The provider exception (a rejected API key, a network failure…) is chained as __cause__ when the node recorded one. PromptiseAgent.ainvoke() lets it propagate, so a broken credential is an exception at the call site, never an "answer" that echoes the question.

Attributes:

Name Type Description
graph_name

Name of the graph that failed.

node_name

Node whose failure ended the run.

error

The node's recorded error text.

report

The :class:ExecutionReport of the failed run.

GraphIterationLimitError

promptise.engine.execution.GraphIterationLimitError

Bases: GraphExecutionError

A run hit an iteration limit in the middle of a tool loop.

The model asked for tools, the limit stopped the run before it saw their results, so there is no answer: the last message is a tool result. Raised instead of returning that tool result as if it were the agent's answer. A subclass of :class:GraphExecutionError.

Which limit: the node's own budget (PromptNode(max_iterations=...), capped by the engine's max_node_iterations; the ReAct agent's reason node has 15) or the engine's max_iterations for the whole run (build_agent(max_agent_iterations=...)).

Attributes:

Name Type Description
limit

The limit that was reached.

pending_tools

Names of the tools whose results the model never saw.

GraphStoppedError

promptise.engine.execution.GraphStoppedError

Bases: GraphExecutionError

A hook ended a run in the middle of a tool loop.

A hook ends a run by setting state.current_node = "__end__" (:class:~promptise.engine.hooks.CycleDetectionHook, or a custom hook). When that happens after the model asked for tools and before it saw their results, there is no answer: the last message is a tool result. Raised instead of returning that tool result as the agent's answer, like :class:GraphIterationLimitError. A hook stop after the model answered returns the answer as usual.

Attributes:

Name Type Description
reason

Why the run stopped (the hook's NodeResult.error when it set one, e.g. "Token budget exceeded: 5078/5000 tokens").

stopped_by

Class name of the hook that ended the run.

pending_tools

Names of the tools whose results the model never saw.

GraphBudgetExceededError

promptise.engine.execution.GraphBudgetExceededError

Bases: GraphStoppedError

A :class:~promptise.engine.hooks.BudgetHook stopped a run over its token or cost budget before the model gave an answer.

A subclass of :class:GraphStoppedError (and so of :class:GraphExecutionError); reason says which budget and by how much.

CodeActionBudgetError

promptise.engine.execution.CodeActionBudgetError

Bases: GraphExecutionError

A code-action run could not do the whole job within max_tool_calls.

Raised when the last program the model wrote reached the per-program tool-call cap, or when, after a program reached it, the model answered that the job cannot be done in fewer calls. The run has no answer: a partial result computed from part of the data is never returned.

A subclass of :class:GraphExecutionError, so existing except GraphExecutionError handlers still catch it. Raise max_tool_calls (CodeActionConfig(max_tool_calls=...)), or give the agent a tool that returns many records per call.

Attributes:

Name Type Description
max_tool_calls

The per-program cap the run hit.

attempts

How many programs the model wrote.

PromptGraph

promptise.engine.graph.PromptGraph

A directed graph of reasoning nodes.

Nodes are added via add_node(). Edges via add_edge(). The entry point is set via set_entry().

The graph supports runtime mutation via apply_mutation() and copy() for per-invocation isolation.

Parameters:

Name Type Description Default
name str

Human-readable graph name (for visualization and logs).

'graph'
edges property

All edges in the graph.

entry property

The entry node name.

nodes property

All nodes in the graph.

add_edge(from_node, to_node, *, condition=None, label='', priority=0)

Add a directed edge. Returns self for chaining.

Warns if from_node or to_node do not exist in the graph yet (they may be added later, so this is a warning, not an error).

add_node(node)

Add a node to the graph. Returns self for chaining.

always(from_node, to_node)

Add an unconditional edge: A always goes to B.

apply_mutation(mutation)

Apply a single mutation to the graph.

Called by the engine during execution to modify the live graph copy. Validates mutations before applying.

copy()

Copy the graph for per-invocation isolation.

Nodes are shared references (they are stateless config objects). Edges use copy-on-write — shared until the copy mutates them, at which point they are lazily copied via _ensure_edges_owned().

describe()

Human-readable graph description.

from_pool(nodes, *, system_prompt='', name='autonomous') classmethod

Create a graph from a pool of nodes (autonomous mode).

The engine will use AutonomousNode to dynamically build the execution path from the node pool. Nodes with is_entry=True start first. Nodes with is_terminal=True can end the graph.

get_edges_from(node_name)

Get all outgoing edges from a node, sorted by priority (desc).

Uses a precomputed adjacency index — O(1) lookup instead of O(E) scan.

get_node(name)

Get a node by name. Raises KeyError if not found.

has_node(name)

Check if a node exists.

loop_limit(node_name)

(max_iterations, exit_to) set by :meth:loop_until for node_name, or None.

loop_until(node_name, exit_to, *, condition, max_iterations=5)

Add a loop: node re-enters itself until condition, then exits.

The node runs at most max_iterations times in a run (fewer when its own max_iterations is lower); once it has used them, the engine exits to exit_to even though condition never held.

on_confidence(from_node, to_node, min_confidence=0.7)

Add an edge that fires when output confidence exceeds threshold.

on_error(from_node, to_node)

Add an edge that fires when a node has an error.

on_guard_fail(from_node, to_node)

Add an edge that fires when any guard fails.

on_no_tool_call(from_node, to_node)

Add an edge that fires when the node made NO tool calls (final answer).

on_output(from_node, to_node, key, value=True)

Add an edge that fires when output[key] == value.

on_tool_call(from_node, to_node)

Add an edge that fires when the node made tool calls.

remove_node(name)

Remove a node and all edges referencing it.

sequential(*node_names)

Chain nodes with always edges: A → B → C → ...

set_entry(node_name)

Set the entry node. Returns self for chaining.

to_mermaid()

Export graph as Mermaid diagram syntax.

validate()

Validate the graph topology. Returns list of error strings.

Checks: - Entry node is set and exists - All edge targets exist (or are __end__) - All transition targets in node configs exist - No unreachable nodes (except entry) - No dead-end nodes without __end__ transition

when(from_node, to_node, condition, label='')

Add a conditional edge: A goes to B when condition is True.

Edge

promptise.engine.graph.Edge dataclass

A directed edge between two nodes.

Attributes:

Name Type Description
from_node str

Source node name.

to_node str

Target node name.

condition Callable[[NodeResult], bool] | None

Optional callable (NodeResult) -> bool. If set, this edge is only followed when the condition returns True. Edges without conditions are Always edges.

label str

Human-readable label for visualization.

priority int

When multiple conditional edges match, the one with the highest priority wins. Default is 0.


Graph State

GraphState

promptise.engine.state.GraphState dataclass

The complete state of a graph execution.

Passed to every node's execute() method. Nodes read from and write to this state. The engine updates it after each node execution.

The graph field carries the live (mutable) copy of the PromptGraph so nodes can inspect and modify the graph topology at runtime.

Attributes:

Name Type Description
messages list[Any]

Full LangChain message history.

context dict[str, Any]

Key-value state that persists across nodes.

current_node str

Name of the node currently being executed.

visited list[str]

Ordered list of node names visited (for cycle detection).

iteration int

Global iteration counter (incremented each time any node executes).

node_iterations dict[str, int]

Per-node execution counter (for detecting stuck nodes).

graph Any

The live PromptGraph copy (mutable during execution). None if not yet set by the engine.

plan list[str]

Current subgoals (set by planning nodes).

completed list[str]

Completed subgoals.

observations list[dict[str, Any]]

Tool results accumulated during execution.

reflections list[dict[str, Any]]

Past learnings/mistakes from reflection nodes.

tool_calls_made int

Total tool calls across all nodes.

node_timings dict[str, float]

Cumulative milliseconds per node name.

total_tokens int

Cumulative token usage across all nodes.

node_history list[NodeResult]

Ordered list of NodeResult objects for full execution trace.

turn_question Any

The current user question: the last HumanMessage of the input. Context compaction always sends it.

turn_input list[Any]

The input up to and including turn_question (system messages and chat history).

Dict messages ({"role": "user", "content": ...}) in messages are converted to LangChain message objects when the state is created.

active_subgoal property

Return the first uncompleted subgoal, or None.

all_subgoals_complete property

Return True if every planned subgoal is completed.

add_observation(tool_name, result, args=None, success=True, duration_ms=0.0)

Record a tool observation.

Observations are capped at 50 entries. When the cap is reached, the oldest entries are discarded.

add_reflection(iteration, mistake, correction, confidence=0.5, stage='')

Record a reflection from a reflect/evaluate node.

complete_subgoal(subgoal)

Mark a subgoal as completed.

increment_node_iteration(node_name)

Increment and return the per-node iteration count.

record_node_timing(node_name, ms)

Add ms to the cumulative timing for node_name.

trim_messages()

Trim message history to max_messages if exceeded.

Keeps all system messages (essential context), the current user question, plus the most recent non-system messages to stay within the configured cap.

NodeResult

promptise.engine.state.NodeResult dataclass

Everything that happened during a single node execution.

This is the richest observability unit in the engine. Every field is populated by the node's execute() method and the engine's post-processing. Stored in GraphState.node_history for full traceability.

NodeEvent

promptise.engine.state.NodeEvent dataclass

An event emitted during streaming execution.

These are engine-level events (on_node_start, on_node_end, on_graph_mutation) that complement the standard LangChain events (on_tool_start, on_chat_model_stream, etc.).

NodeFlag

promptise.engine.state.NodeFlag

Bases: str, Enum

Typed flags that declare a node's role and capabilities.

Use these instead of bare strings for type safety and IDE support::

PlanNode("plan", flags={NodeFlag.ENTRY, NodeFlag.INJECT_TOOLS})
CACHEABLE = 'cacheable' class-attribute instance-attribute

This node's output can be cached for identical inputs.

CRITICAL = 'critical' class-attribute instance-attribute

This node must succeed — graph aborts on failure.

ENTRY = 'entry' class-attribute instance-attribute

This node starts the reasoning graph.

INJECT_TOOLS = 'inject_tools' class-attribute instance-attribute

Receives MCP tools at runtime from build_agent().

ISOLATED_CONTEXT = 'isolated_context' class-attribute instance-attribute

Don't inherit context from previous nodes — start fresh.

LIGHTWEIGHT = 'lightweight' class-attribute instance-attribute

Use a smaller/faster model for this node (if model_override not set).

NO_HISTORY = 'no_history' class-attribute instance-attribute

Don't inject conversation history into this node's context.

OBSERVABLE = 'observable' class-attribute instance-attribute

Emit extra observability events for this node.

PARALLEL_SAFE = 'parallel_safe' class-attribute instance-attribute

Safe to run concurrently with other parallel_safe nodes.

READONLY = 'readonly' class-attribute instance-attribute

This node only reads state, never writes. Safe for parallel execution.

REQUIRES_HUMAN = 'requires_human' class-attribute instance-attribute

This node pauses for human input before proceeding.

RETRYABLE = 'retryable' class-attribute instance-attribute

This node can be retried on failure.

SKIP_ON_ERROR = 'skip_on_error' class-attribute instance-attribute

Skip this node if a previous node errored (don't abort).

STATEFUL = 'stateful' class-attribute instance-attribute

This node modifies state.context (for dependency tracking).

SUMMARIZE_OUTPUT = 'summarize_output' class-attribute instance-attribute

Auto-summarize long outputs before passing to next node.

TERMINAL = 'terminal' class-attribute instance-attribute

Reaching this node can end the graph.

VALIDATE_OUTPUT = 'validate_output' class-attribute instance-attribute

Auto-validate output against output_schema before proceeding.

VERBOSE = 'verbose' class-attribute instance-attribute

Include full prompt and response in observability logs.

GraphMutation

promptise.engine.state.GraphMutation dataclass

A single graph modification requested during execution.

Mutations are applied to the live graph copy (not the original). The engine validates mutations before applying them.

Attributes:

Name Type Description
action str

One of "add_node", "remove_node", "add_edge", "remove_edge".

node_name str

Target node name (for add/remove node).

node_config dict[str, Any]

Configuration dict for new nodes (used with add_node). Passed to node_from_config().

from_node str

Source node for edge operations.

to_node str

Target node for edge operations.

condition str

Optional condition string for conditional edges.

ExecutionReport

promptise.engine.state.ExecutionReport dataclass

Summary of a complete graph execution.

Produced by PromptGraphEngine after ainvoke() completes.

summary()

Human-readable execution summary.


Base Node Types

BaseNode

promptise.engine.base.BaseNode

Concrete base class for graph nodes.

Provides default implementations and common configuration. Subclass this for custom node behaviour, or use one of the built-in types (PromptNode, ToolNode, etc.).

Parameters:

Name Type Description Default
name str

Unique node identifier within the graph.

required
instructions str

Natural-language description of what this node does. Injected into the LLM prompt for PromptNode; used for documentation and visualization for other node types.

''
description str

Short description shown in graph visualization and describe() output.

''
transitions dict[str, str] | None

Mapping of output keys to next-node names. E.g. {"proceed": "act", "replan": "plan"}. The engine resolves transitions after execution.

None
default_next str | None

Fallback node name if no transition matches.

None
max_iterations int

Maximum times this node can execute in a single graph run (prevents infinite loops).

10
metadata dict[str, Any] | None

Arbitrary metadata accessible by hooks and observability.

None
is_entry property

Whether this node starts the graph.

is_terminal property

Whether this node can end the graph.

execute(state, config) async

Execute this node. Subclasses must override.

has_flag(flag)

Check if this node has a specific flag.

stream(state, config) async

Stream execution events.

Default implementation executes the node and yields a single on_node_end event. Override for fine-grained streaming.

NodeProtocol

promptise.engine.base.NodeProtocol

Bases: Protocol

Minimal protocol that every graph node must satisfy.

execute(state, config) async

Execute this node. Return a NodeResult describing what happened.

stream(state, config) async

Stream execution events. Yields NodeEvent objects.

@node decorator

promptise.engine.base.node(name, *, instructions='', transitions=None, default_next=None, max_iterations=10, metadata=None, flags=None, is_entry=False, is_terminal=False)

Decorator that turns an async function into a graph node.

Usage::

@node("fetch_data", default_next="process")
async def fetch_data(state: GraphState) -> NodeResult:
    data = await api.get(state.context["url"])
    state.context["data"] = data
    return NodeResult(node_name="fetch_data", output=data)

graph.add_node(fetch_data)

The decorated function can accept: - (state: GraphState) - (state: GraphState, config: dict) - () (for side-effect-only nodes)

Returns:

Type Description
Any

A _FunctionalNode instance that can be added to a graph.


Standard Nodes

PromptNode

promptise.engine.nodes.PromptNode

Bases: BaseNode

A complete reasoning unit in the graph.

Each PromptNode is a self-contained processing pipeline:

Input → Preprocess → Context Assembly → LLM Call → Tool Execution → Postprocess → Guards → Output

Every aspect is configurable: - What the LLM sees (blocks, strategy, perspective, context layers) - What the LLM can do (tools, tool_choice) - What the LLM must produce (output_schema, guards) - How data flows in (input_keys read from state.context) - How data flows out (output_key writes to state.context) - Pre/post processing (preprocessor, postprocessor callables) - Context from previous node (inherit_context_from)

Parameters:

Name Type Description Default
name str

Unique node identifier.

required
blocks list[Any] | None

PromptBlocks to assemble for this node's system prompt.

None
strategy Any | None

Reasoning strategy (ChainOfThought, SelfCritique, etc.).

None
perspective Any | None

Cognitive perspective (Analyst, Critic, etc.).

None
tools list[BaseTool] | None

Tools available at THIS node. None disables tools.

None
tool_choice str

"auto" (LLM decides), "required" (must call a tool), or "none" (tools shown but not callable).

'auto'
inject_tools bool

When True, the engine auto-injects all discovered MCP tools into this node at runtime. Use this when defining custom graphs where nodes should receive tools from build_agent(servers=...) without hardcoding them. The injected tools merge with any tools already set on the node (no duplicates).

False
tool_summary str | None

An "Available tools" block in the system prompt, on top of the tool definitions the model receives anyway: "off" (none), "names" (one line of tool names) or "full" (each tool's parameters and the first 120 characters of its description). None (default) uses the engine's setting (PromptGraphEngine(tool_summary=...) / build_agent(tool_summary=...)), which defaults to "off".

None
output_schema type | None

Pydantic model for structured output.

None
guards list[Any] | None

Guards that validate the output before proceeding.

None
context_layers dict[str, int] | None

Extra named context layers with priorities. Merged into the system prompt during assembly.

None
max_tokens int

Max response tokens for this node.

4096
temperature float

LLM temperature for this node.

0.0
input_keys list[str] | None

Keys to read from state.context and inject into the system prompt. E.g. ["search_results", "user_query"] makes those values available in the prompt.

None
output_key str | None

Key to write this node's output to in state.context. The next node can read it via input_keys.

None
inherit_context_from str | None

Name of a previous node whose output should be injected as context. Reads from state.context["{node_name}_output"].

None
preprocessor Callable | None

Async callable (state, config) → state that runs before the LLM call. Use for data transformation, enrichment, or filtering.

None
postprocessor Callable | None

Async callable (output, state, config) → output that runs after the LLM call. Use for output transformation, validation, or enrichment.

None
include_observations bool

Auto-inject recent tool results from state.

True
include_plan bool

Auto-inject current plan/subgoals from state.

True
include_reflections bool

Auto-inject past learnings from state.

True
context_scope str

How much history the model sees: "full", "auto" (full, compacting once the tool loop gets long), "ledger" (always compacted) or "scoped". See :mod:promptise.engine.compaction.

'full'
auto_ledger_after int | None

Tool results before an "auto" node compacts (default 6). Shorthand for compaction=ContextCompaction(after_tool_results=...).

None
compaction Any | None

:class:~promptise.engine.compaction.ContextCompaction settings for this node (or True / False / an int). Without it the node uses the settings build_agent passed, else the defaults.

None
inject_tools property

Whether this node receives MCP tools at runtime.

execute(state, config) async

Execute the full node pipeline:

  1. Preprocessor (custom data transformation)
  2. Context assembly (blocks + input_keys + inherited context + state)
  3. LLM call (with tools if configured)
  4. Tool execution (auto-loop if tools called)
  5. Postprocessor (custom output transformation)
  6. Guards (validate output)
  7. Write output to state.context[output_key]
from_config(config) classmethod

Create a PromptNode from a configuration dict.

Handles all PromptNode parameters including data flow, preprocessing, and context control.

stream(state, config) async

Run the :meth:execute pipeline, streaming as it goes.

One model call per step, exactly as :meth:execute: the call is streamed and its chunks are yielded as on_chat_model_stream events (data["chunk"]; all chunks of one call share data["run_id"]). Each tool call yields on_tool_start and then on_tool_end (data["status"] is "error" when the tool reported an error) or on_tool_error (the tool raised); tool calls run in parallel, as in :meth:execute. The last event is on_node_end with the :class:NodeResult under data["result"].

Nodes with output_schema call the model without streaming (there are no text tokens to stream). A subclass that overrides :meth:execute but not this method streams through :meth:BaseNode.stream, so its own logic is never bypassed.

ToolNode

promptise.engine.nodes.ToolNode

Bases: BaseNode

Execute a specific tool or let the engine pick from available tools.

Unlike PromptNode (where tools are called by the LLM), ToolNode executes tools directly based on state context. Useful when tool selection is deterministic rather than LLM-driven.

A call runs as it would in a :class:PromptNode: the engine hooks' pre_tool (may change the arguments, or block the call by raising) and post_tool (may change the result) run around it, the tool is invoked as a tool call (an MCP tool error reaches the model as the server's error envelope, flagged status="error"), and :meth:stream yields on_tool_start / on_tool_end (or on_tool_error) for each call, so astream_events() and astream_with_tools() show it. The node runs one turn's calls one after another, in order.

Parameters:

Name Type Description Default
tools list[BaseTool] | None

Available tools for this node.

None
validate_inputs bool

Check each call's arguments against the tool's parameter schema before calling it (after pre_tool hooks). Invalid arguments do not reach the tool: the call's ToolMessage is an error (status="error") listing each problem by argument path (promptise.tools.ToolArgumentError's message) so the model can correct the call, and callbacks get on_tool_error. As for MCP tools, an explicit null for an optional parameter that cannot be null counts as not given. An MCP tool checks its own arguments the same way, so the node leaves the check to it. With False, the arguments go to the tool as they are.

True
deduplicate bool

Run identical (tool, args) calls of one model turn once. Each repeat in that turn still gets its own ToolMessage with the first call's result, and is reported as reused (trace_tools, observer, callbacks, engine hooks' on_tool_reused instead of pre_tool / post_tool, "reused": True in NodeResult.tool_calls and stream events). A call in a later model turn, a later pass of a tool_selector, or another run runs the tool again, so a status check gets the current value. A failed call is never reused. The record lives in the run, never on the node, so sequential and concurrent runs of a graph do not affect each other.

True
max_result_chars int

Cap the size of a successful tool result.

4000
tool_selector Callable[[GraphState], tuple[str, dict]] | None

Optional callable that selects which tool to call based on state. If None, runs the tool calls of the most recent AI message that has tool calls.

None
execute(state, config) async

Execute tools from state or selector.

stream(state, config) async

Run the :meth:execute pipeline, yielding each call's events.

Each call yields on_tool_start and then on_tool_end (data["status"] is "error" when the tool reported an error, data["reused"] is True for a repeat answered from the turn's earlier result) or on_tool_error (the tool or a hook raised), with the call's run_id and duration_ms -- the events a :class:PromptNode yields for its tool calls. The last event is on_node_end with the :class:NodeResult under data["result"]. A subclass that overrides :meth:execute but not this method streams through :meth:BaseNode.stream.

RouterNode

promptise.engine.nodes.RouterNode

Bases: BaseNode

Lightweight LLM call that decides which path to take.

No tool calling. The LLM sees the current state (via context blocks) and picks from a list of named routes.

Parameters:

Name Type Description Default
routes dict[str, str] | None

Mapping of route names to next-node names.

None
context_blocks list[Any] | None

Blocks to render for the LLM's context.

None
model_override BaseChatModel | None

Optional different model for routing (e.g., a smaller/faster model for routing decisions).

None
execute(state, config) async

Ask the LLM to choose a route.

GuardNode

promptise.engine.nodes.GuardNode

Bases: BaseNode

Validate state against guards and route based on pass/fail.

No LLM call. Runs guards against the current state and routes to on_pass or on_fail nodes.

Parameters:

Name Type Description Default
guards list[Any] | None

List of guard instances to run.

None
target_key str | None

Key in state.context to validate. If None, validates the last message content.

None
on_pass str

Node name if all guards pass.

'__end__'
on_fail str | None

Node name if any guard fails.

None
execute(state, config) async

Run guards and route accordingly.

ParallelNode

promptise.engine.nodes.ParallelNode

Bases: BaseNode

Run multiple child nodes concurrently and merge results.

Parameters:

Name Type Description Default
nodes list[BaseNode] | None

Child nodes to execute in parallel.

None
merge_strategy str

How to merge results. "concatenate" — append all observations. "dict" — return {node_name: output} dict. "custom" — use merge_fn.

'concatenate'
merge_fn Callable | None

Custom merge function (when merge_strategy is "custom").

None
execute(state, config) async

Execute all child nodes concurrently.

LoopNode

promptise.engine.nodes.LoopNode

Bases: BaseNode

Repeat a subgraph until a condition is met.

Parameters:

Name Type Description Default
body_node BaseNode | None

The node to repeat (can be a SubgraphNode).

None
condition Callable[[GraphState], bool] | None

Callable that receives state and returns True when the loop should stop.

None
max_loop_iterations int

Maximum iterations before forcing exit.

5
execute(state, config) async

Execute body node in a loop until condition met.

HumanNode

promptise.engine.nodes.HumanNode

Bases: BaseNode

Pause execution and wait for human input.

Integrates with the existing ApprovalPolicy system.

Parameters:

Name Type Description Default
prompt_template str

Template shown to the human.

'Approve this action?'
timeout float

Seconds to wait before applying on_timeout.

300.0
on_approve str

Next node if human approves.

'__end__'
on_deny str | None

Next node if human denies.

None
on_timeout str

Next node if timeout expires.

'__end__'
execute(state, config) async

Pause for human input.

TransformNode

promptise.engine.nodes.TransformNode

Bases: BaseNode

Transform state data without calling the LLM.

Useful for formatting, aggregation, data extraction, or preparing state for the next node.

Parameters:

Name Type Description Default
transform Callable[[GraphState], Any] | None

Callable that receives state and returns a value to store in state.context[output_key].

None
output_key str

Key in state.context to store the result.

'transform_result'
execute(state, config) async

Execute the transform function.

SubgraphNode

promptise.engine.nodes.SubgraphNode

Bases: BaseNode

Embed a complete sub-graph as a single node.

The subgraph runs to completion, then the parent graph continues. State can be shared (inherit_state=True) or isolated.

Parameters:

Name Type Description Default
subgraph Any

A PromptGraph instance to run.

None
inherit_state bool

If True, the subgraph shares the parent's state (messages, context, observations). If False, the subgraph gets a fresh state and only its final messages are appended to the parent.

True
execute(state, config) async

Run the subgraph to completion.

AutonomousNode

promptise.engine.nodes.AutonomousNode

Bases: BaseNode

dynamically composes its own reasoning path at runtime.

Instead of following a pre-defined graph, the agent:

  1. Sees all available nodes as a menu of capabilities
  2. Decides which node to execute next based on the task
  3. Executes that node (with its tools, blocks, guards)
  4. Evaluates the result
  5. Decides the next node or finishes

The developer provides the LEGO blocks. The agent builds the model.

Usage::

autonomous = AutonomousNode(
    "agent",
    node_pool=[
        web_researcher("search", tools=search_tools),
        data_analyst("analyze", tools=db_tools),
        fact_checker("verify"),
        summarizer("conclude"),
        planner("plan"),
    ],
    planner_instructions="You are a research assistant. "
        "Use the available steps to answer the user's question thoroughly.",
)

graph = PromptGraph("autonomous")
graph.add_node(autonomous)
graph.set_entry("agent")

Parameters:

Name Type Description Default
node_pool list[BaseNode] | None

Available nodes the agent can choose from. Each node is fully configured with its own tools, blocks, guards, and instructions.

None
planner_instructions str

How the agent should approach selecting and sequencing nodes.

''
allow_repeat bool

Whether the agent can re-use the same node multiple times.

True
max_steps int

Maximum nodes the agent can execute per run.

15
entry_node str | None

Pool node to run first, before the agent chooses.

None
terminal_nodes list[str] | None

Pool nodes that end the run once they succeed.

None
answer_node str | None

Pool node that writes the final answer when the agent finishes without a terminal node having run last. Defaults to the first terminal node; with no terminal node, the node asks the model for the final answer itself.

None

A chosen step runs its whole tool loop — the node is re-run while its model keeps calling tools, up to the node's max_iterations — before the agent chooses again. The routing exchange (the step menu and the model's JSON choice) is never added to the conversation, and the run always ends with a written answer. A tool name given as the next step runs the pool node that has that tool.

execute(state, config) async

Autonomously select and execute nodes from the pool.

stream(state, config) async

Run the :meth:execute pipeline, relaying the chosen steps' events (model chunks and tool events). The routing calls are not streamed.

CodeActionNode

promptise.engine.code_action.CodeActionNode

Bases: BaseNode

A node that solves a task by writing and running one sandboxed program.

Parameters:

Name Type Description Default
name str

Node identifier.

required
tools list[Any] | None

Tools the generated program may call (bridged to the host). If empty, falls back to the engine's runtime tools (_engine_tools).

None
system_prompt str

Base system prompt prepended to the code-writing prompt.

''
blocks list[Any] | None

Accepted for signature parity with other prebuilts (unused in the program prompt — system_prompt carries the instructions).

None
sandbox_factory SandboxFactory | None

async () -> SandboxSession. Required at run time; the node creates one session per run and cleans it up.

None
model_override Any | None

Optional per-node model.

None
max_repairs int

How many times to feed a failed program (a crash, or a program that reached max_tool_calls) back for a fix.

1
result_marker str

stdout prefix that marks the final answer line.

'RESULT:'
exec_timeout int

Max seconds the program may run inside the sandbox.

120
max_tool_calls int

Hard upper bound on the bridged tool calls of each program (the first and each repaired one get their own budget). Every call counts: successful, failed, an unknown tool, or one a hook blocked. Once reached, further calls raise ToolBudgetExceeded in the program instead of executing, a warning is logged, and the program's output is discarded: the model is asked for a program that does the whole job in fewer calls (never one over part of the data), or to reply CANNOT_FINISH_WITHIN_CALL_CAP. That reply, or a budget hit with no repairs left, fails the node, and the engine raises CodeActionBudgetError (a GraphExecutionError). The prompt says the cap exists but not its value, so the model cannot fit the data to it. A hook-independent safety bound so a generated program cannot loop a tool unbounded. Set 0 to disable (not advised).

50

Reasoning Nodes

ThinkNode

promptise.engine.reasoning_nodes.ThinkNode

Bases: PromptNode

Analyzes current state, identifies gaps, recommends next step.

Pre-configured with: - Instructions for structured gap analysis - Auto-injects observations and plan from state - No tools (pure reasoning) - Outputs: gap_analysis, confidence, next_step, reasoning

The developer just creates ThinkNode("think") — everything else is handled internally.

PlanNode

promptise.engine.reasoning_nodes.PlanNode

Bases: PromptNode

Creates structured plans with self-evaluation and subgoal tracking.

Pre-configured with: - Instructions for creating prioritized subgoals - Quality self-evaluation (re-plans if below threshold) - Auto-manages state.plan and state.completed - Outputs: subgoals, priorities, active_subgoal, quality_score

execute(state, config) async

Execute planning and update state.plan.

ReflectNode

promptise.engine.reasoning_nodes.ReflectNode

Bases: PromptNode

Reviews what happened, identifies mistakes, generates corrections.

Pre-configured with: - Instructions for honest self-assessment - Auto-injects observations and past reflections - Auto-stores new reflections in state - Outputs: progress_assessment, mistake, correction, confidence, route

execute(state, config) async

Execute reflection and auto-store in state.

CritiqueNode

promptise.engine.reasoning_nodes.CritiqueNode

Bases: PromptNode

Challenges the current answer/plan with counter-arguments.

Pre-configured with: - Instructions for adversarial review - Severity scoring (routes to revision if above threshold) - Outputs: weaknesses, counter_arguments, improvements, severity

execute(state, config) async

Execute critique and route based on severity.

ObserveNode

promptise.engine.reasoning_nodes.ObserveNode

Bases: PromptNode

Processes raw tool results into structured, actionable data.

Pre-configured with: - Instructions to extract entities, facts, and key findings - Auto-injects recent observations - Enriches state.context with extracted data - Outputs: summary, entities, facts, key_findings

execute(state, config) async

Execute observation and enrich state with extracted data.

SynthesizeNode

promptise.engine.reasoning_nodes.SynthesizeNode

Bases: PromptNode

Combines all gathered data into a comprehensive final answer.

Pre-configured with: - Instructions for synthesis with source citation - Auto-injects ALL observations, reflections, plan progress - Default terminal node (ends the graph) - Outputs: answer, confidence, sources

ValidateNode

promptise.engine.reasoning_nodes.ValidateNode

Bases: PromptNode

LLM evaluates whether output meets specified criteria.

Pre-configured with: - Natural language validation criteria - Pass/fail routing - Outputs: passes, issues, suggestions, confidence

execute(state, config) async

Execute validation and route based on pass/fail.

JustifyNode

promptise.engine.reasoning_nodes.JustifyNode

Bases: PromptNode

Forces explicit justification of the last decision/action.

Pre-configured with: - Instructions for structured reasoning chain - Auto-injects the last action from state history - Stores justification in state for audit trail - Outputs: reasoning_chain, evidence, conclusion, confidence

execute(state, config) async

Execute justification and store in state.

RetryNode

promptise.engine.reasoning_nodes.RetryNode

Bases: BaseNode

Wraps another node with retry logic and error enrichment.

On failure, enriches the state with error context so the wrapped node can adapt on the next attempt.

execute(state, config) async

Execute wrapped node with retries.

FanOutNode

promptise.engine.reasoning_nodes.FanOutNode

Bases: BaseNode

Sends different sub-questions to parallel child nodes.

Unlike ParallelNode (which runs the same state through each child), FanOutNode gives each child a focused sub-question via state overrides.

execute(state, config) async

Execute each branch with its state override concurrently.


Prebuilt Graph Factories

Factory functions that build common reasoning patterns. All exposed as static methods on PromptGraph (e.g. PromptGraph.react(...)).

build_react_graph

promptise.engine.prebuilts.build_react_graph(tools=None, system_prompt='', *, blocks=None, max_node_iterations=15)

Build a standard ReAct agent graph.

Single PromptNode with tools. The LLM decides when to call tools and when to produce the final answer. The engine handles the tool-calling loop automatically.

This is the default graph used by build_agent(). It runs with context_scope="auto": simple tasks see the full transcript (unchanged), but once a tool loop grows the node automatically switches to a bounded, deduplicated facts ledger — so context stays manageable and token-efficient on deep tasks without choosing a different pattern.

Parameters:

Name Type Description Default
tools list[BaseTool] | None

Tools available to the agent.

None
system_prompt str

System prompt text.

''
blocks list[Any] | None

Optional PromptBlocks for the reasoning node.

None
max_node_iterations int

Max tool-calling iterations.

15

Returns:

Type Description
PromptGraph

A PromptGraph with a single PromptNode("reason").

build_verify_graph

promptise.engine.prebuilts.build_verify_graph(tools=None, system_prompt='', *, blocks=None, max_node_iterations=6)

Single-pass self-verifying reasoning — one LLM call, but the model must plan, solve, and check its own answer within that generation.

It gives a model the benefit of an explicit verification step at one-turn latency (no multi-call overhead). On models that already reason internally it matches a direct prompt; on weaker models the forced self-check recovers errors a single pass would miss. A good default when you want a self-checking answer without a multi-stage pipeline.

Parameters:

Name Type Description Default
tools list[BaseTool] | None

Optional tools for the reasoning node.

None
system_prompt str

Base system prompt.

''
blocks list[Any] | None

Optional PromptBlocks for the node.

None
max_node_iterations int

Tool-loop budget.

6

Returns:

Type Description
PromptGraph

A single-node PromptGraph that plans, solves, and verifies in one

PromptGraph

generation.

build_managed_graph

promptise.engine.prebuilts.build_managed_graph(tools=None, system_prompt='', *, blocks=None, max_node_iterations=30)

Tool agent with context lifecycle management for long tool chains.

A single tool-using node, but run with context_scope="ledger": instead of feeding the model an ever-growing transcript of tool calls and results (where it loses track and re-queries the same facts dozens of times), each turn it sees the task plus a compact, deduplicated "facts gathered" ledger built from the tool results so far. Context stays bounded and the model stops re-looking-up what it already knows. A call the model does repeat runs the tool again (a status changes); to answer repeats of lookups whose results cannot change from the earlier result, pass build_agent(context_compaction=ContextCompaction(reuse_results=[...])).

Use this for deep multi-tool tasks — traversing a database/graph, gathering many facts then aggregating. It cuts redundant tool calls and bounds token growth on long chains at equal accuracy — an efficiency primitive, not an accuracy claim.

Parameters:

Name Type Description Default
tools list[BaseTool] | None

Tools the agent can call.

None
system_prompt str

Base system prompt.

''
blocks list[Any] | None

Optional PromptBlocks for the node.

None
max_node_iterations int

Tool-loop budget (higher than ReAct's — deep tasks make many calls).

30

Returns:

Type Description
PromptGraph

A single-node PromptGraph whose node manages its tool-loop context.

build_code_action_graph

promptise.engine.prebuilts.build_code_action_graph(tools=None, system_prompt='', *, blocks=None, sandbox_factory=None, max_repairs=1, exec_timeout=120, max_tool_calls=50)

Code-action reasoning — the model writes one program, not a tool chain.

For aggregation / data-traversal tasks (gather many facts then compute), chaining dozens of conversational tool calls is slow, expensive, and error-prone. code-action changes the action space: in a single LLM turn the model writes one Python program that calls the available tools (bridged into a hardened Docker sandbox) and computes the answer deterministically. Validated on agentic tasks — a large accuracy gain at a fraction of the tokens and latency, in one turn.

Requires a sandbox: build_agent(agent_pattern="code-action") auto-enables it (Docker must be installed and running). The generated code runs with a read-only rootfs, dropped capabilities, seccomp, and no network — it can only reach the outside world through the bridged host tools, so every tool call still passes through the engine's hooks (budget, health, audit).

Parameters:

Name Type Description Default
tools list[BaseTool] | None

Tools the program may call (bridged to the host).

None
system_prompt str

Base system prompt.

''
blocks list[Any] | None

Accepted for signature parity (unused in the program prompt).

None
sandbox_factory Any | None

async () -> SandboxSession (injected by build_agent).

None
max_repairs int

Times to feed a failed program (a crash, or one that reached max_tool_calls) back for a fix (default 1).

1
exec_timeout int

Max seconds the program may run inside the sandbox.

120
max_tool_calls int

Hard cap on each program's bridged tool calls (default 50), failed calls included — a hook-independent safety bound against a program looping a tool. A program that reaches it fails (its output is discarded); a repair gets a fresh budget.

50

Returns:

Type Description
PromptGraph

A single-node PromptGraph that writes and runs a program.

build_peoatr_graph

promptise.engine.prebuilts.build_peoatr_graph(tools=None, system_prompt='', *, planning_instructions='', acting_instructions='', thinking_instructions='', reflecting_instructions='', blocks=None, max_act_steps=12)

Build a PEOATR (Plan → Act → Think → Reflect → Answer) graph.

Five-stage reasoning pattern where the agent:

  1. Plans subgoals and rates the plan (re-plans below quality 3)
  2. Executes tools to achieve the active subgoal
  3. Analyzes the results (think): another tool call now, or reflect
  4. Reflects on progress and routes: answer, replan, or continue
  5. Writes the final answer

Plan, think and reflect produce structured output, so their routing decisions always reach the engine. Every stage has an iteration budget; a stage that exhausts it hands over to answer (plan hands over to act), so the run always ends with a written answer.

Parameters:

Name Type Description Default
tools list[BaseTool] | None

Tools available during the Act stage.

None
system_prompt str

Base system prompt prepended to all stages.

''
planning_instructions str

Extra instructions for the Plan stage.

''
acting_instructions str

Extra instructions for the Act stage.

''
thinking_instructions str

Extra instructions for the Think stage.

''
reflecting_instructions str

Extra instructions for the Reflect stage.

''
blocks list[Any] | None

Optional PromptBlocks shared across all stages.

None
max_act_steps int

Budget for the Act stage across the run — each model call (every tool-loop round) counts.

12

Returns:

Type Description
PromptGraph

A PromptGraph with plan → act → think → reflect → answer nodes.

build_research_graph

promptise.engine.prebuilts.build_research_graph(search_tools=None, synthesis_tools=None, system_prompt='', *, blocks=None, verify=True, max_search_steps=12, max_verify_rounds=2)

Build a Search → Verify → Synthesize research pipeline.

Three-stage pattern for research tasks:

  1. Search: use search tools to gather information, then summarize the findings
  2. Verify: a model checks the findings against the tool results gathered (structured :class:ResearchVerdict). A failed check sends the run back to search with the issues; verify runs at most max_verify_rounds times, then the run moves on to synthesize.
  3. Synthesize: write the final answer from the findings

Every stage has an iteration budget; a search that exhausts its budget hands over to synthesize, so the run always ends with a written answer.

Parameters:

Name Type Description Default
search_tools list[BaseTool] | None

Tools for the search stage.

None
synthesis_tools list[BaseTool] | None

Optional tools for the synthesis stage.

None
system_prompt str

Base system prompt.

''
blocks list[Any] | None

Optional PromptBlocks shared across stages.

None
verify bool

Whether to include the verification stage.

True
max_search_steps int

Budget for the search stage across the run — each model call (every tool-loop round) counts.

12
max_verify_rounds int

How many times verify may run.

2

Returns:

Type Description
PromptGraph

A PromptGraph with search → verify → synthesize nodes.

build_autonomous_graph

promptise.engine.prebuilts.build_autonomous_graph(node_pool=None, system_prompt='', *, tools=None, max_steps=15, blocks=None, max_tool_rounds=10)

Build an autonomous agent that composes its own reasoning path.

The agent receives a pool of configured nodes and dynamically decides which to execute at each step. If no node_pool is provided, a default pool is created: reason (with the tools), analyze and synthesize (writes the final answer and ends the run).

A chosen step with tools runs its whole tool loop (several model calls and tool calls) before the agent picks again. When the agent finishes without running synthesize, it runs before the run ends, so the reply is always a written answer.

Parameters:

Name Type Description Default
node_pool list | None

Pre-configured nodes the agent can choose from.

None
system_prompt str

Base instructions for the autonomous planner.

''
tools list[BaseTool] | None

Tools for the default reason node (if no pool given).

None
max_steps int

Maximum reasoning steps.

15
blocks list[Any] | None

Optional PromptBlocks for the default nodes.

None
max_tool_rounds int

Model calls per reason step in the default pool.

10

Returns:

Type Description
PromptGraph

A PromptGraph with a single AutonomousNode.

build_deliberate_graph

promptise.engine.prebuilts.build_deliberate_graph(tools=None, system_prompt='', *, blocks=None, max_act_steps=12, **kwargs)

Build a deliberate reasoning graph: Think → Plan → Act → Observe → Reflect → Answer.

Slower but higher quality. The agent thinks before acting, observes results carefully, and reflects before continuing. Plan and reflect produce structured output (:class:PeoatrPlan, :class:PeoatrReflection), so the plan's quality gate and reflect's continue / replan / answer route always reach the engine. Every stage has an iteration budget and the run ends at answer, which writes the reply.

Parameters:

Name Type Description Default
tools list[BaseTool] | None

Tools for the act stage.

None
system_prompt str

Base system prompt prepended to all stages.

''
blocks list[Any] | None

Optional PromptBlocks shared across all stages.

None
max_act_steps int

Budget for the act stage across the run — each model call (every tool-loop round) counts.

12

Returns:

Type Description
PromptGraph

A PromptGraph with think → plan → act → observe → reflect → answer.

build_debate_graph

promptise.engine.prebuilts.build_debate_graph(system_prompt='', *, tools=None, blocks=None, max_rounds=3, max_proposer_steps=12, **kwargs)

Build a debate graph: Proposer ↔ Critic → Judge → Answer.

The proposer answers (with tools), the critic challenges the answer and sends it back for revision when its severity is above 0.3, the judge renders a verdict (a failed verdict also sends it back), and answer writes the reply. Critic and judge produce structured output (:class:DebateCritique, :class:DebateVerdict), so their routing always reaches the engine, and every stage has a budget, so the run always ends with a written answer.

Parameters:

Name Type Description Default
system_prompt str

Base system prompt prepended to the proposer and answer.

''
tools list[BaseTool] | None

Tools the proposer can call.

None
blocks list[Any] | None

Optional PromptBlocks shared across stages.

None
max_rounds int

How many times the critic and the judge may each run.

3
max_proposer_steps int

Budget for the proposer across the run — each model call (every tool-loop round) counts.

12

Returns:

Type Description
PromptGraph

A PromptGraph with proposer → critic → judge → answer nodes.

build_pipeline_graph

promptise.engine.prebuilts.build_pipeline_graph(*nodes)

Build a simple sequential pipeline from a list of nodes.

Usage::

graph = PromptGraph.pipeline(
    planner("plan"),
    web_researcher("search", tools=my_tools),
    summarizer("conclude"),
)

Engine Hooks

Observers that the engine calls at each node boundary. Used for logging, metrics, cycle detection, and budget enforcement.

Hook

promptise.engine.hooks.Hook

Bases: Protocol

Protocol for engine hooks.

All methods are optional — implement only the ones you need. The engine checks hasattr before calling each method.

on_error(node, error, state) async

Called when a node raises. Return a next-node name to redirect, or None to re-raise.

on_graph_mutation(mutation, state) async

Called before a graph mutation is applied. Return False to block it.

on_tool_reused(tool_name, args, result, state) async

Called instead of pre_tool / post_tool when a call is answered from an earlier result without running the tool (tools opted in with ContextCompaction(reuse_results=...), and a repeat of an identical call in one model turn of a ToolNode(deduplicate=True)).

post_node(node, result, state) async

Called after a node executes. Can modify result.

post_tool(tool_name, result, args, state) async

Called after a tool executes. Can modify result. Return modified result.

pre_node(node, state) async

Called before a node executes. Can modify state.

pre_tool(tool_name, args, state) async

Called before a tool executes. Can modify args. Return modified args.

LoggingHook

promptise.engine.hooks.LoggingHook

Log every node execution with timing and token counts.

post_node(node, result, state) async

Log node exit with timing.

pre_node(node, state) async

Log node entry.

TimingHook

promptise.engine.hooks.TimingHook

Enforce per-node time budgets.

If a node exceeds its time budget, the hook sets an error on the result but does NOT abort the graph (the engine's error recovery handles that).

Parameters:

Name Type Description Default
default_budget_ms float

Default time budget in milliseconds.

30000
per_node_budgets dict[str, float] | None

Override budgets per node name.

None
post_node(node, result, state) async

Check if node exceeded its time budget.

pre_node(node, state) async

Record start time.

CycleDetectionHook

promptise.engine.hooks.CycleDetectionHook

Detect infinite loops by tracking visit patterns.

If the same sequence of N nodes repeats max_repeats times, the hook forces the graph to end.

Parameters:

Name Type Description Default
sequence_length int

Length of the pattern to detect.

3
max_repeats int

How many times the pattern must repeat before triggering.

3
post_node(node, result, state) async

No-op for post_node.

pre_node(node, state) async

Check for repeating patterns in visited nodes.

MetricsHook

promptise.engine.hooks.MetricsHook

Collects per-node metrics: tokens, latency, errors, call counts.

Access metrics via hook.metrics dict after execution.

post_node(node, result, state) async

Accumulate metrics.

pre_node(node, state) async

Initialize metrics for this node.

reset()

Clear all metrics.

summary()

Return collected metrics.

BudgetHook

promptise.engine.hooks.BudgetHook

Enforces a token and/or cost budget for each graph run.

After every node, the hook adds the node's tokens to the run's total (GraphState.total_tokens, so the budget is per run: one hook on an engine caps every run separately, also concurrent ones). When the run goes over max_tokens or max_cost_usd, the hook ends the run (state.current_node = "__end__") and writes the reason to the node's NodeResult.error.

A run the budget stops after the model asked for tools, before it saw their results, has no answer: the engine raises :class:~promptise.engine.GraphBudgetExceededError instead of returning the last tool result. A run whose model answered on the node that went over budget returns that answer.

Attributes:

Name Type Description
tokens_used

Tokens of the latest run (as of its last node).

cost_used

Estimated cost of the latest run, from tokens_used and cost_per_1k_tokens.

post_node(node, result, state) async

Track the run's tokens and enforce the budget.

pre_node(node, state) async

Check budget before node executes.


Preprocessors and Postprocessors

Pluggable functions that run before/after a node's LLM call. Compose with chain_preprocessors and chain_postprocessors.

context_enricher

promptise.engine.processors.context_enricher(*, include_timestamp=True, include_iteration=True)

Add timestamp and iteration info to state.context.

state_summarizer

promptise.engine.processors.state_summarizer(*, max_context_chars=2000)

Truncate long string values in state.context to save tokens.

input_validator

promptise.engine.processors.input_validator(*, required_keys)

Validate that required keys exist in state.context.

json_extractor

promptise.engine.processors.json_extractor(*, keys=None)

Parse JSON from LLM output string, optionally filter to specific keys.

confidence_scorer

promptise.engine.processors.confidence_scorer()

Add a confidence score based on hedging language in text output.

state_writer

promptise.engine.processors.state_writer(*, fields)

Write specific output fields to state.context keys.

Parameters:

Name Type Description Default
fields dict[str, str]

Mapping of output_key → state.context_key. E.g. {"answer": "final_answer", "confidence": "score"}

required

output_truncator

promptise.engine.processors.output_truncator(*, max_chars=4000)

Truncate output text to a maximum length.

chain_preprocessors

promptise.engine.processors.chain_preprocessors(*fns)

Combine multiple preprocessors into one. Runs in order.

chain_postprocessors

promptise.engine.processors.chain_postprocessors(*fns)

Combine multiple postprocessors into one. Pipes output through each.


Conversations

ConversationStore

promptise.conversations.ConversationStore

Bases: Protocol

Interface for conversation persistence backends.

All methods are async. Implementations must handle their own thread-safety and connection management. Call :meth:close when the store is no longer needed.

Security note: Stores do NOT enforce session ownership internally. Ownership checks happen at the PromptiseAgent layer via _enforce_ownership() before calling store methods. If you use stores directly (outside of build_agent()), you MUST verify session ownership yourself by calling get_session() and checking user_id before load_messages() or delete_session().

Implementing a custom store requires four data methods plus :meth:close::

class MyStore:
    async def load_messages(self, session_id: str) -> list[Message]: ...
    async def save_messages(self, session_id: str, messages: list[Message]) -> None: ...
    async def delete_session(self, session_id: str) -> bool: ...
    async def list_sessions(self, *, user_id=None, limit=50, offset=0) -> list[SessionInfo]: ...
    async def close(self) -> None: ...

close() async

Release resources (connections, file handles).

delete_session(session_id) async

Delete a session and all its messages.

Returns True if the session existed and was deleted.

get_session(session_id) async

Return session metadata, or None if the session does not exist.

Used for ownership checks before loading messages.

list_sessions(*, user_id=None, limit=50, offset=0) async

List sessions, optionally filtered by user.

Returns sessions ordered by updated_at descending (most recent first).

load_messages(session_id) async

Load all messages for a session, ordered by creation time.

Returns an empty list if the session does not exist.

save_messages(session_id, messages) async

Persist the full message list for a session.

Creates the session if it does not exist. Replaces all messages if the session already exists.

Message

promptise.conversations.Message dataclass

A single conversation message.

Attributes:

Name Type Description
role str

The message role ("user", "assistant", "system", "tool").

content str

The message text.

metadata dict[str, Any]

Optional metadata (tool calls, token counts, latency, etc.).

created_at datetime

UTC timestamp. Defaults to now.

from_dict(data) classmethod

Deserialize from a plain dict.

to_dict()

Serialize to a plain dict.

SessionInfo

promptise.conversations.SessionInfo dataclass

Metadata about a conversation session.

Attributes:

Name Type Description
session_id str

Unique session identifier.

user_id str | None

Optional user identifier for multi-user applications.

title str | None

Optional human-readable title.

message_count int

Number of messages in the session.

created_at datetime

When the session was first created.

updated_at datetime

When the session was last updated.

metadata dict[str, Any]

Application-specific metadata (tags, source, etc.).

expires_at datetime | None

Optional expiry time for data-retention enforcement. None means the session never expires automatically.

Built-in Stores

promptise.conversations.InMemoryConversationStore

Dict-backed conversation store for testing and prototyping.

No persistence. All data lives in-process and is lost when the process exits.

Parameters:

Name Type Description Default
max_sessions int

Maximum sessions to keep. Oldest evicted first. 0 means unlimited.

0
max_messages_per_session int

Maximum messages per session. Oldest messages are dropped when the limit is reached. 0 means unlimited.

0
cleanup_expired() async

Remove all expired sessions. Returns the count of sessions removed.

Call periodically for data-retention enforcement.

close() async

No-op for in-memory store.

delete_session(session_id) async

Delete a session and its messages.

get_session(session_id) async

Return session metadata or None.

list_sessions(*, user_id=None, limit=50, offset=0) async

List sessions ordered by most recently updated. Expired sessions excluded.

load_messages(session_id) async

Load all messages for a session. Returns empty for expired sessions.

save_messages(session_id, messages) async

Persist messages, creating the session if needed.

session_count()

Return the number of active sessions.

update_session(session_id, *, user_id=..., title=..., metadata=...) async

Update session metadata.

Only provided fields are updated. Returns True if the session existed.

promptise.conversations.SQLiteConversationStore

SQLite-backed conversation store using aiosqlite.

Auto-creates tables on first use. Requires pip install aiosqlite.

Parameters:

Name Type Description Default
path str

Path to the SQLite database file. Use ":memory:" for in-memory storage with full SQL semantics.

'conversations.db'
table_prefix str

Prefix for table names.

'promptise_'
max_messages_per_session int

Rolling window limit. 0 = unlimited.

0
close() async

Close the database connection.

delete_session(session_id) async

Delete a session and all its messages.

get_session(session_id) async

Return session metadata or None.

list_sessions(*, user_id=None, limit=50, offset=0) async

List sessions ordered by most recently updated.

load_messages(session_id) async

Load all messages for a session.

save_messages(session_id, messages) async

Persist messages, creating the session if needed.

update_session(session_id, *, user_id=..., title=..., metadata=...) async

Update session metadata fields.

promptise.conversations.PostgresConversationStore

PostgreSQL-backed conversation store using asyncpg.

Auto-creates tables and indexes on first use. Requires pip install asyncpg.

Parameters:

Name Type Description Default
dsn str

PostgreSQL connection string (e.g. "postgresql://user:pass@localhost/mydb").

required
table_prefix str

Prefix for table names. Defaults to "promptise_" so multiple applications can share one database.

'promptise_'
pool_min int

Minimum connection pool size.

2
pool_max int

Maximum connection pool size.

10
max_messages_per_session int

Rolling window limit. 0 = unlimited.

0
close() async

Close the connection pool.

delete_session(session_id) async

Delete a session and all its messages (cascade).

get_session(session_id) async

Return session metadata or None.

list_sessions(*, user_id=None, limit=50, offset=0) async

List sessions ordered by most recently updated.

load_messages(session_id) async

Load all messages for a session.

save_messages(session_id, messages) async

Persist messages, creating the session if needed.

update_session(session_id, *, user_id=..., title=..., metadata=...) async

Update session metadata fields. Only provided fields are changed.

promptise.conversations.RedisConversationStore

Redis-backed conversation store using redis.asyncio.

Stores sessions and messages as JSON in Redis hashes and sorted sets. Ideal for ephemeral sessions, caching, or high-throughput scenarios.

Requires pip install redis.

Parameters:

Name Type Description Default
url str

Redis connection URL (e.g. "redis://localhost:6379").

'redis://localhost:6379'
key_prefix str

Prefix for all Redis keys.

'promptise:'
ttl int

Default TTL in seconds for session data. 0 = no expiry.

0
max_messages_per_session int

Rolling window limit. 0 = unlimited.

0
close() async

Close the Redis connection.

delete_session(session_id) async

Delete a session from Redis.

get_session(session_id) async

Return session metadata or None.

list_sessions(*, user_id=None, limit=50, offset=0) async

List sessions from Redis sorted set index.

load_messages(session_id) async

Load all messages for a session from a Redis sorted set.

save_messages(session_id, messages) async

Persist messages as a Redis list.

update_session(session_id, *, user_id=..., title=..., metadata=...) async

Update session metadata in Redis hash.