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 { | |
| Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how. | 6 | AbilityLevel, |
| 7 | AbilitySetting, | |
| 8 | AgentAbilities, | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 9 | AgentAutonomy, |
| 10 | AgentBudget, | |
| Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent. | 11 | AgentLook, |
| Chat and workspace agents: channels, DMs and named agents you talk to | 12 | AgentRouting, |
| Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how. | 13 | McpServer, |
| Chat and workspace agents: channels, DMs and named agents you talk to | 14 | NewWorkspaceAgent, |
| 15 | PersonalityPreset, | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 16 | AgentFaces, |
| 17 | SubagentDef, | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 18 | } from "@g1t/contracts"; |
| Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how. | 19 | import { ABILITY_LEVELS, EMPTY_ABILITIES, MAX_MCP_SERVERS, integrationAbilities, maxLevel, mcpAbility, withinLevel } from "../../../packages/contracts/src/abilities.ts"; |
| Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent. | 20 | import { readLook } from "../../../packages/contracts/src/agent-look.ts"; |
| Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how. | 21 | import { FOUNDATIONAL_SKILL_IDS } from "../../../packages/contracts/src/skills.ts"; |
| Chat and workspace agents: channels, DMs and named agents you talk to | 22 | |
| 23 | 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. | 24 | import { isEffort, isTier, limitsAgree } from "./routing.ts"; |
| Chat and workspace agents: channels, DMs and named agents you talk to | 25 | |
| Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how. | 26 | /** A library skill's id (skill-library.ts), as `skills_off` may name it. */ |
| 27 | const LIBRARY_SKILL_ID = /^skl_[0-9a-z]{26}$/; | |
| 28 | ||
| Chat and workspace agents: channels, DMs and named agents you talk to | 29 | export const PRESETS: PersonalityPreset[] = ["crisp", "friendly", "socratic", "terse"]; |
| 30 | ||
| Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how. | 31 | 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 | 32 | export const DEFAULT_BUDGET: AgentBudget = { monthly_micros: null, daily_micros: null, task_micros: null }; |
| 33 | /** What an agent may do alone until someone says otherwise: open pull requests; the rest asks. */ | |
| 34 | export const DEFAULT_AUTONOMY: AgentAutonomy = { | |
| 35 | open_pull_requests: "alone", | |
| 36 | merge: "approval", | |
| 37 | deploy_production: "approval", | |
| 38 | edit_docs: "suggest", | |
| 39 | }; | |
| The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were. | 40 | /** Tasks at once. */ |
| Chat and workspace agents: channels, DMs and named agents you talk to | 41 | export const DEFAULT_CAPACITY = 3; |
| 42 | export const MAX_CAPACITY = 10; | |
| 43 | ||
| An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how. | 44 | const LIMITS = { displayName: 64, role: 120, title: 60, duty: 160, instructions: 8000, personality: 1000, providers: 10, pinned: 200 }; |
| Chat and workspace agents: channels, DMs and named agents you talk to | 45 | /** $100,000 in millionths: a cap above this is a typo. */ |
| 46 | const MAX_MICROS = 100_000_000_000; | |
| 47 | ||
| 48 | /** Everything a definition holds, complete: what is stored and versioned. */ | |
| 49 | export type Definition = { | |
| 50 | handle: string; | |
| 51 | display_name: string; | |
| 52 | role: string; | |
| 53 | instructions: string; | |
| 54 | personality_preset: PersonalityPreset; | |
| 55 | personality: string; | |
| 56 | routing: AgentRouting; | |
| 57 | budget: AgentBudget; | |
| 58 | autonomy: AgentAutonomy; | |
| 59 | capacity: number; | |
| 60 | template: string | null; | |
| Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent. | 61 | /** What its generated face is drawn from, when it has chosen none. */ |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 62 | avatar_seed: string; |
| Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent. | 63 | /** Its face as chosen (@g1t/contracts agent-look.ts), or null for the seed's. */ |
| 64 | look: AgentLook | null; | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 65 | title: string; |
| 66 | responsibilities: string[]; | |
| 67 | subagents: SubagentDef[]; | |
| 68 | faces: AgentFaces; | |
| Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages. | 69 | /** Spaces whose artifacts it reads first. */ |
| Agents recall what Docs say before they answer or work, and each has required reading | 70 | reading: string[]; |
| Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how. | 71 | /** Foundational skills turned off for it, by id (@g1t/contracts skills.ts). */ |
| 72 | skills_off: string[]; | |
| Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how. | 73 | /** Its abilities' levels and credentials, and its MCP servers (@g1t/contracts abilities.ts). */ |
| 74 | abilities: AgentAbilities; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 75 | }; |
| 76 | ||
| An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how. | 77 | /** |
| 78 | * The one-line role its title makes: "QA Engineer". Which teams it is on | |
| 79 | * are team memberships (identity), never part of the agent. | |
| 80 | */ | |
| 81 | export function roleOf(d: Pick<Definition, "title">): string { | |
| 82 | return d.title.trim(); | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 83 | } |
| 84 | ||
| An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how. | 85 | /** |
| 86 | * The role an agent was given when it carried its own team or department, | |
| 87 | * made from them: "QA Engineer on the qa team", "QA Engineer, QA". A | |
| 88 | * stored role that is exactly this follows the title (store.ts). | |
| 89 | */ | |
| 90 | export function legacyRoleOf(title: string, team: string | null, department: string | null): string { | |
| 91 | const t = title.trim(); | |
| 92 | if (!t) return ""; | |
| 93 | if (team) return `${t} on the ${team} team`; | |
| 94 | return department?.trim() ? `${t}, ${department.trim()}` : t; | |
| 95 | } | |
| 96 | ||
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 97 | export const MAX_SUBAGENTS = 8; |
| 98 | ||
| Chat and workspace agents: channels, DMs and named agents you talk to | 99 | export type Checked<T> = { ok: true; value: T } | { ok: false; message: string }; |
| 100 | ||
| 101 | const bad = (message: string): { ok: false; message: string } => ({ ok: false, message }); | |
| 102 | ||
| 103 | function text(value: unknown, what: string, max: number, required: boolean): Checked<string> { | |
| 104 | if (value === undefined || value === null) return required ? bad(`${what} is required.`) : { ok: true, value: "" }; | |
| 105 | if (typeof value !== "string") return bad(`${what} is text.`); | |
| 106 | const trimmed = value.trim(); | |
| 107 | if (required && !trimmed) return bad(`${what} is required.`); | |
| 108 | if (trimmed.length > max) return bad(`${what} is at most ${max} characters.`); | |
| 109 | return { ok: true, value: trimmed }; | |
| 110 | } | |
| 111 | ||
| 112 | function routingOf(base: AgentRouting, given: unknown): Checked<AgentRouting> { | |
| 113 | if (given === undefined || given === null) return { ok: true, value: base }; | |
| 114 | if (typeof given !== "object") return bad("Routing is an object."); | |
| 115 | const g = given as Partial<AgentRouting>; | |
| 116 | const next: AgentRouting = { ...base }; | |
| 117 | for (const key of ["floor", "ceiling"] as const) { | |
| 118 | if (g[key] === undefined) continue; | |
| 119 | if (g[key] !== null && !isTier(g[key])) return bad(`The ${key} is small, large, frontier or none.`); | |
| 120 | next[key] = g[key] ?? null; | |
| 121 | } | |
| 122 | if (!limitsAgree(next.floor, next.ceiling)) return bad("The floor is above the ceiling: lower the floor or raise the ceiling."); | |
| 123 | if (g.providers !== undefined) { | |
| 124 | if (!Array.isArray(g.providers) || g.providers.some((p) => typeof p !== "string" || !p.trim())) return bad("Providers is a list of names."); | |
| 125 | const providers = [...new Set(g.providers.map((p) => p.trim()))]; | |
| 126 | if (providers.length > LIMITS.providers) return bad(`An agent names at most ${LIMITS.providers} providers.`); | |
| 127 | next.providers = providers; | |
| 128 | } | |
| Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how. | 129 | if (g.effort !== undefined) { |
| 130 | if (g.effort !== null && !isEffort(g.effort)) return bad("Effort is auto, low, medium, high or max."); | |
| 131 | next.effort = g.effort ?? "auto"; | |
| 132 | } | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 133 | if (g.pinned !== undefined) { |
| 134 | if (g.pinned === null || g.pinned === "") next.pinned = null; | |
| 135 | else if (typeof g.pinned !== "string" || g.pinned.trim().length > LIMITS.pinned || !/^[^/\s]+\/\S+$/.test(g.pinned.trim())) { | |
| 136 | return bad("A pinned model is written provider/model."); | |
| 137 | } else next.pinned = g.pinned.trim(); | |
| 138 | } | |
| 139 | return { ok: true, value: next }; | |
| 140 | } | |
| 141 | ||
| 142 | function budgetOf(base: AgentBudget, given: unknown): Checked<AgentBudget> { | |
| 143 | if (given === undefined || given === null) return { ok: true, value: base }; | |
| 144 | if (typeof given !== "object") return bad("Budget is an object."); | |
| 145 | const g = given as Partial<AgentBudget>; | |
| 146 | const next: AgentBudget = { ...base }; | |
| 147 | for (const key of ["monthly_micros", "daily_micros", "task_micros"] as const) { | |
| 148 | const value = g[key]; | |
| 149 | if (value === undefined) continue; | |
| 150 | if (value === null) next[key] = null; | |
| 151 | else if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > MAX_MICROS) { | |
| 152 | return bad("A budget is a whole number of millionths of a dollar, or none."); | |
| 153 | } else next[key] = value; | |
| 154 | } | |
| 155 | return { ok: true, value: next }; | |
| 156 | } | |
| 157 | ||
| 158 | const AUTONOMY_CHOICES: { [K in keyof AgentAutonomy]: AgentAutonomy[K][] } = { | |
| 159 | open_pull_requests: ["alone", "approval"], | |
| 160 | merge: ["alone", "approval", "never"], | |
| 161 | deploy_production: ["approval", "never"], | |
| 162 | edit_docs: ["alone", "suggest"], | |
| 163 | }; | |
| 164 | ||
| 165 | function autonomyOf(base: AgentAutonomy, given: unknown): Checked<AgentAutonomy> { | |
| 166 | if (given === undefined || given === null) return { ok: true, value: base }; | |
| 167 | if (typeof given !== "object") return bad("Autonomy is an object."); | |
| 168 | const next = { ...base } as Record<string, string>; | |
| 169 | for (const [key, choices] of Object.entries(AUTONOMY_CHOICES) as [string, string[]][]) { | |
| 170 | const value = (given as Record<string, unknown>)[key]; | |
| 171 | if (value === undefined) continue; | |
| 172 | if (typeof value !== "string" || !choices.includes(value)) return bad(`${key} is one of ${choices.join(", ")}.`); | |
| 173 | next[key] = value; | |
| 174 | } | |
| 175 | return { ok: true, value: next as AgentAutonomy }; | |
| 176 | } | |
| 177 | ||
| Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how. | 178 | /** The most abilities with a choice of their own one agent keeps. */ |
| 179 | const MAX_ABILITY_SETTINGS = 400; | |
| 180 | ||
| 181 | /** | |
| 182 | * The abilities' settings, checked against what the agent can have: an | |
| 183 | * integration ability the catalog knows, or a tool of one of its MCP | |
| 184 | * servers, each at a level its kind allows. g1t's own abilities keep their | |
| 185 | * choices in `autonomy`, so nothing is kept for them here. | |
| 186 | */ | |
| 187 | function abilitiesOf(base: AgentAbilities, given: unknown, servers: McpServer[]): Checked<AgentAbilities> { | |
| 188 | if (given === undefined || given === null) return { ok: true, value: { ...base, mcp_servers: servers } }; | |
| 189 | if (typeof given !== "object" || Array.isArray(given)) return bad("Abilities are an object."); | |
| 190 | const settings = (given as { settings?: unknown }).settings; | |
| 191 | if (settings === undefined) return { ok: true, value: { ...base, mcp_servers: servers } }; | |
| 192 | if (!settings || typeof settings !== "object" || Array.isArray(settings)) return bad("Abilities' settings are an object by ability id."); | |
| 193 | const entries = Object.entries(settings as Record<string, unknown>); | |
| 194 | if (entries.length > MAX_ABILITY_SETTINGS) return bad(`At most ${MAX_ABILITY_SETTINGS} abilities keep a setting.`); | |
| 195 | const next: Record<string, AbilitySetting> = {}; | |
| 196 | for (const [id, raw] of entries) { | |
| 197 | const def = abilityDef(id, servers); | |
| 198 | if (!def) return bad(`There is no ability called ${id.slice(0, 60)} for this agent.`); | |
| 199 | if (!raw || typeof raw !== "object") return bad(`${id}'s setting is an object.`); | |
| 200 | const setting = raw as Record<string, unknown>; | |
| 201 | const kept: AbilitySetting = {}; | |
| 202 | if (setting.level !== undefined && setting.level !== null) { | |
| 203 | if (typeof setting.level !== "string" || !ABILITY_LEVELS.includes(setting.level as AbilityLevel)) return bad(`${id}'s level is alone, asked, ask or never.`); | |
| 204 | const level = setting.level as AbilityLevel; | |
| 205 | const max = maxLevel(def.kind); | |
| 206 | if (!withinLevel(level, max)) return bad(`${def.label} can't be set above ${max === "ask" ? "Ask first" : max}: it ${def.kind === "restricted" ? "is a purchase, a credential or a permission change" : "isn't allowed that freely"}.`); | |
| 207 | kept.level = level; | |
| 208 | } | |
| 209 | if (setting.credentials !== undefined && setting.credentials !== null) { | |
| 210 | if (def.group !== "integration") return bad(`Only an integration's abilities say whose connection they run on.`); | |
| 211 | if (setting.credentials !== "workspace" && setting.credentials !== "asker") return bad(`${id}'s credentials are workspace or asker.`); | |
| 212 | kept.credentials = setting.credentials; | |
| 213 | } | |
| 214 | if (Object.keys(kept).length) next[id] = kept; | |
| 215 | } | |
| 216 | // In id order, so the same choices always read the same. | |
| 217 | return { ok: true, value: { settings: Object.fromEntries(Object.entries(next).sort(([a], [b]) => a.localeCompare(b))), mcp_servers: servers } }; | |
| 218 | } | |
| 219 | ||
| 220 | /** The ability `id` names, among the catalog's integration abilities and the agent's MCP servers' tools; null when there is none. */ | |
| 221 | function abilityDef(id: string, servers: McpServer[]) { | |
| 222 | const parts = id.split(":"); | |
| 223 | if (parts[0] === "integration" && parts.length === 3) return integrationAbilities(parts[1]!).find((def) => def.id === id) ?? null; | |
| 224 | if (parts[0] === "mcp" && parts.length >= 3) { | |
| 225 | const server = servers.find((s) => s.id === parts[1]); | |
| 226 | const tool = server?.tools.find((t) => `mcp:${server.id}:${t.name}` === id); | |
| 227 | return server && tool ? mcpAbility(server, tool) : null; | |
| 228 | } | |
| 229 | return null; | |
| 230 | } | |
| 231 | ||
| 232 | /** The MCP servers as kept: at most `MAX_MCP_SERVERS`, names unique. Only the service's own MCP calls change them. */ | |
| 233 | function mcpServersOf(given: unknown): Checked<McpServer[]> { | |
| 234 | if (!Array.isArray(given)) return bad("MCP servers are a list."); | |
| 235 | if (given.length > MAX_MCP_SERVERS) return bad(`An agent has at most ${MAX_MCP_SERVERS} MCP servers.`); | |
| 236 | const names = new Set<string>(); | |
| 237 | for (const server of given as McpServer[]) { | |
| 238 | if (!server || typeof server !== "object" || typeof server.id !== "string" || typeof server.name !== "string" || typeof server.url !== "string" || !Array.isArray(server.tools)) return bad("An MCP server has an id, a name, a url and tools."); | |
| 239 | if (names.has(server.name)) return bad(`Two servers are called ${server.name}.`); | |
| 240 | names.add(server.name); | |
| 241 | } | |
| 242 | return { ok: true, value: given as McpServer[] }; | |
| 243 | } | |
| 244 | ||
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 245 | function responsibilitiesOf(given: unknown): Checked<string[]> { |
| 246 | if (!Array.isArray(given) || given.some((d) => typeof d !== "string")) return bad("Responsibilities are a list of short duties."); | |
| 247 | const duties = [...new Set((given as string[]).map((d) => d.trim()).filter(Boolean))]; | |
| 248 | if (duties.length && (duties.length < 2 || duties.length > 8)) return bad("Give 2 to 8 responsibilities, or none yet."); | |
| 249 | if (duties.some((d) => d.length > LIMITS.duty)) return bad(`Each responsibility is at most ${LIMITS.duty} characters.`); | |
| 250 | return { ok: true, value: duties }; | |
| 251 | } | |
| 252 | ||
| 253 | const ORDER = ["small", "large", "frontier"] as const; | |
| 254 | ||
| 255 | /** A subagent's limits held within its agent's: never a lower floor, never a higher ceiling. */ | |
| 256 | export function withinParent( | |
| 257 | sub: { floor: AgentRouting["floor"]; ceiling: AgentRouting["ceiling"] }, | |
| 258 | parent: Pick<AgentRouting, "floor" | "ceiling">, | |
| 259 | ): { floor: AgentRouting["floor"]; ceiling: AgentRouting["ceiling"] } { | |
| 260 | const at = (tier: AgentRouting["floor"]) => (tier ? ORDER.indexOf(tier) : -1); | |
| 261 | let floor = sub.floor; | |
| 262 | let ceiling = sub.ceiling; | |
| 263 | if (parent.floor && at(floor) < at(parent.floor)) floor = parent.floor; | |
| 264 | if (parent.ceiling && (!ceiling || at(ceiling) > at(parent.ceiling))) ceiling = parent.ceiling; | |
| 265 | // Held inside, a floor can end above the ceiling: the ceiling, the spending rail, wins. | |
| 266 | if (floor && ceiling && at(floor) > at(ceiling)) floor = ceiling; | |
| 267 | return { floor, ceiling }; | |
| 268 | } | |
| 269 | ||
| 270 | function subagentsOf(given: unknown): Checked<SubagentDef[]> { | |
| 271 | if (!Array.isArray(given)) return bad("Subagents are a list."); | |
| 272 | if (given.length > MAX_SUBAGENTS) return bad(`An agent keeps at most ${MAX_SUBAGENTS} subagents.`); | |
| 273 | const out: SubagentDef[] = []; | |
| 274 | for (const raw of given as Partial<SubagentDef>[]) { | |
| 275 | if (!raw || typeof raw !== "object") return bad("Each subagent has a name, a description and instructions."); | |
| 276 | const name = typeof raw.name === "string" ? raw.name.trim().toLowerCase() : ""; | |
| 277 | if (!/^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){1,31}$/.test(name)) return bad("A subagent's name is 2 to 32 lowercase letters, digits and single hyphens."); | |
| 278 | if (out.some((sub) => sub.name === name)) return bad(`Two subagents are called ${name}.`); | |
| 279 | const description = text(raw.description, `${name}'s description`, 200, true); | |
| 280 | if (!description.ok) return description; | |
| 281 | const instructions = text(raw.instructions, `${name}'s instructions`, 4000, true); | |
| 282 | if (!instructions.ok) return instructions; | |
| 283 | const routing = raw.routing ?? { floor: null, ceiling: null }; | |
| 284 | for (const tier of [routing.floor, routing.ceiling]) { | |
| 285 | if (tier !== null && tier !== undefined && !isTier(tier)) return bad(`${name}'s limits are small, large, frontier or none.`); | |
| 286 | } | |
| 287 | if (!limitsAgree(routing.floor ?? null, routing.ceiling ?? null)) return bad(`${name}'s floor is above its ceiling.`); | |
| 288 | const parallel = raw.max_parallel ?? 2; | |
| 289 | if (typeof parallel !== "number" || !Number.isInteger(parallel) || parallel < 1 || parallel > 8) return bad(`${name} runs 1 to 8 at once.`); | |
| 290 | out.push({ | |
| 291 | name, | |
| 292 | description: description.value, | |
| 293 | instructions: instructions.value, | |
| 294 | routing: { floor: routing.floor ?? null, ceiling: routing.ceiling ?? null }, | |
| 295 | max_parallel: parallel, | |
| 296 | }); | |
| 297 | } | |
| 298 | return { ok: true, value: out }; | |
| 299 | } | |
| 300 | ||
| Chat and workspace agents: channels, DMs and named agents you talk to | 301 | /** |
| 302 | * `changes` applied to `base` (a new agent's defaults, or its current | |
| 303 | * definition), checked. `templates` are the template ids an agent may name. | |
| 304 | */ | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 305 | export function applyChanges( |
| 306 | base: Definition | null, | |
| 307 | changes: Partial<NewWorkspaceAgent>, | |
| 308 | templates: string[], | |
| Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how. | 309 | options: { builtin?: boolean; mcp_servers?: McpServer[] } = {}, |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 310 | ): Checked<Definition> { |
| Chat and workspace agents: channels, DMs and named agents you talk to | 311 | if (!changes || typeof changes !== "object") return bad("Send the agent's fields."); |
| 312 | const creating = base === null; | |
| 313 | const from: Definition = base ?? { | |
| 314 | handle: "", | |
| 315 | display_name: "", | |
| 316 | role: "", | |
| 317 | instructions: "", | |
| 318 | personality_preset: "crisp", | |
| 319 | personality: "", | |
| 320 | routing: DEFAULT_ROUTING, | |
| 321 | budget: DEFAULT_BUDGET, | |
| 322 | autonomy: DEFAULT_AUTONOMY, | |
| 323 | capacity: DEFAULT_CAPACITY, | |
| 324 | template: null, | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 325 | avatar_seed: "", |
| Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent. | 326 | look: null, |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 327 | title: "", |
| 328 | responsibilities: [], | |
| 329 | subagents: [], | |
| 330 | faces: "internal", | |
| Agents recall what Docs say before they answer or work, and each has required reading | 331 | reading: [], |
| Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how. | 332 | skills_off: [], |
| Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how. | 333 | abilities: EMPTY_ABILITIES, |
| Chat and workspace agents: channels, DMs and named agents you talk to | 334 | }; |
| Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent. | 335 | const next: Definition = { ...from, look: from.look ?? null, skills_off: from.skills_off ?? [], abilities: from.abilities ?? EMPTY_ABILITIES }; |
| An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how. | 336 | // Whether the role was made from the title, so it follows it. |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 337 | const roleDerived = !from.role || from.role === roleOf(from); |
| Chat and workspace agents: channels, DMs and named agents you talk to | 338 | if (creating || changes.handle !== undefined) { |
| 339 | const handle = checkHandle(changes.handle); | |
| 340 | if (!handle.ok) return bad(handle.message); | |
| 341 | next.handle = handle.handle; | |
| 342 | } | |
| 343 | const fields = [ | |
| 344 | ["display_name", "A display name", LIMITS.displayName, true], | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 345 | ["role", "The role", LIMITS.role, false], |
| 346 | ["title", "The title", LIMITS.title, false], | |
| 347 | // The built-in agent's instructions are added to its fixed job, and may be empty. | |
| 348 | ["instructions", "The instructions", LIMITS.instructions, !options.builtin], | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 349 | ["personality", "The personality", LIMITS.personality, false], |
| 350 | ] as const; | |
| 351 | for (const [key, what, max, required] of fields) { | |
| 352 | if (!creating && changes[key] === undefined) continue; | |
| 353 | const value = text(changes[key], what, max, required); | |
| 354 | if (!value.ok) return value; | |
| 355 | next[key] = value.value; | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 356 | } |
| 357 | if (changes.responsibilities !== undefined) { | |
| 358 | const duties = responsibilitiesOf(changes.responsibilities); | |
| 359 | if (!duties.ok) return duties; | |
| 360 | next.responsibilities = duties.value; | |
| 361 | } | |
| An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how. | 362 | // A role left empty, or made from the title before, follows it. |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 363 | if (!next.role || (changes.role === undefined && roleDerived)) next.role = roleOf(next); |
| 364 | if (!next.role) return bad("Give the agent a title or a one-line role."); | |
| 365 | if (changes.avatar_seed !== undefined) { | |
| 366 | const seed = text(changes.avatar_seed, "The avatar seed", 64, false); | |
| 367 | if (!seed.ok) return seed; | |
| 368 | next.avatar_seed = seed.value; | |
| 369 | } | |
| 370 | // A new face from the handle, unless one was chosen; renaming keeps the face. | |
| 371 | if (!next.avatar_seed) next.avatar_seed = next.handle; | |
| Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent. | 372 | // A chosen face, part by part; null goes back to the seed's. |
| 373 | if (changes.look !== undefined) { | |
| 374 | if (changes.look === null) next.look = null; | |
| 375 | else { | |
| 376 | const look = readLook(changes.look); | |
| 377 | if (!look) return bad("A look names each part of the face (shape, color, eyes, mouth, antenna, accessory, pattern) with one of its choices."); | |
| 378 | next.look = look; | |
| 379 | } | |
| 380 | } | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 381 | if (changes.personality_preset !== undefined) { |
| 382 | if (!PRESETS.includes(changes.personality_preset)) return bad(`The personality preset is one of ${PRESETS.join(", ")}.`); | |
| 383 | next.personality_preset = changes.personality_preset; | |
| 384 | } | |
| 385 | const routing = routingOf(next.routing, changes.routing); | |
| 386 | if (!routing.ok) return routing; | |
| 387 | next.routing = routing.value; | |
| 388 | const budget = budgetOf(next.budget, changes.budget); | |
| 389 | if (!budget.ok) return budget; | |
| 390 | next.budget = budget.value; | |
| 391 | const autonomy = autonomyOf(next.autonomy, changes.autonomy); | |
| 392 | if (!autonomy.ok) return autonomy; | |
| 393 | next.autonomy = autonomy.value; | |
| 394 | if (changes.capacity !== undefined) { | |
| 395 | const capacity = changes.capacity; | |
| 396 | if (typeof capacity !== "number" || !Number.isInteger(capacity) || capacity < 1 || capacity > MAX_CAPACITY) { | |
| 397 | return bad(`Capacity is 1 to ${MAX_CAPACITY} tasks at once.`); | |
| 398 | } | |
| 399 | next.capacity = capacity; | |
| 400 | } | |
| 401 | if (changes.template !== undefined) { | |
| 402 | if (changes.template !== null && !templates.includes(changes.template)) return bad("There is no such template."); | |
| 403 | next.template = changes.template; | |
| 404 | } | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 405 | if (changes.subagents !== undefined) { |
| 406 | const subagents = subagentsOf(changes.subagents); | |
| 407 | if (!subagents.ok) return subagents; | |
| 408 | next.subagents = subagents.value; | |
| 409 | } | |
| Agents recall what Docs say before they answer or work, and each has required reading | 410 | if (changes.reading !== undefined) { |
| Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages. | 411 | if (!Array.isArray(changes.reading)) return bad("Required reading is a list of spaces."); |
| Agents recall what Docs say before they answer or work, and each has required reading | 412 | const ids = [...new Set(changes.reading.filter((id): id is string => typeof id === "string").map((id) => id.trim()).filter(Boolean))]; |
| 413 | if (ids.length > 10) return bad("An agent has at most 10 spaces of required reading."); | |
| Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages. | 414 | if (ids.some((id) => !/^[A-Za-z0-9_-]{1,80}$/.test(id))) return bad("That isn't a space."); |
| Agents recall what Docs say before they answer or work, and each has required reading | 415 | next.reading = ids; |
| 416 | } | |
| Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how. | 417 | if (changes.skills_off !== undefined) { |
| 418 | if (!Array.isArray(changes.skills_off)) return bad("Skills turned off are a list of skills."); | |
| 419 | const ids = new Set(changes.skills_off.filter((id): id is string => typeof id === "string").map((id) => id.trim()).filter(Boolean)); | |
| Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how. | 420 | // Foundational skills by id, and the library's by theirs (skl_…), which |
| 421 | // may be off before or after they are attached. | |
| 422 | const library = [...ids].filter((id) => LIBRARY_SKILL_ID.test(id)); | |
| 423 | const unknown = [...ids].find((id) => !FOUNDATIONAL_SKILL_IDS.includes(id) && !LIBRARY_SKILL_ID.test(id)); | |
| Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how. | 424 | if (unknown) return bad(`There is no skill called ${unknown.slice(0, 40)}.`); |
| Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how. | 425 | if (library.length > 200) return bad("At most 200 library skills can be off for one agent."); |
| Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how. | 426 | // In the skills' own order, so the same choice always reads the same. |
| Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how. | 427 | next.skills_off = [...FOUNDATIONAL_SKILL_IDS.filter((id) => ids.has(id)), ...library.sort()]; |
| Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how. | 428 | } |
| Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how. | 429 | // MCP servers change only through the service's own calls (index.ts), which pass them here; a change never carries them. |
| 430 | let servers = next.abilities.mcp_servers ?? []; | |
| 431 | if (options.mcp_servers) { | |
| 432 | const checked = mcpServersOf(options.mcp_servers); | |
| 433 | if (!checked.ok) return checked; | |
| 434 | servers = checked.value; | |
| 435 | } | |
| 436 | const abilities = abilitiesOf(next.abilities, changes.abilities, servers); | |
| 437 | if (!abilities.ok) return abilities; | |
| 438 | // A setting for a tool of a server that is gone goes with it. | |
| 439 | abilities.value.settings = Object.fromEntries(Object.entries(abilities.value.settings).filter(([id]) => abilityDef(id, servers))); | |
| 440 | next.abilities = abilities.value; | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 441 | if (changes.faces !== undefined) { |
| 442 | if (changes.faces === "customers") return bad("Customer-facing agents aren't available yet."); | |
| 443 | if (changes.faces !== "internal") return bad("An agent faces internal: the workspace's own people."); | |
| 444 | next.faces = "internal"; | |
| 445 | } | |
| 446 | // Never wider than their agent, whichever of the two changed. | |
| 447 | next.subagents = next.subagents.map((sub) => ({ ...sub, routing: withinParent(sub.routing, next.routing) })); | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 448 | return { ok: true, value: next }; |
| 449 | } | |
| 450 | ||
| 451 | /** A stored JSON column, read defensively: a bad value is the default. */ | |
| 452 | export function readJson<T extends object>(raw: unknown, fallback: T): T { | |
| 453 | if (typeof raw !== "string") return fallback; | |
| 454 | try { | |
| 455 | const parsed = JSON.parse(raw) as unknown; | |
| 456 | return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { ...fallback, ...(parsed as T) } : fallback; | |
| 457 | } catch { | |
| 458 | return fallback; | |
| 459 | } | |
| 460 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.