Skip to content
321 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Chat and workspace agents: channels, DMs and named agents you talk to1/**
2 * A workspace's own agents: named members with a job, a personality,
3 * routing limits and a budget, kept by the agents service
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)4 * (`services/agents`). Every workspace also has `@g1t`, its built-in
5 * orchestrator, kept the same way (`builtin`). Plan: docs/WORKSPACE.md,
6 * "g1t, the orchestrator".
Chat and workspace agents: channels, DMs and named agents you talk to7 *
8 * Wire shapes are snake_case end to end.
9 */
10import type { ServiceBinding } from "./clients";
11import type { Role, User } from "./identity";
12import type { Result } from "./result";
13
14// Model tiers (`ModelTier`, `MODEL_TIERS`) are integrations.ts's, the
15// same ones runs are routed between.
16import type { ModelTier } from "./integrations";
17
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)18/** The built-in orchestrator's handle; nobody else's agent may take it. */
19export const BUILTIN_AGENT_HANDLE = "g1t";
20
21/** The built-in orchestrator's template id: not one a workspace can adopt. */
22export const ORCHESTRATOR_TEMPLATE = "orchestrator";
23
Chat and workspace agents: channels, DMs and named agents you talk to24/** Voice presets; free text in `personality` refines them. */
25export type PersonalityPreset = "crisp" | "friendly" | "socratic" | "terse";
26
27export type AgentRouting = {
28 /** Never route below this tier. Null: no floor. */
29 floor: ModelTier | null;
30 /** Never route above this tier. Null: no ceiling. */
31 ceiling: ModelTier | null;
32 /**
33 * Where its model calls may go. Empty: anything the workspace allows.
34 * `g1t`: g1t's hosted models. `workspace`: the workspace's own providers
35 * (Integrations), whichever they are. Any other entry: one of the
36 * workspace's own providers by integration id. Entries combine, so
37 * `["g1t", "workspace"]` is both.
38 */
39 providers: string[];
40 /** Advanced: a fixed `provider/model`, for own endpoints. Usually null. */
41 pinned: string | null;
42};
43
44export type AgentBudget = {
45 /** Monthly cap in micro-dollars. Null: only the workspace limit applies. */
46 monthly_micros: number | null;
47 daily_micros: number | null;
48 /** Default cap for one task. */
49 task_micros: number | null;
50};
51
52export type AgentAutonomy = {
53 open_pull_requests: "alone" | "approval";
54 merge: "alone" | "approval" | "never";
55 deploy_production: "approval" | "never";
56 edit_docs: "alone" | "suggest";
57};
58
59export type AgentStatus = "idle" | "working" | "waiting" | "out_of_budget" | "paused";
60
61export type WorkspaceAgent = {
62 id: string;
63 workspace_id: string;
64 /** Lowercase, unique in the workspace, never `g1t`. Mentioned as `@handle`. */
65 handle: string;
66 display_name: string;
67 /** Uploaded avatar hash, or null for the generated mark. */
68 avatar: string | 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)69 /**
70 * What its generated avatar is drawn from: a little pixel creature, the
71 * same for the same seed everywhere. Set from the handle when it is
72 * made; changing it gives the agent a new face.
73 */
74 avatar_seed: string;
75 /**
76 * One line, as lists show it: "QA Engineer on the QA team". Made from
77 * the title and team (or department) when not written.
78 */
Chat and workspace agents: channels, DMs and named agents you talk to79 role: 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)80 /**
81 * Agents are hired into roles, not tasks (docs/WORKSPACE.md, "Roles, not
82 * tasks"): a title, a team, and broad responsibilities.
83 */
84 title: string;
85 /** The team it is on, by slug, from the workspace's teams; null for none. */
86 team: string | null;
87 /** A label for where it works when it is on no team: "QA", "Sales". */
88 department: string;
89 /** What it is responsible for: 2 to 8 short duties, or none yet. */
90 responsibilities: string[];
91 /**
92 * Specialised help it will use inside its own work. Never members, never
93 * wider than their agent. Stored now; they run with tasks and sessions.
94 */
95 subagents: SubagentDef[];
96 /**
97 * Who it works with: `internal`, the workspace's own people (back
98 * office), or `customers` (front office). Only `internal` for now.
99 */
100 faces: AgentFaces;
Chat and workspace agents: channels, DMs and named agents you talk to101 /** The job: what it is responsible for and how it works. */
102 instructions: string;
103 personality_preset: PersonalityPreset;
104 /** Free text refining the voice. Never changes what it may do. */
105 personality: string;
106 routing: AgentRouting;
107 budget: AgentBudget;
108 autonomy: AgentAutonomy;
109 /** Tasks it works at once; more queue on its desk. */
110 capacity: number;
111 /** The template it was made from, if any. */
112 template: string | 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)113 /**
114 * The workspace's built-in orchestrator, `@g1t`: every workspace has one,
115 * made the first time its agents are asked for. It cannot be archived,
116 * and its handle, name, role and job are fixed; its `instructions` are
117 * added to that job. Listed first.
118 */
119 builtin: boolean;
Chat and workspace agents: channels, DMs and named agents you talk to120 version: number;
121 status: AgentStatus;
122 /** Spend this calendar month, in micro-dollars. */
123 spent_month_micros: number;
124 created_by: string;
125 created_at: string;
126 updated_at: string;
127 archived_at: string | null;
128};
129
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)130/**
131 * Back office or front office (docs/WORKSPACE.md, "Back office and front
132 * office"). Customer-facing agents are not available yet.
133 */
134export type AgentFaces = "internal" | "customers";
135
136/**
137 * A subagent: help an agent keeps for its own work, such as Margo's
138 * `flake-hunter`. Its routing limits sit within its agent's: a floor below
139 * the agent's is raised to it, a ceiling above is lowered to it.
140 */
141export type SubagentDef = {
142 /** Lowercase letters, digits and hyphens: `flake-hunter`. Unique on the agent. */
143 name: string;
144 /** One line: what it is for. */
145 description: string;
146 instructions: string;
147 routing: { floor: ModelTier | null; ceiling: ModelTier | null };
148 /** How many of it may run at once inside one task, 1 to 8. */
149 max_parallel: number;
150};
151
Chat and workspace agents: channels, DMs and named agents you talk to152export type NewWorkspaceAgent = {
153 handle: string;
154 display_name: 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)155 /** Left out or empty: made from the title and team. */
156 role?: string;
157 title?: string;
158 team?: string | null;
159 department?: string;
160 responsibilities?: string[];
161 subagents?: SubagentDef[];
162 /** Only `internal` for now; `customers` is refused. */
163 faces?: AgentFaces;
Chat and workspace agents: channels, DMs and named agents you talk to164 instructions: string;
165 personality_preset?: PersonalityPreset;
166 personality?: string;
167 routing?: Partial<AgentRouting>;
168 budget?: Partial<AgentBudget>;
169 autonomy?: Partial<AgentAutonomy>;
170 capacity?: number;
171 template?: string | 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)172 /** Its avatar's seed; left out, the handle. */
173 avatar_seed?: string;
Chat and workspace agents: channels, DMs and named agents you talk to174};
175
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)176/**
177 * A role to hire an agent into, by department. Agents get names, not job
178 * titles ("Margo", the QA Engineer).
179 */
Chat and workspace agents: channels, DMs and named agents you talk to180export type AgentTemplate = {
181 id: 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)182 /** The name it suggests first. */
Chat and workspace agents: channels, DMs and named agents you talk to183 display_name: string;
184 handle: 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)185 /** Other names that suit it, for the form's shuffle. Each is also a valid handle, lowercased. */
186 name_ideas: string[];
Chat and workspace agents: channels, DMs and named agents you talk to187 role: 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)188 title: string;
189 department: string;
190 responsibilities: string[];
191 subagents: SubagentDef[];
Chat and workspace agents: channels, DMs and named agents you talk to192 instructions: string;
193 personality_preset: PersonalityPreset;
194 routing: AgentRouting;
195};
196
197/** What the chat service hands an agent: a message it should answer. */
198export type AgentDelivery = {
199 workspace: string;
200 workspace_id: string;
201 channel_id: string;
202 channel_kind: "channel" | "dm";
203 channel_name: string | null;
204 agent_id: string;
205 /** The message that woke it. */
206 message_id: string;
207 thread_root: string | null;
208 /** Who asked: the person's user id. */
209 asked_by: string;
210 /** Agent-to-agent hops so far in this chain. */
211 hops: number;
212 /**
213 * What the person who asked may do, from the viewer the chat service
214 * already holds when the message is posted (`askerAccess`). An agent
215 * never does more for someone than they could do themselves
216 * (docs/WORKSPACE.md, "The whole company"). Absent from an older chat
217 * service: the agent then treats the asker as unable to change code.
218 */
219 asker?: AskerAccess | null;
220 /**
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)221 * The agents that handled this request before this one, by id, oldest
222 * first; the last sent the work here. An agent never hands back or
223 * consults the one that sent it work, and the hop limit counts every
224 * hand-off and consult along the chain. Absent: none (a person asked).
225 */
226 chain?: string[];
227 /**
Chat and workspace agents: channels, DMs and named agents you talk to228 * Where the conversation is: g1t's own chat, or later another chat app
229 * the workspace connected (docs/WORKSPACE.md, "Working from another chat
230 * app"). The agent reads and replies through that surface; its
231 * definition, budget and replies are the same everywhere. Absent: `g1t`.
232 */
233 surface?: AgentSurface;
234};
235
236/** The chat surfaces an agent answers on. Only g1t's own today. */
237export type AgentSurface = "g1t";
238
239/** Who asked an agent, as far as its reply needs to know. */
240export type AskerAccess = {
241 username: string;
242 /** Their role in the workspace; `outside` for someone who is not a member. */
243 role: Role | "outside";
244 /**
245 * Whether they can change code in the workspace: Code is on for them
246 * and they hold write access (or more) on at least one of its
247 * repositories, through the base permission or a grant.
248 */
249 can_write: boolean;
250};
251
252const WRITING_ROLES = new Set(["write", "maintain", "admin"]);
253
254/**
255 * `user`'s access in `workspace`, for `AgentDelivery.asker`. Pure, with no
256 * imports, so the chat service computes it from its viewer for free.
257 */
258export function askerAccess(user: User, workspace: string): AskerAccess {
259 const slug = workspace.toLowerCase();
260 const membership = user.workspaces?.find((m) => m.slug.toLowerCase() === slug);
261 // Someone who uses only Chat, Docs and agents sees no repository at all.
262 const code = membership?.code_access !== false;
263 const base = membership ? membership.role === "owner" || WRITING_ROLES.has(membership.base_permission ?? "write") : false;
264 const granted = (user.grants ?? []).some((grant) => grant.workspace.toLowerCase() === slug && WRITING_ROLES.has(grant.role));
265 return {
266 username: user.username,
267 role: membership?.role ?? "outside",
268 can_write: code && (base || granted),
269 };
270}
271
272export type WorkspaceAgentsApi = {
273 list(workspace: string, viewer: User): Promise<Result<WorkspaceAgent[]>>;
274 get(workspace: string, handle: string, viewer: User): Promise<Result<WorkspaceAgent>>;
275 /** Internal: by id, for the chat service resolving members. */
276 byIds(ids: string[]): Promise<WorkspaceAgent[]>;
277 create(workspace: string, viewer: User, input: NewWorkspaceAgent): Promise<Result<WorkspaceAgent>>;
278 update(
279 workspace: string,
280 handle: string,
281 viewer: User,
282 changes: Partial<NewWorkspaceAgent>,
283 ): Promise<Result<WorkspaceAgent>>;
284 archive(workspace: string, handle: string, viewer: User): Promise<Result<null>>;
285 templates(): Promise<AgentTemplate[]>;
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)286 /**
287 * Internal: the workspace's built-in `@g1t` agent, made if it does not
288 * exist yet. The chat service asks for it when someone mentions @g1t in
289 * a channel it is not in yet.
290 */
291 builtin(workspace: string, workspaceId: string): Promise<Result<WorkspaceAgent>>;
Chat and workspace agents: channels, DMs and named agents you talk to292 /** The chat service hands over a message for an agent to answer. Returns at once. */
293 deliver(delivery: AgentDelivery): Promise<Result<null>>;
294};
295
296async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> {
297 const response = await service.fetch(`https://service/rpc/${method}`, {
298 method: "POST",
299 headers: { "content-type": "application/json" },
300 body: JSON.stringify(args),
301 });
302 if (!response.ok) {
303 throw new Error(`${method} failed with status ${response.status}`);
304 }
305 return (await response.json()) as T;
306}
307
308export function workspaceAgentsClient(service: ServiceBinding): WorkspaceAgentsApi {
309 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
310 return {
311 list: (workspace, viewer) => call("list", { workspace, viewer }),
312 get: (workspace, handle, viewer) => call("get", { workspace, handle, viewer }),
313 byIds: (ids) => call("by_ids", { ids }),
314 create: (workspace, viewer, input) => call("create", { workspace, viewer, input }),
315 update: (workspace, handle, viewer, changes) => call("update", { workspace, handle, viewer, changes }),
316 archive: (workspace, handle, viewer) => call("archive", { workspace, handle, viewer }),
317 templates: () => call("templates", {}),
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)318 builtin: (workspace, workspaceId) => call("builtin", { workspace, workspace_id: workspaceId }),
Chat and workspace agents: channels, DMs and named agents you talk to319 deliver: (delivery) => call("deliver", delivery),
320 };
321}

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