Add the ai-agent commands for the conversations the AI Sessionizer lands (#234)
diff --git a/CHANGES.md b/CHANGES.md
index 3e7a3a8..5e1abc2 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -7,6 +7,7 @@
 
 ### Features
 
+* Add the `ai-agent` commands, `list`, `files` and `view`, for the AI agent conversations the AI Sessionizer lands in the OAP (11.1.0+); `view` reads the whole conversation as one `asz.view` document from the OAP's streamed route on the GraphQL host by @wu-sheng in https://github.com/apache/skywalking-cli/pull/234
 * Add the sub-command `profiling async` for async-profiler query API by @zhengziyi0117 in https://github.com/apache/skywalking-cli/pull/203
 * Support the owner in MQE response by using [10.2 MQE query protocol](https://github.com/apache/skywalking-query-protocol/pull/141) by @zhengziyi0117 in https://github.com/apache/skywalking-cli/pull/203
 * Add the sub-command `alarm autocomplete-keys` and `alarm auto-complete-values` for alarm query API by @mrproliu in https://github.com/apache/skywalking-cli/pull/210
diff --git a/assets/graphqls/aiagent/ConversationRawFiles.graphql b/assets/graphqls/aiagent/ConversationRawFiles.graphql
new file mode 100644
index 0000000..824e2c8
--- /dev/null
+++ b/assets/graphqls/aiagent/ConversationRawFiles.graphql
@@ -0,0 +1,34 @@
+# Licensed to Apache Software Foundation (ASF) under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Apache Software Foundation (ASF) licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# The body is read from storage only when selected; $body is true on the export path.
+query ($condition: ConversationCondition!, $files: [ID!], $body: Boolean!) {
+    result: getConversationRawFiles(condition: $condition, files: $files) {
+        errorReason
+        files {
+            id
+            format
+            session
+            seq
+            round
+            digest
+            bytes
+            timestamp
+            body @include(if: $body)
+        }
+    }
+}
diff --git a/assets/graphqls/aiagent/ListConversations.graphql b/assets/graphqls/aiagent/ListConversations.graphql
new file mode 100644
index 0000000..b7243d7
--- /dev/null
+++ b/assets/graphqls/aiagent/ListConversations.graphql
@@ -0,0 +1,36 @@
+# Licensed to Apache Software Foundation (ASF) under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Apache Software Foundation (ASF) licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+query ($condition: ConversationListCondition!, $duration: Duration!) {
+    result: listConversations(condition: $condition, duration: $duration) {
+        errorReason
+        conversations {
+            conversation
+            serviceInstanceId
+            serviceInstanceName
+            title
+            round
+            talks
+            steps
+            streams
+            segments
+            unresolved
+            from
+            to
+        }
+    }
+}
diff --git a/cmd/swctl/main.go b/cmd/swctl/main.go
index 01ee2c9..f35fff1 100644
--- a/cmd/swctl/main.go
+++ b/cmd/swctl/main.go
@@ -23,6 +23,7 @@
 	"runtime"
 
 	"github.com/apache/skywalking-cli/internal/commands/admin"
+	"github.com/apache/skywalking-cli/internal/commands/aiagent"
 	"github.com/apache/skywalking-cli/internal/commands/alarm"
 	"github.com/apache/skywalking-cli/internal/commands/browser"
 	"github.com/apache/skywalking-cli/internal/commands/completion"
@@ -117,6 +118,7 @@
 		menu.Command,
 		hierarchy.Command,
 		admin.Command,
+		aiagent.Command,
 	}
 
 	app.Before = interceptor.BeforeChain(
diff --git a/dist/LICENSE b/dist/LICENSE
index fad46a8..c63e40c 100644
--- a/dist/LICENSE
+++ b/dist/LICENSE
@@ -213,7 +213,7 @@
     sigs.k8s.io/controller-runtime v0.20.4 Apache-2.0
     sigs.k8s.io/randfill v1.0.0 Apache-2.0
     sigs.k8s.io/structured-merge-diff/v4 v4.7.0 Apache-2.0
-    skywalking.apache.org/repo/goapi v0.0.0-20251011100214-efff910f2031 Apache-2.0
+    skywalking.apache.org/repo/goapi v0.0.0-20260905021802-699be54ca302 Apache-2.0
 
 ========================================================================
 Apache-2.0 and BSD-3-Clause licenses
diff --git a/go.mod b/go.mod
index b45a2f0..d9f7872 100644
--- a/go.mod
+++ b/go.mod
@@ -18,7 +18,7 @@
 	gopkg.in/yaml.v2 v2.4.0
 	k8s.io/apimachinery v0.33.1
 	sigs.k8s.io/controller-runtime v0.20.4
-	skywalking.apache.org/repo/goapi v0.0.0-20251011100214-efff910f2031
+	skywalking.apache.org/repo/goapi v0.0.0-20260905021802-699be54ca302
 )
 
 require (
diff --git a/go.sum b/go.sum
index d664728..ef1ad5c 100644
--- a/go.sum
+++ b/go.sum
@@ -543,5 +543,5 @@
 sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
 sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
 sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
-skywalking.apache.org/repo/goapi v0.0.0-20251011100214-efff910f2031 h1:iMd6gzltWrWOtV3COm0mWydeXhpy7r1vkNlTsm7Co0g=
-skywalking.apache.org/repo/goapi v0.0.0-20251011100214-efff910f2031/go.mod h1:Vj9vINJYsTQASPsbQ1i81YgH8nFC/Xds4GjcXvmRYwM=
+skywalking.apache.org/repo/goapi v0.0.0-20260905021802-699be54ca302 h1:XZ27v0cI1QaSyQK48fJfcvpGyhKUv22ULp2cS3l7snI=
+skywalking.apache.org/repo/goapi v0.0.0-20260905021802-699be54ca302/go.mod h1:tsTLCXFg0zZ1lqr5+7tDMsZvZA+YFUCQe0lRctv23yc=
diff --git a/internal/commands/aiagent/aiagent.go b/internal/commands/aiagent/aiagent.go
new file mode 100644
index 0000000..6eec3df
--- /dev/null
+++ b/internal/commands/aiagent/aiagent.go
@@ -0,0 +1,40 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// Package aiagent holds the commands for the conversations of long-lived AI agents
+// that the AI Sessionizer (apache/skywalking-ai-sessionizer) lands in the OAP under
+// the AI_AGENT layer: the list page, the raw-file export, and the conversation itself
+// as one asz.view document.
+package aiagent
+
+import (
+	"github.com/urfave/cli/v2"
+)
+
+var Command = &cli.Command{
+	Name:  "ai-agent",
+	Usage: "AI agent conversations landed by the AI Sessionizer",
+	UsageText: `The AI Sessionizer collects an agent runtime's transcripts and pushes them to the OAP
+under the AI_AGENT layer. "list" and "files" are GraphQL queries on the "--base-url"
+endpoint; "view" reads the whole conversation as one asz.view document from the OAP's
+streamed route on the same host, GET /ai-agent/conversations/{conversation}/v1/view.`,
+	Subcommands: []*cli.Command{
+		listCommand,
+		filesCommand,
+		viewCommand,
+	},
+}
diff --git a/internal/commands/aiagent/files.go b/internal/commands/aiagent/files.go
new file mode 100644
index 0000000..f3cb293
--- /dev/null
+++ b/internal/commands/aiagent/files.go
@@ -0,0 +1,141 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package aiagent
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+
+	api "skywalking.apache.org/repo/goapi/query"
+
+	"github.com/urfave/cli/v2"
+
+	"github.com/apache/skywalking-cli/internal/commands/interceptor"
+	"github.com/apache/skywalking-cli/internal/flags"
+	"github.com/apache/skywalking-cli/pkg/display"
+	"github.com/apache/skywalking-cli/pkg/display/displayable"
+	"github.com/apache/skywalking-cli/pkg/graphql/aiagent"
+)
+
+var filesCommand = &cli.Command{
+	Name:  "files",
+	Usage: "List or export the raw files of a conversation, as the OAP stores them",
+	UsageText: `List every landed file and round of a conversation with its digest and size, or
+export them: "--export DIR" reads each body and writes it to its id path under DIR,
+which gives a storage root that "asz verify" and "asz view" read like the original.
+
+Examples:
+1. The files of a conversation:
+$ swctl ai-agent files --service-name "Claude Code" --conversation 7a3c882e-0dc0-46a0-b814-6613d24b7ac2
+
+2. Export them all:
+$ swctl ai-agent files --service-name "Claude Code" --conversation 7a3c882e-0dc0-46a0-b814-6613d24b7ac2 --export ./root
+
+3. Export two named files:
+$ swctl ai-agent files --service-name "Claude Code" --conversation 7a3c882e-0dc0-46a0-b814-6613d24b7ac2 \
+    --files 7a3c882e-0dc0-46a0-b814-6613d24b7ac2/streams/main/transcript-20260904T152815.774957000Z-000408.sd \
+    --export ./root`,
+	Flags: flags.Flags(
+		flags.ServiceFlags,
+		flags.InstanceFlags,
+		[]cli.Flag{
+			&cli.StringFlag{
+				Name:     "conversation",
+				Usage:    "`id` of the conversation",
+				Required: true,
+			},
+			&cli.StringFlag{
+				Name:  "files",
+				Usage: "only these file `ids`, comma separated; without it, every file of the conversation",
+			},
+			&cli.StringFlag{
+				Name:  "export",
+				Usage: "write each file's body to its id path under this `directory`",
+			},
+		},
+	),
+	Before: interceptor.BeforeChain(
+		interceptor.ParseService(true),
+		interceptor.ParseInstance(false),
+	),
+	Action: func(ctx *cli.Context) error {
+		condition := &api.ConversationCondition{
+			Service:      &api.ServiceCondition{ServiceName: ctx.String("service-name")},
+			Conversation: ctx.String("conversation"),
+			Instance:     instanceCondition(ctx),
+		}
+		var files []string
+		if arg := strings.TrimSpace(ctx.String("files")); arg != "" {
+			files = strings.Split(arg, ",")
+		}
+		exportDir := ctx.String("export")
+
+		raw, err := aiagent.RawFiles(ctx.Context, condition, files, exportDir != "")
+		if err != nil {
+			return err
+		}
+		if raw.ErrorReason != nil && *raw.ErrorReason != "" {
+			return fmt.Errorf("%s", *raw.ErrorReason)
+		}
+		if exportDir == "" {
+			return display.Display(ctx.Context, &displayable.Displayable{Data: raw, Condition: condition})
+		}
+
+		written, err := export(exportDir, raw.Files)
+		if err != nil {
+			return err
+		}
+		return display.Display(ctx.Context, &displayable.Displayable{Data: written, Condition: condition})
+	},
+}
+
+// Exported is one file written by "--export": its id path and size, the body left out.
+type Exported struct {
+	ID    string `json:"id"`
+	Path  string `json:"path"`
+	Bytes int    `json:"bytes"`
+}
+
+// export writes each body to its id path under dir. An id is a relative path inside the
+// Sessionizer's storage root; one that would leave dir is refused.
+func export(dir string, files []*api.ConversationRawFile) ([]Exported, error) {
+	root, err := filepath.Abs(dir)
+	if err != nil {
+		return nil, err
+	}
+	out := make([]Exported, 0, len(files))
+	for _, f := range files {
+		if f.Body == nil {
+			return nil, fmt.Errorf("the OAP returned no body for %s", f.ID)
+		}
+		path := filepath.Join(root, filepath.FromSlash(f.ID))
+		if !strings.HasPrefix(path, root+string(filepath.Separator)) {
+			return nil, fmt.Errorf("refusing to write %s outside %s", f.ID, root)
+		}
+		if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
+			return nil, err
+		}
+		if err := os.WriteFile(path, []byte(*f.Body), 0o644); err != nil { // #nosec G306 -- a landed file is readable by design
+			return nil, err
+		}
+		out = append(out, Exported{ID: f.ID, Path: path, Bytes: len(*f.Body)})
+	}
+	return out, nil
+}
diff --git a/internal/commands/aiagent/list.go b/internal/commands/aiagent/list.go
new file mode 100644
index 0000000..9523c3c
--- /dev/null
+++ b/internal/commands/aiagent/list.go
@@ -0,0 +1,93 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package aiagent
+
+import (
+	api "skywalking.apache.org/repo/goapi/query"
+
+	"github.com/urfave/cli/v2"
+
+	"github.com/apache/skywalking-cli/internal/commands/interceptor"
+	"github.com/apache/skywalking-cli/internal/flags"
+	"github.com/apache/skywalking-cli/internal/model"
+	"github.com/apache/skywalking-cli/pkg/display"
+	"github.com/apache/skywalking-cli/pkg/display/displayable"
+	"github.com/apache/skywalking-cli/pkg/graphql/aiagent"
+)
+
+var listCommand = &cli.Command{
+	Name:    "list",
+	Aliases: []string{"ls"},
+	Usage:   "List the conversations of an AI agent service",
+	UsageText: `List the conversations of an AI agent service active in the duration, newest first,
+one row per conversation from its newest round.
+
+Examples:
+1. The conversations of service "Claude Code" in the last 30 minutes:
+$ swctl ai-agent list --service-name "Claude Code"
+
+2. Only those pushed by one Sessionizer, in a day:
+$ swctl ai-agent list --service-name "Claude Code" --instance-name laptop --start 2026-09-01 --end 2026-09-02`,
+	Flags: flags.Flags(
+		flags.DurationFlags,
+		flags.ServiceFlags,
+		flags.InstanceFlags,
+		[]cli.Flag{
+			&cli.IntFlag{
+				Name:  "limit",
+				Usage: "at most this many rounds are read, newest first, before folding to one row per conversation; 0 for the OAP's default",
+				Value: 0,
+			},
+		},
+	),
+	Before: interceptor.BeforeChain(
+		interceptor.DurationInterceptor,
+		interceptor.ParseService(true),
+		interceptor.ParseInstance(false),
+	),
+	Action: func(ctx *cli.Context) error {
+		duration := api.Duration{
+			Start: ctx.String("start"),
+			End:   ctx.String("end"),
+			Step:  ctx.Generic("step").(*model.StepEnumValue).Selected,
+		}
+		condition := &api.ConversationListCondition{
+			Service:  &api.ServiceCondition{ServiceName: ctx.String("service-name")},
+			Instance: instanceCondition(ctx),
+		}
+		if limit := ctx.Int("limit"); limit > 0 {
+			condition.Limit = &limit
+		}
+
+		list, err := aiagent.ListConversations(ctx.Context, condition, duration)
+		if err != nil {
+			return err
+		}
+		return display.Display(ctx.Context, &displayable.Displayable{Data: list, Condition: condition, Duration: duration})
+	},
+}
+
+// instanceCondition names the sender when "--instance-name" (or "--instance-id",
+// resolved to the name by the interceptor) was given.
+func instanceCondition(ctx *cli.Context) *api.InstanceCondition {
+	name := ctx.String("instance-name")
+	if name == "" {
+		return nil
+	}
+	return &api.InstanceCondition{ServiceName: ctx.String("service-name"), InstanceName: name}
+}
diff --git a/internal/commands/aiagent/view.go b/internal/commands/aiagent/view.go
new file mode 100644
index 0000000..2bbb385
--- /dev/null
+++ b/internal/commands/aiagent/view.go
@@ -0,0 +1,81 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package aiagent
+
+import (
+	"io"
+	"os"
+
+	"github.com/urfave/cli/v2"
+
+	"github.com/apache/skywalking-cli/internal/commands/interceptor"
+	"github.com/apache/skywalking-cli/internal/flags"
+	"github.com/apache/skywalking-cli/pkg/aiagent/view"
+)
+
+var viewCommand = &cli.Command{
+	Name:  "view",
+	Usage: "Read a whole conversation as one asz.view document",
+	UsageText: `Read the whole conversation, once, as one asz.view 1.0 document from the OAP's route
+GET /ai-agent/conversations/{conversation}/v1/view, on the "--base-url" host. The body
+is streamed to stdout, or to "--output", as it arrives: JSON, or YAML with "--yaml".
+The "--display" option does not apply; the document is printed as the OAP sends it.
+
+Examples:
+1. A conversation as JSON, into a file:
+$ swctl ai-agent view --service-name "Claude Code" --conversation 7a3c882e-0dc0-46a0-b814-6613d24b7ac2 --output conversation.json
+
+2. As YAML, on the terminal:
+$ swctl ai-agent view --service-name "Claude Code" --conversation 7a3c882e-0dc0-46a0-b814-6613d24b7ac2 --yaml`,
+	Flags: flags.Flags(
+		flags.ServiceFlags,
+		flags.InstanceFlags,
+		[]cli.Flag{
+			&cli.StringFlag{
+				Name:     "conversation",
+				Usage:    "`id` of the conversation",
+				Required: true,
+			},
+			&cli.BoolFlag{
+				Name:  "yaml",
+				Usage: "ask for the document as YAML instead of JSON",
+			},
+			&cli.StringFlag{
+				Name:  "output",
+				Usage: "write the document to this `file` instead of stdout",
+			},
+		},
+	),
+	Before: interceptor.BeforeChain(
+		interceptor.ParseService(true),
+		interceptor.ParseInstance(false),
+	),
+	Action: func(ctx *cli.Context) error {
+		var out io.Writer = os.Stdout
+		if path := ctx.String("output"); path != "" {
+			f, err := os.Create(path)
+			if err != nil {
+				return err
+			}
+			defer f.Close()
+			out = f
+		}
+		_, err := view.Fetch(ctx.Context, ctx.String("conversation"), ctx.String("service-name"), ctx.String("instance-name"), ctx.Bool("yaml"), out)
+		return err
+	},
+}
diff --git a/pkg/aiagent/view/view.go b/pkg/aiagent/view/view.go
new file mode 100644
index 0000000..94546c6
--- /dev/null
+++ b/pkg/aiagent/view/view.go
@@ -0,0 +1,132 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// Package view fetches the asz.view document of an AI agent conversation from the OAP's
+// GET /ai-agent/conversations/{conversation}/v1/view route. The route lives on the query
+// host beside /graphql, not on the admin host, because the document is what the UI reads;
+// it is streamed, since a long conversation renders to tens of megabytes, so the body is
+// copied through and never held whole.
+package view
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"io"
+	"mime"
+	"net/http"
+	"net/url"
+	"strings"
+
+	"github.com/apache/skywalking-cli/pkg/contextkey"
+	"github.com/apache/skywalking-cli/pkg/transport"
+)
+
+const (
+	// MediaTypeJSON names the document, its version a parameter: the route's default body.
+	MediaTypeJSON = "application/vnd.skywalking.asz.view+json"
+	// MediaTypeYAML is the same document as YAML, chosen by Accept.
+	MediaTypeYAML = "application/vnd.skywalking.asz.view+yaml"
+	// problemType is the route's error body, RFC 9457.
+	problemType = "application/problem+json"
+
+	defaultBaseURL = "http://127.0.0.1:12800/graphql"
+)
+
+// Problem is the route's error: an RFC 9457 problem document carrying the status.
+type Problem struct {
+	Type   string `json:"type"`
+	Title  string `json:"title"`
+	Status int    `json:"status"`
+	Detail string `json:"detail"`
+	URL    string `json:"-"`
+}
+
+func (p *Problem) Error() string {
+	if p.Detail != "" {
+		return fmt.Sprintf("%d %s: %s (%s)", p.Status, p.Title, p.Detail, p.URL)
+	}
+	return fmt.Sprintf("%d %s (%s)", p.Status, p.Title, p.URL)
+}
+
+// CoreURL is the root of the query host the route lives on, derived from the GraphQL
+// base URL by dropping its path: http://host:12800/graphql becomes http://host:12800.
+// A base URL that does not parse is returned trimmed, so the error surfaces on the call.
+func CoreURL(baseURL string) string {
+	trimmed := strings.TrimRight(strings.TrimSpace(baseURL), "/")
+	u, err := url.Parse(trimmed)
+	if err != nil || u.Host == "" {
+		return trimmed
+	}
+	return u.Scheme + "://" + u.Host
+}
+
+// Path is the route of one conversation.
+func Path(conversation string) string {
+	return "/ai-agent/conversations/" + url.PathEscape(conversation) + "/v1/view"
+}
+
+// Fetch streams the document of the conversation to out and returns the Content-Type
+// it came with. serviceName is required; instanceName narrows the read to one sender.
+// A non-2xx answer is returned as a *Problem when the OAP sent one.
+func Fetch(ctx context.Context, conversation, serviceName, instanceName string, yaml bool, out io.Writer) (string, error) {
+	query := url.Values{"service": {serviceName}}
+	if instanceName != "" {
+		query.Set("instance", instanceName)
+	}
+	full := CoreURL(transport.GetValue(ctx, contextkey.BaseURL{}, defaultBaseURL)) + Path(conversation) + "?" + query.Encode()
+
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, full, http.NoBody)
+	if err != nil {
+		return "", err
+	}
+	if yaml {
+		req.Header.Set("Accept", MediaTypeYAML)
+	} else {
+		req.Header.Set("Accept", MediaTypeJSON)
+	}
+	if authorization := transport.AuthHeader(ctx); authorization != "" {
+		req.Header.Set("Authorization", authorization)
+	}
+
+	resp, err := transport.HTTPClient(ctx).Do(req)
+	if err != nil {
+		return "", err
+	}
+	defer resp.Body.Close()
+
+	contentType := resp.Header.Get("Content-Type")
+	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+		return contentType, readError(resp, full)
+	}
+	_, err = io.Copy(out, resp.Body)
+	return contentType, err
+}
+
+// readError turns a non-2xx response into an error: the problem document when the OAP
+// sent one, otherwise the status and whatever the body says.
+func readError(resp *http.Response, full string) error {
+	body, _ := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
+	mediaType, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type"))
+	if mediaType == problemType {
+		problem := &Problem{URL: full}
+		if json.Unmarshal(body, problem) == nil && problem.Status != 0 {
+			return problem
+		}
+	}
+	return fmt.Errorf("%s: %s: %s", full, resp.Status, strings.TrimSpace(string(body)))
+}
diff --git a/pkg/aiagent/view/view_test.go b/pkg/aiagent/view/view_test.go
new file mode 100644
index 0000000..dadf616
--- /dev/null
+++ b/pkg/aiagent/view/view_test.go
@@ -0,0 +1,130 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package view
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/apache/skywalking-cli/pkg/contextkey"
+)
+
+func TestCoreURL(t *testing.T) {
+	cases := map[string]string{
+		"http://127.0.0.1:12800/graphql":   "http://127.0.0.1:12800",
+		"https://oap.example.com/graphql/": "https://oap.example.com",
+		"http://[::1]:12800/graphql":       "http://[::1]:12800",
+		"http://oap:12800":                 "http://oap:12800",
+		"not a url":                        "not a url",
+	}
+	for in, want := range cases {
+		if got := CoreURL(in); got != want {
+			t.Errorf("CoreURL(%q) = %q, want %q", in, got, want)
+		}
+	}
+}
+
+// server answers the route the way the OAP does: the document as JSON in several flushes or
+// as YAML by Accept, a problem document for anything else, and 418 for a wrong request.
+func server(t *testing.T, document []byte) *httptest.Server {
+	t.Helper()
+	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path != "/ai-agent/conversations/c 1/v1/view" ||
+			r.URL.Query().Get("service") != "agent" || r.URL.Query().Get("instance") != "sender" {
+			w.WriteHeader(http.StatusTeapot)
+			return
+		}
+		if r.Header.Get("Authorization") != "Basic dTpw" {
+			w.WriteHeader(http.StatusUnauthorized)
+			return
+		}
+		switch r.Header.Get("Accept") {
+		case MediaTypeYAML:
+			w.Header().Set("Content-Type", MediaTypeYAML+"; version=1.0")
+			_, _ = w.Write([]byte("format: asz.view\n"))
+		case MediaTypeJSON:
+			w.Header().Set("Content-Type", MediaTypeJSON+"; version=1.0")
+			for i := 0; i < len(document); i += 8192 {
+				end := i + 8192
+				if end > len(document) {
+					end = len(document)
+				}
+				_, _ = w.Write(document[i:end])
+				w.(http.Flusher).Flush()
+			}
+		default:
+			w.Header().Set("Content-Type", "application/problem+json")
+			w.WriteHeader(http.StatusNotFound)
+			_, _ = w.Write([]byte(`{"type":"about:blank","title":"Not Found","status":404,"detail":"no round"}`))
+		}
+	}))
+}
+
+func testContext(serverURL string) context.Context {
+	ctx := context.WithValue(context.Background(), contextkey.BaseURL{}, serverURL+"/graphql")
+	ctx = context.WithValue(ctx, contextkey.Username{}, "u")
+	return context.WithValue(ctx, contextkey.Password{}, "p")
+}
+
+func TestFetchStreamsTheDocument(t *testing.T) {
+	document := bytes.Repeat([]byte("{\"format\":\"asz.view\"}\n"), 4096)
+	srv := server(t, document)
+	defer srv.Close()
+
+	var out bytes.Buffer
+	contentType, err := Fetch(testContext(srv.URL), "c 1", "agent", "sender", false, &out)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if contentType != MediaTypeJSON+"; version=1.0" || !bytes.Equal(out.Bytes(), document) {
+		t.Fatalf("content type %q, %d bytes", contentType, out.Len())
+	}
+}
+
+func TestFetchAsksForYAML(t *testing.T) {
+	srv := server(t, nil)
+	defer srv.Close()
+
+	var out bytes.Buffer
+	contentType, err := Fetch(testContext(srv.URL), "c 1", "agent", "sender", true, &out)
+	if err != nil || contentType != MediaTypeYAML+"; version=1.0" || out.String() != "format: asz.view\n" {
+		t.Fatalf("%v, content type %q, body %q", err, contentType, out.String())
+	}
+}
+
+func TestAProblemDocumentIsTheError(t *testing.T) {
+	srv := server(t, nil)
+	defer srv.Close()
+
+	req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, srv.URL+Path("c 1")+"?service=agent&instance=sender", http.NoBody)
+	req.Header.Set("Authorization", "Basic dTpw")
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer resp.Body.Close()
+	var problem *Problem
+	if err := readError(resp, req.URL.String()); !errors.As(err, &problem) ||
+		problem.Status != 404 || problem.Detail != "no round" || problem.Title != "Not Found" {
+		t.Fatalf("problem: %v", err)
+	}
+}
diff --git a/pkg/graphql/aiagent/conversation.go b/pkg/graphql/aiagent/conversation.go
new file mode 100644
index 0000000..fce0246
--- /dev/null
+++ b/pkg/graphql/aiagent/conversation.go
@@ -0,0 +1,58 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// Package aiagent wraps the GraphQL queries of ai-agent-conversation.graphqls: the
+// list page and the raw-file export of the conversations the AI Sessionizer lands.
+// The conversation document itself is not a GraphQL query; see pkg/aiagent/view.
+package aiagent
+
+import (
+	"context"
+
+	"github.com/machinebox/graphql"
+	api "skywalking.apache.org/repo/goapi/query"
+
+	"github.com/apache/skywalking-cli/assets"
+	"github.com/apache/skywalking-cli/pkg/graphql/client"
+)
+
+// ListConversations lists one row per conversation of a service active in the duration,
+// newest first, from the newest round's attributes.
+func ListConversations(ctx context.Context, condition *api.ConversationListCondition, duration api.Duration) (api.ConversationList, error) {
+	var response map[string]api.ConversationList
+
+	request := graphql.NewRequest(assets.Read("graphqls/aiagent/ListConversations.graphql"))
+	request.Var("condition", condition)
+	request.Var("duration", duration)
+
+	err := client.ExecuteQuery(ctx, request, &response)
+	return response["result"], err
+}
+
+// RawFiles lists every landed file and round of a conversation as stored, or only the
+// named ones; with body, each file comes verbatim, which is the export path.
+func RawFiles(ctx context.Context, condition *api.ConversationCondition, files []string, body bool) (api.ConversationRawFiles, error) {
+	var response map[string]api.ConversationRawFiles
+
+	request := graphql.NewRequest(assets.Read("graphqls/aiagent/ConversationRawFiles.graphql"))
+	request.Var("condition", condition)
+	request.Var("files", files)
+	request.Var("body", body)
+
+	err := client.ExecuteQuery(ctx, request, &response)
+	return response["result"], err
+}
diff --git a/pkg/graphql/menu/menu.go b/pkg/graphql/menu/menu.go
index b46815c..41b5943 100644
--- a/pkg/graphql/menu/menu.go
+++ b/pkg/graphql/menu/menu.go
@@ -24,12 +24,24 @@
 	"github.com/apache/skywalking-cli/pkg/graphql/client"
 
 	"github.com/machinebox/graphql"
-
-	api "skywalking.apache.org/repo/goapi/query"
 )
 
-func GetItems(ctx context.Context) ([]*api.MenuItem, error) {
-	var response map[string][]*api.MenuItem
+// Item is one entry of the UI menu the OAP served before 11.0.0. The query protocol
+// retired getItems, so goapi no longer generates the type; the command stays for the
+// older backends and carries the shape itself.
+type Item struct {
+	Title        string  `json:"title"`
+	Icon         *string `json:"icon,omitempty"`
+	Layer        string  `json:"layer"`
+	Activate     bool    `json:"activate"`
+	SubItems     []*Item `json:"subItems"`
+	Description  *string `json:"description,omitempty"`
+	DocumentLink *string `json:"documentLink,omitempty"`
+	I18nKey      *string `json:"i18nKey,omitempty"`
+}
+
+func GetItems(ctx context.Context) ([]*Item, error) {
+	var response map[string][]*Item
 
 	request := graphql.NewRequest(assets.Read("graphqls/menu/GetItems.graphql"))