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
72 changes: 64 additions & 8 deletions A365_DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,62 @@ invokeScope.dispose();

`ExecuteToolScope` serializes arguments to `gen_ai.tool.call.arguments` and results to
`gen_ai.tool.call.result` as JSON span attributes, so they may contain sensitive data.
Use `extension_data` for provider-specific fields on any typed ExecuteTool model. Non-empty
extension data is emitted under the model's `metadata` JSON property. Metadata keys remain
Use `extension_data` for provider-specific fields on any typed ExecuteTool model. Explicitly
provided extension data, including `{}`, is emitted under the model's `metadata` JSON property;
absent or null extension data is omitted. Metadata keys remain
isolated from declared schema fields, so an `extension_data.action` or
`extension_data.schema_version` value cannot replace the typed `action` or `schema_version`.
Typed payloads that contain invalid enum tokens, non-finite numbers, unsupported values, or
reference cycles are replaced with
`{"serialization_error":"Failed to serialize execute tool payload."}`.

### Tool endpoints and transfers

Supply `endpoint.url` on `ToolCallDetails` to record the complete URL as `url.full`,
including its path, query, and fragment. Existing `endpoint.host` and non-default
`endpoint.port` still populate `server.address` and `server.port`. Host-only endpoints
do not synthesize `url.full`. URLs may contain sensitive data; sanitize them before recording.

Use explicit `transferDetails` for handoffs; ordinary tool calls emit no transfer attributes.

```typescript
import { ExecuteToolScope, TransferMode, TransferTargetType } from "@microsoft/opentelemetry";
import type { ToolCallDetails } from "@microsoft/opentelemetry";

const details: ToolCallDetails = {
toolName: "handoff",
endpoint: {
host: "agents.example.com",
url: "https://agents.example.com/support",
},
transferDetails: {
mode: TransferMode.PASS_CONTROL,
targetType: TransferTargetType.AGENT,
targetAgentDetails: { agentId: "support-agent", agentName: "Support" },
},
};
const scope = ExecuteToolScope.start(
{ conversationId: "conv-123" },
details,
{ agentId: "source-agent", tenantId: "tenant-1" },
);
scope.dispose();
```

`TransferMode` supports `RETURN_TO_CALLER` (`return_to_caller`) and `PASS_CONTROL`
(`pass_control`). `TransferTargetType` supports `AGENT`, `HUMAN`, and `WORKFLOW`;
omitting `targetType` defaults to `agent`. Target identity is optional for agent transfers
and forbidden for human/workflow transfers. Invalid enum values or incompatible target
details throw before a span is created.

Transfers emit `microsoft.a365.transfer.mode` and `microsoft.a365.transfer.target.type`.
Provided agent ID, name, blueprint ID, platform ID, and version are recorded under
`microsoft.a365.transfer.target.agent.*` (`id`, `name`, `blueprint.id`, `platform.id`,
`version`). Other target identity fields are not emitted, and the source agent and tenant
attributes remain unchanged.

### Request and response attributes

`InvokeAgentScope`, `InferenceScope`, and `ExecuteToolScope` accept `request.sessionId`.
When you provide it, those scopes write `microsoft.session.id` directly on the created
span instead of relying on later baggage enrichment. `OutputScope` does not currently
Expand Down Expand Up @@ -296,7 +344,7 @@ network-only delivery. It applies only to the A365 HTTP exporter, so set
| `maxStorageBytes` | `50 * 1024 * 1024` | Bounds pending, quarantined, active leased, and non-stale temporary records within the current `app-<hash>` partition only |
| `maxRecordAgeMilliseconds` | `2 * 24 * 60 * 60 * 1000` | Expired records are pruned before capacity eviction, within the current `app-<hash>` partition only |
| `replayIntervalMilliseconds` | `2 * 60 * 1000` | Scheduled replay cadence |
| `maxReplayBatchSize` | `10` | Maximum records claimed per replay pass |
| `maxReplayBatchSize` | `10` | Maximum attempted records per replay pass; backed-off records do not consume this limit. At most 10 times this many records are claimed |
| `leaseDurationMilliseconds` | `2 * 60 * 1000` | Reclaims stale replay leases |
| `shutdownTimeoutMilliseconds` | `10_000` | Shared shutdown budget for accepted live exports and admitted durable handoff completion |
| `tokenResolutionTimeoutMilliseconds` | `30_000` | Timeout per replay token-resolution attempt |
Expand Down Expand Up @@ -333,20 +381,28 @@ network-only delivery. It applies only to the A365 HTTP exporter, so set
- Delivery is at-least-once. A retryable request can be replayed after a crash, timeout, or
shutdown race, so downstream consumers must tolerate duplicates.
- HTTP 401, 408, 429, and 5xx responses plus transport failures are retryable. Live delivery
persists them, and replay releases the claim while honoring the shared transmission gate.
persists them, and replay releases the claim while honoring that tenant's transmission gate.
- Replay and `forceFlush()` resolve a fresh token for each send attempt and use the exporter's
current cluster/domain routing; durable files do not store bearer tokens or authoritative route
metadata.
- If token resolution returns no token, throws, or times out, live delivery attempts to persist the
record for replay and replay releases the claim without extending the shared transmission backoff.
record for replay and replay releases the claim without extending tenant transmission backoff.
- Storage is bounded by both age and capacity, scoped to the current process's own `app-<hash>`
partition (see above). The SDK sweeps stale temporary files, prunes expired records, then evicts
the oldest remaining pending or quarantined record until the new record fits within
`maxStorageBytes`; active temporary and leased files count against that bound and are not
evicted. `maxStorageBytes` is therefore a per-partition, not a global, limit.
- Live delivery and replay share one `Retry-After` / exponential-backoff transmission gate. A
retryable response pauses both immediate sends and replay probes until the gate reopens. The
effective delay is capped at one hour.
- Live delivery and replay share a `Retry-After` / exponential-backoff transmission gate per
tenant, across that tenant's agents. A retryable response pauses only that tenant's immediate
sends and replay probes; other tenants continue. The effective delay is capped at one hour.
Replay skips released records within the same pass and scans past backed-off tenants within
the bounded claim budget (not a limit on directory listings or preflight reads). Excluded
records are skipped before leasing; post-claim content is still validated. Malformed records,
including blank tenant IDs, are quarantined. Invalid replay endpoints, unexpected send
exceptions, or failed claim releases stop the pass.
- Tenant gates are retained in a registry with a soft capacity of 256 and a 15-minute idle
timeout. Inactive gates are evicted first; active sends and unexpired backoff are never evicted,
so the registry may temporarily exceed its capacity.
- If records must survive container restarts, rescheduling, or host restarts, point
`storageDirectory` at a protected persistent volume. Ephemeral storage can be lost when a
container is replaced and still counts against container ephemeral-storage limits.
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
## [Unreleased]

### Bugs Fixed
- Isolate Agent365 durable delivery backoff by tenant, shared across that tenant's agents and live/replay paths. Continue replay past retryable failures and backed-off tenants without spending the attempt limit on deferred records; bound replay scans and preserve active backoff during registry eviction.
- Skip excluded durable records and backed-off tenants before leasing so blocked prefixes larger than the scan budget cannot starve healthy tenants across replay passes. Quarantine blank-tenant records before replay, retaining post-claim content validation.
- Preserve explicitly empty `extension_data` as nested `metadata: {}` in typed ExecuteTool payloads, including nested models, while continuing to omit absent/null metadata.
- Update OpenAI Agents to 0.15.0, Vitest and its coverage provider to 4.1.12, MSAL Node to 7.1.1, and vulnerable transitive dependencies across the distro and samples. OpenAI Agents 0.15.0 removes the vulnerable MCP/Express dependency chain.
- Require Node.js 22.23.4+, 24.22.1+, or 26.12.1+ on their respective release lines for security fixes.
- **Breaking change on Unix (Linux/macOS):** Agent365 offline buffering/replay now requires an explicitly configured, existing, absolute `durableDelivery.storageDirectory`. No automatic Unix temporary-directory fallback or root creation. Missing/invalid roots are logged and disable only disk buffering/replay; online export continues. Customers provision and maintain ownership, permissions (normally a private service-account-owned `0700` root), ACLs, and path-replacement protection; the SDK does not inspect or repair Unix access controls. Windows behavior, explicit-root cache identities, record compatibility, and retention limits are unchanged. Old temporary backlogs are not automatically migrated; see [storage provisioning and migration](A365_DOCUMENTATION.md#storage-provisioning-and-migration).

### Features Added
- Add optional full tool endpoint URLs (`endpoint.url` -> `url.full`) and typed execute-tool transfer metadata for agent, human, and workflow targets. Export transfer enums and contracts, validate incompatible targets before creating spans, and preserve source agent identity.
- Add privacy-safe Model Observation attributes to `InferenceScope`, including the canonical served response model and cache read/write input-token counts. Attributes can be supplied when the scope starts or recorded after the response is available; inference operations accept enum values or canonical strings and use canonical OpenTelemetry names for both operation attributes and span names. Cache-token semantic-convention constants are exported. [#261](https://github.com/microsoft/opentelemetry-distro-javascript/pull/261)

### Other Changes
Expand Down
20 changes: 20 additions & 0 deletions src/a365/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,26 @@ export class OpenTelemetryConstants {
public static readonly SERVER_ADDRESS_KEY = "server.address";
/** Attribute key for the destination server port (`server.port`). */
public static readonly SERVER_PORT_KEY = "server.port";
/** Attribute key for the complete tool endpoint URL (`url.full`). */
public static readonly URL_FULL_KEY = "url.full";
/** How control passes during an execute-tool transfer. */
public static readonly TRANSFER_MODE_KEY = "microsoft.a365.transfer.mode";
/** Kind of target receiving an execute-tool transfer. */
public static readonly TRANSFER_TARGET_TYPE_KEY = "microsoft.a365.transfer.target.type";
/** Target agent identifier for an execute-tool transfer. */
public static readonly TRANSFER_TARGET_AGENT_ID_KEY = "microsoft.a365.transfer.target.agent.id";
/** Target agent display name for an execute-tool transfer. */
public static readonly TRANSFER_TARGET_AGENT_NAME_KEY =
"microsoft.a365.transfer.target.agent.name";
/** Target agent blueprint identifier for an execute-tool transfer. */
public static readonly TRANSFER_TARGET_AGENT_BLUEPRINT_ID_KEY =
"microsoft.a365.transfer.target.agent.blueprint.id";
/** Target agent platform identifier for an execute-tool transfer. */
public static readonly TRANSFER_TARGET_AGENT_PLATFORM_ID_KEY =
"microsoft.a365.transfer.target.agent.platform.id";
/** Target agent version for an execute-tool transfer. */
public static readonly TRANSFER_TARGET_AGENT_VERSION_KEY =
"microsoft.a365.transfer.target.agent.version";
/** Azure resource provider namespace value for Cognitive Services. */
public static readonly AZURE_RP_NAMESPACE_VALUE = "Microsoft.CognitiveServices";

Expand Down
43 changes: 42 additions & 1 deletion src/a365/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,46 @@ export interface ServiceEndpoint {
port?: number;
/** Protocol scheme of the endpoint (e.g. `https`). */
protocol?: string;
}
/** Complete endpoint URL, including path and query, recorded as `url.full` on tool spans. */
url?: string;
}

/** How control passes during an execute-tool transfer. */
export enum TransferMode {
/** The source agent resumes after the target finishes. */
RETURN_TO_CALLER = "return_to_caller",
/** The target takes over the remaining work. */
PASS_CONTROL = "pass_control",
}

/** Kind of target receiving an execute-tool transfer. */
export enum TransferTargetType {
/** Transfer to another agent. */
AGENT = "agent",
/** Transfer to a human. */
HUMAN = "human",
/** Transfer to a workflow. */
WORKFLOW = "workflow",
}

/** Explicit transfer metadata. Target agent details are only valid for agent targets. */
export type TransferDetails =
| {
/** How control passes to the target. */
mode: TransferMode;
/** Target kind. Defaults to agent when omitted. */
targetType?: TransferTargetType.AGENT;
/** Target identity; only ID, name, blueprint, platform, and version are emitted. */
targetAgentDetails?: AgentDetails;
}
| {
/** How control passes to the target. */
mode: TransferMode;
/** Non-agent target kind. */
targetType: TransferTargetType.HUMAN | TransferTargetType.WORKFLOW;
/** Agent identity cannot be supplied for a non-agent target. */
targetAgentDetails?: never;
};

// ---------------------------------------------------------------------------
// Scope detail types
Expand Down Expand Up @@ -455,6 +494,8 @@ export interface ToolCallDetails {
toolType?: string;
/** Endpoint the tool is hosted on. */
endpoint?: ServiceEndpoint;
/** Optional explicit transfer performed by this tool call. */
transferDetails?: TransferDetails;
}

/** Details for an inference call. */
Expand Down
Loading
Loading