blob: 16d3ed383e366318534e2f11cfa1e813b795168d [file]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
part of 'api.dart';
/// A request an [InputRequiredResult] carries, written the way the schema
/// writes it: the method name next to the request it applies to.
///
/// The schema names [ElicitRequest], [CreateMessageRequest] and
/// [ListRootsRequest] here and nothing else, so there is a constructor for
/// each. Reading one back, dispatch on [method] and then read [params] as the
/// matching type.
///
/// From the 2026-07-28 revision.
extension type InputRequest._(Map<String, Object?> _value) {
/// The methods a request here may be made under.
static const methodNames = {
ElicitRequest.methodName,
CreateMessageRequest.methodName,
ListRootsRequest.methodName,
};
factory InputRequest.fromMap(Map<String, Object?> value) {
assert(value.containsKey(Keys.method));
return InputRequest._(value);
}
factory InputRequest.elicit(ElicitRequest request) => InputRequest._({
Keys.method: ElicitRequest.methodName,
Keys.params: request,
});
factory InputRequest.sample(CreateMessageRequest request) => InputRequest._({
Keys.method: CreateMessageRequest.methodName,
Keys.params: request,
});
factory InputRequest.listRoots(ListRootsRequest request) => InputRequest._({
Keys.method: ListRootsRequest.methodName,
Keys.params: request,
});
/// The method this request is made under.
///
/// The schema requires this on every kind it allows here.
String get method => _value[Keys.method] as String;
/// The request itself, which [method] says how to read.
///
/// The schema requires this next to `elicitation/create` and
/// `sampling/createMessage`. For `roots/list` it only requires the method.
///
/// Throws an [ArgumentError] when a server sent something other than an
/// object here.
Request? get params {
final params = _value[Keys.params];
if (params == null) return null;
if (params is! Map) {
throw ArgumentError(
'The input request params for "$method" were ${params.runtimeType}, '
'expected an object.',
);
}
return params.cast<String, Object?>() as Request;
}
/// Whether this carries an [ElicitRequest].
bool get isElicit => _value[Keys.method] == ElicitRequest.methodName;
/// Whether this carries a [CreateMessageRequest].
bool get isSample => _value[Keys.method] == CreateMessageRequest.methodName;
/// Whether this carries a [ListRootsRequest].
bool get isListRoots => _value[Keys.method] == ListRootsRequest.methodName;
}
/// Sent by a server in place of the result a request asked for, when it needs
/// something from the client first.
///
/// The schema answers with one on `tools/call`, `prompts/get` and
/// `resources/read`, and [Result.resultType] reads `input_required` on it. The
/// client answers the [inputRequests] and then sends the original request
/// again, with the answers and the [requestState] attached. That retry is a
/// new request, so this result ends the exchange it belongs to.
///
/// The schema requires at least one of [inputRequests] and [requestState],
/// since a result carrying neither asks for nothing and hands back nothing to
/// retry with. The unnamed constructor asserts it, and goes one step further
/// by treating an empty [inputRequests] the same way. An empty map is valid on
/// the wire and a server may mean something by it, and `fromMap` keeps
/// whatever a server sent.
///
/// From the 2026-07-28 revision.
extension type InputRequiredResult.fromMap(Map<String, Object?> _value)
implements CallToolResponse, GetPromptResponse, ReadResourceResponse {
factory InputRequiredResult({
Map<String, InputRequest>? inputRequests,
String? requestState,
Meta? meta,
}) {
assert(
(inputRequests != null && inputRequests.isNotEmpty) ||
requestState != null,
'The schema requires at least one of `inputRequests` and `requestState`. '
'An empty `inputRequests` is valid on the wire, and this constructor '
'still takes it as asking for nothing.',
);
return InputRequiredResult.fromMap({
Keys.resultType: ResultTypes.inputRequired,
if (inputRequests != null) Keys.inputRequests: inputRequests,
if (requestState != null) Keys.requestState: requestState,
if (meta != null) Keys.meta: meta,
});
}
/// The requests the server issued, keyed by the name the client answers each
/// one under.
///
/// The keys are the server's own identifiers, and the answers go back under
/// the same ones.
Map<String, InputRequest>? get inputRequests =>
(_value[Keys.inputRequests] as Map?)?.cast<String, InputRequest>();
/// State the server wants back when the client retries the request.
///
/// This is opaque to the client, which sends it on unread.
String? get requestState => _value[Keys.requestState] as String?;
}
/// A "mixin"-like extension type for any request that contains input responses
/// at the keys "inputResponses" and "requestState".
///
/// These are the other half of an [InputRequiredResult], what a client puts on
/// the retry. The requests which can answer with one take them.
///
/// Should be "mixed in" by implementing this type from other extension types.
///
/// This type is not intended to be constructed directly and thus has no public
/// constructor.
///
/// From the 2026-07-28 revision.
extension type WithInputResponses._fromMap(Map<String, Object?> _value)
implements Request {
/// What the client got for each request in
/// [InputRequiredResult.inputRequests], under the keys the server gave them.
///
/// These carry no method field the way an [InputRequest] does, so a server
/// reads each back as the type it asked for under that key, through
/// [elicitResult], [createMessageResult] or [listRootsResult]. A key it did
/// not ask for is one to ignore.
Map<String, Result>? get inputResponses =>
(_value[Keys.inputResponses] as Map?)?.cast<String, Result>();
/// The elicitation arm of the schema `InputResponse` union under [key].
///
/// Missing [key] is null. A value without `action` throws [ArgumentError].
ElicitResult? elicitResult(String key) {
final value = _typedInputResponse(key, const [Keys.action]);
return value == null ? null : value as ElicitResult;
}
/// The sampling arm of the schema `InputResponse` union under [key].
///
/// Missing [key] is null. A value without `content`, `model` and `role`
/// throws [ArgumentError].
CreateMessageResult? createMessageResult(String key) {
final value = _typedInputResponse(key, const [
Keys.content,
Keys.model,
Keys.role,
]);
return value == null ? null : value as CreateMessageResult;
}
/// The roots arm of the schema `InputResponse` union under [key].
///
/// Missing [key] is null. A value without `roots` throws [ArgumentError].
ListRootsResult? listRootsResult(String key) {
final value = _typedInputResponse(key, const [Keys.roots]);
return value == null ? null : value as ListRootsResult;
}
Map<String, Object?>? _typedInputResponse(
String key,
List<String> requiredKeys,
) {
final responses = _value[Keys.inputResponses] as Map<String, Object?>?;
if (responses == null || !responses.containsKey(key)) return null;
final value = responses[key];
if (value is! Map<String, Object?>) {
throw ArgumentError(
'The input response "$key" was ${value.runtimeType}, expected an '
'object.',
);
}
// The schema requires these keys on this arm of the union. Without them
// the cast in the caller would hand back the wrong type without a word.
for (final required in requiredKeys) {
if (!value.containsKey(required)) {
throw ArgumentError(
'The input response "$key" was missing required "$required".',
);
}
}
return value;
}
/// The [InputRequiredResult.requestState] the server sent, echoed back
/// unread.
///
/// The value arrives from the client, and the spec has the server treat it
/// as attacker-controlled input. Nothing here signs or verifies it. A server
/// whose state carries anything it would not accept straight off the wire
/// has to protect and check it itself.
String? get requestState => _value[Keys.requestState] as String?;
}