adk-core 2.0.0

Core traits and types for Rust Agent Development Kit (ADK-Rust) agents, tools, sessions, and events
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
//! Schema normalization adapter for LLM provider function-calling APIs.
//!
//! Each LLM provider has different JSON Schema requirements for tool parameters.
//! The [`SchemaAdapter`] trait provides a consistent interface for transforming
//! raw MCP tool schemas into the provider's accepted format at request time.
//!
//! # Architecture
//!
//! `McpToolset` returns raw schemas verbatim. Each model adapter implements
//! `SchemaAdapter` to normalize schemas according to its backend's limitations.
//! This separation keeps MCP tool discovery independent of LLM-specific concerns.
//!
//! # Example
//!
//! ```rust
//! use adk_core::SchemaAdapter;
//! use serde_json::{json, Value};
//! use std::borrow::Cow;
//!
//! #[derive(Debug)]
//! struct MyAdapter;
//!
//! impl SchemaAdapter for MyAdapter {
//!     fn normalize_schema(&self, schema: Value) -> Value {
//!         // Apply provider-specific transforms
//!         schema
//!     }
//! }
//!
//! let adapter = MyAdapter;
//! let raw = json!({"type": "object", "properties": {"name": {"type": "string"}}});
//! let normalized = adapter.normalize_schema(raw);
//! ```

use serde_json::Value;
use std::borrow::Cow;

use crate::schema_utils;

/// Normalizes JSON Schema for a specific LLM provider's function-calling API.
///
/// Each provider has different schema requirements. The adapter transforms
/// raw MCP tool schemas into the provider's accepted format at request time.
///
/// # Default Implementations
///
/// - [`normalize_tool_name`](SchemaAdapter::normalize_tool_name): Truncates names
///   exceeding 64 bytes at a valid UTF-8 character boundary.
/// - [`empty_schema`](SchemaAdapter::empty_schema): Returns
///   `{"type": "object", "properties": {}}` as the fallback when no input schema
///   is provided.
///
/// # Thread Safety
///
/// All implementations must be `Send + Sync` to support concurrent request building
/// across async tasks.
pub trait SchemaAdapter: Send + Sync + std::fmt::Debug {
    /// Normalize a raw JSON Schema for this provider.
    ///
    /// Called once per tool per request (results may be cached by the model adapter layer).
    ///
    /// # Arguments
    ///
    /// * `schema` - The raw JSON Schema value from an MCP tool's `inputSchema`.
    ///
    /// # Returns
    ///
    /// A normalized JSON Schema value accepted by this provider's API.
    fn normalize_schema(&self, schema: Value) -> Value;

    /// Normalize a tool name for this provider's limits.
    ///
    /// Default implementation truncates names exceeding 64 bytes at the nearest
    /// valid UTF-8 character boundary, preserving the prefix.
    ///
    /// # Arguments
    ///
    /// * `name` - The original tool name.
    ///
    /// # Returns
    ///
    /// A [`Cow::Borrowed`] reference if the name fits within 64 bytes, or a
    /// [`Cow::Owned`] truncated string otherwise.
    ///
    /// # Example
    ///
    /// ```rust
    /// use adk_core::SchemaAdapter;
    /// use serde_json::Value;
    /// use std::borrow::Cow;
    ///
    /// #[derive(Debug)]
    /// struct TestAdapter;
    /// impl SchemaAdapter for TestAdapter {
    ///     fn normalize_schema(&self, schema: Value) -> Value { schema }
    /// }
    ///
    /// let adapter = TestAdapter;
    ///
    /// // Short names are returned as-is
    /// assert_eq!(adapter.normalize_tool_name("get_weather"), Cow::Borrowed("get_weather"));
    ///
    /// // Long names are truncated to at most 64 bytes
    /// let long_name = "a".repeat(100);
    /// let result = adapter.normalize_tool_name(&long_name);
    /// assert!(result.len() <= 64);
    /// ```
    fn normalize_tool_name<'a>(&self, name: &'a str) -> Cow<'a, str> {
        if name.len() <= 64 {
            Cow::Borrowed(name)
        } else {
            // Find the largest valid UTF-8 boundary at or before 64 bytes.
            // Walk backward from byte 64 until we hit a byte that is not a
            // UTF-8 continuation byte (0b10xxxxxx).
            let mut end = 64;
            while end > 0 && !name.is_char_boundary(end) {
                end -= 1;
            }
            Cow::Owned(name[..end].to_string())
        }
    }

    /// Fallback schema when a tool provides no `parameters_schema`.
    ///
    /// Returns `{"type": "object", "properties": {}}` by default, which represents
    /// a tool that accepts no parameters.
    ///
    /// # Example
    ///
    /// ```rust
    /// use adk_core::SchemaAdapter;
    /// use serde_json::{json, Value};
    ///
    /// #[derive(Debug)]
    /// struct TestAdapter;
    /// impl SchemaAdapter for TestAdapter {
    ///     fn normalize_schema(&self, schema: Value) -> Value { schema }
    /// }
    ///
    /// let adapter = TestAdapter;
    /// assert_eq!(adapter.empty_schema(), json!({"type": "object", "properties": {}}));
    /// ```
    fn empty_schema(&self) -> Value {
        serde_json::json!({"type": "object", "properties": {}})
    }

    /// The function-declaration field this adapter's output must be posted under.
    ///
    /// A provider may accept more than one schema dialect on different fields —
    /// Gemini takes an OpenAPI subset on `parameters` and standard JSON Schema
    /// on `parametersJsonSchema`, and the two are mutually exclusive. The
    /// reduction and the field name are therefore **one decision**: a schema
    /// reduced for one dialect but posted under the other's field is either
    /// rejected outright, or silently accepted carrying constraints the model
    /// was never shown.
    ///
    /// Returning the field from the adapter keeps that decision in the one place
    /// that already knows which dialect it produced, so a caller cannot pick the
    /// wrong one. Defaults to `"parameters"`, which is correct for every
    /// provider having only one such field.
    ///
    /// # Example
    ///
    /// ```rust
    /// use adk_core::SchemaAdapter;
    /// use serde_json::Value;
    ///
    /// #[derive(Debug)]
    /// struct TestAdapter;
    /// impl SchemaAdapter for TestAdapter {
    ///     fn normalize_schema(&self, schema: Value) -> Value { schema }
    /// }
    ///
    /// assert_eq!(TestAdapter.parameters_field(), "parameters");
    /// ```
    fn parameters_field(&self) -> &'static str {
        "parameters"
    }
}

/// Default schema adapter for providers with no specific requirements (Ollama, etc.).
///
/// Applies a conservative set of shared utility transforms:
/// 1. Strip `$schema` keyword
/// 2. Strip conditional keywords (`if`/`then`/`else`)
/// 3. Convert `const` to single-element `enum`
/// 4. Add implicit `"type": "object"` when `properties` exists
/// 5. Strip unsupported `format` values
///
/// This adapter does not resolve `$ref`, collapse combiners, or enforce nesting
/// depth limits. It is suitable for providers that accept most JSON Schema features
/// but reject the meta-keywords and conditional constructs.
///
/// Used as the default return value of [`Llm::schema_adapter()`](crate::Llm::schema_adapter)
/// for providers that do not override it.
///
/// # Example
///
/// ```rust
/// use adk_core::{GenericSchemaAdapter, SchemaAdapter};
/// use serde_json::json;
///
/// let adapter = GenericSchemaAdapter;
/// let schema = json!({
///     "$schema": "http://json-schema.org/draft-07/schema#",
///     "properties": {
///         "name": { "type": "string", "const": "fixed" }
///     },
///     "if": { "properties": { "x": { "type": "number" } } },
///     "then": { "required": ["x"] }
/// });
///
/// let normalized = adapter.normalize_schema(schema);
/// assert!(normalized.get("$schema").is_none());
/// assert!(normalized.get("if").is_none());
/// assert!(normalized.get("then").is_none());
/// assert_eq!(normalized["type"], "object");
/// assert_eq!(normalized["properties"]["name"]["enum"], json!(["fixed"]));
/// ```
#[derive(Debug)]
pub struct GenericSchemaAdapter;

/// Allowed format values for the generic adapter (same as Gemini).
const GENERIC_ALLOWED_FORMATS: &[&str] =
    &["date-time", "date", "time", "email", "uri", "uuid", "int32", "int64", "float", "double"];

impl SchemaAdapter for GenericSchemaAdapter {
    fn normalize_schema(&self, mut schema: Value) -> Value {
        schema_utils::strip_schema_keyword(&mut schema);
        schema_utils::strip_conditional_keywords(&mut schema);
        schema_utils::convert_const_to_enum(&mut schema);
        schema_utils::add_implicit_object_type(&mut schema);
        schema_utils::strip_unsupported_formats(&mut schema, GENERIC_ALLOWED_FORMATS);
        schema
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn test_generic_adapter_strips_schema_keyword() {
        let adapter = GenericSchemaAdapter;
        let schema = json!({
            "$schema": "http://json-schema.org/draft-07/schema#",
            "type": "object",
            "properties": { "name": { "type": "string" } }
        });
        let result = adapter.normalize_schema(schema);
        assert!(result.get("$schema").is_none());
        assert_eq!(result["type"], "object");
    }

    #[test]
    fn test_generic_adapter_strips_conditional_keywords() {
        let adapter = GenericSchemaAdapter;
        let schema = json!({
            "type": "object",
            "if": { "properties": { "kind": { "const": "a" } } },
            "then": { "required": ["extra"] },
            "else": { "required": [] }
        });
        let result = adapter.normalize_schema(schema);
        assert!(result.get("if").is_none());
        assert!(result.get("then").is_none());
        assert!(result.get("else").is_none());
    }

    #[test]
    fn test_generic_adapter_converts_const_to_enum() {
        let adapter = GenericSchemaAdapter;
        let schema = json!({
            "type": "string",
            "const": "fixed_value"
        });
        let result = adapter.normalize_schema(schema);
        assert!(result.get("const").is_none());
        assert_eq!(result["enum"], json!(["fixed_value"]));
    }

    #[test]
    fn test_generic_adapter_adds_implicit_object_type() {
        let adapter = GenericSchemaAdapter;
        let schema = json!({
            "properties": {
                "name": { "type": "string" }
            }
        });
        let result = adapter.normalize_schema(schema);
        assert_eq!(result["type"], "object");
    }

    #[test]
    fn test_generic_adapter_strips_unsupported_formats() {
        let adapter = GenericSchemaAdapter;
        let schema = json!({
            "type": "object",
            "properties": {
                "created": { "type": "string", "format": "date-time" },
                "hostname": { "type": "string", "format": "hostname" },
                "email": { "type": "string", "format": "email" }
            }
        });
        let result = adapter.normalize_schema(schema);
        assert_eq!(result["properties"]["created"]["format"], "date-time");
        assert!(result["properties"]["hostname"].get("format").is_none());
        assert_eq!(result["properties"]["email"]["format"], "email");
    }

    #[test]
    fn test_generic_adapter_preserves_allowed_formats() {
        let adapter = GenericSchemaAdapter;
        for format in GENERIC_ALLOWED_FORMATS {
            let schema = json!({ "type": "string", "format": format });
            let result = adapter.normalize_schema(schema);
            assert_eq!(result["format"], *format, "format '{format}' should be preserved");
        }
    }

    #[test]
    fn test_generic_adapter_all_transforms_combined() {
        let adapter = GenericSchemaAdapter;
        let schema = json!({
            "$schema": "http://json-schema.org/draft-07/schema#",
            "properties": {
                "status": { "type": "string", "const": "active" },
                "host": { "type": "string", "format": "hostname" },
                "created": { "type": "string", "format": "date-time" }
            },
            "if": { "properties": { "status": { "const": "active" } } },
            "then": { "required": ["host"] }
        });
        let result = adapter.normalize_schema(schema);

        // $schema removed
        assert!(result.get("$schema").is_none());
        // conditional keywords removed
        assert!(result.get("if").is_none());
        assert!(result.get("then").is_none());
        // implicit type added
        assert_eq!(result["type"], "object");
        // const converted to enum
        assert!(result["properties"]["status"].get("const").is_none());
        assert_eq!(result["properties"]["status"]["enum"], json!(["active"]));
        // unsupported format stripped
        assert!(result["properties"]["host"].get("format").is_none());
        // allowed format preserved
        assert_eq!(result["properties"]["created"]["format"], "date-time");
    }

    #[test]
    fn test_generic_adapter_nested_transforms() {
        let adapter = GenericSchemaAdapter;
        let schema = json!({
            "type": "object",
            "properties": {
                "nested": {
                    "$schema": "draft-07",
                    "properties": {
                        "deep": {
                            "type": "string",
                            "const": "value",
                            "format": "ipv4"
                        }
                    },
                    "if": { "const": true },
                    "then": { "type": "string" }
                }
            }
        });
        let result = adapter.normalize_schema(schema);
        let nested = &result["properties"]["nested"];
        assert!(nested.get("$schema").is_none());
        assert!(nested.get("if").is_none());
        assert!(nested.get("then").is_none());
        assert_eq!(nested["type"], "object");
        assert_eq!(nested["properties"]["deep"]["enum"], json!(["value"]));
        assert!(nested["properties"]["deep"].get("format").is_none());
    }

    #[test]
    fn test_generic_adapter_idempotent() {
        let adapter = GenericSchemaAdapter;
        let schema = json!({
            "$schema": "http://json-schema.org/draft-07/schema#",
            "properties": {
                "name": { "type": "string", "const": "test", "format": "hostname" }
            },
            "if": { "const": true },
            "then": { "required": ["name"] }
        });
        let first = adapter.normalize_schema(schema);
        let second = adapter.normalize_schema(first.clone());
        assert_eq!(first, second);
    }

    #[test]
    fn test_generic_adapter_empty_schema_passthrough() {
        let adapter = GenericSchemaAdapter;
        let schema = json!({});
        let result = adapter.normalize_schema(schema);
        assert_eq!(result, json!({}));
    }

    #[test]
    fn test_generic_adapter_preserves_refs_and_combiners() {
        let adapter = GenericSchemaAdapter;
        let schema = json!({
            "type": "object",
            "$ref": "#/definitions/Foo",
            "anyOf": [{ "type": "string" }, { "type": "number" }],
            "oneOf": [{ "type": "boolean" }],
            "allOf": [{ "required": ["a"] }],
            "additionalProperties": false
        });
        let result = adapter.normalize_schema(schema);
        // GenericSchemaAdapter does NOT resolve refs or collapse combiners
        assert!(result.get("$ref").is_some());
        assert!(result.get("anyOf").is_some());
        assert!(result.get("oneOf").is_some());
        assert!(result.get("allOf").is_some());
        assert!(result.get("additionalProperties").is_some());
    }
}