App agents
agentsGive each agent your app runs an identity in the audit log, a budget, a person's approval for risky steps and a kill switch.
agent-governanceFree in your app, Apache-2.0Status 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.
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: trueimport { agents } from "mainsdk";
const out = await agents.run(
{ agent: "support-bot", action: "orders.refund", user: user.id, spend: "$5.00", risk: "destructive" },
() => refundOrder(input),
);
if (out.status === "pending") return Response.json({ approval: out.approval.id }, { status: 202 });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.
agents:
driver: builtinChange this one line to swap providers; your calls stay the same.Built-in and local drivers integration
Codes
| Code | Where it appears | Meaning |
|---|---|---|
MAIN-AGENT-0001 | RuntimeError responses and logs | A call names an agent that is not under agents |
MAIN-AGENT-0002 | RuntimeError responses and logs | The agent is over its budget; the call is refused and nothing reaches the provider |
MAIN-AGENT-0003 | RuntimeError responses and logs | An agent tried to approve its own step |
MAIN-AGENT-0004 | RuntimeError responses and logs | The agent is switched off (enabled: false) |
MAIN-AGENT-0006 | RuntimeError responses and logs | The store that counts budgets did not answer, and the agent has a budget, so the call is refused (503, retryable) |
MAIN-AGENT-0007 | RuntimeError responses and logs | A sync budget call (admit, charge, request) on a store other instances share |
MAIN-AGENT-0005 | RuntimeError responses and logs | An 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.
| Feature | Phase | Milestone | Spec ticket |
|---|---|---|---|
| Agent and person in every audit event | Phase 1 | 0.3 | agent-governance#actor |
| Spend and call budgets per agent | Phase 1 | 0.3 | agent-governance#budgets |
| Approval above a risk or spend threshold | Phase 1 | 0.3 | agent-governance#approval |
| Calls bound to a named agent | Phase 1 | 0.3 | agent-governance#bind-spend |
| Kill switch without a deploy | Phase 1 | 0.3 | agent-governance#kill |
Quick start
agents:
support-bot:
budget: { spend: $5/day, calls: 500/day }
approve: { risk: destructive, spend-over: $1 }
enabled: trueimport { 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
| Key | What it does |
|---|---|
agents.<name>.budget.spend | The most the agent may spend in the window. Checked before each call |
agents.<name>.budget.calls | The most calls in the window |
agents.<name>.approve.risk | A step at this risk waits for a person |
agents.<name>.approve.spend-over | A step that moves more than this waits for a person |
agents.<name>.enabled | false stops the agent on its next call |
agents.<name>.run | How 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.
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.
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
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 callCheck, record and ask without holding a budget
agents.check(call)tests the kill switch and budgets across instances and holds nothing. It returnsrunnow, 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 thelocalstore, 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 returnsundefinedwith no spend budget or no answer.agents.remainingSpend(name)is the sync form: micro-dollars this instance last saw. It sizes a plan;reserveenforces the budget.agents.onBudgetLow(fn)callsfn(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, soaiand 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
| Code | When | Fix |
|---|---|---|
MAIN-AGENT-0001 | The call names an agent not under agents | Fix the name, or add the agent |
MAIN-AGENT-0002 | The agent is over budget | Wait for the reset, or raise it on purpose |
MAIN-AGENT-0003 | An agent tried to approve its own step | A person approves |
MAIN-AGENT-0004 | The agent is switched off | Set enabled: true when a person decides |
MAIN-AGENT-0005 | The approval id is not pending: unknown, already decided or expired | List agents.approvals.list() and use one of those ids |
MAIN-AGENT-0006 | The budget store did not answer and the agent has a budget | Check runtime.store; agents with no budget keep running |
MAIN-AGENT-0007 | A sync admit, charge or request on a shared store | Use 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: falseworks with no network and no deploy.
FAQ
Is this the coding agent? No. See build with agents for that.
Next
Was this page helpful?