feat: emit the final user message as its own capture attribute #86

Merged
coilysiren merged 2 commits from aos/claude/user-message-capture into main 2026-08-11 00:34:14 +00:00
Owner

Closes #85.

model.request.captured now carries agentproxy.user_message holding the
verbatim text of the final user turn.

The text was already inside request.body, but only as the last matching
element of a variable-length messages list. SigNoz pipeline processors
cannot address a last element: move and copy take fixed field paths
with no indexing, and the add processor's EXPR(...) escape hatch
reaches a known index but has no last-element form. So the projection has
to happen here, where the position is known.

The field is a convenience projection beside the complete body, never a
selected-field capture mode, so extraction is total. An absent, empty, or
unreadable user message omits the field rather than failing a capture that
would otherwise have succeeded. String content and the content-parts form
are both handled, with non-text parts ignored and text parts joined.

Trailing tool rounds are the case that motivated reading backwards for the
last user role rather than taking the last element, since a tool-using
turn ends on a tool result.

Adding an optional field is backward compatible, so the capture schema
version stays at 1. No consumer pins it.

Gates: ward exec format-check, lint, typecheck, and test all pass, 248
tests including 8 new ones.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Co-authored-by: Kai Siren coilysiren@gmail.com
Co-authored-by: Claude noreply@anthropic.com

Closes #85. `model.request.captured` now carries `agentproxy.user_message` holding the verbatim text of the final user turn. The text was already inside `request.body`, but only as the last matching element of a variable-length messages list. SigNoz pipeline processors cannot address a last element: `move` and `copy` take fixed field paths with no indexing, and the `add` processor's `EXPR(...)` escape hatch reaches a known index but has no last-element form. So the projection has to happen here, where the position is known. The field is a convenience projection beside the complete body, never a selected-field capture mode, so extraction is total. An absent, empty, or unreadable user message omits the field rather than failing a capture that would otherwise have succeeded. String content and the content-parts form are both handled, with non-text parts ignored and text parts joined. Trailing tool rounds are the case that motivated reading backwards for the last `user` role rather than taking the last element, since a tool-using turn ends on a tool result. Adding an optional field is backward compatible, so the capture schema version stays at 1. No consumer pins it. Gates: ward exec format-check, lint, typecheck, and test all pass, 248 tests including 8 new ones. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Kai Siren <coilysiren@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
feat: emit the final user message as its own capture attribute
All checks were successful
ci / smoke (pull_request) Successful in 9s
ci / quality (pull_request) Successful in 22s
632745c924
Closes #85.

`model.request.captured` now carries `agentproxy.user_message` holding the
verbatim text of the final user turn.

The text was already inside `request.body`, but only as the last matching
element of a variable-length messages list. SigNoz pipeline processors
cannot address a last element: `move` and `copy` take fixed field paths
with no indexing, and the `add` processor's `EXPR(...)` escape hatch
reaches a known index but has no last-element form. So the projection has
to happen here, where the position is known.

The field is a convenience projection beside the complete body, never a
selected-field capture mode, so extraction is total. An absent, empty, or
unreadable user message omits the field rather than failing a capture that
would otherwise have succeeded. String content and the content-parts form
are both handled, with non-text parts ignored and text parts joined.

Trailing tool rounds are the case that motivated reading backwards for the
last `user` role rather than taking the last element, since a tool-using
turn ends on a tool result.

Adding an optional field is backward compatible, so the capture schema
version stays at 1. No consumer pins it.

Gates: ward exec format-check, lint, typecheck, and test all pass, 248
tests including 8 new ones.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
feat: project the response fields beside the request projection
All checks were successful
ci / smoke (pull_request) Successful in 6s
ci / quality (pull_request) Successful in 24s
ff28539d3e
Follow-up on the same branch, after review asked why only the input half
was derived in the proxy.

`model.response.captured` now carries `agentproxy.finish_reason`,
`agentproxy.assistant_message`, `agentproxy.completion_tokens`, and
`agentproxy.prompt_tokens`, taken from the first choice and the usage
block. The complete body still carries everything, including further
choices and reasoning content.

These paths are fixed, so a log pipeline could reach them and one already
does downstream. Deriving them here anyway gives turn-shape fields a
single owner and serves every capture consumer, including the trajectory
store and evaluation joins, rather than one SigNoz ingest path.

Projection stays total on both sides. An incomplete response keeps its
closed-set reason and still projects whatever it does carry, so a
truncated completion reads as finish_reason length with completion_tokens
at the cap and no assistant message.

Gates: format-check, lint, typecheck, and test all pass, 260 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-flight-deck/agent-proxy!86
No description provided.