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 |
None
|
model
|
ModelLike
|
REQUIRED. A LangChain chat model instance, a provider id
string such as |
required |
instructions
|
str | Any | None
|
Optional system prompt. Defaults to the built-in
|
None
|
identity
|
AgentIdentity | None
|
Optional :class: |
None
|
trace_tools
|
bool
|
Print each tool invocation and result to stdout. Covers
MCP tools, cross-agent tools, sandbox tools and |
False
|
cross_agents
|
Mapping[str, CrossAgent] | None
|
Optional mapping of peer name → CrossAgent. Each
peer is exposed as an |
None
|
include_broadcast
|
bool
|
Also add a |
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: |
DEFAULT_MAX_DELEGATION_DEPTH
|
delegation_timeout
|
float | None
|
Seconds to wait for a peer's answer when its
:class: |
None
|
memory
|
Any | None
|
Optional :class: |
None
|
memory_auto_store
|
bool
|
When |
False
|
memory_max_results
|
int
|
Maximum memories injected per invocation
(the |
5
|
memory_min_score
|
float
|
Drop search results scoring below this
relevance ( |
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: |
5.0
|
sandbox
|
bool | dict[str, Any] | None
|
Optional sandbox configuration ( |
None
|
observer
|
Any | None
|
Optional :class: |
None
|
observer_agent_id
|
str | None
|
Agent identifier for tool-event recording. Also
the |
None
|
observe
|
bool | Any | None
|
Plug-and-play observability. Can be:
- |
None
|
extra_tools
|
list[BaseTool] | None
|
Optional additional :class: |
None
|
expose_resources
|
bool
|
Also give the agent the MCP servers' resources:
adds a |
False
|
expose_prompts
|
bool
|
Also give the agent the MCP servers' prompts: adds a
|
False
|
flow
|
Any | None
|
Optional :class: |
None
|
conversation_store
|
Any | None
|
Optional
:class: |
None
|
conversation_max_messages
|
int
|
Maximum messages to keep per session
when using the conversation store. |
0
|
context_engine
|
Any | None
|
Optional :class: |
None
|
context_compaction
|
bool | int | Any | None
|
How long tool loops are compacted
(:class: |
None
|
approval
|
Any | None
|
Human-in-the-loop approval. An
:class: |
None
|
guardrails
|
Any | None
|
Optional input/output scanner — a
:class: |
None
|
max_agent_iterations
|
int
|
The most node executions one run may make
(the engine's |
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'
|
code_action
|
Any | None
|
Options for |
None
|
events
|
Any | None
|
Optional :class: |
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 ( |
None
|
adaptive
|
Any | None
|
Adaptive strategy (learning from failed tool calls):
|
None
|
agent_pattern
|
str | Any | None
|
The reasoning graph: a :class: |
None
|
pattern
|
str | Any | None
|
Deprecated alias for |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
PromptiseAgent
|
|
PromptiseAgent
|
Use |
|
PromptiseAgent
|
|
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: |
|
provider |
The :class: |
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 |
required |
config
|
dict[str, Any] | None
|
LangGraph config dict (callbacks, etc.). |
None
|
session_id
|
str | None
|
Optional conversation ID. With a conversation
|
None
|
caller
|
CallerContext | None
|
Optional :class: |
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 |
required |
config
|
dict[str, Any] | None
|
LangChain config dict (callbacks, etc.). |
None
|
caller
|
CallerContext | None
|
Optional :class: |
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 |
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: |
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: |
required |
user_id
|
str | None
|
Optional user identifier. Shorthand for
|
None
|
caller
|
CallerContext | None
|
Optional :class: |
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 |
RuntimeError
|
If the conversation store fails while the
session's ownership is being checked. |
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 |
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
|
limit
|
int
|
Maximum sessions to return. |
50
|
offset
|
int
|
Pagination offset. |
0
|
Returns:
| Type | Description |
|---|---|
list[Any]
|
List of :class: |
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
|
|
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If no conversation store is configured. |
SessionAccessDenied
|
If |
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 |
SessionAccessDenied
|
If |
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. |
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: |
bearer_token |
str | None
|
JWT or OAuth token for the caller. Sent as
|
roles |
set[str]
|
Caller's roles (e.g. |
scopes |
set[str]
|
OAuth scopes (e.g. |
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 |
required |
max_iterations
|
int
|
Maximum node executions per run: |
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 |
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 |
True
|
max_mutations_per_run
|
int
|
Cap on graph mutations per run. |
10
|
tools
|
list[Any] | None
|
Tools for nodes with |
None
|
compaction
|
Any | None
|
:class: |
None
|
tool_summary
|
str | None
|
The "Available tools" block that |
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 theAIMessageChunk.on_tool_start/on_tool_end/on_tool_error-- one start and one end per tool call, sharing arun_id.on_tool_endcarriesdata["status"]("success"or"error"when the tool reported an error, e.g. an MCPToolError) anddata["duration_ms"];on_tool_errormeans 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: |
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 |
|
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 |
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 |
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 |
turn_question |
Any
|
The current user question: the last
|
turn_input |
list[Any]
|
The input up to and including |
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 |
node_name |
str
|
Target node name (for add/remove node). |
node_config |
dict[str, Any]
|
Configuration dict for new nodes (used with
|
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
|
''
|
description
|
str
|
Short description shown in graph
visualization and |
''
|
transitions
|
dict[str, str] | None
|
Mapping of output keys to next-node names.
E.g. |
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
¶
@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 |
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
|
tool_choice
|
str
|
|
'auto'
|
inject_tools
|
bool
|
When |
False
|
tool_summary
|
str | None
|
An "Available tools" block in the system prompt, on
top of the tool definitions the model receives anyway:
|
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 |
None
|
output_key
|
str | None
|
Key to write this node's output to in |
None
|
inherit_context_from
|
str | None
|
Name of a previous node whose output
should be injected as context. Reads from
|
None
|
preprocessor
|
Callable | None
|
Async callable |
None
|
postprocessor
|
Callable | None
|
Async callable |
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_ledger_after
|
int | None
|
Tool results before an |
None
|
compaction
|
Any | None
|
:class: |
None
|
inject_tools
property
¶
Whether this node receives MCP tools at runtime.
execute(state, config)
async
¶
Execute the full node pipeline:
- Preprocessor (custom data transformation)
- Context assembly (blocks + input_keys + inherited context + state)
- LLM call (with tools if configured)
- Tool execution (auto-loop if tools called)
- Postprocessor (custom output transformation)
- Guards (validate output)
- 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 |
True
|
deduplicate
|
bool
|
Run identical (tool, args) calls of one model turn
once. Each repeat in that turn still gets its own
|
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
|
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 |
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'
|
merge_fn
|
Callable | None
|
Custom merge function (when merge_strategy is
|
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 |
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 |
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 |
None
|
output_key
|
str
|
Key in |
'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 |
None
|
inherit_state
|
bool
|
If |
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:
- Sees all available nodes as a menu of capabilities
- Decides which node to execute next based on the task
- Executes that node (with its tools, blocks, guards)
- Evaluates the result
- 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.
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 ( |
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 — |
None
|
sandbox_factory
|
SandboxFactory | None
|
|
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 |
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
|
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
¶
FanOutNode¶
promptise.engine.reasoning_nodes.FanOutNode
¶
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 |
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
|
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 |
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
|
|
None
|
max_repairs
|
int
|
Times to feed a failed program (a crash, or one that
reached |
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 |
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:
- Plans subgoals and rates the plan (re-plans below quality 3)
- Executes tools to achieve the active subgoal
- Analyzes the results (think): another tool call now, or reflect
- Reflects on progress and routes: answer, replan, or continue
- 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 |
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:
- Search: use search tools to gather information, then summarize the findings
- 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. - 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 |
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 |
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 |
10
|
Returns:
| Type | Description |
|---|---|
PromptGraph
|
A |
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 |
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 |
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
¶
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
|
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
|
MetricsHook¶
promptise.engine.hooks.MetricsHook
¶
Collects per-node metrics: tokens, latency, errors, call counts.
Access metrics via hook.metrics dict after execution.
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 |
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. |
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 ( |
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. |
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.
|
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
|
max_messages_per_session
|
int
|
Maximum messages per session. Oldest
messages are dropped when the limit is reached. |
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 |
'conversations.db'
|
table_prefix
|
str
|
Prefix for table names. |
'promptise_'
|
max_messages_per_session
|
int
|
Rolling window limit. |
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. |
required |
table_prefix
|
str
|
Prefix for table names. Defaults to |
'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
|
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'
|
key_prefix
|
str
|
Prefix for all Redis keys. |
'promptise:'
|
ttl
|
int
|
Default TTL in seconds for session data. |
0
|
max_messages_per_session
|
int
|
Rolling window limit. |
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.