KestrelDocs

Agents

Kestrel runs the agent you already use. It gives that agent a set of tools inside the sandbox and records every call they make.

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.

Fig. 1Tool calls
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. float field(vec2 p, float t) { if (p.x < 24.0 || p.x > 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.

Pick an agent

Set the agent in kestrel.yaml, or per run in the SDK.

agent: claude-code
AgentValueNotes
Claude Codeclaude-codeThe default
Codexcodex
Your owncommandAny process that speaks MCP over stdio

Bring your own loop

Point Kestrel at a command. It starts the process inside the sandbox and connects Kestrel's tools to it over MCP.

kestrel.yaml
agent:
  command: node
  args: [agent/index.js]
  env: [OPENAI_API_KEY]

Secrets named under env come from Settings → Secrets, never from the repo.

The tools

ToolWhat 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.

Was this page helpful?

Open the dashboard