Skip to content
446 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/**
2 * An agent's definition: what a new one gets by default, what a change may
3 * set, and how a stored row reads. Pure, so it is tested on its own.
4 */
5import type {
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.6 AbilityLevel,
7 AbilitySetting,
8 AgentAbilities,
Chat and workspace agents: channels, DMs and named agents you talk to9 AgentAutonomy,
10 AgentBudget,
11 AgentRouting,
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.12 McpServer,
Chat and workspace agents: channels, DMs and named agents you talk to13 NewWorkspaceAgent,
14 PersonalityPreset,
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)15 AgentFaces,
16 SubagentDef,
Chat and workspace agents: channels, DMs and named agents you talk to17} from "@g1t/contracts";
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.18import { ABILITY_LEVELS, EMPTY_ABILITIES, MAX_MCP_SERVERS, integrationAbilities, maxLevel, mcpAbility, withinLevel } from "../../../packages/contracts/src/abilities.ts";
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.19import { FOUNDATIONAL_SKILL_IDS } from "../../../packages/contracts/src/skills.ts";
Chat and workspace agents: channels, DMs and named agents you talk to20
21import { checkHandle } from "./handle.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.22import { isEffort, isTier, limitsAgree } from "./routing.ts";
Chat and workspace agents: channels, DMs and named agents you talk to23
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.24/** A library skill's id (skill-library.ts), as `skills_off` may name it. */
25const LIBRARY_SKILL_ID = /^skl_[0-9a-z]{26}$/;
26
Chat and workspace agents: channels, DMs and named agents you talk to27export const PRESETS: PersonalityPreset[] = ["crisp", "friendly", "socratic", "terse"];
28
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.29export const DEFAULT_ROUTING: AgentRouting = { floor: null, ceiling: null, providers: [], pinned: null, effort: "auto" };
Chat and workspace agents: channels, DMs and named agents you talk to30export const DEFAULT_BUDGET: AgentBudget = { monthly_micros: null, daily_micros: null, task_micros: null };
31/** What an agent may do alone until someone says otherwise: open pull requests; the rest asks. */
32export const DEFAULT_AUTONOMY: AgentAutonomy = {
33 open_pull_requests: "alone",
34 merge: "approval",
35 deploy_production: "approval",
36 edit_docs: "suggest",
37};
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.38/** Tasks at once. */
Chat and workspace agents: channels, DMs and named agents you talk to39export const DEFAULT_CAPACITY = 3;
40export const MAX_CAPACITY = 10;
41
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.42const LIMITS = { displayName: 64, role: 120, title: 60, duty: 160, instructions: 8000, personality: 1000, providers: 10, pinned: 200 };
Chat and workspace agents: channels, DMs and named agents you talk to43/** $100,000 in millionths: a cap above this is a typo. */
44const MAX_MICROS = 100_000_000_000;
45
46/** Everything a definition holds, complete: what is stored and versioned. */
47export type Definition = {
48 handle: string;
49 display_name: string;
50 role: string;
51 instructions: string;
52 personality_preset: PersonalityPreset;
53 personality: string;
54 routing: AgentRouting;
55 budget: AgentBudget;
56 autonomy: AgentAutonomy;
57 capacity: number;
58 template: string | null;
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)59 /** What its generated avatar is drawn from. */
60 avatar_seed: string;
61 title: string;
62 responsibilities: string[];
63 subagents: SubagentDef[];
64 faces: AgentFaces;
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.65 /** Spaces whose artifacts it reads first. */
Agents recall what Docs say before they answer or work, and each has required reading66 reading: string[];
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.67 /** Foundational skills turned off for it, by id (@g1t/contracts skills.ts). */
68 skills_off: string[];
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.69 /** Its abilities' levels and credentials, and its MCP servers (@g1t/contracts abilities.ts). */
70 abilities: AgentAbilities;
Chat and workspace agents: channels, DMs and named agents you talk to71};
72
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.73/**
74 * The one-line role its title makes: "QA Engineer". Which teams it is on
75 * are team memberships (identity), never part of the agent.
76 */
77export function roleOf(d: Pick<Definition, "title">): string {
78 return d.title.trim();
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)79}
80
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.81/**
82 * The role an agent was given when it carried its own team or department,
83 * made from them: "QA Engineer on the qa team", "QA Engineer, QA". A
84 * stored role that is exactly this follows the title (store.ts).
85 */
86export function legacyRoleOf(title: string, team: string | null, department: string | null): string {
87 const t = title.trim();
88 if (!t) return "";
89 if (team) return `${t} on the ${team} team`;
90 return department?.trim() ? `${t}, ${department.trim()}` : t;
91}
92
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)93export const MAX_SUBAGENTS = 8;
94
Chat and workspace agents: channels, DMs and named agents you talk to95export type Checked<T> = { ok: true; value: T } | { ok: false; message: string };
96
97const bad = (message: string): { ok: false; message: string } => ({ ok: false, message });
98
99function text(value: unknown, what: string, max: number, required: boolean): Checked<string> {
100 if (value === undefined || value === null) return required ? bad(`${what} is required.`) : { ok: true, value: "" };
101 if (typeof value !== "string") return bad(`${what} is text.`);
102 const trimmed = value.trim();
103 if (required && !trimmed) return bad(`${what} is required.`);
104 if (trimmed.length > max) return bad(`${what} is at most ${max} characters.`);
105 return { ok: true, value: trimmed };
106}
107
108function routingOf(base: AgentRouting, given: unknown): Checked<AgentRouting> {
109 if (given === undefined || given === null) return { ok: true, value: base };
110 if (typeof given !== "object") return bad("Routing is an object.");
111 const g = given as Partial<AgentRouting>;
112 const next: AgentRouting = { ...base };
113 for (const key of ["floor", "ceiling"] as const) {
114 if (g[key] === undefined) continue;
115 if (g[key] !== null && !isTier(g[key])) return bad(`The ${key} is small, large, frontier or none.`);
116 next[key] = g[key] ?? null;
117 }
118 if (!limitsAgree(next.floor, next.ceiling)) return bad("The floor is above the ceiling: lower the floor or raise the ceiling.");
119 if (g.providers !== undefined) {
120 if (!Array.isArray(g.providers) || g.providers.some((p) => typeof p !== "string" || !p.trim())) return bad("Providers is a list of names.");
121 const providers = [...new Set(g.providers.map((p) => p.trim()))];
122 if (providers.length > LIMITS.providers) return bad(`An agent names at most ${LIMITS.providers} providers.`);
123 next.providers = providers;
124 }
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.125 if (g.effort !== undefined) {
126 if (g.effort !== null && !isEffort(g.effort)) return bad("Effort is auto, low, medium, high or max.");
127 next.effort = g.effort ?? "auto";
128 }
Chat and workspace agents: channels, DMs and named agents you talk to129 if (g.pinned !== undefined) {
130 if (g.pinned === null || g.pinned === "") next.pinned = null;
131 else if (typeof g.pinned !== "string" || g.pinned.trim().length > LIMITS.pinned || !/^[^/\s]+\/\S+$/.test(g.pinned.trim())) {
132 return bad("A pinned model is written provider/model.");
133 } else next.pinned = g.pinned.trim();
134 }
135 return { ok: true, value: next };
136}
137
138function budgetOf(base: AgentBudget, given: unknown): Checked<AgentBudget> {
139 if (given === undefined || given === null) return { ok: true, value: base };
140 if (typeof given !== "object") return bad("Budget is an object.");
141 const g = given as Partial<AgentBudget>;
142 const next: AgentBudget = { ...base };
143 for (const key of ["monthly_micros", "daily_micros", "task_micros"] as const) {
144 const value = g[key];
145 if (value === undefined) continue;
146 if (value === null) next[key] = null;
147 else if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > MAX_MICROS) {
148 return bad("A budget is a whole number of millionths of a dollar, or none.");
149 } else next[key] = value;
150 }
151 return { ok: true, value: next };
152}
153
154const AUTONOMY_CHOICES: { [K in keyof AgentAutonomy]: AgentAutonomy[K][] } = {
155 open_pull_requests: ["alone", "approval"],
156 merge: ["alone", "approval", "never"],
157 deploy_production: ["approval", "never"],
158 edit_docs: ["alone", "suggest"],
159};
160
161function autonomyOf(base: AgentAutonomy, given: unknown): Checked<AgentAutonomy> {
162 if (given === undefined || given === null) return { ok: true, value: base };
163 if (typeof given !== "object") return bad("Autonomy is an object.");
164 const next = { ...base } as Record<string, string>;
165 for (const [key, choices] of Object.entries(AUTONOMY_CHOICES) as [string, string[]][]) {
166 const value = (given as Record<string, unknown>)[key];
167 if (value === undefined) continue;
168 if (typeof value !== "string" || !choices.includes(value)) return bad(`${key} is one of ${choices.join(", ")}.`);
169 next[key] = value;
170 }
171 return { ok: true, value: next as AgentAutonomy };
172}
173
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.174/** The most abilities with a choice of their own one agent keeps. */
175const MAX_ABILITY_SETTINGS = 400;
176
177/**
178 * The abilities' settings, checked against what the agent can have: an
179 * integration ability the catalog knows, or a tool of one of its MCP
180 * servers, each at a level its kind allows. g1t's own abilities keep their
181 * choices in `autonomy`, so nothing is kept for them here.
182 */
183function abilitiesOf(base: AgentAbilities, given: unknown, servers: McpServer[]): Checked<AgentAbilities> {
184 if (given === undefined || given === null) return { ok: true, value: { ...base, mcp_servers: servers } };
185 if (typeof given !== "object" || Array.isArray(given)) return bad("Abilities are an object.");
186 const settings = (given as { settings?: unknown }).settings;
187 if (settings === undefined) return { ok: true, value: { ...base, mcp_servers: servers } };
188 if (!settings || typeof settings !== "object" || Array.isArray(settings)) return bad("Abilities' settings are an object by ability id.");
189 const entries = Object.entries(settings as Record<string, unknown>);
190 if (entries.length > MAX_ABILITY_SETTINGS) return bad(`At most ${MAX_ABILITY_SETTINGS} abilities keep a setting.`);
191 const next: Record<string, AbilitySetting> = {};
192 for (const [id, raw] of entries) {
193 const def = abilityDef(id, servers);
194 if (!def) return bad(`There is no ability called ${id.slice(0, 60)} for this agent.`);
195 if (!raw || typeof raw !== "object") return bad(`${id}'s setting is an object.`);
196 const setting = raw as Record<string, unknown>;
197 const kept: AbilitySetting = {};
198 if (setting.level !== undefined && setting.level !== null) {
199 if (typeof setting.level !== "string" || !ABILITY_LEVELS.includes(setting.level as AbilityLevel)) return bad(`${id}'s level is alone, asked, ask or never.`);
200 const level = setting.level as AbilityLevel;
201 const max = maxLevel(def.kind);
202 if (!withinLevel(level, max)) return bad(`${def.label} can't be set above ${max === "ask" ? "Ask first" : max}: it ${def.kind === "restricted" ? "is a purchase, a credential or a permission change" : "isn't allowed that freely"}.`);
203 kept.level = level;
204 }
205 if (setting.credentials !== undefined && setting.credentials !== null) {
206 if (def.group !== "integration") return bad(`Only an integration's abilities say whose connection they run on.`);
207 if (setting.credentials !== "workspace" && setting.credentials !== "asker") return bad(`${id}'s credentials are workspace or asker.`);
208 kept.credentials = setting.credentials;
209 }
210 if (Object.keys(kept).length) next[id] = kept;
211 }
212 // In id order, so the same choices always read the same.
213 return { ok: true, value: { settings: Object.fromEntries(Object.entries(next).sort(([a], [b]) => a.localeCompare(b))), mcp_servers: servers } };
214}
215
216/** The ability `id` names, among the catalog's integration abilities and the agent's MCP servers' tools; null when there is none. */
217function abilityDef(id: string, servers: McpServer[]) {
218 const parts = id.split(":");
219 if (parts[0] === "integration" && parts.length === 3) return integrationAbilities(parts[1]!).find((def) => def.id === id) ?? null;
220 if (parts[0] === "mcp" && parts.length >= 3) {
221 const server = servers.find((s) => s.id === parts[1]);
222 const tool = server?.tools.find((t) => `mcp:${server.id}:${t.name}` === id);
223 return server && tool ? mcpAbility(server, tool) : null;
224 }
225 return null;
226}
227
228/** The MCP servers as kept: at most `MAX_MCP_SERVERS`, names unique. Only the service's own MCP calls change them. */
229function mcpServersOf(given: unknown): Checked<McpServer[]> {
230 if (!Array.isArray(given)) return bad("MCP servers are a list.");
231 if (given.length > MAX_MCP_SERVERS) return bad(`An agent has at most ${MAX_MCP_SERVERS} MCP servers.`);
232 const names = new Set<string>();
233 for (const server of given as McpServer[]) {
234 if (!server || typeof server !== "object" || typeof server.id !== "string" || typeof server.name !== "string" || typeof server.url !== "string" || !Array.isArray(server.tools)) return bad("An MCP server has an id, a name, a url and tools.");
235 if (names.has(server.name)) return bad(`Two servers are called ${server.name}.`);
236 names.add(server.name);
237 }
238 return { ok: true, value: given as McpServer[] };
239}
240
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)241function responsibilitiesOf(given: unknown): Checked<string[]> {
242 if (!Array.isArray(given) || given.some((d) => typeof d !== "string")) return bad("Responsibilities are a list of short duties.");
243 const duties = [...new Set((given as string[]).map((d) => d.trim()).filter(Boolean))];
244 if (duties.length && (duties.length < 2 || duties.length > 8)) return bad("Give 2 to 8 responsibilities, or none yet.");
245 if (duties.some((d) => d.length > LIMITS.duty)) return bad(`Each responsibility is at most ${LIMITS.duty} characters.`);
246 return { ok: true, value: duties };
247}
248
249const ORDER = ["small", "large", "frontier"] as const;
250
251/** A subagent's limits held within its agent's: never a lower floor, never a higher ceiling. */
252export function withinParent(
253 sub: { floor: AgentRouting["floor"]; ceiling: AgentRouting["ceiling"] },
254 parent: Pick<AgentRouting, "floor" | "ceiling">,
255): { floor: AgentRouting["floor"]; ceiling: AgentRouting["ceiling"] } {
256 const at = (tier: AgentRouting["floor"]) => (tier ? ORDER.indexOf(tier) : -1);
257 let floor = sub.floor;
258 let ceiling = sub.ceiling;
259 if (parent.floor && at(floor) < at(parent.floor)) floor = parent.floor;
260 if (parent.ceiling && (!ceiling || at(ceiling) > at(parent.ceiling))) ceiling = parent.ceiling;
261 // Held inside, a floor can end above the ceiling: the ceiling, the spending rail, wins.
262 if (floor && ceiling && at(floor) > at(ceiling)) floor = ceiling;
263 return { floor, ceiling };
264}
265
266function subagentsOf(given: unknown): Checked<SubagentDef[]> {
267 if (!Array.isArray(given)) return bad("Subagents are a list.");
268 if (given.length > MAX_SUBAGENTS) return bad(`An agent keeps at most ${MAX_SUBAGENTS} subagents.`);
269 const out: SubagentDef[] = [];
270 for (const raw of given as Partial<SubagentDef>[]) {
271 if (!raw || typeof raw !== "object") return bad("Each subagent has a name, a description and instructions.");
272 const name = typeof raw.name === "string" ? raw.name.trim().toLowerCase() : "";
273 if (!/^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){1,31}$/.test(name)) return bad("A subagent's name is 2 to 32 lowercase letters, digits and single hyphens.");
274 if (out.some((sub) => sub.name === name)) return bad(`Two subagents are called ${name}.`);
275 const description = text(raw.description, `${name}'s description`, 200, true);
276 if (!description.ok) return description;
277 const instructions = text(raw.instructions, `${name}'s instructions`, 4000, true);
278 if (!instructions.ok) return instructions;
279 const routing = raw.routing ?? { floor: null, ceiling: null };
280 for (const tier of [routing.floor, routing.ceiling]) {
281 if (tier !== null && tier !== undefined && !isTier(tier)) return bad(`${name}'s limits are small, large, frontier or none.`);
282 }
283 if (!limitsAgree(routing.floor ?? null, routing.ceiling ?? null)) return bad(`${name}'s floor is above its ceiling.`);
284 const parallel = raw.max_parallel ?? 2;
285 if (typeof parallel !== "number" || !Number.isInteger(parallel) || parallel < 1 || parallel > 8) return bad(`${name} runs 1 to 8 at once.`);
286 out.push({
287 name,
288 description: description.value,
289 instructions: instructions.value,
290 routing: { floor: routing.floor ?? null, ceiling: routing.ceiling ?? null },
291 max_parallel: parallel,
292 });
293 }
294 return { ok: true, value: out };
295}
296
Chat and workspace agents: channels, DMs and named agents you talk to297/**
298 * `changes` applied to `base` (a new agent's defaults, or its current
299 * definition), checked. `templates` are the template ids an agent may name.
300 */
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)301export function applyChanges(
302 base: Definition | null,
303 changes: Partial<NewWorkspaceAgent>,
304 templates: string[],
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.305 options: { builtin?: boolean; mcp_servers?: McpServer[] } = {},
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)306): Checked<Definition> {
Chat and workspace agents: channels, DMs and named agents you talk to307 if (!changes || typeof changes !== "object") return bad("Send the agent's fields.");
308 const creating = base === null;
309 const from: Definition = base ?? {
310 handle: "",
311 display_name: "",
312 role: "",
313 instructions: "",
314 personality_preset: "crisp",
315 personality: "",
316 routing: DEFAULT_ROUTING,
317 budget: DEFAULT_BUDGET,
318 autonomy: DEFAULT_AUTONOMY,
319 capacity: DEFAULT_CAPACITY,
320 template: null,
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)321 avatar_seed: "",
322 title: "",
323 responsibilities: [],
324 subagents: [],
325 faces: "internal",
Agents recall what Docs say before they answer or work, and each has required reading326 reading: [],
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.327 skills_off: [],
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.328 abilities: EMPTY_ABILITIES,
Chat and workspace agents: channels, DMs and named agents you talk to329 };
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.330 const next: Definition = { ...from, skills_off: from.skills_off ?? [], abilities: from.abilities ?? EMPTY_ABILITIES };
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.331 // Whether the role was made from the title, so it follows it.
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)332 const roleDerived = !from.role || from.role === roleOf(from);
Chat and workspace agents: channels, DMs and named agents you talk to333 if (creating || changes.handle !== undefined) {
334 const handle = checkHandle(changes.handle);
335 if (!handle.ok) return bad(handle.message);
336 next.handle = handle.handle;
337 }
338 const fields = [
339 ["display_name", "A display name", LIMITS.displayName, true],
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)340 ["role", "The role", LIMITS.role, false],
341 ["title", "The title", LIMITS.title, false],
342 // The built-in agent's instructions are added to its fixed job, and may be empty.
343 ["instructions", "The instructions", LIMITS.instructions, !options.builtin],
Chat and workspace agents: channels, DMs and named agents you talk to344 ["personality", "The personality", LIMITS.personality, false],
345 ] as const;
346 for (const [key, what, max, required] of fields) {
347 if (!creating && changes[key] === undefined) continue;
348 const value = text(changes[key], what, max, required);
349 if (!value.ok) return value;
350 next[key] = value.value;
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)351 }
352 if (changes.responsibilities !== undefined) {
353 const duties = responsibilitiesOf(changes.responsibilities);
354 if (!duties.ok) return duties;
355 next.responsibilities = duties.value;
356 }
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.357 // A role left empty, or made from the title before, follows it.
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)358 if (!next.role || (changes.role === undefined && roleDerived)) next.role = roleOf(next);
359 if (!next.role) return bad("Give the agent a title or a one-line role.");
360 if (changes.avatar_seed !== undefined) {
361 const seed = text(changes.avatar_seed, "The avatar seed", 64, false);
362 if (!seed.ok) return seed;
363 next.avatar_seed = seed.value;
364 }
365 // A new face from the handle, unless one was chosen; renaming keeps the face.
366 if (!next.avatar_seed) next.avatar_seed = next.handle;
Chat and workspace agents: channels, DMs and named agents you talk to367 if (changes.personality_preset !== undefined) {
368 if (!PRESETS.includes(changes.personality_preset)) return bad(`The personality preset is one of ${PRESETS.join(", ")}.`);
369 next.personality_preset = changes.personality_preset;
370 }
371 const routing = routingOf(next.routing, changes.routing);
372 if (!routing.ok) return routing;
373 next.routing = routing.value;
374 const budget = budgetOf(next.budget, changes.budget);
375 if (!budget.ok) return budget;
376 next.budget = budget.value;
377 const autonomy = autonomyOf(next.autonomy, changes.autonomy);
378 if (!autonomy.ok) return autonomy;
379 next.autonomy = autonomy.value;
380 if (changes.capacity !== undefined) {
381 const capacity = changes.capacity;
382 if (typeof capacity !== "number" || !Number.isInteger(capacity) || capacity < 1 || capacity > MAX_CAPACITY) {
383 return bad(`Capacity is 1 to ${MAX_CAPACITY} tasks at once.`);
384 }
385 next.capacity = capacity;
386 }
387 if (changes.template !== undefined) {
388 if (changes.template !== null && !templates.includes(changes.template)) return bad("There is no such template.");
389 next.template = changes.template;
390 }
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)391 if (changes.subagents !== undefined) {
392 const subagents = subagentsOf(changes.subagents);
393 if (!subagents.ok) return subagents;
394 next.subagents = subagents.value;
395 }
Agents recall what Docs say before they answer or work, and each has required reading396 if (changes.reading !== undefined) {
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.397 if (!Array.isArray(changes.reading)) return bad("Required reading is a list of spaces.");
Agents recall what Docs say before they answer or work, and each has required reading398 const ids = [...new Set(changes.reading.filter((id): id is string => typeof id === "string").map((id) => id.trim()).filter(Boolean))];
399 if (ids.length > 10) return bad("An agent has at most 10 spaces of required reading.");
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.400 if (ids.some((id) => !/^[A-Za-z0-9_-]{1,80}$/.test(id))) return bad("That isn't a space.");
Agents recall what Docs say before they answer or work, and each has required reading401 next.reading = ids;
402 }
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.403 if (changes.skills_off !== undefined) {
404 if (!Array.isArray(changes.skills_off)) return bad("Skills turned off are a list of skills.");
405 const ids = new Set(changes.skills_off.filter((id): id is string => typeof id === "string").map((id) => id.trim()).filter(Boolean));
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.406 // Foundational skills by id, and the library's by theirs (skl_…), which
407 // may be off before or after they are attached.
408 const library = [...ids].filter((id) => LIBRARY_SKILL_ID.test(id));
409 const unknown = [...ids].find((id) => !FOUNDATIONAL_SKILL_IDS.includes(id) && !LIBRARY_SKILL_ID.test(id));
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.410 if (unknown) return bad(`There is no skill called ${unknown.slice(0, 40)}.`);
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.411 if (library.length > 200) return bad("At most 200 library skills can be off for one agent.");
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.412 // In the skills' own order, so the same choice always reads the same.
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.413 next.skills_off = [...FOUNDATIONAL_SKILL_IDS.filter((id) => ids.has(id)), ...library.sort()];
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.414 }
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.415 // MCP servers change only through the service's own calls (index.ts), which pass them here; a change never carries them.
416 let servers = next.abilities.mcp_servers ?? [];
417 if (options.mcp_servers) {
418 const checked = mcpServersOf(options.mcp_servers);
419 if (!checked.ok) return checked;
420 servers = checked.value;
421 }
422 const abilities = abilitiesOf(next.abilities, changes.abilities, servers);
423 if (!abilities.ok) return abilities;
424 // A setting for a tool of a server that is gone goes with it.
425 abilities.value.settings = Object.fromEntries(Object.entries(abilities.value.settings).filter(([id]) => abilityDef(id, servers)));
426 next.abilities = abilities.value;
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)427 if (changes.faces !== undefined) {
428 if (changes.faces === "customers") return bad("Customer-facing agents aren't available yet.");
429 if (changes.faces !== "internal") return bad("An agent faces internal: the workspace's own people.");
430 next.faces = "internal";
431 }
432 // Never wider than their agent, whichever of the two changed.
433 next.subagents = next.subagents.map((sub) => ({ ...sub, routing: withinParent(sub.routing, next.routing) }));
Chat and workspace agents: channels, DMs and named agents you talk to434 return { ok: true, value: next };
435}
436
437/** A stored JSON column, read defensively: a bad value is the default. */
438export function readJson<T extends object>(raw: unknown, fallback: T): T {
439 if (typeof raw !== "string") return fallback;
440 try {
441 const parsed = JSON.parse(raw) as unknown;
442 return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { ...fallback, ...(parsed as T) } : fallback;
443 } catch {
444 return fallback;
445 }
446}

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