use crate::schema::wit::direct::{self, FromWire, IntoWire, WireSchema};
use crate::schema::wit::wire::SchemaValueTree;
use crate::schema::{
FromSchema, FromSchemaError, IntoSchema, MetadataEnvelope, SchemaBuilder, SchemaType,
SchemaValue, SchemaValueStream, TypeId,
};
use std::future::Future;
use std::pin::Pin;
type RawReader = wit_bindgen::StreamReader<SchemaValueTree>;
type RawWriter = wit_bindgen::StreamWriter<SchemaValueTree>;
pub struct AgentStream<T> {
stream: SchemaValueStream,
decode: Box<dyn Fn(SchemaValueTree) -> Result<T, String> + Send + Sync>,
}
type EncodeFuture = Pin<Box<dyn Future<Output = Result<SchemaValueTree, String>>>>;
pub struct AgentStreamWriter<T> {
raw: RawWriter,
encode: Box<dyn Fn(T) -> EncodeFuture + Send + Sync>,
}
impl<T> AgentStream<T> {
pub fn new_with_wire_codecs<F: Future<Output = Result<SchemaValueTree, String>> + 'static>(
encode: impl Fn(T) -> F + Send + Sync + 'static,
decode: impl Fn(SchemaValueTree) -> Result<T, String> + Send + Sync + 'static,
) -> (AgentStreamWriter<T>, Self) {
let (writer, reader) = crate::schema::wit::new_schema_value_stream();
(
AgentStreamWriter {
raw: writer,
encode: Box::new(move |value| Box::pin(encode(value))),
},
Self {
stream: SchemaValueStream::from_native(reader),
decode: Box::new(decode),
},
)
}
pub fn new_with_codecs(
encode: fn(T) -> Result<SchemaValue, String>,
decode: fn(SchemaValue) -> Result<T, String>,
) -> (AgentStreamWriter<T>, Self)
where
T: 'static,
{
Self::new_with_wire_codecs(
move |value| {
let value = encode(value);
async move {
crate::schema::wit::encode_value_async(&value?)
.await
.map_err(|e| format!("failed to encode agent stream item: {e}"))
}
},
move |tree| {
crate::schema::wit::decode_value(tree)
.map_err(|e| format!("failed to decode agent stream item: {e}"))
.and_then(decode)
},
)
}
pub fn from_schema_stream_with_wire_decoder(
stream: SchemaValueStream,
decode: impl Fn(SchemaValueTree) -> Result<T, String> + Send + Sync + 'static,
) -> Self {
Self {
stream,
decode: Box::new(decode),
}
}
pub fn from_schema_stream(
stream: SchemaValueStream,
decode: fn(SchemaValue) -> Result<T, String>,
) -> Self
where
T: 'static,
{
Self::from_schema_stream_with_wire_decoder(stream, move |tree| {
crate::schema::wit::decode_value(tree)
.map_err(|e| format!("failed to decode agent stream item: {e}"))
.and_then(decode)
})
}
pub fn into_schema_stream(self) -> SchemaValueStream {
self.stream
}
#[doc(hidden)]
pub async fn into_raw(self) -> Result<RawReader, String> {
self.stream.take_native().await
}
pub async fn next(&mut self) -> Result<Option<T>, String> {
match self.stream.next_wire().await? {
Some(tree) => (self.decode)(tree).map(Some),
None => Ok(None),
}
}
pub async fn collect(mut self) -> Result<Vec<T>, String> {
let mut result = Vec::new();
while let Some(item) = self.next().await? {
result.push(item);
}
Ok(result)
}
}
impl<T: IntoWire + FromWire + 'static> AgentStream<T> {
pub fn new() -> (AgentStreamWriter<T>, Self) {
Self::new_with_wire_codecs(
|value| async move {
direct::encode_async(&value)
.await
.map_err(|e| e.to_string())
},
|tree| direct::decode(tree).map_err(|e| e.to_string()),
)
}
}
impl<T: FromWire> AgentStream<T> {
#[doc(hidden)]
pub fn from_raw(raw: RawReader) -> Self {
Self {
stream: SchemaValueStream::from_native(raw),
decode: Box::new(|tree| direct::decode(tree).map_err(|e| e.to_string())),
}
}
}
impl<T> AgentStreamWriter<T> {
pub async fn write_one(&mut self, value: T) -> Result<(), String> {
let value = (self.encode)(value).await?;
if self.raw.write_one(value).await.is_none() {
Ok(())
} else {
Err("agent stream reader was dropped".to_string())
}
}
pub async fn write_all(&mut self, values: impl IntoIterator<Item = T>) -> Result<(), String> {
for value in values {
self.write_one(value).await?;
}
Ok(())
}
}
impl<T: IntoSchema> IntoSchema for AgentStream<T> {
fn type_id() -> TypeId {
TypeId::new(format!("golem.AgentStream<{}>", T::type_id()))
}
fn register_in(builder: &mut SchemaBuilder) -> SchemaType {
SchemaType::Stream {
inner: Some(Box::new(T::register_in(builder))),
metadata: MetadataEnvelope::default(),
}
}
fn to_value(&self) -> SchemaValue {
SchemaValue::Stream(self.stream.clone())
}
}
impl<T: FromSchema> FromSchema for AgentStream<T> {
fn from_value(value: &SchemaValue) -> Result<Self, FromSchemaError> {
match value {
SchemaValue::Stream(stream) => Ok(Self {
stream: stream.clone(),
decode: Box::new(|tree| {
let value =
crate::schema::wit::decode_value(tree).map_err(|e| e.to_string())?;
T::from_value(&value).map_err(|e| e.to_string())
}),
}),
other => Err(FromSchemaError::shape_mismatch(
"stream",
crate::schema::conversion::value_kind(other),
"AgentStream",
)),
}
}
}
impl<T: WireSchema> WireSchema for AgentStream<T> {
fn contains_stream(_: &mut std::collections::HashSet<&'static str>) -> bool {
true
}
fn append_schema(builder: &mut direct::WireSchemaBuilder) -> i32 {
let inner = T::append_schema(builder);
builder.push(crate::schema::wit::wire::SchemaTypeBody::StreamType(Some(
inner,
)))
}
}
impl<T: FromWire> FromWire for AgentStream<T> {
fn read_wire(reader: &mut direct::WireReader, index: i32) -> Result<Self, direct::WireError> {
Ok(Self {
stream: SchemaValueStream::read_wire(reader, index)?,
decode: Box::new(|tree| direct::decode(tree).map_err(|e| e.to_string())),
})
}
}
impl<T> IntoWire for AgentStream<T> {
fn preflight(&self, resources: &mut direct::WirePreflight) -> Result<(), direct::WireError> {
self.stream.preflight(resources)
}
async fn prepare_wire(&self) -> Result<(), direct::WireError> {
self.stream.prepare_wire().await
}
fn write_wire(&self, writer: &mut direct::WireWriter) -> Result<i32, direct::WireError> {
self.stream.write_wire(writer)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::wit::wire::{
ResultSpec, ResultValuePayload, SchemaTypeBody, SchemaValueNode,
};
use test_r::test;
#[derive(Debug, PartialEq, crate::FromWire, crate::IntoWire)]
struct Item {
entries: Vec<Result<Option<u32>, String>>,
}
struct Items(std::collections::VecDeque<SchemaValueTree>);
impl crate::schema::stream::SchemaValueStreamSource for Items {
fn next(
&mut self,
) -> Pin<Box<dyn Future<Output = Result<Option<SchemaValueTree>, String>> + Send + '_>>
{
Box::pin(async { Ok(self.0.pop_front()) })
}
}
#[test]
async fn direct_stream_items_reject_aliases_and_continue_after_errors() {
let item = Item {
entries: vec![Ok(Some(19)), Err("bad".to_string()), Ok(None)],
};
let mut stream = AgentStream::<Item>::from_schema_stream_with_wire_decoder(
SchemaValueStream::from_source(Items(
[
SchemaValueTree {
value_nodes: vec![
SchemaValueNode::U32Value(19),
SchemaValueNode::OptionValue(Some(0)),
SchemaValueNode::ResultValue(ResultValuePayload::OkValue(Some(1))),
SchemaValueNode::ListValue(vec![2, 2]),
SchemaValueNode::RecordValue(vec![3]),
],
root: 4,
},
direct::encode(&item).unwrap(),
]
.into(),
)),
|tree| direct::decode(tree).map_err(|e| e.to_string()),
);
assert_eq!(
stream.next().await.unwrap_err(),
"wire value index 2 is referenced more than once"
);
assert_eq!(stream.next().await.unwrap(), Some(item));
assert_eq!(stream.next().await.unwrap(), None);
}
#[test]
fn direct_stream_schema_and_forwarding_preserve_payload_and_ownership() {
let graph = direct::schema::<AgentStream<Result<u32, String>>>();
let SchemaTypeBody::StreamType(Some(inner)) = graph.type_nodes[graph.root as usize].body
else {
panic!("expected typed stream");
};
let SchemaTypeBody::ResultType(ResultSpec {
ok: Some(ok),
err: Some(err),
}) = graph.type_nodes[inner as usize].body
else {
panic!("expected result stream item");
};
assert!(matches!(
graph.type_nodes[ok as usize].body,
SchemaTypeBody::U32Type(None)
));
assert!(matches!(
graph.type_nodes[err as usize].body,
SchemaTypeBody::StringType
));
let stream = direct::decode::<AgentStream<Item>>(SchemaValueTree {
value_nodes: vec![SchemaValueNode::StreamValue(unsafe {
crate::schema::wit::wire::SchemaValueStream::from_handle(59)
})],
root: 0,
})
.unwrap();
let forwarded = direct::encode(&stream).unwrap();
assert!(matches!(
direct::encode(&stream),
Err(direct::WireError::ConsumedResource("stream"))
));
for node in forwarded.value_nodes {
if let SchemaValueNode::StreamValue(handle) = node {
assert_eq!(handle.take_handle(), 59);
} else {
panic!("unexpected stream encoding");
}
}
}
}