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.
| Agents as a team: lifecycle, merge queue, billing and a new shell | 1 | /** The kinds of work a g1t agent does. Each is routed on its own. */ |
| 2 | export type AgentTask = "implement" | "review" | "update" | "plan"; | |
| Members can read a private repository's pull request forks | 3 | |
| Auto model routing: the cheapest tier that can do each piece of work, a retry goes up a tier, and each run records its tier | 4 | /** |
| 5 | * How capable, and how costly, a model is. g1t's hosted models come in | |
| 6 | * two: `small` for work a smaller model does as well, `large` for the rest. | |
| 7 | */ | |
| 8 | export type Tier = "small" | "large"; | |
| 9 | ||
| Agents as a team: lifecycle, merge queue, billing and a new shell | 10 | /** Where one kind of work goes: what people see, and what is sent. */ |
| 11 | export type ModelRoute = { | |
| 12 | /** The model's public name, e.g. `Claude Sonnet 5.5`. */ | |
| 13 | modelName: string; | |
| 14 | /** The identifier sent to the provider. */ | |
| 15 | model: string; | |
| 16 | }; | |
| Members can read a private repository's pull request forks | 17 | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 18 | /** |
| Auto model routing: the cheapest tier that can do each piece of work, a retry goes up a tier, and each run records its tier | 19 | * g1t's routing policy for the runs it pays the model for. Nobody |
| 20 | * assigning an agent picks a model; the work decides, here, and the | |
| 21 | * operator changes it in one place (`AGENT_ROUTING` in wrangler.jsonc). | |
| 22 | */ | |
| 23 | export type AgentRouting = { | |
| 24 | /** The model behind each tier. */ | |
| 25 | tiers: Record<Tier, ModelRoute>; | |
| 26 | /** | |
| 27 | * The tier each kind of work runs on. `change` decides by the change the | |
| 28 | * work reads: small when it is small and touches nothing sensitive. | |
| 29 | */ | |
| 30 | tasks: Record<AgentTask, Tier | "change">; | |
| 31 | /** The largest change `change` sends to the small tier. */ | |
| 32 | smallChange: { files: number; lines: number }; | |
| 33 | /** Labels on the issue behind the work that send `change` to the large tier. */ | |
| 34 | largeLabels: string[]; | |
| 35 | }; | |
| 36 | ||
| 37 | /** What a change is, as far as routing cares. */ | |
| 38 | export type ChangeSize = { | |
| 39 | files: number; | |
| 40 | /** Lines added and removed. */ | |
| 41 | lines: number; | |
| 42 | /** | |
| 43 | * What it touches that runs, configures or guards things: CI, secrets, | |
| 44 | * infrastructure, ownership (work's confidence.rs `sensitive`). | |
| 45 | */ | |
| 46 | sensitive: string[]; | |
| 47 | }; | |
| 48 | ||
| 49 | /** What g1t knows about one piece of work when it routes it. */ | |
| 50 | export type RouteSignals = { | |
| 51 | /** The change the work reads; null or absent when g1t does not know it. */ | |
| 52 | change?: ChangeSize | null; | |
| 53 | /** Labels on the issue the work is for. */ | |
| 54 | labels?: string[]; | |
| 55 | /** The last attempt at the same work failed. */ | |
| 56 | retry?: boolean; | |
| 57 | }; | |
| 58 | ||
| 59 | /** The routing g1t ships with, for whatever the configuration leaves out. */ | |
| 60 | export const DEFAULT_ROUTING: AgentRouting = { | |
| 61 | tiers: { | |
| 62 | small: { modelName: "Claude Haiku 4.5", model: "claude-haiku-4-5-20251001" }, | |
| 63 | large: { modelName: "Claude Sonnet 5.5", model: "claude-sonnet-5-5" }, | |
| 64 | }, | |
| 65 | tasks: { implement: "large", review: "change", update: "small", plan: "small" }, | |
| 66 | smallChange: { files: 10, lines: 200 }, | |
| 67 | largeLabels: ["security"], | |
| 68 | }; | |
| 69 | ||
| 70 | /** | |
| 71 | * The routing in `AGENT_ROUTING`, with anything it leaves out taken from | |
| 72 | * `DEFAULT_ROUTING`. An unset or unreadable value is the default. | |
| 73 | */ | |
| 74 | export function parseRouting(json: string | undefined): AgentRouting { | |
| 75 | let given: Partial<AgentRouting> = {}; | |
| 76 | try { | |
| 77 | given = json ? (JSON.parse(json) as Partial<AgentRouting>) : {}; | |
| 78 | } catch { | |
| 79 | console.log("AGENT_ROUTING is not JSON; using the default routing"); | |
| 80 | } | |
| 81 | return { | |
| 82 | tiers: { ...DEFAULT_ROUTING.tiers, ...given.tiers }, | |
| 83 | tasks: { ...DEFAULT_ROUTING.tasks, ...given.tasks }, | |
| 84 | smallChange: { ...DEFAULT_ROUTING.smallChange, ...given.smallChange }, | |
| 85 | largeLabels: given.largeLabels ?? DEFAULT_ROUTING.largeLabels, | |
| 86 | }; | |
| 87 | } | |
| 88 | ||
| 89 | /** | |
| 90 | * The tier one piece of work runs on: the cheapest that can do it. | |
| 91 | * Planning and catching up are small; making a change is large; a review | |
| 92 | * is small for a small change that touches nothing sensitive, and large | |
| 93 | * for anything else, including a change g1t does not know the size of. | |
| 94 | * A retry after a failed attempt is always large, so that work the small | |
| 95 | * tier could not finish goes up rather than failing again the same way. | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 96 | */ |
| Auto model routing: the cheapest tier that can do each piece of work, a retry goes up a tier, and each run records its tier | 97 | export function chooseTier(task: AgentTask, signals: RouteSignals, routing: AgentRouting = DEFAULT_ROUTING): Tier { |
| 98 | if (signals.retry) return "large"; | |
| 99 | const rule = routing.tasks[task] ?? "large"; | |
| 100 | if (rule !== "change") return rule; | |
| 101 | const change = signals.change; | |
| 102 | if (!change || change.files === 0) return "large"; | |
| 103 | const large = new Set(routing.largeLabels.map((label) => label.toLowerCase())); | |
| 104 | if ((signals.labels ?? []).some((label) => large.has(label.toLowerCase()))) return "large"; | |
| 105 | const small = | |
| 106 | change.sensitive.length === 0 && | |
| 107 | change.files <= routing.smallChange.files && | |
| 108 | change.lines <= routing.smallChange.lines; | |
| 109 | return small ? "small" : "large"; | |
| 110 | } | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 111 | |
| Members can read a private repository's pull request forks | 112 | /** The settings that decide where model requests go. */ |
| 113 | export type ModelRouting = { | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 114 | /** |
| 115 | * The provider's key. Not needed when the gateway holds it and requests | |
| 116 | * authenticate to the gateway instead. | |
| 117 | */ | |
| Members can read a private repository's pull request forks | 118 | ANTHROPIC_API_KEY?: string; |
| 119 | /** A Cloudflare AI Gateway id; empty sends requests to the provider directly. */ | |
| 120 | AI_GATEWAY_ID: string; | |
| 121 | CLOUDFLARE_ACCOUNT_ID: string; | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 122 | /** Authenticates to the gateway, if it requires it. */ |
| Members can read a private repository's pull request forks | 123 | AI_GATEWAY_TOKEN?: string; |
| 124 | }; | |
| 125 | ||
| Agents as a team: lifecycle, merge queue, billing and a new shell | 126 | /** What a run is for, attached to each of its requests at the gateway. */ |
| 127 | export type RunTags = { repo: string; pull: number }; | |
| 128 | ||
| 129 | /** Whether there is a way to reach a model at all. */ | |
| 130 | export function canReachModel(env: ModelRouting): boolean { | |
| 131 | return Boolean(env.ANTHROPIC_API_KEY || (env.AI_GATEWAY_ID && env.AI_GATEWAY_TOKEN)); | |
| 132 | } | |
| 133 | ||
| Auto model routing: the cheapest tier that can do each piece of work, a retry goes up a tier, and each run records its tier | 134 | /** |
| 135 | * The model variables of a run on g1t's hosted models: the tier's model | |
| 136 | * for the work, and the small tier's for the harness's own small tasks. | |
| 137 | */ | |
| 138 | export function tierVars(routing: AgentRouting, tier: Tier): Record<string, string> { | |
| 139 | const route = routing.tiers[tier]; | |
| 140 | return { | |
| 141 | ANTHROPIC_MODEL: route.model, | |
| 142 | // Recorded at the top of the session, so anyone can see what ran. | |
| 143 | AGENT_MODEL_NAME: route.modelName, | |
| 144 | ANTHROPIC_DEFAULT_HAIKU_MODEL: routing.tiers.small.model, | |
| 145 | ANTHROPIC_SMALL_FAST_MODEL: routing.tiers.small.model, | |
| 146 | }; | |
| 147 | } | |
| 148 | ||
| Members can read a private repository's pull request forks | 149 | /** Where the sandbox sends model requests, and what it sends with them. */ |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 150 | export function modelEnv( |
| 151 | env: ModelRouting, | |
| Auto model routing: the cheapest tier that can do each piece of work, a retry goes up a tier, and each run records its tier | 152 | routing: AgentRouting, |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 153 | task: AgentTask, |
| Auto model routing: the cheapest tier that can do each piece of work, a retry goes up a tier, and each run records its tier | 154 | tier: Tier, |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 155 | tags: RunTags, |
| 156 | ): Record<string, string> { | |
| Auto model routing: the cheapest tier that can do each piece of work, a retry goes up a tier, and each run records its tier | 157 | const vars = tierVars(routing, tier); |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 158 | if (env.ANTHROPIC_API_KEY) vars.ANTHROPIC_API_KEY = env.ANTHROPIC_API_KEY; |
| 159 | if (!env.AI_GATEWAY_ID) return vars; | |
| 160 | ||
| 161 | vars.ANTHROPIC_BASE_URL = `https://gateway.ai.cloudflare.com/v1/${env.CLOUDFLARE_ACCOUNT_ID}/${env.AI_GATEWAY_ID}/anthropic`; | |
| 162 | // The gateway logs these with every request, so spend and failures can | |
| Auto model routing: the cheapest tier that can do each piece of work, a retry goes up a tier, and each run records its tier | 163 | // be read per kind of work, tier, repository and pull request. |
| 164 | const headers = [`cf-aig-metadata: ${JSON.stringify({ task, tier, ...tags })}`]; | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 165 | if (env.AI_GATEWAY_TOKEN) { |
| 166 | vars.AI_GATEWAY_TOKEN = env.AI_GATEWAY_TOKEN; | |
| 167 | headers.push(`cf-aig-authorization: Bearer ${env.AI_GATEWAY_TOKEN}`); | |
| 168 | // With the provider's key stored in the gateway, the sandbox never | |
| 169 | // holds it. The harness still wants the variable set. | |
| 170 | vars.ANTHROPIC_API_KEY ??= env.AI_GATEWAY_TOKEN; | |
| Members can read a private repository's pull request forks | 171 | } |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 172 | vars.ANTHROPIC_CUSTOM_HEADERS = headers.join("\n"); |
| Members can read a private repository's pull request forks | 173 | return vars; |
| 174 | } | |
| Auto model routing: the cheapest tier that can do each piece of work, a retry goes up a tier, and each run records its tier | 175 | |
| 176 | /** Lines added and removed across a change's files. */ | |
| 177 | export function changeSize(files: { additions: number; deletions: number }[], sensitive: string[]): ChangeSize { | |
| 178 | return { | |
| 179 | files: files.length, | |
| 180 | lines: files.reduce((sum, file) => sum + file.additions + file.deletions, 0), | |
| 181 | sensitive, | |
| 182 | }; | |
| 183 | } | |
| 184 | ||
| 185 | /** A past run of the same work, as the work service lists it. */ | |
| 186 | export type PastAttempt = { status: string; halted?: string | null; title?: string | null }; | |
| 187 | ||
| 188 | /** | |
| 189 | * Whether the latest attempt at the same work failed: it failed, or g1t | |
| 190 | * stopped it at a cap of its guardrails. A person stopping it is not a | |
| 191 | * failure. `title` narrows it to the same plan, whose runs have no pull | |
| 192 | * request to tell them apart. | |
| 193 | */ | |
| 194 | export function lastAttemptFailed(newestFirst: PastAttempt[], title?: string): boolean { | |
| 195 | const last = newestFirst[0]; | |
| 196 | if (!last) return false; | |
| 197 | if (title !== undefined && (last.title ?? "").trim() !== title.trim()) return false; | |
| 198 | return last.status === "failed" || (last.status === "stopped" && Boolean(last.halted)); | |
| 199 | } |