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"; | |
| 17 | import { isTier, limitsAgree } from "./routing.ts"; | |
| 18 | ||
| 19 | export const PRESETS: PersonalityPreset[] = ["crisp", "friendly", "socratic", "terse"]; | |
| 20 | ||
| 21 | export const DEFAULT_ROUTING: AgentRouting = { floor: null, ceiling: null, providers: [], pinned: null }; | |
| 22 | export 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. */ | |
| 24 | export 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 to | 31 | export const DEFAULT_CAPACITY = 3; |
| 32 | export 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) | 34 | 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 | 35 | /** $100,000 in millionths: a cap above this is a typo. */ |
| 36 | const MAX_MICROS = 100_000_000_000; | |
| 37 | ||
| 38 | /** Everything a definition holds, complete: what is stored and versioned. */ | |
| 39 | export 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 reading | 60 | 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 to | 63 | }; |
| 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". */ |
| 66 | export 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 | ||
| 73 | export const MAX_SUBAGENTS = 8; | |
| 74 | ||
| Chat and workspace agents: channels, DMs and named agents you talk to | 75 | export type Checked<T> = { ok: true; value: T } | { ok: false; message: string }; |
| 76 | ||
| 77 | const bad = (message: string): { ok: false; message: string } => ({ ok: false, message }); | |
| 78 | ||
| 79 | function 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 | ||
| 88 | function 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 | ||
| 114 | function 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 | ||
| 130 | const 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 | ||
| 137 | function 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) | 150 | function 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 | ||
| 158 | const 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. */ | |
| 161 | export 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 | ||
| 175 | function 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 to | 206 | /** |
| 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) | 210 | export 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 to | 216 | 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 reading | 237 | 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 to | 239 | }; |
| 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 to | 243 | 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 to | 255 | ["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 to | 284 | 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 reading | 313 | 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 reading | 315 | 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 reading | 318 | 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 to | 335 | return { ok: true, value: next }; |
| 336 | } | |
| 337 | ||
| 338 | /** A stored JSON column, read defensively: a bad value is the default. */ | |
| 339 | export 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.