Skip to content
202 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 * Agents as stored in D1, and their spend. Shared by the RPC methods and
3 * the desk.
4 */
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)5import type { SubagentDef, WorkspaceAgent } from "@g1t/contracts";
Chat and workspace agents: channels, DMs and named agents you talk to6
7import { agentStatus, budgetBlock, dayKey, monthKey } from "./budget.ts";
8import { type Definition, DEFAULT_AUTONOMY, DEFAULT_BUDGET, DEFAULT_ROUTING, PRESETS, readJson } from "./definition.ts";
9
10export type Row = {
11 id: string;
12 workspace_id: string;
13 handle: string;
14 display_name: string;
15 avatar: string | null;
16 role: string;
17 instructions: string;
18 personality_preset: string;
19 personality: string;
20 routing: string;
21 budget: string;
22 autonomy: string;
23 capacity: number;
24 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)25 avatar_seed: string | null;
26 title: string | null;
27 team: string | null;
28 department: string | null;
29 responsibilities: string | null;
30 subagents: string | null;
31 faces: string | null;
Chat and workspace agents: channels, DMs and named agents you talk to32 version: number;
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)33 /** 1 for the workspace's built-in @g1t. */
34 builtin: number;
Chat and workspace agents: channels, DMs and named agents you talk to35 busy_until: string | null;
36 created_by: string;
37 created_at: string;
38 updated_at: string;
39 archived_at: string | null;
40 /** Joined from agent_spend: this month and today. */
41 spent_month?: number | null;
42 spent_day?: number | null;
43};
44
45/**
46 * Agents with this month's and today's spend: `where` filters `agents`
47 * (aliased `a`); its parameters follow the two periods.
48 */
49export function selectAgents(where: string): string {
50 return `SELECT a.*, m.micros AS spent_month, d.micros AS spent_day FROM agents a
51 LEFT JOIN agent_spend m ON m.agent_id = a.id AND m.period = ?1
52 LEFT JOIN agent_spend d ON d.agent_id = a.id AND d.period = ?2
53 WHERE ${where}`;
54}
55
56export function periods(now: Date): [string, string] {
57 return [monthKey(now), dayKey(now)];
58}
59
60export function definitionOf(row: Row): Definition {
61 return {
62 handle: row.handle,
63 display_name: row.display_name,
64 role: row.role,
65 instructions: row.instructions,
66 personality_preset: PRESETS.includes(row.personality_preset as Definition["personality_preset"])
67 ? (row.personality_preset as Definition["personality_preset"])
68 : "crisp",
69 personality: row.personality,
70 routing: readJson(row.routing, DEFAULT_ROUTING),
71 budget: readJson(row.budget, DEFAULT_BUDGET),
72 autonomy: readJson(row.autonomy, DEFAULT_AUTONOMY),
73 capacity: row.capacity,
74 template: row.template,
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)75 avatar_seed: row.avatar_seed || row.handle,
76 title: row.title ?? "",
77 team: row.team ?? null,
78 department: row.department ?? "",
79 responsibilities: readList<string>(row.responsibilities),
80 subagents: readList<SubagentDef>(row.subagents),
81 faces: "internal",
Chat and workspace agents: channels, DMs and named agents you talk to82 };
83}
84
85export function toAgent(row: Row, now: Date): WorkspaceAgent {
86 const definition = definitionOf(row);
87 const spent = { month: row.spent_month ?? 0, day: row.spent_day ?? 0 };
88 return {
89 id: row.id,
90 workspace_id: row.workspace_id,
91 avatar: row.avatar,
92 ...definition,
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)93 builtin: !!row.builtin,
Chat and workspace agents: channels, DMs and named agents you talk to94 version: row.version,
95 status: row.archived_at ? "paused" : agentStatus({ busyUntil: row.busy_until, now, blocked: budgetBlock(definition.budget, spent, now) !== null }),
96 spent_month_micros: spent.month,
97 created_by: row.created_by,
98 created_at: row.created_at,
99 updated_at: row.updated_at,
100 archived_at: row.archived_at,
101 };
102}
103
104/** Adds a reply's charge to the agent's month and day. */
105export function spendStatements(db: D1Database, agentId: string, micros: number, now: Date): D1PreparedStatement[] {
106 return periods(now).map((period) =>
107 db
108 .prepare(
109 `INSERT INTO agent_spend (agent_id, period, micros, replies) VALUES (?1, ?2, ?3, 1)
110 ON CONFLICT (agent_id, period) DO UPDATE SET micros = micros + ?3, replies = replies + 1`,
111 )
112 .bind(agentId, period, Math.max(0, Math.ceil(micros))),
113 );
114}
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)115
116/** A stored JSON list, read defensively: anything else is empty. */
117function readList<T>(raw: string | null): T[] {
118 if (!raw) return [];
119 try {
120 const parsed = JSON.parse(raw) as unknown;
121 return Array.isArray(parsed) ? (parsed as T[]) : [];
122 } catch {
123 return [];
124 }
125}
126
127/**
128 * The agents table's definition columns, in the order `definitionColumns`
129 * gives their values. Statements are built from this one list, so a new
130 * field is added in one place.
131 */
132export const DEFINITION_COLUMNS = [
133 "handle",
134 "display_name",
135 "role",
136 "instructions",
137 "personality_preset",
138 "personality",
139 "routing",
140 "budget",
141 "autonomy",
142 "capacity",
143 "template",
144 "avatar_seed",
145 "title",
146 "team",
147 "department",
148 "responsibilities",
149 "subagents",
150 "faces",
151] as const;
152
153/** A definition's values, in `DEFINITION_COLUMNS` order. */
154export function definitionColumns(d: Definition): (string | number | null)[] {
155 return [
156 d.handle,
157 d.display_name,
158 d.role,
159 d.instructions,
160 d.personality_preset,
161 d.personality,
162 JSON.stringify(d.routing),
163 JSON.stringify(d.budget),
164 JSON.stringify(d.autonomy),
165 d.capacity,
166 d.template,
167 d.avatar_seed,
168 d.title,
169 d.team,
170 d.department,
171 JSON.stringify(d.responsibilities),
172 JSON.stringify(d.subagents),
173 d.faces,
174 ];
175}
176
177/**
178 * A new agent row: `id`, `workspace_id`, the definition, then `extra`
179 * columns (version, builtin, who made it, when), all bound in order.
180 */
181export function insertAgent(db: D1Database, id: string, workspaceId: string, d: Definition, extra: Record<string, string | number>, orIgnore = false): D1PreparedStatement {
182 const names = ["id", "workspace_id", ...DEFINITION_COLUMNS, ...Object.keys(extra)];
183 const values = [id, workspaceId, ...definitionColumns(d), ...Object.values(extra)];
184 return db
185 .prepare(`INSERT ${orIgnore ? "OR IGNORE " : ""}INTO agents (${names.join(", ")}) VALUES (${names.map(() => "?").join(", ")})`)
186 .bind(...values);
187}
188
189/** Sets a row's definition, from the version read only, with `extra` columns. */
190export function updateAgent(db: D1Database, id: string, readVersion: number, d: Definition, extra: Record<string, string | number>): D1PreparedStatement {
191 const names = [...DEFINITION_COLUMNS, ...Object.keys(extra)];
192 return db
193 .prepare(`UPDATE agents SET ${names.map((name) => `${name} = ?`).join(", ")} WHERE id = ? AND version = ?`)
194 .bind(...definitionColumns(d), ...Object.values(extra), id, readVersion);
195}
196
197/** A version of a definition, as saved. */
198export function versionStatement(db: D1Database, agentId: string, version: number, d: Definition, by: string, at: string): D1PreparedStatement {
199 return db
200 .prepare("INSERT INTO agent_versions (agent_id, version, definition, changed_by, created_at) VALUES (?, ?, ?, ?, ?)")
201 .bind(agentId, version, JSON.stringify(d), by, at);
202}

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