Trace agents built directly on the OpenAI, Anthropic or Google Gen AI SDKs (Python or TypeScript, no agent framework) with Maple: official OTel GenAI instrumentations in Python, a small span helper in TypeScript, plus invoke_agent/execute_tool spans and gen_ai.conversation.id so each conversation is one Agent Session. Triggers on 'trace my openai agent', 'add Maple to my anthropic agent', 'agent sessions for gemini', 'OpenTelemetry for the openai sdk'.
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Goal: every conversation = one Maple Agent Session. Each user message = one turn = one trace rooted at an invoke_agent <agent> span, containing a chat <model> / generate_content <model> span per model call (transcript + tokens) and an execute_tool <tool> span per tool call (args, result, failures).
Mechanism:
opentelemetry-instrumentation-genai-openai, -genai-anthropic, opentelemetry-instrumentation-google-genai, all >= 1.2b0) write chat spans with GenAI semconv on span attributes.@opentelemetry/instrumentation-openai only patches openai < 7 and puts messages in log events; nothing official for @anthropic-ai/sdk / @google/genai). Record the model call with the helper in references/typescript.md.invoke_agent span (with gen_ai.conversation.id) and execute_tool spans.openai-agents/@openai/agents, langchain*, langgraph, pydantic-ai*, crewai, llama-index*, ai (Vercel), @mastra/core, google-adk, strands-agents, smolagents, agno, dspy, haystack-ai, agent-framework, litellm. If one is present, stop and use that framework's skill (npx skills add MapleTechLabs/maple/skills --skill maple-agent-tracing -y routes). Instrumenting the provider SDK under a framework double-records every call.openai < 4 (tested 3.20.0), anthropic < 2 (tested 1.8.0), google-genai < 3 (tested 2.25.0). Outside these ranges the instrumentation won't patch: tell the user.openai 7.x (tested 7.23.0). Anthropic / Gemini: adapt the helper (see reference).TracerProvider(, set_tracer_provider, NodeSDK(, NodeTracerProvider, registerOTel, opentelemetry-instrument, logfire.configure, sentry_sdk.init / Sentry.init, Traceloop.init.
BatchSpanProcessor(OTLPSpanExporter()) to it; do NOT create a second provider.opentelemetry-instrumentation-openai / -anthropic (OpenLLMetry, NOT the official ones), opentelemetry-instrumentation-openai-v2 (deprecated), openinference-instrumentation-*, @arizeai/openinference-*, @traceloop/*, logfire.instrument_openai/anthropic, Sentry OpenAI/Anthropic integrations, langfuse.openai. Keep exactly one; ask before removing one that serves something else.https://ingest.maple.dev. EU: https://ingest.eu.maple.dev.Authorization=Bearer <key>.MAPLE_TEST (ingest accepts and discards it) and tell the user to replace it with their key from Settings → Ingestion.maple_sk_ key in browser code..env, settings module, secret manager) if it has one. Otherwise inline is acceptable: ingest keys are write-only.Env vars (the exporter reads them and appends /v1/traces):
OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.maple.dev
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer <key>"
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_ONLY.env (dotenv, load_dotenv(), --env-file), load it at the top of the init module, before the provider is built; otherwise the exporter silently targets localhost:4318 with no key.MAPLE_INGEST_KEY is not set; Maple telemetry export is disabled) and skip the Maple exporter so the app runs normally. Never throw or exit over the key, and never let it become Bearer undefined (opaque 401) or a bare KeyError on import. Or inline the key when the repo has no env convention.Read the reference for the service's language and apply it exactly:
references/python.md (install, tracing.py, agent_tracing.py helper, loop).references/typescript.md (install, instrumentation.ts, agent-tracing.ts helper incl. tracedChat, loop, Anthropic/Gemini mapping).Rules for both:
service.name (never unknown_service).OTEL_SEMCONV_STABILITY_OPT_IN; the 1.x GenAI packages don't need it.agent_span(<agent_name>, conversation_id) / agentSpan(...). It sets gen_ai.operation.name=invoke_agent, gen_ai.agent.name, gen_ai.conversation.id.conversation_id = the app's stable conversation/thread/ticket id from the request. Same for every turn of one conversation, different across conversations. No uuid4() per request, no process-wide constant, no module-level default.conversation=: the instrumentation also stamps gen_ai.conversation.id=conv_... on the model span. Pass that same id to agent_span.session.id or maple_ai.session.id: Maple reads gen_ai.conversation.id for these spans.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_ONLY puts gen_ai.input.messages, gen_ai.output.messages, gen_ai.system_instructions, gen_ai.tool.definitions on span attributes. The helpers read the same variable for tool args/results (and, in TS, messages).NO_CONTENT (default, empty transcript), SPAN_ONLY (use), EVENT_ONLY (logs only, Maple shows nothing), SPAN_AND_EVENT (also works; duplicates into logs). Legacy true = events: wrong.redaction/transform processor. Never lower OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT / OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT (truncated JSON is dropped).run_tool(call_id, name, arguments_json, fn) / runTool(...), passing the model's tool call id. It catches the exception, marks the span failed (status ERROR + error.type + recorded exception) and returns {"error": ...} to the model.
run_tool (or set status + error.type where it catches). A caught, unmarked failure shows as success.tool_use block → run_tool(block.id, block.name, json.dumps(block.input), fn); return tool_result blocks.execute_tool spans itself; do NOT also wrap tools. With AFC disabled, use run_tool with function_call.id (or name if id is empty).run_tool, and inside wrap its loop in agent_span("<distinct_name>") WITHOUT a conversation id (it's in the caller's trace). Every agent gets a unique gen_ai.agent.name; same names merge lanes.asyncio.gather / Promise.all keep context. ThreadPoolExecutor: submit contextvars.copy_context().run(fn, ...), or tool spans become orphan traces.stream_options={"include_usage": True} (Python) or use the helper's onText path (TS sets it). Otherwise the streamed call has 0 tokens. The final usage chunk has empty choices; skip it when reading text (if chunk.choices).usage.cost (USD) to gen_ai.usage.cost; direct OpenAI sends none. Don't add pricing tables.openai SDK → spans say gen_ai.provider.name=openai with OpenAI-shaped usage. Correct; don't override it.provider.force_flush() after each turn in Lambda/Cloud Functions/Cloud Run jobs, notebooks, Celery/RQ tasks, and anything ending in os._exit. Scripts/CLIs: provider.shutdown() in finally.await sdk.shutdown() before a CLI/script exits. Serverless: await spanProcessor.forceFlush() before returning (or in waitUntil after a streamed response). Both reject when an export failed: add .catch((err) => console.error("telemetry flush failed", err)) so a Maple outage can't crash the app. Long-running server: flush on SIGTERM, nothing per request.If the app has no scriptable entry point (server, REPL, UI only), write a small driver for this run: one conversation id, 2+ turns, at least one tool call, flush before exit.
Run one real conversation: 3+ user messages with the same id, one tool call, one streamed message if the app streams, one failing tool if you can trigger it, one sub-agent call if the app delegates; then a second conversation with a different id. Flush. In Maple → Agent Sessions (filter by service; allow ~30 s):
trace:<id> sessions.invoke_agent <agent>, with all chat/generate_content and execute_tool spans inside it.SPAN_ONLY).usage.cost).Bearer , sk- or maple_sk_.With the Maple MCP: list_agent_sessions with search=<conversation id> returns one row.
Check without Maple access: the run exits with no export errors on stderr (Failed to export, OTLPExporterError, 401 lines) AND a temporary console exporter (SimpleSpanProcessor(ConsoleSpanExporter())) shows the invoke_agent span with gen_ai.conversation.id and model spans with gen_ai.input.messages and gen_ai.usage.*. Silence alone proves nothing: no spans also looks silent.
messages.stream().agent_span, or the span ended before the call (e.g. a stream consumed after it closed).EVENT_ONLY or legacy true; set SPAN_ONLY in the process that makes the calls..instrument() ran after the first request, or the OpenLLMetry package (opentelemetry-instrumentation-openai) was installed instead of -genai-openai.opentelemetry-instrument loads every installed instrumentation package, so uninstall extras rather than just not calling them.stream_options.include_usage.run_tool without status/error.type.gen_ai.agent.name, or never wrapped.ingest_unauthorized / "Invalid ingest key") with a key you trust → keys are region-bound; it usually belongs to the other region. Try the other endpoint.-genai- packages.opentelemetry-instrumentation-openai / -anthropic (OpenLLMetry) or the deprecated -openai-v2; install the -genai- packages.MapleTechLabs/maple
f1305bd
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.