@@ -0,0 +1,146 @@
|
||||
# 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](../../../../sdks/node/packages/kit-jobs-pgboss/README.md#durable-claim-guards).
|
||||
Reference in New Issue
Block a user