You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Forge emits a partial OTel GenAI attribute set today, and — most importantly — it publishes tool telemetry under proprietary forge.tool.* keys instead of the standardized gen_ai.tool.* semantic conventions. GenAI-native backends (Datadog LLM Observability, Honeycomb, Grafana/Tempo, Arize Phoenix, Langfuse) key their agent/tool dashboards off gen_ai.*, so Forge tool spans don't light up there even though the data is already on the span under a different name.
This issue tracks closing the gap — tool attributes first — and bumping the pinned semconv so the newer GenAI keys are available.
Source of truth for the target set: OTel semantic conventions, GenAI + MCP registries (see the attribute tables the reporter attached).
Current state (what Forge emits)
Emitted in forge-core/runtime/loop.go, keys centralized in forge-core/observability/attrs.go:
Span
Attribute
Notes
agent.execute, llm.completion
gen_ai.system
vendor; deprecated key — semconv renamed this to gen_ai.provider.name
declared in attrs.go:41 but never set (no emit site)
Pinned semconv: go.opentelemetry.io/otel/semconv/v1.26.0 + a comment "pinned to OTel semconv 1.26.0 GenAI" in attrs.go. Several target attributes below are from newer semconv and require a version bump.
Primary scope — tool-related gen_ai.tool.* (the emphasis)
Forge already has all the underlying data; this is mostly a re-key + a few additions on the tool.<name> span (loop.go:727). ToolCall carries ID, Type ("function"), Function.Name, Function.Arguments (forge-core/llm/types.go:26); ToolDefinition carries Type + Function schema (name, description, parameters).
semconv attribute
type
Forge status
Data source
gen_ai.operation.name = execute_tool
string
missing
constant — set on the tool span
gen_ai.tool.name
string
have as forge.tool.name
tc.Function.Name
gen_ai.tool.call.id
string
missing
tc.ID (already on the struct, threaded back to the LLM)
gen_ai.tool.call.arguments
any (JSON)
have as forge.tool.args (opt-in)
tc.Function.Arguments
gen_ai.tool.call.result
any (JSON)
have as forge.tool.result (opt-in)
tool result
gen_ai.tool.type
string
missing
tc.Type ("function"); MCP tools → extension
gen_ai.tool.description
string
missing
ToolDefinition.Function.Description
gen_ai.tool.definitions
any (JSON)
missing
toolDefs at agent.execute / llm.completion (opt-in — can be large)
Also consider the semconv span-naming convention for tool execution: execute_tool <tool.name> (paired with gen_ai.operation.name=execute_tool). Forge currently names it tool.<name>. Renaming is a breaking change for existing dashboards — see "dual-emit" below.
MCP tool spans (mcp.* namespace)
Forge is an MCP client, and MCP tool calls surface as <server>__<tool>. MCP tool spans should additionally carry:
mcp.method.name = tools/call
mcp.session.id, mcp.protocol.version
and set gen_ai.tool.type = extension for MCP-backed tools
This dovetails with the MCP audit events Forge already emits (docs/mcp/audit-events.md) — same join key story as the existing gen_ai/audit correlation.
Also in scope — agent & conversation identity
These are cheap (all data already in hand at agent.execute :222) and high-value for grouping traces per agent/session, so they land with the tool work rather than in the deferred pile. Stamp on the agent.execute span:
semconv attribute
type
Forge status
Data source
gen_ai.agent.id
string
missing (only forge.agent.id today)
agent_id from forge.yaml
gen_ai.agent.name
string
missing
agent name from forge.yaml
gen_ai.agent.version
string
missing
agent version from forge.yaml (also service.version)
gen_ai.conversation.id
string
missing
Forge session id = A2A task.ID (the session-store key that persists across turns — .forge/sessions/<task>.json)
Note per semconv: gen_ai.conversation.id should only be set when a real conversation/thread identifier is available (which Forge's session id is) — never a synthesized UUID or trace id as a fallback.
Secondary scope — other GenAI gaps (lower priority)
Grouped; all have a ready data source in Forge:
Operation / provider identity
gen_ai.operation.name = chat on llm.completion (foundational; currently absent)
gen_ai.provider.name — the current key that supersedes the deprecated gen_ai.system (migrate; keep both for one release)
gen_ai.response.model — wire up the already-declared constant (attrs.go:41)
gen_ai.response.id — provider completion id (needs threading from provider responses)
Request params (available in ChatRequest: Temperature, MaxTokens, Stream)
Semconv bump.provider.name, usage.cache_*, usage.reasoning.output_tokens, tool.definitions, tool.call.* come from semconv newer than the pinned 1.26.0. Bump forge-core/observability/otel.go (resource semconv/vX) and the attrs.go constants together — it's a single-file mechanical sweep by design (per the attrs.go header comment).
Replace forge.tool.* outright. These keys are not used in production yet, so drop them and emit gen_ai.tool.* as a straight replacement — no dual-emit window needed. (The gen_ai.system → gen_ai.provider.name rename is separate: gen_ai.system has shipped, so keep it as a deprecated alias for one release.)
Content-capture posture.tool.call.arguments, tool.call.result, tool.definitions, system_instructions, input/output.messages are all flagged sensitive by semconv. Keep them behind the existing CaptureContent + Redact + 4 KiB PrepareSpanContent pipeline. Consider honoring the standard OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT env var as an alias for CaptureContent.
Structured vs string. semconv prefers structured values for *.messages / tool.* and allows a JSON string on spans when structured isn't supported. Forge already emits JSON strings; acceptable, but note it for a future events-based emission.
gen_ai.provider.name emitted; gen_ai.system retained one release as a deprecated alias.
gen_ai.response.model (existing constant) is actually set; gen_ai.operation.name=chat set on llm.completion.
Pinned semconv version bumped; attrs.go constants updated in one sweep.
forge.tool.* removed and replaced by gen_ai.tool.* (not yet used in production — no dual-emit needed). Update any attrs.go constants and tests accordingly.
Content attributes stay behind CaptureContent+Redact; no secrets by default.
docs/core-concepts/observability-tracing.md updated with the full attribute table; unit tests assert the new keys on tool/LLM spans.
Summary
Forge emits a partial OTel GenAI attribute set today, and — most importantly — it publishes tool telemetry under proprietary
forge.tool.*keys instead of the standardizedgen_ai.tool.*semantic conventions. GenAI-native backends (Datadog LLM Observability, Honeycomb, Grafana/Tempo, Arize Phoenix, Langfuse) key their agent/tool dashboards offgen_ai.*, so Forge tool spans don't light up there even though the data is already on the span under a different name.This issue tracks closing the gap — tool attributes first — and bumping the pinned semconv so the newer GenAI keys are available.
Source of truth for the target set: OTel semantic conventions, GenAI + MCP registries (see the attribute tables the reporter attached).
Current state (what Forge emits)
Emitted in
forge-core/runtime/loop.go, keys centralized inforge-core/observability/attrs.go:agent.execute,llm.completiongen_ai.systemgen_ai.provider.nameagent.execute,llm.completiongen_ai.request.modelllm.completiongen_ai.usage.input_tokens/gen_ai.usage.output_tokensllm.completiongen_ai.response.finish_reasonsllm.completiongen_ai.input.messages/gen_ai.output.messagesCaptureContent), emitted as JSON stringtool.<name>forge.tool.name/forge.tool.error/forge.tool.args/forge.tool.resultgen_ai.tool.*gen_ai.response.modelattrs.go:41but never set (no emit site)Pinned semconv:
go.opentelemetry.io/otel/semconv/v1.26.0+ a comment "pinned to OTel semconv 1.26.0 GenAI" inattrs.go. Several target attributes below are from newer semconv and require a version bump.Primary scope — tool-related
gen_ai.tool.*(the emphasis)Forge already has all the underlying data; this is mostly a re-key + a few additions on the
tool.<name>span (loop.go:727).ToolCallcarriesID,Type("function"),Function.Name,Function.Arguments(forge-core/llm/types.go:26);ToolDefinitioncarriesType+Functionschema (name, description, parameters).gen_ai.operation.name=execute_toolgen_ai.tool.nameforge.tool.nametc.Function.Namegen_ai.tool.call.idtc.ID(already on the struct, threaded back to the LLM)gen_ai.tool.call.argumentsforge.tool.args(opt-in)tc.Function.Argumentsgen_ai.tool.call.resultforge.tool.result(opt-in)resultgen_ai.tool.typetc.Type("function"); MCP tools →extensiongen_ai.tool.descriptionToolDefinition.Function.Descriptiongen_ai.tool.definitionstoolDefsatagent.execute/llm.completion(opt-in — can be large)Also consider the semconv span-naming convention for tool execution:
execute_tool <tool.name>(paired withgen_ai.operation.name=execute_tool). Forge currently names ittool.<name>. Renaming is a breaking change for existing dashboards — see "dual-emit" below.MCP tool spans (
mcp.*namespace)Forge is an MCP client, and MCP tool calls surface as
<server>__<tool>. MCP tool spans should additionally carry:mcp.method.name=tools/callmcp.session.id,mcp.protocol.versiongen_ai.tool.type=extensionfor MCP-backed toolsThis dovetails with the MCP audit events Forge already emits (
docs/mcp/audit-events.md) — same join key story as the existing gen_ai/audit correlation.Also in scope — agent & conversation identity
These are cheap (all data already in hand at
agent.execute:222) and high-value for grouping traces per agent/session, so they land with the tool work rather than in the deferred pile. Stamp on theagent.executespan:gen_ai.agent.idforge.agent.idtoday)agent_idfromforge.yamlgen_ai.agent.namenamefromforge.yamlgen_ai.agent.versionversionfromforge.yaml(alsoservice.version)gen_ai.conversation.idtask.ID(the session-store key that persists across turns —.forge/sessions/<task>.json)Note per semconv:
gen_ai.conversation.idshould only be set when a real conversation/thread identifier is available (which Forge's session id is) — never a synthesized UUID or trace id as a fallback.Secondary scope — other GenAI gaps (lower priority)
Grouped; all have a ready data source in Forge:
gen_ai.operation.name=chatonllm.completion(foundational; currently absent)gen_ai.provider.name— the current key that supersedes the deprecatedgen_ai.system(migrate; keep both for one release)gen_ai.response.model— wire up the already-declared constant (attrs.go:41)gen_ai.response.id— provider completion id (needs threading from provider responses)ChatRequest:Temperature,MaxTokens,Stream)gen_ai.request.max_tokens,gen_ai.request.temperature,gen_ai.request.streamgen_ai.usage.cache_read.input_tokens,gen_ai.usage.cache_write.input_tokensgen_ai.usage.reasoning.output_tokensllm.Usage)gen_ai.system_instructions— Forge hassystemPrompt(today folded intoinput.messages); split it out per semconvgen_ai.operation.name=embeddings/search_memory,gen_ai.embeddings.dimension.count,gen_ai.memory.*,gen_ai.retrieval.*Design considerations
provider.name,usage.cache_*,usage.reasoning.output_tokens,tool.definitions,tool.call.*come from semconv newer than the pinned 1.26.0. Bumpforge-core/observability/otel.go(resourcesemconv/vX) and theattrs.goconstants together — it's a single-file mechanical sweep by design (per theattrs.goheader comment).forge.tool.*outright. These keys are not used in production yet, so drop them and emitgen_ai.tool.*as a straight replacement — no dual-emit window needed. (Thegen_ai.system→gen_ai.provider.namerename is separate:gen_ai.systemhas shipped, so keep it as a deprecated alias for one release.)tool.call.arguments,tool.call.result,tool.definitions,system_instructions,input/output.messagesare all flagged sensitive by semconv. Keep them behind the existingCaptureContent+Redact+ 4 KiBPrepareSpanContentpipeline. Consider honoring the standardOTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENTenv var as an alias forCaptureContent.*.messages/tool.*and allows a JSON string on spans when structured isn't supported. Forge already emits JSON strings; acceptable, but note it for a future events-based emission.Acceptance criteria
tool.<name>spans emitgen_ai.operation.name=execute_tool,gen_ai.tool.name,gen_ai.tool.call.id,gen_ai.tool.type, and (opt-in)gen_ai.tool.call.arguments/gen_ai.tool.call.result/gen_ai.tool.description.mcp.method.name=tools/call,mcp.session.id,mcp.protocol.version, andgen_ai.tool.type=extension.gen_ai.tool.definitionsemitted (opt-in) on the LLM/agent span.agent.executespan emitsgen_ai.agent.id,gen_ai.agent.name,gen_ai.agent.version(fromforge.yaml), andgen_ai.conversation.id(Forge session/task.ID).gen_ai.provider.nameemitted;gen_ai.systemretained one release as a deprecated alias.gen_ai.response.model(existing constant) is actually set;gen_ai.operation.name=chatset onllm.completion.attrs.goconstants updated in one sweep.forge.tool.*removed and replaced bygen_ai.tool.*(not yet used in production — no dual-emit needed). Update anyattrs.goconstants and tests accordingly.CaptureContent+Redact; no secrets by default.docs/core-concepts/observability-tracing.mdupdated with the full attribute table; unit tests assert the new keys on tool/LLM spans.References
forge-core/runtime/loop.go(agent.execute:222,llm.completion:425,tool.<name>:727)forge-core/observability/attrs.goforge-core/observability/otel.goforge-core/llm/types.go(ToolCall:26,ToolDefinition:39,ChatRequest)forge-core/mcp/,docs/mcp/audit-events.mdcore-concepts/observability-tracingdoc (Docs sidebar drift: 19 synced pages orphaned from navigation (incl. entire MCP section) useforge.ai#26)Reviewed the current OTel wiring; no code changes in this issue — spec + backlog only.