Changelog

What's new in
Kestrel.

Every release, newest first. We ship most weeks.

New · v2.8

Evals run before every pull request

Evals used to be opt-in. Now every diff runs your tests and Kestrel's evals for scope, style and risk before a pull request can open, and a failing diff goes straight back to the agent with the failure attached.

Eval gate An agent writes a diff that runs through six checks in order; it fails the evals and goes back to the agent with the failure, and the revised diff passes every check before a pull request opens. const float LANE = 200.9; const float LK = 21.0; float stepx(float u) { return u - sin(TAU * u) / TAU; } // one check to the next, at rest at each // A diff hunk of five lines: a gutter mark (added, removed, context) and a code line of its own length. float hunk(float c, float r, float ver) { float kind = 0.0; float len = 2.0; if (ver < 0.5) { if (r < 0.5) { kind = 1.0; len = 4.0; } else if (r < 1.5) { kind = -1.0; len = 3.0; } else if (r < 2.5) { kind = 0.0; len = 2.0; } else if (r < 3.5) { kind = 1.0; len = 4.0; } else { kind = 1.0; len = 2.0; } } else { if (r < 0.5) { kind = 1.0; len = 3.0; } else if (r < 1.5) { kind = 1.0; len = 4.0; } else if (r < 2.5) { kind = -1.0; len = 2.0; } else if (r < 3.5) { kind = 1.0; len = 4.0; } else { kind = 0.0; len = 3.0; } } if (c < 0.5) return kind > 0.5 ? 1.1 : kind < -0.5 ? 0.5 : 0.2; if (c > len + 0.5) return 0.0; return kind > 0.5 ? 0.68 : kind < -0.5 ? 0.4 : 0.3; } // The diff at centre q, written as far as w (0 to 1, line by line), with its failure attached under it (fl). float diff(vec2 p, vec2 q, float ver, float w, float fl, float k) { float c = floor((p.x - q.x) / 7.0 + 2.5); float r = floor((p.y - q.y) / 9.4 + 2.5); if (c < 0.0 || c > 4.0 || r < 0.0 || r > 5.0) return 0.0; if (r > 4.5) return fl * (c < 0.5 ? 1.3 : 1.05); if (r + c / 5.0 >= w * 5.0) return 0.0; return k * hunk(c, r, ver); } // The point a distance d along the return path. vec2 back(float d) { if (d <= 21.0) return mix(vec2(427.5, 200.9), vec2(406.5, 200.9), (d - 0.0) / 21.0); if (d <= 124.4) return mix(vec2(406.5, 200.9), vec2(406.5, 304.3), (d - 21.0) / 103.4); if (d <= 421.9) return mix(vec2(406.5, 304.3), vec2(109.0, 304.3), (d - 124.4) / 297.5); if (d <= 489.3) return mix(vec2(109.0, 304.3), vec2(109.0, 236.9), (d - 421.9) / 67.4); return vec2(109.0, 236.9); } // One check's run for a diff that reached it at a: its cells light out from the lane, a cell at a time, up and down // the stage; stopAt under 7 is a test that failed there, and the run stops. Then the light fades back to idle. float scan(float dc, float a, float stopAt, float t) { float s0 = a - 0.06; if (t < s0) return 0.0; float reach = min(7.0 * (t - s0) / 0.12, stopAt); if (dc > floor(reach) + 0.5) return 0.0; float done = s0 + 0.12 * stopAt / 7.0; float fade = 1.0 - sm(done, done + (stopAt < 6.5 ? 0.25 : 0.45), t); float head = (t < done && abs(dc - floor(reach)) < 0.5) ? 0.2 : 0.0; return fade * (1.0 + head); } // Held across the seam: on from on0, and still on from the last cycle until the board clears. float held(float on0, float t) { return max(sm(on0, on0 + 0.06, t), 1.0 - sm(0.25, 0.43, t)); } float field(vec2 p, float t) { if (p.x < 18.0 || p.x > 702.0) return 0.0; float ph = TAU * t / T; float col = floor((p.x - 4.0) / 7.0 + 0.5) - 2.0; float cell = floor(p.y / 9.4); float v = 0.0; // the agent: a bright rim, an inner ring, a speckled floor and a core that churns, and flares while it writes float wr = sm(0.2, 0.3, t) * (1.0 - sm(0.5, 0.7, t)) + sm(2.26, 2.36, t) * (1.0 - sm(2.58, 2.78, t)); { vec2 c = vec2(109.0, LANE); float d = length(p - c) / 30.0; float inside = 1.0 - sm(0.9, 1.0, d); float churn = noise(p * 0.09 + vec2(1.4 * cos(ph), 1.4 * sin(ph))); v += 0.46 * exp(-pow(d - 1.0, 2.0) / 0.008); v += inside * 0.26 * exp(-pow(d - 0.64, 2.0) / 0.005); v += inside * (0.12 + 0.12 * step(0.82, hash(floor(p / vec2(7.0, 9.4)) + 1.3))); v += blob(p, c, 13.2, 0.82 + 0.5 * (churn - 0.5) + 0.45 * wr); } // the forward lane, agent to pull request, and the return lane under the gate, back up into the agent if (abs(cell - LK) < 0.5 && col > 17.0 && col < 81.0) v = max(v, 0.2); if (abs(cell - 32.0) < 0.5 && col >= 13.0 && col <= 65.0) v = max(v, 0.2); if (abs(col - 13.0) < 0.5 && cell > 24.0 && cell < 32.0) v = max(v, 0.2); // the gate's floor: a speckle between the stages, so the gate reads as one housing if (col > 32.0 && col < 67.0 && abs(cell - LK) < 7.5 && step(0.62, hash(vec2(col, cell) + 2.3)) > 0.5) v = max(v, 0.22); // the gate: six stages of test cells, a header and footer cell on each, in order; each runs as the diff reaches it float rel = col - 34.0; float s = floor(rel / 6.0); float dc = abs(cell - LK); if (s >= 0.0 && s <= 5.0 && rel - 6.0 * s < 1.5 && dc < 7.5) { float hs = hash(vec2(col, cell) + 7.1); float base = dc > 6.5 ? 0.85 : 0.42 + 0.2 * step(0.55, hs) * (0.7 + 0.3 * sin(ph + hs * 40.0)); float a1 = 0.84 + s * 0.18; float a2 = 2.92 + s * 0.18; float lit = 0.0; if (s < 4.0 - 0.5) lit = scan(dc, a1, 7.0, t); else if (s < 4.0 + 0.5) lit = scan(dc, a1, 5.0, t); lit = max(lit, scan(dc, a2, 7.0, t)); v = max(v, max(base, lit * (dc > 6.5 ? 1.1 : 1.0))); // the failing test: it flares as the run reaches it, and dims once the diff is back at the agent if (s > 4.0 - 0.5 && s < 4.0 + 0.5 && abs(cell - 16.0) < 0.5) v = max(v, 1.3 * sm(1.576, 1.616, t) * (1.0 - sm(2.07, 2.42, t))); } // the record: a row of six slots per run, each filled as its check passes; run 1 stops at EVALS if (s >= 0.0 && s <= 5.0 && rel - 6.0 * s < 1.5) { if (cell > 35.5 && cell < 37.5) { float fl = s < 4.0 - 0.5 ? held(0.9 + s * 0.18, t) : 0.0; v = max(v, 0.15 + 0.85 * fl); } if (cell > 38.5 && cell < 40.5) v = max(v, 0.15 + 0.85 * held(2.98 + s * 0.18, t)); } // the pull request: a faint outline waiting; it opens once the diff that passed reaches it float open = held(4.18, t); float pc = col - 81.0; float pk = cell - 16.0; if (pc >= 0.0 && pc <= 8.0 && pk >= 0.0 && pk <= 10.0) { bool rim = pc < 0.5 || pc > 7.5 || pk < 0.5 || pk > 9.5; float card = rim ? 0.42 : 0.0; if (abs(pk - 1.0) < 0.5 && pc >= 1.0 && pc <= 6.0) card = 1.05; // the title if (abs(pk - 2.0) < 0.5 && pc >= 1.0 && pc <= 3.0) card = 0.3; // the branch if (pk > 3.5 && pk < 8.5 && pc >= 1.0 && pc <= 5.0) card = max(card, 0.95 * hunk(pc - 1.0, pk - 4.0, 1.0)); // the diff's lines if (abs(pk - 9.0) < 0.5 && pc >= 1.0 && pc <= 6.0) card = max(0.2, 0.95 * held(4.24 + 0.035 * pc, t)); // six check pips v = max(v, rim ? max(0.15, card * open) : card * open); } // diff 1: written, through the four checks it passes, stopped at EVALS, then back to the agent with its failure float fl = sm(1.586, 1.646, t); if (t > 0.25 && t < 0.5) v = max(v, diff(p, vec2(179.0, LANE), 0.0, (t - 0.25) / 0.25, 0.0, 1.0)); else if (t >= 0.5 && t < 0.84) v = max(v, diff(p, vec2(mix(179.0, 259.5, ease((t - 0.5) / 0.34)), LANE), 0.0, 1.0, 0.0, 1.0)); else if (t >= 0.84 && t < 1.66) { float x = 259.5 + 42.0 * stepx(min((t - 0.84) / 0.18, 4.0)); v = max(v, diff(p, vec2(x, LANE), 0.0, 1.0, fl, 1.0)); } else if (t >= 1.66 && t < 2.32) { float u = (t - 1.66) / 0.66; float k = 1.0 - sm(0.88, 1.0, u); v = max(v, diff(p, back(ease(u) * 489.3), 0.0, 1.0, k, k)); } // diff 2: written, through all six checks, out to the pull request if (t > 2.34 && t < 2.58) v = max(v, diff(p, vec2(179.0, LANE), 1.0, (t - 2.34) / 0.24, 0.0, 1.0)); else if (t >= 2.58 && t < 2.92) v = max(v, diff(p, vec2(mix(179.0, 259.5, ease((t - 2.58) / 0.34)), LANE), 1.0, 1.0, 0.0, 1.0)); else if (t >= 2.92 && t < 3.88) v = max(v, diff(p, vec2(259.5 + 42.0 * stepx((t - 2.92) / 0.18), LANE), 1.0, 1.0, 0.0, 1.0)); else if (t >= 3.88 && t < 4.2) { float u = (t - 3.88) / 0.32; v = max(v, diff(p, vec2(mix(469.5, 613.0, ease(u)), LANE), 1.0, 1.0, 0.0, 1.0 - sm(0.8, 1.0, u))); } return v; }
  • Eval results show on the pull request as a check
  • Write your own evals in kestrel.yaml
  • Failed evals no longer count as a new run
v2.7

Rate limits per repo

Set how many runs a repo may start a minute and how much it may spend a day. Bursts above the limit are turned away at the edge, before they cost anything.

Rate limiting float rate(float x) { float P = 420.0; float b = 0.0; for (int j = -1; j <= 3; j++) b += exp(-pow(x - 420.0 - float(j) * P, 2.0) / 2116.0); return 70.0 + 36.0 * sin(TAU * 3.0 * x / P + 0.3) + 24.0 * sin(TAU * 7.0 * x / P + 1.0) + 190.0 * b; } float field(vec2 p, float t) { float X0 = 60.0, X1 = 655.0, FLOOR = 400.0, LIM = 220.0; if (p.x < X0 || p.x > X1 || p.y > FLOOR) return 0.0; float xx = p.x + 420.0 * t / T; float top = FLOOR - rate(xx); if (p.y < top) return 0.0; if (p.y >= LIM) { float flare = top < LIM ? 0.45 * exp(-pow(p.y - LIM, 2.0) / 50.0) : 0.0; return 0.42 + 0.5 * (p.y - max(top, LIM)) / (FLOOR - max(top, LIM)) + flare; } // above the limit: sheared off, breaking into spray that rises and fades. It moves 60 columns and 30 cells a cycle, // and its pattern repeats over exactly those, so the loop closes. float rise = (LIM - p.y) / (LIM - top + 1.0); float spray = step(0.6, hash(vec2(mod(floor(xx / 7.0), 60.0), mod(floor((p.y + 9.4 * 30.0 * t / T) / 9.4), 30.0)))); return spray * (0.55 - 0.42 * rise); }
  • Limits per repo or per team
  • A 429 with a retry time when a run is turned away
  • Daily spend caps on Team and Enterprise
v2.6

Shared team memory

What one teammate's run learns about a repo is now available to everyone's next run. Memory stays per repo and your team can read and edit it from the dashboard.

  • Memory viewer and editor in the dashboard
  • Forget a fact with one click
  • 38% fewer steps by a repo's tenth run
v2.5

Context compaction

Long runs used to stop when their context filled. Kestrel now squeezes older turns into a summary and keeps going, up to one million tokens of working context.

Context window float endAt(float t) { return t < 0.3 ? 494.0 : t < 0.9 ? mix(494.0, 284.0, ease((t - 0.3) / 0.6)) : t < 1.3 ? 284.0 : t < 3.4 ? mix(284.0, 494.0, (t - 1.3) / 2.1) : 494.0; } float field(vec2 p, float t) { float X0 = 67.0, LIMIT = 641.0, END = 494.0, COMPACT = 284.0; float e = endAt(t); float most = (END - X0) / (COMPACT - X0); float sq; float zone; if (t < 0.9) { sq = (END - X0) / (e - X0); zone = e; } else { sq = mix(1.0, most, 1.0 - sm(3.5, 4.1, t)); zone = COMPACT; } float squeezed = (sq - 1.0) / (most - 1.0); float tau = p.x <= zone ? X0 + (p.x - X0) * sq : p.x; // new tokens beyond the compacted zone: the pattern they had at t = 0 float k3[3]; k3[0] = 0.95; k3[1] = 0.85; k3[2] = 0.7; float y3[3]; y3[0] = 168.0; y3[1] = 232.0; y3[2] = 296.0; float r3[3]; r3[0] = 13.0; r3[1] = 17.0; r3[2] = 11.0; float v = 0.0; float wob = 5.0 * sin(p.y * 0.18 + TAU * 8.0 * t / T); // whole turns a cycle, so the loop closes for (int i = 0; i < 3; i++) { float band = exp(-pow(p.y - y3[i], 2.0) / (r3[i] * r3[i])); float on = step(0.28, hash(vec2(floor(tau / 7.0), float(i)))); float front = 1.0 - sm(e - 8.0, e + 6.0 + wob, p.x); if (p.x >= X0 && p.x <= LIMIT) { v += band * on * k3[i] * (p.x <= zone ? 1.0 + 0.45 * squeezed : 1.0) * front; v += band * 0.14 * (1.0 - front) * step(p.x, LIMIT); // the write head: a glow at the front while new tokens stream in if (t > 1.3 && t < 3.5) v += blob(p, vec2(e + 3.0, y3[i]), 9.0, 0.8 * sm(1.3, 1.5, t) * (1.0 - sm(3.3, 3.5, t))); } } return v; }
  • Up to 4x compaction before anything is dropped
  • Compaction events shown in the trace
  • Pinned instructions are never compacted
v2.4

Webhooks and a faster CLI

Post run events to your own tools, and start runs from a terminal in half the time.

  • Webhooks for run started, evals failed and pull request opened
  • kestrel run starts 2x faster
  • kestrel logs follows a run live

Give your agents
a runtime.