Implement Tool.Annotations (#47)
This commit is contained in:
parent
c748bc1549
commit
4143f35b41
2 changed files with 233 additions and 1 deletions
|
|
@ -16,10 +16,83 @@ public struct Tool: Hashable, Codable, Sendable {
|
||||||
/// The tool input schema
|
/// The tool input schema
|
||||||
public let inputSchema: Value?
|
public let inputSchema: Value?
|
||||||
|
|
||||||
public init(name: String, description: String, inputSchema: Value? = nil) {
|
/// Annotations that provide display-facing and operational information for a Tool.
|
||||||
|
///
|
||||||
|
/// - Note: All properties in `ToolAnnotations` are **hints**.
|
||||||
|
/// They are not guaranteed to provide a faithful description of
|
||||||
|
/// tool behavior (including descriptive properties like `title`).
|
||||||
|
///
|
||||||
|
/// Clients should never make tool use decisions based on `ToolAnnotations`
|
||||||
|
/// received from untrusted servers.
|
||||||
|
public struct Annotations: Hashable, Codable, Sendable, ExpressibleByNilLiteral {
|
||||||
|
/// A human-readable title for the tool
|
||||||
|
public var title: String?
|
||||||
|
|
||||||
|
/// If true, the tool may perform destructive updates to its environment.
|
||||||
|
/// If false, the tool performs only additive updates.
|
||||||
|
/// (This property is meaningful only when `readOnlyHint == false`)
|
||||||
|
///
|
||||||
|
/// When unspecified, the implicit default is `true`.
|
||||||
|
public var destructiveHint: Bool?
|
||||||
|
|
||||||
|
/// If true, calling the tool repeatedly with the same arguments
|
||||||
|
/// will have no additional effect on its environment.
|
||||||
|
/// (This property is meaningful only when `readOnlyHint == false`)
|
||||||
|
///
|
||||||
|
/// When unspecified, the implicit default is `false`.
|
||||||
|
public var idempotentHint: Bool?
|
||||||
|
|
||||||
|
/// If true, this tool may interact with an "open world" of external
|
||||||
|
/// entities. If false, the tool's domain of interaction is closed.
|
||||||
|
/// For example, the world of a web search tool is open, whereas that
|
||||||
|
/// of a memory tool is not.
|
||||||
|
///
|
||||||
|
/// When unspecified, the implicit default is `true`.
|
||||||
|
public var openWorldHint: Bool?
|
||||||
|
|
||||||
|
/// If true, the tool does not modify its environment.
|
||||||
|
///
|
||||||
|
/// When unspecified, the implicit default is `false`.
|
||||||
|
public var readOnlyHint: Bool?
|
||||||
|
|
||||||
|
/// Returns true if all properties are nil
|
||||||
|
public var isEmpty: Bool {
|
||||||
|
title == nil && readOnlyHint == nil && destructiveHint == nil && idempotentHint == nil
|
||||||
|
&& openWorldHint == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(
|
||||||
|
title: String? = nil,
|
||||||
|
readOnlyHint: Bool? = nil,
|
||||||
|
destructiveHint: Bool? = nil,
|
||||||
|
idempotentHint: Bool? = nil,
|
||||||
|
openWorldHint: Bool? = nil
|
||||||
|
) {
|
||||||
|
self.title = title
|
||||||
|
self.readOnlyHint = readOnlyHint
|
||||||
|
self.destructiveHint = destructiveHint
|
||||||
|
self.idempotentHint = idempotentHint
|
||||||
|
self.openWorldHint = openWorldHint
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize an empty annotations object
|
||||||
|
public init(nilLiteral: ()) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Annotations that provide display-facing and operational information
|
||||||
|
public var annotations: Annotations
|
||||||
|
|
||||||
|
/// Initialize a tool with a name, description, input schema, and annotations
|
||||||
|
public init(
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
|
inputSchema: Value? = nil,
|
||||||
|
annotations: Annotations = nil
|
||||||
|
) {
|
||||||
self.name = name
|
self.name = name
|
||||||
self.description = description
|
self.description = description
|
||||||
self.inputSchema = inputSchema
|
self.inputSchema = inputSchema
|
||||||
|
self.annotations = annotations
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Content types that can be returned by a tool
|
/// Content types that can be returned by a tool
|
||||||
|
|
@ -103,6 +176,7 @@ public struct Tool: Hashable, Codable, Sendable {
|
||||||
case name
|
case name
|
||||||
case description
|
case description
|
||||||
case inputSchema
|
case inputSchema
|
||||||
|
case annotations
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
|
|
@ -110,6 +184,8 @@ public struct Tool: Hashable, Codable, Sendable {
|
||||||
name = try container.decode(String.self, forKey: .name)
|
name = try container.decode(String.self, forKey: .name)
|
||||||
description = try container.decode(String.self, forKey: .description)
|
description = try container.decode(String.self, forKey: .description)
|
||||||
inputSchema = try container.decodeIfPresent(Value.self, forKey: .inputSchema)
|
inputSchema = try container.decodeIfPresent(Value.self, forKey: .inputSchema)
|
||||||
|
annotations =
|
||||||
|
try container.decodeIfPresent(Tool.Annotations.self, forKey: .annotations) ?? .init()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func encode(to encoder: Encoder) throws {
|
public func encode(to encoder: Encoder) throws {
|
||||||
|
|
@ -119,6 +195,9 @@ public struct Tool: Hashable, Codable, Sendable {
|
||||||
if let schema = inputSchema {
|
if let schema = inputSchema {
|
||||||
try container.encode(schema, forKey: .inputSchema)
|
try container.encode(schema, forKey: .inputSchema)
|
||||||
}
|
}
|
||||||
|
if !annotations.isEmpty {
|
||||||
|
try container.encode(annotations, forKey: .annotations)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,159 @@ struct ToolTests {
|
||||||
#expect(tool.inputSchema != nil)
|
#expect(tool.inputSchema != nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test("Tool Annotations initialization and properties")
|
||||||
|
func testToolAnnotationsInitialization() throws {
|
||||||
|
// Empty annotations
|
||||||
|
let emptyAnnotations = Tool.Annotations()
|
||||||
|
#expect(emptyAnnotations.isEmpty)
|
||||||
|
#expect(emptyAnnotations.title == nil)
|
||||||
|
#expect(emptyAnnotations.readOnlyHint == nil)
|
||||||
|
#expect(emptyAnnotations.destructiveHint == nil)
|
||||||
|
#expect(emptyAnnotations.idempotentHint == nil)
|
||||||
|
#expect(emptyAnnotations.openWorldHint == nil)
|
||||||
|
|
||||||
|
// Full annotations
|
||||||
|
let fullAnnotations = Tool.Annotations(
|
||||||
|
title: "Test Tool",
|
||||||
|
readOnlyHint: true,
|
||||||
|
destructiveHint: false,
|
||||||
|
idempotentHint: true,
|
||||||
|
openWorldHint: false
|
||||||
|
)
|
||||||
|
|
||||||
|
#expect(!fullAnnotations.isEmpty)
|
||||||
|
#expect(fullAnnotations.title == "Test Tool")
|
||||||
|
#expect(fullAnnotations.readOnlyHint == true)
|
||||||
|
#expect(fullAnnotations.destructiveHint == false)
|
||||||
|
#expect(fullAnnotations.idempotentHint == true)
|
||||||
|
#expect(fullAnnotations.openWorldHint == false)
|
||||||
|
|
||||||
|
// Partial annotations - should not be empty
|
||||||
|
let partialAnnotations = Tool.Annotations(title: "Partial Test")
|
||||||
|
#expect(!partialAnnotations.isEmpty)
|
||||||
|
#expect(partialAnnotations.title == "Partial Test")
|
||||||
|
|
||||||
|
// Initialize with nil literal
|
||||||
|
let nilAnnotations: Tool.Annotations = nil
|
||||||
|
#expect(nilAnnotations.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Tool Annotations encoding and decoding")
|
||||||
|
func testToolAnnotationsEncodingDecoding() throws {
|
||||||
|
let annotations = Tool.Annotations(
|
||||||
|
title: "Test Tool",
|
||||||
|
readOnlyHint: true,
|
||||||
|
destructiveHint: false,
|
||||||
|
idempotentHint: true,
|
||||||
|
openWorldHint: false
|
||||||
|
)
|
||||||
|
|
||||||
|
#expect(!annotations.isEmpty)
|
||||||
|
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
|
||||||
|
let data = try encoder.encode(annotations)
|
||||||
|
let decoded = try decoder.decode(Tool.Annotations.self, from: data)
|
||||||
|
|
||||||
|
#expect(decoded.title == annotations.title)
|
||||||
|
#expect(decoded.readOnlyHint == annotations.readOnlyHint)
|
||||||
|
#expect(decoded.destructiveHint == annotations.destructiveHint)
|
||||||
|
#expect(decoded.idempotentHint == annotations.idempotentHint)
|
||||||
|
#expect(decoded.openWorldHint == annotations.openWorldHint)
|
||||||
|
|
||||||
|
// Test that empty annotations are encoded as expected
|
||||||
|
let emptyAnnotations = Tool.Annotations()
|
||||||
|
let emptyData = try encoder.encode(emptyAnnotations)
|
||||||
|
let decodedEmpty = try decoder.decode(Tool.Annotations.self, from: emptyData)
|
||||||
|
|
||||||
|
#expect(decodedEmpty.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Tool with annotations encoding and decoding")
|
||||||
|
func testToolWithAnnotationsEncodingDecoding() throws {
|
||||||
|
let annotations = Tool.Annotations(
|
||||||
|
title: "Calculator",
|
||||||
|
destructiveHint: false
|
||||||
|
)
|
||||||
|
|
||||||
|
let tool = Tool(
|
||||||
|
name: "calculate",
|
||||||
|
description: "Performs calculations",
|
||||||
|
inputSchema: .object([
|
||||||
|
"expression": .string("Mathematical expression to evaluate")
|
||||||
|
]),
|
||||||
|
annotations: annotations
|
||||||
|
)
|
||||||
|
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
|
||||||
|
let data = try encoder.encode(tool)
|
||||||
|
let decoded = try decoder.decode(Tool.self, from: data)
|
||||||
|
|
||||||
|
#expect(decoded.name == tool.name)
|
||||||
|
#expect(decoded.description == tool.description)
|
||||||
|
#expect(decoded.annotations.title == annotations.title)
|
||||||
|
#expect(decoded.annotations.destructiveHint == annotations.destructiveHint)
|
||||||
|
|
||||||
|
// Verify that the annotations field is properly included in the JSON
|
||||||
|
let jsonString = String(data: data, encoding: .utf8)!
|
||||||
|
#expect(jsonString.contains("\"annotations\""))
|
||||||
|
#expect(jsonString.contains("\"title\":\"Calculator\""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Tool with empty annotations")
|
||||||
|
func testToolWithEmptyAnnotations() throws {
|
||||||
|
var tool = Tool(
|
||||||
|
name: "test_tool",
|
||||||
|
description: "Test tool description"
|
||||||
|
)
|
||||||
|
|
||||||
|
do {
|
||||||
|
#expect(tool.annotations.isEmpty)
|
||||||
|
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
let data = try encoder.encode(tool)
|
||||||
|
|
||||||
|
// Verify that empty annotations are not included in the JSON
|
||||||
|
let jsonString = String(data: data, encoding: .utf8)!
|
||||||
|
#expect(!jsonString.contains("\"annotations\""))
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
tool.annotations.title = "Test"
|
||||||
|
|
||||||
|
#expect(!tool.annotations.isEmpty)
|
||||||
|
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
let data = try encoder.encode(tool)
|
||||||
|
|
||||||
|
// Verify that empty annotations are not included in the JSON
|
||||||
|
let jsonString = String(data: data, encoding: .utf8)!
|
||||||
|
#expect(jsonString.contains("\"annotations\""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Tool with nil literal annotations")
|
||||||
|
func testToolWithNilLiteralAnnotations() throws {
|
||||||
|
let tool = Tool(
|
||||||
|
name: "test_tool",
|
||||||
|
description: "Test tool description",
|
||||||
|
inputSchema: nil,
|
||||||
|
annotations: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
#expect(tool.annotations.isEmpty)
|
||||||
|
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
let data = try encoder.encode(tool)
|
||||||
|
|
||||||
|
// Verify that nil literal annotations are not included in the JSON
|
||||||
|
let jsonString = String(data: data, encoding: .utf8)!
|
||||||
|
#expect(!jsonString.contains("\"annotations\""))
|
||||||
|
}
|
||||||
|
|
||||||
@Test("Tool encoding and decoding")
|
@Test("Tool encoding and decoding")
|
||||||
func testToolEncodingDecoding() throws {
|
func testToolEncodingDecoding() throws {
|
||||||
let tool = Tool(
|
let tool = Tool(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue