Skip to content
516 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";
36import { audiencePorts, toolPorts } from "./ports.ts";
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";
43
44export { billingRepo } from "./meter.ts";
Chat and workspace agents: channels, DMs and named agents you talk to45
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 asked46export type ReplyEnv = MeterEnv & {
Chat and workspace agents: channels, DMs and named agents you talk to47 DB: D1Database;
48 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)49 /** People and their access, for the audience; members and teams for the roster. */
50 IDENTITY: ServiceBinding;
51 /** Code, for read tools. */
52 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 asked53 /** 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)54 WORK: ServiceBinding;
55 /** Code search, for read tools. */
56 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 asked57 /** Notifications: a session waiting for more budget. */
58 NOTIFY?: ServiceBinding;
59 /** Every agent's desk: sessions are worked on their agent's. */
60 DESKS: DurableObjectNamespace<Desk>;
Chat and workspace agents: channels, DMs and named agents you talk to61};
62
63/** A notice that the agent cannot reply is posted once per conversation in this long. */
64const 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 asked65/** Sessions one reply may start. */
66const MAX_SPIN_OFFS = 2;
Chat and workspace agents: channels, DMs and named agents you talk to67
68const APOLOGY = "Sorry, something went wrong on my side and I couldn't answer that. Try again in a moment.";
69
70/** Who asked, from the message that woke the agent (or their latest one). */
71function askerIn(history: SurfaceMessage[], delivery: AgentDelivery): SurfaceMessage["author"] | null {
72 const woken = history.find((m) => m.id === delivery.message_id);
73 if (woken) return woken.author;
74 return [...history].reverse().find((m) => m.author.kind === "user" && m.author.id === delivery.asked_by)?.author ?? null;
75}
76
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)77/**
78 * 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.79 * 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)80 */
81async function team(db: D1Database, workspaceId: string, selfId: string, now: Date): Promise<Specialist[]> {
82 const rows = await db
83 .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`)
84 .bind(...periods(now), workspaceId, selfId)
85 .all<Row>();
86 return rows.results.map((row) => {
87 const agent = toAgent(row, now);
88 return {
89 handle: agent.handle,
90 display_name: agent.display_name,
91 role: agent.role,
92 title: agent.title,
93 team: agent.team,
94 department: agent.department,
95 responsibilities: agent.responsibilities,
96 status: agent.status,
97 spent_month_micros: agent.spent_month_micros,
98 monthly_micros: agent.budget.monthly_micros,
99 };
100 });
101}
102
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 asked103/** The agent's latest sessions in this conversation, one line each. */
104async function sessionsHere(db: D1Database, agentId: string, channelId: string): Promise<string | null> {
105 const rows = await db
106 .prepare(
107 "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",
108 )
109 .bind(agentId, channelId)
110 .all<{ id: string; title: string; status: string; summary: string | null; created_at: string }>();
111 if (!rows.results.length) return null;
112 return rows.results
113 .map((s) => `- "${s.title}" (${s.status}, ${s.created_at.slice(0, 10)})${s.summary ? `: ${s.summary.replace(/\s+/g, " ").slice(0, 300)}` : ""}`)
114 .join("\n");
115}
116
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)117/**
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.118 * 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)119 * the colleague answers in a nested turn that posts nothing, with the same
120 * audience (so it can read no more than the conversation may), the same
121 * asker, one hop further, its own routing limits, on the same model
122 * session, billed to this reply. A compact card in the thread says who
123 * asked whom. Consults don't nest: the colleague can't consult in turn.
124 */
125function consulting(input: {
126 db: D1Database;
127 row: Row;
128 delivery: DeskWork;
129 send: Send;
130 policy: Policy;
131 ownModel: boolean;
132 sessionModel: string | null;
133 own: string | null;
134 ports: (consult: ToolPorts["consult"]) => ToolPorts;
135 hops: number;
136 now: Date;
137 surface: Surface;
138 asker: { name: string; display_name: string | null; access: NonNullable<AgentDelivery["asker"]> | null };
139 spent: { tokens: Tokens; cost: number };
140}): { ask: ToolPorts["consult"]; attach(box: ToolBox): void } {
141 let parent: ToolBox | null = null;
142 const noNesting: ToolPorts["consult"] = async () => ({ ok: false, message: "Consults don't nest: answer with what you have." });
143 const ask: ToolPorts["consult"] = async (handle, question) => {
144 const { row, delivery } = input;
145 const colleague = await input.db
146 .prepare("SELECT * FROM agents WHERE workspace_id = ? AND handle = ? AND archived_at IS NULL")
147 .bind(row.workspace_id, handle)
148 .first<Row>();
149 if (!colleague || colleague.id === row.id) return { ok: false, message: `There is no other agent called @${handle} here.` };
150 // No ping-pong: never back to the agent that sent this work.
151 if ((delivery.chain ?? []).at(-1) === colleague.id) return { ok: false, message: `@${handle} sent you this work; answer with what you have.` };
152 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." };
153 const definition = definitionOf(colleague);
154 const allowed = allowedProviders(definition.routing, input.own);
155 if (input.ownModel ? !allowed.own : !allowed.hosted) return { ok: false, message: `@${handle} can't use the model this conversation runs on.` };
156 const model = replyModel(
157 input.policy,
158 { ...definition.routing, pinned: input.ownModel ? definition.routing.pinned : null },
159 { named: input.ownModel ? input.sessionModel : null },
160 );
161 const tools = parent
162 ? parent.forColleague(input.ports(noNesting), { agentId: colleague.id, notConsult: [colleague.handle, row.handle], hops: input.hops + 1, maxHops: input.hops + 1 })
163 : null;
164 const system = systemPrompt({
165 agent: { ...definition, id: colleague.id },
166 workspace: delivery.workspace,
167 channel: { kind: delivery.channel_kind, name: delivery.channel_name },
168 asker: input.asker,
169 today: input.now,
170 tools: tools ? { code: tools.definitions().some((tool) => tool.name === "read_file") } : null,
171 consultedBy: row.handle,
172 });
173 const result = await runTurn(input.send, {
174 model: model.model,
175 system,
176 messages: [{ role: "user", content: `@${row.handle} (agent) asks you: ${question}` }],
177 tools,
178 price: input.ownModel ? null : model.price,
179 });
180 input.spent.tokens = addTokens(input.spent.tokens, result.tokens);
181 input.spent.cost += input.ownModel ? 0 : result.cost;
182 const answer = result.text || "(no answer)";
183 const exchange = `Q: ${question}\nA: ${answer}`;
184 // The exchange, collapsed, in the thread. No @ in its text: it wakes nobody.
185 await input.surface
186 .post(`${row.display_name} asked ${colleague.display_name}`, {
187 kind: "consult",
188 title: `${row.display_name} asked ${colleague.display_name}`,
189 detail: exchange.length > 1000 ? `${exchange.slice(0, 1000)}…` : exchange,
190 state: null,
191 href: null,
192 })
193 .catch((error: unknown) => console.error("agents: a consult card was not posted", String(error)));
194 return { ok: true, colleague: colleague.handle, answer };
195 };
196 return {
197 ask,
198 attach(box) {
199 parent = box;
200 },
201 };
202}
203
Chat and workspace agents: channels, DMs and named agents you talk to204type 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 asked205 status: "replied" | "steered" | "blocked" | "skipped" | "failed";
Chat and workspace agents: channels, DMs and named agents you talk to206 error?: string | null;
207 reply_id?: string | null;
208 model?: string | null;
209 tier?: string | null;
210 tokens?: Tokens;
211 cost?: number;
212 charged?: number;
213};
214
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)215/**
216 * What a desk is handed: a message to answer, or (`hello`) the agent's
217 * first message to the person who made it, in the DM that just opened.
218 */
219export type DeskWork = AgentDelivery & { hello?: boolean };
220
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 asked221/** How a limit's refusal reads in chat, in the agent's voice. */
222function noticeFor(reason: string, message: string): string {
223 if (reason.startsWith("budget_") || reason === "workspace_agent_budget" || message.startsWith("My settings") || message.startsWith("I ")) return message;
224 if (reason === "no_model") return `I can't reply yet: ${message}`;
225 return `I can't reply right now: ${message}`;
226}
227
228/** The session a message in this thread is for: a reply under one of this agent's session cards. */
229async function steeredSession(db: D1Database, agentId: string, threadRoot: string | null): Promise<SessionRow | null> {
230 if (!threadRoot) return null;
231 const found = await db.prepare("SELECT id FROM agent_sessions WHERE agent_id = ? AND card_message_id = ?").bind(agentId, threadRoot).first<{ id: string }>();
232 return found ? sessionRow(db, found.id) : null;
233}
234
235/**
236 * Answers `delivery` as its agent. Never throws: every way it ends is
237 * recorded on the reply's row. A message handed over twice is answered
238 * once.
239 */
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)240export async function reply(env: ReplyEnv, delivery: DeskWork, now = new Date()): Promise<void> {
Chat and workspace agents: channels, DMs and named agents you talk to241 const db = env.DB;
242 const row = await db.prepare("SELECT * FROM agents WHERE id = ?").bind(delivery.agent_id).first<Row>();
243 if (!row || row.archived_at || row.workspace_id !== delivery.workspace_id) return;
244 const id = newId("arp", now.getTime());
245 const claimed = await db
246 .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 asked247 `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)
248 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'working', ?)
Chat and workspace agents: channels, DMs and named agents you talk to249 ON CONFLICT (agent_id, message_id) DO NOTHING RETURNING id`,
250 )
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 asked251 .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 to252 .first<{ id: string }>();
253 if (!claimed) return;
254
255 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)256 // 👀 as soon as the desk has it, while everything else goes on.
257 const acknowledging = surface.acknowledge();
Chat and workspace agents: channels, DMs and named agents you talk to258 const slug = delivery.workspace.toLowerCase();
259
260 // What the answer used, once there is one: counted however the reply ends.
261 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 asked262 // 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)263 let toolCalls: ToolCall[] = [];
264 let audienceHash: string | null = null;
Chat and workspace agents: channels, DMs and named agents you talk to265
266 const finish = async (outcome: Outcome) => {
267 const statements = [
268 db
269 .prepare(
270 `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 asked271 cost_micros = ?, charged_micros = ?, tool_count = ?, finished_at = ? WHERE id = ?`,
Chat and workspace agents: channels, DMs and named agents you talk to272 )
273 .bind(
274 outcome.status,
275 outcome.error?.slice(0, 1000) ?? null,
276 outcome.reply_id ?? null,
277 outcome.model ?? null,
278 outcome.tier ?? null,
279 (outcome.tokens?.input ?? 0) + (outcome.tokens?.cacheRead ?? 0) + (outcome.tokens?.cacheWrite ?? 0),
280 outcome.tokens?.output ?? 0,
281 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 asked282 outcome.charged ?? 0,
283 toolCalls.length,
Chat and workspace agents: channels, DMs and named agents you talk to284 new Date().toISOString(),
285 id,
286 ),
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)287 // Every tool call: what was asked, for whom, and whether it was read or withheld.
288 ...toolCalls.map((call, n) =>
289 db
290 .prepare(
291 `INSERT INTO agent_tool_calls (id, reply_id, agent_id, workspace_id, asked_by, tool, args, audience_hash, outcome, bytes, created_at)
292 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
293 )
294 .bind(`${id}_${n}`, id, row.id, row.workspace_id, delivery.asked_by, call.tool, call.args, audienceHash ?? "", call.outcome, call.bytes, now.toISOString()),
295 ),
Chat and workspace agents: channels, DMs and named agents you talk to296 ];
297 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)298 // ✅ when it answered; 👀 taken back after a notice, an apology or nothing.
299 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 asked300 await surface.settle(outcome.status === "replied" || outcome.status === "steered" ? "done" : "withdrawn");
Chat and workspace agents: channels, DMs and named agents you talk to301 };
302
303 /** Says once, in this conversation, why the agent cannot answer; a repeat within hours is kept back. */
304 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)305 // An agent's first words are never an excuse: without a model, a fixed hello.
306 if (delivery.hello) {
307 const posted = await surface.post(fixedHello(row, delivery.asker?.username ?? null)).catch(() => null);
308 return await finish({ status: "blocked", error: reason, reply_id: posted });
309 }
Chat and workspace agents: channels, DMs and named agents you talk to310 const since = new Date(now.getTime() - NOTICE_QUIET_MS).toISOString();
311 const recent = await db
312 .prepare(
313 `SELECT 1 FROM agent_replies WHERE agent_id = ? AND channel_id = ? AND status = 'blocked' AND error = ?
314 AND reply_id IS NOT NULL AND created_at > ? AND id <> ? LIMIT 1`,
315 )
316 .bind(row.id, delivery.channel_id, reason, since, id)
317 .first();
318 const posted = recent ? null : await surface.post(message).catch(() => null);
319 await finish({ status: "blocked", error: reason, reply_id: posted });
320 };
321
322 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 asked323 // A reply under one of its session cards steers that session; nothing is answered here.
324 const steered = delivery.hello ? null : await steeredSession(db, row.id, delivery.thread_root);
325 if (steered) {
326 const history = await surface.history(HISTORY_LIMIT).catch(() => [] as SurfaceMessage[]);
327 const message = history.find((m) => m.id === delivery.message_id);
328 if (message?.body.trim()) {
329 await steer(env as unknown as SessionEnv, steered, message.author.name, message.body);
330 return await finish({ status: "steered" });
331 }
Chat and workspace agents: channels, DMs and named agents you talk to332 }
333
334 // 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)335 // A hello has no conversation yet: it is asked to introduce itself.
Merge branch 'worktree-agent-a1398e81ad1a64c5f'336 // And who is here: said every turn, so the agent knows who reads it and who doesn't.
337 const [, history, conversationHere] = await Promise.all([
338 surface.typing(),
339 delivery.hello ? Promise.resolve([]) : surface.history(HISTORY_LIMIT),
340 delivery.hello ? Promise.resolve(null) : surface.conversation(),
341 ]);
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)342 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 to343 if (!conversation.length) return await finish({ status: "skipped", error: "nothing to answer" });
344 const author = askerIn(history, delivery);
345 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)346 // Every agent knows its colleagues; @g1t also steps up a tier to decide
347 // who gets the work in a long thread.
348 const specialists = await team(db, row.workspace_id, row.id, now);
349 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 asked350 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)351 const hops = Math.max(0, Math.floor(delivery.hops || 0));
352 const chain = delivery.chain ?? [];
353 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 asked354 const asker = { name: askerName ?? "someone", display_name: author?.display_name ?? null, access: delivery.asker ?? null };
355 let posted: string | null = null;
356 let spinOffs = 0;
357
The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how.358 const done = await metered(env, { row, payer: row, slug, task: "reply", start, askerName, person: delivery.asker?.username ?? null }, 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 asked359 // What colleagues consulted along the way used: billed to this reply.
360 const consulted = { tokens: NO_TOKENS, cost: 0 };
361 let toolbox: ToolBox | null = null;
362 let place: RecallPlace = { channel_id: delivery.channel_id, kind: delivery.channel_kind === "dm" ? "dm" : "private", people: [delivery.asked_by] };
363 if (!delivery.hello) {
364 try {
365 const audience = await Audience.build(slug, delivery.asked_by, audiencePorts(env, slug, delivery.channel_id));
366 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 together367 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 asked368 audienceHash = audience.hash;
369 const consult = consulting({
370 db,
371 row,
372 delivery,
373 send: model.send,
374 policy: model.policy,
375 ownModel: model.ownModel,
376 sessionModel: model.sessionModel,
377 own: model.own,
378 ports,
379 hops,
380 now,
381 surface,
382 asker,
383 spent: consulted,
384 });
385 const where = delivery.channel_kind === "dm" ? "a direct message" : `#${delivery.channel_name ?? "a channel"}`;
386 const actions = actionPorts(env as unknown as SessionEnv, {
387 agent: row,
388 place,
389 source: { kind: "message", ref: delivery.message_id, label: askerName ? `@${askerName} in ${where}` : where, channel_id: delivery.channel_id },
390 asker: { id: delivery.asked_by, username: askerName },
391 workspace: slug,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar392 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 asked393 // Real work becomes a session, with its card in this conversation.
394 spinOff: async (title, goal) => {
395 if (spinOffs >= MAX_SPIN_OFFS) return { ok: false, message: "You've started enough sessions from this message." };
396 spinOffs++;
397 const session = await startSession(env as unknown as SessionEnv, {
398 agent: row,
399 kind: "chat",
400 title,
401 goal: `${askerName ? `@${askerName}` : "Someone"} asked in ${where}:\n\n${goal}`,
402 workspace: slug,
403 channel_id: delivery.channel_id,
404 channel_kind: delivery.channel_kind,
405 channel_name: delivery.channel_name,
406 thread_root: delivery.thread_root,
407 message_id: delivery.message_id,
408 asked_by: delivery.asked_by,
409 asked_by_username: askerName,
410 asker: delivery.asker ?? null,
411 chain: [...chain],
412 hops,
413 });
414 const cap = session.cap_micros ? ` with a cap of ${dollars(session.cap_micros)}` : "";
415 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.` };
416 },
417 });
Merge branch 'worktree-agent-a1398e81ad1a64c5f'418 // Work handed to a colleague: the only way an agent gets another working (handoff.ts).
419 const viewer = audience.asker;
420 actions.handOff = handOffPort({
421 self: { id: row.id, handle: row.handle },
422 chain,
423 asker: delivery.asker ?? null,
424 agent: async (handle) => {
425 const found = await db
426 .prepare(selectAgents("a.workspace_id = ?3 AND a.handle = ?4 AND a.archived_at IS NULL"))
427 .bind(...periods(now), row.workspace_id, handle)
428 .first<Row>();
429 if (!found) return null;
430 const agent = toAgent(found, now);
431 return { id: agent.id, handle: agent.handle, display_name: agent.display_name, builtin: !!found.builtin, status: agent.status };
432 },
433 person: async (handle) => {
434 if (!viewer) return false;
435 const members = await identityClient(env.IDENTITY).listMembers(slug, viewer);
436 return members.ok && members.value.some((m) => m.username.toLowerCase() === handle);
437 },
438 handOff: (colleagueId, brief) => surface.handOff(colleagueId, brief),
439 });
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 asked440 toolbox = new ToolBox(
441 audience,
442 ports(consult.ask),
443 { agentId: row.id, notConsult: [row.handle, ...(sender ? [sender.handle] : [])], hops, maxHops: CHAT_MAX_HOPS },
444 [],
445 actions,
446 );
447 consult.attach(toolbox);
448 } catch (error) {
449 // Without an audience nothing may be read: the reply goes on with this conversation only.
450 console.error("agents: no audience for a reply, so no tools", row.id, String(error));
451 }
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)452 }
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.453 // 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 reading454 const said = [...history].reverse().filter((m) => m.author.kind === "user").slice(0, 3).map((m) => m.body);
455 const [facts, recent, passages] = delivery.hello
456 ? [[], null, []]
457 : await Promise.all([
458 recall(db, row.id, place).catch(() => []),
459 sessionsHere(db, row.id, delivery.channel_id).catch(() => null),
460 toolbox ? toolbox.recall(recallQuery(said), definition.reading ?? []) : Promise.resolve([]),
461 ]);
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 asked462 const system = [
463 systemPrompt({
464 agent: {
465 ...definition,
466 id: row.id,
467 // @g1t's job is fixed; what the workspace wrote is added to it.
468 instructions: row.builtin ? orchestratorInstructions(specialists.filter((a) => a.handle !== "g1t"), definition.instructions) : definition.instructions,
469 },
470 workspace: delivery.workspace,
471 channel: { kind: delivery.channel_kind, name: delivery.channel_name },
472 asker,
473 today: now,
474 tools: toolbox ? { code: toolbox.definitions().some((tool) => tool.name === "read_file") } : null,
475 // @g1t's team is in its job; everyone else is told who their colleagues are.
476 colleagues: row.builtin ? null : rosterLines(specialists),
477 recentSessions: recent,
Merge branch 'worktree-agent-a1398e81ad1a64c5f'478 conversation: conversationHere,
479 canHandOff: !!toolbox?.definitions().some((tool) => tool.name === "hand_off"),
480 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.481 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 asked482 }),
483 memorySection(facts),
Agents recall what Docs say before they answer or work, and each has required reading484 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 asked485 ]
486 .filter(Boolean)
487 .join("\n\n");
488 toolCalls = toolbox?.calls ?? [];
489 const answer = await runTurn(model.send, { model: model.model.model, system, messages: conversation as ModelMessage[], tools: toolbox, price: model.ownModel ? null : model.model.price });
490 // Post as soon as there is an answer; the bill is settled after.
Merge branch 'worktree-agent-a1398e81ad1a64c5f'491 // It wakes nobody, @mentions or not: colleagues get work only through hand_off.
492 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 asked493 return {
494 text: answer.text,
495 tokens: addTokens(answer.tokens, consulted.tokens),
496 cost: (model.ownModel ? 0 : answer.cost) + consulted.cost,
497 rounds: answer.rounds,
498 };
Chat and workspace agents: channels, DMs and named agents you talk to499 });
500
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 asked501 if (!done.ok) return await notice(noticeFor(done.reason, done.message), done.reason);
502 usage = { model: done.model, tier: done.tier, tokens: done.tokens, cost: done.cost, charged: done.charged };
503 if (!done.value.text) {
504 const apology = await surface.post(APOLOGY).catch(() => null);
505 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 to506 }
507 await finish({ status: "replied", reply_id: posted, ...usage });
508 } catch (error) {
509 const message = error instanceof Error ? error.message : String(error);
510 console.error("agents: a reply failed", row.id, delivery.message_id, message);
511 const posted = await surface.post(APOLOGY).catch(() => null);
512 await finish({ status: "failed", error: message, reply_id: posted, ...usage }).catch((failure: unknown) =>
513 console.error("agents: a failed reply was not recorded", id, String(failure)),
514 );
515 }
516}

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