AI agent conversations: provider bodies in the view, and files over HTTP - #14084
Merged
Merged
Conversation
wu-sheng
force-pushed
the
ai-agent-provider-bodies
branch
from
September 15, 2026 15:05
a6c475b to
4117ed1
Compare
Provider bodies in the conversation view. The AI Sessionizer can land the request and response bodies an agent runtime exchanged with its model provider, apache/skywalking-ai-sessionizer#10. They arrive as Session Data files of kind provider_body, one directory per session: <session>/provider_body/provider_body-<stamp>-<seq>.sd. The OAP already stored them like any other file, but gave them a wrong name, <session>/runs//..., which the raw-file query could not parse back. - FileNames names the provider_body directory and parses it back. - The asz.view document follows the Sessionizer's. An llm.call step lists its request and then its response under provider_bodies, each as a role and the ref of the landed record, never the body. summary.provider_bodies and summary.captured_prompts count them. The join uses the bodies' own ids: a response by its message id, a request by the previous call's response request id and its prompt, and only when exactly one request and one call carry them. A synthetic call takes part in no join, and no request joins in a stream whose landed transcript lines have a gap. Calls on one record are ordered by id, as the Sessionizer now orders them. - What the join reads is decoded as the Sessionizer decodes it. A manifest whose known keys, or its segments' keys, have the wrong JSON type is no body. A record whose call or run is not a string is gone to the join. An ord is read from the raw line: the digits after a leading {"ord":, as an unsigned 64-bit number, or else the decoded value, where null is 0. A transcript whose records end at a line that does not decode has a gap. SessionDataFile keeps each record's leading ord digits and whether its records stopped early. - The test data is refreshed from the Sessionizer: the two existing sets gain the two summary counts, and the provider-bodies and provider-bodies-errors sets are added. Both documents equal the Sessionizer's key for key. A session without its provider_body file folds to the same nodes, a stream with a gap joins responses only, and a manifest or an ord the Sessionizer reads differently counts as it does; each of those cases gave the same counts from asz conversation. - The e2e builds the Sessionizer's provider-bodies scenario into the same root and checks the file, every call's bodies and the export by name. The views, list, sender and token expectations count the new session. The Sessionizer is pinned to the commit that lands provider bodies, since its view now carries the two summary counts. Conversation files over HTTP, not GraphQL. The query protocol drops getConversationRawFiles, apache/skywalking-query-protocol#175. A conversation's stored Session Data files are read from a second HTTP route beside the view, so a page loads what a step points at, such as an llm.call's provider bodies, only when a reader opens it: GET /ai-agent/conversations/{conversation}/v1/files ?service=&instance=&session=&seq=[&seq=...][&coldStage=true] - A file is chosen by its session and its landed seq, the two columns the storage reads it by; the Sessionizer assigns a seq once per file in a session. One to 32 seqs a request: a file is cut at 2 MiB, so a response holds about 64 MiB. There is no read of every file. - The body is application/vnd.skywalking.asz.files+ndjson: for each file a naming line {file, seq, lines, bytes, digest}, exactly that many bytes, and a newline after a non-empty file that does not end with one. Files come in seq order, the order provider bodies are read in. - The read takes its time range from the newest intact round, reading the rounds down from the head only until one is intact, then reads one storage window of files and hands it to the response before reading the next. Windows are produced one at a time and cannot overflow. - The route compresses with gzip itself, a chunk at a time. Armeria's encoder keeps every compressed chunk of a response in one growing buffer until the response ends. The view route still uses it. - Both routes require the service and the sender's instance, as a list row names them, so every read is a full series lookup; serviceId goes. A bad coldStage gets a problem document, not Armeria's plain-text 400. - The e2e reads files through swctl by session and seq taken from the document, and passes the instance on every view and route call. The raw-files case counts the three Session Data files of the first conversation, since rounds are no longer served. The e2e pins swctl to the merge commit of apache/skywalking-cli#235, which adds swctl ai-agent files with --session and --seqs.
wu-sheng
force-pushed
the
ai-agent-provider-bodies
branch
from
September 16, 2026 06:19
4117ed1 to
02b1da9
Compare
wankai123
approved these changes
Sep 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two things for the AI agent conversations the SkyWalking AI Sessionizer lands:
asz.viewdocument OAP serves — anllm.callstep names where its request and its response landed, and the document never carries the bodies;GET /ai-agent/conversations/{conversation}/v1/files, which serves the raw Session Data files of one session by(session, seq), at most 32 a request, asapplication/vnd.skywalking.asz.files+ndjson: a line naming each file, then exactly the bytes it names.Together they let a page show the exact prompt a model call sent and the answer it got, loading only the files that call points at. The protocol side is apache/skywalking-query-protocol#175; the reader is apache/skywalking-horizon-ui#165.
The join
Ported from the Sessionizer's own reader, rule for rule. A response joins by the message id it carries, which is the call's own. A request names no call, only the request before it and its prompt, so it joins to the call of its stream whose previous call's response carries that request id and whose prompt is the one it names, when exactly one request and exactly one call carry the pair. A synthetic call takes part in no join, a body landed twice is one body, and no request joins in a stream whose landed lines have a gap. Nothing is joined by position or by time.
The route
service,instance,sessionand at least oneseq; at most 32, each a positive whole number.coldStageselects cold storage.file,seq,lines,bytes,digest, andcopieswhere the read saw that seq more than once.Bounds and cancellation
readWindowat a time (16), and every read asks whether the caller is still there, so an abandoned request stops instead of running to completion.fileRangelooks for the newest intact round one round at a time, as before, but stops after 16 rounds instead of descending to round 1. A head round that does not parse used to cost one storage read per round — measured at 100,001 reads for one request on a long chain, all of which ran on after the client had gone.nullinflagsdecodes to an empty string rather than throwing. What is insideparts,droppedandusageis deliberately not inspected — Go ignores fields it does not declare, and refusing a record it accepts would hide that record and every one behind it.Settings
Five, in the order an operator meets them:
conversationListMaxLimit(renamed frommaxListLimit, and its comment now says plainly that it counts rounds, not conversations, so a busy conversation spends the budget of the quiet ones),viewRequestTimeout,readWindow(the formerfileReadWindowandroundReadWindow, which were the same idea with the same value),maxResponseBytes(BanyanDB alone accepts it; Elasticsearch and JDBC bound a read by hits and by rows) andmaxFileBytes.Testing
64 module tests pass on a clean build, including the e2e case under
test/e2e-v2/cases/ai-agent/, which pushes real Sessionizer output, checks seven concrete request/response joins, and calls the files route with the pinned CLI, verifying the digest each naming line advertises.Reviewed by GPT-6 across four rounds; every finding is fixed in this commit. Two are worth naming: the first version of the bounded
fileRangeread a window of round bodies, which fetched sixteen to find one in the healthy case and could lose a readable duplicate when a storage capped the window — it is now the original per-round read with a bound; and a JDBC change that claimed to bound memory did not, because the driver buffers the result regardless.