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

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