Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

287 lines
8.8 KiB
Python

"""Typed event contract for the shared agent runtime."""
# ruff: noqa: UP040
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any, Literal, TypeAlias
# CodeQL's explicit-export query does not recognize Python 3.12 ``type``
# statements in __all__, so keep these exported aliases as TypeAlias assignments.
RuntimeEventType: TypeAlias = Literal[
"agent_start",
"turn_start",
"message_start",
"message_update",
"provider_request_start",
"provider_request_end",
"tool_execution_start",
"tool_execution_update",
"tool_execution_end",
"turn_end",
"agent_end",
]
RuntimeEventKind: TypeAlias = RuntimeEventType
@dataclass(frozen=True)
class AgentStartEvent:
type: Literal["agent_start"] = "agent_start"
data: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class TurnStartEvent:
iteration: int
type: Literal["turn_start"] = "turn_start"
data: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class MessageStartEvent:
message: Any
iteration: int | None = None
type: Literal["message_start"] = "message_start"
data: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class MessageUpdateEvent:
message: Any
delta: str | None = None
iteration: int | None = None
type: Literal["message_update"] = "message_update"
data: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class ProviderRequestStartEvent:
iteration: int
message_count: int
type: Literal["provider_request_start"] = "provider_request_start"
data: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class ProviderRequestEndEvent:
iteration: int
has_tool_calls: bool
type: Literal["provider_request_end"] = "provider_request_end"
data: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class ToolExecutionStartEvent:
tool_call_id: str
tool_name: str
args: dict[str, Any]
iteration: int
type: Literal["tool_execution_start"] = "tool_execution_start"
data: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class ToolExecutionUpdateEvent:
tool_call_id: str
tool_name: str
args: dict[str, Any]
partial_result: Any
iteration: int
type: Literal["tool_execution_update"] = "tool_execution_update"
data: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class ToolExecutionEndEvent:
tool_call_id: str
tool_name: str
args: dict[str, Any]
result: Any
is_error: bool
iteration: int
type: Literal["tool_execution_end"] = "tool_execution_end"
data: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class TurnEndEvent:
iteration: int
message: Any
tool_results: tuple[Any, ...] = ()
type: Literal["turn_end"] = "turn_end"
data: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class AgentEndEvent:
messages: tuple[Any, ...] = ()
type: Literal["agent_end"] = "agent_end"
data: dict[str, Any] = field(default_factory=dict)
RuntimeEvent: TypeAlias = (
AgentStartEvent
| TurnStartEvent
| MessageStartEvent
| MessageUpdateEvent
| ProviderRequestStartEvent
| ProviderRequestEndEvent
| ToolExecutionStartEvent
| ToolExecutionUpdateEvent
| ToolExecutionEndEvent
| TurnEndEvent
| AgentEndEvent
)
RuntimeEventCallback: TypeAlias = Callable[[RuntimeEvent], None]
# Callback shape used by surfaces to receive events as ``(kind, data)`` tuples.
# The typed :data:`RuntimeEventCallback` is the internal shape ``Agent`` calls;
# ``TupleEventCallback`` is what surfaces provide from their observer hooks.
TupleEventCallback: TypeAlias = Callable[[str, dict[str, Any]], None]
def tool_result_is_error(result: Any) -> bool:
return isinstance(result, dict) and "error" in result
def runtime_event_from_tuple(kind: str, data: dict[str, Any]) -> RuntimeEvent | None:
"""Convert a ``(kind, data)`` tuple payload to a typed :class:`RuntimeEvent` when possible."""
payload = dict(data)
if kind == "agent_start":
return AgentStartEvent(data=payload)
if kind == "llm_start":
return TurnStartEvent(iteration=int(payload.get("iteration", 0)), data=payload)
if kind == "provider_request_start":
return ProviderRequestStartEvent(
iteration=int(payload.get("iteration", 0)),
message_count=int(payload.get("message_count", 0)),
data=payload,
)
if kind == "provider_request_end":
return ProviderRequestEndEvent(
iteration=int(payload.get("iteration", 0)),
has_tool_calls=bool(payload.get("has_tool_calls", False)),
data=payload,
)
if kind == "tool_start":
args = payload.get("input")
return ToolExecutionStartEvent(
tool_call_id=str(payload.get("id") or payload.get("tool_call_id") or ""),
tool_name=str(payload.get("name") or payload.get("tool_name") or "tool"),
args=dict(args) if isinstance(args, dict) else {},
iteration=int(payload.get("iteration", -1)),
data=payload,
)
if kind == "tool_update":
args = payload.get("input")
return ToolExecutionUpdateEvent(
tool_call_id=str(payload.get("id") or payload.get("tool_call_id") or ""),
tool_name=str(payload.get("name") or payload.get("tool_name") or "tool"),
args=dict(args) if isinstance(args, dict) else {},
partial_result=payload.get("update"),
iteration=int(payload.get("iteration", -1)),
data=payload,
)
if kind == "tool_end":
args = payload.get("input")
output = payload.get("output")
return ToolExecutionEndEvent(
tool_call_id=str(payload.get("id") or payload.get("tool_call_id") or ""),
tool_name=str(payload.get("name") or payload.get("tool_name") or "tool"),
args=dict(args) if isinstance(args, dict) else {},
result=output,
is_error=tool_result_is_error(output),
iteration=int(payload.get("iteration", -1)),
data=payload,
)
if kind == "agent_end":
return AgentEndEvent(data=payload)
return None
def tuple_payload_from_event(event: RuntimeEvent) -> tuple[str, dict[str, Any]] | None:
"""Map a typed :class:`RuntimeEvent` onto the ``(kind, data)`` tuple callback shape."""
if isinstance(event, AgentStartEvent):
return "agent_start", dict(event.data)
if isinstance(event, TurnStartEvent):
return "llm_start", {"iteration": event.iteration, **event.data}
if isinstance(event, ToolExecutionStartEvent):
return (
"tool_start",
{
"id": event.tool_call_id,
"name": event.tool_name,
"input": event.args,
**event.data,
},
)
if isinstance(event, ToolExecutionUpdateEvent):
return (
"tool_update",
{
"id": event.tool_call_id,
"name": event.tool_name,
"input": event.args,
"update": event.partial_result,
**event.data,
},
)
if isinstance(event, ToolExecutionEndEvent):
return (
"tool_end",
{
"id": event.tool_call_id,
"name": event.tool_name,
"input": event.args,
"output": event.result,
**event.data,
},
)
if isinstance(event, AgentEndEvent):
return "agent_end", dict(event.data)
return None
def runtime_event_callback_from_observer(
observer: Callable[..., None] | None,
) -> RuntimeEventCallback | None:
"""Adapt a ``(kind, data)``-tuple observer to a :class:`RuntimeEventCallback`.
Returns ``None`` when ``observer`` is omitted so callers can pass the result
straight to ``Agent(on_runtime_event=...)``.
"""
if observer is None:
return None
def on_runtime_event(event: RuntimeEvent) -> None:
payload = tuple_payload_from_event(event)
if payload is not None:
observer(*payload)
return on_runtime_event
__all__ = [
"AgentEndEvent",
"AgentStartEvent",
"MessageStartEvent",
"MessageUpdateEvent",
"ProviderRequestEndEvent",
"ProviderRequestStartEvent",
"RuntimeEvent",
"RuntimeEventCallback",
"RuntimeEventKind",
"RuntimeEventType",
"ToolExecutionEndEvent",
"ToolExecutionStartEvent",
"ToolExecutionUpdateEvent",
"TupleEventCallback",
"TurnEndEvent",
"TurnStartEvent",
"runtime_event_callback_from_observer",
"runtime_event_from_tuple",
"tool_result_is_error",
"tuple_payload_from_event",
]