Files
opensquilla--opensquilla/tests/test_engine/test_ensemble_progress_forwarding.py
2026-07-13 13:12:33 +08:00

95 lines
3.1 KiB
Python

from __future__ import annotations
from collections.abc import AsyncIterator
from typing import Any
import pytest
from opensquilla.engine import Agent, AgentConfig, ToolResult
from opensquilla.engine.types import EnsembleProgressEvent as EngineEnsembleProgressEvent
from opensquilla.engine.types import ToolCall
from opensquilla.provider import (
ChatConfig,
Message,
ToolDefinition,
ToolInputSchema,
)
from opensquilla.provider import (
DoneEvent as ProviderDone,
)
from opensquilla.provider import (
TextDeltaEvent as ProviderText,
)
from opensquilla.provider.types import (
EnsembleProgressEvent as ProviderEnsembleProgressEvent,
)
class _EnsembleLikeProvider:
"""A provider that emits mid-stream ensemble_progress deltas, exactly like the
real EnsembleProvider does for its proposers."""
provider_name = "fake"
def chat(
self,
messages: list[Message],
tools: list[Any] | None = None,
config: ChatConfig | None = None,
) -> AsyncIterator[Any]:
return self._stream()
async def _stream(self) -> AsyncIterator[Any]:
yield ProviderEnsembleProgressEvent(
event_type="proposer_start",
proposer_label="anchor",
proposer_provider="openrouter",
proposer_model="qwen/qwen3.7-plus",
)
yield ProviderEnsembleProgressEvent(
event_type="proposer_finish",
proposer_label="anchor",
proposer_provider="openrouter",
proposer_model="qwen/qwen3.7-plus",
input_tokens=10,
output_tokens=5,
)
yield ProviderText(text="synthesized answer")
yield ProviderDone(stop_reason="stop", input_tokens=1, output_tokens=1)
async def list_models(self) -> list[Any]:
return []
async def _tool_handler(call: ToolCall) -> ToolResult:
return ToolResult(tool_use_id=call.tool_use_id, tool_name=call.tool_name, content="ok")
@pytest.mark.asyncio
async def test_agent_forwards_provider_ensemble_progress_as_engine_event() -> None:
# This is the previously-unverified link: the ensemble provider yields
# provider-level EnsembleProgressEvents; the agent loop must re-emit them as
# engine-level EnsembleProgressEvents so channel_dispatch can broadcast them.
agent = Agent(
provider=_EnsembleLikeProvider(),
config=AgentConfig(max_iterations=2),
tool_definitions=[
ToolDefinition(
name="echo",
description="Echo.",
input_schema=ToolInputSchema(properties={}, required=[]),
)
],
tool_handler=_tool_handler,
)
events = [event async for event in agent.run_turn("hi")]
progress = [e for e in events if isinstance(e, EngineEnsembleProgressEvent)]
assert len(progress) == 2, f"expected 2 engine ensemble_progress events, got {len(progress)}"
assert {p.event_type for p in progress} == {"proposer_start", "proposer_finish"}
assert progress[0].proposer_model == "qwen/qwen3.7-plus"
finish = next(p for p in progress if p.event_type == "proposer_finish")
assert finish.input_tokens == 10
assert finish.output_tokens == 5