Kestrel doesn't ship its own agent. It runs Claude Code, Codex, or your own loop inside the sandbox and hands it tools that work there: read and write files, run commands, search the repo and ask its memory. Each call is a step in the run's trace .
Tool calls
A task card is handed to an agent, which calls three tools in turn, a search, a code runner and a calendar; each call goes out, the tool works and its result comes back, ticking a step on the card. Then the agent writes the answer and the task is marked done.
696.0 || p.y < 24.0 || p.y > 436.0) return 0.0;
float ph = TAU * t / T;
float v = 0.0;
float c = floor((p.x - 4.0) / 7.0 + 0.5);
float k = floor(p.y / 9.4);
float clear = 1.0 - sm(0.300, 0.500, t); // the last task's marks, until the new task is written
// the card
if (c > 4.500 && c < 19.500 && k > 16.500 && k < 30.500) {
float a = 0.0;
float done = max(clear, sm(4.0, 4.060, t));
if (k < 17.500) a = abs(c - 18.0) < 0.5 ? mix(0.25, 1.15, done) : 0.7; // title, check
else if (c < 5.500 || c > 18.500 || k > 29.500) a = 0.25; // edges
else if (abs(k - 19.0) < 0.5 || abs(k - 20.0) < 0.5) {
float len = abs(k - 19.0) < 0.5 ? 9.0 : 6.0;
if (c > 6.5 && c < 7.5 + len) a = 0.42 + 0.6 * exp(-pow(t - 0.380 - 0.03 * (c - 7.0), 2.0) / 0.003);
}
else if (abs(k - 22.0) < 0.5) {
float on = max(clear, sm(1.750, 1.810, t));
if (abs(c - 7.0) < 0.5) a = mix(0.25, 1.1, on);
else if (c > 8.5 && c < 16.500) a = mix(0.25, 0.72, on);
}
else if (abs(k - 24.0) < 0.5) {
float on = max(clear, sm(2.700, 2.760, t));
if (abs(c - 7.0) < 0.5) a = mix(0.25, 1.1, on);
else if (c > 8.5 && c < 14.500) a = mix(0.25, 0.72, on);
}
else if (abs(k - 26.0) < 0.5) {
float on = max(clear, sm(3.650, 3.710, t));
if (abs(c - 7.0) < 0.5) a = mix(0.25, 1.1, on);
else if (c > 8.5 && c < 15.500) a = mix(0.25, 0.72, on);
}
else if (abs(k - 28.0) < 0.5 && c > 6.5 && c < 17.500) {
float tw = 3.700 + 0.012 * (c - 7.0); // written left to right
a = 0.72 * max(clear, step(tw, t));
if (t > tw && t < tw + 0.06) a = 1.15;
}
else if (abs(k - 29.0) < 0.5 && c > 6.5 && c < 13.500) {
float tw = 3.850 + 0.012 * (c - 7.0); // written left to right
a = 0.72 * max(clear, step(tw, t));
if (t > tw && t < tw + 0.06) a = 1.15;
}
v = max(v, a);
}
// the agent: awake from the hand-off until the answer is written
float awake = sm(0.700, 0.800, t) * (1.0 - sm(4.0, 4.300, t));
{
vec2 d = p - vec2(282.0, 229.100);
float r = length(d);
float an = atan(d.y, d.x);
// a churning core, an inner ring of twelve marks, a faint floor and the rim
float churn = noise(d * 0.09 + vec2(3.0 * cos(ph), 3.0 * sin(ph)));
v = max(v, blob(p, vec2(282.0, 229.100), 27.0, 1.35 + awake * 0.35 * (churn - 0.5)));
// the marks turn one place while it is awake, so they end where they began
if (abs(r - 38.0) < 4.5 && abs(fract(an / TAU * 12.0 + sm(0.750, 4.300, t)) - 0.5) > 0.3) v = max(v, mix(0.42, 0.72, awake));
if (r > 28.0 && r < 50.0 && hash(floor(p / vec2(7.0, 9.4))) > 0.8) v = max(v, 0.2);
v = max(v, 0.42 * exp(-pow(r - 56.0, 2.0) / 14.0));
}
// the ports on the rim, and the dotted links to the card and the tools
v = max(v, blob(p, vec2(226.0, 229.100), 6.0, 1.05));
v = max(v, blob(p, vec2(326.577, 195.204), 6.0, 1.05));
v = max(v, blob(p, vec2(338.0, 229.100), 6.0, 1.05));
v = max(v, blob(p, vec2(324.343, 265.748), 6.0, 1.05));
if (mod(c, 2.0) < 0.5) {
float ln = seg(p, vec2(147.0, 229.100), vec2(216.0, 229.100), 4.0, 0.22);
ln = max(ln, seg(p, vec2(326.577, 195.204), vec2(436.0, 112.0), 5.0, 0.22));
ln = max(ln, seg(p, vec2(338.0, 229.100), vec2(424.0, 229.100), 5.0, 0.22));
ln = max(ln, seg(p, vec2(324.343, 265.748), vec2(424.0, 352.0), 5.0, 0.22));
v = max(v, ln);
}
// the hand-off, card to agent, and each call: out along its link, then the result back
{
float u = clamp((t - 0.450) / 0.300, 0.0, 1.0);
if (u > 0.0 && u < 1.0) v += blob(p, mix(vec2(147.0, 229.100), vec2(226.0, 229.100), ease(u)), 8.0, 1.1 * sm(0.0, 0.1, u) * (1.0 - sm(0.85, 1.0, u)));
}
{
float uo = clamp((t - 0.800) / 0.330, 0.0, 1.0);
float ub = clamp((t - 1.420) / 0.330, 0.0, 1.0);
if (uo > 0.0 && uo < 1.0) v += blob(p, mix(vec2(326.577, 195.204), vec2(436.0, 112.0), ease(uo)), 8.0, 1.1 * sm(0.0, 0.1, uo) * (1.0 - sm(0.85, 1.0, uo)));
if (ub > 0.0 && ub < 1.0) v += blob(p, mix(vec2(436.0, 112.0), vec2(326.577, 195.204), ease(ub)), 9.5, 1.15 * sm(0.0, 0.1, ub) * (1.0 - sm(0.85, 1.0, ub)));
}
{
float uo = clamp((t - 1.750) / 0.330, 0.0, 1.0);
float ub = clamp((t - 2.370) / 0.330, 0.0, 1.0);
if (uo > 0.0 && uo < 1.0) v += blob(p, mix(vec2(338.0, 229.100), vec2(424.0, 229.100), ease(uo)), 8.0, 1.1 * sm(0.0, 0.1, uo) * (1.0 - sm(0.85, 1.0, uo)));
if (ub > 0.0 && ub < 1.0) v += blob(p, mix(vec2(424.0, 229.100), vec2(338.0, 229.100), ease(ub)), 9.5, 1.15 * sm(0.0, 0.1, ub) * (1.0 - sm(0.85, 1.0, ub)));
}
{
float uo = clamp((t - 2.700) / 0.330, 0.0, 1.0);
float ub = clamp((t - 3.320) / 0.330, 0.0, 1.0);
if (uo > 0.0 && uo < 1.0) v += blob(p, mix(vec2(324.343, 265.748), vec2(424.0, 352.0), ease(uo)), 8.0, 1.1 * sm(0.0, 0.1, uo) * (1.0 - sm(0.85, 1.0, uo)));
if (ub > 0.0 && ub < 1.0) v += blob(p, mix(vec2(424.0, 352.0), vec2(324.343, 265.748), ease(ub)), 9.5, 1.15 * sm(0.0, 0.1, ub) * (1.0 - sm(0.85, 1.0, ub)));
}
// the tools: each lights only while it works, from the call's arrival until its result leaves
float work0 = sm(1.130, 1.180, t) * (1.0 - sm(1.420, 1.540, t));
float work1 = sm(2.080, 2.130, t) * (1.0 - sm(2.370, 2.490, t));
float work2 = sm(3.030, 3.080, t) * (1.0 - sm(3.320, 3.440, t));
float tw0 = 1.130;
float tw1 = 2.080;
float tw2 = 3.030;
// search: a lens with a rim, its glass filling as it searches, a handle, and three result lines lit in turn
{
float r = length(p - vec2(466.0, 104.0));
v = max(v, 0.72 * exp(-pow(r - 26.0, 2.0) / 18.0));
if (r < 21.0) v = max(v, 0.18 + 0.5 * work0);
v = max(v, seg(p, vec2(486.0, 124.0), vec2(506.0, 144.0), 5.0, 0.8));
for (int j = 0; j < 3; j++) {
float fj = float(j);
float kk = 8.0 + 2.0 * fj;
float len = fj < 0.5 ? 9.0 : fj < 1.5 ? 7.0 : 8.0;
if (abs(k - kk) < 0.5 && c > 73.5 && c < 74.5 + len) v = max(v, 0.25 + 0.5 * work0 * sm(tw0 + 0.06 * fj, tw0 + 0.06 * fj + 0.05, t));
}
}
// code: a gutter of line marks and indented lines, run top to bottom while it works
if (c > 60.5 && c < 84.5 && k > 19.5 && k < 28.5 && mod(k, 2.0) < 0.5) {
float j = (k - 20.0) / 2.0;
float ind = j < 0.5 ? 0.0 : j < 1.5 ? 2.0 : j < 3.5 ? 4.0 : 2.0;
float len = 6.0 + floor(9.0 * hash(vec2(j, 21.0)));
float run = work1 * exp(-pow(t - tw1 - 0.05 - 0.05 * j, 2.0) / 0.004);
if (c < 61.5) v = max(v, 0.42);
else if (c > 62.5 + ind && c < 63.5 + ind + len) v = max(v, 0.42 + 0.3 * work1 + 0.4 * run);
}
// calendar: a header with two binding rings, a grid of days; the free slot lights while it is found
{
if (k > 31.5 && k < 32.5 && c > 60.5 && c < 81.5) v = max(v, 0.7);
if (k > 30.5 && k < 31.5 && (abs(c - 64.0) < 0.5 || abs(c - 78.0) < 0.5)) v = max(v, 1.05);
if (k > 33.5 && k < 39.5 && mod(k, 2.0) < 0.5 && c > 60.5 && c < 81.5 && mod(c - 61.0, 3.0) < 1.5) {
bool slot = abs(k - 36.0) < 0.5 && c > 69.5 && c < 71.5;
v = max(v, slot ? 0.42 + 0.7 * work2 * sm(tw2 + 0.08, tw2 + 0.14, t) : 0.42 + 0.2 * work2);
}
}
return v;
}]]>
The agent takes the task and calls one tool at a time. Each call goes out, the tool does its work, and the result comes back before the next step.
Set the agent in kestrel.yaml, or per run in the SDK.
await kestrel.runs. create ({
repo: "acme/web-app" ,
task: "Fix the flaky checkout test" ,
agent: "codex" ,
}); Agent Value Notes Claude Code claude-codeThe default Codex codexYour own commandAny process that speaks MCP over stdio
Point Kestrel at a command. It starts the process inside the sandbox and connects Kestrel's tools to it over MCP.
agent :
command : node
args : [ agent/index.js ]
env : [ OPENAI_API_KEY ] Secrets named under env come from Settings → Secrets , never from the repo.
Tool What it does read_fileRead a file from the repo write_fileWrite a file; the change joins the diff run_commandRun a shell command and return its output search_repoFind code by text or by symbol recallAsk the repo's memory rememberSave a fact about the repo for later runs
Tip Turn a tool off for one repo with tools: { run_command: false }. The agent then plans with what is left, and the trace shows any call it tried.