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