Agentao 0.4.5
Agentao 0.4.5
A core-boundary review release on top of 0.4.4. No breaking changes;
no public API or wire-format change. pip install -U agentao upgrades
in place from any 0.4.x release.
The headline:
- Replay state externalized. The
Agentaofacade's replay surface
(constructor kwarg + 4 instance attributes + 6 facade methods +
close()teardown leg) is consolidated behind
agentao.replay.manager.ReplayManager. The recorder is now wired by
the embedding factory as aTransport.subscribe()listener — the
chat loop andruntime/turn.pyno longer reach intoagentfor
replay emission. Transport.subscribe(listener)is now a documented public surface
on theTransportProtocol, withEventBroadcasterre-exported from
agentao.transportas the composition helper.TURN_BEGIN/TURN_ENDevent types distinguish the
user-driven turn boundary from the per-LLM-iterationTURN_START.- Eight legacy
Agentao(...)callback kwargs now emit a single
DeprecationWarning— final notice before their 0.5.0 removal. - Persistent session module relocated to
agentao.embedding.sessions;agentao/session.pybecomes a
deprecation shim. PermissionEnginefile I/O extracted to
agentao.embedding.permission_loader; the engine constructor now
acceptsrules=/loaded_sources=kwargs to skip disk reads.- Plugin loader relocated under
agentao/embedding/plugins/;
agentao/plugins/is runtime-only (validators + LLM-facing
surfaces). agentao.hostno longer eagerly importsagentao.acp—
export_host_acp_json_schema()lazy-delegates to a new
agentao.acp.schema_exportmodule.
Plus the developer guide picks up a full CLI section, the §5.7 Plugin
Hooks rule-author guide, and the documentation for everything above.
Why this release
docs/design/core-boundary-review.{md,zh.md} audited the embedded-host
boundary for everything core doesn't import but lives in the main
package. The audit produced a 7-item priority table; this release ships
items #1–#5b plus the #6 boundary prep, all behind deprecation shims
that preserve every existing import path. Item #7 (agentao.harness/
alias removal) is queued for 0.5.0 alongside the other 0.5.0 surgeries
catalogued at the bottom of this note.
The motivation is consistent: things that read .agentao/*.json from
disk, materialize MemoryManager / PermissionEngine /
FileBackedMCPRegistry / replay recorders, or depend on Path.cwd()
defaults, belong in agentao.embedding/ — not in the runtime package
that an embedded host pulls in. Decoupling these makes the
"Agentao(...) direct vs. build_from_environment(...)" boundary
(Constructor Reference §2.2)
match the actual import shape: hosts that want zero disk side effects
at construction time can import only the runtime package.
Replay externalized into ReplayManager
The pre-0.4.5 Agentao carried replay state on the facade itself: the
replay_config constructor kwarg, four instance attributes
(replay_recorder, replay_session_id, replay_run_id,
replay_run_kind), six facade methods (replay_start_run,
replay_emit_event, replay_finalize_run, replay_session_id_for,
replay_run_kind_for, replay_close), and a teardown leg in
close(). Six call sites in chat_loop invoked
agent._emit_*(...); runtime/turn.py:82 and
runtime/llm_call.py:59 read agent attributes directly.
After this release:
agentao.replay.manager.ReplayManagerowns the state. The recorder
is wired byagentao.embedding.factoryas aTransport.subscribe()
listener.chat_loopemitsTURN_BEGIN/TURN_END(and the existing turn /
tool / sub-agent events) onto the transport; the recorder receives
them through subscription instead of through agent state.- The six
Agentao.replay_*facade methods and thereplay_config=
kwarg remain as back-compat shims that delegate to the manager;
scheduled removal in 0.5.0.
The migration was done as one PR — splitting the constructor /
attribute / method removal from the runtime migration would have left
replay broken mid-way (docs/design/core-boundary-review.md §7
priority #1 captures the rationale).
Transport.subscribe(listener) as a public surface
def subscribe(self, listener: Callable[[AgentEvent], None]) -> Callable[[], None]:
"""Register an extra listener that receives every emitted event.
Returns an idempotent unsubscribe function."""subscribe() is optional — the Transport Protocol declares it,
NullTransport and SdkTransport provide it by composing
agentao.transport.EventBroadcaster, and bespoke transports (ACP,
custom message-queue bridges) may omit it. Probe with
getattr(transport, "subscribe", None).
The notify path uses snapshot iteration so subscribing or
unsubscribing mid-emit is safe; listener exceptions are swallowed and
never poison the runtime emit path. Internally it backs the new replay
recorder wiring (above) and the host event stream behind
agent.events() (Embedded Harness Contract §4.7).
A from-scratch transport opts in by composing the helper:
from agentao.transport import EventBroadcaster
class MyTransport:
def __init__(self):
self._broadcast = EventBroadcaster()
def emit(self, event):
# ... your real send path ...
self._broadcast.notify(event)
def subscribe(self, listener):
return self._broadcast.subscribe(listener)TURN_BEGIN / TURN_END event types
TURN_START predates this release and fires once per LLM
iteration inside a turn — a single user-driven turn that fans out
into multiple tool calls produces many TURN_START events. The new
pair fires once per user-driven turn, carrying:
TURN_BEGIN→{"user_message": "..."}TURN_END→{"final_text": "...", "status": "ok" | "error" | "cancelled", "error": None}
This is the seam replay recorders subscribe to via
Transport.subscribe() instead of being reached through agent state.
Hosts that want per-turn audit / metrics frames should subscribe to the
outer pair; UI streaming code keeps using the inner per-iteration
events. Full event tree in
Developer Guide §4.2.
Eight legacy callback kwargs — formal deprecation
Pre-0.2.10 the Agentao(...) constructor accepted eight per-callback
kwargs. They have been documented as deprecated since 0.2.10 (the
runtime decoupling release) but accepted silently. As of 0.4.5:
- Passing any of
confirmation_callback,step_callback,
thinking_callback,ask_user_callback,output_callback,
tool_complete_callback,llm_text_callback,
on_max_iterations_callbackemits oneDeprecationWarningper
construction, naming all eight and pointing at
agentao.embedding.compat.build_compat_transport(...)as the
documented migration surface. - Mixing
transport=with legacy callbacks (which silently ignored
the callbacks) also emits a warning, so the dead kwargs surface
in test runs. - The kwargs themselves remain accepted. Scheduled removal in
0.5.0 — the actual signature surgery is the headline of that
release alongside theagentao.harnessalias removal and the
agentao/session.pyshim removal.
The four legacy-callback tests
(test_tool_confirmation.py, test_reliability_prompt.py) now emit
the new warning (visible in pytest's warnings summary) but don't fail;
they're testing the deprecated path on purpose and migrate at 0.5.0
alongside the kwarg removal.
Persistent session module relocation
agentao/session.py (305 lines, JSON save / load / list / delete +
rotation under <wd>/.agentao/sessions/) was a top-level module that
core didn't import — exactly the kind of "transactional persistence"
the codex audit moved out into a separate crate. After this release:
- The implementation lives at
agentao/embedding/sessions.py. - Production import sites in
cli/{commands,session,replay_commands}.py
andacp/session_load.pyimport from the new path and pass
project_rootexplicitly. agentao/session.pyis a deprecation shim that wraps the new module
with the old permissive signature
(project_root: Optional[Path] = Nonefalling back toPath.cwd()).
External users and four test files keep working through the shim.
In 0.5.0 the shim and the Path.cwd() fallback are removed; the
new module's API requires project_root explicitly. tests/test_session.py
got migrated immediately (its isolated_session_dir fixture
monkeypatches the private _session_dir helper, and wrapper-shim
mechanics cannot forward private-helper monkeypatching across module
boundaries — the patch target was a one-line move to
agentao.embedding.sessions._session_dir); the other three test files
migrate at 0.5.0.
PermissionEngine file I/O extracted
The PermissionEngine(project_root=..., user_root=...) legacy
constructor read permissions.json from project + user scope at
construction time. Disk I/O inside what should be a pure runtime gate
violates the embedded-harness "no globals" principle. After this
release:
- Loading lives in
agentao.embedding.permission_loader(parsing,
env-var expansion, source tracking). PermissionEngine.__init__acceptsrules=/loaded_sources=
kwargs to skip disk reads. Hosts that build rule sets programmatically
pass these directly.- The legacy auto-load path
(PermissionEngine(project_root=...)withoutrules=) is
preserved via lazy delegation to the loader and is not deprecated
in this release. Tightening it into a hard error is queued for a
future cycle alongside other API surgery — 117+ test instantiations
and the published examples use the convenience form.
Plugin loader relocated under embedding/
Plugin code has two halves with different import audiences:
- Validators (runtime, LLM-facing) — front-matter shape checks,
activate_skillargument validation, agent-spawn argument
validation. Stays inagentao/plugins/{skills,agents}.py. - Loader (config-source, embedding-facing) — manifest reading,
file discovery, MCP integration, resolver dispatch. Now in
agentao/embedding/plugins/{manager,manifest,diagnostics,mcp,resolvers/}.
agentao/plugins/ is therefore runtime-only after this release — the
boundary matches the rest of agentao.embedding. No public import
path breaks; the relocated modules are imported through their new
location only by embedding/factory.py and the CLI loader.
host.export_host_acp_json_schema lazy-delegated
The host-facing schema export (snapshot lives at
docs/schema/host.acp.v1.json) used to import agentao.acp at
agentao.host.schema import time. After this release the function
lazy-delegates to agentao.acp.schema_export, so importing
agentao.host no longer pulls in the entire ACP server stack — relevant
for embedded hosts that do not run ACP. Function signature, return
type, and the snapshot itself are unchanged; this is internal
indirection only.
This is the boundary prep for a future acp/ wheel split. Actual
packaging (separate agentao-acp distribution) is not part of this
release — see docs/design/core-boundary-review.md priority #6.
What did not change
- No public API or wire-format change.
agentao.hostPydantic
models, thehost.events.v1.json/host.acp.v1.jsonschemas, and
theAgentao(...)constructor signature are unchanged from 0.4.4.
Eight kwargs now warn but still work. - No required code change to upgrade.
pip install -U agentaois
the only step. Hosts that already passtransport=SdkTransport(...)
see zero behavior change. - No CLI command rename.
/replay,/sessions,/resume,/mcp,
/skills,/agent,/permissionsall behave identically. - The
agentao.harnessdeprecated alias is still alive. Its
removal stays scheduled for 0.5.0.
Migration notes
- CI logs will start showing
DeprecationWarningfor hosts that
still pass any of the eight legacy callback kwargs toAgentao(...).
Two clean migration paths:- Build an
SdkTransport(...)and passtransport=instead. - Call
agentao.embedding.compat.build_compat_transport(...)
explicitly with the same callback kwargs and pass its return as
transport=. This bypasses the constructor-level warning.
- Build an
- Hosts that subclass
Transportcan opt into fan-out by
composingagentao.transport.EventBroadcaster(see the
copy-paste recipe above). - Hosts that import
agentao.session.*keep working via the
shim; passingproject_rootexplicitly future-proofs the call site
ahead of 0.5.0. - Hosts that build
PermissionEngineprogrammatically can now
passrules=/loaded_sources=to skip the legacy disk-read
path; existingPermissionEngine(project_root=...)calls continue
to work unchanged.
Tests
The four release gates from 0.4.4 are preserved:
AGENTAO_TEST_LIVE_MODELS=0 AGENTAO_TEST_LIVE_LLM=0 uv run pytest tests/
uv run mypy --strict --package agentao.host
uv run python scripts/write_host_schema.py --check
uv run python scripts/write_replay_schema.py --checkThe boundary review batches preserve every public assertion; the only
mechanical migration was tests/test_session.py's monkeypatch target
(see Persistent session above).
Upgrade
pip install -U agentaoOut of scope (deferred)
agentao.harnessalias removal. Still scheduled for 0.5.0.agentao/session.pyshim removal plusPath.cwd()fallback
removal. Scheduled for 0.5.0; carries the four ACP-tests migration.- Eight legacy callback kwargs — signature surgery on the
Agentao(...)constructor. Scheduled for 0.5.0. PermissionEnginelegacy auto-load path tightening. Conversion
into a hard error is deferred; the convenience form
(PermissionEngine(project_root=...)) stays accepted.acp/wheel split (separateagentao-acpdistribution). The
boundary prep landed in 0.4.5 (host.export_host_acp_json_schema
lazy-delegate); actual packaging is opportunistic.docs/releases/v0.4.0.mdandv0.4.1.md— backfilling these
remains deferred; carried over from 0.4.4.- PreCompact gate,
http-type Stop hooks, plugin-hook events in the
host public model, hook attachment pipeline — all carried over
from 0.4.4 unchanged. bashlex-based supersedence of the workspace-write
sensitive-write preset's regex tier. Carried over from 0.4.3.