Skip to main content

objectiveai_sdk/cli/command/agents/logs/list/
mod.rs

1//! `agents logs read all` — fetch every log row for a target AIH,
2//! coalesced into [`ResponseItem`] blocks.
3//!
4//! Each yielded `ResponseItem` is either a single-row request blob
5//! (`AgentCompletionRequest` / `VectorCompletionRequest` /
6//! `FunctionExecutionRequest`) or a multi-row block
7//! (`ClientNotification` / `AssistantResponse` / `ToolResponse`)
8//! formed by joining consecutive `logs.messages` rows that share
9//! `(block_class, agent_instance_hierarchy, response_id)` —
10//! `ToolResponse` additionally splits per `tool_call_id`.
11//!
12//! `response_id` is carried on every variant — for the three
13//! request-blob variants it's the chunk's own id, for the three
14//! block variants it's the immediately-enclosing
15//! agent-completion's id (even when the underlying rows were
16//! emitted inside a function execution or vector completion
17//! chunk).
18
19use std::str::FromStr;
20
21use crate::cli::command::CommandRequest;
22use crate::cli::command::path_ref::tokenize;
23
24/// One queue-read target. Either direct `(parent, instance)` (parent
25/// defaults to the cli's own `Config.agent_instance_hierarchy` when
26/// omitted), a tag name the cli resolves at handler time, or `me`
27/// (the caller's own `Config.agent_instance_hierarchy`, with no child
28/// leaf appended). Shared with `agents read pending` via re-export.
29///
30/// Docker-style `key=value,key=value` wire form on the CLI:
31///   `--target instance=L`           (direct; parent defaults to ctx)
32///   `--target instance=L,parent=P`  (direct; explicit parent)
33///   `--target tag=T`                (tag; cli resolves via the tags tier)
34///   `--target me`                   (the caller's own AIH; bare valueless key)
35#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
36#[serde(tag = "by", rename_all = "snake_case")]
37#[schemars(rename = "cli.command.agents.logs.list.Target")]
38pub enum Target {
39    #[schemars(title = "Direct")]
40    Direct {
41        /// Optional lineage prefix. `None` ⇒ cli substitutes
42        /// `Config.agent_instance_hierarchy`.
43        #[serde(default, skip_serializing_if = "Option::is_none")]
44        #[schemars(extend("omitempty" = true))]
45        parent_agent_instance_hierarchy: Option<String>,
46        /// Leaf id of the target agent.
47        agent_instance: String,
48    },
49    #[schemars(title = "Tag")]
50    Tag { agent_tag: String },
51    /// The caller's own `Config.agent_instance_hierarchy`, verbatim.
52    /// CLI wire form is the bare valueless key `me` (docker
53    /// `readonly`-style), mutually exclusive with the other keys.
54    #[schemars(title = "Me")]
55    Me,
56}
57
58impl FromStr for Target {
59    type Err = String;
60    /// Parse a `--target` arg. Accepted forms: the bare valueless key
61    /// `me`; `instance` + optional `parent`; or `tag` alone. `tag` is
62    /// mutually exclusive with the other two keys.
63    fn from_str(s: &str) -> Result<Self, Self::Err> {
64        // Bare valueless key (docker `readonly`-style). Handled before
65        // `tokenize`, which requires every token to be `key=value`.
66        if s.trim() == "me" {
67            return Ok(Target::Me);
68        }
69        let mut tag: Option<String> = None;
70        let mut parent: Option<String> = None;
71        let mut instance: Option<String> = None;
72        for (k, v) in tokenize(s)? {
73            match k {
74                "tag" => tag = Some(v.to_string()),
75                "instance" => instance = Some(v.to_string()),
76                "parent" => parent = Some(v.to_string()),
77                other => return Err(format!("unknown key: {other}")),
78            }
79        }
80        match (tag, instance, parent) {
81            (Some(t), None, None) => Ok(Target::Tag { agent_tag: t }),
82            (Some(_), _, _) => Err(
83                "tag is mutually exclusive with instance and parent".to_string(),
84            ),
85            (None, Some(i), p) => Ok(Target::Direct {
86                parent_agent_instance_hierarchy: p,
87                agent_instance: i,
88            }),
89            (None, None, _) => Err("instance or tag is required".to_string()),
90        }
91    }
92}
93
94impl Target {
95    /// Inverse of [`FromStr::from_str`]: emit the docker-style
96    /// `key=value,key=value` wire form for round-tripping.
97    pub fn into_arg_string(&self) -> String {
98        match self {
99            Target::Me => "me".to_string(),
100            Target::Tag { agent_tag } => format!("tag={agent_tag}"),
101            Target::Direct {
102                parent_agent_instance_hierarchy: None,
103                agent_instance,
104            } => format!("instance={agent_instance}"),
105            Target::Direct {
106                parent_agent_instance_hierarchy: Some(p),
107                agent_instance,
108            } => format!("instance={agent_instance},parent={p}"),
109        }
110    }
111}
112
113#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
114#[schemars(rename = "cli.command.agents.logs.list.Request")]
115pub struct Request {
116    pub path_type: Path,
117    /// `true` = list only pending (unfinalized) rows; `false` = list
118    /// every logged row. Selected on the CLI by `--pending` / `--all`.
119    pub pending: bool,
120    pub targets: Vec<Target>,
121    /// Skip rows with `logs.messages."index" <= after_id`. Use the
122    /// highest `id` from a previous page to paginate forward.
123    #[serde(default, skip_serializing_if = "Option::is_none")]
124    #[schemars(extend("omitempty" = true))]
125    pub after_id: Option<i64>,
126    /// Cap on rows scanned per target. `None` = unlimited.
127    #[serde(default, skip_serializing_if = "Option::is_none")]
128    #[schemars(extend("omitempty" = true))]
129    pub limit: Option<i64>,
130    #[serde(flatten)]
131    pub base: crate::cli::command::RequestBase,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
135#[schemars(rename = "cli.command.agents.logs.list.Path")]
136pub enum Path {
137    #[serde(rename = "agents/logs/list")]
138    AgentsLogsList,
139}
140
141impl CommandRequest for Request {
142    fn request_base(&self) -> &crate::cli::command::RequestBase {
143        &self.base
144    }
145
146    fn request_base_mut(&mut self) -> Option<&mut crate::cli::command::RequestBase> {
147        Some(&mut self.base)
148    }
149}
150
151/// Type tag for one `ClientNotification` part — the table-kind of
152/// the underlying `message_queue_*` content row.
153#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
154#[serde(rename_all = "snake_case")]
155#[schemars(rename = "cli.command.agents.logs.list.ClientNotificationPartType")]
156pub enum ClientNotificationPartType {
157    Text,
158    Image,
159    Audio,
160    Video,
161    File,
162}
163
164/// One row inside a `ClientNotification` block — a consumed
165/// `message_queue_contents` entry. `queued_at` is on the
166/// enclosing block (it lives on `message_queue.enqueued_at`, not
167/// per-content); only the per-row consumption timestamp is here.
168#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
169#[schemars(rename = "cli.command.agents.logs.list.ClientNotificationPart")]
170pub struct ClientNotificationPart {
171    /// `logs.messages."index"` for this row. Pass to
172    /// `agents logs read id <n>` to fetch the consumed
173    /// `message_queue_contents` body.
174    pub id: i64,
175    /// `logs.messages."timestamp"` — when the receiver consumed
176    /// this content row and the LogWriter committed the
177    /// consumption event.
178    pub delivered_at: String,
179    pub r#type: ClientNotificationPartType,
180}
181
182/// One row inside an `AssistantResponse` block, tagged by the
183/// table-kind of the underlying `assistant_response_*` row.
184///
185/// The `ToolCall` variant inlines the call's metadata
186/// (`function_name` / `tool_call_id` / `tool_call_index`) so callers
187/// can dedupe and correlate without a per-row round-trip; its `id`
188/// addresses the same `assistant_response_tool_calls` row, which
189/// `agents logs read id <id>` returns as the call's `arguments`
190/// (text). Every other variant carries only `id` (the
191/// `logs.messages."index"` to pass to `agents logs read id`) and the
192/// delivery timestamp.
193#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
194#[serde(tag = "type", rename_all = "snake_case")]
195#[schemars(rename = "cli.command.agents.logs.list.AssistantResponsePart")]
196pub enum AssistantResponsePart {
197    #[schemars(title = "ToolCall")]
198    ToolCall {
199        /// `logs.messages."index"` for the tool-call row. Pass to
200        /// `agents logs read id <n>` to read the call's `arguments`
201        /// as text.
202        id: i64,
203        delivered_at: String,
204        /// `objectiveai.assistant_response_tool_calls.function_name`.
205        function_name: String,
206        /// The wire tool-call id this row carries.
207        tool_call_id: String,
208        /// The tool call's wire index within the assistant message's
209        /// `tool_calls[]`.
210        tool_call_index: i64,
211    },
212    #[schemars(title = "Refusal")]
213    Refusal { id: i64, delivered_at: String },
214    #[schemars(title = "Reasoning")]
215    Reasoning { id: i64, delivered_at: String },
216    #[schemars(title = "Text")]
217    Text { id: i64, delivered_at: String },
218    #[schemars(title = "Image")]
219    Image { id: i64, delivered_at: String },
220    #[schemars(title = "Audio")]
221    Audio { id: i64, delivered_at: String },
222    #[schemars(title = "Video")]
223    Video { id: i64, delivered_at: String },
224    #[schemars(title = "File")]
225    File { id: i64, delivered_at: String },
226}
227
228/// Type tag for one `ToolResponse` part — the table-kind of the
229/// underlying `tool_response_content_*` row. The tool-call linkage
230/// (`tool_call_id`) lives on the enclosing `ResponseItem::ToolResponse`
231/// block, not on the parts.
232#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
233#[serde(rename_all = "snake_case")]
234#[schemars(rename = "cli.command.agents.logs.list.ToolResponsePartType")]
235pub enum ToolResponsePartType {
236    Text,
237    Image,
238    Audio,
239    Video,
240    File,
241}
242
243/// One row inside a `ToolResponse` block.
244#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
245#[schemars(rename = "cli.command.agents.logs.list.ToolResponsePart")]
246pub struct ToolResponsePart {
247    /// `logs.messages."index"` for this row. Pass to
248    /// `agents logs read id <n>` for the typed body.
249    pub id: i64,
250    pub delivered_at: String,
251    pub r#type: ToolResponsePartType,
252}
253
254/// Type tag for one `RequestMessageUser` part — the kind of the
255/// underlying `request_message_user_content_*` row.
256#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
257#[serde(rename_all = "snake_case")]
258#[schemars(rename = "cli.command.agents.logs.list.RequestMessageUserPartType")]
259pub enum RequestMessageUserPartType {
260    Text,
261    Image,
262    Audio,
263    Video,
264    File,
265}
266
267/// One content part of a user request message. Its `id` addresses the
268/// stored content the same way a response part's `id` does — pass it
269/// to `agents logs read id <n>`.
270#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
271#[schemars(rename = "cli.command.agents.logs.list.RequestMessageUserPart")]
272pub struct RequestMessageUserPart {
273    /// `logs.messages."index"` for this row.
274    pub id: i64,
275    pub delivered_at: String,
276    pub r#type: RequestMessageUserPartType,
277}
278
279/// Type tag for one `VectorRequestChoices` choice part — the kind of
280/// the underlying `request_vector_choice_content_*` row.
281#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
282#[serde(rename_all = "snake_case")]
283#[schemars(rename = "cli.command.agents.logs.list.VectorRequestChoicePartType")]
284pub enum VectorRequestChoicePartType {
285    Text,
286    Image,
287    Audio,
288    Video,
289    File,
290}
291
292/// One content part of a vector-completion request choice. `id`
293/// addresses the stored content via `agents logs read id <n>`.
294#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
295#[schemars(rename = "cli.command.agents.logs.list.VectorRequestChoicePart")]
296pub struct VectorRequestChoicePart {
297    /// `logs.messages."index"` for this row.
298    pub id: i64,
299    pub delivered_at: String,
300    pub r#type: VectorRequestChoicePartType,
301}
302
303/// One choice in a `VectorRequestChoices` block: this agent's inline
304/// voting `key` for the choice plus the choice's content parts (in
305/// request order). The key is returned inline — no `read id` needed.
306#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
307#[schemars(rename = "cli.command.agents.logs.list.VectorRequestChoice")]
308pub struct VectorRequestChoice {
309    /// The prefix-tree voting key this agent assigned to the choice.
310    pub key: String,
311    pub parts: Vec<VectorRequestChoicePart>,
312}
313
314/// One yielded item. Three single-row request blobs +
315/// three multi-row blocks. Every variant carries `response_id`.
316/// `sender_agent_instance_hierarchy` appears only on the four
317/// variants that have a sender ≠ producer: the three request
318/// variants (caller AIH) and `ClientNotification` (enqueuer
319/// AIH). `AssistantResponse` and `ToolResponse` are emitted BY
320/// the agent itself — their `agent_instance_hierarchy` IS the
321/// producer, so no separate sender field exists.
322///
323/// Block-coalescing boundary tuple: `(class,
324/// agent_instance_hierarchy, response_id)` for `AssistantResponse`;
325/// `(class, agent_instance_hierarchy, response_id, tool_call_id)`
326/// for `ToolResponse` (one block per tool call); `(class,
327/// agent_instance_hierarchy, response_id, sender, message_queue_id)`
328/// for `ClientNotification` blocks.
329/// One `ClientNotification` block = one consumed
330/// `message_queue` parent row, so `queued_at` and
331/// `sender_agent_instance_hierarchy` are well-defined
332/// block-level.
333#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
334#[serde(tag = "type", rename_all = "snake_case")]
335#[schemars(rename = "cli.command.agents.logs.list.ResponseItem")]
336pub enum ResponseItem {
337    /// A `user`-role message from the request/task input, unpacked
338    /// into content parts (each addressable via `read id`).
339    #[schemars(title = "RequestMessageUser")]
340    RequestMessageUser {
341        agent_instance_hierarchy: String,
342        response_id: String,
343        parts: Vec<RequestMessageUserPart>,
344    },
345    /// An `assistant`-role message from the request/task input. Same
346    /// part shape as an assistant response — reasoning / tool calls /
347    /// refusal / content — but sourced from the request.
348    #[schemars(title = "RequestMessageAssistant")]
349    RequestMessageAssistant {
350        agent_instance_hierarchy: String,
351        response_id: String,
352        parts: Vec<AssistantResponsePart>,
353    },
354    /// A `tool`-role message from the request/task input, answering a
355    /// prior tool call. Same part shape as a tool response.
356    #[schemars(title = "RequestMessageTool")]
357    RequestMessageTool {
358        agent_instance_hierarchy: String,
359        response_id: String,
360        /// The wire tool-call id this request message answers.
361        tool_call_id: String,
362        parts: Vec<ToolResponsePart>,
363    },
364    /// The response choices a vector-completion task voted over,
365    /// yielded as one block: an ordered list of choices, each with its
366    /// content parts and this agent's inline voting `key`.
367    #[schemars(title = "VectorRequestChoices")]
368    VectorRequestChoices {
369        agent_instance_hierarchy: String,
370        response_id: String,
371        choices: Vec<VectorRequestChoice>,
372    },
373    /// The closer for a function-execution vector task: this agent's
374    /// own vote (its score for each choice, in choice order). Inline —
375    /// no `read id` needed.
376    #[schemars(title = "VectorResponseVote")]
377    VectorResponseVote {
378        agent_instance_hierarchy: String,
379        response_id: String,
380        #[serde(deserialize_with = "crate::serde_util::vec_decimal")]
381        #[schemars(with = "Vec<f64>")]
382        vote: Vec<rust_decimal::Decimal>,
383    },
384    #[schemars(title = "ClientNotification")]
385    ClientNotification {
386        agent_instance_hierarchy: String,
387        /// AIH of the enqueuer — from `message_queue.sender_*`
388        /// joined through `message_queue_contents.id`.
389        sender_agent_instance_hierarchy: String,
390        response_id: String,
391        /// `message_queue.enqueued_at` of the consumed parent
392        /// queue row. One block = one parent queue row, so this
393        /// is well-defined block-level (each part's individual
394        /// `delivered_at` still records its own
395        /// consumption moment).
396        queued_at: String,
397        /// Idempotency token, if the row was enqueued with
398        /// `--key` via `agents message --enqueue-with-key`.
399        /// Surfacing it lets readers attribute a notification
400        /// to a specific enqueue beyond just the sender AIH.
401        #[serde(default, skip_serializing_if = "Option::is_none")]
402        #[schemars(extend("omitempty" = true))]
403        key: Option<String>,
404        parts: Vec<ClientNotificationPart>,
405    },
406    /// Agent emissions — the agent IS the producer of these
407    /// rows, so there's no separate sender. The
408    /// `agent_instance_hierarchy` field IS the sender.
409    #[schemars(title = "AssistantResponse")]
410    AssistantResponse {
411        agent_instance_hierarchy: String,
412        response_id: String,
413        parts: Vec<AssistantResponsePart>,
414    },
415    /// One tool call's response. Blocks are grouped per
416    /// `tool_call_id` (in addition to `agent_instance_hierarchy` +
417    /// `response_id`), so two responses in the same turn yield two
418    /// blocks.
419    #[schemars(title = "ToolResponse")]
420    ToolResponse {
421        agent_instance_hierarchy: String,
422        response_id: String,
423        /// The wire tool-call id this response answers.
424        tool_call_id: String,
425        parts: Vec<ToolResponsePart>,
426    },
427    /// One logged failure (`objectiveai.errors`) — a spawn-path error
428    /// persisted into the agent's history. Always a single row: the
429    /// failing attempt dies at its first raised error, so there is
430    /// nothing to coalesce. The value is returned inline (like the
431    /// vote) — no `read id` needed — but the row is still
432    /// id-addressable like every other event.
433    #[schemars(title = "Error")]
434    Error {
435        agent_instance_hierarchy: String,
436        /// The response the failure belongs to when one existed;
437        /// `None` for post-lock pre-stream failures (the only kind
438        /// allowed a NULL response_id).
439        #[serde(default, skip_serializing_if = "Option::is_none")]
440        #[schemars(extend("omitempty" = true))]
441        response_id: Option<String>,
442        /// `logs.messages."index"` for this row.
443        id: i64,
444        delivered_at: String,
445        /// The CLI's user-facing error value — a structured object for
446        /// API response errors, a plain string otherwise.
447        error: serde_json::Value,
448    },
449}
450
451#[derive(clap::Args)]
452#[command(group(
453    clap::ArgGroup::new("logs_list_mode")
454        .required(true)
455        .multiple(false)
456        .args(["all", "pending"])
457))]
458pub struct Args {
459    /// List every logged row. Mutually exclusive with `--pending`;
460    /// exactly one of the two is required.
461    #[arg(long)]
462    pub all: bool,
463    /// List only pending (unfinalized) rows. Mutually exclusive with
464    /// `--all`; exactly one of the two is required.
465    #[arg(long)]
466    pub pending: bool,
467    /// One or more `--target instance=L[,parent=P]` entries. `parent`
468    /// defaults to the cli's own `Config.agent_instance_hierarchy`
469    /// when omitted on an individual target. Also accepts
470    /// `--target tag=T` and `--target me` (the caller's own AIH).
471    #[arg(long = "target", required = true)]
472    pub targets: Vec<String>,
473    /// Skip rows with `logs.messages."index" <= after_id` per target.
474    #[arg(long)]
475    pub after_id: Option<i64>,
476    /// Cap on rows scanned per target.
477    #[arg(long)]
478    pub limit: Option<i64>,
479    #[command(flatten)]
480    pub base: crate::cli::command::RequestBaseArgs,
481}
482
483#[derive(clap::Args)]
484#[command(args_conflicts_with_subcommands = true)]
485pub struct Command {
486    #[command(flatten)]
487    pub args: Args,
488    #[command(subcommand)]
489    pub schema: Option<Schema>,
490}
491
492#[derive(clap::Subcommand)]
493pub enum Schema {
494    /// Emit the JSON Schema for this leaf's `Request` type and exit.
495    RequestSchema(request_schema::Args),
496    /// Emit the JSON Schema for this leaf's `Response` type and exit.
497    ResponseSchema(response_schema::Args),
498}
499
500impl TryFrom<Args> for Request {
501    type Error = crate::cli::command::FromArgsError;
502    fn try_from(args: Args) -> Result<Self, Self::Error> {
503        let targets = args
504            .targets
505            .iter()
506            .map(|s| {
507                s.parse::<Target>().map_err(|msg| {
508                    crate::cli::command::FromArgsError::path_parse("target", msg)
509                })
510            })
511            .collect::<Result<Vec<_>, _>>()?;
512        Ok(Self {
513            path_type: Path::AgentsLogsList,
514            // The `logs_list_mode` group guarantees exactly one of
515            // `--all` / `--pending`, so `pending` is the full mode.
516            pending: args.pending,
517            targets,
518            after_id: args.after_id,
519            limit: args.limit,
520            base: args.base.into(),
521        })
522    }
523}
524
525#[cfg(feature = "cli-executor")]
526pub async fn execute<E: crate::cli::command::CommandExecutor>(
527    executor: &E,
528    mut request: Request,
529
530        agent_arguments: Option<&crate::cli::command::AgentArguments>,
531    ) -> Result<E::Stream<ResponseItem>, E::Error> {
532    request.base.clear_transform();
533    executor.execute(request, agent_arguments).await
534}
535
536#[cfg(feature = "cli-executor")]
537pub async fn execute_transform<E: crate::cli::command::CommandExecutor>(
538    executor: &E,
539    mut request: Request,
540    transform: crate::cli::command::Transform,
541
542        agent_arguments: Option<&crate::cli::command::AgentArguments>,
543    ) -> Result<E::Stream<serde_json::Value>, E::Error> {
544    request.base.set_transform(transform);
545    executor.execute(request, agent_arguments).await
546}
547
548#[cfg(feature = "mcp")]
549impl crate::cli::command::CommandResponse for ResponseItem {
550    fn into_mcp(self) -> crate::cli::command::McpResponseItem {
551        crate::cli::command::McpResponseItem::JSONL(serde_json::to_value(self).unwrap())
552    }
553}
554
555pub mod request_schema;
556
557pub mod response_schema;
558
559#[cfg(test)]
560mod tests {
561    use super::Target;
562
563    #[test]
564    fn me_target_parses_and_round_trips() {
565        assert_eq!("me".parse::<Target>(), Ok(Target::Me));
566        // Surrounding whitespace is trimmed, same as the other forms.
567        assert_eq!("  me  ".parse::<Target>(), Ok(Target::Me));
568        assert_eq!(Target::Me.into_arg_string(), "me");
569    }
570
571    #[test]
572    fn me_is_mutually_exclusive_with_other_keys() {
573        // `me` combined with any other key falls through to the
574        // `key=value` tokenizer and is rejected.
575        assert!("me,instance=x".parse::<Target>().is_err());
576    }
577
578    #[test]
579    fn json_wire_shape_is_by_me() {
580        assert_eq!(
581            serde_json::to_value(Target::Me).unwrap(),
582            serde_json::json!({ "by": "me" }),
583        );
584    }
585}
586
587/// One `/listen` broadcast run of `agents logs list`: the actual
588/// [`Request`], the producer's
589/// [`AgentArguments`](crate::cli::command::AgentArguments), and the
590/// response-item stream. See [`crate::cli::websocket_listener`].
591#[cfg(feature = "cli-listener")]
592pub struct ListenerExecution {
593    pub request: Request,
594    pub agent_arguments: crate::cli::command::AgentArguments,
595    pub response: crate::cli::websocket_listener::ResponseItemStream<ResponseItem>,
596}