Simplify Python hosting core (#6492)

Remove linking, multicast, durable delivery, and host push machinery from the v1 hosting core. Keep those scenarios in a proposed follow-up ADR and update channel packages, samples, docs, tests, and workspace metadata around the smaller host/channel contract.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Eduard van Valkenburg
2026-06-12 08:34:08 +02:00
committed by GitHub
Unverified
parent e5a6e35843
commit 36ce0950e4
50 changed files with 1290 additions and 11651 deletions
+25 -222
View File
@@ -1,32 +1,19 @@
# Copyright (c) Microsoft. All rights reserved.
"""Tests for ``state_dir`` wired through :class:`AgentFrameworkHost`."""
"""Tests for narrowed ``state_dir`` support in :class:`AgentFrameworkHost`."""
from __future__ import annotations
import asyncio
from pathlib import Path
from typing import Any
import pytest
from agent_framework_hosting import (
AgentFrameworkHost,
ChannelContext,
ChannelContribution,
ChannelIdentity,
LinkChallenge,
)
from agent_framework_hosting import AgentFrameworkHost, ChannelContext, ChannelContribution
# Skip the whole module when the optional disk extra isn't installed.
pytest.importorskip("diskcache")
# --------------------------------------------------------------------------- #
# Test helpers #
# --------------------------------------------------------------------------- #
class _AgentStub:
"""Bare-minimum SupportsAgentRun stub for host construction."""
@@ -42,65 +29,22 @@ class _ChannelStub:
return ChannelContribution()
class _NonConfigurableLinker:
async def resolve(self, _identity: ChannelIdentity) -> LinkChallenge:
return LinkChallenge("link")
class _ConfigurableLinker:
def __init__(self) -> None:
self.configured_path: Path | None = None
def configure_link_store_path(self, path: str | Path) -> None:
self.configured_path = Path(path)
async def resolve(self, _identity: ChannelIdentity) -> LinkChallenge:
return LinkChallenge("link")
def _close_host_disk(host: AgentFrameworkHost) -> None:
"""Mirror the lifespan shutdown ordering for tests that simulate restart.
The real shutdown order is ``runner.shutdown()`` → ``sessions_store.close()``;
both release their advisory file locks so a second host can take ownership.
"""
runner = host._durable_task_runner
try:
asyncio.get_event_loop().run_until_complete(runner.shutdown(timeout=1.0))
except RuntimeError:
# No running loop; spin up a throw-away one.
asyncio.run(runner.shutdown(timeout=1.0))
"""Release any session-alias store held by ``host``."""
if host._sessions_store is not None:
host._sessions_store.close()
# --------------------------------------------------------------------------- #
# state_dir=None preserves the in-memory contract #
# --------------------------------------------------------------------------- #
def test_state_dir_none_keeps_plain_dicts(tmp_path: Path) -> None:
"""No store, no sessions persistence, no files written."""
def test_state_dir_none_keeps_plain_alias_dict(tmp_path: Path) -> None:
"""No store, no alias persistence, no files written."""
host = AgentFrameworkHost(target=_AgentStub(), channels=[_ChannelStub()])
try:
assert host._sessions_store is None
assert isinstance(host._session_aliases, dict)
assert isinstance(host._active, dict)
assert isinstance(host._identities, dict)
# No accidental disk writes anywhere under tmp_path.
assert list(tmp_path.iterdir()) == []
finally:
# Nothing to close.
pass
assert host._sessions_store is None
assert isinstance(host._session_aliases, dict)
assert list(tmp_path.iterdir()) == []
# --------------------------------------------------------------------------- #
# Single string state_dir creates default subfolders #
# --------------------------------------------------------------------------- #
def test_string_state_dir_creates_subfolders(tmp_path: Path) -> None:
"""Passing a single path expands to ``runner/`` and ``sessions/``."""
def test_string_state_dir_creates_sessions_subfolder_only(tmp_path: Path) -> None:
"""Passing a single path expands to ``sessions/`` plus lazy checkpoint path."""
host = AgentFrameworkHost(
target=_AgentStub(),
channels=[_ChannelStub()],
@@ -108,100 +52,42 @@ def test_string_state_dir_creates_subfolders(tmp_path: Path) -> None:
)
try:
assert host._sessions_store is not None
assert (tmp_path / "runner").is_dir()
assert (tmp_path / "sessions").is_dir()
assert not (tmp_path / "runner").exists()
assert not (tmp_path / "links").exists()
# Checkpoint path is derived but not created for agent targets.
assert not (tmp_path / "checkpoints").exists()
finally:
_close_host_disk(host)
# --------------------------------------------------------------------------- #
# Per-component override via HostStatePaths-shaped dict #
# --------------------------------------------------------------------------- #
def test_per_component_paths(tmp_path: Path) -> None:
"""Dict form lets the caller route components to different roots."""
runner_dir = tmp_path / "tasks"
def test_per_component_session_path(tmp_path: Path) -> None:
"""Dict form lets callers route session aliases to a specific root."""
sessions_dir = tmp_path / "state"
host = AgentFrameworkHost(
target=_AgentStub(),
channels=[_ChannelStub()],
state_dir={"runner": runner_dir, "sessions": sessions_dir},
state_dir={"sessions": sessions_dir},
)
try:
assert runner_dir.is_dir()
assert sessions_dir.is_dir()
# Default subfolders should NOT exist when the caller provides
# explicit overrides.
assert not (tmp_path / "runner").is_dir() or runner_dir == (tmp_path / "runner")
assert not (tmp_path / "sessions").is_dir() or sessions_dir == (tmp_path / "sessions")
assert host._sessions_store is not None
assert host._checkpoint_location is None
finally:
_close_host_disk(host)
def test_unknown_component_key_raises(tmp_path: Path) -> None:
"""Misspelled keys should fail loudly so the user catches typos."""
@pytest.mark.parametrize("key", ["runner", "links", "active", "identities"])
def test_removed_state_dir_component_keys_raise(tmp_path: Path, key: str) -> None:
"""Obsolete follow-up components should fail loudly instead of becoming no-ops."""
with pytest.raises(ValueError, match="unknown"):
AgentFrameworkHost(
target=_AgentStub(),
channels=[_ChannelStub()],
state_dir={"runnerr": tmp_path / "x"}, # type: ignore[dict-item]
state_dir={key: tmp_path / key}, # type: ignore[dict-item]
)
def test_links_state_path_configures_compatible_identity_linker(tmp_path: Path) -> None:
"""``state_dir['links']`` is offered to linkers that accept host-owned persistence."""
linker = _ConfigurableLinker()
host = AgentFrameworkHost(
target=_AgentStub(),
channels=[_ChannelStub()],
identity_linker=linker,
state_dir=tmp_path,
)
try:
assert linker.configured_path == tmp_path / "links"
finally:
_close_host_disk(host)
def test_explicit_links_state_path_without_linker_warns(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
"""Explicit ``links`` path with no linker is almost certainly dead config."""
with caplog.at_level("WARNING", logger="agent_framework.hosting"):
host = AgentFrameworkHost(
target=_AgentStub(),
channels=[_ChannelStub()],
state_dir={"links": tmp_path / "links"},
)
try:
assert any(
"state_dir['links']" in rec.message and "no identity_linker" in rec.message for rec in caplog.records
)
finally:
_close_host_disk(host)
def test_links_state_path_with_nonconfigurable_linker_warns(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
"""A linker that owns its persistence directly gets a clear warning."""
with caplog.at_level("WARNING", logger="agent_framework.hosting"):
host = AgentFrameworkHost(
target=_AgentStub(),
channels=[_ChannelStub()],
identity_linker=_NonConfigurableLinker(),
state_dir={"links": tmp_path / "links"},
)
try:
assert any(
"state_dir['links']" in rec.message and "SupportsLinkStorePath" in rec.message for rec in caplog.records
)
finally:
_close_host_disk(host)
# --------------------------------------------------------------------------- #
# Session bookkeeping survives a host restart #
# --------------------------------------------------------------------------- #
def test_session_aliases_survive_restart(tmp_path: Path) -> None:
"""Aliases written on host #1 must be visible to host #2."""
state_dir = tmp_path / "state"
@@ -219,84 +105,6 @@ def test_session_aliases_survive_restart(tmp_path: Path) -> None:
_close_host_disk(host2)
def test_active_channel_survives_restart(tmp_path: Path) -> None:
"""``_active`` must round-trip through the store."""
state_dir = tmp_path / "state"
host1 = AgentFrameworkHost(target=_AgentStub(), channels=[_ChannelStub()], state_dir=state_dir)
host1._active["user-1"] = "telegram"
host1._active["user-2"] = "responses"
_close_host_disk(host1)
host2 = AgentFrameworkHost(target=_AgentStub(), channels=[_ChannelStub()], state_dir=state_dir)
try:
assert host2._active["user-1"] == "telegram"
assert host2._active["user-2"] == "responses"
finally:
_close_host_disk(host2)
def test_identities_nested_mutation_survives_restart(tmp_path: Path) -> None:
"""Setting ``self._identities[ik][channel] = identity`` must persist.
This exercises the proxy-inner-dict ``__setitem__`` write-through path,
not just the outer-key replacement path.
"""
state_dir = tmp_path / "state"
host1 = AgentFrameworkHost(target=_AgentStub(), channels=[_ChannelStub()], state_dir=state_dir)
ident_tg = ChannelIdentity("telegram", "tg-123", {"username": "alice"})
ident_rsp = ChannelIdentity("responses", "rsp-456")
# Mirrors the host-internal path in ``_register_identity``.
host1._identities.setdefault("user-1", {})["telegram"] = ident_tg
host1._identities.setdefault("user-1", {})["responses"] = ident_rsp
host1._identities.setdefault("user-2", {})["telegram"] = ChannelIdentity("telegram", "tg-789")
_close_host_disk(host1)
host2 = AgentFrameworkHost(target=_AgentStub(), channels=[_ChannelStub()], state_dir=state_dir)
try:
u1 = host2._identities["user-1"]
assert set(u1.keys()) == {"telegram", "responses"}
assert u1["telegram"].native_id == "tg-123"
assert u1["telegram"].attributes["username"] == "alice"
assert u1["responses"].native_id == "rsp-456"
assert host2._identities["user-2"]["telegram"].native_id == "tg-789"
finally:
_close_host_disk(host2)
# --------------------------------------------------------------------------- #
# Explicit durable_task_runner + state_dir['runner'] warns #
# --------------------------------------------------------------------------- #
def test_explicit_runner_with_runner_state_warns(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
"""Caller-owned runner + state_dir['runner'] → ignore + warn."""
from agent_framework_hosting import InProcessTaskRunner
user_runner = InProcessTaskRunner()
try:
with caplog.at_level("WARNING"):
host = AgentFrameworkHost(
target=_AgentStub(),
channels=[_ChannelStub()],
durable_task_runner=user_runner,
allow_in_process_runner=True,
state_dir={"runner": tmp_path / "runner"},
)
assert any("state_dir['runner']" in rec.message for rec in caplog.records)
# Sessions store wasn't requested, so still None.
assert host._sessions_store is None
finally:
# user_runner has no disk state, so nothing else to clean up.
pass
# --------------------------------------------------------------------------- #
# Workflow checkpoint integration #
# --------------------------------------------------------------------------- #
def _build_simple_workflow() -> Any:
"""Build a no-op workflow for checkpoint-wiring tests."""
from tests._workflow_fixtures import build_upper_workflow
@@ -313,7 +121,6 @@ def test_single_path_state_dir_wires_workflow_checkpoints(tmp_path: Path) -> Non
state_dir=tmp_path,
)
try:
# Checkpoint location is derived from the single state_dir.
assert host._checkpoint_location == tmp_path / "checkpoints"
finally:
_close_host_disk(host)
@@ -330,7 +137,6 @@ def test_mapping_state_dir_checkpoints_key_wires_workflow_checkpoints(tmp_path:
)
try:
assert host._checkpoint_location == ckpt_dir
# No diskcache components were requested.
assert host._sessions_store is None
finally:
_close_host_disk(host)
@@ -342,9 +148,7 @@ def test_mapping_state_dir_omits_checkpoints_for_workflow(tmp_path: Path) -> Non
host = AgentFrameworkHost(
target=workflow,
channels=[_ChannelStub()],
# No 'checkpoints' key → no checkpoint persistence even though
# other components are persisted.
state_dir={"runner": tmp_path / "r", "sessions": tmp_path / "s"},
state_dir={"sessions": tmp_path / "s"},
)
try:
assert host._checkpoint_location is None
@@ -381,7 +185,6 @@ def test_state_dir_checkpoints_for_agent_target_silent_for_single_path(tmp_path:
)
try:
assert host._checkpoint_location is None
# ``checkpoints/`` subfolder is not eagerly created (no consumer).
assert not (tmp_path / "checkpoints").exists()
finally:
_close_host_disk(host)
@@ -390,7 +193,7 @@ def test_state_dir_checkpoints_for_agent_target_silent_for_single_path(tmp_path:
def test_state_dir_checkpoints_for_agent_target_warns_when_explicit(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""Mapping form with ``checkpoints`` + agent target → warn (dead config)."""
"""Mapping form with ``checkpoints`` + agent target → warn."""
with caplog.at_level("WARNING", logger="agent_framework.hosting"):
host = AgentFrameworkHost(
target=_AgentStub(),