Skip to content
583 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";
@g1t welcomes you when you join a workspace, and that is the one introduction any agent makes. Identity now says when a person becomes a member, workspace.member_joined, by invitation, by an invite code, or by creating the workspace, and webhooks can hear it; the agents service hears it too, and for anyone who joined, never the creator, the workspace's @g1t opens your direct message and says in its own voice who it is, what to ask it, how agents answer, and where things are, once per person per workspace and charged to @g1t like any reply, with a fixed welcome when no model can be used. The agents, workspaces, authentication and webhooks guides say so.27import { HISTORY_LIMIT, fixedWelcome, lastAddressed, systemPrompt, turns, welcomeAsk } 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";
In a group message with several agents, a message that names no agent goes to the one you are talking with: the agents that answered you last, else the ones you last mentioned, else, before anyone has been addressed, all of them; a message that mentions agents still goes only to those, and a conversation with one agent is as it was. Each agent is also told, when several are here, who you are talking with, so one that is woken alongside answers only what is clearly for it and does not redo a colleague's work. And an agent shows as typing for as long as it works, not for eight seconds: its desk says so every five seconds until the answer is posted, and after ten seconds the line reads that it is still working on it. The chat and agents guides say so.37import { type SurfaceMessage, keepTyping, 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
@g1t welcomes you when you join a workspace, and that is the one introduction any agent makes. Identity now says when a person becomes a member, workspace.member_joined, by invitation, by an invite code, or by creating the workspace, and webhooks can hear it; the agents service hears it too, and for anyone who joined, never the creator, the workspace's @g1t opens your direct message and says in its own voice who it is, what to ask it, how agents answer, and where things are, once per person per workspace and charged to @g1t like any reply, with a fixed welcome when no model can be used. The agents, workspaces, authentication and webhooks guides say so.229/**
230 * What a desk is handed: a message to answer, or (`welcome`) a word for a
231 * person who just joined the workspace, in the direct message that was
232 * opened for it (welcome.ts). Only the built-in @g1t honours `welcome`:
233 * no other agent speaks first.
234 */
235export type DeskWork = AgentDelivery & { welcome?: boolean };
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)236
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 asked237/** How a limit's refusal reads in chat, in the agent's voice. */
238function noticeFor(reason: string, message: string): string {
239 if (reason.startsWith("budget_") || reason === "workspace_agent_budget" || message.startsWith("My settings") || message.startsWith("I ")) return message;
240 if (reason === "no_model") return `I can't reply yet: ${message}`;
241 return `I can't reply right now: ${message}`;
242}
243
244/** The session a message in this thread is for: a reply under one of this agent's session cards. */
245async function steeredSession(db: D1Database, agentId: string, threadRoot: string | null): Promise<SessionRow | null> {
246 if (!threadRoot) return null;
247 const found = await db.prepare("SELECT id FROM agent_sessions WHERE agent_id = ? AND card_message_id = ?").bind(agentId, threadRoot).first<{ id: string }>();
248 return found ? sessionRow(db, found.id) : null;
249}
250
251/**
252 * Answers `delivery` as its agent. Never throws: every way it ends is
253 * recorded on the reply's row. A message handed over twice is answered
254 * once.
255 */
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)256export async function reply(env: ReplyEnv, delivery: DeskWork, now = new Date()): Promise<void> {
Chat and workspace agents: channels, DMs and named agents you talk to257 const db = env.DB;
258 const row = await db.prepare("SELECT * FROM agents WHERE id = ?").bind(delivery.agent_id).first<Row>();
259 if (!row || row.archived_at || row.workspace_id !== delivery.workspace_id) return;
@g1t welcomes you when you join a workspace, and that is the one introduction any agent makes. Identity now says when a person becomes a member, workspace.member_joined, by invitation, by an invite code, or by creating the workspace, and webhooks can hear it; the agents service hears it too, and for anyone who joined, never the creator, the workspace's @g1t opens your direct message and says in its own voice who it is, what to ask it, how agents answer, and where things are, once per person per workspace and charged to @g1t like any reply, with a fixed welcome when no model can be used. The agents, workspaces, authentication and webhooks guides say so.260 // A welcome is @g1t's alone: any other desk handed one says nothing.
261 const welcome = !!delivery.welcome;
262 if (welcome && !row.builtin) return;
Chat and workspace agents: channels, DMs and named agents you talk to263 const id = newId("arp", now.getTime());
264 const claimed = await db
265 .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 asked266 `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)
267 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'working', ?)
Chat and workspace agents: channels, DMs and named agents you talk to268 ON CONFLICT (agent_id, message_id) DO NOTHING RETURNING id`,
269 )
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 asked270 .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 to271 .first<{ id: string }>();
272 if (!claimed) return;
273
274 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)275 // 👀 as soon as the desk has it, while everything else goes on.
276 const acknowledging = surface.acknowledge();
Chat and workspace agents: channels, DMs and named agents you talk to277 const slug = delivery.workspace.toLowerCase();
278
279 // What the answer used, once there is one: counted however the reply ends.
280 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 asked281 // 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)282 let toolCalls: ToolCall[] = [];
283 let audienceHash: string | null = null;
In a group message with several agents, a message that names no agent goes to the one you are talking with: the agents that answered you last, else the ones you last mentioned, else, before anyone has been addressed, all of them; a message that mentions agents still goes only to those, and a conversation with one agent is as it was. Each agent is also told, when several are here, who you are talking with, so one that is woken alongside answers only what is clearly for it and does not redo a colleague's work. And an agent shows as typing for as long as it works, not for eight seconds: its desk says so every five seconds until the answer is posted, and after ten seconds the line reads that it is still working on it. The chat and agents guides say so.284 // Stops the typing heartbeat (surface.ts `keepTyping`), once the reply reads the conversation.
285 let stopTyping: () => void = () => undefined;
Chat and workspace agents: channels, DMs and named agents you talk to286
287 const finish = async (outcome: Outcome) => {
In a group message with several agents, a message that names no agent goes to the one you are talking with: the agents that answered you last, else the ones you last mentioned, else, before anyone has been addressed, all of them; a message that mentions agents still goes only to those, and a conversation with one agent is as it was. Each agent is also told, when several are here, who you are talking with, so one that is woken alongside answers only what is clearly for it and does not redo a colleague's work. And an agent shows as typing for as long as it works, not for eight seconds: its desk says so every five seconds until the answer is posted, and after ten seconds the line reads that it is still working on it. The chat and agents guides say so.288 stopTyping();
Chat and workspace agents: channels, DMs and named agents you talk to289 const statements = [
290 db
291 .prepare(
292 `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.293 cost_micros = ?, charged_micros = ?, tool_count = ?, finished_at = ?, effort = ? WHERE id = ?`,
Chat and workspace agents: channels, DMs and named agents you talk to294 )
295 .bind(
296 outcome.status,
297 outcome.error?.slice(0, 1000) ?? null,
298 outcome.reply_id ?? null,
299 outcome.model ?? null,
300 outcome.tier ?? null,
301 (outcome.tokens?.input ?? 0) + (outcome.tokens?.cacheRead ?? 0) + (outcome.tokens?.cacheWrite ?? 0),
302 outcome.tokens?.output ?? 0,
303 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 asked304 outcome.charged ?? 0,
305 toolCalls.length,
Chat and workspace agents: channels, DMs and named agents you talk to306 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.307 outcome.effort ?? null,
Chat and workspace agents: channels, DMs and named agents you talk to308 id,
309 ),
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 // Every tool call: what was asked, for whom, and whether it was read or withheld.
311 ...toolCalls.map((call, n) =>
312 db
313 .prepare(
314 `INSERT INTO agent_tool_calls (id, reply_id, agent_id, workspace_id, asked_by, tool, args, audience_hash, outcome, bytes, created_at)
315 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
316 )
317 .bind(`${id}_${n}`, id, row.id, row.workspace_id, delivery.asked_by, call.tool, call.args, audienceHash ?? "", call.outcome, call.bytes, now.toISOString()),
318 ),
Chat and workspace agents: channels, DMs and named agents you talk to319 ];
320 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)321 // ✅ when it answered; 👀 taken back after a notice, an apology or nothing.
322 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 asked323 await surface.settle(outcome.status === "replied" || outcome.status === "steered" ? "done" : "withdrawn");
Chat and workspace agents: channels, DMs and named agents you talk to324 };
325
326 /** Says once, in this conversation, why the agent cannot answer; a repeat within hours is kept back. */
327 const notice = async (message: string, reason: string) => {
@g1t welcomes you when you join a workspace, and that is the one introduction any agent makes. Identity now says when a person becomes a member, workspace.member_joined, by invitation, by an invite code, or by creating the workspace, and webhooks can hear it; the agents service hears it too, and for anyone who joined, never the creator, the workspace's @g1t opens your direct message and says in its own voice who it is, what to ask it, how agents answer, and where things are, once per person per workspace and charged to @g1t like any reply, with a fixed welcome when no model can be used. The agents, workspaces, authentication and webhooks guides say so.328 // A welcome is never an excuse: without a model, a fixed one.
329 if (welcome) {
330 const posted = await surface.post(fixedWelcome(delivery.asker?.username ?? null, delivery.workspace)).catch(() => null);
331 return await finish({ status: "blocked", error: reason, reply_id: posted });
332 }
Chat and workspace agents: channels, DMs and named agents you talk to333 const since = new Date(now.getTime() - NOTICE_QUIET_MS).toISOString();
334 const recent = await db
335 .prepare(
336 `SELECT 1 FROM agent_replies WHERE agent_id = ? AND channel_id = ? AND status = 'blocked' AND error = ?
337 AND reply_id IS NOT NULL AND created_at > ? AND id <> ? LIMIT 1`,
338 )
339 .bind(row.id, delivery.channel_id, reason, since, id)
340 .first();
341 const posted = recent ? null : await surface.post(message).catch(() => null);
342 await finish({ status: "blocked", error: reason, reply_id: posted });
343 };
344
345 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 asked346 // A reply under one of its session cards steers that session; nothing is answered here.
A new agent says nothing until you do. Creating one still opens your direct message with it, but no introduction is written there, nothing is charged to its budget and nothing waits in Chat; the desk's special first message, its fixed fallback and the prompt that asked for it are gone. The agents guide says so.347 const steered = await steeredSession(db, row.id, delivery.thread_root);
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 asked348 if (steered) {
349 const history = await surface.history(HISTORY_LIMIT).catch(() => [] as SurfaceMessage[]);
350 const message = history.find((m) => m.id === delivery.message_id);
351 if (message?.body.trim()) {
352 await steer(env as unknown as SessionEnv, steered, message.author.name, message.body);
353 return await finish({ status: "steered" });
354 }
Chat and workspace agents: channels, DMs and named agents you talk to355 }
356
In a group message with several agents, a message that names no agent goes to the one you are talking with: the agents that answered you last, else the ones you last mentioned, else, before anyone has been addressed, all of them; a message that mentions agents still goes only to those, and a conversation with one agent is as it was. Each agent is also told, when several are here, who you are talking with, so one that is woken alongside answers only what is clearly for it and does not redo a colleague's work. And an agent shows as typing for as long as it works, not for eight seconds: its desk says so every five seconds until the answer is posted, and after ten seconds the line reads that it is still working on it. The chat and agents guides say so.357 // Read the conversation while showing that the agent is on it, and keep
358 // showing it until the answer is posted or the reply ends another way.
Merge branch 'worktree-agent-a1398e81ad1a64c5f'359 // 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.360 // And its teams, from their pages: who leads, who owns what, who is around.
In a group message with several agents, a message that names no agent goes to the one you are talking with: the agents that answered you last, else the ones you last mentioned, else, before anyone has been addressed, all of them; a message that mentions agents still goes only to those, and a conversation with one agent is as it was. Each agent is also told, when several are here, who you are talking with, so one that is woken alongside answers only what is clearly for it and does not redo a colleague's work. And an agent shows as typing for as long as it works, not for eight seconds: its desk says so every five seconds until the answer is posted, and after ten seconds the line reads that it is still working on it. The chat and agents guides say so.361 stopTyping = keepTyping(surface);
362 const [history, conversationHere, teamsHere] = await Promise.all([
A new agent says nothing until you do. Creating one still opens your direct message with it, but no introduction is written there, nothing is charged to its budget and nothing waits in Chat; the desk's special first message, its fixed fallback and the prompt that asked for it are gone. The agents guide says so.363 surface.history(HISTORY_LIMIT),
364 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.365 loadTeams(env, slug, row.workspace_id, { id: row.id }).catch(() => null),
Merge branch 'worktree-agent-a1398e81ad1a64c5f'366 ]);
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.367 // A personal agent answers only its member, in the DM of the two of them (access.ts).
368 const refused = personalRefusal(row, {
369 asked_by: delivery.asked_by,
370 channel_kind: delivery.channel_kind,
371 members: conversationHere ? conversationHere.people + conversationHere.agents : null,
372 });
373 if (refused) return await notice(refused, "personal_agent");
@g1t welcomes you when you join a workspace, and that is the one introduction any agent makes. Identity now says when a person becomes a member, workspace.member_joined, by invitation, by an invite code, or by creating the workspace, and webhooks can hear it; the agents service hears it too, and for anyone who joined, never the creator, the workspace's @g1t opens your direct message and says in its own voice who it is, what to ask it, how agents answer, and where things are, once per person per workspace and charged to @g1t like any reply, with a fixed welcome when no model can be used. The agents, workspaces, authentication and webhooks guides say so.374 // A welcome has no conversation yet: @g1t is asked to welcome the person.
375 const conversation = welcome ? [{ role: "user" as const, content: welcomeAsk(delivery.asker?.username ?? null, delivery.workspace) }] : turns(history, row.id);
Chat and workspace agents: channels, DMs and named agents you talk to376 if (!conversation.length) return await finish({ status: "skipped", error: "nothing to answer" });
377 const author = askerIn(history, delivery);
378 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)379 // Every agent knows its colleagues; @g1t also steps up a tier to decide
380 // 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.381 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 asked382 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.383 // Its effort setting decides where the reply starts and how hard the model reasons.
384 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)385 const hops = Math.max(0, Math.floor(delivery.hops || 0));
386 const chain = delivery.chain ?? [];
387 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 asked388 const asker = { name: askerName ?? "someone", display_name: author?.display_name ?? null, access: delivery.asker ?? null };
In a group message with several agents, a message that names no agent goes to the one you are talking with: the agents that answered you last, else the ones you last mentioned, else, before anyone has been addressed, all of them; a message that mentions agents still goes only to those, and a conversation with one agent is as it was. Each agent is also told, when several are here, who you are talking with, so one that is woken alongside answers only what is clearly for it and does not redo a colleague's work. And an agent shows as typing for as long as it works, not for eight seconds: its desk says so every five seconds until the answer is posted, and after ten seconds the line reads that it is still working on it. The chat and agents guides say so.389 // With other agents here: who the person is talking with (prompt.ts `lastAddressed`).
390 const addressed = conversationHere && conversationHere.agents > 1 ? lastAddressed(history, conversationHere, delivery.asked_by) : 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 asked391 let posted: string | null = null;
392 let spinOffs = 0;
393
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.394 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 asked395 // What colleagues consulted along the way used: billed to this reply.
396 const consulted = { tokens: NO_TOKENS, cost: 0 };
397 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.398 // The "Your abilities outside g1t" section, once the tool box has them.
399 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 asked400 let place: RecallPlace = { channel_id: delivery.channel_id, kind: delivery.channel_kind === "dm" ? "dm" : "private", people: [delivery.asked_by] };
A new agent says nothing until you do. Creating one still opens your direct message with it, but no introduction is written there, nothing is charged to its budget and nothing waits in Chat; the desk's special first message, its fixed fallback and the prompt that asked for it are gone. The agents guide says so.401 try {
402 const audience = await Audience.build(slug, delivery.asked_by, audiencePorts(env, slug, delivery.channel_id));
403 place = { channel_id: delivery.channel_id, kind: audience.kind, people: audience.shared ? [delivery.asked_by] : audience.members.map((m) => m.id) };
404 const ports = (consult: ToolPorts["consult"]) => toolPorts(env, slug, row.workspace_id, delivery.channel_id, consult, row.id);
405 audienceHash = audience.hash;
406 const consult = consulting({
407 db,
408 row,
409 delivery,
410 send: model.send,
411 policy: model.policy,
412 ownModel: model.ownModel,
413 sessionModel: model.sessionModel,
414 own: model.own,
415 ports,
416 hops,
417 now,
418 surface,
419 asker,
420 spent: consulted,
421 });
422 const where = delivery.channel_kind === "dm" ? "a direct message" : `#${delivery.channel_name ?? "a channel"}`;
423 const actions = actionPorts(env as unknown as SessionEnv, {
424 agent: row,
425 place,
426 source: { kind: "message", ref: delivery.message_id, label: askerName ? `@${askerName} in ${where}` : where, channel_id: delivery.channel_id },
427 asker: { id: delivery.asked_by, username: askerName },
428 workspace: slug,
429 postCard: (card) => surface.post("", card).catch(() => null),
430 // Real work becomes a session, with its card in this conversation.
431 spinOff: async (title, goal) => {
432 if (spinOffs >= MAX_SPIN_OFFS) return { ok: false, message: "You've started enough sessions from this message." };
433 spinOffs++;
434 const session = await startSession(env as unknown as SessionEnv, {
435 agent: row,
436 kind: "chat",
437 title,
438 goal: `${askerName ? `@${askerName}` : "Someone"} asked in ${where}:\n\n${goal}`,
439 workspace: slug,
440 channel_id: delivery.channel_id,
441 channel_kind: delivery.channel_kind,
442 channel_name: delivery.channel_name,
443 thread_root: delivery.thread_root,
444 message_id: delivery.message_id,
445 asked_by: delivery.asked_by,
446 asked_by_username: askerName,
447 asker: delivery.asker ?? null,
448 chain: [...chain],
449 hops,
450 });
451 const cap = session.cap_micros ? ` with a cap of ${dollars(session.cap_micros)}` : "";
452 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.` };
453 },
454 });
455 // Work handed to a colleague: the only way an agent gets another working (handoff.ts).
456 const viewer = audience.asker;
457 actions.handOff = handOffPort({
458 self: { id: row.id, handle: row.handle },
459 chain,
460 asker: delivery.asker ?? null,
461 agent: async (handle) => {
462 const found = await db
463 .prepare(selectAgents("a.workspace_id = ?3 AND a.handle = ?4 AND a.archived_at IS NULL AND a.scope = 'workspace'"))
464 .bind(...periods(now), row.workspace_id, handle)
465 .first<Row>();
466 if (!found) return null;
467 const agent = toAgent(found, now);
468 return { id: agent.id, handle: agent.handle, display_name: agent.display_name, builtin: !!found.builtin, status: agent.status };
469 },
470 person: async (handle) => {
471 if (!viewer) return false;
472 const members = await identityClient(env.IDENTITY).listMembers(slug, viewer);
473 return members.ok && members.value.some((m) => m.username.toLowerCase() === handle);
474 },
475 handOff: (colleagueId, brief) => surface.handOff(colleagueId, brief),
476 });
477 toolbox = new ToolBox(
478 audience,
479 ports(consult.ask),
480 { agentId: row.id, notConsult: [row.handle, ...(sender ? [sender.handle] : [])], hops, maxHops: CHAT_MAX_HOPS },
481 [],
482 actions,
483 );
484 consult.attach(toolbox);
485 // Its abilities outside g1t (abilities.ts): offered and enforced by the tool box, for the person it acts for.
486 if (viewer) {
487 const sections = await abilitiesFor(env as unknown as AbilityEnv, { agent: row, definition, workspace: slug, asker: viewer }).catch(() => null);
488 if (sections) {
489 const saidByPeople = saidText([...history].reverse().filter((m) => m.author.kind === "user").slice(0, 3).map((m) => m.body));
490 toolbox.useAbilities(
491 sections,
492 abilityPorts(env as unknown as AbilityEnv, {
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 asked493 agent: row,
494 workspace: slug,
495 channel_id: delivery.channel_id,
A new agent says nothing until you do. Creating one still opens your direct message with it, but no introduction is written there, nothing is charged to its budget and nothing waits in Chat; the desk's special first message, its fixed fallback and the prompt that asked for it are gone. The agents guide says so.496 session: null,
497 asker: { id: delivery.asked_by, username: askerName },
498 postCard: (card) => surface.post("", card).catch(() => null),
499 }),
500 saidByPeople,
501 definition.abilities.mcp_servers,
502 );
503 abilitiesText = abilitiesSection(sections);
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.504 }
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 asked505 }
A new agent says nothing until you do. Creating one still opens your direct message with it, but no introduction is written there, nothing is charged to its budget and nothing waits in Chat; the desk's special first message, its fixed fallback and the prompt that asked for it are gone. The agents guide says so.506 } catch (error) {
507 // Without an audience nothing may be read: the reply goes on with this conversation only.
508 console.error("agents: no audience for a reply, so no tools", row.id, String(error));
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)509 }
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.510 // 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 reading511 const said = [...history].reverse().filter((m) => m.author.kind === "user").slice(0, 3).map((m) => m.body);
A new agent says nothing until you do. Creating one still opens your direct message with it, but no introduction is written there, nothing is charged to its budget and nothing waits in Chat; the desk's special first message, its fixed fallback and the prompt that asked for it are gone. The agents guide says so.512 const [facts, recent, passages, shelf] = await Promise.all([
Agents recall what Docs say before they answer or work, and each has required reading513 recall(db, row.id, place).catch(() => []),
514 sessionsHere(db, row.id, delivery.channel_id).catch(() => null),
515 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.516 // 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.517 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 reading518 ]);
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.519 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 asked520 const system = [
521 systemPrompt({
522 agent: {
523 ...definition,
524 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.525 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 asked526 // @g1t's job is fixed; what the workspace wrote is added to it.
527 instructions: row.builtin ? orchestratorInstructions(specialists.filter((a) => a.handle !== "g1t"), definition.instructions) : definition.instructions,
528 },
529 workspace: delivery.workspace,
530 channel: { kind: delivery.channel_kind, name: delivery.channel_name },
531 asker,
532 today: now,
533 tools: toolbox ? { code: toolbox.definitions().some((tool) => tool.name === "read_file") } : null,
534 // @g1t's team is in its job; everyone else is told who their colleagues are.
535 colleagues: row.builtin ? null : rosterLines(specialists),
536 recentSessions: recent,
Merge branch 'worktree-agent-a1398e81ad1a64c5f'537 conversation: conversationHere,
In a group message with several agents, a message that names no agent goes to the one you are talking with: the agents that answered you last, else the ones you last mentioned, else, before anyone has been addressed, all of them; a message that mentions agents still goes only to those, and a conversation with one agent is as it was. Each agent is also told, when several are here, who you are talking with, so one that is woken alongside answers only what is clearly for it and does not redo a colleague's work. And an agent shows as typing for as long as it works, not for eight seconds: its desk says so every five seconds until the answer is posted, and after ten seconds the line reads that it is still working on it. The chat and agents guides say so.538 addressed,
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.539 teams: teamsSection(row.id, teamsHere, now),
Merge branch 'worktree-agent-a1398e81ad1a64c5f'540 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.541 abilities: abilitiesText,
Merge branch 'worktree-agent-a1398e81ad1a64c5f'542 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.543 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 asked544 }),
545 memorySection(facts),
Agents recall what Docs say before they answer or work, and each has required reading546 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 asked547 ]
548 .filter(Boolean)
549 .join("\n\n");
550 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.551 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 asked552 // Post as soon as there is an answer; the bill is settled after.
Merge branch 'worktree-agent-a1398e81ad1a64c5f'553 // It wakes nobody, @mentions or not: colleagues get work only through hand_off.
554 if (answer.text) posted = await surface.post(answer.text);
In a group message with several agents, a message that names no agent goes to the one you are talking with: the agents that answered you last, else the ones you last mentioned, else, before anyone has been addressed, all of them; a message that mentions agents still goes only to those, and a conversation with one agent is as it was. Each agent is also told, when several are here, who you are talking with, so one that is woken alongside answers only what is clearly for it and does not redo a colleague's work. And an agent shows as typing for as long as it works, not for eight seconds: its desk says so every five seconds until the answer is posted, and after ten seconds the line reads that it is still working on it. The chat and agents guides say so.555 // Posted, or nothing to post: no longer typing.
556 stopTyping();
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 return {
558 text: answer.text,
559 tokens: addTokens(answer.tokens, consulted.tokens),
560 cost: (model.ownModel ? 0 : answer.cost) + consulted.cost,
561 rounds: answer.rounds,
562 };
Chat and workspace agents: channels, DMs and named agents you talk to563 });
564
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 asked565 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.566 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 asked567 if (!done.value.text) {
568 const apology = await surface.post(APOLOGY).catch(() => null);
569 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 to570 }
571 await finish({ status: "replied", reply_id: posted, ...usage });
572 } catch (error) {
573 const message = error instanceof Error ? error.message : String(error);
574 console.error("agents: a reply failed", row.id, delivery.message_id, message);
575 const posted = await surface.post(APOLOGY).catch(() => null);
576 await finish({ status: "failed", error: message, reply_id: posted, ...usage }).catch((failure: unknown) =>
577 console.error("agents: a failed reply was not recorded", id, String(failure)),
578 );
In a group message with several agents, a message that names no agent goes to the one you are talking with: the agents that answered you last, else the ones you last mentioned, else, before anyone has been addressed, all of them; a message that mentions agents still goes only to those, and a conversation with one agent is as it was. Each agent is also told, when several are here, who you are talking with, so one that is woken alongside answers only what is clearly for it and does not redo a colleague's work. And an agent shows as typing for as long as it works, not for eight seconds: its desk says so every five seconds until the answer is posted, and after ten seconds the line reads that it is still working on it. The chat and agents guides say so.579 } finally {
580 // However it ended, the heartbeat ends with it.
581 stopTyping();
Chat and workspace agents: channels, DMs and named agents you talk to582 }
583}

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