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    pub async fn get_forwarding_outputs(
282        mut self,
283        stdout: Option<OutputStream>,
284        stderr: Option<OutputStream>,
285    ) -> Result<T, ToolInvokeError<E>> {
286        let underlying_stdout = self.stdout.take();
287        let underlying_stderr = self.stderr.take();
288        let stdout = forward_output("stdout", underlying_stdout, stdout);
289        let stderr = forward_output("stderr", underlying_stderr, stderr);
290        let (result, stdout, stderr) = join_three(self.get(), stdout, stderr).await;
291        match result {
292            Err(error) => Err(error),
293            Ok(result) => {
294                stdout?;
295                stderr?;
296                Ok(result)
297            }
298        }
299    }
300}
301
302#[cfg(any(test, feature = "export_golem_agentic",))]
303fn validate_output_presence<E>(
304    channel: &str,
305    expected: bool,
306    present: bool,
307) -> Result<(), ToolInvokeError<E>> {
308    match (expected, present) {
309        (true, false) => Err(ToolInvokeError::InvalidResult(format!(
310            "tool result did not contain declared {channel} stream"
311        ))),
312        (false, true) => Err(ToolInvokeError::InvalidResult(format!(
313            "tool result unexpectedly contained {channel} stream"
314        ))),
315        _ => Ok(()),
316    }
317}
318
319enum UnderlyingInvocationResult {
320    #[cfg(feature = "export_golem_agentic")]
321    Raw(crate::tool_underlying_bindings::UnderlyingInvokeResult),
322    #[cfg(test)]
323    Fake(FakeInvocationResult),
324}
325
326#[cfg(test)]
327type FakeInvocationResultFuture = Pin<
328    Box<
329        dyn Future<
330            Output = Result<Option<crate::schema::wit::wire::TypedSchemaValue>, wire::ToolError>,
331        >,
332    >,
333>;
334
335#[cfg(test)]
336struct FakeInvocationResult {
337    result: std::cell::RefCell<Option<FakeInvocationResultFuture>>,
338    cancelled: Rc<std::cell::Cell<bool>>,
339}
340
341#[cfg(any(test, feature = "export_golem_agentic"))]
342impl UnderlyingInvocation {
343    fn new(
344        result: UnderlyingInvocationResult,
345        stdout: Option<InputStream>,
346        stderr: Option<InputStream>,
347    ) -> Self {
348        let result = Rc::new(result);
349        let source = Rc::clone(&result);
350        let terminal = Rc::new(super::invocation_result::InvocationResultDriver::new(
351            move || {
352                Box::pin(async move {
353                    let result = match source.as_ref() {
354                        #[cfg(feature = "export_golem_agentic")]
355                        UnderlyingInvocationResult::Raw(result) => result
356                            .get()
357                            .await
358                            .map_err(|error| decode_underlying_error(error, |_, _| Ok(None)))?,
359                        #[cfg(test)]
360                        UnderlyingInvocationResult::Fake(result) => {
361                            let future = result
362                                .result
363                                .borrow_mut()
364                                .take()
365                                .expect("observer starts once");
366                            future
367                                .await
368                                .map_err(|error| decode_wire_error(error, |_, _| Ok(None)))?
369                        }
370                    };
371                    result
372                        .map(decode_typed_schema_value_owned)
373                        .transpose()
374                        .map_err(|error| ToolInvokeError::InvalidResult(error.to_string()))
375                })
376            },
377        ));
378        Self {
379            result,
380            terminal,
381            stdout,
382            stderr,
383        }
384    }
385
386    pub fn cancel(&self) {
387        match self.result.as_ref() {
388            #[cfg(feature = "export_golem_agentic")]
389            UnderlyingInvocationResult::Raw(result) => result.cancel(),
390            #[cfg(test)]
391            UnderlyingInvocationResult::Fake(result) => result.cancelled.set(true),
392        }
393    }
394
395    pub async fn get(
396        &self,
397    ) -> Result<Option<TypedSchemaValue>, ToolInvokeError<RawCustomToolError>> {
398        self.get_with(|name, payload| Ok(Some(RawCustomToolError::from_payload(name, payload))))
399            .await
400    }
401
402    #[doc(hidden)]
403    pub async fn get_with<E>(
404        &self,
405        decode_custom_error: impl Fn(String, TypedSchemaValue) -> Result<Option<E>, String>,
406    ) -> Result<Option<TypedSchemaValue>, ToolInvokeError<E>> {
407        Rc::clone(&self.terminal)
408            .wait()
409            .await
410            .map_err(|error| match error {
411                ToolInvokeError::UnknownCustomError(raw) => {
412                    match raw
413                        .payload()
414                        .and_then(|payload| decode_custom_error(raw.name.clone(), payload.clone()))
415                    {
416                        Ok(Some(value)) => ToolInvokeError::Tool(value),
417                        Ok(None) => ToolInvokeError::UnknownCustomError(raw),
418                        Err(error) => ToolInvokeError::InvalidResult(error),
419                    }
420                }
421                other => other.map_tool(|impossible| match impossible {}),
422            })
423    }
424}
425
426enum UnderlyingToolInner {
427    #[cfg(feature = "export_golem_agentic")]
428    Raw(crate::tool_underlying_bindings::UnderlyingTool),
429    #[cfg(test)]
430    Fake(FakeInvoke),
431}
432
433#[cfg(test)]
434type FakeRawInvoke = Box<
435    dyn Fn(
436        Vec<String>,
437        crate::schema::wit::wire::TypedSchemaValue,
438        Option<InputStream>,
439    ) -> Pin<Box<dyn Future<Output = Result<wire::InvocationResult, wire::ToolError>>>>,
440>;
441
442#[cfg(test)]
443pub(crate) type FakeInvoke = Box<
444    dyn Fn(
445        Vec<String>,
446        crate::schema::wit::wire::TypedSchemaValue,
447        Option<InputStream>,
448    ) -> (
449        Pin<
450            Box<
451                dyn Future<
452                        Output = Result<
453                            Option<crate::schema::wit::wire::TypedSchemaValue>,
454                            wire::ToolError,
455                        >,
456                    > + 'static,
457            >,
458        >,
459        Option<InputStream>,
460        Option<InputStream>,
461        Rc<std::cell::Cell<bool>>,
462    ),
463>;
464
465#[cfg(any(test, feature = "export_golem_agentic"))]
466impl UnderlyingTool {
467    #[cfg(feature = "export_golem_agentic")]
468    #[allow(dead_code)]
469    pub(crate) fn from_raw(raw: crate::tool_underlying_bindings::UnderlyingTool) -> Self {
470        Self {
471            inner: UnderlyingToolInner::Raw(raw),
472        }
473    }
474
475    #[cfg(test)]
476    pub(crate) fn from_fake(invoke: FakeRawInvoke) -> Self {
477        let invoke = Box::new(move |path, input, stdin| {
478            let result = invoke(path, input, stdin);
479            let result = Box::pin(async move {
480                let result = result.await?;
481                if result.stdout.is_some() || result.stderr.is_some() {
482                    return Err(wire::ToolError::InvalidResult(
483                        "fake invocation must provide outputs when it is started".to_string(),
484                    ));
485                }
486                Ok(result.result)
487            });
488            (
489                result as _,
490                None,
491                None,
492                Rc::new(std::cell::Cell::new(false)),
493            )
494        });
495        Self {
496            inner: UnderlyingToolInner::Fake(invoke),
497        }
498    }
499
500    #[cfg(test)]
501    pub(crate) fn from_fake_started(invoke: FakeInvoke) -> Self {
502        Self {
503            inner: UnderlyingToolInner::Fake(invoke),
504        }
505    }
506
507    pub async fn invoke(
508        &self,
509        command_path: Vec<String>,
510        input: TypedSchemaValue,
511        stdin: Option<InputStream>,
512    ) -> Result<InvocationResult, ToolInvokeError<RawCustomToolError>> {
513        self.invoke_with(command_path, input, stdin, |name, payload| {
514            Ok(Some(RawCustomToolError::from_payload(name, payload)))
515        })
516        .await
517    }
518
519    /// Invokes the next layer while forwarding both outputs to the writers
520    /// supplied to this middleware invocation.
521    pub async fn invoke_forwarding_outputs(
522        &self,
523        command_path: Vec<String>,
524        input: TypedSchemaValue,
525        stdin: Option<InputStream>,
526        stdout: Option<OutputStream>,
527        stderr: Option<OutputStream>,
528    ) -> Result<InvocationResult, ToolInvokeError<RawCustomToolError>> {
529        let mut invocation = self.start(command_path, input, stdin).await?;
530        let stdout = forward_output("stdout", invocation.stdout.take(), stdout);
531        let stderr = forward_output("stderr", invocation.stderr.take(), stderr);
532        let (result, stdout, stderr) = join_three(invocation.get(), stdout, stderr).await;
533        match result {
534            Err(error) => Err(error),
535            Ok(result) => {
536                stdout?;
537                stderr?;
538                Ok(InvocationResult {
539                    result,
540                    stdout: None,
541                    stderr: None,
542                })
543            }
544        }
545    }
546
547    /// Admits a call and returns its independently cancellable result observer
548    /// before waiting for the structured result.
549    pub async fn start(
550        &self,
551        command_path: Vec<String>,
552        input: TypedSchemaValue,
553        stdin: Option<InputStream>,
554    ) -> Result<UnderlyingInvocation, ToolInvokeError<RawCustomToolError>> {
555        self.start_with(command_path, input, stdin).await
556    }
557
558    #[doc(hidden)]
559    pub async fn start_with<E>(
560        &self,
561        command_path: Vec<String>,
562        input: TypedSchemaValue,
563        stdin: Option<InputStream>,
564    ) -> Result<UnderlyingInvocation, ToolInvokeError<E>> {
565        let input = crate::encode_typed_schema_value_async(&input)
566            .await
567            .map_err(|error| ToolInvokeError::InvalidInput(error.to_string()))?;
568        match &self.inner {
569            #[cfg(feature = "export_golem_agentic")]
570            UnderlyingToolInner::Raw(raw) => {
571                let (result, stdout, stderr) = raw.invoke(command_path, input, stdin).await;
572                Ok(UnderlyingInvocation::new(
573                    UnderlyingInvocationResult::Raw(result),
574                    stdout,
575                    stderr,
576                ))
577            }
578            #[cfg(test)]
579            UnderlyingToolInner::Fake(invoke) => {
580                let (result, stdout, stderr, cancelled) = invoke(command_path, input, stdin);
581                Ok(UnderlyingInvocation::new(
582                    UnderlyingInvocationResult::Fake(FakeInvocationResult {
583                        result: std::cell::RefCell::new(Some(result)),
584                        cancelled,
585                    }),
586                    stdout,
587                    stderr,
588                ))
589            }
590        }
591    }
592
593    #[doc(hidden)]
594    pub async fn invoke_with<E>(
595        &self,
596        command_path: Vec<String>,
597        input: TypedSchemaValue,
598        stdin: Option<InputStream>,
599        decode_custom_error: impl Fn(String, TypedSchemaValue) -> Result<Option<E>, String>,
600    ) -> Result<InvocationResult, ToolInvokeError<E>> {
601        let mut invocation = self.start_with(command_path, input, stdin).await?;
602        let result = invocation.get_with(decode_custom_error).await?;
603        Ok(InvocationResult {
604            result,
605            stdout: invocation.stdout.take(),
606            stderr: invocation.stderr.take(),
607        })
608    }
609}
610
611#[cfg(feature = "export_golem_agentic")]
612async fn forward_output<E>(
613    channel: &str,
614    stream: Option<InputStream>,
615    mut output: Option<OutputStream>,
616) -> Result<(), ToolInvokeError<E>> {
617    let Some(mut stream) = stream else {
618        return match output {
619            Some(output) => classify_stream_write_result(channel, output.finish().await).map(drop),
620            None => Ok(()),
621        };
622    };
623    while let Some(item) = stream.next().await {
624        match item {
625            Ok(bytes) => {
626                if let Some(output) = &mut output
627                    && classify_stream_write_result(channel, output.write(bytes).await)?
628                {
629                    return Ok(());
630                }
631            }
632            Err(reason) => {
633                return match output {
634                    Some(output) => {
635                        classify_stream_write_result(channel, output.fail(reason).await).map(drop)
636                    }
637                    None => Ok(()),
638                };
639            }
640        }
641    }
642    match output {
643        Some(output) => classify_stream_write_result(channel, output.finish().await).map(drop),
644        None => Ok(()),
645    }
646}
647
648#[cfg(feature = "export_golem_agentic")]
649fn classify_stream_write_result<E>(
650    channel: &str,
651    result: Result<(), StreamWriteError>,
652) -> Result<bool, ToolInvokeError<E>> {
653    match result {
654        Ok(()) => Ok(false),
655        Err(StreamWriteError::Closed(ByteStreamCloseCause::ConsumerCancelled)) => Ok(true),
656        Err(error) => Err(ToolInvokeError::InvalidResult(format!(
657            "failed to forward underlying {channel}: {error:?}"
658        ))),
659    }
660}
661
662#[cfg(feature = "export_golem_agentic")]
663use crate::golem_agentic::golem::tool::streams::{ByteStreamCloseCause, StreamWriteError};
664
665#[cfg(any(test, feature = "export_golem_agentic"))]
666async fn join_results<A, B>(a: A, b: B) -> (A::Output, B::Output)
667where
668    A: Future,
669    B: Future,
670{
671    let mut a = std::pin::pin!(a);
672    let mut b = std::pin::pin!(b);
673    let mut a_result = None;
674    let mut b_result = None;
675    std::future::poll_fn(|cx| {
676        if a_result.is_none()
677            && let std::task::Poll::Ready(result) = a.as_mut().poll(cx)
678        {
679            a_result = Some(result);
680        }
681        if b_result.is_none()
682            && let std::task::Poll::Ready(result) = b.as_mut().poll(cx)
683        {
684            b_result = Some(result);
685        }
686        match (a_result.take(), b_result.take()) {
687            (Some(a), Some(b)) => std::task::Poll::Ready((a, b)),
688            (a, b) => {
689                a_result = a;
690                b_result = b;
691                std::task::Poll::Pending
692            }
693        }
694    })
695    .await
696}
697
698#[cfg(any(test, feature = "export_golem_agentic",))]
699async fn join_three<A, B, C>(a: A, b: B, c: C) -> (A::Output, B::Output, C::Output)
700where
701    A: Future,
702    B: Future,
703    C: Future,
704{
705    let (a, (b, c)) = join_results(a, async { join_results(b, c).await }).await;
706    (a, b, c)
707}
708
709#[cfg(feature = "export_golem_agentic")]
710fn decode_underlying_error<E>(
711    error: crate::tool_underlying_bindings::UnderlyingError,
712    decode_custom_error: impl Fn(String, TypedSchemaValue) -> Result<Option<E>, String>,
713) -> ToolInvokeError<E> {
714    use crate::tool_underlying_bindings::UnderlyingError;
715    match error {
716        UnderlyingError::ToolError(error) => decode_wire_error(error, decode_custom_error),
717        UnderlyingError::ProtocolError(message) => ToolInvokeError::ProtocolError(message),
718        UnderlyingError::Denied(message) => ToolInvokeError::Denied(message),
719        UnderlyingError::InternalError(message) => ToolInvokeError::InternalError(message),
720        UnderlyingError::Cancelled => ToolInvokeError::Cancelled,
721        UnderlyingError::ResourceExhausted(message) => ToolInvokeError::ResourceExhausted(message),
722    }
723}
724
725#[cfg(any(test, feature = "export_golem_agentic"))]
726fn decode_wire_error<E>(
727    error: wire::ToolError,
728    decode_custom_error: impl Fn(String, TypedSchemaValue) -> Result<Option<E>, String>,
729) -> ToolInvokeError<E> {
730    match error {
731        wire::ToolError::InvalidToolName(name) => ToolInvokeError::InvalidToolName(name),
732        wire::ToolError::InvalidCommandPath(path) => ToolInvokeError::InvalidCommandPath(path),
733        wire::ToolError::InvalidInput(message) => ToolInvokeError::InvalidInput(message),
734        wire::ToolError::ConstraintViolation(message) => {
735            ToolInvokeError::ConstraintViolation(message)
736        }
737        wire::ToolError::InvalidResult(message) => ToolInvokeError::InvalidResult(message),
738        wire::ToolError::CustomError(error) => {
739            let value = match decode_typed_schema_value_owned(error.payload) {
740                Ok(value) => value,
741                Err(error) => return ToolInvokeError::InvalidResult(error.to_string()),
742            };
743            match decode_custom_error(error.name.clone(), value.clone()) {
744                Ok(Some(error)) => ToolInvokeError::Tool(error),
745                Ok(None) => ToolInvokeError::UnknownCustomError(RawCustomToolError::from_payload(
746                    error.name, value,
747                )),
748                Err(error) => ToolInvokeError::InvalidResult(error),
749            }
750        }
751    }
752}
753
754pub fn decode_result_with_stdout<T: FromSchema + IntoSchema, E>(
755    result: InvocationResult,
756) -> Result<(T, InputStream), ToolInvokeError<E>> {
757    let stdout = expect_stdout(result.stdout)?;
758    expect_no_stderr(result.stderr)?;
759    let value = decode_expected_value(result.result)?;
760    Ok((value, stdout))
761}
762
763pub fn decode_result_with_stderr<T: FromSchema + IntoSchema, E>(
764    result: InvocationResult,
765) -> Result<(T, InputStream), ToolInvokeError<E>> {
766    expect_no_stdout(result.stdout)?;
767    let stderr = expect_stderr(result.stderr)?;
768    let value = decode_expected_value(result.result)?;
769    Ok((value, stderr))
770}
771
772pub fn decode_result_with_outputs<T: FromSchema + IntoSchema, E>(
773    result: InvocationResult,
774) -> Result<(T, InputStream, InputStream), ToolInvokeError<E>> {
775    let stdout = expect_stdout(result.stdout)?;
776    let stderr = expect_stderr(result.stderr)?;
777    let value = decode_expected_value(result.result)?;
778    Ok((value, stdout, stderr))
779}
780
781pub fn decode_result_value<T: FromSchema + IntoSchema, E>(
782    result: InvocationResult,
783) -> Result<T, ToolInvokeError<E>> {
784    expect_no_stdout(result.stdout)?;
785    expect_no_stderr(result.stderr)?;
786    decode_expected_value(result.result)
787}
788
789pub fn decode_result_stdout_only<E>(
790    result: InvocationResult,
791) -> Result<InputStream, ToolInvokeError<E>> {
792    let stdout = expect_stdout(result.stdout)?;
793    expect_no_stderr(result.stderr)?;
794    expect_no_value(result.result)?;
795    Ok(stdout)
796}
797
798pub fn decode_result_stderr_only<E>(
799    result: InvocationResult,
800) -> Result<InputStream, ToolInvokeError<E>> {
801    expect_no_stdout(result.stdout)?;
802    let stderr = expect_stderr(result.stderr)?;
803    expect_no_value(result.result)?;
804    Ok(stderr)
805}
806
807pub fn decode_result_outputs_only<E>(
808    result: InvocationResult,
809) -> Result<(InputStream, InputStream), ToolInvokeError<E>> {
810    let stdout = expect_stdout(result.stdout)?;
811    let stderr = expect_stderr(result.stderr)?;
812    expect_no_value(result.result)?;
813    Ok((stdout, stderr))
814}
815
816pub fn decode_result_empty<E>(result: InvocationResult) -> Result<(), ToolInvokeError<E>> {
817    expect_no_stdout(result.stdout)?;
818    expect_no_stderr(result.stderr)?;
819    expect_no_value(result.result)
820}
821
822fn decode_expected_value<T: FromSchema + IntoSchema, E>(
823    value: Option<TypedSchemaValue>,
824) -> Result<T, ToolInvokeError<E>> {
825    let value = value.ok_or_else(|| {
826        ToolInvokeError::InvalidResult("tool result did not contain a value".to_string())
827    })?;
828    let expected = crate::schema::try_into_schema_graph::<T>()
829        .map_err(|error| ToolInvokeError::InvalidResult(error.to_string()))?;
830    if value.graph() != &expected {
831        return Err(ToolInvokeError::InvalidResult(
832            "tool result schema does not match the expected result schema".to_string(),
833        ));
834    }
835    T::from_value(value.value()).map_err(|error| ToolInvokeError::InvalidResult(error.to_string()))
836}
837
838fn expect_stdout<E>(stdout: Option<InputStream>) -> Result<InputStream, ToolInvokeError<E>> {
839    stdout.ok_or_else(|| {
840        ToolInvokeError::InvalidResult(
841            "tool result did not contain declared stdout stream".to_string(),
842        )
843    })
844}
845
846fn expect_no_stdout<E>(stdout: Option<InputStream>) -> Result<(), ToolInvokeError<E>> {
847    if stdout.is_some() {
848        return Err(ToolInvokeError::InvalidResult(
849            "tool result unexpectedly contained stdout stream".to_string(),
850        ));
851    }
852    Ok(())
853}
854
855fn expect_stderr<E>(stderr: Option<InputStream>) -> Result<InputStream, ToolInvokeError<E>> {
856    stderr.ok_or_else(|| {
857        ToolInvokeError::InvalidResult(
858            "tool result did not contain declared stderr stream".to_string(),
859        )
860    })
861}
862
863fn expect_no_stderr<E>(stderr: Option<InputStream>) -> Result<(), ToolInvokeError<E>> {
864    if stderr.is_some() {
865        return Err(ToolInvokeError::InvalidResult(
866            "tool result unexpectedly contained stderr stream".to_string(),
867        ));
868    }
869    Ok(())
870}
871
872fn expect_no_value<E>(value: Option<TypedSchemaValue>) -> Result<(), ToolInvokeError<E>> {
873    if value.is_some() {
874        return Err(ToolInvokeError::InvalidResult(
875            "tool result unexpectedly contained a value".to_string(),
876        ));
877    }
878    Ok(())
879}
880
881#[cfg(test)]
882mod tests {
883    use super::*;
884    use crate::IntoTypedSchemaValue;
885    use std::cell::Cell;
886    use std::rc::Rc;
887    use test_r::test;
888
889    #[cfg(feature = "export_golem_agentic")]
890    #[test]
891    fn stdout_write_result_classification_only_accepts_consumer_cancellation() {
892        #[cfg(not(feature = "export_golem_agentic"))]
893        use crate::bindings::golem::tool::streams::ByteStreamFailure;
894        #[cfg(feature = "export_golem_agentic")]
895        use crate::golem_agentic::golem::tool::streams::ByteStreamFailure;
896
897        assert_eq!(
898            classify_stream_write_result::<()>("stdout", Ok(())),
899            Ok(false)
900        );
901        assert_eq!(
902            classify_stream_write_result::<()>(
903                "stdout",
904                Err(StreamWriteError::Closed(
905                    ByteStreamCloseCause::ConsumerCancelled
906                ))
907            ),
908            Ok(true)
909        );
910        for error in [
911            StreamWriteError::Closed(ByteStreamCloseCause::Finished),
912            StreamWriteError::Closed(ByteStreamCloseCause::Failed(ByteStreamFailure::Abandoned)),
913            StreamWriteError::ConcurrentOperation,
914        ] {
915            assert!(matches!(
916                classify_stream_write_result::<()>("stdout", Err(error)),
917                Err(ToolInvokeError::InvalidResult(_))
918            ));
919        }
920    }
921
922    #[test]
923    fn map_tool_preserves_every_protocol_variant() {
924        let variants = [
925            ToolInvokeError::InvalidToolName("tool".to_string()),
926            ToolInvokeError::InvalidCommandPath(vec!["sub".to_string()]),
927            ToolInvokeError::InvalidInput("input".to_string()),
928            ToolInvokeError::ConstraintViolation("constraint".to_string()),
929            ToolInvokeError::InvalidResult("result".to_string()),
930        ];
931
932        for variant in variants {
933            let mapped: ToolInvokeError<u64> = variant.clone().map_tool(|_: u32| unreachable!());
934            assert_eq!(mapped.map_tool(|value| value as u32), variant);
935        }
936        assert_eq!(
937            ToolInvokeError::Tool(41u32).map_tool(|value| value + 1),
938            ToolInvokeError::Tool(42u32)
939        );
940    }
941
942    #[test]
943    fn wire_protocol_errors_are_preserved_exactly() {
944        let variants = [
945            wire::ToolError::InvalidToolName("tool".to_string()),
946            wire::ToolError::InvalidCommandPath(vec!["sub".to_string()]),
947            wire::ToolError::InvalidInput("input".to_string()),
948            wire::ToolError::ConstraintViolation("constraint".to_string()),
949            wire::ToolError::InvalidResult("result".to_string()),
950        ];
951
952        for variant in variants {
953            let decoded = decode_wire_error(variant, |_, _| -> Result<Option<u32>, String> {
954                unreachable!()
955            });
956            match decoded {
957                ToolInvokeError::InvalidToolName(value) => assert_eq!(value, "tool"),
958                ToolInvokeError::InvalidCommandPath(value) => assert_eq!(value, ["sub"]),
959                ToolInvokeError::InvalidInput(value) => assert_eq!(value, "input"),
960                ToolInvokeError::ConstraintViolation(value) => assert_eq!(value, "constraint"),
961                ToolInvokeError::InvalidResult(value) => assert_eq!(value, "result"),
962                ToolInvokeError::Tool(_) => panic!("protocol error became a custom error"),
963                ToolInvokeError::UnknownCustomError(_) => {
964                    panic!("protocol error became an unknown custom error")
965                }
966                ToolInvokeError::ProtocolError(_)
967                | ToolInvokeError::Denied(_)
968                | ToolInvokeError::InternalError(_)
969                | ToolInvokeError::Cancelled
970                | ToolInvokeError::ResourceExhausted(_) => {
971                    panic!("wire protocol error became an underlying lifecycle error")
972                }
973            }
974        }
975    }
976
977    #[test]
978    fn custom_error_is_decoded_and_decode_failure_is_invalid_result() {
979        let payload = "failure".to_string().into_typed_schema_value().unwrap();
980        let wire_payload = encode_typed_schema_value_owned(payload).unwrap();
981        let decoded = decode_wire_error(
982            wire::ToolError::CustomError(crate::schema::wit::wire::CustomToolError {
983                name: "failure".to_string(),
984                payload: wire_payload,
985            }),
986            |name, value| {
987                assert_eq!(name, "failure");
988                String::from_value(value.value())
989                    .map(Some)
990                    .map_err(|error| error.to_string())
991            },
992        );
993        assert_eq!(decoded, ToolInvokeError::Tool("failure".to_string()));
994
995        let payload = "failure".to_string().into_typed_schema_value().unwrap();
996        let wire_payload = encode_typed_schema_value_owned(payload).unwrap();
997        let decoded = decode_wire_error::<String>(
998            wire::ToolError::CustomError(crate::schema::wit::wire::CustomToolError {
999                name: "failure".to_string(),
1000                payload: wire_payload,
1001            }),
1002            |_, _| Err("wrong custom payload".to_string()),
1003        );
1004        assert_eq!(
1005            decoded,
1006            ToolInvokeError::InvalidResult("wrong custom payload".to_string())
1007        );
1008    }
1009
1010    #[test]
1011    async fn one_handle_allows_sequential_owned_invocations() {
1012        let calls = Rc::new(Cell::new(0));
1013        let calls_for_fake = Rc::clone(&calls);
1014        let underlying = UnderlyingTool::from_fake(Box::new(move |path, input, stdin| {
1015            assert!(stdin.is_none());
1016            assert_eq!(path, ["run"]);
1017            calls_for_fake.set(calls_for_fake.get() + 1);
1018            Box::pin(async move {
1019                Ok(wire::InvocationResult {
1020                    result: Some(input),
1021                    stdout: None,
1022                    stderr: None,
1023                })
1024            })
1025        }));
1026
1027        for value in ["first", "second"] {
1028            let result = underlying
1029                .invoke(
1030                    vec!["run".to_string()],
1031                    value.to_string().into_typed_schema_value().unwrap(),
1032                    None,
1033                )
1034                .await
1035                .unwrap();
1036            let decoded = String::from_value(result.result.unwrap().value()).unwrap();
1037            assert_eq!(decoded, value);
1038        }
1039        assert_eq!(calls.get(), 2);
1040    }
1041
1042    #[test]
1043    async fn one_shared_handle_allows_overlapping_invocations_to_complete_in_reverse_order() {
1044        let admitted = Rc::new(Cell::new(0));
1045        let release_first = Rc::new(Cell::new(false));
1046        let admitted_for_fake = Rc::clone(&admitted);
1047        let release_first_for_fake = Rc::clone(&release_first);
1048        let underlying = UnderlyingTool::from_fake(Box::new(move |_, input, _| {
1049            admitted_for_fake.set(admitted_for_fake.get() + 1);
1050            let admitted = Rc::clone(&admitted_for_fake);
1051            let release_first = Rc::clone(&release_first_for_fake);
1052            Box::pin(async move {
1053                let value = decode_typed_schema_value_owned(input).unwrap();
1054                let value = String::from_value(value.value()).unwrap();
1055                if value == "first" {
1056                    std::future::poll_fn(|cx| {
1057                        if release_first.get() {
1058                            std::task::Poll::Ready(())
1059                        } else {
1060                            cx.waker().wake_by_ref();
1061                            std::task::Poll::Pending
1062                        }
1063                    })
1064                    .await;
1065                } else {
1066                    assert_eq!(admitted.get(), 2);
1067                    release_first.set(true);
1068                }
1069                Ok(wire::InvocationResult {
1070                    result: Some(
1071                        encode_typed_schema_value_owned(value.into_typed_schema_value().unwrap())
1072                            .unwrap(),
1073                    ),
1074                    stdout: None,
1075                    stderr: None,
1076                })
1077            })
1078        }));
1079
1080        let first = underlying.invoke(
1081            vec!["run".to_string()],
1082            "first".to_string().into_typed_schema_value().unwrap(),
1083            None,
1084        );
1085        let second = underlying.invoke(
1086            vec!["run".to_string()],
1087            "second".to_string().into_typed_schema_value().unwrap(),
1088            None,
1089        );
1090        let mut first = std::pin::pin!(first);
1091        let mut second = std::pin::pin!(second);
1092        let mut first_result = None;
1093        let mut second_result = None;
1094        let (first, second) = std::future::poll_fn(|cx| {
1095            if first_result.is_none()
1096                && let std::task::Poll::Ready(result) = first.as_mut().poll(cx)
1097            {
1098                first_result = Some(result);
1099            }
1100            if second_result.is_none()
1101                && let std::task::Poll::Ready(result) = second.as_mut().poll(cx)
1102            {
1103                assert!(first_result.is_none());
1104                second_result = Some(result);
1105            }
1106            match (first_result.take(), second_result.take()) {
1107                (Some(first), Some(second)) => std::task::Poll::Ready((first, second)),
1108                (first, second) => {
1109                    first_result = first;
1110                    second_result = second;
1111                    std::task::Poll::Pending
1112                }
1113            }
1114        })
1115        .await;
1116        assert_eq!(admitted.get(), 2);
1117        assert_eq!(
1118            String::from_value(second.unwrap().result.unwrap().value()).unwrap(),
1119            "second"
1120        );
1121        assert_eq!(
1122            String::from_value(first.unwrap().result.unwrap().value()).unwrap(),
1123            "first"
1124        );
1125    }
1126
1127    #[test]
1128    async fn underlying_observer_shares_concurrent_gets_and_caches_terminal() {
1129        let polls = Rc::new(Cell::new(0));
1130        let underlying = UnderlyingTool::from_fake_started(Box::new({
1131            let polls = Rc::clone(&polls);
1132            move |_, input, _| {
1133                let polls = Rc::clone(&polls);
1134                let mut value = Some(input);
1135                let future = Box::pin(std::future::poll_fn(move |cx| {
1136                    polls.set(polls.get() + 1);
1137                    if polls.get() == 1 {
1138                        cx.waker().wake_by_ref();
1139                        std::task::Poll::Pending
1140                    } else {
1141                        std::task::Poll::Ready(Ok(value.take()))
1142                    }
1143                }));
1144                (future as _, None, None, Rc::new(Cell::new(false)))
1145            }
1146        }));
1147        let expected = "shared terminal"
1148            .to_string()
1149            .into_typed_schema_value()
1150            .unwrap();
1151        let invocation = underlying
1152            .start(vec![], expected.clone(), None)
1153            .await
1154            .unwrap();
1155        assert_eq!(polls.get(), 0);
1156        let (first, second) = join_results(invocation.get(), invocation.get()).await;
1157        assert_eq!(first.unwrap(), Some(expected.clone()));
1158        assert_eq!(second.unwrap(), Some(expected.clone()));
1159        assert_eq!(invocation.get().await.unwrap(), Some(expected));
1160        assert_eq!(polls.get(), 2);
1161    }
1162
1163    #[test]
1164    async fn result_wait_and_cancel_share_the_same_invocation_borrow() {
1165        let underlying = UnderlyingTool::from_fake_started(Box::new(|_, _, _| {
1166            let cancelled = Rc::new(Cell::new(false));
1167            let observed = Rc::clone(&cancelled);
1168            let result = Box::pin(std::future::poll_fn(move |cx| {
1169                if observed.get() {
1170                    std::task::Poll::Ready(Err(wire::ToolError::ConstraintViolation(
1171                        "cancelled".to_string(),
1172                    )))
1173                } else {
1174                    cx.waker().wake_by_ref();
1175                    std::task::Poll::Pending
1176                }
1177            }));
1178            (result as _, None, None, cancelled)
1179        }));
1180        let invocation = underlying
1181            .start(
1182                vec!["run".to_string()],
1183                ().into_typed_schema_value().unwrap(),
1184                None,
1185            )
1186            .await
1187            .unwrap();
1188        let get = invocation.get();
1189        let mut get = std::pin::pin!(get);
1190        std::future::poll_fn(|cx| {
1191            assert!(get.as_mut().poll(cx).is_pending());
1192            std::task::Poll::Ready(())
1193        })
1194        .await;
1195        invocation.cancel();
1196        assert!(matches!(
1197            get.await,
1198            Err(ToolInvokeError::ConstraintViolation(message)) if message == "cancelled"
1199        ));
1200        assert!(matches!(
1201            invocation.get().await,
1202            Err(ToolInvokeError::ConstraintViolation(message)) if message == "cancelled"
1203        ));
1204    }
1205
1206    #[test]
1207    fn result_slot_projection_rejects_missing_and_unexpected_values() {
1208        let missing = decode_result_value::<String, ()>(InvocationResult {
1209            result: None,
1210            stdout: None,
1211            stderr: None,
1212        });
1213        assert!(matches!(missing, Err(ToolInvokeError::InvalidResult(_))));
1214
1215        let unexpected = decode_result_empty::<()>(InvocationResult {
1216            result: Some("value".to_string().into_typed_schema_value().unwrap()),
1217            stdout: None,
1218            stderr: None,
1219        });
1220        assert!(matches!(unexpected, Err(ToolInvokeError::InvalidResult(_))));
1221    }
1222
1223    #[test]
1224    fn result_slot_projection_rejects_missing_stream() {
1225        let missing = decode_result_stdout_only::<()>(InvocationResult {
1226            result: None,
1227            stdout: None,
1228            stderr: None,
1229        });
1230        assert!(matches!(missing, Err(ToolInvokeError::InvalidResult(_))));
1231    }
1232}