Skip to content
106 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
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked33export type TurnResult = { text: string; tokens: Tokens; cost: number; rounds: number; stopped?: boolean };
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)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
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked41/** A session step's rails: more rounds, more input, longer answers than a chat reply. */
42export const SESSION_LIMITS = { rounds: 30, input: 400_000, output: 8192 } as const;
43
44export type TurnOptions = {
45 /** At most this many requests (default `MAX_ROUNDS`). */
46 maxRounds?: number;
47 /** Input tokens read before it must answer (default `INPUT_BUDGET`). */
48 inputBudget?: number;
49 /** The longest answer (default `MAX_OUTPUT_TOKENS`). */
50 maxOutput?: number;
51 /** Told what the model said in each round that also called tools: a session's transcript. */
52 onText?: (text: string) => void;
53 /** Asked before each round; true ends the turn with what it has (a session was stopped). */
54 stopped?: () => Promise<boolean>;
55};
56
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)57export async function runTurn(
58 send: Send,
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked59 input: { model: string; system: string; messages: ModelMessage[]; tools: ToolBox | null; price: TokenPrice | null } & TurnOptions,
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)60): Promise<TurnResult> {
61 const messages = [...input.messages];
62 let tokens = NO_TOKENS;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked63 const maxRounds = input.maxRounds ?? MAX_ROUNDS;
64 const inputBudget = input.inputBudget ?? INPUT_BUDGET;
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)65 for (let round = 1; ; round++) {
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked66 if (round > 1 && input.stopped && (await input.stopped())) {
67 return { text: "", tokens, cost: costMicros(tokens, input.price), rounds: round - 1, stopped: true };
68 }
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)69 const definitions = input.tools?.definitions() ?? [];
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked70 const canUse = !!input.tools && definitions.length > 0 && !input.tools.spent && tokens.input + tokens.cacheRead < inputBudget && round < maxRounds;
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)71 const answer = await send({
72 model: input.model,
73 system: input.system,
74 messages,
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked75 max_tokens: input.maxOutput ?? MAX_OUTPUT_TOKENS,
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)76 // Tools stay listed once the conversation has used them, so their
77 // results still read; past the rails, the model must answer in text.
78 ...(definitions.length ? { tools: definitions, tool_choice: { type: canUse ? "auto" : "none" } } : {}),
79 });
80 const usage = answer.usage ?? {};
81 tokens = addTokens(tokens, {
82 input: usage.input_tokens ?? 0,
83 output: usage.output_tokens ?? 0,
84 cacheRead: usage.cache_read_input_tokens ?? 0,
85 cacheWrite: usage.cache_creation_input_tokens ?? 0,
86 });
87 const content = answer.content ?? [];
88 const calls = content.filter((block) => block.type === "tool_use");
89 const text = content
90 .filter((block) => block.type === "text" && block.text)
91 .map((block) => block.text!.trim())
92 .join("\n\n")
93 .trim();
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked94 if (!calls.length || !input.tools || round >= maxRounds) {
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)95 return { text, tokens, cost: costMicros(tokens, input.price), rounds: round };
96 }
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked97 if (text) input.onText?.(text);
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)98 messages.push({ role: "assistant", content });
99 const results: Block[] = [];
100 for (const call of calls) {
101 const ran = await input.tools.run(String(call.name ?? ""), (call.input as Record<string, unknown>) ?? {});
102 results.push({ type: "tool_result", tool_use_id: call.id, content: ran.text });
103 }
104 messages.push({ role: "user", content: results });
105 }
106}

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