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 | |
| 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 to | 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 | */ |
| Merge branch 'worktree-agent-a1398e81ad1a64c5f' | 20 | import { type AgentDelivery, type ServiceBinding, identityClient, 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 branch 'worktree-agent-a1398e81ad1a64c5f' | 24 | import { 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) | 25 | import { 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. | 26 | import { skillsSection } from "./skills.ts"; |
| Merge branch 'worktree-agent-a1398e81ad1a64c5f' | 27 | import { 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 asked | 28 | import { type MeterEnv, metered } from "./meter.ts"; |
| 29 | import { type RecallPlace, memorySection, recall } from "./memory.ts"; | |
| Agents recall what Docs say before they answer or work, and each has required reading | 30 | 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) | 31 | 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 | 32 | import { type SessionEnv, type SessionRow, actionPorts, sessionRow, startSession, steer } from "./sessions.ts"; |
| 33 | import { type Row, definitionOf, periods, selectAgents, toAgent } from "./store.ts"; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 34 | 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) | 35 | import { Audience } from "./audience.ts"; |
| 36 | import { audiencePorts, toolPorts } from "./ports.ts"; | |
| 37 | import { type ToolCall, type ToolPorts, ToolBox } from "./tools.ts"; | |
| 38 | 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 | 39 | import type { Desk } from "./desk.ts"; |
| 40 | import { type ModelMessage, type Send, NO_TOKENS, addTokens, runTurn } from "./turn.ts"; | |
| 41 | import type { AgentRouting as Policy } from "../../runner/src/model-env.ts"; | |
| 42 | import { dollars } from "./money.ts"; | |
| 43 | ||
| 44 | export { billingRepo } from "./meter.ts"; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 45 | |
| 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 | 46 | export type ReplyEnv = MeterEnv & { |
| Chat and workspace agents: channels, DMs and named agents you talk to | 47 | 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 asked | 53 | /** 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 asked | 57 | /** 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 to | 61 | }; |
| 62 | ||
| 63 | /** A notice that the agent cannot reply is posted once per conversation in this long. */ | |
| 64 | 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 | 65 | /** Sessions one reply may start. */ |
| 66 | const MAX_SPIN_OFFS = 2; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 67 | |
| 68 | const 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). */ | |
| 71 | function 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 | */ |
| 81 | async 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 asked | 103 | /** The agent's latest sessions in this conversation, one line each. */ |
| 104 | async 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 | */ | |
| 125 | function 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 to | 204 | 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 | 205 | status: "replied" | "steered" | "blocked" | "skipped" | "failed"; |
| Chat and workspace agents: channels, DMs and named agents you talk to | 206 | 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 | */ | |
| 219 | export 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 asked | 221 | /** How a limit's refusal reads in chat, in the agent's voice. */ |
| 222 | function 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. */ | |
| 229 | async 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) | 240 | 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 | 241 | 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 asked | 247 | `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 to | 249 | 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 asked | 251 | .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 | 252 | .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 to | 258 | 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 asked | 262 | // 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 to | 265 | |
| 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 asked | 271 | cost_micros = ?, charged_micros = ?, tool_count = ?, finished_at = ? WHERE id = ?`, |
| Chat and workspace agents: channels, DMs and named agents you talk to | 272 | ) |
| 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 asked | 282 | outcome.charged ?? 0, |
| 283 | toolCalls.length, | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 284 | 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 to | 296 | ]; |
| 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 asked | 300 | await surface.settle(outcome.status === "replied" || outcome.status === "steered" ? "done" : "withdrawn"); |
| Chat and workspace agents: channels, DMs and named agents you talk to | 301 | }; |
| 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 to | 310 | 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 asked | 323 | // 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 to | 332 | } |
| 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 to | 343 | 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 asked | 350 | 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 asked | 354 | 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 asked | 359 | // 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 together | 367 | 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 | 368 | 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 calendar | 392 | 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 | 393 | // 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 asked | 440 | 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 reading | 454 | 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 asked | 462 | 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 asked | 482 | }), |
| 483 | memorySection(facts), | |
| Agents recall what Docs say before they answer or work, and each has required reading | 484 | 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 | 485 | ] |
| 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 asked | 493 | 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 to | 499 | }); |
| 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 asked | 501 | 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 to | 506 | } |
| 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.