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 to | 1 | /** |
| 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 | */ | |
| 5 | import 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 to | 13 | } 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. | 14 | import { FOUNDATIONAL_SKILL_IDS } from "../../../packages/contracts/src/skills.ts"; |
| Chat and workspace agents: channels, DMs and named agents you talk to | 15 | |
| 16 | import { 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. | 17 | import { isEffort, isTier, limitsAgree } from "./routing.ts"; |
| Chat and workspace agents: channels, DMs and named agents you talk to | 18 | |
| 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. | 19 | /** A library skill's id (skill-library.ts), as `skills_off` may name it. */ |
| 20 | const LIBRARY_SKILL_ID = /^skl_[0-9a-z]{26}$/; | |
| 21 | ||
| Chat and workspace agents: channels, DMs and named agents you talk to | 22 | export const PRESETS: PersonalityPreset[] = ["crisp", "friendly", "socratic", "terse"]; |
| 23 | ||
| Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how. | 24 | export const DEFAULT_ROUTING: AgentRouting = { floor: null, ceiling: null, providers: [], pinned: null, effort: "auto" }; |
| Chat and workspace agents: channels, DMs and named agents you talk to | 25 | export const DEFAULT_BUDGET: AgentBudget = { monthly_micros: null, daily_micros: null, task_micros: null }; |
| 26 | /** What an agent may do alone until someone says otherwise: open pull requests; the rest asks. */ | |
| 27 | export const DEFAULT_AUTONOMY: AgentAutonomy = { | |
| 28 | open_pull_requests: "alone", | |
| 29 | merge: "approval", | |
| 30 | deploy_production: "approval", | |
| 31 | edit_docs: "suggest", | |
| 32 | }; | |
| 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. | 33 | /** Tasks at once. */ |
| Chat and workspace agents: channels, DMs and named agents you talk to | 34 | export const DEFAULT_CAPACITY = 3; |
| 35 | export const MAX_CAPACITY = 10; | |
| 36 | ||
| 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) | 37 | const 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 to | 38 | /** $100,000 in millionths: a cap above this is a typo. */ |
| 39 | const MAX_MICROS = 100_000_000_000; | |
| 40 | ||
| 41 | /** Everything a definition holds, complete: what is stored and versioned. */ | |
| 42 | export type Definition = { | |
| 43 | handle: string; | |
| 44 | display_name: string; | |
| 45 | role: string; | |
| 46 | instructions: string; | |
| 47 | personality_preset: PersonalityPreset; | |
| 48 | personality: string; | |
| 49 | routing: AgentRouting; | |
| 50 | budget: AgentBudget; | |
| 51 | autonomy: AgentAutonomy; | |
| 52 | capacity: number; | |
| 53 | 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) | 54 | /** What its generated avatar is drawn from. */ |
| 55 | avatar_seed: string; | |
| 56 | title: string; | |
| 57 | team: string | null; | |
| 58 | department: string; | |
| 59 | responsibilities: string[]; | |
| 60 | subagents: SubagentDef[]; | |
| 61 | 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. | 62 | /** Spaces whose artifacts it reads first. */ |
| Agents recall what Docs say before they answer or work, and each has required reading | 63 | 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. | 64 | /** Foundational skills turned off for it, by id (@g1t/contracts skills.ts). */ |
| 65 | skills_off: string[]; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 66 | }; |
| 67 | ||
| 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) | 68 | /** The one-line role a title and team (or department) make: "QA Engineer on the qa team". */ |
| 69 | export function roleOf(d: Pick<Definition, "title" | "team" | "department">): string { | |
| 70 | const title = d.title.trim(); | |
| 71 | if (!title) return ""; | |
| 72 | if (d.team) return `${title} on the ${d.team} team`; | |
| 73 | return d.department.trim() ? `${title}, ${d.department.trim()}` : title; | |
| 74 | } | |
| 75 | ||
| 76 | export const MAX_SUBAGENTS = 8; | |
| 77 | ||
| Chat and workspace agents: channels, DMs and named agents you talk to | 78 | export type Checked<T> = { ok: true; value: T } | { ok: false; message: string }; |
| 79 | ||
| 80 | const bad = (message: string): { ok: false; message: string } => ({ ok: false, message }); | |
| 81 | ||
| 82 | function text(value: unknown, what: string, max: number, required: boolean): Checked<string> { | |
| 83 | if (value === undefined || value === null) return required ? bad(`${what} is required.`) : { ok: true, value: "" }; | |
| 84 | if (typeof value !== "string") return bad(`${what} is text.`); | |
| 85 | const trimmed = value.trim(); | |
| 86 | if (required && !trimmed) return bad(`${what} is required.`); | |
| 87 | if (trimmed.length > max) return bad(`${what} is at most ${max} characters.`); | |
| 88 | return { ok: true, value: trimmed }; | |
| 89 | } | |
| 90 | ||
| 91 | function routingOf(base: AgentRouting, given: unknown): Checked<AgentRouting> { | |
| 92 | if (given === undefined || given === null) return { ok: true, value: base }; | |
| 93 | if (typeof given !== "object") return bad("Routing is an object."); | |
| 94 | const g = given as Partial<AgentRouting>; | |
| 95 | const next: AgentRouting = { ...base }; | |
| 96 | for (const key of ["floor", "ceiling"] as const) { | |
| 97 | if (g[key] === undefined) continue; | |
| 98 | if (g[key] !== null && !isTier(g[key])) return bad(`The ${key} is small, large, frontier or none.`); | |
| 99 | next[key] = g[key] ?? null; | |
| 100 | } | |
| 101 | if (!limitsAgree(next.floor, next.ceiling)) return bad("The floor is above the ceiling: lower the floor or raise the ceiling."); | |
| 102 | if (g.providers !== undefined) { | |
| 103 | if (!Array.isArray(g.providers) || g.providers.some((p) => typeof p !== "string" || !p.trim())) return bad("Providers is a list of names."); | |
| 104 | const providers = [...new Set(g.providers.map((p) => p.trim()))]; | |
| 105 | if (providers.length > LIMITS.providers) return bad(`An agent names at most ${LIMITS.providers} providers.`); | |
| 106 | next.providers = providers; | |
| 107 | } | |
| 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. | 108 | if (g.effort !== undefined) { |
| 109 | if (g.effort !== null && !isEffort(g.effort)) return bad("Effort is auto, low, medium, high or max."); | |
| 110 | next.effort = g.effort ?? "auto"; | |
| 111 | } | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 112 | if (g.pinned !== undefined) { |
| 113 | if (g.pinned === null || g.pinned === "") next.pinned = null; | |
| 114 | else if (typeof g.pinned !== "string" || g.pinned.trim().length > LIMITS.pinned || !/^[^/\s]+\/\S+$/.test(g.pinned.trim())) { | |
| 115 | return bad("A pinned model is written provider/model."); | |
| 116 | } else next.pinned = g.pinned.trim(); | |
| 117 | } | |
| 118 | return { ok: true, value: next }; | |
| 119 | } | |
| 120 | ||
| 121 | function budgetOf(base: AgentBudget, given: unknown): Checked<AgentBudget> { | |
| 122 | if (given === undefined || given === null) return { ok: true, value: base }; | |
| 123 | if (typeof given !== "object") return bad("Budget is an object."); | |
| 124 | const g = given as Partial<AgentBudget>; | |
| 125 | const next: AgentBudget = { ...base }; | |
| 126 | for (const key of ["monthly_micros", "daily_micros", "task_micros"] as const) { | |
| 127 | const value = g[key]; | |
| 128 | if (value === undefined) continue; | |
| 129 | if (value === null) next[key] = null; | |
| 130 | else if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > MAX_MICROS) { | |
| 131 | return bad("A budget is a whole number of millionths of a dollar, or none."); | |
| 132 | } else next[key] = value; | |
| 133 | } | |
| 134 | return { ok: true, value: next }; | |
| 135 | } | |
| 136 | ||
| 137 | const AUTONOMY_CHOICES: { [K in keyof AgentAutonomy]: AgentAutonomy[K][] } = { | |
| 138 | open_pull_requests: ["alone", "approval"], | |
| 139 | merge: ["alone", "approval", "never"], | |
| 140 | deploy_production: ["approval", "never"], | |
| 141 | edit_docs: ["alone", "suggest"], | |
| 142 | }; | |
| 143 | ||
| 144 | function autonomyOf(base: AgentAutonomy, given: unknown): Checked<AgentAutonomy> { | |
| 145 | if (given === undefined || given === null) return { ok: true, value: base }; | |
| 146 | if (typeof given !== "object") return bad("Autonomy is an object."); | |
| 147 | const next = { ...base } as Record<string, string>; | |
| 148 | for (const [key, choices] of Object.entries(AUTONOMY_CHOICES) as [string, string[]][]) { | |
| 149 | const value = (given as Record<string, unknown>)[key]; | |
| 150 | if (value === undefined) continue; | |
| 151 | if (typeof value !== "string" || !choices.includes(value)) return bad(`${key} is one of ${choices.join(", ")}.`); | |
| 152 | next[key] = value; | |
| 153 | } | |
| 154 | return { ok: true, value: next as AgentAutonomy }; | |
| 155 | } | |
| 156 | ||
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 157 | function responsibilitiesOf(given: unknown): Checked<string[]> { |
| 158 | if (!Array.isArray(given) || given.some((d) => typeof d !== "string")) return bad("Responsibilities are a list of short duties."); | |
| 159 | const duties = [...new Set((given as string[]).map((d) => d.trim()).filter(Boolean))]; | |
| 160 | if (duties.length && (duties.length < 2 || duties.length > 8)) return bad("Give 2 to 8 responsibilities, or none yet."); | |
| 161 | if (duties.some((d) => d.length > LIMITS.duty)) return bad(`Each responsibility is at most ${LIMITS.duty} characters.`); | |
| 162 | return { ok: true, value: duties }; | |
| 163 | } | |
| 164 | ||
| 165 | const ORDER = ["small", "large", "frontier"] as const; | |
| 166 | ||
| 167 | /** A subagent's limits held within its agent's: never a lower floor, never a higher ceiling. */ | |
| 168 | export function withinParent( | |
| 169 | sub: { floor: AgentRouting["floor"]; ceiling: AgentRouting["ceiling"] }, | |
| 170 | parent: Pick<AgentRouting, "floor" | "ceiling">, | |
| 171 | ): { floor: AgentRouting["floor"]; ceiling: AgentRouting["ceiling"] } { | |
| 172 | const at = (tier: AgentRouting["floor"]) => (tier ? ORDER.indexOf(tier) : -1); | |
| 173 | let floor = sub.floor; | |
| 174 | let ceiling = sub.ceiling; | |
| 175 | if (parent.floor && at(floor) < at(parent.floor)) floor = parent.floor; | |
| 176 | if (parent.ceiling && (!ceiling || at(ceiling) > at(parent.ceiling))) ceiling = parent.ceiling; | |
| 177 | // Held inside, a floor can end above the ceiling: the ceiling, the spending rail, wins. | |
| 178 | if (floor && ceiling && at(floor) > at(ceiling)) floor = ceiling; | |
| 179 | return { floor, ceiling }; | |
| 180 | } | |
| 181 | ||
| 182 | function subagentsOf(given: unknown): Checked<SubagentDef[]> { | |
| 183 | if (!Array.isArray(given)) return bad("Subagents are a list."); | |
| 184 | if (given.length > MAX_SUBAGENTS) return bad(`An agent keeps at most ${MAX_SUBAGENTS} subagents.`); | |
| 185 | const out: SubagentDef[] = []; | |
| 186 | for (const raw of given as Partial<SubagentDef>[]) { | |
| 187 | if (!raw || typeof raw !== "object") return bad("Each subagent has a name, a description and instructions."); | |
| 188 | const name = typeof raw.name === "string" ? raw.name.trim().toLowerCase() : ""; | |
| 189 | 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."); | |
| 190 | if (out.some((sub) => sub.name === name)) return bad(`Two subagents are called ${name}.`); | |
| 191 | const description = text(raw.description, `${name}'s description`, 200, true); | |
| 192 | if (!description.ok) return description; | |
| 193 | const instructions = text(raw.instructions, `${name}'s instructions`, 4000, true); | |
| 194 | if (!instructions.ok) return instructions; | |
| 195 | const routing = raw.routing ?? { floor: null, ceiling: null }; | |
| 196 | for (const tier of [routing.floor, routing.ceiling]) { | |
| 197 | if (tier !== null && tier !== undefined && !isTier(tier)) return bad(`${name}'s limits are small, large, frontier or none.`); | |
| 198 | } | |
| 199 | if (!limitsAgree(routing.floor ?? null, routing.ceiling ?? null)) return bad(`${name}'s floor is above its ceiling.`); | |
| 200 | const parallel = raw.max_parallel ?? 2; | |
| 201 | if (typeof parallel !== "number" || !Number.isInteger(parallel) || parallel < 1 || parallel > 8) return bad(`${name} runs 1 to 8 at once.`); | |
| 202 | out.push({ | |
| 203 | name, | |
| 204 | description: description.value, | |
| 205 | instructions: instructions.value, | |
| 206 | routing: { floor: routing.floor ?? null, ceiling: routing.ceiling ?? null }, | |
| 207 | max_parallel: parallel, | |
| 208 | }); | |
| 209 | } | |
| 210 | return { ok: true, value: out }; | |
| 211 | } | |
| 212 | ||
| Chat and workspace agents: channels, DMs and named agents you talk to | 213 | /** |
| 214 | * `changes` applied to `base` (a new agent's defaults, or its current | |
| 215 | * definition), checked. `templates` are the template ids an agent may name. | |
| 216 | */ | |
| 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) | 217 | export function applyChanges( |
| 218 | base: Definition | null, | |
| 219 | changes: Partial<NewWorkspaceAgent>, | |
| 220 | templates: string[], | |
| 221 | options: { builtin?: boolean } = {}, | |
| 222 | ): Checked<Definition> { | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 223 | if (!changes || typeof changes !== "object") return bad("Send the agent's fields."); |
| 224 | const creating = base === null; | |
| 225 | const from: Definition = base ?? { | |
| 226 | handle: "", | |
| 227 | display_name: "", | |
| 228 | role: "", | |
| 229 | instructions: "", | |
| 230 | personality_preset: "crisp", | |
| 231 | personality: "", | |
| 232 | routing: DEFAULT_ROUTING, | |
| 233 | budget: DEFAULT_BUDGET, | |
| 234 | autonomy: DEFAULT_AUTONOMY, | |
| 235 | capacity: DEFAULT_CAPACITY, | |
| 236 | 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) | 237 | avatar_seed: "", |
| 238 | title: "", | |
| 239 | team: null, | |
| 240 | department: "", | |
| 241 | responsibilities: [], | |
| 242 | subagents: [], | |
| 243 | faces: "internal", | |
| Agents recall what Docs say before they answer or work, and each has required reading | 244 | 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. | 245 | skills_off: [], |
| Chat and workspace agents: channels, DMs and named agents you talk to | 246 | }; |
| 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. | 247 | 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) | 248 | // Whether the role was made from the title and team, so it follows them. |
| 249 | const roleDerived = !from.role || from.role === roleOf(from); | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 250 | if (creating || changes.handle !== undefined) { |
| 251 | const handle = checkHandle(changes.handle); | |
| 252 | if (!handle.ok) return bad(handle.message); | |
| 253 | next.handle = handle.handle; | |
| 254 | } | |
| 255 | const fields = [ | |
| 256 | ["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) | 257 | ["role", "The role", LIMITS.role, false], |
| 258 | ["title", "The title", LIMITS.title, false], | |
| 259 | ["department", "The department", LIMITS.department, false], | |
| 260 | // The built-in agent's instructions are added to its fixed job, and may be empty. | |
| 261 | ["instructions", "The instructions", LIMITS.instructions, !options.builtin], | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 262 | ["personality", "The personality", LIMITS.personality, false], |
| 263 | ] as const; | |
| 264 | for (const [key, what, max, required] of fields) { | |
| 265 | if (!creating && changes[key] === undefined) continue; | |
| 266 | const value = text(changes[key], what, max, required); | |
| 267 | if (!value.ok) return value; | |
| 268 | next[key] = value.value; | |
| 269 | } | |
| 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) | 270 | if (changes.team !== undefined) { |
| 271 | if (changes.team === null || changes.team === "") next.team = null; | |
| 272 | else if (typeof changes.team !== "string" || !/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(changes.team.trim().toLowerCase())) { | |
| 273 | return bad("A team is named by its slug."); | |
| 274 | } else next.team = changes.team.trim().toLowerCase(); | |
| 275 | } | |
| 276 | if (changes.responsibilities !== undefined) { | |
| 277 | const duties = responsibilitiesOf(changes.responsibilities); | |
| 278 | if (!duties.ok) return duties; | |
| 279 | next.responsibilities = duties.value; | |
| 280 | } | |
| 281 | // A role left empty, or made from the title and team before, follows them. | |
| 282 | if (!next.role || (changes.role === undefined && roleDerived)) next.role = roleOf(next); | |
| 283 | if (!next.role) return bad("Give the agent a title or a one-line role."); | |
| 284 | if (changes.avatar_seed !== undefined) { | |
| 285 | const seed = text(changes.avatar_seed, "The avatar seed", 64, false); | |
| 286 | if (!seed.ok) return seed; | |
| 287 | next.avatar_seed = seed.value; | |
| 288 | } | |
| 289 | // A new face from the handle, unless one was chosen; renaming keeps the face. | |
| 290 | if (!next.avatar_seed) next.avatar_seed = next.handle; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 291 | if (changes.personality_preset !== undefined) { |
| 292 | if (!PRESETS.includes(changes.personality_preset)) return bad(`The personality preset is one of ${PRESETS.join(", ")}.`); | |
| 293 | next.personality_preset = changes.personality_preset; | |
| 294 | } | |
| 295 | const routing = routingOf(next.routing, changes.routing); | |
| 296 | if (!routing.ok) return routing; | |
| 297 | next.routing = routing.value; | |
| 298 | const budget = budgetOf(next.budget, changes.budget); | |
| 299 | if (!budget.ok) return budget; | |
| 300 | next.budget = budget.value; | |
| 301 | const autonomy = autonomyOf(next.autonomy, changes.autonomy); | |
| 302 | if (!autonomy.ok) return autonomy; | |
| 303 | next.autonomy = autonomy.value; | |
| 304 | if (changes.capacity !== undefined) { | |
| 305 | const capacity = changes.capacity; | |
| 306 | if (typeof capacity !== "number" || !Number.isInteger(capacity) || capacity < 1 || capacity > MAX_CAPACITY) { | |
| 307 | return bad(`Capacity is 1 to ${MAX_CAPACITY} tasks at once.`); | |
| 308 | } | |
| 309 | next.capacity = capacity; | |
| 310 | } | |
| 311 | if (changes.template !== undefined) { | |
| 312 | if (changes.template !== null && !templates.includes(changes.template)) return bad("There is no such template."); | |
| 313 | next.template = changes.template; | |
| 314 | } | |
| 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) | 315 | if (changes.subagents !== undefined) { |
| 316 | const subagents = subagentsOf(changes.subagents); | |
| 317 | if (!subagents.ok) return subagents; | |
| 318 | next.subagents = subagents.value; | |
| 319 | } | |
| Agents recall what Docs say before they answer or work, and each has required reading | 320 | 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. | 321 | 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 reading | 322 | const ids = [...new Set(changes.reading.filter((id): id is string => typeof id === "string").map((id) => id.trim()).filter(Boolean))]; |
| 323 | 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. | 324 | 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 reading | 325 | next.reading = ids; |
| 326 | } | |
| Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how. | 327 | if (changes.skills_off !== undefined) { |
| 328 | if (!Array.isArray(changes.skills_off)) return bad("Skills turned off are a list of skills."); | |
| 329 | 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. | 330 | // Foundational skills by id, and the library's by theirs (skl_…), which |
| 331 | // may be off before or after they are attached. | |
| 332 | const library = [...ids].filter((id) => LIBRARY_SKILL_ID.test(id)); | |
| 333 | 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. | 334 | 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. | 335 | 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. | 336 | // 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. | 337 | 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. | 338 | } |
| 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) | 339 | if (changes.faces !== undefined) { |
| 340 | if (changes.faces === "customers") return bad("Customer-facing agents aren't available yet."); | |
| 341 | if (changes.faces !== "internal") return bad("An agent faces internal: the workspace's own people."); | |
| 342 | next.faces = "internal"; | |
| 343 | } | |
| 344 | // Never wider than their agent, whichever of the two changed. | |
| 345 | next.subagents = next.subagents.map((sub) => ({ ...sub, routing: withinParent(sub.routing, next.routing) })); | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 346 | return { ok: true, value: next }; |
| 347 | } | |
| 348 | ||
| 349 | /** A stored JSON column, read defensively: a bad value is the default. */ | |
| 350 | export function readJson<T extends object>(raw: unknown, fallback: T): T { | |
| 351 | if (typeof raw !== "string") return fallback; | |
| 352 | try { | |
| 353 | const parsed = JSON.parse(raw) as unknown; | |
| 354 | return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { ...fallback, ...(parsed as T) } : fallback; | |
| 355 | } catch { | |
| 356 | return fallback; | |
| 357 | } | |
| 358 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.