Skip to content
979 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Chat and workspace agents: channels, DMs and named agents you talk to1/**
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.2 * The agents service: a workspace's own agents
3 * (docs.g1t.sh/guides/agents/). Their definitions and every version of
4 * them, the templates they start from, each agent's desk, and their replies
5 * in chat.
Chat and workspace agents: channels, DMs and named agents you talk to6 *
7 * `@g1t`, the platform's own agent, is not one of these.
8 *
9 * Reached through service bindings: `POST /rpc/<method>`, with snake_case
10 * JSON (@g1t/contracts workspace-agents.ts).
11 */
12import {
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar13 type AgentCardAction,
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.14 type AgentProposal,
15 type AgentRedraft,
16 type DraftReply,
17 MAX_PERSONAL_AGENTS,
Chat and workspace agents: channels, DMs and named agents you talk to18 type AgentDelivery,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar19 type CardActionResult,
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened20 type G1tEvent,
The Marketplace is where a workspace finds what to add, reached from Apps: the agent catalog of roles with who already does each job and Add to workspace; every integration with whether it's connected; g1t's own extensions, listed as Soon; and requests, where members ask and owners add or turn down, each told by a notification. Underneath, an extension is a manifest (publisher and tier, source and version from a tag, scopes, the domains its data goes to, its page on g1tusercontent.com, no price) and an install pins its version with an off switch, a budget and the audit log. Apps lists only what was added from the Marketplace, with pins; g1t's own pages stay in their sidebars, and AI Gateway joins Workspace's. The marketplace guide says how.21 type ExtensionInstall,
22 type InstallRequest,
23 type InstallRequests,
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,
25 type AgentRecommendation,
26 type AgentRecommendations,
Chat and workspace agents: channels, DMs and named agents you talk to27 type NewWorkspaceAgent,
28 type Result,
29 type ServiceBinding,
30 type User,
31 type WorkspaceAgent,
32 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)33 askerAccess,
Chat and workspace agents: channels, DMs and named agents you talk to34 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)35 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.36 cleanRequestNote,
37 extensionById,
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";
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)50import { builtinChanges } from "./orchestrator.ts";
Chat and workspace agents: channels, DMs and named agents you talk to51import type { Desk } from "./desk.ts";
52import 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)53import { 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.54import { type Row, definitionOf, insertAgent, isPersonal, periods, selectAgents, toAgent, updateAgent, versionStatement } from "./store.ts";
Chat and workspace agents: channels, DMs and named agents you talk to55import { 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 asked56import { readPolicy } from "./policy.ts";
57import { runDue } from "./routines.ts";
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened58import { onEvents } from "./triggers.ts";
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar59import { 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 asked60import { 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.61import { EFFORT_NAMES, checkDue, effortCostsOf, markResolved, outcomesSince, readRecommendations, recommendationRow, sinceWindow, toRecommendation } from "./recommend.ts";
62import { 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 asked63import * as views from "./views.ts";
64import { monthKey } from "./budget.ts";
The Marketplace is where a workspace finds what to add, reached from Apps: the agent catalog of roles with who already does each job and Add to workspace; every integration with whether it's connected; g1t's own extensions, listed as Soon; and requests, where members ask and owners add or turn down, each told by a notification. Underneath, an extension is a manifest (publisher and tier, source and version from a tag, scopes, the domains its data goes to, its page on g1tusercontent.com, no price) and an install pins its version with an off switch, a budget and the audit log. Apps lists only what was added from the Marketplace, with pins; g1t's own pages stay in their sidebars, and AI Gateway joins Workspace's. The marketplace guide says how.65import * as extensions from "./extensions.ts";
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.66import { 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.67import { 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 to68
69export { Desk } from "./desk.ts";
70
71type Env = ReplyEnv & {
72 IDENTITY: ServiceBinding;
73 /** The audit log. */
74 EVENTS: ServiceBinding;
75 DESKS: DurableObjectNamespace<Desk>;
76};
77
78/** Workspace ids by slug, kept a minute: every call names a workspace by slug. */
79const workspaceIds = new Map<string, { id: string | null; until: number }>();
80
81class Agents {
82 private readonly env: Env;
83 private readonly db: D1Database;
84 private readonly defer: (work: Promise<unknown>) => void;
85
86 // Plain fields, not parameter properties: Node's type stripping does not take those.
87 constructor(env: Env, defer: (work: Promise<unknown>) => void) {
88 this.env = env;
89 this.db = env.DB;
90 this.defer = defer;
91 }
92
93 private async workspaceId(slug: string): Promise<string | null> {
94 const key = slug.toLowerCase();
95 const kept = workspaceIds.get(key);
96 if (kept && kept.until > Date.now()) return kept.id;
97 const workspace = await identityClient(this.env.IDENTITY).getWorkspace(key);
98 if (workspaceIds.size > 5_000) workspaceIds.clear();
99 workspaceIds.set(key, { id: workspace?.id ?? null, until: Date.now() + 60_000 });
100 return workspace?.id ?? null;
101 }
102
103 /** The workspace's id, if the viewer may see its agents. */
104 private async seen(workspace: string, viewer: User | null): Promise<Result<string>> {
105 if (!canSee(viewer, workspace)) return fail("not_found", "There is no such workspace.");
106 const id = await this.workspaceId(workspace);
107 return id ? ok(id) : fail("not_found", "There is no such workspace.");
108 }
109
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar110 /** Internal, from chat: a person pressed an action on one of agents' cards. */
111 async cardAction(a: AgentCardAction): Promise<Result<CardActionResult>> {
112 if (a?.viewer && awaitsConfirmation(a.viewer)) return UNVERIFIED;
113 if (!a?.viewer || !canSee(a.viewer, a.workspace ?? "")) return fail("not_found", "No such card.");
114 return cardAction(this.env as unknown as SessionEnv, a);
115 }
116
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked117 /** What the views need: the workspace, the viewer, and whether they own it. */
118 private async context(workspace: string, viewer: User | null): Promise<Result<views.ViewContext>> {
119 if (viewer && awaitsConfirmation(viewer)) return UNVERIFIED;
120 const seen = await this.seen(workspace, viewer);
121 if (!seen.ok) return seen;
122 return ok({
123 env: this.env as unknown as SessionEnv,
124 db: this.db,
125 slug: workspace.toLowerCase(),
126 workspaceId: seen.value,
127 viewer: viewer!,
128 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.129 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 asked130 });
131 }
132
133 /** Runs `view` with the context, or answers why it can't. */
134 async view<T>(a: { workspace: string; viewer: User | null }, view: (ctx: views.ViewContext) => Promise<Result<T>>): Promise<Result<T>> {
135 const ctx = await this.context(a?.workspace ?? "", a?.viewer ?? null);
136 if (!ctx.ok) return ctx;
137 return view(ctx.value);
138 }
139
Chat and workspace agents: channels, DMs and named agents you talk to140 /** The workspace's id, if the viewer may change its agents. */
141 private async managed(workspace: string, viewer: User | null): Promise<Result<string>> {
142 if (awaitsConfirmation(viewer)) return UNVERIFIED;
143 const seen = await this.seen(workspace, viewer);
144 if (!seen.ok) return seen;
145 return canManage(viewer, workspace) ? seen : fail("forbidden", MANAGE_REFUSAL);
146 }
147
148 private async row(workspaceId: string, handle: unknown): Promise<Row | null> {
149 if (typeof handle !== "string") return null;
150 const now = new Date();
151 return this.db
152 .prepare(selectAgents("a.workspace_id = ?3 AND a.handle = ?4 AND a.archived_at IS NULL"))
153 .bind(...periods(now), workspaceId, handle.trim().replace(/^@/, "").toLowerCase())
154 .first<Row>();
155 }
156
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)157 /** Whether `team` (a slug, or none) is one of the workspace's teams, as the person changing the agent sees them. */
158 private async teamExists(workspace: string, viewer: User, team: string | null): Promise<Result<null>> {
159 if (!team) return ok(null);
160 const found = await identityClient(this.env.IDENTITY)
161 .getTeam(viewer, workspace.toLowerCase(), team)
162 .catch(() => null);
163 return found?.ok ? ok(null) : fail("invalid", `${workspace} has no team called ${team}.`);
164 }
165
Chat and workspace agents: channels, DMs and named agents you talk to166 private async handleTaken(workspaceId: string, handle: string, except: string | null): Promise<boolean> {
167 const found = await this.db
168 .prepare("SELECT id FROM agents WHERE workspace_id = ? AND handle = ? AND archived_at IS NULL")
169 .bind(workspaceId, handle)
170 .first<{ id: string }>();
171 return !!found && found.id !== except;
172 }
173
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.174 /**
175 * The workspace's agents, and personal agents when asked for: `mine`, the
176 * viewer's own; `all`, every member's for an owner.
177 */
178 async list(a: { workspace: string; viewer: User | null; personal?: unknown }): Promise<Result<WorkspaceAgent[]>> {
Chat and workspace agents: channels, DMs and named agents you talk to179 const seen = await this.seen(a.workspace, a.viewer);
180 if (!seen.ok) return seen;
181 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)182 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.183 const all = a.personal === "all" && canManage(a.viewer, a.workspace);
184 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 to185 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.186 .prepare(
187 `${selectAgents(
188 `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)" : ""})`,
189 )} ORDER BY a.builtin DESC, a.handle`,
190 )
191 .bind(...periods(now), seen.value, ...(mine && !all ? [mine] : []))
Chat and workspace agents: channels, DMs and named agents you talk to192 .all<Row>();
193 return ok(rows.results.map((row) => toAgent(row, now)));
194 }
195
196 async get(a: { workspace: string; handle: string; viewer: User | null }): Promise<Result<WorkspaceAgent>> {
197 const seen = await this.seen(a.workspace, a.viewer);
198 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)199 await this.ensureBuiltin(seen.value);
Chat and workspace agents: channels, DMs and named agents you talk to200 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.201 // Another member's personal agent is theirs: it isn't there for anyone else but owners.
202 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 to203 }
204
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.205 /** The agent by handle, if the viewer may see it. */
206 private async visible(workspace: string, viewer: User | null, handle: unknown): Promise<Result<{ workspaceId: string; row: Row }>> {
207 if (awaitsConfirmation(viewer)) return UNVERIFIED;
208 const seen = await this.seen(workspace, viewer);
209 if (!seen.ok) return seen;
210 const row = await this.row(seen.value, handle);
211 if (!row || !canSeeAgent(viewer, workspace, row)) return fail("not_found", `There is no agent called @${String(handle ?? "")}.`);
212 return ok({ workspaceId: seen.value, row });
213 }
214
Chat and workspace agents: channels, DMs and named agents you talk to215 /** Internal: agents by id, archived ones too, so old messages still show who wrote them. */
216 async byIds(a: { ids: string[] }): Promise<WorkspaceAgent[]> {
217 const ids = [...new Set((Array.isArray(a.ids) ? a.ids : []).filter((id) => typeof id === "string"))].slice(0, 100);
218 if (!ids.length) return [];
219 const now = new Date();
220 const rows = await this.db
221 .prepare(selectAgents(`a.id IN (${ids.map((_, i) => `?${i + 3}`).join(", ")})`))
222 .bind(...periods(now), ...ids)
223 .all<Row>();
224 return rows.results.map((row) => toAgent(row, now));
225 }
226
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.227 /**
228 * Who may create an agent, and of which scope: the workspace's id and
229 * policy, and the scope it gets (access.ts `creatableScope`).
230 */
231 private async creatable(workspace: string, viewer: User | null, asked: unknown): Promise<Result<{ workspaceId: string; scope: "workspace" | "personal"; policy: Awaited<ReturnType<typeof readPolicy>> }>> {
232 if (awaitsConfirmation(viewer)) return UNVERIFIED;
233 const seen = await this.seen(workspace, viewer);
234 if (!seen.ok) return seen;
235 const policy = await readPolicy(this.db, seen.value, monthKey(new Date()));
236 const scope = creatableScope(viewer, workspace, asked, policy.members_create_agents);
237 if (!scope.ok) return fail("forbidden", scope.message);
238 return ok({ workspaceId: seen.value, scope: scope.scope, policy });
239 }
240
Chat and workspace agents: channels, DMs and named agents you talk to241 async create(a: { workspace: string; viewer: User | null; input: 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.242 const allowed = await this.creatable(a.workspace, a.viewer, a.input?.scope);
243 if (!allowed.ok) return allowed;
244 const { workspaceId, scope, policy } = allowed.value;
245 const personal = scope === "personal";
246 const viewer = a.viewer!;
247 if (personal) {
248 const count = await this.db
249 .prepare("SELECT COUNT(*) AS n FROM agents WHERE workspace_id = ? AND owner_id = ? AND scope = 'personal' AND archived_at IS NULL")
250 .bind(workspaceId, viewer.id)
251 .first<{ n: number }>();
252 if ((count?.n ?? 0) >= MAX_PERSONAL_AGENTS) {
253 return fail("invalid", `You have ${MAX_PERSONAL_AGENTS} personal agents here, the most one person keeps. Archive one to make another.`);
254 }
255 }
256 // A new agent starts with a budget, unless one was given: a member's own
257 // $20 a month and $2 a session, or the workspace's default for its agents.
258 const given = a.input?.budget ?? {};
259 const start = startingBudget(scope, policy.default_agent_monthly_micros);
260 const budget = {
261 monthly_micros: given.monthly_micros !== undefined ? given.monthly_micros : start.monthly_micros,
262 daily_micros: given.daily_micros !== undefined ? given.daily_micros : start.daily_micros,
263 task_micros: given.task_micros !== undefined ? given.task_micros : start.task_micros,
264 };
265 const { scope: _scope, ...rest } = a.input ?? ({} as NewWorkspaceAgent);
266 // A personal agent is on no team: teams are shared, and it answers only its member.
267 const input = { ...rest, budget, ...(personal ? { team: 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 asked268 const checked = applyChanges(null, input, TEMPLATE_IDS);
Chat and workspace agents: channels, DMs and named agents you talk to269 if (!checked.ok) return fail("invalid", checked.message);
270 const definition = checked.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.271 const team = await this.teamExists(a.workspace, viewer, definition.team);
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)272 if (!team.ok) return team;
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.273 if (await this.handleTaken(workspaceId, definition.handle, null)) {
Chat and workspace agents: channels, DMs and named agents you talk to274 return fail("conflict", `${a.workspace} already has an agent called @${definition.handle}.`);
275 }
276 const now = new Date().toISOString();
277 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.278 const by = viewer.username;
279 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 to280 try {
281 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.282 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 to283 this.versionStatement(id, 1, definition, by, now),
284 ]);
285 } catch (error) {
286 if (String(error).includes("UNIQUE")) return fail("conflict", `${a.workspace} already has an agent called @${definition.handle}.`);
287 throw error;
288 }
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.289 this.audit(viewer, a.workspace, "create_agent", definition.handle, `Created ${personal ? "the personal agent " : ""}@${definition.handle} (version 1)`, "agents", personal ? "member" : "owner");
290 this.defer(this.hello(a.workspace, workspaceId, id, viewer));
291 const row = await this.row(workspaceId, definition.handle);
Chat and workspace agents: channels, DMs and named agents you talk to292 return ok(toAgent(row!, new Date()));
293 }
294
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.295 // ── The builder: describe it, try it, change it in words (./builder.ts) ──
296
297 /** Drafts a whole agent from a description, charged to the viewer. Nothing is saved. */
298 async draft(a: { workspace: string; viewer: User | null; description: unknown; scope?: unknown }): Promise<Result<AgentProposal>> {
299 const allowed = await this.creatable(a?.workspace ?? "", a?.viewer ?? null, a?.scope);
300 if (!allowed.ok) return allowed;
301 const words = wordsOf(a.description, "description");
302 if (!words.ok) return fail("invalid", words.message);
303 const { workspaceId, scope, policy } = allowed.value;
304 const slug = a.workspace.toLowerCase();
305 const [answer, handles] = await Promise.all([
306 callBuilder(this.env, {
307 slug,
308 workspaceId,
309 viewer: a.viewer!,
310 system: draftSystem(slug, scope),
311 messages: [{ role: "user", content: `What the agent should do:\n\n${words.value}` }],
312 maxOutput: DRAFT_OUTPUT_TOKENS,
313 }),
314 this.db.prepare("SELECT handle FROM agents WHERE workspace_id = ? AND archived_at IS NULL").bind(workspaceId).all<{ handle: string }>(),
315 ]);
316 if (!answer.ok) return fail(answer.code, answer.message);
317 const proposal = proposalFrom(jsonIn(answer.text), {
318 scope,
319 taken: new Set(handles.results.map((row) => row.handle)),
320 budget: startingBudget(scope, policy.default_agent_monthly_micros),
321 });
322 if (!proposal.ok) return fail("invalid", proposal.message);
323 return ok({ ...proposal.value, charged_micros: answer.charged });
324 }
325
326 /** Try it: the unsaved definition answers the conversation so far. Charged to the viewer; nothing is saved. */
327 async tryDraft(a: { workspace: string; viewer: User | null; definition: unknown; messages: unknown }): Promise<Result<DraftReply>> {
328 const given = (a?.definition ?? {}) as NewWorkspaceAgent;
329 const allowed = await this.creatable(a?.workspace ?? "", a?.viewer ?? null, given?.scope);
330 if (!allowed.ok) return allowed;
331 const { scope: _scope, ...rest } = given;
332 // A handle someone has already taken is no reason not to try it.
333 const checked = applyChanges(null, { ...rest, handle: rest.handle || "preview" }, TEMPLATE_IDS);
334 if (!checked.ok) return fail("invalid", checked.message);
335 const turns = tryTurns(a.messages);
336 if (!turns.ok) return fail("invalid", turns.message);
337 const viewer = a.viewer!;
338 const answer = await callBuilder(this.env, {
339 slug: a.workspace.toLowerCase(),
340 workspaceId: allowed.value.workspaceId,
341 viewer,
342 system: trySystem({ workspace: a.workspace.toLowerCase(), definition: checked.value, asker: { username: viewer.username, display_name: viewer.display_username ?? null } }),
343 messages: turns.value,
344 routing: { floor: checked.value.routing.floor, ceiling: checked.value.routing.ceiling },
345 });
346 if (!answer.ok) return fail(answer.code, answer.message);
347 return ok({ text: answer.text, charged_micros: answer.charged });
348 }
349
350 /** Drafts changes from a request in words, for whoever may change the agent. Saved only through `update`. */
351 async redraft(a: { workspace: string; handle: string; viewer: User | null; request: unknown }): Promise<Result<AgentRedraft>> {
352 const found = await this.visible(a?.workspace ?? "", a?.viewer ?? null, a?.handle);
353 if (!found.ok) return found;
354 const { workspaceId, row } = found.value;
355 if (!canChange(a.viewer, a.workspace, row)) return fail("forbidden", isPersonal(row) ? NOT_YOURS_REFUSAL : MANAGE_REFUSAL);
356 const words = wordsOf(a.request, "request");
357 if (!words.ok) return fail("invalid", words.message);
358 const before = definitionOf(row);
359 const slug = a.workspace.toLowerCase();
360 const answer = await callBuilder(this.env, {
361 slug,
362 workspaceId,
363 viewer: a.viewer!,
364 system: redraftSystem(slug, before, !!row.builtin),
365 messages: [{ role: "user", content: `The change to make to @${row.handle}:\n\n${words.value}` }],
366 maxOutput: DRAFT_OUTPUT_TOKENS,
367 });
368 if (!answer.ok) return fail(answer.code, answer.message);
369 const drafted = redraftFrom(jsonIn(answer.text), before, !!row.builtin);
370 if (!drafted.ok) return fail("invalid", drafted.message);
371 return ok({ ...drafted.value, from_version: row.version, charged_micros: answer.charged });
372 }
373
374 /**
375 * Owners make a personal agent a workspace agent: a new workspace agent
376 * with the same handle, definition and every version, and one more
377 * version saying so; the personal one is archived in the same step, its
378 * memory and direct messages kept with it (owners can't read a member's
379 * memory to choose from it, so none is carried).
380 */
381 async promote(a: { workspace: string; handle: string; viewer: User | null }): Promise<Result<WorkspaceAgent>> {
382 const managed = await this.managed(a?.workspace ?? "", a?.viewer ?? null);
383 if (!managed.ok) return managed.error.code === "forbidden" ? fail("forbidden", "Only the workspace's owners promote personal agents.") : managed;
384 const row = await this.row(managed.value, a.handle);
385 if (!row) return fail("not_found", `There is no agent called @${a.handle}.`);
386 if (!isPersonal(row)) return fail("invalid", `@${row.handle} is already a workspace agent.`);
387 const versions = await this.db
388 .prepare("SELECT version, definition, changed_by, created_at FROM agent_versions WHERE agent_id = ? ORDER BY version")
389 .bind(row.id)
390 .all<{ version: number; definition: string; changed_by: string; created_at: string }>();
391 const definition = definitionOf(row);
392 const now = new Date().toISOString();
393 const id = newId("agt");
394 const version = row.version + 1;
395 const by = a.viewer!.username;
396 try {
397 const [archived] = await this.db.batch([
398 // Archived first, from the version read, so the handle is free for the new one in the same step.
399 this.db.prepare("UPDATE agents SET archived_at = ?, updated_at = ? WHERE id = ? AND version = ? AND archived_at IS NULL").bind(now, now, row.id, row.version),
400 insertAgent(this.db, id, managed.value, definition, { version, created_by: row.created_by, created_at: now, updated_at: now }),
401 ...versions.results.map((v) =>
402 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),
403 ),
404 this.versionStatement(id, version, definition, by, now),
405 ]);
406 if (!archived.meta.changes) throw new Error("changed meanwhile");
407 } catch (error) {
408 console.error("agents: a promotion failed", row.id, String(error));
409 return fail("conflict", `@${row.handle} was changed meanwhile. Reload it and try again.`);
410 }
411 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})`);
412 const saved = await this.row(managed.value, row.handle);
413 return ok(toAgent(saved!, new Date()));
414 }
415
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.416 // ── The Marketplace's install requests (./installs.ts) ─────────────────
417
418 /** Requests as the viewer sees them: every one for an owner, their own for anyone else. */
419 async installRequests(a: { workspace: string; viewer: User | null }): Promise<Result<InstallRequests>> {
420 if (a?.viewer && awaitsConfirmation(a.viewer)) return UNVERIFIED;
421 const seen = await this.seen(a?.workspace ?? "", a?.viewer ?? null);
422 if (!seen.ok) return seen;
423 const owner = canManage(a.viewer, a.workspace);
424 const requests = await listRequests(this.db, seen.value, a.viewer!, owner);
425 return ok({ requests, can_resolve: owner });
426 }
427
428 /** A member asks the owners to add something; each owner is notified. */
429 async requestInstall(a: { workspace: string; viewer: User | null; listing: unknown; note?: unknown }): Promise<Result<InstallRequest>> {
430 if (a?.viewer && awaitsConfirmation(a.viewer)) return UNVERIFIED;
431 const seen = await this.seen(a?.workspace ?? "", a?.viewer ?? null);
432 if (!seen.ok) return seen;
433 const viewer = a.viewer!;
434 if ((viewer.kind ?? "user") !== "user") return fail("forbidden", "Only people ask the workspace's owners to add things.");
435 if (canManage(viewer, a.workspace)) return fail("invalid", "You're an owner of this workspace: add it yourself.");
436 const listing = findListing(a.listing);
437 if (!listing) return fail("not_found", "That isn't something a workspace can add yet.");
438 const opened = await openRequest(this.db, seen.value, newId("ins"), listing, viewer, cleanRequestNote(a.note));
439 if (!opened.ok) return opened;
440 const slug = a.workspace.toLowerCase();
441 this.audit(viewer, slug, "request_install", listing.ref, `Asked the owners to add ${listing.name}`, "marketplace");
442 this.defer(this.tellOwners(slug, viewer, opened.value));
443 return opened;
444 }
445
446 /** An owner adds or turns down a request; whoever asked is told. */
447 async resolveInstallRequest(a: { workspace: string; viewer: User | null; id: unknown; status: unknown }): Promise<Result<InstallRequest>> {
448 const managed = await this.managed(a?.workspace ?? "", a?.viewer ?? null);
449 if (!managed.ok) {
450 return managed.error.code === "forbidden" ? fail("forbidden", "Only the workspace's owners answer requests.") : managed;
451 }
452 if (a.status !== "done" && a.status !== "declined") return fail("invalid", "Answer a request with done or declined.");
453 const answered = await resolveRequest(this.db, managed.value, String(a.id ?? ""), a.status, a.viewer!);
454 if (!answered.ok) return answered;
455 const slug = a.workspace.toLowerCase();
456 const { request } = answered.value;
457 this.audit(a.viewer!, slug, "answer_install_request", request.listing, `${request.status === "done" ? "Added" : "Turned down"} ${request.name} for @${request.requested_by}`, "marketplace");
458 this.defer(this.tellRequester(slug, a.viewer!, answered.value));
459 return ok(request);
460 }
461
462 // ── Extensions installed in the workspace (./extensions.ts) ──────────────
463
464 /** The workspace's installs; any member sees them. */
465 async extensionInstalls(a: { workspace: string; viewer: User | null }): Promise<Result<ExtensionInstall[]>> {
466 if (a?.viewer && awaitsConfirmation(a.viewer)) return UNVERIFIED;
467 const seen = await this.seen(a?.workspace ?? "", a?.viewer ?? null);
468 if (!seen.ok) return seen;
469 return ok(await extensions.listInstalls(this.db, seen.value));
470 }
471
472 /** Owners install a published extension; whoever asked for it is told. */
473 async installExtension(a: { workspace: string; viewer: User | null; extension: unknown }): Promise<Result<ExtensionInstall>> {
474 const managed = await this.managed(a?.workspace ?? "", a?.viewer ?? null);
475 if (!managed.ok) return managed.error.code === "forbidden" ? fail("forbidden", "Only the workspace's owners install extensions.") : managed;
476 const installed = await extensions.install(this.db, extensionById, managed.value, newId("ins"), String(a.extension ?? ""), a.viewer!.username);
477 if (!installed.ok) return installed;
478 const slug = a.workspace.toLowerCase();
479 this.audit(a.viewer!, slug, "install_extension", installed.value.listing, `Installed ${installed.value.listing} ${installed.value.version}`, "marketplace");
480 this.defer(this.answerListing(slug, managed.value, installed.value.listing, a.viewer!));
481 return installed;
482 }
483
484 /** Owners switch an install on or off. */
485 async setExtensionEnabled(a: { workspace: string; viewer: User | null; listing: unknown; enabled: unknown }): Promise<Result<ExtensionInstall>> {
486 const managed = await this.managed(a?.workspace ?? "", a?.viewer ?? null);
487 if (!managed.ok) return managed;
488 const listing = String(a.listing ?? "");
489 if (!extensions.extensionIdOf(listing)) return fail("invalid", "Name an extension as extension:<id>.");
490 const changed = await extensions.setEnabled(this.db, managed.value, listing, a.enabled === true, a.viewer!.username);
491 if (changed.ok) this.audit(a.viewer!, a.workspace, a.enabled === true ? "enable_extension" : "disable_extension", listing, `${a.enabled === true ? "Switched on" : "Switched off"} ${listing}`, "marketplace");
492 return changed;
493 }
494
495 /** Owners cap what an install spends a month. */
496 async setExtensionBudget(a: { workspace: string; viewer: User | null; listing: unknown; monthly_micros: unknown }): Promise<Result<ExtensionInstall>> {
497 const managed = await this.managed(a?.workspace ?? "", a?.viewer ?? null);
498 if (!managed.ok) return managed;
499 const listing = String(a.listing ?? "");
500 const micros = a.monthly_micros == null ? null : Number(a.monthly_micros);
501 const changed = await extensions.setBudget(this.db, managed.value, listing, micros);
502 if (changed.ok) this.audit(a.viewer!, a.workspace, "set_extension_budget", listing, `Set ${listing}'s monthly budget`, "marketplace");
503 return changed;
504 }
505
506 /** Owners remove an install. */
507 async uninstallExtension(a: { workspace: string; viewer: User | null; listing: unknown }): Promise<Result<null>> {
508 const managed = await this.managed(a?.workspace ?? "", a?.viewer ?? null);
509 if (!managed.ok) return managed;
510 const listing = String(a.listing ?? "");
511 const removed = await extensions.uninstall(this.db, managed.value, listing, a.viewer!.username);
512 if (removed.ok) this.audit(a.viewer!, a.workspace, "uninstall_extension", listing, `Uninstalled ${listing}`, "marketplace");
513 return removed;
514 }
515
516 /** Marks every open request for `listing` added, and tells each person who asked. */
517 private async answerListing(slug: string, workspaceId: string, listing: string, by: User): Promise<void> {
518 try {
519 const answered = await resolveListing(this.db, workspaceId, listing, by as Person);
520 await Promise.all(answered.map((one) => this.tellRequester(slug.toLowerCase(), by, one)));
521 } catch (error) {
522 console.error("agents: requests for a listing were not answered", listing, String(error));
523 }
524 }
525
526 /** Every owner hears of a new request (at most 20 of them). */
527 private async tellOwners(slug: string, asker: User, request: InstallRequest): Promise<void> {
528 if (!this.env.NOTIFY) return;
529 try {
530 const members = await identityClient(this.env.IDENTITY).listMembers(slug, asker);
531 if (!members.ok) throw new Error(members.error.message);
532 const owners = members.value.filter((member) => member.role === "owner").slice(0, 20);
533 const notify = notifyClient(this.env.NOTIFY);
534 await Promise.all(
535 owners.map((owner) =>
536 notify
537 .notify(
538 { username: owner.username },
539 {
540 id: `install-request:${request.id}:${owner.username}`,
541 kind: "approval",
542 workspace: slug,
543 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.544 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.545 href: requestsPath(slug),
546 actor: { kind: "user", id: asker.id, name: asker.username, avatar: asker.avatar ?? null, avatar_seed: null },
547 created_at: new Date().toISOString(),
548 },
549 )
550 .catch(() => undefined),
551 ),
552 );
553 } catch (error) {
554 console.error("agents: owners were not told of an install request", request.id, String(error));
555 }
556 }
557
558 /** The person who asked hears their request was answered. */
559 private async tellRequester(slug: string, by: User, answered: Answered): Promise<void> {
560 if (!this.env.NOTIFY) return;
561 const { request } = answered;
562 const line = answerLine(request, `@${by.username}`);
563 await notifyClient(this.env.NOTIFY)
564 .notify(
565 { user_id: answered.requested_by_id },
566 {
567 id: `install-answer:${request.id}`,
568 kind: "inbox",
569 workspace: slug,
570 title: line.title,
571 body: line.body,
572 href: request.status === "done" ? listingPath(slug, request.listing) : requestsPath(slug),
573 actor: { kind: "user", id: by.id, name: by.username, avatar: by.avatar ?? null, avatar_seed: null },
574 created_at: new Date().toISOString(),
575 },
576 )
577 .catch((error: unknown) => console.error("agents: a requester was not told", request.id, String(error)));
578 }
579
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)580 /**
581 * A new agent's first words: the DM with the person who made it is
582 * opened, and the agent says hello there in its own voice, as a reply
583 * billed like any other (a fixed hello when no model can be used).
584 * After the answer; a failure only logs.
585 */
586 private async hello(workspace: string, workspaceId: string, agentId: string, creator: User): Promise<void> {
587 if ((creator.kind ?? "user") !== "user") return;
588 try {
589 const dm = await chatClient(this.env.CHAT).openDm(workspace, creator, [{ kind: "agent", id: agentId }]);
590 if (!dm.ok) throw new Error(dm.error.message);
591 const desk = this.env.DESKS.get(this.env.DESKS.idFromName(agentId));
592 await desk.take({
593 workspace,
594 workspace_id: workspaceId,
595 channel_id: dm.value.id,
596 channel_kind: "dm",
597 channel_name: null,
598 agent_id: agentId,
599 // One hello per agent, however often this runs.
600 message_id: `hello:${agentId}`,
601 thread_root: null,
602 asked_by: creator.id,
603 hops: 0,
604 asker: askerAccess(creator, workspace),
605 hello: true,
606 });
607 } catch (error) {
608 console.error("agents: a new agent's hello was not sent", agentId, String(error));
609 }
610 }
611
Chat and workspace agents: channels, DMs and named agents you talk to612 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.613 const found = await this.visible(a.workspace, a.viewer, a.handle);
614 if (!found.ok) return found;
615 const { row } = found.value;
616 // Owners change the workspace's agents; a member changes their own personal one.
617 if (!canChange(a.viewer, a.workspace, row)) return fail("forbidden", isPersonal(row) ? NOT_YOURS_REFUSAL : MANAGE_REFUSAL);
618 const workspaceId = found.value.workspaceId;
Chat and workspace agents: channels, DMs and named agents you talk to619 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.620 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)621 // The built-in @g1t keeps who it is and its job; the rest is the workspace's.
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.622 // A personal agent stays on no team.
623 const allowed = row.builtin
624 ? builtinChanges(before, asked)
625 : { ok: true as const, value: isPersonal(row) && asked.team !== undefined ? { ...asked, team: null } : 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)626 if (!allowed.ok) return fail("invalid", allowed.message);
627 const checked = applyChanges(before, allowed.value, TEMPLATE_IDS, { builtin: !!row.builtin });
Chat and workspace agents: channels, DMs and named agents you talk to628 if (!checked.ok) return fail("invalid", checked.message);
629 const definition = checked.value;
630 if (JSON.stringify(definition) === JSON.stringify(before)) return ok(toAgent(row, 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)631 if (definition.team !== before.team) {
632 const team = await this.teamExists(a.workspace, a.viewer!, definition.team);
633 if (!team.ok) return team;
634 }
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.635 if (definition.handle !== before.handle && (await this.handleTaken(workspaceId, definition.handle, row.id))) {
Chat and workspace agents: channels, DMs and named agents you talk to636 return fail("conflict", `${a.workspace} already has an agent called @${definition.handle}.`);
637 }
638 const now = new Date().toISOString();
639 const version = row.version + 1;
640 const by = a.viewer!.username;
641 try {
642 const [updated] = await this.db.batch([
643 // Only from the version read: two owners saving at once never lose
644 // 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)645 updateAgent(this.db, row.id, row.version, definition, { version, updated_at: now }),
Chat and workspace agents: channels, DMs and named agents you talk to646 this.db
647 .prepare(
648 `INSERT INTO agent_versions (agent_id, version, definition, changed_by, created_at)
649 SELECT ?1, ?2, ?3, ?4, ?5 WHERE EXISTS (SELECT 1 FROM agents WHERE id = ?1 AND version = ?2 AND updated_at = ?5)`,
650 )
651 .bind(row.id, version, JSON.stringify(definition), by, now),
652 ]);
653 if (!updated.meta.changes) return fail("conflict", `@${before.handle} was changed meanwhile. Reload it and try again.`);
654 } catch (error) {
655 if (String(error).includes("UNIQUE")) return fail("conflict", `${a.workspace} already has an agent called @${definition.handle}.`);
656 throw error;
657 }
658 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.659 this.audit(a.viewer!, a.workspace, "update_agent", definition.handle, `Changed @${definition.handle}${renamed} to version ${version}`, "agents", isPersonal(row) ? "member" : "owner");
660 const saved = await this.row(workspaceId, definition.handle);
Chat and workspace agents: channels, DMs and named agents you talk to661 return ok(toAgent(saved!, new Date()));
662 }
663
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.664 /** What each effort level has cost one agent, from its own finished sessions. */
665 async effortCosts(a: { workspace: string; handle: string; viewer: User | null }): Promise<Result<AgentEffortCosts>> {
666 if (a?.viewer && awaitsConfirmation(a.viewer)) return UNVERIFIED;
667 const seen = await this.seen(a?.workspace ?? "", a?.viewer ?? null);
668 if (!seen.ok) return seen;
669 const row = await this.row(seen.value, a.handle);
670 if (!row) return fail("not_found", `There is no agent called @${a.handle}.`);
671 const outcomes = await outcomesSince(this.db, seen.value, sinceWindow(), row.id);
672 return ok(effortCostsOf(row.handle, effortOf(definitionOf(row).routing), outcomes.get(row.id) ?? []));
673 }
674
675 /** Ways to spend less, checked against past work: every agent's, or one's. */
676 async recommendations(a: { workspace: string; viewer: User | null; handle?: string | null }): Promise<Result<AgentRecommendations>> {
677 if (a?.viewer && awaitsConfirmation(a.viewer)) return UNVERIFIED;
678 const seen = await this.seen(a?.workspace ?? "", a?.viewer ?? null);
679 if (!seen.ok) return seen;
680 let agentId: string | null = null;
681 if (a.handle) {
682 const row = await this.row(seen.value, a.handle);
683 if (!row) return fail("not_found", `There is no agent called @${a.handle}.`);
684 agentId = row.id;
685 }
686 return ok(await readRecommendations(this.db, seen.value, agentId));
687 }
688
689 /**
690 * Owners apply a suggestion (the agent's effort changes as a new version
691 * of it, and the audit log says so) or dismiss it. Only an open one, and
692 * only while the agent's setting is still the one it was made for.
693 */
694 async resolveRecommendation(a: { workspace: string; viewer: User | null; id: string; action: string }): Promise<Result<AgentRecommendation>> {
695 const managed = await this.managed(a?.workspace ?? "", a?.viewer ?? null);
696 if (!managed.ok) return managed;
697 if (a.action !== "apply" && a.action !== "dismiss") return fail("invalid", "Apply or dismiss.");
698 const found = await recommendationRow(this.db, managed.value, String(a.id ?? ""));
699 if (!found) return fail("not_found", "There is no such suggestion.");
700 if (found.status !== "open") return fail("conflict", "This suggestion was already decided, or is no longer current.");
701 const by = a.viewer!.username;
702 if (a.action === "dismiss") {
703 if (!(await markResolved(this.db, found.id, "dismissed", by))) return fail("conflict", "This suggestion was decided meanwhile.");
704 this.audit(a.viewer!, a.workspace, "dismiss_recommendation", found.handle ?? found.agent_id, `Dismissed "${found.title}"`);
705 } else {
706 const agent = await this.row(managed.value, found.handle);
707 if (!agent || agent.id !== found.agent_id) return fail("not_found", "That agent is gone.");
708 if (effortOf(definitionOf(agent).routing) !== found.from_effort) {
709 await this.db.prepare("UPDATE agent_recommendations SET status = 'stale' WHERE id = ? AND status = 'open'").bind(found.id).run();
710 return fail("conflict", `@${agent.handle}'s effort was changed since this was suggested. The next check looks again.`);
711 }
712 // Claimed first, so two owners pressing Apply change the agent once.
713 if (!(await markResolved(this.db, found.id, "applied", by))) return fail("conflict", "This suggestion was decided meanwhile.");
714 const changed = await this.update({ workspace: a.workspace, handle: agent.handle, viewer: a.viewer, changes: { routing: { effort: found.to_effort as never } } });
715 if (!changed.ok) {
716 await this.db.prepare("UPDATE agent_recommendations SET status = 'open', resolved_by = NULL, resolved_at = NULL WHERE id = ?").bind(found.id).run();
717 return changed;
718 }
719 this.audit(
720 a.viewer!,
721 a.workspace,
722 "apply_recommendation",
723 agent.handle,
724 `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}`,
725 );
726 }
727 const after = await recommendationRow(this.db, managed.value, found.id);
728 return ok(toRecommendation(after!));
729 }
730
Chat and workspace agents: channels, DMs and named agents you talk to731 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.732 const found = await this.visible(a.workspace, a.viewer, a.handle);
733 if (!found.ok) return found;
734 const { row } = found.value;
735 // Owners archive any agent; a member their own personal one.
736 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)737 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 to738 const now = new Date().toISOString();
739 await this.db.prepare("UPDATE agents SET archived_at = ?, updated_at = ? WHERE id = ? AND archived_at IS NULL").bind(now, now, row.id).run();
740 this.audit(a.viewer!, a.workspace, "archive_agent", row.handle, `Archived @${row.handle}`);
741 return ok(null);
742 }
743
744 /**
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)745 * Makes the workspace's built-in @g1t if it does not exist yet: an
746 * ordinary agent row, marked builtin, at version 1. Safe to call on
747 * every request; once seen, an isolate does not write again.
748 */
749 private async ensureBuiltin(workspaceId: string): Promise<void> {
750 await ensureBuiltin(this.db, workspaceId);
751 }
752
753 /** Internal: the workspace's @g1t, made if need be, for the chat service. */
754 async builtin(a: { workspace: string; workspace_id: string }): Promise<Result<WorkspaceAgent>> {
755 if (typeof a?.workspace_id !== "string" || !a.workspace_id) return fail("invalid", "Name the workspace by id.");
756 await this.ensureBuiltin(a.workspace_id);
757 const now = new Date();
758 const row = await this.db
759 .prepare(selectAgents("a.workspace_id = ?3 AND a.builtin = 1"))
760 .bind(...periods(now), a.workspace_id)
761 .first<Row>();
762 return row ? ok(toAgent(row, now)) : fail("not_found", "This workspace's @g1t could not be made.");
763 }
764
765 /**
Chat and workspace agents: channels, DMs and named agents you talk to766 * Hands a message to the agent's desk, which answers it in the
767 * background. Returns as soon as the desk holds it.
768 */
769 async deliver(delivery: AgentDelivery): Promise<Result<null>> {
770 const fields = ["workspace", "workspace_id", "channel_id", "agent_id", "message_id", "asked_by"] as const;
771 if (!delivery || fields.some((field) => typeof delivery[field] !== "string" || !delivery[field])) {
772 return fail("invalid", "A delivery names the workspace, channel, agent, message and who asked.");
773 }
774 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)775 await this.ensureBuiltin(delivery.workspace_id);
Chat and workspace agents: channels, DMs and named agents you talk to776 const desk = this.env.DESKS.get(this.env.DESKS.idFromName(delivery.agent_id));
777 await desk.take({ ...delivery, hops: Math.max(0, Math.floor(Number(delivery.hops) || 0)), thread_root: delivery.thread_root ?? null });
778 return ok(null);
779 }
780
781 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)782 return versionStatement(this.db, agentId, version, d, by, at);
Chat and workspace agents: channels, DMs and named agents you talk to783 }
784
785 /**
786 * Records a change to an agent in the workspace's audit log, after the
787 * answer. Never fails the change: a log that cannot be written is logged.
788 * The events service's audit contract speaks camelCase (Rust's
789 * `NewAuditEntry`).
790 */
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.791 /** A change to the skill library, in the audit log as `agents/skills/<name>`. */
792 auditSkill(actor: User, workspace: string, action: string, name: string, message: string): void {
793 this.audit(actor, workspace, action, `skills/${name}`, message);
794 }
795
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.796 private audit(
797 actor: User,
798 workspace: string,
799 action: string,
800 handle: string,
801 message: string,
802 area: "agents" | "marketplace" = "agents",
803 rule: "owner" | "member" | null = null,
804 ): void {
Chat and workspace agents: channels, DMs and named agents you talk to805 const kind = actor.kind === "workspace" ? "workspace" : actor.kind === "agent" ? "agent" : actor.kind === "system" ? "system" : "person";
806 const entry = {
807 actorKind: kind,
808 actor: actor.username,
809 actorId: actor.id,
810 agent: null,
811 onBehalfOf: null,
812 runId: null,
813 runKind: null,
814 credentialId: null,
815 action,
816 surface: "web",
817 workspace: workspace.toLowerCase(),
818 repo: null,
819 number: null,
820 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.821 path: `${area}/${handle}`,
Chat and workspace agents: channels, DMs and named agents you talk to822 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.823 rule: rule ?? (area === "marketplace" && action === "request_install" ? "member" : "owner"),
Chat and workspace agents: channels, DMs and named agents you talk to824 result: "ok",
825 message,
826 requestId: `req_${crypto.randomUUID()}`,
827 };
828 this.defer(
829 this.env.EVENTS.fetch("https://service/rpc/audit_record", {
830 method: "POST",
831 headers: { "content-type": "application/json" },
832 body: JSON.stringify({ entries: [entry] }),
833 })
834 .then((response) => {
835 if (!response.ok) throw new Error(`status ${response.status}`);
836 })
837 .catch((error: unknown) => console.error("agents: audit entry not recorded", action, handle, String(error))),
838 );
839 }
840}
841
842/** One RPC method's answer. */
843async 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.844 // The skill library (./skill-rpc.ts).
845 const skill = await skillRpc(method, args, (a, run) => service.view(a, run), (viewer, workspace, action, name, message) => service.auditSkill(viewer, workspace, action, name, message));
846 if (skill) return Response.json(skill);
Chat and workspace agents: channels, DMs and named agents you talk to847 switch (method) {
848 case "list":
849 return Response.json(await service.list(args));
850 case "get":
851 return Response.json(await service.get(args));
852 case "by_ids":
853 return Response.json(await service.byIds(args));
854 case "create":
855 return Response.json(await service.create(args));
856 case "update":
857 return Response.json(await service.update(args));
858 case "archive":
859 return Response.json(await service.archive(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.860 case "draft":
861 return Response.json(await service.draft(args));
862 case "try_draft":
863 return Response.json(await service.tryDraft(args));
864 case "redraft":
865 return Response.json(await service.redraft(args));
866 case "promote":
867 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)868 case "builtin":
869 return Response.json(await service.builtin(args));
Chat and workspace agents: channels, DMs and named agents you talk to870 case "templates":
871 return Response.json(TEMPLATES);
872 case "deliver":
873 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 asked874 case "overview":
875 return Response.json(await service.view(args, (ctx) => views.overview(ctx)));
876 case "sessions":
877 return Response.json(await service.view(args, (ctx) => views.listSessions(ctx, args)));
878 case "session":
879 return Response.json(await service.view(args, (ctx) => views.sessionDetail(ctx, args.id)));
880 case "stop_session":
881 return Response.json(await service.view(args, (ctx) => views.stopSession(ctx, args.id)));
882 case "approve_session":
883 return Response.json(await service.view(args, (ctx) => views.approveSession(ctx, args.id, args.cap_micros)));
884 case "steer_session":
885 return Response.json(await service.view(args, (ctx) => views.steerSession(ctx, args.id, args.body)));
886 case "memories":
887 return Response.json(await service.view(args, (ctx) => views.memories(ctx, args.handle)));
888 case "remember":
889 return Response.json(await service.view(args, (ctx) => views.remember(ctx, args.handle, args.input)));
890 case "update_memory":
891 return Response.json(await service.view(args, (ctx) => views.updateMemory(ctx, args.handle, args.id, args.changes)));
892 case "forget":
893 return Response.json(await service.view(args, (ctx) => views.forget(ctx, args.handle, args.id)));
894 case "routines":
895 return Response.json(await service.view(args, (ctx) => views.routines(ctx, args.handle)));
896 case "save_routine":
897 return Response.json(await service.view(args, (ctx) => views.saveRoutine(ctx, args.handle, args.input, args.id ?? null)));
898 case "delete_routine":
899 return Response.json(await service.view(args, (ctx) => views.deleteRoutine(ctx, args.handle, args.id)));
900 case "run_routine":
901 return Response.json(await service.view(args, (ctx) => views.runRoutineNow(ctx, args.handle, args.id)));
902 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.903 return Response.json(await service.view(args, (ctx) => views.spend(ctx, args.handle ?? null, { period: args.period, person: args.person })));
904 case "person_budgets":
905 return Response.json(await service.view(args, (ctx) => views.personBudgetsView(ctx)));
906 case "set_person_budget":
907 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.908 case "team_context":
909 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 asked910 case "activity":
911 return Response.json(await service.view(args, (ctx) => views.activity(ctx, args.handle)));
912 case "versions":
913 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 calendar914 case "card_action":
915 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 asked916 case "policy":
917 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.918 case "install_requests":
919 return Response.json(await service.installRequests(args));
920 case "request_install":
921 return Response.json(await service.requestInstall(args));
922 case "resolve_install_request":
923 return Response.json(await service.resolveInstallRequest(args));
924 case "extension_installs":
925 return Response.json(await service.extensionInstalls(args));
926 case "install_extension":
927 return Response.json(await service.installExtension(args));
928 case "set_extension_enabled":
929 return Response.json(await service.setExtensionEnabled(args));
930 case "set_extension_budget":
931 return Response.json(await service.setExtensionBudget(args));
932 case "uninstall_extension":
933 return Response.json(await service.uninstallExtension(args));
Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how.934 case "effort_costs":
935 return Response.json(await service.effortCosts(args));
936 case "recommendations":
937 return Response.json(await service.recommendations(args));
938 case "resolve_recommendation":
939 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 asked940 case "set_policy":
941 return Response.json(await service.view(args, (ctx) => views.setPolicy(ctx, args.policy)));
Chat and workspace agents: channels, DMs and named agents you talk to942 default:
943 return new Response("Unknown method\n", { status: 404 });
944 }
945}
946
947export default {
948 async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
949 const match = new URL(request.url).pathname.match(/^\/rpc\/([a-z_]+)$/);
950 if (request.method !== "POST" || !match) return new Response("Not found\n", { status: 404 });
951 // A replica near the caller when it asks for one (@g1t/contracts d1.ts).
952 const opened = openD1(env.DB, request);
953 const service = new Agents(Object.create(env, { DB: { value: opened.db } }) as Env, (work) => ctx.waitUntil(work));
954 const args = (await request.json().catch(() => ({}))) as any;
955 return opened.finish(await answer(service, match[1], args));
956 },
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 asked957
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened958 /** Events routines run on, from the events service (SUBSCRIBER_AGENTS). */
959 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.960 const events = batch.messages.map((message) => message.body as G1tEvent);
961 // A push to a default branch: skill libraries that follow that repository read it again (./skill-library.ts).
962 const pushes = events.flatMap((event) => (event?.type === "git.push" && event.data?.defaultBranch && event.data.repoId ? [{ repoId: event.data.repoId }] : []));
963 await Promise.all([onEvents(env as unknown as SessionEnv, events.filter((event) => event?.type !== "git.push")), pushes.length ? skillPushes(env as unknown as SessionEnv, pushes) : null]);
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened964 batch.ackAll();
965 },
966
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.967 /** Every few minutes: routines that are due, session steps a desk lost, and once an hour the spend check. */
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 asked968 async scheduled(_controller: ScheduledController, env: Env, ctx: ExecutionContext): Promise<void> {
969 const sessions = env as unknown as SessionEnv;
970 ctx.waitUntil(
971 Promise.all([
972 runDue(sessions).catch((error: unknown) => console.error("agents: routines did not run", String(error))),
973 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.974 // Spend less, keep quality: each workspace checked weekly (src/recommend.ts).
975 checkDue(env.DB).catch((error: unknown) => console.error("agents: the spend check 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 asked976 ]),
977 );
978 },
Chat and workspace agents: channels, DMs and named agents you talk to979} satisfies ExportedHandler<Env>;

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