Skip to main content

objectiveai_sdk/cli/command/
command_response.rs

1/// Convert a typed CLI response item into its MCP-shaped projection —
2/// either a media [`ContentBlock`] for MCP tool results or a JSONL
3/// `serde_json::Value` for the line-oriented stream wire format.
4/// Implementors are the per-leaf response shapes in the surrounding
5/// tree (`agents::spawn::ResponseItem`, `tools::run::ResponseItem`, …)
6/// — not yet wired up; the trait exists to anchor the contract.
7#[cfg(feature = "mcp")]
8pub trait CommandResponse {
9    fn into_mcp(self) -> McpResponseItem;
10}
11
12/// MCP-side projection of one CLI response item.
13///
14/// - [`McpResponseItem::Media`] carries a typed [`ContentBlock`] for
15///   image / audio / file payloads that ride the MCP tool result as
16///   real media blocks.
17/// - [`McpResponseItem::JSONL`] carries a `serde_json::Value` for the
18///   line-oriented JSONL wire format every other leaf emits.
19///
20/// Not serde-shaped on purpose: this is the runtime carrier between
21/// `CommandResponse::into_mcp` and the formatter; nothing on either
22/// side needs to round-trip it through JSON.
23#[cfg(feature = "mcp")]
24#[derive(Debug, Clone)]
25pub enum McpResponseItem {
26    Media(crate::mcp::tool::ContentBlock),
27    JSONL(serde_json::Value),
28}
29
30// Bare-media leaf impls. Every log media leaf under
31// `cli/command/logs/agents/completions/{request,response}/messages/.../{get,subscribe}`
32// declares `pub type Response = <media type>` for one of the four media
33// types below, so these four impls cover all 20 such leaves through the
34// type aliases. Each `self.into()` reaches the matching
35// `From<T> for ContentBlock` impl in `mcp/tool/content_block.rs`.
36
37#[cfg(feature = "mcp")]
38impl CommandResponse for crate::agent::completions::message::ImageUrl {
39    fn into_mcp(self) -> McpResponseItem {
40        McpResponseItem::Media(self.into())
41    }
42}
43
44#[cfg(feature = "mcp")]
45impl CommandResponse for crate::agent::completions::message::InputAudio {
46    fn into_mcp(self) -> McpResponseItem {
47        McpResponseItem::Media(self.into())
48    }
49}
50
51#[cfg(feature = "mcp")]
52impl CommandResponse for crate::agent::completions::message::VideoUrl {
53    fn into_mcp(self) -> McpResponseItem {
54        McpResponseItem::Media(self.into())
55    }
56}
57
58#[cfg(feature = "mcp")]
59impl CommandResponse for crate::agent::completions::message::File {
60    fn into_mcp(self) -> McpResponseItem {
61        McpResponseItem::Media(self.into())
62    }
63}
64
65// `RichContentPart` is a sum over the four media types above plus
66// `Text(String)`. Used directly as the `agents queue read id` leaf's
67// `Response` type so the queue-content reader's wire shape is bit-
68// identical to a rich-content part. Each arm delegates to the inner
69// type's impl: media variants pick up
70// `McpResponseItem::Media(ContentBlock::…)`, `Text` picks up
71// `Value::String` via the `String` impl below.
72#[cfg(feature = "mcp")]
73impl CommandResponse for crate::agent::completions::message::RichContentPart {
74    fn into_mcp(self) -> McpResponseItem {
75        use crate::agent::completions::message::RichContentPart;
76        match self {
77            RichContentPart::Text { text } => text.into_mcp(),
78            RichContentPart::ImageUrl { image_url } => image_url.into_mcp(),
79            RichContentPart::InputAudio { input_audio } => input_audio.into_mcp(),
80            RichContentPart::InputVideo { video_url } => video_url.into_mcp(),
81            RichContentPart::VideoUrl { video_url } => video_url.into_mcp(),
82            RichContentPart::File { file } => file.into_mcp(),
83        }
84    }
85}
86
87// JSONL passthrough — `serde_json::to_value(self)` is the body for
88// every alias target whose leaf simply emits its serde-shaped value
89// as one JSONL line. Special-cased shortcuts: `String` constructs
90// `Value::String` directly; `serde_json::Value` rides through as-is;
91// `Option<T>` delegates to `T::into_mcp` on `Some` and emits a JSONL
92// null on `None`.
93
94#[cfg(feature = "mcp")]
95impl CommandResponse for String {
96    fn into_mcp(self) -> McpResponseItem {
97        McpResponseItem::JSONL(serde_json::Value::String(self))
98    }
99}
100
101#[cfg(feature = "mcp")]
102impl CommandResponse for serde_json::Value {
103    fn into_mcp(self) -> McpResponseItem {
104        McpResponseItem::JSONL(self)
105    }
106}
107
108#[cfg(feature = "mcp")]
109impl CommandResponse for schemars::Schema {
110    fn into_mcp(self) -> McpResponseItem {
111        McpResponseItem::JSONL(serde_json::to_value(self).unwrap())
112    }
113}
114
115// The `agents`/`functions`/`functions profiles`/`swarms` `list` leaves all
116// alias their `ResponseItem` to [`crate::RemotePath`], so the shared JSONL
117// projection lives here — one impl for every aliasing leaf (a per-leaf impl
118// would be a duplicate `impl … for RemotePath`).
119#[cfg(feature = "mcp")]
120impl CommandResponse for crate::RemotePath {
121    fn into_mcp(self) -> McpResponseItem {
122        McpResponseItem::JSONL(serde_json::to_value(self).unwrap())
123    }
124}
125
126#[cfg(feature = "mcp")]
127impl CommandResponse for crate::cli::command::ResponseSchema {
128    fn into_mcp(self) -> McpResponseItem {
129        self.0.into_mcp()
130    }
131}
132
133#[cfg(feature = "mcp")]
134impl<T: CommandResponse> CommandResponse for Option<T> {
135    fn into_mcp(self) -> McpResponseItem {
136        match self {
137            Some(v) => v.into_mcp(),
138            None => McpResponseItem::JSONL(serde_json::Value::Null),
139        }
140    }
141}
142
143#[cfg(feature = "mcp")]
144impl<T: CommandResponse> CommandResponse for Result<T, crate::cli::Error> {
145    fn into_mcp(self) -> McpResponseItem {
146        match self {
147            Ok(v) => v.into_mcp(),
148            Err(e) => e.into_mcp(),
149        }
150    }
151}
152
153#[cfg(feature = "mcp")]
154impl CommandResponse for crate::agent::completions::message::AssistantToolCallDelta {
155    fn into_mcp(self) -> McpResponseItem {
156        McpResponseItem::JSONL(
157            serde_json::to_value(self).unwrap(),
158        )
159    }
160}
161
162#[cfg(feature = "mcp")]
163impl CommandResponse for crate::agent::completions::message::AssistantToolCall {
164    fn into_mcp(self) -> McpResponseItem {
165        McpResponseItem::JSONL(
166            serde_json::to_value(self).unwrap(),
167        )
168    }
169}
170
171#[cfg(feature = "mcp")]
172impl CommandResponse for crate::agent::completions::response::Logprobs {
173    fn into_mcp(self) -> McpResponseItem {
174        McpResponseItem::JSONL(
175            serde_json::to_value(self).unwrap(),
176        )
177    }
178}
179
180#[cfg(feature = "mcp")]
181impl CommandResponse for crate::Remote {
182    fn into_mcp(self) -> McpResponseItem {
183        McpResponseItem::JSONL(
184            serde_json::to_value(self).unwrap(),
185        )
186    }
187}
188
189#[cfg(feature = "mcp")]
190impl CommandResponse for crate::vector::completions::request::VectorCompletionCreateParams {
191    fn into_mcp(self) -> McpResponseItem {
192        McpResponseItem::JSONL(
193            serde_json::to_value(self).unwrap(),
194        )
195    }
196}
197