Skip to content
1,168 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 InstallRequest,
22 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.23 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.24 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.25 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.26 type AgentRecommendation,
27 type AgentRecommendations,
Chat and workspace agents: channels, DMs and named agents you talk to28 type NewWorkspaceAgent,
29 type Result,
30 type ServiceBinding,
31 type User,
32 type WorkspaceAgent,
33 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)34 askerAccess,
Chat and workspace agents: channels, DMs and named agents you talk to35 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)36 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.37 cleanRequestNote,
Chat and workspace agents: channels, DMs and named agents you talk to38 fail,
39 identityClient,
40 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.41 notifyClient,
Chat and workspace agents: channels, DMs and named agents you talk to42 ok,
43 openD1,
44} from "@g1t/contracts";
45
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.46import { MANAGE_REFUSAL, NOT_YOURS_REFUSAL, canArchive, canChange, canManage, canSee, canSeeAgent, creatableScope } from "./access.ts";
47import { DRAFT_OUTPUT_TOKENS, draftSystem, jsonIn, proposalFrom, redraftFrom, redraftSystem, startingBudget, trySystem, tryTurns, wordsOf } from "./builder.ts";
48import { callBuilder } from "./builder-call.ts";
Chat and workspace agents: channels, DMs and named agents you talk to49import { 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.50import { listMcpTools } from "./mcp-client.ts";
51import { 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)52import { builtinChanges } from "./orchestrator.ts";
Chat and workspace agents: channels, DMs and named agents you talk to53import type { Desk } from "./desk.ts";
54import 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)55import { 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.56import { type Row, definitionOf, insertAgent, isPersonal, periods, selectAgents, toAgent, updateAgent, versionStatement } from "./store.ts";
Chat and workspace agents: channels, DMs and named agents you talk to57import { 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 asked58import { readPolicy } from "./policy.ts";
59import { runDue } from "./routines.ts";
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened60import { 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.61import { welcome } from "./welcome.ts";
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar62import { 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 asked63import { 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.64import { 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.65import { 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.66import { 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 asked67import * 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.68import { computerCommands, computerView, eraseComputer, forgetComputer, keepComputer, resetComputer, sleepComputer, wakeComputer } from "./computer.ts";
69import { 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 asked70import { monthKey } from "./budget.ts";
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.71import { customizeRpc } from "./customize-rpc.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.72import { 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.73import { 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 to74
75export { Desk } from "./desk.ts";
76
77type Env = ReplyEnv & {
78 IDENTITY: ServiceBinding;
79 /** The audit log. */
80 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.81 /** The runner, for agents' own computers (./computer.ts). */
82 RUNNER?: ServiceBinding;
Chat and workspace agents: channels, DMs and named agents you talk to83 DESKS: DurableObjectNamespace<Desk>;
84};
85
86/** Workspace ids by slug, kept a minute: every call names a workspace by slug. */
87const workspaceIds = new Map<string, { id: string | null; until: number }>();
88
89class Agents {
90 private readonly env: Env;
91 private readonly db: D1Database;
92 private readonly defer: (work: Promise<unknown>) => void;
93
94 // Plain fields, not parameter properties: Node's type stripping does not take those.
95 constructor(env: Env, defer: (work: Promise<unknown>) => void) {
96 this.env = env;
97 this.db = env.DB;
98 this.defer = defer;
99 }
100
101 private async workspaceId(slug: string): Promise<string | null> {
102 const key = slug.toLowerCase();
103 const kept = workspaceIds.get(key);
104 if (kept && kept.until > Date.now()) return kept.id;
105 const workspace = await identityClient(this.env.IDENTITY).getWorkspace(key);
106 if (workspaceIds.size > 5_000) workspaceIds.clear();
107 workspaceIds.set(key, { id: workspace?.id ?? null, until: Date.now() + 60_000 });
108 return workspace?.id ?? null;
109 }
110
111 /** The workspace's id, if the viewer may see its agents. */
112 private async seen(workspace: string, viewer: User | null): Promise<Result<string>> {
113 if (!canSee(viewer, workspace)) return fail("not_found", "There is no such workspace.");
114 const id = await this.workspaceId(workspace);
115 return id ? ok(id) : fail("not_found", "There is no such workspace.");
116 }
117
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar118 /** Internal, from chat: a person pressed an action on one of agents' cards. */
119 async cardAction(a: AgentCardAction): Promise<Result<CardActionResult>> {
120 if (a?.viewer && awaitsConfirmation(a.viewer)) return UNVERIFIED;
121 if (!a?.viewer || !canSee(a.viewer, a.workspace ?? "")) return fail("not_found", "No such card.");
122 return cardAction(this.env as unknown as SessionEnv, a);
123 }
124
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 asked125 /** What the views need: the workspace, the viewer, and whether they own it. */
126 private async context(workspace: string, viewer: User | null): Promise<Result<views.ViewContext>> {
127 if (viewer && awaitsConfirmation(viewer)) return UNVERIFIED;
128 const seen = await this.seen(workspace, viewer);
129 if (!seen.ok) return seen;
130 return ok({
131 env: this.env as unknown as SessionEnv,
132 db: this.db,
133 slug: workspace.toLowerCase(),
134 workspaceId: seen.value,
135 viewer: viewer!,
136 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.137 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 asked138 });
139 }
140
141 /** Runs `view` with the context, or answers why it can't. */
142 async view<T>(a: { workspace: string; viewer: User | null }, view: (ctx: views.ViewContext) => Promise<Result<T>>): Promise<Result<T>> {
143 const ctx = await this.context(a?.workspace ?? "", a?.viewer ?? null);
144 if (!ctx.ok) return ctx;
145 return view(ctx.value);
146 }
147
Chat and workspace agents: channels, DMs and named agents you talk to148 /** The workspace's id, if the viewer may change its agents. */
149 private async managed(workspace: string, viewer: User | null): Promise<Result<string>> {
150 if (awaitsConfirmation(viewer)) return UNVERIFIED;
151 const seen = await this.seen(workspace, viewer);
152 if (!seen.ok) return seen;
153 return canManage(viewer, workspace) ? seen : fail("forbidden", MANAGE_REFUSAL);
154 }
155
156 private async row(workspaceId: string, handle: unknown): Promise<Row | null> {
157 if (typeof handle !== "string") return null;
158 const now = new Date();
159 return this.db
160 .prepare(selectAgents("a.workspace_id = ?3 AND a.handle = ?4 AND a.archived_at IS NULL"))
161 .bind(...periods(now), workspaceId, handle.trim().replace(/^@/, "").toLowerCase())
162 .first<Row>();
163 }
164
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.165 /**
166 * The teams a new agent joins as it is made, by slug: each must be one the
167 * viewer manages (an owner, or the team's maintainer), as adding anyone
168 * to a team asks. Membership lives on the team, in identity.
169 */
170 private async joinable(workspace: string, viewer: User, asked: unknown): Promise<Result<string[]>> {
171 if (asked === undefined || asked === null) return ok([]);
172 if (!Array.isArray(asked) || asked.some((slug) => typeof slug !== "string")) return fail("invalid", "Teams are a list of team slugs.");
173 const slugs = [...new Set((asked as string[]).map((slug) => slug.trim().replace(/^@/, "").toLowerCase()).filter(Boolean))];
174 if (slugs.length > 20) return fail("invalid", "Add a new agent to at most 20 teams.");
175 const identity = identityClient(this.env.IDENTITY);
176 const found = await Promise.all(slugs.map((slug) => identity.getTeam(viewer, workspace.toLowerCase(), slug).catch(() => null)));
177 for (const [i, team] of found.entries()) {
178 if (!team?.ok) return fail("invalid", `${workspace} has no team called ${slugs[i]}.`);
179 if (!team.value.can_manage) return fail("forbidden", `Only owners and ${team.value.name}'s maintainers add agents to it.`);
180 }
181 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)182 }
183
Chat and workspace agents: channels, DMs and named agents you talk to184 private async handleTaken(workspaceId: string, handle: string, except: string | null): Promise<boolean> {
185 const found = await this.db
186 .prepare("SELECT id FROM agents WHERE workspace_id = ? AND handle = ? AND archived_at IS NULL")
187 .bind(workspaceId, handle)
188 .first<{ id: string }>();
189 return !!found && found.id !== except;
190 }
191
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.192 /**
193 * The workspace's agents, and personal agents when asked for: `mine`, the
194 * viewer's own; `all`, every member's for an owner.
195 */
196 async list(a: { workspace: string; viewer: User | null; personal?: unknown }): Promise<Result<WorkspaceAgent[]>> {
Chat and workspace agents: channels, DMs and named agents you talk to197 const seen = await this.seen(a.workspace, a.viewer);
198 if (!seen.ok) return seen;
199 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)200 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.201 const all = a.personal === "all" && canManage(a.viewer, a.workspace);
202 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 to203 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.204 .prepare(
205 `${selectAgents(
206 `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)" : ""})`,
207 )} ORDER BY a.builtin DESC, a.handle`,
208 )
209 .bind(...periods(now), seen.value, ...(mine && !all ? [mine] : []))
Chat and workspace agents: channels, DMs and named agents you talk to210 .all<Row>();
211 return ok(rows.results.map((row) => toAgent(row, now)));
212 }
213
214 async get(a: { workspace: string; handle: string; viewer: User | null }): Promise<Result<WorkspaceAgent>> {
215 const seen = await this.seen(a.workspace, a.viewer);
216 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)217 await this.ensureBuiltin(seen.value);
Chat and workspace agents: channels, DMs and named agents you talk to218 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.219 // Another member's personal agent is theirs: it isn't there for anyone else but owners.
220 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 to221 }
222
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.223 /** The agent by handle, if the viewer may see it. */
224 private async visible(workspace: string, viewer: User | null, handle: unknown): Promise<Result<{ workspaceId: string; row: Row }>> {
225 if (awaitsConfirmation(viewer)) return UNVERIFIED;
226 const seen = await this.seen(workspace, viewer);
227 if (!seen.ok) return seen;
228 const row = await this.row(seen.value, handle);
229 if (!row || !canSeeAgent(viewer, workspace, row)) return fail("not_found", `There is no agent called @${String(handle ?? "")}.`);
230 return ok({ workspaceId: seen.value, row });
231 }
232
Chat and workspace agents: channels, DMs and named agents you talk to233 /** Internal: agents by id, archived ones too, so old messages still show who wrote them. */
234 async byIds(a: { ids: string[] }): Promise<WorkspaceAgent[]> {
235 const ids = [...new Set((Array.isArray(a.ids) ? a.ids : []).filter((id) => typeof id === "string"))].slice(0, 100);
236 if (!ids.length) return [];
237 const now = new Date();
238 const rows = await this.db
239 .prepare(selectAgents(`a.id IN (${ids.map((_, i) => `?${i + 3}`).join(", ")})`))
240 .bind(...periods(now), ...ids)
241 .all<Row>();
242 return rows.results.map((row) => toAgent(row, now));
243 }
244
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.245 /**
246 * Who may create an agent, and of which scope: the workspace's id and
247 * policy, and the scope it gets (access.ts `creatableScope`).
248 */
249 private async creatable(workspace: string, viewer: User | null, asked: unknown): Promise<Result<{ workspaceId: string; scope: "workspace" | "personal"; policy: Awaited<ReturnType<typeof readPolicy>> }>> {
250 if (awaitsConfirmation(viewer)) return UNVERIFIED;
251 const seen = await this.seen(workspace, viewer);
252 if (!seen.ok) return seen;
253 const policy = await readPolicy(this.db, seen.value, monthKey(new Date()));
254 const scope = creatableScope(viewer, workspace, asked, policy.members_create_agents);
255 if (!scope.ok) return fail("forbidden", scope.message);
256 return ok({ workspaceId: seen.value, scope: scope.scope, policy });
257 }
258
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.259 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.260 const allowed = await this.creatable(a.workspace, a.viewer, a.input?.scope);
261 if (!allowed.ok) return allowed;
262 const { workspaceId, scope, policy } = allowed.value;
263 const personal = scope === "personal";
264 const viewer = a.viewer!;
265 if (personal) {
266 const count = await this.db
267 .prepare("SELECT COUNT(*) AS n FROM agents WHERE workspace_id = ? AND owner_id = ? AND scope = 'personal' AND archived_at IS NULL")
268 .bind(workspaceId, viewer.id)
269 .first<{ n: number }>();
270 if ((count?.n ?? 0) >= MAX_PERSONAL_AGENTS) {
271 return fail("invalid", `You have ${MAX_PERSONAL_AGENTS} personal agents here, the most one person keeps. Archive one to make another.`);
272 }
273 }
274 // A new agent starts with a budget, unless one was given: a member's own
275 // $20 a month and $2 a session, or the workspace's default for its agents.
276 const given = a.input?.budget ?? {};
277 const start = startingBudget(scope, policy.default_agent_monthly_micros);
278 const budget = {
279 monthly_micros: given.monthly_micros !== undefined ? given.monthly_micros : start.monthly_micros,
280 daily_micros: given.daily_micros !== undefined ? given.daily_micros : start.daily_micros,
281 task_micros: given.task_micros !== undefined ? given.task_micros : start.task_micros,
282 };
283 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.284 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 asked285 const checked = applyChanges(null, input, TEMPLATE_IDS);
Chat and workspace agents: channels, DMs and named agents you talk to286 if (!checked.ok) return fail("invalid", checked.message);
287 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.288 // A personal agent is on no team: teams are shared, and it answers only its member.
289 const teams = personal ? ok([] as string[]) : await this.joinable(a.workspace, viewer, a.teams);
290 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.291 if (await this.handleTaken(workspaceId, definition.handle, null)) {
Chat and workspace agents: channels, DMs and named agents you talk to292 return fail("conflict", `${a.workspace} already has an agent called @${definition.handle}.`);
293 }
294 const now = new Date().toISOString();
295 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.296 const by = viewer.username;
297 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 to298 try {
299 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.300 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 to301 this.versionStatement(id, 1, definition, by, now),
302 ]);
303 } catch (error) {
304 if (String(error).includes("UNIQUE")) return fail("conflict", `${a.workspace} already has an agent called @${definition.handle}.`);
305 throw error;
306 }
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.307 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.308 // 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.309 if (teams.value.length) {
310 const identity = identityClient(this.env.IDENTITY);
311 await Promise.all(
312 teams.value.map((slug) =>
313 identity
314 .setTeamAgent(viewer, a.workspace.toLowerCase(), slug, id)
315 .then((added) => (added.ok ? null : console.error("agents: a new agent was not added to its team", id, slug, added.error.message)))
316 .catch((error: unknown) => console.error("agents: a new agent was not added to its team", id, slug, String(error))),
317 ),
318 );
319 }
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.320 const row = await this.row(workspaceId, definition.handle);
Chat and workspace agents: channels, DMs and named agents you talk to321 return ok(toAgent(row!, new Date()));
322 }
323
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.324 // ── The builder: describe it, try it, change it in words (./builder.ts) ──
325
326 /** Drafts a whole agent from a description, charged to the viewer. Nothing is saved. */
327 async draft(a: { workspace: string; viewer: User | null; description: unknown; scope?: unknown }): Promise<Result<AgentProposal>> {
328 const allowed = await this.creatable(a?.workspace ?? "", a?.viewer ?? null, a?.scope);
329 if (!allowed.ok) return allowed;
330 const words = wordsOf(a.description, "description");
331 if (!words.ok) return fail("invalid", words.message);
332 const { workspaceId, scope, policy } = allowed.value;
333 const slug = a.workspace.toLowerCase();
334 const [answer, handles] = await Promise.all([
335 callBuilder(this.env, {
336 slug,
337 workspaceId,
338 viewer: a.viewer!,
339 system: draftSystem(slug, scope),
340 messages: [{ role: "user", content: `What the agent should do:\n\n${words.value}` }],
341 maxOutput: DRAFT_OUTPUT_TOKENS,
342 }),
343 this.db.prepare("SELECT handle FROM agents WHERE workspace_id = ? AND archived_at IS NULL").bind(workspaceId).all<{ handle: string }>(),
344 ]);
345 if (!answer.ok) return fail(answer.code, answer.message);
346 const proposal = proposalFrom(jsonIn(answer.text), {
347 scope,
348 taken: new Set(handles.results.map((row) => row.handle)),
349 budget: startingBudget(scope, policy.default_agent_monthly_micros),
350 });
351 if (!proposal.ok) return fail("invalid", proposal.message);
352 return ok({ ...proposal.value, charged_micros: answer.charged });
353 }
354
355 /** Try it: the unsaved definition answers the conversation so far. Charged to the viewer; nothing is saved. */
356 async tryDraft(a: { workspace: string; viewer: User | null; definition: unknown; messages: unknown }): Promise<Result<DraftReply>> {
357 const given = (a?.definition ?? {}) as NewWorkspaceAgent;
358 const allowed = await this.creatable(a?.workspace ?? "", a?.viewer ?? null, given?.scope);
359 if (!allowed.ok) return allowed;
360 const { scope: _scope, ...rest } = given;
361 // A handle someone has already taken is no reason not to try it.
362 const checked = applyChanges(null, { ...rest, handle: rest.handle || "preview" }, TEMPLATE_IDS);
363 if (!checked.ok) return fail("invalid", checked.message);
364 const turns = tryTurns(a.messages);
365 if (!turns.ok) return fail("invalid", turns.message);
366 const viewer = a.viewer!;
367 const answer = await callBuilder(this.env, {
368 slug: a.workspace.toLowerCase(),
369 workspaceId: allowed.value.workspaceId,
370 viewer,
371 system: trySystem({ workspace: a.workspace.toLowerCase(), definition: checked.value, asker: { username: viewer.username, display_name: viewer.display_username ?? null } }),
372 messages: turns.value,
373 routing: { floor: checked.value.routing.floor, ceiling: checked.value.routing.ceiling },
374 });
375 if (!answer.ok) return fail(answer.code, answer.message);
376 return ok({ text: answer.text, charged_micros: answer.charged });
377 }
378
379 /** Drafts changes from a request in words, for whoever may change the agent. Saved only through `update`. */
380 async redraft(a: { workspace: string; handle: string; viewer: User | null; request: unknown }): Promise<Result<AgentRedraft>> {
381 const found = await this.visible(a?.workspace ?? "", a?.viewer ?? null, a?.handle);
382 if (!found.ok) return found;
383 const { workspaceId, row } = found.value;
384 if (!canChange(a.viewer, a.workspace, row)) return fail("forbidden", isPersonal(row) ? NOT_YOURS_REFUSAL : MANAGE_REFUSAL);
385 const words = wordsOf(a.request, "request");
386 if (!words.ok) return fail("invalid", words.message);
387 const before = definitionOf(row);
388 const slug = a.workspace.toLowerCase();
389 const answer = await callBuilder(this.env, {
390 slug,
391 workspaceId,
392 viewer: a.viewer!,
393 system: redraftSystem(slug, before, !!row.builtin),
394 messages: [{ role: "user", content: `The change to make to @${row.handle}:\n\n${words.value}` }],
395 maxOutput: DRAFT_OUTPUT_TOKENS,
396 });
397 if (!answer.ok) return fail(answer.code, answer.message);
398 const drafted = redraftFrom(jsonIn(answer.text), before, !!row.builtin);
399 if (!drafted.ok) return fail("invalid", drafted.message);
400 return ok({ ...drafted.value, from_version: row.version, charged_micros: answer.charged });
401 }
402
403 /**
404 * Owners make a personal agent a workspace agent: a new workspace agent
405 * with the same handle, definition and every version, and one more
406 * version saying so; the personal one is archived in the same step, its
407 * memory and direct messages kept with it (owners can't read a member's
408 * memory to choose from it, so none is carried).
409 */
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.410 // ── MCP servers (docs.g1t.sh/guides/agent-abilities/, "MCP servers") ─────
411
412 /** The agent, if the viewer may add servers to it: owners only, a workspace agent or a personal one alike. */
413 private async forMcp(a: { workspace: string; handle: string; viewer: User | null }): Promise<Result<{ row: Row; workspaceId: string }>> {
414 const found = await this.visible(a?.workspace ?? "", a?.viewer ?? null, a?.handle);
415 if (!found.ok) return found;
416 if (!canManage(a.viewer, a.workspace)) return fail("forbidden", "Only the workspace's owners add MCP servers to an agent.");
417 return ok({ row: found.value.row, workspaceId: found.value.workspaceId });
418 }
419
420 /** Saves `definition` as the agent's next version, from the version read; what the agent is now. */
421 private async saveVersion(row: Row, definition: Definition, by: User, workspace: string, what: string): Promise<Result<WorkspaceAgent>> {
422 const now = new Date().toISOString();
423 const version = row.version + 1;
424 const [updated] = await this.db.batch([
425 updateAgent(this.db, row.id, row.version, definition, { version, updated_at: now }),
426 this.db
427 .prepare(
428 `INSERT INTO agent_versions (agent_id, version, definition, changed_by, created_at)
429 SELECT ?1, ?2, ?3, ?4, ?5 WHERE EXISTS (SELECT 1 FROM agents WHERE id = ?1 AND version = ?2 AND updated_at = ?5)`,
430 )
431 .bind(row.id, version, JSON.stringify(definition), by.username, now),
432 ]);
433 if (!updated.meta.changes) return fail("conflict", `@${row.handle} was changed meanwhile. Reload it and try again.`);
434 this.audit(by, workspace, "update_agent", row.handle, `${what} (version ${version})`, "agents", "owner");
435 const saved = await this.row(row.workspace_id, row.handle);
436 return ok(toAgent(saved!, new Date()));
437 }
438
439 /**
440 * Adds an MCP server to an agent: the name and address are checked, its
441 * tools are listed from it now, and the agent gets a new version with
442 * the server and its tools as abilities (each a write unless the server
443 * says it only reads). Owners only.
444 */
445 async addMcpServer(a: { workspace: string; handle: string; viewer: User | null; name: unknown; url: unknown }): Promise<Result<McpServer>> {
446 const found = await this.forMcp(a);
447 if (!found.ok) return found;
448 const { row } = found.value;
449 const name = checkMcpName(a.name);
450 if (!name.ok) return fail("invalid", name.message);
451 const url = checkMcpUrl(a.url);
452 if (!url.ok) return fail("invalid", url.message);
453 const before = definitionOf(row);
454 const servers = before.abilities.mcp_servers ?? [];
455 if (servers.length >= MAX_MCP_SERVERS) return fail("limit", `An agent has at most ${MAX_MCP_SERVERS} MCP servers.`);
456 if (servers.some((server) => server.name === name.value)) return fail("conflict", `@${row.handle} already has a server called ${name.value}.`);
457 if (servers.some((server) => server.url === url.value)) return fail("conflict", `@${row.handle} already has that server.`);
458 const listed = await listMcpTools(url.value);
459 const now = new Date().toISOString();
460 const server: McpServer = {
461 id: newId("mcp"),
462 name: name.value,
463 url: url.value,
464 tools: listed.ok ? listed.tools : [],
465 added_by: a.viewer!.username,
466 added_at: now,
467 checked_at: now,
468 problem: listed.ok ? null : listed.message,
469 };
470 const checked = applyChanges(before, {}, TEMPLATE_IDS, { builtin: !!row.builtin, mcp_servers: [...servers, server] });
471 if (!checked.ok) return fail("invalid", checked.message);
472 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}`);
473 return saved.ok ? ok(server) : saved;
474 }
475
476 /** Takes an MCP server off an agent, with its abilities' settings: a new version. Owners only. */
477 async removeMcpServer(a: { workspace: string; handle: string; viewer: User | null; id: unknown }): Promise<Result<null>> {
478 const found = await this.forMcp(a);
479 if (!found.ok) return found;
480 const { row } = found.value;
481 const before = definitionOf(row);
482 const servers = before.abilities.mcp_servers ?? [];
483 const server = servers.find((s) => s.id === a.id);
484 if (!server) return fail("not_found", "There is no such server on this agent.");
485 const checked = applyChanges(before, {}, TEMPLATE_IDS, { builtin: !!row.builtin, mcp_servers: servers.filter((s) => s.id !== server.id) });
486 if (!checked.ok) return fail("invalid", checked.message);
487 const saved = await this.saveVersion(row, checked.value, a.viewer!, a.workspace, `Removed the MCP server ${server.name} from @${row.handle}`);
488 return saved.ok ? ok(null) : saved;
489 }
490
491 /** Lists a server's tools again; a new version when they changed. Owners only. */
492 async refreshMcpServer(a: { workspace: string; handle: string; viewer: User | null; id: unknown }): Promise<Result<McpServer>> {
493 const found = await this.forMcp(a);
494 if (!found.ok) return found;
495 const { row } = found.value;
496 const before = definitionOf(row);
497 const servers = before.abilities.mcp_servers ?? [];
498 const server = servers.find((s) => s.id === a.id);
499 if (!server) return fail("not_found", "There is no such server on this agent.");
500 const listed = await listMcpTools(server.url);
501 const now = new Date().toISOString();
502 const fresh: McpServer = { ...server, tools: listed.ok ? listed.tools : server.tools, checked_at: now, problem: listed.ok ? null : listed.message };
503 if (JSON.stringify(fresh.tools) === JSON.stringify(server.tools) && fresh.problem === server.problem) return ok(fresh);
504 const checked = applyChanges(before, {}, TEMPLATE_IDS, { builtin: !!row.builtin, mcp_servers: servers.map((s) => (s.id === server.id ? fresh : s)) });
505 if (!checked.ok) return fail("invalid", checked.message);
506 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)`);
507 return saved.ok ? ok(fresh) : saved;
508 }
509
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.510 async promote(a: { workspace: string; handle: string; viewer: User | null }): Promise<Result<WorkspaceAgent>> {
511 const managed = await this.managed(a?.workspace ?? "", a?.viewer ?? null);
512 if (!managed.ok) return managed.error.code === "forbidden" ? fail("forbidden", "Only the workspace's owners promote personal agents.") : managed;
513 const row = await this.row(managed.value, a.handle);
514 if (!row) return fail("not_found", `There is no agent called @${a.handle}.`);
515 if (!isPersonal(row)) return fail("invalid", `@${row.handle} is already a workspace agent.`);
516 const versions = await this.db
517 .prepare("SELECT version, definition, changed_by, created_at FROM agent_versions WHERE agent_id = ? ORDER BY version")
518 .bind(row.id)
519 .all<{ version: number; definition: string; changed_by: string; created_at: string }>();
520 const definition = definitionOf(row);
521 const now = new Date().toISOString();
522 const id = newId("agt");
523 const version = row.version + 1;
524 const by = a.viewer!.username;
525 try {
526 const [archived] = await this.db.batch([
527 // 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.528 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.529 insertAgent(this.db, id, managed.value, definition, { version, created_by: row.created_by, created_at: now, updated_at: now }),
530 ...versions.results.map((v) =>
531 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),
532 ),
533 this.versionStatement(id, version, definition, by, now),
534 ]);
535 if (!archived.meta.changes) throw new Error("changed meanwhile");
536 } catch (error) {
537 console.error("agents: a promotion failed", row.id, String(error));
538 return fail("conflict", `@${row.handle} was changed meanwhile. Reload it and try again.`);
539 }
540 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})`);
541 const saved = await this.row(managed.value, row.handle);
542 return ok(toAgent(saved!, new Date()));
543 }
544
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.545 // ── The Marketplace's install requests (./installs.ts) ─────────────────
546
547 /** Requests as the viewer sees them: every one for an owner, their own for anyone else. */
548 async installRequests(a: { workspace: string; viewer: User | null }): Promise<Result<InstallRequests>> {
549 if (a?.viewer && awaitsConfirmation(a.viewer)) return UNVERIFIED;
550 const seen = await this.seen(a?.workspace ?? "", a?.viewer ?? null);
551 if (!seen.ok) return seen;
552 const owner = canManage(a.viewer, a.workspace);
553 const requests = await listRequests(this.db, seen.value, a.viewer!, owner);
554 return ok({ requests, can_resolve: owner });
555 }
556
557 /** A member asks the owners to add something; each owner is notified. */
558 async requestInstall(a: { workspace: string; viewer: User | null; listing: unknown; note?: unknown }): Promise<Result<InstallRequest>> {
559 if (a?.viewer && awaitsConfirmation(a.viewer)) return UNVERIFIED;
560 const seen = await this.seen(a?.workspace ?? "", a?.viewer ?? null);
561 if (!seen.ok) return seen;
562 const viewer = a.viewer!;
563 if ((viewer.kind ?? "user") !== "user") return fail("forbidden", "Only people ask the workspace's owners to add things.");
564 if (canManage(viewer, a.workspace)) return fail("invalid", "You're an owner of this workspace: add it yourself.");
565 const listing = findListing(a.listing);
566 if (!listing) return fail("not_found", "That isn't something a workspace can add yet.");
567 const opened = await openRequest(this.db, seen.value, newId("ins"), listing, viewer, cleanRequestNote(a.note));
568 if (!opened.ok) return opened;
569 const slug = a.workspace.toLowerCase();
570 this.audit(viewer, slug, "request_install", listing.ref, `Asked the owners to add ${listing.name}`, "marketplace");
571 this.defer(this.tellOwners(slug, viewer, opened.value));
572 return opened;
573 }
574
575 /** An owner adds or turns down a request; whoever asked is told. */
576 async resolveInstallRequest(a: { workspace: string; viewer: User | null; id: unknown; status: unknown }): Promise<Result<InstallRequest>> {
577 const managed = await this.managed(a?.workspace ?? "", a?.viewer ?? null);
578 if (!managed.ok) {
579 return managed.error.code === "forbidden" ? fail("forbidden", "Only the workspace's owners answer requests.") : managed;
580 }
581 if (a.status !== "done" && a.status !== "declined") return fail("invalid", "Answer a request with done or declined.");
582 const answered = await resolveRequest(this.db, managed.value, String(a.id ?? ""), a.status, a.viewer!);
583 if (!answered.ok) return answered;
584 const slug = a.workspace.toLowerCase();
585 const { request } = answered.value;
586 this.audit(a.viewer!, slug, "answer_install_request", request.listing, `${request.status === "done" ? "Added" : "Turned down"} ${request.name} for @${request.requested_by}`, "marketplace");
587 this.defer(this.tellRequester(slug, a.viewer!, answered.value));
588 return ok(request);
589 }
590
591 /** Marks every open request for `listing` added, and tells each person who asked. */
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.592 async answerListing(slug: string, workspaceId: string, listing: string, by: User): Promise<void> {
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.593 try {
594 const answered = await resolveListing(this.db, workspaceId, listing, by as Person);
595 await Promise.all(answered.map((one) => this.tellRequester(slug.toLowerCase(), by, one)));
596 } catch (error) {
597 console.error("agents: requests for a listing were not answered", listing, String(error));
598 }
599 }
600
601 /** Every owner hears of a new request (at most 20 of them). */
602 private async tellOwners(slug: string, asker: User, request: InstallRequest): Promise<void> {
603 if (!this.env.NOTIFY) return;
604 try {
605 const members = await identityClient(this.env.IDENTITY).listMembers(slug, asker);
606 if (!members.ok) throw new Error(members.error.message);
607 const owners = members.value.filter((member) => member.role === "owner").slice(0, 20);
608 const notify = notifyClient(this.env.NOTIFY);
609 await Promise.all(
610 owners.map((owner) =>
611 notify
612 .notify(
613 { username: owner.username },
614 {
615 id: `install-request:${request.id}:${owner.username}`,
616 kind: "approval",
617 workspace: slug,
618 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.619 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.620 href: requestsPath(slug),
621 actor: { kind: "user", id: asker.id, name: asker.username, avatar: asker.avatar ?? null, avatar_seed: null },
622 created_at: new Date().toISOString(),
623 },
624 )
625 .catch(() => undefined),
626 ),
627 );
628 } catch (error) {
629 console.error("agents: owners were not told of an install request", request.id, String(error));
630 }
631 }
632
633 /** The person who asked hears their request was answered. */
634 private async tellRequester(slug: string, by: User, answered: Answered): Promise<void> {
635 if (!this.env.NOTIFY) return;
636 const { request } = answered;
637 const line = answerLine(request, `@${by.username}`);
638 await notifyClient(this.env.NOTIFY)
639 .notify(
640 { user_id: answered.requested_by_id },
641 {
642 id: `install-answer:${request.id}`,
643 kind: "inbox",
644 workspace: slug,
645 title: line.title,
646 body: line.body,
647 href: request.status === "done" ? listingPath(slug, request.listing) : requestsPath(slug),
648 actor: { kind: "user", id: by.id, name: by.username, avatar: by.avatar ?? null, avatar_seed: null },
649 created_at: new Date().toISOString(),
650 },
651 )
652 .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)653 }
654
Chat and workspace agents: channels, DMs and named agents you talk to655 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.656 const found = await this.visible(a.workspace, a.viewer, a.handle);
657 if (!found.ok) return found;
658 const { row } = found.value;
659 // Owners change the workspace's agents; a member changes their own personal one.
660 if (!canChange(a.viewer, a.workspace, row)) return fail("forbidden", isPersonal(row) ? NOT_YOURS_REFUSAL : MANAGE_REFUSAL);
661 const workspaceId = found.value.workspaceId;
Chat and workspace agents: channels, DMs and named agents you talk to662 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.663 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)664 // 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.665 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)666 if (!allowed.ok) return fail("invalid", allowed.message);
667 const checked = applyChanges(before, allowed.value, TEMPLATE_IDS, { builtin: !!row.builtin });
Chat and workspace agents: channels, DMs and named agents you talk to668 if (!checked.ok) return fail("invalid", checked.message);
669 const definition = checked.value;
670 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.671 if (definition.handle !== before.handle && (await this.handleTaken(workspaceId, definition.handle, row.id))) {
Chat and workspace agents: channels, DMs and named agents you talk to672 return fail("conflict", `${a.workspace} already has an agent called @${definition.handle}.`);
673 }
674 const now = new Date().toISOString();
675 const version = row.version + 1;
676 const by = a.viewer!.username;
677 try {
678 const [updated] = await this.db.batch([
679 // Only from the version read: two owners saving at once never lose
680 // 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)681 updateAgent(this.db, row.id, row.version, definition, { version, updated_at: now }),
Chat and workspace agents: channels, DMs and named agents you talk to682 this.db
683 .prepare(
684 `INSERT INTO agent_versions (agent_id, version, definition, changed_by, created_at)
685 SELECT ?1, ?2, ?3, ?4, ?5 WHERE EXISTS (SELECT 1 FROM agents WHERE id = ?1 AND version = ?2 AND updated_at = ?5)`,
686 )
687 .bind(row.id, version, JSON.stringify(definition), by, now),
688 ]);
689 if (!updated.meta.changes) return fail("conflict", `@${before.handle} was changed meanwhile. Reload it and try again.`);
690 } catch (error) {
691 if (String(error).includes("UNIQUE")) return fail("conflict", `${a.workspace} already has an agent called @${definition.handle}.`);
692 throw error;
693 }
694 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.695 this.audit(a.viewer!, a.workspace, "update_agent", definition.handle, `Changed @${definition.handle}${renamed} to version ${version}`, "agents", isPersonal(row) ? "member" : "owner");
696 const saved = await this.row(workspaceId, definition.handle);
Chat and workspace agents: channels, DMs and named agents you talk to697 return ok(toAgent(saved!, new Date()));
698 }
699
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.700 /** What each effort level has cost one agent, from its own finished sessions. */
701 async effortCosts(a: { workspace: string; handle: string; viewer: User | null }): Promise<Result<AgentEffortCosts>> {
702 if (a?.viewer && awaitsConfirmation(a.viewer)) return UNVERIFIED;
703 const seen = await this.seen(a?.workspace ?? "", a?.viewer ?? null);
704 if (!seen.ok) return seen;
705 const row = await this.row(seen.value, a.handle);
706 if (!row) return fail("not_found", `There is no agent called @${a.handle}.`);
707 const outcomes = await outcomesSince(this.db, seen.value, sinceWindow(), row.id);
708 return ok(effortCostsOf(row.handle, effortOf(definitionOf(row).routing), outcomes.get(row.id) ?? []));
709 }
710
711 /** Ways to spend less, checked against past work: every agent's, or one's. */
712 async recommendations(a: { workspace: string; viewer: User | null; handle?: string | null }): Promise<Result<AgentRecommendations>> {
713 if (a?.viewer && awaitsConfirmation(a.viewer)) return UNVERIFIED;
714 const seen = await this.seen(a?.workspace ?? "", a?.viewer ?? null);
715 if (!seen.ok) return seen;
716 let agentId: string | null = null;
717 if (a.handle) {
718 const row = await this.row(seen.value, a.handle);
719 if (!row) return fail("not_found", `There is no agent called @${a.handle}.`);
720 agentId = row.id;
721 }
722 return ok(await readRecommendations(this.db, seen.value, agentId));
723 }
724
725 /**
726 * Owners apply a suggestion (the agent's effort changes as a new version
727 * of it, and the audit log says so) or dismiss it. Only an open one, and
728 * only while the agent's setting is still the one it was made for.
729 */
730 async resolveRecommendation(a: { workspace: string; viewer: User | null; id: string; action: string }): Promise<Result<AgentRecommendation>> {
731 const managed = await this.managed(a?.workspace ?? "", a?.viewer ?? null);
732 if (!managed.ok) return managed;
733 if (a.action !== "apply" && a.action !== "dismiss") return fail("invalid", "Apply or dismiss.");
734 const found = await recommendationRow(this.db, managed.value, String(a.id ?? ""));
735 if (!found) return fail("not_found", "There is no such suggestion.");
736 if (found.status !== "open") return fail("conflict", "This suggestion was already decided, or is no longer current.");
737 const by = a.viewer!.username;
738 if (a.action === "dismiss") {
739 if (!(await markResolved(this.db, found.id, "dismissed", by))) return fail("conflict", "This suggestion was decided meanwhile.");
740 this.audit(a.viewer!, a.workspace, "dismiss_recommendation", found.handle ?? found.agent_id, `Dismissed "${found.title}"`);
741 } else {
742 const agent = await this.row(managed.value, found.handle);
743 if (!agent || agent.id !== found.agent_id) return fail("not_found", "That agent is gone.");
744 if (effortOf(definitionOf(agent).routing) !== found.from_effort) {
745 await this.db.prepare("UPDATE agent_recommendations SET status = 'stale' WHERE id = ? AND status = 'open'").bind(found.id).run();
746 return fail("conflict", `@${agent.handle}'s effort was changed since this was suggested. The next check looks again.`);
747 }
748 // Claimed first, so two owners pressing Apply change the agent once.
749 if (!(await markResolved(this.db, found.id, "applied", by))) return fail("conflict", "This suggestion was decided meanwhile.");
750 const changed = await this.update({ workspace: a.workspace, handle: agent.handle, viewer: a.viewer, changes: { routing: { effort: found.to_effort as never } } });
751 if (!changed.ok) {
752 await this.db.prepare("UPDATE agent_recommendations SET status = 'open', resolved_by = NULL, resolved_at = NULL WHERE id = ?").bind(found.id).run();
753 return changed;
754 }
755 this.audit(
756 a.viewer!,
757 a.workspace,
758 "apply_recommendation",
759 agent.handle,
760 `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}`,
761 );
762 }
763 const after = await recommendationRow(this.db, managed.value, found.id);
764 return ok(toRecommendation(after!));
765 }
766
Chat and workspace agents: channels, DMs and named agents you talk to767 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.768 const found = await this.visible(a.workspace, a.viewer, a.handle);
769 if (!found.ok) return found;
770 const { row } = found.value;
771 // Owners archive any agent; a member their own personal one.
772 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)773 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 to774 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.775 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 to776 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.777 // Its computer goes to sleep now; its disk is kept 30 days, then deleted (computer.ts).
778 this.defer(forgetComputer(this.env, row.id));
Chat and workspace agents: channels, DMs and named agents you talk to779 return ok(null);
780 }
781
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.782 // ── Archived agents (./archived.ts) ──────────────────────────────────────
783
784 /** The archived agents the viewer may see: every one for an owner, their own archived personal agents for anyone else. */
785 async listArchived(a: { workspace: string; viewer: User | null }): Promise<Result<ArchivedAgent[]>> {
786 if (a?.viewer && awaitsConfirmation(a.viewer)) return UNVERIFIED;
787 const seen = await this.seen(a?.workspace ?? "", a?.viewer ?? null);
788 if (!seen.ok) return seen;
789 const viewer = a.viewer!;
790 return ok(await listArchived(this.db, seen.value, { id: (viewer.kind ?? "user") === "user" ? viewer.id : "", owner: canManage(viewer, a.workspace) }));
791 }
792
793 /**
794 * An archived agent the viewer may see, by id, else the one most recently
795 * archived under `handle` (a handle is unique only among live agents).
796 */
797 private async archivedRow(workspace: string, viewer: User | null, a: { id?: unknown; handle?: unknown }): Promise<Result<{ workspaceId: string; row: Row }>> {
798 if (awaitsConfirmation(viewer)) return UNVERIFIED;
799 const seen = await this.seen(workspace, viewer);
800 if (!seen.ok) return seen;
801 const now = new Date();
802 const handle = typeof a.handle === "string" ? a.handle.trim().replace(/^@/, "").toLowerCase() : "";
803 const row =
804 typeof a.id === "string" && a.id
805 ? 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>()
806 : handle
807 ? await this.db
808 .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`)
809 .bind(...periods(now), seen.value, handle)
810 .first<Row>()
811 : null;
812 if (!row || !canSeeAgent(viewer, workspace, row)) return fail("not_found", `There is no archived agent called @${handle || String(a.id ?? "")}.`);
813 return ok({ workspaceId: seen.value, row });
814 }
815
816 /**
817 * Brings an archived agent back, live again under its handle, or under
818 * `new_handle` when a live agent took the old one (`conflict` says so).
819 * Owners any agent, a member their own personal one. A new handle is a
820 * new version, as any rename is. Its computer's disk is kept after all.
821 */
822 async restore(a: { workspace: string; handle?: string; id?: string; viewer: User | null; new_handle?: unknown }): Promise<Result<WorkspaceAgent>> {
823 const found = await this.archivedRow(a?.workspace ?? "", a?.viewer ?? null, a ?? {});
824 if (!found.ok) return found;
825 const { workspaceId, row } = found.value;
826 const refused = restoreRefusal(a.viewer, a.workspace, row);
827 if (refused) return { ok: false, error: refused };
828 const live = await this.db
829 .prepare("SELECT handle FROM agents WHERE workspace_id = ? AND archived_at IS NULL")
830 .bind(workspaceId)
831 .all<{ handle: string }>();
832 const taken = new Set(live.results.map((r) => r.handle));
833 const handle = restoreHandle(row, a.new_handle, (h) => taken.has(h));
834 if (!handle.ok) return handle;
835 const now = new Date().toISOString();
836 const renamed = handle.handle !== row.handle;
837 const version = renamed ? row.version + 1 : row.version;
838 try {
839 const [restored] = await this.db.batch([
840 this.db
841 .prepare("UPDATE agents SET archived_at = NULL, archived_by = NULL, handle = ?, version = ?, updated_at = ? WHERE id = ? AND archived_at IS NOT NULL")
842 .bind(handle.handle, version, now, row.id),
843 ...(renamed ? [this.versionStatement(row.id, version, { ...definitionOf(row), handle: handle.handle }, a.viewer!.username, now)] : []),
844 ]);
845 if (!restored.meta.changes) return fail("not_found", `@${row.handle} was restored or deleted meanwhile.`);
846 } catch (error) {
847 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.`);
848 throw error;
849 }
850 const was = renamed ? ` (was @${row.handle})` : "";
851 this.audit(a.viewer!, a.workspace, "restore_agent", handle.handle, `Restored @${handle.handle}${was}`, "agents", isPersonal(row) ? "member" : "owner");
852 this.defer(keepComputer(this.env, row.id));
853 const saved = await this.row(workspaceId, handle.handle);
854 return ok(toAgent(saved!, new Date()));
855 }
856
857 /**
858 * Deletes an archived agent for good (archived.ts `DESTROYED`): every row
859 * the service keeps about it, in one batch; then its computer on the
860 * runner, its chat memberships (its messages stay under the name it
861 * had) and its team memberships. Owners any agent, a member their own
862 * personal one; `confirm` is its handle typed back. A live agent is
863 * archived first. A second call finds nothing.
864 */
865 async destroy(a: { workspace: string; handle?: string; id?: string; viewer: User | null; confirm?: unknown }): Promise<Result<null>> {
866 const found = await this.archivedRow(a?.workspace ?? "", a?.viewer ?? null, a ?? {});
867 if (!found.ok) {
868 // A live agent of that handle: say what to do, rather than "no such archived agent".
869 if (found.error.code === "not_found" && typeof a?.handle === "string") {
870 const live = await this.visible(a.workspace, a.viewer, a.handle);
871 if (live.ok) {
872 const refused = destroyRefusal(a.viewer, a.workspace, live.value.row, a.confirm);
873 return { ok: false, error: refused ?? found.error };
874 }
875 }
876 return found;
877 }
878 const { workspaceId, row } = found.value;
879 const refused = destroyRefusal(a.viewer, a.workspace, row, a.confirm);
880 if (refused) return { ok: false, error: refused };
881 const results = await this.db.batch(destroyStatements(this.db, row.id));
882 if (!results[results.length - 1]!.meta.changes) return fail("not_found", `@${row.handle} was restored or deleted meanwhile.`);
883 this.audit(a.viewer!, a.workspace, "delete_agent", row.handle, `Deleted the archived agent @${row.handle} for good`, "agents", isPersonal(row) ? "member" : "owner");
884 const slug = a.workspace.toLowerCase();
885 const definition = definitionOf(row);
886 this.defer(eraseComputer(this.env, row.id));
887 this.defer(
888 chatClient(this.env.CHAT)
889 .agentDestroyed({ workspace_id: workspaceId, agent_id: row.id, handle: row.handle, display_name: row.display_name, avatar_seed: definition.avatar_seed, look: definition.look })
890 .then((told) => (told.ok ? null : console.error("agents: chat was not told of a deleted agent", row.id, told.error.message)))
891 .catch((error: unknown) => console.error("agents: chat was not told of a deleted agent", row.id, String(error))),
892 );
893 this.defer(this.leaveTeams(slug, a.viewer!, row.id));
894 return ok(null);
895 }
896
897 /** A deleted agent comes off every team it was on (memberships live in identity). */
898 private async leaveTeams(slug: string, by: User, agentId: string): Promise<void> {
899 try {
900 const identity = identityClient(this.env.IDENTITY);
901 const teams = await identity.agentTeams(slug, agentId);
902 await Promise.all(
903 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)))),
904 );
905 } catch (error) {
906 console.error("agents: a deleted agent's teams were not read", agentId, String(error));
907 }
908 }
909
Chat and workspace agents: channels, DMs and named agents you talk to910 /**
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)911 * Makes the workspace's built-in @g1t if it does not exist yet: an
912 * ordinary agent row, marked builtin, at version 1. Safe to call on
913 * every request; once seen, an isolate does not write again.
914 */
915 private async ensureBuiltin(workspaceId: string): Promise<void> {
916 await ensureBuiltin(this.db, workspaceId);
917 }
918
919 /** Internal: the workspace's @g1t, made if need be, for the chat service. */
920 async builtin(a: { workspace: string; workspace_id: string }): Promise<Result<WorkspaceAgent>> {
921 if (typeof a?.workspace_id !== "string" || !a.workspace_id) return fail("invalid", "Name the workspace by id.");
922 await this.ensureBuiltin(a.workspace_id);
923 const now = new Date();
924 const row = await this.db
925 .prepare(selectAgents("a.workspace_id = ?3 AND a.builtin = 1"))
926 .bind(...periods(now), a.workspace_id)
927 .first<Row>();
928 return row ? ok(toAgent(row, now)) : fail("not_found", "This workspace's @g1t could not be made.");
929 }
930
931 /**
Chat and workspace agents: channels, DMs and named agents you talk to932 * Hands a message to the agent's desk, which answers it in the
933 * background. Returns as soon as the desk holds it.
934 */
935 async deliver(delivery: AgentDelivery): Promise<Result<null>> {
936 const fields = ["workspace", "workspace_id", "channel_id", "agent_id", "message_id", "asked_by"] as const;
937 if (!delivery || fields.some((field) => typeof delivery[field] !== "string" || !delivery[field])) {
938 return fail("invalid", "A delivery names the workspace, channel, agent, message and who asked.");
939 }
940 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)941 await this.ensureBuiltin(delivery.workspace_id);
Chat and workspace agents: channels, DMs and named agents you talk to942 const desk = this.env.DESKS.get(this.env.DESKS.idFromName(delivery.agent_id));
943 await desk.take({ ...delivery, hops: Math.max(0, Math.floor(Number(delivery.hops) || 0)), thread_root: delivery.thread_root ?? null });
944 return ok(null);
945 }
946
947 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)948 return versionStatement(this.db, agentId, version, d, by, at);
Chat and workspace agents: channels, DMs and named agents you talk to949 }
950
951 /**
952 * Records a change to an agent in the workspace's audit log, after the
953 * answer. Never fails the change: a log that cannot be written is logged.
954 * The events service's audit contract speaks camelCase (Rust's
955 * `NewAuditEntry`).
956 */
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.957 /** A change to the skill library, in the audit log as `agents/skills/<name>`. */
958 auditSkill(actor: User, workspace: string, action: string, name: string, message: string): void {
959 this.audit(actor, workspace, action, `skills/${name}`, message);
960 }
961
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.962 private audit(
963 actor: User,
964 workspace: string,
965 action: string,
966 handle: string,
967 message: string,
968 area: "agents" | "marketplace" = "agents",
969 rule: "owner" | "member" | null = null,
970 ): void {
Chat and workspace agents: channels, DMs and named agents you talk to971 const kind = actor.kind === "workspace" ? "workspace" : actor.kind === "agent" ? "agent" : actor.kind === "system" ? "system" : "person";
972 const entry = {
973 actorKind: kind,
974 actor: actor.username,
975 actorId: actor.id,
976 agent: null,
977 onBehalfOf: null,
978 runId: null,
979 runKind: null,
980 credentialId: null,
981 action,
982 surface: "web",
983 workspace: workspace.toLowerCase(),
984 repo: null,
985 number: null,
986 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.987 path: `${area}/${handle}`,
Chat and workspace agents: channels, DMs and named agents you talk to988 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.989 rule: rule ?? (area === "marketplace" && action === "request_install" ? "member" : "owner"),
Chat and workspace agents: channels, DMs and named agents you talk to990 result: "ok",
991 message,
992 requestId: `req_${crypto.randomUUID()}`,
993 };
994 this.defer(
995 this.env.EVENTS.fetch("https://service/rpc/audit_record", {
996 method: "POST",
997 headers: { "content-type": "application/json" },
998 body: JSON.stringify({ entries: [entry] }),
999 })
1000 .then((response) => {
1001 if (!response.ok) throw new Error(`status ${response.status}`);
1002 })
1003 .catch((error: unknown) => console.error("agents: audit entry not recorded", action, handle, String(error))),
1004 );
1005 }
1006}
1007
1008/** One RPC method's answer. */
1009async 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.1010 // The skill library (./skill-rpc.ts).
1011 const skill = await skillRpc(method, args, (a, run) => service.view(a, run), (viewer, workspace, action, name, message) => service.auditSkill(viewer, workspace, action, name, message));
1012 if (skill) return Response.json(skill);
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.1013 // Customize: marketplaces, plugins and connectors (./customize-rpc.ts).
1014 const customize = await customizeRpc(method, args, (a, run) => service.view(a, run));
1015 if (customize) return Response.json(customize);
Chat and workspace agents: channels, DMs and named agents you talk to1016 switch (method) {
1017 case "list":
1018 return Response.json(await service.list(args));
1019 case "get":
1020 return Response.json(await service.get(args));
1021 case "by_ids":
1022 return Response.json(await service.byIds(args));
1023 case "create":
1024 return Response.json(await service.create(args));
1025 case "update":
1026 return Response.json(await service.update(args));
1027 case "archive":
1028 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.1029 case "list_archived":
1030 return Response.json(await service.listArchived(args));
1031 case "restore":
1032 return Response.json(await service.restore(args));
1033 case "destroy":
1034 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.1035 case "draft":
1036 return Response.json(await service.draft(args));
1037 case "try_draft":
1038 return Response.json(await service.tryDraft(args));
1039 case "redraft":
1040 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.1041 case "add_mcp_server":
1042 return Response.json(await service.addMcpServer(args));
1043 case "remove_mcp_server":
1044 return Response.json(await service.removeMcpServer(args));
1045 case "refresh_mcp_server":
1046 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.1047 case "promote":
1048 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)1049 case "builtin":
1050 return Response.json(await service.builtin(args));
Chat and workspace agents: channels, DMs and named agents you talk to1051 case "templates":
1052 return Response.json(TEMPLATES);
1053 case "deliver":
1054 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 asked1055 case "overview":
1056 return Response.json(await service.view(args, (ctx) => views.overview(ctx)));
1057 case "sessions":
1058 return Response.json(await service.view(args, (ctx) => views.listSessions(ctx, args)));
1059 case "session":
1060 return Response.json(await service.view(args, (ctx) => views.sessionDetail(ctx, args.id)));
1061 case "stop_session":
1062 return Response.json(await service.view(args, (ctx) => views.stopSession(ctx, args.id)));
1063 case "approve_session":
1064 return Response.json(await service.view(args, (ctx) => views.approveSession(ctx, args.id, args.cap_micros)));
1065 case "steer_session":
1066 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.1067 // The agent's own computer (./computer.ts).
1068 case "computer":
1069 return Response.json(await service.view(args, (ctx) => computerView(ctx, args.handle)));
1070 case "computer_wake":
1071 return Response.json(await service.view(args, (ctx) => wakeComputer(ctx, args.handle)));
1072 case "computer_sleep":
1073 return Response.json(await service.view(args, (ctx) => sleepComputer(ctx, args.handle)));
1074 case "computer_reset":
1075 return Response.json(await service.view(args, (ctx) => resetComputer(ctx, args.handle)));
1076 case "computer_commands":
1077 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 asked1078 case "memories":
1079 return Response.json(await service.view(args, (ctx) => views.memories(ctx, args.handle)));
1080 case "remember":
1081 return Response.json(await service.view(args, (ctx) => views.remember(ctx, args.handle, args.input)));
1082 case "update_memory":
1083 return Response.json(await service.view(args, (ctx) => views.updateMemory(ctx, args.handle, args.id, args.changes)));
1084 case "forget":
1085 return Response.json(await service.view(args, (ctx) => views.forget(ctx, args.handle, args.id)));
1086 case "routines":
1087 return Response.json(await service.view(args, (ctx) => views.routines(ctx, args.handle)));
1088 case "save_routine":
1089 return Response.json(await service.view(args, (ctx) => views.saveRoutine(ctx, args.handle, args.input, args.id ?? null)));
1090 case "delete_routine":
1091 return Response.json(await service.view(args, (ctx) => views.deleteRoutine(ctx, args.handle, args.id)));
1092 case "run_routine":
1093 return Response.json(await service.view(args, (ctx) => views.runRoutineNow(ctx, args.handle, args.id)));
1094 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.1095 return Response.json(await service.view(args, (ctx) => views.spend(ctx, args.handle ?? null, { period: args.period, person: args.person })));
1096 case "person_budgets":
1097 return Response.json(await service.view(args, (ctx) => views.personBudgetsView(ctx)));
1098 case "set_person_budget":
1099 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.1100 case "team_context":
1101 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 asked1102 case "activity":
1103 return Response.json(await service.view(args, (ctx) => views.activity(ctx, args.handle)));
1104 case "versions":
1105 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 calendar1106 case "card_action":
1107 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 asked1108 case "policy":
1109 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.1110 case "install_requests":
1111 return Response.json(await service.installRequests(args));
1112 case "request_install":
1113 return Response.json(await service.requestInstall(args));
1114 case "resolve_install_request":
1115 return Response.json(await service.resolveInstallRequest(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.1116 case "effort_costs":
1117 return Response.json(await service.effortCosts(args));
1118 case "recommendations":
1119 return Response.json(await service.recommendations(args));
1120 case "resolve_recommendation":
1121 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 asked1122 case "set_policy":
1123 return Response.json(await service.view(args, (ctx) => views.setPolicy(ctx, args.policy)));
Chat and workspace agents: channels, DMs and named agents you talk to1124 default:
1125 return new Response("Unknown method\n", { status: 404 });
1126 }
1127}
1128
1129export default {
1130 async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
1131 const match = new URL(request.url).pathname.match(/^\/rpc\/([a-z_]+)$/);
1132 if (request.method !== "POST" || !match) return new Response("Not found\n", { status: 404 });
1133 // A replica near the caller when it asks for one (@g1t/contracts d1.ts).
1134 const opened = openD1(env.DB, request);
1135 const service = new Agents(Object.create(env, { DB: { value: opened.db } }) as Env, (work) => ctx.waitUntil(work));
1136 const args = (await request.json().catch(() => ({}))) as any;
1137 return opened.finish(await answer(service, match[1], args));
1138 },
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 asked1139
@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.1140 /** 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 opened1141 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.1142 const events = batch.messages.map((message) => message.body as G1tEvent);
1143 // A push to a default branch: skill libraries that follow that repository read it again (./skill-library.ts).
1144 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.1145 await Promise.all([
1146 onEvents(env as unknown as SessionEnv, events.filter((event) => event?.type !== "git.push")),
1147 pushes.length ? skillPushes(env as unknown as SessionEnv, pushes) : null,
1148 // A person joined a workspace: its @g1t says welcome, once (./welcome.ts).
1149 welcome(env as unknown as SessionEnv, events).catch((error: unknown) => console.error("agents: welcomes were not sent", String(error))),
1150 ]);
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened1151 batch.ackAll();
1152 },
1153
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.1154 /** 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 asked1155 async scheduled(_controller: ScheduledController, env: Env, ctx: ExecutionContext): Promise<void> {
1156 const sessions = env as unknown as SessionEnv;
1157 ctx.waitUntil(
1158 Promise.all([
1159 runDue(sessions).catch((error: unknown) => console.error("agents: routines did not run", String(error))),
1160 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.1161 // Spend less, keep quality: each workspace checked weekly (src/recommend.ts).
1162 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.1163 // Once: agents' own old teams become team memberships (src/team-move.ts); after that, one cheap read.
1164 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 asked1165 ]),
1166 );
1167 },
Chat and workspace agents: channels, DMs and named agents you talk to1168} satisfies ExportedHandler<Env>;

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