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
|
|
@ -187,7 +187,7 @@ func NewServer(impl *Implementation, options *ServerOptions) *Server {
|
|||
opts.Logger = ensureLogger(nil)
|
||||
}
|
||||
|
||||
return &Server{
|
||||
s := &Server{
|
||||
impl: impl,
|
||||
opts: opts,
|
||||
prompts: newFeatureSet(func(p *serverPrompt) string { return p.prompt.Name }),
|
||||
|
|
@ -199,6 +199,8 @@ func NewServer(impl *Implementation, options *ServerOptions) *Server {
|
|||
resourceSubscriptions: make(map[string]map[*ServerSession]bool),
|
||||
pendingNotifications: make(map[string]*time.Timer),
|
||||
}
|
||||
s.AddReceivingMiddleware(serverMultiRoundTripMiddleware())
|
||||
return s
|
||||
}
|
||||
|
||||
// AddPrompt adds a [Prompt] to the server, or replaces one with the same name.
|
||||
|
|
@ -370,8 +372,12 @@ func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out], cache *SchemaCa
|
|||
}
|
||||
|
||||
// Marshal the output and put the RawMessage in the StructuredContent field.
|
||||
// Skip when the handler returned input requests (multi round-trip): content and
|
||||
// inputRequests are mutually exclusive on the wire.
|
||||
var outval any = out
|
||||
if elemZero != nil {
|
||||
if res.InputRequests != nil {
|
||||
outval = nil
|
||||
} else if elemZero != nil {
|
||||
// Avoid typed nil, which will serialize as JSON null.
|
||||
// Instead, use the zero value of the unpointered type.
|
||||
var z Out
|
||||
|
|
@ -742,7 +748,13 @@ func (s *Server) getPrompt(ctx context.Context, req *GetPromptRequest) (*GetProm
|
|||
Message: fmt.Sprintf("unknown prompt %q", req.Params.Name),
|
||||
}
|
||||
}
|
||||
return prompt.handler(ctx, req)
|
||||
res, err := prompt.handler(ctx, req)
|
||||
if err == nil && res != nil {
|
||||
if err := handleMultiRoundTripResult(req.Session, s.opts.Logger, res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (s *Server) listTools(_ context.Context, req *ListToolsRequest) (*ListToolsResult, error) {
|
||||
|
|
@ -775,10 +787,15 @@ func (s *Server) callTool(ctx context.Context, req *CallToolRequest) (*CallToolR
|
|||
}
|
||||
}
|
||||
res, err := st.handler(ctx, req)
|
||||
if err == nil && res != nil && res.Content == nil {
|
||||
res2 := *res
|
||||
res2.Content = []Content{} // avoid "null"
|
||||
res = &res2
|
||||
if err == nil && res != nil {
|
||||
if err := handleMultiRoundTripResult(req.Session, s.opts.Logger, res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.Content == nil && res.resultType != resultTypeInputRequired {
|
||||
res2 := *res
|
||||
res2.Content = []Content{} // avoid "null"
|
||||
res = &res2
|
||||
}
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
|
@ -826,6 +843,12 @@ func (s *Server) readResource(ctx context.Context, req *ReadResourceRequest) (*R
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := handleMultiRoundTripResult(req.Session, s.opts.Logger, res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.resultType == resultTypeInputRequired {
|
||||
return res, nil
|
||||
}
|
||||
if res == nil || res.Contents == nil {
|
||||
return nil, fmt.Errorf("reading resource %s: read handler returned nil information", uri)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue