Skip to main content

golem_rust/tool/
tool_middleware.rs

1// Copyright 2024-2026 Golem Cloud
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#[cfg(any(test, feature = "export_golem_agentic"))]
16use super::wire;
17use crate::TypedSchemaValue;
18#[cfg(any(test, feature = "export_golem_agentic"))]
19use crate::decode_typed_schema_value_owned;
20#[cfg(test)]
21use crate::encode_typed_schema_value_owned;
22use crate::schema::{FromSchema, IntoSchema};
23use std::error::Error;
24use std::fmt::{Display, Formatter};
25use std::future::Future;
26use std::pin::Pin;
27use std::rc::Rc;
28
29/// Readable byte stream used for tool stdin and tool outputs.
30#[cfg(not(feature = "export_golem_agentic"))]
31pub type InputStream = wit_bindgen::StreamReader<
32    Result<Vec<u8>, crate::bindings::golem::tool::streams::ByteStreamFailure>,
33>;
34
35/// Writable byte stream supplied to middleware for a tool output.
36#[cfg(feature = "export_golem_agentic")]
37pub type OutputStream = crate::agentic::OutputStream;
38#[cfg(not(feature = "export_golem_agentic"))]
39#[doc(hidden)]
40pub struct OutputStream;
41#[cfg(feature = "export_golem_agentic")]
42pub type InputStream = wit_bindgen::StreamReader<
43    Result<Vec<u8>, crate::golem_agentic::golem::tool::streams::ByteStreamFailure>,
44>;
45
46/// Successful result of a tool invocation.
47pub struct InvocationResult {
48    pub result: Option<TypedSchemaValue>,
49    pub stdout: Option<InputStream>,
50    pub stderr: Option<InputStream>,
51}
52
53#[doc(hidden)]
54pub type ToolMiddlewareInvokeFutureFor<'a> = Pin<
55    Box<dyn Future<Output = Result<InvocationResult, ToolInvokeError<RawCustomToolError>>> + 'a>,
56>;
57
58#[doc(hidden)]
59pub type ToolMiddlewareInvokeFuture = ToolMiddlewareInvokeFutureFor<'static>;
60
61/// A custom tool error whose payload is decoded only on explicit inspection.
62#[derive(Clone)]
63pub struct RawCustomToolError {
64    pub name: String,
65    payload: Rc<RawCustomToolPayload>,
66}
67
68struct RawCustomToolPayload {
69    wire: std::cell::RefCell<Option<crate::schema::wit::wire::TypedSchemaValue>>,
70    decoded: std::cell::OnceCell<Result<TypedSchemaValue, String>>,
71}
72
73impl RawCustomToolError {
74    pub fn from_payload(name: String, payload: TypedSchemaValue) -> Self {
75        Self {
76            name,
77            payload: Rc::new(RawCustomToolPayload {
78                wire: std::cell::RefCell::new(None),
79                decoded: std::cell::OnceCell::from(Ok(payload)),
80            }),
81        }
82    }
83
84    pub fn from_wire(name: String, payload: crate::schema::wit::wire::TypedSchemaValue) -> Self {
85        Self {
86            name,
87            payload: Rc::new(RawCustomToolPayload {
88                wire: std::cell::RefCell::new(Some(payload)),
89                decoded: std::cell::OnceCell::new(),
90            }),
91        }
92    }
93
94    /// Materializes the dynamic schema model for an undeclared error.
95    pub fn payload(&self) -> Result<&TypedSchemaValue, String> {
96        self.payload
97            .decoded
98            .get_or_init(|| {
99                crate::decode_typed_schema_value_owned(
100                    self.payload
101                        .wire
102                        .borrow_mut()
103                        .take()
104                        .expect("undecoded payload"),
105                )
106                .map_err(|error| error.to_string())
107            })
108            .as_ref()
109            .map_err(Clone::clone)
110    }
111}
112
113impl std::fmt::Debug for RawCustomToolError {
114    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
115        f.debug_struct("RawCustomToolError")
116            .field("name", &self.name)
117            .finish_non_exhaustive()
118    }
119}
120
121impl PartialEq for RawCustomToolError {
122    fn eq(&self, other: &Self) -> bool {
123        self.name == other.name && self.payload() == other.payload()
124    }
125}
126
127/// Exact error channel shared by middleware guest dispatch and its underlying layer.
128#[derive(Clone, Debug, PartialEq)]
129#[allow(clippy::large_enum_variant)]
130pub enum ToolInvokeError<E> {
131    InvalidToolName(String),
132    InvalidCommandPath(Vec<String>),
133    InvalidInput(String),
134    ConstraintViolation(String),
135    InvalidResult(String),
136    Tool(E),
137    UnknownCustomError(RawCustomToolError),
138    ProtocolError(String),
139    Denied(String),
140    InternalError(String),
141    Cancelled,
142    ResourceExhausted(String),
143}
144
145impl<E> ToolInvokeError<E> {
146    /// Transforms only the tool-defined custom error payload.
147    pub fn map_tool<F, O>(self, transform: F) -> ToolInvokeError<O>
148    where
149        F: FnOnce(E) -> O,
150    {
151        match self {
152            Self::InvalidToolName(name) => ToolInvokeError::InvalidToolName(name),
153            Self::InvalidCommandPath(path) => ToolInvokeError::InvalidCommandPath(path),
154            Self::InvalidInput(message) => ToolInvokeError::InvalidInput(message),
155            Self::ConstraintViolation(message) => ToolInvokeError::ConstraintViolation(message),
156            Self::InvalidResult(message) => ToolInvokeError::InvalidResult(message),
157            Self::Tool(error) => ToolInvokeError::Tool(transform(error)),
158            Self::UnknownCustomError(error) => ToolInvokeError::UnknownCustomError(error),
159            Self::ProtocolError(message) => ToolInvokeError::ProtocolError(message),
160            Self::Denied(message) => ToolInvokeError::Denied(message),
161            Self::InternalError(message) => ToolInvokeError::InternalError(message),
162            Self::Cancelled => ToolInvokeError::Cancelled,
163            Self::ResourceExhausted(message) => ToolInvokeError::ResourceExhausted(message),
164        }
165    }
166}
167
168impl<E> From<E> for ToolInvokeError<E> {
169    fn from(error: E) -> Self {
170        Self::Tool(error)
171    }
172}
173
174impl<E: Display> Display for ToolInvokeError<E> {
175    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
176        match self {
177            Self::InvalidToolName(name) => write!(f, "invalid tool name `{name}`"),
178            Self::InvalidCommandPath(path) => {
179                write!(f, "invalid command path `{}`", path.join(" "))
180            }
181            Self::InvalidInput(message) => write!(f, "invalid input: {message}"),
182            Self::ConstraintViolation(message) => write!(f, "constraint violation: {message}"),
183            Self::InvalidResult(message) => write!(f, "invalid result: {message}"),
184            Self::Tool(error) => error.fmt(f),
185            Self::UnknownCustomError(error) => {
186                write!(f, "unknown custom tool error `{}`", error.name)
187            }
188            Self::ProtocolError(message) => write!(f, "protocol error: {message}"),
189            Self::Denied(message) => write!(f, "denied: {message}"),
190            Self::InternalError(message) => write!(f, "internal error: {message}"),
191            Self::Cancelled => write!(f, "underlying tool invocation was cancelled"),
192            Self::ResourceExhausted(message) => write!(f, "resource exhausted: {message}"),
193        }
194    }
195}
196
197impl<E: Error + 'static> Error for ToolInvokeError<E> {
198    fn source(&self) -> Option<&(dyn Error + 'static)> {
199        match self {
200            Self::Tool(error) => Some(error),
201            _ => None,
202        }
203    }
204}
205
206/// Invocation-scoped access to exactly the next inner tool-middleware layer.
207///
208/// The runtime is the only producer of this handle. It is intentionally not
209/// cloneable. Shared invocation permits a middleware to overlap calls to the
210/// same runtime-minted capability.
211#[cfg_attr(not(any(test, feature = "export_golem_agentic")), allow(dead_code))]
212pub struct UnderlyingTool {
213    inner: UnderlyingToolInner,
214}
215
216/// One admitted invocation of the next middleware layer.
217///
218/// Dropping this value only stops observing the invocation. Use [`Self::cancel`]
219/// to explicitly request cancellation of this child.
220#[cfg_attr(not(any(test, feature = "export_golem_agentic")), allow(dead_code))]
221pub struct UnderlyingInvocation {
222    result: Rc<UnderlyingInvocationResult>,
223    #[cfg(any(test, feature = "export_golem_agentic"))]
224    terminal: Rc<
225        super::invocation_result::InvocationResultDriver<
226            Result<Option<TypedSchemaValue>, ToolInvokeError<std::convert::Infallible>>,
227        >,
228    >,
229    pub stdout: Option<InputStream>,
230    pub stderr: Option<InputStream>,
231}
232
233/// Typed view of a started underlying invocation generated for a tool command.
234#[cfg_attr(not(any(test, feature = "export_golem_agentic")), allow(dead_code))]
235pub struct TypedUnderlyingInvocation<T, E> {
236    invocation: UnderlyingInvocation,
237    pub stdout: Option<InputStream>,
238    pub stderr: Option<InputStream>,
239    decode: fn(InvocationResult) -> Result<T, ToolInvokeError<E>>,
240    decode_error: fn(String, TypedSchemaValue) -> Result<Option<E>, String>,
241}
242
243#[cfg(any(test, feature = "export_golem_agentic"))]
244impl<T, E> TypedUnderlyingInvocation<T, E> {
245    #[doc(hidden)]
246    pub fn new(
247        mut invocation: UnderlyingInvocation,
248        expect_stdout: bool,
249        expect_stderr: bool,
250        decode: fn(InvocationResult) -> Result<T, ToolInvokeError<E>>,
251        decode_error: fn(String, TypedSchemaValue) -> Result<Option<E>, String>,
252    ) -> Result<Self, ToolInvokeError<E>> {
253        validate_output_presence("stdout", expect_stdout, invocation.stdout.is_some())?;
254        validate_output_presence("stderr", expect_stderr, invocation.stderr.is_some())?;
255        let stdout = invocation.stdout.take();
256        let stderr = invocation.stderr.take();
257        Ok(Self {
258            invocation,
259            stdout,
260            stderr,
261            decode,
262            decode_error,
263        })
264    }
265
266    pub fn cancel(&self) {
267        self.invocation.cancel();
268    }
269
270    pub async fn get(&self) -> Result<T, ToolInvokeError<E>> {
271        let result = self.invocation.get_with(self.decode_error).await?;
272        (self.decode)(InvocationResult {
273            result,
274            stdout: None,
275            stderr: None,
276        })
277    }
278
279    /// Waits for the structured result while forwarding both underlying outputs
280    /// to the writers supplied to this middleware invocation.
281    #[cfg(feature = "export_golem_agentic")]
282    pub async fn get_forwarding_outputs(
283        mut self,
284        stdout: Option<OutputStream>,
285        stderr: Option<OutputStream>,
286    ) -> Result<T, ToolInvokeError<E>> {
287        let underlying_stdout = self.stdout.take();
288        let underlying_stderr = self.stderr.take();
289        let stdout = forward_output("stdout", underlying_stdout, stdout);
290        let stderr = forward_output("stderr", underlying_stderr, stderr);
291        let (result, stdout, stderr) = join_three(self.get(), stdout, stderr).await;
292        match result {
293            Err(error) => Err(error),
294            Ok(result) => {
295                stdout?;
296                stderr?;
297                Ok(result)
298            }
299        }
300    }
301}
302
303#[cfg(any(test, feature = "export_golem_agentic",))]
304fn validate_output_presence<E>(
305    channel: &str,
306    expected: bool,
307    present: bool,
308) -> Result<(), ToolInvokeError<E>> {
309    match (expected, present) {
310        (true, false) => Err(ToolInvokeError::InvalidResult(format!(
311            "tool result did not contain declared {channel} stream"
312        ))),
313        (false, true) => Err(ToolInvokeError::InvalidResult(format!(
314            "tool result unexpectedly contained {channel} stream"
315        ))),
316        _ => Ok(()),
317    }
318}
319
320enum UnderlyingInvocationResult {
321    #[cfg(feature = "export_golem_agentic")]
322    Raw(crate::tool_underlying_bindings::UnderlyingInvokeResult),
323    #[cfg(test)]
324    Fake(FakeInvocationResult),
325}
326
327#[cfg(test)]
328type FakeInvocationResultFuture = Pin<
329    Box<
330        dyn Future<
331            Output = Result<Option<crate::schema::wit::wire::TypedSchemaValue>, wire::ToolError>,
332        >,
333    >,
334>;
335
336#[cfg(test)]
337struct FakeInvocationResult {
338    result: std::cell::RefCell<Option<FakeInvocationResultFuture>>,
339    cancelled: Rc<std::cell::Cell<bool>>,
340}
341
342#[cfg(any(test, feature = "export_golem_agentic"))]
343impl UnderlyingInvocation {
344    fn new(
345        result: UnderlyingInvocationResult,
346        stdout: Option<InputStream>,
347        stderr: Option<InputStream>,
348    ) -> Self {
349        let result = Rc::new(result);
350        let source = Rc::clone(&result);
351        let terminal = Rc::new(super::invocation_result::InvocationResultDriver::new(
352            move || {
353                Box::pin(async move {
354                    let result = match source.as_ref() {
355                        #[cfg(feature = "export_golem_agentic")]
356                        UnderlyingInvocationResult::Raw(result) => result
357                            .get()
358                            .await
359                            .map_err(|error| decode_underlying_error(error, |_, _| Ok(None)))?,
360                        #[cfg(test)]
361                        UnderlyingInvocationResult::Fake(result) => {
362                            let future = result
363                                .result
364                                .borrow_mut()
365                                .take()
366                                .expect("observer starts once");
367                            future
368                                .await
369                                .map_err(|error| decode_wire_error(error, |_, _| Ok(None)))?
370                        }
371                    };
372                    result
373                        .map(decode_typed_schema_value_owned)
374                        .transpose()
375                        .map_err(|error| ToolInvokeError::InvalidResult(error.to_string()))
376                })
377            },
378        ));
379        Self {
380            result,
381            terminal,
382            stdout,
383            stderr,
384        }
385    }
386
387    pub fn cancel(&self) {
388        match self.result.as_ref() {
389            #[cfg(feature = "export_golem_agentic")]
390            UnderlyingInvocationResult::Raw(result) => result.cancel(),
391            #[cfg(test)]
392            UnderlyingInvocationResult::Fake(result) => result.cancelled.set(true),
393        }
394    }
395
396    pub async fn get(
397        &self,
398    ) -> Result<Option<TypedSchemaValue>, ToolInvokeError<RawCustomToolError>> {
399        self.get_with(|name, payload| Ok(Some(RawCustomToolError::from_payload(name, payload))))
400            .await
401    }
402
403    #[doc(hidden)]
404    pub async fn get_with<E>(
405        &self,
406        decode_custom_error: impl Fn(String, TypedSchemaValue) -> Result<Option<E>, String>,
407    ) -> Result<Option<TypedSchemaValue>, ToolInvokeError<E>> {
408        Rc::clone(&self.terminal)
409            .wait()
410            .await
411            .map_err(|error| match error {
412                ToolInvokeError::UnknownCustomError(raw) => {
413                    match raw
414                        .payload()
415                        .and_then(|payload| decode_custom_error(raw.name.clone(), payload.clone()))
416                    {
417                        Ok(Some(value)) => ToolInvokeError::Tool(value),
418                        Ok(None) => ToolInvokeError::UnknownCustomError(raw),
419                        Err(error) => ToolInvokeError::InvalidResult(error),
420                    }
421                }
422                other => other.map_tool(|impossible| match impossible {}),
423            })
424    }
425}
426
427enum UnderlyingToolInner {
428    #[cfg(feature = "export_golem_agentic")]
429    Raw(crate::tool_underlying_bindings::UnderlyingTool),
430    #[cfg(test)]
431    Fake(FakeInvoke),
432}
433
434#[cfg(test)]
435type FakeRawInvoke = Box<
436    dyn Fn(
437        Vec<String>,
438        crate::schema::wit::wire::TypedSchemaValue,
439        Option<InputStream>,
440    ) -> Pin<Box<dyn Future<Output = Result<wire::InvocationResult, wire::ToolError>>>>,
441>;
442
443#[cfg(test)]
444pub(crate) type FakeInvoke = Box<
445    dyn Fn(
446        Vec<String>,
447        crate::schema::wit::wire::TypedSchemaValue,
448        Option<InputStream>,
449    ) -> (
450        Pin<
451            Box<
452                dyn Future<
453                        Output = Result<
454                            Option<crate::schema::wit::wire::TypedSchemaValue>,
455                            wire::ToolError,
456                        >,
457                    > + 'static,
458            >,
459        >,
460        Option<InputStream>,
461        Option<InputStream>,
462        Rc<std::cell::Cell<bool>>,
463    ),
464>;
465
466#[cfg(any(test, feature = "export_golem_agentic"))]
467impl UnderlyingTool {
468    #[cfg(feature = "export_golem_agentic")]
469    #[allow(dead_code)]
470    pub(crate) fn from_raw(raw: crate::tool_underlying_bindings::UnderlyingTool) -> Self {
471        Self {
472            inner: UnderlyingToolInner::Raw(raw),
473        }
474    }
475
476    #[cfg(test)]
477    pub(crate) fn from_fake(invoke: FakeRawInvoke) -> Self {
478        let invoke = Box::new(move |path, input, stdin| {
479            let result = invoke(path, input, stdin);
480            let result = Box::pin(async move {
481                let result = result.await?;
482                if result.stdout.is_some() || result.stderr.is_some() {
483                    return Err(wire::ToolError::InvalidResult(
484                        "fake invocation must provide outputs when it is started".to_string(),
485                    ));
486                }
487                Ok(result.result)
488            });
489            (
490                result as _,
491                None,
492                None,
493                Rc::new(std::cell::Cell::new(false)),
494            )
495        });
496        Self {
497            inner: UnderlyingToolInner::Fake(invoke),
498        }
499    }
500
501    #[cfg(test)]
502    pub(crate) fn from_fake_started(invoke: FakeInvoke) -> Self {
503        Self {
504            inner: UnderlyingToolInner::Fake(invoke),
505        }
506    }
507
508    pub async fn invoke(
509        &self,
510        command_path: Vec<String>,
511        input: TypedSchemaValue,
512        stdin: Option<InputStream>,
513    ) -> Result<InvocationResult, ToolInvokeError<RawCustomToolError>> {
514        self.invoke_with(command_path, input, stdin, |name, payload| {
515            Ok(Some(RawCustomToolError::from_payload(name, payload)))
516        })
517        .await
518    }
519
520    /// Invokes the next layer while forwarding both outputs to the writers
521    /// supplied to this middleware invocation.
522    #[cfg(feature = "export_golem_agentic")]
523    pub async fn invoke_forwarding_outputs(
524        &self,
525        command_path: Vec<String>,
526        input: TypedSchemaValue,
527        stdin: Option<InputStream>,
528        stdout: Option<OutputStream>,
529        stderr: Option<OutputStream>,
530    ) -> Result<InvocationResult, ToolInvokeError<RawCustomToolError>> {
531        let mut invocation = self.start(command_path, input, stdin).await?;
532        let stdout = forward_output("stdout", invocation.stdout.take(), stdout);
533        let stderr = forward_output("stderr", invocation.stderr.take(), stderr);
534        let (result, stdout, stderr) = join_three(invocation.get(), stdout, stderr).await;
535        match result {
536            Err(error) => Err(error),
537            Ok(result) => {
538                stdout?;
539                stderr?;
540                Ok(InvocationResult {
541                    result,
542                    stdout: None,
543                    stderr: None,
544                })
545            }
546        }
547    }
548
549    /// Admits a call and returns its independently cancellable result observer
550    /// before waiting for the structured result.
551    pub async fn start(
552        &self,
553        command_path: Vec<String>,
554        input: TypedSchemaValue,
555        stdin: Option<InputStream>,
556    ) -> Result<UnderlyingInvocation, ToolInvokeError<RawCustomToolError>> {
557        self.start_with(command_path, input, stdin).await
558    }
559
560    #[doc(hidden)]
561    pub async fn start_with<E>(
562        &self,
563        command_path: Vec<String>,
564        input: TypedSchemaValue,
565        stdin: Option<InputStream>,
566    ) -> Result<UnderlyingInvocation, ToolInvokeError<E>> {
567        let input = crate::encode_typed_schema_value_async(&input)
568            .await
569            .map_err(|error| ToolInvokeError::InvalidInput(error.to_string()))?;
570        match &self.inner {
571            #[cfg(feature = "export_golem_agentic")]
572            UnderlyingToolInner::Raw(raw) => {
573                let (result, stdout, stderr) = raw.invoke(command_path, input, stdin).await;
574                Ok(UnderlyingInvocation::new(
575                    UnderlyingInvocationResult::Raw(result),
576                    stdout,
577                    stderr,
578                ))
579            }
580            #[cfg(test)]
581            UnderlyingToolInner::Fake(invoke) => {
582                let (result, stdout, stderr, cancelled) = invoke(command_path, input, stdin);
583                Ok(UnderlyingInvocation::new(
584                    UnderlyingInvocationResult::Fake(FakeInvocationResult {
585                        result: std::cell::RefCell::new(Some(result)),
586                        cancelled,
587                    }),
588                    stdout,
589                    stderr,
590                ))
591            }
592        }
593    }
594
595    #[doc(hidden)]
596    pub async fn invoke_with<E>(
597        &self,
598        command_path: Vec<String>,
599        input: TypedSchemaValue,
600        stdin: Option<InputStream>,
601        decode_custom_error: impl Fn(String, TypedSchemaValue) -> Result<Option<E>, String>,
602    ) -> Result<InvocationResult, ToolInvokeError<E>> {
603        let mut invocation = self.start_with(command_path, input, stdin).await?;
604        let result = invocation.get_with(decode_custom_error).await?;
605        Ok(InvocationResult {
606            result,
607            stdout: invocation.stdout.take(),
608            stderr: invocation.stderr.take(),
609        })
610    }
611}
612
613#[cfg(feature = "export_golem_agentic")]
614async fn forward_output<E>(
615    channel: &str,
616    stream: Option<InputStream>,
617    mut output: Option<OutputStream>,
618) -> Result<(), ToolInvokeError<E>> {
619    let Some(mut stream) = stream else {
620        return match output {
621            Some(output) => classify_stream_write_result(channel, output.finish().await).map(drop),
622            None => Ok(()),
623        };
624    };
625    while let Some(item) = stream.next().await {
626        match item {
627            Ok(bytes) => {
628                if let Some(output) = &mut output
629                    && classify_stream_write_result(channel, output.write(bytes).await)?
630                {
631                    return Ok(());
632                }
633            }
634            Err(reason) => {
635                return match output {
636                    Some(output) => {
637                        classify_stream_write_result(channel, output.fail(reason).await).map(drop)
638                    }
639                    None => Ok(()),
640                };
641            }
642        }
643    }
644    match output {
645        Some(output) => classify_stream_write_result(channel, output.finish().await).map(drop),
646        None => Ok(()),
647    }
648}
649
650#[cfg(feature = "export_golem_agentic")]
651fn classify_stream_write_result<E>(
652    channel: &str,
653    result: Result<(), StreamWriteError>,
654) -> Result<bool, ToolInvokeError<E>> {
655    match result {
656        Ok(()) => Ok(false),
657        Err(StreamWriteError::Closed(ByteStreamCloseCause::ConsumerCancelled)) => Ok(true),
658        Err(error) => Err(ToolInvokeError::InvalidResult(format!(
659            "failed to forward underlying {channel}: {error:?}"
660        ))),
661    }
662}
663
664#[cfg(feature = "export_golem_agentic")]
665use crate::golem_agentic::golem::tool::streams::{ByteStreamCloseCause, StreamWriteError};
666
667#[cfg(any(test, feature = "export_golem_agentic"))]
668async fn join_results<A, B>(a: A, b: B) -> (A::Output, B::Output)
669where
670    A: Future,
671    B: Future,
672{
673    let mut a = std::pin::pin!(a);
674    let mut b = std::pin::pin!(b);
675    let mut a_result = None;
676    let mut b_result = None;
677    std::future::poll_fn(|cx| {
678        if a_result.is_none()
679            && let std::task::Poll::Ready(result) = a.as_mut().poll(cx)
680        {
681            a_result = Some(result);
682        }
683        if b_result.is_none()
684            && let std::task::Poll::Ready(result) = b.as_mut().poll(cx)
685        {
686            b_result = Some(result);
687        }
688        match (a_result.take(), b_result.take()) {
689            (Some(a), Some(b)) => std::task::Poll::Ready((a, b)),
690            (a, b) => {
691                a_result = a;
692                b_result = b;
693                std::task::Poll::Pending
694            }
695        }
696    })
697    .await
698}
699
700#[cfg(feature = "export_golem_agentic")]
701async fn join_three<A, B, C>(a: A, b: B, c: C) -> (A::Output, B::Output, C::Output)
702where
703    A: Future,
704    B: Future,
705    C: Future,
706{
707    let (a, (b, c)) = join_results(a, async { join_results(b, c).await }).await;
708    (a, b, c)
709}
710
711#[cfg(feature = "export_golem_agentic")]
712fn decode_underlying_error<E>(
713    error: crate::tool_underlying_bindings::UnderlyingError,
714    decode_custom_error: impl Fn(String, TypedSchemaValue) -> Result<Option<E>, String>,
715) -> ToolInvokeError<E> {
716    use crate::tool_underlying_bindings::UnderlyingError;
717    match error {
718        UnderlyingError::ToolError(error) => decode_wire_error(error, decode_custom_error),
719        UnderlyingError::ProtocolError(message) => ToolInvokeError::ProtocolError(message),
720        UnderlyingError::Denied(message) => ToolInvokeError::Denied(message),
721        UnderlyingError::InternalError(message) => ToolInvokeError::InternalError(message),
722        UnderlyingError::Cancelled => ToolInvokeError::Cancelled,
723        UnderlyingError::ResourceExhausted(message) => ToolInvokeError::ResourceExhausted(message),
724    }
725}
726
727#[cfg(any(test, feature = "export_golem_agentic"))]
728fn decode_wire_error<E>(
729    error: wire::ToolError,
730    decode_custom_error: impl Fn(String, TypedSchemaValue) -> Result<Option<E>, String>,
731) -> ToolInvokeError<E> {
732    match error {
733        wire::ToolError::InvalidToolName(name) => ToolInvokeError::InvalidToolName(name),
734        wire::ToolError::InvalidCommandPath(path) => ToolInvokeError::InvalidCommandPath(path),
735        wire::ToolError::InvalidInput(message) => ToolInvokeError::InvalidInput(message),
736        wire::ToolError::ConstraintViolation(message) => {
737            ToolInvokeError::ConstraintViolation(message)
738        }
739        wire::ToolError::InvalidResult(message) => ToolInvokeError::InvalidResult(message),
740        wire::ToolError::CustomError(error) => {
741            let value = match decode_typed_schema_value_owned(error.payload) {
742                Ok(value) => value,
743                Err(error) => return ToolInvokeError::InvalidResult(error.to_string()),
744            };
745            match decode_custom_error(error.name.clone(), value.clone()) {
746                Ok(Some(error)) => ToolInvokeError::Tool(error),
747                Ok(None) => ToolInvokeError::UnknownCustomError(RawCustomToolError::from_payload(
748                    error.name, value,
749                )),
750                Err(error) => ToolInvokeError::InvalidResult(error),
751            }
752        }
753    }
754}
755
756pub fn decode_result_with_stdout<T: FromSchema + IntoSchema, E>(
757    result: InvocationResult,
758) -> Result<(T, InputStream), ToolInvokeError<E>> {
759    let stdout = expect_stdout(result.stdout)?;
760    expect_no_stderr(result.stderr)?;
761    let value = decode_expected_value(result.result)?;
762    Ok((value, stdout))
763}
764
765pub fn decode_result_with_stderr<T: FromSchema + IntoSchema, E>(
766    result: InvocationResult,
767) -> Result<(T, InputStream), ToolInvokeError<E>> {
768    expect_no_stdout(result.stdout)?;
769    let stderr = expect_stderr(result.stderr)?;
770    let value = decode_expected_value(result.result)?;
771    Ok((value, stderr))
772}
773
774pub fn decode_result_with_outputs<T: FromSchema + IntoSchema, E>(
775    result: InvocationResult,
776) -> Result<(T, InputStream, InputStream), ToolInvokeError<E>> {
777    let stdout = expect_stdout(result.stdout)?;
778    let stderr = expect_stderr(result.stderr)?;
779    let value = decode_expected_value(result.result)?;
780    Ok((value, stdout, stderr))
781}
782
783pub fn decode_result_value<T: FromSchema + IntoSchema, E>(
784    result: InvocationResult,
785) -> Result<T, ToolInvokeError<E>> {
786    expect_no_stdout(result.stdout)?;
787    expect_no_stderr(result.stderr)?;
788    decode_expected_value(result.result)
789}
790
791pub fn decode_result_stdout_only<E>(
792    result: InvocationResult,
793) -> Result<InputStream, ToolInvokeError<E>> {
794    let stdout = expect_stdout(result.stdout)?;
795    expect_no_stderr(result.stderr)?;
796    expect_no_value(result.result)?;
797    Ok(stdout)
798}
799
800pub fn decode_result_stderr_only<E>(
801    result: InvocationResult,
802) -> Result<InputStream, ToolInvokeError<E>> {
803    expect_no_stdout(result.stdout)?;
804    let stderr = expect_stderr(result.stderr)?;
805    expect_no_value(result.result)?;
806    Ok(stderr)
807}
808
809pub fn decode_result_outputs_only<E>(
810    result: InvocationResult,
811) -> Result<(InputStream, InputStream), ToolInvokeError<E>> {
812    let stdout = expect_stdout(result.stdout)?;
813    let stderr = expect_stderr(result.stderr)?;
814    expect_no_value(result.result)?;
815    Ok((stdout, stderr))
816}
817
818pub fn decode_result_empty<E>(result: InvocationResult) -> Result<(), ToolInvokeError<E>> {
819    expect_no_stdout(result.stdout)?;
820    expect_no_stderr(result.stderr)?;
821    expect_no_value(result.result)
822}
823
824fn decode_expected_value<T: FromSchema + IntoSchema, E>(
825    value: Option<TypedSchemaValue>,
826) -> Result<T, ToolInvokeError<E>> {
827    let value = value.ok_or_else(|| {
828        ToolInvokeError::InvalidResult("tool result did not contain a value".to_string())
829    })?;
830    let expected = crate::schema::try_into_schema_graph::<T>()
831        .map_err(|error| ToolInvokeError::InvalidResult(error.to_string()))?;
832    if value.graph() != &expected {
833        return Err(ToolInvokeError::InvalidResult(
834            "tool result schema does not match the expected result schema".to_string(),
835        ));
836    }
837    T::from_value(value.value()).map_err(|error| ToolInvokeError::InvalidResult(error.to_string()))
838}
839
840fn expect_stdout<E>(stdout: Option<InputStream>) -> Result<InputStream, ToolInvokeError<E>> {
841    stdout.ok_or_else(|| {
842        ToolInvokeError::InvalidResult(
843            "tool result did not contain declared stdout stream".to_string(),
844        )
845    })
846}
847
848fn expect_no_stdout<E>(stdout: Option<InputStream>) -> Result<(), ToolInvokeError<E>> {
849    if stdout.is_some() {
850        return Err(ToolInvokeError::InvalidResult(
851            "tool result unexpectedly contained stdout stream".to_string(),
852        ));
853    }
854    Ok(())
855}
856
857fn expect_stderr<E>(stderr: Option<InputStream>) -> Result<InputStream, ToolInvokeError<E>> {
858    stderr.ok_or_else(|| {
859        ToolInvokeError::InvalidResult(
860            "tool result did not contain declared stderr stream".to_string(),
861        )
862    })
863}
864
865fn expect_no_stderr<E>(stderr: Option<InputStream>) -> Result<(), ToolInvokeError<E>> {
866    if stderr.is_some() {
867        return Err(ToolInvokeError::InvalidResult(
868            "tool result unexpectedly contained stderr stream".to_string(),
869        ));
870    }
871    Ok(())
872}
873
874fn expect_no_value<E>(value: Option<TypedSchemaValue>) -> Result<(), ToolInvokeError<E>> {
875    if value.is_some() {
876        return Err(ToolInvokeError::InvalidResult(
877            "tool result unexpectedly contained a value".to_string(),
878        ));
879    }
880    Ok(())
881}
882
883#[cfg(test)]
884mod tests {
885    use super::*;
886    use crate::IntoTypedSchemaValue;
887    use std::cell::Cell;
888    use std::rc::Rc;
889    use test_r::test;
890
891    #[cfg(feature = "export_golem_agentic")]
892    #[test]
893    fn stdout_write_result_classification_only_accepts_consumer_cancellation() {
894        #[cfg(not(feature = "export_golem_agentic"))]
895        use crate::bindings::golem::tool::streams::ByteStreamFailure;
896        #[cfg(feature = "export_golem_agentic")]
897        use crate::golem_agentic::golem::tool::streams::ByteStreamFailure;
898
899        assert_eq!(
900            classify_stream_write_result::<()>("stdout", Ok(())),
901            Ok(false)
902        );
903        assert_eq!(
904            classify_stream_write_result::<()>(
905                "stdout",
906                Err(StreamWriteError::Closed(
907                    ByteStreamCloseCause::ConsumerCancelled
908                ))
909            ),
910            Ok(true)
911        );
912        for error in [
913            StreamWriteError::Closed(ByteStreamCloseCause::Finished),
914            StreamWriteError::Closed(ByteStreamCloseCause::Failed(ByteStreamFailure::Abandoned)),
915            StreamWriteError::ConcurrentOperation,
916        ] {
917            assert!(matches!(
918                classify_stream_write_result::<()>("stdout", Err(error)),
919                Err(ToolInvokeError::InvalidResult(_))
920            ));
921        }
922    }
923
924    #[test]
925    fn map_tool_preserves_every_protocol_variant() {
926        let variants = [
927            ToolInvokeError::InvalidToolName("tool".to_string()),
928            ToolInvokeError::InvalidCommandPath(vec!["sub".to_string()]),
929            ToolInvokeError::InvalidInput("input".to_string()),
930            ToolInvokeError::ConstraintViolation("constraint".to_string()),
931            ToolInvokeError::InvalidResult("result".to_string()),
932        ];
933
934        for variant in variants {
935            let mapped: ToolInvokeError<u64> = variant.clone().map_tool(|_: u32| unreachable!());
936            assert_eq!(mapped.map_tool(|value| value as u32), variant);
937        }
938        assert_eq!(
939            ToolInvokeError::Tool(41u32).map_tool(|value| value + 1),
940            ToolInvokeError::Tool(42u32)
941        );
942    }
943
944    #[test]
945    fn wire_protocol_errors_are_preserved_exactly() {
946        let variants = [
947            wire::ToolError::InvalidToolName("tool".to_string()),
948            wire::ToolError::InvalidCommandPath(vec!["sub".to_string()]),
949            wire::ToolError::InvalidInput("input".to_string()),
950            wire::ToolError::ConstraintViolation("constraint".to_string()),
951            wire::ToolError::InvalidResult("result".to_string()),
952        ];
953
954        for variant in variants {
955            let decoded = decode_wire_error(variant, |_, _| -> Result<Option<u32>, String> {
956                unreachable!()
957            });
958            match decoded {
959                ToolInvokeError::InvalidToolName(value) => assert_eq!(value, "tool"),
960                ToolInvokeError::InvalidCommandPath(value) => assert_eq!(value, ["sub"]),
961                ToolInvokeError::InvalidInput(value) => assert_eq!(value, "input"),
962                ToolInvokeError::ConstraintViolation(value) => assert_eq!(value, "constraint"),
963                ToolInvokeError::InvalidResult(value) => assert_eq!(value, "result"),
964                ToolInvokeError::Tool(_) => panic!("protocol error became a custom error"),
965                ToolInvokeError::UnknownCustomError(_) => {
966                    panic!("protocol error became an unknown custom error")
967                }
968                ToolInvokeError::ProtocolError(_)
969                | ToolInvokeError::Denied(_)
970                | ToolInvokeError::InternalError(_)
971                | ToolInvokeError::Cancelled
972                | ToolInvokeError::ResourceExhausted(_) => {
973                    panic!("wire protocol error became an underlying lifecycle error")
974                }
975            }
976        }
977    }
978
979    #[test]
980    fn custom_error_is_decoded_and_decode_failure_is_invalid_result() {
981        let payload = "failure".to_string().into_typed_schema_value().unwrap();
982        let wire_payload = encode_typed_schema_value_owned(payload).unwrap();
983        let decoded = decode_wire_error(
984            wire::ToolError::CustomError(crate::schema::wit::wire::CustomToolError {
985                name: "failure".to_string(),
986                payload: wire_payload,
987            }),
988            |name, value| {
989                assert_eq!(name, "failure");
990                String::from_value(value.value())
991                    .map(Some)
992                    .map_err(|error| error.to_string())
993            },
994        );
995        assert_eq!(decoded, ToolInvokeError::Tool("failure".to_string()));
996
997        let payload = "failure".to_string().into_typed_schema_value().unwrap();
998        let wire_payload = encode_typed_schema_value_owned(payload).unwrap();
999        let decoded = decode_wire_error::<String>(
1000            wire::ToolError::CustomError(crate::schema::wit::wire::CustomToolError {
1001                name: "failure".to_string(),
1002                payload: wire_payload,
1003            }),
1004            |_, _| Err("wrong custom payload".to_string()),
1005        );
1006        assert_eq!(
1007            decoded,
1008            ToolInvokeError::InvalidResult("wrong custom payload".to_string())
1009        );
1010    }
1011
1012    #[test]
1013    async fn one_handle_allows_sequential_owned_invocations() {
1014        let calls = Rc::new(Cell::new(0));
1015        let calls_for_fake = Rc::clone(&calls);
1016        let underlying = UnderlyingTool::from_fake(Box::new(move |path, input, stdin| {
1017            assert!(stdin.is_none());
1018            assert_eq!(path, ["run"]);
1019            calls_for_fake.set(calls_for_fake.get() + 1);
1020            Box::pin(async move {
1021                Ok(wire::InvocationResult {
1022                    result: Some(input),
1023                    stdout: None,
1024                    stderr: None,
1025                })
1026            })
1027        }));
1028
1029        for value in ["first", "second"] {
1030            let result = underlying
1031                .invoke(
1032                    vec!["run".to_string()],
1033                    value.to_string().into_typed_schema_value().unwrap(),
1034                    None,
1035                )
1036                .await
1037                .unwrap();
1038            let decoded = String::from_value(result.result.unwrap().value()).unwrap();
1039            assert_eq!(decoded, value);
1040        }
1041        assert_eq!(calls.get(), 2);
1042    }
1043
1044    #[test]
1045    async fn one_shared_handle_allows_overlapping_invocations_to_complete_in_reverse_order() {
1046        let admitted = Rc::new(Cell::new(0));
1047        let release_first = Rc::new(Cell::new(false));
1048        let admitted_for_fake = Rc::clone(&admitted);
1049        let release_first_for_fake = Rc::clone(&release_first);
1050        let underlying = UnderlyingTool::from_fake(Box::new(move |_, input, _| {
1051            admitted_for_fake.set(admitted_for_fake.get() + 1);
1052            let admitted = Rc::clone(&admitted_for_fake);
1053            let release_first = Rc::clone(&release_first_for_fake);
1054            Box::pin(async move {
1055                let value = decode_typed_schema_value_owned(input).unwrap();
1056                let value = String::from_value(value.value()).unwrap();
1057                if value == "first" {
1058                    std::future::poll_fn(|cx| {
1059                        if release_first.get() {
1060                            std::task::Poll::Ready(())
1061                        } else {
1062                            cx.waker().wake_by_ref();
1063                            std::task::Poll::Pending
1064                        }
1065                    })
1066                    .await;
1067                } else {
1068                    assert_eq!(admitted.get(), 2);
1069                    release_first.set(true);
1070                }
1071                Ok(wire::InvocationResult {
1072                    result: Some(
1073                        encode_typed_schema_value_owned(value.into_typed_schema_value().unwrap())
1074                            .unwrap(),
1075                    ),
1076                    stdout: None,
1077                    stderr: None,
1078                })
1079            })
1080        }));
1081
1082        let first = underlying.invoke(
1083            vec!["run".to_string()],
1084            "first".to_string().into_typed_schema_value().unwrap(),
1085            None,
1086        );
1087        let second = underlying.invoke(
1088            vec!["run".to_string()],
1089            "second".to_string().into_typed_schema_value().unwrap(),
1090            None,
1091        );
1092        let mut first = std::pin::pin!(first);
1093        let mut second = std::pin::pin!(second);
1094        let mut first_result = None;
1095        let mut second_result = None;
1096        let (first, second) = std::future::poll_fn(|cx| {
1097            if first_result.is_none()
1098                && let std::task::Poll::Ready(result) = first.as_mut().poll(cx)
1099            {
1100                first_result = Some(result);
1101            }
1102            if second_result.is_none()
1103                && let std::task::Poll::Ready(result) = second.as_mut().poll(cx)
1104            {
1105                assert!(first_result.is_none());
1106                second_result = Some(result);
1107            }
1108            match (first_result.take(), second_result.take()) {
1109                (Some(first), Some(second)) => std::task::Poll::Ready((first, second)),
1110                (first, second) => {
1111                    first_result = first;
1112                    second_result = second;
1113                    std::task::Poll::Pending
1114                }
1115            }
1116        })
1117        .await;
1118        assert_eq!(admitted.get(), 2);
1119        assert_eq!(
1120            String::from_value(second.unwrap().result.unwrap().value()).unwrap(),
1121            "second"
1122        );
1123        assert_eq!(
1124            String::from_value(first.unwrap().result.unwrap().value()).unwrap(),
1125            "first"
1126        );
1127    }
1128
1129    #[test]
1130    async fn underlying_observer_shares_concurrent_gets_and_caches_terminal() {
1131        let polls = Rc::new(Cell::new(0));
1132        let underlying = UnderlyingTool::from_fake_started(Box::new({
1133            let polls = Rc::clone(&polls);
1134            move |_, input, _| {
1135                let polls = Rc::clone(&polls);
1136                let mut value = Some(input);
1137                let future = Box::pin(std::future::poll_fn(move |cx| {
1138                    polls.set(polls.get() + 1);
1139                    if polls.get() == 1 {
1140                        cx.waker().wake_by_ref();
1141                        std::task::Poll::Pending
1142                    } else {
1143                        std::task::Poll::Ready(Ok(value.take()))
1144                    }
1145                }));
1146                (future as _, None, None, Rc::new(Cell::new(false)))
1147            }
1148        }));
1149        let expected = "shared terminal"
1150            .to_string()
1151            .into_typed_schema_value()
1152            .unwrap();
1153        let invocation = underlying
1154            .start(vec![], expected.clone(), None)
1155            .await
1156            .unwrap();
1157        assert_eq!(polls.get(), 0);
1158        let (first, second) = join_results(invocation.get(), invocation.get()).await;
1159        assert_eq!(first.unwrap(), Some(expected.clone()));
1160        assert_eq!(second.unwrap(), Some(expected.clone()));
1161        assert_eq!(invocation.get().await.unwrap(), Some(expected));
1162        assert_eq!(polls.get(), 2);
1163    }
1164
1165    #[test]
1166    async fn result_wait_and_cancel_share_the_same_invocation_borrow() {
1167        let underlying = UnderlyingTool::from_fake_started(Box::new(|_, _, _| {
1168            let cancelled = Rc::new(Cell::new(false));
1169            let observed = Rc::clone(&cancelled);
1170            let result = Box::pin(std::future::poll_fn(move |cx| {
1171                if observed.get() {
1172                    std::task::Poll::Ready(Err(wire::ToolError::ConstraintViolation(
1173                        "cancelled".to_string(),
1174                    )))
1175                } else {
1176                    cx.waker().wake_by_ref();
1177                    std::task::Poll::Pending
1178                }
1179            }));
1180            (result as _, None, None, cancelled)
1181        }));
1182        let invocation = underlying
1183            .start(
1184                vec!["run".to_string()],
1185                ().into_typed_schema_value().unwrap(),
1186                None,
1187            )
1188            .await
1189            .unwrap();
1190        let get = invocation.get();
1191        let mut get = std::pin::pin!(get);
1192        std::future::poll_fn(|cx| {
1193            assert!(get.as_mut().poll(cx).is_pending());
1194            std::task::Poll::Ready(())
1195        })
1196        .await;
1197        invocation.cancel();
1198        assert!(matches!(
1199            get.await,
1200            Err(ToolInvokeError::ConstraintViolation(message)) if message == "cancelled"
1201        ));
1202        assert!(matches!(
1203            invocation.get().await,
1204            Err(ToolInvokeError::ConstraintViolation(message)) if message == "cancelled"
1205        ));
1206    }
1207
1208    #[test]
1209    fn result_slot_projection_rejects_missing_and_unexpected_values() {
1210        let missing = decode_result_value::<String, ()>(InvocationResult {
1211            result: None,
1212            stdout: None,
1213            stderr: None,
1214        });
1215        assert!(matches!(missing, Err(ToolInvokeError::InvalidResult(_))));
1216
1217        let unexpected = decode_result_empty::<()>(InvocationResult {
1218            result: Some("value".to_string().into_typed_schema_value().unwrap()),
1219            stdout: None,
1220            stderr: None,
1221        });
1222        assert!(matches!(unexpected, Err(ToolInvokeError::InvalidResult(_))));
1223    }
1224
1225    #[test]
1226    fn result_slot_projection_rejects_missing_stream() {
1227        let missing = decode_result_stdout_only::<()>(InvocationResult {
1228            result: None,
1229            stdout: None,
1230            stderr: None,
1231        });
1232        assert!(matches!(missing, Err(ToolInvokeError::InvalidResult(_))));
1233    }
1234}