MainSDK
Search⌘K
Interfaces

AI and agents

App agents

agents

Give each agent your app runs an identity in the audit log, a budget, a person's approval for risky steps and a kill switch.

Building for 0.3ships as a experimental APIPhase 1 · Core, public betaSpec agent-governanceFree in your app, Apache-2.0

Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.

Stability The promise an interface ships with.

  • Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
  • Beta Usable in production; may change before it becomes stable.
  • Experimental May change or go away; needs an explicit opt-in in main.yaml.
Stability levels
Updated 2026-10-28

agents governs the agents your app runs, not the coding agent that builds it. Each gets an identity in the audit log, a budget, a person's approval for risky steps and a kill switch.

At a glance

agents:
  support-bot:
    budget: { spend: $5/day, calls: 500/day }
    approve: { risk: destructive, spend-over: $1 }
    enabled: true

Calls

  • agents.reserve(call)
  • agents.settle(hold, actual)
  • agents.release(hold)
  • agents.record(call)
  • agents.check(call)
  • agents.ask(req)
  • agents.remaining(name)
  • agents.counters()
  • agents.attach(budget)
  • agents.admit(call)
  • agents.charge(call)
  • agents.run(call, fn)
  • agents.approve(approvalId, by)
  • agents.isApproved(approvalId)
  • agents.approvals.list()
  • agents.get(name)
  • agents.reload(config)
  • agents.close(approvalId, why)
  • agents.decline(approvalId, by)
  • agents.isDeclined(approvalId)
  • agents.isPending(approvalId)
  • agents.onApprove(fn)
  • agents.remainingSpend(name)
  • agents.request(req)
  • agents.pendingApprovals()
  • agents.onBudgetLow(fn)

Guarantees

Whatever the driver.

  • Every step is audited with the agent as actor and the person it acts for
  • A budget is checked before the call; at the budget the call is refused
  • A destructive or costly step waits for a person, and an agent never approves its own
  • enabled false stops an agent on its next call, with no deploy
  • Works offline from local config

Drivers

1 drivers. Select one to see its line in main.yaml.

main.yamlBuilt in (default)
agents:
  driver: builtin

Change this one line to swap providers; your calls stay the same.Built-in and local drivers integration

Codes

CodeWhere it appearsMeaning
MAIN-AGENT-0001RuntimeError responses and logsA call names an agent that is not under agents
MAIN-AGENT-0002RuntimeError responses and logsThe agent is over its budget; the call is refused and nothing reaches the provider
MAIN-AGENT-0003RuntimeError responses and logsAn agent tried to approve its own step
MAIN-AGENT-0004RuntimeError responses and logsThe agent is switched off (enabled: false)
MAIN-AGENT-0006RuntimeError responses and logsThe store that counts budgets did not answer, and the agent has a budget, so the call is refused (503, retryable)
MAIN-AGENT-0007RuntimeError responses and logsA sync budget call (admit, charge, request) on a store other instances share
MAIN-AGENT-0005RuntimeError responses and logsAn approval id that isn't pending: unknown, already approved or expired

Features

Nothing here has shipped yet. Each feature lands in its phase; the spec ticket builds it.

FeaturePhaseMilestoneSpec ticket
Agent and person in every audit eventPhase 10.3agent-governance#actor
Spend and call budgets per agentPhase 10.3agent-governance#budgets
Approval above a risk or spend thresholdPhase 10.3agent-governance#approval
Calls bound to a named agentPhase 10.3agent-governance#bind-spend
Kill switch without a deployPhase 10.3agent-governance#kill

Quick start

main.yaml
agents:
  support-bot:
    budget: { spend: $5/day, calls: 500/day }
    approve: { risk: destructive, spend-over: $1 }
    enabled: true
lib/refund.ts
import { agents } from "mainsdk";

declare function refundOrder(input: { orderId: string }): Promise<unknown>;   // your own action

export async function refund(user: { id: string }, input: { orderId: string }) {
  const out = await agents.run(
    { agent: "support-bot", action: "orders.refund", user: user.id, spend: "$5.00", risk: "destructive" },
    () => refundOrder(input),
  );
  if (out.status === "done") return Response.json(out.value);
  if (out.status === "pending") return Response.json({ approval: out.approval.id }, { status: 202 });
  throw new Error("unreachable: run returns done or pending");
}

Most apps never call agents directly: ai.generate, pay.agent and actions reached over MCP name the agent and pass through it. The walk-through is in agents with an identity, a budget and approvals.

Configuration

KeyWhat it does
agents.<name>.budget.spendThe most the agent may spend in the window. Checked before each call
agents.<name>.budget.callsThe most calls in the window
agents.<name>.approve.riskA step at this risk waits for a person
agents.<name>.approve.spend-overA step that moves more than this waits for a person
agents.<name>.enabledfalse stops the agent on its next call
agents.<name>.runHow ai.run treats the agent: max-steps, timeout, handoff, delegate. See AI calls

What you get

agents governs the agents your app runs, as opposed to the coding agent that builds it. Every call that names an agent is charged to that agent's budget before it runs, held for a person's approval above a risk or spend threshold, and audited with the agent as actor and the user as on_behalf_of. Setting enabled: false stops an agent on its next call, with no deploy. It works offline and is free. Spec agent-governance is experimental, so the shape may change. The calls on this page are built and tested.

Examples

Reserve, settle and release a call

agents.reserve(call) checks the kill switch, holds the call's estimate and one call on each budget the agent has, and returns { status: "run", hold } or a pending approval. The hold is atomic across instances when runtime.store is redis or db. agents.settle(hold, actual) changes it to the real cost; agents.release(hold) gives it back when the call failed. agents.run(call, fn) does all three around your function and returns { status: "done", value } or the pending approval. When the store is down, an agent with a budget fails closed with MAIN-AGENT-0006 and an agent with none keeps running. agents.admit and agents.charge are deprecated sync forms for the local store, removed in 1.0.

lib/agent-step.ts
import { agents } from "mainsdk";

declare function grantCredits(): Promise<void>;   // your own code

const call = { agent: "support-bot", action: "credits.grant", user: "user_42", spend: "$0.50" };
const a = await agents.reserve(call);
if (a.status === "run") {
  try {
    await grantCredits();
    await agents.settle(a.hold);
  } catch (err) {
    await agents.release(a.hold);
    throw err;
  }
}

Approve a waiting step

A person approves. An agent approving its own step fails with MAIN-AGENT-0003.

app/api/approvals/route.ts
import { agents, auth } from "mainsdk";

export async function GET() {
  return Response.json(agents.pendingApprovals());   // oldest first
}

export async function POST(req: Request) {
  const session = await auth.require(req, { role: "admin" });
  const { id } = (await req.json()) as { id: string };
  const call = agents.approve(id, { kind: "user", id: session.user });
  return Response.json({ approved: agents.isApproved(id), call });
}

Decline a waiting step

agents.decline(id, { kind: "user", id }) closes the step without running it; agents.isDeclined(id) reports it. An agent declining fails with MAIN-AGENT-0003. agents.onApprove(fn) calls fn when a step is approved and returns an unsubscribe function.

Read and reload an agent's config

lib/agent-config.ts
import { agents } from "mainsdk";

const bot = agents.get("support-bot");   // MAIN-AGENT-0001 for an unknown name
agents.reload({ "support-bot": { ...bot, enabled: false } });   // takes effect on the next call

Check, record and ask without holding a budget

  • agents.check(call) tests the kill switch and budgets across instances and holds nothing. It returns run now, or a pending approval. Use it for a plan made now and charged later.
  • agents.record(call) charges spend that already happened, such as a child run's cost. It adds the spend and one call to the budgets and writes the allow audit event. A store that does not answer is logged, not thrown.
  • agents.ask(req) puts a step in the approval queue and checks the named agent's kill switch and budget first. agents.request(req) is the deprecated sync form for the local store, removed in 1.0.

Read budgets and watch them

  • agents.remaining(name) reads the spend the agent may still use now from the shared store. It returns undefined with no spend budget or no answer.
  • agents.remainingSpend(name) is the sync form: micro-dollars this instance last saw. It sizes a plan; reserve enforces the budget.
  • agents.onBudgetLow(fn) calls fn(agent, used, window) once per window when an agent passes 80% of a budget. It returns a stop function.
  • agents.counters() returns the budget counters agents use, so ai and the MCP client can count in the same ones. agents.attach(budget) replaces them.

Inspect and close approvals

agents.isPending(approvalId) is true while an approval waits for a person. agents.close(approvalId, why) removes one nobody will answer, with why set to timeout or cancelled, and records it.

Errors and checks

CodeWhenFix
MAIN-AGENT-0001The call names an agent not under agentsFix the name, or add the agent
MAIN-AGENT-0002The agent is over budgetWait for the reset, or raise it on purpose
MAIN-AGENT-0003An agent tried to approve its own stepA person approves
MAIN-AGENT-0004The agent is switched offSet enabled: true when a person decides
MAIN-AGENT-0005The approval id is not pending: unknown, already decided or expiredList agents.approvals.list() and use one of those ids
MAIN-AGENT-0006The budget store did not answer and the agent has a budgetCheck runtime.store; agents with no budget keep running
MAIN-AGENT-0007A sync admit, charge or request on a shared storeUse reserve, settle, release or ask

Swapping drivers

agents has one built-in driver. MainSDK Cloud adds shared budgets and an approvals console, never as a requirement.

Testing

Run the calls above on createTestApp() and move the clock with clock.advance() to cross a budget window. See the testing kit.

Security and privacy

  • An agent never approves its own step.
  • Audit events hold the agent, the person and the surface, never the prompt.
  • enabled: false works with no network and no deploy.

FAQ

Is this the coding agent? No. See build with agents for that.

Next

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet