Read AI agent conversation files over the OAP's HTTP route - #235
Merged
Merged
Conversation
The OAP drops the getConversationRawFiles GraphQL query. A conversation's
stored Session Data files are read from its HTTP route beside the view,
GET /ai-agent/conversations/{conversation}/v1/files, and the view and the
files route now both require the service and the sender's instance.
- ai-agent files requires --instance-name, --session and --seqs. A file is
chosen by its session and its landed seq, which the conversation's
asz.view document lists; there is no read of every file. The seqs are
asked for 32 at a time, the route's limit, over one HTTP client.
- The body, application/vnd.skywalking.asz.files+ndjson, is read file by
file: a naming line, then exactly the bytes it names, then the newline
that follows a non-empty file not ending with one. Each file is checked
against its sha256, and a size no response carries is never allocated.
- --export writes through os.Root, so a file's name cannot reach outside
the export directory, through ".." or through a symbolic link.
- ai-agent view requires --instance-name.
wankai123
approved these changes
Sep 15, 2026
wu-sheng
added a commit
to apache/skywalking
that referenced
this pull request
Sep 15, 2026
The AI agent e2e reads conversation files through swctl ai-agent files with --session and --seqs, which apache/skywalking-cli#235 added.
wu-sheng
added a commit
to apache/skywalking
that referenced
this pull request
Sep 15, 2026
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
added a commit
to apache/skywalking
that referenced
this pull request
Sep 16, 2026
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
added a commit
to apache/skywalking
that referenced
this pull request
Sep 16, 2026
…TTP (#14084) 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.
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.
What
The OAP drops the
getConversationRawFilesGraphQL query (apache/skywalking-query-protocol#175, merged). A conversation's stored Session Data files are now read from an HTTP route beside the view route:swctl ai-agentfollows it.files--instance-name,--sessionand--seqs. A file is chosen by its session and its landed seq, both listed in the conversation'sasz.viewdocument. There is no read of every file.application/vnd.skywalking.asz.files+ndjsonbody one file at a time: a naming line, then exactly the bytes it names, then the newline that follows a non-empty file not ending with one.--exportwrites throughos.Root, so a file name can never reach outside the export directory, whether through..or a symbolic link.view--instance-name, as the OAP route now does.The GraphQL query asset for raw files is removed.
liststays on GraphQL.Tests
go test ./pkg/aiagent/... ./internal/commands/aiagent/...passes.golangci-lintreports only the goconst findings that already exist on master.