From c56a155178bb91125c8cee2e570436e098842211 Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Fri, 11 Sep 2026 15:04:18 -0600 Subject: [PATCH 01/14] feat: add InvokeAgent semantic parameter models Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../opentelemetry/a365/core/__init__.py | 4 +++ .../a365/core/gen_ai_request_parameters.py | 25 +++++++++++++++++++ .../a365/core/gen_ai_response_parameters.py | 18 +++++++++++++ .../a365/core/invoke_agent_details.py | 4 +++ tests/a365/test_invoke_agent_scope.py | 19 ++++++++++++++ 5 files changed, 70 insertions(+) create mode 100644 src/microsoft/opentelemetry/a365/core/gen_ai_request_parameters.py create mode 100644 src/microsoft/opentelemetry/a365/core/gen_ai_response_parameters.py create mode 100644 tests/a365/test_invoke_agent_scope.py diff --git a/src/microsoft/opentelemetry/a365/core/__init__.py b/src/microsoft/opentelemetry/a365/core/__init__.py index a3e5347f..5f033c32 100644 --- a/src/microsoft/opentelemetry/a365/core/__init__.py +++ b/src/microsoft/opentelemetry/a365/core/__init__.py @@ -15,6 +15,8 @@ from microsoft.opentelemetry.a365.core.guardrail_finding import GuardrailFinding from microsoft.opentelemetry.a365.core.guardrail_risk_severity import GuardrailRiskSeverity from microsoft.opentelemetry.a365.core.guardrail_target_type import GuardrailTargetType +from microsoft.opentelemetry.a365.core.gen_ai_request_parameters import GenAiRequestParameters +from microsoft.opentelemetry.a365.core.gen_ai_response_parameters import GenAiResponseParameters from microsoft.opentelemetry.a365.core.inference_call_details import InferenceCallDetails from microsoft.opentelemetry.a365.core.models.service_endpoint import ServiceEndpoint from microsoft.opentelemetry.a365.core.inference_operation_type import InferenceOperationType @@ -84,6 +86,8 @@ "SpanDetails", "InferenceCallDetails", "ServiceEndpoint", + "GenAiRequestParameters", + "GenAiResponseParameters", # Enums "InferenceOperationType", "ToolType", diff --git a/src/microsoft/opentelemetry/a365/core/gen_ai_request_parameters.py b/src/microsoft/opentelemetry/a365/core/gen_ai_request_parameters.py new file mode 100644 index 00000000..98fd70c5 --- /dev/null +++ b/src/microsoft/opentelemetry/a365/core/gen_ai_request_parameters.py @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""GenAI request parameters for InvokeAgent telemetry.""" + +from dataclasses import dataclass +from typing import Sequence + + +@dataclass +class GenAiRequestParameters: + """Optional GenAI request parameters recorded on InvokeAgent spans.""" + + model: str | None = None + seed: int | None = None + choice_count: int | None = None + frequency_penalty: float | None = None + max_tokens: int | None = None + presence_penalty: float | None = None + stop_sequences: Sequence[str] | None = None + temperature: float | None = None + top_p: float | None = None + data_source_id: str | None = None + output_type: str | None = None + system_instructions: str | None = None diff --git a/src/microsoft/opentelemetry/a365/core/gen_ai_response_parameters.py b/src/microsoft/opentelemetry/a365/core/gen_ai_response_parameters.py new file mode 100644 index 00000000..3b975646 --- /dev/null +++ b/src/microsoft/opentelemetry/a365/core/gen_ai_response_parameters.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""GenAI response parameters for InvokeAgent telemetry.""" + +from dataclasses import dataclass +from typing import Sequence + + +@dataclass +class GenAiResponseParameters: + """Optional GenAI response parameters recorded on InvokeAgent spans.""" + + finish_reasons: Sequence[str] | None = None + input_tokens: int | None = None + output_tokens: int | None = None + cache_creation_input_tokens: int | None = None + cache_read_input_tokens: int | None = None diff --git a/src/microsoft/opentelemetry/a365/core/invoke_agent_details.py b/src/microsoft/opentelemetry/a365/core/invoke_agent_details.py index 2014b6af..e86abd38 100644 --- a/src/microsoft/opentelemetry/a365/core/invoke_agent_details.py +++ b/src/microsoft/opentelemetry/a365/core/invoke_agent_details.py @@ -5,6 +5,8 @@ from dataclasses import dataclass +from microsoft.opentelemetry.a365.core.gen_ai_request_parameters import GenAiRequestParameters +from microsoft.opentelemetry.a365.core.gen_ai_response_parameters import GenAiResponseParameters from microsoft.opentelemetry.a365.core.models.service_endpoint import ServiceEndpoint @@ -13,3 +15,5 @@ class InvokeAgentScopeDetails: """Scope-level configuration for agent invocation tracing.""" endpoint: ServiceEndpoint | None = None + request_parameters: GenAiRequestParameters | None = None + response_parameters: GenAiResponseParameters | None = None diff --git a/tests/a365/test_invoke_agent_scope.py b/tests/a365/test_invoke_agent_scope.py new file mode 100644 index 00000000..ae40523f --- /dev/null +++ b/tests/a365/test_invoke_agent_scope.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from microsoft.opentelemetry.a365.core import ( + GenAiRequestParameters, + GenAiResponseParameters, + InvokeAgentScopeDetails, +) + + +def test_invoke_agent_details_accepts_semantic_parameters(): + request = GenAiRequestParameters(model="gpt-4o", max_tokens=128) + response = GenAiResponseParameters(input_tokens=10, output_tokens=4) + details = InvokeAgentScopeDetails( + request_parameters=request, + response_parameters=response, + ) + assert details.request_parameters is request + assert details.response_parameters is response From 2a52744225d582ee2792387151fdea74eebb29b7 Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Fri, 11 Sep 2026 15:06:09 -0600 Subject: [PATCH 02/14] feat: emit InvokeAgent GenAI semantic fields Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/microsoft/opentelemetry/a365/constants.py | 9 ++ .../opentelemetry/a365/core/constants.py | 9 ++ .../a365/core/invoke_agent_scope.py | 61 ++++++++++ tests/a365/test_invoke_agent_scope.py | 110 ++++++++++++++++++ 4 files changed, 189 insertions(+) diff --git a/src/microsoft/opentelemetry/a365/constants.py b/src/microsoft/opentelemetry/a365/constants.py index a5af7008..763f0d05 100644 --- a/src/microsoft/opentelemetry/a365/constants.py +++ b/src/microsoft/opentelemetry/a365/constants.py @@ -36,8 +36,15 @@ GEN_AI_CLIENT_OPERATION_DURATION_METRIC_NAME = "gen_ai.client.operation.duration" GEN_AI_CLIENT_TOKEN_USAGE_METRIC_NAME = "gen_ai.client.token.usage" GEN_AI_OPERATION_NAME_KEY = "gen_ai.operation.name" +GEN_AI_DATA_SOURCE_ID_KEY = "gen_ai.data_source.id" +GEN_AI_OUTPUT_TYPE_KEY = "gen_ai.output.type" +GEN_AI_REQUEST_CHOICE_COUNT_KEY = "gen_ai.request.choice.count" +GEN_AI_REQUEST_FREQUENCY_PENALTY_KEY = "gen_ai.request.frequency_penalty" GEN_AI_REQUEST_MAX_TOKENS_KEY = "gen_ai.request.max_tokens" GEN_AI_REQUEST_MODEL_KEY = "gen_ai.request.model" +GEN_AI_REQUEST_PRESENCE_PENALTY_KEY = "gen_ai.request.presence_penalty" +GEN_AI_REQUEST_SEED_KEY = "gen_ai.request.seed" +GEN_AI_REQUEST_STOP_SEQUENCES_KEY = "gen_ai.request.stop_sequences" GEN_AI_REQUEST_TEMPERATURE_KEY = "gen_ai.request.temperature" GEN_AI_REQUEST_TOP_P_KEY = "gen_ai.request.top_p" GEN_AI_RESPONSE_FINISH_REASONS_KEY = "gen_ai.response.finish_reasons" @@ -52,6 +59,8 @@ GEN_AI_CONVERSATION_ID_KEY = "gen_ai.conversation.id" GEN_AI_CONVERSATION_ITEM_LINK_KEY = "microsoft.conversation.item.link" GEN_AI_TOKEN_TYPE_KEY = "gen_ai.token.type" +GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS_KEY = "gen_ai.usage.cache_creation.input_tokens" +GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_KEY = "gen_ai.usage.cache_read.input_tokens" GEN_AI_USAGE_INPUT_TOKENS_KEY = "gen_ai.usage.input_tokens" GEN_AI_USAGE_OUTPUT_TOKENS_KEY = "gen_ai.usage.output_tokens" GEN_AI_CHOICE = "gen_ai.choice" diff --git a/src/microsoft/opentelemetry/a365/core/constants.py b/src/microsoft/opentelemetry/a365/core/constants.py index 6644ed03..eae338bc 100644 --- a/src/microsoft/opentelemetry/a365/core/constants.py +++ b/src/microsoft/opentelemetry/a365/core/constants.py @@ -33,8 +33,15 @@ GEN_AI_CLIENT_OPERATION_DURATION_METRIC_NAME = "gen_ai.client.operation.duration" GEN_AI_CLIENT_TOKEN_USAGE_METRIC_NAME = "gen_ai.client.token.usage" GEN_AI_OPERATION_NAME_KEY = "gen_ai.operation.name" +GEN_AI_DATA_SOURCE_ID_KEY = "gen_ai.data_source.id" +GEN_AI_OUTPUT_TYPE_KEY = "gen_ai.output.type" +GEN_AI_REQUEST_CHOICE_COUNT_KEY = "gen_ai.request.choice.count" +GEN_AI_REQUEST_FREQUENCY_PENALTY_KEY = "gen_ai.request.frequency_penalty" GEN_AI_REQUEST_MAX_TOKENS_KEY = "gen_ai.request.max_tokens" GEN_AI_REQUEST_MODEL_KEY = "gen_ai.request.model" +GEN_AI_REQUEST_PRESENCE_PENALTY_KEY = "gen_ai.request.presence_penalty" +GEN_AI_REQUEST_SEED_KEY = "gen_ai.request.seed" +GEN_AI_REQUEST_STOP_SEQUENCES_KEY = "gen_ai.request.stop_sequences" GEN_AI_REQUEST_TEMPERATURE_KEY = "gen_ai.request.temperature" GEN_AI_REQUEST_TOP_P_KEY = "gen_ai.request.top_p" GEN_AI_RESPONSE_FINISH_REASONS_KEY = "gen_ai.response.finish_reasons" @@ -49,6 +56,8 @@ GEN_AI_CONVERSATION_ID_KEY = "gen_ai.conversation.id" GEN_AI_CONVERSATION_ITEM_LINK_KEY = "microsoft.conversation.item.link" GEN_AI_TOKEN_TYPE_KEY = "gen_ai.token.type" +GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS_KEY = "gen_ai.usage.cache_creation.input_tokens" +GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_KEY = "gen_ai.usage.cache_read.input_tokens" GEN_AI_USAGE_INPUT_TOKENS_KEY = "gen_ai.usage.input_tokens" GEN_AI_USAGE_OUTPUT_TOKENS_KEY = "gen_ai.usage.output_tokens" GEN_AI_CHOICE = "gen_ai.choice" diff --git a/src/microsoft/opentelemetry/a365/core/invoke_agent_scope.py b/src/microsoft/opentelemetry/a365/core/invoke_agent_scope.py index b6a04071..3daca012 100644 --- a/src/microsoft/opentelemetry/a365/core/invoke_agent_scope.py +++ b/src/microsoft/opentelemetry/a365/core/invoke_agent_scope.py @@ -20,8 +20,25 @@ GEN_AI_CALLER_AGENT_VERSION_KEY, GEN_AI_CALLER_CLIENT_IP_KEY, GEN_AI_CONVERSATION_ID_KEY, + GEN_AI_DATA_SOURCE_ID_KEY, GEN_AI_INPUT_MESSAGES_KEY, GEN_AI_OUTPUT_MESSAGES_KEY, + GEN_AI_OUTPUT_TYPE_KEY, + GEN_AI_REQUEST_CHOICE_COUNT_KEY, + GEN_AI_REQUEST_FREQUENCY_PENALTY_KEY, + GEN_AI_REQUEST_MAX_TOKENS_KEY, + GEN_AI_REQUEST_MODEL_KEY, + GEN_AI_REQUEST_PRESENCE_PENALTY_KEY, + GEN_AI_REQUEST_SEED_KEY, + GEN_AI_REQUEST_STOP_SEQUENCES_KEY, + GEN_AI_REQUEST_TEMPERATURE_KEY, + GEN_AI_REQUEST_TOP_P_KEY, + GEN_AI_RESPONSE_FINISH_REASONS_KEY, + GEN_AI_SYSTEM_INSTRUCTIONS_KEY, + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS_KEY, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_KEY, + GEN_AI_USAGE_INPUT_TOKENS_KEY, + GEN_AI_USAGE_OUTPUT_TOKENS_KEY, INVOKE_AGENT_OPERATION_NAME, SERVER_ADDRESS_KEY, SERVER_PORT_KEY, @@ -30,6 +47,8 @@ USER_ID_KEY, USER_NAME_KEY, ) +from microsoft.opentelemetry.a365.core.gen_ai_request_parameters import GenAiRequestParameters +from microsoft.opentelemetry.a365.core.gen_ai_response_parameters import GenAiResponseParameters from microsoft.opentelemetry.a365.core.invoke_agent_details import InvokeAgentScopeDetails from microsoft.opentelemetry.a365.core.message_utils import ( normalize_input_messages, @@ -129,6 +148,11 @@ def __init__( if endpoint.port and endpoint.port != 443: self.set_tag_maybe(SERVER_PORT_KEY, endpoint.port) + if scope_details.request_parameters: + self._record_request_parameters(scope_details.request_parameters) + if scope_details.response_parameters: + self._record_response_parameters(scope_details.response_parameters) + # Set request metadata if request.channel: self.set_tag_maybe(CHANNEL_NAME_KEY, request.channel.name) @@ -182,6 +206,10 @@ def record_response(self, response: str) -> None: """ self.record_output_messages([response]) + def record_response_parameters(self, parameters: GenAiResponseParameters) -> None: + """Record GenAI response parameters that are only known after completion.""" + self._record_response_parameters(parameters) + def record_input_messages(self, messages: InputMessagesParam) -> None: """Record the input messages for telemetry tracking. @@ -205,3 +233,36 @@ def record_output_messages(self, messages: OutputMessagesParam) -> None: """ wrapper = normalize_output_messages(messages) self.set_tag_maybe(GEN_AI_OUTPUT_MESSAGES_KEY, serialize_messages(wrapper)) + + def _record_request_parameters(self, parameters: GenAiRequestParameters) -> None: + self.set_tag_maybe(GEN_AI_REQUEST_MODEL_KEY, parameters.model) + self.set_tag_maybe(GEN_AI_REQUEST_SEED_KEY, parameters.seed) + self.set_tag_maybe(GEN_AI_REQUEST_CHOICE_COUNT_KEY, parameters.choice_count) + self.set_tag_maybe(GEN_AI_REQUEST_FREQUENCY_PENALTY_KEY, parameters.frequency_penalty) + self.set_tag_maybe(GEN_AI_REQUEST_MAX_TOKENS_KEY, parameters.max_tokens) + self.set_tag_maybe(GEN_AI_REQUEST_PRESENCE_PENALTY_KEY, parameters.presence_penalty) + self.set_tag_maybe( + GEN_AI_REQUEST_STOP_SEQUENCES_KEY, + tuple(parameters.stop_sequences) if parameters.stop_sequences is not None else None, + ) + self.set_tag_maybe(GEN_AI_REQUEST_TEMPERATURE_KEY, parameters.temperature) + self.set_tag_maybe(GEN_AI_REQUEST_TOP_P_KEY, parameters.top_p) + self.set_tag_maybe(GEN_AI_DATA_SOURCE_ID_KEY, parameters.data_source_id) + self.set_tag_maybe(GEN_AI_OUTPUT_TYPE_KEY, parameters.output_type) + self.set_tag_maybe(GEN_AI_SYSTEM_INSTRUCTIONS_KEY, parameters.system_instructions) + + def _record_response_parameters(self, parameters: GenAiResponseParameters) -> None: + self.set_tag_maybe( + GEN_AI_RESPONSE_FINISH_REASONS_KEY, + tuple(parameters.finish_reasons) if parameters.finish_reasons is not None else None, + ) + self.set_tag_maybe(GEN_AI_USAGE_INPUT_TOKENS_KEY, parameters.input_tokens) + self.set_tag_maybe(GEN_AI_USAGE_OUTPUT_TOKENS_KEY, parameters.output_tokens) + self.set_tag_maybe( + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS_KEY, + parameters.cache_creation_input_tokens, + ) + self.set_tag_maybe( + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_KEY, + parameters.cache_read_input_tokens, + ) diff --git a/tests/a365/test_invoke_agent_scope.py b/tests/a365/test_invoke_agent_scope.py index ae40523f..efba01e6 100644 --- a/tests/a365/test_invoke_agent_scope.py +++ b/tests/a365/test_invoke_agent_scope.py @@ -2,10 +2,43 @@ # Licensed under the MIT License. from microsoft.opentelemetry.a365.core import ( + AgentDetails, GenAiRequestParameters, GenAiResponseParameters, + InvokeAgentScope, InvokeAgentScopeDetails, + Request, ) +from microsoft.opentelemetry.a365.core.constants import SOURCE_NAME +from microsoft.opentelemetry.a365.core.opentelemetry_scope import OpenTelemetryScope +from opentelemetry.sdk.trace import TracerProvider + + +def _start_invoke_agent_scope(scope_details: InvokeAgentScopeDetails): + return InvokeAgentScope.start( + request=Request(), + scope_details=scope_details, + agent_details=AgentDetails(agent_id="agent-1"), + ) + + +def setup_function(): + import os + + os.environ["ENABLE_OBSERVABILITY"] = "true" + provider = TracerProvider() + OpenTelemetryScope._tracer = provider.get_tracer(SOURCE_NAME) + setup_function.provider = provider # type: ignore[attr-defined] + + +def teardown_function(): + import os + + provider = getattr(setup_function, "provider", None) + if provider: + provider.shutdown() + OpenTelemetryScope._tracer = None + os.environ.pop("ENABLE_OBSERVABILITY", None) def test_invoke_agent_details_accepts_semantic_parameters(): @@ -17,3 +50,80 @@ def test_invoke_agent_details_accepts_semantic_parameters(): ) assert details.request_parameters is request assert details.response_parameters is response + + +def test_invoke_agent_scope_records_request_parameters(): + parameters = GenAiRequestParameters( + model="gpt-4o", + seed=42, + choice_count=2, + frequency_penalty=0.5, + max_tokens=128, + presence_penalty=0.25, + stop_sequences=["stop", "halt"], + temperature=0.7, + top_p=0.9, + data_source_id="data-source-1", + output_type="json", + system_instructions="Be concise.", + ) + scope = _start_invoke_agent_scope(InvokeAgentScopeDetails(request_parameters=parameters)) + try: + attrs = dict(scope._span.attributes) + assert attrs["gen_ai.request.model"] == "gpt-4o" + assert attrs["gen_ai.request.seed"] == 42 + assert attrs["gen_ai.request.choice.count"] == 2 + assert attrs["gen_ai.request.frequency_penalty"] == 0.5 + assert attrs["gen_ai.request.max_tokens"] == 128 + assert attrs["gen_ai.request.presence_penalty"] == 0.25 + assert attrs["gen_ai.request.stop_sequences"] == ("stop", "halt") + assert attrs["gen_ai.request.temperature"] == 0.7 + assert attrs["gen_ai.request.top_p"] == 0.9 + assert attrs["gen_ai.data_source.id"] == "data-source-1" + assert attrs["gen_ai.output.type"] == "json" + assert attrs["gen_ai.system_instructions"] == "Be concise." + finally: + scope.dispose() + + +def test_invoke_agent_scope_records_response_parameters_after_completion(): + scope = _start_invoke_agent_scope(InvokeAgentScopeDetails()) + try: + scope.record_response_parameters( + GenAiResponseParameters( + finish_reasons=["stop"], + input_tokens=10, + output_tokens=4, + cache_creation_input_tokens=2, + cache_read_input_tokens=1, + ) + ) + attrs = dict(scope._span.attributes) + assert attrs["gen_ai.response.finish_reasons"] == ("stop",) + assert attrs["gen_ai.usage.input_tokens"] == 10 + assert attrs["gen_ai.usage.output_tokens"] == 4 + assert attrs["gen_ai.usage.cache_creation.input_tokens"] == 2 + assert attrs["gen_ai.usage.cache_read.input_tokens"] == 1 + finally: + scope.dispose() + + +def test_invoke_agent_scope_omits_none_semantic_parameters(): + scope = _start_invoke_agent_scope( + InvokeAgentScopeDetails( + request_parameters=GenAiRequestParameters(model="gpt-4o"), + response_parameters=GenAiResponseParameters(input_tokens=10), + ) + ) + try: + attrs = dict(scope._span.attributes) + assert attrs["gen_ai.request.model"] == "gpt-4o" + assert attrs["gen_ai.usage.input_tokens"] == 10 + assert "gen_ai.request.seed" not in attrs + assert "gen_ai.request.stop_sequences" not in attrs + assert "gen_ai.response.finish_reasons" not in attrs + assert "gen_ai.usage.output_tokens" not in attrs + assert "gen_ai.usage.cache_creation.input_tokens" not in attrs + assert "gen_ai.usage.cache_read.input_tokens" not in attrs + finally: + scope.dispose() From 33868f864d2a54182e0a46c2352a9ae644004332 Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Fri, 11 Sep 2026 15:10:03 -0600 Subject: [PATCH 03/14] docs: describe InvokeAgent semantic parameters Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- A365_DOCUMENTATION.md | 37 ++++++++++++++++++++++++++++++++++--- CHANGELOG.md | 4 ++++ README.md | 22 ++++++++++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/A365_DOCUMENTATION.md b/A365_DOCUMENTATION.md index 17aa49ac..de519562 100644 --- a/A365_DOCUMENTATION.md +++ b/A365_DOCUMENTATION.md @@ -356,22 +356,53 @@ Top-level agent invocation — wraps the entire request/response cycle: ```python from microsoft.opentelemetry.a365.core import ( - AgentDetails, CallerDetails, Channel, InvokeAgentScope, - InvokeAgentScopeDetails, Request, ServiceEndpoint, UserDetails, + AgentDetails, CallerDetails, Channel, GenAiRequestParameters, + GenAiResponseParameters, InvokeAgentScope, InvokeAgentScopeDetails, + Request, ServiceEndpoint, UserDetails, ) agent = AgentDetails(agent_id="agent-001", agent_name="My Agent", tenant_id="t1") with InvokeAgentScope.start( request=Request(content="Hello", session_id="s1", conversation_id="c1", channel=Channel(name="msteams")), - scope_details=InvokeAgentScopeDetails(endpoint=ServiceEndpoint(hostname="agent.contoso.com")), + scope_details=InvokeAgentScopeDetails( + endpoint=ServiceEndpoint(hostname="agent.contoso.com"), + request_parameters=GenAiRequestParameters( + model="gpt-4o", + max_tokens=256, + temperature=0.2, + stop_sequences=["END"], + output_type="text", + ), + ), agent_details=agent, caller_details=CallerDetails(user_details=UserDetails(user_id="u1", user_email="u@contoso.com")), ) as scope: # ... do work ... + scope.record_response_parameters( + GenAiResponseParameters(finish_reasons=["stop"], input_tokens=42, output_tokens=18) + ) scope.record_response("Here is the answer.") ``` +`GenAiRequestParameters` values are recorded when the scope starts. Use +`scope.record_response_parameters()` for response values that are only known +after completion. Fields left as `None` are omitted from the span. Sequence +fields such as `stop_sequences` and `finish_reasons` are emitted as +OpenTelemetry string arrays. + +Supported semantic attributes: + +- Request: `gen_ai.request.model`, `gen_ai.request.seed`, + `gen_ai.request.choice.count`, `gen_ai.request.frequency_penalty`, + `gen_ai.request.max_tokens`, `gen_ai.request.presence_penalty`, + `gen_ai.request.stop_sequences`, `gen_ai.request.temperature`, + `gen_ai.request.top_p`, `gen_ai.data_source.id`, `gen_ai.output.type`, + `gen_ai.system_instructions` +- Response: `gen_ai.response.finish_reasons`, `gen_ai.usage.input_tokens`, + `gen_ai.usage.output_tokens`, `gen_ai.usage.cache_creation.input_tokens`, + `gen_ai.usage.cache_read.input_tokens` + ### ExecuteToolScope ```python diff --git a/CHANGELOG.md b/CHANGELOG.md index 9da53e9e..efcaed78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ # Release History +# Unreleased +### Features Added +- Add Python-native `InvokeAgentScope` request and response parameter models that emit GenAI semantic attributes introduced by .NET PR #120. + # 1.3.9 (2026-09-09) ### Features Added - Update OpenTelemetry dependencies to latest versions, bump `langchain-core` minimum version to address S360, and support the new `httpx2` entry point exposed by `opentelemetry-instrumentation-httpx`. diff --git a/README.md b/README.md index edae89b4..66b8e848 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,28 @@ See the [A365 guide](https://github.com/microsoft/opentelemetry-distro-python/bl > For A365 token resolver patterns, baggage, and scope classes, see the [A365 guide](https://github.com/microsoft/opentelemetry-distro-python/blob/main/A365_DOCUMENTATION.md). +### InvokeAgent semantic parameters + +Manual `InvokeAgentScope` instrumentation accepts Python-native request and response parameter models: + +```python +from microsoft.opentelemetry.a365.core import GenAiRequestParameters, GenAiResponseParameters, InvokeAgentScopeDetails + +details = InvokeAgentScopeDetails( + request_parameters=GenAiRequestParameters(model="gpt-4o", max_tokens=256, stop_sequences=["END"]) +) + +with InvokeAgentScope.start(request, details, agent_details) as scope: + response = call_agent(request) + scope.record_response_parameters(GenAiResponseParameters(finish_reasons=["stop"], output_tokens=18)) +``` + +Unset fields are omitted. Supported attributes include `gen_ai.request.*`, +`gen_ai.data_source.id`, `gen_ai.output.type`, `gen_ai.system_instructions`, +`gen_ai.response.finish_reasons`, and `gen_ai.usage.*` token counts. See the +[A365 guide](https://github.com/microsoft/opentelemetry-distro-python/blob/main/A365_DOCUMENTATION.md#invokeagentscope) +for the full list. + ### Sampling Configured via standard OpenTelemetry environment variables: From 5f9e40ab61ec3981536e21d731580d59be014b52 Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Fri, 11 Sep 2026 18:03:42 -0600 Subject: [PATCH 04/14] Docs: update A365 module table for invoke agent Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/microsoft/opentelemetry/a365/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/microsoft/opentelemetry/a365/README.md b/src/microsoft/opentelemetry/a365/README.md index 16cdae73..393a3e87 100644 --- a/src/microsoft/opentelemetry/a365/README.md +++ b/src/microsoft/opentelemetry/a365/README.md @@ -21,11 +21,13 @@ Core tracing primitives — scopes, configuration, data models, and internal uti | `agent_details.py` | `AgentDetails` dataclass — metadata about an AI agent (ID, name, description, blueprint/platform IDs, tenant, version). | | `channel.py` | `Channel` dataclass — channel context (name, link) for agent execution. | | `constants.py` | Core-level constants for span operations, OTel conventions, feature switches, and error types. | +| `gen_ai_request_parameters.py` | Gen-AI request parameter models for structured input message payloads. | +| `gen_ai_response_parameters.py` | Gen-AI response parameter models for structured output message payloads. | | `execute_tool_scope.py` | `ExecuteToolScope` — tracing scope for AI tool executions. Records tool name, arguments, call ID, type, and endpoint. | | `inference_call_details.py` | `InferenceCallDetails` dataclass — LLM call metadata (model, provider, token counts, finish reasons, endpoint). | | `inference_operation_type.py` | `InferenceOperationType` enum — Chat, TextCompletion, GenerateContent. | | `inference_scope.py` | `InferenceScope` — tracing scope for LLM/AI inference operations. Records input/output messages, model details, token usage, and user info. | -| `invoke_agent_details.py` | `InvokeAgentScopeDetails` dataclass — configuration for agent invocation tracing (endpoint). | +| `invoke_agent_details.py` | `InvokeAgentScopeDetails` dataclass — configuration for agent invocation tracing (endpoint, optional request parameters, optional response parameters). | | `invoke_agent_scope.py` | `InvokeAgentScope` — tracing scope for agent invocations. Records request/response, caller details (human and agent-to-agent), channel, and endpoint info. | | `message_utils.py` | Conversion and serialization helpers for OTel gen-ai message format. Normalizes strings/lists to structured `InputMessages`/`OutputMessages`. | | `opentelemetry_scope.py` | `OpenTelemetryScope` — base class for all tracing scopes. Manages span creation, attribute setting, context management, and baggage building. | From 133fe1cf4a39521262605129e4bffebf18fa1223 Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Tue, 15 Sep 2026 12:46:34 -0600 Subject: [PATCH 05/14] docs: address InvokeAgent review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 13 +++++++++++-- src/microsoft/opentelemetry/a365/README.md | 4 ++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 66b8e848..d1058f42 100644 --- a/README.md +++ b/README.md @@ -127,14 +127,23 @@ See the [A365 guide](https://github.com/microsoft/opentelemetry-distro-python/bl Manual `InvokeAgentScope` instrumentation accepts Python-native request and response parameter models: ```python -from microsoft.opentelemetry.a365.core import GenAiRequestParameters, GenAiResponseParameters, InvokeAgentScopeDetails +from microsoft.opentelemetry.a365.core import ( + AgentDetails, + GenAiRequestParameters, + GenAiResponseParameters, + InvokeAgentScope, + InvokeAgentScopeDetails, + Request, +) +request = Request(content="Hello") +agent_details = AgentDetails(agent_id="agent-001") details = InvokeAgentScopeDetails( request_parameters=GenAiRequestParameters(model="gpt-4o", max_tokens=256, stop_sequences=["END"]) ) with InvokeAgentScope.start(request, details, agent_details) as scope: - response = call_agent(request) + # Invoke your agent here. scope.record_response_parameters(GenAiResponseParameters(finish_reasons=["stop"], output_tokens=18)) ``` diff --git a/src/microsoft/opentelemetry/a365/README.md b/src/microsoft/opentelemetry/a365/README.md index 393a3e87..dce127ba 100644 --- a/src/microsoft/opentelemetry/a365/README.md +++ b/src/microsoft/opentelemetry/a365/README.md @@ -21,8 +21,8 @@ Core tracing primitives — scopes, configuration, data models, and internal uti | `agent_details.py` | `AgentDetails` dataclass — metadata about an AI agent (ID, name, description, blueprint/platform IDs, tenant, version). | | `channel.py` | `Channel` dataclass — channel context (name, link) for agent execution. | | `constants.py` | Core-level constants for span operations, OTel conventions, feature switches, and error types. | -| `gen_ai_request_parameters.py` | Gen-AI request parameter models for structured input message payloads. | -| `gen_ai_response_parameters.py` | Gen-AI response parameter models for structured output message payloads. | +| `gen_ai_request_parameters.py` | GenAI request parameter models for generation settings and related semantic attributes. | +| `gen_ai_response_parameters.py` | GenAI response parameter models for finish reasons and token usage attributes. | | `execute_tool_scope.py` | `ExecuteToolScope` — tracing scope for AI tool executions. Records tool name, arguments, call ID, type, and endpoint. | | `inference_call_details.py` | `InferenceCallDetails` dataclass — LLM call metadata (model, provider, token counts, finish reasons, endpoint). | | `inference_operation_type.py` | `InferenceOperationType` enum — Chat, TextCompletion, GenerateContent. | From 3e49a38b2996fefd3225a1406242c3698d961a6a Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Wed, 16 Sep 2026 12:48:04 -0600 Subject: [PATCH 06/14] docs: design InvokeAgent semantic alignment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...6-invoke-agent-semconv-alignment-design.md | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-16-invoke-agent-semconv-alignment-design.md diff --git a/docs/superpowers/specs/2026-09-16-invoke-agent-semconv-alignment-design.md b/docs/superpowers/specs/2026-09-16-invoke-agent-semconv-alignment-design.md new file mode 100644 index 00000000..8093a094 --- /dev/null +++ b/docs/superpowers/specs/2026-09-16-invoke-agent-semconv-alignment-design.md @@ -0,0 +1,91 @@ +# InvokeAgent Semantic Convention Alignment + +## Goal + +Align the new `InvokeAgentScope` request and response parameter API with the +current OpenTelemetry GenAI semantic conventions before the API is released. + +## Cache-write token naming + +The current OpenTelemetry GenAI registry defines +`gen_ai.usage.cache_write.input_tokens` as the number of input tokens written +to a provider-managed cache. The PR's `cache_creation` name is provider +terminology and must not be exposed as the OpenTelemetry attribute name. + +Rename the new response model field, constants, emitted attribute, tests, and +documentation from `cache_creation` to `cache_write`: + +- `cache_creation_input_tokens` becomes `cache_write_input_tokens`. +- `GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS_KEY` becomes + `GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_KEY`. +- `gen_ai.usage.cache_creation.input_tokens` becomes + `gen_ai.usage.cache_write.input_tokens`. + +This change applies only to the new `InvokeAgentScope` API introduced by the +PR. Existing provider adapters may continue reading provider-specific fields +named `cache_creation_input_tokens`, but their exported semantic attribute +names are outside this PR's scope. + +## Structured system instructions + +OpenTelemetry defines `gen_ai.system_instructions` as an array of structured +parts. The public request model will therefore use: + +```python +system_instructions: Sequence[TextPart] | None = None +``` + +Callers pass a list or tuple of the existing `TextPart` objects: + +```python +system_instructions=[ + TextPart(content="Be concise."), + TextPart(content="Return JSON."), +] +``` + +`TextPart` automatically supplies the `"type": "text"` discriminator. The span +attribute will contain a JSON-encoded representation of the OpenTelemetry +array because the OpenTelemetry Python span API cannot directly store +structured objects: + +```json +[{"content":"Be concise.","type":"text"},{"content":"Return JSON.","type":"text"}] +``` + +Plain strings will no longer be accepted by this new, unreleased field. This +keeps the public API aligned with the semantic convention from its first +release rather than establishing a non-compliant compatibility contract. + +## Serialization + +Add a focused serializer for system-instruction parts in the existing message +utility module. It will reuse the same dataclass conversion, enum handling, +`None` omission, JSON encoding, logging, and non-throwing telemetry behavior as +the existing message serializers. + +The request-recording path will omit the attribute when instructions are +`None`. Otherwise, it will serialize the sequence and set +`gen_ai.system_instructions` to the resulting JSON string. + +## Tests and documentation + +Tests will verify: + +- Cache-write tokens use the new model field and standard emitted key. +- The obsolete cache-creation key is not emitted. +- One and multiple `TextPart` instructions serialize to the expected JSON + array with automatic `type: "text"` fields. +- Unset instructions remain omitted. +- Serialization failure follows the repository's non-throwing telemetry + behavior. + +README and A365 guide examples will use `TextPart` objects and the cache-write +terminology. The two GitHub review threads will receive concise replies +referencing the implementation commit and will be resolved after validation. + +## Validation + +Run the targeted A365 InvokeAgent tests, formatting and static checks covering +the changed modules, and `git diff --check`. Push the implementation to the PR +branch only after those checks pass. From 1b1bae9e6222dca9b5b096bdc5aee1fd31ffb500 Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Wed, 16 Sep 2026 12:50:26 -0600 Subject: [PATCH 07/14] docs: plan InvokeAgent semantic alignment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...26-09-16-invoke-agent-semconv-alignment.md | 461 ++++++++++++++++++ 1 file changed, 461 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-16-invoke-agent-semconv-alignment.md diff --git a/docs/superpowers/plans/2026-09-16-invoke-agent-semconv-alignment.md b/docs/superpowers/plans/2026-09-16-invoke-agent-semconv-alignment.md new file mode 100644 index 00000000..d06bf014 --- /dev/null +++ b/docs/superpowers/plans/2026-09-16-invoke-agent-semconv-alignment.md @@ -0,0 +1,461 @@ +# InvokeAgent Semantic Convention Alignment Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make the new `InvokeAgentScope` cache-token and system-instruction telemetry conform to the current OpenTelemetry GenAI semantic conventions. + +**Architecture:** Rename the unreleased cache-creation API and attribute to the canonical cache-write terminology. Represent system instructions as a sequence of existing `TextPart` objects and serialize that sequence through a focused helper that shares the current message serialization primitives. + +**Tech Stack:** Python 3.10+, dataclasses, OpenTelemetry Python SDK, pytest, Black, Pylint, Mypy, tox. + +## Global Constraints + +- Emit cache-write tokens as `gen_ai.usage.cache_write.input_tokens`. +- Expose the response field as `cache_write_input_tokens`; do not retain the unreleased `cache_creation_input_tokens` alias. +- Accept structured system instructions only as `Sequence[TextPart] | None`. +- Emit `gen_ai.system_instructions` as a JSON-encoded array of objects with automatic `"type": "text"` discriminators. +- Preserve non-throwing telemetry serialization behavior. +- Do not modify provider-specific extraction fields such as Anthropic's `cache_creation_input_tokens` outside the new `InvokeAgentScope` API. + +--- + +### Task 1: Rename InvokeAgent cache creation to cache write + +**Files:** +- Modify: `tests/a365/test_invoke_agent_scope.py:87-128` +- Modify: `src/microsoft/opentelemetry/a365/core/gen_ai_response_parameters.py:9-18` +- Modify: `src/microsoft/opentelemetry/a365/core/constants.py:56-62` +- Modify: `src/microsoft/opentelemetry/a365/constants.py:59-65` +- Modify: `src/microsoft/opentelemetry/a365/core/invoke_agent_scope.py:35-41,254-268` + +**Interfaces:** +- Consumes: `GenAiResponseParameters` passed to `InvokeAgentScope.record_response_parameters`. +- Produces: `cache_write_input_tokens: int | None` and `GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_KEY = "gen_ai.usage.cache_write.input_tokens"`. + +- [ ] **Step 1: Update the focused tests to require cache-write naming** + +Replace the cache assertions and constructor argument in +`tests/a365/test_invoke_agent_scope.py`: + +```python +scope.record_response_parameters( + GenAiResponseParameters( + finish_reasons=["stop"], + input_tokens=10, + output_tokens=4, + cache_write_input_tokens=2, + cache_read_input_tokens=1, + ) +) +attrs = dict(scope._span.attributes) +assert attrs["gen_ai.response.finish_reasons"] == ("stop",) +assert attrs["gen_ai.usage.input_tokens"] == 10 +assert attrs["gen_ai.usage.output_tokens"] == 4 +assert attrs["gen_ai.usage.cache_write.input_tokens"] == 2 +assert "gen_ai.usage.cache_creation.input_tokens" not in attrs +assert attrs["gen_ai.usage.cache_read.input_tokens"] == 1 +``` + +Update the omission assertions: + +```python +assert "gen_ai.usage.cache_write.input_tokens" not in attrs +assert "gen_ai.usage.cache_creation.input_tokens" not in attrs +``` + +- [ ] **Step 2: Run the cache tests and verify they fail** + +Run: + +```powershell +python -m pytest tests\a365\test_invoke_agent_scope.py -q +``` + +Expected: failure because `GenAiResponseParameters` does not accept +`cache_write_input_tokens`. + +- [ ] **Step 3: Rename the response model field and constants** + +In `src/microsoft/opentelemetry/a365/core/gen_ai_response_parameters.py`: + +```python +@dataclass +class GenAiResponseParameters: + """Optional GenAI response parameters recorded on InvokeAgent spans.""" + + finish_reasons: Sequence[str] | None = None + input_tokens: int | None = None + output_tokens: int | None = None + cache_write_input_tokens: int | None = None + cache_read_input_tokens: int | None = None +``` + +In both constants modules, replace the cache-creation declaration with: + +```python +GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_KEY = "gen_ai.usage.cache_write.input_tokens" +``` + +In `src/microsoft/opentelemetry/a365/core/invoke_agent_scope.py`, import the +new constant and emit the renamed field: + +```python +self.set_tag_maybe( + GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_KEY, + parameters.cache_write_input_tokens, +) +``` + +- [ ] **Step 4: Run the focused tests and verify they pass** + +Run: + +```powershell +python -m pytest tests\a365\test_invoke_agent_scope.py -q +``` + +Expected: all tests in the file pass. + +- [ ] **Step 5: Confirm no new InvokeAgent cache-creation symbols remain** + +Run: + +```powershell +rg "GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS_KEY|cache_creation_input_tokens|gen_ai\.usage\.cache_creation\.input_tokens" src\microsoft\opentelemetry\a365 tests\a365\test_invoke_agent_scope.py +``` + +Expected: no matches. Provider-specific matches elsewhere in `_genai` are +intentionally outside scope. + +- [ ] **Step 6: Commit the cache rename** + +```powershell +git add src\microsoft\opentelemetry\a365\constants.py src\microsoft\opentelemetry\a365\core\constants.py src\microsoft\opentelemetry\a365\core\gen_ai_response_parameters.py src\microsoft\opentelemetry\a365\core\invoke_agent_scope.py tests\a365\test_invoke_agent_scope.py +git commit -m "fix: use OTel cache-write token attribute" -m "Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>" +``` + +### Task 2: Serialize structured system instructions + +**Files:** +- Modify: `tests/a365/test_invoke_agent_scope.py:5-85,109-128` +- Modify: `src/microsoft/opentelemetry/a365/core/gen_ai_request_parameters.py:5-25` +- Modify: `src/microsoft/opentelemetry/a365/core/message_utils.py:10-119` +- Modify: `src/microsoft/opentelemetry/a365/core/invoke_agent_scope.py:45-55,238-253` + +**Interfaces:** +- Consumes: `TextPart` from `microsoft.opentelemetry.a365.core.models.messages`. +- Produces: `GenAiRequestParameters.system_instructions: Sequence[TextPart] | None`. +- Produces: `serialize_system_instructions(parts: Sequence[TextPart]) -> str`. + +- [ ] **Step 1: Add tests for the required OTel JSON shape** + +Add `json` and `TextPart` imports in `tests/a365/test_invoke_agent_scope.py`: + +```python +import json + +from microsoft.opentelemetry.a365.core import ( + AgentDetails, + GenAiRequestParameters, + GenAiResponseParameters, + InvokeAgentScope, + InvokeAgentScopeDetails, + Request, + TextPart, +) +``` + +Change the request parameters in +`test_invoke_agent_scope_records_request_parameters`: + +```python +system_instructions=[ + TextPart(content="Be concise."), + TextPart(content="Return JSON."), +], +``` + +Replace the system-instruction assertion with: + +```python +assert json.loads(attrs["gen_ai.system_instructions"]) == [ + {"content": "Be concise.", "type": "text"}, + {"content": "Return JSON.", "type": "text"}, +] +``` + +Add an omission assertion to +`test_invoke_agent_scope_omits_none_semantic_parameters`: + +```python +assert "gen_ai.system_instructions" not in attrs +``` + +Add a serializer failure test: + +```python +def test_invoke_agent_scope_system_instructions_serialization_is_non_throwing(): + parameters = GenAiRequestParameters( + system_instructions=[object()], # type: ignore[list-item] + ) + + scope = _start_invoke_agent_scope(InvokeAgentScopeDetails(request_parameters=parameters)) + try: + attrs = dict(scope._span.attributes) + assert json.loads(attrs["gen_ai.system_instructions"]) == [ + { + "content": "[serialization failed: 1 instruction part]", + "type": "text", + } + ] + finally: + scope.dispose() +``` + +- [ ] **Step 2: Run the structured-instruction tests and verify they fail** + +Run: + +```powershell +python -m pytest tests\a365\test_invoke_agent_scope.py -q +``` + +Expected: the JSON-shape assertion fails because the span currently contains a +raw sequence unsupported by OpenTelemetry attributes. + +- [ ] **Step 3: Type the field as structured instruction parts** + +In `src/microsoft/opentelemetry/a365/core/gen_ai_request_parameters.py`: + +```python +from typing import Sequence + +from microsoft.opentelemetry.a365.core.models.messages import TextPart + + +@dataclass +class GenAiRequestParameters: + """Optional GenAI request parameters recorded on InvokeAgent spans.""" + + # Existing fields remain unchanged. + system_instructions: Sequence[TextPart] | None = None +``` + +- [ ] **Step 4: Extract shared dataclass JSON serialization** + +In `src/microsoft/opentelemetry/a365/core/message_utils.py`, import `Sequence`, +then add: + +```python +def _serialize_dataclasses(items: Sequence[object]) -> str: + """Serialize dataclass instances as a JSON array.""" + return json.dumps( + [asdict(item, dict_factory=_message_dict_factory) for item in items], + default=str, + ensure_ascii=False, + ) +``` + +Use it in `serialize_messages`: + +```python +try: + return _serialize_dataclasses(wrapper.messages) +except Exception: + logger.warning("Failed to serialize messages; using fallback.", exc_info=True) + # Preserve the existing fallback construction. +``` + +Add the structured-instruction serializer: + +```python +def serialize_system_instructions(parts: Sequence[TextPart]) -> str: + """Serialize OTel system-instruction parts as a JSON array.""" + try: + return _serialize_dataclasses(parts) + except Exception: + logger.warning("Failed to serialize system instructions; using fallback.", exc_info=True) + count = len(parts) + noun = "instruction part" if count == 1 else "instruction parts" + return _serialize_dataclasses( + [TextPart(content=f"[serialization failed: {count} {noun}]")] + ) +``` + +- [ ] **Step 5: Emit serialized instructions from InvokeAgentScope** + +Import `serialize_system_instructions` in +`src/microsoft/opentelemetry/a365/core/invoke_agent_scope.py`, then replace the +raw assignment: + +```python +if parameters.system_instructions is not None: + self.set_tag_maybe( + GEN_AI_SYSTEM_INSTRUCTIONS_KEY, + serialize_system_instructions(parameters.system_instructions), + ) +``` + +- [ ] **Step 6: Run focused tests and verify they pass** + +Run: + +```powershell +python -m pytest tests\a365\test_invoke_agent_scope.py -q +``` + +Expected: all tests pass, including structured JSON and non-throwing fallback. + +- [ ] **Step 7: Run static checks for the changed Python surface** + +Run: + +```powershell +python -m black --check src\microsoft\opentelemetry\a365\core\gen_ai_request_parameters.py src\microsoft\opentelemetry\a365\core\gen_ai_response_parameters.py src\microsoft\opentelemetry\a365\core\message_utils.py src\microsoft\opentelemetry\a365\core\invoke_agent_scope.py tests\a365\test_invoke_agent_scope.py +python -m pylint src\microsoft\opentelemetry\a365\core\gen_ai_request_parameters.py src\microsoft\opentelemetry\a365\core\gen_ai_response_parameters.py src\microsoft\opentelemetry\a365\core\message_utils.py src\microsoft\opentelemetry\a365\core\invoke_agent_scope.py tests\a365\test_invoke_agent_scope.py +python -m mypy --python-version 3.10 --show-error-codes --ignore-missing-imports src\microsoft\opentelemetry\a365\core tests\a365\test_invoke_agent_scope.py +``` + +Expected: each command exits successfully with no errors. + +- [ ] **Step 8: Commit structured instruction support** + +```powershell +git add src\microsoft\opentelemetry\a365\core\gen_ai_request_parameters.py src\microsoft\opentelemetry\a365\core\message_utils.py src\microsoft\opentelemetry\a365\core\invoke_agent_scope.py tests\a365\test_invoke_agent_scope.py +git commit -m "fix: serialize structured system instructions" -m "Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>" +``` + +### Task 3: Update documentation and close review threads + +**Files:** +- Modify: `README.md:127-153` +- Modify: `A365_DOCUMENTATION.md:353-404` +- Modify: `CHANGELOG.md:1-5` + +**Interfaces:** +- Consumes: `TextPart`, `cache_write_input_tokens`, and the canonical emitted attribute names from Tasks 1 and 2. +- Produces: Copyable documentation matching the final public API. + +- [ ] **Step 1: Update the top-level README example** + +Add `TextPart` to the import list and demonstrate structured instructions: + +```python +from microsoft.opentelemetry.a365.core import ( + AgentDetails, + GenAiRequestParameters, + GenAiResponseParameters, + InvokeAgentScope, + InvokeAgentScopeDetails, + Request, + TextPart, +) + +details = InvokeAgentScopeDetails( + request_parameters=GenAiRequestParameters( + model="gpt-4o", + max_tokens=256, + stop_sequences=["END"], + system_instructions=[TextPart(content="Be concise.")], + ) +) +``` + +Include cache-write usage in the response call: + +```python +scope.record_response_parameters( + GenAiResponseParameters( + finish_reasons=["stop"], + output_tokens=18, + cache_write_input_tokens=4, + ) +) +``` + +- [ ] **Step 2: Update the A365 guide** + +Add `TextPart` to the `InvokeAgentScope` import list, add: + +```python +system_instructions=[TextPart(content="Be concise.")], +``` + +to `GenAiRequestParameters`, and add: + +```python +cache_write_input_tokens=4, +``` + +to the response example. Replace +`gen_ai.usage.cache_creation.input_tokens` with +`gen_ai.usage.cache_write.input_tokens` in the supported attribute list. + +- [ ] **Step 3: Clarify the unreleased changelog entry** + +Replace the current feature line with: + +```markdown +- Add Python-native `InvokeAgentScope` request and response parameter models + that emit OpenTelemetry GenAI semantic attributes, including structured + system instructions and cache read/write token counts, introduced by .NET + PR #120. +``` + +- [ ] **Step 4: Validate documentation and the complete focused change** + +Run: + +```powershell +python -m pytest tests\a365\test_invoke_agent_scope.py -q +git diff --check +rg "gen_ai\.usage\.cache_creation\.input_tokens|cache_creation_input_tokens" README.md A365_DOCUMENTATION.md CHANGELOG.md src\microsoft\opentelemetry\a365 tests\a365\test_invoke_agent_scope.py +``` + +Expected: tests pass, `git diff --check` exits successfully, and `rg` returns no +matches in the scoped InvokeAgent surfaces. + +- [ ] **Step 5: Commit documentation alignment** + +```powershell +git add README.md A365_DOCUMENTATION.md CHANGELOG.md +git commit -m "docs: align InvokeAgent semantic parameters" -m "Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>" +``` + +- [ ] **Step 6: Run repository-standard final validation** + +Run: + +```powershell +tox -e black +tox -e pylint +tox -e mypy +tox -e pytest-py310 -- tests\a365\test_invoke_agent_scope.py +tox -e docs +git diff --check origin/main...HEAD +``` + +Expected: every tox environment exits successfully and the diff check reports +no whitespace errors. + +- [ ] **Step 7: Push the branch** + +```powershell +git push https://github.com/nikhilNava/opentelemetry-distro-python.git HEAD:copilot/invoke-agent-semantic-fields +``` + +- [ ] **Step 8: Reply to and resolve both GitHub review threads** + +Use the REST replies endpoint: + +```powershell +$commit = git rev-parse --short HEAD +$cacheReply = "Fixed in $commit. Renamed the new model field, constants, emitted attribute, tests, and docs to the current OTel gen_ai.usage.cache_write.input_tokens convention." +$instructionsReply = "Fixed in $commit. system_instructions now accepts structured TextPart sequences and emits the OTel JSON-array shape through the shared serialization path." +gh api --method POST repos/microsoft/opentelemetry-distro-python/pulls/263/comments/4019541268/replies -f body=$cacheReply +gh api --method POST repos/microsoft/opentelemetry-distro-python/pulls/263/comments/4019544444/replies -f body=$instructionsReply +``` + +Query the PR review threads with GraphQL, resolve the two matching thread IDs +using `resolveReviewThread`, then query again and confirm both report +`isResolved: true`. From 126f7f990999ebdaa1bc3a6a3ef1bb8e26bc994e Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Wed, 16 Sep 2026 12:52:40 -0600 Subject: [PATCH 08/14] chore: ignore local worktrees Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ed511ea9..4789fac3 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ build/ .env .pytest_cache/ .mypy_cache/ +.worktrees/ docs/_build/ docs/api/ docs/superpowers/ From e4efba7ec0571c111b347e49da4687807ca8f437 Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Wed, 16 Sep 2026 13:00:50 -0600 Subject: [PATCH 09/14] fix: use OTel cache-write token attribute Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/microsoft/opentelemetry/a365/constants.py | 2 +- src/microsoft/opentelemetry/a365/core/constants.py | 2 +- .../opentelemetry/a365/core/gen_ai_response_parameters.py | 2 +- src/microsoft/opentelemetry/a365/core/invoke_agent_scope.py | 6 +++--- tests/a365/test_invoke_agent_scope.py | 5 +++-- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/microsoft/opentelemetry/a365/constants.py b/src/microsoft/opentelemetry/a365/constants.py index 763f0d05..90ae6351 100644 --- a/src/microsoft/opentelemetry/a365/constants.py +++ b/src/microsoft/opentelemetry/a365/constants.py @@ -59,7 +59,7 @@ GEN_AI_CONVERSATION_ID_KEY = "gen_ai.conversation.id" GEN_AI_CONVERSATION_ITEM_LINK_KEY = "microsoft.conversation.item.link" GEN_AI_TOKEN_TYPE_KEY = "gen_ai.token.type" -GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS_KEY = "gen_ai.usage.cache_creation.input_tokens" +GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_KEY = "gen_ai.usage.cache_write.input_tokens" GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_KEY = "gen_ai.usage.cache_read.input_tokens" GEN_AI_USAGE_INPUT_TOKENS_KEY = "gen_ai.usage.input_tokens" GEN_AI_USAGE_OUTPUT_TOKENS_KEY = "gen_ai.usage.output_tokens" diff --git a/src/microsoft/opentelemetry/a365/core/constants.py b/src/microsoft/opentelemetry/a365/core/constants.py index eae338bc..ca24cb87 100644 --- a/src/microsoft/opentelemetry/a365/core/constants.py +++ b/src/microsoft/opentelemetry/a365/core/constants.py @@ -56,7 +56,7 @@ GEN_AI_CONVERSATION_ID_KEY = "gen_ai.conversation.id" GEN_AI_CONVERSATION_ITEM_LINK_KEY = "microsoft.conversation.item.link" GEN_AI_TOKEN_TYPE_KEY = "gen_ai.token.type" -GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS_KEY = "gen_ai.usage.cache_creation.input_tokens" +GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_KEY = "gen_ai.usage.cache_write.input_tokens" GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_KEY = "gen_ai.usage.cache_read.input_tokens" GEN_AI_USAGE_INPUT_TOKENS_KEY = "gen_ai.usage.input_tokens" GEN_AI_USAGE_OUTPUT_TOKENS_KEY = "gen_ai.usage.output_tokens" diff --git a/src/microsoft/opentelemetry/a365/core/gen_ai_response_parameters.py b/src/microsoft/opentelemetry/a365/core/gen_ai_response_parameters.py index 3b975646..f7747721 100644 --- a/src/microsoft/opentelemetry/a365/core/gen_ai_response_parameters.py +++ b/src/microsoft/opentelemetry/a365/core/gen_ai_response_parameters.py @@ -14,5 +14,5 @@ class GenAiResponseParameters: finish_reasons: Sequence[str] | None = None input_tokens: int | None = None output_tokens: int | None = None - cache_creation_input_tokens: int | None = None + cache_write_input_tokens: int | None = None cache_read_input_tokens: int | None = None diff --git a/src/microsoft/opentelemetry/a365/core/invoke_agent_scope.py b/src/microsoft/opentelemetry/a365/core/invoke_agent_scope.py index 3daca012..7d752247 100644 --- a/src/microsoft/opentelemetry/a365/core/invoke_agent_scope.py +++ b/src/microsoft/opentelemetry/a365/core/invoke_agent_scope.py @@ -35,8 +35,8 @@ GEN_AI_REQUEST_TOP_P_KEY, GEN_AI_RESPONSE_FINISH_REASONS_KEY, GEN_AI_SYSTEM_INSTRUCTIONS_KEY, - GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS_KEY, GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_KEY, + GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_KEY, GEN_AI_USAGE_INPUT_TOKENS_KEY, GEN_AI_USAGE_OUTPUT_TOKENS_KEY, INVOKE_AGENT_OPERATION_NAME, @@ -259,8 +259,8 @@ def _record_response_parameters(self, parameters: GenAiResponseParameters) -> No self.set_tag_maybe(GEN_AI_USAGE_INPUT_TOKENS_KEY, parameters.input_tokens) self.set_tag_maybe(GEN_AI_USAGE_OUTPUT_TOKENS_KEY, parameters.output_tokens) self.set_tag_maybe( - GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS_KEY, - parameters.cache_creation_input_tokens, + GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_KEY, + parameters.cache_write_input_tokens, ) self.set_tag_maybe( GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_KEY, diff --git a/tests/a365/test_invoke_agent_scope.py b/tests/a365/test_invoke_agent_scope.py index efba01e6..c465fe66 100644 --- a/tests/a365/test_invoke_agent_scope.py +++ b/tests/a365/test_invoke_agent_scope.py @@ -94,7 +94,7 @@ def test_invoke_agent_scope_records_response_parameters_after_completion(): finish_reasons=["stop"], input_tokens=10, output_tokens=4, - cache_creation_input_tokens=2, + cache_write_input_tokens=2, cache_read_input_tokens=1, ) ) @@ -102,7 +102,7 @@ def test_invoke_agent_scope_records_response_parameters_after_completion(): assert attrs["gen_ai.response.finish_reasons"] == ("stop",) assert attrs["gen_ai.usage.input_tokens"] == 10 assert attrs["gen_ai.usage.output_tokens"] == 4 - assert attrs["gen_ai.usage.cache_creation.input_tokens"] == 2 + assert attrs["gen_ai.usage.cache_write.input_tokens"] == 2 assert attrs["gen_ai.usage.cache_read.input_tokens"] == 1 finally: scope.dispose() @@ -123,6 +123,7 @@ def test_invoke_agent_scope_omits_none_semantic_parameters(): assert "gen_ai.request.stop_sequences" not in attrs assert "gen_ai.response.finish_reasons" not in attrs assert "gen_ai.usage.output_tokens" not in attrs + assert "gen_ai.usage.cache_write.input_tokens" not in attrs assert "gen_ai.usage.cache_creation.input_tokens" not in attrs assert "gen_ai.usage.cache_read.input_tokens" not in attrs finally: From f2040e455cc174465fad87d8d93046a62f45f591 Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Wed, 16 Sep 2026 13:04:03 -0600 Subject: [PATCH 10/14] test: guard cache-write attribute rename Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/a365/test_invoke_agent_scope.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/a365/test_invoke_agent_scope.py b/tests/a365/test_invoke_agent_scope.py index c465fe66..577eefbc 100644 --- a/tests/a365/test_invoke_agent_scope.py +++ b/tests/a365/test_invoke_agent_scope.py @@ -103,6 +103,7 @@ def test_invoke_agent_scope_records_response_parameters_after_completion(): assert attrs["gen_ai.usage.input_tokens"] == 10 assert attrs["gen_ai.usage.output_tokens"] == 4 assert attrs["gen_ai.usage.cache_write.input_tokens"] == 2 + assert "gen_ai.usage.cache_creation.input_tokens" not in attrs assert attrs["gen_ai.usage.cache_read.input_tokens"] == 1 finally: scope.dispose() From 0754af0104dd89d6b375d65257a56ca5b468f123 Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Wed, 16 Sep 2026 13:09:37 -0600 Subject: [PATCH 11/14] fix: serialize structured system instructions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../a365/core/gen_ai_request_parameters.py | 4 +- .../a365/core/invoke_agent_scope.py | 7 +- .../opentelemetry/a365/core/message_utils.py | 29 +++++++-- tests/a365/test_invoke_agent_scope.py | 65 +++++++++++++++---- 4 files changed, 84 insertions(+), 21 deletions(-) diff --git a/src/microsoft/opentelemetry/a365/core/gen_ai_request_parameters.py b/src/microsoft/opentelemetry/a365/core/gen_ai_request_parameters.py index 98fd70c5..126cce9d 100644 --- a/src/microsoft/opentelemetry/a365/core/gen_ai_request_parameters.py +++ b/src/microsoft/opentelemetry/a365/core/gen_ai_request_parameters.py @@ -6,6 +6,8 @@ from dataclasses import dataclass from typing import Sequence +from microsoft.opentelemetry.a365.core.models.messages import TextPart + @dataclass class GenAiRequestParameters: @@ -22,4 +24,4 @@ class GenAiRequestParameters: top_p: float | None = None data_source_id: str | None = None output_type: str | None = None - system_instructions: str | None = None + system_instructions: Sequence[TextPart] | None = None diff --git a/src/microsoft/opentelemetry/a365/core/invoke_agent_scope.py b/src/microsoft/opentelemetry/a365/core/invoke_agent_scope.py index 7d752247..9ca2dea6 100644 --- a/src/microsoft/opentelemetry/a365/core/invoke_agent_scope.py +++ b/src/microsoft/opentelemetry/a365/core/invoke_agent_scope.py @@ -54,6 +54,7 @@ normalize_input_messages, normalize_output_messages, serialize_messages, + serialize_system_instructions, ) from microsoft.opentelemetry.a365.core.models.caller_details import CallerDetails from microsoft.opentelemetry.a365.core.models.messages import InputMessagesParam, OutputMessagesParam @@ -249,7 +250,11 @@ def _record_request_parameters(self, parameters: GenAiRequestParameters) -> None self.set_tag_maybe(GEN_AI_REQUEST_TOP_P_KEY, parameters.top_p) self.set_tag_maybe(GEN_AI_DATA_SOURCE_ID_KEY, parameters.data_source_id) self.set_tag_maybe(GEN_AI_OUTPUT_TYPE_KEY, parameters.output_type) - self.set_tag_maybe(GEN_AI_SYSTEM_INSTRUCTIONS_KEY, parameters.system_instructions) + if parameters.system_instructions is not None: + self.set_tag_maybe( + GEN_AI_SYSTEM_INSTRUCTIONS_KEY, + serialize_system_instructions(parameters.system_instructions), + ) def _record_response_parameters(self, parameters: GenAiResponseParameters) -> None: self.set_tag_maybe( diff --git a/src/microsoft/opentelemetry/a365/core/message_utils.py b/src/microsoft/opentelemetry/a365/core/message_utils.py index 21f86565..71064161 100644 --- a/src/microsoft/opentelemetry/a365/core/message_utils.py +++ b/src/microsoft/opentelemetry/a365/core/message_utils.py @@ -13,7 +13,7 @@ import logging from dataclasses import asdict from enum import Enum -from typing import Union +from typing import Any, Sequence, Union from microsoft.opentelemetry.a365.core.models.messages import ( ChatMessage, @@ -105,6 +105,15 @@ def _message_dict_factory(items: list[tuple[str, object]]) -> dict[str, object]: return {k: (v.value if isinstance(v, Enum) else v) for k, v in items if v is not None} +def _serialize_dataclasses(items: Sequence[Any]) -> str: + """Serialize dataclass instances as a JSON array.""" + return json.dumps( + [asdict(item, dict_factory=_message_dict_factory) for item in items], + default=str, + ensure_ascii=False, + ) + + def serialize_messages( wrapper: Union[InputMessages, OutputMessages], ) -> str: @@ -117,12 +126,7 @@ def serialize_messages( message parts contain non-JSON-serializable values. """ try: - serialized_list = [asdict(msg, dict_factory=_message_dict_factory) for msg in wrapper.messages] - return json.dumps( - serialized_list, - default=str, - ensure_ascii=False, - ) + return _serialize_dataclasses(wrapper.messages) except Exception: logger.warning("Failed to serialize messages; using fallback.", exc_info=True) messages = getattr(wrapper, "messages", []) @@ -140,3 +144,14 @@ def serialize_messages( if isinstance(wrapper, OutputMessages): fallback_msg["finish_reason"] = "error" return json.dumps([fallback_msg], ensure_ascii=False) + + +def serialize_system_instructions(parts: Sequence[TextPart]) -> str: + """Serialize OTel system-instruction parts as a JSON array.""" + try: + return _serialize_dataclasses(parts) + except Exception: + logger.warning("Failed to serialize system instructions; using fallback.", exc_info=True) + count = len(parts) + noun = "instruction part" if count == 1 else "instruction parts" + return _serialize_dataclasses([TextPart(content=f"[serialization failed: {count} {noun}]")]) diff --git a/tests/a365/test_invoke_agent_scope.py b/tests/a365/test_invoke_agent_scope.py index 577eefbc..d0ba4b58 100644 --- a/tests/a365/test_invoke_agent_scope.py +++ b/tests/a365/test_invoke_agent_scope.py @@ -1,6 +1,9 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +import json +from typing import Any, cast + from microsoft.opentelemetry.a365.core import ( AgentDetails, GenAiRequestParameters, @@ -8,13 +11,14 @@ InvokeAgentScope, InvokeAgentScopeDetails, Request, + TextPart, ) from microsoft.opentelemetry.a365.core.constants import SOURCE_NAME from microsoft.opentelemetry.a365.core.opentelemetry_scope import OpenTelemetryScope from opentelemetry.sdk.trace import TracerProvider -def _start_invoke_agent_scope(scope_details: InvokeAgentScopeDetails): +def _start_invoke_agent_scope(scope_details: InvokeAgentScopeDetails) -> InvokeAgentScope: return InvokeAgentScope.start( request=Request(), scope_details=scope_details, @@ -22,7 +26,19 @@ def _start_invoke_agent_scope(scope_details: InvokeAgentScopeDetails): ) -def setup_function(): +def _span_attributes(scope: InvokeAgentScope) -> dict[str, object]: + assert scope._span is not None + span = cast(Any, scope._span) + return dict(span.attributes) + + +def _string_attribute(attrs: dict[str, object], key: str) -> str: + value = attrs[key] + assert isinstance(value, str) + return value + + +def setup_function() -> None: import os os.environ["ENABLE_OBSERVABILITY"] = "true" @@ -31,7 +47,7 @@ def setup_function(): setup_function.provider = provider # type: ignore[attr-defined] -def teardown_function(): +def teardown_function() -> None: import os provider = getattr(setup_function, "provider", None) @@ -41,7 +57,7 @@ def teardown_function(): os.environ.pop("ENABLE_OBSERVABILITY", None) -def test_invoke_agent_details_accepts_semantic_parameters(): +def test_invoke_agent_details_accepts_semantic_parameters() -> None: request = GenAiRequestParameters(model="gpt-4o", max_tokens=128) response = GenAiResponseParameters(input_tokens=10, output_tokens=4) details = InvokeAgentScopeDetails( @@ -52,7 +68,7 @@ def test_invoke_agent_details_accepts_semantic_parameters(): assert details.response_parameters is response -def test_invoke_agent_scope_records_request_parameters(): +def test_invoke_agent_scope_records_request_parameters() -> None: parameters = GenAiRequestParameters( model="gpt-4o", seed=42, @@ -65,11 +81,14 @@ def test_invoke_agent_scope_records_request_parameters(): top_p=0.9, data_source_id="data-source-1", output_type="json", - system_instructions="Be concise.", + system_instructions=[ + TextPart(content="Be concise."), + TextPart(content="Return JSON."), + ], ) scope = _start_invoke_agent_scope(InvokeAgentScopeDetails(request_parameters=parameters)) try: - attrs = dict(scope._span.attributes) + attrs = _span_attributes(scope) assert attrs["gen_ai.request.model"] == "gpt-4o" assert attrs["gen_ai.request.seed"] == 42 assert attrs["gen_ai.request.choice.count"] == 2 @@ -81,12 +100,15 @@ def test_invoke_agent_scope_records_request_parameters(): assert attrs["gen_ai.request.top_p"] == 0.9 assert attrs["gen_ai.data_source.id"] == "data-source-1" assert attrs["gen_ai.output.type"] == "json" - assert attrs["gen_ai.system_instructions"] == "Be concise." + assert json.loads(_string_attribute(attrs, "gen_ai.system_instructions")) == [ + {"content": "Be concise.", "type": "text"}, + {"content": "Return JSON.", "type": "text"}, + ] finally: scope.dispose() -def test_invoke_agent_scope_records_response_parameters_after_completion(): +def test_invoke_agent_scope_records_response_parameters_after_completion() -> None: scope = _start_invoke_agent_scope(InvokeAgentScopeDetails()) try: scope.record_response_parameters( @@ -98,7 +120,7 @@ def test_invoke_agent_scope_records_response_parameters_after_completion(): cache_read_input_tokens=1, ) ) - attrs = dict(scope._span.attributes) + attrs = _span_attributes(scope) assert attrs["gen_ai.response.finish_reasons"] == ("stop",) assert attrs["gen_ai.usage.input_tokens"] == 10 assert attrs["gen_ai.usage.output_tokens"] == 4 @@ -109,7 +131,7 @@ def test_invoke_agent_scope_records_response_parameters_after_completion(): scope.dispose() -def test_invoke_agent_scope_omits_none_semantic_parameters(): +def test_invoke_agent_scope_omits_none_semantic_parameters() -> None: scope = _start_invoke_agent_scope( InvokeAgentScopeDetails( request_parameters=GenAiRequestParameters(model="gpt-4o"), @@ -117,7 +139,7 @@ def test_invoke_agent_scope_omits_none_semantic_parameters(): ) ) try: - attrs = dict(scope._span.attributes) + attrs = _span_attributes(scope) assert attrs["gen_ai.request.model"] == "gpt-4o" assert attrs["gen_ai.usage.input_tokens"] == 10 assert "gen_ai.request.seed" not in attrs @@ -127,5 +149,24 @@ def test_invoke_agent_scope_omits_none_semantic_parameters(): assert "gen_ai.usage.cache_write.input_tokens" not in attrs assert "gen_ai.usage.cache_creation.input_tokens" not in attrs assert "gen_ai.usage.cache_read.input_tokens" not in attrs + assert "gen_ai.system_instructions" not in attrs + finally: + scope.dispose() + + +def test_invoke_agent_scope_system_instructions_serialization_is_non_throwing() -> None: + parameters = GenAiRequestParameters( + system_instructions=[object()], # type: ignore[list-item] + ) + + scope = _start_invoke_agent_scope(InvokeAgentScopeDetails(request_parameters=parameters)) + try: + attrs = _span_attributes(scope) + assert json.loads(_string_attribute(attrs, "gen_ai.system_instructions")) == [ + { + "content": "[serialization failed: 1 instruction part]", + "type": "text", + } + ] finally: scope.dispose() From d85a7e8f8d19c1f2bd9f71af1ea8c8c983fdbac7 Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Wed, 16 Sep 2026 13:13:50 -0600 Subject: [PATCH 12/14] test: cover shared message serialization Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/a365/test_message_utils.py | 74 ++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/a365/test_message_utils.py diff --git a/tests/a365/test_message_utils.py b/tests/a365/test_message_utils.py new file mode 100644 index 00000000..bc7fab41 --- /dev/null +++ b/tests/a365/test_message_utils.py @@ -0,0 +1,74 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import json + +from microsoft.opentelemetry.a365.core.message_utils import serialize_messages +from microsoft.opentelemetry.a365.core.models.messages import ( + ChatMessage, + InputMessages, + MessageRole, + OutputMessage, + OutputMessages, + TextPart, +) + + +def test_serialize_messages_preserves_input_message_shape() -> None: + payload = InputMessages( + messages=[ + ChatMessage( + role=MessageRole.USER, + parts=[TextPart(content="hello")], + ) + ] + ) + + assert json.loads(serialize_messages(payload)) == [ + { + "role": "user", + "parts": [{"content": "hello", "type": "text"}], + } + ] + + +def test_serialize_messages_preserves_output_message_fallback_shape() -> None: + payload = OutputMessages(messages=[object()]) # type: ignore[list-item] + + assert json.loads(serialize_messages(payload)) == [ + { + "role": "system", + "parts": [{"type": "text", "content": "[serialization failed: 1 message]"}], + "finish_reason": "error", + } + ] + + +def test_serialize_messages_preserves_input_message_fallback_shape() -> None: + payload = InputMessages(messages=[object()]) # type: ignore[list-item] + + assert json.loads(serialize_messages(payload)) == [ + { + "role": "system", + "parts": [{"type": "text", "content": "[serialization failed: 1 message]"}], + } + ] + + +def test_serialize_messages_preserves_output_message_shape() -> None: + payload = OutputMessages( + messages=[ + OutputMessage( + role=MessageRole.ASSISTANT, + parts=[TextPart(content="hello back")], + ) + ] + ) + + assert json.loads(serialize_messages(payload)) == [ + { + "role": "assistant", + "parts": [{"content": "hello back", "type": "text"}], + "finish_reason": "stop", + } + ] From 6fc4cc7804327071fffb31853ad9811c3c1c98ca Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Wed, 16 Sep 2026 13:32:17 -0600 Subject: [PATCH 13/14] docs: align InvokeAgent semantic parameters Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- A365_DOCUMENTATION.md | 12 +++++++++--- CHANGELOG.md | 5 ++++- README.md | 16 ++++++++++++++-- tests/a365/test_invoke_agent_scope.py | 7 +++++-- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/A365_DOCUMENTATION.md b/A365_DOCUMENTATION.md index de519562..723aafea 100644 --- a/A365_DOCUMENTATION.md +++ b/A365_DOCUMENTATION.md @@ -358,7 +358,7 @@ Top-level agent invocation — wraps the entire request/response cycle: from microsoft.opentelemetry.a365.core import ( AgentDetails, CallerDetails, Channel, GenAiRequestParameters, GenAiResponseParameters, InvokeAgentScope, InvokeAgentScopeDetails, - Request, ServiceEndpoint, UserDetails, + Request, ServiceEndpoint, TextPart, UserDetails, ) agent = AgentDetails(agent_id="agent-001", agent_name="My Agent", tenant_id="t1") @@ -373,6 +373,7 @@ with InvokeAgentScope.start( temperature=0.2, stop_sequences=["END"], output_type="text", + system_instructions=[TextPart(content="Be concise.")], ), ), agent_details=agent, @@ -380,7 +381,12 @@ with InvokeAgentScope.start( ) as scope: # ... do work ... scope.record_response_parameters( - GenAiResponseParameters(finish_reasons=["stop"], input_tokens=42, output_tokens=18) + GenAiResponseParameters( + finish_reasons=["stop"], + input_tokens=42, + output_tokens=18, + cache_write_input_tokens=4, + ) ) scope.record_response("Here is the answer.") ``` @@ -400,7 +406,7 @@ Supported semantic attributes: `gen_ai.request.top_p`, `gen_ai.data_source.id`, `gen_ai.output.type`, `gen_ai.system_instructions` - Response: `gen_ai.response.finish_reasons`, `gen_ai.usage.input_tokens`, - `gen_ai.usage.output_tokens`, `gen_ai.usage.cache_creation.input_tokens`, + `gen_ai.usage.output_tokens`, `gen_ai.usage.cache_write.input_tokens`, `gen_ai.usage.cache_read.input_tokens` ### ExecuteToolScope diff --git a/CHANGELOG.md b/CHANGELOG.md index efcaed78..b518c6ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,10 @@ # Release History # Unreleased ### Features Added -- Add Python-native `InvokeAgentScope` request and response parameter models that emit GenAI semantic attributes introduced by .NET PR #120. +- Add Python-native `InvokeAgentScope` request and response parameter models + that emit OpenTelemetry GenAI semantic attributes, including structured + system instructions and cache read/write token counts, introduced by .NET + PR #120. # 1.3.9 (2026-09-09) ### Features Added diff --git a/README.md b/README.md index d1058f42..4875e67d 100644 --- a/README.md +++ b/README.md @@ -134,17 +134,29 @@ from microsoft.opentelemetry.a365.core import ( InvokeAgentScope, InvokeAgentScopeDetails, Request, + TextPart, ) request = Request(content="Hello") agent_details = AgentDetails(agent_id="agent-001") details = InvokeAgentScopeDetails( - request_parameters=GenAiRequestParameters(model="gpt-4o", max_tokens=256, stop_sequences=["END"]) + request_parameters=GenAiRequestParameters( + model="gpt-4o", + max_tokens=256, + stop_sequences=["END"], + system_instructions=[TextPart(content="Be concise.")], + ) ) with InvokeAgentScope.start(request, details, agent_details) as scope: # Invoke your agent here. - scope.record_response_parameters(GenAiResponseParameters(finish_reasons=["stop"], output_tokens=18)) + scope.record_response_parameters( + GenAiResponseParameters( + finish_reasons=["stop"], + output_tokens=18, + cache_write_input_tokens=4, + ) + ) ``` Unset fields are omitted. Supported attributes include `gen_ai.request.*`, diff --git a/tests/a365/test_invoke_agent_scope.py b/tests/a365/test_invoke_agent_scope.py index d0ba4b58..c387cf55 100644 --- a/tests/a365/test_invoke_agent_scope.py +++ b/tests/a365/test_invoke_agent_scope.py @@ -38,6 +38,9 @@ def _string_attribute(attrs: dict[str, object], key: str) -> str: return value +LEGACY_CACHE_CREATION_INPUT_TOKENS_KEY = ".".join(("gen_ai", "usage", "cache_creation", "input_tokens")) + + def setup_function() -> None: import os @@ -125,7 +128,7 @@ def test_invoke_agent_scope_records_response_parameters_after_completion() -> No assert attrs["gen_ai.usage.input_tokens"] == 10 assert attrs["gen_ai.usage.output_tokens"] == 4 assert attrs["gen_ai.usage.cache_write.input_tokens"] == 2 - assert "gen_ai.usage.cache_creation.input_tokens" not in attrs + assert LEGACY_CACHE_CREATION_INPUT_TOKENS_KEY not in attrs assert attrs["gen_ai.usage.cache_read.input_tokens"] == 1 finally: scope.dispose() @@ -147,7 +150,7 @@ def test_invoke_agent_scope_omits_none_semantic_parameters() -> None: assert "gen_ai.response.finish_reasons" not in attrs assert "gen_ai.usage.output_tokens" not in attrs assert "gen_ai.usage.cache_write.input_tokens" not in attrs - assert "gen_ai.usage.cache_creation.input_tokens" not in attrs + assert LEGACY_CACHE_CREATION_INPUT_TOKENS_KEY not in attrs assert "gen_ai.usage.cache_read.input_tokens" not in attrs assert "gen_ai.system_instructions" not in attrs finally: From c0b84204746cc9758120918d9f3bcdabeae8e99b Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Wed, 16 Sep 2026 13:39:11 -0600 Subject: [PATCH 14/14] chore: remove local workflow artifacts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 1 - ...26-09-16-invoke-agent-semconv-alignment.md | 461 ------------------ ...6-invoke-agent-semconv-alignment-design.md | 91 ---- 3 files changed, 553 deletions(-) delete mode 100644 docs/superpowers/plans/2026-09-16-invoke-agent-semconv-alignment.md delete mode 100644 docs/superpowers/specs/2026-09-16-invoke-agent-semconv-alignment-design.md diff --git a/.gitignore b/.gitignore index 4789fac3..ed511ea9 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ build/ .env .pytest_cache/ .mypy_cache/ -.worktrees/ docs/_build/ docs/api/ docs/superpowers/ diff --git a/docs/superpowers/plans/2026-09-16-invoke-agent-semconv-alignment.md b/docs/superpowers/plans/2026-09-16-invoke-agent-semconv-alignment.md deleted file mode 100644 index d06bf014..00000000 --- a/docs/superpowers/plans/2026-09-16-invoke-agent-semconv-alignment.md +++ /dev/null @@ -1,461 +0,0 @@ -# InvokeAgent Semantic Convention Alignment Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Make the new `InvokeAgentScope` cache-token and system-instruction telemetry conform to the current OpenTelemetry GenAI semantic conventions. - -**Architecture:** Rename the unreleased cache-creation API and attribute to the canonical cache-write terminology. Represent system instructions as a sequence of existing `TextPart` objects and serialize that sequence through a focused helper that shares the current message serialization primitives. - -**Tech Stack:** Python 3.10+, dataclasses, OpenTelemetry Python SDK, pytest, Black, Pylint, Mypy, tox. - -## Global Constraints - -- Emit cache-write tokens as `gen_ai.usage.cache_write.input_tokens`. -- Expose the response field as `cache_write_input_tokens`; do not retain the unreleased `cache_creation_input_tokens` alias. -- Accept structured system instructions only as `Sequence[TextPart] | None`. -- Emit `gen_ai.system_instructions` as a JSON-encoded array of objects with automatic `"type": "text"` discriminators. -- Preserve non-throwing telemetry serialization behavior. -- Do not modify provider-specific extraction fields such as Anthropic's `cache_creation_input_tokens` outside the new `InvokeAgentScope` API. - ---- - -### Task 1: Rename InvokeAgent cache creation to cache write - -**Files:** -- Modify: `tests/a365/test_invoke_agent_scope.py:87-128` -- Modify: `src/microsoft/opentelemetry/a365/core/gen_ai_response_parameters.py:9-18` -- Modify: `src/microsoft/opentelemetry/a365/core/constants.py:56-62` -- Modify: `src/microsoft/opentelemetry/a365/constants.py:59-65` -- Modify: `src/microsoft/opentelemetry/a365/core/invoke_agent_scope.py:35-41,254-268` - -**Interfaces:** -- Consumes: `GenAiResponseParameters` passed to `InvokeAgentScope.record_response_parameters`. -- Produces: `cache_write_input_tokens: int | None` and `GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_KEY = "gen_ai.usage.cache_write.input_tokens"`. - -- [ ] **Step 1: Update the focused tests to require cache-write naming** - -Replace the cache assertions and constructor argument in -`tests/a365/test_invoke_agent_scope.py`: - -```python -scope.record_response_parameters( - GenAiResponseParameters( - finish_reasons=["stop"], - input_tokens=10, - output_tokens=4, - cache_write_input_tokens=2, - cache_read_input_tokens=1, - ) -) -attrs = dict(scope._span.attributes) -assert attrs["gen_ai.response.finish_reasons"] == ("stop",) -assert attrs["gen_ai.usage.input_tokens"] == 10 -assert attrs["gen_ai.usage.output_tokens"] == 4 -assert attrs["gen_ai.usage.cache_write.input_tokens"] == 2 -assert "gen_ai.usage.cache_creation.input_tokens" not in attrs -assert attrs["gen_ai.usage.cache_read.input_tokens"] == 1 -``` - -Update the omission assertions: - -```python -assert "gen_ai.usage.cache_write.input_tokens" not in attrs -assert "gen_ai.usage.cache_creation.input_tokens" not in attrs -``` - -- [ ] **Step 2: Run the cache tests and verify they fail** - -Run: - -```powershell -python -m pytest tests\a365\test_invoke_agent_scope.py -q -``` - -Expected: failure because `GenAiResponseParameters` does not accept -`cache_write_input_tokens`. - -- [ ] **Step 3: Rename the response model field and constants** - -In `src/microsoft/opentelemetry/a365/core/gen_ai_response_parameters.py`: - -```python -@dataclass -class GenAiResponseParameters: - """Optional GenAI response parameters recorded on InvokeAgent spans.""" - - finish_reasons: Sequence[str] | None = None - input_tokens: int | None = None - output_tokens: int | None = None - cache_write_input_tokens: int | None = None - cache_read_input_tokens: int | None = None -``` - -In both constants modules, replace the cache-creation declaration with: - -```python -GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_KEY = "gen_ai.usage.cache_write.input_tokens" -``` - -In `src/microsoft/opentelemetry/a365/core/invoke_agent_scope.py`, import the -new constant and emit the renamed field: - -```python -self.set_tag_maybe( - GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_KEY, - parameters.cache_write_input_tokens, -) -``` - -- [ ] **Step 4: Run the focused tests and verify they pass** - -Run: - -```powershell -python -m pytest tests\a365\test_invoke_agent_scope.py -q -``` - -Expected: all tests in the file pass. - -- [ ] **Step 5: Confirm no new InvokeAgent cache-creation symbols remain** - -Run: - -```powershell -rg "GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS_KEY|cache_creation_input_tokens|gen_ai\.usage\.cache_creation\.input_tokens" src\microsoft\opentelemetry\a365 tests\a365\test_invoke_agent_scope.py -``` - -Expected: no matches. Provider-specific matches elsewhere in `_genai` are -intentionally outside scope. - -- [ ] **Step 6: Commit the cache rename** - -```powershell -git add src\microsoft\opentelemetry\a365\constants.py src\microsoft\opentelemetry\a365\core\constants.py src\microsoft\opentelemetry\a365\core\gen_ai_response_parameters.py src\microsoft\opentelemetry\a365\core\invoke_agent_scope.py tests\a365\test_invoke_agent_scope.py -git commit -m "fix: use OTel cache-write token attribute" -m "Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>" -``` - -### Task 2: Serialize structured system instructions - -**Files:** -- Modify: `tests/a365/test_invoke_agent_scope.py:5-85,109-128` -- Modify: `src/microsoft/opentelemetry/a365/core/gen_ai_request_parameters.py:5-25` -- Modify: `src/microsoft/opentelemetry/a365/core/message_utils.py:10-119` -- Modify: `src/microsoft/opentelemetry/a365/core/invoke_agent_scope.py:45-55,238-253` - -**Interfaces:** -- Consumes: `TextPart` from `microsoft.opentelemetry.a365.core.models.messages`. -- Produces: `GenAiRequestParameters.system_instructions: Sequence[TextPart] | None`. -- Produces: `serialize_system_instructions(parts: Sequence[TextPart]) -> str`. - -- [ ] **Step 1: Add tests for the required OTel JSON shape** - -Add `json` and `TextPart` imports in `tests/a365/test_invoke_agent_scope.py`: - -```python -import json - -from microsoft.opentelemetry.a365.core import ( - AgentDetails, - GenAiRequestParameters, - GenAiResponseParameters, - InvokeAgentScope, - InvokeAgentScopeDetails, - Request, - TextPart, -) -``` - -Change the request parameters in -`test_invoke_agent_scope_records_request_parameters`: - -```python -system_instructions=[ - TextPart(content="Be concise."), - TextPart(content="Return JSON."), -], -``` - -Replace the system-instruction assertion with: - -```python -assert json.loads(attrs["gen_ai.system_instructions"]) == [ - {"content": "Be concise.", "type": "text"}, - {"content": "Return JSON.", "type": "text"}, -] -``` - -Add an omission assertion to -`test_invoke_agent_scope_omits_none_semantic_parameters`: - -```python -assert "gen_ai.system_instructions" not in attrs -``` - -Add a serializer failure test: - -```python -def test_invoke_agent_scope_system_instructions_serialization_is_non_throwing(): - parameters = GenAiRequestParameters( - system_instructions=[object()], # type: ignore[list-item] - ) - - scope = _start_invoke_agent_scope(InvokeAgentScopeDetails(request_parameters=parameters)) - try: - attrs = dict(scope._span.attributes) - assert json.loads(attrs["gen_ai.system_instructions"]) == [ - { - "content": "[serialization failed: 1 instruction part]", - "type": "text", - } - ] - finally: - scope.dispose() -``` - -- [ ] **Step 2: Run the structured-instruction tests and verify they fail** - -Run: - -```powershell -python -m pytest tests\a365\test_invoke_agent_scope.py -q -``` - -Expected: the JSON-shape assertion fails because the span currently contains a -raw sequence unsupported by OpenTelemetry attributes. - -- [ ] **Step 3: Type the field as structured instruction parts** - -In `src/microsoft/opentelemetry/a365/core/gen_ai_request_parameters.py`: - -```python -from typing import Sequence - -from microsoft.opentelemetry.a365.core.models.messages import TextPart - - -@dataclass -class GenAiRequestParameters: - """Optional GenAI request parameters recorded on InvokeAgent spans.""" - - # Existing fields remain unchanged. - system_instructions: Sequence[TextPart] | None = None -``` - -- [ ] **Step 4: Extract shared dataclass JSON serialization** - -In `src/microsoft/opentelemetry/a365/core/message_utils.py`, import `Sequence`, -then add: - -```python -def _serialize_dataclasses(items: Sequence[object]) -> str: - """Serialize dataclass instances as a JSON array.""" - return json.dumps( - [asdict(item, dict_factory=_message_dict_factory) for item in items], - default=str, - ensure_ascii=False, - ) -``` - -Use it in `serialize_messages`: - -```python -try: - return _serialize_dataclasses(wrapper.messages) -except Exception: - logger.warning("Failed to serialize messages; using fallback.", exc_info=True) - # Preserve the existing fallback construction. -``` - -Add the structured-instruction serializer: - -```python -def serialize_system_instructions(parts: Sequence[TextPart]) -> str: - """Serialize OTel system-instruction parts as a JSON array.""" - try: - return _serialize_dataclasses(parts) - except Exception: - logger.warning("Failed to serialize system instructions; using fallback.", exc_info=True) - count = len(parts) - noun = "instruction part" if count == 1 else "instruction parts" - return _serialize_dataclasses( - [TextPart(content=f"[serialization failed: {count} {noun}]")] - ) -``` - -- [ ] **Step 5: Emit serialized instructions from InvokeAgentScope** - -Import `serialize_system_instructions` in -`src/microsoft/opentelemetry/a365/core/invoke_agent_scope.py`, then replace the -raw assignment: - -```python -if parameters.system_instructions is not None: - self.set_tag_maybe( - GEN_AI_SYSTEM_INSTRUCTIONS_KEY, - serialize_system_instructions(parameters.system_instructions), - ) -``` - -- [ ] **Step 6: Run focused tests and verify they pass** - -Run: - -```powershell -python -m pytest tests\a365\test_invoke_agent_scope.py -q -``` - -Expected: all tests pass, including structured JSON and non-throwing fallback. - -- [ ] **Step 7: Run static checks for the changed Python surface** - -Run: - -```powershell -python -m black --check src\microsoft\opentelemetry\a365\core\gen_ai_request_parameters.py src\microsoft\opentelemetry\a365\core\gen_ai_response_parameters.py src\microsoft\opentelemetry\a365\core\message_utils.py src\microsoft\opentelemetry\a365\core\invoke_agent_scope.py tests\a365\test_invoke_agent_scope.py -python -m pylint src\microsoft\opentelemetry\a365\core\gen_ai_request_parameters.py src\microsoft\opentelemetry\a365\core\gen_ai_response_parameters.py src\microsoft\opentelemetry\a365\core\message_utils.py src\microsoft\opentelemetry\a365\core\invoke_agent_scope.py tests\a365\test_invoke_agent_scope.py -python -m mypy --python-version 3.10 --show-error-codes --ignore-missing-imports src\microsoft\opentelemetry\a365\core tests\a365\test_invoke_agent_scope.py -``` - -Expected: each command exits successfully with no errors. - -- [ ] **Step 8: Commit structured instruction support** - -```powershell -git add src\microsoft\opentelemetry\a365\core\gen_ai_request_parameters.py src\microsoft\opentelemetry\a365\core\message_utils.py src\microsoft\opentelemetry\a365\core\invoke_agent_scope.py tests\a365\test_invoke_agent_scope.py -git commit -m "fix: serialize structured system instructions" -m "Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>" -``` - -### Task 3: Update documentation and close review threads - -**Files:** -- Modify: `README.md:127-153` -- Modify: `A365_DOCUMENTATION.md:353-404` -- Modify: `CHANGELOG.md:1-5` - -**Interfaces:** -- Consumes: `TextPart`, `cache_write_input_tokens`, and the canonical emitted attribute names from Tasks 1 and 2. -- Produces: Copyable documentation matching the final public API. - -- [ ] **Step 1: Update the top-level README example** - -Add `TextPart` to the import list and demonstrate structured instructions: - -```python -from microsoft.opentelemetry.a365.core import ( - AgentDetails, - GenAiRequestParameters, - GenAiResponseParameters, - InvokeAgentScope, - InvokeAgentScopeDetails, - Request, - TextPart, -) - -details = InvokeAgentScopeDetails( - request_parameters=GenAiRequestParameters( - model="gpt-4o", - max_tokens=256, - stop_sequences=["END"], - system_instructions=[TextPart(content="Be concise.")], - ) -) -``` - -Include cache-write usage in the response call: - -```python -scope.record_response_parameters( - GenAiResponseParameters( - finish_reasons=["stop"], - output_tokens=18, - cache_write_input_tokens=4, - ) -) -``` - -- [ ] **Step 2: Update the A365 guide** - -Add `TextPart` to the `InvokeAgentScope` import list, add: - -```python -system_instructions=[TextPart(content="Be concise.")], -``` - -to `GenAiRequestParameters`, and add: - -```python -cache_write_input_tokens=4, -``` - -to the response example. Replace -`gen_ai.usage.cache_creation.input_tokens` with -`gen_ai.usage.cache_write.input_tokens` in the supported attribute list. - -- [ ] **Step 3: Clarify the unreleased changelog entry** - -Replace the current feature line with: - -```markdown -- Add Python-native `InvokeAgentScope` request and response parameter models - that emit OpenTelemetry GenAI semantic attributes, including structured - system instructions and cache read/write token counts, introduced by .NET - PR #120. -``` - -- [ ] **Step 4: Validate documentation and the complete focused change** - -Run: - -```powershell -python -m pytest tests\a365\test_invoke_agent_scope.py -q -git diff --check -rg "gen_ai\.usage\.cache_creation\.input_tokens|cache_creation_input_tokens" README.md A365_DOCUMENTATION.md CHANGELOG.md src\microsoft\opentelemetry\a365 tests\a365\test_invoke_agent_scope.py -``` - -Expected: tests pass, `git diff --check` exits successfully, and `rg` returns no -matches in the scoped InvokeAgent surfaces. - -- [ ] **Step 5: Commit documentation alignment** - -```powershell -git add README.md A365_DOCUMENTATION.md CHANGELOG.md -git commit -m "docs: align InvokeAgent semantic parameters" -m "Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>" -``` - -- [ ] **Step 6: Run repository-standard final validation** - -Run: - -```powershell -tox -e black -tox -e pylint -tox -e mypy -tox -e pytest-py310 -- tests\a365\test_invoke_agent_scope.py -tox -e docs -git diff --check origin/main...HEAD -``` - -Expected: every tox environment exits successfully and the diff check reports -no whitespace errors. - -- [ ] **Step 7: Push the branch** - -```powershell -git push https://github.com/nikhilNava/opentelemetry-distro-python.git HEAD:copilot/invoke-agent-semantic-fields -``` - -- [ ] **Step 8: Reply to and resolve both GitHub review threads** - -Use the REST replies endpoint: - -```powershell -$commit = git rev-parse --short HEAD -$cacheReply = "Fixed in $commit. Renamed the new model field, constants, emitted attribute, tests, and docs to the current OTel gen_ai.usage.cache_write.input_tokens convention." -$instructionsReply = "Fixed in $commit. system_instructions now accepts structured TextPart sequences and emits the OTel JSON-array shape through the shared serialization path." -gh api --method POST repos/microsoft/opentelemetry-distro-python/pulls/263/comments/4019541268/replies -f body=$cacheReply -gh api --method POST repos/microsoft/opentelemetry-distro-python/pulls/263/comments/4019544444/replies -f body=$instructionsReply -``` - -Query the PR review threads with GraphQL, resolve the two matching thread IDs -using `resolveReviewThread`, then query again and confirm both report -`isResolved: true`. diff --git a/docs/superpowers/specs/2026-09-16-invoke-agent-semconv-alignment-design.md b/docs/superpowers/specs/2026-09-16-invoke-agent-semconv-alignment-design.md deleted file mode 100644 index 8093a094..00000000 --- a/docs/superpowers/specs/2026-09-16-invoke-agent-semconv-alignment-design.md +++ /dev/null @@ -1,91 +0,0 @@ -# InvokeAgent Semantic Convention Alignment - -## Goal - -Align the new `InvokeAgentScope` request and response parameter API with the -current OpenTelemetry GenAI semantic conventions before the API is released. - -## Cache-write token naming - -The current OpenTelemetry GenAI registry defines -`gen_ai.usage.cache_write.input_tokens` as the number of input tokens written -to a provider-managed cache. The PR's `cache_creation` name is provider -terminology and must not be exposed as the OpenTelemetry attribute name. - -Rename the new response model field, constants, emitted attribute, tests, and -documentation from `cache_creation` to `cache_write`: - -- `cache_creation_input_tokens` becomes `cache_write_input_tokens`. -- `GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS_KEY` becomes - `GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_KEY`. -- `gen_ai.usage.cache_creation.input_tokens` becomes - `gen_ai.usage.cache_write.input_tokens`. - -This change applies only to the new `InvokeAgentScope` API introduced by the -PR. Existing provider adapters may continue reading provider-specific fields -named `cache_creation_input_tokens`, but their exported semantic attribute -names are outside this PR's scope. - -## Structured system instructions - -OpenTelemetry defines `gen_ai.system_instructions` as an array of structured -parts. The public request model will therefore use: - -```python -system_instructions: Sequence[TextPart] | None = None -``` - -Callers pass a list or tuple of the existing `TextPart` objects: - -```python -system_instructions=[ - TextPart(content="Be concise."), - TextPart(content="Return JSON."), -] -``` - -`TextPart` automatically supplies the `"type": "text"` discriminator. The span -attribute will contain a JSON-encoded representation of the OpenTelemetry -array because the OpenTelemetry Python span API cannot directly store -structured objects: - -```json -[{"content":"Be concise.","type":"text"},{"content":"Return JSON.","type":"text"}] -``` - -Plain strings will no longer be accepted by this new, unreleased field. This -keeps the public API aligned with the semantic convention from its first -release rather than establishing a non-compliant compatibility contract. - -## Serialization - -Add a focused serializer for system-instruction parts in the existing message -utility module. It will reuse the same dataclass conversion, enum handling, -`None` omission, JSON encoding, logging, and non-throwing telemetry behavior as -the existing message serializers. - -The request-recording path will omit the attribute when instructions are -`None`. Otherwise, it will serialize the sequence and set -`gen_ai.system_instructions` to the resulting JSON string. - -## Tests and documentation - -Tests will verify: - -- Cache-write tokens use the new model field and standard emitted key. -- The obsolete cache-creation key is not emitted. -- One and multiple `TextPart` instructions serialize to the expected JSON - array with automatic `type: "text"` fields. -- Unset instructions remain omitted. -- Serialization failure follows the repository's non-throwing telemetry - behavior. - -README and A365 guide examples will use `TextPart` objects and the cache-write -terminology. The two GitHub review threads will receive concise replies -referencing the implementation commit and will be resolved after validation. - -## Validation - -Run the targeted A365 InvokeAgent tests, formatting and static checks covering -the changed modules, and `git diff --check`. Push the implementation to the PR -branch only after those checks pass.