ConusAI vendor v0.1.0-dev.20261027+9b1131fb
ci / gate (push) Waiting to run

This commit is contained in:
ConusAI vendor v0.1.0-dev.20261027+9b1131fb
2026-10-27 18:05:17 +00:00
commit 3102273ec7
538 changed files with 80203 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
# 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'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`); the platform owns dispatch policy, spend admission and the run ledger.
## 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`).
+23
View File
@@ -0,0 +1,23 @@
// The module's messages: en and lt declare the same keys, or a page in one language breaks.
import { buildRegistry } from "@conusai-kit/core"
import { tenancyModule } from "@conusai-kit/tenancy"
import { describe, expect, it } from "vitest"
import { agentModule } from "../index.js"
describe("the agent module's messages", () => {
const catalogues = buildRegistry([tenancyModule, agentModule]).catalogues
it("declares en and lt, and every en key resolves in lt", () => {
const en = catalogues.en?.agent ?? {}
const lt = catalogues.lt?.agent ?? {}
const flatten = (tree: Record<string, unknown>, prefix = ""): string[] =>
Object.entries(tree).flatMap(([key, value]) =>
typeof value === "string" ? [`${prefix}${key}`] : flatten(value as Record<string, unknown>, `${prefix}${key}.`),
)
const enKeys = flatten(en)
const ltKeys = new Set(flatten(lt))
expect(enKeys.length).toBeGreaterThan(0)
for (const key of enKeys) expect(ltKeys.has(key), `agent.${key} has no Lithuanian message`).toBe(true)
})
})
+38
View File
@@ -0,0 +1,38 @@
// What the generated tenant-isolation suite needs of this module: seeds and one fixture per
// tenant-scoped procedure, built from the OTHER tenant's ids.
import type { TransportCaller } from "@conusai-kit/core"
import type { IsolationFixture, IsolationTenant } from "@conusai-kit/testing"
export interface ModuleIsolation {
seed(
call: TransportCaller,
inProject: { org: string; workspace: string; project: string },
): Promise<Record<string, string>>
fixtures: Record<string, IsolationFixture>
}
const NOWHERE = "00000000-0000-0000-0000-000000000000"
const inProjectOf = (t: IsolationTenant) => ({ org: t.orgSlug, workspace: t.workspaceSlug, project: t.projectSlug })
const runOf = (t: IsolationTenant) => t.extra.agentRunId ?? NOWHERE
const PLAN = [{ key: "read", label: "Read", tool: "documents.read", args: {} }]
export const isolation: ModuleIsolation = {
async seed(call, inProject) {
const started = (await call.callByName("agent.run.start", { ...inProject, goal: "Seed run", plan: PLAN })) as {
runId: string
}
return { agentRunId: started.runId }
},
fixtures: {
"agent.run.start": { input: (t) => ({ ...inProjectOf(t), goal: "Intruder", plan: PLAN }) },
"agent.run.status": { input: (t) => ({ ...inProjectOf(t), runId: runOf(t) }) },
"agent.run.cancel": { input: (t) => ({ ...inProjectOf(t), runId: runOf(t) }) },
"agent.run.list": { input: (t) => inProjectOf(t) },
"agent.approval.list": { input: (t) => inProjectOf(t) },
"agent.approval.review": {
input: (t) => ({ ...inProjectOf(t), runId: runOf(t), stepKey: "read", decision: "approve" }),
},
},
}
+33
View File
@@ -0,0 +1,33 @@
// The module's migration is its own file and exactly `schemaStatements()` over the agent runtime's
// two entities (decision AT-D20).
import { readFileSync } from "node:fs"
import { AGENT_ENTITIES } from "@conusai-kit/agent"
import { schemaStatements } from "@conusai-kit/data-kysely"
import { describe, expect, it } from "vitest"
import { agentModule } from "../index.js"
function tablesOf(entities: Parameters<typeof schemaStatements>[0]): string {
const kernel = schemaStatements([]).length
return `${schemaStatements(entities)
.slice(kernel)
.map((statement) => `${statement};`)
.join("\n\n")}\n`
}
describe("the migration of the agent module", () => {
it("is its own file, exactly its two tables", async () => {
await expect(
`-- SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0\n${tablesOf(AGENT_ENTITIES)}`,
).toMatchFileSnapshot("../migrations/0001_agent.sql")
})
it("is declared by the manifest, and every table is row-level secured", () => {
expect(agentModule.migrations).toEqual({ postgres: "migrations" })
const sql = readFileSync(new URL("../migrations/0001_agent.sql", import.meta.url), "utf8")
for (const table of ["agent_run", "agent_step"]) {
expect(sql).toContain(`create table "${table}"`)
expect(sql).toContain(`alter table "${table}" force row level security`)
}
})
})
+126
View File
@@ -0,0 +1,126 @@
// The agent module's use cases on the in-memory adapter: starting lists and statuses, cancel's
// resume event, and the approval queue — which must surface the EXACT stored proposed arguments.
import { AGENT_ENTITIES } from "@conusai-kit/agent"
import type { TenantScope } from "@conusai-kit/core"
import { createMemoryDataSource } from "@conusai-kit/data"
import { describe, expect, it } from "vitest"
import { cancelRun, getRun, listApprovals, listRuns, reviewApproval, startRun } from "../core/runs.js"
const USER = "01900000-0000-7000-0000-000000000009"
const scope: TenantScope = {
level: "project",
actor: { userId: USER },
orgId: "01900000-0000-7000-0000-000000000001",
workspaceId: "01900000-0000-7000-0000-000000000002",
projectId: "01900000-0000-7000-0000-000000000003",
}
const makeDeps = () => ({ data: createMemoryDataSource({ entities: [...AGENT_ENTITIES] }) })
const PLAN = [
{ key: "read", label: "Read the document", tool: "documents.read", args: { fileId: "f-1" } },
{ key: "propose", label: "Propose lines", tool: "documents.extract", args: { fileId: "f-1", schema: "line" } },
]
describe("the agent module's run use cases", () => {
const deps = makeDeps()
it("starts a run queued, with the plan visible, and lists it", async () => {
const run = await startRun(deps, scope, { goal: "Review the contract", plan: PLAN })
expect(run.status).toBe("queued")
expect(run.goal).toBe("Review the contract")
expect(run.plan).toEqual([
{
key: "read",
label: "Read the document",
tool: "documents.read",
args: { fileId: "f-1" },
status: "pending",
attempts: 0,
error: null,
},
{
key: "propose",
label: "Propose lines",
tool: "documents.extract",
args: { fileId: "f-1", schema: "line" },
status: "pending",
attempts: 0,
error: null,
},
])
expect(run.percent).toBe(0)
const listed = await listRuns(deps, scope, {})
expect(listed.runs).toHaveLength(1)
expect((await getRun(deps, scope, run.runId)).runId).toBe(run.runId)
// The outbox event that enqueues the run job in the worker.
const pending = await deps.data.outbox.pending(10)
expect(pending.some((record) => record.name === "agent.runResumeRequested")).toBe(true)
})
it("refuses an over-long or repeated plan before anything is written", async () => {
const fresh = makeDeps()
const tooLong = await startRun(fresh, scope, {
goal: "x",
plan: Array.from({ length: 26 }, (_, at) => ({ key: `s${at}`, label: `Step ${at}`, tool: "t", args: {} })),
}).catch((error: unknown) => error)
expect((tooLong as { status?: number }).status).toBe(422)
const repeated = await startRun(fresh, scope, {
goal: "x",
plan: [
{ key: "same", label: "One", tool: "t", args: {} },
{ key: "same", label: "Two", tool: "t", args: {} },
],
}).catch((error: unknown) => error)
expect((repeated as { status?: number }).status).toBe(422)
expect((await listRuns(fresh, scope, {})).runs).toHaveLength(0)
})
it("cancel persists the request and re-enqueues the resume; a parked approval surfaces its exact arguments", async () => {
const fresh = makeDeps()
const run = await startRun(fresh, scope, { goal: "Approve-aware run", plan: PLAN })
// Simulate the engine parking an approval by writing the step row as the engine would.
const { dataRunStore } = await import("@conusai-kit/agent")
const store = dataRunStore(fresh.data, scope)
await store.beginStep(run.runId, { key: "read", index: 0, label: "Read the document" }, new Date().toISOString())
await store.parkStepApproval(
run.runId,
"read",
{
actionId: "act-7",
tool: "documents.read",
arguments: { fileId: "f-1", dryRun: false },
reviewUrl: "https://console/review?action=act-7",
},
new Date().toISOString(),
)
await fresh.data
.forScope(scope)
// biome-ignore lint/suspicious/noExplicitAny: test adjusts the run's status as the engine would.
.of(AGENT_ENTITIES[0] as any)
.update(run.runId, { status: "awaiting_approval" }, 1)
const queue = await listApprovals(fresh, scope)
expect(queue.approvals).toHaveLength(1)
expect(queue.approvals[0]).toMatchObject({
runId: run.runId,
stepKey: "read",
tool: "documents.read",
arguments: { fileId: "f-1", dryRun: false },
reviewUrl: "https://console/review?action=act-7",
})
const cancelled = await cancelRun(fresh, scope, run.runId)
expect(cancelled.cancelled).toBe(true)
expect((await getRun(fresh, scope, run.runId)).cancelRequested).toBe(true)
// The cancel re-published the resume event: a parked run notices the request only when resumed.
const pending = await fresh.data.outbox.pending(10)
expect(pending.filter((record) => record.name === "agent.runResumeRequested")).toHaveLength(2)
const review = await reviewApproval(fresh, scope, { runId: run.runId, stepKey: "read", decision: "approve" })
expect(["queued", "running", "awaiting_approval", "cancelled"]).toContain(review.status)
const bad = await reviewApproval(fresh, scope, { runId: run.runId, stepKey: "propose", decision: "approve" }).catch(
(error: unknown) => error,
)
expect((bad as { status?: number }).status).toBe(422)
})
})
+105
View File
@@ -0,0 +1,105 @@
// The approval queue's markup: the EXACT stored proposed arguments are rendered (never a
// narration), the Art. 50 disclosure rides with them, and the controls are named for a keyboard.
import { buildRegistry } from "@conusai-kit/core"
import { createI18n } from "@conusai-kit/i18n"
import { TransportProvider } from "@conusai-kit/ui"
import { renderToStaticMarkup } from "react-dom/server"
import { describe, expect, it } from "vitest"
import { appModules } from "../../index.js"
import type { ApprovalView } from "../contract/index.js"
import { ApprovalQueue } from "../ui/approval-queue.js"
import { RunControls } from "../ui/run-controls.js"
const catalogues = buildRegistry([...appModules]).catalogues
const i18n = createI18n("en", { en: { agent: catalogues.en?.agent ?? {} } })
const t = (key: string, values?: Record<string, string | number>) => i18n.message(`agent.${key}`, values)
const failed = "That did not work: {detail}"
const slugs = { org: "acme", workspace: "ws", project: "p" }
const inProvider = (node: React.ReactNode) =>
renderToStaticMarkup(
<TransportProvider call={async () => undefined} describe={() => "x"}>
{node}
</TransportProvider>,
)
const approval: ApprovalView = {
runId: "r1",
stepKey: "read",
goal: "Review the contract",
tool: "orders.create",
arguments: { total: 4200, currency: "EUR" },
reviewUrl: "https://console/tool-approvals?action=act-1",
createdAt: "2026-10-23T00:00:00.000Z",
}
const labels = {
heading: t("approvals.heading"),
empty: t("approvals.empty"),
proposed: t("approvals.proposed"),
review: t("approvals.review"),
approve: t("approvals.approve"),
reject: t("approvals.reject"),
decided: t("approvals.decided"),
disclosure: t("approvals.disclosure"),
failed,
}
describe("the approval queue", () => {
const html = inProvider(<ApprovalQueue slugs={slugs} approvals={[approval]} labels={labels} />)
it("renders the exact stored proposed arguments, pretty-printed, with the disclosure", () => {
// The exact stored arguments, HTML-escaped as any rendered text is.
expect(html).toContain("&quot;total&quot;: 4200")
expect(html).toContain("&quot;currency&quot;: &quot;EUR&quot;")
expect(html).toContain(t("approvals.disclosure"))
expect(html).toContain(t("approvals.proposed"))
})
it("names the decision controls and links the console review", () => {
expect(html).toContain(`>${t("approvals.approve")}<`)
expect(html).toContain(`>${t("approvals.reject")}<`)
expect(html).toContain('href="https://console/tool-approvals?action=act-1"')
})
it("announces an empty queue", () => {
const empty = inProvider(<ApprovalQueue slugs={slugs} approvals={[]} labels={labels} />)
expect(empty).toContain(t("approvals.empty"))
})
})
describe("the run controls", () => {
const labels = {
cancel: t("runs.cancel"),
cancelling: t("runs.cancelling"),
refresh: t("runs.refresh"),
failedHint: t("recovery.failedHint"),
cancelledHint: t("recovery.cancelledHint"),
uncertainHint: t("recovery.uncertainHint"),
budgetExhausted: t("recovery.budgetExhausted"),
approvalUnavailable: t("recovery.approvalUnavailable"),
toolError: t("recovery.toolError"),
planRefused: t("recovery.planRefused"),
internal: t("recovery.internal"),
}
it("offers cancel on an active run and recovery copy on an uncertain one", () => {
const active = inProvider(
<RunControls slugs={slugs} run={{ runId: "r1", status: "running", failureCode: null }} labels={labels} />,
)
expect(active).toContain(`>${t("runs.cancel")}<`)
const uncertain = inProvider(
<RunControls slugs={slugs} run={{ runId: "r1", status: "uncertain", failureCode: null }} labels={labels} />,
)
expect(uncertain).toContain(
t("recovery.uncertainHint")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll(String.fromCharCode(34), "&quot;")
.replaceAll(String.fromCharCode(39), "&#x27;"),
)
expect(uncertain).not.toContain(`>${t("runs.cancel")}<`)
})
})
+178
View File
@@ -0,0 +1,178 @@
// The agent module's adapters: the procedure handlers, the tools gateway over the registry client,
// the run job that drives the engine, and the subscriber that enqueues it.
import { AGENT_TOOLS, type AgentStep, dataRunStore, executeRun } from "@conusai-kit/agent"
import type { Container } from "@conusai-kit/core"
import { type EventContext, implement, internal, type JobDef, subscribe } from "@conusai-kit/core"
import { DATA_SOURCE, JOB_QUEUE } from "@conusai-kit/data"
import { createRegistryClient, type RegistryClient } from "@conusai-sdk/ai"
import { z } from "zod"
import {
approvalList,
approvalReview,
RUN_JOB,
runCancel,
runList,
runResumeRequested,
runStart,
runStatus,
} from "../contract/index.js"
import { AGENT_SCOPE, type AgentActionScope, type AgentDeps, type AgentScope } from "../core/ports.js"
import { cancelRun, getRun, listApprovals, listRuns, reviewApproval, startRun } from "../core/runs.js"
export function agentDeps(container: Container): AgentDeps {
return { data: container.resolve(DATA_SOURCE) }
}
export const runStartImpl = implement(runStart, ({ input, ctx }) =>
startRun(agentDeps(ctx.container), ctx.scope, {
goal: input.goal,
plan: input.plan,
...(input.budget === undefined ? {} : { budget: input.budget }),
}),
)
export const runStatusImpl = implement(runStatus, ({ input, ctx }) =>
getRun(agentDeps(ctx.container), ctx.scope, input.runId),
)
export const runCancelImpl = implement(runCancel, ({ input, ctx }) =>
cancelRun(agentDeps(ctx.container), ctx.scope, input.runId),
)
export const runListImpl = implement(runList, ({ input, ctx }) =>
listRuns(agentDeps(ctx.container), ctx.scope, {
...(input.cursor === undefined ? {} : { cursor: input.cursor }),
...(input.limit === undefined ? {} : { limit: input.limit }),
}),
)
export const approvalListImpl = implement(approvalList, ({ ctx }) => listApprovals(agentDeps(ctx.container), ctx.scope))
export const approvalReviewImpl = implement(approvalReview, ({ input, ctx }) =>
reviewApproval(agentDeps(ctx.container), ctx.scope, input),
)
export const AGENT_PROCEDURE_IMPLS = [
runStartImpl,
runStatusImpl,
runCancelImpl,
runListImpl,
approvalListImpl,
approvalReviewImpl,
]
// --- The tools gateway -----------------------------------------------------
/**
* The tools gateway over the organization's registry, with the app's scoped credentials. Each
* step's call carries the step's idempotency key, so a resumed step re-calls the same persisted
* action instead of creating a second one. When a write comes back `approval_required`, the exact
* stored proposed arguments are parked for a person; this adapter never approves.
*/
export function registryAgentTools(options: {
apiUrl: string
token: string
/** The organization's id the registry is addressed by. */
orgId: string
/** Fixed, or resolved per call: jobs of different tenants share the process binding. */
scope: AgentActionScope | (() => Promise<AgentActionScope>)
fetch?: typeof fetch
}): import("@conusai-kit/agent").AgentTools {
// Lazy: building the client validates the organization id, and a bare test container binds the
// tools without one until a call actually leaves the process.
let registry: RegistryClient | undefined
const client = (): RegistryClient => {
registry ??= createRegistryClient({
env: {
CONUSAI_API_URL: options.apiUrl,
CONUSAI_API_TOKEN: options.token,
CONUSAI_ORG_ID: options.orgId,
},
...(options.fetch === undefined ? {} : { fetch: options.fetch }),
})
return registry
}
const resolveScope =
typeof options.scope === "function" ? options.scope : () => Promise.resolve(options.scope as AgentActionScope)
return {
async call(name, args, callOptions) {
const result = await client().tools.call(name, args, { idempotencyKey: callOptions.idempotencyKey })
if (result.status === "completed") {
return {
status: "completed",
content: result.content,
// The registry call result does not carry metered usage yet (the metering slice owns
// that); steps and duration are tracked, and cost lands when the platform answers it.
usage: { costMicrocents: 0, steps: 1, tokens: 0 },
}
}
return {
status: "approval_required",
approval: {
actionId: result.request.actionId ?? null,
tool: name,
arguments: result.request.arguments,
reviewUrl: result.request.actionId
? client()
.tools.actions(await resolveScope())
.reviewUrl(result.request.actionId)
: null,
},
}
},
}
}
// --- The run job and its subscriber ----------------------------------------
const runPayload = z.object({ runId: z.string() })
type RunJobPayload = z.output<typeof runPayload>
export const runJob: JobDef<RunJobPayload> = {
payload: runPayload,
async handler(payload, ctx) {
const resolved = ctx.container.resolve(AGENT_SCOPE)
if (resolved.level !== "project")
throw internal(`an agent job runs in a project scope, not a ${resolved.level} one`)
const data = ctx.container.resolve(DATA_SOURCE)
const tools = ctx.container.resolve(AGENT_TOOLS)
const store = dataRunStore(data, resolved)
const record = await store.get(payload.runId)
if (!record) throw internal("the run this job belongs to is gone")
const plan: AgentStep[] = record.plan.map((planned) => ({
key: planned.key,
label: planned.label,
invoke: async (stepCtx) => {
const result = await tools.call(planned.tool, planned.args, {
signal: stepCtx.signal,
idempotencyKey: `${record.id}:${planned.key}`,
})
if (result.status === "completed") {
return {
ok: true as const,
usage: { costMicrocents: result.usage.costMicrocents, tokens: result.usage.tokens },
artifacts: [{ kind: "tool_result", ref: planned.tool }],
}
}
return { ok: false as const, awaitingApproval: result.approval }
},
}))
await executeRun({ store }, payload.runId, plan, { ...(ctx.signal === undefined ? {} : { signal: ctx.signal }) })
},
}
async function onRunResumeRequested(
event: { name: string; payload: { workspaceId: string; projectId: string; runId: string } },
ctx: EventContext,
): Promise<void> {
const ambient = ctx.container.resolve(AGENT_SCOPE)
if (ambient.level !== "project") throw internal("an agent event resolves to a project scope")
const scope: AgentScope = { ...ambient, workspaceId: event.payload.workspaceId, projectId: event.payload.projectId }
await ctx.container
.resolve(JOB_QUEUE)
.enqueue(scope, RUN_JOB, { runId: event.payload.runId }, { key: `agent.run.${event.payload.runId}` })
}
export const AGENT_JOBS = { [RUN_JOB]: runJob }
export const AGENT_SUBSCRIPTIONS = [subscribe(runResumeRequested, onRunResumeRequested)]
+9
View File
@@ -0,0 +1,9 @@
// The constants of the agent module: its job, events and audit actions.
/** The worker job that executes (or resumes) one run. */
export const RUN_JOB = "agent.run"
export const RUN_STARTED = "agent.run.started"
export const RUN_RESUME_REQUESTED = "agent.run.resume_requested"
export const RUN_CANCELLED_ACTION = "agent.run.cancelled"
export const APPROVAL_REVIEWED = "agent.approval.reviewed"
+23
View File
@@ -0,0 +1,23 @@
// The events of the agent module. A web write publishes the request; the module's own subscriber
// (in the worker's outbox relay) enqueues the run job with the run id as its singleton key, so a
// start, a cancel or a review resumes the one job.
import { defineEvent } from "@conusai-kit/core"
import { z } from "zod"
const inProject = {
orgId: z.string(),
workspaceId: z.string(),
projectId: z.string(),
}
/** A run was asked to start (or resume after a cancel/review decision). */
export const runResumeRequested = defineEvent(
"agent.runResumeRequested",
z.object({
...inProject,
runId: z.string(),
}),
)
export const AGENT_EVENTS = [runResumeRequested]
+25
View File
@@ -0,0 +1,25 @@
// The public API of the agent module: what a page, another module and the OpenAPI document may
// import.
export {
APPROVAL_REVIEWED,
RUN_CANCELLED_ACTION,
RUN_JOB,
RUN_RESUME_REQUESTED,
RUN_STARTED,
} from "./constants.js"
export { AGENT_EVENTS, runResumeRequested } from "./events.js"
export type { ApprovalView, RunView } from "./procedures.js"
export {
AGENT_PROCEDURES,
approvalList,
approvalReview,
approvalViewSchema,
planStepSchema,
runBudgetSchema,
runCancel,
runList,
runStart,
runStatus,
runViewSchema,
} from "./procedures.js"
+155
View File
@@ -0,0 +1,155 @@
// 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: 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 of them.
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({
key: z
.string()
.min(1)
.max(128)
.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()),
})
.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()),
/** 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(),
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()
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()
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, runCancel, runList, approvalList, approvalReview]
+39
View File
@@ -0,0 +1,39 @@
// The ports the agent module's use cases are written against, and the tokens the composition
// roots bind. The runtime itself (durable run record, step log, engine) is @conusai-kit/agent;
// this module is its template integration: procedures, job and pages.
import { type TenantScope, token } from "@conusai-kit/core"
import type { DataSource } from "@conusai-kit/data"
export type AgentScope = TenantScope & { workspaceId: string; projectId: string }
export interface AgentDeps {
readonly data: DataSource
}
/**
* The project a run's job executes in, with a system actor. The worker binds it per job from the
* enqueueing scope (the same arrangement as `DOCUMENTS_SCOPE`, decision AT-F6): a payload never
* names a tenant.
*/
export const AGENT_SCOPE = token<TenantScope>("agent.scope")
/** The scope the tools adapter needs to review persisted proposals in the console. */
export interface AgentActionScope {
readonly orgSlug: string
readonly workspaceSlug: string
readonly projectSlug: string
}
/** Resolves the slug scope the registry's tool-actions API is addressed by, from ids. */
export async function actionScopeOf(
data: import("@conusai-kit/data").DataSource,
scope: AgentScope,
): Promise<AgentActionScope> {
const { organizations, workspaces, projects } = await import("@conusai-kit/tenancy")
const org = await data.global(organizations).get(scope.orgId)
const workspace = await data.forScope(scope).of(workspaces).get(scope.workspaceId)
const project = await data.forScope(scope).of(projects).get(scope.projectId)
if (!org || !workspace || !project) throw new Error("the run's organization, workspace or project is gone")
return { orgSlug: org.slug, workspaceSlug: workspace.slug, projectSlug: project.slug }
}
+230
View File
@@ -0,0 +1,230 @@
// The agent module's use cases: start a run, read one, list them, cancel one, and the approval
// queue. Every write is the run row and the outbox event in one transaction; the worker's
// subscriber enqueues the run job, and the engine (@conusai-kit/agent) does the durable execution.
import { AGENT_ENTITIES, admitPlan, agentRuns, clampBudget, dataRunStore } from "@conusai-kit/agent"
import { notFound, validationFailed } from "@conusai-kit/core"
import { type ApprovalView, RUN_JOB, type RunView, runResumeRequested } from "../contract/index.js"
import type { AgentDeps, AgentScope } from "./ports.js"
const DEFAULT_BUDGET = { maxCostMicrocents: 100_000_000, maxSteps: 10, maxDurationSeconds: 600 }
const PAGE = 50
export const runViewOf = async (deps: AgentDeps, scope: AgentScope, run: { id: string }): Promise<RunView> => {
const store = dataRunStore(deps.data, scope)
const record = await store.get(run.id)
if (!record) throw notFound("no such run")
const steps = await store.listSteps(run.id)
const stepViews = record.plan.map((planned) => {
const logged = steps.find((candidate) => candidate.key === planned.key)
return {
key: planned.key,
label: planned.label,
tool: planned.tool,
args: planned.args,
status: logged?.status ?? ("pending" as const),
attempts: logged?.attempts ?? 0,
error: logged?.error ?? null,
}
})
return {
runId: record.id,
goal: record.goal,
status: record.status,
failureCode: record.failureCode,
failureDetail: record.failureDetail,
plan: stepViews,
usageCostMicrocents: record.usageCostMicrocents,
usageSteps: record.usageSteps,
percent: record.plan.length === 0 ? 0 : Math.min(record.usageSteps, record.plan.length) / record.plan.length,
cancelRequested: record.cancelRequestedAt !== null,
actorId: record.actorId,
createdAt: record.createdAt,
startedAt: record.startedAt,
finishedAt: record.finishedAt,
}
}
/**
* Starts a run: the plan is admitted against the hard ceilings before anything is written, the run
* row and the resume event land in one transaction, and the subscriber's enqueue (singleton on
* the run id) is what actually starts the work. Idempotency is the caller's idempotency key at
* the transport; a repeated start with the same key answers the first run.
*/
export async function startRun(
deps: AgentDeps,
scope: AgentScope,
input: {
goal: string
plan: { key: string; label: string; tool: string; args: Record<string, unknown> }[]
budget?: {
maxCostMicrocents?: number | undefined
maxSteps?: number | undefined
maxDurationSeconds?: number | undefined
}
},
): Promise<RunView> {
const budget = clampBudget({
maxCostMicrocents: input.budget?.maxCostMicrocents ?? DEFAULT_BUDGET.maxCostMicrocents,
maxSteps: input.budget?.maxSteps ?? DEFAULT_BUDGET.maxSteps,
maxDurationSeconds: input.budget?.maxDurationSeconds ?? DEFAULT_BUDGET.maxDurationSeconds,
})
// The plan is data here; the engine's steps come from these rows in the worker.
admitPlan(
input.plan.map((planned) => ({
key: planned.key,
label: planned.label,
invoke: () => Promise.resolve({ ok: true as const }),
})),
budget,
)
const run = await deps.data.unitOfWork.run(scope, async (tx) => {
const row = await tx.of(agentRuns).insert({
goal: input.goal,
plan: input.plan.map((planned) => ({
key: planned.key,
label: planned.label,
tool: planned.tool,
args: planned.args,
})),
status: "queued",
failureCode: null,
failureDetail: null,
actorId: scope.actor.userId,
budgetCostMicrocents: budget.maxCostMicrocents,
budgetSteps: budget.maxSteps,
budgetSeconds: budget.maxDurationSeconds,
usageCostMicrocents: 0,
usageSteps: 0,
cancelRequestedAt: null,
startedAt: null,
finishedAt: null,
})
await tx.publish(runResumeRequested, {
orgId: scope.orgId,
workspaceId: scope.workspaceId,
projectId: scope.projectId,
runId: row.id,
})
return row
})
return runViewOf(deps, scope, run)
}
export async function getRun(deps: AgentDeps, scope: AgentScope, runId: string): Promise<RunView> {
return runViewOf(deps, scope, { id: runId })
}
export async function listRuns(
deps: AgentDeps,
scope: AgentScope,
input: { cursor?: string | undefined; limit?: number | undefined },
): Promise<{ runs: RunView[]; cursor?: string; hasMore: boolean }> {
const store = dataRunStore(deps.data, scope, { pageSize: PAGE })
const page = await store.list({
...(input.cursor === undefined ? {} : { cursor: input.cursor }),
...(input.limit === undefined ? {} : { limit: input.limit }),
})
return {
runs: await Promise.all(page.runs.map((run) => runViewOf(deps, scope, run))),
...("cursor" in page ? { cursor: page.cursor } : {}),
hasMore: page.hasMore,
}
}
/** Cancels a run: the persisted request is honored between steps and inside the in-flight call. */
export async function cancelRun(
deps: AgentDeps,
scope: AgentScope,
runId: string,
): Promise<{ runId: string; cancelled: boolean }> {
const store = dataRunStore(deps.data, scope)
const record = await store.get(runId)
if (!record) throw notFound("no such run")
if (record.cancelRequestedAt !== null) return { runId, cancelled: true }
await store.requestCancel(runId, new Date().toISOString())
// A parked run only notices the request when it is resumed; enqueue the resume so the cancel
// lands even while the run waits in awaiting_approval.
await deps.data.unitOfWork.run(scope, async (tx) => {
await tx.publish(runResumeRequested, {
orgId: scope.orgId,
workspaceId: scope.workspaceId,
projectId: scope.projectId,
runId,
})
})
return { runId, cancelled: true }
}
/**
* The approval queue: every step that parked an approval, with the EXACT stored proposed
* arguments. Nothing here is a model's narration; the arguments are what the tool was called
* with, copied before they were proposed.
*/
export async function listApprovals(deps: AgentDeps, scope: AgentScope): Promise<{ approvals: ApprovalView[] }> {
const approvals: ApprovalView[] = []
let cursor: string | undefined
for (;;) {
const page = await deps.data
.forScope(scope)
.of(agentRuns)
.list({
order: [{ field: "createdAt", direction: "desc" }],
limit: PAGE,
...(cursor === undefined ? {} : { cursor }),
})
for (const run of page.items) {
const steps = await dataRunStore(deps.data, scope).listSteps(run.id)
for (const step of steps) {
if (step.approval === null) continue
approvals.push({
runId: run.id,
stepKey: step.key,
goal: run.goal,
tool: step.approval.tool,
arguments: step.approval.arguments,
reviewUrl: step.approval.reviewUrl,
createdAt: run.createdAt,
})
}
}
if (!page.hasMore) break
cursor = page.cursor
}
return { approvals }
}
/**
* Records a person's decision and resumes the run. Approval re-enters the parked step: the tools
* adapter re-calls with the step's idempotency key, and the platform executes the approved,
* persisted proposal exactly once. Rejection cancels the run: the proposal will not be executed.
*/
export async function reviewApproval(
deps: AgentDeps,
scope: AgentScope,
input: { runId: string; stepKey: string; decision: "approve" | "reject" },
): Promise<{ runId: string; status: RunView["status"] }> {
const store = dataRunStore(deps.data, scope)
const record = await store.get(input.runId)
if (!record) throw notFound("no such run")
const steps = await store.listSteps(input.runId)
const step = steps.find((candidate) => candidate.key === input.stepKey)
if (!step || step.approval === null) {
throw validationFailed([{ path: "stepKey", message: "this step has no parked approval" }], "nothing to review")
}
if (input.decision === "reject") {
await store.requestCancel(input.runId, new Date().toISOString())
}
await deps.data.unitOfWork.run(scope, async (tx) => {
await tx.publish(runResumeRequested, {
orgId: scope.orgId,
workspaceId: scope.workspaceId,
projectId: scope.projectId,
runId: input.runId,
})
})
return { runId: input.runId, status: (await store.get(input.runId))?.status ?? record.status }
}
export { AGENT_ENTITIES, RUN_JOB }
+73
View File
@@ -0,0 +1,73 @@
// English messages of the agent module. Every string a page of this module shows is a key here
// and in lt.ts; `__tests__/i18n.test.ts` fails when the two differ.
export const en = {
title: "Agent runs",
intro: "Bounded runs of published tools. A run proposes; a person approves. AI output is disclosed where it appears.",
runs: {
heading: "Runs",
empty: "No runs yet.",
goal: "Goal",
status: "Status",
statusQueued: "Queued",
statusRunning: "Running",
statusAwaitingApproval: "Awaiting approval",
statusCompleted: "Completed",
statusFailed: "Failed",
statusCancelled: "Cancelled",
statusUncertain: "Uncertain — needs reconciliation",
progress: "Progress",
percent: "{percent}%",
startedBy: "Started by",
startedAt: "Started",
finishedAt: "Finished",
usage: "{steps} steps, {cost} microcents",
failure: "Failure",
cancel: "Cancel run",
cancelling: "Cancelling",
refresh: "Refresh",
back: "All runs",
plan: "Plan",
step: "Step",
tool: "Tool",
stepPending: "Pending",
stepCommitted: "Done",
stepFailed: "Failed",
stepCancelled: "Cancelled",
stepUncertain: "Uncertain",
attempts: "{count} attempt(s)",
},
start: {
heading: "Start a run",
goalLabel: "Goal",
goalHint: "What this run should achieve, in a sentence a reviewer reads.",
planLabel: "Plan",
planHint: "One tool call per step, as JSON: {example}",
submit: "Start run",
starting: "Starting",
failed: "That did not work: {detail}",
},
approvals: {
heading: "Approvals waiting for you",
empty: "Nothing is waiting for a decision.",
proposed: "Proposed arguments",
review: "Review in console",
approve: "Approve",
reject: "Reject",
decided: "Decision recorded; the run resumes.",
disclosure: "AI-proposed change: a model produced these arguments. Approve only what you would type yourself.",
},
recovery: {
failedHint: "A failed run can be started again; the plan is visible above.",
cancelledHint: "This run was cancelled. Start a new run to do the work again.",
uncertainHint:
"This run timed out after an external effect. Check the affected service before deciding: reconcile by cancelling, or let the platform's reconciliation settle it.",
reconcileCancel: "Mark reconciled (cancel)",
budgetExhausted: "The run reached its budget.",
approvalUnavailable:
"The approval could not be completed here. Open the console to review the persisted proposal, then approve again.",
toolError: "A tool refused the call.",
planRefused: "The plan was refused at admission.",
internal: "The run failed internally.",
},
} as const
+2
View File
@@ -0,0 +1,2 @@
export { en } from "./en.js"
export { lt } from "./lt.js"
+79
View File
@@ -0,0 +1,79 @@
// Lietuviški agent modulio pranešimai. Kiekvienas puslapio tekstas yra raktas čia ir en.ts faile;
// `__tests__/i18n.test.ts` žlunga, kai jie skiriasi.
import type { en } from "./en.js"
type Messages<T> = { [K in keyof T]: T[K] extends string ? string : Messages<T[K]> }
export const lt: Messages<typeof en> = {
title: "Agentų vykdymai",
intro:
"Riboti paskelbtų įrankių vykdymai. Agentas pasiūlo; žmogus patvirtina. DI rezultatai atskleidžiami ten, kur jie rodomi.",
runs: {
heading: "Vykdymai",
empty: "Vykdymų dar nėra.",
goal: "Tikslas",
status: "Būsena",
statusQueued: "Laukia",
statusRunning: "Vyksta",
statusAwaitingApproval: "Laukia patvirtinimo",
statusCompleted: "Baigta",
statusFailed: "Nepavyko",
statusCancelled: "Atšaukta",
statusUncertain: "Neaišku — reikia suderinti",
progress: "Pažanga",
percent: "{percent} %",
startedBy: "Pradėjo",
startedAt: "Pradžia",
finishedAt: "Pabaiga",
usage: "{steps} žingsniai, {cost} mikrocentų",
failure: "Nepavyko",
cancel: "Atšaukti vykdymą",
cancelling: "Atšaukiama",
refresh: "Atnaujinti",
back: "Visi vykdymai",
plan: "Planas",
step: "Žingsnis",
tool: "Įrankis",
stepPending: "Laukia",
stepCommitted: "Atlikta",
stepFailed: "Nepavyko",
stepCancelled: "Atšaukta",
stepUncertain: "Neaišku",
attempts: "{count} bandymas(-ai)",
},
start: {
heading: "Pradėti vykdymą",
goalLabel: "Tikslas",
goalHint: "Ką šis vykdymas turi pasiekti — sakiniu, kurį perskaitys tikrintojas.",
planLabel: "Planas",
planHint: "Vienas įrankio iškvietimas žingsniui, kaip JSON: {example}",
submit: "Pradėti vykdymą",
starting: "Pradedama",
failed: "Nepavyko: {detail}",
},
approvals: {
heading: "Patvirtinimai, laukiantys jūsų",
empty: "Nieko nelaukia sprendimo.",
proposed: "Pasiūlyti argumentai",
review: "Peržiūrėti konsolėje",
approve: "Patvirtinti",
reject: "Atmesti",
decided: "Sprendimas įrašytas; vykdymas tęsiamas.",
disclosure:
"DI pasiūlytas pakeitimas: šiuos argumentus sugeneravo modelis. Patvirtinkite tik tai, ką patys įvestumėte.",
},
recovery: {
failedHint: "Nepavykusį vykdymą galima pradėti iš naujo; planas matomas aukščiau.",
cancelledHint: "Šis vykdymas atšauktas. Norėdami pakartoti darbą, pradėkite naują vykdymą.",
uncertainHint:
"Vykdymas baigėsi neaiškiai po išorinio poveikio. Prieš spręsdami patikrinkite paveiktą paslaugą: suderinkite atšaukdami arba leiskite platformos suderinimui užbaigti.",
reconcileCancel: "Pažymėti suderinta (atšaukti)",
budgetExhausted: "Vykdymas pasiekė biudžetą.",
approvalUnavailable:
"Patvirtinimo čia užbaigti nepavyko. Atidarykite konsolę ir peržiūrėkite įrašytą pasiūlymą, tada vėl patvirtinkite.",
toolError: "Įrankis atmetė iškvietimą.",
planRefused: "Planas atmestas priėmimo metu.",
internal: "Vykdymas nutrūko vidinės klaidos.",
},
}
+15
View File
@@ -0,0 +1,15 @@
// The agent module's public surface: the manifest and what the composition roots bind.
export {
AGENT_JOBS,
AGENT_PROCEDURE_IMPLS,
AGENT_SUBSCRIPTIONS,
registryAgentTools,
} from "./adapters/procedures.js"
export * from "./contract/index.js"
export type { AgentActionScope, AgentDeps, AgentScope } from "./core/ports.js"
export { AGENT_SCOPE, actionScopeOf } from "./core/ports.js"
export { cancelRun, getRun, listApprovals, listRuns, reviewApproval, startRun } from "./core/runs.js"
export { convexAgentModule } from "./module.convex.js"
export { agentModule } from "./module.js"
export { AGENT_MODULE_BASE } from "./module.shared.js"
+66
View File
@@ -0,0 +1,66 @@
-- SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
create table "agent_run" (
"id" uuid primary key,
"version" integer not null,
"created_at" timestamptz not null,
"updated_at" timestamptz not null,
"org_id" uuid not null,
"workspace_id" uuid not null,
"project_id" uuid not null,
"goal" text not null,
"plan" jsonb not null,
"status" text not null,
"failure_code" text,
"failure_detail" text,
"actor_id" text not null,
"budget_cost_microcents" integer not null,
"budget_steps" integer not null,
"budget_seconds" integer not null,
"usage_cost_microcents" integer not null,
"usage_steps" integer not null,
"cancel_requested_at" text,
"started_at" text,
"finished_at" text
);
create index "agent_run_org_id_workspace_id_project_id_status_idx" on "agent_run" ("org_id", "workspace_id", "project_id", "status");
create table "agent_step" (
"id" uuid primary key,
"version" integer not null,
"created_at" timestamptz not null,
"updated_at" timestamptz not null,
"org_id" uuid not null,
"workspace_id" uuid not null,
"project_id" uuid not null,
"run_id" uuid not null,
"key" text not null,
"index" integer not null,
"label" text not null,
"status" text not null,
"attempts" integer not null,
"usage_cost_microcents" integer not null,
"usage_tokens" integer not null,
"error" text,
"external_effect" boolean not null,
"artifacts" jsonb not null,
"approval" jsonb,
"started_at" text,
"finished_at" text
);
alter table "agent_step" add constraint "agent_step_org_id_workspace_id_project_id_run_id_key_key" unique nulls not distinct ("org_id", "workspace_id", "project_id", "run_id", "key");
create index "agent_step_org_id_workspace_id_project_id_run_id_idx" on "agent_step" ("org_id", "workspace_id", "project_id", "run_id");
alter table "agent_run" enable row level security;
alter table "agent_run" force row level security;
create policy "agent_run_tenant" on "agent_run" using ("org_id" = nullif(current_setting('app.org_id', true), '')::uuid) with check ("org_id" = nullif(current_setting('app.org_id', true), '')::uuid);
alter table "agent_step" enable row level security;
alter table "agent_step" force row level security;
create policy "agent_step_tenant" on "agent_step" using ("org_id" = nullif(current_setting('app.org_id', true), '')::uuid) with check ("org_id" = nullif(current_setting('app.org_id', true), '')::uuid);
+23
View File
@@ -0,0 +1,23 @@
// The Convex profile binding of the agent module, shipped fail-closed for a future wiring: the
// integration review kept the module Postgres-only because a run's tool calls leave the process
// (the registry client, the AI gateway) and the run job has no composed Convex Node action yet
// (see README.md). The job fails closed here so the work can only reach the platform SDKs through
// a composed Node action, never the Convex transaction dispatcher — the same arrangement as
// documents' OCR.
import { defineModule, internal, type JobDef } from "@conusai-kit/core"
import { z } from "zod"
import { RUN_JOB } from "./contract/index.js"
import { AGENT_MODULE_BASE } from "./module.shared.js"
const actionOnly: JobDef<{ runId: string }> = {
payload: z.object({ runId: z.string() }),
async handler() {
throw internal(`${RUN_JOB} must run through the Convex Node action boundary`)
},
}
export const convexAgentModule = defineModule({
...AGENT_MODULE_BASE,
jobs: { [RUN_JOB]: actionOnly },
})
+38
View File
@@ -0,0 +1,38 @@
// Shared, runtime-neutral agent manifest data. The normal app binds the worker implementation of
// agent.run; the Convex binding file (module.convex.ts) ships fail-closed for a future wiring —
// the module stays Postgres-only until the run job has its composed Node action (see README.md).
import { AGENT_ENTITIES } from "@conusai-kit/agent"
import type { ModuleManifest } from "@conusai-kit/core"
import { repositoryBindings } from "@conusai-kit/data"
import { AGENT_PROCEDURE_IMPLS } from "./adapters/procedures.js"
import { APPROVAL_REVIEWED, RUN_CANCELLED_ACTION } from "./contract/index.js"
import { en, lt } from "./i18n/index.js"
const tables = repositoryBindings(...AGENT_ENTITIES)
export const AGENT_MODULE_BASE: ModuleManifest = {
id: "agent",
version: "0.1.0",
kit: "^0.1.0",
requires: ["tenancy"],
permissions: {
// Watching runs and reading one is open to the project's readers.
"agent.run.read": "viewer",
// Starting, cancelling and reviewing are a person's acts on the project's behalf.
"agent.run.start": "editor",
"agent.run.cancel": "editor",
"agent.approval.review": "editor",
},
procedures: AGENT_PROCEDURE_IMPLS,
repositories: { postgres: tables },
migrations: { postgres: "migrations" },
// The module ships the Postgres profile; module.convex.ts is the fail-closed Convex binding,
// deliberately not registered until the run job has its composed Node action (see README.md).
profiles: ["postgres"],
audit: [RUN_CANCELLED_ACTION, APPROVAL_REVIEWED],
ui: {
nav: [{ scope: "project", label: "agent.title", href: "agent", permission: "agent.run.read" }],
},
i18n: { en, lt },
}
+14
View File
@@ -0,0 +1,14 @@
// The agent module's manifest (spec §4.1): the one place it registers anything. The module's own
// outbox subscriber enqueues the run job (singleton on the run id), so a request writes the run
// row and answers; the worker executes and resumes it.
import { defineModule } from "@conusai-kit/core"
import { AGENT_JOBS, AGENT_SUBSCRIPTIONS } from "./adapters/procedures.js"
import { AGENT_EVENTS } from "./contract/index.js"
import { AGENT_MODULE_BASE } from "./module.shared.js"
export const agentModule = defineModule({
...AGENT_MODULE_BASE,
jobs: AGENT_JOBS,
events: { publishes: [...AGENT_EVENTS], subscribes: AGENT_SUBSCRIPTIONS },
})
+83
View File
@@ -0,0 +1,83 @@
"use client"
// The approval queue: what a run's tool asked a person to decide. The panel renders the EXACT
// stored proposed arguments — a pretty-printed JSON of what the tool was called with — never a
// model's narration of them, and it carries the AI disclosure a reviewer reads before deciding.
import { AiDisclosureBadge, type ProjectSlugs, useTransport } from "@conusai-kit/ui"
import { type ReactElement, useState } from "react"
import type { ApprovalView } from "../contract/index.js"
export interface ApprovalQueueLabels {
readonly heading: string
readonly empty: string
readonly proposed: string
readonly review: string
readonly approve: string
readonly reject: string
readonly decided: string
readonly disclosure: string
readonly failed: string
}
export function ApprovalQueue({
slugs,
approvals,
labels,
}: {
slugs: ProjectSlugs
approvals: readonly ApprovalView[]
labels: ApprovalQueueLabels
}): ReactElement {
const { call, describe } = useTransport()
const [busy, setBusy] = useState<string | null>(null)
const [message, setMessage] = useState<string | null>(null)
const decide = async (approval: ApprovalView, decision: "approve" | "reject"): Promise<void> => {
setBusy(`${approval.runId}:${approval.stepKey}`)
setMessage(null)
try {
await call("agent.approval.review", { ...slugs, runId: approval.runId, stepKey: approval.stepKey, decision })
setMessage(labels.decided)
window.location.reload()
} catch (error) {
setMessage(labels.failed.replace("{detail}", describe(error)))
} finally {
setBusy(null)
}
}
return (
<section aria-labelledby="approvals-heading">
<h2 id="approvals-heading">{labels.heading}</h2>
{approvals.length === 0 ? <p>{labels.empty}</p> : null}
{approvals.map((approval) => (
<article key={`${approval.runId}:${approval.stepKey}`} aria-label={approval.tool}>
<h3>{approval.tool}</h3>
<p>{approval.goal}</p>
<AiDisclosureBadge label={labels.disclosure} />
<h4>{labels.proposed}</h4>
<pre>{JSON.stringify(approval.arguments, null, 2)}</pre>
<div>
<button type="button" disabled={busy !== null} onClick={() => void decide(approval, "approve")}>
{labels.approve}
</button>
<button type="button" disabled={busy !== null} onClick={() => void decide(approval, "reject")}>
{labels.reject}
</button>
{approval.reviewUrl !== null ? (
<a href={approval.reviewUrl} target="_blank" rel="noreferrer">
{labels.review}
</a>
) : null}
</div>
</article>
))}
{message !== null ? (
<p role="status" aria-live="polite">
{message}
</p>
) : null}
</section>
)
}
+4
View File
@@ -0,0 +1,4 @@
export { ApprovalQueue } from "./approval-queue.js"
export { agentPage, runStatusKey } from "./pages.js"
export { RunControls } from "./run-controls.js"
export { StartForm } from "./start-form.js"
+179
View File
@@ -0,0 +1,179 @@
// The agent module's page, mounted by the web shell under `/<org>/<workspace>/<project>/agent`.
//
// /agent the run list, the approval queue and the start form
// /agent/<runId> one run: the plan with per-step progress, the budget, the recovery controls
//
// Every read is a procedure through the same caller the API uses. Progress is rendered from the
// durable step log, so a refresh after a restart tells the truth; the progress bar is a real
// <progress> element, keyboard-operable controls only.
import type { Container, TransportCaller } from "@conusai-kit/core"
import type { I18n } from "@conusai-kit/i18n"
import type { ProjectPage } from "@conusai-kit/ui"
import type { ReactElement } from "react"
import { type ApprovalView, approvalList, type RunView, runList, runStatus } from "../contract/index.js"
import { ApprovalQueue } from "./approval-queue.js"
import { RunControls } from "./run-controls.js"
import { StartForm } from "./start-form.js"
export type AgentPage = ProjectPage<TransportCaller, I18n, Container>
export const runStatusKey: Record<RunView["status"], string> = {
queued: "runs.statusQueued",
running: "runs.statusRunning",
awaiting_approval: "runs.statusAwaitingApproval",
completed: "runs.statusCompleted",
failed: "runs.statusFailed",
cancelled: "runs.statusCancelled",
uncertain: "runs.statusUncertain",
}
const stepStatusKey: Record<RunView["plan"][number]["status"], string> = {
pending: "runs.stepPending",
committed: "runs.stepCommitted",
failed: "runs.stepFailed",
cancelled: "runs.stepCancelled",
uncertain: "runs.stepUncertain",
}
export const agentPage: AgentPage = async ({ slugs, segments, caller, i18n }): Promise<ReactElement> => {
const t = (key: string, values?: Record<string, string | number>) => i18n.message(`agent.${key}`, values)
const failed = i18n.message("app.failed")
const [runs, approvals] = await Promise.all([caller.call(runList, slugs), caller.call(approvalList, slugs)])
const startLabels = {
heading: t("start.heading"),
goalLabel: t("start.goalLabel"),
goalHint: t("start.goalHint"),
planLabel: t("start.planLabel"),
planHint: t("start.planHint"),
submit: t("start.submit"),
starting: t("start.starting"),
failed,
}
const approvalLabels = {
heading: t("approvals.heading"),
empty: t("approvals.empty"),
proposed: t("approvals.proposed"),
review: t("approvals.review"),
approve: t("approvals.approve"),
reject: t("approvals.reject"),
decided: t("approvals.decided"),
disclosure: t("approvals.disclosure"),
failed,
}
const controlsLabels = {
cancel: t("runs.cancel"),
cancelling: t("runs.cancelling"),
refresh: t("runs.refresh"),
failedHint: t("recovery.failedHint"),
cancelledHint: t("recovery.cancelledHint"),
uncertainHint: t("recovery.uncertainHint"),
budgetExhausted: t("recovery.budgetExhausted"),
approvalUnavailable: t("recovery.approvalUnavailable"),
toolError: t("recovery.toolError"),
planRefused: t("recovery.planRefused"),
internal: t("recovery.internal"),
}
if (segments.length === 0) {
return (
<>
<h1>{t("title")}</h1>
<p>{t("intro")}</p>
<ApprovalQueue slugs={slugs} approvals={approvals.approvals as ApprovalView[]} labels={approvalLabels} />
<StartForm slugs={slugs} labels={startLabels} />
<section aria-labelledby="runs-heading">
<h2 id="runs-heading">{t("runs.heading")}</h2>
{runs.runs.length === 0 ? <p>{t("runs.empty")}</p> : null}
<table>
<caption>{t("runs.heading")}</caption>
<thead>
<tr>
<th scope="col">{t("runs.goal")}</th>
<th scope="col">{t("runs.status")}</th>
<th scope="col">{t("runs.progress")}</th>
<th scope="col">{t("runs.startedBy")}</th>
</tr>
</thead>
<tbody>
{runs.runs.map((run) => (
<tr key={run.runId}>
<td>
<a href={`/${slugs.org}/${slugs.workspace}/${slugs.project}/agent/${run.runId}`}>{run.goal}</a>
</td>
<td>{t(runStatusKey[run.status])}</td>
<td>
<progress value={run.percent} max={1}>
{Math.round(run.percent * 100)}%
</progress>
</td>
<td>{run.actorId}</td>
</tr>
))}
</tbody>
</table>
</section>
</>
)
}
const [runId, ...rest] = segments
if (runId === undefined || rest.length > 0) throw notFoundPage()
const run = await caller.call(runStatus, { ...slugs, runId })
return (
<>
<p>
<a href={`/${slugs.org}/${slugs.workspace}/${slugs.project}/agent`}>{t("runs.back")}</a>
</p>
<h1>{run.goal}</h1>
<dl>
<dt>{t("runs.status")}</dt>
<dd>{t(runStatusKey[run.status])}</dd>
<dt>{t("runs.startedBy")}</dt>
<dd>{run.actorId}</dd>
<dt>{t("runs.usage")}</dt>
<dd>{t("runs.usage", { steps: run.usageSteps, cost: run.usageCostMicrocents })}</dd>
</dl>
<progress value={run.percent} max={1} aria-label={t("runs.progress")}>
{Math.round(run.percent * 100)}%
</progress>
<RunControls slugs={slugs} run={run} labels={controlsLabels} />
<section aria-labelledby="plan-heading">
<h2 id="plan-heading">{t("runs.plan")}</h2>
<table>
<caption>{t("runs.plan")}</caption>
<thead>
<tr>
<th scope="col">{t("runs.step")}</th>
<th scope="col">{t("runs.tool")}</th>
<th scope="col">{t("runs.status")}</th>
</tr>
</thead>
<tbody>
{run.plan.map((step) => (
<tr key={step.key}>
<td>{step.label}</td>
<td>
<code>{step.tool}</code>
</td>
<td>
{t(stepStatusKey[step.status])}
{step.attempts > 1 ? ` (${t("runs.attempts", { count: step.attempts })})` : ""}
{step.error !== null ? `: ${step.error}` : ""}
</td>
</tr>
))}
</tbody>
</table>
</section>
</>
)
}
const notFoundPage = () => {
// A segment that is not a run id is a page that does not exist.
return Object.assign(new Error("no such page"), { status: 404 })
}
+81
View File
@@ -0,0 +1,81 @@
"use client"
// One run's controls: cancel while it runs, and recovery from its terminal states. The recovery
// copy tells the person what the state means and what they can do — failed runs can be started
// again from the same plan, cancelled runs are simply over, and an uncertain run (a timeout after
// an external effect) is reconciled by a person, not retried blindly.
import { type ProjectSlugs, useTransport } from "@conusai-kit/ui"
import { type ReactElement, useState } from "react"
import type { RunView } from "../contract/index.js"
export interface RunControlsLabels {
readonly cancel: string
readonly cancelling: string
readonly refresh: string
readonly failedHint: string
readonly cancelledHint: string
readonly uncertainHint: string
readonly budgetExhausted: string
readonly approvalUnavailable: string
readonly toolError: string
readonly planRefused: string
readonly internal: string
}
export function RunControls({
slugs,
run,
labels,
}: {
slugs: ProjectSlugs
run: Pick<RunView, "runId" | "status" | "failureCode">
labels: RunControlsLabels
}): ReactElement {
const { call } = useTransport()
const [busy, setBusy] = useState(false)
const cancel = async (): Promise<void> => {
setBusy(true)
try {
await call("agent.run.cancel", { ...slugs, runId: run.runId })
window.location.reload()
} finally {
setBusy(false)
}
}
const active = run.status === "queued" || run.status === "running" || run.status === "awaiting_approval"
const failureText = (code: RunView["failureCode"]): string => {
switch (code) {
case "budget_exhausted":
return labels.budgetExhausted
case "approval_unavailable":
return labels.approvalUnavailable
case "tool_error":
return labels.toolError
case "plan_refused":
return labels.planRefused
default:
return labels.internal
}
}
return (
<div>
{active ? (
<button type="button" disabled={busy} onClick={() => void cancel()}>
{busy ? labels.cancelling : labels.cancel}
</button>
) : null}
{run.status === "failed" ? (
<p role="alert">
{failureText(run.failureCode)} {labels.failedHint}
</p>
) : null}
{run.status === "cancelled" ? <p role="status">{labels.cancelledHint}</p> : null}
{run.status === "uncertain" ? <p role="alert">{labels.uncertainHint}</p> : null}
<a href="?">{labels.refresh}</a>
</div>
)
}
+76
View File
@@ -0,0 +1,76 @@
"use client"
// Starting a run: the goal and the plan as JSON. The plan is what the run will do, shown to the
// person before they start it, and admitted by the server against the hard ceilings.
import { type ProjectSlugs, useTransport } from "@conusai-kit/ui"
import { type FormEvent, type ReactElement, useId, useState } from "react"
export interface StartFormLabels {
readonly heading: string
readonly goalLabel: string
readonly goalHint: string
readonly planLabel: string
readonly planHint: string
readonly submit: string
readonly starting: string
readonly failed: string
}
type State = { kind: "idle" } | { kind: "busy" } | { kind: "failed"; detail: string }
const EXAMPLE = JSON.stringify([
{ key: "read", label: "Read the document", tool: "documents.read", args: { fileId: "…" } },
])
export function StartForm({ slugs, labels }: { slugs: ProjectSlugs; labels: StartFormLabels }): ReactElement {
const { call, describe } = useTransport()
const goalId = useId()
const planId = useId()
const [state, setState] = useState<State>({ kind: "idle" })
const start = async (event: FormEvent<HTMLFormElement>): Promise<void> => {
event.preventDefault()
const form = new FormData(event.currentTarget)
const goal = String(form.get("goal") ?? "").trim()
let plan: unknown
try {
plan = JSON.parse(String(form.get("plan") ?? ""))
} catch {
setState({ kind: "failed", detail: "invalid JSON" })
return
}
setState({ kind: "busy" })
try {
await call("agent.run.start", { ...slugs, goal, plan })
window.location.reload()
} catch (error) {
setState({ kind: "failed", detail: describe(error) })
}
}
return (
<section aria-labelledby={`${goalId}-heading`}>
<h2 id={`${goalId}-heading`}>{labels.heading}</h2>
<form onSubmit={(event) => void start(event)}>
<label htmlFor={goalId}>{labels.goalLabel}</label>
<input id={goalId} name="goal" required maxLength={2_000} aria-describedby={`${goalId}-hint`} />
<p id={`${goalId}-hint`}>{labels.goalHint}</p>
<label htmlFor={planId}>{labels.planLabel}</label>
<textarea
id={planId}
name="plan"
required
rows={6}
defaultValue={EXAMPLE}
aria-describedby={`${planId}-hint`}
/>
<p id={`${planId}-hint`}>{labels.planHint.replace("{example}", EXAMPLE)}</p>
<button type="submit" disabled={state.kind === "busy"}>
{state.kind === "busy" ? labels.starting : labels.submit}
</button>
</form>
{state.kind === "failed" ? <p role="alert">{labels.failed.replace("{detail}", state.detail)}</p> : null}
</section>
)
}