Generation Governance
The Fastify API shares backend selection, input moderation, quota reservation and submission identity across Dify and LangChain. Only a missing backend type defaults to Dify; explicitly unsupported types are rejected. Public capabilities come from the implemented backend registry, not arbitrary configuration claims. Explicitly opted-in LangChain applications also support the controlled native human-input profile described in AG-UI Integration. It reuses this governance boundary; it does not enable arbitrary AG-UI resume, Dify human input, token billing or a general policy platform.
Deployment And Request Contract
Apply 20261113150000_add_backend_conversation_runs.sql before
20261114160000_add_agent_submission_governance.sql, using the normal migration
process. The latter is required for both backends, including Dify-only use.
Then apply 20261114170000_protect_pending_agent_credentials_and_quota.sql
before deploying the matching API. It adds non-secret credential versions and
protects unresolved counted quota as well as reserved quota. Do not rewrite
previously applied migrations.
Deploy API and browser changes together; already cached pages must refresh.
Missing governance storage fails closed with 503 rather than invoking upstream.
Do not apply migrations without an isolated PostgreSQL upgrade/RLS/concurrency
check and the deployment’s database approval.
For negotiated Dify conversations, also apply
20261114180000_allow_trusted_dify_conversations.sql. Before deploying the
human-input implementation, apply 20261115090000_add_agent_human_input.sql
followed by 20261115100000_add_agent_resume_dispatch_fence.sql. Existing turn
rows retain their messages and become their own run roots. Retain the additive
schema on code rollback, and resolve pending interactions before reverting to
an implementation unable to display or resume them. See
AG-UI Integration for migration coverage and limitations.
- Dify generation requires
x-agentifui-submission-id: <UUID>. The five browser chat/workflow/completion helpers supply it without changing request payloads. Their final optional{ submissionId }argument supports an explicit replay. - LangChain continues to accept the existing body
submissionIdUUID. - A LangChain replay while registration has no run binding returns 409
AGENT_SUBMISSION_IN_PROGRESS; it never starts another execution. Once bound, an explicit replay with identical input returns the existing run and local conversation, including its current or saved result. Do not automatically resend a generation request after an ambiguous response. - New question: new UUID. Same operation/network replay: same UUID and input. Never automatically resend after an ambiguous failure. Reusing an ID with different input or target returns 409 without calling the service.
x-agentifui-root-task-ididentifies the common task. For Dify, a repeated submission returns 409, not a reconstructed stream. QueryGET /api/internal/apps/:appId/submissions/:submissionIdusing the canonical local application UUID and the same signed-in actor. It returns task/quota state, local run ID and a safe error code, never the submitted text or keys.- Dify native
userremains bound to the session actor. Unmigrated Dify conversations retain browser-managed saving; trusted negotiated AG-UI conversations use the server as their only writer. LangChain retains server-side history and save-only retry. The selected path must never enable both browser and server writers for the same conversation.
Existing Policy, Now Enforced Before Dispatch
No new administrator exemption is introduced. Public applications and private
applications available to administrators do not consume group quota, as in the
existing SQL. For group-only applications, use the first enabled membership
permission ordered by created_at, id, including for administrators. An
administrator without group membership may see the application under the old
visibility rule but cannot generate under its existing group-quota rule. Do not
fall through to another group when that first group’s quota is exhausted.
Unlimited group permissions still record usage. These are the existing
check_user_app_permission / increment_app_usage semantics, not new grants.
Input moderation retains the previous configuration behavior: disabled or
missing configuration skips moderation; configured service failure or an
unparseable result rejects with 503, unsafe input with 400. Both backends and
workflow generation aliases pass through this gate. Platform rejection consumes
no usage. Client-driven groups.incrementAppUsage now returns 409; only the
server acceptance path increments usage, preventing duplicate client charging.
Task State And Counting
| State | Quota | Computing slot / replay |
|---|---|---|
| registered | none | No slot; moderation/validation not finished |
| admitted | reserved for group, otherwise exempt | Slot held; one dispatch claim |
| submitting | unchanged | Slot held; native acceptance not yet proven |
| accepted | counted once for group | Slot held until native terminal evidence |
| finished | counted/exempt | Slot released; saving is independent |
| rejected | none/released/exempt | No slot; never dispatch this ID again |
| unknown | reserved or already counted | Slot retained; never auto-release/resend |
Admission locks the user and app across replicas, rechecks active identity,
current visibility/backend/target and group quota, and reserves atomically.
used_count + pending reservations must stay within a finite quota. Confirmation
increments used_count once, atomically with marking the task counted. Existing
active LangChain runs and active Dify tasks share computing limits without
counting a LangChain task twice. Existing per-user/per-app admission settings
apply to both. Saving/retrying history never constitutes a second generation.
Native task/message/workflow identifiers prove acceptance, not HTTP 200 alone.
Native completion or an accepted SSE error ends execution, including failures.
Known pre-acceptance HTTP 400/401/403/404/405/413/415/422 releases reservation.
An identified native refusal inside a successful blocking response also releases
it; bare error/code fields do not prove rejection. HTTP 429/5xx, broken streams,
paused workflows and uncertain dispatch stay unknown unless stronger native
evidence exists. This P2 path does not reconcile unknown tasks automatically;
operational resolution requires verifying native execution, never blind retry.
LangChain setup failures before the generation dispatch boundary are terminal errors and release both the governance reservation and the run’s computing slot. Creating an empty native thread is not evidence that generation started. When replay attaches governance to a pre-upgrade active LangChain run, its existing native execution remains the only execution. A terminal write from the older API also finishes that legacy association without charging historical usage again.
Only undispatched registered records expire opportunistically on a later
registration after 15 minutes; a user can have at most eight such pending
registrations. There is no background cleanup or timeout release of unknown.
Rejected records remain queryable and the same ID remains non-executable.
Storage And Extension Boundary
agent_submissions.root_task_id remains the task’s quota/idempotency identity;
nullable run_id links a trusted execution where the selected path uses one.
Unmigrated Dify proxy requests do not require local messages or backend_runs
before native dispatch; negotiated Dify and LangChain use trusted local runs.
The human-input migration adds root_run_id, run_kind and
backend_run_interactions without introducing another quota ledger. A response
creates a distinct resume run under the original root, without a new ordinary
user message or a second question charge.
Resume still requires current ownership, active identity, application access, applicable moderation, expiry checks and execution capacity. Preparation reserves a slot; a final transactional authorization check and CAS permit native dispatch. Only preparation proven not to have crossed that dispatch fence can reopen the interaction for a new explicit response. Submitting or unknown outcomes are queried, never blindly released or resent. A process crash leaving a reserved attempt requires diagnosis; there is no generic automatic recovery worker. See the human-input boundary for response replay and failure states.
Deleting a resolved quota permission nulls its historical task association; completed counted history does not permanently block group deletion. Reserved quota and counted quota with unresolved tasks cannot lose their permission row, be repointed, or have usage reset. Disabling a permission revokes access immediately without deleting its usage history. Active/unknown task deletion and target identity changes are guarded in the database; ordinary display edits and provider disabling remain available. User/app historical deletion keeps existing cascade behavior where other existing constraints allow it. The scoped trigger system context sees system-only tasks under strict RLS; it does not relax application-visible RLS policies.
Reserved permission deletion is rejected before foreign-key updates can wait on task confirmation. Target configuration edits use nonblocking admission-lock checks and reject competing edits as busy rather than reversing the admission lock order. Retry the administrative edit only after the active operation has been verified; this is not authorization to resend a generation request.
Before deployment, test the migration with an application role without SUPERUSER/BYPASSRLS: counted permission deletion, reserved deletion/reset rejection, active deletion protection, display/config changes, RLS isolation, two-client last-quota admission, acceptance replay, and failed-save replay. Unit tests with fake PostgreSQL clients do not establish these DB properties.
Effective default-key changes are rejected while affected tasks remain unresolved. Administrative credential edits and admission coordinate on the same application lock; admission rechecks a non-secret selected-key version to reject a key that became stale before registration or moderation finished. Key material is not part of the conversation target fingerprint, so completed tasks do not prevent normal same-identity rotation. Display or unused-key edits remain available. Disable the application/provider or revoke a compromised key upstream in an emergency; this does not claim to cancel an accepted task.
Descriptors distinguish backend support from effectiveActions.cancellation
for the current actor and application. Workflow cancellation remains admin-only;
LangChain cancellation remains unsupported. Both concurrency and usage limits
may return 429, but AGENT_CONCURRENCY_LIMIT is retryable after active work
settles, while AGENT_QUOTA_EXCEEDED requires allowance adjustment.
Adding Another Backend
The current registry supports Dify and LangChain only. A future integration must add an implemented backend, not an unknown type that falls back to Dify. Use the existing integrations as examples, without copying an entire chat UI or adding another authentication or quota system:
- Extend
apps/api/src/lib/agent-backends.tsand application configuration validation with the backend’s verified capabilities and persistence owner. Resolve credentials and application access on the server; never accept an arbitrary endpoint, native user or credential from a generation request. - Reuse moderation, submission registration and atomic admission. Define the target fingerprint, native acceptance and terminal evidence, uncertain dispatch handling and replay contract before enabling generation.
- Keep native threads, conversations, runs and control requests inside the adapter. Bind them to the current actor and local application/conversation; do not treat a vendor ID or thread memory as local history authorization.
- Feed the shared
ChatInputView,UserMessageandAssistantMessagethrough a thin controller. Keep global history as the entry point, designate exactly one history writer, and show only actions the integration actually supports. Use the implemented common AG-UI contract for migrated paths; legacy private stream events remain compatibility paths, not a second public interaction contract. - Add transport/contract tests, strict-role PostgreSQL ownership, quota and cross-instance replay tests, then real two-turn streaming, refresh/history, navigation and failure acceptance. An existing integration’s success does not establish another backend’s support for files, cancellation, human input or stream resumption.