Skip to content
478 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
3 * sandbox (docs/WORKSPACE.md, "Two kinds of turn").
4 *
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 */
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 asked20import { type AgentDelivery, type ServiceBinding, 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 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 { HISTORY_LIMIT, fixedHello, helloAsk, systemPrompt, turns } from "./prompt.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 Specialist, capMentions, orchestratorInstructions, orchestratorTier, rosterLines } from "./orchestrator.ts";
26import { type MeterEnv, metered } from "./meter.ts";
27import { type RecallPlace, memorySection, recall } from "./memory.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)28import { 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 asked29import { type SessionEnv, type SessionRow, actionPorts, sessionRow, startSession, steer } from "./sessions.ts";
30import { type Row, definitionOf, periods, selectAgents, toAgent } from "./store.ts";
Chat and workspace agents: channels, DMs and named agents you talk to31import { 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)32import { Audience } from "./audience.ts";
33import { audiencePorts, toolPorts } from "./ports.ts";
34import { type ToolCall, type ToolPorts, ToolBox } from "./tools.ts";
35import 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 asked36import type { Desk } from "./desk.ts";
37import { type ModelMessage, type Send, NO_TOKENS, addTokens, runTurn } from "./turn.ts";
38import type { AgentRouting as Policy } from "../../runner/src/model-env.ts";
39import { dollars } from "./money.ts";
40
41export { billingRepo } from "./meter.ts";
Chat and workspace agents: channels, DMs and named agents you talk to42
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 asked43export type ReplyEnv = MeterEnv & {
Chat and workspace agents: channels, DMs and named agents you talk to44 DB: D1Database;
45 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)46 /** People and their access, for the audience; members and teams for the roster. */
47 IDENTITY: ServiceBinding;
48 /** Code, for read tools. */
49 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 asked50 /** 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)51 WORK: ServiceBinding;
52 /** Code search, for read tools. */
53 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 asked54 /** Notifications: a session waiting for more budget. */
55 NOTIFY?: ServiceBinding;
56 /** Every agent's desk: sessions are worked on their agent's. */
57 DESKS: DurableObjectNamespace<Desk>;
Chat and workspace agents: channels, DMs and named agents you talk to58};
59
60/** A notice that the agent cannot reply is posted once per conversation in this long. */
61const 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 asked62/** Sessions one reply may start. */
63const MAX_SPIN_OFFS = 2;
Chat and workspace agents: channels, DMs and named agents you talk to64
65const APOLOGY = "Sorry, something went wrong on my side and I couldn't answer that. Try again in a moment.";
66
67/** Who asked, from the message that woke the agent (or their latest one). */
68function askerIn(history: SurfaceMessage[], delivery: AgentDelivery): SurfaceMessage["author"] | null {
69 const woken = history.find((m) => m.id === delivery.message_id);
70 if (woken) return woken.author;
71 return [...history].reverse().find((m) => m.author.kind === "user" && m.author.id === delivery.asked_by)?.author ?? null;
72}
73
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)74/**
75 * An agent's colleagues: every agent of the workspace but itself that is
76 * not archived (docs/WORKSPACE.md, "Agents know each other").
77 */
78async function team(db: D1Database, workspaceId: string, selfId: string, now: Date): Promise<Specialist[]> {
79 const rows = await db
80 .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`)
81 .bind(...periods(now), workspaceId, selfId)
82 .all<Row>();
83 return rows.results.map((row) => {
84 const agent = toAgent(row, now);
85 return {
86 handle: agent.handle,
87 display_name: agent.display_name,
88 role: agent.role,
89 title: agent.title,
90 team: agent.team,
91 department: agent.department,
92 responsibilities: agent.responsibilities,
93 status: agent.status,
94 spent_month_micros: agent.spent_month_micros,
95 monthly_micros: agent.budget.monthly_micros,
96 };
97 });
98}
99
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 asked100/** The agent's latest sessions in this conversation, one line each. */
101async function sessionsHere(db: D1Database, agentId: string, channelId: string): Promise<string | null> {
102 const rows = await db
103 .prepare(
104 "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",
105 )
106 .bind(agentId, channelId)
107 .all<{ id: string; title: string; status: string; summary: string | null; created_at: string }>();
108 if (!rows.results.length) return null;
109 return rows.results
110 .map((s) => `- "${s.title}" (${s.status}, ${s.created_at.slice(0, 10)})${s.summary ? `: ${s.summary.replace(/\s+/g, " ").slice(0, 300)}` : ""}`)
111 .join("\n");
112}
113
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)114/**
115 * Consulting a colleague (docs/WORKSPACE.md, "Agents know each other"):
116 * the colleague answers in a nested turn that posts nothing, with the same
117 * audience (so it can read no more than the conversation may), the same
118 * asker, one hop further, its own routing limits, on the same model
119 * session, billed to this reply. A compact card in the thread says who
120 * asked whom. Consults don't nest: the colleague can't consult in turn.
121 */
122function consulting(input: {
123 db: D1Database;
124 row: Row;
125 delivery: DeskWork;
126 send: Send;
127 policy: Policy;
128 ownModel: boolean;
129 sessionModel: string | null;
130 own: string | null;
131 ports: (consult: ToolPorts["consult"]) => ToolPorts;
132 hops: number;
133 now: Date;
134 surface: Surface;
135 asker: { name: string; display_name: string | null; access: NonNullable<AgentDelivery["asker"]> | null };
136 spent: { tokens: Tokens; cost: number };
137}): { ask: ToolPorts["consult"]; attach(box: ToolBox): void } {
138 let parent: ToolBox | null = null;
139 const noNesting: ToolPorts["consult"] = async () => ({ ok: false, message: "Consults don't nest: answer with what you have." });
140 const ask: ToolPorts["consult"] = async (handle, question) => {
141 const { row, delivery } = input;
142 const colleague = await input.db
143 .prepare("SELECT * FROM agents WHERE workspace_id = ? AND handle = ? AND archived_at IS NULL")
144 .bind(row.workspace_id, handle)
145 .first<Row>();
146 if (!colleague || colleague.id === row.id) return { ok: false, message: `There is no other agent called @${handle} here.` };
147 // No ping-pong: never back to the agent that sent this work.
148 if ((delivery.chain ?? []).at(-1) === colleague.id) return { ok: false, message: `@${handle} sent you this work; answer with what you have.` };
149 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." };
150 const definition = definitionOf(colleague);
151 const allowed = allowedProviders(definition.routing, input.own);
152 if (input.ownModel ? !allowed.own : !allowed.hosted) return { ok: false, message: `@${handle} can't use the model this conversation runs on.` };
153 const model = replyModel(
154 input.policy,
155 { ...definition.routing, pinned: input.ownModel ? definition.routing.pinned : null },
156 { named: input.ownModel ? input.sessionModel : null },
157 );
158 const tools = parent
159 ? parent.forColleague(input.ports(noNesting), { agentId: colleague.id, notConsult: [colleague.handle, row.handle], hops: input.hops + 1, maxHops: input.hops + 1 })
160 : null;
161 const system = systemPrompt({
162 agent: { ...definition, id: colleague.id },
163 workspace: delivery.workspace,
164 channel: { kind: delivery.channel_kind, name: delivery.channel_name },
165 asker: input.asker,
166 today: input.now,
167 tools: tools ? { code: tools.definitions().some((tool) => tool.name === "read_file") } : null,
168 consultedBy: row.handle,
169 });
170 const result = await runTurn(input.send, {
171 model: model.model,
172 system,
173 messages: [{ role: "user", content: `@${row.handle} (agent) asks you: ${question}` }],
174 tools,
175 price: input.ownModel ? null : model.price,
176 });
177 input.spent.tokens = addTokens(input.spent.tokens, result.tokens);
178 input.spent.cost += input.ownModel ? 0 : result.cost;
179 const answer = result.text || "(no answer)";
180 const exchange = `Q: ${question}\nA: ${answer}`;
181 // The exchange, collapsed, in the thread. No @ in its text: it wakes nobody.
182 await input.surface
183 .post(`${row.display_name} asked ${colleague.display_name}`, {
184 kind: "consult",
185 title: `${row.display_name} asked ${colleague.display_name}`,
186 detail: exchange.length > 1000 ? `${exchange.slice(0, 1000)}…` : exchange,
187 state: null,
188 href: null,
189 })
190 .catch((error: unknown) => console.error("agents: a consult card was not posted", String(error)));
191 return { ok: true, colleague: colleague.handle, answer };
192 };
193 return {
194 ask,
195 attach(box) {
196 parent = box;
197 },
198 };
199}
200
Chat and workspace agents: channels, DMs and named agents you talk to201type 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 asked202 status: "replied" | "steered" | "blocked" | "skipped" | "failed";
Chat and workspace agents: channels, DMs and named agents you talk to203 error?: string | null;
204 reply_id?: string | null;
205 model?: string | null;
206 tier?: string | null;
207 tokens?: Tokens;
208 cost?: number;
209 charged?: number;
210};
211
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)212/**
213 * What a desk is handed: a message to answer, or (`hello`) the agent's
214 * first message to the person who made it, in the DM that just opened.
215 */
216export type DeskWork = AgentDelivery & { hello?: boolean };
217
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 asked218/** How a limit's refusal reads in chat, in the agent's voice. */
219function noticeFor(reason: string, message: string): string {
220 if (reason.startsWith("budget_") || reason === "workspace_agent_budget" || message.startsWith("My settings") || message.startsWith("I ")) return message;
221 if (reason === "no_model") return `I can't reply yet: ${message}`;
222 return `I can't reply right now: ${message}`;
223}
224
225/** The session a message in this thread is for: a reply under one of this agent's session cards. */
226async function steeredSession(db: D1Database, agentId: string, threadRoot: string | null): Promise<SessionRow | null> {
227 if (!threadRoot) return null;
228 const found = await db.prepare("SELECT id FROM agent_sessions WHERE agent_id = ? AND card_message_id = ?").bind(agentId, threadRoot).first<{ id: string }>();
229 return found ? sessionRow(db, found.id) : null;
230}
231
232/**
233 * Answers `delivery` as its agent. Never throws: every way it ends is
234 * recorded on the reply's row. A message handed over twice is answered
235 * once.
236 */
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)237export async function reply(env: ReplyEnv, delivery: DeskWork, now = new Date()): Promise<void> {
Chat and workspace agents: channels, DMs and named agents you talk to238 const db = env.DB;
239 const row = await db.prepare("SELECT * FROM agents WHERE id = ?").bind(delivery.agent_id).first<Row>();
240 if (!row || row.archived_at || row.workspace_id !== delivery.workspace_id) return;
241 const id = newId("arp", now.getTime());
242 const claimed = await db
243 .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 asked244 `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)
245 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'working', ?)
Chat and workspace agents: channels, DMs and named agents you talk to246 ON CONFLICT (agent_id, message_id) DO NOTHING RETURNING id`,
247 )
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 .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 to249 .first<{ id: string }>();
250 if (!claimed) return;
251
252 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)253 // 👀 as soon as the desk has it, while everything else goes on.
254 const acknowledging = surface.acknowledge();
Chat and workspace agents: channels, DMs and named agents you talk to255 const slug = delivery.workspace.toLowerCase();
256
257 // What the answer used, once there is one: counted however the reply ends.
258 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 asked259 // 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)260 let toolCalls: ToolCall[] = [];
261 let audienceHash: string | null = null;
Chat and workspace agents: channels, DMs and named agents you talk to262
263 const finish = async (outcome: Outcome) => {
264 const statements = [
265 db
266 .prepare(
267 `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 asked268 cost_micros = ?, charged_micros = ?, tool_count = ?, finished_at = ? WHERE id = ?`,
Chat and workspace agents: channels, DMs and named agents you talk to269 )
270 .bind(
271 outcome.status,
272 outcome.error?.slice(0, 1000) ?? null,
273 outcome.reply_id ?? null,
274 outcome.model ?? null,
275 outcome.tier ?? null,
276 (outcome.tokens?.input ?? 0) + (outcome.tokens?.cacheRead ?? 0) + (outcome.tokens?.cacheWrite ?? 0),
277 outcome.tokens?.output ?? 0,
278 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 asked279 outcome.charged ?? 0,
280 toolCalls.length,
Chat and workspace agents: channels, DMs and named agents you talk to281 new Date().toISOString(),
282 id,
283 ),
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)284 // Every tool call: what was asked, for whom, and whether it was read or withheld.
285 ...toolCalls.map((call, n) =>
286 db
287 .prepare(
288 `INSERT INTO agent_tool_calls (id, reply_id, agent_id, workspace_id, asked_by, tool, args, audience_hash, outcome, bytes, created_at)
289 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
290 )
291 .bind(`${id}_${n}`, id, row.id, row.workspace_id, delivery.asked_by, call.tool, call.args, audienceHash ?? "", call.outcome, call.bytes, now.toISOString()),
292 ),
Chat and workspace agents: channels, DMs and named agents you talk to293 ];
294 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)295 // ✅ when it answered; 👀 taken back after a notice, an apology or nothing.
296 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 asked297 await surface.settle(outcome.status === "replied" || outcome.status === "steered" ? "done" : "withdrawn");
Chat and workspace agents: channels, DMs and named agents you talk to298 };
299
300 /** Says once, in this conversation, why the agent cannot answer; a repeat within hours is kept back. */
301 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)302 // An agent's first words are never an excuse: without a model, a fixed hello.
303 if (delivery.hello) {
304 const posted = await surface.post(fixedHello(row, delivery.asker?.username ?? null)).catch(() => null);
305 return await finish({ status: "blocked", error: reason, reply_id: posted });
306 }
Chat and workspace agents: channels, DMs and named agents you talk to307 const since = new Date(now.getTime() - NOTICE_QUIET_MS).toISOString();
308 const recent = await db
309 .prepare(
310 `SELECT 1 FROM agent_replies WHERE agent_id = ? AND channel_id = ? AND status = 'blocked' AND error = ?
311 AND reply_id IS NOT NULL AND created_at > ? AND id <> ? LIMIT 1`,
312 )
313 .bind(row.id, delivery.channel_id, reason, since, id)
314 .first();
315 const posted = recent ? null : await surface.post(message).catch(() => null);
316 await finish({ status: "blocked", error: reason, reply_id: posted });
317 };
318
319 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 asked320 // A reply under one of its session cards steers that session; nothing is answered here.
321 const steered = delivery.hello ? null : await steeredSession(db, row.id, delivery.thread_root);
322 if (steered) {
323 const history = await surface.history(HISTORY_LIMIT).catch(() => [] as SurfaceMessage[]);
324 const message = history.find((m) => m.id === delivery.message_id);
325 if (message?.body.trim()) {
326 await steer(env as unknown as SessionEnv, steered, message.author.name, message.body);
327 return await finish({ status: "steered" });
328 }
Chat and workspace agents: channels, DMs and named agents you talk to329 }
330
331 // 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)332 // A hello has no conversation yet: it is asked to introduce itself.
333 const [, history] = await Promise.all([surface.typing(), delivery.hello ? Promise.resolve([]) : surface.history(HISTORY_LIMIT)]);
334 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 to335 if (!conversation.length) return await finish({ status: "skipped", error: "nothing to answer" });
336 const author = askerIn(history, delivery);
337 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)338 // Every agent knows its colleagues; @g1t also steps up a tier to decide
339 // who gets the work in a long thread.
340 const specialists = await team(db, row.workspace_id, row.id, now);
341 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 asked342 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)343 const hops = Math.max(0, Math.floor(delivery.hops || 0));
344 const chain = delivery.chain ?? [];
345 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 asked346 const asker = { name: askerName ?? "someone", display_name: author?.display_name ?? null, access: delivery.asker ?? null };
347 let posted: string | null = null;
348 let spinOffs = 0;
349
350 const done = await metered(env, { row, payer: row, slug, task: "reply", start, askerName }, async (model) => {
351 // What colleagues consulted along the way used: billed to this reply.
352 const consulted = { tokens: NO_TOKENS, cost: 0 };
353 let toolbox: ToolBox | null = null;
354 let place: RecallPlace = { channel_id: delivery.channel_id, kind: delivery.channel_kind === "dm" ? "dm" : "private", people: [delivery.asked_by] };
355 if (!delivery.hello) {
356 try {
357 const audience = await Audience.build(slug, delivery.asked_by, audiencePorts(env, slug, delivery.channel_id));
358 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 together359 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 asked360 audienceHash = audience.hash;
361 const consult = consulting({
362 db,
363 row,
364 delivery,
365 send: model.send,
366 policy: model.policy,
367 ownModel: model.ownModel,
368 sessionModel: model.sessionModel,
369 own: model.own,
370 ports,
371 hops,
372 now,
373 surface,
374 asker,
375 spent: consulted,
376 });
377 const where = delivery.channel_kind === "dm" ? "a direct message" : `#${delivery.channel_name ?? "a channel"}`;
378 const actions = actionPorts(env as unknown as SessionEnv, {
379 agent: row,
380 place,
381 source: { kind: "message", ref: delivery.message_id, label: askerName ? `@${askerName} in ${where}` : where, channel_id: delivery.channel_id },
382 asker: { id: delivery.asked_by, username: askerName },
383 workspace: slug,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar384 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 asked385 // Real work becomes a session, with its card in this conversation.
386 spinOff: async (title, goal) => {
387 if (spinOffs >= MAX_SPIN_OFFS) return { ok: false, message: "You've started enough sessions from this message." };
388 spinOffs++;
389 const session = await startSession(env as unknown as SessionEnv, {
390 agent: row,
391 kind: "chat",
392 title,
393 goal: `${askerName ? `@${askerName}` : "Someone"} asked in ${where}:\n\n${goal}`,
394 workspace: slug,
395 channel_id: delivery.channel_id,
396 channel_kind: delivery.channel_kind,
397 channel_name: delivery.channel_name,
398 thread_root: delivery.thread_root,
399 message_id: delivery.message_id,
400 asked_by: delivery.asked_by,
401 asked_by_username: askerName,
402 asker: delivery.asker ?? null,
403 chain: [...chain],
404 hops,
405 });
406 const cap = session.cap_micros ? ` with a cap of ${dollars(session.cap_micros)}` : "";
407 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.` };
408 },
409 });
410 toolbox = new ToolBox(
411 audience,
412 ports(consult.ask),
413 { agentId: row.id, notConsult: [row.handle, ...(sender ? [sender.handle] : [])], hops, maxHops: CHAT_MAX_HOPS },
414 [],
415 actions,
416 );
417 consult.attach(toolbox);
418 } catch (error) {
419 // Without an audience nothing may be read: the reply goes on with this conversation only.
420 console.error("agents: no audience for a reply, so no tools", row.id, String(error));
421 }
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)422 }
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 asked423 const [facts, recent] = delivery.hello
424 ? [[], null]
425 : await Promise.all([recall(db, row.id, place).catch(() => []), sessionsHere(db, row.id, delivery.channel_id).catch(() => null)]);
426 const system = [
427 systemPrompt({
428 agent: {
429 ...definition,
430 id: row.id,
431 // @g1t's job is fixed; what the workspace wrote is added to it.
432 instructions: row.builtin ? orchestratorInstructions(specialists.filter((a) => a.handle !== "g1t"), definition.instructions) : definition.instructions,
433 },
434 workspace: delivery.workspace,
435 channel: { kind: delivery.channel_kind, name: delivery.channel_name },
436 asker,
437 today: now,
438 tools: toolbox ? { code: toolbox.definitions().some((tool) => tool.name === "read_file") } : null,
439 // @g1t's team is in its job; everyone else is told who their colleagues are.
440 colleagues: row.builtin ? null : rosterLines(specialists),
441 recentSessions: recent,
442 }),
443 memorySection(facts),
444 ]
445 .filter(Boolean)
446 .join("\n\n");
447 toolCalls = toolbox?.calls ?? [];
448 const answer = await runTurn(model.send, { model: model.model.model, system, messages: conversation as ModelMessage[], tools: toolbox, price: model.ownModel ? null : model.model.price });
449 // Post as soon as there is an answer; the bill is settled after.
450 if (answer.text) {
451 // At most two specialists woken by one of @g1t's messages: a rail, not only a rule in its prompt.
452 const text = row.builtin ? capMentions(answer.text, specialists.map((agent) => agent.handle)) : answer.text;
453 posted = await surface.post(text);
454 }
455 return {
456 text: answer.text,
457 tokens: addTokens(answer.tokens, consulted.tokens),
458 cost: (model.ownModel ? 0 : answer.cost) + consulted.cost,
459 rounds: answer.rounds,
460 };
Chat and workspace agents: channels, DMs and named agents you talk to461 });
462
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 asked463 if (!done.ok) return await notice(noticeFor(done.reason, done.message), done.reason);
464 usage = { model: done.model, tier: done.tier, tokens: done.tokens, cost: done.cost, charged: done.charged };
465 if (!done.value.text) {
466 const apology = await surface.post(APOLOGY).catch(() => null);
467 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 to468 }
469 await finish({ status: "replied", reply_id: posted, ...usage });
470 } catch (error) {
471 const message = error instanceof Error ? error.message : String(error);
472 console.error("agents: a reply failed", row.id, delivery.message_id, message);
473 const posted = await surface.post(APOLOGY).catch(() => null);
474 await finish({ status: "failed", error: message, reply_id: posted, ...usage }).catch((failure: unknown) =>
475 console.error("agents: a failed reply was not recorded", id, String(failure)),
476 );
477 }
478}

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