Skip to content
520 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
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)22import { 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 asked23import type { Tokens } from "./budget.ts";
Merge branch 'worktree-agent-a1398e81ad1a64c5f'24import { 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)25import { HISTORY_LIMIT, fixedHello, helloAsk, systemPrompt, turns } from "./prompt.ts";
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.26import { skillsSection } from "./skills.ts";
Merge branch 'worktree-agent-a1398e81ad1a64c5f'27import { 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 asked28import { type MeterEnv, metered } from "./meter.ts";
29import { type RecallPlace, memorySection, recall } from "./memory.ts";
Agents recall what Docs say before they answer or work, and each has required reading30import { recallQuery, recallSection } from "./recall.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)31import { REPLY_TIER, allowedProviders, 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 asked32import { type SessionEnv, type SessionRow, actionPorts, sessionRow, startSession, steer } from "./sessions.ts";
33import { type Row, definitionOf, periods, selectAgents, toAgent } from "./store.ts";
Chat and workspace agents: channels, DMs and named agents you talk to34import { 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)35import { Audience } from "./audience.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.36import { audiencePorts, loadTeams, 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)37import { type ToolCall, type ToolPorts, ToolBox } from "./tools.ts";
38import 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 asked39import type { Desk } from "./desk.ts";
40import { type ModelMessage, type Send, NO_TOKENS, addTokens, runTurn } from "./turn.ts";
41import type { AgentRouting as Policy } from "../../runner/src/model-env.ts";
42import { 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.43import { teamsSection } from "./teammates.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 asked44
45export { billingRepo } from "./meter.ts";
Chat and workspace agents: channels, DMs and named agents you talk to46
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 asked47export type ReplyEnv = MeterEnv & {
Chat and workspace agents: channels, DMs and named agents you talk to48 DB: D1Database;
49 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)50 /** People and their access, for the audience; members and teams for the roster. */
51 IDENTITY: ServiceBinding;
52 /** Code, for read tools. */
53 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 asked54 /** 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)55 WORK: ServiceBinding;
56 /** Code search, for read tools. */
57 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 asked58 /** Notifications: a session waiting for more budget. */
59 NOTIFY?: ServiceBinding;
60 /** Every agent's desk: sessions are worked on their agent's. */
61 DESKS: DurableObjectNamespace<Desk>;
Chat and workspace agents: channels, DMs and named agents you talk to62};
63
64/** A notice that the agent cannot reply is posted once per conversation in this long. */
65const 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 asked66/** Sessions one reply may start. */
67const MAX_SPIN_OFFS = 2;
Chat and workspace agents: channels, DMs and named agents you talk to68
69const APOLOGY = "Sorry, something went wrong on my side and I couldn't answer that. Try again in a moment.";
70
71/** Who asked, from the message that woke the agent (or their latest one). */
72function askerIn(history: SurfaceMessage[], delivery: AgentDelivery): SurfaceMessage["author"] | null {
73 const woken = history.find((m) => m.id === delivery.message_id);
74 if (woken) return woken.author;
75 return [...history].reverse().find((m) => m.author.kind === "user" && m.author.id === delivery.asked_by)?.author ?? null;
76}
77
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)78/**
79 * 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.80 * 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)81 */
82async function team(db: D1Database, workspaceId: string, selfId: string, now: Date): Promise<Specialist[]> {
83 const rows = await db
84 .prepare(`${selectAgents("a.workspace_id = ?3 AND a.archived_at IS NULL AND a.id <> ?4")} ORDER BY a.builtin DESC, a.handle LIMIT 50`)
85 .bind(...periods(now), workspaceId, selfId)
86 .all<Row>();
87 return rows.results.map((row) => {
88 const agent = toAgent(row, now);
89 return {
90 handle: agent.handle,
91 display_name: agent.display_name,
92 role: agent.role,
93 title: agent.title,
94 team: agent.team,
95 department: agent.department,
96 responsibilities: agent.responsibilities,
97 status: agent.status,
98 spent_month_micros: agent.spent_month_micros,
99 monthly_micros: agent.budget.monthly_micros,
100 };
101 });
102}
103
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 asked104/** The agent's latest sessions in this conversation, one line each. */
105async function sessionsHere(db: D1Database, agentId: string, channelId: string): Promise<string | null> {
106 const rows = await db
107 .prepare(
108 "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",
109 )
110 .bind(agentId, channelId)
111 .all<{ id: string; title: string; status: string; summary: string | null; created_at: string }>();
112 if (!rows.results.length) return null;
113 return rows.results
114 .map((s) => `- "${s.title}" (${s.status}, ${s.created_at.slice(0, 10)})${s.summary ? `: ${s.summary.replace(/\s+/g, " ").slice(0, 300)}` : ""}`)
115 .join("\n");
116}
117
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)118/**
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.119 * 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)120 * the colleague answers in a nested turn that posts nothing, with the same
121 * audience (so it can read no more than the conversation may), the same
122 * asker, one hop further, its own routing limits, on the same model
123 * session, billed to this reply. A compact card in the thread says who
124 * asked whom. Consults don't nest: the colleague can't consult in turn.
125 */
126function consulting(input: {
127 db: D1Database;
128 row: Row;
129 delivery: DeskWork;
130 send: Send;
131 policy: Policy;
132 ownModel: boolean;
133 sessionModel: string | null;
134 own: string | null;
135 ports: (consult: ToolPorts["consult"]) => ToolPorts;
136 hops: number;
137 now: Date;
138 surface: Surface;
139 asker: { name: string; display_name: string | null; access: NonNullable<AgentDelivery["asker"]> | null };
140 spent: { tokens: Tokens; cost: number };
141}): { ask: ToolPorts["consult"]; attach(box: ToolBox): void } {
142 let parent: ToolBox | null = null;
143 const noNesting: ToolPorts["consult"] = async () => ({ ok: false, message: "Consults don't nest: answer with what you have." });
144 const ask: ToolPorts["consult"] = async (handle, question) => {
145 const { row, delivery } = input;
146 const colleague = await input.db
147 .prepare("SELECT * FROM agents WHERE workspace_id = ? AND handle = ? AND archived_at IS NULL")
148 .bind(row.workspace_id, handle)
149 .first<Row>();
150 if (!colleague || colleague.id === row.id) return { ok: false, message: `There is no other agent called @${handle} here.` };
151 // No ping-pong: never back to the agent that sent this work.
152 if ((delivery.chain ?? []).at(-1) === colleague.id) return { ok: false, message: `@${handle} sent you this work; answer with what you have.` };
153 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." };
154 const definition = definitionOf(colleague);
155 const allowed = allowedProviders(definition.routing, input.own);
156 if (input.ownModel ? !allowed.own : !allowed.hosted) return { ok: false, message: `@${handle} can't use the model this conversation runs on.` };
157 const model = replyModel(
158 input.policy,
159 { ...definition.routing, pinned: input.ownModel ? definition.routing.pinned : null },
160 { named: input.ownModel ? input.sessionModel : null },
161 );
162 const tools = parent
163 ? parent.forColleague(input.ports(noNesting), { agentId: colleague.id, notConsult: [colleague.handle, row.handle], hops: input.hops + 1, maxHops: input.hops + 1 })
164 : null;
165 const system = systemPrompt({
166 agent: { ...definition, id: colleague.id },
167 workspace: delivery.workspace,
168 channel: { kind: delivery.channel_kind, name: delivery.channel_name },
169 asker: input.asker,
170 today: input.now,
171 tools: tools ? { code: tools.definitions().some((tool) => tool.name === "read_file") } : null,
172 consultedBy: row.handle,
173 });
174 const result = await runTurn(input.send, {
175 model: model.model,
176 system,
177 messages: [{ role: "user", content: `@${row.handle} (agent) asks you: ${question}` }],
178 tools,
179 price: input.ownModel ? null : model.price,
180 });
181 input.spent.tokens = addTokens(input.spent.tokens, result.tokens);
182 input.spent.cost += input.ownModel ? 0 : result.cost;
183 const answer = result.text || "(no answer)";
184 const exchange = `Q: ${question}\nA: ${answer}`;
185 // The exchange, collapsed, in the thread. No @ in its text: it wakes nobody.
186 await input.surface
187 .post(`${row.display_name} asked ${colleague.display_name}`, {
188 kind: "consult",
189 title: `${row.display_name} asked ${colleague.display_name}`,
190 detail: exchange.length > 1000 ? `${exchange.slice(0, 1000)}…` : exchange,
191 state: null,
192 href: null,
193 })
194 .catch((error: unknown) => console.error("agents: a consult card was not posted", String(error)));
195 return { ok: true, colleague: colleague.handle, answer };
196 };
197 return {
198 ask,
199 attach(box) {
200 parent = box;
201 },
202 };
203}
204
Chat and workspace agents: channels, DMs and named agents you talk to205type 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 asked206 status: "replied" | "steered" | "blocked" | "skipped" | "failed";
Chat and workspace agents: channels, DMs and named agents you talk to207 error?: string | null;
208 reply_id?: string | null;
209 model?: string | null;
210 tier?: string | null;
211 tokens?: Tokens;
212 cost?: number;
213 charged?: number;
214};
215
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)216/**
217 * What a desk is handed: a message to answer, or (`hello`) the agent's
218 * first message to the person who made it, in the DM that just opened.
219 */
220export type DeskWork = AgentDelivery & { hello?: boolean };
221
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 asked222/** How a limit's refusal reads in chat, in the agent's voice. */
223function noticeFor(reason: string, message: string): string {
224 if (reason.startsWith("budget_") || reason === "workspace_agent_budget" || message.startsWith("My settings") || message.startsWith("I ")) return message;
225 if (reason === "no_model") return `I can't reply yet: ${message}`;
226 return `I can't reply right now: ${message}`;
227}
228
229/** The session a message in this thread is for: a reply under one of this agent's session cards. */
230async function steeredSession(db: D1Database, agentId: string, threadRoot: string | null): Promise<SessionRow | null> {
231 if (!threadRoot) return null;
232 const found = await db.prepare("SELECT id FROM agent_sessions WHERE agent_id = ? AND card_message_id = ?").bind(agentId, threadRoot).first<{ id: string }>();
233 return found ? sessionRow(db, found.id) : null;
234}
235
236/**
237 * Answers `delivery` as its agent. Never throws: every way it ends is
238 * recorded on the reply's row. A message handed over twice is answered
239 * once.
240 */
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)241export async function reply(env: ReplyEnv, delivery: DeskWork, now = new Date()): Promise<void> {
Chat and workspace agents: channels, DMs and named agents you talk to242 const db = env.DB;
243 const row = await db.prepare("SELECT * FROM agents WHERE id = ?").bind(delivery.agent_id).first<Row>();
244 if (!row || row.archived_at || row.workspace_id !== delivery.workspace_id) return;
245 const id = newId("arp", now.getTime());
246 const claimed = await db
247 .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 asked248 `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)
249 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'working', ?)
Chat and workspace agents: channels, DMs and named agents you talk to250 ON CONFLICT (agent_id, message_id) DO NOTHING RETURNING id`,
251 )
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 asked252 .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 to253 .first<{ id: string }>();
254 if (!claimed) return;
255
256 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)257 // 👀 as soon as the desk has it, while everything else goes on.
258 const acknowledging = surface.acknowledge();
Chat and workspace agents: channels, DMs and named agents you talk to259 const slug = delivery.workspace.toLowerCase();
260
261 // What the answer used, once there is one: counted however the reply ends.
262 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 asked263 // 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)264 let toolCalls: ToolCall[] = [];
265 let audienceHash: string | null = null;
Chat and workspace agents: channels, DMs and named agents you talk to266
267 const finish = async (outcome: Outcome) => {
268 const statements = [
269 db
270 .prepare(
271 `UPDATE agent_replies SET status = ?, error = ?, reply_id = ?, model = ?, tier = ?, input_tokens = ?, output_tokens = ?,
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 asked272 cost_micros = ?, charged_micros = ?, tool_count = ?, finished_at = ? WHERE id = ?`,
Chat and workspace agents: channels, DMs and named agents you talk to273 )
274 .bind(
275 outcome.status,
276 outcome.error?.slice(0, 1000) ?? null,
277 outcome.reply_id ?? null,
278 outcome.model ?? null,
279 outcome.tier ?? null,
280 (outcome.tokens?.input ?? 0) + (outcome.tokens?.cacheRead ?? 0) + (outcome.tokens?.cacheWrite ?? 0),
281 outcome.tokens?.output ?? 0,
282 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 asked283 outcome.charged ?? 0,
284 toolCalls.length,
Chat and workspace agents: channels, DMs and named agents you talk to285 new Date().toISOString(),
286 id,
287 ),
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)288 // Every tool call: what was asked, for whom, and whether it was read or withheld.
289 ...toolCalls.map((call, n) =>
290 db
291 .prepare(
292 `INSERT INTO agent_tool_calls (id, reply_id, agent_id, workspace_id, asked_by, tool, args, audience_hash, outcome, bytes, created_at)
293 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
294 )
295 .bind(`${id}_${n}`, id, row.id, row.workspace_id, delivery.asked_by, call.tool, call.args, audienceHash ?? "", call.outcome, call.bytes, now.toISOString()),
296 ),
Chat and workspace agents: channels, DMs and named agents you talk to297 ];
298 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)299 // ✅ when it answered; 👀 taken back after a notice, an apology or nothing.
300 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 asked301 await surface.settle(outcome.status === "replied" || outcome.status === "steered" ? "done" : "withdrawn");
Chat and workspace agents: channels, DMs and named agents you talk to302 };
303
304 /** Says once, in this conversation, why the agent cannot answer; a repeat within hours is kept back. */
305 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)306 // An agent's first words are never an excuse: without a model, a fixed hello.
307 if (delivery.hello) {
308 const posted = await surface.post(fixedHello(row, delivery.asker?.username ?? null)).catch(() => null);
309 return await finish({ status: "blocked", error: reason, reply_id: posted });
310 }
Chat and workspace agents: channels, DMs and named agents you talk to311 const since = new Date(now.getTime() - NOTICE_QUIET_MS).toISOString();
312 const recent = await db
313 .prepare(
314 `SELECT 1 FROM agent_replies WHERE agent_id = ? AND channel_id = ? AND status = 'blocked' AND error = ?
315 AND reply_id IS NOT NULL AND created_at > ? AND id <> ? LIMIT 1`,
316 )
317 .bind(row.id, delivery.channel_id, reason, since, id)
318 .first();
319 const posted = recent ? null : await surface.post(message).catch(() => null);
320 await finish({ status: "blocked", error: reason, reply_id: posted });
321 };
322
323 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 asked324 // A reply under one of its session cards steers that session; nothing is answered here.
325 const steered = delivery.hello ? null : await steeredSession(db, row.id, delivery.thread_root);
326 if (steered) {
327 const history = await surface.history(HISTORY_LIMIT).catch(() => [] as SurfaceMessage[]);
328 const message = history.find((m) => m.id === delivery.message_id);
329 if (message?.body.trim()) {
330 await steer(env as unknown as SessionEnv, steered, message.author.name, message.body);
331 return await finish({ status: "steered" });
332 }
Chat and workspace agents: channels, DMs and named agents you talk to333 }
334
335 // 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)336 // A hello has no conversation yet: it is asked to introduce itself.
Merge branch 'worktree-agent-a1398e81ad1a64c5f'337 // 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.338 // And its teams, from their pages: who leads, who owns what, who is around.
339 const [, history, conversationHere, teamsHere] = await Promise.all([
Merge branch 'worktree-agent-a1398e81ad1a64c5f'340 surface.typing(),
341 delivery.hello ? Promise.resolve([]) : surface.history(HISTORY_LIMIT),
342 delivery.hello ? Promise.resolve(null) : surface.conversation(),
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.343 loadTeams(env, slug, row.workspace_id, { id: row.id, team: row.team ?? null }).catch(() => null),
Merge branch 'worktree-agent-a1398e81ad1a64c5f'344 ]);
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)345 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 to346 if (!conversation.length) return await finish({ status: "skipped", error: "nothing to answer" });
347 const author = askerIn(history, delivery);
348 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)349 // Every agent knows its colleagues; @g1t also steps up a tier to decide
350 // who gets the work in a long thread.
351 const specialists = await team(db, row.workspace_id, row.id, now);
352 const start = row.builtin ? orchestratorTier(history.length, specialists.filter((a) => a.handle !== "g1t").length) : REPLY_TIER;
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 asked353 const definition = definitionOf(row);
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)354 const hops = Math.max(0, Math.floor(delivery.hops || 0));
355 const chain = delivery.chain ?? [];
356 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 asked357 const asker = { name: askerName ?? "someone", display_name: author?.display_name ?? null, access: delivery.asker ?? null };
358 let posted: string | null = null;
359 let spinOffs = 0;
360
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.361 const done = await metered(env, { row, payer: row, slug, task: "reply", start, 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 asked362 // What colleagues consulted along the way used: billed to this reply.
363 const consulted = { tokens: NO_TOKENS, cost: 0 };
364 let toolbox: ToolBox | null = null;
365 let place: RecallPlace = { channel_id: delivery.channel_id, kind: delivery.channel_kind === "dm" ? "dm" : "private", people: [delivery.asked_by] };
366 if (!delivery.hello) {
367 try {
368 const audience = await Audience.build(slug, delivery.asked_by, audiencePorts(env, slug, delivery.channel_id));
369 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 together370 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 asked371 audienceHash = audience.hash;
372 const consult = consulting({
373 db,
374 row,
375 delivery,
376 send: model.send,
377 policy: model.policy,
378 ownModel: model.ownModel,
379 sessionModel: model.sessionModel,
380 own: model.own,
381 ports,
382 hops,
383 now,
384 surface,
385 asker,
386 spent: consulted,
387 });
388 const where = delivery.channel_kind === "dm" ? "a direct message" : `#${delivery.channel_name ?? "a channel"}`;
389 const actions = actionPorts(env as unknown as SessionEnv, {
390 agent: row,
391 place,
392 source: { kind: "message", ref: delivery.message_id, label: askerName ? `@${askerName} in ${where}` : where, channel_id: delivery.channel_id },
393 asker: { id: delivery.asked_by, username: askerName },
394 workspace: slug,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar395 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 asked396 // Real work becomes a session, with its card in this conversation.
397 spinOff: async (title, goal) => {
398 if (spinOffs >= MAX_SPIN_OFFS) return { ok: false, message: "You've started enough sessions from this message." };
399 spinOffs++;
400 const session = await startSession(env as unknown as SessionEnv, {
401 agent: row,
402 kind: "chat",
403 title,
404 goal: `${askerName ? `@${askerName}` : "Someone"} asked in ${where}:\n\n${goal}`,
405 workspace: slug,
406 channel_id: delivery.channel_id,
407 channel_kind: delivery.channel_kind,
408 channel_name: delivery.channel_name,
409 thread_root: delivery.thread_root,
410 message_id: delivery.message_id,
411 asked_by: delivery.asked_by,
412 asked_by_username: askerName,
413 asker: delivery.asker ?? null,
414 chain: [...chain],
415 hops,
416 });
417 const cap = session.cap_micros ? ` with a cap of ${dollars(session.cap_micros)}` : "";
418 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.` };
419 },
420 });
Merge branch 'worktree-agent-a1398e81ad1a64c5f'421 // Work handed to a colleague: the only way an agent gets another working (handoff.ts).
422 const viewer = audience.asker;
423 actions.handOff = handOffPort({
424 self: { id: row.id, handle: row.handle },
425 chain,
426 asker: delivery.asker ?? null,
427 agent: async (handle) => {
428 const found = await db
429 .prepare(selectAgents("a.workspace_id = ?3 AND a.handle = ?4 AND a.archived_at IS NULL"))
430 .bind(...periods(now), row.workspace_id, handle)
431 .first<Row>();
432 if (!found) return null;
433 const agent = toAgent(found, now);
434 return { id: agent.id, handle: agent.handle, display_name: agent.display_name, builtin: !!found.builtin, status: agent.status };
435 },
436 person: async (handle) => {
437 if (!viewer) return false;
438 const members = await identityClient(env.IDENTITY).listMembers(slug, viewer);
439 return members.ok && members.value.some((m) => m.username.toLowerCase() === handle);
440 },
441 handOff: (colleagueId, brief) => surface.handOff(colleagueId, brief),
442 });
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 asked443 toolbox = new ToolBox(
444 audience,
445 ports(consult.ask),
446 { agentId: row.id, notConsult: [row.handle, ...(sender ? [sender.handle] : [])], hops, maxHops: CHAT_MAX_HOPS },
447 [],
448 actions,
449 );
450 consult.attach(toolbox);
451 } catch (error) {
452 // Without an audience nothing may be read: the reply goes on with this conversation only.
453 console.error("agents: no audience for a reply, so no tools", row.id, String(error));
454 }
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)455 }
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.456 // 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 reading457 const said = [...history].reverse().filter((m) => m.author.kind === "user").slice(0, 3).map((m) => m.body);
458 const [facts, recent, passages] = delivery.hello
459 ? [[], null, []]
460 : await Promise.all([
461 recall(db, row.id, place).catch(() => []),
462 sessionsHere(db, row.id, delivery.channel_id).catch(() => null),
463 toolbox ? toolbox.recall(recallQuery(said), definition.reading ?? []) : Promise.resolve([]),
464 ]);
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 asked465 const system = [
466 systemPrompt({
467 agent: {
468 ...definition,
469 id: row.id,
470 // @g1t's job is fixed; what the workspace wrote is added to it.
471 instructions: row.builtin ? orchestratorInstructions(specialists.filter((a) => a.handle !== "g1t"), definition.instructions) : definition.instructions,
472 },
473 workspace: delivery.workspace,
474 channel: { kind: delivery.channel_kind, name: delivery.channel_name },
475 asker,
476 today: now,
477 tools: toolbox ? { code: toolbox.definitions().some((tool) => tool.name === "read_file") } : null,
478 // @g1t's team is in its job; everyone else is told who their colleagues are.
479 colleagues: row.builtin ? null : rosterLines(specialists),
480 recentSessions: recent,
Merge branch 'worktree-agent-a1398e81ad1a64c5f'481 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.482 teams: teamsSection(row.id, teamsHere, now),
Merge branch 'worktree-agent-a1398e81ad1a64c5f'483 canHandOff: !!toolbox?.definitions().some((tool) => tool.name === "hand_off"),
484 handedOffBy: sender?.handle ?? null,
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.485 skills: skillsSection(definition.skills_off, 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 asked486 }),
487 memorySection(facts),
Agents recall what Docs say before they answer or work, and each has required reading488 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 asked489 ]
490 .filter(Boolean)
491 .join("\n\n");
492 toolCalls = toolbox?.calls ?? [];
493 const answer = await runTurn(model.send, { model: model.model.model, system, messages: conversation as ModelMessage[], tools: toolbox, price: model.ownModel ? null : model.model.price });
494 // Post as soon as there is an answer; the bill is settled after.
Merge branch 'worktree-agent-a1398e81ad1a64c5f'495 // It wakes nobody, @mentions or not: colleagues get work only through hand_off.
496 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 asked497 return {
498 text: answer.text,
499 tokens: addTokens(answer.tokens, consulted.tokens),
500 cost: (model.ownModel ? 0 : answer.cost) + consulted.cost,
501 rounds: answer.rounds,
502 };
Chat and workspace agents: channels, DMs and named agents you talk to503 });
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 if (!done.ok) return await notice(noticeFor(done.reason, done.message), done.reason);
506 usage = { model: done.model, tier: done.tier, tokens: done.tokens, cost: done.cost, charged: done.charged };
507 if (!done.value.text) {
508 const apology = await surface.post(APOLOGY).catch(() => null);
509 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 to510 }
511 await finish({ status: "replied", reply_id: posted, ...usage });
512 } catch (error) {
513 const message = error instanceof Error ? error.message : String(error);
514 console.error("agents: a reply failed", row.id, delivery.message_id, message);
515 const posted = await surface.post(APOLOGY).catch(() => null);
516 await finish({ status: "failed", error: message, reply_id: posted, ...usage }).catch((failure: unknown) =>
517 console.error("agents: a failed reply was not recorded", id, String(failure)),
518 );
519 }
520}

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