Skip to main content

ferrin_tool/
model_output.rs

1//! Conversion of execution results into the output the model receives.
2
3use ferrin_spec::JsonValue;
4use ferrin_spec::ToolCallId;
5use ferrin_spec::language_model::prompt::ToolResultOutput;
6
7use crate::error::ToolError;
8use crate::tool::ModelOutputArgs;
9use crate::tool::Tool;
10
11/// How an output should be reported.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13#[non_exhaustive]
14pub enum ErrorMode {
15    /// A successful result.
16    #[default]
17    None,
18    /// An error rendered as text.
19    Text,
20    /// An error rendered as JSON.
21    Json,
22}
23
24/// Builds the model-facing output for a tool result.
25///
26/// Errors become `error-text` / `error-json`; successful outputs go through
27/// the tool's `to_model_output` when defined, otherwise strings become
28/// `text` and everything else `json`.
29#[must_use]
30pub fn create_tool_model_output(
31    tool: Option<&Tool>,
32    tool_call_id: &ToolCallId,
33    input: &JsonValue,
34    output: &JsonValue,
35    error_mode: ErrorMode,
36) -> ToolResultOutput {
37    match error_mode {
38        ErrorMode::Text => return ToolResultOutput::error_text(error_message(output)),
39        ErrorMode::Json => return ToolResultOutput::error_json(output.clone()),
40        ErrorMode::None => {}
41    }
42    if let Some(convert) = tool.and_then(Tool::to_model_output) {
43        return convert(ModelOutputArgs {
44            tool_call_id,
45            input,
46            output,
47        });
48    }
49    match output {
50        JsonValue::String(text) => ToolResultOutput::text(text.clone()),
51        other => ToolResultOutput::json(other.clone()),
52    }
53}
54
55/// Renders a [`ToolError`] as a model-facing output.
56#[must_use]
57pub fn tool_error_output(error: &ToolError) -> ToolResultOutput {
58    match error {
59        ToolError::Json { value } => ToolResultOutput::error_json(value.clone()),
60        other => ToolResultOutput::error_text(other.to_string()),
61    }
62}
63
64/// Extracts a message from an error value: strings as-is, objects through
65/// their `message` field, `null` as `unknown error`, anything else as JSON.
66#[must_use]
67pub fn error_message(value: &JsonValue) -> String {
68    match value {
69        JsonValue::Null => "unknown error".to_owned(),
70        JsonValue::String(text) => text.clone(),
71        JsonValue::Object(map) => match map.get("message") {
72            Some(JsonValue::String(message)) => message.clone(),
73            _ => value.to_string(),
74        },
75        other => other.to_string(),
76    }
77}