Skip to content

Commit e6c3e50

Browse files
committed
feat(schema): implement structured result wrapper for tool output schemas
Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com>
1 parent b033b70 commit e6c3e50

File tree

6 files changed

+459
-101
lines changed

6 files changed

+459
-101
lines changed

crates/component2json/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ let wit_vals = json_to_vals(&json_args, &func_param_types)?;
3838

3939
// Convert WIT values back to JSON
4040
let json_result = vals_to_json(&wit_vals);
41+
assert_eq!(json_result, serde_json::json!({"result": {"val0": "example", "val1": 42}}));
4142

4243
// Create placeholder results for function call results
4344
// This is useful when you need to prepare storage for function return values
@@ -47,6 +48,15 @@ let placeholder_results = create_placeholder_results(&result_types);
4748
# }
4849
```
4950

51+
## Structured Result Wrapper
52+
53+
All function return values produced by `component_exports_to_json_schema` and `vals_to_json` are wrapped in an object with a required `result` property.
54+
55+
- Single return values appear as `{ "result": VALUE }`.
56+
- Multiple return values appear as `{ "result": { "val0": VALUE0, "val1": VALUE1, ... } }`.
57+
58+
The generated `outputSchema` for each tool mirrors this shape, ensuring downstream consumers can always access the payload through the `result` key.
59+
5060
## Type Conversion Specification
5161

5262
### WIT to JSON Schema

crates/component2json/src/lib.rs

Lines changed: 123 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -161,13 +161,20 @@ pub fn component_exports_to_json_schema(
161161
pub fn vals_to_json(vals: &[Val]) -> Value {
162162
match vals.len() {
163163
0 => Value::Null,
164-
1 => val_to_json(&vals[0]),
164+
1 => {
165+
let mut wrapper = Map::new();
166+
wrapper.insert("result".to_string(), val_to_json(&vals[0]));
167+
Value::Object(wrapper)
168+
}
165169
_ => {
166-
let mut map = Map::new();
170+
let mut tuple_map = Map::new();
167171
for (i, v) in vals.iter().enumerate() {
168-
map.insert(format!("val{i}"), val_to_json(v));
172+
tuple_map.insert(format!("val{i}"), val_to_json(v));
169173
}
170-
Value::Object(map)
174+
175+
let mut wrapper = Map::new();
176+
wrapper.insert("result".to_string(), Value::Object(tuple_map));
177+
Value::Object(wrapper)
171178
}
172179
}
173180
}
@@ -394,25 +401,55 @@ fn component_func_to_schema(name: &str, func: &ComponentFunc, output: bool) -> s
394401
tool_obj.insert("inputSchema".to_string(), input_schema);
395402

396403
if output {
397-
let mut results_iter = func.results();
398-
let output_schema = match results_iter.len() {
399-
0 => None,
400-
1 => Some(type_to_json_schema(&results_iter.next().unwrap())),
401-
_ => {
402-
let schemas: Vec<_> = results_iter.map(|ty| type_to_json_schema(&ty)).collect();
403-
Some(json!({
404-
"type": "array",
405-
"items": schemas
406-
}))
407-
}
408-
};
409-
if let Some(o) = output_schema {
404+
let results: Vec<_> = func.results().collect();
405+
if let Some(o) = canonical_output_schema_for_results(&results) {
410406
tool_obj.insert("outputSchema".to_string(), o);
411407
}
412408
}
413409
json!(tool_obj)
414410
}
415411

412+
fn canonical_output_schema_for_results(results: &[Type]) -> Option<Value> {
413+
if results.is_empty() {
414+
return None;
415+
}
416+
417+
let result_schema = if results.len() == 1 {
418+
type_to_json_schema(&results[0])
419+
} else {
420+
let mut props = Map::new();
421+
let mut required = Vec::new();
422+
423+
for (idx, ty) in results.iter().enumerate() {
424+
let key = format!("val{idx}");
425+
props.insert(key.clone(), type_to_json_schema(ty));
426+
required.push(Value::String(key));
427+
}
428+
429+
let mut tuple_schema = Map::new();
430+
tuple_schema.insert("type".to_string(), Value::String("object".to_string()));
431+
tuple_schema.insert("properties".to_string(), Value::Object(props));
432+
tuple_schema.insert("required".to_string(), Value::Array(required));
433+
Value::Object(tuple_schema)
434+
};
435+
436+
Some(build_result_wrapper(result_schema))
437+
}
438+
439+
fn build_result_wrapper(result_schema: Value) -> Value {
440+
let mut props = Map::new();
441+
props.insert("result".to_string(), result_schema);
442+
443+
let mut wrapper = Map::new();
444+
wrapper.insert("type".to_string(), Value::String("object".to_string()));
445+
wrapper.insert("properties".to_string(), Value::Object(props));
446+
wrapper.insert(
447+
"required".to_string(),
448+
Value::Array(vec![Value::String("result".to_string())]),
449+
);
450+
Value::Object(wrapper)
451+
}
452+
416453
fn gather_exported_functions_with_metadata(
417454
export_name: &str,
418455
previous_name: Option<String>,
@@ -842,6 +879,13 @@ mod tests {
842879

843880
use super::*;
844881

882+
fn result_schema<'a>(schema: &'a Value) -> &'a Value {
883+
schema
884+
.get("properties")
885+
.and_then(|props| props.get("result"))
886+
.unwrap_or(schema)
887+
}
888+
845889
#[test]
846890
fn test_vals_to_json_empty() {
847891
let json_val = vals_to_json(&[]);
@@ -852,17 +896,54 @@ mod tests {
852896
fn test_vals_to_json_single() {
853897
let val = Val::Bool(true);
854898
let json_val = vals_to_json(std::slice::from_ref(&val));
855-
assert_eq!(json_val, val_to_json(&val));
899+
assert_eq!(json_val, json!({"result": true}));
900+
}
901+
902+
#[test]
903+
fn test_canonical_output_schema_single_scalar() {
904+
let schema = canonical_output_schema_for_results(&[Type::String]).unwrap();
905+
assert_eq!(
906+
schema,
907+
json!({
908+
"type": "object",
909+
"properties": {
910+
"result": { "type": "string" }
911+
},
912+
"required": ["result"]
913+
})
914+
);
915+
}
916+
917+
#[test]
918+
fn test_canonical_output_schema_multi_result() {
919+
let schema = canonical_output_schema_for_results(&[Type::String, Type::S64]).unwrap();
920+
assert_eq!(
921+
schema,
922+
json!({
923+
"type": "object",
924+
"properties": {
925+
"result": {
926+
"type": "object",
927+
"properties": {
928+
"val0": { "type": "string" },
929+
"val1": { "type": "number" }
930+
},
931+
"required": ["val0", "val1"]
932+
}
933+
},
934+
"required": ["result"]
935+
})
936+
);
856937
}
857938

858939
#[test]
859940
fn test_vals_to_json_multiple_values() {
860941
let wit_vals = vec![Val::String("example".to_string()), Val::S64(42)];
861942
let json_result = vals_to_json(&wit_vals);
862-
863-
let obj = json_result.as_object().unwrap();
864-
assert_eq!(obj.get("val0").unwrap(), &json!("example"));
865-
assert_eq!(obj.get("val1").unwrap(), &json!(42));
943+
assert_eq!(
944+
json_result,
945+
json!({"result": {"val0": "example", "val1": 42}})
946+
);
866947
}
867948

868949
#[test]
@@ -1053,9 +1134,10 @@ mod tests {
10531134
}
10541135

10551136
let output_schema = tool.get("outputSchema").unwrap();
1137+
let result_schema_ref = result_schema(output_schema);
10561138
if expected_exports[i] == "list-directory" {
10571139
assert!(
1058-
output_schema.get("oneOf").unwrap().as_array().unwrap()[0]
1140+
result_schema_ref.get("oneOf").unwrap().as_array().unwrap()[0]
10591141
.get("properties")
10601142
.unwrap()
10611143
.get("ok")
@@ -1066,7 +1148,7 @@ mod tests {
10661148
);
10671149
} else {
10681150
assert!(
1069-
output_schema.get("oneOf").unwrap().as_array().unwrap()[0]
1151+
result_schema_ref.get("oneOf").unwrap().as_array().unwrap()[0]
10701152
.get("properties")
10711153
.unwrap()
10721154
.get("ok")
@@ -1113,7 +1195,7 @@ mod tests {
11131195
assert!(properties.contains_key("wit"));
11141196

11151197
let output_schema = generate_tool.get("outputSchema").unwrap();
1116-
assert!(output_schema.get("oneOf").is_some());
1198+
assert!(result_schema(output_schema).get("oneOf").is_some());
11171199
}
11181200

11191201
#[test]
@@ -1293,14 +1375,15 @@ mod tests {
12931375
assert!(properties.contains_key("c"));
12941376
assert!(properties.contains_key("d"));
12951377
let output_schema = root_b.get("outputSchema").unwrap();
1296-
assert_eq!(output_schema.get("type").unwrap(), "string");
1378+
assert_eq!(result_schema(output_schema).get("type").unwrap(), "string");
12971379

12981380
let root_c = find_tool(tools, "c").unwrap();
12991381
let output_schema = root_c.get("outputSchema").unwrap();
1300-
assert_eq!(output_schema.get("type").unwrap(), "array");
1301-
assert_eq!(output_schema.get("minItems").unwrap(), 4);
1302-
assert_eq!(output_schema.get("maxItems").unwrap(), 4);
1303-
let prefix_items = output_schema
1382+
let result_schema_ref = result_schema(output_schema);
1383+
assert_eq!(result_schema_ref.get("type").unwrap(), "array");
1384+
assert_eq!(result_schema_ref.get("minItems").unwrap(), 4);
1385+
assert_eq!(result_schema_ref.get("maxItems").unwrap(), 4);
1386+
let prefix_items = result_schema_ref
13041387
.get("prefixItems")
13051388
.unwrap()
13061389
.as_array()
@@ -1333,7 +1416,11 @@ mod tests {
13331416
assert!(input_props.contains_key("x")); // string
13341417

13351418
let output_schema = foo_b.get("outputSchema").unwrap();
1336-
let cases = output_schema.get("oneOf").unwrap().as_array().unwrap();
1419+
let cases = result_schema(output_schema)
1420+
.get("oneOf")
1421+
.unwrap()
1422+
.as_array()
1423+
.unwrap();
13371424
assert_eq!(cases.len(), 3);
13381425

13391426
let case_a = &cases[0];
@@ -1406,7 +1493,7 @@ mod tests {
14061493
assert!(input_props.contains_key("x")); // variant type
14071494

14081495
let output_schema = foo_c.get("outputSchema").unwrap();
1409-
assert_eq!(output_schema.get("type").unwrap(), "string");
1496+
assert_eq!(result_schema(output_schema).get("type").unwrap(), "string");
14101497
}
14111498
}
14121499

@@ -1448,10 +1535,10 @@ mod tests {
14481535
fn test_vals_to_json_multiple() {
14491536
let wit_vals = vec![Val::String("example".to_string()), Val::S64(42)];
14501537
let json_result = vals_to_json(&wit_vals);
1451-
1452-
let obj = json_result.as_object().unwrap();
1453-
assert_eq!(obj.get("val0").unwrap(), &json!("example"));
1454-
assert_eq!(obj.get("val1").unwrap(), &json!(42));
1538+
assert_eq!(
1539+
json_result,
1540+
json!({"result": {"val0": "example", "val1": 42}})
1541+
);
14551542
}
14561543

14571544
#[test]

0 commit comments

Comments
 (0)