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

201 lines
6.5 KiB
Python

"""Telemetry for bounded ReAct agent runs in action and gather phases.
``stop_reason`` mapping from :class:`~core.agent.run_io.AgentRunResult`:
- ``completed`` — loop ended normally (conclusion accepted or tool terminate)
- ``iteration_cap`` — ``hit_iteration_cap`` is true
- ``error`` — ``Agent.run`` raised before returning
- ``cancelled`` — ``KeyboardInterrupt`` during ``Agent.run``
- ``no_tools_needed`` — loop finished without executing any tools
"""
from __future__ import annotations
import time
from collections.abc import Sequence
from typing import Any, Literal
from core.agent import Agent
from core.agent.run_io import AgentRunResult
from core.agent_harness.accounting.token_accounting import resolve_model_name, resolve_provider_name
from core.agent_harness.ports import SessionStore
from core.messages import RuntimeMessageLike
from platform.analytics.cli import capture_react_turn_completed
from platform.analytics.investigation_loop import bound_loop_metrics
from platform.analytics.repl_context import (
get_cli_session_id,
get_cli_turn_kind,
get_prompt_turn_id,
)
ReactPhase = Literal["action", "gather"]
ReactStopReason = Literal["completed", "iteration_cap", "error", "cancelled", "no_tools_needed"]
def resolve_react_stop_reason(
*,
hit_iteration_cap: bool,
tool_calls_executed: int,
error: BaseException | None = None,
cancelled: bool = False,
) -> ReactStopReason:
"""Map a finished or failed Agent.run to the public stop-reason enum."""
if cancelled:
return "cancelled"
if error is not None:
return "error"
if hit_iteration_cap:
return "iteration_cap"
if tool_calls_executed == 0:
return "no_tools_needed"
return "completed"
def _session_investigation_id(session: SessionStore | None) -> str | None:
if session is None:
return None
investigation_id = getattr(session, "last_investigation_id", None)
if isinstance(investigation_id, str) and investigation_id.strip():
return investigation_id.strip()
return None
def _session_investigation_loop_count(session: SessionStore | None) -> int | None:
bound = bound_loop_metrics()
if bound is not None:
return bound[0]
if session is None:
return None
loop_count = getattr(session, "investigation_loop_count", None)
if isinstance(loop_count, bool):
return None
if isinstance(loop_count, int | float):
return int(loop_count)
return None
def _resolve_cli_session_id(session: SessionStore | None) -> str:
bound = get_cli_session_id()
if bound:
return bound
session_id = getattr(session, "session_id", None) if session is not None else None
return session_id if isinstance(session_id, str) and session_id else ""
def _partial_result_from_agent(agent: Agent[Any]) -> AgentRunResult | None:
"""Build a partial run result when Agent.run aborts before finalize."""
iterations_used = int(getattr(agent, "_react_iterations_used", 0) or 0)
executed = getattr(agent, "_react_executed", None)
if not isinstance(executed, list):
executed = []
if iterations_used == 0 and not executed:
return None
return AgentRunResult(
messages=[],
final_text="",
executed=executed,
hit_iteration_cap=bool(getattr(agent, "_react_hit_iteration_cap", False)),
llm_iterations_used=iterations_used,
)
def emit_react_turn_completed(
*,
phase: ReactPhase,
result: AgentRunResult | None,
iteration_cap: int,
duration_ms: int,
llm: Any,
session: SessionStore | None = None,
error: BaseException | None = None,
cancelled: bool = False,
) -> None:
"""Emit one ``react_turn_completed`` lifecycle event for an Agent.run."""
tool_calls_executed = len(result.executed) if result is not None else 0
llm_iterations_used = result.llm_iterations_used if result is not None else 0
hit_iteration_cap = bool(result.hit_iteration_cap) if result is not None else False
stop_reason = resolve_react_stop_reason(
hit_iteration_cap=hit_iteration_cap,
tool_calls_executed=tool_calls_executed,
error=error,
cancelled=cancelled,
)
hit_iteration_cap = stop_reason == "iteration_cap"
cli_turn_kind = get_cli_turn_kind() or "agent"
investigation_id = _session_investigation_id(session)
investigation_loop_count = _session_investigation_loop_count(session)
capture_react_turn_completed(
phase=phase,
llm_iterations_used=llm_iterations_used,
llm_iteration_cap=iteration_cap,
hit_iteration_cap=hit_iteration_cap,
stop_reason=stop_reason,
tool_calls_executed=tool_calls_executed,
duration_ms=duration_ms,
cli_session_id=_resolve_cli_session_id(session),
cli_turn_kind=cli_turn_kind,
llm_provider=resolve_provider_name(llm) or "unknown",
llm_model=resolve_model_name(llm) or "unknown",
investigation_id=investigation_id,
investigation_loop_count=investigation_loop_count,
prompt_turn_id=get_prompt_turn_id(),
)
def run_react_agent_with_telemetry(
agent: Agent[Any],
initial_messages: Sequence[RuntimeMessageLike],
*,
phase: ReactPhase,
iteration_cap: int,
llm: Any,
session: SessionStore | None = None,
) -> AgentRunResult:
"""Run ``agent.run`` and emit exactly one ``react_turn_completed`` event."""
started = time.monotonic()
try:
result = agent.run(initial_messages)
except KeyboardInterrupt:
emit_react_turn_completed(
phase=phase,
result=_partial_result_from_agent(agent),
iteration_cap=iteration_cap,
duration_ms=int((time.monotonic() - started) * 1000),
llm=llm,
session=session,
cancelled=True,
)
raise
except Exception as exc:
emit_react_turn_completed(
phase=phase,
result=_partial_result_from_agent(agent),
iteration_cap=iteration_cap,
duration_ms=int((time.monotonic() - started) * 1000),
llm=llm,
session=session,
error=exc,
)
raise
emit_react_turn_completed(
phase=phase,
result=result,
iteration_cap=iteration_cap,
duration_ms=int((time.monotonic() - started) * 1000),
llm=llm,
session=session,
)
return result
__all__ = [
"ReactPhase",
"ReactStopReason",
"emit_react_turn_completed",
"resolve_react_stop_reason",
"run_react_agent_with_telemetry",
]