Skip to content

DurableAgent experimental_telemetry does not emit AI SDK spans for LLM or tool calls #1296

Description

@craze3

Summary

@workflow/ai DurableAgent accepts experimental_telemetry, but durable agent turns do not emit the expected AI SDK telemetry spans (ai.streamText, ai.streamText.doStream, ai.toolCall).

In the same app, plain AI SDK streamText() calls emit Langfuse/OpenTelemetry spans correctly. Only the durable workflow path is missing.

Expected behavior

When experimental_telemetry: { isEnabled: true, functionId, metadata } is passed to DurableAgent, the durable execution path should emit the same AI SDK telemetry shape developers get from streamText() / generateText(), including:

  • ai.streamText
  • ai.streamText.doStream
  • ai.toolCall

This should work with standard OpenTelemetry setups such as @vercel/otel + langfuse-vercel.

Actual behavior

DurableAgent stores and forwards telemetry config, but the internal execution path appears to bypass AI SDK telemetry wrappers:

  • DurableAgent stores experimental_telemetry
  • streamTextIterator forwards it into doStreamStep
  • doStreamStep then calls raw model.doStream(callOptions) without applying telemetry
  • durable tool execution also bypasses AI SDK ai.toolCall tracing helpers

Result:

  • plain streamText() spans show up
  • durable workflow turns do not create observations/spans in Langfuse

Reproduction

  1. Next.js app with instrumentation.ts using registerOTel({ traceExporter: new LangfuseExporter() })
  2. Verify a normal streamText() call emits spans in Langfuse
  3. Create a DurableAgent with experimental_telemetry: { isEnabled: true, functionId: 'my-agent' }
  4. Run agent.stream(...) inside a workflow with tool calls
  5. Observe that planner / plain AI SDK traces appear, but durable main-turn spans do not

Why this seems to be an implementation bug

In the published package:

  • durable-agent.js stores telemetry on the agent and forwards it during stream()
  • stream-text-iterator.js passes telemetry into doStreamStep
  • do-stream-step.js performs a raw model.doStream(...) and does not use the telemetry setting
  • executeTool() in durable-agent.js does not create AI SDK-style ai.toolCall spans

So telemetry is exposed in the API surface but not honored by the durable execution path.

What we need

Please make DurableAgent telemetry behavior match AI SDK core behavior.

Acceptable fixes would be:

  1. Route durable LLM execution through the same AI SDK telemetry wrappers used by streamText() / generateText(), or
  2. Add an internal telemetry implementation in @workflow/ai that preserves the same span names/structure/attributes, including ai.toolCall, or
  3. Expose an official helper from AI SDK / Workflow so DurableAgent can reuse the supported telemetry path instead of duplicating logic

Request for regression coverage

Please add a regression test that verifies DurableAgent emits telemetry spans when experimental_telemetry.isEnabled === true, including:

  • outer generation span
  • provider call span
  • tool call span
  • functionId / metadata propagation

Activity

  1. pranaygp commented on Mar 13, 2026

    @pranaygp
    Contributor

    In #1362 we fixed how the options are wired through and added testing to track this gap so we can ensure it actually works correctly going forward

    note that it's not yet actually implemented so I'm keeping this issue open

  2. mattschaller commented on Mar 14, 2026

    @mattschaller

    Following this one too. Once the DurableAgent execution path properly emits spans, you'll likely run into the ai.* vs gen_ai.* attribute naming gap — the SDK uses ai.usage.inputTokens but most OTel backends expect the standard gen_ai.* semantic conventions. I built a vendor-neutral SpanProcessor called ai-sdk-otel-adapter that handles that remapping automatically. Might save some config pain once the spans start flowing.

  3. craze3 commented on Mar 21, 2026

    @craze3
    ContributorAuthor

    Thanks for shipping #1385. I upgraded to the published release that includes it, but the core interoperability issue still seems unresolved for AI SDK telemetry consumers like langfuse-vercel.

    As of March 21, 2026, I am on:

    • @workflow/ai@4.1.0-beta.57
    • workflow@4.2.0-beta.71

    What improved:

    • I now see Workflow/OTel spans such as ai.streamText / ai.streamText.doStream.

    What is still wrong:

    • Langfuse traces are still sparse compared to plain AI SDK streamText.
    • In practice, the spans show up, but prompt/output/tool-call data is mostly missing, so Langfuse renders things like input: undefined / output: undefined instead of the rich trace structure I got before moving to Workflow.

    The important gap seems to be that the released DurableAgent telemetry emits some span names and some model/gen_ai attributes, but not the full AI SDK-style telemetry attributes that downstream exporters expect.

    Specifically, I still do not see emitted attributes equivalent to:

    • ai.prompt.messages
    • ai.prompt.tools
    • ai.prompt.toolChoice
    • ai.response.text
    • ai.response.finishReason
    • ai.response.model
    • ai.usage.*
    • ai.toolCall.result

    Tool spans appear to include args, but not the tool result payload.

    Why this matters:

    • langfuse-vercel does not just key off span names. It reads AI SDK-style attributes like ai.prompt.messages, ai.response.text, and ai.toolCall.result to populate prompt/output/tool rendering.
    • So restoring span creation/names is helpful, but it is not sufficient for parity with the plain AI SDK tracing experience.

    What I think the proper fix is:

    1. DurableAgent / Workflow experimental_telemetry should emit the same AI SDK telemetry schema as regular AI SDK streamText and tool execution, not just similarly named spans.
    2. The stream span should stay open until the stream is fully finished, so final response text, finish reason, and token usage can be attached after the finish event.
    3. Tool-call spans should include both ai.toolCall.args and ai.toolCall.result.

    I do not think this needs a Langfuse-specific integration. AI SDK telemetry parity should be enough for existing OTel exporters/adapters to work again without app-level patches.

    @VaguelySerious @pranaygp can you please reopen the issue? I think this is important. Advanced workflows are impossible to get right without proper tracing.

  4. VaguelySerious commented on Apr 3, 2026

    @VaguelySerious
    Member

    (AI) Reproduction

    Confirmed by code inspection. doStreamStep wrapped only model.doStream() in the telemetry span, so the span closed before the stream was consumed — usage, finish reason, response text, and prompt data were never recorded. executeTool similarly omitted ai.toolCall.result. These gaps explain why Langfuse shows input: undefined / output: undefined despite span names being correct.

    Related issues

    No duplicates found. PRs #1362 and #1385 partially addressed this by adding span creation and names, but did not populate response-time attributes.

    Fix

    #1608 adds full AI SDK-compatible telemetry attributes:

    • ai.streamText.doStream span now covers the entire stream lifecycle and records ai.prompt.messages, ai.prompt.tools, ai.response.text, ai.response.finishReason, ai.usage.*, gen_ai.response.*, gen_ai.usage.*, and timing
    • ai.toolCall span now records ai.toolCall.result
    • New outer ai.streamText span wraps the full step iteration with aggregated usage
    • recordInputs/recordOutputs gating honored throughout

    To see if this fix solves your issue, could you try to install the pre-release in your package.json? Just replace the workflow dep like so:

    {
      "dependencies": {
        "workflow": "https://workflow-docs-7t3kulveh.vercel.sh/workflow.tgz",
        "@workflow/ai": "https://workflow-docs-7t3kulveh.vercel.sh/workflow-ai.tgz"
      }
    }
  5. craze3 commented on Apr 5, 2026

    @craze3
    ContributorAuthor

    @VaguelySerious It's working! I'm successfully getting the expected traces after runs.

    I did see some inconsistency where, for some models, tool calls showed up only as ai.streamText.doStream rather than distinct ai.toolCall.* spans (in Langfuse). For example, Kimi K2.5 reports all the spans as "doStream", even when they are tool calls like readFile and writeFile, whereas Minimax M2.7 (among other models) shows the correct tool names on each span.

    Thank you for all your hard work btw 🙏

  6. VaguelySerious commented on Apr 6, 2026

    @VaguelySerious
    Member

    @craze3 Made some smaller changes to see if I can address the inconsistencies you're seeing. Could you give this a try?

    {
      "dependencies": {
        "workflow": "https://workflow-docs-8eop8l5bh.vercel.sh/workflow.tgz",
        "@workflow/ai": "https://workflow-docs-8eop8l5bh.vercel.sh/workflow-ai.tgz"
      }
    }
  7. craze3 commented on Apr 7, 2026

    @craze3
    ContributorAuthor

    @VaguelySerious I'm sorry but I'm not seeing much improvement. For example, I now see readFile tool calls correctly where I previously saw doStreams, but those doStreams still appear later at the bottom of the trace. I'm not sure if this is expected behavior or if they are detached or simply redundant. Happy to share more details on email/call. But I think this is a great improvement compared to the current implementation in the latest public release!

  8. VaguelySerious commented on Apr 7, 2026

    @VaguelySerious
    Member

    @craze3 Thanks, will ship this in the next release. Feel free to make PRs against this repository with any smaller improvements you'd like to see for telemetry!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions