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