Skip to content
570 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Chat and workspace agents: channels, DMs and named agents you talk to1/**
2 * One reply: an agent answering a message in chat, in this Worker, with no
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.3 * sandbox.
Chat and workspace agents: channels, DMs and named agents you talk to4 *
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked5 * A reply is quick and bounded: it reads the latest messages of the
6 * conversation (never all of it), what the agent remembers for this place,
7 * and its recent sessions here. When a request needs real work, the agent
8 * spins off a session (sessions.ts) and says so; the session reports back.
9 * A message in a session's card thread is not a reply at all: it steers
10 * that session.
Chat and workspace agents: channels, DMs and named agents you talk to11 *
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked12 * A reply is metered exactly as a session step is (`metered`, meter.ts):
13 * the agent's and the workspace's agent budgets, model routing, the
14 * compute gate, a billing run, the model proxy, and the spend recorded.
Chat and workspace agents: channels, DMs and named agents you talk to15 *
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked16 * Whatever happens, the reply's row says so: replied, steered, blocked
17 * (with one short notice in the conversation, not repeated), skipped or
18 * failed (with one short apology).
Chat and workspace agents: channels, DMs and named agents you talk to19 */
Merge branch 'worktree-agent-a1398e81ad1a64c5f'20import { type AgentDelivery, type ServiceBinding, identityClient, newId } from "@g1t/contracts";
Chat and workspace agents: channels, DMs and named agents you talk to21
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.22import { type AbilityEnv, abilitiesFor, abilitiesSection, abilityPorts, saidText } from "./abilities.ts";
23
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)24import { CHAT_MAX_HOPS } from "../../../packages/contracts/src/chat.ts";
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked25import type { Tokens } from "./budget.ts";
Merge branch 'worktree-agent-a1398e81ad1a64c5f'26import { handOffPort } from "./handoff.ts";
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)27import { HISTORY_LIMIT, fixedHello, helloAsk, systemPrompt, turns } from "./prompt.ts";
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.28import { loadShelf, skillsSection, teamSlugs } from "./skills.ts";
29import { readVersion } from "./skill-library.ts";
Merge branch 'worktree-agent-a1398e81ad1a64c5f'30import { type Specialist, orchestratorInstructions, orchestratorTier, rosterLines } from "./orchestrator.ts";
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked31import { type MeterEnv, metered } from "./meter.ts";
32import { type RecallPlace, memorySection, recall } from "./memory.ts";
Agents recall what Docs say before they answer or work, and each has required reading33import { recallQuery, recallSection } from "./recall.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.34import { REPLY_TIER, allowedProviders, effortOf, effortPlan, replyModel } from "./routing.ts";
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked35import { type SessionEnv, type SessionRow, actionPorts, sessionRow, startSession, steer } from "./sessions.ts";
36import { type Row, definitionOf, periods, selectAgents, toAgent } from "./store.ts";
Chat and workspace agents: channels, DMs and named agents you talk to37import { type SurfaceMessage, surfaceFor } from "./surface.ts";
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)38import { Audience } from "./audience.ts";
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.39import { audiencePorts, loadTeams, teamsOfAgents, toolPorts } from "./ports.ts";
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)40import { type ToolCall, type ToolPorts, ToolBox } from "./tools.ts";
41import type { Surface } from "./surface.ts";
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked42import type { Desk } from "./desk.ts";
43import { type ModelMessage, type Send, NO_TOKENS, addTokens, runTurn } from "./turn.ts";
44import type { AgentRouting as Policy } from "../../runner/src/model-env.ts";
45import { dollars } from "./money.ts";
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.46import { teamsSection } from "./teammates.ts";
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.47import { personalRefusal } from "./access.ts";
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked48
49export { billingRepo } from "./meter.ts";
Chat and workspace agents: channels, DMs and named agents you talk to50
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked51export type ReplyEnv = MeterEnv & {
Chat and workspace agents: channels, DMs and named agents you talk to52 DB: D1Database;
53 CHAT: ServiceBinding;
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 /** People and their access, for the audience; members and teams for the roster. */
55 IDENTITY: ServiceBinding;
56 /** Code, for read tools. */
57 REPOS: ServiceBinding;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked58 /** Issues and pull requests, for read tools and filing issues. */
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)59 WORK: ServiceBinding;
60 /** Code search, for read tools. */
61 SEARCH: ServiceBinding;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked62 /** Notifications: a session waiting for more budget. */
63 NOTIFY?: ServiceBinding;
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.64 /** The workspace's connections, for abilities outside g1t (abilities.ts). */
65 INTEGRATIONS: ServiceBinding;
66 /** The audit log, for refusals. */
67 EVENTS: ServiceBinding;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked68 /** Every agent's desk: sessions are worked on their agent's. */
69 DESKS: DurableObjectNamespace<Desk>;
Chat and workspace agents: channels, DMs and named agents you talk to70};
71
72/** A notice that the agent cannot reply is posted once per conversation in this long. */
73const NOTICE_QUIET_MS = 6 * 60 * 60 * 1000;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked74/** Sessions one reply may start. */
75const MAX_SPIN_OFFS = 2;
Chat and workspace agents: channels, DMs and named agents you talk to76
77const APOLOGY = "Sorry, something went wrong on my side and I couldn't answer that. Try again in a moment.";
78
79/** Who asked, from the message that woke the agent (or their latest one). */
80function askerIn(history: SurfaceMessage[], delivery: AgentDelivery): SurfaceMessage["author"] | null {
81 const woken = history.find((m) => m.id === delivery.message_id);
82 if (woken) return woken.author;
83 return [...history].reverse().find((m) => m.author.kind === "user" && m.author.id === delivery.asked_by)?.author ?? null;
84}
85
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)86/**
87 * An agent's colleagues: every agent of the workspace but itself that is
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.88 * not archived (docs.g1t.sh/guides/agents/, "Agents know each other").
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)89 */
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.90async function team(db: D1Database, workspaceId: string, selfId: string, now: Date, teamsOf: Promise<Map<string, string[]>>): Promise<Specialist[]> {
91 const [rows, onTeams] = await Promise.all([
92 db
93 // Personal agents are their members' own: no colleague of anyone.
94 .prepare(`${selectAgents("a.workspace_id = ?3 AND a.archived_at IS NULL AND a.id <> ?4 AND a.scope = 'workspace'")} ORDER BY a.builtin DESC, a.handle LIMIT 50`)
95 .bind(...periods(now), workspaceId, selfId)
96 .all<Row>(),
97 teamsOf,
98 ]);
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)99 return rows.results.map((row) => {
100 const agent = toAgent(row, now);
101 return {
102 handle: agent.handle,
103 display_name: agent.display_name,
104 role: agent.role,
105 title: agent.title,
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.106 teams: onTeams.get(agent.id) ?? [],
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)107 responsibilities: agent.responsibilities,
108 status: agent.status,
109 spent_month_micros: agent.spent_month_micros,
110 monthly_micros: agent.budget.monthly_micros,
111 };
112 });
113}
114
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked115/** The agent's latest sessions in this conversation, one line each. */
116async function sessionsHere(db: D1Database, agentId: string, channelId: string): Promise<string | null> {
117 const rows = await db
118 .prepare(
119 "SELECT id, title, status, summary, created_at FROM agent_sessions WHERE agent_id = ? AND channel_id = ? AND parent_id IS NULL ORDER BY created_at DESC LIMIT 5",
120 )
121 .bind(agentId, channelId)
122 .all<{ id: string; title: string; status: string; summary: string | null; created_at: string }>();
123 if (!rows.results.length) return null;
124 return rows.results
125 .map((s) => `- "${s.title}" (${s.status}, ${s.created_at.slice(0, 10)})${s.summary ? `: ${s.summary.replace(/\s+/g, " ").slice(0, 300)}` : ""}`)
126 .join("\n");
127}
128
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)129/**
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.130 * Consulting a colleague:
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)131 * the colleague answers in a nested turn that posts nothing, with the same
132 * audience (so it can read no more than the conversation may), the same
133 * asker, one hop further, its own routing limits, on the same model
134 * session, billed to this reply. A compact card in the thread says who
135 * asked whom. Consults don't nest: the colleague can't consult in turn.
136 */
137function consulting(input: {
138 db: D1Database;
139 row: Row;
140 delivery: DeskWork;
141 send: Send;
142 policy: Policy;
143 ownModel: boolean;
144 sessionModel: string | null;
145 own: string | null;
146 ports: (consult: ToolPorts["consult"]) => ToolPorts;
147 hops: number;
148 now: Date;
149 surface: Surface;
150 asker: { name: string; display_name: string | null; access: NonNullable<AgentDelivery["asker"]> | null };
151 spent: { tokens: Tokens; cost: number };
152}): { ask: ToolPorts["consult"]; attach(box: ToolBox): void } {
153 let parent: ToolBox | null = null;
154 const noNesting: ToolPorts["consult"] = async () => ({ ok: false, message: "Consults don't nest: answer with what you have." });
155 const ask: ToolPorts["consult"] = async (handle, question) => {
156 const { row, delivery } = input;
157 const colleague = await input.db
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.158 .prepare("SELECT * FROM agents WHERE workspace_id = ? AND handle = ? AND archived_at IS NULL AND scope = 'workspace'")
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)159 .bind(row.workspace_id, handle)
160 .first<Row>();
161 if (!colleague || colleague.id === row.id) return { ok: false, message: `There is no other agent called @${handle} here.` };
162 // No ping-pong: never back to the agent that sent this work.
163 if ((delivery.chain ?? []).at(-1) === colleague.id) return { ok: false, message: `@${handle} sent you this work; answer with what you have.` };
164 if (input.hops + 1 > CHAT_MAX_HOPS) return { ok: false, message: "This request has been passed along too many times; answer with what you have." };
165 const definition = definitionOf(colleague);
166 const allowed = allowedProviders(definition.routing, input.own);
167 if (input.ownModel ? !allowed.own : !allowed.hosted) return { ok: false, message: `@${handle} can't use the model this conversation runs on.` };
168 const model = replyModel(
169 input.policy,
170 { ...definition.routing, pinned: input.ownModel ? definition.routing.pinned : null },
171 { named: input.ownModel ? input.sessionModel : null },
172 );
173 const tools = parent
174 ? parent.forColleague(input.ports(noNesting), { agentId: colleague.id, notConsult: [colleague.handle, row.handle], hops: input.hops + 1, maxHops: input.hops + 1 })
175 : null;
176 const system = systemPrompt({
177 agent: { ...definition, id: colleague.id },
178 workspace: delivery.workspace,
179 channel: { kind: delivery.channel_kind, name: delivery.channel_name },
180 asker: input.asker,
181 today: input.now,
182 tools: tools ? { code: tools.definitions().some((tool) => tool.name === "read_file") } : null,
183 consultedBy: row.handle,
184 });
185 const result = await runTurn(input.send, {
186 model: model.model,
187 system,
188 messages: [{ role: "user", content: `@${row.handle} (agent) asks you: ${question}` }],
189 tools,
190 price: input.ownModel ? null : model.price,
191 });
192 input.spent.tokens = addTokens(input.spent.tokens, result.tokens);
193 input.spent.cost += input.ownModel ? 0 : result.cost;
194 const answer = result.text || "(no answer)";
195 const exchange = `Q: ${question}\nA: ${answer}`;
196 // The exchange, collapsed, in the thread. No @ in its text: it wakes nobody.
197 await input.surface
198 .post(`${row.display_name} asked ${colleague.display_name}`, {
199 kind: "consult",
200 title: `${row.display_name} asked ${colleague.display_name}`,
201 detail: exchange.length > 1000 ? `${exchange.slice(0, 1000)}…` : exchange,
202 state: null,
203 href: null,
204 })
205 .catch((error: unknown) => console.error("agents: a consult card was not posted", String(error)));
206 return { ok: true, colleague: colleague.handle, answer };
207 };
208 return {
209 ask,
210 attach(box) {
211 parent = box;
212 },
213 };
214}
215
Chat and workspace agents: channels, DMs and named agents you talk to216type Outcome = {
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked217 status: "replied" | "steered" | "blocked" | "skipped" | "failed";
Chat and workspace agents: channels, DMs and named agents you talk to218 error?: string | null;
219 reply_id?: string | null;
220 model?: string | null;
221 tier?: string | null;
222 tokens?: Tokens;
223 cost?: number;
224 charged?: number;
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.225 /** The effort level it ran at. */
226 effort?: string | null;
Chat and workspace agents: channels, DMs and named agents you talk to227};
228
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)229/**
230 * What a desk is handed: a message to answer, or (`hello`) the agent's
231 * first message to the person who made it, in the DM that just opened.
232 */
233export type DeskWork = AgentDelivery & { hello?: boolean };
234
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked235/** How a limit's refusal reads in chat, in the agent's voice. */
236function noticeFor(reason: string, message: string): string {
237 if (reason.startsWith("budget_") || reason === "workspace_agent_budget" || message.startsWith("My settings") || message.startsWith("I ")) return message;
238 if (reason === "no_model") return `I can't reply yet: ${message}`;
239 return `I can't reply right now: ${message}`;
240}
241
242/** The session a message in this thread is for: a reply under one of this agent's session cards. */
243async function steeredSession(db: D1Database, agentId: string, threadRoot: string | null): Promise<SessionRow | null> {
244 if (!threadRoot) return null;
245 const found = await db.prepare("SELECT id FROM agent_sessions WHERE agent_id = ? AND card_message_id = ?").bind(agentId, threadRoot).first<{ id: string }>();
246 return found ? sessionRow(db, found.id) : null;
247}
248
249/**
250 * Answers `delivery` as its agent. Never throws: every way it ends is
251 * recorded on the reply's row. A message handed over twice is answered
252 * once.
253 */
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)254export async function reply(env: ReplyEnv, delivery: DeskWork, now = new Date()): Promise<void> {
Chat and workspace agents: channels, DMs and named agents you talk to255 const db = env.DB;
256 const row = await db.prepare("SELECT * FROM agents WHERE id = ?").bind(delivery.agent_id).first<Row>();
257 if (!row || row.archived_at || row.workspace_id !== delivery.workspace_id) return;
258 const id = newId("arp", now.getTime());
259 const claimed = await db
260 .prepare(
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked261 `INSERT INTO agent_replies (id, agent_id, workspace_id, channel_id, message_id, asked_by, asked_by_username, channel_name, agent_version, status, created_at)
262 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'working', ?)
Chat and workspace agents: channels, DMs and named agents you talk to263 ON CONFLICT (agent_id, message_id) DO NOTHING RETURNING id`,
264 )
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked265 .bind(id, row.id, row.workspace_id, delivery.channel_id, delivery.message_id, delivery.asked_by, delivery.asker?.username ?? null, delivery.channel_name, row.version, now.toISOString())
Chat and workspace agents: channels, DMs and named agents you talk to266 .first<{ id: string }>();
267 if (!claimed) return;
268
269 const surface = surfaceFor(env.CHAT, delivery);
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 // 👀 as soon as the desk has it, while everything else goes on.
271 const acknowledging = surface.acknowledge();
Chat and workspace agents: channels, DMs and named agents you talk to272 const slug = delivery.workspace.toLowerCase();
273
274 // What the answer used, once there is one: counted however the reply ends.
275 let usage: Partial<Outcome> = {};
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked276 // What the tools did, for the audit table, and who the audience was.
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)277 let toolCalls: ToolCall[] = [];
278 let audienceHash: string | null = null;
Chat and workspace agents: channels, DMs and named agents you talk to279
280 const finish = async (outcome: Outcome) => {
281 const statements = [
282 db
283 .prepare(
284 `UPDATE agent_replies SET status = ?, error = ?, reply_id = ?, model = ?, tier = ?, input_tokens = ?, output_tokens = ?,
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.285 cost_micros = ?, charged_micros = ?, tool_count = ?, finished_at = ?, effort = ? WHERE id = ?`,
Chat and workspace agents: channels, DMs and named agents you talk to286 )
287 .bind(
288 outcome.status,
289 outcome.error?.slice(0, 1000) ?? null,
290 outcome.reply_id ?? null,
291 outcome.model ?? null,
292 outcome.tier ?? null,
293 (outcome.tokens?.input ?? 0) + (outcome.tokens?.cacheRead ?? 0) + (outcome.tokens?.cacheWrite ?? 0),
294 outcome.tokens?.output ?? 0,
295 outcome.cost ?? 0,
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked296 outcome.charged ?? 0,
297 toolCalls.length,
Chat and workspace agents: channels, DMs and named agents you talk to298 new Date().toISOString(),
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.299 outcome.effort ?? null,
Chat and workspace agents: channels, DMs and named agents you talk to300 id,
301 ),
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)302 // Every tool call: what was asked, for whom, and whether it was read or withheld.
303 ...toolCalls.map((call, n) =>
304 db
305 .prepare(
306 `INSERT INTO agent_tool_calls (id, reply_id, agent_id, workspace_id, asked_by, tool, args, audience_hash, outcome, bytes, created_at)
307 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
308 )
309 .bind(`${id}_${n}`, id, row.id, row.workspace_id, delivery.asked_by, call.tool, call.args, audienceHash ?? "", call.outcome, call.bytes, now.toISOString()),
310 ),
Chat and workspace agents: channels, DMs and named agents you talk to311 ];
312 await db.batch(statements);
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)313 // ✅ when it answered; 👀 taken back after a notice, an apology or nothing.
314 await acknowledging;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked315 await surface.settle(outcome.status === "replied" || outcome.status === "steered" ? "done" : "withdrawn");
Chat and workspace agents: channels, DMs and named agents you talk to316 };
317
318 /** Says once, in this conversation, why the agent cannot answer; a repeat within hours is kept back. */
319 const notice = async (message: string, reason: string) => {
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)320 // An agent's first words are never an excuse: without a model, a fixed hello.
321 if (delivery.hello) {
322 const posted = await surface.post(fixedHello(row, delivery.asker?.username ?? null)).catch(() => null);
323 return await finish({ status: "blocked", error: reason, reply_id: posted });
324 }
Chat and workspace agents: channels, DMs and named agents you talk to325 const since = new Date(now.getTime() - NOTICE_QUIET_MS).toISOString();
326 const recent = await db
327 .prepare(
328 `SELECT 1 FROM agent_replies WHERE agent_id = ? AND channel_id = ? AND status = 'blocked' AND error = ?
329 AND reply_id IS NOT NULL AND created_at > ? AND id <> ? LIMIT 1`,
330 )
331 .bind(row.id, delivery.channel_id, reason, since, id)
332 .first();
333 const posted = recent ? null : await surface.post(message).catch(() => null);
334 await finish({ status: "blocked", error: reason, reply_id: posted });
335 };
336
337 try {
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked338 // A reply under one of its session cards steers that session; nothing is answered here.
339 const steered = delivery.hello ? null : await steeredSession(db, row.id, delivery.thread_root);
340 if (steered) {
341 const history = await surface.history(HISTORY_LIMIT).catch(() => [] as SurfaceMessage[]);
342 const message = history.find((m) => m.id === delivery.message_id);
343 if (message?.body.trim()) {
344 await steer(env as unknown as SessionEnv, steered, message.author.name, message.body);
345 return await finish({ status: "steered" });
346 }
Chat and workspace agents: channels, DMs and named agents you talk to347 }
348
349 // Read the conversation while showing that the agent is on 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)350 // A hello has no conversation yet: it is asked to introduce itself.
Merge branch 'worktree-agent-a1398e81ad1a64c5f'351 // And who is here: said every turn, so the agent knows who reads it and who doesn't.
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.352 // And its teams, from their pages: who leads, who owns what, who is around.
353 const [, history, conversationHere, teamsHere] = await Promise.all([
Merge branch 'worktree-agent-a1398e81ad1a64c5f'354 surface.typing(),
355 delivery.hello ? Promise.resolve([]) : surface.history(HISTORY_LIMIT),
356 delivery.hello ? Promise.resolve(null) : surface.conversation(),
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.357 loadTeams(env, slug, row.workspace_id, { id: row.id }).catch(() => null),
Merge branch 'worktree-agent-a1398e81ad1a64c5f'358 ]);
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.359 // A personal agent answers only its member, in the DM of the two of them (access.ts).
360 const refused = personalRefusal(row, {
361 asked_by: delivery.asked_by,
362 channel_kind: delivery.channel_kind,
363 members: conversationHere ? conversationHere.people + conversationHere.agents : null,
364 });
365 if (refused) return await notice(refused, "personal_agent");
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)366 const conversation = delivery.hello ? [{ role: "user" as const, content: helloAsk(delivery.asker?.username ?? null) }] : turns(history, row.id);
Chat and workspace agents: channels, DMs and named agents you talk to367 if (!conversation.length) return await finish({ status: "skipped", error: "nothing to answer" });
368 const author = askerIn(history, delivery);
369 const askerName = delivery.asker?.username ?? author?.name ?? 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)370 // Every agent knows its colleagues; @g1t also steps up a tier to decide
371 // who gets the work in a long thread.
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.372 const specialists = await team(db, row.workspace_id, row.id, now, teamsOfAgents(env, slug));
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked373 const definition = definitionOf(row);
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.374 // Its effort setting decides where the reply starts and how hard the model reasons.
375 const plan = effortPlan(effortOf(definition.routing), row.builtin ? orchestratorTier(history.length, specialists.filter((a) => a.handle !== "g1t").length) : REPLY_TIER);
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)376 const hops = Math.max(0, Math.floor(delivery.hops || 0));
377 const chain = delivery.chain ?? [];
378 const sender = chain.length ? await db.prepare("SELECT handle FROM agents WHERE id = ?").bind(chain[chain.length - 1]).first<{ handle: string }>() : null;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked379 const asker = { name: askerName ?? "someone", display_name: author?.display_name ?? null, access: delivery.asker ?? null };
380 let posted: string | null = null;
381 let spinOffs = 0;
382
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.383 const done = await metered(env, { row, payer: row, slug, task: "reply", start: plan.start, effort: plan.level, askerName, person: delivery.asker?.username ?? null, teams: teamsHere }, async (model) => {
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked384 // What colleagues consulted along the way used: billed to this reply.
385 const consulted = { tokens: NO_TOKENS, cost: 0 };
386 let toolbox: ToolBox | null = null;
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.387 // The "Your abilities outside g1t" section, once the tool box has them.
388 let abilitiesText: string | null = null;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked389 let place: RecallPlace = { channel_id: delivery.channel_id, kind: delivery.channel_kind === "dm" ? "dm" : "private", people: [delivery.asked_by] };
390 if (!delivery.hello) {
391 try {
392 const audience = await Audience.build(slug, delivery.asked_by, audiencePorts(env, slug, delivery.channel_id));
393 place = { channel_id: delivery.channel_id, kind: audience.kind, people: audience.shared ? [delivery.asked_by] : audience.members.map((m) => m.id) };
Docs: a workspace knowledge base people and agents write together394 const ports = (consult: ToolPorts["consult"]) => toolPorts(env, slug, row.workspace_id, delivery.channel_id, consult, row.id);
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked395 audienceHash = audience.hash;
396 const consult = consulting({
397 db,
398 row,
399 delivery,
400 send: model.send,
401 policy: model.policy,
402 ownModel: model.ownModel,
403 sessionModel: model.sessionModel,
404 own: model.own,
405 ports,
406 hops,
407 now,
408 surface,
409 asker,
410 spent: consulted,
411 });
412 const where = delivery.channel_kind === "dm" ? "a direct message" : `#${delivery.channel_name ?? "a channel"}`;
413 const actions = actionPorts(env as unknown as SessionEnv, {
414 agent: row,
415 place,
416 source: { kind: "message", ref: delivery.message_id, label: askerName ? `@${askerName} in ${where}` : where, channel_id: delivery.channel_id },
417 asker: { id: delivery.asked_by, username: askerName },
418 workspace: slug,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar419 postCard: (card) => surface.post("", card).catch(() => null),
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked420 // Real work becomes a session, with its card in this conversation.
421 spinOff: async (title, goal) => {
422 if (spinOffs >= MAX_SPIN_OFFS) return { ok: false, message: "You've started enough sessions from this message." };
423 spinOffs++;
424 const session = await startSession(env as unknown as SessionEnv, {
425 agent: row,
426 kind: "chat",
427 title,
428 goal: `${askerName ? `@${askerName}` : "Someone"} asked in ${where}:\n\n${goal}`,
429 workspace: slug,
430 channel_id: delivery.channel_id,
431 channel_kind: delivery.channel_kind,
432 channel_name: delivery.channel_name,
433 thread_root: delivery.thread_root,
434 message_id: delivery.message_id,
435 asked_by: delivery.asked_by,
436 asked_by_username: askerName,
437 asker: delivery.asker ?? null,
438 chain: [...chain],
439 hops,
440 });
441 const cap = session.cap_micros ? ` with a cap of ${dollars(session.cap_micros)}` : "";
442 return { ok: true, message: `Started the session "${session.title}"${cap}. Its card is in the conversation and it reports back there. Tell them in a sentence; don't do the work here.` };
443 },
444 });
Merge branch 'worktree-agent-a1398e81ad1a64c5f'445 // Work handed to a colleague: the only way an agent gets another working (handoff.ts).
446 const viewer = audience.asker;
447 actions.handOff = handOffPort({
448 self: { id: row.id, handle: row.handle },
449 chain,
450 asker: delivery.asker ?? null,
451 agent: async (handle) => {
452 const found = await db
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.453 .prepare(selectAgents("a.workspace_id = ?3 AND a.handle = ?4 AND a.archived_at IS NULL AND a.scope = 'workspace'"))
Merge branch 'worktree-agent-a1398e81ad1a64c5f'454 .bind(...periods(now), row.workspace_id, handle)
455 .first<Row>();
456 if (!found) return null;
457 const agent = toAgent(found, now);
458 return { id: agent.id, handle: agent.handle, display_name: agent.display_name, builtin: !!found.builtin, status: agent.status };
459 },
460 person: async (handle) => {
461 if (!viewer) return false;
462 const members = await identityClient(env.IDENTITY).listMembers(slug, viewer);
463 return members.ok && members.value.some((m) => m.username.toLowerCase() === handle);
464 },
465 handOff: (colleagueId, brief) => surface.handOff(colleagueId, brief),
466 });
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked467 toolbox = new ToolBox(
468 audience,
469 ports(consult.ask),
470 { agentId: row.id, notConsult: [row.handle, ...(sender ? [sender.handle] : [])], hops, maxHops: CHAT_MAX_HOPS },
471 [],
472 actions,
473 );
474 consult.attach(toolbox);
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.475 // Its abilities outside g1t (abilities.ts): offered and enforced by the tool box, for the person it acts for.
476 if (viewer && !delivery.hello) {
477 const sections = await abilitiesFor(env as unknown as AbilityEnv, { agent: row, definition, workspace: slug, asker: viewer }).catch(() => null);
478 if (sections) {
479 const saidByPeople = saidText([...history].reverse().filter((m) => m.author.kind === "user").slice(0, 3).map((m) => m.body));
480 toolbox.useAbilities(
481 sections,
482 abilityPorts(env as unknown as AbilityEnv, {
483 agent: row,
484 workspace: slug,
485 channel_id: delivery.channel_id,
486 session: null,
487 asker: { id: delivery.asked_by, username: askerName },
488 postCard: (card) => surface.post("", card).catch(() => null),
489 }),
490 saidByPeople,
491 definition.abilities.mcp_servers,
492 );
493 abilitiesText = abilitiesSection(sections);
494 }
495 }
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked496 } catch (error) {
497 // Without an audience nothing may be read: the reply goes on with this conversation only.
498 console.error("agents: no audience for a reply, so no tools", row.id, String(error));
499 }
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)500 }
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.501 // What people said last, for recalling what the workspace's artifacts say about it.
Agents recall what Docs say before they answer or work, and each has required reading502 const said = [...history].reverse().filter((m) => m.author.kind === "user").slice(0, 3).map((m) => m.body);
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.503 const [facts, recent, passages, shelf] = delivery.hello
504 ? [[], null, [], []]
Agents recall what Docs say before they answer or work, and each has required reading505 : await Promise.all([
506 recall(db, row.id, place).catch(() => []),
507 sessionsHere(db, row.id, delivery.channel_id).catch(() => null),
508 toolbox ? toolbox.recall(recallQuery(said), definition.reading ?? []) : Promise.resolve([]),
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.509 // Its skills: named in the prompt, read with use_skill (skills.ts).
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.510 toolbox ? loadShelf(db, row.workspace_id, { id: row.id, skills_off: definition.skills_off }, teamSlugs(teamsHere)) : Promise.resolve([]),
Agents recall what Docs say before they answer or work, and each has required reading511 ]);
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.512 toolbox?.useShelf(shelf, (skillId, version) => readVersion(db, skillId, version));
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked513 const system = [
514 systemPrompt({
515 agent: {
516 ...definition,
517 id: row.id,
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.518 teams: teamsHere?.teams.map((t) => t.name) ?? [],
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked519 // @g1t's job is fixed; what the workspace wrote is added to it.
520 instructions: row.builtin ? orchestratorInstructions(specialists.filter((a) => a.handle !== "g1t"), definition.instructions) : definition.instructions,
521 },
522 workspace: delivery.workspace,
523 channel: { kind: delivery.channel_kind, name: delivery.channel_name },
524 asker,
525 today: now,
526 tools: toolbox ? { code: toolbox.definitions().some((tool) => tool.name === "read_file") } : null,
527 // @g1t's team is in its job; everyone else is told who their colleagues are.
528 colleagues: row.builtin ? null : rosterLines(specialists),
529 recentSessions: recent,
Merge branch 'worktree-agent-a1398e81ad1a64c5f'530 conversation: conversationHere,
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.531 teams: teamsSection(row.id, teamsHere, now),
Merge branch 'worktree-agent-a1398e81ad1a64c5f'532 canHandOff: !!toolbox?.definitions().some((tool) => tool.name === "hand_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.533 abilities: abilitiesText,
Merge branch 'worktree-agent-a1398e81ad1a64c5f'534 handedOffBy: sender?.handle ?? null,
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.535 skills: skillsSection(shelf, toolbox?.definitions().map((tool) => tool.name) ?? []),
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked536 }),
537 memorySection(facts),
Agents recall what Docs say before they answer or work, and each has required reading538 recallSection(passages),
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked539 ]
540 .filter(Boolean)
541 .join("\n\n");
542 toolCalls = toolbox?.calls ?? [];
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.543 const answer = await runTurn(model.send, { model: model.model.model, system, messages: conversation as ModelMessage[], tools: toolbox, price: model.ownModel ? null : model.model.price, effort: model.effort });
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked544 // Post as soon as there is an answer; the bill is settled after.
Merge branch 'worktree-agent-a1398e81ad1a64c5f'545 // It wakes nobody, @mentions or not: colleagues get work only through hand_off.
546 if (answer.text) posted = await surface.post(answer.text);
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked547 return {
548 text: answer.text,
549 tokens: addTokens(answer.tokens, consulted.tokens),
550 cost: (model.ownModel ? 0 : answer.cost) + consulted.cost,
551 rounds: answer.rounds,
552 };
Chat and workspace agents: channels, DMs and named agents you talk to553 });
554
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked555 if (!done.ok) return await notice(noticeFor(done.reason, done.message), done.reason);
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.556 usage = { model: done.model, tier: done.tier, tokens: done.tokens, cost: done.cost, charged: done.charged, effort: plan.level };
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked557 if (!done.value.text) {
558 const apology = await surface.post(APOLOGY).catch(() => null);
559 return await finish({ status: "failed", error: "the model gave no text", reply_id: apology, ...usage });
Chat and workspace agents: channels, DMs and named agents you talk to560 }
561 await finish({ status: "replied", reply_id: posted, ...usage });
562 } catch (error) {
563 const message = error instanceof Error ? error.message : String(error);
564 console.error("agents: a reply failed", row.id, delivery.message_id, message);
565 const posted = await surface.post(APOLOGY).catch(() => null);
566 await finish({ status: "failed", error: message, reply_id: posted, ...usage }).catch((failure: unknown) =>
567 console.error("agents: a failed reply was not recorded", id, String(failure)),
568 );
569 }
570}

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