Skip to content

fix(export): keep usage metadata on inline function handoff - #2721

Open
abhiramshaji-cyber wants to merge 1 commit into
aws:v0.xfrom
abhiramshaji-cyber:fix/harness-inline-function-metadata
Open

abhiramshaji-cyber wants to merge 1 commit into
aws:v0.xfrom
abhiramshaji-cyber:fix/harness-inline-function-metadata

Conversation

@abhiramshaji-cyber

Copy link
Copy Markdown

Description

Agents exported from a harness now report token usage for the model call that hands work back to the caller through an inline function.

What this code is for

src/assets/python/http/strands/base/main.py is the Strands agent the CLI writes when a harness is exported to a standalone agent. It streams model events to the caller the same way the harness does. When the model asks for an inline function (a tool the caller runs on its own side), the agent ends the stream so the caller can run the tool and send the result back in its next request.

Before

Dana runs an exported harness agent and bills her users per request from the usage numbers in the stream.

  1. Dana sends a prompt.
  2. The model decides to call an inline function and ends its reply with messageStop (stop reason tool_use).
  3. The agent forwards messageStop and ends the stream right there.
  4. The model's metadata event, which carries the token counts and arrives right after messageStop, is never sent.
  5. Dana's usage total for the request is missing that whole model call, so she has to guess its cost.

After

Dana runs the same agent and sends the same prompt.

  1. Dana sends a prompt.
  2. The model decides to call an inline function and ends its reply with messageStop (stop reason tool_use).
  3. The agent forwards messageStop and looks at the very next event before ending the stream.
  4. That event is the metadata for the same model call, so the agent forwards it and then ends the stream.
  5. Dana's usage total for the request includes every model call.
flowchart LR
  subgraph BEFORE
    direction TB
    B1["Model asks for an inline function"] --> B2["Agent forwards messageStop"]
    B2 ==> B3["Agent ends the stream at once"]
    B3 --> B4["Usage of that call is never sent"]
  end
  subgraph AFTER
    direction TB
    A1["Model asks for an inline function"] --> A2["Agent forwards messageStop"]
    A2 ==> A3["THE CHANGE: agent checks the next event"]
    A3 --> A4["Next event is metadata: forwarded, then the stream ends"]
    A3 --> A5["Model sent no usage: stream ends, nothing extra sent"]
  end
  B1 ~~~ A1
  linkStyle 1 stroke:#d32f2f,stroke-width:4px
  linkStyle 4 stroke:#2e7d32,stroke-width:4px
Loading

The change

Both copies of the stream loop (with and without execution limits) used to return right after forwarding the messageStop that follows an inline function call. They now set handed_off instead. On the next event, the loop forwards it only when it is a metadata event, then returns. Nothing else is ever forwarded after the handoff, so the stream still ends at the same model call and the tool is still left for the caller to run. Strands sends a model call's metadata directly after its messageStop, so checking only the next event is enough.

The asset snapshot is updated. The new test src/cli/templates/__tests__/strands-inline-function-stream.test.ts renders the template, runs its real stream loop under python3 against a fake agent, and checks three runs for both loop copies: a handoff followed by metadata, a handoff with no metadata, and a run with no inline function. The first case fails on the main branch before this change.

What changes for a user

Usage numbers from an exported agent now cover every model call in a request, including the one that hands off an inline function, so per request totals match the agent's own traces. Everything else works as before: runs that end with a normal reply, regular tools, timeouts and execution limit stops stream the same events, and the follow up request that carries the tool result is handled the same way. This change covers the exported agent in this repository; the hosted harness service is not part of this repository.

Related Issue

Closes #2695

Documentation PR

None needed; no documented behavior changes.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Other (please describe):

Testing

How have you tested the change?

  • I ran bun run test (444 files, 6386 tests passed, 2 skipped) and npx vitest run --project integ (36 files, 355 tests passed, 1 skipped)
  • I ran bun run typecheck (no errors)
  • I ran bun run lint (0 errors) and bun run format:check (all files pass), plus bun run build (succeeds)
  • If I modified src/assets/, I ran npm run test:update-snapshots and committed the updated snapshots (I updated only src/assets/__tests__/assets.snapshot.test.ts with --update)

Checklist

  • I have read the CONTRIBUTING document
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the
terms of your choice.

When a model call ends by asking for an inline function, the exported
Strands agent returned right after forwarding messageStop. Strands sends
that call's metadata event, which carries the token usage, directly
after messageStop, so the caller never received usage for the call that
handed off the tool and could not account for it.

The stream loop now marks the handoff and inspects only the next event:
a metadata event is forwarded, anything else is dropped, and the stream
ends. Both loop variants (with and without execution limits) share the
change. Runs without an inline function are unaffected.

Closes aws#2695
@abhiramshaji-cyber
abhiramshaji-cyber requested a review from a team October 28, 2026 01:26
@github-actions github-actions Bot added the size/m PR size: M label Oct 28, 2026
@abhiramshaji-cyber

Copy link
Copy Markdown
Author

@nborges-aws this is ready for a look whenever you have time. The CodeBuild end to end run passed, and the new test runs the real template stream loop under python3.

This branch has not been deployed

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

Labels

size/m PR size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant