simbiot-harness/venv/lib/python3.12/site-packages/strands/experimental/bidi/_telemetry.py
Simbiot Harness b60439152b Simbiot Harness v0.1: тактовая лестница на Strands + CQRS/ES-спина
- harness/es.py: append-only event store + проекции (образец — кооп-портал)
- harness/levels.py: уровни 1с/1м/1ч/1д, контекст ∝ масштабу
- harness/scheduler.py: уровневые потоки, идемпотентные тики, absorbed-стабы
- harness/isolated_runner.py + _llm_worker.py: LLM-тик в сабпроцессе
  (глобальный asyncio-рантайм Strands не переживает 2+ create_harness)
- cli.py: init/task/tasks/status/events + requeue (написан самим минутным агентом)
- 9router: отдельный ключ sk-simbiot, модель auto (glm-5.3 первым)
2026-09-29 15:28:50 +00:00

201 lines
6.8 KiB
Python

"""Telemetry helpers for bidirectional streaming agent.
Wraps the shared Tracer primitives with bidi-specific span creation and attribute names.
Uses Tracer._start_span/_end_span directly rather than adding bidi-specific public methods
to the shared Tracer class, keeping experimental bidi telemetry self-contained.
"""
from opentelemetry.trace import Span
from ...telemetry.tracer import Tracer, serialize
from ...types.traces import AttributeValue
def start_session_span(
tracer: Tracer,
agent_name: str,
model_id: str | None = None,
tools: list[str] | None = None,
system_prompt: str | None = None,
) -> Span:
"""Start a span for the bidi session lifecycle.
Args:
tracer: Tracer instance.
agent_name: Name of the bidi agent.
model_id: Model identifier.
tools: List of tool names available to the agent.
system_prompt: System prompt configured on the agent.
Returns:
The session span.
"""
attributes: dict[str, AttributeValue] = tracer._get_common_attributes(operation_name="bidi_session")
attributes["gen_ai.agent.name"] = agent_name
if model_id:
attributes["gen_ai.request.model"] = model_id
if tools:
attributes["gen_ai.agent.tools"] = serialize(tools)
if system_prompt:
attributes["gen_ai.system_instructions"] = tracer._redact("gen_ai.system_instructions", system_prompt)
return tracer._start_span(f"bidi_session {agent_name}", attributes=attributes)
def end_session_span(
tracer: Tracer,
span: Span,
input_tokens: int = 0,
output_tokens: int = 0,
total_tokens: int = 0,
cache_read_input_tokens: int = 0,
error: Exception | None = None,
) -> None:
"""End the bidi session span.
Args:
tracer: Tracer instance.
span: The session span to end.
input_tokens: Accumulated input tokens for the session.
output_tokens: Accumulated output tokens for the session.
total_tokens: Accumulated total tokens for the session.
cache_read_input_tokens: Accumulated tokens read from cache for the session.
error: Exception if the session ended with an error.
"""
token_attributes = {
"gen_ai.usage.input_tokens": input_tokens,
"gen_ai.usage.output_tokens": output_tokens,
"gen_ai.usage.total_tokens": total_tokens,
"gen_ai.usage.cache_read.input_tokens": cache_read_input_tokens,
}
if not tracer.use_latest_genai_conventions:
# Deprecated pre-semconv name, dual-emitted unless opted into the latest conventions
token_attributes["gen_ai.usage.cache_read_input_tokens"] = cache_read_input_tokens
attributes: dict[str, AttributeValue] = {name: value for name, value in token_attributes.items() if value > 0}
tracer._end_span(span, attributes=attributes, error=error)
def start_response_span(tracer: Tracer, response_id: str, parent_span: Span | None = None) -> Span:
"""Start a span for a model response.
Args:
tracer: Tracer instance.
response_id: Unique identifier for the response.
parent_span: Parent session span.
Returns:
The response span.
"""
attributes: dict[str, AttributeValue] = tracer._get_common_attributes(operation_name="bidi_response")
attributes["gen_ai.response.id"] = response_id
# Static span name (response_id is recorded as an attribute) to avoid high-cardinality names.
return tracer._start_span("bidi_response", parent_span, attributes=attributes)
def end_response_span(
tracer: Tracer,
span: Span,
time_to_first_audio_ms: int | None = None,
error: Exception | None = None,
) -> None:
"""End a model response span.
Args:
tracer: Tracer instance.
span: The response span to end.
time_to_first_audio_ms: Milliseconds from response start to the first audio chunk, if any
audio was emitted for this response.
error: Exception if the response ended with an error.
"""
attributes: dict[str, AttributeValue] = {}
if time_to_first_audio_ms is not None:
attributes["gen_ai.server.time_to_first_audio"] = time_to_first_audio_ms
tracer._end_span(span, attributes=attributes, error=error)
def start_restart_span(
tracer: Tracer,
parent_span: Span | None = None,
reason: str = "timeout",
error_message: str | None = None,
) -> Span:
"""Start a span for a connection restart.
Args:
tracer: Tracer instance.
parent_span: Parent session span.
reason: What triggered the restart ("timeout" reactively, "scheduled" proactively).
error_message: The timeout error message that triggered a reactive restart, if any.
Returns:
The restart span.
"""
attributes: dict[str, AttributeValue] = tracer._get_common_attributes(operation_name="bidi_connection_restart")
attributes["gen_ai.bidi.restart_reason"] = reason
# The message of the timeout that triggered a reactive restart. Namespaced under
# gen_ai.bidi.* (not gen_ai.error.*) so it does not read as a failure of this span,
# which may end successfully; a failed restart is recorded via end_restart_span.
if error_message:
attributes["gen_ai.bidi.restart_error_message"] = error_message
return tracer._start_span("bidi_connection_restart", parent_span, attributes=attributes)
def end_restart_span(tracer: Tracer, span: Span, error: Exception | None = None) -> None:
"""End a connection restart span.
Args:
tracer: Tracer instance.
span: The restart span to end.
error: Exception if the restart failed.
"""
tracer._end_span(span, error=error)
def start_connection_span(tracer: Tracer, parent_span: Span | None = None, model_id: str | None = None) -> Span:
"""Start a span for the model connection establishment.
Args:
tracer: Tracer instance.
parent_span: Parent session span.
model_id: Model identifier being connected to.
Returns:
The connection span.
"""
attributes: dict[str, AttributeValue] = tracer._get_common_attributes(operation_name="bidi_connect")
if model_id:
attributes["gen_ai.request.model"] = model_id
return tracer._start_span("bidi_connect", parent_span, attributes=attributes)
def end_connection_span(tracer: Tracer, span: Span, error: Exception | None = None) -> None:
"""End the model connection span.
Args:
tracer: Tracer instance.
span: The connection span to end.
error: Exception if the connection failed.
"""
tracer._end_span(span, error=error)
def add_barge_in_event(span: Span, reason: str) -> None:
"""Record a barge-in as a span event on the session span.
Args:
span: The session span to add the event to.
reason: Reason for the barge-in.
"""
if span and span.is_recording():
span.add_event("bidi_barge_in", attributes={"barge_in.reason": reason})