Skip to content
1,231 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.

Chat and workspace agents: channels, DMs and named agents you talk to1/**
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.2 * The agents service: a workspace's own agents
3 * (docs.g1t.sh/guides/agents/). Their definitions and every version of
4 * them, the templates they start from, each agent's desk, and their replies
5 * in chat.
Chat and workspace agents: channels, DMs and named agents you talk to6 *
7 * `@g1t`, the platform's own agent, is not one of these.
8 *
9 * Reached through service bindings: `POST /rpc/<method>`, with snake_case
10 * JSON (@g1t/contracts workspace-agents.ts).
11 */
12import {
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar13 type AgentCardAction,
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.14 type AgentProposal,
15 type AgentRedraft,
16 type DraftReply,
17 MAX_PERSONAL_AGENTS,
Chat and workspace agents: channels, DMs and named agents you talk to18 type AgentDelivery,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar19 type CardActionResult,
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened20 type G1tEvent,
The Marketplace is where a workspace finds what to add, reached from Apps: the agent catalog of roles with who already does each job and Add to workspace; every integration with whether it's connected; g1t's own extensions, listed as Soon; and requests, where members ask and owners add or turn down, each told by a notification. Underneath, an extension is a manifest (publisher and tier, source and version from a tag, scopes, the domains its data goes to, its page on g1tusercontent.com, no price) and an install pins its version with an off switch, a budget and the audit log. Apps lists only what was added from the Marketplace, with pins; g1t's own pages stay in their sidebars, and AI Gateway joins Workspace's. The marketplace guide says how.21 type ExtensionInstall,
22 type InstallRequest,
23 type InstallRequests,
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.24 type McpServer,
Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how.25 type AgentEffortCosts,
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.26 type ArchivedAgent,
Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how.27 type AgentRecommendation,
28 type AgentRecommendations,
Chat and workspace agents: channels, DMs and named agents you talk to29 type NewWorkspaceAgent,
30 type Result,
31 type ServiceBinding,
32 type User,
33 type WorkspaceAgent,
34 UNVERIFIED,
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)35 askerAccess,
Chat and workspace agents: channels, DMs and named agents you talk to36 awaitsConfirmation,
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)37 chatClient,
The Marketplace is where a workspace finds what to add, reached from Apps: the agent catalog of roles with who already does each job and Add to workspace; every integration with whether it's connected; g1t's own extensions, listed as Soon; and requests, where members ask and owners add or turn down, each told by a notification. Underneath, an extension is a manifest (publisher and tier, source and version from a tag, scopes, the domains its data goes to, its page on g1tusercontent.com, no price) and an install pins its version with an off switch, a budget and the audit log. Apps lists only what was added from the Marketplace, with pins; g1t's own pages stay in their sidebars, and AI Gateway joins Workspace's. The marketplace guide says how.38 cleanRequestNote,
39 extensionById,
Chat and workspace agents: channels, DMs and named agents you talk to40 fail,
41 identityClient,
42 newId,
The Marketplace is where a workspace finds what to add, reached from Apps: the agent catalog of roles with who already does each job and Add to workspace; every integration with whether it's connected; g1t's own extensions, listed as Soon; and requests, where members ask and owners add or turn down, each told by a notification. Underneath, an extension is a manifest (publisher and tier, source and version from a tag, scopes, the domains its data goes to, its page on g1tusercontent.com, no price) and an install pins its version with an off switch, a budget and the audit log. Apps lists only what was added from the Marketplace, with pins; g1t's own pages stay in their sidebars, and AI Gateway joins Workspace's. The marketplace guide says how.43 notifyClient,
Chat and workspace agents: channels, DMs and named agents you talk to44 ok,
45 openD1,
46} from "@g1t/contracts";
47
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.48import { MANAGE_REFUSAL, NOT_YOURS_REFUSAL, canArchive, canChange, canManage, canSee, canSeeAgent, creatableScope } from "./access.ts";
49import { DRAFT_OUTPUT_TOKENS, draftSystem, jsonIn, proposalFrom, redraftFrom, redraftSystem, startingBudget, trySystem, tryTurns, wordsOf } from "./builder.ts";
50import { callBuilder } from "./builder-call.ts";
Chat and workspace agents: channels, DMs and named agents you talk to51import { type Definition, applyChanges } from "./definition.ts";
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.52import { listMcpTools } from "./mcp-client.ts";
53import { MAX_MCP_SERVERS, checkMcpName, checkMcpUrl } from "../../../packages/contracts/src/abilities.ts";
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)54import { builtinChanges } from "./orchestrator.ts";
Chat and workspace agents: channels, DMs and named agents you talk to55import type { Desk } from "./desk.ts";
56import type { ReplyEnv } from "./reply.ts";
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)57import { ensureBuiltin } from "./builtin.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.58import { type Row, definitionOf, insertAgent, isPersonal, periods, selectAgents, toAgent, updateAgent, versionStatement } from "./store.ts";
Chat and workspace agents: channels, DMs and named agents you talk to59import { TEMPLATES, TEMPLATE_IDS } from "./templates.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 asked60import { readPolicy } from "./policy.ts";
61import { runDue } from "./routines.ts";
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened62import { onEvents } from "./triggers.ts";
@g1t welcomes you when you join a workspace, and that is the one introduction any agent makes. Identity now says when a person becomes a member, workspace.member_joined, by invitation, by an invite code, or by creating the workspace, and webhooks can hear it; the agents service hears it too, and for anyone who joined, never the creator, the workspace's @g1t opens your direct message and says in its own voice who it is, what to ask it, how agents answer, and where things are, once per person per workspace and charged to @g1t like any reply, with a fixed welcome when no model can be used. The agents, workspaces, authentication and webhooks guides say so.63import { welcome } from "./welcome.ts";
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar64import { cardAction } from "./cards.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 asked65import { type SessionEnv, sweep } from "./sessions.ts";
Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how.66import { EFFORT_NAMES, checkDue, effortCostsOf, markResolved, outcomesSince, readRecommendations, recommendationRow, sinceWindow, toRecommendation } from "./recommend.ts";
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.67import { moveAgentTeams } from "./team-move.ts";
Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how.68import { effortOf } from "./routing.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 asked69import * as views from "./views.ts";
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.70import { computerCommands, computerView, eraseComputer, forgetComputer, keepComputer, resetComputer, sleepComputer, wakeComputer } from "./computer.ts";
71import { destroyRefusal, destroyStatements, listArchived, restoreHandle, restoreRefusal } from "./archived.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 asked72import { monthKey } from "./budget.ts";
The Marketplace is where a workspace finds what to add, reached from Apps: the agent catalog of roles with who already does each job and Add to workspace; every integration with whether it's connected; g1t's own extensions, listed as Soon; and requests, where members ask and owners add or turn down, each told by a notification. Underneath, an extension is a manifest (publisher and tier, source and version from a tag, scopes, the domains its data goes to, its page on g1tusercontent.com, no price) and an install pins its version with an off switch, a budget and the audit log. Apps lists only what was added from the Marketplace, with pins; g1t's own pages stay in their sidebars, and AI Gateway joins Workspace's. The marketplace guide says how.73import * as extensions from "./extensions.ts";
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.74import { skillPushes, skillRpc } from "./skill-rpc.ts";
The Marketplace is where a workspace finds what to add, reached from Apps: the agent catalog of roles with who already does each job and Add to workspace; every integration with whether it's connected; g1t's own extensions, listed as Soon; and requests, where members ask and owners add or turn down, each told by a notification. Underneath, an extension is a manifest (publisher and tier, source and version from a tag, scopes, the domains its data goes to, its page on g1tusercontent.com, no price) and an install pins its version with an off switch, a budget and the audit log. Apps lists only what was added from the Marketplace, with pins; g1t's own pages stay in their sidebars, and AI Gateway joins Workspace's. The marketplace guide says how.75import { type Answered, type Person, answerLine, findListing, listRequests, listingPath, openRequest, requestsPath, resolveListing, resolveRequest } from "./installs.ts";
Chat and workspace agents: channels, DMs and named agents you talk to76
77export { Desk } from "./desk.ts";
78
79type Env = ReplyEnv & {
80 IDENTITY: ServiceBinding;
81 /** The audit log. */
82 EVENTS: ServiceBinding;
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.83 /** The runner, for agents' own computers (./computer.ts). */
84 RUNNER?: ServiceBinding;
Chat and workspace agents: channels, DMs and named agents you talk to85 DESKS: DurableObjectNamespace<Desk>;
86};
87
88/** Workspace ids by slug, kept a minute: every call names a workspace by slug. */
89const workspaceIds = new Map<string, { id: string | null; until: number }>();
90
91class Agents {
92 private readonly env: Env;
93 private readonly db: D1Database;
94 private readonly defer: (work: Promise<unknown>) => void;
95
96 // Plain fields, not parameter properties: Node's type stripping does not take those.
97 constructor(env: Env, defer: (work: Promise<unknown>) => void) {
98 this.env = env;
99 this.db = env.DB;
100 this.defer = defer;
101 }
102
103 private async workspaceId(slug: string): Promise<string | null> {
104 const key = slug.toLowerCase();
105 const kept = workspaceIds.get(key);
106 if (kept && kept.until > Date.now()) return kept.id;
107 const workspace = await identityClient(this.env.IDENTITY).getWorkspace(key);
108 if (workspaceIds.size > 5_000) workspaceIds.clear();
109 workspaceIds.set(key, { id: workspace?.id ?? null, until: Date.now() + 60_000 });
110 return workspace?.id ?? null;
111 }
112
113 /** The workspace's id, if the viewer may see its agents. */
114 private async seen(workspace: string, viewer: User | null): Promise<Result<string>> {
115 if (!canSee(viewer, workspace)) return fail("not_found", "There is no such workspace.");
116 const id = await this.workspaceId(workspace);
117 return id ? ok(id) : fail("not_found", "There is no such workspace.");
118 }
119
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar120 /** Internal, from chat: a person pressed an action on one of agents' cards. */
121 async cardAction(a: AgentCardAction): Promise<Result<CardActionResult>> {
122 if (a?.viewer && awaitsConfirmation(a.viewer)) return UNVERIFIED;
123 if (!a?.viewer || !canSee(a.viewer, a.workspace ?? "")) return fail("not_found", "No such card.");
124 return cardAction(this.env as unknown as SessionEnv, a);
125 }
126
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 asked127 /** What the views need: the workspace, the viewer, and whether they own it. */
128 private async context(workspace: string, viewer: User | null): Promise<Result<views.ViewContext>> {
129 if (viewer && awaitsConfirmation(viewer)) return UNVERIFIED;
130 const seen = await this.seen(workspace, viewer);
131 if (!seen.ok) return seen;
132 return ok({
133 env: this.env as unknown as SessionEnv,
134 db: this.db,
135 slug: workspace.toLowerCase(),
136 workspaceId: seen.value,
137 viewer: viewer!,
138 owner: canManage(viewer, workspace),
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.139 audit: (action, handle, message) => this.audit(viewer!, workspace, action, handle, message),
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 asked140 });
141 }
142
143 /** Runs `view` with the context, or answers why it can't. */
144 async view<T>(a: { workspace: string; viewer: User | null }, view: (ctx: views.ViewContext) => Promise<Result<T>>): Promise<Result<T>> {
145 const ctx = await this.context(a?.workspace ?? "", a?.viewer ?? null);
146 if (!ctx.ok) return ctx;
147 return view(ctx.value);
148 }
149
Chat and workspace agents: channels, DMs and named agents you talk to150 /** The workspace's id, if the viewer may change its agents. */
151 private async managed(workspace: string, viewer: User | null): Promise<Result<string>> {
152 if (awaitsConfirmation(viewer)) return UNVERIFIED;
153 const seen = await this.seen(workspace, viewer);
154 if (!seen.ok) return seen;
155 return canManage(viewer, workspace) ? seen : fail("forbidden", MANAGE_REFUSAL);
156 }
157
158 private async row(workspaceId: string, handle: unknown): Promise<Row | null> {
159 if (typeof handle !== "string") return null;
160 const now = new Date();
161 return this.db
162 .prepare(selectAgents("a.workspace_id = ?3 AND a.handle = ?4 AND a.archived_at IS NULL"))
163 .bind(...periods(now), workspaceId, handle.trim().replace(/^@/, "").toLowerCase())
164 .first<Row>();
165 }
166
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.167 /**
168 * The teams a new agent joins as it is made, by slug: each must be one the
169 * viewer manages (an owner, or the team's maintainer), as adding anyone
170 * to a team asks. Membership lives on the team, in identity.
171 */
172 private async joinable(workspace: string, viewer: User, asked: unknown): Promise<Result<string[]>> {
173 if (asked === undefined || asked === null) return ok([]);
174 if (!Array.isArray(asked) || asked.some((slug) => typeof slug !== "string")) return fail("invalid", "Teams are a list of team slugs.");
175 const slugs = [...new Set((asked as string[]).map((slug) => slug.trim().replace(/^@/, "").toLowerCase()).filter(Boolean))];
176 if (slugs.length > 20) return fail("invalid", "Add a new agent to at most 20 teams.");
177 const identity = identityClient(this.env.IDENTITY);
178 const found = await Promise.all(slugs.map((slug) => identity.getTeam(viewer, workspace.toLowerCase(), slug).catch(() => null)));
179 for (const [i, team] of found.entries()) {
180 if (!team?.ok) return fail("invalid", `${workspace} has no team called ${slugs[i]}.`);
181 if (!team.value.can_manage) return fail("forbidden", `Only owners and ${team.value.name}'s maintainers add agents to it.`);
182 }
183 return ok(slugs);
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)184 }
185
Chat and workspace agents: channels, DMs and named agents you talk to186 private async handleTaken(workspaceId: string, handle: string, except: string | null): Promise<boolean> {
187 const found = await this.db
188 .prepare("SELECT id FROM agents WHERE workspace_id = ? AND handle = ? AND archived_at IS NULL")
189 .bind(workspaceId, handle)
190 .first<{ id: string }>();
191 return !!found && found.id !== except;
192 }
193
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.194 /**
195 * The workspace's agents, and personal agents when asked for: `mine`, the
196 * viewer's own; `all`, every member's for an owner.
197 */
198 async list(a: { workspace: string; viewer: User | null; personal?: unknown }): Promise<Result<WorkspaceAgent[]>> {
Chat and workspace agents: channels, DMs and named agents you talk to199 const seen = await this.seen(a.workspace, a.viewer);
200 if (!seen.ok) return seen;
201 const now = new Date();
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)202 await this.ensureBuiltin(seen.value);
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.203 const all = a.personal === "all" && canManage(a.viewer, a.workspace);
204 const mine = (a.personal === "mine" || a.personal === "all") && (a.viewer?.kind ?? "user") === "user" ? (a.viewer?.id ?? "") : "";
Chat and workspace agents: channels, DMs and named agents you talk to205 const rows = await this.db
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.206 .prepare(
207 `${selectAgents(
208 `a.workspace_id = ?3 AND a.archived_at IS NULL AND (a.scope = 'workspace'${all ? " OR a.scope = 'personal'" : mine ? " OR (a.scope = 'personal' AND a.owner_id = ?4)" : ""})`,
209 )} ORDER BY a.builtin DESC, a.handle`,
210 )
211 .bind(...periods(now), seen.value, ...(mine && !all ? [mine] : []))
Chat and workspace agents: channels, DMs and named agents you talk to212 .all<Row>();
213 return ok(rows.results.map((row) => toAgent(row, now)));
214 }
215
216 async get(a: { workspace: string; handle: string; viewer: User | null }): Promise<Result<WorkspaceAgent>> {
217 const seen = await this.seen(a.workspace, a.viewer);
218 if (!seen.ok) return seen;
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)219 await this.ensureBuiltin(seen.value);
Chat and workspace agents: channels, DMs and named agents you talk to220 const row = await this.row(seen.value, a.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.221 // Another member's personal agent is theirs: it isn't there for anyone else but owners.
222 return row && canSeeAgent(a.viewer, a.workspace, row) ? ok(toAgent(row, new Date())) : fail("not_found", `There is no agent called @${a.handle}.`);
Chat and workspace agents: channels, DMs and named agents you talk to223 }
224
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.225 /** The agent by handle, if the viewer may see it. */
226 private async visible(workspace: string, viewer: User | null, handle: unknown): Promise<Result<{ workspaceId: string; row: Row }>> {
227 if (awaitsConfirmation(viewer)) return UNVERIFIED;
228 const seen = await this.seen(workspace, viewer);
229 if (!seen.ok) return seen;
230 const row = await this.row(seen.value, handle);
231 if (!row || !canSeeAgent(viewer, workspace, row)) return fail("not_found", `There is no agent called @${String(handle ?? "")}.`);
232 return ok({ workspaceId: seen.value, row });
233 }
234
Chat and workspace agents: channels, DMs and named agents you talk to235 /** Internal: agents by id, archived ones too, so old messages still show who wrote them. */
236 async byIds(a: { ids: string[] }): Promise<WorkspaceAgent[]> {
237 const ids = [...new Set((Array.isArray(a.ids) ? a.ids : []).filter((id) => typeof id === "string"))].slice(0, 100);
238 if (!ids.length) return [];
239 const now = new Date();
240 const rows = await this.db
241 .prepare(selectAgents(`a.id IN (${ids.map((_, i) => `?${i + 3}`).join(", ")})`))
242 .bind(...periods(now), ...ids)
243 .all<Row>();
244 return rows.results.map((row) => toAgent(row, now));
245 }
246
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.247 /**
248 * Who may create an agent, and of which scope: the workspace's id and
249 * policy, and the scope it gets (access.ts `creatableScope`).
250 */
251 private async creatable(workspace: string, viewer: User | null, asked: unknown): Promise<Result<{ workspaceId: string; scope: "workspace" | "personal"; policy: Awaited<ReturnType<typeof readPolicy>> }>> {
252 if (awaitsConfirmation(viewer)) return UNVERIFIED;
253 const seen = await this.seen(workspace, viewer);
254 if (!seen.ok) return seen;
255 const policy = await readPolicy(this.db, seen.value, monthKey(new Date()));
256 const scope = creatableScope(viewer, workspace, asked, policy.members_create_agents);
257 if (!scope.ok) return fail("forbidden", scope.message);
258 return ok({ workspaceId: seen.value, scope: scope.scope, policy });
259 }
260
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.261 async create(a: { workspace: string; viewer: User | null; input: NewWorkspaceAgent; teams?: unknown }): Promise<Result<WorkspaceAgent>> {
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.262 const allowed = await this.creatable(a.workspace, a.viewer, a.input?.scope);
263 if (!allowed.ok) return allowed;
264 const { workspaceId, scope, policy } = allowed.value;
265 const personal = scope === "personal";
266 const viewer = a.viewer!;
267 if (personal) {
268 const count = await this.db
269 .prepare("SELECT COUNT(*) AS n FROM agents WHERE workspace_id = ? AND owner_id = ? AND scope = 'personal' AND archived_at IS NULL")
270 .bind(workspaceId, viewer.id)
271 .first<{ n: number }>();
272 if ((count?.n ?? 0) >= MAX_PERSONAL_AGENTS) {
273 return fail("invalid", `You have ${MAX_PERSONAL_AGENTS} personal agents here, the most one person keeps. Archive one to make another.`);
274 }
275 }
276 // A new agent starts with a budget, unless one was given: a member's own
277 // $20 a month and $2 a session, or the workspace's default for its agents.
278 const given = a.input?.budget ?? {};
279 const start = startingBudget(scope, policy.default_agent_monthly_micros);
280 const budget = {
281 monthly_micros: given.monthly_micros !== undefined ? given.monthly_micros : start.monthly_micros,
282 daily_micros: given.daily_micros !== undefined ? given.daily_micros : start.daily_micros,
283 task_micros: given.task_micros !== undefined ? given.task_micros : start.task_micros,
284 };
285 const { scope: _scope, ...rest } = a.input ?? ({} as NewWorkspaceAgent);
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.286 const input = { ...rest, budget };
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 asked287 const checked = applyChanges(null, input, TEMPLATE_IDS);
Chat and workspace agents: channels, DMs and named agents you talk to288 if (!checked.ok) return fail("invalid", checked.message);
289 const definition = checked.value;
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.290 // A personal agent is on no team: teams are shared, and it answers only its member.
291 const teams = personal ? ok([] as string[]) : await this.joinable(a.workspace, viewer, a.teams);
292 if (!teams.ok) return teams;
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.293 if (await this.handleTaken(workspaceId, definition.handle, null)) {
Chat and workspace agents: channels, DMs and named agents you talk to294 return fail("conflict", `${a.workspace} already has an agent called @${definition.handle}.`);
295 }
296 const now = new Date().toISOString();
297 const id = newId("agt");
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 const by = viewer.username;
299 const owned: Record<string, string> = personal ? { scope: "personal", owner_id: viewer.id, owner_username: viewer.username.toLowerCase() } : {};
Chat and workspace agents: channels, DMs and named agents you talk to300 try {
301 await this.db.batch([
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.302 insertAgent(this.db, id, workspaceId, definition, { version: 1, created_by: by, created_at: now, updated_at: now, ...owned }),
Chat and workspace agents: channels, DMs and named agents you talk to303 this.versionStatement(id, 1, definition, by, now),
304 ]);
305 } catch (error) {
306 if (String(error).includes("UNIQUE")) return fail("conflict", `${a.workspace} already has an agent called @${definition.handle}.`);
307 throw error;
308 }
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.309 this.audit(viewer, a.workspace, "create_agent", definition.handle, `Created ${personal ? "the personal agent " : ""}@${definition.handle} (version 1)`, "agents", personal ? "member" : "owner");
A new agent says nothing until you do. Creating one still opens your direct message with it, but no introduction is written there, nothing is charged to its budget and nothing waits in Chat; the desk's special first message, its fixed fallback and the prompt that asked for it are gone. The agents guide says so.310 // On its teams as it is made, so it knows them from its first message.
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.311 if (teams.value.length) {
312 const identity = identityClient(this.env.IDENTITY);
313 await Promise.all(
314 teams.value.map((slug) =>
315 identity
316 .setTeamAgent(viewer, a.workspace.toLowerCase(), slug, id)
317 .then((added) => (added.ok ? null : console.error("agents: a new agent was not added to its team", id, slug, added.error.message)))
318 .catch((error: unknown) => console.error("agents: a new agent was not added to its team", id, slug, String(error))),
319 ),
320 );
321 }
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.322 const row = await this.row(workspaceId, definition.handle);
Chat and workspace agents: channels, DMs and named agents you talk to323 return ok(toAgent(row!, new Date()));
324 }
325
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.326 // ── The builder: describe it, try it, change it in words (./builder.ts) ──
327
328 /** Drafts a whole agent from a description, charged to the viewer. Nothing is saved. */
329 async draft(a: { workspace: string; viewer: User | null; description: unknown; scope?: unknown }): Promise<Result<AgentProposal>> {
330 const allowed = await this.creatable(a?.workspace ?? "", a?.viewer ?? null, a?.scope);
331 if (!allowed.ok) return allowed;
332 const words = wordsOf(a.description, "description");
333 if (!words.ok) return fail("invalid", words.message);
334 const { workspaceId, scope, policy } = allowed.value;
335 const slug = a.workspace.toLowerCase();
336 const [answer, handles] = await Promise.all([
337 callBuilder(this.env, {
338 slug,
339 workspaceId,
340 viewer: a.viewer!,
341 system: draftSystem(slug, scope),
342 messages: [{ role: "user", content: `What the agent should do:\n\n${words.value}` }],
343 maxOutput: DRAFT_OUTPUT_TOKENS,
344 }),
345 this.db.prepare("SELECT handle FROM agents WHERE workspace_id = ? AND archived_at IS NULL").bind(workspaceId).all<{ handle: string }>(),
346 ]);
347 if (!answer.ok) return fail(answer.code, answer.message);
348 const proposal = proposalFrom(jsonIn(answer.text), {
349 scope,
350 taken: new Set(handles.results.map((row) => row.handle)),
351 budget: startingBudget(scope, policy.default_agent_monthly_micros),
352 });
353 if (!proposal.ok) return fail("invalid", proposal.message);
354 return ok({ ...proposal.value, charged_micros: answer.charged });
355 }
356
357 /** Try it: the unsaved definition answers the conversation so far. Charged to the viewer; nothing is saved. */
358 async tryDraft(a: { workspace: string; viewer: User | null; definition: unknown; messages: unknown }): Promise<Result<DraftReply>> {
359 const given = (a?.definition ?? {}) as NewWorkspaceAgent;
360 const allowed = await this.creatable(a?.workspace ?? "", a?.viewer ?? null, given?.scope);
361 if (!allowed.ok) return allowed;
362 const { scope: _scope, ...rest } = given;
363 // A handle someone has already taken is no reason not to try it.
364 const checked = applyChanges(null, { ...rest, handle: rest.handle || "preview" }, TEMPLATE_IDS);
365 if (!checked.ok) return fail("invalid", checked.message);
366 const turns = tryTurns(a.messages);
367 if (!turns.ok) return fail("invalid", turns.message);
368 const viewer = a.viewer!;
369 const answer = await callBuilder(this.env, {
370 slug: a.workspace.toLowerCase(),
371 workspaceId: allowed.value.workspaceId,
372 viewer,
373 system: trySystem({ workspace: a.workspace.toLowerCase(), definition: checked.value, asker: { username: viewer.username, display_name: viewer.display_username ?? null } }),
374 messages: turns.value,
375 routing: { floor: checked.value.routing.floor, ceiling: checked.value.routing.ceiling },
376 });
377 if (!answer.ok) return fail(answer.code, answer.message);
378 return ok({ text: answer.text, charged_micros: answer.charged });
379 }
380
381 /** Drafts changes from a request in words, for whoever may change the agent. Saved only through `update`. */
382 async redraft(a: { workspace: string; handle: string; viewer: User | null; request: unknown }): Promise<Result<AgentRedraft>> {
383 const found = await this.visible(a?.workspace ?? "", a?.viewer ?? null, a?.handle);
384 if (!found.ok) return found;
385 const { workspaceId, row } = found.value;
386 if (!canChange(a.viewer, a.workspace, row)) return fail("forbidden", isPersonal(row) ? NOT_YOURS_REFUSAL : MANAGE_REFUSAL);
387 const words = wordsOf(a.request, "request");
388 if (!words.ok) return fail("invalid", words.message);
389 const before = definitionOf(row);
390 const slug = a.workspace.toLowerCase();
391 const answer = await callBuilder(this.env, {
392 slug,
393 workspaceId,
394 viewer: a.viewer!,
395 system: redraftSystem(slug, before, !!row.builtin),
396 messages: [{ role: "user", content: `The change to make to @${row.handle}:\n\n${words.value}` }],
397 maxOutput: DRAFT_OUTPUT_TOKENS,
398 });
399 if (!answer.ok) return fail(answer.code, answer.message);
400 const drafted = redraftFrom(jsonIn(answer.text), before, !!row.builtin);
401 if (!drafted.ok) return fail("invalid", drafted.message);
402 return ok({ ...drafted.value, from_version: row.version, charged_micros: answer.charged });
403 }
404
405 /**
406 * Owners make a personal agent a workspace agent: a new workspace agent
407 * with the same handle, definition and every version, and one more
408 * version saying so; the personal one is archived in the same step, its
409 * memory and direct messages kept with it (owners can't read a member's
410 * memory to choose from it, so none is carried).
411 */
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.412 // ── MCP servers (docs.g1t.sh/guides/agent-abilities/, "MCP servers") ─────
413
414 /** The agent, if the viewer may add servers to it: owners only, a workspace agent or a personal one alike. */
415 private async forMcp(a: { workspace: string; handle: string; viewer: User | null }): Promise<Result<{ row: Row; workspaceId: string }>> {
416 const found = await this.visible(a?.workspace ?? "", a?.viewer ?? null, a?.handle);
417 if (!found.ok) return found;
418 if (!canManage(a.viewer, a.workspace)) return fail("forbidden", "Only the workspace's owners add MCP servers to an agent.");
419 return ok({ row: found.value.row, workspaceId: found.value.workspaceId });
420 }
421
422 /** Saves `definition` as the agent's next version, from the version read; what the agent is now. */
423 private async saveVersion(row: Row, definition: Definition, by: User, workspace: string, what: string): Promise<Result<WorkspaceAgent>> {
424 const now = new Date().toISOString();
425 const version = row.version + 1;
426 const [updated] = await this.db.batch([
427 updateAgent(this.db, row.id, row.version, definition, { version, updated_at: now }),
428 this.db
429 .prepare(
430 `INSERT INTO agent_versions (agent_id, version, definition, changed_by, created_at)
431 SELECT ?1, ?2, ?3, ?4, ?5 WHERE EXISTS (SELECT 1 FROM agents WHERE id = ?1 AND version = ?2 AND updated_at = ?5)`,
432 )
433 .bind(row.id, version, JSON.stringify(definition), by.username, now),
434 ]);
435 if (!updated.meta.changes) return fail("conflict", `@${row.handle} was changed meanwhile. Reload it and try again.`);
436 this.audit(by, workspace, "update_agent", row.handle, `${what} (version ${version})`, "agents", "owner");
437 const saved = await this.row(row.workspace_id, row.handle);
438 return ok(toAgent(saved!, new Date()));
439 }
440
441 /**
442 * Adds an MCP server to an agent: the name and address are checked, its
443 * tools are listed from it now, and the agent gets a new version with
444 * the server and its tools as abilities (each a write unless the server
445 * says it only reads). Owners only.
446 */
447 async addMcpServer(a: { workspace: string; handle: string; viewer: User | null; name: unknown; url: unknown }): Promise<Result<McpServer>> {
448 const found = await this.forMcp(a);
449 if (!found.ok) return found;
450 const { row } = found.value;
451 const name = checkMcpName(a.name);
452 if (!name.ok) return fail("invalid", name.message);
453 const url = checkMcpUrl(a.url);
454 if (!url.ok) return fail("invalid", url.message);
455 const before = definitionOf(row);
456 const servers = before.abilities.mcp_servers ?? [];
457 if (servers.length >= MAX_MCP_SERVERS) return fail("limit", `An agent has at most ${MAX_MCP_SERVERS} MCP servers.`);
458 if (servers.some((server) => server.name === name.value)) return fail("conflict", `@${row.handle} already has a server called ${name.value}.`);
459 if (servers.some((server) => server.url === url.value)) return fail("conflict", `@${row.handle} already has that server.`);
460 const listed = await listMcpTools(url.value);
461 const now = new Date().toISOString();
462 const server: McpServer = {
463 id: newId("mcp"),
464 name: name.value,
465 url: url.value,
466 tools: listed.ok ? listed.tools : [],
467 added_by: a.viewer!.username,
468 added_at: now,
469 checked_at: now,
470 problem: listed.ok ? null : listed.message,
471 };
472 const checked = applyChanges(before, {}, TEMPLATE_IDS, { builtin: !!row.builtin, mcp_servers: [...servers, server] });
473 if (!checked.ok) return fail("invalid", checked.message);
474 const saved = await this.saveVersion(row, checked.value, a.viewer!, a.workspace, `Added the MCP server ${server.name} (${server.tools.length} tools) to @${row.handle}`);
475 return saved.ok ? ok(server) : saved;
476 }
477
478 /** Takes an MCP server off an agent, with its abilities' settings: a new version. Owners only. */
479 async removeMcpServer(a: { workspace: string; handle: string; viewer: User | null; id: unknown }): Promise<Result<null>> {
480 const found = await this.forMcp(a);
481 if (!found.ok) return found;
482 const { row } = found.value;
483 const before = definitionOf(row);
484 const servers = before.abilities.mcp_servers ?? [];
485 const server = servers.find((s) => s.id === a.id);
486 if (!server) return fail("not_found", "There is no such server on this agent.");
487 const checked = applyChanges(before, {}, TEMPLATE_IDS, { builtin: !!row.builtin, mcp_servers: servers.filter((s) => s.id !== server.id) });
488 if (!checked.ok) return fail("invalid", checked.message);
489 const saved = await this.saveVersion(row, checked.value, a.viewer!, a.workspace, `Removed the MCP server ${server.name} from @${row.handle}`);
490 return saved.ok ? ok(null) : saved;
491 }
492
493 /** Lists a server's tools again; a new version when they changed. Owners only. */
494 async refreshMcpServer(a: { workspace: string; handle: string; viewer: User | null; id: unknown }): Promise<Result<McpServer>> {
495 const found = await this.forMcp(a);
496 if (!found.ok) return found;
497 const { row } = found.value;
498 const before = definitionOf(row);
499 const servers = before.abilities.mcp_servers ?? [];
500 const server = servers.find((s) => s.id === a.id);
501 if (!server) return fail("not_found", "There is no such server on this agent.");
502 const listed = await listMcpTools(server.url);
503 const now = new Date().toISOString();
504 const fresh: McpServer = { ...server, tools: listed.ok ? listed.tools : server.tools, checked_at: now, problem: listed.ok ? null : listed.message };
505 if (JSON.stringify(fresh.tools) === JSON.stringify(server.tools) && fresh.problem === server.problem) return ok(fresh);
506 const checked = applyChanges(before, {}, TEMPLATE_IDS, { builtin: !!row.builtin, mcp_servers: servers.map((s) => (s.id === server.id ? fresh : s)) });
507 if (!checked.ok) return fail("invalid", checked.message);
508 const saved = await this.saveVersion(row, checked.value, a.viewer!, a.workspace, `Listed the MCP server ${server.name}'s tools again for @${row.handle} (${fresh.tools.length} tools)`);
509 return saved.ok ? ok(fresh) : saved;
510 }
511
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.512 async promote(a: { workspace: string; handle: string; viewer: User | null }): Promise<Result<WorkspaceAgent>> {
513 const managed = await this.managed(a?.workspace ?? "", a?.viewer ?? null);
514 if (!managed.ok) return managed.error.code === "forbidden" ? fail("forbidden", "Only the workspace's owners promote personal agents.") : managed;
515 const row = await this.row(managed.value, a.handle);
516 if (!row) return fail("not_found", `There is no agent called @${a.handle}.`);
517 if (!isPersonal(row)) return fail("invalid", `@${row.handle} is already a workspace agent.`);
518 const versions = await this.db
519 .prepare("SELECT version, definition, changed_by, created_at FROM agent_versions WHERE agent_id = ? ORDER BY version")
520 .bind(row.id)
521 .all<{ version: number; definition: string; changed_by: string; created_at: string }>();
522 const definition = definitionOf(row);
523 const now = new Date().toISOString();
524 const id = newId("agt");
525 const version = row.version + 1;
526 const by = a.viewer!.username;
527 try {
528 const [archived] = await this.db.batch([
529 // Archived first, from the version read, so the handle is free for the new one in the same step.
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.530 this.db.prepare("UPDATE agents SET archived_at = ?, archived_by = ?, updated_at = ? WHERE id = ? AND version = ? AND archived_at IS NULL").bind(now, by, now, row.id, row.version),
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.531 insertAgent(this.db, id, managed.value, definition, { version, created_by: row.created_by, created_at: now, updated_at: now }),
532 ...versions.results.map((v) =>
533 this.db.prepare("INSERT INTO agent_versions (agent_id, version, definition, changed_by, created_at) VALUES (?, ?, ?, ?, ?)").bind(id, v.version, v.definition, v.changed_by, v.created_at),
534 ),
535 this.versionStatement(id, version, definition, by, now),
536 ]);
537 if (!archived.meta.changes) throw new Error("changed meanwhile");
538 } catch (error) {
539 console.error("agents: a promotion failed", row.id, String(error));
540 return fail("conflict", `@${row.handle} was changed meanwhile. Reload it and try again.`);
541 }
542 this.audit(a.viewer!, a.workspace, "promote_agent", row.handle, `Promoted @${row.owner_username ?? "a member"}'s personal agent @${row.handle} to a workspace agent (version ${version})`);
543 const saved = await this.row(managed.value, row.handle);
544 return ok(toAgent(saved!, new Date()));
545 }
546
The Marketplace is where a workspace finds what to add, reached from Apps: the agent catalog of roles with who already does each job and Add to workspace; every integration with whether it's connected; g1t's own extensions, listed as Soon; and requests, where members ask and owners add or turn down, each told by a notification. Underneath, an extension is a manifest (publisher and tier, source and version from a tag, scopes, the domains its data goes to, its page on g1tusercontent.com, no price) and an install pins its version with an off switch, a budget and the audit log. Apps lists only what was added from the Marketplace, with pins; g1t's own pages stay in their sidebars, and AI Gateway joins Workspace's. The marketplace guide says how.547 // ── The Marketplace's install requests (./installs.ts) ─────────────────
548
549 /** Requests as the viewer sees them: every one for an owner, their own for anyone else. */
550 async installRequests(a: { workspace: string; viewer: User | null }): Promise<Result<InstallRequests>> {
551 if (a?.viewer && awaitsConfirmation(a.viewer)) return UNVERIFIED;
552 const seen = await this.seen(a?.workspace ?? "", a?.viewer ?? null);
553 if (!seen.ok) return seen;
554 const owner = canManage(a.viewer, a.workspace);
555 const requests = await listRequests(this.db, seen.value, a.viewer!, owner);
556 return ok({ requests, can_resolve: owner });
557 }
558
559 /** A member asks the owners to add something; each owner is notified. */
560 async requestInstall(a: { workspace: string; viewer: User | null; listing: unknown; note?: unknown }): Promise<Result<InstallRequest>> {
561 if (a?.viewer && awaitsConfirmation(a.viewer)) return UNVERIFIED;
562 const seen = await this.seen(a?.workspace ?? "", a?.viewer ?? null);
563 if (!seen.ok) return seen;
564 const viewer = a.viewer!;
565 if ((viewer.kind ?? "user") !== "user") return fail("forbidden", "Only people ask the workspace's owners to add things.");
566 if (canManage(viewer, a.workspace)) return fail("invalid", "You're an owner of this workspace: add it yourself.");
567 const listing = findListing(a.listing);
568 if (!listing) return fail("not_found", "That isn't something a workspace can add yet.");
569 const opened = await openRequest(this.db, seen.value, newId("ins"), listing, viewer, cleanRequestNote(a.note));
570 if (!opened.ok) return opened;
571 const slug = a.workspace.toLowerCase();
572 this.audit(viewer, slug, "request_install", listing.ref, `Asked the owners to add ${listing.name}`, "marketplace");
573 this.defer(this.tellOwners(slug, viewer, opened.value));
574 return opened;
575 }
576
577 /** An owner adds or turns down a request; whoever asked is told. */
578 async resolveInstallRequest(a: { workspace: string; viewer: User | null; id: unknown; status: unknown }): Promise<Result<InstallRequest>> {
579 const managed = await this.managed(a?.workspace ?? "", a?.viewer ?? null);
580 if (!managed.ok) {
581 return managed.error.code === "forbidden" ? fail("forbidden", "Only the workspace's owners answer requests.") : managed;
582 }
583 if (a.status !== "done" && a.status !== "declined") return fail("invalid", "Answer a request with done or declined.");
584 const answered = await resolveRequest(this.db, managed.value, String(a.id ?? ""), a.status, a.viewer!);
585 if (!answered.ok) return answered;
586 const slug = a.workspace.toLowerCase();
587 const { request } = answered.value;
588 this.audit(a.viewer!, slug, "answer_install_request", request.listing, `${request.status === "done" ? "Added" : "Turned down"} ${request.name} for @${request.requested_by}`, "marketplace");
589 this.defer(this.tellRequester(slug, a.viewer!, answered.value));
590 return ok(request);
591 }
592
593 // ── Extensions installed in the workspace (./extensions.ts) ──────────────
594
595 /** The workspace's installs; any member sees them. */
596 async extensionInstalls(a: { workspace: string; viewer: User | null }): Promise<Result<ExtensionInstall[]>> {
597 if (a?.viewer && awaitsConfirmation(a.viewer)) return UNVERIFIED;
598 const seen = await this.seen(a?.workspace ?? "", a?.viewer ?? null);
599 if (!seen.ok) return seen;
600 return ok(await extensions.listInstalls(this.db, seen.value));
601 }
602
603 /** Owners install a published extension; whoever asked for it is told. */
604 async installExtension(a: { workspace: string; viewer: User | null; extension: unknown }): Promise<Result<ExtensionInstall>> {
605 const managed = await this.managed(a?.workspace ?? "", a?.viewer ?? null);
606 if (!managed.ok) return managed.error.code === "forbidden" ? fail("forbidden", "Only the workspace's owners install extensions.") : managed;
607 const installed = await extensions.install(this.db, extensionById, managed.value, newId("ins"), String(a.extension ?? ""), a.viewer!.username);
608 if (!installed.ok) return installed;
609 const slug = a.workspace.toLowerCase();
610 this.audit(a.viewer!, slug, "install_extension", installed.value.listing, `Installed ${installed.value.listing} ${installed.value.version}`, "marketplace");
611 this.defer(this.answerListing(slug, managed.value, installed.value.listing, a.viewer!));
612 return installed;
613 }
614
615 /** Owners switch an install on or off. */
616 async setExtensionEnabled(a: { workspace: string; viewer: User | null; listing: unknown; enabled: unknown }): Promise<Result<ExtensionInstall>> {
617 const managed = await this.managed(a?.workspace ?? "", a?.viewer ?? null);
618 if (!managed.ok) return managed;
619 const listing = String(a.listing ?? "");
620 if (!extensions.extensionIdOf(listing)) return fail("invalid", "Name an extension as extension:<id>.");
621 const changed = await extensions.setEnabled(this.db, managed.value, listing, a.enabled === true, a.viewer!.username);
622 if (changed.ok) this.audit(a.viewer!, a.workspace, a.enabled === true ? "enable_extension" : "disable_extension", listing, `${a.enabled === true ? "Switched on" : "Switched off"} ${listing}`, "marketplace");
623 return changed;
624 }
625
626 /** Owners cap what an install spends a month. */
627 async setExtensionBudget(a: { workspace: string; viewer: User | null; listing: unknown; monthly_micros: unknown }): Promise<Result<ExtensionInstall>> {
628 const managed = await this.managed(a?.workspace ?? "", a?.viewer ?? null);
629 if (!managed.ok) return managed;
630 const listing = String(a.listing ?? "");
631 const micros = a.monthly_micros == null ? null : Number(a.monthly_micros);
632 const changed = await extensions.setBudget(this.db, managed.value, listing, micros);
633 if (changed.ok) this.audit(a.viewer!, a.workspace, "set_extension_budget", listing, `Set ${listing}'s monthly budget`, "marketplace");
634 return changed;
635 }
636
637 /** Owners remove an install. */
638 async uninstallExtension(a: { workspace: string; viewer: User | null; listing: unknown }): Promise<Result<null>> {
639 const managed = await this.managed(a?.workspace ?? "", a?.viewer ?? null);
640 if (!managed.ok) return managed;
641 const listing = String(a.listing ?? "");
642 const removed = await extensions.uninstall(this.db, managed.value, listing, a.viewer!.username);
643 if (removed.ok) this.audit(a.viewer!, a.workspace, "uninstall_extension", listing, `Uninstalled ${listing}`, "marketplace");
644 return removed;
645 }
646
647 /** Marks every open request for `listing` added, and tells each person who asked. */
648 private async answerListing(slug: string, workspaceId: string, listing: string, by: User): Promise<void> {
649 try {
650 const answered = await resolveListing(this.db, workspaceId, listing, by as Person);
651 await Promise.all(answered.map((one) => this.tellRequester(slug.toLowerCase(), by, one)));
652 } catch (error) {
653 console.error("agents: requests for a listing were not answered", listing, String(error));
654 }
655 }
656
657 /** Every owner hears of a new request (at most 20 of them). */
658 private async tellOwners(slug: string, asker: User, request: InstallRequest): Promise<void> {
659 if (!this.env.NOTIFY) return;
660 try {
661 const members = await identityClient(this.env.IDENTITY).listMembers(slug, asker);
662 if (!members.ok) throw new Error(members.error.message);
663 const owners = members.value.filter((member) => member.role === "owner").slice(0, 20);
664 const notify = notifyClient(this.env.NOTIFY);
665 await Promise.all(
666 owners.map((owner) =>
667 notify
668 .notify(
669 { username: owner.username },
670 {
671 id: `install-request:${request.id}:${owner.username}`,
672 kind: "approval",
673 workspace: slug,
674 title: `@${asker.username} asks you to add ${request.name}`,
The Marketplace adds what g1t doesn't do and connects what you already use: extensions (Mail, Support, CRM, Recruiting, On-call, and bridges to a helpdesk, CRM or ERP you run, all Soon), starter kits, and integrations connected by the workspace or by each person; agents aren't installed there. Agent templates live in Agents, as starting points you configure, at Agents → Templates, and the Marketplace's old agent addresses lead there. The marketplace and agents guides say how.675 body: request.note ?? (request.kind === "extension" ? "An extension. Install it, or turn the request down." : "An integration. Connect it, or turn the request down."),
The Marketplace is where a workspace finds what to add, reached from Apps: the agent catalog of roles with who already does each job and Add to workspace; every integration with whether it's connected; g1t's own extensions, listed as Soon; and requests, where members ask and owners add or turn down, each told by a notification. Underneath, an extension is a manifest (publisher and tier, source and version from a tag, scopes, the domains its data goes to, its page on g1tusercontent.com, no price) and an install pins its version with an off switch, a budget and the audit log. Apps lists only what was added from the Marketplace, with pins; g1t's own pages stay in their sidebars, and AI Gateway joins Workspace's. The marketplace guide says how.676 href: requestsPath(slug),
677 actor: { kind: "user", id: asker.id, name: asker.username, avatar: asker.avatar ?? null, avatar_seed: null },
678 created_at: new Date().toISOString(),
679 },
680 )
681 .catch(() => undefined),
682 ),
683 );
684 } catch (error) {
685 console.error("agents: owners were not told of an install request", request.id, String(error));
686 }
687 }
688
689 /** The person who asked hears their request was answered. */
690 private async tellRequester(slug: string, by: User, answered: Answered): Promise<void> {
691 if (!this.env.NOTIFY) return;
692 const { request } = answered;
693 const line = answerLine(request, `@${by.username}`);
694 await notifyClient(this.env.NOTIFY)
695 .notify(
696 { user_id: answered.requested_by_id },
697 {
698 id: `install-answer:${request.id}`,
699 kind: "inbox",
700 workspace: slug,
701 title: line.title,
702 body: line.body,
703 href: request.status === "done" ? listingPath(slug, request.listing) : requestsPath(slug),
704 actor: { kind: "user", id: by.id, name: by.username, avatar: by.avatar ?? null, avatar_seed: null },
705 created_at: new Date().toISOString(),
706 },
707 )
708 .catch((error: unknown) => console.error("agents: a requester was not told", request.id, String(error)));
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)709 }
710
Chat and workspace agents: channels, DMs and named agents you talk to711 async update(a: { workspace: string; handle: string; viewer: User | null; changes: Partial<NewWorkspaceAgent> }): Promise<Result<WorkspaceAgent>> {
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.712 const found = await this.visible(a.workspace, a.viewer, a.handle);
713 if (!found.ok) return found;
714 const { row } = found.value;
715 // Owners change the workspace's agents; a member changes their own personal one.
716 if (!canChange(a.viewer, a.workspace, row)) return fail("forbidden", isPersonal(row) ? NOT_YOURS_REFUSAL : MANAGE_REFUSAL);
717 const workspaceId = found.value.workspaceId;
Chat and workspace agents: channels, DMs and named agents you talk to718 const before = definitionOf(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.719 const { scope: _scope, ...asked } = (a.changes ?? {}) as Partial<NewWorkspaceAgent>;
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)720 // The built-in @g1t keeps who it is and its job; the rest is the workspace's.
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.721 const allowed = row.builtin ? builtinChanges(before, asked) : { ok: true as const, value: asked };
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)722 if (!allowed.ok) return fail("invalid", allowed.message);
723 const checked = applyChanges(before, allowed.value, TEMPLATE_IDS, { builtin: !!row.builtin });
Chat and workspace agents: channels, DMs and named agents you talk to724 if (!checked.ok) return fail("invalid", checked.message);
725 const definition = checked.value;
726 if (JSON.stringify(definition) === JSON.stringify(before)) return ok(toAgent(row, new Date()));
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.727 if (definition.handle !== before.handle && (await this.handleTaken(workspaceId, definition.handle, row.id))) {
Chat and workspace agents: channels, DMs and named agents you talk to728 return fail("conflict", `${a.workspace} already has an agent called @${definition.handle}.`);
729 }
730 const now = new Date().toISOString();
731 const version = row.version + 1;
732 const by = a.viewer!.username;
733 try {
734 const [updated] = await this.db.batch([
735 // Only from the version read: two owners saving at once never lose
736 // one's change silently; the second is told to look again.
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)737 updateAgent(this.db, row.id, row.version, definition, { version, updated_at: now }),
Chat and workspace agents: channels, DMs and named agents you talk to738 this.db
739 .prepare(
740 `INSERT INTO agent_versions (agent_id, version, definition, changed_by, created_at)
741 SELECT ?1, ?2, ?3, ?4, ?5 WHERE EXISTS (SELECT 1 FROM agents WHERE id = ?1 AND version = ?2 AND updated_at = ?5)`,
742 )
743 .bind(row.id, version, JSON.stringify(definition), by, now),
744 ]);
745 if (!updated.meta.changes) return fail("conflict", `@${before.handle} was changed meanwhile. Reload it and try again.`);
746 } catch (error) {
747 if (String(error).includes("UNIQUE")) return fail("conflict", `${a.workspace} already has an agent called @${definition.handle}.`);
748 throw error;
749 }
750 const renamed = definition.handle !== before.handle ? ` (was @${before.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.751 this.audit(a.viewer!, a.workspace, "update_agent", definition.handle, `Changed @${definition.handle}${renamed} to version ${version}`, "agents", isPersonal(row) ? "member" : "owner");
752 const saved = await this.row(workspaceId, definition.handle);
Chat and workspace agents: channels, DMs and named agents you talk to753 return ok(toAgent(saved!, new Date()));
754 }
755
Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how.756 /** What each effort level has cost one agent, from its own finished sessions. */
757 async effortCosts(a: { workspace: string; handle: string; viewer: User | null }): Promise<Result<AgentEffortCosts>> {
758 if (a?.viewer && awaitsConfirmation(a.viewer)) return UNVERIFIED;
759 const seen = await this.seen(a?.workspace ?? "", a?.viewer ?? null);
760 if (!seen.ok) return seen;
761 const row = await this.row(seen.value, a.handle);
762 if (!row) return fail("not_found", `There is no agent called @${a.handle}.`);
763 const outcomes = await outcomesSince(this.db, seen.value, sinceWindow(), row.id);
764 return ok(effortCostsOf(row.handle, effortOf(definitionOf(row).routing), outcomes.get(row.id) ?? []));
765 }
766
767 /** Ways to spend less, checked against past work: every agent's, or one's. */
768 async recommendations(a: { workspace: string; viewer: User | null; handle?: string | null }): Promise<Result<AgentRecommendations>> {
769 if (a?.viewer && awaitsConfirmation(a.viewer)) return UNVERIFIED;
770 const seen = await this.seen(a?.workspace ?? "", a?.viewer ?? null);
771 if (!seen.ok) return seen;
772 let agentId: string | null = null;
773 if (a.handle) {
774 const row = await this.row(seen.value, a.handle);
775 if (!row) return fail("not_found", `There is no agent called @${a.handle}.`);
776 agentId = row.id;
777 }
778 return ok(await readRecommendations(this.db, seen.value, agentId));
779 }
780
781 /**
782 * Owners apply a suggestion (the agent's effort changes as a new version
783 * of it, and the audit log says so) or dismiss it. Only an open one, and
784 * only while the agent's setting is still the one it was made for.
785 */
786 async resolveRecommendation(a: { workspace: string; viewer: User | null; id: string; action: string }): Promise<Result<AgentRecommendation>> {
787 const managed = await this.managed(a?.workspace ?? "", a?.viewer ?? null);
788 if (!managed.ok) return managed;
789 if (a.action !== "apply" && a.action !== "dismiss") return fail("invalid", "Apply or dismiss.");
790 const found = await recommendationRow(this.db, managed.value, String(a.id ?? ""));
791 if (!found) return fail("not_found", "There is no such suggestion.");
792 if (found.status !== "open") return fail("conflict", "This suggestion was already decided, or is no longer current.");
793 const by = a.viewer!.username;
794 if (a.action === "dismiss") {
795 if (!(await markResolved(this.db, found.id, "dismissed", by))) return fail("conflict", "This suggestion was decided meanwhile.");
796 this.audit(a.viewer!, a.workspace, "dismiss_recommendation", found.handle ?? found.agent_id, `Dismissed "${found.title}"`);
797 } else {
798 const agent = await this.row(managed.value, found.handle);
799 if (!agent || agent.id !== found.agent_id) return fail("not_found", "That agent is gone.");
800 if (effortOf(definitionOf(agent).routing) !== found.from_effort) {
801 await this.db.prepare("UPDATE agent_recommendations SET status = 'stale' WHERE id = ? AND status = 'open'").bind(found.id).run();
802 return fail("conflict", `@${agent.handle}'s effort was changed since this was suggested. The next check looks again.`);
803 }
804 // Claimed first, so two owners pressing Apply change the agent once.
805 if (!(await markResolved(this.db, found.id, "applied", by))) return fail("conflict", "This suggestion was decided meanwhile.");
806 const changed = await this.update({ workspace: a.workspace, handle: agent.handle, viewer: a.viewer, changes: { routing: { effort: found.to_effort as never } } });
807 if (!changed.ok) {
808 await this.db.prepare("UPDATE agent_recommendations SET status = 'open', resolved_by = NULL, resolved_at = NULL WHERE id = ?").bind(found.id).run();
809 return changed;
810 }
811 this.audit(
812 a.viewer!,
813 a.workspace,
814 "apply_recommendation",
815 agent.handle,
816 `Applied "${found.title}": @${agent.handle}'s effort from ${EFFORT_NAMES[found.from_effort as keyof typeof EFFORT_NAMES] ?? found.from_effort} to ${EFFORT_NAMES[found.to_effort as keyof typeof EFFORT_NAMES] ?? found.to_effort}`,
817 );
818 }
819 const after = await recommendationRow(this.db, managed.value, found.id);
820 return ok(toRecommendation(after!));
821 }
822
Chat and workspace agents: channels, DMs and named agents you talk to823 async archive(a: { workspace: string; handle: string; viewer: User | null }): Promise<Result<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.824 const found = await this.visible(a.workspace, a.viewer, a.handle);
825 if (!found.ok) return found;
826 const { row } = found.value;
827 // Owners archive any agent; a member their own personal one.
828 if (!canArchive(a.viewer, a.workspace, row)) return fail("forbidden", MANAGE_REFUSAL);
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)829 if (row.builtin) return fail("invalid", "@g1t is every workspace's orchestrator and can't be archived. You can set its budget to limit it.");
Chat and workspace agents: channels, DMs and named agents you talk to830 const now = new Date().toISOString();
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.831 await this.db.prepare("UPDATE agents SET archived_at = ?, archived_by = ?, updated_at = ? WHERE id = ? AND archived_at IS NULL").bind(now, a.viewer!.username, now, row.id).run();
Chat and workspace agents: channels, DMs and named agents you talk to832 this.audit(a.viewer!, a.workspace, "archive_agent", row.handle, `Archived @${row.handle}`);
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.833 // Its computer goes to sleep now; its disk is kept 30 days, then deleted (computer.ts).
834 this.defer(forgetComputer(this.env, row.id));
Chat and workspace agents: channels, DMs and named agents you talk to835 return ok(null);
836 }
837
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.838 // ── Archived agents (./archived.ts) ──────────────────────────────────────
839
840 /** The archived agents the viewer may see: every one for an owner, their own archived personal agents for anyone else. */
841 async listArchived(a: { workspace: string; viewer: User | null }): Promise<Result<ArchivedAgent[]>> {
842 if (a?.viewer && awaitsConfirmation(a.viewer)) return UNVERIFIED;
843 const seen = await this.seen(a?.workspace ?? "", a?.viewer ?? null);
844 if (!seen.ok) return seen;
845 const viewer = a.viewer!;
846 return ok(await listArchived(this.db, seen.value, { id: (viewer.kind ?? "user") === "user" ? viewer.id : "", owner: canManage(viewer, a.workspace) }));
847 }
848
849 /**
850 * An archived agent the viewer may see, by id, else the one most recently
851 * archived under `handle` (a handle is unique only among live agents).
852 */
853 private async archivedRow(workspace: string, viewer: User | null, a: { id?: unknown; handle?: unknown }): Promise<Result<{ workspaceId: string; row: Row }>> {
854 if (awaitsConfirmation(viewer)) return UNVERIFIED;
855 const seen = await this.seen(workspace, viewer);
856 if (!seen.ok) return seen;
857 const now = new Date();
858 const handle = typeof a.handle === "string" ? a.handle.trim().replace(/^@/, "").toLowerCase() : "";
859 const row =
860 typeof a.id === "string" && a.id
861 ? await this.db.prepare(selectAgents("a.workspace_id = ?3 AND a.id = ?4 AND a.archived_at IS NOT NULL")).bind(...periods(now), seen.value, a.id).first<Row>()
862 : handle
863 ? await this.db
864 .prepare(`${selectAgents("a.workspace_id = ?3 AND a.handle = ?4 AND a.archived_at IS NOT NULL")} ORDER BY a.archived_at DESC LIMIT 1`)
865 .bind(...periods(now), seen.value, handle)
866 .first<Row>()
867 : null;
868 if (!row || !canSeeAgent(viewer, workspace, row)) return fail("not_found", `There is no archived agent called @${handle || String(a.id ?? "")}.`);
869 return ok({ workspaceId: seen.value, row });
870 }
871
872 /**
873 * Brings an archived agent back, live again under its handle, or under
874 * `new_handle` when a live agent took the old one (`conflict` says so).
875 * Owners any agent, a member their own personal one. A new handle is a
876 * new version, as any rename is. Its computer's disk is kept after all.
877 */
878 async restore(a: { workspace: string; handle?: string; id?: string; viewer: User | null; new_handle?: unknown }): Promise<Result<WorkspaceAgent>> {
879 const found = await this.archivedRow(a?.workspace ?? "", a?.viewer ?? null, a ?? {});
880 if (!found.ok) return found;
881 const { workspaceId, row } = found.value;
882 const refused = restoreRefusal(a.viewer, a.workspace, row);
883 if (refused) return { ok: false, error: refused };
884 const live = await this.db
885 .prepare("SELECT handle FROM agents WHERE workspace_id = ? AND archived_at IS NULL")
886 .bind(workspaceId)
887 .all<{ handle: string }>();
888 const taken = new Set(live.results.map((r) => r.handle));
889 const handle = restoreHandle(row, a.new_handle, (h) => taken.has(h));
890 if (!handle.ok) return handle;
891 const now = new Date().toISOString();
892 const renamed = handle.handle !== row.handle;
893 const version = renamed ? row.version + 1 : row.version;
894 try {
895 const [restored] = await this.db.batch([
896 this.db
897 .prepare("UPDATE agents SET archived_at = NULL, archived_by = NULL, handle = ?, version = ?, updated_at = ? WHERE id = ? AND archived_at IS NOT NULL")
898 .bind(handle.handle, version, now, row.id),
899 ...(renamed ? [this.versionStatement(row.id, version, { ...definitionOf(row), handle: handle.handle }, a.viewer!.username, now)] : []),
900 ]);
901 if (!restored.meta.changes) return fail("not_found", `@${row.handle} was restored or deleted meanwhile.`);
902 } catch (error) {
903 if (String(error).includes("UNIQUE")) return fail("conflict", `A live agent is called @${handle.handle} now. Rename that one first, or restore this one under a new handle.`);
904 throw error;
905 }
906 const was = renamed ? ` (was @${row.handle})` : "";
907 this.audit(a.viewer!, a.workspace, "restore_agent", handle.handle, `Restored @${handle.handle}${was}`, "agents", isPersonal(row) ? "member" : "owner");
908 this.defer(keepComputer(this.env, row.id));
909 const saved = await this.row(workspaceId, handle.handle);
910 return ok(toAgent(saved!, new Date()));
911 }
912
913 /**
914 * Deletes an archived agent for good (archived.ts `DESTROYED`): every row
915 * the service keeps about it, in one batch; then its computer on the
916 * runner, its chat memberships (its messages stay under the name it
917 * had) and its team memberships. Owners any agent, a member their own
918 * personal one; `confirm` is its handle typed back. A live agent is
919 * archived first. A second call finds nothing.
920 */
921 async destroy(a: { workspace: string; handle?: string; id?: string; viewer: User | null; confirm?: unknown }): Promise<Result<null>> {
922 const found = await this.archivedRow(a?.workspace ?? "", a?.viewer ?? null, a ?? {});
923 if (!found.ok) {
924 // A live agent of that handle: say what to do, rather than "no such archived agent".
925 if (found.error.code === "not_found" && typeof a?.handle === "string") {
926 const live = await this.visible(a.workspace, a.viewer, a.handle);
927 if (live.ok) {
928 const refused = destroyRefusal(a.viewer, a.workspace, live.value.row, a.confirm);
929 return { ok: false, error: refused ?? found.error };
930 }
931 }
932 return found;
933 }
934 const { workspaceId, row } = found.value;
935 const refused = destroyRefusal(a.viewer, a.workspace, row, a.confirm);
936 if (refused) return { ok: false, error: refused };
937 const results = await this.db.batch(destroyStatements(this.db, row.id));
938 if (!results[results.length - 1]!.meta.changes) return fail("not_found", `@${row.handle} was restored or deleted meanwhile.`);
939 this.audit(a.viewer!, a.workspace, "delete_agent", row.handle, `Deleted the archived agent @${row.handle} for good`, "agents", isPersonal(row) ? "member" : "owner");
940 const slug = a.workspace.toLowerCase();
941 const definition = definitionOf(row);
942 this.defer(eraseComputer(this.env, row.id));
943 this.defer(
944 chatClient(this.env.CHAT)
945 .agentDestroyed({ workspace_id: workspaceId, agent_id: row.id, handle: row.handle, display_name: row.display_name, avatar_seed: definition.avatar_seed, look: definition.look })
946 .then((told) => (told.ok ? null : console.error("agents: chat was not told of a deleted agent", row.id, told.error.message)))
947 .catch((error: unknown) => console.error("agents: chat was not told of a deleted agent", row.id, String(error))),
948 );
949 this.defer(this.leaveTeams(slug, a.viewer!, row.id));
950 return ok(null);
951 }
952
953 /** A deleted agent comes off every team it was on (memberships live in identity). */
954 private async leaveTeams(slug: string, by: User, agentId: string): Promise<void> {
955 try {
956 const identity = identityClient(this.env.IDENTITY);
957 const teams = await identity.agentTeams(slug, agentId);
958 await Promise.all(
959 teams.map((team) => identity.removeTeamAgent(by, slug, team.slug, agentId).catch((error: unknown) => console.error("agents: a deleted agent was not taken off its team", agentId, team.slug, String(error)))),
960 );
961 } catch (error) {
962 console.error("agents: a deleted agent's teams were not read", agentId, String(error));
963 }
964 }
965
Chat and workspace agents: channels, DMs and named agents you talk to966 /**
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)967 * Makes the workspace's built-in @g1t if it does not exist yet: an
968 * ordinary agent row, marked builtin, at version 1. Safe to call on
969 * every request; once seen, an isolate does not write again.
970 */
971 private async ensureBuiltin(workspaceId: string): Promise<void> {
972 await ensureBuiltin(this.db, workspaceId);
973 }
974
975 /** Internal: the workspace's @g1t, made if need be, for the chat service. */
976 async builtin(a: { workspace: string; workspace_id: string }): Promise<Result<WorkspaceAgent>> {
977 if (typeof a?.workspace_id !== "string" || !a.workspace_id) return fail("invalid", "Name the workspace by id.");
978 await this.ensureBuiltin(a.workspace_id);
979 const now = new Date();
980 const row = await this.db
981 .prepare(selectAgents("a.workspace_id = ?3 AND a.builtin = 1"))
982 .bind(...periods(now), a.workspace_id)
983 .first<Row>();
984 return row ? ok(toAgent(row, now)) : fail("not_found", "This workspace's @g1t could not be made.");
985 }
986
987 /**
Chat and workspace agents: channels, DMs and named agents you talk to988 * Hands a message to the agent's desk, which answers it in the
989 * background. Returns as soon as the desk holds it.
990 */
991 async deliver(delivery: AgentDelivery): Promise<Result<null>> {
992 const fields = ["workspace", "workspace_id", "channel_id", "agent_id", "message_id", "asked_by"] as const;
993 if (!delivery || fields.some((field) => typeof delivery[field] !== "string" || !delivery[field])) {
994 return fail("invalid", "A delivery names the workspace, channel, agent, message and who asked.");
995 }
996 if (delivery.channel_kind !== "channel" && delivery.channel_kind !== "dm") return fail("invalid", "channel_kind is channel or dm.");
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)997 await this.ensureBuiltin(delivery.workspace_id);
Chat and workspace agents: channels, DMs and named agents you talk to998 const desk = this.env.DESKS.get(this.env.DESKS.idFromName(delivery.agent_id));
999 await desk.take({ ...delivery, hops: Math.max(0, Math.floor(Number(delivery.hops) || 0)), thread_root: delivery.thread_root ?? null });
1000 return ok(null);
1001 }
1002
1003 private versionStatement(agentId: string, version: number, d: Definition, by: string, at: string): D1PreparedStatement {
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1004 return versionStatement(this.db, agentId, version, d, by, at);
Chat and workspace agents: channels, DMs and named agents you talk to1005 }
1006
1007 /**
1008 * Records a change to an agent in the workspace's audit log, after the
1009 * answer. Never fails the change: a log that cannot be written is logged.
1010 * The events service's audit contract speaks camelCase (Rust's
1011 * `NewAuditEntry`).
1012 */
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.1013 /** A change to the skill library, in the audit log as `agents/skills/<name>`. */
1014 auditSkill(actor: User, workspace: string, action: string, name: string, message: string): void {
1015 this.audit(actor, workspace, action, `skills/${name}`, message);
1016 }
1017
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.1018 private audit(
1019 actor: User,
1020 workspace: string,
1021 action: string,
1022 handle: string,
1023 message: string,
1024 area: "agents" | "marketplace" = "agents",
1025 rule: "owner" | "member" | null = null,
1026 ): void {
Chat and workspace agents: channels, DMs and named agents you talk to1027 const kind = actor.kind === "workspace" ? "workspace" : actor.kind === "agent" ? "agent" : actor.kind === "system" ? "system" : "person";
1028 const entry = {
1029 actorKind: kind,
1030 actor: actor.username,
1031 actorId: actor.id,
1032 agent: null,
1033 onBehalfOf: null,
1034 runId: null,
1035 runKind: null,
1036 credentialId: null,
1037 action,
1038 surface: "web",
1039 workspace: workspace.toLowerCase(),
1040 repo: null,
1041 number: null,
1042 gitRef: null,
The Marketplace is where a workspace finds what to add, reached from Apps: the agent catalog of roles with who already does each job and Add to workspace; every integration with whether it's connected; g1t's own extensions, listed as Soon; and requests, where members ask and owners add or turn down, each told by a notification. Underneath, an extension is a manifest (publisher and tier, source and version from a tag, scopes, the domains its data goes to, its page on g1tusercontent.com, no price) and an install pins its version with an off switch, a budget and the audit log. Apps lists only what was added from the Marketplace, with pins; g1t's own pages stay in their sidebars, and AI Gateway joins Workspace's. The marketplace guide says how.1043 path: `${area}/${handle}`,
Chat and workspace agents: channels, DMs and named agents you talk to1044 outcome: "allowed",
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.1045 rule: rule ?? (area === "marketplace" && action === "request_install" ? "member" : "owner"),
Chat and workspace agents: channels, DMs and named agents you talk to1046 result: "ok",
1047 message,
1048 requestId: `req_${crypto.randomUUID()}`,
1049 };
1050 this.defer(
1051 this.env.EVENTS.fetch("https://service/rpc/audit_record", {
1052 method: "POST",
1053 headers: { "content-type": "application/json" },
1054 body: JSON.stringify({ entries: [entry] }),
1055 })
1056 .then((response) => {
1057 if (!response.ok) throw new Error(`status ${response.status}`);
1058 })
1059 .catch((error: unknown) => console.error("agents: audit entry not recorded", action, handle, String(error))),
1060 );
1061 }
1062}
1063
1064/** One RPC method's answer. */
1065async function answer(service: Agents, method: string, args: any): Promise<Response> {
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.1066 // The skill library (./skill-rpc.ts).
1067 const skill = await skillRpc(method, args, (a, run) => service.view(a, run), (viewer, workspace, action, name, message) => service.auditSkill(viewer, workspace, action, name, message));
1068 if (skill) return Response.json(skill);
Chat and workspace agents: channels, DMs and named agents you talk to1069 switch (method) {
1070 case "list":
1071 return Response.json(await service.list(args));
1072 case "get":
1073 return Response.json(await service.get(args));
1074 case "by_ids":
1075 return Response.json(await service.byIds(args));
1076 case "create":
1077 return Response.json(await service.create(args));
1078 case "update":
1079 return Response.json(await service.update(args));
1080 case "archive":
1081 return Response.json(await service.archive(args));
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.1082 case "list_archived":
1083 return Response.json(await service.listArchived(args));
1084 case "restore":
1085 return Response.json(await service.restore(args));
1086 case "destroy":
1087 return Response.json(await service.destroy(args));
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.1088 case "draft":
1089 return Response.json(await service.draft(args));
1090 case "try_draft":
1091 return Response.json(await service.tryDraft(args));
1092 case "redraft":
1093 return Response.json(await service.redraft(args));
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.1094 case "add_mcp_server":
1095 return Response.json(await service.addMcpServer(args));
1096 case "remove_mcp_server":
1097 return Response.json(await service.removeMcpServer(args));
1098 case "refresh_mcp_server":
1099 return Response.json(await service.refreshMcpServer(args));
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.1100 case "promote":
1101 return Response.json(await service.promote(args));
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1102 case "builtin":
1103 return Response.json(await service.builtin(args));
Chat and workspace agents: channels, DMs and named agents you talk to1104 case "templates":
1105 return Response.json(TEMPLATES);
1106 case "deliver":
1107 return Response.json(await service.deliver(args));
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 asked1108 case "overview":
1109 return Response.json(await service.view(args, (ctx) => views.overview(ctx)));
1110 case "sessions":
1111 return Response.json(await service.view(args, (ctx) => views.listSessions(ctx, args)));
1112 case "session":
1113 return Response.json(await service.view(args, (ctx) => views.sessionDetail(ctx, args.id)));
1114 case "stop_session":
1115 return Response.json(await service.view(args, (ctx) => views.stopSession(ctx, args.id)));
1116 case "approve_session":
1117 return Response.json(await service.view(args, (ctx) => views.approveSession(ctx, args.id, args.cap_micros)));
1118 case "steer_session":
1119 return Response.json(await service.view(args, (ctx) => views.steerSession(ctx, args.id, args.body)));
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.1120 // The agent's own computer (./computer.ts).
1121 case "computer":
1122 return Response.json(await service.view(args, (ctx) => computerView(ctx, args.handle)));
1123 case "computer_wake":
1124 return Response.json(await service.view(args, (ctx) => wakeComputer(ctx, args.handle)));
1125 case "computer_sleep":
1126 return Response.json(await service.view(args, (ctx) => sleepComputer(ctx, args.handle)));
1127 case "computer_reset":
1128 return Response.json(await service.view(args, (ctx) => resetComputer(ctx, args.handle)));
1129 case "computer_commands":
1130 return Response.json(await service.view(args, (ctx) => computerCommands(ctx, args.handle, args.session_id ?? 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 asked1131 case "memories":
1132 return Response.json(await service.view(args, (ctx) => views.memories(ctx, args.handle)));
1133 case "remember":
1134 return Response.json(await service.view(args, (ctx) => views.remember(ctx, args.handle, args.input)));
1135 case "update_memory":
1136 return Response.json(await service.view(args, (ctx) => views.updateMemory(ctx, args.handle, args.id, args.changes)));
1137 case "forget":
1138 return Response.json(await service.view(args, (ctx) => views.forget(ctx, args.handle, args.id)));
1139 case "routines":
1140 return Response.json(await service.view(args, (ctx) => views.routines(ctx, args.handle)));
1141 case "save_routine":
1142 return Response.json(await service.view(args, (ctx) => views.saveRoutine(ctx, args.handle, args.input, args.id ?? null)));
1143 case "delete_routine":
1144 return Response.json(await service.view(args, (ctx) => views.deleteRoutine(ctx, args.handle, args.id)));
1145 case "run_routine":
1146 return Response.json(await service.view(args, (ctx) => views.runRoutineNow(ctx, args.handle, args.id)));
1147 case "spend":
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.1148 return Response.json(await service.view(args, (ctx) => views.spend(ctx, args.handle ?? null, { period: args.period, person: args.person })));
1149 case "person_budgets":
1150 return Response.json(await service.view(args, (ctx) => views.personBudgetsView(ctx)));
1151 case "set_person_budget":
1152 return Response.json(await service.view(args, (ctx) => views.setPersonBudget(ctx, args.username, args.monthly_micros)));
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.1153 case "team_context":
1154 return Response.json(await service.view(args, (ctx) => views.teamContext(ctx, args.handle)));
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 asked1155 case "activity":
1156 return Response.json(await service.view(args, (ctx) => views.activity(ctx, args.handle)));
1157 case "versions":
1158 return Response.json(await service.view(args, (ctx) => views.versions(ctx, args.handle)));
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1159 case "card_action":
1160 return Response.json(await service.cardAction(args));
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 asked1161 case "policy":
1162 return Response.json(await service.view(args, (ctx) => views.policy(ctx)));
The Marketplace is where a workspace finds what to add, reached from Apps: the agent catalog of roles with who already does each job and Add to workspace; every integration with whether it's connected; g1t's own extensions, listed as Soon; and requests, where members ask and owners add or turn down, each told by a notification. Underneath, an extension is a manifest (publisher and tier, source and version from a tag, scopes, the domains its data goes to, its page on g1tusercontent.com, no price) and an install pins its version with an off switch, a budget and the audit log. Apps lists only what was added from the Marketplace, with pins; g1t's own pages stay in their sidebars, and AI Gateway joins Workspace's. The marketplace guide says how.1163 case "install_requests":
1164 return Response.json(await service.installRequests(args));
1165 case "request_install":
1166 return Response.json(await service.requestInstall(args));
1167 case "resolve_install_request":
1168 return Response.json(await service.resolveInstallRequest(args));
1169 case "extension_installs":
1170 return Response.json(await service.extensionInstalls(args));
1171 case "install_extension":
1172 return Response.json(await service.installExtension(args));
1173 case "set_extension_enabled":
1174 return Response.json(await service.setExtensionEnabled(args));
1175 case "set_extension_budget":
1176 return Response.json(await service.setExtensionBudget(args));
1177 case "uninstall_extension":
1178 return Response.json(await service.uninstallExtension(args));
Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how.1179 case "effort_costs":
1180 return Response.json(await service.effortCosts(args));
1181 case "recommendations":
1182 return Response.json(await service.recommendations(args));
1183 case "resolve_recommendation":
1184 return Response.json(await service.resolveRecommendation(args));
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 asked1185 case "set_policy":
1186 return Response.json(await service.view(args, (ctx) => views.setPolicy(ctx, args.policy)));
Chat and workspace agents: channels, DMs and named agents you talk to1187 default:
1188 return new Response("Unknown method\n", { status: 404 });
1189 }
1190}
1191
1192export default {
1193 async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
1194 const match = new URL(request.url).pathname.match(/^\/rpc\/([a-z_]+)$/);
1195 if (request.method !== "POST" || !match) return new Response("Not found\n", { status: 404 });
1196 // A replica near the caller when it asks for one (@g1t/contracts d1.ts).
1197 const opened = openD1(env.DB, request);
1198 const service = new Agents(Object.create(env, { DB: { value: opened.db } }) as Env, (work) => ctx.waitUntil(work));
1199 const args = (await request.json().catch(() => ({}))) as any;
1200 return opened.finish(await answer(service, match[1], args));
1201 },
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 asked1202
@g1t welcomes you when you join a workspace, and that is the one introduction any agent makes. Identity now says when a person becomes a member, workspace.member_joined, by invitation, by an invite code, or by creating the workspace, and webhooks can hear it; the agents service hears it too, and for anyone who joined, never the creator, the workspace's @g1t opens your direct message and says in its own voice who it is, what to ask it, how agents answer, and where things are, once per person per workspace and charged to @g1t like any reply, with a fixed welcome when no model can be used. The agents, workspaces, authentication and webhooks guides say so.1203 /** Events routines run on, and people joining whom @g1t welcomes, from the events service (SUBSCRIBER_AGENTS). */
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened1204 async queue(batch: MessageBatch<unknown>, env: Env): Promise<void> {
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.1205 const events = batch.messages.map((message) => message.body as G1tEvent);
1206 // A push to a default branch: skill libraries that follow that repository read it again (./skill-library.ts).
1207 const pushes = events.flatMap((event) => (event?.type === "git.push" && event.data?.defaultBranch && event.data.repoId ? [{ repoId: event.data.repoId }] : []));
@g1t welcomes you when you join a workspace, and that is the one introduction any agent makes. Identity now says when a person becomes a member, workspace.member_joined, by invitation, by an invite code, or by creating the workspace, and webhooks can hear it; the agents service hears it too, and for anyone who joined, never the creator, the workspace's @g1t opens your direct message and says in its own voice who it is, what to ask it, how agents answer, and where things are, once per person per workspace and charged to @g1t like any reply, with a fixed welcome when no model can be used. The agents, workspaces, authentication and webhooks guides say so.1208 await Promise.all([
1209 onEvents(env as unknown as SessionEnv, events.filter((event) => event?.type !== "git.push")),
1210 pushes.length ? skillPushes(env as unknown as SessionEnv, pushes) : null,
1211 // A person joined a workspace: its @g1t says welcome, once (./welcome.ts).
1212 welcome(env as unknown as SessionEnv, events).catch((error: unknown) => console.error("agents: welcomes were not sent", String(error))),
1213 ]);
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened1214 batch.ackAll();
1215 },
1216
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.1217 /** Every few minutes: routines that are due, session steps a desk lost, the spend check, and the one-time team move. */
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 asked1218 async scheduled(_controller: ScheduledController, env: Env, ctx: ExecutionContext): Promise<void> {
1219 const sessions = env as unknown as SessionEnv;
1220 ctx.waitUntil(
1221 Promise.all([
1222 runDue(sessions).catch((error: unknown) => console.error("agents: routines did not run", String(error))),
1223 sweep(sessions).catch((error: unknown) => console.error("agents: the session sweep failed", String(error))),
Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how.1224 // Spend less, keep quality: each workspace checked weekly (src/recommend.ts).
1225 checkDue(env.DB).catch((error: unknown) => console.error("agents: the spend check failed", String(error))),
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.1226 // Once: agents' own old teams become team memberships (src/team-move.ts); after that, one cheap read.
1227 moveAgentTeams(env.DB, (id, agents) => identityClient(env.IDENTITY).adoptAgentTeams(id, agents)).catch((error: unknown) => console.error("agents: moving agents' teams failed", String(error))),
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 asked1228 ]),
1229 );
1230 },
Chat and workspace agents: channels, DMs and named agents you talk to1231} satisfies ExportedHandler<Env>;

This file's history is long; its oldest lines are credited to the oldest commit read.