Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions crates/graphql-orm-ai/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,28 @@ 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.95.0] - 2026-08-25

Persistent schema module: **0.63.0** (unchanged from 0.94.0).

### Fixed

- Initial provider-retained dynamic application-tool plans now admit
independently governed provider built-ins, including hosted search, instead
of rejecting the combined plan before provider transport.

### Security

- Coexistence does not grant a built-in capability. The provider call still
requires a structurally valid built-in request, an exact provider capability,
resolved-rule permission, separately authorized egress, bounded call and
budget capacity, and authoritative lifecycle usage accounting. Attachments,
tool-result input, reasoning summaries, output schemas, and continuations
remain prohibited by the initial dynamic-tool constructor.

There is no database, data, table, column, index, constraint, backfill,
protected-payload, GraphQL SDL, backup, or restore migration.

## [0.94.0] - 2026-08-23

Persistent schema module: **0.63.0** (unchanged from 0.93.2).
Expand Down
2 changes: 1 addition & 1 deletion crates/graphql-orm-ai/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "graphql-orm-ai"
version = "0.94.0"
version = "0.95.0"
edition = "2024"
authors = ["Toby Martin <toby@dastari.net>"]
description = "Project-agnostic AI agent runtime for graphql-orm applications"
Expand Down
14 changes: 14 additions & 0 deletions crates/graphql-orm-ai/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ 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.94.0 to 0.95.0: retained dynamic tools with governed built-ins

Adopt `graphql-orm-ai` 0.95.0 from one reviewed full monorepo revision. Hosts
may now use `AiReadOnlyAgentTurnPlan::new_experimental_dynamic_tools` for an
initial provider-retained request that combines registered application tools
with provider built-ins already validated by `AiProviderCallPlan`.

No new authority is implied by coexistence. Continue supplying each built-in's
provider capability, resolved-rule permission, separate egress manifest,
bounded call ceiling, atomic budget capacity, and lifecycle usage accounting.
Existing dynamic-tool requests without built-ins are unchanged. The AI schema
module remains **0.63.0**; there is no database, data, GraphQL SDL,
protected-payload, backup, or restore migration.

## 0.93.2 to 0.94.0: shape-aware capability discovery

Adopt `graphql-orm-ai` 0.94.0 and `graphql-orm-ai-tool-profiles` 0.10.0 from one
Expand Down
2 changes: 1 addition & 1 deletion crates/graphql-orm-ai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<reviewed-full-40-character-commit-sha>", version = "0.94.0", default-features = false, features = ["sqlite"] }
graphql-orm-ai = { git = "https://github.com/Dastari/graphql-orm.git", rev = "<reviewed-full-40-character-commit-sha>", version = "0.95.0", default-features = false, features = ["sqlite"] }
```

Exactly one persistence backend is required: `sqlite` (default), `postgres`,
Expand Down
2 changes: 1 addition & 1 deletion crates/graphql-orm-ai/docs/implementation-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ supersedes: []

# Implementation Status

`graphql-orm-ai` is at crate version `0.94.0` with AI schema module
`graphql-orm-ai` is at crate version `0.95.0` with AI schema module
`0.63.0`. It uses workspace `graphql-orm` `0.26.0`, backend-neutral
`graphql-orm-ai-tool-profiles` `0.10.0`, and external `agql-auth`
`0.18.0` at `527d15d28e3c295a6f6b5e6d74559a7aecdc1322`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,14 @@ disclosure- and egress-approved result is returned to app-server. Unknown,
duplicate, stale, over-limit, changed-policy, or incomplete calls poison the
turn and make a retained cursor cleanup-only.

A retained dynamic-tool turn may also carry provider built-ins that already
passed the ordinary provider-call contract. Coexistence itself grants nothing:
each built-in still requires exact provider capability and resolved-rule
permission, its own authorized egress transfer, a request call ceiling, atomic
budget capacity, and authoritative start/completion accounting. The initial
dynamic-tool boundary continues to reject attachments, tool-result input,
reasoning summaries, output schemas, and continuations.

Owning subgraphs compile generated or custom profiles into a canonical
manifest and register it in `AiToolCatalog`. Build the provider definition with
`AiToolCatalog::read_only_model_definition`; do not copy the description,
Expand Down
27 changes: 25 additions & 2 deletions crates/graphql-orm-ai/src/orm_coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,11 @@ impl AiReadOnlyAgentTurnPlan {
///
/// Returns [`AiError::InvalidInput`] unless the plan contains at least one
/// application tool, is an initial provider-retained request without
/// built-ins, attachments, result input, reasoning summaries, or an output
/// schema, is exactly rule-bound, and has a valid result-egress route.
/// attachments, result input, reasoning summaries, or an output schema,
/// is exactly rule-bound, and has a valid result-egress route. Validated
/// provider built-ins may coexist with the application tools; their
/// capability, egress, budget, and usage proofs remain independently
/// enforced by the provider-call plan and executor.
pub fn new_experimental_dynamic_tools(
provider_call: AiProviderCallPlan,
result_egress_route: AiToolResultEgressRoute,
Expand Down Expand Up @@ -3833,6 +3836,26 @@ mod tests {
));
}

#[test]
fn retained_dynamic_tool_factory_accepts_validated_provider_builtins() {
let lease = AiRunLease::test_running(principal_reference());
let scope = test_scope();
let provider_call = AiProviderCallPlan::test_dynamic_builtin_plan(&lease, scope.clone());

let plan = AiReadOnlyAgentTurnPlan::new_experimental_dynamic_tools(
provider_call,
test_route(),
test_rules(scope),
false,
)
.expect("a validated hosted built-in may coexist with retained dynamic tools");
let (_, _, _, mode, _, _, _, _) = plan.into_parts();
assert!(matches!(
mode,
AiReadOnlyAgentTurnMode::ExperimentalDynamicTools(_)
));
}

#[tokio::test]
async fn pre_transport_budget_denial_fails_cleanly_instead_of_requiring_recovery() {
let lease = AiRunLease::test_running(principal_reference());
Expand Down
69 changes: 68 additions & 1 deletion crates/graphql-orm-ai/src/provider_calls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1124,7 +1124,6 @@ impl AiProviderCallPlan {
!self.request.tools.is_empty()
&& self.request.continuation.is_none()
&& self.request.continuation_mode == ModelContinuationMode::ProviderRetained
&& self.request.builtin_tools.is_empty()
&& self.request.reasoning_summary.maximum_bytes().is_none()
&& self.request.output_schema.is_none()
&& !self.request.input.iter().any(|block| {
Expand Down Expand Up @@ -1479,6 +1478,17 @@ impl AiProviderCallPlan {
plan
}

#[cfg(test)]
pub(crate) fn test_dynamic_builtin_plan(lease: &AiRunLease, scope: crate::AiScope) -> Self {
let mut plan = Self::test_plan(lease, scope, false);
plan.request.builtin_tools = vec![ModelBuiltinTool::WebSearch {
domains: crate::ModelWebSearchDomainPolicy::PublicWeb,
}];
plan.request.maximum_builtin_tool_calls = Some(2);
plan.budget.estimate.tool_units = 2;
plan
}

#[cfg(test)]
pub(crate) fn test_chat_continuation_plan(lease: &AiRunLease, scope: crate::AiScope) -> Self {
let mut plan = Self::test_chat_plan(lease, scope);
Expand Down Expand Up @@ -5669,6 +5679,35 @@ mod tests {
.expect("both custom-tool capabilities should permit the estimate");
}

#[tokio::test]
async fn retained_dynamic_tools_and_governed_web_search_share_one_plan() {
let fixture = fixture(Vec::new()).await;
let plan = dynamic_web_search_plan(&fixture, 2);
let resolution = AiAgentRuleResolution::new(
test_rules(fixture.scope.clone()),
OffsetDateTime::now_utc(),
)
.expect("test rules should resolve");
plan.project_rule_usage(&resolution, AiRuleRunUsage::default(), false)
.expect("the combined tool and hosted-search estimate should fit current rules");

crate::AiReadOnlyAgentTurnPlan::new_experimental_dynamic_tools(
plan,
crate::AiToolResultEgressRoute::new(
"mock-profile",
"local-mock",
AiDestinationTrust::ManagedProvider,
"test_inference",
"none",
"egress-v1",
)
.expect("test result-egress route should validate"),
resolution.rules().clone(),
false,
)
.expect("governed hosted search should coexist with retained dynamic tools");
}

fn plan(fixture: &Fixture) -> AiProviderCallPlan {
let request = ModelRequest {
model: "mock-model".to_owned(),
Expand Down Expand Up @@ -8297,6 +8336,34 @@ mod tests {
.expect("registered enabled read-only tool plan should validate")
}

fn dynamic_web_search_plan(
fixture: &Fixture,
maximum_builtin_tool_calls: u64,
) -> AiProviderCallPlan {
let mut base = tool_plan(fixture);
base.request.builtin_tools = vec![ModelBuiltinTool::WebSearch {
domains: crate::ModelWebSearchDomainPolicy::AllowedDomains {
domains: vec!["example.com".to_owned()],
},
}];
base.request.maximum_builtin_tool_calls = Some(maximum_builtin_tool_calls);
base.budget.estimate.tool_units = maximum_builtin_tool_calls;
base.transfers[0].estimated_bytes = base.request.conservative_egress_bytes();
let mut web_search_manifest = base.transfers[0].clone();
web_search_manifest.capability = AiEgressCapability::WebSearch;
base.transfers.push(web_search_manifest);
AiProviderCallPlan::new_with_tools(
base.provider_kind,
base.request,
base.budget,
base.transfers,
base.correlation_id,
fixture.runtime.tool_catalog(),
&static_read_policy(fixture),
)
.expect("dynamic-tool and web-search provider plan should validate")
}

fn static_read_policy(fixture: &Fixture) -> AiToolPolicySet {
let descriptor = fixture
.runtime
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/reference/workspace-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ changes.
| Package | Version | Path | Default features | Direct internal dependencies |
| --- | --- | --- | --- | --- |
| `graphql-orm` | `0.26.0` | `crates/graphql-orm` | `sqlite` | `graphql-orm-macros`, `graphql-orm-operation-catalog`, `graphql-orm-router-protocol` (dev-only) |
| `graphql-orm-ai` | `0.94.0` | `crates/graphql-orm-ai` | `sqlite` | `graphql-orm`, `graphql-orm-ai-tool-profiles`, `graphql-orm-storage` |
| `graphql-orm-ai` | `0.95.0` | `crates/graphql-orm-ai` | `sqlite` | `graphql-orm`, `graphql-orm-ai-tool-profiles`, `graphql-orm-storage` |
| `graphql-orm-ai-tool-profiles` | `0.10.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.26.0` | `crates/graphql-orm-macros` | `sqlite` | none |
Expand Down