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
4 changes: 2 additions & 2 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ futures = "0.3"
getrandom = "0.3"
graphql-composition = "=0.12.2"
graphql-orm = { path = "crates/graphql-orm", version = "0.23.0", default-features = false }
graphql-orm-ai-tool-profiles = { path = "crates/graphql-orm-ai-tool-profiles", version = "0.6.0" }
graphql-orm-ai-tool-profiles = { path = "crates/graphql-orm-ai-tool-profiles", version = "0.7.0" }
graphql-orm-backup = { path = "crates/graphql-orm-backup", version = "0.7.1", default-features = false }
graphql-orm-operation-catalog = { path = "crates/graphql-orm-operation-catalog", version = "0.3.0" }
graphql-orm-router-protocol = { path = "crates/graphql-orm-router-protocol", version = "0.2.1" }
Expand Down
17 changes: 17 additions & 0 deletions crates/graphql-orm-ai-tool-profiles/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,23 @@ supersedes: []

# Changelog

## [0.7.0] - 2026-08-21

### Added

- `AiError::PreTransportBudgetDenied` is the closed execution-boundary signal
for an atomic budget refusal proven to occur before provider dispatch and
after any created reservation was released. It retains the existing public
`AI_BUDGET_DENIED` code while preventing generic tool-loop budget limits from
being mistaken for proof that provider transport never occurred.

### Breaking

- `AiError` gained `PreTransportBudgetDenied`. Although the enum is
non-exhaustive, in-crate and deliberately exhaustive consumers must handle
the new variant. `BudgetDenied` remains the generic limit error and carries
no transport-absence proof.

## [0.6.0] - 2026-08-16

### Added
Expand Down
2 changes: 1 addition & 1 deletion crates/graphql-orm-ai-tool-profiles/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "graphql-orm-ai-tool-profiles"
version = "0.6.0"
version = "0.7.0"
edition = "2024"
authors = ["Toby Martin <toby@dastari.net>"]
description = "Backend-neutral GraphQL AI tool profile compiler and manifest contracts"
Expand Down
17 changes: 17 additions & 0 deletions crates/graphql-orm-ai-tool-profiles/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,23 @@ supersedes: []

# Migration Guide

## 0.6.0 to 0.7.0: proof-bearing pre-transport budget denial

Adopt `graphql-orm-ai-tool-profiles` 0.7.0 with `graphql-orm-ai` 0.84.0 from
one reviewed full monorepo revision.

`AiError` gains `PreTransportBudgetDenied`. Only the provider-execution
boundary may return it, and only after proving provider dispatch was never
attempted and any created reservation was durably released. A budget or rule
limit reached during a provider/tool loop remains `BudgetDenied`; it must not
be reclassified as a certain local refusal. Both variants intentionally expose
the same stable public error code, `AI_BUDGET_DENIED`.

The enum was already non-exhaustive. Update any deliberately exhaustive
internal matches. There is no schema, database, data, GraphQL SDL, manifest,
capability, fingerprint, protected-content, credential, or AI schema-module
migration from this package change.

## 0.5.0 to 0.6.0: compact discovery and query-plan wire v3

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

Expand Down
9 changes: 9 additions & 0 deletions crates/graphql-orm-ai-tool-profiles/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ pub enum AiError {
/// No applicable atomic budget had enough capacity for the operation.
#[error("AI budget denied")]
BudgetDenied,
/// Provider execution was denied by an atomic budget before dispatch.
///
/// This is an execution-boundary proof, not a generic budget error. It may
/// be returned only when provider transport was never attempted and no
/// budget reservation remains held. Budget limits reached during a
/// provider/tool loop must use [`Self::BudgetDenied`] instead.
#[error("AI budget denied")]
PreTransportBudgetDenied,
/// Input failed a public schema contract.
#[error("invalid AI input: {0}")]
InvalidInput(String),
Expand Down Expand Up @@ -66,6 +74,7 @@ impl AiError {
Self::RecentMfaRequired => "AI_RECENT_MFA_REQUIRED",
Self::EgressDenied => "AI_EGRESS_DENIED",
Self::BudgetDenied => "AI_BUDGET_DENIED",
Self::PreTransportBudgetDenied => "AI_BUDGET_DENIED",
Self::InvalidInput(_) => "AI_INVALID_INPUT",
Self::ReauthorizationFailed => "AI_REAUTHORIZATION_FAILED",
Self::ToolExecutionFailed => "AI_TOOL_EXECUTION_FAILED",
Expand Down
60 changes: 60 additions & 0 deletions crates/graphql-orm-ai/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,66 @@ 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).

## [Unreleased]

Persistent schema module: **0.63.0**.

### Added

- `aiBudgetScopeCapacity` reports, for one exact scope, each budget policy's
current-period reserved and committed amounts beside its ceilings, counts of
unresolved reservations, and a bounded oldest-first list of them with expiry,
owning-run terminality, CAS version, and reclaimability. Reserved capacity
counts against a ceiling exactly like committed usage, so a host can now
alarm on stranded reservations instead of discovering them as a total refusal
to serve. Authorized by the existing `ReadBudgetPolicies` action.
- `reclaimAiBudgetReservation` resolves one stranded budget reservation whose
capacity could previously never leave the reserved column. It requires the
new `AiConfigurationAction::ManageBudgetReclamation`, recent MFA, an exact
CAS version, an expiry already past the deployment's `minimum_expired_age`,
and an owning run in a durable terminal state holding no lease. It commits
the reservation's own reserved amounts as authoritative usage and appends one
usage fact and one redacted audit fact in the same transaction. It never
releases capacity: an `uncertain` or `reserved` reservation carries no proof
that the provider was not reached, so the only safe resolution is the
conservative one, which can over-count and never under-counts.
- `AiBudgetReclamationLimits` and
`OrmAiConfigurationService::with_budget_reservation_reclamation` are the
deployment opt-in for the reclamation surface. Without them, capacity
reporting still works and every reservation reports `reclaimable: false`.

### Changed

- **A proven pre-transport budget denial is no longer reported as provider
uncertainty.** `AiError::PreTransportBudgetDenied` is produced only when the
atomic reservation was refused before dispatch, or an already-created
reservation was durably released before dispatch. Such a run now terminates
`Failed` with outcome code `provider_budget_denied` instead of
`RecoveryRequired` with `provider_turn_uncertain`, and
`provider_budget_denied` joins the retryable failure allowlist so the
terminal-event failure record reports `AiRunRetryAdmission::Allowed`. The
supervised coordinator makes the same distinction. Generic
`AiError::BudgetDenied`, including a limit reached inside a dynamic tool
loop, remains uncertain and can never claim transport absence.
- A provider call whose post-reservation authorization binding fails now
releases its reservation instead of leaving capacity held until the policy
period rolled. Nothing had been dispatched, so the release is provable.

### Breaking

- `AiConfigurationAction` gained `ManageBudgetReclamation`. Exhaustive matches
in host access policies must handle it; a host that does not recognize it
must deny.
- `AiError` gained the proof-bearing `PreTransportBudgetDenied` variant through
`graphql-orm-ai-tool-profiles` 0.7.0. It shares the existing public code with
`BudgetDenied`, but exhaustive internal matches must preserve their distinct
transport semantics.
- The persistent schema module advances to 0.63.0. It adds no entity, column,
or constraint. It makes `scope_kind`, `scope_id`, `tenant_id`, and
`expires_at` available to typed internal predicates and adds one composite
scope/tenant/state/expiry index, so stranded-reservation reporting is a
bounded indexed read.

## [0.83.0] - 2026-08-21

Persistent schema module: **0.62.0**.
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.83.0"
version = "0.84.0"
edition = "2024"
authors = ["Toby Martin <toby@dastari.net>"]
description = "Project-agnostic AI agent runtime for graphql-orm applications"
Expand Down
102 changes: 102 additions & 0 deletions crates/graphql-orm-ai/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,108 @@ 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).

## Unreleased: budget reclamation and pre-transport denial (crate 0.83.0 to 0.84.0; schema 0.62.0 to 0.63.0)

### Schema module

The AI schema module advances **0.62.0 to 0.63.0**. It adds **no entity, no
column, and no constraint**, and needs **no data migration or backfill**.
`graphql_orm_ai_budget_reservations` exposes `scope_kind`, `scope_id`,
`tenant_id`, and `expires_at` to typed internal predicates and gains the composite index
`idx_graphql_orm_ai_budget_reservations_scope_state`
(`scope_kind, scope_id, tenant_id, state, expires_at`), so the new stranded-reservation
report is a bounded indexed read rather than a table scan. Existing rows and
events remain readable at the previous module version and after the upgrade.
Apply and verify the module before serving traffic.

### Source-breaking changes

`AiConfigurationAction` gained the variant `ManageBudgetReclamation`. Any
exhaustive `match` in a host `AiConfigurationAccessPolicy` must handle it.
Authorize it only for the administrators you would trust to charge an
unprovable provider turn to a budget; a host that does not want the surface
returns `false` and it stays closed.

`AiConfigurationService` gained `budget_scope_capacity` and
`reclaim_budget_reservation`. Both have fail-closed default implementations
that return `AiError::InvalidConfiguration`, so an existing custom
implementation still compiles and does not silently gain the surface.

### New public API

- `AiBudgetScopeCapacityView`, `AiBudgetPolicyCapacityView`, and
`AiBudgetReservationCapacityView` are the redacted capacity views. They carry
capacity accounting, reservation state, expiry, owning-run linkage and CAS
versions only; never a prompt, transcript, provider payload, principal
identity, or credential. `reclaimable` identifies a deployment/time/run
candidate only; mutation authorization, recent MFA, CAS, scope and stored
graph integrity are rechecked separately.
- `ReclaimAiBudgetReservationInput { scope, reservation_id, expected_version }`.
- `AiBudgetReclamationLimits::new(minimum_expired_age, maximum_reservation_scan)`
and `OrmAiConfigurationService::with_budget_reservation_reclamation`.

### New GraphQL

`AiConfigurationQueryRoot` gains `aiBudgetScopeCapacity(scope)`, authorized by
the existing `ReadBudgetPolicies` action. `AiConfigurationMutationRoot` gains
`reclaimAiBudgetReservation(input)`, authorized by `ManageBudgetReclamation`
plus recent MFA plus the deployment opt-in. No existing field changed.

To enable reclamation:

```rust,ignore
let configuration = OrmAiConfigurationService::new(/* ... */)
.with_budget_policy_management(policy_limits)
.with_budget_reservation_reclamation(AiBudgetReclamationLimits::new(
time::Duration::hours(6),
200,
)?);
```

Without that call, `aiBudgetScopeCapacity` still works and every reservation
reports `reclaimable: false`, while `reclaimAiBudgetReservation` fails closed
as invalid configuration.

### Behavioural changes with no API change

A run returning the proof-bearing `AiError::PreTransportBudgetDenied` now
terminates `Failed` with outcome code `provider_budget_denied`. The executor
may produce that variant only when reservation failed before dispatch or when
an already-created reservation was durably released before dispatch. It
previously terminated `RecoveryRequired` with `provider_turn_uncertain`, which
told users a proven local refusal could not be confirmed and made the run
permanently unretryable. `provider_budget_denied` is on the retryable failure
allowlist, so `AiRunFailure.admission` is `AiRunRetryAdmission::Allowed` and a
client may author a new run for the same durable user message once capacity
exists. A client that keys UI text off `provider_turn_uncertain` for this case
must move it to the new code. The supervised coordinator makes the same
distinction. Generic `AiError::BudgetDenied`, including post-transport dynamic
tool-call and rule ceilings, remains uncertain and must not use this path.

A provider call whose post-reservation authorization binding fails now releases
its reservation. Nothing had been dispatched, so the release is provable.

### What deliberately did not change

Reclamation commits; it never releases. An `uncertain` or `reserved`
reservation carries no durable proof that the provider was not reached, so
releasing it would fabricate an absence proof. Committing the held estimate can
only over-count.

Reclamation therefore **does not create headroom**: the reserved column falls
by exactly the amount the committed column rises, and `reserved + committed`
against the ceiling is unchanged. A deployment whose ceiling is already
exhausted by stranded reservations raises or replaces the policy through
`upsertAiBudgetPolicy`. The value of reclamation is that held capacity becomes
accountable, reportable, and finite instead of permanently unreachable, and
that the condition is now observable before it becomes an outage.

Reclamation is not automatic. Expired-lease recovery and every other
maintenance pass are unchanged. Automating a commit would free no headroom
while adding an unattended writer of authoritative usage facts attributed to an
absent principal; the decision to charge an unprovable turn stays with an
authorized, MFA-current, audited human.

## 0.82.0 to 0.83.0: settled retained Codex interruption

Adopt `graphql-orm-ai` 0.83.0 at one reviewed full monorepo revision.
Expand Down
18 changes: 17 additions & 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.82.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.84.0", default-features = false, features = ["sqlite"] }
```

Exactly one persistence backend is required: `sqlite` (default), `postgres`,
Expand Down Expand Up @@ -173,6 +173,22 @@ Use `scripts/check-ai-provider-lanes.sh` from the repository root to verify one
feature at a time; provider feature unification is not required for a valid
adapter build.

## Budget capacity and stranded reservations

Reserved capacity counts against a budget ceiling exactly like committed
usage, so a reservation that never reconciles consumes the ceiling for the rest
of its policy period. `aiBudgetScopeCapacity` reports per-policy reserved and
committed amounts, ceilings, and a bounded list of unresolved reservations
under `ReadBudgetPolicies`. `reclaimAiBudgetReservation` resolves one expired
reservation whose owning run is terminal, under `ManageBudgetReclamation`,
recent MFA, an exact CAS version, and the
`with_budget_reservation_reclamation` deployment opt-in. It commits the held
estimate as authoritative usage rather than releasing it, because an
unreconciled reservation carries no proof that the provider was not reached.
A denial at reservation is pre-transport and certain: the run fails with
`provider_budget_denied` and stays retryable. See the
[usage and budgets guide](docs/usage-and-budgets.md).

## Reasoning effort profiles

`ModelReasoningEffort` is the closed provider-neutral selection:
Expand Down
6 changes: 3 additions & 3 deletions crates/graphql-orm-ai/docs/implementation-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ supersedes: []

# Implementation Status

`graphql-orm-ai` is at crate version `0.81.0` with AI schema module
`0.60.0`. It uses workspace `graphql-orm` `0.23.0`, backend-neutral
`graphql-orm-ai-tool-profiles` `0.6.0`, and external `agql-auth`
`graphql-orm-ai` is at crate version `0.84.0` with AI schema module
`0.63.0`. It uses workspace `graphql-orm` `0.23.0`, backend-neutral
`graphql-orm-ai-tool-profiles` `0.7.0`, and external `agql-auth`
`0.15.0` at `e841ffd382082ad7419be259fe957f949b956ff7`.

The active work order, dependencies, and exit gates are maintained in the
Expand Down
Loading
Loading