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

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)1/**
2 * One model turn with tools: ask, run the tools it calls through the
3 * reply's `ToolBox`, give it the results, until it answers in text. Pure
4 * apart from `send`, so the loop's rails are tested on their own.
5 *
6 * Rails: the tool box allows at most `MAX_TOOL_CALLS` for the whole reply
7 * (consults included); once they are spent, or the turn has read
8 * `INPUT_BUDGET` input tokens, the model is asked to answer without tools.
9 * At most `MAX_ROUNDS` requests, whatever happens.
10 */
11import type { TokenPrice } from "../../runner/src/model-env.ts";
12import { type Tokens, costMicros } from "./budget.ts";
13import type { ToolBox } from "./tools.ts";
14
15/** The input tokens one turn may read before it must answer. */
16export const INPUT_BUDGET = 150_000;
17const MAX_ROUNDS = 12;
18/** A chat answer is short; this bounds the cost of one that is not. */
19export const MAX_OUTPUT_TOKENS = 2048;
20
21type Block = { type: string; text?: string; id?: string; name?: string; input?: unknown; [key: string]: unknown };
22export type ModelMessage = { role: "user" | "assistant"; content: string | Block[] };
23
24export type ModelAnswer = {
25 content?: Block[];
26 stop_reason?: string;
27 usage?: { input_tokens?: number; output_tokens?: number; cache_read_input_tokens?: number; cache_creation_input_tokens?: number };
28};
29
30/** Sends one Messages API request; throws when the model did not answer. */
31export type Send = (body: Record<string, unknown>) => Promise<ModelAnswer>;
32
33export type TurnResult = { text: string; tokens: Tokens; cost: number; rounds: number };
34
35export function addTokens(a: Tokens, b: Tokens): Tokens {
36 return { input: a.input + b.input, output: a.output + b.output, cacheRead: a.cacheRead + b.cacheRead, cacheWrite: a.cacheWrite + b.cacheWrite };
37}
38
39export const NO_TOKENS: Tokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
40
41export async function runTurn(
42 send: Send,
43 input: { model: string; system: string; messages: ModelMessage[]; tools: ToolBox | null; price: TokenPrice | null },
44): Promise<TurnResult> {
45 const messages = [...input.messages];
46 let tokens = NO_TOKENS;
47 for (let round = 1; ; round++) {
48 const definitions = input.tools?.definitions() ?? [];
49 const canUse = !!input.tools && definitions.length > 0 && !input.tools.spent && tokens.input + tokens.cacheRead < INPUT_BUDGET && round < MAX_ROUNDS;
50 const answer = await send({
51 model: input.model,
52 system: input.system,
53 messages,
54 max_tokens: MAX_OUTPUT_TOKENS,
55 // Tools stay listed once the conversation has used them, so their
56 // results still read; past the rails, the model must answer in text.
57 ...(definitions.length ? { tools: definitions, tool_choice: { type: canUse ? "auto" : "none" } } : {}),
58 });
59 const usage = answer.usage ?? {};
60 tokens = addTokens(tokens, {
61 input: usage.input_tokens ?? 0,
62 output: usage.output_tokens ?? 0,
63 cacheRead: usage.cache_read_input_tokens ?? 0,
64 cacheWrite: usage.cache_creation_input_tokens ?? 0,
65 });
66 const content = answer.content ?? [];
67 const calls = content.filter((block) => block.type === "tool_use");
68 const text = content
69 .filter((block) => block.type === "text" && block.text)
70 .map((block) => block.text!.trim())
71 .join("\n\n")
72 .trim();
73 if (!calls.length || !input.tools || round >= MAX_ROUNDS) {
74 return { text, tokens, cost: costMicros(tokens, input.price), rounds: round };
75 }
76 messages.push({ role: "assistant", content });
77 const results: Block[] = [];
78 for (const call of calls) {
79 const ran = await input.tools.run(String(call.name ?? ""), (call.input as Record<string, unknown>) ?? {});
80 results.push({ type: "tool_result", tool_use_id: call.id, content: ran.text });
81 }
82 messages.push({ role: "user", content: results });
83 }
84}

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