This is the missing middle layer between “an agent replied” and “an operator can actually manage ongoing work.” Kestrel exposes several stateful control-plane surfaces for that job:
task.graph.gettask.graph.updateproject.snapshot.getproject.actionproject.review.getproject.review.actionmission_control.project.getmission_control.action.execute
Use them when a conversation and memory record are no longer enough to show the current plan, project status, or review decision.
Choose the right state record
| Record | Use it for | Do not use it for |
|---|---|---|
| task graph | durable structured task state tied to threads and sessions | general project metadata |
| Mission Control project | the project-scoped work-item lifecycle, attempts, evidence, and acceptance | session-scoped task or board state |
| project snapshot | read-only workspace, policy, review, and activity projection | work-item lifecycle authority |
| project action | explicit project Git mutations | task or work-item lifecycle editing |
| project review | code/file review workflows tied to the active session | broad project state storage |
Browser workflow through /api/kchat/control
The Kestrel One can use the control route to move through the full inspection path.
const inbox = await sendRunnerControl({
command: "operator.inbox",
sessionId: "reference-session-001",
});
const thread = await sendRunnerControl({
command: "operator.thread",
threadId: "thread-main",
});
const graph = await sendRunnerControl({
command: "task.graph.get",
sessionId: "reference-session-001",
threadId: "thread-main",
});
const snapshot = await sendRunnerControl({
command: "project.snapshot.get",
sessionId: "reference-session-001",
});
const review = await sendRunnerControl({
command: "project.review.get",
sessionId: "reference-session-001",
reviewTarget: {
taskId: "task:thread:thread-main",
filePath: "src/project/workspace.ts",
},
});That flow matches the current browser state-sync code: read the operator signals first, then fetch task graph, project snapshot, and review detail as needed. The browser is using the same runtime model the SDK uses, which is why inspection and control stay aligned across surfaces.
Browser review action example
When an operator wants to leave or apply a review action, the control route accepts a structured payload:
{
"command": "project.review.action",
"sessionId": "reference-session-001",
"reviewAction": {
"type": "review.comment.create",
"sessionId": "reference-session-001",
"target": {
"taskId": "task:thread:thread-main",
"pullRequestNumber": 42,
"filePath": "src/project/workspace.ts"
},
"body": "Please tighten this parser.",
"path": "src/project/workspace.ts",
"line": 12,
"side": "RIGHT"
}
}The response includes a project.review event so other clients and subscribers can observe the same saved review decision.
SDK workflow with KestrelClient
If the product is server-owned rather than browser-driven, use the runner client directly.
import { KestrelClient } from "@kestrel-agents/sdk/runner";
const client = new KestrelClient({
target: {
kind: "remote",
baseUrl: process.env.KESTREL_RUNNER_SERVICE_URL!,
authToken: process.env.KESTREL_RUNNER_SERVICE_TOKEN!,
},
});
const context = {
actor: {
actorId: "user-123",
actorType: "operator",
displayName: "Taylor Example",
tenantId: "acme",
},
tenantId: "acme",
};
const graph = await client.getTaskGraph(
{
sessionId: "reference-session-001",
threadId: "thread-main",
},
context,
);
const snapshot = await client.getProjectSnapshot(
{ sessionId: "reference-session-001" },
context,
);
const detail = await client.getProjectReview(
{
sessionId: "reference-session-001",
target: {
taskId: "task:thread:thread-main",
filePath: "src/project/workspace.ts",
},
},
context,
);Updating state through the SDK
Reading the surfaces is only half the workflow. The same client can update them with explicit payloads:
await client.updateTaskGraph(
{
sessionId: "reference-session-001",
threadId: "thread-main",
graph: {
tasks: {
"task:thread:thread-main": {
status: "in_review",
summary: "Waiting on operator review for the workspace parser changes.",
},
},
},
},
context,
);
await client.runProjectAction(
{
type: "branch.create",
sessionId: "reference-session-001",
branchName: "docs/search-hardening",
},
context,
);Example control responses
Task graph response:
{
"type": "task.graph",
"sessionId": "reference-session-001",
"threadId": "thread-main",
"graph": {
"taskId": "task:thread:thread-main",
"status": "in_review",
"summary": "Waiting on operator review for the workspace parser changes."
}
}Project snapshot response:
{
"type": "project.snapshot",
"sessionId": "reference-session-001",
"snapshot": {
"branch": "docs/search-hardening",
"focus": "review-state and search behavior",
"nextMilestone": "ship the docs example pass"
}
}Choose where to store the state
Use these records in this order:
- read
operator.threadif the operator needs thread context first - read
task.graphwhen the question is "what work items exist and what state are they in?" - read
project.snapshotwhen the question is "what is the higher-level project state right now?" - read or apply
project.reviewwhen the question is "what review work or comment actions are attached to this state?" - use
project.actiononly for intentional project-level mutations
That keeps the state model legible. It stops teams from forcing every state question into one oversized snapshot object or a transcript-only workflow.
What these records enable
Task graphs, project snapshots, and project reviews let your application manage plans, project status, and review decisions without burying them in conversation text.
Keep state out of transcripts and side stores
Store task and review state in these shared records instead of burying it in transcripts or prompts. The browser, CLI, and server integrations can then read and update the same information.