You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A2A (Agent2Agent) client and server.reactifact.a2a speaks the A2A
JSON-RPC binding (v0.3 shape) in both directions, with no new dependency (the
client uses httpx; the server, FastAPI from the web extra, is imported
lazily). Client: A2AClient (Agent Card, message/send, message/stream, tasks/get, tasks/cancel) and A2AAgentTool/a2a_tool — a reactifact Tool so an LLMAgent can delegate to a remote agent. remote_agent(...) / A2ARemoteProduce — a remote agent as a first-class scheduled node (the
analogue of LangGraph's A2ARemoteGraph): it consumes a local artifact, calls message/send, produces a local artifact, and maps a remote input-required
to a local PendingQuestion (resume continues the same remote task). Server: create_a2a_router(agents, ...) builds an Agent Card (skills from each
agent's produces) and a JSON-RPC endpoint where a Task is one conversation
— a message/send seeds via create_message, runs the agents to a fixpoint,
and replies via reply; a paused HITL run maps to input-required and the
next message with the same taskId resumes it; message/stream emits the
working task and a status update per context.announce() progress event over
SSE. Docs in docs/en|ru/a2a.md. Verified against the official a2a-sdk
client (the SDK resolves our Agent Card and gets a completed task from message/send); a2a-sdk is a test-only dependency (its own a2a group),
the runtime stays on httpx + FastAPI. The examples/a2a app demonstrates it
end to end (serve, call directly, schedule as a node, HITL) with no LLM or
network.
Outbox for outbound side effects. A produce no longer performs external
I/O (a notification, a webhook, a deploy) directly: effects.act(kind=..., key=..., payload=...) records a PendingAction
intent under a stable id (action:{key}), and Runtime(dispatcher=...)
performs every committed-but-undispatched action after the commit — once
per stable id, recording dispatched (or failed, then re-raising) as state. flush_pending_actions() drains it without a generation (e.g. after merge()); the runtime keeps a turn alive until the outbox is empty. Replay
reconstructs the record without running the runtime, so it never re-sends; a
retried produce re-derives the same id and creates no second intent; two
branches that reach the same action share the id and merge to one — dispatch
bookkeeping (status/dispatched_at/error) is excluded from the three-way
merge signature, so a mixed dispatched/pending pair converges instead of
raising MergeConflict — and the dispatched side wins whichever branch is
the merge target, so a merge never re-sends an already-sent action — while a
changed kind/key/payload still conflicts. PendingAction and Context.pending_actions()/ mark_dispatched()/mark_failed()/retry_action() are exported; replay_summary and python -m reactifact replay report dispatched/pending/failed action counts. Delivery remains at-least-once
(a crash between the send and the dispatched commit re-runs it), so the
dispatcher must honor idempotency_key — stated, not hidden. The offline examples/outbox demo walks seven cases: commit→dispatch, re-derivation,
same-generation duplicate, merged branches, replay, failure→retry, and an
app-owned retry wrapper around the dispatcher. A failed dispatch bumps PendingAction.attempts and fires Runtime(on_dispatch_error=action, exc)
before re-raising; retry_action resets attempts. The framework runs no
background relay — the next arun() or an explicit flush_pending_actions() drains the outbox, and retry/backoff policy stays
with the application (deliberately not a workflow engine).
Correlated structured logging.reactifact.logging (exported as configure_logging/get_logger) turns on structured logs under the reactifact.* namespace — silent until configured (a NullHandler by
default, never basicConfig), human-readable or json=True, with configure_logging(level=..., stream=...). Every record carries the turn's
correlation fields (run_id/session_id/generation/agent, plus the
request's request_id) so a line ties back to the exact run/agent; app code
adds fields with bind(...). The runtime emits run started/run finished
and action dispatched at INFO, generation/agent completion at DEBUG,
and failing agents/dispatches at WARNING. Logs are metadata only — no
artifact data or prompt contents. examples/starter_app shows it in a real
lifespan (LOG_LEVEL=…, LOG_JSON=1) alongside a GET /api/ready readiness
probe backed by active_runs().
Hard turn deadline, graceful shutdown, in-flight registry. Budget.max_seconds is now a hard per-turn deadline: the runtime cancels the
generation in flight (a hung LLM call is actually stopped) instead of only
checking between generations, leaves the cancelled trigger batch unconsumed
for a resume, and ends with RunOutcome.BUDGET_TIME_EXCEEDED. Runtime.request_stop() stops at the next generation boundary
(RunOutcome.STOPPED; the current generation still commits), and Runtime.ashutdown(timeout=…) waits for the turn, force-cancelling after the
timeout. Shared RuntimeResources are closed by the host app, never by the
runtime. reactifact.runtime.active_runs() / cancel_run(run_id) expose the
process's in-flight turns read-only (readiness/ops; in-process only), and Runtime.in_flight reports whether it is running.
Changed
Budget.max_seconds is enforced hard (behavior-visible, non-breaking): the
runtime now cancels the generation in flight when the deadline passes, instead
of only checking between generations, and ends the turn with RunOutcome.BUDGET_TIME_EXCEEDED. A run that previously overshot a hung LLM
call now stops on time. See release.md.
Logging stays silent by default (a library NullHandler); nothing is
emitted until configure_logging() is called.
Docs
patterns.md (EN + RU) gains an Outbox: external side effects recipe,
and effects.md/durability.md/replay.md/api.md (EN + RU) document effects.act/PendingAction/Runtime(dispatcher=...); the reactifact/reactifact-agents/reactifact-observability skills are
updated too. durability.md (EN + RU) also gains an Outbox in production
checklist (idempotent dispatcher, periodic drain, app-owned retry,
observability), stating that the framework ships no relay by design. The example count is now stated once (twenty-three) across examples.md, index.md and quickstart.md (EN + RU) and the constitution's
demo list, replacing the disagreeing subset counts (14/15/16/17).
durability.md (EN + RU) states plainly that artifact types do not need
registering — the persisted id is the qualified name by default, resolved by
import on load. It lists the only three reasons to register_type (rename/
move, aliases, migration), notes the TYPE_ID classvar shortcut, and calls
out the one caveat: a model that is not importable in a new process
(__main__, a notebook, <locals>, pydantic.create_model) needs an
explicit type_id.