Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions A365_DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,16 @@ import {

const invokeScope = InvokeAgentScope.start(
{ conversationId: "conv-123", sessionId: "session-456" },
{},
{ agentId: "agent-1", tenantId: "tenant-1" },
{
requestParameters: {
model: "gpt-4o",
outputType: "json",
systemInstructions: [
{ type: "text", content: "You are a helpful assistant." },
],
},
},
{ agentId: "agent-1", tenantId: "tenant-1", providerName: "openai" },
);

invokeScope.run(async () => {
Expand All @@ -46,9 +54,43 @@ invokeScope.run(async () => {
inferenceScope.dispose();
});

invokeScope.recordResponseParameters({
finishReasons: ["stop"],
inputTokens: 120,
outputTokens: 42,
cacheWriteInputTokens: 10,
cacheReadInputTokens: 8,
});
invokeScope.dispose();
```

`InvokeAgentScope.start()` captures request parameters immediately, while `recordResponseParameters()`
captures response and usage values after the agent completes.

| Input field | Emitted attribute key |
| --- | --- |
| `requestParameters.model` | `gen_ai.request.model` |
| `requestParameters.seed` | `gen_ai.request.seed` |
| `requestParameters.choiceCount` | `gen_ai.request.choice.count` |
| `requestParameters.frequencyPenalty` | `gen_ai.request.frequency_penalty` |
| `requestParameters.maxTokens` | `gen_ai.request.max_tokens` |
| `requestParameters.presencePenalty` | `gen_ai.request.presence_penalty` |
| `requestParameters.stopSequences` | `gen_ai.request.stop_sequences` |
| `requestParameters.temperature` | `gen_ai.request.temperature` |
| `requestParameters.topP` | `gen_ai.request.top_p` |
| `requestParameters.dataSourceId` | `gen_ai.data_source.id` |
| `requestParameters.outputType` | `gen_ai.output.type` |
| `requestParameters.systemInstructions` | `gen_ai.system_instructions` (JSON-serialized parts array) |
| `responseParameters.finishReasons` | `gen_ai.response.finish_reasons` |
| `responseParameters.inputTokens` | `gen_ai.usage.input_tokens` |
| `responseParameters.outputTokens` | `gen_ai.usage.output_tokens` |
| `responseParameters.cacheWriteInputTokens` | `gen_ai.usage.cache_write.input_tokens` |
| `responseParameters.cacheReadInputTokens` | `gen_ai.usage.cache_read.input_tokens` |
| `agentDetails.providerName` | `gen_ai.provider.name` |

System instructions may contain sensitive content. Only capture them when you
intend to store prompt text and have reviewed downstream access controls.

## Baggage And Context

Use `BaggageBuilder` when you want tenant, agent, user, conversation, or session data to flow with the active context.
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## [Unreleased]

### Features Added
- Add GenAI v1.42 InvokeAgent request, response, cache-token, and provider attribute capture for manual A365 scopes. [#239](https://github.com/microsoft/opentelemetry-distro-javascript/pull/239)

### Other Changes
- Consolidate Dependabot updates for Vitest 4.1.11, Hono 4.13.7, qs 6.16.0, fast-uri 3.1.7, actions/deploy-pages 5.0.1, and actions/checkout 7.0.1.

Expand Down
20 changes: 20 additions & 0 deletions src/a365/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,16 @@ export class OpenTelemetryConstants {

/** Attribute key for the gen-ai operation name (`gen_ai.operation.name`). */
public static readonly GEN_AI_OPERATION_NAME_KEY = "gen_ai.operation.name";
/** Attribute key for the external data source identifier (`gen_ai.data_source.id`). */
public static readonly GEN_AI_DATA_SOURCE_ID_KEY = "gen_ai.data_source.id";
/** Attribute key for the requested output type (`gen_ai.output.type`). */
public static readonly GEN_AI_OUTPUT_TYPE_KEY = "gen_ai.output.type";
/** Attribute key for the requested model name (`gen_ai.request.model`). */
public static readonly GEN_AI_REQUEST_MODEL_KEY = "gen_ai.request.model";
/** Attribute key for the requested number of choices (`gen_ai.request.choice.count`). */
public static readonly GEN_AI_REQUEST_CHOICE_COUNT_KEY = "gen_ai.request.choice.count";
/** Attribute key for the frequency penalty (`gen_ai.request.frequency_penalty`). */
public static readonly GEN_AI_REQUEST_FREQUENCY_PENALTY_KEY = "gen_ai.request.frequency_penalty";
/** Attribute key for the model that produced the response (`gen_ai.response.model`). */
public static readonly GEN_AI_RESPONSE_MODEL_KEY = "gen_ai.response.model";
/** Attribute key for the finish reasons returned by the model (`gen_ai.response.finish_reasons`). */
Expand All @@ -77,6 +85,12 @@ export class OpenTelemetryConstants {
public static readonly GEN_AI_PROVIDER_NAME_KEY = "gen_ai.provider.name";
/** Attribute key for the requested maximum number of tokens (`gen_ai.request.max_tokens`). */
public static readonly GEN_AI_REQUEST_MAX_TOKENS_KEY = "gen_ai.request.max_tokens";
/** Attribute key for the presence penalty (`gen_ai.request.presence_penalty`). */
public static readonly GEN_AI_REQUEST_PRESENCE_PENALTY_KEY = "gen_ai.request.presence_penalty";
/** Attribute key for the request seed (`gen_ai.request.seed`). */
public static readonly GEN_AI_REQUEST_SEED_KEY = "gen_ai.request.seed";
/** Attribute key for the stop sequences (`gen_ai.request.stop_sequences`). */
public static readonly GEN_AI_REQUEST_STOP_SEQUENCES_KEY = "gen_ai.request.stop_sequences";
/** Attribute key for the sampling temperature (`gen_ai.request.temperature`). */
public static readonly GEN_AI_REQUEST_TEMPERATURE_KEY = "gen_ai.request.temperature";
/** Attribute key for the nucleus-sampling top-p value (`gen_ai.request.top_p`). */
Expand All @@ -94,6 +108,12 @@ export class OpenTelemetryConstants {

// ── GenAI usage ──────────────────────────────────────────────────

/** Attribute key for the number of input tokens written into the cache (`gen_ai.usage.cache_write.input_tokens`). */
public static readonly GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_KEY =
"gen_ai.usage.cache_write.input_tokens";
/** Attribute key for the number of input tokens read from the cache (`gen_ai.usage.cache_read.input_tokens`). */
public static readonly GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_KEY =
"gen_ai.usage.cache_read.input_tokens";
/** Attribute key for the number of input (prompt) tokens (`gen_ai.usage.input_tokens`). */
public static readonly GEN_AI_USAGE_INPUT_TOKENS_KEY = "gen_ai.usage.input_tokens";
/** Attribute key for the number of output (completion) tokens (`gen_ai.usage.output_tokens`). */
Expand Down
49 changes: 49 additions & 0 deletions src/a365/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ export interface GenericPart {
[key: string]: unknown;
}

/** Content part accepted for system instructions. */
export type SystemInstructionPart = TextPart | GenericPart;

/** Union of all message part types per OTEL gen-ai semantic conventions. */
export type MessagePart =
| TextPart
Expand Down Expand Up @@ -363,10 +366,56 @@ export interface ServiceEndpoint {
// Scope detail types
// ---------------------------------------------------------------------------

/** Request-side GenAI parameters captured for agent invocation telemetry. */
export interface GenAiRequestParameters {
/** Name of the requested model. */
model?: string;
/** Seed used to make sampling reproducible. */
seed?: number;
/** Number of response choices requested from the model. */
choiceCount?: number;
/** Frequency penalty applied during token sampling. */
frequencyPenalty?: number;
/** Maximum number of tokens requested for generation. */
maxTokens?: number;
/** Presence penalty applied during token sampling. */
presencePenalty?: number;
/** Stop sequences supplied with the request. */
stopSequences?: string[];
/** Sampling temperature for the request. */
temperature?: number;
/** Nucleus-sampling top-p value for the request. */
topP?: number;
/** Identifier of the external data source used to ground the request. */
dataSourceId?: string;
/** Requested output type (for example, `json`). */
outputType?: string;
/** Structured system instructions provided to the model. */
systemInstructions?: SystemInstructionPart[];
}

/** Response-side GenAI parameters captured for agent invocation telemetry. */
export interface GenAiResponseParameters {
/** Finish reasons returned by the model. */
finishReasons?: string[];
/** Number of input (prompt) tokens consumed by the response. */
inputTokens?: number;
/** Number of output (completion) tokens produced by the response. */
outputTokens?: number;
/** Number of input tokens written into the cache. */
cacheWriteInputTokens?: number;
/** Number of input tokens read from the cache. */
cacheReadInputTokens?: number;
}

/** Details for invoking agent scope. */
export interface InvokeAgentScopeDetails {
/** Endpoint the agent is being invoked on. */
endpoint?: ServiceEndpoint;
/** Request-side GenAI parameters associated with the invoke-agent span. */
requestParameters?: GenAiRequestParameters;
/** Response-side GenAI parameters associated with the invoke-agent span. */
responseParameters?: GenAiResponseParameters;
}

/** Details of a tool call made by an agent. */
Expand Down
3 changes: 3 additions & 0 deletions src/a365/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export type {
OutputMessagesParam,
ResponseMessagesParam,
MessagePart,
SystemInstructionPart,
TextPart,
ToolCallRequestPart,
ToolCallResponsePart,
Expand All @@ -61,6 +62,8 @@ export type {
Request,
Channel,
ServiceEndpoint,
GenAiRequestParameters,
GenAiResponseParameters,
InvokeAgentScopeDetails,
ToolCallDetails,
InferenceDetails,
Expand Down
20 changes: 20 additions & 0 deletions src/a365/message-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
OutputMessages,
InputMessagesParam,
OutputMessagesParam,
SystemInstructionPart,
} from "./contracts.js";
import { MessageRole, DEFAULT_FINISH_REASON } from "./contracts.js";

Expand Down Expand Up @@ -105,6 +106,25 @@ export function serializeMessages(wrapper: InputMessages | OutputMessages): stri
}
}

/**
* Serializes system instruction parts to a JSON array.
*
* The fallback keeps telemetry recording non-throwing when a part contains
* non-JSON-serializable values.
*/
export function serializeSystemInstructions(parts: SystemInstructionPart[]): string {
try {
return JSON.stringify(parts);
} catch {
return JSON.stringify([
{
type: "text",
content: `[serialization failed: ${parts.length} ${parts.length === 1 ? "instruction" : "instructions"}]`,
},
]);
}
}

/**
* Ensures the value is always a JSON-parseable string.
* - Objects are serialized via JSON.stringify.
Expand Down
82 changes: 79 additions & 3 deletions src/a365/scopes/InvokeAgentScope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
import { SpanKind } from "@opentelemetry/api";
import { OpenTelemetryScope } from "./OpenTelemetryScope.js";
import { OpenTelemetryConstants } from "../constants.js";
import { serializeSystemInstructions } from "../message-utils.js";
import type {
InvokeAgentScopeDetails,
CallerDetails,
Request,
SpanDetails,
AgentDetails,
GenAiRequestParameters,
GenAiResponseParameters,
InputMessagesParam,
OutputMessagesParam,
} from "../contracts.js";
Expand Down Expand Up @@ -69,9 +72,6 @@ export class InvokeAgentScope extends OpenTelemetryScope {
callerDetails?.userDetails,
);

// Provider name
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_PROVIDER_NAME_KEY, agentDetails.providerName);

// Session ID
this.setTagMaybe(OpenTelemetryConstants.SESSION_ID_KEY, request.sessionId);

Expand All @@ -97,6 +97,14 @@ export class InvokeAgentScope extends OpenTelemetryScope {
this.recordInputMessages(request.content);
}

if (invokeScopeDetails.requestParameters) {
this.mapRequestParameters(invokeScopeDetails.requestParameters);
}

if (invokeScopeDetails.responseParameters) {
this.mapResponseParameters(invokeScopeDetails.responseParameters);
}

// Caller agent details for A2A scenarios
const callerAgent = callerDetails?.callerAgentDetails;
if (callerAgent) {
Expand Down Expand Up @@ -130,6 +138,10 @@ export class InvokeAgentScope extends OpenTelemetryScope {
this.recordOutputMessages(response);
}

public recordResponseParameters(responseParameters: GenAiResponseParameters): void {
this.mapResponseParameters(responseParameters);
}

/** Records the input messages for telemetry tracking. */
public override recordInputMessages(messages: InputMessagesParam): void {
super.recordInputMessages(messages);
Expand All @@ -139,4 +151,68 @@ export class InvokeAgentScope extends OpenTelemetryScope {
public override recordOutputMessages(messages: OutputMessagesParam): void {
super.recordOutputMessages(messages);
}

private mapRequestParameters(requestParameters: GenAiRequestParameters): void {
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_REQUEST_MODEL_KEY, requestParameters.model);
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_REQUEST_SEED_KEY, requestParameters.seed);
this.setTagMaybe(
OpenTelemetryConstants.GEN_AI_REQUEST_CHOICE_COUNT_KEY,
requestParameters.choiceCount,
);
this.setTagMaybe(
OpenTelemetryConstants.GEN_AI_REQUEST_FREQUENCY_PENALTY_KEY,
requestParameters.frequencyPenalty,
);
this.setTagMaybe(
OpenTelemetryConstants.GEN_AI_REQUEST_MAX_TOKENS_KEY,
requestParameters.maxTokens,
);
this.setTagMaybe(
OpenTelemetryConstants.GEN_AI_REQUEST_PRESENCE_PENALTY_KEY,
requestParameters.presencePenalty,
);
this.setTagMaybe(
OpenTelemetryConstants.GEN_AI_REQUEST_STOP_SEQUENCES_KEY,
requestParameters.stopSequences,
);
this.setTagMaybe(
OpenTelemetryConstants.GEN_AI_REQUEST_TEMPERATURE_KEY,
requestParameters.temperature,
);
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_REQUEST_TOP_P_KEY, requestParameters.topP);
this.setTagMaybe(
OpenTelemetryConstants.GEN_AI_DATA_SOURCE_ID_KEY,
requestParameters.dataSourceId,
);
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_OUTPUT_TYPE_KEY, requestParameters.outputType);
this.setTagMaybe(
OpenTelemetryConstants.GEN_AI_SYSTEM_INSTRUCTIONS_KEY,
requestParameters.systemInstructions === undefined
? undefined
: serializeSystemInstructions(requestParameters.systemInstructions),
);
}

private mapResponseParameters(responseParameters: GenAiResponseParameters): void {
this.setTagMaybe(
OpenTelemetryConstants.GEN_AI_RESPONSE_FINISH_REASONS_KEY,
responseParameters.finishReasons,
);
this.setTagMaybe(
OpenTelemetryConstants.GEN_AI_USAGE_INPUT_TOKENS_KEY,
responseParameters.inputTokens,
);
this.setTagMaybe(
OpenTelemetryConstants.GEN_AI_USAGE_OUTPUT_TOKENS_KEY,
responseParameters.outputTokens,
);
this.setTagMaybe(
OpenTelemetryConstants.GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_KEY,
responseParameters.cacheWriteInputTokens,
);
this.setTagMaybe(
OpenTelemetryConstants.GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_KEY,
responseParameters.cacheReadInputTokens,
);
}
}
1 change: 1 addition & 0 deletions src/a365/scopes/OpenTelemetryScope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export abstract class OpenTelemetryScope {
agentDetails.agentBlueprintId,
);
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_AGENT_VERSION_KEY, agentDetails.agentVersion);
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_PROVIDER_NAME_KEY, agentDetails.providerName);
}

// Set tenant ID
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export type {
Request as A365Request,
Channel,
ServiceEndpoint,
GenAiRequestParameters,
GenAiResponseParameters,
InvokeAgentScopeDetails,
ToolCallDetails,
InferenceDetails,
Expand All @@ -86,6 +88,7 @@ export type {
OutputMessagesParam,
ResponseMessagesParam,
MessagePart,
SystemInstructionPart,
TextPart,
ToolCallRequestPart,
ToolCallResponsePart,
Expand Down
Loading
Loading