Skip to content
220 linesCodeBlameRaw
1/**
2 * A workspace's own agents: named members with a job, a personality,
3 * routing limits and a budget, kept by the agents service
4 * (`services/agents`). `@g1t` is the platform's agent and is not one of
5 * these. Plan: docs/WORKSPACE.md.
6 *
7 * Wire shapes are snake_case end to end.
8 */
9import type { ServiceBinding } from "./clients";
10import type { Role, User } from "./identity";
11import type { Result } from "./result";
12
13// Model tiers (`ModelTier`, `MODEL_TIERS`) are integrations.ts's, the
14// same ones runs are routed between.
15import type { ModelTier } from "./integrations";
16
17/** Voice presets; free text in `personality` refines them. */
18export type PersonalityPreset = "crisp" | "friendly" | "socratic" | "terse";
19
20export type AgentRouting = {
21 /** Never route below this tier. Null: no floor. */
22 floor: ModelTier | null;
23 /** Never route above this tier. Null: no ceiling. */
24 ceiling: ModelTier | null;
25 /**
26 * Where its model calls may go. Empty: anything the workspace allows.
27 * `g1t`: g1t's hosted models. `workspace`: the workspace's own providers
28 * (Integrations), whichever they are. Any other entry: one of the
29 * workspace's own providers by integration id. Entries combine, so
30 * `["g1t", "workspace"]` is both.
31 */
32 providers: string[];
33 /** Advanced: a fixed `provider/model`, for own endpoints. Usually null. */
34 pinned: string | null;
35};
36
37export type AgentBudget = {
38 /** Monthly cap in micro-dollars. Null: only the workspace limit applies. */
39 monthly_micros: number | null;
40 daily_micros: number | null;
41 /** Default cap for one task. */
42 task_micros: number | null;
43};
44
45export type AgentAutonomy = {
46 open_pull_requests: "alone" | "approval";
47 merge: "alone" | "approval" | "never";
48 deploy_production: "approval" | "never";
49 edit_docs: "alone" | "suggest";
50};
51
52export type AgentStatus = "idle" | "working" | "waiting" | "out_of_budget" | "paused";
53
54export type WorkspaceAgent = {
55 id: string;
56 workspace_id: string;
57 /** Lowercase, unique in the workspace, never `g1t`. Mentioned as `@handle`. */
58 handle: string;
59 display_name: string;
60 /** Uploaded avatar hash, or null for the generated mark. */
61 avatar: string | null;
62 /** One line: "Reviews every pull request in g1t". */
63 role: string;
64 /** The job: what it is responsible for and how it works. */
65 instructions: string;
66 personality_preset: PersonalityPreset;
67 /** Free text refining the voice. Never changes what it may do. */
68 personality: string;
69 routing: AgentRouting;
70 budget: AgentBudget;
71 autonomy: AgentAutonomy;
72 /** Tasks it works at once; more queue on its desk. */
73 capacity: number;
74 /** The template it was made from, if any. */
75 template: string | null;
76 version: number;
77 status: AgentStatus;
78 /** Spend this calendar month, in micro-dollars. */
79 spent_month_micros: number;
80 created_by: string;
81 created_at: string;
82 updated_at: string;
83 archived_at: string | null;
84};
85
86export type NewWorkspaceAgent = {
87 handle: string;
88 display_name: string;
89 role: string;
90 instructions: string;
91 personality_preset?: PersonalityPreset;
92 personality?: string;
93 routing?: Partial<AgentRouting>;
94 budget?: Partial<AgentBudget>;
95 autonomy?: Partial<AgentAutonomy>;
96 capacity?: number;
97 template?: string | null;
98};
99
100export type AgentTemplate = {
101 id: string;
102 display_name: string;
103 handle: string;
104 role: string;
105 instructions: string;
106 personality_preset: PersonalityPreset;
107 routing: AgentRouting;
108};
109
110/** What the chat service hands an agent: a message it should answer. */
111export type AgentDelivery = {
112 workspace: string;
113 workspace_id: string;
114 channel_id: string;
115 channel_kind: "channel" | "dm";
116 channel_name: string | null;
117 agent_id: string;
118 /** The message that woke it. */
119 message_id: string;
120 thread_root: string | null;
121 /** Who asked: the person's user id. */
122 asked_by: string;
123 /** Agent-to-agent hops so far in this chain. */
124 hops: number;
125 /**
126 * What the person who asked may do, from the viewer the chat service
127 * already holds when the message is posted (`askerAccess`). An agent
128 * never does more for someone than they could do themselves
129 * (docs/WORKSPACE.md, "The whole company"). Absent from an older chat
130 * service: the agent then treats the asker as unable to change code.
131 */
132 asker?: AskerAccess | null;
133 /**
134 * Where the conversation is: g1t's own chat, or later another chat app
135 * the workspace connected (docs/WORKSPACE.md, "Working from another chat
136 * app"). The agent reads and replies through that surface; its
137 * definition, budget and replies are the same everywhere. Absent: `g1t`.
138 */
139 surface?: AgentSurface;
140};
141
142/** The chat surfaces an agent answers on. Only g1t's own today. */
143export type AgentSurface = "g1t";
144
145/** Who asked an agent, as far as its reply needs to know. */
146export type AskerAccess = {
147 username: string;
148 /** Their role in the workspace; `outside` for someone who is not a member. */
149 role: Role | "outside";
150 /**
151 * Whether they can change code in the workspace: Code is on for them
152 * and they hold write access (or more) on at least one of its
153 * repositories, through the base permission or a grant.
154 */
155 can_write: boolean;
156};
157
158const WRITING_ROLES = new Set(["write", "maintain", "admin"]);
159
160/**
161 * `user`'s access in `workspace`, for `AgentDelivery.asker`. Pure, with no
162 * imports, so the chat service computes it from its viewer for free.
163 */
164export function askerAccess(user: User, workspace: string): AskerAccess {
165 const slug = workspace.toLowerCase();
166 const membership = user.workspaces?.find((m) => m.slug.toLowerCase() === slug);
167 // Someone who uses only Chat, Docs and agents sees no repository at all.
168 const code = membership?.code_access !== false;
169 const base = membership ? membership.role === "owner" || WRITING_ROLES.has(membership.base_permission ?? "write") : false;
170 const granted = (user.grants ?? []).some((grant) => grant.workspace.toLowerCase() === slug && WRITING_ROLES.has(grant.role));
171 return {
172 username: user.username,
173 role: membership?.role ?? "outside",
174 can_write: code && (base || granted),
175 };
176}
177
178export type WorkspaceAgentsApi = {
179 list(workspace: string, viewer: User): Promise<Result<WorkspaceAgent[]>>;
180 get(workspace: string, handle: string, viewer: User): Promise<Result<WorkspaceAgent>>;
181 /** Internal: by id, for the chat service resolving members. */
182 byIds(ids: string[]): Promise<WorkspaceAgent[]>;
183 create(workspace: string, viewer: User, input: NewWorkspaceAgent): Promise<Result<WorkspaceAgent>>;
184 update(
185 workspace: string,
186 handle: string,
187 viewer: User,
188 changes: Partial<NewWorkspaceAgent>,
189 ): Promise<Result<WorkspaceAgent>>;
190 archive(workspace: string, handle: string, viewer: User): Promise<Result<null>>;
191 templates(): Promise<AgentTemplate[]>;
192 /** The chat service hands over a message for an agent to answer. Returns at once. */
193 deliver(delivery: AgentDelivery): Promise<Result<null>>;
194};
195
196async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> {
197 const response = await service.fetch(`https://service/rpc/${method}`, {
198 method: "POST",
199 headers: { "content-type": "application/json" },
200 body: JSON.stringify(args),
201 });
202 if (!response.ok) {
203 throw new Error(`${method} failed with status ${response.status}`);
204 }
205 return (await response.json()) as T;
206}
207
208export function workspaceAgentsClient(service: ServiceBinding): WorkspaceAgentsApi {
209 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
210 return {
211 list: (workspace, viewer) => call("list", { workspace, viewer }),
212 get: (workspace, handle, viewer) => call("get", { workspace, handle, viewer }),
213 byIds: (ids) => call("by_ids", { ids }),
214 create: (workspace, viewer, input) => call("create", { workspace, viewer, input }),
215 update: (workspace, handle, viewer, changes) => call("update", { workspace, handle, viewer, changes }),
216 archive: (workspace, handle, viewer) => call("archive", { workspace, handle, viewer }),
217 templates: () => call("templates", {}),
218 deliver: (delivery) => call("deliver", delivery),
219 };
220}