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 | |
| Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how. | 22 | import { type AbilityEnv, abilitiesFor, abilitiesSection, abilityPorts, saidText } from "./abilities.ts"; |
| 23 | ||
| 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 { 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 | 25 | import type { Tokens } from "./budget.ts"; |
| Merge branch 'worktree-agent-a1398e81ad1a64c5f' | 26 | 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) | 27 | import { HISTORY_LIMIT, fixedHello, helloAsk, systemPrompt, turns } from "./prompt.ts"; |
| Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how. | 28 | import { loadShelf, skillsSection, teamSlugs } from "./skills.ts"; |
| 29 | import { readVersion } from "./skill-library.ts"; | |
| Merge branch 'worktree-agent-a1398e81ad1a64c5f' | 30 | 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 | 31 | import { type MeterEnv, metered } from "./meter.ts"; |
| 32 | import { type RecallPlace, memorySection, recall } from "./memory.ts"; | |
| Agents recall what Docs say before they answer or work, and each has required reading | 33 | import { recallQuery, recallSection } from "./recall.ts"; |
| Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how. | 34 | import { REPLY_TIER, allowedProviders, effortOf, effortPlan, 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 | 35 | import { type SessionEnv, type SessionRow, actionPorts, sessionRow, startSession, steer } from "./sessions.ts"; |
| 36 | import { type Row, definitionOf, periods, selectAgents, toAgent } from "./store.ts"; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 37 | 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) | 38 | import { Audience } from "./audience.ts"; |
| An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how. | 39 | import { audiencePorts, loadTeams, teamsOfAgents, toolPorts } from "./ports.ts"; |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 40 | import { type ToolCall, type ToolPorts, ToolBox } from "./tools.ts"; |
| 41 | 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 | 42 | import type { Desk } from "./desk.ts"; |
| 43 | import { type ModelMessage, type Send, NO_TOKENS, addTokens, runTurn } from "./turn.ts"; | |
| 44 | import type { AgentRouting as Policy } from "../../runner/src/model-env.ts"; | |
| 45 | import { dollars } from "./money.ts"; | |
| People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how. | 46 | import { teamsSection } from "./teammates.ts"; |
| Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how. | 47 | import { personalRefusal } from "./access.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 | 48 | |
| 49 | export { billingRepo } from "./meter.ts"; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 50 | |
| 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 | export type ReplyEnv = MeterEnv & { |
| Chat and workspace agents: channels, DMs and named agents you talk to | 52 | DB: D1Database; |
| 53 | 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) | 54 | /** People and their access, for the audience; members and teams for the roster. */ |
| 55 | IDENTITY: ServiceBinding; | |
| 56 | /** Code, for read tools. */ | |
| 57 | 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 | 58 | /** 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) | 59 | WORK: ServiceBinding; |
| 60 | /** Code search, for read tools. */ | |
| 61 | 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 | 62 | /** Notifications: a session waiting for more budget. */ |
| 63 | NOTIFY?: ServiceBinding; | |
| Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how. | 64 | /** The workspace's connections, for abilities outside g1t (abilities.ts). */ |
| 65 | INTEGRATIONS: ServiceBinding; | |
| 66 | /** The audit log, for refusals. */ | |
| 67 | EVENTS: 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 | 68 | /** Every agent's desk: sessions are worked on their agent's. */ |
| 69 | DESKS: DurableObjectNamespace<Desk>; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 70 | }; |
| 71 | ||
| 72 | /** A notice that the agent cannot reply is posted once per conversation in this long. */ | |
| 73 | 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 | 74 | /** Sessions one reply may start. */ |
| 75 | const MAX_SPIN_OFFS = 2; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 76 | |
| 77 | const APOLOGY = "Sorry, something went wrong on my side and I couldn't answer that. Try again in a moment."; | |
| 78 | ||
| 79 | /** Who asked, from the message that woke the agent (or their latest one). */ | |
| 80 | function askerIn(history: SurfaceMessage[], delivery: AgentDelivery): SurfaceMessage["author"] | null { | |
| 81 | const woken = history.find((m) => m.id === delivery.message_id); | |
| 82 | if (woken) return woken.author; | |
| 83 | return [...history].reverse().find((m) => m.author.kind === "user" && m.author.id === delivery.asked_by)?.author ?? null; | |
| 84 | } | |
| 85 | ||
| 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) | 86 | /** |
| 87 | * 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. | 88 | * 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) | 89 | */ |
| An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how. | 90 | async function team(db: D1Database, workspaceId: string, selfId: string, now: Date, teamsOf: Promise<Map<string, string[]>>): Promise<Specialist[]> { |
| 91 | const [rows, onTeams] = await Promise.all([ | |
| 92 | db | |
| 93 | // Personal agents are their members' own: no colleague of anyone. | |
| 94 | .prepare(`${selectAgents("a.workspace_id = ?3 AND a.archived_at IS NULL AND a.id <> ?4 AND a.scope = 'workspace'")} ORDER BY a.builtin DESC, a.handle LIMIT 50`) | |
| 95 | .bind(...periods(now), workspaceId, selfId) | |
| 96 | .all<Row>(), | |
| 97 | teamsOf, | |
| 98 | ]); | |
| 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) | 99 | return rows.results.map((row) => { |
| 100 | const agent = toAgent(row, now); | |
| 101 | return { | |
| 102 | handle: agent.handle, | |
| 103 | display_name: agent.display_name, | |
| 104 | role: agent.role, | |
| 105 | title: agent.title, | |
| An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how. | 106 | teams: onTeams.get(agent.id) ?? [], |
| 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) | 107 | responsibilities: agent.responsibilities, |
| 108 | status: agent.status, | |
| 109 | spent_month_micros: agent.spent_month_micros, | |
| 110 | monthly_micros: agent.budget.monthly_micros, | |
| 111 | }; | |
| 112 | }); | |
| 113 | } | |
| 114 | ||
| 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 | 115 | /** The agent's latest sessions in this conversation, one line each. */ |
| 116 | async function sessionsHere(db: D1Database, agentId: string, channelId: string): Promise<string | null> { | |
| 117 | const rows = await db | |
| 118 | .prepare( | |
| 119 | "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", | |
| 120 | ) | |
| 121 | .bind(agentId, channelId) | |
| 122 | .all<{ id: string; title: string; status: string; summary: string | null; created_at: string }>(); | |
| 123 | if (!rows.results.length) return null; | |
| 124 | return rows.results | |
| 125 | .map((s) => `- "${s.title}" (${s.status}, ${s.created_at.slice(0, 10)})${s.summary ? `: ${s.summary.replace(/\s+/g, " ").slice(0, 300)}` : ""}`) | |
| 126 | .join("\n"); | |
| 127 | } | |
| 128 | ||
| 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) | 129 | /** |
| 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. | 130 | * 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) | 131 | * the colleague answers in a nested turn that posts nothing, with the same |
| 132 | * audience (so it can read no more than the conversation may), the same | |
| 133 | * asker, one hop further, its own routing limits, on the same model | |
| 134 | * session, billed to this reply. A compact card in the thread says who | |
| 135 | * asked whom. Consults don't nest: the colleague can't consult in turn. | |
| 136 | */ | |
| 137 | function consulting(input: { | |
| 138 | db: D1Database; | |
| 139 | row: Row; | |
| 140 | delivery: DeskWork; | |
| 141 | send: Send; | |
| 142 | policy: Policy; | |
| 143 | ownModel: boolean; | |
| 144 | sessionModel: string | null; | |
| 145 | own: string | null; | |
| 146 | ports: (consult: ToolPorts["consult"]) => ToolPorts; | |
| 147 | hops: number; | |
| 148 | now: Date; | |
| 149 | surface: Surface; | |
| 150 | asker: { name: string; display_name: string | null; access: NonNullable<AgentDelivery["asker"]> | null }; | |
| 151 | spent: { tokens: Tokens; cost: number }; | |
| 152 | }): { ask: ToolPorts["consult"]; attach(box: ToolBox): void } { | |
| 153 | let parent: ToolBox | null = null; | |
| 154 | const noNesting: ToolPorts["consult"] = async () => ({ ok: false, message: "Consults don't nest: answer with what you have." }); | |
| 155 | const ask: ToolPorts["consult"] = async (handle, question) => { | |
| 156 | const { row, delivery } = input; | |
| 157 | const colleague = await input.db | |
| Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how. | 158 | .prepare("SELECT * FROM agents WHERE workspace_id = ? AND handle = ? AND archived_at IS NULL AND scope = 'workspace'") |
| 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) | 159 | .bind(row.workspace_id, handle) |
| 160 | .first<Row>(); | |
| 161 | if (!colleague || colleague.id === row.id) return { ok: false, message: `There is no other agent called @${handle} here.` }; | |
| 162 | // No ping-pong: never back to the agent that sent this work. | |
| 163 | if ((delivery.chain ?? []).at(-1) === colleague.id) return { ok: false, message: `@${handle} sent you this work; answer with what you have.` }; | |
| 164 | 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." }; | |
| 165 | const definition = definitionOf(colleague); | |
| 166 | const allowed = allowedProviders(definition.routing, input.own); | |
| 167 | if (input.ownModel ? !allowed.own : !allowed.hosted) return { ok: false, message: `@${handle} can't use the model this conversation runs on.` }; | |
| 168 | const model = replyModel( | |
| 169 | input.policy, | |
| 170 | { ...definition.routing, pinned: input.ownModel ? definition.routing.pinned : null }, | |
| 171 | { named: input.ownModel ? input.sessionModel : null }, | |
| 172 | ); | |
| 173 | const tools = parent | |
| 174 | ? parent.forColleague(input.ports(noNesting), { agentId: colleague.id, notConsult: [colleague.handle, row.handle], hops: input.hops + 1, maxHops: input.hops + 1 }) | |
| 175 | : null; | |
| 176 | const system = systemPrompt({ | |
| 177 | agent: { ...definition, id: colleague.id }, | |
| 178 | workspace: delivery.workspace, | |
| 179 | channel: { kind: delivery.channel_kind, name: delivery.channel_name }, | |
| 180 | asker: input.asker, | |
| 181 | today: input.now, | |
| 182 | tools: tools ? { code: tools.definitions().some((tool) => tool.name === "read_file") } : null, | |
| 183 | consultedBy: row.handle, | |
| 184 | }); | |
| 185 | const result = await runTurn(input.send, { | |
| 186 | model: model.model, | |
| 187 | system, | |
| 188 | messages: [{ role: "user", content: `@${row.handle} (agent) asks you: ${question}` }], | |
| 189 | tools, | |
| 190 | price: input.ownModel ? null : model.price, | |
| 191 | }); | |
| 192 | input.spent.tokens = addTokens(input.spent.tokens, result.tokens); | |
| 193 | input.spent.cost += input.ownModel ? 0 : result.cost; | |
| 194 | const answer = result.text || "(no answer)"; | |
| 195 | const exchange = `Q: ${question}\nA: ${answer}`; | |
| 196 | // The exchange, collapsed, in the thread. No @ in its text: it wakes nobody. | |
| 197 | await input.surface | |
| 198 | .post(`${row.display_name} asked ${colleague.display_name}`, { | |
| 199 | kind: "consult", | |
| 200 | title: `${row.display_name} asked ${colleague.display_name}`, | |
| 201 | detail: exchange.length > 1000 ? `${exchange.slice(0, 1000)}…` : exchange, | |
| 202 | state: null, | |
| 203 | href: null, | |
| 204 | }) | |
| 205 | .catch((error: unknown) => console.error("agents: a consult card was not posted", String(error))); | |
| 206 | return { ok: true, colleague: colleague.handle, answer }; | |
| 207 | }; | |
| 208 | return { | |
| 209 | ask, | |
| 210 | attach(box) { | |
| 211 | parent = box; | |
| 212 | }, | |
| 213 | }; | |
| 214 | } | |
| 215 | ||
| Chat and workspace agents: channels, DMs and named agents you talk to | 216 | 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 | 217 | status: "replied" | "steered" | "blocked" | "skipped" | "failed"; |
| Chat and workspace agents: channels, DMs and named agents you talk to | 218 | error?: string | null; |
| 219 | reply_id?: string | null; | |
| 220 | model?: string | null; | |
| 221 | tier?: string | null; | |
| 222 | tokens?: Tokens; | |
| 223 | cost?: number; | |
| 224 | charged?: number; | |
| Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how. | 225 | /** The effort level it ran at. */ |
| 226 | effort?: string | null; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 227 | }; |
| 228 | ||
| 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) | 229 | /** |
| 230 | * What a desk is handed: a message to answer, or (`hello`) the agent's | |
| 231 | * first message to the person who made it, in the DM that just opened. | |
| 232 | */ | |
| 233 | export type DeskWork = AgentDelivery & { hello?: boolean }; | |
| 234 | ||
| 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 | 235 | /** How a limit's refusal reads in chat, in the agent's voice. */ |
| 236 | function noticeFor(reason: string, message: string): string { | |
| 237 | if (reason.startsWith("budget_") || reason === "workspace_agent_budget" || message.startsWith("My settings") || message.startsWith("I ")) return message; | |
| 238 | if (reason === "no_model") return `I can't reply yet: ${message}`; | |
| 239 | return `I can't reply right now: ${message}`; | |
| 240 | } | |
| 241 | ||
| 242 | /** The session a message in this thread is for: a reply under one of this agent's session cards. */ | |
| 243 | async function steeredSession(db: D1Database, agentId: string, threadRoot: string | null): Promise<SessionRow | null> { | |
| 244 | if (!threadRoot) return null; | |
| 245 | const found = await db.prepare("SELECT id FROM agent_sessions WHERE agent_id = ? AND card_message_id = ?").bind(agentId, threadRoot).first<{ id: string }>(); | |
| 246 | return found ? sessionRow(db, found.id) : null; | |
| 247 | } | |
| 248 | ||
| 249 | /** | |
| 250 | * Answers `delivery` as its agent. Never throws: every way it ends is | |
| 251 | * recorded on the reply's row. A message handed over twice is answered | |
| 252 | * once. | |
| 253 | */ | |
| 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 | 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 | 255 | const db = env.DB; |
| 256 | const row = await db.prepare("SELECT * FROM agents WHERE id = ?").bind(delivery.agent_id).first<Row>(); | |
| 257 | if (!row || row.archived_at || row.workspace_id !== delivery.workspace_id) return; | |
| 258 | const id = newId("arp", now.getTime()); | |
| 259 | const claimed = await db | |
| 260 | .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 | 261 | `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) |
| 262 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'working', ?) | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 263 | ON CONFLICT (agent_id, message_id) DO NOTHING RETURNING id`, |
| 264 | ) | |
| 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 | 265 | .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 | 266 | .first<{ id: string }>(); |
| 267 | if (!claimed) return; | |
| 268 | ||
| 269 | 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) | 270 | // 👀 as soon as the desk has it, while everything else goes on. |
| 271 | const acknowledging = surface.acknowledge(); | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 272 | const slug = delivery.workspace.toLowerCase(); |
| 273 | ||
| 274 | // What the answer used, once there is one: counted however the reply ends. | |
| 275 | 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 | 276 | // 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) | 277 | let toolCalls: ToolCall[] = []; |
| 278 | let audienceHash: string | null = null; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 279 | |
| 280 | const finish = async (outcome: Outcome) => { | |
| 281 | const statements = [ | |
| 282 | db | |
| 283 | .prepare( | |
| 284 | `UPDATE agent_replies SET status = ?, error = ?, reply_id = ?, model = ?, tier = ?, input_tokens = ?, output_tokens = ?, | |
| Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how. | 285 | cost_micros = ?, charged_micros = ?, tool_count = ?, finished_at = ?, effort = ? WHERE id = ?`, |
| Chat and workspace agents: channels, DMs and named agents you talk to | 286 | ) |
| 287 | .bind( | |
| 288 | outcome.status, | |
| 289 | outcome.error?.slice(0, 1000) ?? null, | |
| 290 | outcome.reply_id ?? null, | |
| 291 | outcome.model ?? null, | |
| 292 | outcome.tier ?? null, | |
| 293 | (outcome.tokens?.input ?? 0) + (outcome.tokens?.cacheRead ?? 0) + (outcome.tokens?.cacheWrite ?? 0), | |
| 294 | outcome.tokens?.output ?? 0, | |
| 295 | 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 | 296 | outcome.charged ?? 0, |
| 297 | toolCalls.length, | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 298 | new Date().toISOString(), |
| Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how. | 299 | outcome.effort ?? null, |
| Chat and workspace agents: channels, DMs and named agents you talk to | 300 | id, |
| 301 | ), | |
| 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 | // Every tool call: what was asked, for whom, and whether it was read or withheld. |
| 303 | ...toolCalls.map((call, n) => | |
| 304 | db | |
| 305 | .prepare( | |
| 306 | `INSERT INTO agent_tool_calls (id, reply_id, agent_id, workspace_id, asked_by, tool, args, audience_hash, outcome, bytes, created_at) | |
| 307 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, | |
| 308 | ) | |
| 309 | .bind(`${id}_${n}`, id, row.id, row.workspace_id, delivery.asked_by, call.tool, call.args, audienceHash ?? "", call.outcome, call.bytes, now.toISOString()), | |
| 310 | ), | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 311 | ]; |
| 312 | 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) | 313 | // ✅ when it answered; 👀 taken back after a notice, an apology or nothing. |
| 314 | 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 | 315 | await surface.settle(outcome.status === "replied" || outcome.status === "steered" ? "done" : "withdrawn"); |
| Chat and workspace agents: channels, DMs and named agents you talk to | 316 | }; |
| 317 | ||
| 318 | /** Says once, in this conversation, why the agent cannot answer; a repeat within hours is kept back. */ | |
| 319 | 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) | 320 | // An agent's first words are never an excuse: without a model, a fixed hello. |
| 321 | if (delivery.hello) { | |
| 322 | const posted = await surface.post(fixedHello(row, delivery.asker?.username ?? null)).catch(() => null); | |
| 323 | return await finish({ status: "blocked", error: reason, reply_id: posted }); | |
| 324 | } | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 325 | const since = new Date(now.getTime() - NOTICE_QUIET_MS).toISOString(); |
| 326 | const recent = await db | |
| 327 | .prepare( | |
| 328 | `SELECT 1 FROM agent_replies WHERE agent_id = ? AND channel_id = ? AND status = 'blocked' AND error = ? | |
| 329 | AND reply_id IS NOT NULL AND created_at > ? AND id <> ? LIMIT 1`, | |
| 330 | ) | |
| 331 | .bind(row.id, delivery.channel_id, reason, since, id) | |
| 332 | .first(); | |
| 333 | const posted = recent ? null : await surface.post(message).catch(() => null); | |
| 334 | await finish({ status: "blocked", error: reason, reply_id: posted }); | |
| 335 | }; | |
| 336 | ||
| 337 | 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 | 338 | // A reply under one of its session cards steers that session; nothing is answered here. |
| 339 | const steered = delivery.hello ? null : await steeredSession(db, row.id, delivery.thread_root); | |
| 340 | if (steered) { | |
| 341 | const history = await surface.history(HISTORY_LIMIT).catch(() => [] as SurfaceMessage[]); | |
| 342 | const message = history.find((m) => m.id === delivery.message_id); | |
| 343 | if (message?.body.trim()) { | |
| 344 | await steer(env as unknown as SessionEnv, steered, message.author.name, message.body); | |
| 345 | return await finish({ status: "steered" }); | |
| 346 | } | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 347 | } |
| 348 | ||
| 349 | // 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) | 350 | // A hello has no conversation yet: it is asked to introduce itself. |
| Merge branch 'worktree-agent-a1398e81ad1a64c5f' | 351 | // And who is here: said every turn, so the agent knows who reads it and who doesn't. |
| People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how. | 352 | // And its teams, from their pages: who leads, who owns what, who is around. |
| 353 | const [, history, conversationHere, teamsHere] = await Promise.all([ | |
| Merge branch 'worktree-agent-a1398e81ad1a64c5f' | 354 | surface.typing(), |
| 355 | delivery.hello ? Promise.resolve([]) : surface.history(HISTORY_LIMIT), | |
| 356 | delivery.hello ? Promise.resolve(null) : surface.conversation(), | |
| An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how. | 357 | loadTeams(env, slug, row.workspace_id, { id: row.id }).catch(() => null), |
| Merge branch 'worktree-agent-a1398e81ad1a64c5f' | 358 | ]); |
| Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how. | 359 | // A personal agent answers only its member, in the DM of the two of them (access.ts). |
| 360 | const refused = personalRefusal(row, { | |
| 361 | asked_by: delivery.asked_by, | |
| 362 | channel_kind: delivery.channel_kind, | |
| 363 | members: conversationHere ? conversationHere.people + conversationHere.agents : null, | |
| 364 | }); | |
| 365 | if (refused) return await notice(refused, "personal_agent"); | |
| 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) | 366 | 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 | 367 | if (!conversation.length) return await finish({ status: "skipped", error: "nothing to answer" }); |
| 368 | const author = askerIn(history, delivery); | |
| 369 | 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) | 370 | // Every agent knows its colleagues; @g1t also steps up a tier to decide |
| 371 | // who gets the work in a long thread. | |
| An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how. | 372 | const specialists = await team(db, row.workspace_id, row.id, now, teamsOfAgents(env, slug)); |
| 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 | 373 | const definition = definitionOf(row); |
| Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how. | 374 | // Its effort setting decides where the reply starts and how hard the model reasons. |
| 375 | const plan = effortPlan(effortOf(definition.routing), row.builtin ? orchestratorTier(history.length, specialists.filter((a) => a.handle !== "g1t").length) : REPLY_TIER); | |
| 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) | 376 | const hops = Math.max(0, Math.floor(delivery.hops || 0)); |
| 377 | const chain = delivery.chain ?? []; | |
| 378 | 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 | 379 | const asker = { name: askerName ?? "someone", display_name: author?.display_name ?? null, access: delivery.asker ?? null }; |
| 380 | let posted: string | null = null; | |
| 381 | let spinOffs = 0; | |
| 382 | ||
| Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how. | 383 | const done = await metered(env, { row, payer: row, slug, task: "reply", start: plan.start, effort: plan.level, askerName, person: delivery.asker?.username ?? null, teams: teamsHere }, async (model) => { |
| Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked | 384 | // What colleagues consulted along the way used: billed to this reply. |
| 385 | const consulted = { tokens: NO_TOKENS, cost: 0 }; | |
| 386 | let toolbox: ToolBox | null = null; | |
| Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how. | 387 | // The "Your abilities outside g1t" section, once the tool box has them. |
| 388 | let abilitiesText: string | null = 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 | 389 | let place: RecallPlace = { channel_id: delivery.channel_id, kind: delivery.channel_kind === "dm" ? "dm" : "private", people: [delivery.asked_by] }; |
| 390 | if (!delivery.hello) { | |
| 391 | try { | |
| 392 | const audience = await Audience.build(slug, delivery.asked_by, audiencePorts(env, slug, delivery.channel_id)); | |
| 393 | 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 | 394 | 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 | 395 | audienceHash = audience.hash; |
| 396 | const consult = consulting({ | |
| 397 | db, | |
| 398 | row, | |
| 399 | delivery, | |
| 400 | send: model.send, | |
| 401 | policy: model.policy, | |
| 402 | ownModel: model.ownModel, | |
| 403 | sessionModel: model.sessionModel, | |
| 404 | own: model.own, | |
| 405 | ports, | |
| 406 | hops, | |
| 407 | now, | |
| 408 | surface, | |
| 409 | asker, | |
| 410 | spent: consulted, | |
| 411 | }); | |
| 412 | const where = delivery.channel_kind === "dm" ? "a direct message" : `#${delivery.channel_name ?? "a channel"}`; | |
| 413 | const actions = actionPorts(env as unknown as SessionEnv, { | |
| 414 | agent: row, | |
| 415 | place, | |
| 416 | source: { kind: "message", ref: delivery.message_id, label: askerName ? `@${askerName} in ${where}` : where, channel_id: delivery.channel_id }, | |
| 417 | asker: { id: delivery.asked_by, username: askerName }, | |
| 418 | workspace: slug, | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 419 | 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 | 420 | // Real work becomes a session, with its card in this conversation. |
| 421 | spinOff: async (title, goal) => { | |
| 422 | if (spinOffs >= MAX_SPIN_OFFS) return { ok: false, message: "You've started enough sessions from this message." }; | |
| 423 | spinOffs++; | |
| 424 | const session = await startSession(env as unknown as SessionEnv, { | |
| 425 | agent: row, | |
| 426 | kind: "chat", | |
| 427 | title, | |
| 428 | goal: `${askerName ? `@${askerName}` : "Someone"} asked in ${where}:\n\n${goal}`, | |
| 429 | workspace: slug, | |
| 430 | channel_id: delivery.channel_id, | |
| 431 | channel_kind: delivery.channel_kind, | |
| 432 | channel_name: delivery.channel_name, | |
| 433 | thread_root: delivery.thread_root, | |
| 434 | message_id: delivery.message_id, | |
| 435 | asked_by: delivery.asked_by, | |
| 436 | asked_by_username: askerName, | |
| 437 | asker: delivery.asker ?? null, | |
| 438 | chain: [...chain], | |
| 439 | hops, | |
| 440 | }); | |
| 441 | const cap = session.cap_micros ? ` with a cap of ${dollars(session.cap_micros)}` : ""; | |
| 442 | 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.` }; | |
| 443 | }, | |
| 444 | }); | |
| Merge branch 'worktree-agent-a1398e81ad1a64c5f' | 445 | // Work handed to a colleague: the only way an agent gets another working (handoff.ts). |
| 446 | const viewer = audience.asker; | |
| 447 | actions.handOff = handOffPort({ | |
| 448 | self: { id: row.id, handle: row.handle }, | |
| 449 | chain, | |
| 450 | asker: delivery.asker ?? null, | |
| 451 | agent: async (handle) => { | |
| 452 | const found = await db | |
| Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how. | 453 | .prepare(selectAgents("a.workspace_id = ?3 AND a.handle = ?4 AND a.archived_at IS NULL AND a.scope = 'workspace'")) |
| Merge branch 'worktree-agent-a1398e81ad1a64c5f' | 454 | .bind(...periods(now), row.workspace_id, handle) |
| 455 | .first<Row>(); | |
| 456 | if (!found) return null; | |
| 457 | const agent = toAgent(found, now); | |
| 458 | return { id: agent.id, handle: agent.handle, display_name: agent.display_name, builtin: !!found.builtin, status: agent.status }; | |
| 459 | }, | |
| 460 | person: async (handle) => { | |
| 461 | if (!viewer) return false; | |
| 462 | const members = await identityClient(env.IDENTITY).listMembers(slug, viewer); | |
| 463 | return members.ok && members.value.some((m) => m.username.toLowerCase() === handle); | |
| 464 | }, | |
| 465 | handOff: (colleagueId, brief) => surface.handOff(colleagueId, brief), | |
| 466 | }); | |
| 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 | 467 | toolbox = new ToolBox( |
| 468 | audience, | |
| 469 | ports(consult.ask), | |
| 470 | { agentId: row.id, notConsult: [row.handle, ...(sender ? [sender.handle] : [])], hops, maxHops: CHAT_MAX_HOPS }, | |
| 471 | [], | |
| 472 | actions, | |
| 473 | ); | |
| 474 | consult.attach(toolbox); | |
| Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how. | 475 | // Its abilities outside g1t (abilities.ts): offered and enforced by the tool box, for the person it acts for. |
| 476 | if (viewer && !delivery.hello) { | |
| 477 | const sections = await abilitiesFor(env as unknown as AbilityEnv, { agent: row, definition, workspace: slug, asker: viewer }).catch(() => null); | |
| 478 | if (sections) { | |
| 479 | const saidByPeople = saidText([...history].reverse().filter((m) => m.author.kind === "user").slice(0, 3).map((m) => m.body)); | |
| 480 | toolbox.useAbilities( | |
| 481 | sections, | |
| 482 | abilityPorts(env as unknown as AbilityEnv, { | |
| 483 | agent: row, | |
| 484 | workspace: slug, | |
| 485 | channel_id: delivery.channel_id, | |
| 486 | session: null, | |
| 487 | asker: { id: delivery.asked_by, username: askerName }, | |
| 488 | postCard: (card) => surface.post("", card).catch(() => null), | |
| 489 | }), | |
| 490 | saidByPeople, | |
| 491 | definition.abilities.mcp_servers, | |
| 492 | ); | |
| 493 | abilitiesText = abilitiesSection(sections); | |
| 494 | } | |
| 495 | } | |
| 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 | 496 | } catch (error) { |
| 497 | // Without an audience nothing may be read: the reply goes on with this conversation only. | |
| 498 | console.error("agents: no audience for a reply, so no tools", row.id, String(error)); | |
| 499 | } | |
| 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) | 500 | } |
| 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. | 501 | // 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 | 502 | const said = [...history].reverse().filter((m) => m.author.kind === "user").slice(0, 3).map((m) => m.body); |
| Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how. | 503 | const [facts, recent, passages, shelf] = delivery.hello |
| 504 | ? [[], null, [], []] | |
| Agents recall what Docs say before they answer or work, and each has required reading | 505 | : await Promise.all([ |
| 506 | recall(db, row.id, place).catch(() => []), | |
| 507 | sessionsHere(db, row.id, delivery.channel_id).catch(() => null), | |
| 508 | toolbox ? toolbox.recall(recallQuery(said), definition.reading ?? []) : Promise.resolve([]), | |
| Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how. | 509 | // Its skills: named in the prompt, read with use_skill (skills.ts). |
| An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how. | 510 | toolbox ? loadShelf(db, row.workspace_id, { id: row.id, skills_off: definition.skills_off }, teamSlugs(teamsHere)) : Promise.resolve([]), |
| Agents recall what Docs say before they answer or work, and each has required reading | 511 | ]); |
| Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how. | 512 | toolbox?.useShelf(shelf, (skillId, version) => readVersion(db, skillId, version)); |
| 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 | 513 | const system = [ |
| 514 | systemPrompt({ | |
| 515 | agent: { | |
| 516 | ...definition, | |
| 517 | id: row.id, | |
| An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how. | 518 | teams: teamsHere?.teams.map((t) => t.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 | 519 | // @g1t's job is fixed; what the workspace wrote is added to it. |
| 520 | instructions: row.builtin ? orchestratorInstructions(specialists.filter((a) => a.handle !== "g1t"), definition.instructions) : definition.instructions, | |
| 521 | }, | |
| 522 | workspace: delivery.workspace, | |
| 523 | channel: { kind: delivery.channel_kind, name: delivery.channel_name }, | |
| 524 | asker, | |
| 525 | today: now, | |
| 526 | tools: toolbox ? { code: toolbox.definitions().some((tool) => tool.name === "read_file") } : null, | |
| 527 | // @g1t's team is in its job; everyone else is told who their colleagues are. | |
| 528 | colleagues: row.builtin ? null : rosterLines(specialists), | |
| 529 | recentSessions: recent, | |
| Merge branch 'worktree-agent-a1398e81ad1a64c5f' | 530 | conversation: conversationHere, |
| People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how. | 531 | teams: teamsSection(row.id, teamsHere, now), |
| Merge branch 'worktree-agent-a1398e81ad1a64c5f' | 532 | canHandOff: !!toolbox?.definitions().some((tool) => tool.name === "hand_off"), |
| Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how. | 533 | abilities: abilitiesText, |
| Merge branch 'worktree-agent-a1398e81ad1a64c5f' | 534 | handedOffBy: sender?.handle ?? null, |
| Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how. | 535 | skills: skillsSection(shelf, 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 | 536 | }), |
| 537 | memorySection(facts), | |
| Agents recall what Docs say before they answer or work, and each has required reading | 538 | 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 | 539 | ] |
| 540 | .filter(Boolean) | |
| 541 | .join("\n\n"); | |
| 542 | toolCalls = toolbox?.calls ?? []; | |
| Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how. | 543 | const answer = await runTurn(model.send, { model: model.model.model, system, messages: conversation as ModelMessage[], tools: toolbox, price: model.ownModel ? null : model.model.price, effort: model.effort }); |
| 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 | 544 | // Post as soon as there is an answer; the bill is settled after. |
| Merge branch 'worktree-agent-a1398e81ad1a64c5f' | 545 | // It wakes nobody, @mentions or not: colleagues get work only through hand_off. |
| 546 | 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 | 547 | return { |
| 548 | text: answer.text, | |
| 549 | tokens: addTokens(answer.tokens, consulted.tokens), | |
| 550 | cost: (model.ownModel ? 0 : answer.cost) + consulted.cost, | |
| 551 | rounds: answer.rounds, | |
| 552 | }; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 553 | }); |
| 554 | ||
| 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 | 555 | if (!done.ok) return await notice(noticeFor(done.reason, done.message), done.reason); |
| Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how. | 556 | usage = { model: done.model, tier: done.tier, tokens: done.tokens, cost: done.cost, charged: done.charged, effort: plan.level }; |
| 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 | 557 | if (!done.value.text) { |
| 558 | const apology = await surface.post(APOLOGY).catch(() => null); | |
| 559 | 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 | 560 | } |
| 561 | await finish({ status: "replied", reply_id: posted, ...usage }); | |
| 562 | } catch (error) { | |
| 563 | const message = error instanceof Error ? error.message : String(error); | |
| 564 | console.error("agents: a reply failed", row.id, delivery.message_id, message); | |
| 565 | const posted = await surface.post(APOLOGY).catch(() => null); | |
| 566 | await finish({ status: "failed", error: message, reply_id: posted, ...usage }).catch((failure: unknown) => | |
| 567 | console.error("agents: a failed reply was not recorded", id, String(failure)), | |
| 568 | ); | |
| 569 | } | |
| 570 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.