Skip to content
374 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked1/**
2 * Metered model work: the one door every reply and every session step goes
3 * through, so g1t meters and bills an agent's model work one way
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.4 * (docs.g1t.sh/guides/agent-budgets/).
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked5 *
The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how.6 * 1. The paying agent's own monthly and daily caps (`budget.ts`), the
7 * workspace's budget for all its agents together (`policy.ts`), and the
8 * budget of the person who asked (`person-budget.ts`).
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked9 * 2. Whether the agent may use a model at all: g1t's hosted models are open
10 * as the runner decides (`hostedOpen`, `HOSTED_AGENT_WORKSPACES` and
11 * billing's status), or the workspace's own provider; the agent's
12 * `providers` narrows that.
13 * 3. A model session from integrations (`openModelSession`), routed by the
14 * workspace's model routes, as for a run.
15 * 4. The compute gate's reservation (`ComputeGate.admit`, kind `agent`):
16 * the workspace's spend limit, AI credit, pauses and g1t's breaker.
17 * 5. A billing run (`start_run`), so the work is charged as Agent tokens on
18 * the workspace's bill, under the paying agent.
19 * 6. The work itself, through the model proxy with the session's token.
20 * 7. `finish_run` with its cost and tokens, the reservation settled at
21 * cost, and the charge added to the paying agent's and the workspace's
22 * agent spend.
23 *
24 * Who does the work and who pays can differ: a colleague brought into a
25 * session, or a subagent, works on the budget of the agent at the root of
26 * the session's tree, so a chain never escapes the budget that started it.
27 */
28import { type ModelSession, type ModelTier, type RunTicket, ComputeGate, MODEL_ESTIMATE_MICROS, billingClient, integrationsClient } from "@g1t/contracts";
29
30import { hostedOpen } from "../../runner/src/hosted.ts";
31import { type AgentRouting as Policy, routingReader } from "../../runner/src/model-env.ts";
The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how.32import { type Spent, type Tokens, budgetBlock, chargedMicros, personBlock, personLimit, replyCapMicros, totalTokens } from "./budget.ts";
33import { monthStart, ownBudget, personSpent } from "./person-budget.ts";
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked34import { BUILTIN_NO_MODEL } from "./orchestrator.ts";
Agents mode: an overview of every agent's work and spend, and each agent's sessions, memory, routines, spend, activity and versions35import { type PolicyRow, alertDue, markAlerted, policyBlock, readPolicy, workspaceSpendStatements } from "./policy.ts";
36import { dollars } from "./money.ts";
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.37import { type TeamsHere, teamBudgetBlock, teamSpends } from "./teammates.ts";
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked38import { type ReplyModel, allowedProviders, replyModel } from "./routing.ts";
39import { type Row, definitionOf, periods, spendStatements } from "./store.ts";
40import type { ModelAnswer, Send } from "./turn.ts";
41import type { ServiceBinding } from "@g1t/contracts";
42
43export type MeterEnv = {
44 DB: D1Database;
45 BILLING: ServiceBinding;
46 INTEGRATIONS: ServiceBinding;
47 MODELS?: ServiceBinding;
48 MODELS_URL?: string;
49 HOSTED_AGENT_WORKSPACES: string;
50 AGENT_ROUTING: string;
Agents mode: an overview of every agent's work and spend, and each agent's sessions, memory, routines, spend, activity and versions51 /** Notifications: the workspace's agent budget crossing 75, 90 or 100%. */
52 NOTIFY?: ServiceBinding;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked53};
54
55/** The longest one model answer may take. */
56const MODEL_TIMEOUT_MS = 120_000;
57
58/** Staff's model defaults on top of `AGENT_ROUTING`, read at most once a minute, as in the runner. */
59const routingNow = routingReader();
60let gate: ComputeGate | null = null;
61
62/**
63 * Where an agent's spend shows in billing: the workspace, under the agent
64 * that pays. Billing keys runs and reservations by a repository; no
65 * repository is named with an `@`, so the agent's line never mixes with a
66 * project's.
67 */
68export function billingRepo(workspace: string, handle: string): { namespace: string; name: string } {
69 return { namespace: workspace.toLowerCase(), name: `@${handle}` };
70}
71
72async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> {
73 const response = await service.fetch(`https://service/rpc/${method}`, {
74 method: "POST",
75 headers: { "content-type": "application/json" },
76 body: JSON.stringify(args),
77 });
78 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
79 return (await response.json()) as T;
80}
81
82type PriceTerms = { marginPercent: number; rate: number; rateOwn: number };
83let terms: { value: PriceTerms; until: number } | null = null;
84
85/** The model margin and the agent rates from billing's price book, kept ten minutes. */
86async function priceTerms(billing: ServiceBinding): Promise<PriceTerms> {
87 if (terms && terms.until > Date.now()) return terms.value;
88 type Book = { prices?: { meter: string; priceMicros?: number; price_micros?: number }[]; modelMarginPercent?: number; model_margin_percent?: number };
89 const book = await rpc<Book>(billing, "prices", {}).catch(() => null);
90 const price = (meter: string) => {
91 const found = book?.prices?.find((p) => p.meter === meter);
92 return found?.priceMicros ?? found?.price_micros ?? 0;
93 };
94 const value = {
95 marginPercent: book?.modelMarginPercent ?? book?.model_margin_percent ?? 0,
96 rate: price("agent_tokens"),
97 rateOwn: price("agent_tokens_own"),
98 };
99 // A failed read is tried again in a minute, not kept.
100 terms = { value, until: Date.now() + (book ? 10 * 60_000 : 60_000) };
101 return value;
102}
103
104export async function sha256Hex(text: string): Promise<string> {
105 const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
106 return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
107}
108
109/**
110 * How work asks the model: one Messages API request through the model
111 * proxy, with the model session's token, non-streamed.
112 */
113function sendFor(env: MeterEnv, token: string): Send {
114 // The binding, not the proxy's public address: a Worker fetching another
115 // Worker's domain on the same zone can be refused or loop. The proxy reads
116 // only the path and the token, so the host does not matter.
117 const path = "/anthropic/v1/messages";
118 const fetcher = (url: string, init: RequestInit) => (env.MODELS ? env.MODELS.fetch(url, init) : fetch(url, init));
119 return async (body) => {
120 const response = await fetcher(env.MODELS ? `https://models${path}` : `${env.MODELS_URL!.replace(/\/+$/, "")}${path}`, {
121 method: "POST",
122 headers: { "content-type": "application/json", "x-api-key": token, "anthropic-version": "2023-06-01" },
123 body: JSON.stringify(body),
124 signal: AbortSignal.timeout(MODEL_TIMEOUT_MS),
125 });
126 const json = (await response.json().catch(() => null)) as (ModelAnswer & { error?: { message?: string } }) | null;
127 if (!response.ok || !json) throw new Error(`the model answered ${response.status}: ${json?.error?.message ?? "no answer"}`);
128 return json;
129 };
130}
131
132/** What the work is given: how to ask the model, and on what. */
133export type Model = {
134 send: Send;
135 model: ReplyModel;
136 /** The workspace's own provider pays for the model (g1t charges only the agent rate). */
137 ownModel: boolean;
138 policy: Policy;
139 /** The model the workspace's route names, on its own provider. */
140 sessionModel: string | null;
141 /** The workspace's own model connection, if any. */
142 own: string | null;
143};
144
145/** What the work used: its own tokens and cost, and any it spent for others (consults). */
146export type WorkUsage = { tokens: Tokens; cost: number; rounds: number };
147
148export type MeterInput = {
149 /** The agent doing the work. */
150 row: Row;
151 /** The agent whose budget pays; the same agent unless this is part of another's session. */
152 payer: Row;
153 /** The workspace's slug. */
154 slug: string;
155 task: "reply" | "session";
156 /** The tier the work starts on, before the agent's limits. */
157 start: ModelTier;
158 /** Who asked, by username, for billing's record. */
159 askerName: string | null;
The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how.160 /** The person the work is for, by username: their budget counts it. Null for work no person asked for. */
161 person?: string | null;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked162 /** What is left of a session's cap, so one step never overruns it. */
163 leftMicros?: number | null;
164 /** Agent tier limits narrower than the agent's own (a subagent's). */
165 limits?: { floor: ModelTier | null; ceiling: ModelTier | null } | null;
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.166 /** The paying agent's teams (teammates.ts): a team's budget caps what its agents spend together. */
167 teams?: TeamsHere | null;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked168};
169
170export type MeterBlock = { ok: false; reason: string; message: string };
171export type MeterDone<T> = { ok: true; value: T; model: string; tier: ModelTier | null; tokens: Tokens; cost: number; charged: number };
172
173/**
174 * Runs `work` as metered model work for `input.row`, paid by
175 * `input.payer`: checks every limit, opens and closes the model session,
176 * bills it, and records the spend. A limit that says no comes back as a
177 * block with what to tell people, before anything was spent. Whatever
178 * `work` throws is thrown on, after what was held is given back.
179 */
180export async function metered<T extends WorkUsage>(env: MeterEnv, input: MeterInput, work: (model: Model) => Promise<T>, now = new Date()): Promise<MeterBlock | MeterDone<T>> {
181 const db = env.DB;
182 const { row, payer, slug } = input;
183 if (!env.MODELS && !env.MODELS_URL) return { ok: false, reason: "no_models_url", message: "This installation has no model proxy set up." };
184 const billing = billingClient(env.BILLING);
185 const integrations = integrationsClient(env.INTEGRATIONS);
186 gate ??= new ComputeGate(env.BILLING);
187
188 // 1. The paying agent's caps, and the workspace's budget for every agent.
189 const payerDefinition = definitionOf(payer);
190 const [month, day] = periods(now);
The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how.191 const asker = input.person ? input.person.toLowerCase() : null;
192 const [spentRows, policy, ownCap] = await Promise.all([
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked193 db
194 .prepare("SELECT period, micros FROM agent_spend WHERE agent_id = ? AND period IN (?, ?)")
195 .bind(payer.id, month, day)
196 .all<{ period: string; micros: number }>(),
197 readPolicy(db, row.workspace_id, month),
The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how.198 asker ? ownBudget(db, row.workspace_id, asker) : Promise.resolve(null),
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked199 ]);
200 const spent: Spent = {
201 month: spentRows.results.find((r) => r.period === month)?.micros ?? 0,
202 day: spentRows.results.find((r) => r.period === day)?.micros ?? 0,
203 };
204 const blocked = budgetBlock(payerDefinition.budget, spent, now);
205 if (blocked) {
206 const message = payer.id === row.id ? blocked.message : `@${payer.handle}, who this work is for, is out of budget.`;
207 return { ok: false, reason: `budget_${blocked.cap}`, message };
208 }
209 const pool = policyBlock(policy);
210 if (pool) return { ok: false, reason: "workspace_agent_budget", message: pool };
The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how.211 // The budget of the person the work is for, summed only when one applies.
212 const personCap = asker ? personLimit(policy.person_monthly_micros, ownCap) : null;
213 const personUsed = asker && personCap != null ? await personSpent(db, row.workspace_id, asker, monthStart(now)) : 0;
214 const personStop = asker ? personBlock(asker, personCap, personUsed, now) : null;
215 if (personStop) return { ok: false, reason: "person_budget", message: personStop };
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.216 // The budgets of the teams the paying agent is on, read only when one has one.
217 const teamStop = input.teams?.teams.some((team) => team.budget_micros) ? teamBudgetBlock(await teamSpends(db, input.teams, month)) : null;
218 if (teamStop) return { ok: false, reason: "team_budget", message: teamStop };
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked219
220 // 2. Whether it may use a model at all.
221 const definition = definitionOf(row);
222 const [own, status] = await Promise.all([
223 integrations.modelProvider(slug).catch(() => null),
224 billing.status().catch(() => ({ enabled: false, live: false })),
225 ]);
226 const allowed = allowedProviders(definition.routing, own?.id ?? null);
227 const mayHosted = hostedOpen(slug, env.HOSTED_AGENT_WORKSPACES, status) && allowed.hosted;
228 if (!mayHosted && !allowed.own) {
229 const message = own
230 ? "My settings don't let me use any model this workspace has. An owner can change my providers on my profile."
231 : allowed.hosted
232 ? row.builtin
233 ? BUILTIN_NO_MODEL
234 : "g1t's hosted models aren't open to this workspace, and it has no model provider of its own. An owner can connect one under Integrations."
235 : "My settings let me use only this workspace's own model providers, and it has none. An owner can connect one under Integrations, or change my providers on my profile.";
236 return { ok: false, reason: "no_model", message };
237 }
238
239 // 3. A model session, routed by the workspace's model routes, billed under the payer.
240 const repo = billingRepo(slug, payer.handle);
241 const routing = await routingNow(env.AGENT_ROUTING, () => billing.modelDefaults());
242 const limits = { ...definition.routing, ...(input.limits ?? {}) };
243 const provisional = replyModel(routing, { ...limits, pinned: null }, { start: input.start });
244 const opened = await integrations.openModelSession({
245 workspace: slug,
246 repo,
247 number: 0,
248 task: input.task,
249 hostedOpen: mayHosted,
250 tier: provisional.tier,
251 requestedBy: input.askerName,
252 });
253 if (!opened.ok) return { ok: false, reason: "model_route", message: opened.error.message };
254 const session: ModelSession = opened.value;
255 let reservation: string | null = null;
256 let settled = false;
257 try {
258 const ownModel = session.billedTo === "workspace";
259 if (ownModel ? !allowed.own : !mayHosted) {
260 return {
261 ok: false,
262 reason: "provider_not_allowed",
263 message:
264 "This workspace routes agents to a model my settings don't allow. An owner can change my providers on my profile, or the workspace's model routes under Integrations.",
265 };
266 }
267 // A pinned model is for the workspace's own endpoints; on g1t's models the tier decides.
268 const model = replyModel(
269 routing,
270 { ...limits, pinned: ownModel ? definition.routing.pinned : null },
271 { chosen: session.tierChoice ?? null, named: ownModel ? session.model : null, start: input.start },
272 );
273
274 // 4. The workspace's own limits, through the compute gate.
275 const ent = await gate.entitlements(slug);
276 const estimate = ownModel ? 0 : input.task === "reply" ? MODEL_ESTIMATE_MICROS.reply : MODEL_ESTIMATE_MICROS.reply * 4;
277 const admission = await gate.admit({ workspace: slug, repo, public: false, kind: "agent", estimateMicros: estimate, hostedModel: !ownModel }, ent);
278 if (!admission.ok) return { ok: false, reason: `workspace_${admission.code}`, message: admission.message };
279 reservation = admission.reservation?.id ?? null;
280
281 // 5. The run it is billed as.
282 const named = ownModel && session.model ? session.model : null;
283 const started = await billing.startRun({
284 workspace: slug,
285 repo,
286 number: 0,
287 task: input.task,
288 model: ownModel ? `${model.modelName} (${session.providerName ?? "own provider"})` : model.modelName,
289 billedTo: ownModel ? "workspace" : "g1t",
290 session: session.id,
291 tier: named ? null : model.tier,
292 });
293 if (!started.ok) return { ok: false, reason: "billing", message: started.error.message };
294 const ticket: RunTicket | null = started.value;
295 const caps = [replyCapMicros(payerDefinition.budget, spent, ent && ent.runCapMicros > 0 ? ent.runCapMicros : null)];
296 if (input.leftMicros != null) caps.push(Math.max(1, Math.floor(input.leftMicros)));
297 const left = policy.monthly_micros ? policy.monthly_micros - policy.spent : null;
298 if (left != null) caps.push(Math.max(1, left));
The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how.299 if (personCap != null) caps.push(Math.max(1, personCap - personUsed));
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked300 const cap = caps.filter((c): c is number => c != null);
301 if (cap.length) await integrations.capModelSessions([await sha256Hex(session.token)], Math.min(...cap)).catch(() => 0);
302
303 // 6. The work.
304 const result = await work({ send: sendFor(env, session.token), model, ownModel, policy: routing, sessionModel: session.model, own: own?.id ?? null });
305 const priced = await priceTerms(env.BILLING);
306 const charged = chargedMicros({
307 costMicros: result.cost,
308 hosted: !ownModel,
309 marginPercent: priced.marginPercent,
310 ratePerMillionMicros: ownModel ? priced.rateOwn : priced.rate,
311 tokens: totalTokens(result.tokens),
312 });
313
314 // 7. Bill it, settle, and count it against the payer and the workspace.
315 if (ticket) {
316 await rpc(env.BILLING, "finish_run", { runId: ticket.runId, token: ticket.token, costUsd: result.cost / 1_000_000, turns: result.rounds, tokens: result.tokens }).catch(
317 (error: unknown) => console.error("agents: finish_run failed", ticket.runId, String(error)),
318 );
319 }
320 if (reservation) {
321 settled = true;
322 await gate.settle(reservation, result.cost);
323 }
324 if (charged > 0) {
325 await db.batch([...spendStatements(db, payer.id, charged, now, input.task), ...workspaceSpendStatements(db, row.workspace_id, charged, now)]);
Agents mode: an overview of every agent's work and spend, and each agent's sessions, memory, routines, spend, activity and versions326 await budgetAlert(env, slug, row.workspace_id, { ...policy, spent: policy.spent + charged }, month).catch((error: unknown) =>
327 console.error("agents: a budget alert was not sent", slug, String(error)),
328 );
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked329 }
330 return { ok: true, value: result, model: model.modelName, tier: named ? null : model.tier, tokens: result.tokens, cost: result.cost, charged };
331 } finally {
332 // What was held is given back however the work ended, and the model
333 // session's token stops working.
334 if (reservation && !settled) await gate.settle(reservation, 0);
335 await integrations.closeModelSessions([await sha256Hex(session.token)]).catch(() => 0);
336 }
337}
338
Agents mode: an overview of every agent's work and spend, and each agent's sessions, memory, routines, spend, activity and versions339/**
340 * Tells the owner who set the workspace's agent budget, once per level a
341 * month, when every agent's spend together crosses 75, 90 or 100% of it.
342 */
343async function budgetAlert(env: MeterEnv, slug: string, workspaceId: string, policy: PolicyRow, month: string): Promise<void> {
344 const level = alertDue(policy);
345 if (!level || !env.NOTIFY || !policy.monthly_micros) return;
346 if (!(await markAlerted(env.DB, workspaceId, month, level))) return;
347 const setBy = await env.DB.prepare("SELECT updated_by FROM agent_policies WHERE workspace_id = ?").bind(workspaceId).first<{ updated_by: string | null }>();
348 if (!setBy?.updated_by) return;
349 const title = level >= 100 ? "Your agents have used this month's budget" : `Your agents have used ${level}% of this month's budget`;
350 const body =
351 level >= 100
352 ? `${dollars(policy.spent)} of ${dollars(policy.monthly_micros)}. They won't start new work until it's raised or the month turns.`
353 : `${dollars(policy.spent)} of ${dollars(policy.monthly_micros)} so far this month.`;
354 await env.NOTIFY.fetch("https://service/rpc/notify", {
355 method: "POST",
356 headers: { "content-type": "application/json" },
357 body: JSON.stringify({
358 target: { username: setBy.updated_by },
359 notification: {
360 id: `agent-budget:${workspaceId}:${month}:${level}`,
361 kind: "approval",
362 workspace: slug,
363 title,
364 body,
365 href: `/${slug}/-/agents`,
366 actor: { kind: "system", id: "g1t", name: "g1t" },
367 channel_id: null,
368 created_at: new Date().toISOString(),
369 },
370 }),
371 });
372}
373
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked374export type { PolicyRow };