From 00bff1809fa6f27d286012da6c426cc3a44dd4a1 Mon Sep 17 00:00:00 2001 From: Toby Martin Date: Sat, 22 Aug 2026 08:08:02 +1000 Subject: [PATCH 1/4] feat(ai): expose retained dynamic-tool readiness input --- Cargo.lock | 2 +- crates/graphql-orm-ai/CHANGELOG.md | 19 +++++ crates/graphql-orm-ai/Cargo.toml | 2 +- crates/graphql-orm-ai/MIGRATION.md | 15 ++++ crates/graphql-orm-ai/README.md | 2 +- .../docs/implementation-status.md | 2 +- .../src/providers/codex_app_server.rs | 80 +++++++++++++++++++ 7 files changed, 118 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 706f074..ae2512e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3104,7 +3104,7 @@ dependencies = [ [[package]] name = "graphql-orm-ai" -version = "0.86.0" +version = "0.87.0" dependencies = [ "agql-auth", "async-graphql", diff --git a/crates/graphql-orm-ai/CHANGELOG.md b/crates/graphql-orm-ai/CHANGELOG.md index c39378a..4bf7f19 100644 --- a/crates/graphql-orm-ai/CHANGELOG.md +++ b/crates/graphql-orm-ai/CHANGELOG.md @@ -18,6 +18,25 @@ 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.87.0] - 2026-08-22 + +Persistent schema module: **0.63.0** (unchanged from 0.86.0). + +### Added + +- `AiCodexAppServerTurnInput::retained_dynamic_tool_readiness_probe` lets a + reviewed host issue one startup probe against the exact bootstrap-bound, + complete retained dynamic-tool surface before advertising readiness. + +### Security + +- The constructor only prepares validated input. It grants no provider, + process, tool-execution, egress or application authority, requires a + nonempty exact tool set and binds the retained bootstrap fingerprint. + +There is no schema, data, protected-payload, GraphQL SDL, backup or restore +migration in this release. + ## [0.86.0] - 2026-08-22 Persistent schema module: **0.63.0** (unchanged from 0.85.0). diff --git a/crates/graphql-orm-ai/Cargo.toml b/crates/graphql-orm-ai/Cargo.toml index 0559962..e91d6d8 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.86.0" +version = "0.87.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 4d5e701..b03c8f3 100644 --- a/crates/graphql-orm-ai/MIGRATION.md +++ b/crates/graphql-orm-ai/MIGRATION.md @@ -19,6 +19,21 @@ 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.86.0 to 0.87.0: retained dynamic-tool readiness input + +Adopt `graphql-orm-ai` 0.87.0 from one reviewed full monorepo revision. The AI +schema module remains **0.63.0**. There is no database, data, table, column, +index, constraint, backfill, GraphQL SDL, protected-payload, backup or restore +migration. + +Hosts that readiness-test an installed Codex app-server may construct a +bootstrap-fingerprint-bound retained turn with +`AiCodexAppServerTurnInput::retained_dynamic_tool_readiness_probe`. Supply the +same complete dynamic-tool definitions and reasoning effort used to create the +empty retained thread. The constructor only validates input; it does not grant +process, provider, application-tool, egress or result authority. Keep the +probe inside the reviewed host sandbox and return no application data. + ## 0.85.0 to 0.86.0: metered stateless native-item refusal Adopt `graphql-orm-ai` 0.86.0 and `graphql-orm-ai-tool-profiles` 0.9.0 from one diff --git a/crates/graphql-orm-ai/README.md b/crates/graphql-orm-ai/README.md index 7da5614..d2eefab 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.86.0", default-features = false, features = ["sqlite"] } +graphql-orm-ai = { git = "https://github.com/Dastari/graphql-orm.git", rev = "", version = "0.87.0", default-features = false, features = ["sqlite"] } ``` Exactly one persistence backend is required: `sqlite` (default), `postgres`, diff --git a/crates/graphql-orm-ai/docs/implementation-status.md b/crates/graphql-orm-ai/docs/implementation-status.md index 07c177a..bb23b1f 100644 --- a/crates/graphql-orm-ai/docs/implementation-status.md +++ b/crates/graphql-orm-ai/docs/implementation-status.md @@ -10,7 +10,7 @@ supersedes: [] # Implementation Status -`graphql-orm-ai` is at crate version `0.86.0` with AI schema module +`graphql-orm-ai` is at crate version `0.87.0` with AI schema module `0.63.0`. It uses workspace `graphql-orm` `0.23.0`, backend-neutral `graphql-orm-ai-tool-profiles` `0.9.0`, and external `agql-auth` `0.15.0` at `e841ffd382082ad7419be259fe957f949b956ff7`. diff --git a/crates/graphql-orm-ai/src/providers/codex_app_server.rs b/crates/graphql-orm-ai/src/providers/codex_app_server.rs index dbba26c..34a2cc2 100644 --- a/crates/graphql-orm-ai/src/providers/codex_app_server.rs +++ b/crates/graphql-orm-ai/src/providers/codex_app_server.rs @@ -618,6 +618,45 @@ impl AiCodexAppServerTurnInput { Ok(turn) } + /// Creates one retained dynamic-tool turn for a host-owned readiness + /// probe. + /// + /// The input is bound to the exact bootstrap instructions and complete + /// dynamic-tool set that the host supplied when it created the empty + /// retained thread. This constructor adds no provider, process, tool + /// execution, egress, or application authority; the host remains + /// responsible for running the probe in its reviewed sandbox and for + /// returning no application data. + /// + /// # Errors + /// + /// Returns [`ProviderError::InvalidRequest`] when the model, prompt, + /// tools, bootstrap binding, reasoning effort, or output ceiling cannot + /// form a valid retained dynamic-tool turn. + pub fn retained_dynamic_tool_readiness_probe( + model: impl Into, + bootstrap: &AiCodexAppServerBootstrapInstructions, + prompt: impl Into, + tools: Vec, + reasoning_effort: ModelReasoningEffort, + maximum_output_tokens: u64, + ) -> Result { + if tools.is_empty() { + return Err(ProviderError::InvalidRequest); + } + let mut turn = Self::new( + model, + bootstrap.blocks.clone(), + vec![prompt.into()], + reasoning_effort, + maximum_output_tokens, + )?; + turn.retained_bootstrap_fingerprint = Some(bootstrap.fingerprint().to_owned()); + turn.tools = tools; + turn.validate()?; + Ok(turn) + } + fn validate(&self) -> Result<(), ProviderError> { let text_bytes = self .instructions @@ -6135,6 +6174,47 @@ pub(crate) mod tests { ); } + #[test] + fn retained_dynamic_readiness_probe_is_bootstrap_and_surface_bound() { + let bootstrap = bootstrap_instructions(); + let tool = dynamic_tool(); + let input = AiCodexAppServerTurnInput::retained_dynamic_tool_readiness_probe( + "model-1", + &bootstrap, + "Call the named readiness tool exactly once.", + vec![tool.clone()], + ModelReasoningEffort::Medium, + 128, + ) + .expect("readiness input should validate"); + + assert_eq!(input.model(), "model-1"); + assert_eq!( + input.input(), + &["Call the named readiness tool exactly once.".to_owned()] + ); + assert_eq!(input.instructions(), bootstrap.blocks.as_slice()); + assert_eq!(input.tools(), &[tool]); + assert_eq!( + input.retained_bootstrap_fingerprint.as_deref(), + Some(bootstrap.fingerprint()) + ); + assert_eq!(input.reasoning_effort(), ModelReasoningEffort::Medium); + assert_eq!(input.maximum_output_tokens(), 128); + + assert!(matches!( + AiCodexAppServerTurnInput::retained_dynamic_tool_readiness_probe( + "model-1", + &bootstrap, + "Call the named readiness tool exactly once.", + Vec::new(), + ModelReasoningEffort::Medium, + 128, + ), + Err(ProviderError::InvalidRequest) + )); + } + #[test] fn retained_bootstrap_is_static_registration_bound_and_active_on_first_turn() { let bootstrap = bootstrap_instructions(); From 7bc8a5c084489a922da4c0c9cdce392623959d55 Mon Sep 17 00:00:00 2001 From: Toby Martin Date: Sat, 22 Aug 2026 08:17:10 +1000 Subject: [PATCH 2/4] feat(ai): settle fixed readiness tool responses --- crates/graphql-orm-ai/CHANGELOG.md | 7 +- crates/graphql-orm-ai/MIGRATION.md | 11 +- crates/graphql-orm-ai/src/provider.rs | 8 + .../src/providers/codex_app_server.rs | 159 ++++++++++++++++++ 4 files changed, 181 insertions(+), 4 deletions(-) diff --git a/crates/graphql-orm-ai/CHANGELOG.md b/crates/graphql-orm-ai/CHANGELOG.md index 4bf7f19..7ecef46 100644 --- a/crates/graphql-orm-ai/CHANGELOG.md +++ b/crates/graphql-orm-ai/CHANGELOG.md @@ -27,12 +27,17 @@ Persistent schema module: **0.63.0** (unchanged from 0.86.0). - `AiCodexAppServerTurnInput::retained_dynamic_tool_readiness_probe` lets a reviewed host issue one startup probe against the exact bootstrap-bound, complete retained dynamic-tool surface before advertising readiness. +- `AiCodexAppServerProtocolActor::dynamic_tool_readiness_response` can settle + only the probe's exact expected tool call with the fixed content-free result + `{"ready":true}`, allowing the host to finish and delete the test thread. ### Security - The constructor only prepares validated input. It grants no provider, process, tool-execution, egress or application authority, requires a - nonempty exact tool set and binds the retained bootstrap fingerprint. + nonempty exact tool set and binds the retained bootstrap fingerprint. The + actor cannot use the fixed readiness response for an ordinary or different + tool call, and it cannot carry application content. There is no schema, data, protected-payload, GraphQL SDL, backup or restore migration in this release. diff --git a/crates/graphql-orm-ai/MIGRATION.md b/crates/graphql-orm-ai/MIGRATION.md index b03c8f3..0b6acf4 100644 --- a/crates/graphql-orm-ai/MIGRATION.md +++ b/crates/graphql-orm-ai/MIGRATION.md @@ -30,9 +30,14 @@ Hosts that readiness-test an installed Codex app-server may construct a bootstrap-fingerprint-bound retained turn with `AiCodexAppServerTurnInput::retained_dynamic_tool_readiness_probe`. Supply the same complete dynamic-tool definitions and reasoning effort used to create the -empty retained thread. The constructor only validates input; it does not grant -process, provider, application-tool, egress or result authority. Keep the -probe inside the reviewed host sandbox and return no application data. +empty retained thread, plus the exact tool ID that the model must select. The +constructor only validates input; it does not grant process, provider, +application-tool, egress or result authority. After the actor admits that +exact call, use `dynamic_tool_readiness_response` to return the sole fixed +`{"ready":true}` result, finish the turn and delete the thread. The actor +rejects this response on an ordinary turn or another tool, and the result +cannot contain application data. Keep the probe inside the reviewed host +sandbox. ## 0.85.0 to 0.86.0: metered stateless native-item refusal diff --git a/crates/graphql-orm-ai/src/provider.rs b/crates/graphql-orm-ai/src/provider.rs index 5e26e01..19f54d4 100644 --- a/crates/graphql-orm-ai/src/provider.rs +++ b/crates/graphql-orm-ai/src/provider.rs @@ -2203,6 +2203,14 @@ impl std::fmt::Debug for ProviderDynamicToolResult { } impl ProviderDynamicToolResult { + pub(crate) fn readiness_probe(call_id: String, tool_id: String) -> Self { + Self { + call_id, + tool_id, + output: serde_json::json!({"ready": true}), + } + } + #[cfg(any(feature = "sqlite", feature = "postgres"))] pub(crate) fn new( call: &ProviderDynamicToolCall, diff --git a/crates/graphql-orm-ai/src/providers/codex_app_server.rs b/crates/graphql-orm-ai/src/providers/codex_app_server.rs index 34a2cc2..fd50029 100644 --- a/crates/graphql-orm-ai/src/providers/codex_app_server.rs +++ b/crates/graphql-orm-ai/src/providers/codex_app_server.rs @@ -568,6 +568,7 @@ pub struct AiCodexAppServerTurnInput { tools: Vec, reasoning_effort: ModelReasoningEffort, maximum_output_tokens: u64, + readiness_probe_tool_id: Option, } impl std::fmt::Debug for AiCodexAppServerTurnInput { @@ -586,6 +587,7 @@ impl std::fmt::Debug for AiCodexAppServerTurnInput { .field("tool_count", &self.tools.len()) .field("reasoning_effort", &self.reasoning_effort) .field("maximum_output_tokens", &self.maximum_output_tokens) + .field("readiness_probe", &self.readiness_probe_tool_id.is_some()) .finish() } } @@ -613,6 +615,7 @@ impl AiCodexAppServerTurnInput { tools: Vec::new(), reasoning_effort, maximum_output_tokens, + readiness_probe_tool_id: None, }; turn.validate()?; Ok(turn) @@ -638,12 +641,17 @@ impl AiCodexAppServerTurnInput { bootstrap: &AiCodexAppServerBootstrapInstructions, prompt: impl Into, tools: Vec, + readiness_tool_id: impl Into, reasoning_effort: ModelReasoningEffort, maximum_output_tokens: u64, ) -> Result { if tools.is_empty() { return Err(ProviderError::InvalidRequest); } + let readiness_tool_id = readiness_tool_id.into(); + if !tools.iter().any(|tool| tool.tool_id == readiness_tool_id) { + return Err(ProviderError::InvalidRequest); + } let mut turn = Self::new( model, bootstrap.blocks.clone(), @@ -653,6 +661,7 @@ impl AiCodexAppServerTurnInput { )?; turn.retained_bootstrap_fingerprint = Some(bootstrap.fingerprint().to_owned()); turn.tools = tools; + turn.readiness_probe_tool_id = Some(readiness_tool_id); turn.validate()?; Ok(turn) } @@ -688,6 +697,16 @@ impl AiCodexAppServerTurnInput { return Err(ProviderError::InvalidRequest); } } + if self + .readiness_probe_tool_id + .as_ref() + .is_some_and(|tool_id| { + self.retained_bootstrap_fingerprint.is_none() + || !self.tools.iter().any(|tool| tool.tool_id == *tool_id) + }) + { + return Err(ProviderError::InvalidRequest); + } Ok(()) } @@ -3017,6 +3036,7 @@ pub struct AiCodexAppServerProtocolActor { pending_dynamic_requests: BTreeMap, started_dynamic_calls: BTreeMap, responded_dynamic_calls: BTreeMap, + readiness_probe_tool_id: Option, started_items: BTreeMap, completed_items: BTreeSet, initialization_complete: bool, @@ -3060,6 +3080,7 @@ impl AiCodexAppServerProtocolActor { pending_dynamic_requests: BTreeMap::new(), started_dynamic_calls: BTreeMap::new(), responded_dynamic_calls: BTreeMap::new(), + readiness_probe_tool_id: None, started_items: BTreeMap::new(), completed_items: BTreeSet::new(), initialization_complete: false, @@ -3503,6 +3524,39 @@ impl AiCodexAppServerProtocolActor { Ok(frame) } + /// Acknowledges the exact dynamic-tool call selected by a marked + /// host-owned readiness probe with one fixed content-free result. + /// + /// This method cannot acknowledge an ordinary turn, a different tool, or + /// an already answered call. Its output is always `{"ready":true}` and + /// therefore cannot carry application data or substitute for the + /// coordinator-owned application-tool result path. + /// + /// # Errors + /// + /// Returns [`ProviderError::Rejected`] unless the active turn was created + /// by [`AiCodexAppServerTurnInput::retained_dynamic_tool_readiness_probe`] + /// and `request_id` names its exact expected pending tool call. + pub fn dynamic_tool_readiness_response( + &mut self, + request_id: u64, + ) -> Result, ProviderError> { + let expected_tool_id = self + .readiness_probe_tool_id + .as_deref() + .ok_or(ProviderError::Rejected)?; + let (call_id, tool_id) = self + .pending_dynamic_requests + .get(&request_id) + .filter(|(_, tool_id)| tool_id == expected_tool_id) + .cloned() + .ok_or(ProviderError::Rejected)?; + let result = crate::ProviderDynamicToolResult::readiness_probe(call_id, tool_id); + let frame = self.dynamic_tool_response(request_id, &result)?; + self.readiness_probe_tool_id = None; + Ok(frame) + } + /// Encodes text-only user input for one exact lifecycle-complete thread. /// /// Trusted instructions are deliberately not copied into the user input @@ -3548,6 +3602,7 @@ impl AiCodexAppServerProtocolActor { || self.turn_started_observed || !self.started_items.is_empty() || !self.completed_items.is_empty() + || self.readiness_probe_tool_id.is_some() { return Err(ProviderError::InvalidRequest); } @@ -3571,6 +3626,7 @@ impl AiCodexAppServerProtocolActor { let frame = self.request(ClientMethod::TurnStart, "turn/start", params)?; self.thread_lifecycle_operation = None; self.pending_turn_thread_id = Some(thread_id.to_owned()); + self.readiness_probe_tool_id = input.readiness_probe_tool_id.clone(); self.runtime_warning_count = 0; self.runtime_warning_bytes = 0; Ok(frame) @@ -3784,6 +3840,7 @@ impl AiCodexAppServerProtocolActor { if !self.pending_dynamic_requests.is_empty() || !self.started_dynamic_calls.is_empty() || !self.responded_dynamic_calls.is_empty() + || self.readiness_probe_tool_id.is_some() { return Err(ProviderError::Rejected); } @@ -6183,6 +6240,7 @@ pub(crate) mod tests { &bootstrap, "Call the named readiness tool exactly once.", vec![tool.clone()], + tool.tool_id.clone(), ModelReasoningEffort::Medium, 128, ) @@ -6208,6 +6266,7 @@ pub(crate) mod tests { &bootstrap, "Call the named readiness tool exactly once.", Vec::new(), + "missing.tool", ModelReasoningEffort::Medium, 128, ), @@ -6215,6 +6274,106 @@ pub(crate) mod tests { )); } + #[test] + fn retained_dynamic_readiness_response_is_fixed_exact_and_settleable() { + let bootstrap = AiCodexAppServerBootstrapInstructions::disabled(); + let tool = dynamic_tool(); + let input = AiCodexAppServerTurnInput::retained_dynamic_tool_readiness_probe( + "model-1", + &bootstrap, + "Call inventory_count once with Limit 3.", + vec![tool.clone()], + tool.tool_id.clone(), + ModelReasoningEffort::Unspecified, + 128, + ) + .expect("readiness input should validate"); + let mut actor = initialized_protocol_actor(); + let thread_id = start_bound_dynamic_thread(&mut actor, std::slice::from_ref(&tool)); + start_bound_dynamic_turn(&mut actor, &thread_id, &input, "turn-readiness-1", 3); + + let started = lifecycle_notification( + "item/started", + json!({ + "item": { + "arguments": {"Limit": 3}, + "id": "call-readiness-1", + "namespace": null, + "status": "inProgress", + "tool": "inventory_count", + "type": "dynamicToolCall" + }, + "startedAtMs": 1, + "threadId": thread_id, + "turnId": "turn-readiness-1" + }), + ); + actor + .accept(&started) + .expect("readiness lifecycle should start"); + let request = serde_json::to_vec(&json!({ + "id": 0, + "method": "item/tool/call", + "params": { + "arguments": {"Limit": 3}, + "callId": "call-readiness-1", + "namespace": null, + "threadId": thread_id, + "tool": "inventory_count", + "turnId": "turn-readiness-1" + } + })) + .expect("readiness call should encode"); + assert!(matches!( + actor.accept(&request), + Ok(AiCodexAppServerInbound::DynamicToolCall { request_id: 0, .. }) + )); + + let response = String::from_utf8( + actor + .dynamic_tool_readiness_response(0) + .expect("exact readiness call should receive the fixed response"), + ) + .expect("readiness response should be UTF-8"); + assert!(response.contains("\\\"ready\\\":true")); + assert!(!response.contains("count")); + assert!(matches!( + actor.dynamic_tool_readiness_response(0), + Err(ProviderError::Rejected) + )); + + let completed = lifecycle_notification( + "item/completed", + json!({ + "item": { + "arguments": {"Limit": 3}, + "contentItems": [{"type": "inputText", "text": "{\"ready\":true}"}], + "durationMs": 2, + "id": "call-readiness-1", + "namespace": null, + "status": "completed", + "success": true, + "tool": "inventory_count", + "type": "dynamicToolCall" + }, + "completedAtMs": 3, + "threadId": thread_id, + "turnId": "turn-readiness-1" + }), + ); + actor + .accept(&completed) + .expect("fixed readiness result should settle"); + actor + .accept(&turn_completed_notification(&thread_id, "turn-readiness-1")) + .expect("readiness turn should complete"); + let cursor = crate::AiProviderSessionCursor::new("codex.app_server.thread.v2", thread_id) + .expect("readiness cursor should validate"); + actor + .delete_thread(&cursor) + .expect("settled readiness thread should be deletable"); + } + #[test] fn retained_bootstrap_is_static_registration_bound_and_active_on_first_turn() { let bootstrap = bootstrap_instructions(); From cc77cfa480278f10fa4916957c06362bf88ac16c Mon Sep 17 00:00:00 2001 From: Toby Martin Date: Sat, 22 Aug 2026 08:19:59 +1000 Subject: [PATCH 3/4] docs: refresh workspace package inventory --- docs/reference/workspace-packages.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/workspace-packages.md b/docs/reference/workspace-packages.md index 012e8d4..6ab0a4b 100644 --- a/docs/reference/workspace-packages.md +++ b/docs/reference/workspace-packages.md @@ -19,7 +19,7 @@ changes. | Package | Version | Path | Default features | Direct internal dependencies | | --- | --- | --- | --- | --- | | `graphql-orm` | `0.23.0` | `crates/graphql-orm` | `sqlite` | `graphql-orm-macros`, `graphql-orm-operation-catalog`, `graphql-orm-router-protocol` (dev-only) | -| `graphql-orm-ai` | `0.86.0` | `crates/graphql-orm-ai` | `sqlite` | `graphql-orm`, `graphql-orm-ai-tool-profiles`, `graphql-orm-storage` | +| `graphql-orm-ai` | `0.87.0` | `crates/graphql-orm-ai` | `sqlite` | `graphql-orm`, `graphql-orm-ai-tool-profiles`, `graphql-orm-storage` | | `graphql-orm-ai-tool-profiles` | `0.9.0` | `crates/graphql-orm-ai-tool-profiles` | none | `graphql-orm-operation-catalog`, `graphql-orm-router-protocol` (dev-only) | | `graphql-orm-backup` | `0.7.1` | `crates/graphql-orm-backup` | `local` | `graphql-orm` (optional), `graphql-orm-storage` | | `graphql-orm-macros` | `0.23.0` | `crates/graphql-orm-macros` | `sqlite` | none | From 18f0eb2921643e61f4fa53e4080cca5e1cb8ff5d Mon Sep 17 00:00:00 2001 From: Toby Martin Date: Sat, 22 Aug 2026 08:56:27 +1000 Subject: [PATCH 4/4] fix(ai): gate retained readiness result by adapter --- crates/graphql-orm-ai/src/provider.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/graphql-orm-ai/src/provider.rs b/crates/graphql-orm-ai/src/provider.rs index 19f54d4..bc522f9 100644 --- a/crates/graphql-orm-ai/src/provider.rs +++ b/crates/graphql-orm-ai/src/provider.rs @@ -2203,6 +2203,7 @@ impl std::fmt::Debug for ProviderDynamicToolResult { } impl ProviderDynamicToolResult { + #[cfg(feature = "provider-codex-app-server")] pub(crate) fn readiness_probe(call_id: String, tool_id: String) -> Self { Self { call_id,