feat: multi-round-trip request implementation (SEP-2322) (#950)
### Context
Design (`design/mrtr.md`) and implementation proposal for Multi
Round-Trip Requests (MRTR) per
[SEP-2322](de6d76fba3/seps/2322-MRTR.md).
### Changes
- Introduce `InputRequest` and `InputResponse` sealed interfaces with
corresponding `InputRequestMap` and `InputResponseMap` types for custom
map JSON codec implementation.
- Extend `CallToolParams`, `GetPromptParams`, and `ReadResourceParams`
with `InputResponses`, `RequestState` and an unexported `resultType`
("complete" or "input_required") for retry round-trips. The type is set
by the SDK based on input requests presence.
- Add client-side MRTR middleware enabled by default that automatically
fulfills input requests and retries.
- Added MRTROptions on ClientOptions to allow disabling the middleware
and configuring the number of retries.
- Add server-side MRTR middleware for backward compatibility:
transparently bridges input requests to direct
Elicit/CreateMessage/ListRoots calls for older clients
This commit is contained in:
parent
2d47cc9664
commit
189a85ad92
1 changed files with 1587 additions and 23 deletions
|
|
@ -1151,6 +1151,71 @@ func TestToWithTools_Conversion(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestInputRequestMapJSON(t *testing.T) {
|
||||
t.Run("nil is omitted from JSON", func(t *testing.T) {
|
||||
result := CallToolResult{Content: []Content{&TextContent{Text: "ok"}}}
|
||||
data, err := json.Marshal(&result)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := raw["inputRequests"]; ok {
|
||||
t.Errorf("nil InputRequests should be omitted, got %s", raw["inputRequests"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-nil empty round-trips", func(t *testing.T) {
|
||||
result := CallToolResult{
|
||||
Content: []Content{&TextContent{Text: "ok"}},
|
||||
InputRequests: InputRequestMap{},
|
||||
}
|
||||
data, err := json.Marshal(&result)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(raw["inputRequests"]) != "{}" {
|
||||
t.Errorf("empty InputRequests should marshal to {}, got %s", raw["inputRequests"])
|
||||
}
|
||||
var got CallToolResult
|
||||
if err := json.Unmarshal(data, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.InputRequests == nil {
|
||||
t.Error("empty InputRequests should round-trip as non-nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("populated round-trips", func(t *testing.T) {
|
||||
result := CallToolResult{
|
||||
Content: []Content{&TextContent{Text: "ok"}},
|
||||
InputRequests: InputRequestMap{
|
||||
"r1": &ElicitParams{Message: "confirm?"},
|
||||
},
|
||||
}
|
||||
data, err := json.Marshal(&result)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got CallToolResult
|
||||
if err := json.Unmarshal(data, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.InputRequests == nil {
|
||||
t.Fatal("InputRequests should not be nil after round-trip")
|
||||
}
|
||||
if _, ok := got.InputRequests["r1"]; !ok {
|
||||
t.Error("expected key r1 in InputRequests")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestContentUnmarshal(t *testing.T) {
|
||||
// Verify that types with a Content field round-trip properly.
|
||||
roundtrip := func(in, out any) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue