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
15 changes: 15 additions & 0 deletions .changeset/otel-byok-cookout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@a2a-wrapper/core": patch
"a2a-copilot": patch
"a2a-claude": patch
"a2a-codex": patch
"a2a-opencode": patch
"a2a-antigravity": patch
---

feat(otel): BYOK cookout + shared mock-ollama; wire CLI OTel bootstrap

- examples/mock-ollama/ — reusable fake Ollama (OpenAI + Anthropic) for any cookout
- examples/otel-stack/cookout-byok.mjs layers Phoenix/OTel on that stub
- Hand-rolled wrapper CLIs now call bootstrapOtelSdkFromConfig / shutdownOtelSdk
- CLI bootstrap uses SimpleSpanProcessor for reliable short-lived export
10 changes: 10 additions & 0 deletions .changeset/otel-core-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@a2a-wrapper/core": minor
---

feat(otel): optional OpenTelemetry task spans via `@opentelemetry/api`

- Optional peer `@opentelemetry/api` (no SDK/exporter in core)
- `instrumentExecutor` in `createA2AServer` for `a2a.task.execute` / cancel
- `AgentEventEmitter` respects emission policy (no duplicate tool spans when backend OTel is on)
- Exports: `setOtelTracer`, `withSpan`, `extractA2ATraceContext`, `getW3cTraceContext`, `resolveOtelEmissionPolicy`, …
13 changes: 13 additions & 0 deletions .changeset/otel-per-wrapper-schema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@a2a-wrapper/core": patch
"a2a-copilot": patch
"a2a-claude": patch
"a2a-codex": patch
"a2a-opencode": patch
"a2a-antigravity": patch
---

fix(otel): per-wrapper OTel schema — common at top, backend knobs only for that package

OpenCode no longer advertises `otel.backend.codex` (and peers). Shared flags stay
on `otel.*`; only `otel.backend.<this-wrapper>` appears in each agent schema.
15 changes: 15 additions & 0 deletions .changeset/otel-phase-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@a2a-wrapper/core": minor
"a2a-copilot": patch
"a2a-claude": patch
"a2a-codex": patch
"a2a-opencode": patch
"a2a-antigravity": patch
---

feat(otel): Phase 2 usage mapping + shared execution observability

- Hook E: `applyUsageSummaryToActiveSpan` with anti-dupe policy — `a2a.task.usage.*` rollup always; `gen_ai.usage.*` only when backend OTel is off
- Hook B: `createExecutionObservability` adopted by all wrappers
- Hook C: `annotateSessionOnTaskSpan`; W3C `injectW3cTraceHeaders` for outbound calls
- Docs: usage double-count rules in `docs/observability.md`
13 changes: 13 additions & 0 deletions .changeset/otel-phase-3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@a2a-wrapper/core": minor
"a2a-copilot": minor
"a2a-claude": minor
"a2a-codex": minor
---

feat(otel): Phase 3 backend passthrough (Hook F)

- Core helpers: Copilot telemetry/onGetTraceContext, Claude OTel env + TRACEPARENT, Codex otel overrides
- `withSpan` activates real OTel context so parent-link works
- Shared `otel.exporter` endpoint inheritance
- Wire Copilot / Claude / Codex; OpenCode/Antigravity documented
10 changes: 10 additions & 0 deletions .changeset/otel-phase-4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@a2a-wrapper/core": minor
---

feat(otel): Phase 4 CLI bootstrap, span links, Jaeger demo stack

- `bootstrapOtelSdkFromConfig` / `shutdownOtelSdk` via dynamic import in `createCli`
- Span links on continue/retry invocations
- Optional peers for sdk-node + OTLP HTTP exporter
- `examples/otel-stack` docker-compose (Jaeger OTLP)
12 changes: 12 additions & 0 deletions .changeset/otel-phase-5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@a2a-wrapper/core": minor
"a2a-claude": minor
"a2a-codex": minor
---

feat(otel): Phase 5 attribute ownership + gateway ID mapping

- docs/otel-attribute-ownership.md aligned with draft GenAI A2A #254
- Span attrs: gen_ai.operation.name, a2a.method.name, session.id, a2a.gateway.*
- extractA2ATraceContext reads conversation/session/passkey/spectrum/ticket metadata
- Claude + Codex applyUsageSummaryToActiveSpan + x-usage on completed turns
17 changes: 17 additions & 0 deletions .changeset/otel-phoenix-identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@a2a-wrapper/core": minor
"a2a-claude": minor
"a2a-codex": minor
"a2a-copilot": minor
"a2a-opencode": minor
"a2a-antigravity": minor
---

feat(otel): Phoenix demo, package identity attrs, usage flags

- Replace Jaeger compose requirement with Arize Phoenix (`phoenix serve`, optional Docker)
- CLI bootstrap defaults to `http/protobuf` via `@opentelemetry/exporter-trace-otlp-proto` (Phoenix rejects JSON)
- Stamp `a2a.wrapper.core.version`, `a2a.wrapper.sdk`, `a2a.wrapper.sdk.version` on task spans
- Wire all wrapper servers with package name/version
- `otel.taskUsageRollup` (default true) + `otel.annotateUsageCalls` (default false; Claude/Codex per-call events)
- Smoke scripts: `examples/otel-stack/smoke-export.mjs` + `smoke-providers.mjs` (copilot/claude executor → Phoenix)
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ Wrappers run coding agents behind an HTTP endpoint. Read the [Security Guide](do

## Roadmap

- **OpenTelemetry observability** — optional distributed tracing/metrics for task lifecycle and HTTP requests, via `@opentelemetry/api` (no-op by default, zero cost unless a host app registers a real SDK/exporter). Not yet implemented.
- **OpenTelemetry observability** — optional distributed tracing (`a2a.task.execute` + backend passthrough). Optional peers for `@opentelemetry/api` / SDK; CLI auto-bootstraps when `otel.exporter.endpoint` is set. Lightweight Phoenix demo (no Docker): [`examples/otel-stack`](examples/otel-stack). Guides: [Observability](docs/observability.md), [attribute ownership](docs/otel-attribute-ownership.md).

## Contributing

Expand Down
65 changes: 65 additions & 0 deletions a2a-antigravity/schemas/agent-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@
"memory": {
"$ref": "#/definitions/MemoryConfig"
},
"otel": {
"$ref": "#/definitions/OtelAntigravityConfig",
"description": "Wrapper-level OTel only (no Hook F backend passthrough yet)."
},
"server": {
"$ref": "#/definitions/ServerConfig"
},
Expand Down Expand Up @@ -538,6 +542,67 @@
},
"type": "object"
},
"OtelAntigravityConfig": {
"$ref": "#/definitions/OtelCommonConfig",
"description": "`a2a-antigravity` agent `otel` — common settings only (no Hook F backend passthrough yet)."
},
"OtelCommonConfig": {
"additionalProperties": false,
"description": "Common wrapper-level OTel settings — shared by every `a2a-*` package.\n\nThese are **not** provider-specific. Put only Hook F / vendor knobs under {@link OtelConfig.backend } (and each wrapper's schema only exposes *its* backend key — see {@link OtelCopilotConfig } , {@link OtelClaudeConfig } , …).",
"properties": {
"annotateUsageCalls": {
"default": false,
"description": "Emit per-LLM-call span **events** (`a2a.llm.call`) on the task span when the wrapper is the sole OTel token source. Off by default (noisy).",
"type": "boolean"
},
"emitOverlappingBackendSpans": {
"default": false,
"description": "Unsafe escape hatch: allow wrapper tool/LLM spans even when backend OTel is configured (duplicates Copilot/Claude tool spans in the collector).",
"type": "boolean"
},
"enabled": {
"default": false,
"description": "Master switch for wrapper-owned OTel instrumentation.",
"type": "boolean"
},
"exporter": {
"additionalProperties": false,
"description": "Shared OTLP exporter hint for wrapper hosts and backend passthrough. Backends inherit `endpoint` / `protocol` when their own fields are omitted.",
"properties": {
"endpoint": {
"description": "e.g. `http://127.0.0.1:6006` (Phoenix) or `http://127.0.0.1:4318`",
"type": "string"
},
"protocol": {
"description": "OTLP encoding for CLI bootstrap.\n- `http/protobuf` (default) — `@opentelemetry/exporter-trace-otlp-proto` (Phoenix accepts only protobuf over HTTP)\n- `http/json` — `@opentelemetry/exporter-trace-otlp-http`",
"type": "string"
}
},
"type": "object"
},
"mirrorAgentEvents": {
"default": false,
"description": "When true, map `AgentEvent` tool calls into wrapper OTel child spans. Defaults to false. Forced off when a backend OTel block is present unless {@link emitOverlappingBackendSpans } is true.",
"type": "boolean"
},
"serviceName": {
"default": "derived from agent card / wrapper name",
"description": "Service name resource attribute for wrapper spans.",
"type": "string"
},
"taskUsageRollup": {
"default": true,
"description": "Stamp per-request usage rollups (`a2a.task.usage.*`, and `gen_ai.usage.*` when the wrapper is the sole OTel source) via {@link applyUsageSummaryToActiveSpan } .\n\nDefault `true` when `enabled`. Sideband `x-usage` is unaffected. Set `false` only to debug de-dupe / cardinality — rarely needed because the whole bridge is already a no-op when `enabled` is false.",
"type": "boolean"
},
"tracerName": {
"default": "a2a-wrapper",
"description": "Instrumentation scope name passed to `getTracer`.",
"type": "string"
}
},
"type": "object"
},
"ServerConfig": {
"additionalProperties": false,
"properties": {
Expand Down
26 changes: 10 additions & 16 deletions a2a-antigravity/src/antigravity/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import { existsSync, statSync } from "node:fs";
import type { AgentExecutor, ExecutionEventBus, RequestContext } from "@a2a-js/sdk/server";
import { v4 as uuidv4 } from "uuid";

import type { AgentConfig, McpStdioServerConfig } from "../config/types.js";
import { BridgeClient } from "./bridge-client.js";
Expand All @@ -13,7 +12,6 @@ import { EventMapper, sanitizeMessage, usageToCallRecord } from "./event-mapper.
import { validateMcpServers, toAntigravityMcpEntry } from "./mcp-adapter.js";
import { ANTIGRAVITY_BACKEND_PATHS } from "./backend-paths.js";
import {
AgentEventEmitter,
bootstrapSubAgents,
extractUserText,
LlmUsageAccumulator,
Expand All @@ -24,7 +22,8 @@ import {
publishStreamingChunk,
publishTask,
publishTraceArtifact,
resolveTransport,
createExecutionObservability,
applyUsageSummaryToActiveSpan,
} from "@a2a-wrapper/core";
import type {
EventTransport,
Expand Down Expand Up @@ -114,19 +113,12 @@ export class AntigravityExecutor implements AgentExecutor {
const { taskId, contextId, userMessage, task } = ctx;
await this.initialize();

const agentId = this.config.agentCard.name.toLowerCase().replace(/\s+/g, "-");
const transport = resolveTransport(
this.config.events,
bus,
taskId,
contextId,
this.customTransport,
);
const emitter = new AgentEventEmitter({
agentId,
const { emitter } = createExecutionObservability({
agentName: this.config.agentCard.name,
traceId: contextId || uuidv4(),
transport,
ctx,
bus,
events: this.config.events,
customTransport: this.customTransport,
});
const mapper = new EventMapper(emitter, this.config);
const accumulator = new LlmUsageAccumulator();
Expand Down Expand Up @@ -222,14 +214,16 @@ export class AntigravityExecutor implements AgentExecutor {
} else {
publishFinalArtifact(bus, taskId, contextId, finalText);
}
const usageSummary = accumulator.summary();
applyUsageSummaryToActiveSpan(usageSummary);
publishStatus(
bus,
taskId,
contextId,
"completed",
undefined,
true,
{ "x-usage": accumulator.summary() },
{ "x-usage": usageSummary },
);
bus.finished();
return;
Expand Down
8 changes: 7 additions & 1 deletion a2a-antigravity/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { createRequire } from "node:module";
import { dirname, resolve } from "node:path";
import { parseArgs } from "node:util";
import { Logger } from "@a2a-wrapper/core";
import { Logger, bootstrapOtelSdkFromConfig, shutdownOtelSdk } from "@a2a-wrapper/core";
import { resolveConfig } from "./config/loader.js";
import type { AgentConfig, AntigravityConfig } from "./config/types.js";
import { createA2AServer } from "./server/index.js";
Expand Down Expand Up @@ -211,6 +211,10 @@ async function main(): Promise<void> {
authMode: config.antigravity.provider?.authMode ?? "sdkDefault",
});

if (await bootstrapOtelSdkFromConfig(config.otel)) {
log.info("OpenTelemetry SDK bootstrapped from otel.exporter.endpoint");
}

const handle = await createA2AServer(config);
let shuttingDown = false;

Expand All @@ -220,6 +224,8 @@ async function main(): Promise<void> {
log.info(`${signal} received, shutting down...`);
try {
await handle.shutdown();
await shutdownOtelSdk();
await new Promise((r) => setTimeout(r, 300));
} catch (err) {
log.error("Shutdown failed", { error: (err as Error).message, stack: (err as Error).stack });
process.exit(1);
Expand Down
1 change: 1 addition & 0 deletions a2a-antigravity/src/config/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,6 @@ export const DEFAULTS: Required<AgentConfig> = {
},
memory: undefined as unknown as Required<AgentConfig>["memory"],
subAgents: undefined as unknown as Required<AgentConfig>["subAgents"],
otel: undefined as unknown as Required<AgentConfig>["otel"],
configDir: process.cwd(),
};
4 changes: 4 additions & 0 deletions a2a-antigravity/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
AgentCardConfig,
EventsConfig,
MemoryConfig,
OtelAntigravityConfig,
SubAgentsConfig,
} from "@a2a-wrapper/core";

Expand Down Expand Up @@ -181,6 +182,9 @@ export interface AgentConfig {
events?: EventsConfig;
memory?: MemoryConfig;
subAgents?: SubAgentsConfig;
/** Optional OpenTelemetry settings (see docs/observability.md). */
/** Wrapper-level OTel only (no Hook F backend passthrough yet). */
otel?: OtelAntigravityConfig;
/** Directory containing the loaded config file; set by CLI/loader. */
configDir?: string;
}
6 changes: 6 additions & 0 deletions a2a-antigravity/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* antigravity-specific executor and startup banner.
*/

import { createRequire } from "node:module";
import { createA2AServer as coreCreateA2AServer } from "@a2a-wrapper/core";
import type { ServerHandle as CoreServerHandle } from "@a2a-wrapper/core";

Expand All @@ -15,6 +16,9 @@ import { AntigravityExecutor } from "../antigravity/executor.js";
import { logger } from "../utils/logger.js";

const log = logger.child("server");
const { name: WRAPPER_SDK, version: WRAPPER_SDK_VERSION } = createRequire(import.meta.url)(
"../../package.json",
) as { name: string; version: string };

export interface ServerHandle extends Omit<CoreServerHandle, "executor"> {
executor: AntigravityExecutor;
Expand All @@ -23,6 +27,8 @@ export interface ServerHandle extends Omit<CoreServerHandle, "executor"> {
export async function createA2AServer(config: Required<AgentConfig>): Promise<ServerHandle> {
const handle = await coreCreateA2AServer<Required<AgentConfig>>(config, (cfg) => new AntigravityExecutor(cfg), {
protocolVersion: "1.0",
wrapperName: WRAPPER_SDK,
wrapperVersion: WRAPPER_SDK_VERSION,
onListening: ({ port, hostname, advertiseHost, advertiseProtocol }) => {
log.info("A2A server started", {
bind: hostname,
Expand Down
2 changes: 2 additions & 0 deletions a2a-claude/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"clean": "rm -rf dist",
"rebuild": "npm run clean && npm run build",
"typecheck": "tsc --noEmit",
"schema": "ts-json-schema-generator --path src/config/types.ts --type AgentConfig --tsconfig tsconfig.json --no-type-check --out schemas/agent-config.schema.json",
"test": "vitest --run"
},
"files": [
Expand Down Expand Up @@ -62,6 +63,7 @@
"@types/express": "^4.17.21",
"@types/node": "^20.10.0",
"@types/uuid": "^9.0.7",
"ts-json-schema-generator": "^2.9.0",
"tsx": "^4.6.0",
"typescript": "^5.3.0",
"vitest": "^1.0.0"
Expand Down
Loading
Loading