Skip to content
347 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 {
6 AgentAutonomy,
7 AgentBudget,
8 AgentRouting,
9 NewWorkspaceAgent,
10 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)11 AgentFaces,
12 SubagentDef,
Chat and workspace agents: channels, DMs and named agents you talk to13} from "@g1t/contracts";
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.14import { FOUNDATIONAL_SKILL_IDS } from "../../../packages/contracts/src/skills.ts";
Chat and workspace agents: channels, DMs and named agents you talk to15
16import { checkHandle } from "./handle.ts";
17import { isTier, limitsAgree } from "./routing.ts";
18
19export const PRESETS: PersonalityPreset[] = ["crisp", "friendly", "socratic", "terse"];
20
21export const DEFAULT_ROUTING: AgentRouting = { floor: null, ceiling: null, providers: [], pinned: null };
22export const DEFAULT_BUDGET: AgentBudget = { monthly_micros: null, daily_micros: null, task_micros: null };
23/** What an agent may do alone until someone says otherwise: open pull requests; the rest asks. */
24export const DEFAULT_AUTONOMY: AgentAutonomy = {
25 open_pull_requests: "alone",
26 merge: "approval",
27 deploy_production: "approval",
28 edit_docs: "suggest",
29};
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.30/** Tasks at once. */
Chat and workspace agents: channels, DMs and named agents you talk to31export const DEFAULT_CAPACITY = 3;
32export const MAX_CAPACITY = 10;
33
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)34const LIMITS = { displayName: 64, role: 120, title: 60, department: 40, duty: 160, instructions: 8000, personality: 1000, providers: 10, pinned: 200 };
Chat and workspace agents: channels, DMs and named agents you talk to35/** $100,000 in millionths: a cap above this is a typo. */
36const MAX_MICROS = 100_000_000_000;
37
38/** Everything a definition holds, complete: what is stored and versioned. */
39export type Definition = {
40 handle: string;
41 display_name: string;
42 role: string;
43 instructions: string;
44 personality_preset: PersonalityPreset;
45 personality: string;
46 routing: AgentRouting;
47 budget: AgentBudget;
48 autonomy: AgentAutonomy;
49 capacity: number;
50 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)51 /** What its generated avatar is drawn from. */
52 avatar_seed: string;
53 title: string;
54 team: string | null;
55 department: string;
56 responsibilities: string[];
57 subagents: SubagentDef[];
58 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.59 /** Spaces whose artifacts it reads first. */
Agents recall what Docs say before they answer or work, and each has required reading60 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.61 /** Foundational skills turned off for it, by id (@g1t/contracts skills.ts). */
62 skills_off: string[];
Chat and workspace agents: channels, DMs and named agents you talk to63};
64
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/** The one-line role a title and team (or department) make: "QA Engineer on the qa team". */
66export function roleOf(d: Pick<Definition, "title" | "team" | "department">): string {
67 const title = d.title.trim();
68 if (!title) return "";
69 if (d.team) return `${title} on the ${d.team} team`;
70 return d.department.trim() ? `${title}, ${d.department.trim()}` : title;
71}
72
73export const MAX_SUBAGENTS = 8;
74
Chat and workspace agents: channels, DMs and named agents you talk to75export type Checked<T> = { ok: true; value: T } | { ok: false; message: string };
76
77const bad = (message: string): { ok: false; message: string } => ({ ok: false, message });
78
79function text(value: unknown, what: string, max: number, required: boolean): Checked<string> {
80 if (value === undefined || value === null) return required ? bad(`${what} is required.`) : { ok: true, value: "" };
81 if (typeof value !== "string") return bad(`${what} is text.`);
82 const trimmed = value.trim();
83 if (required && !trimmed) return bad(`${what} is required.`);
84 if (trimmed.length > max) return bad(`${what} is at most ${max} characters.`);
85 return { ok: true, value: trimmed };
86}
87
88function routingOf(base: AgentRouting, given: unknown): Checked<AgentRouting> {
89 if (given === undefined || given === null) return { ok: true, value: base };
90 if (typeof given !== "object") return bad("Routing is an object.");
91 const g = given as Partial<AgentRouting>;
92 const next: AgentRouting = { ...base };
93 for (const key of ["floor", "ceiling"] as const) {
94 if (g[key] === undefined) continue;
95 if (g[key] !== null && !isTier(g[key])) return bad(`The ${key} is small, large, frontier or none.`);
96 next[key] = g[key] ?? null;
97 }
98 if (!limitsAgree(next.floor, next.ceiling)) return bad("The floor is above the ceiling: lower the floor or raise the ceiling.");
99 if (g.providers !== undefined) {
100 if (!Array.isArray(g.providers) || g.providers.some((p) => typeof p !== "string" || !p.trim())) return bad("Providers is a list of names.");
101 const providers = [...new Set(g.providers.map((p) => p.trim()))];
102 if (providers.length > LIMITS.providers) return bad(`An agent names at most ${LIMITS.providers} providers.`);
103 next.providers = providers;
104 }
105 if (g.pinned !== undefined) {
106 if (g.pinned === null || g.pinned === "") next.pinned = null;
107 else if (typeof g.pinned !== "string" || g.pinned.trim().length > LIMITS.pinned || !/^[^/\s]+\/\S+$/.test(g.pinned.trim())) {
108 return bad("A pinned model is written provider/model.");
109 } else next.pinned = g.pinned.trim();
110 }
111 return { ok: true, value: next };
112}
113
114function budgetOf(base: AgentBudget, given: unknown): Checked<AgentBudget> {
115 if (given === undefined || given === null) return { ok: true, value: base };
116 if (typeof given !== "object") return bad("Budget is an object.");
117 const g = given as Partial<AgentBudget>;
118 const next: AgentBudget = { ...base };
119 for (const key of ["monthly_micros", "daily_micros", "task_micros"] as const) {
120 const value = g[key];
121 if (value === undefined) continue;
122 if (value === null) next[key] = null;
123 else if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > MAX_MICROS) {
124 return bad("A budget is a whole number of millionths of a dollar, or none.");
125 } else next[key] = value;
126 }
127 return { ok: true, value: next };
128}
129
130const AUTONOMY_CHOICES: { [K in keyof AgentAutonomy]: AgentAutonomy[K][] } = {
131 open_pull_requests: ["alone", "approval"],
132 merge: ["alone", "approval", "never"],
133 deploy_production: ["approval", "never"],
134 edit_docs: ["alone", "suggest"],
135};
136
137function autonomyOf(base: AgentAutonomy, given: unknown): Checked<AgentAutonomy> {
138 if (given === undefined || given === null) return { ok: true, value: base };
139 if (typeof given !== "object") return bad("Autonomy is an object.");
140 const next = { ...base } as Record<string, string>;
141 for (const [key, choices] of Object.entries(AUTONOMY_CHOICES) as [string, string[]][]) {
142 const value = (given as Record<string, unknown>)[key];
143 if (value === undefined) continue;
144 if (typeof value !== "string" || !choices.includes(value)) return bad(`${key} is one of ${choices.join(", ")}.`);
145 next[key] = value;
146 }
147 return { ok: true, value: next as AgentAutonomy };
148}
149
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)150function responsibilitiesOf(given: unknown): Checked<string[]> {
151 if (!Array.isArray(given) || given.some((d) => typeof d !== "string")) return bad("Responsibilities are a list of short duties.");
152 const duties = [...new Set((given as string[]).map((d) => d.trim()).filter(Boolean))];
153 if (duties.length && (duties.length < 2 || duties.length > 8)) return bad("Give 2 to 8 responsibilities, or none yet.");
154 if (duties.some((d) => d.length > LIMITS.duty)) return bad(`Each responsibility is at most ${LIMITS.duty} characters.`);
155 return { ok: true, value: duties };
156}
157
158const ORDER = ["small", "large", "frontier"] as const;
159
160/** A subagent's limits held within its agent's: never a lower floor, never a higher ceiling. */
161export function withinParent(
162 sub: { floor: AgentRouting["floor"]; ceiling: AgentRouting["ceiling"] },
163 parent: Pick<AgentRouting, "floor" | "ceiling">,
164): { floor: AgentRouting["floor"]; ceiling: AgentRouting["ceiling"] } {
165 const at = (tier: AgentRouting["floor"]) => (tier ? ORDER.indexOf(tier) : -1);
166 let floor = sub.floor;
167 let ceiling = sub.ceiling;
168 if (parent.floor && at(floor) < at(parent.floor)) floor = parent.floor;
169 if (parent.ceiling && (!ceiling || at(ceiling) > at(parent.ceiling))) ceiling = parent.ceiling;
170 // Held inside, a floor can end above the ceiling: the ceiling, the spending rail, wins.
171 if (floor && ceiling && at(floor) > at(ceiling)) floor = ceiling;
172 return { floor, ceiling };
173}
174
175function subagentsOf(given: unknown): Checked<SubagentDef[]> {
176 if (!Array.isArray(given)) return bad("Subagents are a list.");
177 if (given.length > MAX_SUBAGENTS) return bad(`An agent keeps at most ${MAX_SUBAGENTS} subagents.`);
178 const out: SubagentDef[] = [];
179 for (const raw of given as Partial<SubagentDef>[]) {
180 if (!raw || typeof raw !== "object") return bad("Each subagent has a name, a description and instructions.");
181 const name = typeof raw.name === "string" ? raw.name.trim().toLowerCase() : "";
182 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.");
183 if (out.some((sub) => sub.name === name)) return bad(`Two subagents are called ${name}.`);
184 const description = text(raw.description, `${name}'s description`, 200, true);
185 if (!description.ok) return description;
186 const instructions = text(raw.instructions, `${name}'s instructions`, 4000, true);
187 if (!instructions.ok) return instructions;
188 const routing = raw.routing ?? { floor: null, ceiling: null };
189 for (const tier of [routing.floor, routing.ceiling]) {
190 if (tier !== null && tier !== undefined && !isTier(tier)) return bad(`${name}'s limits are small, large, frontier or none.`);
191 }
192 if (!limitsAgree(routing.floor ?? null, routing.ceiling ?? null)) return bad(`${name}'s floor is above its ceiling.`);
193 const parallel = raw.max_parallel ?? 2;
194 if (typeof parallel !== "number" || !Number.isInteger(parallel) || parallel < 1 || parallel > 8) return bad(`${name} runs 1 to 8 at once.`);
195 out.push({
196 name,
197 description: description.value,
198 instructions: instructions.value,
199 routing: { floor: routing.floor ?? null, ceiling: routing.ceiling ?? null },
200 max_parallel: parallel,
201 });
202 }
203 return { ok: true, value: out };
204}
205
Chat and workspace agents: channels, DMs and named agents you talk to206/**
207 * `changes` applied to `base` (a new agent's defaults, or its current
208 * definition), checked. `templates` are the template ids an agent may name.
209 */
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)210export function applyChanges(
211 base: Definition | null,
212 changes: Partial<NewWorkspaceAgent>,
213 templates: string[],
214 options: { builtin?: boolean } = {},
215): Checked<Definition> {
Chat and workspace agents: channels, DMs and named agents you talk to216 if (!changes || typeof changes !== "object") return bad("Send the agent's fields.");
217 const creating = base === null;
218 const from: Definition = base ?? {
219 handle: "",
220 display_name: "",
221 role: "",
222 instructions: "",
223 personality_preset: "crisp",
224 personality: "",
225 routing: DEFAULT_ROUTING,
226 budget: DEFAULT_BUDGET,
227 autonomy: DEFAULT_AUTONOMY,
228 capacity: DEFAULT_CAPACITY,
229 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)230 avatar_seed: "",
231 title: "",
232 team: null,
233 department: "",
234 responsibilities: [],
235 subagents: [],
236 faces: "internal",
Agents recall what Docs say before they answer or work, and each has required reading237 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.238 skills_off: [],
Chat and workspace agents: channels, DMs and named agents you talk to239 };
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.240 const next: Definition = { ...from, skills_off: from.skills_off ?? [] };
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)241 // Whether the role was made from the title and team, so it follows them.
242 const roleDerived = !from.role || from.role === roleOf(from);
Chat and workspace agents: channels, DMs and named agents you talk to243 if (creating || changes.handle !== undefined) {
244 const handle = checkHandle(changes.handle);
245 if (!handle.ok) return bad(handle.message);
246 next.handle = handle.handle;
247 }
248 const fields = [
249 ["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)250 ["role", "The role", LIMITS.role, false],
251 ["title", "The title", LIMITS.title, false],
252 ["department", "The department", LIMITS.department, false],
253 // The built-in agent's instructions are added to its fixed job, and may be empty.
254 ["instructions", "The instructions", LIMITS.instructions, !options.builtin],
Chat and workspace agents: channels, DMs and named agents you talk to255 ["personality", "The personality", LIMITS.personality, false],
256 ] as const;
257 for (const [key, what, max, required] of fields) {
258 if (!creating && changes[key] === undefined) continue;
259 const value = text(changes[key], what, max, required);
260 if (!value.ok) return value;
261 next[key] = value.value;
262 }
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)263 if (changes.team !== undefined) {
264 if (changes.team === null || changes.team === "") next.team = null;
265 else if (typeof changes.team !== "string" || !/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(changes.team.trim().toLowerCase())) {
266 return bad("A team is named by its slug.");
267 } else next.team = changes.team.trim().toLowerCase();
268 }
269 if (changes.responsibilities !== undefined) {
270 const duties = responsibilitiesOf(changes.responsibilities);
271 if (!duties.ok) return duties;
272 next.responsibilities = duties.value;
273 }
274 // A role left empty, or made from the title and team before, follows them.
275 if (!next.role || (changes.role === undefined && roleDerived)) next.role = roleOf(next);
276 if (!next.role) return bad("Give the agent a title or a one-line role.");
277 if (changes.avatar_seed !== undefined) {
278 const seed = text(changes.avatar_seed, "The avatar seed", 64, false);
279 if (!seed.ok) return seed;
280 next.avatar_seed = seed.value;
281 }
282 // A new face from the handle, unless one was chosen; renaming keeps the face.
283 if (!next.avatar_seed) next.avatar_seed = next.handle;
Chat and workspace agents: channels, DMs and named agents you talk to284 if (changes.personality_preset !== undefined) {
285 if (!PRESETS.includes(changes.personality_preset)) return bad(`The personality preset is one of ${PRESETS.join(", ")}.`);
286 next.personality_preset = changes.personality_preset;
287 }
288 const routing = routingOf(next.routing, changes.routing);
289 if (!routing.ok) return routing;
290 next.routing = routing.value;
291 const budget = budgetOf(next.budget, changes.budget);
292 if (!budget.ok) return budget;
293 next.budget = budget.value;
294 const autonomy = autonomyOf(next.autonomy, changes.autonomy);
295 if (!autonomy.ok) return autonomy;
296 next.autonomy = autonomy.value;
297 if (changes.capacity !== undefined) {
298 const capacity = changes.capacity;
299 if (typeof capacity !== "number" || !Number.isInteger(capacity) || capacity < 1 || capacity > MAX_CAPACITY) {
300 return bad(`Capacity is 1 to ${MAX_CAPACITY} tasks at once.`);
301 }
302 next.capacity = capacity;
303 }
304 if (changes.template !== undefined) {
305 if (changes.template !== null && !templates.includes(changes.template)) return bad("There is no such template.");
306 next.template = changes.template;
307 }
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)308 if (changes.subagents !== undefined) {
309 const subagents = subagentsOf(changes.subagents);
310 if (!subagents.ok) return subagents;
311 next.subagents = subagents.value;
312 }
Agents recall what Docs say before they answer or work, and each has required reading313 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.314 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 reading315 const ids = [...new Set(changes.reading.filter((id): id is string => typeof id === "string").map((id) => id.trim()).filter(Boolean))];
316 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.317 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 reading318 next.reading = ids;
319 }
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.320 if (changes.skills_off !== undefined) {
321 if (!Array.isArray(changes.skills_off)) return bad("Skills turned off are a list of skills.");
322 const ids = new Set(changes.skills_off.filter((id): id is string => typeof id === "string").map((id) => id.trim()).filter(Boolean));
323 const unknown = [...ids].find((id) => !FOUNDATIONAL_SKILL_IDS.includes(id));
324 if (unknown) return bad(`There is no skill called ${unknown.slice(0, 40)}.`);
325 // In the skills' own order, so the same choice always reads the same.
326 next.skills_off = FOUNDATIONAL_SKILL_IDS.filter((id) => ids.has(id));
327 }
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)328 if (changes.faces !== undefined) {
329 if (changes.faces === "customers") return bad("Customer-facing agents aren't available yet.");
330 if (changes.faces !== "internal") return bad("An agent faces internal: the workspace's own people.");
331 next.faces = "internal";
332 }
333 // Never wider than their agent, whichever of the two changed.
334 next.subagents = next.subagents.map((sub) => ({ ...sub, routing: withinParent(sub.routing, next.routing) }));
Chat and workspace agents: channels, DMs and named agents you talk to335 return { ok: true, value: next };
336}
337
338/** A stored JSON column, read defensively: a bad value is the default. */
339export function readJson<T extends object>(raw: unknown, fallback: T): T {
340 if (typeof raw !== "string") return fallback;
341 try {
342 const parsed = JSON.parse(raw) as unknown;
343 return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { ...fallback, ...(parsed as T) } : fallback;
344 } catch {
345 return fallback;
346 }
347}

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