Files
template-app/modules/agent
2026-11-14 09:56:45 +00:00
..
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00

agent

The bounded agent runtime's app integration: durable runs with step state, the approval queue a person reviews, and the run controls. The run job drives @conusai-kit/agent's loop on TanStack AI's server chat(): the model (AGENT_MODEL, the AI gateway's conusaiText adapter) calls the admitted steps in order and answers the goal from their results, and the conversation is kept in agent_thread through @tanstack/ai-persistence. The run's tool calls go through the organization's tool registry with the app's scoped deployment credentials (apps/worker/src/ports.ts binds AGENT_TOOLS and AGENT_MODEL); the platform owns dispatch policy, metering, spend admission and the run ledger.

A plan step with "needsApproval": true is approved by a person before it runs: the run parks in awaiting_approval with the step's exact tool and arguments in the approval queue, and agent.approval.review records the decision on the step (approve resumes the run, reject cancels it; a step is decided once). A completed run's answer is AI-generated text: agent.run.status and the run page show it with the Art. 50 disclosure (AiDisclosureBadge).

The grant a run's tools need

A run's tool calls go to the organization's MCP endpoint with the app's deployment token, and that endpoint admits a deployment token only with ai_tools:call. The scope is not a default and * never holds it, so conusai.json requests it, in the root file and in every profile copy that has a token block (profiles/convex, profiles/mariadb; profiles/mongodb has none). This module is Postgres-only, so nothing calls the endpoint on those profiles and their request can stay undecided.

A request is not a grant. A deployment that asks for the scope runs without it until an organization owner or admin approves it, in the project's Settings, Token permissions screen or with conusai tokens approve --id <token_id> ai_tools:call; anyone else gets a 403, and the approval is in the audit log. Approve it before starting runs.

Without the grant a run's tool call is refused with a 403: the run ends failed with the failure code tool_error and the reason in failureDetail and on its step, and the queue does not run it again. A policy denial ends the run the same way, and so does a tool that answers an error: its step is not committed, and none of the tool's text is kept on the run. A failed or cancelled run is final, and a later approval does not resume it: start a new run.

A failure that can pass later is left to the queue: a 429 or a 5xx, a network failure, busy compute, a catalog that is still indexing, or a runtime-unavailable denial. The next delivery resumes the step on its idempotency key. When the queue proves this is the final attempt (ctx.finalAttempt), an unhandled failure ends the run as failed/internal with a fixed exhaustion message. Worker shutdown preserves the recoverable state. Terminal replay never calls the model or tools again.

The job reads the initiating member from the stored run's actorId, and checks real current app membership and agent.run.start through TENANCY.authorizeIn at entry, before each model turn, and before each tool effect, including approval resumption. A removal or permission revocation ends the run with a fixed failure message; a request's cached permission never authorizes worker execution. App tenant UUIDs remain separate from Cloud deployment authority. The scoped run store matches org, workspace and project; wrong-scope lookups cannot execute another tenant's run.

The supported Postgres worker exposes its actual envelope and claim through JobContext.execution. Agent verifies scope and attribution and probes the data source's real transaction hook before any run/provider work. A raw factory without that hook fails closed. Every scoped repository operation and UOW locks the exact current claim in its own transaction until commit/rollback; this lock survives loss of the separate guard session. Owned-client error/end events abort a sticky signal; same-session health checks surround authorization, model/tool/resume, store and error boundaries. Claim loss remains recoverable queue interruption and never becomes terminal business failure.

The tools run with the token's own permissions, so a tool that needs another scope needs that scope requested and approved as well. A step calls only the tool and arguments a person admitted in the plan.

Tool cost is not reported yet. The registry's call result carries no metered usage, so a step records a cost of 0 (registryAgentTools): the budget guard counts a run's steps and time, and no tool's cost. Model calls are metered by the AI gateway, under the organization's spend cap.

Procedures

Procedure Kind Permission What it answers
agent.run.start write agent.run.start a queued run with its admitted plan
agent.run.status read (MCP tool) agent.run.read the run, its step states and progress
agent.run.events effect, GET stream agent.run.read the run's AG-UI events as Server-Sent Events
agent.run.list read agent.run.read the project's runs, a page at a time
agent.run.cancel write agent.run.cancel the persisted cancel request
agent.approval.list read agent.approval.review the parked approvals with their exact arguments
agent.approval.review write agent.approval.review the run after a person's decision

agent.run.events (GET /api/v1/agent.run.events?org=…&workspace=…&project=…&runId=…[&after=…]) answers text/event-stream from @conusai-kit/agent/agui's projection of the durable record: what agent.run.status shows the same reader, and nothing more (no tool results, artifacts or an approval's proposed arguments). after is the last delivered sequence number or SSE id (agk1:{runId}:{seq}); the stream replays strictly after it, re-reads the record every second until the run's terminal event, and closes a 25-second window with an untagged notice so the client reconnects and is authorized again. Another project's or organization's run is not found, exactly as agent.run.status answers, and an offset of another run is refused (422). The browser client is @conusai-kit/agent/client (agentRunConnection, watchAgentRun): the run page server-renders agent.run.status as the seed of a client island (ui/live-run.tsx) that follows this stream, so step states, a parked approval and the answer appear without a reload and nothing polls the run.

The run list and the approval queue are live too. The module declares the live collection agent.run (contract/collections.ts: table agent_run, key runId, batch-get agent.run.getMany under agent.run.read, at most 100 ids). The list keeps its server-rendered first page as the seed of a liveCollection (ui/live-runs.tsx), so a run another member starts appears and each run's status and progress move as its steps commit. The approval queue is a useRead of agent.approval.list read again on every agent.run change (a run that parks an approval or leaves one changes its status) and after the reviewer's own decision, which changes a step and not the run. Starting a run is a useWrite that resolves once the live stream has seen the new row; no page re-runs its loader after a write (test/live-writes.test.ts).

The procedure is declared effect rather than read only because the kernel admits a streamed HTTP response (.download()) for that kind alone; its handler reads and never writes, and it counts against the default rate-limit bucket like a read. Being an effect, a session caller's token is re-checked as high-risk on every connection.

Why the module is Postgres-only (no Convex profile yet)

module.convex.ts ships a fail-closed Convex binding for a future wiring, but the module is not registered in the Convex profile (convex/lib/runtime.ts), and the manifest stays profiles: ["postgres"]. The integration review (2026-10-23) kept it Postgres-only because the run cannot execute on the Convex runtime today:

  1. The agent.run job leaves the process on every step — the tool registry, the AI gateway, and whatever the managed tools call. Convex queries/mutations cannot perform network I/O; the Convex binding fails closed (… must run through the Convex Node action boundary) and, unlike documents' documentsActions:process, no composed Node action exists for the run job yet.
  2. A run's steps must recheck policy and meter through the platform between actions; that orchestration lives in the worker, and a partial registration would let a project start runs that always dispatch into the fence.

The read/write procedures (run list/get, cancel, approval review) would technically run in Convex transactions, but registering the module for metadata alone — while every started run fails — is a registered-broken feature, not a fail-closed gate. Wiring the profile means composing the run job's Node action first (mirror convex/documentsActions.ts).

S08 hardening rechecks current local membership and agent.run.read before every stream poll, without the request's permission memo. Revocation closes the body; reconnect is a fresh authorized request. Approval interrupts compare the resumed tool and canonical arguments with the stored review; drift fails durably, rejection cancels without a second cancel write, and an awaiting run expires on observation at its admitted duration. These local controls do not establish live model, registry-tool or sandbox readiness. See the S08 evidence boundaries.

Durable Agent jobs require JobContext.execution from the supported Postgres worker factory (createPostgresWorker in @conusai-kit/host/worker). The actual pg-boss envelope must match AGENT_SCOPE, the job name, real system actor and immutable initiating run owner. The handler holds the adapter guard through all awaited work while its session is alive; domain transactions also hold their own compatible claim lock until commit/rollback. Missing or lost claims refuse separately from business failure or retry exhaustion. The guard uses its own bounded pool; shutdown aborts and drains callbacks without releasing live sessions early. Raw queue constructors without the factory's exact timestamp evidence, BullMQ and Convex supply no accepted Agent claim guarantee. Current membership checks still repeat between model/tool calls and approved resumes.

External tool keys remain ${record.id}:${planned.key}. Providers must enforce idempotency/fencing and cooperate with cancellation; an already-started action cannot be undone by an abort signal. The supported domain fence and external-provider guarantees are separate. See the adapter contract.