Skip to content

fix(agent): propagate messages session ids to runner traces#4767

Closed
mmabrouk wants to merge 1 commit into
refactor/vercel-adapter-cleanupfrom
fix/agent-session-id-tracing
Closed

fix(agent): propagate messages session ids to runner traces#4767
mmabrouk wants to merge 1 commit into
refactor/vercel-adapter-cleanupfrom
fix/agent-session-id-tracing

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Jun 19, 2026

Copy link
Copy Markdown
Member

This PR is part of a stack. Review bottom-up.

Each PR's diff is only its own delta. Merge from the bottom. This PR's base is #4766 (merge that first).

Context

The /messages endpoint already carries a session_id on the request envelope, and the response already echoes it back. But the id stopped at the route. It never reached the agent runner, so Pi and Rivet traces and run metadata were stamped with the harness's own ephemeral session id instead of the platform conversation id. That broke correlation: a multi-turn conversation produced a fresh, unrelated id on every span.

This PR threads the platform session_id from the request envelope down to the runner so traces carry it. Base branch: refactor/vercel-adapter-cleanup. This is the session-id seam from slice #2 (protocol) in docs/design/agent-workflows/pr-stack.md, reaching toward slice #5.

What this changes

Before, the runner used whatever id the harness minted for itself. Each turn looked like a new conversation in the traces.

After, the runner prefers the platform id when the request supplies one, and falls back to the harness id only when it does not.

The id now travels the full path:

  • services/oss/src/agent/app.py: the _agent handler accepts session_id and passes it into SessionConfig.
  • normalizer.py: the middleware recognizes session_id as a top-level request field (sitting next to inputs and parameters) and binds it to a handler argument of the same name.
  • protocol.ts: resolveRunSessionId(request, fallback) returns request.sessionId when it is non-empty, else the harness fallback.
  • pi.ts and rivet.ts: both engines call resolveRunSessionId and stamp the resolved id onto OTel config, run.start, and the returned run metadata.

Key architectural decision to review

session_id is modeled as a request-envelope primitive, like a trace id or a span id, not as an HTTP header or an inputs field. See normalizer.py:100-102. The middleware already special-cases request and the DATA_FIELDS (inputs/outputs/parameters); this adds session_id as a third class of binding: a recognized top-level envelope field, extracted from request.session_id rather than from request.data.inputs.

The tradeoff to scrutinize: this hard-codes one field name in the middleware. A future second envelope field would need the same treatment, so if more arrive, this wants a small allowlist set rather than a chain of elif name == ... branches. The test test_session_id_is_not_added_to_var_kwargs pins the deliberate boundary: session_id binds only to an explicit handler argument, never to **kwargs, so it does not leak into handlers that did not ask for it.

The id is for correlation, not for state. The cold runtime still receives the full conversation in messages, so the runner does not load history from the id. The doc-comment edit on AgentRunRequest.sessionId in protocol.ts:192 makes that contract explicit: read it and confirm no engine is treating the id as a key to replay history from.

How to review this PR

  1. Start with protocol.ts: read resolveRunSessionId (the whole behavior is here) and the sessionId doc comment.
  2. Then normalizer.py:100-102: confirm the new branch sits before the **kwargs branch, so an explicit session_id argument wins over kwargs capture.
  3. Then pi.ts and rivet.ts: confirm every place that previously used session.sessionId / session.id for tracing or returned metadata now uses the resolved id. In pi.ts, note the local sessionId moved up so OTel config gets the resolved value.
  4. Then app.py: confirm session_id reaches SessionConfig.

Likely regression: a request with no session_id (plain /invoke, or the first turn of a server-minted session) must still get the harness id. resolveRunSessionId({}, fallback) returning the fallback covers this; the test on that line guards it.

Tests / notes

  • test_normalizer_passthrough.py: explicit-arg binding plus the **kwargs exclusion.
  • test_invoke_handler.py: session_id="sess_request" reaches the backend SessionConfig (asserted via the fake backend's created_session_ids).
  • continuation.test.ts: resolveRunSessionId prefers the platform id and falls back when absent.

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e1a457ed-f80e-462d-a50f-403bd62143f8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/agent-session-id-tracing

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Jun 19, 2026
@mmabrouk

Copy link
Copy Markdown
Member Author

Reviewer guide: interesting code

  • services/agent/src/protocol.ts:292resolveRunSessionId is the whole policy: prefer a non-empty platform sessionId, else the harness's ephemeral fallback.
  • services/agent/src/protocol.ts:192 — the doc-comment change on sessionId states the contract: the id is for correlation, the cold runtime still gets history in messages.
  • sdks/python/agenta/sdk/middlewares/running/normalizer.py:100 — the new session_id branch binds a top-level envelope field, placed before the **kwargs branch so an explicit arg wins.
  • services/agent/src/engines/pi.ts:303 — the local sessionId moved up so OTel config carries the resolved id, not the raw session.sessionId.
  • services/agent/src/engines/rivet.ts:821 — the resolved id flows into both run.start and the returned run metadata, replacing two raw session.id uses.


/** Prefer the platform conversation id, falling back to the harness's ephemeral id. */
export function resolveRunSessionId(request: AgentRunRequest, fallback: string): string {
return request.sessionId && request.sessionId.trim() ? request.sessionId : fallback;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The empty-string guard matters: request.sessionId.trim() means a blank id from the envelope is treated as absent, so the harness fallback wins instead of stamping an empty session onto every span.

)
consumed.add(name)

elif name == "session_id":

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sits before the VAR_KEYWORD branch, so a handler that declares an explicit session_id argument gets the envelope value, while a **kwargs handler does not receive it. That asymmetry is intentional and pinned by the two new normalizer tests.


// Hand the session id + model to the extension so spans carry them.
otel.config.sessionId = session.sessionId;
const sessionId = resolveRunSessionId(request, session.sessionId);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous code stamped session.sessionId onto OTel config and returned it later. Moving the resolved sessionId up here is what actually fixes the trace: the span now carries the platform id, not Pi's ephemeral one.

@@ -922,7 +924,7 @@ export async function runRivet(
// `streamingDeltas` advertises end-to-end live deltas, which is only true when a live

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirm both replaced session.id sites (here in the result and at run.start) use the same resolved sessionId, so the trace id and the returned metadata stay consistent for a continued conversation.

@mmabrouk

Copy link
Copy Markdown
Member Author

Superseded. Replacing the path-based stack with PRs sliced by functional area showing final code only, so reviewers don't comment on intermediate scaffolding that a later PR rewrites. See the new set.

@mmabrouk mmabrouk closed this Jun 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant