Skip to content

fix: record the user turn an agent renders from its template - #367

Merged
TonsOfFun merged 1 commit into
mainfrom
fix/capture-rendered-user-message
Aug 19, 2026
Merged

fix: record the user turn an agent renders from its template#367
TonsOfFun merged 1 commit into
mainfrom
fix/capture-rendered-user-message

Conversation

@TonsOfFun

Copy link
Copy Markdown
Contributor

The gap

A trace from a template-rendering agent carries prompt.input.instructions and llm.output.message but not prompt.input.messages. The user turn — what the model was actually asked — is missing.

Easy to miss, because the other two attributes are there: the trace looks populated, the Conversation tab renders System and Assistant, and only the User row is absent. It is also the half an evaluation scores.

Why

The instrumentation serializes prompt_options[:messages], which holds turns a caller passed explicitly. An agent written the idiomatic way —

prompt(
  instructions: { template: "instructions_summaries_batch", locals: shared_locals },
  locals: shared_locals
)

— has none at that point. Its user turn is rendered later, in prepare_prompt_parameters. So the message reaches the model and never reaches the trace.

The fix

Fall back to the rendered parameters when there are no explicit messages.

prepare_prompt_parameters is a pure function of prompt_options — it deep_dups its input and mutates no instance state — so reading it is safe. It does re-render the templates, which is why the fallback is only reached when there is nothing else to record. It never raises: an agent whose templates need context this call cannot supply loses the attribute, not the generation.

Also handles message objects responding to #content, not just hashes and strings, since that is what the rendered parameters contain.

Verification

Against a self-hosted dashboard, same agent, stock gems before and after:

attribute before after
prompt.input.instructions
prompt.input.messages
llm.output.message

The captured turn is the real rendered content ("Title: Emergency shutdown procedure\nDocument id: …"), and the trace's Conversation tab now shows System / User / Assistant.

An app-side workaround for exactly this had accumulated in a host app's ApplicationAgent — hooking prepare_prompt_parameters and writing the gem's own attribute name. Removing it and relying on this change reproduces the same result, which is the outcome that matters: apps should not each have to discover and patch this.

Tests

test/telemetry/rendered_messages_test.rb covers reading the rendered messages, failing safe when rendering raises, and skipping agents that do not implement prepare_prompt_parameters.

⚠️ Not run in the repo's harness — the dummy app's encrypted credentials fail to decrypt in my checkout (ActiveSupport::MessageEncryptor::InvalidMessage, no master key), which breaks existing tests on a clean tree too. The three behaviours were verified directly against the module in a host app's console instead; CI should be the judge of the file itself.

Risk

Confined to the telemetry path and only reached when prompt_options[:messages] is blank — an agent that passes messages explicitly takes the existing branch unchanged. The extra template render costs one pass, and only for agents that were previously recording nothing.

🤖 Generated with Claude Code

Traces from a template-rendering agent carried the system prompt and the
completion but not the user message. That is the half an evaluation scores:
what the model was actually asked. The gap was easy to miss precisely
because the other two attributes were present, so a trace looked populated.

The instrumentation serializes prompt_options[:messages] — the turns a caller
passed explicitly. An agent written the idiomatic way, `instructions:` plus
`locals:` with the user turn in the action's ERB, has none at that point: the
rendering happens later, in prepare_prompt_parameters. So the message reached
the model and never reached the trace.

Falls back to the rendered parameters when no explicit messages exist.
prepare_prompt_parameters is a pure function of prompt_options — it deep_dups
its input and mutates no instance state — so reading it here is safe. It does
re-render the templates, which is why the fallback is only reached when there
is nothing else to record. It never raises: an agent whose templates need
context this call cannot supply loses the attribute, not the generation.

Also handles message objects that respond to #content, not just hashes and
strings, since that is what the rendered parameters contain.

Verified against a self-hosted dashboard: a document-enrichment agent whose
user turn is a rendered page-text template now shows System / User /
Assistant in the trace's Conversation tab, where the User row was previously
absent. An app-side workaround for this in ApplicationAgent has been removed
and the behaviour holds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@TonsOfFun
TonsOfFun marked this pull request as ready for review August 19, 2026 03:50
@TonsOfFun
TonsOfFun merged commit 5a11138 into main Aug 19, 2026
8 checks passed
@TonsOfFun TonsOfFun mentioned this pull request Aug 19, 2026
3 tasks
TonsOfFun added a commit that referenced this pull request Aug 19, 2026
Ships the rendered-user-turn capture (#367) under a version whose tag
actually produces it.

1.3.0 was published from a tree that already carried that fix while the PR
was still open, so the released gem and the v1.3.0 tag disagree: the tag's
source does not reproduce the gem. Anyone auditing the release, or building
from the tag, gets a different artifact than the one on rubygems.

There is no functional change relative to the published 1.3.0 gem. main is
byte-identical to it across lib/, verified file by file. This release exists
so the tag, main and the published gem agree again, and upgrading is
optional.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant