// Elicitation: the agent asks the user for a few fields, and the client renders them. The schema is
// the protocol's own small subset of JSON Schema, so it is typed here rather than passed through.
///|
/// A string field's format hint.
#derive(Eq)
pub(all) enum StringFormat {
Email
Uri
Date
DateTime
}
///|
pub extend StringFormat with Eq::{equal, not_equal}
///|
/// The name the format travels as.
pub fn StringFormat::to_string(self : StringFormat) -> String {
match self {
Email => "email"
Uri => "uri"
Date => "date"
DateTime => "date-time"
}
}
///|
/// The format with that name, or `None`.
pub fn StringFormat::of(name : String) -> StringFormat? {
match name {
"email" => Some(Email)
"uri" => Some(Uri)
"date" => Some(Date)
"date-time" => Some(DateTime)
_ => None
}
}
///|
pub extend StringFormat with @json.ToJson::{to_json}
///|
pub impl ToJson for StringFormat with fn to_json(self) {
Json::string(self.to_string())
}
///|
pub extend StringFormat with @json.FromJson::{from_json}
///|
pub impl @json.FromJson for StringFormat with fn from_json(json, path) {
match StringFormat::of(text_of(json, path)) {
Some(format) => format
None => raise want(path, "a string format", json)
}
}
///|
/// One titled choice of a string field.
#derive(Eq)
pub(all) struct EnumOption {
value : String
title : String
description : String?
meta : Json?
}
///|
pub extend EnumOption with Eq::{equal, not_equal}
///|
pub extend EnumOption with @json.ToJson::{to_json}
///|
pub impl ToJson for EnumOption with fn to_json(self) {
json_of([
("const", Some(Json::string(self.value))),
("title", Some(Json::string(self.title))),
("description", json_string(self.description)),
("_meta", self.meta),
])
}
///|
pub extend EnumOption with @json.FromJson::{from_json}
///|
pub impl @json.FromJson for EnumOption with fn from_json(json, path) {
let fields = expect_object(json, path)
{
value: required_string(fields, "const", path),
title: required_string(fields, "title", path),
description: string_member(fields, "description", path),
meta: meta_of(fields, path),
}
}
///|
/// A field the user types into.
#derive(Eq)
pub(all) struct TextField {
title : String?
description : String?
min_length : Int64?
max_length : Int64?
pattern : String?
format : StringFormat?
default : String?
choices : Array[String]?
titled_choices : Array[EnumOption]?
meta : Json?
}
///|
pub extend TextField with Eq::{equal, not_equal}
///|
pub extend TextField with @json.ToJson::{to_json}
///|
pub impl ToJson for TextField with fn to_json(self) {
json_of([
("title", json_string(self.title)),
("description", json_string(self.description)),
("minLength", json_int(self.min_length)),
("maxLength", json_int(self.max_length)),
("pattern", json_string(self.pattern)),
("format", self.format.map(write)),
("default", json_string(self.default)),
("enum", json_list(self.choices, Json::string)),
("oneOf", json_list_of(self.titled_choices)),
("_meta", self.meta),
])
}
///|
pub extend TextField with @json.FromJson::{from_json}
///|
pub impl @json.FromJson for TextField with fn from_json(json, path) {
let fields = expect_object(json, path)
{
title: string_member(fields, "title", path),
description: string_member(fields, "description", path),
min_length: int_member(fields, "minLength", path),
max_length: int_member(fields, "maxLength", path),
pattern: string_member(fields, "pattern", path),
format: enum_member(fields, "format", path, StringFormat::of),
default: string_member(fields, "default", path),
choices: list_member(fields, "enum", path, (value, at) => text_of(value, at)),
titled_choices: list_member(fields, "oneOf", path, (value, at) => {
read(value, at)
}),
meta: meta_of(fields, path),
}
}
///|
/// A field for a fractional number.
#derive(Eq)
pub(all) struct NumberField {
title : String?
description : String?
minimum : Double?
maximum : Double?
default : Double?
meta : Json?
}
///|
pub extend NumberField with Eq::{equal, not_equal}
///|
pub extend NumberField with @json.ToJson::{to_json}
///|
pub impl ToJson for NumberField with fn to_json(self) {
json_of([
("title", json_string(self.title)),
("description", json_string(self.description)),
("minimum", json_float(self.minimum)),
("maximum", json_float(self.maximum)),
("default", json_float(self.default)),
("_meta", self.meta),
])
}
///|
pub extend NumberField with @json.FromJson::{from_json}
///|
pub impl @json.FromJson for NumberField with fn from_json(json, path) {
let fields = expect_object(json, path)
{
title: string_member(fields, "title", path),
description: string_member(fields, "description", path),
minimum: float_member(fields, "minimum", path),
maximum: float_member(fields, "maximum", path),
default: float_member(fields, "default", path),
meta: meta_of(fields, path),
}
}
///|
/// A field for a whole number.
#derive(Eq)
pub(all) struct IntegerField {
title : String?
description : String?
minimum : Int64?
maximum : Int64?
default : Int64?
meta : Json?
}
///|
pub extend IntegerField with Eq::{equal, not_equal}
///|
pub extend IntegerField with @json.ToJson::{to_json}
///|
pub impl ToJson for IntegerField with fn to_json(self) {
json_of([
("title", json_string(self.title)),
("description", json_string(self.description)),
("minimum", json_int(self.minimum)),
("maximum", json_int(self.maximum)),
("default", json_int(self.default)),
("_meta", self.meta),
])
}
///|
pub extend IntegerField with @json.FromJson::{from_json}
///|
pub impl @json.FromJson for IntegerField with fn from_json(json, path) {
let fields = expect_object(json, path)
{
title: string_member(fields, "title", path),
description: string_member(fields, "description", path),
minimum: int_member(fields, "minimum", path),
maximum: int_member(fields, "maximum", path),
default: int_member(fields, "default", path),
meta: meta_of(fields, path),
}
}
///|
/// A field for yes or no.
#derive(Eq)
pub(all) struct BooleanField {
title : String?
description : String?
default : Bool?
meta : Json?
}
///|
pub extend BooleanField with Eq::{equal, not_equal}
///|
pub extend BooleanField with @json.ToJson::{to_json}
///|
pub impl ToJson for BooleanField with fn to_json(self) {
json_of([
("title", json_string(self.title)),
("description", json_string(self.description)),
("default", json_bool(self.default)),
("_meta", self.meta),
])
}
///|
pub extend BooleanField with @json.FromJson::{from_json}
///|
pub impl @json.FromJson for BooleanField with fn from_json(json, path) {
let fields = expect_object(json, path)
{
title: string_member(fields, "title", path),
description: string_member(fields, "description", path),
default: bool_member(fields, "default", path),
meta: meta_of(fields, path),
}
}
///|
/// What a multi-select offers: bare values or titled ones.
#derive(Eq)
pub(all) enum ChoiceItems {
Values(Array[String])
Titled(Array[EnumOption])
}
///|
pub extend ChoiceItems with Eq::{equal, not_equal}
///|
pub extend ChoiceItems with @json.ToJson::{to_json}
///|
pub impl ToJson for ChoiceItems with fn to_json(self) {
match self {
Values(values) => Json::array(values.map(Json::string))
Titled(options) => Json::array(options.map(write))
}
}
///|
/// A field for picking several of a set.
#derive(Eq)
pub(all) struct MultiSelectField {
title : String?
description : String?
min_items : Int64?
max_items : Int64?
items : ChoiceItems
default : Array[String]?
meta : Json?
}
///|
pub extend MultiSelectField with Eq::{equal, not_equal}
///|
pub extend MultiSelectField with @json.ToJson::{to_json}
///|
pub impl ToJson for MultiSelectField with fn to_json(self) {
json_of([
("title", json_string(self.title)),
("description", json_string(self.description)),
("minItems", json_int(self.min_items)),
("maxItems", json_int(self.max_items)),
("items", Some(write(self.items))),
("default", json_list(self.default, Json::string)),
("_meta", self.meta),
])
}
///|
pub extend MultiSelectField with @json.FromJson::{from_json}
///|
pub impl @json.FromJson for MultiSelectField with fn from_json(json, path) {
let fields = expect_object(json, path)
let items = match field_of(fields, "items", path) {
None => Values([])
Some(value) => {
let at = path.add_key("items")
if value is Array(entries) &&
entries.length() > 0 &&
(match entries[0] {
Object(entry) => entry.contains("const") || entry.contains("title")
_ => false
}) {
Titled(decode_list(value, at, (item, at) => read(item, at)))
} else if value is Object(entry) && entry.contains("anyOf") {
Titled(
match object_member(entry, "anyOf", at) {
Some(options) => {
let options : Array[EnumOption] = options
options
}
None => []
},
)
} else {
Values(decode_list(value, at, (item, at) => text_of(item, at)))
}
}
}
{
title: string_member(fields, "title", path),
description: string_member(fields, "description", path),
min_items: int_member(fields, "minItems", path),
max_items: int_member(fields, "maxItems", path),
items,
default: list_member(fields, "default", path, (value, at) => {
text_of(value, at)
}),
meta: meta_of(fields, path),
}
}
///|
/// One field of a form, tagged by `type` on the wire. A kind the protocol does not define yet is
/// kept as it came so a client can still show it.
#derive(Eq)
pub(all) enum Field {
Text(TextField)
Number(NumberField)
Integer(IntegerField)
Boolean(BooleanField)
MultiSelect(MultiSelectField)
Other(Json)
}
///|
pub extend Field with Eq::{equal, not_equal}
///|
pub extend Field with @json.ToJson::{to_json}
///|
pub impl ToJson for Field with fn to_json(self) {
match self {
Text(field) => json_tagged(write(field), "type", "string")
Number(field) => json_tagged(write(field), "type", "number")
Integer(field) => json_tagged(write(field), "type", "integer")
Boolean(field) => json_tagged(write(field), "type", "boolean")
MultiSelect(field) => json_tagged(write(field), "type", "array")
Other(json) => json
}
}
///|
pub extend Field with @json.FromJson::{from_json}
///|
pub impl @json.FromJson for Field with fn from_json(json, path) {
let fields = expect_object(json, path)
match string_member(fields, "type", path) {
Some("string") => Text(read(json, path))
Some("number") => Number(read(json, path))
Some("integer") => Integer(read(json, path))
Some("boolean") => Boolean(read(json, path))
Some("array") => MultiSelect(read(json, path))
_ => Other(json)
}
}
///|
/// The form to render.
#derive(Eq)
pub(all) struct ElicitationSchema {
properties : Map[String, Field]
required : Array[String]?
title : String?
description : String?
meta : Json?
}
///|
pub extend ElicitationSchema with Eq::{equal, not_equal}
///|
pub extend ElicitationSchema with @json.ToJson::{to_json}
///|
pub impl ToJson for ElicitationSchema with fn to_json(self) {
let properties : Map[String, Json] = Map([])
for entry in self.properties.iter() {
let (name, field) = entry
properties[name] = write(field)
}
json_of([
("type", Some(Json::string("object"))),
("title", json_string(self.title)),
("description", json_string(self.description)),
("properties", Some(Json::object(properties))),
("required", json_list(self.required, Json::string)),
("_meta", self.meta),
])
}
///|
pub extend ElicitationSchema with @json.FromJson::{from_json}
///|
pub impl @json.FromJson for ElicitationSchema with fn from_json(json, path) {
let fields = expect_object(json, path)
let properties : Map[String, Field] = Map([])
match field_of(fields, "properties", path) {
None => ()
Some(value) => {
let at = path.add_key("properties")
let entries = expect_object(value, at)
for entry in entries.iter() {
let (name, field) = entry
properties[name] = @json.from_json(field, path=at.add_key(name))
}
}
}
{
properties,
required: list_member(fields, "required", path, (value, at) => {
text_of(value, at)
}),
title: string_member(fields, "title", path),
description: string_member(fields, "description", path),
meta: meta_of(fields, path),
}
}
///|
/// Which request an elicitation belongs to: a session's turn, or a bare JSON-RPC request.
#derive(Eq)
pub(all) enum ElicitScope {
OfSession(String, String?)
OfRequest(Json)
}
///|
pub extend ElicitScope with Eq::{equal, not_equal}
///|
pub extend ElicitScope with @json.ToJson::{to_json}
///|
pub impl ToJson for ElicitScope with fn to_json(self) {
match self {
OfSession(session_id, tool_call_id) =>
json_of([
("sessionId", Some(Json::string(session_id))),
("toolCallId", json_string(tool_call_id)),
])
OfRequest(request_id) => json_of([("requestId", Some(request_id))])
}
}
///|
pub extend ElicitScope with @json.FromJson::{from_json}
///|
pub impl @json.FromJson for ElicitScope with fn from_json(json, path) {
let fields = expect_object(json, path)
match string_member(fields, "sessionId", path) {
Some(session_id) =>
OfSession(session_id, string_member(fields, "toolCallId", path))
None => OfRequest(required(fields, "requestId", path))
}
}
///|
/// A form for the user to fill in.
#derive(Eq)
pub(all) struct FormRequest {
message : String
schema : ElicitationSchema
scope : ElicitScope
meta : Json?
}
///|
pub extend FormRequest with Eq::{equal, not_equal}
///|
pub extend FormRequest with @json.ToJson::{to_json}
///|
pub impl ToJson for FormRequest with fn to_json(self) {
json_tagged(
merge_json(
write(self.scope),
json_of([
("message", Some(Json::string(self.message))),
("requestedSchema", Some(write(self.schema))),
("_meta", self.meta),
]),
),
"mode",
"form",
)
}
///|
pub extend FormRequest with @json.FromJson::{from_json}
///|
pub impl @json.FromJson for FormRequest with fn from_json(json, path) {
let fields = expect_object(json, path)
{
message: required_string(fields, "message", path),
schema: required_object(fields, "requestedSchema", path),
scope: read(json, path),
meta: meta_of(fields, path),
}
}
///|
/// A link the user follows.
#derive(Eq)
pub(all) struct UrlRequest {
message : String
elicitation_id : String
url : String
scope : ElicitScope
meta : Json?
}
///|
pub extend UrlRequest with Eq::{equal, not_equal}
///|
pub extend UrlRequest with @json.ToJson::{to_json}
///|
pub impl ToJson for UrlRequest with fn to_json(self) {
json_tagged(
merge_json(
write(self.scope),
json_of([
("message", Some(Json::string(self.message))),
("elicitationId", Some(Json::string(self.elicitation_id))),
("url", Some(Json::string(self.url))),
("_meta", self.meta),
]),
),
"mode",
"url",
)
}
///|
pub extend UrlRequest with @json.FromJson::{from_json}
///|
pub impl @json.FromJson for UrlRequest with fn from_json(json, path) {
let fields = expect_object(json, path)
{
message: required_string(fields, "message", path),
elicitation_id: required_string(fields, "elicitationId", path),
url: required_string(fields, "url", path),
scope: read(json, path),
meta: meta_of(fields, path),
}
}
///|
/// An ask, tagged by `mode` on the wire.
#derive(Eq)
pub(all) enum Elicitation {
Form(FormRequest)
Url(UrlRequest)
}
///|
pub extend Elicitation with Eq::{equal, not_equal}
///|
pub extend Elicitation with @json.ToJson::{to_json}
///|
pub impl ToJson for Elicitation with fn to_json(self) {
match self {
Form(request) => write(request)
Url(request) => write(request)
}
}
///|
pub extend Elicitation with @json.FromJson::{from_json}
///|
pub impl @json.FromJson for Elicitation with fn from_json(json, path) {
let fields = expect_object(json, path)
match string_member(fields, "mode", path) {
Some("form") => Form(read(json, path))
Some("url") => Url(read(json, path))
Some(other) =>
raise @json.JsonDecodeError(
(path.add_key("mode"), "unknown elicitation \{other}"),
)
None => raise @json.JsonDecodeError((path.add_key("mode"), "missing"))
}
}
///|
/// What the user did with the ask. `content` holds one value per field, of whatever type the field
/// asked for, so it stays raw.
#derive(Eq)
pub(all) enum Answer {
Accept(Map[String, Json]?)
Decline
Cancel
Other(String)
}
///|
pub extend Answer with Eq::{equal, not_equal}
///|
pub extend Answer with @json.ToJson::{to_json}
///|
pub impl ToJson for Answer with fn to_json(self) {
match self {
Accept(content) =>
json_tagged(
json_of([("content", content.map(Json::object))]),
"action",
"accept",
)
Decline => json_tagged(Json::empty_object(), "action", "decline")
Cancel => json_tagged(Json::empty_object(), "action", "cancel")
Other(action) => json_tagged(Json::empty_object(), "action", action)
}
}
///|
pub extend Answer with @json.FromJson::{from_json}
///|
pub impl @json.FromJson for Answer with fn from_json(json, path) {
let fields = expect_object(json, path)
match required_string(fields, "action", path) {
"accept" =>
Accept(
match field_of(fields, "content", path) {
None => None
Some(value) => Some(expect_object(value, path.add_key("content")))
},
)
"decline" => Decline
"cancel" => Cancel
other => Other(other)
}
}