Skip to content
734 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,
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.14 type AgentLook,
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 asked15 type AgentMemory,
16 type AgentPolicy,
17 type AgentRoutine,
18 type AgentSession,
19 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.20 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 asked21 type AgentSpendBreakdown,
22 type AgentVersion,
23 type AgentsOverview,
24 type AgentMemoryScope,
25 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.26 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 asked27 type Result,
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened28 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 asked29 type SessionEvent,
30 type SpendSlice,
31 type User,
32 type WorkspaceAgent,
33 chatClient,
34 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.35 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 asked36 newId,
37 ok,
38} from "@g1t/contracts";
39
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.40import { readLook } from "../../../packages/contracts/src/agent-look.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.41import { monthKey, spendSpan } from "./budget.ts";
42import { 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 asked43import { type MemoryRow, type MemoryViewer, changeableBy, cleanFact, toMemory, visibleTo } from "./memory.ts";
44import { DEFAULT_POLICY, checkPolicy, readPolicy } from "./policy.ts";
45import { type RoutineRow, MAX_ROUTINES, checkRoutine, newRoutineId, nextRun, runRoutine, toRoutine } from "./routines.ts";
46import { 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.47import { 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 opened48import { 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.49import { teamsSection } from "./teammates.ts";
50import { 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.51import { canSeeAgent, isOwnerOf } from "./access.ts";
52import { 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 asked53
54export type ViewContext = {
55 env: SessionEnv;
56 db: D1Database;
57 slug: string;
58 workspaceId: string;
59 viewer: User;
60 /** Whether the viewer owns the workspace (or is its token). */
61 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.62 /** Records a change in the workspace's audit log, as the viewer (index.ts). */
63 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 asked64};
65
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.66type AgentFace = { handle: string; display_name: string; avatar_seed: string; look: AgentLook | 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 asked67
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.68/** The workspace's agents by id, archived ones too, for names and faces on sessions and spend. */
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 asked69async function faces(ctx: ViewContext): Promise<Map<string, AgentFace>> {
70 const rows = await ctx.db
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.71 .prepare("SELECT id, handle, display_name, avatar_seed, look 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 asked72 .bind(ctx.workspaceId)
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.73 .all<{ id: string; handle: string; display_name: string; avatar_seed: string | null; look: string | null }>();
74 return new Map(rows.results.map((r) => [r.id, { handle: r.handle, display_name: r.display_name, avatar_seed: r.avatar_seed || r.handle, look: readLook(r.look) }]));
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 asked75}
76
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.77/** 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. */
78async function teamsOfAgents(ctx: ViewContext): Promise<Map<string, { slug: string; name: string }[]>> {
79 const out = new Map<string, { slug: string; name: string }[]>();
80 const directory = await identityClient(ctx.env.IDENTITY)
81 .peopleDirectory(ctx.viewer, ctx.slug)
82 .catch(() => null);
83 if (!directory?.ok) return out;
84 for (const team of directory.value.teams) {
85 for (const id of team.agent_ids) out.set(id, [...(out.get(id) ?? []), { slug: team.slug, name: team.name }]);
86 }
87 return out;
88}
89
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 asked90/**
91 * Which of these conversations the viewer is in (or can read: a public
92 * channel). Asked of chat once per conversation, at most 60.
93 */
94async function readable(ctx: ViewContext, channelIds: string[]): Promise<Set<string>> {
95 const out = new Set<string>();
96 if (ctx.viewer.kind === "workspace") return out;
97 const chat = chatClient(ctx.env.CHAT);
98 const ids = [...new Set(channelIds)].slice(0, 60);
99 await Promise.all(
100 ids.map(async (id) => {
101 const audience = await chat.audience(ctx.slug, id).catch(() => null);
102 if (audience?.ok && (audience.value.kind === "public" || audience.value.member_user_ids.includes(ctx.viewer.id))) out.add(id);
103 }),
104 );
105 return out;
106}
107
108async function sessionsOut(ctx: ViewContext, rows: SessionRow[], agents?: Map<string, AgentFace>): Promise<AgentSession[]> {
109 const names = agents ?? (await faces(ctx));
110 const can = await readable(ctx, rows.map((r) => r.channel_id));
111 return rows.map((row) => toSession(row, names.get(row.agent_id) ?? null, can.has(row.channel_id)));
112}
113
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/** 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 asked115async 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.116 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 asked117 .prepare("SELECT * FROM agents WHERE workspace_id = ? AND handle = ? AND archived_at IS NULL")
118 .bind(ctx.workspaceId, String(handle ?? "").trim().replace(/^@/, "").toLowerCase())
119 .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.120 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 asked121}
122
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.123// ── Teams ─────────────────────────────────────────────────────────────────
124
125/**
126 * What an agent is told about its teams this turn, word for word as it
127 * gets it (teammates.ts), for its People profile and its teams' pages.
128 * Null when it is on no visible team.
129 */
130export async function teamContext(ctx: ViewContext, handle: string): Promise<Result<AgentTeamContext>> {
131 const agent = await agentByHandle(ctx, handle);
132 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.133 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.134 if (!here) return fail("not_found", "Its teams could not be read just now.");
135 return ok({ handle: agent.handle, teams: here.teams.map((team) => team.slug), text: teamsSection(agent.id, here, new Date()) });
136}
137
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 asked138// ── Sessions ──────────────────────────────────────────────────────────────
139
140export async function listSessions(ctx: ViewContext, filter: { handle?: string | null; status?: "live" | "done" | null; limit?: number | null }): Promise<Result<AgentSession[]>> {
141 const limit = Math.min(200, Math.max(1, Math.floor(Number(filter.limit) || 50)));
142 const where = ["workspace_id = ?"];
143 const binds: (string | number)[] = [ctx.workspaceId];
144 if (filter.handle) {
145 const agent = await agentByHandle(ctx, filter.handle);
146 if (!agent) return fail("not_found", `There is no agent called @${filter.handle}.`);
147 where.push("agent_id = ?");
148 binds.push(agent.id);
149 }
150 if (filter.status === "live") where.push(`status IN (${LIVE.map(() => "?").join(", ")})`), binds.push(...LIVE);
151 if (filter.status === "done") where.push(`status NOT IN (${LIVE.map(() => "?").join(", ")})`), binds.push(...LIVE);
152 const rows = await ctx.db
153 .prepare(`SELECT * FROM agent_sessions WHERE ${where.join(" AND ")} ORDER BY created_at DESC LIMIT ?`)
154 .bind(...binds, limit)
155 .all<SessionRow>();
156 return ok(await sessionsOut(ctx, rows.results));
157}
158
159export async function sessionDetail(ctx: ViewContext, id: string): Promise<Result<AgentSessionDetail>> {
160 const row = await sessionRow(ctx.db, String(id ?? ""));
161 if (!row || row.workspace_id !== ctx.workspaceId) return fail("not_found", "There is no such session.");
162 const agents = await faces(ctx);
163 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>();
164 const [session] = await sessionsOut(ctx, [row], agents);
165 const tree = await sessionsOut(ctx, treeRows.results, agents);
166 let events: SessionEvent[] = [];
167 if (session.visible) {
168 const rows = await ctx.db
169 .prepare("SELECT seq, kind, by_name, body, tool, outcome, created_at FROM agent_session_events WHERE session_id = ? ORDER BY seq LIMIT 1000")
170 .bind(row.id)
171 .all<{ seq: number; kind: SessionEvent["kind"]; by_name: string | null; body: string; tool: string | null; outcome: string | null; created_at: string }>();
172 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 }));
173 }
174 const live = LIVE.includes(row.status as (typeof LIVE)[number]);
175 return ok({
176 session,
177 events,
178 tree,
179 can_stop: session.visible && live,
180 can_steer: session.visible,
181 can_approve: row.status === "needs_approval" && (ctx.owner || (session.visible && row.asked_by === ctx.viewer.id && ctx.owner)),
182 });
183}
184
185async function visibleSession(ctx: ViewContext, id: string): Promise<Result<SessionRow>> {
186 const row = await sessionRow(ctx.db, String(id ?? ""));
187 if (!row || row.workspace_id !== ctx.workspaceId) return fail("not_found", "There is no such session.");
188 const can = await readable(ctx, [row.channel_id]);
189 if (!can.has(row.channel_id)) return fail("not_found", "There is no such session.");
190 return ok(row);
191}
192
193export async function stopSession(ctx: ViewContext, id: string): Promise<Result<AgentSession>> {
194 const found = await visibleSession(ctx, id);
195 if (!found.ok) return found;
196 if (!LIVE.includes(found.value.status as (typeof LIVE)[number])) return fail("invalid", "That session is already over.");
197 const row = await stop(ctx.env, found.value, ctx.viewer.username);
198 return ok((await sessionsOut(ctx, [row]))[0]);
199}
200
201export async function steerSession(ctx: ViewContext, id: string, body: string): Promise<Result<AgentSession>> {
202 const found = await visibleSession(ctx, id);
203 if (!found.ok) return found;
204 const text = typeof body === "string" ? body.trim() : "";
205 if (!text) return fail("invalid", "Say something to the session.");
206 const row = await steer(ctx.env, found.value, ctx.viewer.username, text.slice(0, 4000));
207 return ok((await sessionsOut(ctx, [row]))[0]);
208}
209
210/** Owners raise a session's cap; it must end up above what it has spent. */
211export async function approveSession(ctx: ViewContext, id: string, capMicros: number): Promise<Result<AgentSession>> {
212 if (!ctx.owner) return fail("forbidden", "Only the workspace's owners can approve more spend.");
213 const row = await sessionRow(ctx.db, String(id ?? ""));
214 if (!row || row.workspace_id !== ctx.workspaceId) return fail("not_found", "There is no such session.");
215 if (row.status !== "needs_approval") return fail("invalid", "That session isn't waiting for approval.");
216 const cap = Math.floor(Number(capMicros));
217 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.");
218 const fresh = await approve(ctx.env, row, ctx.viewer.username, cap);
219 return ok((await sessionsOut(ctx, [fresh]))[0]);
220}
221
222// ── Memory ────────────────────────────────────────────────────────────────
223
224async function memoryViewer(ctx: ViewContext, rows: MemoryRow[]): Promise<MemoryViewer> {
225 const channels = await readable(ctx, rows.filter((r) => r.scope === "channel").map((r) => r.scope_ref));
226 return { id: ctx.viewer.id, owner: ctx.owner, inChannel: (id) => channels.has(id) };
227}
228
229export async function memories(ctx: ViewContext, handle: string): Promise<Result<AgentMemory[]>> {
230 const agent = await agentByHandle(ctx, handle);
231 if (!agent) return fail("not_found", `There is no agent called @${handle}.`);
232 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>();
233 const viewer = await memoryViewer(ctx, rows.results);
234 return ok(rows.results.filter((row) => visibleTo(row, viewer)).map(toMemory));
235}
236
237/**
238 * A fact a person gives an agent. Workspace facts are the owners'; a
239 * channel fact needs the person to be in that channel; a person fact is
240 * always their own.
241 */
242export async function remember(ctx: ViewContext, handle: string, input: { body: string; scope: AgentMemoryScope; scope_ref?: string | null }): Promise<Result<AgentMemory>> {
243 const agent = await agentByHandle(ctx, handle);
244 if (!agent) return fail("not_found", `There is no agent called @${handle}.`);
245 const body = cleanFact(input?.body);
246 if (!body) return fail("invalid", "Say what it should remember.");
247 let scope: AgentMemoryScope = input?.scope === "workspace" || input?.scope === "channel" ? input.scope : "person";
248 let ref = "";
249 let label: string | null = null;
250 if (scope === "workspace" && !ctx.owner) return fail("forbidden", "Only owners give an agent facts for the whole workspace.");
251 if (scope === "channel") {
252 ref = String(input.scope_ref ?? "");
253 const can = await readable(ctx, [ref]);
254 if (!can.has(ref)) return fail("forbidden", "You can only give it facts for conversations you're in.");
255 const audience = await chatClient(ctx.env.CHAT).audience(ctx.slug, ref).catch(() => null);
256 label = audience?.ok && "name" in audience.value ? ((audience.value as { name?: string | null }).name ?? null) : null;
257 }
258 if (scope === "person") {
259 scope = "person";
260 ref = ctx.viewer.id;
261 label = ctx.viewer.username;
262 }
263 const id = newId("mem");
264 const now = new Date().toISOString();
265 await ctx.db
266 .prepare(
267 `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)
268 VALUES (?, ?, ?, ?, ?, ?, ?, 'person', ?, ?, ?, 'user', 1, ?, ?)`,
269 )
270 .bind(id, agent.id, ctx.workspaceId, scope, ref, label, body, ctx.viewer.username, `@${ctx.viewer.username}`, ctx.viewer.username, now, now)
271 .run();
272 const row = await ctx.db.prepare("SELECT * FROM agent_memories WHERE id = ?").bind(id).first<MemoryRow>();
273 return ok(toMemory(row!));
274}
275
276async function changeable(ctx: ViewContext, handle: string, id: string): Promise<Result<MemoryRow>> {
277 const agent = await agentByHandle(ctx, handle);
278 if (!agent) return fail("not_found", `There is no agent called @${handle}.`);
279 const row = await ctx.db.prepare("SELECT * FROM agent_memories WHERE id = ? AND agent_id = ?").bind(String(id ?? ""), agent.id).first<MemoryRow>();
280 if (!row) return fail("not_found", "There is no such memory.");
281 const viewer = await memoryViewer(ctx, [row]);
282 if (!visibleTo(row, viewer)) return fail("not_found", "There is no such memory.");
283 if (!changeableBy(row, viewer)) return fail("forbidden", "Only owners change what an agent knows for the whole workspace.");
284 return ok(row);
285}
286
287export async function updateMemory(ctx: ViewContext, handle: string, id: string, changes: { body?: string; pinned?: boolean }): Promise<Result<AgentMemory>> {
288 const found = await changeable(ctx, handle, id);
289 if (!found.ok) return found;
290 const body = changes?.body === undefined ? found.value.body : cleanFact(changes.body);
291 if (!body) return fail("invalid", "A memory can't be empty; forget it instead.");
292 const pinned = changes?.pinned === undefined ? found.value.pinned : changes.pinned ? 1 : 0;
293 const now = new Date().toISOString();
294 const edited = body !== found.value.body;
295 await ctx.db
296 .prepare(
297 `UPDATE agent_memories SET body = ?, pinned = ?, updated_at = ?${edited ? ", source_kind = 'person', source_ref = ?, source_label = ?" : ""} WHERE id = ?`,
298 )
299 .bind(...(edited ? [body, pinned, now, ctx.viewer.username, `@${ctx.viewer.username} (corrected)`, found.value.id] : [body, pinned, now, found.value.id]))
300 .run();
301 const row = await ctx.db.prepare("SELECT * FROM agent_memories WHERE id = ?").bind(found.value.id).first<MemoryRow>();
302 return ok(toMemory(row!));
303}
304
305export async function forget(ctx: ViewContext, handle: string, id: string): Promise<Result<null>> {
306 const found = await changeable(ctx, handle, id);
307 if (!found.ok) return found;
308 await ctx.db.prepare("DELETE FROM agent_memories WHERE id = ?").bind(found.value.id).run();
309 return ok(null);
310}
311
312// ── Routines ──────────────────────────────────────────────────────────────
313
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.314/** Routines post where a person and the agent both are; a personal agent is only in its member's DM. */
315const PERSONAL_ROUTINES = "Personal agents can't run routines yet. An owner can promote it to a workspace agent, which can.";
316
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened317/** An agent's routines, and routines its responsibilities suggest that it doesn't have yet. */
318export 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 asked319 const agent = await agentByHandle(ctx, handle);
320 if (!agent) return fail("not_found", `There is no agent called @${handle}.`);
321 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 opened322 const list = rows.results.map(toRoutine);
323 const duties = definitionOf(agent).responsibilities;
324 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 asked325}
326
327/**
328 * Owners keep an agent's routines. The person who saves one becomes its
329 * sponsor: it runs with their access, in a channel they and the agent are in.
330 */
331export async function saveRoutine(ctx: ViewContext, handle: string, input: NewRoutine, id: string | null): Promise<Result<AgentRoutine>> {
332 if (!ctx.owner) return fail("forbidden", "Only the workspace's owners set up routines.");
333 if (ctx.viewer.kind === "workspace") return fail("invalid", "A routine runs with a person's access: set it up signed in as yourself.");
334 const agent = await agentByHandle(ctx, handle);
335 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.336 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 asked337 const checked = checkRoutine(input);
338 if (!checked.ok) return fail("invalid", checked.message);
339 const r = checked.value;
340 const audience = await chatClient(ctx.env.CHAT).audience(ctx.slug, r.channel_id).catch(() => null);
341 if (!audience?.ok || (audience.value.kind !== "public" && !audience.value.member_user_ids.includes(ctx.viewer.id))) {
342 return fail("invalid", "Choose a channel you're in.");
343 }
344 const channels = await chatClient(ctx.env.CHAT).sidebar(ctx.slug, ctx.viewer).catch(() => null);
345 const channelName = channels?.ok ? (channelNameIn(channels.value, r.channel_id) ?? null) : null;
346 const now = new Date();
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened347 const next = r.enabled !== false && r.schedule ? nextRun(r.schedule, now).toISOString() : null;
348 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 asked349 if (id) {
350 const existing = await ctx.db.prepare("SELECT id FROM agent_routines WHERE id = ? AND agent_id = ?").bind(id, agent.id).first();
351 if (!existing) return fail("not_found", "There is no such routine.");
352 await ctx.db
353 .prepare(
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened354 `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 asked355 enabled = ?, paused_note = NULL, next_run_at = ?, workspace = ?, updated_at = ? WHERE id = ?`,
356 )
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened357 .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 asked358 .run();
359 } else {
360 const count = await ctx.db.prepare("SELECT COUNT(*) AS n FROM agent_routines WHERE agent_id = ?").bind(agent.id).first<{ n: number }>();
361 if ((count?.n ?? 0) >= MAX_ROUTINES) return fail("invalid", `An agent keeps at most ${MAX_ROUTINES} routines.`);
362 id = newRoutineId();
363 await ctx.db
364 .prepare(
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened365 `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)
366 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 asked367 )
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened368 .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 asked369 .run();
370 }
371 const row = await ctx.db.prepare("SELECT * FROM agent_routines WHERE id = ?").bind(id).first<RoutineRow>();
372 return ok(toRoutine(row!));
373}
374
375function channelNameIn(sidebar: unknown, id: string): string | null {
376 const seen: unknown[] = [sidebar];
377 while (seen.length) {
378 const value = seen.pop();
379 if (Array.isArray(value)) seen.push(...value);
380 else if (value && typeof value === "object") {
381 const v = value as Record<string, unknown>;
382 if (v.id === id && typeof v.name === "string") return v.name;
383 seen.push(...Object.values(v));
384 }
385 }
386 return null;
387}
388
389export async function deleteRoutine(ctx: ViewContext, handle: string, id: string): Promise<Result<null>> {
390 if (!ctx.owner) return fail("forbidden", "Only the workspace's owners change routines.");
391 const agent = await agentByHandle(ctx, handle);
392 if (!agent) return fail("not_found", `There is no agent called @${handle}.`);
393 await ctx.db.prepare("DELETE FROM agent_routines WHERE id = ? AND agent_id = ?").bind(String(id ?? ""), agent.id).run();
394 return ok(null);
395}
396
397export async function runRoutineNow(ctx: ViewContext, handle: string, id: string): Promise<Result<AgentSession>> {
398 if (!ctx.owner) return fail("forbidden", "Only the workspace's owners run routines by hand.");
399 const agent = await agentByHandle(ctx, handle);
400 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.401 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 asked402 const routine = await ctx.db.prepare("SELECT * FROM agent_routines WHERE id = ? AND agent_id = ?").bind(String(id ?? ""), agent.id).first<RoutineRow>();
403 if (!routine) return fail("not_found", "There is no such routine.");
404 const ran = await runRoutine(ctx.env, routine, agent, ctx.slug);
405 if (!ran.ok) return fail("invalid", ran.message);
406 const row = await sessionRow(ctx.db, ran.session);
407 return ok((await sessionsOut(ctx, [row!]))[0]);
408}
409
410// ── Spend ─────────────────────────────────────────────────────────────────
411
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.412const 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 asked413
414function slices(rows: { key: string | null; micros: number; n: number }[], label: (key: string) => string): SpendSlice[] {
415 return rows
416 .filter((r) => r.micros > 0 || r.n > 0)
417 .map((r) => ({ key: r.key ?? "", label: label(r.key ?? ""), micros: r.micros, count: r.n }))
418 .sort((a, b) => b.micros - a.micros);
419}
420
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.421/** The most channels named in a breakdown; the rest of the channels fold into one slice. */
422const CHANNEL_SLICES = 12;
423
424/**
425 * Spend by where it was asked: the costliest channels by name, if the
426 * viewer can read them; direct messages together; channels they can't
427 * read, and channels past the first few, folded together without names.
428 */
429async function channelSlices(ctx: ViewContext, rows: { key: string | null; label: string | null; micros: number; n: number }[]): Promise<SpendSlice[]> {
430 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.431 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.432 const can = await readable(ctx, named.map((r) => r.key!));
433 const out: SpendSlice[] = [];
434 const hidden: SpendSlice = { key: "private", label: "Channels you're not in", micros: 0, count: 0 };
435 const rest: SpendSlice = { key: "other", label: "Other channels", micros: 0, count: 0 };
436 for (const row of ranked) {
437 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.438 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.439 else if (!named.includes(row)) (rest.micros += row.micros), (rest.count += row.n);
440 else if (can.has(row.key!)) out.push({ key: row.key!, label: row.label ? `#${row.label}` : "A channel", micros: row.micros, count: row.n });
441 else (hidden.micros += row.micros), (hidden.count += row.n);
442 }
443 for (const folded of [hidden, rest]) if (folded.count > 0) out.push(folded);
444 return out.sort((a, b) => b.micros - a.micros);
445}
446
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 asked447/**
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.448 * Budgets per person this month (person-budget.ts): owners see everyone's,
449 * 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 asked450 */
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.451export 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 asked452 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.453 const policyRow = await readPolicy(ctx.db, ctx.workspaceId, monthKey(now));
454 const only = ctx.owner ? null : ctx.viewer.kind === "workspace" ? null : ctx.viewer.username.toLowerCase();
455 return ok(await personBudgets(ctx.db, ctx.workspaceId, policyRow.person_monthly_micros, now, only));
456}
457
458/** Gives a person a budget of their own (0: none at all), or with null puts them back on the default. Owners only. */
459export async function setPersonBudget(ctx: ViewContext, username: unknown, monthly: unknown): Promise<Result<PersonBudgets>> {
460 if (!ctx.owner) return fail("forbidden", "Only the workspace's owners set budgets for people.");
461 const name = cleanUsername(username);
462 if (!name) return fail("invalid", "That isn't a username.");
463 if (monthly === null || monthly === undefined) {
464 await ctx.db.prepare("DELETE FROM person_budgets WHERE workspace_id = ? AND username = ?").bind(ctx.workspaceId, name).run();
465 } else {
466 const value = Number(monthly);
467 if (!Number.isFinite(value) || value < 0 || value > 1_000_000_000_000) return fail("invalid", "A budget is a positive amount, or none.");
468 await ctx.db
469 .prepare(
470 `INSERT INTO person_budgets (workspace_id, username, monthly_micros, updated_by, updated_at) VALUES (?1, ?2, ?3, ?4, ?5)
471 ON CONFLICT (workspace_id, username) DO UPDATE SET monthly_micros = ?3, updated_by = ?4, updated_at = ?5`,
472 )
473 .bind(ctx.workspaceId, name, Math.floor(value), ctx.viewer.username, new Date().toISOString())
474 .run();
475 }
476 return personBudgetsView(ctx);
477}
478
479/**
480 * Where the spend went over a span of days (this month unless asked), for
481 * one agent (what it was paid for: its replies and every session it paid
482 * for, colleagues' help included) or for every agent; for everyone, or only
483 * the work one person asked for.
484 */
485export async function spend(ctx: ViewContext, handle: string | null, options: { period?: unknown; person?: unknown } = {}): Promise<Result<AgentSpendBreakdown>> {
486 const now = new Date();
487 const span = spendSpan(options.period, now);
488 const from = `${span.from}T00:00:00.000Z`;
489 const end = new Date(`${span.until}T00:00:00.000Z`);
490 end.setUTCDate(end.getUTCDate() + 1);
491 const to = end.toISOString();
492 const person = options.person == null || options.person === "" ? null : cleanUsername(options.person);
493 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 asked494 let agentId: string | null = null;
495 if (handle) {
496 const agent = await agentByHandle(ctx, handle);
497 if (!agent) return fail("not_found", `There is no agent called @${handle}.`);
498 agentId = agent.id;
499 }
500 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.501 const who = person ? " AND asked_by_username = ?4" : "";
502 const rFilter = `${agentId ? "agent_id = ?2" : "workspace_id = ?2"} AND created_at >= ?1 AND created_at < ?3${who}`;
503 const sFilter = `${agentId ? "payer_agent_id = ?2" : "workspace_id = ?2"} AND created_at >= ?1 AND created_at < ?3${who}`;
504 const binds: string[] = [from, agentId ?? ctx.workspaceId, to, ...(person ? [person] : [])];
505 // 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.506 // Drafting and trying new agents (builder.ts) is its own kind, agent and place.
507 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,
508 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.509 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,
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 NULL
511 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,
512 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 asked513 // A child's spend is already counted on its root (sessions.ts), so children add counts, not money.
514 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.515 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 }>();
516 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 asked517 group("kind"),
518 group("model"),
519 group("person"),
520 group("agent"),
521 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.522 db
523 .prepare(`SELECT channel AS key, MAX(channel_label) AS label, COALESCE(SUM(micros), 0) AS micros, COUNT(*) AS n FROM (${union}) GROUP BY channel`)
524 .bind(...binds)
525 .all<{ key: string | null; label: string | null; micros: number; n: number }>(),
526 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 asked527 faces(ctx),
528 ]);
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.529 const [channels, onTeams] = await Promise.all([channelSlices(ctx, byChannel.results), teamsOfAgents(ctx)]);
530 // 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 asked531 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.532 const add = (key: string, label: string, row: { micros: number; n: number }) => {
533 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 asked534 slice.micros += row.micros;
535 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.536 byTeamMap.set(key, slice);
537 };
538 for (const row of byAgent.results) {
539 if (row.key === BUILDER_ID) {
540 add(BUILDER_ID, BUILDER_LABEL, row);
541 continue;
542 }
543 const teams = onTeams.get(row.key ?? "") ?? [];
544 if (!teams.length) add("", "Not on a team", row);
545 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 asked546 }
547 const total = byKind.results.reduce((n, r) => n + r.micros, 0);
548 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.549 period: span.period,
550 span: span.span,
551 from: span.from,
552 until: span.until,
553 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 asked554 total_micros: total,
555 by_kind: slices(byKind.results, (k) => KIND_LABELS[k] ?? k),
556 by_model: slices(byModel.results, (k) => k || "No model"),
557 by_person: slices(byPerson.results, (k) => (k ? `@${k}` : "Routines and agents")),
558 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.559 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 asked560 const face = agents.get(k);
An archived agent can be found, brought back, or deleted for good. The Agents page ends with Archived, folded, where owners see every archived agent and members their own personal ones, each with when and by whom it was archived and what deleting it would take; Restore brings it back, under a new handle when a live agent has taken its old one, and Delete permanently asks for the handle to be typed and then removes its sessions, replies, memories, routines, versions, drafts, skills and its computer's home, leaving its messages in chat under its saved name, the workspace's spend totals and the audit log. In Chat an archived agent is deactivated the way people expect: never offered as someone to message or mention, never woken, a conversation with it kept in the list, dimmed and marked Archived, open to read with a note in place of the composer and a way to restore it for owners; after deletion the conversation still reads, its messages marked as a deleted agent's. The agents, chat and agent budgets guides say so.561 // Archived agents are still in `agents`; one without a face was deleted for good (its spend stays in the month's total).
562 return face ? `${face.display_name} (@${face.handle})` : "A deleted agent";
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 }),
564 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.565 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 asked566 top_sessions: await sessionsOut(ctx, top.results, agents),
567 days: byDay.results.map((r) => ({ day: r.key ?? "", micros: r.micros })).sort((a, b) => a.day.localeCompare(b.day)),
568 });
569}
570
571// ── Activity and versions ────────────────────────────────────────────────
572
573export async function activity(ctx: ViewContext, handle: string): Promise<Result<AgentActivity[]>> {
574 const agent = await agentByHandle(ctx, handle);
575 if (!agent) return fail("not_found", `There is no agent called @${handle}.`);
576 const [replies, sessions] = await Promise.all([
577 ctx.db
578 .prepare(
579 "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",
580 )
581 .bind(agent.id)
582 .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 }>(),
583 ctx.db.prepare("SELECT * FROM agent_sessions WHERE agent_id = ? ORDER BY created_at DESC LIMIT 40").bind(agent.id).all<SessionRow>(),
584 ]);
585 const can = await readable(ctx, [...replies.results.map((r) => r.channel_id), ...sessions.results.map((s) => s.channel_id)]);
586 const items: AgentActivity[] = [
587 ...replies.results.map((r) => {
588 const visible = can.has(r.channel_id);
589 return {
590 id: r.id,
591 kind: "reply" as const,
592 status: r.status,
593 channel_id: r.channel_id,
594 channel_name: visible ? r.channel_name : null,
595 title: null,
596 asked_by_username: visible ? r.asked_by_username : null,
597 model: r.model,
598 tools: r.tool_count ?? 0,
599 charged_micros: r.charged_micros,
600 created_at: r.created_at,
601 visible,
602 ref: visible ? r.reply_id : null,
603 };
604 }),
605 ...sessions.results.map((s) => {
606 const visible = can.has(s.channel_id);
607 return {
608 id: s.id,
609 kind: "session" as const,
610 status: s.status,
611 channel_id: s.channel_id,
612 channel_name: visible ? s.channel_name : null,
613 title: visible ? s.title : null,
614 asked_by_username: visible ? s.asked_by_username : null,
615 model: s.model,
616 tools: s.tool_calls,
617 charged_micros: s.charged_micros,
618 created_at: s.created_at,
619 visible,
620 ref: s.id,
621 };
622 }),
623 ];
624 return ok(items.sort((a, b) => b.created_at.localeCompare(a.created_at)).slice(0, 80));
625}
626
627export async function versions(ctx: ViewContext, handle: string): Promise<Result<AgentVersion[]>> {
628 const agent = await agentByHandle(ctx, handle);
629 if (!agent) return fail("not_found", `There is no agent called @${handle}.`);
630 const rows = await ctx.db
631 .prepare("SELECT version, definition, changed_by, created_at FROM agent_versions WHERE agent_id = ? ORDER BY version DESC LIMIT 50")
632 .bind(agent.id)
633 .all<{ version: number; definition: string; changed_by: string; created_at: string }>();
634 return ok(
635 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.636 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 asked637 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.638 // Teams are memberships, not part of a version: older versions' team and department are left out.
639 const { team: _team, department: _department, ...rest } = JSON.parse(r.definition) as Record<string, unknown>;
640 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 asked641 } catch {
642 // An unreadable old version shows as empty.
643 }
644 return { version: r.version, changed_by: r.changed_by, created_at: r.created_at, definition };
645 }),
646 );
647}
648
649// ── Policy and overview ──────────────────────────────────────────────────
650
651export async function policy(ctx: ViewContext): Promise<Result<AgentPolicy>> {
652 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.653 return ok(policyOf(row));
654}
655
656function policyOf(row: AgentPolicy): AgentPolicy {
657 return {
658 monthly_micros: row.monthly_micros,
659 default_agent_monthly_micros: row.default_agent_monthly_micros,
660 default_session_micros: row.default_session_micros,
661 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.662 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.663 };
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 asked664}
665
666export async function setPolicy(ctx: ViewContext, changes: Partial<AgentPolicy>): Promise<Result<AgentPolicy>> {
667 if (!ctx.owner) return fail("forbidden", "Only the workspace's owners set its agents' budget.");
668 const current = await policy(ctx);
669 const checked = checkPolicy(current.ok ? current.value : DEFAULT_POLICY, changes ?? {});
670 if (!checked.ok) return fail("invalid", checked.message);
671 const p = checked.value;
672 await ctx.db
673 .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.674 `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)
675 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
676 ON CONFLICT (workspace_id) DO UPDATE SET monthly_micros = ?2, default_agent_monthly_micros = ?3, default_session_micros = ?4, person_monthly_micros = ?5,
677 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 asked678 )
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.679 .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 asked680 .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.681 const current_ = current.ok ? current.value : DEFAULT_POLICY;
682 if (current_.members_create_agents !== p.members_create_agents) {
683 ctx.audit?.("set_agent_policy", "policy", p.members_create_agents ? "Let members create personal agents" : "Turned off personal agents for members");
684 }
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 asked685 return ok(p);
686}
687
688export async function overview(ctx: ViewContext): Promise<Result<AgentsOverview>> {
689 const now = new Date();
690 const db = ctx.db;
691 const [rows, policyRow, live, recent, upcoming, breakdown, agentFaces] = await Promise.all([
692 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>(),
693 readPolicy(db, ctx.workspaceId, monthKey(now)),
694 db
695 .prepare(`SELECT * FROM agent_sessions WHERE workspace_id = ? AND status IN (${LIVE.map(() => "?").join(", ")}) ORDER BY created_at DESC LIMIT 60`)
696 .bind(ctx.workspaceId, ...LIVE)
697 .all<SessionRow>(),
698 db
699 .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`)
700 .bind(ctx.workspaceId)
701 .all<SessionRow>(),
702 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>(),
703 spend(ctx, null),
704 faces(ctx),
705 ]);
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.706 // The workspace's agents and the viewer's own personal ones; other members' are theirs.
707 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 asked708 const liveSessions = await sessionsOut(ctx, live.results, agentFaces);
709 const liveByAgent: Record<string, number> = {};
710 for (const s of live.results) liveByAgent[s.agent_id] = (liveByAgent[s.agent_id] ?? 0) + 1;
711 const level = policyRow.monthly_micros ? [100, 90, 75].find((l) => (policyRow.spent * 100) / policyRow.monthly_micros! >= l) ?? null : null;
712 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.713 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 asked714 spent_month_micros: policyRow.spent,
715 alert: level,
716 agents,
717 live_by_agent: liveByAgent,
718 live: liveSessions.filter((s) => s.visible && !s.parent_id).slice(0, 20),
719 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),
720 recent: (await sessionsOut(ctx, recent.results, agentFaces)).filter((s) => s.visible).slice(0, 8),
721 upcoming: upcoming.results.map((r) => {
722 const face = agentFaces.get(r.agent_id);
723 return { ...toRoutine(r), agent_handle: face?.handle ?? "agent", agent_name: face?.display_name ?? "An agent" };
724 }),
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.725 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 asked726 can_manage: ctx.owner,
727 });
728}
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.729
730/** A breakdown with nothing in it, for this month. */
731function emptySpend(now: Date): AgentSpendBreakdown {
732 const span = spendSpan("month", now);
733 return { ...span, person: null, total_micros: 0, by_kind: [], by_model: [], by_person: [], by_agent: [], by_team: [], by_channel: [], top_sessions: [], days: [] };
734}