From 28e701744011043eaec70a825c32da12e699122e Mon Sep 17 00:00:00 2001 From: Toby Martin Date: Thu, 3 Sep 2026 13:21:19 +1000 Subject: [PATCH] Classify oversized tool results safely --- Cargo.lock | 4 +- Cargo.toml | 2 +- .../graphql-orm-ai-tool-profiles/CHANGELOG.md | 16 ++++++++ .../graphql-orm-ai-tool-profiles/Cargo.toml | 2 +- .../graphql-orm-ai-tool-profiles/MIGRATION.md | 14 +++++++ crates/graphql-orm-ai-tool-profiles/README.md | 2 +- .../graphql-orm-ai-tool-profiles/src/error.rs | 12 ++++++ .../src/execution.rs | 3 ++ crates/graphql-orm-ai/CHANGELOG.md | 20 +++++++++ crates/graphql-orm-ai/Cargo.toml | 2 +- crates/graphql-orm-ai/MIGRATION.md | 17 ++++++++ crates/graphql-orm-ai/README.md | 9 +++- .../docs/implementation-status.md | 4 +- .../docs/remote-graphql-execution.md | 3 ++ crates/graphql-orm-ai/src/provider.rs | 7 +++- crates/graphql-orm-ai/src/runtime.rs | 25 +++++++---- .../graphql-orm-ai/tests/runtime_contracts.rs | 41 +++++++++++++++++++ .../fixtures/backend-coexistence/Cargo.lock | 4 +- docs/reference/workspace-packages.md | 4 +- 19 files changed, 168 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 14e3a4f0..83a670e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3104,7 +3104,7 @@ dependencies = [ [[package]] name = "graphql-orm-ai" -version = "0.96.2" +version = "0.97.0" dependencies = [ "agql-auth", "async-graphql", @@ -3136,7 +3136,7 @@ dependencies = [ [[package]] name = "graphql-orm-ai-tool-profiles" -version = "0.10.4" +version = "0.11.0" dependencies = [ "async-graphql", "async-graphql-parser", diff --git a/Cargo.toml b/Cargo.toml index cd748517..66e242ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ futures = "0.3" getrandom = "0.3" graphql-composition = "=0.12.2" graphql-orm = { path = "crates/graphql-orm", version = "0.30.0", default-features = false } -graphql-orm-ai-tool-profiles = { path = "crates/graphql-orm-ai-tool-profiles", version = "0.10.4" } +graphql-orm-ai-tool-profiles = { path = "crates/graphql-orm-ai-tool-profiles", version = "0.11.0" } graphql-orm-backup = { path = "crates/graphql-orm-backup", version = "0.7.2", default-features = false } graphql-orm-operation-catalog = { path = "crates/graphql-orm-operation-catalog", version = "0.4.0" } graphql-orm-router-protocol = { path = "crates/graphql-orm-router-protocol", version = "0.2.1" } diff --git a/crates/graphql-orm-ai-tool-profiles/CHANGELOG.md b/crates/graphql-orm-ai-tool-profiles/CHANGELOG.md index 10e6c4d8..b8182869 100644 --- a/crates/graphql-orm-ai-tool-profiles/CHANGELOG.md +++ b/crates/graphql-orm-ai-tool-profiles/CHANGELOG.md @@ -10,6 +10,22 @@ supersedes: [] # Changelog +## [0.11.0] - 2026-09-03 + +### Added + +- `ToolExecutionError::ResultBudgetExceeded` lets a bounded host transport + distinguish an oversized GraphQL response from a resolver or transport + failure. +- `AiError::ResultBudgetExceeded` carries the stable + `AI_RESULT_BUDGET_EXCEEDED` public code. + +### Security + +- The new variants carry no response content, transport destination, schema, + policy, credential, or resolver detail. They are size proofs only and do + not weaken the descriptor's result-byte or disclosure checks. + ## [0.10.4] - 2026-09-02 ### Fixed diff --git a/crates/graphql-orm-ai-tool-profiles/Cargo.toml b/crates/graphql-orm-ai-tool-profiles/Cargo.toml index ac7ed714..a5aba225 100644 --- a/crates/graphql-orm-ai-tool-profiles/Cargo.toml +++ b/crates/graphql-orm-ai-tool-profiles/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "graphql-orm-ai-tool-profiles" -version = "0.10.4" +version = "0.11.0" edition = "2024" authors = ["Toby Martin "] description = "Backend-neutral GraphQL AI tool profile compiler and manifest contracts" diff --git a/crates/graphql-orm-ai-tool-profiles/MIGRATION.md b/crates/graphql-orm-ai-tool-profiles/MIGRATION.md index bf60c863..73e6e1a4 100644 --- a/crates/graphql-orm-ai-tool-profiles/MIGRATION.md +++ b/crates/graphql-orm-ai-tool-profiles/MIGRATION.md @@ -10,6 +10,20 @@ supersedes: [] # Migration Guide +## 0.10.4 to 0.11.0: typed result-budget failures + +Adopt `graphql-orm-ai-tool-profiles` 0.11.0 with `graphql-orm-ai` 0.97.0 from +one reviewed full monorepo revision. Host transports should return +`ToolExecutionError::ResultBudgetExceeded` only when a bounded response is +refused because its byte ceiling was exceeded. Deliberately exhaustive +in-crate matches must handle the new non-exhaustive error variants. + +The runtime converts that proof, and its own descriptor result-byte overflow, +to `AiError::ResultBudgetExceeded`. The stable public code is +`AI_RESULT_BUDGET_EXCEEDED`. There is no schema, database, data, GraphQL SDL, +manifest, capability, fingerprint, protected-content, credential, backup, +restore, or persistent AI schema-module migration. + ## 0.10.3 to 0.10.4: resolver-first semantic discovery ranking Adopt `graphql-orm-ai-tool-profiles` 0.10.4 from one reviewed full monorepo diff --git a/crates/graphql-orm-ai-tool-profiles/README.md b/crates/graphql-orm-ai-tool-profiles/README.md index 32889445..c64d1f64 100644 --- a/crates/graphql-orm-ai-tool-profiles/README.md +++ b/crates/graphql-orm-ai-tool-profiles/README.md @@ -24,7 +24,7 @@ are separate runtime decisions and must remain default-deny. ```toml [dependencies] -graphql-orm-ai-tool-profiles = { git = "https://github.com/Dastari/graphql-orm.git", rev = "", version = "0.10.4" } +graphql-orm-ai-tool-profiles = { git = "https://github.com/Dastari/graphql-orm.git", rev = "", version = "0.11.0" } serde_json = "1" ``` diff --git a/crates/graphql-orm-ai-tool-profiles/src/error.rs b/crates/graphql-orm-ai-tool-profiles/src/error.rs index deb30ebc..96bf34fb 100644 --- a/crates/graphql-orm-ai-tool-profiles/src/error.rs +++ b/crates/graphql-orm-ai-tool-profiles/src/error.rs @@ -51,6 +51,9 @@ pub enum AiError { /// Input failed a public schema contract. #[error("invalid AI input: {0}")] InvalidInput(String), + /// A tool result exceeded its reviewed byte or record budget. + #[error("AI tool result exceeded its reviewed budget")] + ResultBudgetExceeded, /// Authentication dependency failed closed. #[error("AI principal reauthorization failed")] ReauthorizationFailed, @@ -97,6 +100,7 @@ impl AiError { Self::PreTransportBudgetDenied => "AI_BUDGET_DENIED", Self::PreTransportProviderFailed => "AI_PROVIDER_FAILED", Self::InvalidInput(_) => "AI_INVALID_INPUT", + Self::ResultBudgetExceeded => "AI_RESULT_BUDGET_EXCEEDED", Self::ReauthorizationFailed => "AI_REAUTHORIZATION_FAILED", Self::ToolExecutionFailed => "AI_TOOL_EXECUTION_FAILED", Self::ProviderFailed => "AI_PROVIDER_FAILED", @@ -135,4 +139,12 @@ mod tests { "AI_PROVIDER_FAILED" ); } + + #[test] + fn result_budget_exceeded_has_a_distinct_public_code() { + assert_eq!( + AiError::ResultBudgetExceeded.public_code(), + "AI_RESULT_BUDGET_EXCEEDED" + ); + } } diff --git a/crates/graphql-orm-ai-tool-profiles/src/execution.rs b/crates/graphql-orm-ai-tool-profiles/src/execution.rs index 7874d299..0b327f4d 100644 --- a/crates/graphql-orm-ai-tool-profiles/src/execution.rs +++ b/crates/graphql-orm-ai-tool-profiles/src/execution.rs @@ -562,4 +562,7 @@ pub enum ToolExecutionError { /// Host execution failed safely. #[error("tool GraphQL execution failed")] Execution, + /// The host transport refused a response above its reviewed byte ceiling. + #[error("tool GraphQL result exceeded its reviewed budget")] + ResultBudgetExceeded, } diff --git a/crates/graphql-orm-ai/CHANGELOG.md b/crates/graphql-orm-ai/CHANGELOG.md index b2cbe2ab..3b2d95dc 100644 --- a/crates/graphql-orm-ai/CHANGELOG.md +++ b/crates/graphql-orm-ai/CHANGELOG.md @@ -18,6 +18,26 @@ checkpoint facts. For the current workspace baseline and active gates, use the [implementation status](docs/implementation-status.md) and the central [AI production-readiness plan](../../docs/plans/active/ai-production-readiness/README.md). +## [0.97.0] - 2026-09-03 + +Persistent schema module: **0.64.0** (unchanged from 0.96.2). + +### Fixed + +- A host transport's typed oversized-response refusal and the runtime's own + descriptor byte-bound rejection now remain a distinct + `result_budget_exceeded` application-tool failure instead of being reported + as resolver validation failure. + +### Security + +- Oversized response bodies remain undisclosed. The typed result is + content-free and retryable, allowing a provider to narrow or paginate the + request without treating the resolver contract as invalid. + +There is no database, data, table, column, index, constraint, backfill, +protected-payload, GraphQL SDL, backup, or restore migration. + ## [0.96.2] - 2026-09-02 Persistent schema module: **0.64.0** (unchanged from 0.96.1). diff --git a/crates/graphql-orm-ai/Cargo.toml b/crates/graphql-orm-ai/Cargo.toml index 90d0962d..8a3ddbd5 100644 --- a/crates/graphql-orm-ai/Cargo.toml +++ b/crates/graphql-orm-ai/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "graphql-orm-ai" -version = "0.96.2" +version = "0.97.0" edition = "2024" authors = ["Toby Martin "] description = "Project-agnostic AI agent runtime for graphql-orm applications" diff --git a/crates/graphql-orm-ai/MIGRATION.md b/crates/graphql-orm-ai/MIGRATION.md index e8407ed5..1b13ddf9 100644 --- a/crates/graphql-orm-ai/MIGRATION.md +++ b/crates/graphql-orm-ai/MIGRATION.md @@ -19,6 +19,23 @@ they describe. For the current workspace baseline and active delivery gates, use [implementation status](docs/implementation-status.md) and the central [AI production-readiness plan](../../docs/plans/active/ai-production-readiness/README.md). +## 0.96.2 to 0.97.0: recoverable application-tool result budgets + +Adopt `graphql-orm-ai` 0.97.0 and `graphql-orm-ai-tool-profiles` 0.11.0 from +one reviewed full monorepo revision. Bounded host transports should return +`ToolExecutionError::ResultBudgetExceeded` when they refuse an oversized +GraphQL response. The runtime preserves that proof as +`AiError::ResultBudgetExceeded`; application-tool execution emits the +content-free, retryable `result_budget_exceeded` failure envelope so the +provider can narrow or paginate its next request. + +The same typed failure is returned when a successfully decoded result exceeds +the registered descriptor's `maximum_result_bytes`. Other transport, +resolver, disclosure, and authorization failures retain their prior +classification. The AI schema module remains **0.64.0**. There is no database, +data, GraphQL SDL, protected-payload, backup, restore, or data backfill +migration. + ## 0.96.1 to 0.96.2: schema-aligned fixed-broker execution wrappers Adopt `graphql-orm-ai` 0.96.2 from one reviewed full monorepo revision. No host diff --git a/crates/graphql-orm-ai/README.md b/crates/graphql-orm-ai/README.md index 8a9c6351..ed1dd636 100644 --- a/crates/graphql-orm-ai/README.md +++ b/crates/graphql-orm-ai/README.md @@ -28,7 +28,7 @@ for AI, ORM, storage, backup, and tool-profile packages: ```toml [dependencies] -graphql-orm-ai = { git = "https://github.com/Dastari/graphql-orm.git", rev = "", version = "0.96.2", default-features = false, features = ["sqlite"] } +graphql-orm-ai = { git = "https://github.com/Dastari/graphql-orm.git", rev = "", version = "0.97.0", default-features = false, features = ["sqlite"] } ``` Exactly one persistence backend is required: `sqlite` (default), `postgres`, @@ -118,6 +118,13 @@ and total-record bounds and rejects response fields outside the selected disclosure shape. Total records include sibling and nested relationship expansion, not merely the largest returned list. +A bounded host transport may reject an oversized response before decoding it +with `ToolExecutionError::ResultBudgetExceeded`. The runtime preserves that +proof, and its own descriptor-byte rejection, as the content-free retryable +`result_budget_exceeded` application-tool outcome. Providers can then narrow +or paginate the request without receiving response content or mistaking a +size limit for resolver validation failure. + Private remote execution carries a crate-authored `AiRemoteGraphqlCapabilityBinding` to the deployment issuer. Static reads bind their exact registered descriptor; generated reads additionally bind the diff --git a/crates/graphql-orm-ai/docs/implementation-status.md b/crates/graphql-orm-ai/docs/implementation-status.md index aaa35ffa..79d142a5 100644 --- a/crates/graphql-orm-ai/docs/implementation-status.md +++ b/crates/graphql-orm-ai/docs/implementation-status.md @@ -10,9 +10,9 @@ supersedes: [] # Implementation Status -`graphql-orm-ai` is at crate version `0.96.2` with AI schema module +`graphql-orm-ai` is at crate version `0.97.0` with AI schema module `0.64.0`. It uses workspace `graphql-orm` `0.30.0`, backend-neutral -`graphql-orm-ai-tool-profiles` `0.10.4`, and external `agql-auth` +`graphql-orm-ai-tool-profiles` `0.11.0`, and external `agql-auth` `0.19.0` at `1d2e9fe2e1576105212a7b340a11abf8cad0382d`. Completed stateless local-provider turns can carry a proof-bearing contained diff --git a/crates/graphql-orm-ai/docs/remote-graphql-execution.md b/crates/graphql-orm-ai/docs/remote-graphql-execution.md index 112fa097..85a78415 100644 --- a/crates/graphql-orm-ai/docs/remote-graphql-execution.md +++ b/crates/graphql-orm-ai/docs/remote-graphql-execution.md @@ -82,6 +82,9 @@ It must: - propagate correlation, causation, actor/mechanism, and ordinary application audit metadata; - apply bounded response size/time limits before returning; and +- return `ToolExecutionError::ResultBudgetExceeded` only when the response is + refused for crossing that byte ceiling, without retaining or disclosing its + content; and - ensure `PrivateDirect` authorization is never broader than the equivalent `PrivateRouted` path. diff --git a/crates/graphql-orm-ai/src/provider.rs b/crates/graphql-orm-ai/src/provider.rs index 32ac57bd..e018aa49 100644 --- a/crates/graphql-orm-ai/src/provider.rs +++ b/crates/graphql-orm-ai/src/provider.rs @@ -2309,7 +2309,7 @@ pub enum AiApplicationToolFailureCode { SelectionTooLarge, /// A selected relationship path exceeds the configured depth. RelationshipDepthExceeded, - /// The complete selected result exceeds the aggregate record budget. + /// The complete selected result exceeds its byte or aggregate record budget. ResultBudgetExceeded, /// The loaded schema/catalogue/target/capability binding is stale. CapabilityStale, @@ -2412,6 +2412,7 @@ pub fn classify_safe_application_tool_error( Some(AiApplicationToolFailureCode::CapabilityStale) } AiError::InvalidInput(_) => Some(AiApplicationToolFailureCode::InvalidArguments), + AiError::ResultBudgetExceeded => Some(AiApplicationToolFailureCode::ResultBudgetExceeded), AiError::Forbidden => Some(AiApplicationToolFailureCode::AuthorizationDenied), AiError::InvalidConfiguration(_) => Some(AiApplicationToolFailureCode::ToolUnavailable), AiError::NotFound => Some(AiApplicationToolFailureCode::NotFound), @@ -3343,6 +3344,10 @@ mod safe_failure_tests { assert_eq!(encoded["code"], "authorization_denied"); assert_eq!(encoded["retryable"], false); assert!(!format!("{encoded}").contains("policy")); + assert_eq!( + classify_safe_application_tool_error(&AiError::ResultBudgetExceeded), + Some(AiApplicationToolFailureCode::ResultBudgetExceeded) + ); assert!(classify_safe_application_tool_error(&AiError::Conflict).is_none()); assert!(classify_safe_application_tool_error(&AiError::PersistenceFailed).is_none()); } diff --git a/crates/graphql-orm-ai/src/runtime.rs b/crates/graphql-orm-ai/src/runtime.rs index f7d980c5..10fd474f 100644 --- a/crates/graphql-orm-ai/src/runtime.rs +++ b/crates/graphql-orm-ai/src/runtime.rs @@ -17,8 +17,8 @@ use crate::{ GraphqlInvocationContext, GraphqlRequestContextFactory, ModelRequest, ProviderBackgroundBinding, ProviderBackgroundObservation, ProviderBackgroundRetrievalBinding, ProviderBackgroundRetrievalContext, ProviderBackgroundSubmission, ProviderError, - ProviderEventStream, ProviderKind, ProviderRequestContext, ToolGraphqlRequest, - ToolGraphqlResponse, ToolMaturity, + ProviderEventStream, ProviderKind, ProviderRequestContext, ToolExecutionError, + ToolGraphqlRequest, ToolGraphqlResponse, ToolMaturity, }; use graphql_orm::graphql::orm::{AiMutationExecutionPolicy, OrmSchemaModule, SchemaModuleCatalog}; @@ -365,7 +365,7 @@ impl AiRuntime { .tool_bridge .execute(principal_reference, descriptor, request) .await - .map_err(|_| AiError::ToolExecutionFailed)?; + .map_err(Self::map_tool_execution_error)?; self.finish_tool_execution(descriptor, disclosure_schema, response, authorization) } @@ -431,7 +431,7 @@ impl AiRuntime { request, ) .await - .map_err(|_| AiError::ToolExecutionFailed)?; + .map_err(Self::map_tool_execution_error)?; self.finish_tool_execution(&descriptor, &disclosure_schema, response, authorization) } @@ -585,7 +585,7 @@ impl AiRuntime { .map_err(|_| AiError::ToolExecutionFailed)? .len() as u64; if response_bytes > descriptor.maximum_result_bytes { - return Err(AiError::ToolExecutionFailed); + return Err(AiError::ResultBudgetExceeded); } let disclosure = disclosure_schema .evaluate_graphql_with_record_limit(&response.data, descriptor.maximum_result_records) @@ -629,7 +629,7 @@ impl AiRuntime { prepared.request, ) .await - .map_err(|_| AiError::ToolExecutionFailed)?; + .map_err(Self::map_tool_execution_error)?; self.finish_tool_execution( &prepared.descriptor, &prepared.disclosure_schema, @@ -674,7 +674,7 @@ impl AiRuntime { &binding.authorization_state_digest, ) .await - .map_err(|_| AiError::ToolExecutionFailed)?; + .map_err(Self::map_tool_execution_error)?; self.finish_tool_execution( &prepared.descriptor, &prepared.disclosure_schema, @@ -786,7 +786,7 @@ impl AiRuntime { &binding.authorization_state_digest, ) .await - .map_err(|_| AiError::ToolExecutionFailed)?; + .map_err(Self::map_tool_execution_error)?; self.finish_tool_execution(descriptor, disclosure_schema, response, authorization) } @@ -820,7 +820,7 @@ impl AiRuntime { .map_err(|_| AiError::ToolExecutionFailed)? .len() as u64; if response_bytes > descriptor.maximum_result_bytes { - return Err(AiError::ToolExecutionFailed); + return Err(AiError::ResultBudgetExceeded); } let disclosure = disclosure_schema .evaluate_graphql_with_record_limit(&response.data, descriptor.maximum_result_records) @@ -834,6 +834,13 @@ impl AiRuntime { }) } + fn map_tool_execution_error(error: ToolExecutionError) -> AiError { + match error { + ToolExecutionError::ResultBudgetExceeded => AiError::ResultBudgetExceeded, + _ => AiError::ToolExecutionFailed, + } + } + /// Calls a registered provider only after start readiness and exact egress /// authorization. pub async fn stream_provider( diff --git a/crates/graphql-orm-ai/tests/runtime_contracts.rs b/crates/graphql-orm-ai/tests/runtime_contracts.rs index a95c8e54..9b09ca04 100644 --- a/crates/graphql-orm-ai/tests/runtime_contracts.rs +++ b/crates/graphql-orm-ai/tests/runtime_contracts.rs @@ -68,6 +68,14 @@ impl AuthenticatedGraphqlExecutor for Executor { context: GraphqlRequestContext, request: ToolGraphqlRequest, ) -> Result { + if request + .variables + .get("rejectOversizedResult") + .and_then(serde_json::Value::as_bool) + .unwrap_or(false) + { + return Err(ToolExecutionError::ResultBudgetExceeded); + } let scopes = context .downcast_ref::>() .ok_or(ToolExecutionError::RequestContext)?; @@ -78,6 +86,13 @@ impl AuthenticatedGraphqlExecutor for Executor { .unwrap_or(false) { json!({"scopes": scopes, "credential": "must-not-escape"}) + } else if request + .variables + .get("emitOversizedResult") + .and_then(serde_json::Value::as_bool) + .unwrap_or(false) + { + json!({"scopes": ["x".repeat(65 * 1024)]}) } else { json!({"scopes": scopes}) }; @@ -208,6 +223,8 @@ fn runtime() -> AiRuntime { "type": "object", "properties": { "emitUnknown": { "type": "boolean" }, + "emitOversizedResult": { "type": "boolean" }, + "rejectOversizedResult": { "type": "boolean" }, "incompleteAuthorization": { "type": "boolean" } }, "additionalProperties": false @@ -405,6 +422,30 @@ async fn runtime_rejects_invalid_arguments_and_non_disclosed_resolver_fields() { )); } +#[tokio::test] +async fn runtime_preserves_result_budget_failures_from_transport_and_descriptor_limits() { + let runtime = runtime(); + open_runtime(&runtime); + let principal_reference = principal(&["records:read"]).reference(); + let tool_id = AiToolId::parse("records.current").expect("tool ID"); + + for variables in [ + json!({"rejectOversizedResult": true}), + json!({"emitOversizedResult": true}), + ] { + assert!(matches!( + runtime + .execute_tool( + &principal_reference, + &tool_id, + current_request(&runtime, variables), + ) + .await, + Err(AiError::ResultBudgetExceeded) + )); + } +} + #[test] fn runtime_builder_requires_every_security_boundary() { let result = AiRuntime::builder().build(); diff --git a/crates/graphql-orm/tests/fixtures/backend-coexistence/Cargo.lock b/crates/graphql-orm/tests/fixtures/backend-coexistence/Cargo.lock index 26fdb099..48be6127 100644 --- a/crates/graphql-orm/tests/fixtures/backend-coexistence/Cargo.lock +++ b/crates/graphql-orm/tests/fixtures/backend-coexistence/Cargo.lock @@ -1329,7 +1329,7 @@ dependencies = [ [[package]] name = "graphql-orm-ai" -version = "0.96.2" +version = "0.97.0" dependencies = [ "agql-auth", "async-graphql", @@ -1357,7 +1357,7 @@ dependencies = [ [[package]] name = "graphql-orm-ai-tool-profiles" -version = "0.10.4" +version = "0.11.0" dependencies = [ "async-graphql", "async-graphql-parser", diff --git a/docs/reference/workspace-packages.md b/docs/reference/workspace-packages.md index 8553e8d0..9aaacf75 100644 --- a/docs/reference/workspace-packages.md +++ b/docs/reference/workspace-packages.md @@ -19,8 +19,8 @@ changes. | Package | Version | Path | Default features | Direct internal dependencies | | --- | --- | --- | --- | --- | | `graphql-orm` | `0.30.0` | `crates/graphql-orm` | `sqlite` | `graphql-orm-macros`, `graphql-orm-operation-catalog`, `graphql-orm-router-protocol` (dev-only) | -| `graphql-orm-ai` | `0.96.2` | `crates/graphql-orm-ai` | `sqlite` | `graphql-orm`, `graphql-orm-ai-tool-profiles`, `graphql-orm-storage` | -| `graphql-orm-ai-tool-profiles` | `0.10.4` | `crates/graphql-orm-ai-tool-profiles` | none | `graphql-orm-operation-catalog`, `graphql-orm-router-protocol` (dev-only) | +| `graphql-orm-ai` | `0.97.0` | `crates/graphql-orm-ai` | `sqlite` | `graphql-orm`, `graphql-orm-ai-tool-profiles`, `graphql-orm-storage` | +| `graphql-orm-ai-tool-profiles` | `0.11.0` | `crates/graphql-orm-ai-tool-profiles` | none | `graphql-orm-operation-catalog`, `graphql-orm-router-protocol` (dev-only) | | `graphql-orm-backup` | `0.7.2` | `crates/graphql-orm-backup` | `local` | `graphql-orm` (optional), `graphql-orm-storage` | | `graphql-orm-macros` | `0.30.0` | `crates/graphql-orm-macros` | `sqlite` | none | | `graphql-orm-operation-catalog` | `0.4.0` | `crates/graphql-orm-operation-catalog` | none | `graphql-orm-router-protocol` (optional) |