Skip to content
460 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,
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.11 AgentLook,
Chat and workspace agents: channels, DMs and named agents you talk to12 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.13 McpServer,
Chat and workspace agents: channels, DMs and named agents you talk to14 NewWorkspaceAgent,
15 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)16 AgentFaces,
17 SubagentDef,
Chat and workspace agents: channels, DMs and named agents you talk to18} 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.19import { ABILITY_LEVELS, EMPTY_ABILITIES, MAX_MCP_SERVERS, integrationAbilities, maxLevel, mcpAbility, withinLevel } from "../../../packages/contracts/src/abilities.ts";
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.20import { readLook } from "../../../packages/contracts/src/agent-look.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.21import { FOUNDATIONAL_SKILL_IDS } from "../../../packages/contracts/src/skills.ts";
Chat and workspace agents: channels, DMs and named agents you talk to22
23import { 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.24import { isEffort, isTier, limitsAgree } from "./routing.ts";
Chat and workspace agents: channels, DMs and named agents you talk to25
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.26/** A library skill's id (skill-library.ts), as `skills_off` may name it. */
27const LIBRARY_SKILL_ID = /^skl_[0-9a-z]{26}$/;
28
Chat and workspace agents: channels, DMs and named agents you talk to29export const PRESETS: PersonalityPreset[] = ["crisp", "friendly", "socratic", "terse"];
30
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.31export const DEFAULT_ROUTING: AgentRouting = { floor: null, ceiling: null, providers: [], pinned: null, effort: "auto" };
Chat and workspace agents: channels, DMs and named agents you talk to32export const DEFAULT_BUDGET: AgentBudget = { monthly_micros: null, daily_micros: null, task_micros: null };
33/** What an agent may do alone until someone says otherwise: open pull requests; the rest asks. */
34export const DEFAULT_AUTONOMY: AgentAutonomy = {
35 open_pull_requests: "alone",
36 merge: "approval",
37 deploy_production: "approval",
38 edit_docs: "suggest",
39};
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.40/** Tasks at once. */
Chat and workspace agents: channels, DMs and named agents you talk to41export const DEFAULT_CAPACITY = 3;
42export const MAX_CAPACITY = 10;
43
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.44const 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 to45/** $100,000 in millionths: a cap above this is a typo. */
46const MAX_MICROS = 100_000_000_000;
47
48/** Everything a definition holds, complete: what is stored and versioned. */
49export type Definition = {
50 handle: string;
51 display_name: string;
52 role: string;
53 instructions: string;
54 personality_preset: PersonalityPreset;
55 personality: string;
56 routing: AgentRouting;
57 budget: AgentBudget;
58 autonomy: AgentAutonomy;
59 capacity: number;
60 template: string | null;
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.61 /** What its generated face is drawn from, when it has chosen none. */
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)62 avatar_seed: string;
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.63 /** Its face as chosen (@g1t/contracts agent-look.ts), or null for the seed's. */
64 look: AgentLook | 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)65 title: string;
66 responsibilities: string[];
67 subagents: SubagentDef[];
68 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.69 /** Spaces whose artifacts it reads first. */
Agents recall what Docs say before they answer or work, and each has required reading70 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.71 /** Foundational skills turned off for it, by id (@g1t/contracts skills.ts). */
72 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.73 /** Its abilities' levels and credentials, and its MCP servers (@g1t/contracts abilities.ts). */
74 abilities: AgentAbilities;
Chat and workspace agents: channels, DMs and named agents you talk to75};
76
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.77/**
78 * The one-line role its title makes: "QA Engineer". Which teams it is on
79 * are team memberships (identity), never part of the agent.
80 */
81export function roleOf(d: Pick<Definition, "title">): string {
82 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)83}
84
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.85/**
86 * The role an agent was given when it carried its own team or department,
87 * made from them: "QA Engineer on the qa team", "QA Engineer, QA". A
88 * stored role that is exactly this follows the title (store.ts).
89 */
90export function legacyRoleOf(title: string, team: string | null, department: string | null): string {
91 const t = title.trim();
92 if (!t) return "";
93 if (team) return `${t} on the ${team} team`;
94 return department?.trim() ? `${t}, ${department.trim()}` : t;
95}
96
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)97export const MAX_SUBAGENTS = 8;
98
Chat and workspace agents: channels, DMs and named agents you talk to99export type Checked<T> = { ok: true; value: T } | { ok: false; message: string };
100
101const bad = (message: string): { ok: false; message: string } => ({ ok: false, message });
102
103function text(value: unknown, what: string, max: number, required: boolean): Checked<string> {
104 if (value === undefined || value === null) return required ? bad(`${what} is required.`) : { ok: true, value: "" };
105 if (typeof value !== "string") return bad(`${what} is text.`);
106 const trimmed = value.trim();
107 if (required && !trimmed) return bad(`${what} is required.`);
108 if (trimmed.length > max) return bad(`${what} is at most ${max} characters.`);
109 return { ok: true, value: trimmed };
110}
111
112function routingOf(base: AgentRouting, given: unknown): Checked<AgentRouting> {
113 if (given === undefined || given === null) return { ok: true, value: base };
114 if (typeof given !== "object") return bad("Routing is an object.");
115 const g = given as Partial<AgentRouting>;
116 const next: AgentRouting = { ...base };
117 for (const key of ["floor", "ceiling"] as const) {
118 if (g[key] === undefined) continue;
119 if (g[key] !== null && !isTier(g[key])) return bad(`The ${key} is small, large, frontier or none.`);
120 next[key] = g[key] ?? null;
121 }
122 if (!limitsAgree(next.floor, next.ceiling)) return bad("The floor is above the ceiling: lower the floor or raise the ceiling.");
123 if (g.providers !== undefined) {
124 if (!Array.isArray(g.providers) || g.providers.some((p) => typeof p !== "string" || !p.trim())) return bad("Providers is a list of names.");
125 const providers = [...new Set(g.providers.map((p) => p.trim()))];
126 if (providers.length > LIMITS.providers) return bad(`An agent names at most ${LIMITS.providers} providers.`);
127 next.providers = providers;
128 }
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.129 if (g.effort !== undefined) {
130 if (g.effort !== null && !isEffort(g.effort)) return bad("Effort is auto, low, medium, high or max.");
131 next.effort = g.effort ?? "auto";
132 }
Chat and workspace agents: channels, DMs and named agents you talk to133 if (g.pinned !== undefined) {
134 if (g.pinned === null || g.pinned === "") next.pinned = null;
135 else if (typeof g.pinned !== "string" || g.pinned.trim().length > LIMITS.pinned || !/^[^/\s]+\/\S+$/.test(g.pinned.trim())) {
136 return bad("A pinned model is written provider/model.");
137 } else next.pinned = g.pinned.trim();
138 }
139 return { ok: true, value: next };
140}
141
142function budgetOf(base: AgentBudget, given: unknown): Checked<AgentBudget> {
143 if (given === undefined || given === null) return { ok: true, value: base };
144 if (typeof given !== "object") return bad("Budget is an object.");
145 const g = given as Partial<AgentBudget>;
146 const next: AgentBudget = { ...base };
147 for (const key of ["monthly_micros", "daily_micros", "task_micros"] as const) {
148 const value = g[key];
149 if (value === undefined) continue;
150 if (value === null) next[key] = null;
151 else if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > MAX_MICROS) {
152 return bad("A budget is a whole number of millionths of a dollar, or none.");
153 } else next[key] = value;
154 }
155 return { ok: true, value: next };
156}
157
158const AUTONOMY_CHOICES: { [K in keyof AgentAutonomy]: AgentAutonomy[K][] } = {
159 open_pull_requests: ["alone", "approval"],
160 merge: ["alone", "approval", "never"],
161 deploy_production: ["approval", "never"],
162 edit_docs: ["alone", "suggest"],
163};
164
165function autonomyOf(base: AgentAutonomy, given: unknown): Checked<AgentAutonomy> {
166 if (given === undefined || given === null) return { ok: true, value: base };
167 if (typeof given !== "object") return bad("Autonomy is an object.");
168 const next = { ...base } as Record<string, string>;
169 for (const [key, choices] of Object.entries(AUTONOMY_CHOICES) as [string, string[]][]) {
170 const value = (given as Record<string, unknown>)[key];
171 if (value === undefined) continue;
172 if (typeof value !== "string" || !choices.includes(value)) return bad(`${key} is one of ${choices.join(", ")}.`);
173 next[key] = value;
174 }
175 return { ok: true, value: next as AgentAutonomy };
176}
177
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.178/** The most abilities with a choice of their own one agent keeps. */
179const MAX_ABILITY_SETTINGS = 400;
180
181/**
182 * The abilities' settings, checked against what the agent can have: an
183 * integration ability the catalog knows, or a tool of one of its MCP
184 * servers, each at a level its kind allows. g1t's own abilities keep their
185 * choices in `autonomy`, so nothing is kept for them here.
186 */
187function abilitiesOf(base: AgentAbilities, given: unknown, servers: McpServer[]): Checked<AgentAbilities> {
188 if (given === undefined || given === null) return { ok: true, value: { ...base, mcp_servers: servers } };
189 if (typeof given !== "object" || Array.isArray(given)) return bad("Abilities are an object.");
190 const settings = (given as { settings?: unknown }).settings;
191 if (settings === undefined) return { ok: true, value: { ...base, mcp_servers: servers } };
192 if (!settings || typeof settings !== "object" || Array.isArray(settings)) return bad("Abilities' settings are an object by ability id.");
193 const entries = Object.entries(settings as Record<string, unknown>);
194 if (entries.length > MAX_ABILITY_SETTINGS) return bad(`At most ${MAX_ABILITY_SETTINGS} abilities keep a setting.`);
195 const next: Record<string, AbilitySetting> = {};
196 for (const [id, raw] of entries) {
197 const def = abilityDef(id, servers);
198 if (!def) return bad(`There is no ability called ${id.slice(0, 60)} for this agent.`);
199 if (!raw || typeof raw !== "object") return bad(`${id}'s setting is an object.`);
200 const setting = raw as Record<string, unknown>;
201 const kept: AbilitySetting = {};
202 if (setting.level !== undefined && setting.level !== null) {
203 if (typeof setting.level !== "string" || !ABILITY_LEVELS.includes(setting.level as AbilityLevel)) return bad(`${id}'s level is alone, asked, ask or never.`);
204 const level = setting.level as AbilityLevel;
205 const max = maxLevel(def.kind);
206 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"}.`);
207 kept.level = level;
208 }
209 if (setting.credentials !== undefined && setting.credentials !== null) {
210 if (def.group !== "integration") return bad(`Only an integration's abilities say whose connection they run on.`);
211 if (setting.credentials !== "workspace" && setting.credentials !== "asker") return bad(`${id}'s credentials are workspace or asker.`);
212 kept.credentials = setting.credentials;
213 }
214 if (Object.keys(kept).length) next[id] = kept;
215 }
216 // In id order, so the same choices always read the same.
217 return { ok: true, value: { settings: Object.fromEntries(Object.entries(next).sort(([a], [b]) => a.localeCompare(b))), mcp_servers: servers } };
218}
219
220/** The ability `id` names, among the catalog's integration abilities and the agent's MCP servers' tools; null when there is none. */
221function abilityDef(id: string, servers: McpServer[]) {
222 const parts = id.split(":");
223 if (parts[0] === "integration" && parts.length === 3) return integrationAbilities(parts[1]!).find((def) => def.id === id) ?? null;
224 if (parts[0] === "mcp" && parts.length >= 3) {
225 const server = servers.find((s) => s.id === parts[1]);
226 const tool = server?.tools.find((t) => `mcp:${server.id}:${t.name}` === id);
227 return server && tool ? mcpAbility(server, tool) : null;
228 }
229 return null;
230}
231
232/** The MCP servers as kept: at most `MAX_MCP_SERVERS`, names unique. Only the service's own MCP calls change them. */
233function mcpServersOf(given: unknown): Checked<McpServer[]> {
234 if (!Array.isArray(given)) return bad("MCP servers are a list.");
235 if (given.length > MAX_MCP_SERVERS) return bad(`An agent has at most ${MAX_MCP_SERVERS} MCP servers.`);
236 const names = new Set<string>();
237 for (const server of given as McpServer[]) {
238 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.");
239 if (names.has(server.name)) return bad(`Two servers are called ${server.name}.`);
240 names.add(server.name);
241 }
242 return { ok: true, value: given as McpServer[] };
243}
244
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)245function responsibilitiesOf(given: unknown): Checked<string[]> {
246 if (!Array.isArray(given) || given.some((d) => typeof d !== "string")) return bad("Responsibilities are a list of short duties.");
247 const duties = [...new Set((given as string[]).map((d) => d.trim()).filter(Boolean))];
248 if (duties.length && (duties.length < 2 || duties.length > 8)) return bad("Give 2 to 8 responsibilities, or none yet.");
249 if (duties.some((d) => d.length > LIMITS.duty)) return bad(`Each responsibility is at most ${LIMITS.duty} characters.`);
250 return { ok: true, value: duties };
251}
252
253const ORDER = ["small", "large", "frontier"] as const;
254
255/** A subagent's limits held within its agent's: never a lower floor, never a higher ceiling. */
256export function withinParent(
257 sub: { floor: AgentRouting["floor"]; ceiling: AgentRouting["ceiling"] },
258 parent: Pick<AgentRouting, "floor" | "ceiling">,
259): { floor: AgentRouting["floor"]; ceiling: AgentRouting["ceiling"] } {
260 const at = (tier: AgentRouting["floor"]) => (tier ? ORDER.indexOf(tier) : -1);
261 let floor = sub.floor;
262 let ceiling = sub.ceiling;
263 if (parent.floor && at(floor) < at(parent.floor)) floor = parent.floor;
264 if (parent.ceiling && (!ceiling || at(ceiling) > at(parent.ceiling))) ceiling = parent.ceiling;
265 // Held inside, a floor can end above the ceiling: the ceiling, the spending rail, wins.
266 if (floor && ceiling && at(floor) > at(ceiling)) floor = ceiling;
267 return { floor, ceiling };
268}
269
270function subagentsOf(given: unknown): Checked<SubagentDef[]> {
271 if (!Array.isArray(given)) return bad("Subagents are a list.");
272 if (given.length > MAX_SUBAGENTS) return bad(`An agent keeps at most ${MAX_SUBAGENTS} subagents.`);
273 const out: SubagentDef[] = [];
274 for (const raw of given as Partial<SubagentDef>[]) {
275 if (!raw || typeof raw !== "object") return bad("Each subagent has a name, a description and instructions.");
276 const name = typeof raw.name === "string" ? raw.name.trim().toLowerCase() : "";
277 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.");
278 if (out.some((sub) => sub.name === name)) return bad(`Two subagents are called ${name}.`);
279 const description = text(raw.description, `${name}'s description`, 200, true);
280 if (!description.ok) return description;
281 const instructions = text(raw.instructions, `${name}'s instructions`, 4000, true);
282 if (!instructions.ok) return instructions;
283 const routing = raw.routing ?? { floor: null, ceiling: null };
284 for (const tier of [routing.floor, routing.ceiling]) {
285 if (tier !== null && tier !== undefined && !isTier(tier)) return bad(`${name}'s limits are small, large, frontier or none.`);
286 }
287 if (!limitsAgree(routing.floor ?? null, routing.ceiling ?? null)) return bad(`${name}'s floor is above its ceiling.`);
288 const parallel = raw.max_parallel ?? 2;
289 if (typeof parallel !== "number" || !Number.isInteger(parallel) || parallel < 1 || parallel > 8) return bad(`${name} runs 1 to 8 at once.`);
290 out.push({
291 name,
292 description: description.value,
293 instructions: instructions.value,
294 routing: { floor: routing.floor ?? null, ceiling: routing.ceiling ?? null },
295 max_parallel: parallel,
296 });
297 }
298 return { ok: true, value: out };
299}
300
Chat and workspace agents: channels, DMs and named agents you talk to301/**
302 * `changes` applied to `base` (a new agent's defaults, or its current
303 * definition), checked. `templates` are the template ids an agent may name.
304 */
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)305export function applyChanges(
306 base: Definition | null,
307 changes: Partial<NewWorkspaceAgent>,
308 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.309 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)310): Checked<Definition> {
Chat and workspace agents: channels, DMs and named agents you talk to311 if (!changes || typeof changes !== "object") return bad("Send the agent's fields.");
312 const creating = base === null;
313 const from: Definition = base ?? {
314 handle: "",
315 display_name: "",
316 role: "",
317 instructions: "",
318 personality_preset: "crisp",
319 personality: "",
320 routing: DEFAULT_ROUTING,
321 budget: DEFAULT_BUDGET,
322 autonomy: DEFAULT_AUTONOMY,
323 capacity: DEFAULT_CAPACITY,
324 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)325 avatar_seed: "",
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.326 look: 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)327 title: "",
328 responsibilities: [],
329 subagents: [],
330 faces: "internal",
Agents recall what Docs say before they answer or work, and each has required reading331 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.332 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.333 abilities: EMPTY_ABILITIES,
Chat and workspace agents: channels, DMs and named agents you talk to334 };
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.335 const next: Definition = { ...from, look: from.look ?? null, 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.336 // 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)337 const roleDerived = !from.role || from.role === roleOf(from);
Chat and workspace agents: channels, DMs and named agents you talk to338 if (creating || changes.handle !== undefined) {
339 const handle = checkHandle(changes.handle);
340 if (!handle.ok) return bad(handle.message);
341 next.handle = handle.handle;
342 }
343 const fields = [
344 ["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)345 ["role", "The role", LIMITS.role, false],
346 ["title", "The title", LIMITS.title, false],
347 // The built-in agent's instructions are added to its fixed job, and may be empty.
348 ["instructions", "The instructions", LIMITS.instructions, !options.builtin],
Chat and workspace agents: channels, DMs and named agents you talk to349 ["personality", "The personality", LIMITS.personality, false],
350 ] as const;
351 for (const [key, what, max, required] of fields) {
352 if (!creating && changes[key] === undefined) continue;
353 const value = text(changes[key], what, max, required);
354 if (!value.ok) return value;
355 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)356 }
357 if (changes.responsibilities !== undefined) {
358 const duties = responsibilitiesOf(changes.responsibilities);
359 if (!duties.ok) return duties;
360 next.responsibilities = duties.value;
361 }
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.362 // 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)363 if (!next.role || (changes.role === undefined && roleDerived)) next.role = roleOf(next);
364 if (!next.role) return bad("Give the agent a title or a one-line role.");
365 if (changes.avatar_seed !== undefined) {
366 const seed = text(changes.avatar_seed, "The avatar seed", 64, false);
367 if (!seed.ok) return seed;
368 next.avatar_seed = seed.value;
369 }
370 // A new face from the handle, unless one was chosen; renaming keeps the face.
371 if (!next.avatar_seed) next.avatar_seed = next.handle;
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.372 // A chosen face, part by part; null goes back to the seed's.
373 if (changes.look !== undefined) {
374 if (changes.look === null) next.look = null;
375 else {
376 const look = readLook(changes.look);
377 if (!look) return bad("A look names each part of the face (shape, color, eyes, mouth, antenna, accessory, pattern) with one of its choices.");
378 next.look = look;
379 }
380 }
Chat and workspace agents: channels, DMs and named agents you talk to381 if (changes.personality_preset !== undefined) {
382 if (!PRESETS.includes(changes.personality_preset)) return bad(`The personality preset is one of ${PRESETS.join(", ")}.`);
383 next.personality_preset = changes.personality_preset;
384 }
385 const routing = routingOf(next.routing, changes.routing);
386 if (!routing.ok) return routing;
387 next.routing = routing.value;
388 const budget = budgetOf(next.budget, changes.budget);
389 if (!budget.ok) return budget;
390 next.budget = budget.value;
391 const autonomy = autonomyOf(next.autonomy, changes.autonomy);
392 if (!autonomy.ok) return autonomy;
393 next.autonomy = autonomy.value;
394 if (changes.capacity !== undefined) {
395 const capacity = changes.capacity;
396 if (typeof capacity !== "number" || !Number.isInteger(capacity) || capacity < 1 || capacity > MAX_CAPACITY) {
397 return bad(`Capacity is 1 to ${MAX_CAPACITY} tasks at once.`);
398 }
399 next.capacity = capacity;
400 }
401 if (changes.template !== undefined) {
402 if (changes.template !== null && !templates.includes(changes.template)) return bad("There is no such template.");
403 next.template = changes.template;
404 }
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)405 if (changes.subagents !== undefined) {
406 const subagents = subagentsOf(changes.subagents);
407 if (!subagents.ok) return subagents;
408 next.subagents = subagents.value;
409 }
Agents recall what Docs say before they answer or work, and each has required reading410 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.411 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 reading412 const ids = [...new Set(changes.reading.filter((id): id is string => typeof id === "string").map((id) => id.trim()).filter(Boolean))];
413 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.414 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 reading415 next.reading = ids;
416 }
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.417 if (changes.skills_off !== undefined) {
418 if (!Array.isArray(changes.skills_off)) return bad("Skills turned off are a list of skills.");
419 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.420 // Foundational skills by id, and the library's by theirs (skl_…), which
421 // may be off before or after they are attached.
422 const library = [...ids].filter((id) => LIBRARY_SKILL_ID.test(id));
423 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.424 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.425 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.426 // 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.427 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.428 }
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.429 // MCP servers change only through the service's own calls (index.ts), which pass them here; a change never carries them.
430 let servers = next.abilities.mcp_servers ?? [];
431 if (options.mcp_servers) {
432 const checked = mcpServersOf(options.mcp_servers);
433 if (!checked.ok) return checked;
434 servers = checked.value;
435 }
436 const abilities = abilitiesOf(next.abilities, changes.abilities, servers);
437 if (!abilities.ok) return abilities;
438 // A setting for a tool of a server that is gone goes with it.
439 abilities.value.settings = Object.fromEntries(Object.entries(abilities.value.settings).filter(([id]) => abilityDef(id, servers)));
440 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)441 if (changes.faces !== undefined) {
442 if (changes.faces === "customers") return bad("Customer-facing agents aren't available yet.");
443 if (changes.faces !== "internal") return bad("An agent faces internal: the workspace's own people.");
444 next.faces = "internal";
445 }
446 // Never wider than their agent, whichever of the two changed.
447 next.subagents = next.subagents.map((sub) => ({ ...sub, routing: withinParent(sub.routing, next.routing) }));
Chat and workspace agents: channels, DMs and named agents you talk to448 return { ok: true, value: next };
449}
450
451/** A stored JSON column, read defensively: a bad value is the default. */
452export function readJson<T extends object>(raw: unknown, fallback: T): T {
453 if (typeof raw !== "string") return fallback;
454 try {
455 const parsed = JSON.parse(raw) as unknown;
456 return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { ...fallback, ...(parsed as T) } : fallback;
457 } catch {
458 return fallback;
459 }
460}

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