Skip to content
731 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 * What Agents mode reads and changes beyond definitions: sessions, memory,
3 * routines, spend, activity, versions and the workspace's agent policy.
4 * Called by the RPC methods in index.ts once they know who is asking and
5 * that they may see the workspace.
6 *
7 * Privacy follows the conversations work came from. A session, a reply or
8 * a memory from a conversation the viewer is not in shows that it happened
9 * and what it cost, never what it was about, owners included: owners
10 * control money and agents, not other people's conversations.
11 */
12import {
13 type AgentActivity,
14 type AgentMemory,
15 type AgentPolicy,
16 type AgentRoutine,
17 type AgentSession,
18 type AgentSessionDetail,
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.19 type AgentTeamContext,
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 asked20 type AgentSpendBreakdown,
21 type AgentVersion,
22 type AgentsOverview,
23 type AgentMemoryScope,
24 type NewRoutine,
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.25 type PersonBudgets,
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 asked26 type Result,
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened27 type RoutineSuggestion,
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 asked28 type SessionEvent,
29 type SpendSlice,
30 type User,
31 type WorkspaceAgent,
32 chatClient,
33 fail,
An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how.34 identityClient,
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 asked35 newId,
36 ok,
37} from "@g1t/contracts";
38
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.39import { monthKey, spendSpan } from "./budget.ts";
40import { cleanUsername, personBudgets } 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 asked41import { type MemoryRow, type MemoryViewer, changeableBy, cleanFact, toMemory, visibleTo } from "./memory.ts";
42import { DEFAULT_POLICY, checkPolicy, readPolicy } from "./policy.ts";
43import { type RoutineRow, MAX_ROUTINES, checkRoutine, newRoutineId, nextRun, runRoutine, toRoutine } from "./routines.ts";
44import { type SessionEnv, type SessionRow, LIVE, approve, sessionRow, steer, stop, toSession } from "./sessions.ts";
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.45import { type Row, definitionOf, isPersonal, periods, selectAgents, toAgent } from "./store.ts";
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened46import { suggestRoutines } from "./suggest.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.47import { teamsSection } from "./teammates.ts";
48import { loadTeams } from "./ports.ts";
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.49import { canSeeAgent, isOwnerOf } from "./access.ts";
50import { BUILDER_ID, BUILDER_LABEL } from "./builder.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 asked51
52export type ViewContext = {
53 env: SessionEnv;
54 db: D1Database;
55 slug: string;
56 workspaceId: string;
57 viewer: User;
58 /** Whether the viewer owns the workspace (or is its token). */
59 owner: boolean;
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.60 /** Records a change in the workspace's audit log, as the viewer (index.ts). */
61 audit?: (action: string, handle: string, message: string) => void;
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 asked62};
63
An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how.64type AgentFace = { handle: string; display_name: string; avatar_seed: string };
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 asked65
66/** The workspace's agents by id, archived ones too, for names on sessions and spend. */
67async function faces(ctx: ViewContext): Promise<Map<string, AgentFace>> {
68 const rows = await ctx.db
An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how.69 .prepare("SELECT id, handle, display_name, avatar_seed FROM agents WHERE workspace_id = ?")
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 asked70 .bind(ctx.workspaceId)
71 .all<{ id: string } & AgentFace>();
72 return new Map(rows.results.map((r) => [r.id, { ...r, avatar_seed: r.avatar_seed || r.handle }]));
73}
74
An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how.75/** The teams each agent is on, by agent id, from the workspace's teams the viewer can see. Empty when they can't be read. */
76async function teamsOfAgents(ctx: ViewContext): Promise<Map<string, { slug: string; name: string }[]>> {
77 const out = new Map<string, { slug: string; name: string }[]>();
78 const directory = await identityClient(ctx.env.IDENTITY)
79 .peopleDirectory(ctx.viewer, ctx.slug)
80 .catch(() => null);
81 if (!directory?.ok) return out;
82 for (const team of directory.value.teams) {
83 for (const id of team.agent_ids) out.set(id, [...(out.get(id) ?? []), { slug: team.slug, name: team.name }]);
84 }
85 return out;
86}
87
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 asked88/**
89 * Which of these conversations the viewer is in (or can read: a public
90 * channel). Asked of chat once per conversation, at most 60.
91 */
92async function readable(ctx: ViewContext, channelIds: string[]): Promise<Set<string>> {
93 const out = new Set<string>();
94 if (ctx.viewer.kind === "workspace") return out;
95 const chat = chatClient(ctx.env.CHAT);
96 const ids = [...new Set(channelIds)].slice(0, 60);
97 await Promise.all(
98 ids.map(async (id) => {
99 const audience = await chat.audience(ctx.slug, id).catch(() => null);
100 if (audience?.ok && (audience.value.kind === "public" || audience.value.member_user_ids.includes(ctx.viewer.id))) out.add(id);
101 }),
102 );
103 return out;
104}
105
106async function sessionsOut(ctx: ViewContext, rows: SessionRow[], agents?: Map<string, AgentFace>): Promise<AgentSession[]> {
107 const names = agents ?? (await faces(ctx));
108 const can = await readable(ctx, rows.map((r) => r.channel_id));
109 return rows.map((row) => toSession(row, names.get(row.agent_id) ?? null, can.has(row.channel_id)));
110}
111
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.112/** The agent by handle, if the viewer may see it: a personal agent only its member and the owners. */
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 asked113async function agentByHandle(ctx: ViewContext, handle: string): Promise<Row | null> {
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.114 const row = await ctx.db
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 asked115 .prepare("SELECT * FROM agents WHERE workspace_id = ? AND handle = ? AND archived_at IS NULL")
116 .bind(ctx.workspaceId, String(handle ?? "").trim().replace(/^@/, "").toLowerCase())
117 .first<Row>();
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.118 return row && canSeeAgent(ctx.viewer, ctx.slug, row) ? row : 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 asked119}
120
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.121// ── Teams ─────────────────────────────────────────────────────────────────
122
123/**
124 * What an agent is told about its teams this turn, word for word as it
125 * gets it (teammates.ts), for its People profile and its teams' pages.
126 * Null when it is on no visible team.
127 */
128export async function teamContext(ctx: ViewContext, handle: string): Promise<Result<AgentTeamContext>> {
129 const agent = await agentByHandle(ctx, handle);
130 if (!agent) return fail("not_found", `There is no agent called @${handle}.`);
An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how.131 const here = await loadTeams(ctx.env, ctx.slug, ctx.workspaceId, { id: agent.id });
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.132 if (!here) return fail("not_found", "Its teams could not be read just now.");
133 return ok({ handle: agent.handle, teams: here.teams.map((team) => team.slug), text: teamsSection(agent.id, here, new Date()) });
134}
135
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 asked136// ── Sessions ──────────────────────────────────────────────────────────────
137
138export async function listSessions(ctx: ViewContext, filter: { handle?: string | null; status?: "live" | "done" | null; limit?: number | null }): Promise<Result<AgentSession[]>> {
139 const limit = Math.min(200, Math.max(1, Math.floor(Number(filter.limit) || 50)));
140 const where = ["workspace_id = ?"];
141 const binds: (string | number)[] = [ctx.workspaceId];
142 if (filter.handle) {
143 const agent = await agentByHandle(ctx, filter.handle);
144 if (!agent) return fail("not_found", `There is no agent called @${filter.handle}.`);
145 where.push("agent_id = ?");
146 binds.push(agent.id);
147 }
148 if (filter.status === "live") where.push(`status IN (${LIVE.map(() => "?").join(", ")})`), binds.push(...LIVE);
149 if (filter.status === "done") where.push(`status NOT IN (${LIVE.map(() => "?").join(", ")})`), binds.push(...LIVE);
150 const rows = await ctx.db
151 .prepare(`SELECT * FROM agent_sessions WHERE ${where.join(" AND ")} ORDER BY created_at DESC LIMIT ?`)
152 .bind(...binds, limit)
153 .all<SessionRow>();
154 return ok(await sessionsOut(ctx, rows.results));
155}
156
157export async function sessionDetail(ctx: ViewContext, id: string): Promise<Result<AgentSessionDetail>> {
158 const row = await sessionRow(ctx.db, String(id ?? ""));
159 if (!row || row.workspace_id !== ctx.workspaceId) return fail("not_found", "There is no such session.");
160 const agents = await faces(ctx);
161 const treeRows = await ctx.db.prepare("SELECT * FROM agent_sessions WHERE root_id = ? ORDER BY created_at LIMIT 100").bind(row.root_id).all<SessionRow>();
162 const [session] = await sessionsOut(ctx, [row], agents);
163 const tree = await sessionsOut(ctx, treeRows.results, agents);
164 let events: SessionEvent[] = [];
165 if (session.visible) {
166 const rows = await ctx.db
167 .prepare("SELECT seq, kind, by_name, body, tool, outcome, created_at FROM agent_session_events WHERE session_id = ? ORDER BY seq LIMIT 1000")
168 .bind(row.id)
169 .all<{ seq: number; kind: SessionEvent["kind"]; by_name: string | null; body: string; tool: string | null; outcome: string | null; created_at: string }>();
170 events = rows.results.map((e) => ({ seq: e.seq, kind: e.kind, by: e.by_name, body: e.body, tool: e.tool, outcome: e.outcome, created_at: e.created_at }));
171 }
172 const live = LIVE.includes(row.status as (typeof LIVE)[number]);
173 return ok({
174 session,
175 events,
176 tree,
177 can_stop: session.visible && live,
178 can_steer: session.visible,
179 can_approve: row.status === "needs_approval" && (ctx.owner || (session.visible && row.asked_by === ctx.viewer.id && ctx.owner)),
180 });
181}
182
183async function visibleSession(ctx: ViewContext, id: string): Promise<Result<SessionRow>> {
184 const row = await sessionRow(ctx.db, String(id ?? ""));
185 if (!row || row.workspace_id !== ctx.workspaceId) return fail("not_found", "There is no such session.");
186 const can = await readable(ctx, [row.channel_id]);
187 if (!can.has(row.channel_id)) return fail("not_found", "There is no such session.");
188 return ok(row);
189}
190
191export async function stopSession(ctx: ViewContext, id: string): Promise<Result<AgentSession>> {
192 const found = await visibleSession(ctx, id);
193 if (!found.ok) return found;
194 if (!LIVE.includes(found.value.status as (typeof LIVE)[number])) return fail("invalid", "That session is already over.");
195 const row = await stop(ctx.env, found.value, ctx.viewer.username);
196 return ok((await sessionsOut(ctx, [row]))[0]);
197}
198
199export async function steerSession(ctx: ViewContext, id: string, body: string): Promise<Result<AgentSession>> {
200 const found = await visibleSession(ctx, id);
201 if (!found.ok) return found;
202 const text = typeof body === "string" ? body.trim() : "";
203 if (!text) return fail("invalid", "Say something to the session.");
204 const row = await steer(ctx.env, found.value, ctx.viewer.username, text.slice(0, 4000));
205 return ok((await sessionsOut(ctx, [row]))[0]);
206}
207
208/** Owners raise a session's cap; it must end up above what it has spent. */
209export async function approveSession(ctx: ViewContext, id: string, capMicros: number): Promise<Result<AgentSession>> {
210 if (!ctx.owner) return fail("forbidden", "Only the workspace's owners can approve more spend.");
211 const row = await sessionRow(ctx.db, String(id ?? ""));
212 if (!row || row.workspace_id !== ctx.workspaceId) return fail("not_found", "There is no such session.");
213 if (row.status !== "needs_approval") return fail("invalid", "That session isn't waiting for approval.");
214 const cap = Math.floor(Number(capMicros));
215 if (!Number.isFinite(cap) || cap <= row.charged_micros || cap > 1_000_000_000) return fail("invalid", "The new cap must be above what it has spent.");
216 const fresh = await approve(ctx.env, row, ctx.viewer.username, cap);
217 return ok((await sessionsOut(ctx, [fresh]))[0]);
218}
219
220// ── Memory ────────────────────────────────────────────────────────────────
221
222async function memoryViewer(ctx: ViewContext, rows: MemoryRow[]): Promise<MemoryViewer> {
223 const channels = await readable(ctx, rows.filter((r) => r.scope === "channel").map((r) => r.scope_ref));
224 return { id: ctx.viewer.id, owner: ctx.owner, inChannel: (id) => channels.has(id) };
225}
226
227export async function memories(ctx: ViewContext, handle: string): Promise<Result<AgentMemory[]>> {
228 const agent = await agentByHandle(ctx, handle);
229 if (!agent) return fail("not_found", `There is no agent called @${handle}.`);
230 const rows = await ctx.db.prepare("SELECT * FROM agent_memories WHERE agent_id = ? ORDER BY pinned DESC, updated_at DESC LIMIT 500").bind(agent.id).all<MemoryRow>();
231 const viewer = await memoryViewer(ctx, rows.results);
232 return ok(rows.results.filter((row) => visibleTo(row, viewer)).map(toMemory));
233}
234
235/**
236 * A fact a person gives an agent. Workspace facts are the owners'; a
237 * channel fact needs the person to be in that channel; a person fact is
238 * always their own.
239 */
240export async function remember(ctx: ViewContext, handle: string, input: { body: string; scope: AgentMemoryScope; scope_ref?: string | null }): Promise<Result<AgentMemory>> {
241 const agent = await agentByHandle(ctx, handle);
242 if (!agent) return fail("not_found", `There is no agent called @${handle}.`);
243 const body = cleanFact(input?.body);
244 if (!body) return fail("invalid", "Say what it should remember.");
245 let scope: AgentMemoryScope = input?.scope === "workspace" || input?.scope === "channel" ? input.scope : "person";
246 let ref = "";
247 let label: string | null = null;
248 if (scope === "workspace" && !ctx.owner) return fail("forbidden", "Only owners give an agent facts for the whole workspace.");
249 if (scope === "channel") {
250 ref = String(input.scope_ref ?? "");
251 const can = await readable(ctx, [ref]);
252 if (!can.has(ref)) return fail("forbidden", "You can only give it facts for conversations you're in.");
253 const audience = await chatClient(ctx.env.CHAT).audience(ctx.slug, ref).catch(() => null);
254 label = audience?.ok && "name" in audience.value ? ((audience.value as { name?: string | null }).name ?? null) : null;
255 }
256 if (scope === "person") {
257 scope = "person";
258 ref = ctx.viewer.id;
259 label = ctx.viewer.username;
260 }
261 const id = newId("mem");
262 const now = new Date().toISOString();
263 await ctx.db
264 .prepare(
265 `INSERT INTO agent_memories (id, agent_id, workspace_id, scope, scope_ref, scope_label, body, source_kind, source_ref, source_label, created_by, created_by_kind, pinned, created_at, updated_at)
266 VALUES (?, ?, ?, ?, ?, ?, ?, 'person', ?, ?, ?, 'user', 1, ?, ?)`,
267 )
268 .bind(id, agent.id, ctx.workspaceId, scope, ref, label, body, ctx.viewer.username, `@${ctx.viewer.username}`, ctx.viewer.username, now, now)
269 .run();
270 const row = await ctx.db.prepare("SELECT * FROM agent_memories WHERE id = ?").bind(id).first<MemoryRow>();
271 return ok(toMemory(row!));
272}
273
274async function changeable(ctx: ViewContext, handle: string, id: string): Promise<Result<MemoryRow>> {
275 const agent = await agentByHandle(ctx, handle);
276 if (!agent) return fail("not_found", `There is no agent called @${handle}.`);
277 const row = await ctx.db.prepare("SELECT * FROM agent_memories WHERE id = ? AND agent_id = ?").bind(String(id ?? ""), agent.id).first<MemoryRow>();
278 if (!row) return fail("not_found", "There is no such memory.");
279 const viewer = await memoryViewer(ctx, [row]);
280 if (!visibleTo(row, viewer)) return fail("not_found", "There is no such memory.");
281 if (!changeableBy(row, viewer)) return fail("forbidden", "Only owners change what an agent knows for the whole workspace.");
282 return ok(row);
283}
284
285export async function updateMemory(ctx: ViewContext, handle: string, id: string, changes: { body?: string; pinned?: boolean }): Promise<Result<AgentMemory>> {
286 const found = await changeable(ctx, handle, id);
287 if (!found.ok) return found;
288 const body = changes?.body === undefined ? found.value.body : cleanFact(changes.body);
289 if (!body) return fail("invalid", "A memory can't be empty; forget it instead.");
290 const pinned = changes?.pinned === undefined ? found.value.pinned : changes.pinned ? 1 : 0;
291 const now = new Date().toISOString();
292 const edited = body !== found.value.body;
293 await ctx.db
294 .prepare(
295 `UPDATE agent_memories SET body = ?, pinned = ?, updated_at = ?${edited ? ", source_kind = 'person', source_ref = ?, source_label = ?" : ""} WHERE id = ?`,
296 )
297 .bind(...(edited ? [body, pinned, now, ctx.viewer.username, `@${ctx.viewer.username} (corrected)`, found.value.id] : [body, pinned, now, found.value.id]))
298 .run();
299 const row = await ctx.db.prepare("SELECT * FROM agent_memories WHERE id = ?").bind(found.value.id).first<MemoryRow>();
300 return ok(toMemory(row!));
301}
302
303export async function forget(ctx: ViewContext, handle: string, id: string): Promise<Result<null>> {
304 const found = await changeable(ctx, handle, id);
305 if (!found.ok) return found;
306 await ctx.db.prepare("DELETE FROM agent_memories WHERE id = ?").bind(found.value.id).run();
307 return ok(null);
308}
309
310// ── Routines ──────────────────────────────────────────────────────────────
311
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.312/** Routines post where a person and the agent both are; a personal agent is only in its member's DM. */
313const PERSONAL_ROUTINES = "Personal agents can't run routines yet. An owner can promote it to a workspace agent, which can.";
314
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened315/** An agent's routines, and routines its responsibilities suggest that it doesn't have yet. */
316export async function routines(ctx: ViewContext, handle: string): Promise<Result<{ routines: AgentRoutine[]; suggestions: RoutineSuggestion[] }>> {
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 asked317 const agent = await agentByHandle(ctx, handle);
318 if (!agent) return fail("not_found", `There is no agent called @${handle}.`);
319 const rows = await ctx.db.prepare("SELECT * FROM agent_routines WHERE agent_id = ? ORDER BY created_at").bind(agent.id).all<RoutineRow>();
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened320 const list = rows.results.map(toRoutine);
321 const duties = definitionOf(agent).responsibilities;
322 return ok({ routines: list, suggestions: suggestRoutines(duties, list.map((r) => ({ name: r.name, events: r.events }))) });
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 asked323}
324
325/**
326 * Owners keep an agent's routines. The person who saves one becomes its
327 * sponsor: it runs with their access, in a channel they and the agent are in.
328 */
329export async function saveRoutine(ctx: ViewContext, handle: string, input: NewRoutine, id: string | null): Promise<Result<AgentRoutine>> {
330 if (!ctx.owner) return fail("forbidden", "Only the workspace's owners set up routines.");
331 if (ctx.viewer.kind === "workspace") return fail("invalid", "A routine runs with a person's access: set it up signed in as yourself.");
332 const agent = await agentByHandle(ctx, handle);
333 if (!agent) return fail("not_found", `There is no agent called @${handle}.`);
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.334 if (isPersonal(agent)) return fail("invalid", PERSONAL_ROUTINES);
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 asked335 const checked = checkRoutine(input);
336 if (!checked.ok) return fail("invalid", checked.message);
337 const r = checked.value;
338 const audience = await chatClient(ctx.env.CHAT).audience(ctx.slug, r.channel_id).catch(() => null);
339 if (!audience?.ok || (audience.value.kind !== "public" && !audience.value.member_user_ids.includes(ctx.viewer.id))) {
340 return fail("invalid", "Choose a channel you're in.");
341 }
342 const channels = await chatClient(ctx.env.CHAT).sidebar(ctx.slug, ctx.viewer).catch(() => null);
343 const channelName = channels?.ok ? (channelNameIn(channels.value, r.channel_id) ?? null) : null;
344 const now = new Date();
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened345 const next = r.enabled !== false && r.schedule ? nextRun(r.schedule, now).toISOString() : null;
346 const schedule = r.schedule ? JSON.stringify(r.schedule) : 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 asked347 if (id) {
348 const existing = await ctx.db.prepare("SELECT id FROM agent_routines WHERE id = ? AND agent_id = ?").bind(id, agent.id).first();
349 if (!existing) return fail("not_found", "There is no such routine.");
350 await ctx.db
351 .prepare(
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened352 `UPDATE agent_routines SET name = ?, instructions = ?, schedule = ?, events = ?, repos = ?, channel_id = ?, channel_name = ?, sponsor = ?, sponsor_username = ?,
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 asked353 enabled = ?, paused_note = NULL, next_run_at = ?, workspace = ?, updated_at = ? WHERE id = ?`,
354 )
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened355 .bind(r.name, r.instructions, schedule, JSON.stringify(r.events), JSON.stringify(r.repos), r.channel_id, channelName, ctx.viewer.id, ctx.viewer.username, r.enabled !== false ? 1 : 0, next, ctx.slug, now.toISOString(), id)
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 asked356 .run();
357 } else {
358 const count = await ctx.db.prepare("SELECT COUNT(*) AS n FROM agent_routines WHERE agent_id = ?").bind(agent.id).first<{ n: number }>();
359 if ((count?.n ?? 0) >= MAX_ROUTINES) return fail("invalid", `An agent keeps at most ${MAX_ROUTINES} routines.`);
360 id = newRoutineId();
361 await ctx.db
362 .prepare(
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened363 `INSERT INTO agent_routines (id, agent_id, workspace_id, workspace, name, instructions, schedule, events, repos, channel_id, channel_name, sponsor, sponsor_username, enabled, next_run_at, created_at, updated_at)
364 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
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 asked365 )
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened366 .bind(id, agent.id, ctx.workspaceId, ctx.slug, r.name, r.instructions, schedule, JSON.stringify(r.events), JSON.stringify(r.repos), r.channel_id, channelName, ctx.viewer.id, ctx.viewer.username, r.enabled !== false ? 1 : 0, next, now.toISOString(), now.toISOString())
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 asked367 .run();
368 }
369 const row = await ctx.db.prepare("SELECT * FROM agent_routines WHERE id = ?").bind(id).first<RoutineRow>();
370 return ok(toRoutine(row!));
371}
372
373function channelNameIn(sidebar: unknown, id: string): string | null {
374 const seen: unknown[] = [sidebar];
375 while (seen.length) {
376 const value = seen.pop();
377 if (Array.isArray(value)) seen.push(...value);
378 else if (value && typeof value === "object") {
379 const v = value as Record<string, unknown>;
380 if (v.id === id && typeof v.name === "string") return v.name;
381 seen.push(...Object.values(v));
382 }
383 }
384 return null;
385}
386
387export async function deleteRoutine(ctx: ViewContext, handle: string, id: string): Promise<Result<null>> {
388 if (!ctx.owner) return fail("forbidden", "Only the workspace's owners change routines.");
389 const agent = await agentByHandle(ctx, handle);
390 if (!agent) return fail("not_found", `There is no agent called @${handle}.`);
391 await ctx.db.prepare("DELETE FROM agent_routines WHERE id = ? AND agent_id = ?").bind(String(id ?? ""), agent.id).run();
392 return ok(null);
393}
394
395export async function runRoutineNow(ctx: ViewContext, handle: string, id: string): Promise<Result<AgentSession>> {
396 if (!ctx.owner) return fail("forbidden", "Only the workspace's owners run routines by hand.");
397 const agent = await agentByHandle(ctx, handle);
398 if (!agent) return fail("not_found", `There is no agent called @${handle}.`);
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.399 if (isPersonal(agent)) return fail("invalid", PERSONAL_ROUTINES);
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 asked400 const routine = await ctx.db.prepare("SELECT * FROM agent_routines WHERE id = ? AND agent_id = ?").bind(String(id ?? ""), agent.id).first<RoutineRow>();
401 if (!routine) return fail("not_found", "There is no such routine.");
402 const ran = await runRoutine(ctx.env, routine, agent, ctx.slug);
403 if (!ran.ok) return fail("invalid", ran.message);
404 const row = await sessionRow(ctx.db, ran.session);
405 return ok((await sessionsOut(ctx, [row!]))[0]);
406}
407
408// ── Spend ─────────────────────────────────────────────────────────────────
409
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.410const KIND_LABELS: Record<string, string> = { [BUILDER_ID]: BUILDER_LABEL, reply: "Chat replies", chat: "Sessions", routine: "Routines", helper: "Helping colleagues", subagent: "Subagents" };
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 asked411
412function slices(rows: { key: string | null; micros: number; n: number }[], label: (key: string) => string): SpendSlice[] {
413 return rows
414 .filter((r) => r.micros > 0 || r.n > 0)
415 .map((r) => ({ key: r.key ?? "", label: label(r.key ?? ""), micros: r.micros, count: r.n }))
416 .sort((a, b) => b.micros - a.micros);
417}
418
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.419/** The most channels named in a breakdown; the rest of the channels fold into one slice. */
420const CHANNEL_SLICES = 12;
421
422/**
423 * Spend by where it was asked: the costliest channels by name, if the
424 * viewer can read them; direct messages together; channels they can't
425 * read, and channels past the first few, folded together without names.
426 */
427async function channelSlices(ctx: ViewContext, rows: { key: string | null; label: string | null; micros: number; n: number }[]): Promise<SpendSlice[]> {
428 const ranked = rows.filter((r) => r.micros > 0 || r.n > 0).sort((a, b) => b.micros - a.micros);
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.429 const named = ranked.filter((r) => r.key && r.key !== "dm" && r.key !== BUILDER_ID).slice(0, CHANNEL_SLICES);
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.430 const can = await readable(ctx, named.map((r) => r.key!));
431 const out: SpendSlice[] = [];
432 const hidden: SpendSlice = { key: "private", label: "Channels you're not in", micros: 0, count: 0 };
433 const rest: SpendSlice = { key: "other", label: "Other channels", micros: 0, count: 0 };
434 for (const row of ranked) {
435 if (row.key === "dm") out.push({ key: "dm", label: "Direct messages", micros: row.micros, count: row.n });
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.436 else if (row.key === BUILDER_ID) out.push({ key: BUILDER_ID, label: BUILDER_LABEL, micros: row.micros, count: row.n });
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.437 else if (!named.includes(row)) (rest.micros += row.micros), (rest.count += row.n);
438 else if (can.has(row.key!)) out.push({ key: row.key!, label: row.label ? `#${row.label}` : "A channel", micros: row.micros, count: row.n });
439 else (hidden.micros += row.micros), (hidden.count += row.n);
440 }
441 for (const folded of [hidden, rest]) if (folded.count > 0) out.push(folded);
442 return out.sort((a, b) => b.micros - a.micros);
443}
444
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 asked445/**
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.446 * Budgets per person this month (person-budget.ts): owners see everyone's,
447 * anyone else only their own.
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 asked448 */
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.449export async function personBudgetsView(ctx: ViewContext): Promise<Result<PersonBudgets>> {
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 asked450 const now = new Date();
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.451 const policyRow = await readPolicy(ctx.db, ctx.workspaceId, monthKey(now));
452 const only = ctx.owner ? null : ctx.viewer.kind === "workspace" ? null : ctx.viewer.username.toLowerCase();
453 return ok(await personBudgets(ctx.db, ctx.workspaceId, policyRow.person_monthly_micros, now, only));
454}
455
456/** Gives a person a budget of their own (0: none at all), or with null puts them back on the default. Owners only. */
457export async function setPersonBudget(ctx: ViewContext, username: unknown, monthly: unknown): Promise<Result<PersonBudgets>> {
458 if (!ctx.owner) return fail("forbidden", "Only the workspace's owners set budgets for people.");
459 const name = cleanUsername(username);
460 if (!name) return fail("invalid", "That isn't a username.");
461 if (monthly === null || monthly === undefined) {
462 await ctx.db.prepare("DELETE FROM person_budgets WHERE workspace_id = ? AND username = ?").bind(ctx.workspaceId, name).run();
463 } else {
464 const value = Number(monthly);
465 if (!Number.isFinite(value) || value < 0 || value > 1_000_000_000_000) return fail("invalid", "A budget is a positive amount, or none.");
466 await ctx.db
467 .prepare(
468 `INSERT INTO person_budgets (workspace_id, username, monthly_micros, updated_by, updated_at) VALUES (?1, ?2, ?3, ?4, ?5)
469 ON CONFLICT (workspace_id, username) DO UPDATE SET monthly_micros = ?3, updated_by = ?4, updated_at = ?5`,
470 )
471 .bind(ctx.workspaceId, name, Math.floor(value), ctx.viewer.username, new Date().toISOString())
472 .run();
473 }
474 return personBudgetsView(ctx);
475}
476
477/**
478 * Where the spend went over a span of days (this month unless asked), for
479 * one agent (what it was paid for: its replies and every session it paid
480 * for, colleagues' help included) or for every agent; for everyone, or only
481 * the work one person asked for.
482 */
483export async function spend(ctx: ViewContext, handle: string | null, options: { period?: unknown; person?: unknown } = {}): Promise<Result<AgentSpendBreakdown>> {
484 const now = new Date();
485 const span = spendSpan(options.period, now);
486 const from = `${span.from}T00:00:00.000Z`;
487 const end = new Date(`${span.until}T00:00:00.000Z`);
488 end.setUTCDate(end.getUTCDate() + 1);
489 const to = end.toISOString();
490 const person = options.person == null || options.person === "" ? null : cleanUsername(options.person);
491 if (options.person != null && options.person !== "" && !person) return fail("invalid", "That isn't a username.");
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 asked492 let agentId: string | null = null;
493 if (handle) {
494 const agent = await agentByHandle(ctx, handle);
495 if (!agent) return fail("not_found", `There is no agent called @${handle}.`);
496 agentId = agent.id;
497 }
498 const db = ctx.db;
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.499 const who = person ? " AND asked_by_username = ?4" : "";
500 const rFilter = `${agentId ? "agent_id = ?2" : "workspace_id = ?2"} AND created_at >= ?1 AND created_at < ?3${who}`;
501 const sFilter = `${agentId ? "payer_agent_id = ?2" : "workspace_id = ?2"} AND created_at >= ?1 AND created_at < ?3${who}`;
502 const binds: string[] = [from, agentId ?? ctx.workspaceId, to, ...(person ? [person] : [])];
503 // Where it was asked: a channel by id; direct messages (a reply's channel has no name) together.
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.504 // Drafting and trying new agents (builder.ts) is its own kind, agent and place.
505 const union = `SELECT CASE WHEN agent_id = '${BUILDER_ID}' THEN '${BUILDER_ID}' ELSE 'reply' END AS kind, agent_id AS agent, asked_by_username AS person, model, charged_micros AS micros, substr(created_at, 1, 10) AS day,
506 CASE WHEN agent_id = '${BUILDER_ID}' THEN '${BUILDER_ID}' WHEN channel_name IS NULL THEN 'dm' ELSE channel_id END AS channel, channel_name AS channel_label FROM agent_replies WHERE ${rFilter}
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.507 UNION ALL SELECT kind, payer_agent_id AS agent, asked_by_username AS person, model, charged_micros AS micros, substr(created_at, 1, 10) AS day,
508 CASE WHEN channel_kind = 'dm' THEN 'dm' ELSE channel_id END AS channel, channel_name AS channel_label FROM agent_sessions WHERE ${sFilter} AND parent_id IS NULL
509 UNION ALL SELECT kind, payer_agent_id AS agent, asked_by_username AS person, model, 0 AS micros, substr(created_at, 1, 10) AS day,
510 CASE WHEN channel_kind = 'dm' THEN 'dm' ELSE channel_id END AS channel, channel_name AS channel_label FROM agent_sessions WHERE ${sFilter} AND parent_id IS NOT 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 asked511 // A child's spend is already counted on its root (sessions.ts), so children add counts, not money.
512 const group = (column: string) =>
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.513 db.prepare(`SELECT ${column} AS key, COALESCE(SUM(micros), 0) AS micros, COUNT(*) AS n FROM (${union}) GROUP BY ${column}`).bind(...binds).all<{ key: string | null; micros: number; n: number }>();
514 const [byKind, byModel, byPerson, byAgent, byDay, byChannel, top, agents] = 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 asked515 group("kind"),
516 group("model"),
517 group("person"),
518 group("agent"),
519 group("day"),
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.520 db
521 .prepare(`SELECT channel AS key, MAX(channel_label) AS label, COALESCE(SUM(micros), 0) AS micros, COUNT(*) AS n FROM (${union}) GROUP BY channel`)
522 .bind(...binds)
523 .all<{ key: string | null; label: string | null; micros: number; n: number }>(),
524 db.prepare(`SELECT * FROM agent_sessions WHERE ${sFilter} AND parent_id IS NULL ORDER BY charged_micros DESC LIMIT 8`).bind(...binds).all<SessionRow>(),
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 asked525 faces(ctx),
526 ]);
An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how.527 const [channels, onTeams] = await Promise.all([channelSlices(ctx, byChannel.results), teamsOfAgents(ctx)]);
528 // By the teams each agent is on (identity): an agent on two teams counts toward both.
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 asked529 const byTeamMap = new Map<string, SpendSlice>();
An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how.530 const add = (key: string, label: string, row: { micros: number; n: number }) => {
531 const slice = byTeamMap.get(key) ?? { key, label, micros: 0, count: 0 };
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 asked532 slice.micros += row.micros;
533 slice.count += row.n;
An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how.534 byTeamMap.set(key, slice);
535 };
536 for (const row of byAgent.results) {
537 if (row.key === BUILDER_ID) {
538 add(BUILDER_ID, BUILDER_LABEL, row);
539 continue;
540 }
541 const teams = onTeams.get(row.key ?? "") ?? [];
542 if (!teams.length) add("", "Not on a team", row);
543 for (const team of teams) add(team.slug, team.name, row);
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 asked544 }
545 const total = byKind.results.reduce((n, r) => n + r.micros, 0);
546 return ok({
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.547 period: span.period,
548 span: span.span,
549 from: span.from,
550 until: span.until,
551 person,
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 asked552 total_micros: total,
553 by_kind: slices(byKind.results, (k) => KIND_LABELS[k] ?? k),
554 by_model: slices(byModel.results, (k) => k || "No model"),
555 by_person: slices(byPerson.results, (k) => (k ? `@${k}` : "Routines and agents")),
556 by_agent: slices(byAgent.results, (k) => {
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.557 if (k === BUILDER_ID) return BUILDER_LABEL;
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 asked558 const face = agents.get(k);
559 return face ? `${face.display_name} (@${face.handle})` : "An archived agent";
560 }),
561 by_team: [...byTeamMap.values()].sort((a, b) => b.micros - a.micros),
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.562 by_channel: channels,
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 asked563 top_sessions: await sessionsOut(ctx, top.results, agents),
564 days: byDay.results.map((r) => ({ day: r.key ?? "", micros: r.micros })).sort((a, b) => a.day.localeCompare(b.day)),
565 });
566}
567
568// ── Activity and versions ────────────────────────────────────────────────
569
570export async function activity(ctx: ViewContext, handle: string): Promise<Result<AgentActivity[]>> {
571 const agent = await agentByHandle(ctx, handle);
572 if (!agent) return fail("not_found", `There is no agent called @${handle}.`);
573 const [replies, sessions] = await Promise.all([
574 ctx.db
575 .prepare(
576 "SELECT id, status, channel_id, channel_name, asked_by_username, model, tool_count, charged_micros, created_at, reply_id FROM agent_replies WHERE agent_id = ? ORDER BY created_at DESC LIMIT 60",
577 )
578 .bind(agent.id)
579 .all<{ id: string; status: string; channel_id: string; channel_name: string | null; asked_by_username: string | null; model: string | null; tool_count: number | null; charged_micros: number; created_at: string; reply_id: string | null }>(),
580 ctx.db.prepare("SELECT * FROM agent_sessions WHERE agent_id = ? ORDER BY created_at DESC LIMIT 40").bind(agent.id).all<SessionRow>(),
581 ]);
582 const can = await readable(ctx, [...replies.results.map((r) => r.channel_id), ...sessions.results.map((s) => s.channel_id)]);
583 const items: AgentActivity[] = [
584 ...replies.results.map((r) => {
585 const visible = can.has(r.channel_id);
586 return {
587 id: r.id,
588 kind: "reply" as const,
589 status: r.status,
590 channel_id: r.channel_id,
591 channel_name: visible ? r.channel_name : null,
592 title: null,
593 asked_by_username: visible ? r.asked_by_username : null,
594 model: r.model,
595 tools: r.tool_count ?? 0,
596 charged_micros: r.charged_micros,
597 created_at: r.created_at,
598 visible,
599 ref: visible ? r.reply_id : null,
600 };
601 }),
602 ...sessions.results.map((s) => {
603 const visible = can.has(s.channel_id);
604 return {
605 id: s.id,
606 kind: "session" as const,
607 status: s.status,
608 channel_id: s.channel_id,
609 channel_name: visible ? s.channel_name : null,
610 title: visible ? s.title : null,
611 asked_by_username: visible ? s.asked_by_username : null,
612 model: s.model,
613 tools: s.tool_calls,
614 charged_micros: s.charged_micros,
615 created_at: s.created_at,
616 visible,
617 ref: s.id,
618 };
619 }),
620 ];
621 return ok(items.sort((a, b) => b.created_at.localeCompare(a.created_at)).slice(0, 80));
622}
623
624export async function versions(ctx: ViewContext, handle: string): Promise<Result<AgentVersion[]>> {
625 const agent = await agentByHandle(ctx, handle);
626 if (!agent) return fail("not_found", `There is no agent called @${handle}.`);
627 const rows = await ctx.db
628 .prepare("SELECT version, definition, changed_by, created_at FROM agent_versions WHERE agent_id = ? ORDER BY version DESC LIMIT 50")
629 .bind(agent.id)
630 .all<{ version: number; definition: string; changed_by: string; created_at: string }>();
631 return ok(
632 rows.results.map((r) => {
An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how.633 let definition: Record<string, unknown> = {};
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 asked634 try {
An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how.635 // Teams are memberships, not part of a version: older versions' team and department are left out.
636 const { team: _team, department: _department, ...rest } = JSON.parse(r.definition) as Record<string, unknown>;
637 definition = rest;
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 asked638 } catch {
639 // An unreadable old version shows as empty.
640 }
641 return { version: r.version, changed_by: r.changed_by, created_at: r.created_at, definition };
642 }),
643 );
644}
645
646// ── Policy and overview ──────────────────────────────────────────────────
647
648export async function policy(ctx: ViewContext): Promise<Result<AgentPolicy>> {
649 const row = await readPolicy(ctx.db, ctx.workspaceId, monthKey(new Date()));
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.650 return ok(policyOf(row));
651}
652
653function policyOf(row: AgentPolicy): AgentPolicy {
654 return {
655 monthly_micros: row.monthly_micros,
656 default_agent_monthly_micros: row.default_agent_monthly_micros,
657 default_session_micros: row.default_session_micros,
658 person_monthly_micros: row.person_monthly_micros,
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.659 members_create_agents: row.members_create_agents !== false,
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.660 };
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 asked661}
662
663export async function setPolicy(ctx: ViewContext, changes: Partial<AgentPolicy>): Promise<Result<AgentPolicy>> {
664 if (!ctx.owner) return fail("forbidden", "Only the workspace's owners set its agents' budget.");
665 const current = await policy(ctx);
666 const checked = checkPolicy(current.ok ? current.value : DEFAULT_POLICY, changes ?? {});
667 if (!checked.ok) return fail("invalid", checked.message);
668 const p = checked.value;
669 await ctx.db
670 .prepare(
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.671 `INSERT INTO agent_policies (workspace_id, monthly_micros, default_agent_monthly_micros, default_session_micros, person_monthly_micros, members_create_agents, updated_by, updated_at)
672 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
673 ON CONFLICT (workspace_id) DO UPDATE SET monthly_micros = ?2, default_agent_monthly_micros = ?3, default_session_micros = ?4, person_monthly_micros = ?5,
674 members_create_agents = ?6, updated_by = ?7, updated_at = ?8`,
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 asked675 )
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.676 .bind(ctx.workspaceId, p.monthly_micros, p.default_agent_monthly_micros, p.default_session_micros, p.person_monthly_micros, p.members_create_agents ? 1 : 0, ctx.viewer.username, new Date().toISOString())
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 asked677 .run();
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.678 const current_ = current.ok ? current.value : DEFAULT_POLICY;
679 if (current_.members_create_agents !== p.members_create_agents) {
680 ctx.audit?.("set_agent_policy", "policy", p.members_create_agents ? "Let members create personal agents" : "Turned off personal agents for members");
681 }
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 asked682 return ok(p);
683}
684
685export async function overview(ctx: ViewContext): Promise<Result<AgentsOverview>> {
686 const now = new Date();
687 const db = ctx.db;
688 const [rows, policyRow, live, recent, upcoming, breakdown, agentFaces] = await Promise.all([
689 db.prepare(`${selectAgents("a.workspace_id = ?3 AND a.archived_at IS NULL")} ORDER BY a.builtin DESC, a.handle`).bind(...periods(now), ctx.workspaceId).all<Row>(),
690 readPolicy(db, ctx.workspaceId, monthKey(now)),
691 db
692 .prepare(`SELECT * FROM agent_sessions WHERE workspace_id = ? AND status IN (${LIVE.map(() => "?").join(", ")}) ORDER BY created_at DESC LIMIT 60`)
693 .bind(ctx.workspaceId, ...LIVE)
694 .all<SessionRow>(),
695 db
696 .prepare(`SELECT * FROM agent_sessions WHERE workspace_id = ? AND parent_id IS NULL AND status IN ('done','failed','stopped') ORDER BY finished_at DESC LIMIT 12`)
697 .bind(ctx.workspaceId)
698 .all<SessionRow>(),
699 db.prepare("SELECT * FROM agent_routines WHERE workspace_id = ? AND enabled = 1 AND next_run_at IS NOT NULL ORDER BY next_run_at LIMIT 6").bind(ctx.workspaceId).all<RoutineRow>(),
700 spend(ctx, null),
701 faces(ctx),
702 ]);
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.703 // The workspace's agents and the viewer's own personal ones; other members' are theirs.
704 const agents: WorkspaceAgent[] = rows.results.filter((row) => !isPersonal(row) || isOwnerOf(ctx.viewer, row)).map((row) => toAgent(row, now));
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 asked705 const liveSessions = await sessionsOut(ctx, live.results, agentFaces);
706 const liveByAgent: Record<string, number> = {};
707 for (const s of live.results) liveByAgent[s.agent_id] = (liveByAgent[s.agent_id] ?? 0) + 1;
708 const level = policyRow.monthly_micros ? [100, 90, 75].find((l) => (policyRow.spent * 100) / policyRow.monthly_micros! >= l) ?? null : null;
709 return ok({
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.710 policy: policyOf(policyRow),
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 asked711 spent_month_micros: policyRow.spent,
712 alert: level,
713 agents,
714 live_by_agent: liveByAgent,
715 live: liveSessions.filter((s) => s.visible && !s.parent_id).slice(0, 20),
716 waiting_on_you: ctx.owner ? liveSessions.filter((s) => s.status === "needs_approval") : liveSessions.filter((s) => s.status === "needs_approval" && s.visible && s.asked_by === ctx.viewer.id),
717 recent: (await sessionsOut(ctx, recent.results, agentFaces)).filter((s) => s.visible).slice(0, 8),
718 upcoming: upcoming.results.map((r) => {
719 const face = agentFaces.get(r.agent_id);
720 return { ...toRoutine(r), agent_handle: face?.handle ?? "agent", agent_name: face?.display_name ?? "An agent" };
721 }),
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.722 spend: breakdown.ok ? breakdown.value : emptySpend(now),
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 asked723 can_manage: ctx.owner,
724 });
725}
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.726
727/** A breakdown with nothing in it, for this month. */
728function emptySpend(now: Date): AgentSpendBreakdown {
729 const span = spendSpan("month", now);
730 return { ...span, person: null, total_micros: 0, by_kind: [], by_model: [], by_person: [], by_agent: [], by_team: [], by_channel: [], top_sessions: [], days: [] };
731}