Skip to content

[BUG] execute_agent_loop_cycle span leaks on cancellation — try has no finally #3800

Description

@roy-melio

Checks

  • I have updated to the lastest minor and patch version of Strands
  • I have checked the documentation and this is not expected behavior
  • I have searched ./issues and there are no duplicates of my issue

SDK Language

TypeScript

Strands Version

main @ 2c6c531779538ec75957cb114f4514a4e0addc3e

Language Runtime Version

Node.js 26.5.0

Operating System

macOS 26.6.1

Installation Method

git clone

Steps to Reproduce

execute_agent_loop_cycle's span (cycleSpan in strands-ts/src/agent/agent.ts) is only ended
by explicit this._tracer.endAgentLoopSpan(cycleSpan) calls at each of the loop body's exit
points (currently 6, plus one in the catch) — there is no finally around the try that wraps
the cycle body (agent.ts:1508-1719).

The loop body is an async generator, and it has unguarded yield/yield* points ahead of every
one of those explicit end-span calls, e.g.:

  • agent.ts:1513yield this._appendMessage(...)
  • agent.ts:1528yield* this._invokeModel(...)
  • agent.ts:1624yield* this.executeTools(...)

Breaking out of a for await loop over this generator (which is exactly what a caller does to
cancel a run, e.g. agent.cancel() + break) calls the generator's .return(). .return()
unwinds through a pending yield the same way a return statement would: it runs finally
blocks, but does not enter catch. Since there is no finally here, .return() landing on
any of those yield points skips every span-ending call entirely.

Minimal repro shape (mirrors how a consumer cancels a run):

const gen = agent.streamAsync(input) // or whatever the public async-generator entry point is
for await (const event of gen) {
  // some cancellation condition
  break // <- triggers gen.return(), unwinding through the unguarded yield inside the try
}

Expected Behavior

The execute_agent_loop_cycle span (and its ancestor invoke_agent span) is still ended —
ideally marked with a distinct status/attribute for "cancelled mid-cycle" — even when the
consuming for await breaks or otherwise triggers .return() on the loop generator.

Actual Behavior

The cycle span (and therefore its invoke_agent root, since OTel can't export a parent before
its children finish and the parent's own end() never runs either) is never ended and never
exported. Downstream, this deletes the entire trace's identity — everything that already ended
(earlier cycles, chats, tool spans) still exports, but there's no way to attribute the run to a
session/user/etc. because that attribution lives on the un-exported root.

We hit this in production via agent.cancel() during a graceful-shutdown drain timeout racing a
long-running tool call; a multi-day sample showed roughly 9% of exported trace volume missing
its invoke_agent root, consistently showing the same signature (one more chat span than
execute_agent_loop_cycle spans, i.e. the last cycle's span never closed). We're currently
working around it downstream with a periodic sweep that force-ends spans past an age threshold,
but that's a mitigation, not a fix — it can't recover attribution promptly (sweep runs on a timer)
and it records a fictional span duration.

Additional Context

This looks like the TypeScript-SDK counterpart to #3609 (spans dropped on cancellation in the
Python SDK) — same class of symptom, but a different mechanism: #3609 is about except Exception
not catching BaseException-derived cancellation signals, whereas this is about a try with no
finally combined with async-generator .return() semantics, which skips catch but would have
still gone through a finally had one existed.

Possible Solution

Wrap the cycle body in try { ... } finally { ... } and move the span-ending (and
this._meter.endCycle(cycleStartTime)) into the finally, guarding against double-ending since
several explicit call sites already end the span before returning/continuing. Track "already
ended" via a local flag or by making endAgentLoopSpan idempotent, so the finally only fires
the end call when none of the explicit call sites already ran.

Related Issues

#3609

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-agentRelated to the agent class or general agent questionsarea-otelOpen-telemetry relatedbugSomething isn't workingtypescriptPull requests that update typescript code

    Type

    Fields

    Language

    TypeScript

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions