Skip to content

Agentao 0.4.5

Choose a tag to compare

@jin-bo jin-bo released this 07 May 15:16
· 89 commits to main since this release
11339e2

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 Agentao facade'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 a Transport.subscribe() listener — the
    chat loop and runtime/turn.py no longer reach into agent for
    replay emission.
  • Transport.subscribe(listener) is now a documented public surface
    on the Transport Protocol, with EventBroadcaster re-exported from
    agentao.transport as the composition helper.
  • TURN_BEGIN / TURN_END event types distinguish the
    user-driven turn boundary from the per-LLM-iteration TURN_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.py becomes a
    deprecation shim.
  • PermissionEngine file I/O extracted to
    agentao.embedding.permission_loader; the engine constructor now
    accepts rules= / loaded_sources= kwargs to skip disk reads.
  • Plugin loader relocated under agentao/embedding/plugins/;
    agentao/plugins/ is runtime-only (validators + LLM-facing
    surfaces).
  • agentao.host no longer eagerly imports agentao.acp
    export_host_acp_json_schema() lazy-delegates to a new
    agentao.acp.schema_export module.

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.ReplayManager owns the state. The recorder
    is wired by agentao.embedding.factory as a Transport.subscribe()
    listener.
  • chat_loop emits TURN_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 the replay_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_callback emits one DeprecationWarning per
    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 the agentao.harness alias removal and the
    agentao/session.py shim 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
    and acp/session_load.py import from the new path and pass
    project_root explicitly
    .
  • agentao/session.py is a deprecation shim that wraps the new module
    with the old permissive signature
    (project_root: Optional[Path] = None falling back to Path.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__ accepts rules= / loaded_sources=
    kwargs to skip disk reads. Hosts that build rule sets programmatically
    pass these directly.
  • The legacy auto-load path
    (PermissionEngine(project_root=...) without rules=) 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_skill argument validation, agent-spawn argument
    validation. Stays in agentao/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.host Pydantic
    models, the host.events.v1.json / host.acp.v1.json schemas, and
    the Agentao(...) constructor signature are unchanged from 0.4.4.
    Eight kwargs now warn but still work.
  • No required code change to upgrade. pip install -U agentao is
    the only step. Hosts that already pass transport=SdkTransport(...)
    see zero behavior change.
  • No CLI command rename. /replay, /sessions, /resume, /mcp,
    /skills, /agent, /permissions all behave identically.
  • The agentao.harness deprecated alias is still alive. Its
    removal stays scheduled for 0.5.0.

Migration notes

  • CI logs will start showing DeprecationWarning for hosts that
    still pass any of the eight legacy callback kwargs to Agentao(...).
    Two clean migration paths:
    • Build an SdkTransport(...) and pass transport= 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.
  • Hosts that subclass Transport can opt into fan-out by
    composing agentao.transport.EventBroadcaster (see the
    copy-paste recipe above).
  • Hosts that import agentao.session.* keep working via the
    shim; passing project_root explicitly future-proofs the call site
    ahead of 0.5.0.
  • Hosts that build PermissionEngine programmatically can now
    pass rules= / loaded_sources= to skip the legacy disk-read
    path; existing PermissionEngine(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 --check

The 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 agentao

Out of scope (deferred)

  • agentao.harness alias removal. Still scheduled for 0.5.0.
  • agentao/session.py shim removal plus Path.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.
  • PermissionEngine legacy auto-load path tightening. Conversion
    into a hard error is deferred; the convenience form
    (PermissionEngine(project_root=...)) stays accepted.
  • acp/ wheel split (separate agentao-acp distribution). 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.md and v0.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.