Skip to content
109 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.

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.1/**
2 * One call of the agent builder (builder.ts): a fast-tier model request
3 * through `metered`, charged to the person who asked and recorded as a
4 * builder reply, so their budget and Spend count it. Saves nothing else.
5 */
6import { type User, newId } from "@g1t/contracts";
7
8import { BUILDER_ID, BUILDER_PER_HOUR, builderRow } from "./builder.ts";
9import type { Definition } from "./definition.ts";
10import { type MeterEnv, metered } from "./meter.ts";
11import { runTurn } from "./turn.ts";
12
13export type BuilderCall = {
14 slug: string;
15 workspaceId: string;
16 viewer: User;
17 system: string;
18 messages: { role: "user" | "assistant"; content: string }[];
19 maxOutput?: number;
20 /** For Try it: the unsaved agent's own model limits, so it answers on what it would run on. */
21 routing?: Partial<Definition["routing"]>;
22};
23
24export type BuilderResult = { ok: true; text: string; charged: number } | { ok: false; code: "invalid" | "forbidden"; message: string };
25
26/** How many builder calls `username` made in the last hour. */
27async function recentCalls(db: D1Database, workspaceId: string, username: string, now: Date): Promise<number> {
28 const since = new Date(now.getTime() - 60 * 60 * 1000).toISOString();
29 const row = await db
30 .prepare("SELECT COUNT(*) AS n FROM agent_replies WHERE agent_id = ? AND workspace_id = ? AND asked_by_username = ? AND created_at > ?")
31 .bind(BUILDER_ID, workspaceId, username, since)
32 .first<{ n: number }>();
33 return row?.n ?? 0;
34}
35
36/**
37 * One builder call: checks the person's hourly allowance, asks the model
38 * through `metered` (charged to them), and records it as a builder reply
39 * so their budget and Spend count it. Saves nothing else.
40 */
41export async function callBuilder(env: MeterEnv, call: BuilderCall, now = new Date()): Promise<BuilderResult> {
42 const username = call.viewer.username.toLowerCase();
43 if ((await recentCalls(env.DB, call.workspaceId, username, now)) >= BUILDER_PER_HOUR) {
44 return { ok: false, code: "invalid", message: `That's ${BUILDER_PER_HOUR} drafts and tries in an hour. Wait a little, or edit all fields yourself.` };
45 }
46 const row = builderRow(call.workspaceId, call.routing);
47 const id = newId("arp", now.getTime());
48 const record = (status: string, extra: { model?: string | null; tier?: string | null; input?: number; output?: number; cost?: number; charged?: number; error?: string | null }) =>
49 env.DB.prepare(
50 `INSERT INTO agent_replies (id, agent_id, workspace_id, channel_id, message_id, asked_by, asked_by_username, channel_name, agent_version, model, tier,
51 input_tokens, output_tokens, cost_micros, charged_micros, status, error, created_at, finished_at)
52 VALUES (?, ?, ?, ?, ?, ?, ?, NULL, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
53 )
54 .bind(
55 id,
56 BUILDER_ID,
57 call.workspaceId,
58 BUILDER_ID,
59 id,
60 call.viewer.id,
61 username,
62 extra.model ?? null,
63 extra.tier ?? null,
64 extra.input ?? 0,
65 extra.output ?? 0,
66 extra.cost ?? 0,
67 extra.charged ?? 0,
68 status,
69 extra.error?.slice(0, 500) ?? null,
70 now.toISOString(),
71 new Date().toISOString(),
72 )
73 .run()
74 .catch((error: unknown) => console.error("agents: a builder call was not recorded", String(error)));
75 try {
76 const done = await metered(env, { row, payer: row, slug: call.slug, task: "reply", start: "small", askerName: username, person: username }, async (model) => {
77 const answer = await runTurn(model.send, {
78 model: model.model.model,
79 system: call.system,
80 messages: call.messages,
81 tools: null,
82 price: model.ownModel ? null : model.model.price,
83 maxOutput: call.maxOutput,
84 maxRounds: 1,
85 });
86 return { text: answer.text, tokens: answer.tokens, cost: model.ownModel ? 0 : answer.cost, rounds: answer.rounds };
87 });
88 if (!done.ok) {
89 const forbidden = done.reason === "person_budget" || done.reason === "workspace_agent_budget" || done.reason.startsWith("workspace_");
90 return { ok: false, code: forbidden ? "forbidden" : "invalid", message: done.message };
91 }
92 await record(done.value.text ? "replied" : "failed", {
93 model: done.model,
94 tier: done.tier,
95 input: done.tokens.input + done.tokens.cacheRead + done.tokens.cacheWrite,
96 output: done.tokens.output,
97 cost: done.cost,
98 charged: done.charged,
99 error: done.value.text ? null : "the model gave no text",
100 });
101 if (!done.value.text) return { ok: false, code: "invalid", message: "The model gave no answer. Try again in a moment." };
102 return { ok: true, text: done.value.text, charged: done.charged };
103 } catch (error) {
104 const message = error instanceof Error ? error.message : String(error);
105 console.error("agents: a builder call failed", message);
106 await record("failed", { error: message });
107 return { ok: false, code: "invalid", message: "Something went wrong drafting that. Try again in a moment." };
108 }
109}