// The procedure contracts of the agent module: schemas and names, no handler and no port. // // A run is a bounded, person-requested plan of tool calls that the worker executes durably, as a // TanStack AI agent loop through the AI gateway (`@conusai-kit/agent`): the model calls the steps // in order and answers the goal from their results. The progress a page renders is the run row // and its step log, and the only writes a tool's result can cause are proposals a person decides. // The approval queue renders the EXACT stored proposed arguments — never a model's narration. import { answerProvenanceSchema } from "@conusai-kit/agent/client" import { procedure } from "@conusai-kit/core" import { z } from "zod" import { APPROVAL_REVIEWED, RUN_CANCELLED_ACTION } from "./constants.js" const inProject = { org: z.string(), workspace: z.string(), project: z.string() } /** One step of the plan a person admits: one explicitly published tool, exact arguments. */ export const planStepSchema = z .object({ // The key is also the step's tool name in the model loop: providers allow at most 64. key: z .string() .min(1) .max(64) .regex(/^[a-z][a-z0-9-]*$/, "lower-case letters, digits and dashes"), label: z.string().min(1).max(200), tool: z.string().min(1).max(200), args: z.record(z.string(), z.unknown()), /** A person approves this call (its tool and exact arguments) before it runs. */ needsApproval: z.boolean().optional(), }) .strict() /** What a run may spend; the engine clamps it by the hard safety ceilings. */ export const runBudgetSchema = z .object({ maxCostMicrocents: z.number().int().positive().optional(), maxSteps: z.number().int().positive().optional(), maxDurationSeconds: z.number().int().positive().optional(), }) .strict() export const runViewSchema = z.object({ runId: z.string(), goal: z.string(), status: z.enum(["queued", "running", "awaiting_approval", "completed", "failed", "cancelled", "uncertain"]), failureCode: z .enum(["tool_error", "budget_exhausted", "plan_refused", "approval_unavailable", "internal"]) .nullable(), failureDetail: z.string().nullable(), /** The plan as admitted: what the run will do, visible before it does it. */ plan: z.array( z.object({ key: z.string(), label: z.string(), tool: z.string(), args: z.record(z.string(), z.unknown()), needsApproval: z.boolean(), /** The step's state from the step log; steps not begun have no row yet. */ status: z.enum(["pending", "committed", "failed", "cancelled", "uncertain"]), attempts: z.number().int(), error: z.string().nullable(), }), ), usageCostMicrocents: z.number().int(), usageSteps: z.number().int(), /** Committed steps over the plan: the progress bar. */ percent: z.number(), cancelRequested: z.boolean(), /** A completed run's answer: AI-generated text (EU AI Act Art. 50), shown with its disclosure. */ answer: z.string().nullable(), /** * The answer's machine-readable marking (EU AI Act Art. 50(2)): the provenance the AI gateway * returned with it (model, provider, generation time). Null without an answer, or when the gateway * marked none; keep it with the answer wherever the answer is copied or exported. */ answerProvenance: answerProvenanceSchema.nullable(), actorId: z.string(), createdAt: z.string(), startedAt: z.string().nullable(), finishedAt: z.string().nullable(), }) export type RunView = z.output /** One parked approval: the exact stored proposed arguments, copied before they were proposed. */ export const approvalViewSchema = z.object({ runId: z.string(), stepKey: z.string(), goal: z.string(), tool: z.string(), /** The exact arguments the tool was called with; a reviewer reads THESE, never a narration. */ arguments: z.record(z.string(), z.unknown()), /** Where the persisted proposal is reviewed in the console, when the platform names one. */ reviewUrl: z.string().nullable(), createdAt: z.string(), }) export type ApprovalView = z.output export const runStart = procedure("agent.run.start") .kind("write") .scope("project") .permission("agent.run.start") .input( z.object({ ...inProject, goal: z.string().min(1).max(2_000), plan: z.array(planStepSchema).min(1).max(25), budget: runBudgetSchema.optional(), }), ) .output(runViewSchema) .contract() export const runStatus = procedure("agent.run.status") .kind("read") .scope("project") .permission("agent.run.read") .tool() .input(z.object({ ...inProject, runId: z.string() })) .output(runViewSchema) .contract() /** * The resume point of a run's event stream: the last delivered sequence number, or the SSE id * `agk1:{runId}:{seq}` the client last saw (`-1` or absent: from the start). */ export const runEventsAfterSchema = z.string().regex(/^(?:-1|\d{1,9}|agk1:[A-Za-z0-9_-]{1,128}:\d{1,9})$/) /** * The run's AG-UI 1.0 events as Server-Sent Events (`@conusai-kit/agent/agui`), for TanStack AI's * resumable client (`@conusai-kit/agent/client`). A reader sees exactly what `agent.run.status` * shows, under the same permission; the stream only reads the durable record. * * It is declared an `effect` because the kernel admits a streamed HTTP response (`download`) only * for that kind (kit-core registry); its handler reads and never writes. It counts against the * `default` bucket, like a read, because a watched run reconnects every stream window. */ export const runEvents = procedure("agent.run.events") .kind("effect") .scope("project") .permission("agent.run.read") .input(z.object({ ...inProject, runId: z.string(), after: runEventsAfterSchema.optional() })) .output(z.custom((value) => value instanceof Response, "a streamed event response")) .download() .rateLimit("default") .contract() export const runCancel = procedure("agent.run.cancel") .kind("write") .scope("project") .permission("agent.run.cancel") .input(z.object({ ...inProject, runId: z.string() })) .output(z.object({ runId: z.string(), cancelled: z.boolean() })) .audit(RUN_CANCELLED_ACTION) .contract() export const runList = procedure("agent.run.list") .kind("read") .scope("project") .permission("agent.run.read") .input(z.object({ ...inProject, cursor: z.string().optional(), limit: z.number().int().min(1).max(100).optional() })) .output(z.object({ runs: z.array(runViewSchema), cursor: z.string().optional(), hasMore: z.boolean() })) .contract() /** * The live collection's batch-get (contract C-05): the runs with these ids, under the list's permission. An * id of another tenant, of another project or of nothing is left out of the answer. */ export const runGetMany = procedure("agent.run.getMany") .kind("read") .scope("project") .permission("agent.run.read") .input(z.object({ ...inProject, ids: z.array(z.string().min(1).max(64)).min(1).max(100) })) .output(z.object({ items: z.array(runViewSchema) })) .contract() export const approvalList = procedure("agent.approval.list") .kind("read") .scope("project") .permission("agent.approval.review") .input(z.object({ ...inProject })) .output(z.object({ approvals: z.array(approvalViewSchema) })) .contract() export const approvalReview = procedure("agent.approval.review") .kind("write") .scope("project") .permission("agent.approval.review") .input( z.object({ ...inProject, runId: z.string(), stepKey: z.string(), decision: z.enum(["approve", "reject"]), reason: z.string().max(500).optional(), }), ) .output( z.object({ runId: z.string(), status: z.enum(["queued", "running", "awaiting_approval", "completed", "failed", "cancelled", "uncertain"]), }), ) .audit(APPROVAL_REVIEWED) .contract() export const AGENT_PROCEDURES = [ runStart, runStatus, runEvents, runCancel, runList, runGetMany, approvalList, approvalReview, ]