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 asked | 1 | /** |
| 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 asked | 5 | * |
| 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 asked | 9 | * 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 | */ | |
| 28 | import { type ModelSession, type ModelTier, type RunTicket, ComputeGate, MODEL_ESTIMATE_MICROS, billingClient, integrationsClient } from "@g1t/contracts"; | |
| 29 | ||
| 30 | import { hostedOpen } from "../../runner/src/hosted.ts"; | |
| 31 | import { 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. | 32 | import { type Spent, type Tokens, budgetBlock, chargedMicros, personBlock, personLimit, replyCapMicros, totalTokens } from "./budget.ts"; |
| 33 | import { 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 asked | 34 | import { 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 versions | 35 | import { type PolicyRow, alertDue, markAlerted, policyBlock, readPolicy, workspaceSpendStatements } from "./policy.ts"; |
| 36 | import { 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. | 37 | import { 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 asked | 38 | import { type ReplyModel, allowedProviders, replyModel } from "./routing.ts"; |
| 39 | import { type Row, definitionOf, periods, spendStatements } from "./store.ts"; | |
| 40 | import type { ModelAnswer, Send } from "./turn.ts"; | |
| 41 | import type { ServiceBinding } from "@g1t/contracts"; | |
| 42 | ||
| 43 | export 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 versions | 51 | /** 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 asked | 53 | }; |
| 54 | ||
| 55 | /** The longest one model answer may take. */ | |
| 56 | const 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. */ | |
| 59 | const routingNow = routingReader(); | |
| 60 | let 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 | */ | |
| 68 | export function billingRepo(workspace: string, handle: string): { namespace: string; name: string } { | |
| 69 | return { namespace: workspace.toLowerCase(), name: `@${handle}` }; | |
| 70 | } | |
| 71 | ||
| 72 | async 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 | ||
| 82 | type PriceTerms = { marginPercent: number; rate: number; rateOwn: number }; | |
| 83 | let terms: { value: PriceTerms; until: number } | null = null; | |
| 84 | ||
| 85 | /** The model margin and the agent rates from billing's price book, kept ten minutes. */ | |
| 86 | async 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 | ||
| 104 | export 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 | */ | |
| 113 | function 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. */ | |
| 133 | export 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). */ | |
| 146 | export type WorkUsage = { tokens: Tokens; cost: number; rounds: number }; | |
| 147 | ||
| 148 | export 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 asked | 162 | /** 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 asked | 168 | }; |
| 169 | ||
| 170 | export type MeterBlock = { ok: false; reason: string; message: string }; | |
| 171 | export 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 | */ | |
| 180 | export 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 asked | 193 | 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 asked | 199 | ]); |
| 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 asked | 219 | |
| 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 asked | 300 | 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 versions | 326 | 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 asked | 329 | } |
| 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 versions | 339 | /** |
| 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 | */ | |
| 343 | async 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 asked | 374 | export type { PolicyRow }; |