|
|
|
@@ -0,0 +1,215 @@
|
|
|
|
|
// 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<typeof runViewSchema>
|
|
|
|
|
|
|
|
|
|
/** 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<typeof approvalViewSchema>
|
|
|
|
|
|
|
|
|
|
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<Response>((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,
|
|
|
|
|
]
|