Auto-generated reference¶
Every entry below is rendered straight from its docstring in the source — signature, type hints, and the actual rationale text, not a hand-maintained copy of it. If this page and api.md or a docstring ever disagree, the docstring is the truth; open an issue.
Grouped the same way as the roadmap's API-stability tiers: Core first, then "in the box". Recipes stay documented by example in recipes.md rather than duplicated here — a recipe's value is in how it's used, not its bare signature.
Core¶
Context¶
Context ¶
Central artifact store with an event queue.
Git-like model: every applied commit forms a new Context version (head). Commits chain through parent_id and carry a reads/writes trace — the actual agent linkage via consumes/produces.
Source code in reactifact/context/context.py
request
property
¶
The active turn's request mapping (empty when none was set).
Read-only view of reactifact.request.current_request() — the same
data a produce sees as call.request. Exposed on Context so hooks
that receive only a context (e.g. ChatAssistant(create_message=…))
can read per-turn metadata (category, tenant, user) without a
hand-rolled ContextVar of their own.
persisted_version
property
¶
Highest version written by the last mark_persisted call
(-1 when this Context has never been persisted).
persisted_compacted_at
property
¶
Compaction baseline written by the last mark_persisted call.
compacted_at
property
¶
Highest version collapsed into the baseline (0 = never compacted).
persisted_state ¶
(version, compacted_at) already written for key.
(-1, 0) when this Context has never been persisted under that key.
mark_persisted ¶
Records that the Context has been persisted up to version (head
when omitted) for key, including its current compaction baseline.
Source code in reactifact/context/context.py
announce ¶
Publishes a progress event to active streams (no-op without subscribers).
kind is a category for the application: "status" (domain agent statuses),
"agent" (internal, from framework producers like ToolUse).
The application itself decides which kinds to show the user.
Source code in reactifact/context/context.py
create ¶
Creates a new artifact and generates an ARTIFACT_CREATED event.
If a stable id is given and an artifact with it already exists, returns the existing one without creating a duplicate or an event (idempotency, §42).
Source code in reactifact/context/context.py
get ¶
update ¶
Updates artifact data, creates a new version, generates ARTIFACT_UPDATED.
If the data did not change, the version and the event are left untouched: no-op patches must not cascade into reactions (§41, §42).
Source code in reactifact/context/context.py
delete ¶
Deletes the artifact and generates ARTIFACT_DELETED.
Source code in reactifact/context/context.py
list_artifacts ¶
Returns a list of artifacts, optionally filtered by data type.
Preserves insertion order (matters: ties in a caller's own sort key,
e.g. updated_at, break in creation order, same as before this
method stopped isinstance-scanning every artifact).
Source code in reactifact/context/context.py
latest ¶
The most recently created artifact of a type, or None.
Sugar over list_artifacts for "grab the latest answer/finding" —
the common read after a run.
Source code in reactifact/context/context.py
view ¶
view(
artifact_type: type[BaseModel]
| tuple[type[BaseModel], ...]
| None = None,
*,
condition: Callable[[Artifact[Any]], bool]
| None = None,
limit: int | None = None,
) -> View
Artifact projection for the agent (§27): by type/condition/limit.
The View does not copy state — it is references to artifacts plus
serialization for the prompt. tokens_estimate lets the agent stay within
the token budget (§58): build the view, check the estimate, reduce limit
if needed.
Source code in reactifact/context/context.py
link ¶
Establishes a link source_id —relation→ target_id (idempotently, §42).
unlink ¶
Removes links; relation/target_id = None mean "any".
Source code in reactifact/context/context.py
relations ¶
relations(
source_id: str | None = None,
relation: str | None = None,
target_id: str | None = None,
) -> list[Relation]
All links, optionally filtered by any edge component.
Source code in reactifact/context/context.py
incoming ¶
Links pointing at target_id (for provenance: who references what).
related ¶
Target artifacts of outgoing links (existing ones; "dangling" ones are skipped).
Source code in reactifact/context/context.py
dangling_relations ¶
Links with a non-existent source or target (§69): the state is visible, not hidden in a string.
Source code in reactifact/context/context.py
drain_events ¶
pending_events ¶
A snapshot of the queued events, without consuming them.
Non-destructive counterpart of drain_events() (which stays for
callers that explicitly want to flush the queue — tests, seeding).
The runtime reads a batch with this, schedules the generation, and
only calls consume_events() once the generation's patches are
committed: a generation that raises leaves its triggers queued
instead of silently dropping the consumers they would have woken, so
a retry re-runs the work rather than settling at zero runs on an
empty queue.
Source code in reactifact/context/context.py
consume_events ¶
Removes a batch previously read with pending_events().
Matching is by identity, not by count: events appended while the generation ran — its commits' effects, staged for the next generation — are preserved, and it stays correct even if a produce drained or reordered the queue directly during the generation.
Source code in reactifact/context/context.py
clone ¶
Deep copy of this context's live state. See reactifact.branching.
merge_from ¶
Two-way merge, no conflict detection. See reactifact.branching.
branch ¶
Forks an isolated copy for alternative state exploration (§39).
The fork records a snapshot of its base, so a later merge of two
fork-mates can detect diverged artifacts three-way (§40). The branch
shares resources with the parent but is otherwise fully independent:
subsequent changes on either side do not affect the other. Algorithm
lives in reactifact.branching.fork_context.
Source code in reactifact/context/context.py
merge ¶
Merges other into self with explicit conflicts, atomically (§40).
Three-way merge against the shared fork base (the fork snapshot of
other, or of self when other has none) — raises MergeConflict
(reactifact.branching.MergeConflict, re-exported as reactifact.MergeConflict)
rather than silently choosing a side. Algorithm lives in
reactifact.branching.merge_contexts.
Source code in reactifact/context/context.py
log_commit ¶
Applies the commit to the repository: fills in parent/version, moves head.
Source code in reactifact/context/context.py
apply_commit ¶
Applies a commit's operations to the live state and logs it.
This is the load path for the append-only session format (snapshot +
commit log): it replays operations without emitting events (a
restored session must not re-trigger its own agents). reindex=False
skips the final full re-index — use it when applying several commits
and call rebuild_indices() once at the end.
Source code in reactifact/context/context.py
rebuild_indices ¶
Rebuilds the type/staleness indices from the live state.
Used after a bulk load (append-only replay) that bypasses the
incremental create/update/delete paths.
Source code in reactifact/context/context.py
replace_events ¶
commit_graph ¶
history ¶
compact ¶
Bounds memory: squash old commit history into a baseline snapshot.
Keeps the last keep_commits commits replayable and collapses
everything older into a baseline; the absolute version/head_id and
therefore context_hash are unchanged (this is the history of how
state got here, not the state). With keep_versions, each artifact's
retained version history is also trimmed to its last keep_versions
(0 drops it) — Artifact.version itself does not change.
Irreversible, like squashing git history: checkout/diff below the
resulting baseline raise, and provenance for artifacts last written
before it is no longer available. Call it between turns. Returns the
number of commits collapsed.
Source code in reactifact/context/context.py
diff ¶
State delta between two Context versions.
Returns {"added": {id: data}, "removed": {id: data}, "changed": {id: {old, new}}}. The diff compares the versioned state (commits); artifacts created directly outside commits (the "working tree") do not participate.
Source code in reactifact/context/context.py
snapshot ¶
Consistent snapshot of the current artifact state (id → data).
artifact_version ¶
One immutable artifact revision from the version store.
version is the artifact's absolute version counter (0 = create).
Compaction is authoritative: trimmed versions return None. The
current version resolves the producing commit lazily when the record
was written before the commit existed (the runtime path).
Source code in reactifact/context/context.py
version_history ¶
All readable revisions of an artifact, oldest to newest.
Source code in reactifact/context/context.py
find_versions ¶
Every stored revision with this payload hash (content addressing).
at ¶
An immutable snapshot view of this Context at a commit version.
self is not mutated (unlike checkout); the returned Context is a
detached copy with no pending events and no step cursors. Useful for
reading a historical state without rewinding the live run.
Source code in reactifact/context/context.py
checkout ¶
Moves head back to a previous version (rollback along the commit chain).
Artifacts not part of the versioned history (created directly outside commits — the "working tree") are preserved.
Source code in reactifact/context/context.py
to_kv
async
¶
Serializes and stores this context under key in a KV backend.
The one to_dict() round-trip shared by SessionStore/BranchStore
(session_id / branch keys are just a naming convention over the same
backend, §39) — call this instead of hand-rolling backend.set(key,
context.to_dict()).
Source code in reactifact/context/context.py
from_kv
async
classmethod
¶
Loads a context previously stored with to_kv, or None if absent.
Source code in reactifact/context/context.py
Artifact¶
Artifact
dataclass
¶
Artifact(
data: TData,
id: str | None = None,
created_by_commit: str | None = None,
*,
version: int = 0,
content_hash: str | None = None,
created_at: datetime | None = None,
updated_at: datetime | None = None,
store: ArtifactStore | None = None,
)
Bases: Generic[TData]
An immutable snapshot/handle for one artifact version.
data is a Pydantic payload; _store is an internal reference used by the
deprecated history/at/diff bridges. Mutation goes through Context.
Source code in reactifact/artifacts.py
history
property
¶
Previous versions (deprecated bridge; use Context.version_history).
get_all_versions ¶
at ¶
One retained version (deprecated bridge; use Context.artifact_version).
Source code in reactifact/artifacts.py
diff ¶
Diff two retained versions by list index (deprecated bridge).
Source code in reactifact/artifacts.py
to_dict ¶
Legacy v1 serialization; Context.to_dict still uses it.
Source code in reactifact/artifacts.py
Effects¶
Effects ¶
The current produce's effect set (creates/updates/links to commit once).
Source code in reactifact/effects.py
create ¶
Plans a new artifact; returns a linkable/updatable handle (§38).
If id names an artifact that already exists when this effect is
applied, the runtime treats it as a refresh (a new version of the
same logical entity, §42/§43) rather than creating a duplicate — the
same rule Runtime._apply_patch documents. Use upsert at the call
site when that "may already exist" intent should be explicit instead
of implicit in a plain create(..., id=...).
Source code in reactifact/effects.py
create_once ¶
Idempotent create (§42): None if id already exists in the
context, otherwise the same as create(data, id=id).
Folds the "already done" guard every produce needs for a re-derived
id (f"answer:{qid}") into the call itself:
handle = effects.create_once(Answer(...), id=f"answer:{qid}")
if handle is None:
return None # already answered — nothing to do
instead of a separate if context.get(f"answer:{qid}") is not None:
return None above the call — one less place to get the id string
wrong between the guard and the create.
Source code in reactifact/effects.py
create_once_from ¶
create_once_from(
source: Artifact[Any] | Handle | str,
data: Any,
*,
prefix: str | None = None,
) -> Handle | None
Idempotent create whose id is derived from another artifact (§42).
Folds the single most common way a re-derivable id is built by hand —
f"answer:{question.id}", f"review:{pr.id}" — into the call, so a
produce doesn't have to invent the id string (nor get it subtly wrong
between its guard and its create):
answer = effects.create_once_from(question, Answer(...))
if answer is None:
return None # already answered this question
prefix defaults to the created model's class name, lowercased
(Answer → answer:{source_id}). source may be an Artifact, an
effects Handle, or a plain id string. None back means an artifact
with the derived id already exists — skip, exactly like create_once.
Source code in reactifact/effects.py
upsert ¶
Explicit create-or-refresh: same effect as create(data, id=id).
Purely a call-site name: identical to create(..., id=...), but says
at the call site that a refresh is an expected outcome, not a
surprise — reach for it when the artifact may already exist (e.g. a
re-derived id like f"answer:{qid}").
Source code in reactifact/effects.py
update ¶
Bumps fields of an existing artifact (a new version).
Note the name means something different here than on Patch.update/
Context.update/Artifact.update (a full data replacement) — this is
the one intentional exception, matching Patch.update_fields
instead. Effects is the everyday authoring surface where "update
some fields" is the common case (§18 above), so it gets the short
name; the lower-level, less-used Patch/Context/Artifact surface
keeps update for the operation it's actually named after.
Source code in reactifact/effects.py
ask ¶
ask(
question: str,
*,
kind: str = "general",
notes: dict[str, Any] | None = None,
id: str | None = None,
) -> Handle
Poses a question to a human (HITL, §60): creates a PendingQuestion.
Returns a handle you can link later; the human answer is recorded via
effects.resume(question_art, resolution) (§60). Pass id for a
stable, re-derivable question (e.g. f"steer:{qid}:{round}") so a
guard like if context.get(id) is not None: return None can stop the
produce from asking again while the question is still unanswered.
Source code in reactifact/effects.py
act ¶
act(
kind: str,
*,
key: str,
payload: dict[str, Any] | None = None,
id: str | None = None,
) -> Handle | None
Records an outbound side effect intent — a PendingAction (outbox).
Returns None if an action with the same id already exists (idempotent
re-run, §42). The produce must not perform the I/O itself: the
runtime dispatches pending actions via Runtime(dispatcher=...) after
the intent is committed, so replay/retry/merge cannot re-send it.
key is the stable idempotency key (derived from the action's content
and context, e.g. f"notify:{order_id}:{channel}"); the artifact id
defaults to f"action:{key}", and key travels in the payload to the
external system.
Source code in reactifact/effects.py
resume ¶
Records the human answer on a PendingQuestion (HITL, §60).
Source code in reactifact/effects.py
to_patch ¶
Patch¶
Patch ¶
An ordered set of operations to apply to the Context (§12).
Source code in reactifact/patches.py
update_fields ¶
Update artifact fields without rebuilding the model.
Sugar over update: it does the model_copy(update=fields) itself and puts
the full new model into Update (preserving commit replayability).
Source code in reactifact/patches.py
merge ¶
Adds operations from patches to this patch; None are skipped.
Returns self (chaining, like add/create/update/delete).
Source code in reactifact/patches.py
Agent¶
Agent ¶
Agent(
name: str | None = None,
triggers: list[Trigger] | None = None,
priority: int | None = None,
*,
steps: Sequence[Step[Any]] | None = None,
wakes: bool
| Callable[[Event, Context | None], bool]
| None = None,
)
Bases: ABC
Base container: consumes some artifacts and produces others.
If run is not overridden, automatically collects inputs according to
consumes and calls produce on every produce, merging the patches.
Source code in reactifact/agents.py
matching_triggers ¶
Every trigger that matches event — matches() is just
bool(...) of this. Runtime._arun_once_impl uses the full list
(not just the bool) to decide whether every matching trigger asks
for debouncing (Trigger.debounce) before collapsing repeat events
into one run.
Source code in reactifact/agents.py
collect_inputs ¶
Public access to the consumed artifacts.
Used by the runtime to record the reads linkage (provenance) on run.
When the agent has no explicit consumes, this is the union of its
steps' own consumes.
Source code in reactifact/agents.py
run
async
¶
Default: runs self.steps via execute() (the effects-first
path — write a Step subclass or @step function instead of
overriding this).
Overriding run() to return a Patch by hand is a low-level,
internal escape hatch for cases effects genuinely can't express —
not a third everyday produce style (see reactifact.produce's module
docstring for the two you should reach for first). No example in
this repo overrides it; only this repo's own tests do.
Source code in reactifact/agents.py
execute
async
¶
Runs the agent's produces (usually on an event).
Effects-first (§24): class steps write effects.*, function steps call.effects.*
and return None; the runtime compiles the effect slot into one
atomic patch. This method
only runs the produces — it does not build a patch. (run remains the
agent-level escape hatch for custom Agent subclasses that assemble a
change-set by hand; the runtime merges its result after the effects.)
Source code in reactifact/agents.py
Produce¶
Consume¶
Consume ¶
Consume(
artifact_type: ArtifactType | None = None,
condition: Callable[[Artifact[Any]], bool]
| None = None,
event_types: Sequence[EventType] | None = None,
*,
wakes: bool | None = None,
debounce: bool | None = None,
include_subclasses: bool | None = None,
)
Describes the consumed artifact type, condition and triggering events.
All parameters can be set as class attributes (for inheritance) or passed to the constructor.
wakes (default True): whether this Consume also contributes to the
agent's triggers (Agent.matches() — does the agent run at all).
False means "read this as input, but don't wake up on it" — e.g. an
agent that should run when a Question arrives but also wants the
existing ConversationHistory as input, without re-running once per
history artifact. For the declarative style (consumes/produces,
the common case), this covers the one real reason to decouple "what
wakes me" from "what I read" without reaching for Agent's separate
triggers= override. triggers= itself still exists and is not
deprecated — it's the only option for the imperative style (an Agent
subclass that overrides run() directly and has no consumes at all to
attach a condition to; see Agent.triggers's own docstring).
include_subclasses (default False): wake on artifacts whose type is a
subclass of artifact_type, not only the exact type — the declarative
form of a marker base / protocol base (Consume(ChatMessage,
include_subclasses=True) fires for UserQuery and Answer). collect()
already reads by issubclass (Context.list_artifacts), so the flag only
affects triggering. Opt-in so an agent subscribed to both a base and a
concrete type never wakes twice for the same event.
debounce (default False): when several events in the same
generation would each independently wake this agent via this Consume
(a fan-out step creating five Evidence artifacts in one commit, five
separate ARTIFACT_CREATED events), collapse them into a single run
instead of five — the fifth (last) event is what the runtime happens to
pass as event, but a debounced produce should read inputs
(collected fresh from Context regardless of which event triggered the
run) rather than rely on event for "what changed", since that's
exactly the information debouncing discards. See
Runtime._arun_once_impl for where the collapsing actually happens —
Consume/Trigger only carry the flag, they don't implement it, since
collapsing needs to compare other events in the same drained batch,
which is Runtime-level state neither of them has access to.
Source code in reactifact/consume.py
to_triggers ¶
Converts into a list of triggers for automatic reaction.
Empty when wakes=False — this Consume still feeds
Agent._collect_inputs(), it just never causes Agent.matches()
to fire on its own.
Source code in reactifact/consume.py
collect ¶
The artifacts this Consume contributes to an agent's inputs.
Agent._collect_inputs() calls this once per entry in consumes —
overridden by CorrelatedConsume to return a correlated group across
several types instead of one type's own matching instances.
Source code in reactifact/consume.py
by_status
classmethod
¶
by_status(
artifact_type: ArtifactType,
status: str,
event_types: Sequence[EventType] = (
EventType.ARTIFACT_CREATED,
EventType.ARTIFACT_UPDATED,
),
*,
wakes: bool = True,
debounce: bool = False,
) -> Consume
Creates a Consume with a condition on the equality of the status field.
Source code in reactifact/consume.py
by_base
classmethod
¶
by_base(
artifact_type: ArtifactType,
event_types: Sequence[EventType] = (
EventType.ARTIFACT_CREATED,
EventType.ARTIFACT_UPDATED,
),
*,
wakes: bool = True,
debounce: bool = False,
) -> Consume
Creates a Consume that also wakes on subclasses of artifact_type.
Sugar for Consume(artifact_type, include_subclasses=True) — e.g. a
marker base ChatMessage with concrete UserQuery/Answer artifacts.
Source code in reactifact/consume.py
by_field
classmethod
¶
by_field(
artifact_type: ArtifactType,
field: str,
value: Any,
event_types: Sequence[EventType] = (
EventType.ARTIFACT_CREATED,
EventType.ARTIFACT_UPDATED,
),
*,
wakes: bool = True,
debounce: bool = False,
) -> Consume
Creates a Consume with a condition on the equality of an arbitrary field.
Source code in reactifact/consume.py
Runtime¶
Runtime ¶
Runtime(
context: Context,
agents: list[Agent] | None = None,
max_concurrency: int | None = None,
session: Session | None = None,
budget: Budget | None = None,
tracer: Tracer | list[Tracer] | None = None,
scheduler: Scheduler | None = None,
isolate_errors: bool = False,
on_agent_error: Callable[
[Agent, Event, BaseException], None
]
| None = None,
session_save_policy: Literal[
"per_commit", "per_turn"
] = "per_commit",
dispatcher: Dispatcher | None = None,
on_dispatch_error: DispatchErrorHandler | None = None,
)
Source code in reactifact/runtime/runtime.py
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | |
request_stop ¶
Ask the run to stop at the next generation boundary (§59).
The in-flight generation finishes (its commit lands); no new generation
starts, and the run ends with RunOutcome.STOPPED. For a bounded wait
plus a forced cancel, use ashutdown().
Source code in reactifact/runtime/runtime.py
ashutdown
async
¶
Graceful shutdown: stop at a boundary, then wait for the turn to end.
Calls request_stop() first, then awaits the in-flight turn (if any).
With timeout, force-cancels the turn after that many seconds. Does
not close shared RuntimeResources — close those yourself (a
ResourceScope, or await resources.aclose() in your own lifespan).
Source code in reactifact/runtime/runtime.py
flush_pending_actions
async
¶
Dispatches committed-but-undispatched actions without a generation.
The explicit outbox flush after context.merge() (merge is a pure
state operation and never dispatches). Returns the number attempted.
Source code in reactifact/runtime/runtime.py
astream
async
¶
astream(
budget: Budget | None = None,
max_iterations: int = 1000,
*,
request: Mapping[str, Any] | None = None,
) -> AsyncIterator[ProgressEvent]
Stream of a run: run_start → status (agent announces) → run_end.
Agents publish statuses via context.announce(...); the app
re-renders them in the chat ("Thinking…", "Searching docs…", "Found N…").
At the end a run_end with a summary (outcome/runs/duration) is emitted.
Source code in reactifact/runtime/runtime.py
RuntimeResources¶
RuntimeResources ¶
RuntimeResources(
llm: LLMProvider | None = None,
embedder: EmbeddingProvider | None = None,
sources: dict[str, Source] | None = None,
context_builder: ContextBuilder | None = None,
verification_threshold: float | None = None,
redactor: Redactor | None = None,
trace_truncate: int
| None
| _UnsetTraceTruncate = _UNSET_TRACE_TRUNCATE,
record_tool_args: bool = False,
id_factory: IdFactory | None = None,
pricer: Pricer | None = None,
metrics: MetricsSink | None = None,
principal: Principal | None = None,
authorizer: Authorizer | None = None,
guardrails: GuardrailPolicy | None = None,
quota: QuotaTracker | None = None,
**additional: Any,
)
Source code in reactifact/resources.py
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | |
registered
property
¶
The keys of every registered typed resource (for diagnostics).
authorize ¶
Whether the current principal may action on resource.
True when no authorizer is configured (authz is opt-in).
Source code in reactifact/resources.py
require_authorized ¶
authorize or raise AuthorizationError — the produce-side gate.
quota_key ¶
The key a QuotaTracker counts against: the principal, else the run.
register ¶
Attaches instance under key (its type, or a ResourceKey).
Returns the instance, so store = resources.register(Store, Store(...))
reads as a one-liner. Re-registering a key overwrites it.
Source code in reactifact/resources.py
get ¶
A string key reads additional; a type/ResourceKey reads typed.
require ¶
Like get, but a missing resource is a loud, early LookupError.
Reach for this inside a produce/agent for a resource the app must
have configured — the failure names the missing type instead of being a
None that blows up later.
Source code in reactifact/resources.py
has ¶
typed_values ¶
Every registered typed resource value (read-only introspection).
registered gives the keys; this gives the values — e.g. so a generic
tool scanner (reactifact.testing.fault) can find a list[Tool]
registered with register(...), not just one stashed via set(...).
Source code in reactifact/resources.py
aclose
async
¶
Closes the llm/embedder clients if they support it.
Duck-typed: LLMProvider/EmbeddingProvider don't require aclose
(a fake/no-op test double doesn't need one), so it's called only when
present. Nothing in the runtime calls this automatically — resources
are typically shared across many turns/runtimes, and closing them
early would break whatever still holds a reference. Call it yourself
once, at real shutdown: a FastAPI lifespan, or the end of a script.
ChatAssistant is the one exception — see its docstring.
Source code in reactifact/resources.py
scope
classmethod
¶
async with RuntimeResources.scope(build) as resources: — see ResourceScope.
Source¶
Source ¶
Bases: ABC
Source code in reactifact/sources.py
search ¶
Finds references to relevant content (by default — cannot do it).
The agent must not know the search mechanics: vector/keywords/SQL/CQL — that is the source's choice (§8). Sources without search simply return an empty list and stay available via resolve.
Source code in reactifact/sources.py
asearch
async
¶
Asynchronous search (embeddings, API). Default — synchronous search.
Vector sources cannot compute the query embedding synchronously,
so the aggregator (ScoutSources) uses asearch.
Source code in reactifact/sources.py
resolve
abstractmethod
async
¶
PendingQuestion (HITL)¶
PendingQuestion ¶
Bases: BaseModel
Artifact awaiting a human response (HITL, constitution §60).
Created by an agent (or directly) to block a step until user input.
A human answer is recorded via effects.resume(question, answer) (§60),
after which agents subscribed to PendingQuestion(answered=True) continue.
In the box¶
Tool¶
Tool ¶
Bases: ABC
Contract for an external operation: "Do this operation" (§46).
For an LLM agent (LLMAgent) a tool must provide a JSON schema of its
arguments (schema), from which the model picks the operation and arguments.
@tool builds it from the signature automatically.
execute
abstractmethod
async
¶
ToolUse¶
ToolUse ¶
ToolUse(
system: str,
tools: Sequence[Tool] | dict[str, Tool],
*,
name: str = "llm",
max_steps: int = 8,
temperature: float | None = None,
max_tokens: int | None = None,
deferred_tool_groups: Sequence[DeferredToolGroup] = (),
)
Bases: _ToolLoopBase
Blocking loop "LLM decides → tool → … → answer" in a single produce.
Simple, no HITL: the LLM either calls a tool or answers. The logic lives here, not in the container agent. Destructive tools are not offered to the LLM.
Source code in reactifact/tools/use/blocking.py
ToolUseHITL¶
ToolUseHITL ¶
ToolUseHITL(
system: str,
tools: Sequence[Tool] | dict[str, Tool],
*,
name: str = "llm",
max_steps: int = 8,
max_asks: int = 2,
max_approvals: int = 3,
resume_announce: Callable[[str], str] | None = None,
temperature: float | None = None,
max_tokens: int | None = None,
)
Bases: _ToolLoopBase
Reactive loop: step by step, can ask the human (HITL, §60).
The LLM may answer (answer), call a tool (tool_call, result goes into an
Observation), or ask a clarifying question (ask → PendingQuestion).
The human answer comes back into the loop as Observation(source="user").
Destructive tools are offered to the LLM (unlike ToolUse, which excludes
them entirely) but never executed straight away: a tool_call targeting a
destructive tool creates a PendingQuestion(kind="approve") instead, and
the call only runs once a human resolves it with an affirmative answer
(max_approvals bounds how many times one conversation can ask).
_history/the ask dedup check filter context.list_artifacts(Observation
| PendingQuestion) by query_id in Python — O(count of that type in the
whole context), not indexed by query_id. Bounded per conversation
(max_steps, max_asks), so this is only a real cost if many
long-running conversations share one Context — the standard
one-Context-per-session pattern (SessionStore, every example in this
repo) keeps each conversation's own artifact count small regardless of
how many sessions exist. A real fix would need RelationGraph indexed by
source_id (it currently isn't either) plus linking each Observation to
its goal artifact instead of filtering by field — deliberately not done
here; flag it if you're sharing one long-lived Context across many
concurrent tool-use conversations.
Source code in reactifact/tools/use/hitl.py
tools.native¶
tools_payload ¶
Builds the OpenAI tools=[...] array from Tools — Tool.schema is
already the right JSON-schema shape (tools.py), this just wraps it.
Source code in reactifact/tools/native.py
native_complete
async
¶
native_complete(
context: Context,
*,
messages: list[dict[str, Any]],
tools: Sequence[Tool] | dict[str, Tool] = (),
temperature: float | None = None,
max_tokens: int | None = None,
) -> LLMResponse | None
One native tool-calling turn.
messages are raw OpenAI-format dicts (system/user/assistant-with-
tool_calls/tool-with-tool_call_id) — not providers.Message, which
has no field for either. The full, verbatim dicts go via
LLMRequest.extra["messages"], which every OpenAICompatProvider-family
provider applies after building its own messages list from
LLMRequest.messages (providers/chat.py's _payload — request.extra
is merged into the payload last, overriding anything built from typed
fields). That's the documented escape hatch for provider-specific wire
shapes, not a hack — see _payload's own comment on arbitrary extra
fields.
LLMRequest.messages is also populated, with a best-effort typed
rendering (role + content, tool_calls/tool_call_id dropped — Message
has no field for either). OpenAICompatProvider ignores it in favor of
the full-fidelity extra["messages"], but a LLMProvider that doesn't
know this module's extra convention still sees a real conversation
instead of an empty list.
Returns None if no provider is configured (§67 — the same honest
no-provider fallback structured_llm uses) or the provider call itself
raised (network/outage) — logged, not raised further. Bring your own
history/retry/approval logic around this — see the module docstring
for why that isn't this function's job.
Source code in reactifact/tools/native.py
parse_tool_calls ¶
Extracts message.tool_calls from the provider's raw response.
LLMResponse.raw is the full, unparsed JSON body every
OpenAICompatProvider-family provider already returns
(providers/chat.py) — this is deliberately the only place that reaches
into it, so a provider shaping raw differently only needs to override
this one function, not anything that calls it. Malformed arguments
JSON parses to {} rather than raising (§67 — the model's mistake is
not a crash), same tolerance structured_llm.parse_structured uses.
Source code in reactifact/tools/native.py
AgentAsTool¶
AgentAsTool ¶
AgentAsTool(
*,
name: str,
description: str,
agent_factory: Callable[[], Agent],
resources: RuntimeResources,
input_type: type[BaseModel] = SubTask,
output_type: type[BaseModel] | None = None,
max_runs: int = 20,
destructive: bool = False,
)
Bases: Tool
Wraps agent_factory() as a callable tool (see module docstring).
Source code in reactifact/tools/agent.py
ContextBuilder¶
ContextBuilder ¶
Bases: ABC
Ranks/truncates the artifacts a single agent run would otherwise see.
Called once per agent run with every candidate matched by that agent's
consumes (combined across all of them). Returning the list unchanged
reproduces today's behavior.
TokenBudgetContextBuilder¶
TokenBudgetContextBuilder ¶
TokenBudgetContextBuilder(
*,
max_tokens: int | None = None,
token_counter: TokenCounter | None = None,
rank_key: Callable[[Artifact[Any]], Any] | None = None,
render: Callable[[Artifact[Any]], str] | None = None,
exempt_types: tuple[type, ...] = (),
min_keep: dict[type, int] | None = None,
)
Bases: ContextBuilder
Ranks candidates (newest-first by default) and keeps a prefix that
fits max_tokens, estimated via token_counter.
At least one costed (non-exempt) artifact is always kept, even if it
alone exceeds the budget — an agent silently getting zero real content
is worse than one that gets an oversized item; max_tokens is a soft
cap, not a hard clip.
exempt_types: artifact types that cost nothing and never trigger that
clip — for a marker/trigger type an agent only consumes to wake up
(little or no meaningful text), not to reason over. Without this, a
freshly created marker can rank first (newest) and either eat the whole
budget itself or — worse — silently consume the "at least one" slot,
leaving zero real content if the budget is then too tight for the next
(real) candidate. Exempt artifacts are still ranked and still kept
(still wake the agent up); they just never count toward max_tokens or
toward what counts as "at least one" content item.
min_keep: {type: count} — for a type with real content that must
still survive the budget regardless of rank (unlike exempt_types, it
does count toward max_tokens), guarantees its top-count-ranked
instances a reserved slice of the budget, filled before the shared
greedy pass runs over everything else. Without this, a low-volume type
that happens to rank lower than a high-volume one sharing the same
budget (e.g. the single triggering Question, if older than a pile of
freshly-ranked Evidence) can be crowded out entirely — min_keep
reserves its slot first instead of leaving it to rank order. The final
list is still returned in overall rank order, not with reserved items
forced to the front.
Source code in reactifact/context_builder.py
Verify¶
Verify ¶
Verify(
*,
metrics: Mapping[str, MetricFn] = core_metrics,
threshold: float | None = None,
required_metrics: tuple[str, ...] = (),
on_fail: Literal["ask", "retry"] = "ask",
answer_type: str = "Answer",
)
Bases: Step[VerificationResult]
Scores a live Answer-shaped artifact with eval.py metrics.
threshold: explicit per-instance cutoff. None (default) resolves at
produce-time from context.resources.verification_threshold, itself
falling back to DEFAULT_THRESHOLD when that is also unset — so setting
it once on RuntimeResources changes the bar for every Verify in the
Runtime, while a specific agent can still opt out with its own value.
required_metrics: names that must score exactly 1.0 regardless of the
weighted average — e.g. ("provenance_grounded",) so an ungrounded
answer never passes just because other metrics compensate.
answer_type: the artifact class name (by name, no domain import —
same convention as eval.py) this instance verifies; artifacts of any
other type are ignored.
On top of VerificationResult, this creates a PendingQuestion
(on_fail="ask") or a VerificationFailed (on_fail="retry") — the
containing agent's produces must declare those too (a bare
Step(PendingQuestion)/Step(VerificationFailed) widens the
allowed Create types without adding real logic — same convention
HITLLMAgent uses for Observation/PendingQuestion, llm_agent.py):
class Answerer(Agent):
consumes = [Consume(Question)]
produces = [BuildAnswer(), Verify(), Step(PendingQuestion)]
Source code in reactifact/verify.py
Audit & reproducibility¶
build_report¶
build_report ¶
build_report(
context: Context,
answer: Artifact[Any] | str,
*,
relations: Sequence[str] | None = None,
session_id: str = "",
) -> AuditReport
Walks the provenance chain behind answer into a verifiable report.
Breadth-first over context.relations(), following relations (all by
default), so every artifact that contributed to the answer is included with
its content hash and producing author. SourceRef locators in the chain
become sources.
Source code in reactifact/audit.py
context_hash¶
context_hash ¶
sha256 over the run's canonical state: version, artifacts, relations.
Includes each artifact's id, type, version and content hash, plus every relation edge — everything that makes two runs' states the same or different. Timestamps are deliberately excluded, so a re-run that reaches the same state hashes identically.
Source code in reactifact/audit.py
AuditReport¶
AuditReport ¶
Bases: BaseModel
A reproducible record of an answer and the evidence behind it.
Redaction¶
RegexRedactor¶
RegexRedactor ¶
RegexRedactor(
patterns: Sequence[tuple[str, str]] | None = None,
*,
replacement: str = "[REDACTED:{name}]",
)
Pattern-based Redactor with conservative, ready-to-use defaults.
patterns is a sequence of (name, regex) pairs; None uses
DEFAULT_PATTERNS. Everything that matches is replaced with
replacement (default "[REDACTED:<name>]", so a reader can tell what
was removed without seeing it).
Source code in reactifact/redaction.py
Tool loop¶
run_tool_loop¶
run_tool_loop
async
¶
run_tool_loop(
context: Context,
*,
system: str,
user: str,
tools: Sequence[Tool] | dict[str, Tool],
max_rounds: int = 6,
parallel: bool = True,
mandatory: str | Sequence[str] | None = None,
temperature: float | None = None,
max_tokens: int | None = None,
max_observation_chars: int | None = None,
redact_tool_outputs: bool = False,
retry_tool_errors: bool = False,
tool_retry_attempts: int = 2,
tool_retry_delay: float = 0.5,
parse_calls: Callable[[LLMResponse], list[ToolCall]]
| None = None,
fallback: Callable[
[Context, list[dict[str, Any]], str],
Awaitable[str | None],
]
| None = None,
on_event: OnToolLoopEvent | None = None,
) -> ToolLoopResult
Runs native tool-calling for up to max_rounds, then forces an answer.
- A turn that returns no tool calls ends the loop with its text.
- Tool calls are executed (in parallel by default) and their results fed
back as
toolmessages. mandatory(a tool name or names that must run): while none has been called and rounds remain, a reminder is injected so the model is nudged rather than the loop silently finishing without it.- If
max_roundsis reached without a final answer, one more call is made with no tools, to force a plain-text answer. - The turn's
Budget(context.resources.budget) is honoured between rounds, likeToolUse's own loop:max_tool_callscaps executed calls,max_seconds(viabudget_deadline) and token/cost limits stop new rounds. The exhausted calls are reported back as tool errors, then the loop falls through to the forced answer. max_observation_charstruncates each tool result before it goes back to the model (the returnedobservationskeep the full text).redact_tool_outputs=Trueappliescontext.resources.redactorto tool outputs/errors before they reach the model.retry_tool_errors=Trueretries a tool call when it returns aToolError(retryable=True), up totool_retry_attemptstotal attempts with exponentialtool_retry_delaybackoff. Retries count as tool executions forBudget.max_tool_calls.parse_callsoverrides how a response's tool calls are extracted (e.g. a textual DSML/XML codec);fallback(ctx, messages, text)runs when no calls were parsed and the text looks like tool-call markup, and its string (when notNone) becomes the final answer.on_event(name, payload)andContext.announce(kind="agent")report progress for streaming:round_start,tool_call,tool_result,final.- No provider / a failed call yields
text=""with whatever was observed — the same honest-failure contract the rest of the framework uses.
Source code in reactifact/recipes/tool_loop.py
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 | |
Budgets¶
Budget¶
Budget ¶
Bases: BaseModel
Resource limit for a single run (turn), applied by the runtime.
tracks_llm_usage
property
¶
Whether this budget needs an LLM-usage tracker (tokens/cost limits).
BudgetTracker¶
BudgetTracker ¶
Per-turn LLM usage counters, checked against a Budget.
The runtime creates one per turn when the budget has token/cost limits and
records every LLM response's usage into it — independent of tracing, which
is optional (RecordingLLM is only installed when a tracer is set).
Source code in reactifact/budget.py
exhausted ¶
The budget-exceeded outcome, or None while still within budget.
Source code in reactifact/budget.py
Schema types¶
register¶
register ¶
register(
model: type[BaseModel],
*,
type_id: str | None = None,
aliases: Iterable[str] = (),
migrate: Migrate | None = None,
) -> type[BaseModel]
Registers model's stable type_id (default: its TYPE_ID classvar, or
the qualified name), aliases (previously-persisted ids — e.g. a model's old
qualified name after a move), and an optional migrate hook.
Returns model, so it works as a decorator. Re-registering the same model
replaces its spec; two different models claiming one id is an error.
Source code in reactifact/types.py
resolve¶
resolve ¶
The class for a persisted type_id, or raise TypeNotFoundError.
Registry first, then a registered alias, then importing the id as a qualified name (so pre-registry payloads keep loading).
Source code in reactifact/types.py
Golden runs¶
capture¶
capture ¶
Freezes context (and optionally a trace) into a GoldenRun.
Source code in reactifact/testing/golden.py
assert_golden¶
assert_golden ¶
Fails loudly when state (or, with trace, prompts) drifted.
Source code in reactifact/testing/golden.py
assert_golden_file¶
assert_golden_file ¶
assert_golden_file(
context: Context,
path: str | Path,
*,
trace: RunTrace | None = None,
update: bool | None = None,
) -> GoldenRun
Path-aware snapshot assertion: writes the snapshot when it is missing
(or update/$REACTIFACT_GOLDEN_UPDATE is set), otherwise compares it.
The first run seeds the file, so a golden test is a one-liner and later
drift fails. Returns the GoldenRun now on disk.
Source code in reactifact/testing/golden.py
Testing harness¶
ScenarioLab¶
ScenarioLab ¶
ScenarioLab(
agents: list[Agent],
*,
resources: RuntimeResources
| Callable[[], RuntimeResources]
| None = None,
budget: Budget | None = None,
max_concurrency: int | None = None,
isolate_errors: bool = True,
mode: Mode = "live",
recording_path: str | Path = "scenario_calls.jsonl",
)
Runs agents against seeded artifacts, once per run() call.
mode controls the LLM behind resources.llm (see reactifact.testing.record):
- "live" (default): the real provider, unchanged.
- "record": wraps it in ReplayLLM(mode="record"), appending every call
to recording_path.
- "replay": wraps it in ReplayLLM(mode="replay") — no live network
calls; a call that diverges from the recording raises ReplayMiss
rather than being answered with a guess (§59).
isolate_errors defaults to True here (unlike Runtime's own default)
so a scenario can assert on a crashed agent via result.errors instead of
the whole run() raising; pass False to let an unexpected agent
exception fail the test the ordinary way.
Source code in reactifact/testing/lab.py
fail ¶
fail(
tool_name: str,
error: BaseException | Callable[[], BaseException],
*,
times: int | None = None,
when: Callable[[dict[str, Any]], bool] | None = None,
delay: float = 0.0,
) -> None
Queues a fault for the next run()/.turn().
tool_name raises error (or the result of calling it, if callable —
useful for a fresh exception instance per call) instead of executing.
times=None (default) faults every call; times=N faults the first
N calls, then delegates to the real tool; when(args) narrows it to
matching calls; delay seconds are slept before raising (a
slow-then-fail tool). Queued faults are one-shot — consumed by the next
run()/.turn() only.
Source code in reactifact/testing/lab.py
stub_tool ¶
stub_tool(
tool_name: str,
*,
text: str = "",
error: str | None = None,
times: int | None = None,
when: Callable[[dict[str, Any]], bool] | None = None,
delay: float = 0.0,
) -> None
Queues a canned-response stub for tool_name — the non-failing
counterpart of fail(): the tool returns ToolOutput(text=..., error=...)
instead of executing. Same times/when/delay semantics.
Source code in reactifact/testing/lab.py
fail_resource ¶
fail_resource(
resource: str | type[Any] | ResourceKey[Any],
error: BaseException | Callable[[], BaseException],
*,
method: str | None = None,
times: int | None = None,
when: Callable[[tuple[Any, ...], dict[str, Any]], bool]
| None = None,
delay: float = 0.0,
) -> None
Queues a fault for a resource — the general-purpose analog of
fail() for anything that isn't a tool.
resource addresses it either by string name — "llm",
"embedder", a source id (resources.sources[id]), or a name set via
resources.set(name, ...) — or by a typed key: a class or a
ResourceKey registered with resources.register(...).
Wraps the resource in a duck-typed proxy for the next run()/
.turn(): method=None (default) fails every callable on it;
naming one method (e.g. "embed", "search") faults only that
method. times=None faults every call; times=N faults the first
N, then delegates; when(args, kwargs) narrows it to matching calls;
delay seconds are slept first. Raises ScenarioError at run time if
resource doesn't match any resource, or matches one that's None
(nothing configured to fail).
Source code in reactifact/testing/lab.py
stub_resource ¶
stub_resource(
resource: str | type[Any] | ResourceKey[Any],
*,
returns: Any = UNSET,
side_effect: list[Any]
| Callable[..., Any]
| None = None,
method: str | None = None,
times: int | None = None,
when: Callable[[tuple[Any, ...], dict[str, Any]], bool]
| None = None,
delay: float = 0.0,
) -> None
Queues a stub for a resource — the non-failing counterpart of
fail_resource() for the same addressing (string name or typed key).
returns is returned on every stubbed call; or side_effect walks a
sequence/list (unittest.mock semantics: an item that is an
Exception raises, anything else is returned) or a callable called as
side_effect(*args, **kwargs). times/when/delay/method behave
as on fail_resource.
Source code in reactifact/testing/lab.py
run
async
¶
Seeds seed artifacts into a fresh Context, runs the agents to
completion (or budget/iteration exhaustion), and returns the result.
Source code in reactifact/testing/lab.py
run_sync ¶
Synchronous run() — mirrors Runtime.run()/run_once() for plain
(non-async) pytest tests. Uses asyncio.run, so it can't be called from
inside an already-running event loop.
Source code in reactifact/testing/lab.py
scenario ¶
Starts a multi-turn scenario: one Context reused across .turn()
calls, for flows that need more than one round of user input to reach
the state under test (e.g. a design pick, then a plan, then an
approval — each its own turn on the same project). The record/replay
LLM wrapper is built once here, so its call index persists correctly
across turns.
Source code in reactifact/testing/lab.py
RelationAssertions¶
RelationAssertions ¶
Assertions over the artifact graph's relations (Context.relations()).
ArtifactAssertions checks an artifact's fields; this checks the
edges: that provenance exists, that it doesn't, and how many. Any filter
left None is a wildcard.