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.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.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.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.py
delete ¶
Deletes the artifact and generates ARTIFACT_DELETED.
Source code in reactifact/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.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.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.py
link ¶
Establishes a link source_id —relation→ target_id (idempotently, §42).
unlink ¶
Removes links; relation/target_id = None mean "any".
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.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.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.py
interrupt ¶
interrupt(
question: str,
*,
kind: str = "general",
notes: dict[str, Any] | None = None,
) -> Artifact[PendingQuestion]
Poses a question to a human: creates a PendingQuestion in the context.
Source code in reactifact/context.py
pending_questions ¶
resume ¶
A human's answer is a regular patch: marks the question as answered.
Generates ARTIFACT_UPDATED, which agents subscribed to PendingQuestion(answered=True) react to.
Source code in reactifact/context.py
drain_events ¶
clone ¶
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.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.py
log_commit ¶
Applies the commit to the repository: fills in parent/version, moves head.
Source code in reactifact/context.py
history ¶
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.py
snapshot ¶
stale_artifacts ¶
Artifacts whose parents (reads in the producing commit) are now newer versions.
Dependencies are built from the actual reads recorded by the runtime via
consumes — a link derived from execution, not an author-drawn graph.
Backed by the incrementally-maintained _stale set (kept current by
update()/log_commit()), not a rescan of every artifact.
Source code in reactifact/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.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.py
from_kv
async
classmethod
¶
Loads a context previously stored with to_kv, or None if absent.
Source code in reactifact/context.py
Artifact¶
Artifact ¶
Bases: Generic[TData]
Wrapper around a Pydantic model with versioning.
Source code in reactifact/artifacts.py
history
property
¶
Returns a copy of the list of previous versions (excluding the current one).
update ¶
Saves the current version to history and replaces the data.
get_all_versions ¶
diff ¶
Returns a diff between two versions by their numbers (0 – the oldest).
Source code in reactifact/artifacts.py
to_dict ¶
Serializes the artifact, including its full version history.
Memoized per version (bumped by update()): an unchanged artifact
returns the same dict instance on a later call instead of re-walking
model_dump() over its entire history again — the history only grows
monotonically, so a cache keyed by version can never go stale.
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 = self.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 = self.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
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,
)
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.
run
async
¶
Default: runs self.produces via execute() (the effects-first
path — write a Produce subclass or @produce 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): produces write self.effects.*/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¶
Produce ¶
Produce(
artifact_type: ArtifactType | None = None,
*,
also_creates: Sequence[ArtifactType] | None = None,
reacts_to: ArtifactType
| Sequence[ArtifactType]
| None = None,
)
Bases: Generic[TOut]
Describes the produced artifact type and how it is created.
artifact_type is auto-derived from the generic when a subclass is written
as class X(Produce[Foo]) — write it explicitly only to override or when
the class has no generic (e.g. programmatic Produce(Foo)).
A produce whose produce() body legitimately creates more than one
artifact type declares the extra ones in also_creates — either as a
class attribute (also_creates = (Bar, Baz)) or a constructor argument
(Produce(Foo, also_creates=[Bar])). Runtime._validate_patch_types
checks every Create op an agent's generation produces against the union
of artifact_type/also_creates across that agent's produces list —
without declaring Bar here, a self.effects.create(Bar(...)) inside
artifact_type = Foo's own produce() raises "not declared in produces"
at commit time, since nothing recorded that this produce is allowed to
write it. Before also_creates existed, the only way to widen that set
was an inert second Produce(Bar) placeholder added to the agent's
produces list purely so its unused artifact_type got unioned in —
correct, but nothing at the call site said why that placeholder was
there; also_creates puts the declaration on the produce that actually
does the writing.
The input-side mirror of that is reacts_to: an agent with several
consumes and several produces runs every produce on every
matching event by default (Agent.execute() has no idea which of an
agent's several Consumes a given produce actually cares about) — every
produce ends up guarding itself by hand, if call.event is None or not
isinstance(call.trigger.data, TheOneTypeICareAbout): return None, at
the top of its own body. Declaring reacts_to = (TheType,) (a tuple,
same convention as also_creates — more than one entry for more than
one type) moves that guard to where the intent already lives — the
class declaration — and Agent.execute() simply skips calling
produce() at all for an event none of reacts_to matches, the same
way Trigger.artifact_type gates whether the agent wakes up at all.
Two produces sharing one artifact_type (the same output) but different
reacts_to (different triggers) is exactly why this can't just reuse
artifact_type for both directions — see examples/product code with a
FinalizeWithDocuments/DirectFinalize-shaped pair, both producing the
same result type from two different upstream events. None (the
default) means unrestricted — today's behavior, unchanged, so this is
purely additive.
Once reacts_to narrows which event a produce runs for, resolving
that event's own artifact is still work every such produce repeats —
ProduceCall.trigger does it once, in Agent.execute(), for every
produce that reads it. For a produce that also declares reacts_to,
this is a real guarantee, not best-effort: Agent.execute() also skips
calling it if context.get(event.artifact_id) no longer resolves for a
CREATED/UPDATED/STALE event (the artifact was deleted by another agent
earlier in the same generation — the same race Trigger.matches()
documents) — so call.trigger is never None when such a produce
actually runs, and the body needs no guard at all. This guarantee does
not apply to a DELETED event on the produce's own reacts_to type:
there, context.get(...) correctly returning None is the event, not
a race, so call.trigger is None and the produce still runs —
deletion-reacting code is expected to handle that itself (call.event
still carries artifact_id/artifact_type there). Without reacts_to,
call.trigger is still resolved on a best-effort basis whenever
call.event is not None, but never a reason to skip the call — there
is no per-type contract to enforce.
Source code in reactifact/produce.py
effects
property
¶
The produce-scoped effect slot (authoring surface, §24).
Only meaningful inside produce(): the runtime pushes a fresh slot
per execution. Returns an error outside a run. Equivalent to (and
backed by the same contextvar as) call.effects on the ProduceCall
the current produce() call received — kept here too so a
class-style produce can write self.effects.create(...) without
threading call through every helper method.
produce
async
¶
No-op by default (§24).
Subclass-style overrides write self.effects.* (or call.effects.*)
and return None; a None return means "no work". A bare
Produce(Model) (no override) is a valid, deliberate no-op — used
e.g. to widen an agent's allowed Create types when the actual
write happens via self.effects.ask(...) in another produce (see
examples/supervisor).
Source code in reactifact/produce.py
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,
)
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).
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_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",
)
Source code in reactifact/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.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,
id_factory: IdFactory | None = None,
**additional: Any,
)
Source code in reactifact/resources.py
registered
property
¶
The keys of every registered typed resource (for diagnostics).
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 self.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/tool_use.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/tool_use.py
native_tool_use¶
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/native_tool_use.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/native_tool_use.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/native_tool_use.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/agent_tool.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: Produce[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
Produce(PendingQuestion)/Produce(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(), Produce(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,
) -> 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. - 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
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 | |
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.