Skip to main content

lash_lashlang_runtime/
typed_output.rs

1//! The RLM typed-output vocabulary: parsing the `output` argument that
2//! RLM tools (`llm.query`, `agents.spawn`) accept into a JSON Schema.
3
4use serde_json::Value;
5
6/// Parse a tool's `output` argument into the JSON Schema it requests.
7///
8/// Two shapes are accepted:
9/// - a record of field-name → type-descriptor strings (`"str"`, `"int"`,
10///   `"float"`, `"bool"`, `"record"`, or `"list[...]"` of those), compiled
11///   into a strict object schema; or
12/// - a Lashlang `Type { ... }` literal — a single-field
13///   `{"$lash_type": <schema>}` wrapper as produced by the Lashlang
14///   compiler — whose inner schema is passed through after validation.
15///
16/// Returns `Ok(None)` when `output` is absent or `null` (the tool falls back
17/// to its untyped default).
18pub fn parse_output_schema(value: Option<&Value>) -> Result<Option<Value>, String> {
19    lashlang::parse_output_schema(value)
20}
21
22#[cfg(test)]
23mod tests {
24    use serde_json::json;
25
26    use super::*;
27    use crate::LASH_TYPE_KEY;
28
29    #[test]
30    fn output_schema_supports_scalars_and_lists() {
31        let schema = parse_output_schema(Some(&json!({
32            "answer": "str",
33            "count": "int",
34            "items": "list[str]"
35        })))
36        .expect("schema")
37        .expect("present");
38        assert_eq!(schema["properties"]["answer"]["type"], json!("string"));
39        assert_eq!(schema["properties"]["count"]["type"], json!("integer"));
40        assert_eq!(schema["properties"]["items"]["type"], json!("array"));
41    }
42
43    #[test]
44    fn output_schema_passes_through_lash_type_wrapper() {
45        let inner_schema = json!({
46            "type": "object",
47            "properties": {
48                "name": { "type": "string" },
49                "tags": { "type": "array", "items": { "type": "string" } },
50                "status": { "type": "string", "enum": ["ok", "err"] }
51            },
52            "required": ["name", "tags", "status"],
53            "additionalProperties": false
54        });
55        let wrapped = json!({ LASH_TYPE_KEY: inner_schema.clone() });
56        let schema = parse_output_schema(Some(&wrapped))
57            .expect("schema")
58            .expect("present");
59        assert_eq!(schema, inner_schema);
60    }
61
62    #[test]
63    fn output_schema_rejects_lash_type_without_type_field() {
64        let wrapped = json!({ LASH_TYPE_KEY: {"properties": {}} });
65        let err = parse_output_schema(Some(&wrapped)).expect_err("missing type");
66        assert!(err.contains("type"), "error: {err}");
67    }
68
69    #[test]
70    fn output_schema_accepts_array_top_level_type() {
71        let wrapped = json!({
72            LASH_TYPE_KEY: {
73                "type": "array",
74                "items": {"type": "string"}
75            }
76        });
77        let schema = parse_output_schema(Some(&wrapped))
78            .expect("schema")
79            .expect("present");
80        assert_eq!(schema["type"], json!("array"));
81    }
82}