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