1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
//! Incremental, frontend-neutral faculty output.
//!
//! A handler pushes native content to the frontend as it becomes available.
//! There is no stored result, transport encoding, or endpoint selection here.
//! A finite frontend may explicitly collect parts; a long-lived frontend can
//! deliver each part without retaining the preceding output.
//!
//! This interface is synchronous: a slow emitter holds up the handler, and an
//! emitter error is returned to it immediately. Blocking CLI transport belongs
//! in the emitter. An asynchronous frontend such as MCP must invoke synchronous
//! handlers at its blocking-work boundary, constructing [`Out`] there, rather
//! than block a shared async executor. No runtime or queue is supplied here.
use anybytes::Bytes;
use anyhow::Result;
/// One ordered emission, with resident media bytes rather than an encoding.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Part {
/// Exact UTF-8 text; no separator is added by the output interface.
Text { text: String },
/// An image and its declared media type. Cloning shares its byte storage.
Image { bytes: Bytes, mime_type: String },
/// An audio clip and its declared media type. Cloning shares its storage.
Audio { bytes: Bytes, mime_type: String },
/// An exact binary export, not a request to display or play its contents.
/// The URI identifies the exported value; transports carry resident bytes.
/// CLI exports stay on stdout even when perception uses DRIVE_ENDPOINT;
/// MCP exports are file resources, never sensory input.
Blob {
bytes: Bytes,
mime_type: String,
uri: String,
},
}
/// A borrowed synchronous emitter, with no implicit buffering.
///
/// Handlers propagate emission errors with `?`. Previously accepted parts are
/// not rolled back, retried, or rerouted. The frontend decides what acceptance
/// means for its transport; it does not imply remote processing or durability.
/// An emitter can report cancellation at an emission, but this interface does
/// not interrupt a handler that is computing or waiting without emitting.
pub struct Out<'a> {
emit: &'a mut dyn FnMut(Part) -> Result<()>,
}
impl<'a> Out<'a> {
pub fn new(emit: &'a mut dyn FnMut(Part) -> Result<()>) -> Self {
Self { emit }
}
/// Deliver one part before returning; any backpressure or error is direct.
pub fn emit(&mut self, part: Part) -> Result<()> {
(self.emit)(part)
}
/// Emit text exactly, including any newlines supplied by the handler.
pub fn text(&mut self, text: impl Into<String>) -> Result<()> {
self.emit(Part::Text { text: text.into() })
}
/// Emit text followed by one newline, including for an empty line.
pub fn line(&mut self, line: impl Into<String>) -> Result<()> {
let mut text = line.into();
text.push('\n');
self.text(text)
}
pub fn image(&mut self, bytes: impl Into<Bytes>, mime_type: impl Into<String>) -> Result<()> {
self.emit(Part::Image {
bytes: bytes.into(),
mime_type: mime_type.into(),
})
}
pub fn audio(&mut self, bytes: impl Into<Bytes>, mime_type: impl Into<String>) -> Result<()> {
self.emit(Part::Audio {
bytes: bytes.into(),
mime_type: mime_type.into(),
})
}
/// Export bytes without interpreting them as text or a displayed modality.
pub fn blob(
&mut self,
bytes: impl Into<Bytes>,
mime_type: impl Into<String>,
uri: impl Into<String>,
) -> Result<()> {
self.emit(Part::Blob {
bytes: bytes.into(),
mime_type: mime_type.into(),
uri: uri.into(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn preserves_mixed_order_exact_text_and_line_endings() {
let image = Bytes::from(vec![0_u8, 0xff]);
let audio = Bytes::from(vec![1_u8, 2, 3]);
let mut parts = Vec::new();
{
let mut collect = |part| {
parts.push(part);
Ok(())
};
let mut output = Out::new(&mut collect);
output.line("first").unwrap();
output.image(image.clone(), "image/png").unwrap();
output.text("between").unwrap();
output.audio(audio.clone(), "audio/wav").unwrap();
output.line("").unwrap();
output.line("last\n").unwrap();
}
assert_eq!(
parts,
[
Part::Text {
text: "first\n".into()
},
Part::Image {
bytes: image.clone(),
mime_type: "image/png".into()
},
Part::Text {
text: "between".into()
},
Part::Audio {
bytes: audio.clone(),
mime_type: "audio/wav".into()
},
Part::Text { text: "\n".into() },
Part::Text {
text: "last\n\n".into()
},
]
);
let Part::Image { bytes, .. } = &parts[1] else {
panic!("expected image")
};
assert_eq!(bytes.as_ptr(), image.as_ptr(), "image storage is shared");
let Part::Audio { bytes, .. } = &parts[3] else {
panic!("expected audio")
};
assert_eq!(bytes.as_ptr(), audio.as_ptr(), "audio storage is shared");
}
#[test]
fn no_output_and_an_explicit_empty_text_part_remain_distinct() {
let mut parts = Vec::new();
{
let mut collect = |part| {
parts.push(part);
Ok(())
};
let _output = Out::new(&mut collect);
}
assert!(parts.is_empty());
{
let mut collect = |part| {
parts.push(part);
Ok(())
};
Out::new(&mut collect).text("").unwrap();
}
assert_eq!(
parts,
[Part::Text {
text: String::new()
}]
);
}
}