Waiting for approval and resuming
Answer permission requests and questions, and pause and resume a session
When a turn waits for a person's permission before running a tool, or asks a person a question, the session becomes needs_input. When you answer, the turn continues. To let a session rest for a while, pause it and resume it later. Continue using the session created in Start (sessionId in the SDK, SID in the HTTP API). Answering needs sessions:approve on the key, and pause and resume need sessions:control. Only the session's creator, its agent's admins and owners and admins answer (Permissions). The examples below answer with the key that created the session. That works only with the development server's operator key (scripts/local.sh key). A personal or agent key issued in the web console cannot answer requests in a session it started (403 self_approval), so answer with a client made from another key (A separate key for approvals).
Pending requests
import { waitForTurn } from "@werft-ai/sdk";
const sent = await werft.sessions.messages.send(sessionId, {
message:
'GATE-SPEC {"id":"deny","steps":[{"tool":"Bash","input":{"command":"rm -rf build","description":"clean build"}}],"final":"cleaned"}',
});
let pending = await werft.sessions.requests.list(sessionId);
while (pending.length === 0) {
await new Promise((resolve) => setTimeout(resolve, 2000));
pending = await werft.sessions.requests.list(sessionId);
}
const [request] = pending;
if (request.kind === "permission") console.log(request.tool, request.input);Bash { command: 'rm -rf build', description: 'clean build' }You can also leave the requests to a handler instead of reading them yourself. onRequest of waitForTurn passes the requests that appear while it waits for the turn, and answerPending(werft, sessionId, handlers) passes the requests open now, oldest first, once each (Answer a question).
A request is one of two kinds.
kind: "permission": permission to run a tool.toolandinputshow what would run.kind: "question": a question the model asks withAskUserQuestion. Each entry ofquestions[]has aquestion_id, the question textprompt, and theoptions[]to choose from.
If a request is not answered within 30 minutes, it ends as a denial. Answering after that gets 409 REQUEST_EXPIRED.
For a tool permission whose calls the engine would keep asking about, an allow can carry remember: "session" so the same rule is not asked again until the session ends. It is accepted only when the request proposed such a rule, and not on a deny or on a request with no proposal. It never widens past the server allowlist, a push approval or a question, and the allowlist check still applies to every call whatever this answer says. A request opened by an older worker that does not know remember carries no proposal, so its Slack card shows no Allow for session button. The web console has no such button; for now it is available only from the Slack card and this API field.
Deny
const answered = await werft.sessions.requests.answer(sessionId, {
request_id: request.request_id,
kind: "permission",
action: "deny",
reason: "Do not delete build output.",
});
console.log(answered.receipt_status, answered.approval?.surface);
await waitForTurn(werft, sessionId, sent.turn_id);accepted apiTo allow, use "action": "allow". reason is used only for a denial and is optional. The model receives that reason as the tool result and continues the turn. The response's approval records who answered (answered_by) and where (surface: api, web, slack).
Answer a question
const asked = await werft.sessions.messages.send(sessionId, {
message:
'GATE-SPEC {"id":"ask","steps":[{"tool":"AskUserQuestion","input":{"questions":[{"question":"Which environment?","header":"Environment","options":[{"label":"staging","description":"safe"},{"label":"production","description":"live"}],"multiSelect":false}]}}],"final":"selected"}',
});
const answeredTurn = await waitForTurn(werft, sessionId, asked.turn_id, {
onRequest: {
question(request) {
const [question] = request.questions;
console.log(question.prompt, question.options.map((o) => o.label));
return {
answers: [
{
question_id: question.question_id,
selected_option_ids: [question.options[0].option_id],
},
],
};
},
},
onAnswer: (outcome) => console.log(`answered: ${outcome.outcome}`),
});
console.log(`turn ${answeredTurn.turn_id}: ${answeredTurn.status}`);Which environment? [ 'staging', 'production' ]
answered: accepted
turn 3: completedTo answer in text instead of choosing, send selected_option_ids: [] and free_text.
A handler is one of two kinds: permission and question. If a handler returns undefined, or there is no handler for that kind, the request stays open and is left to a person. The outcome that onAnswer receives is one of accepted (the answer was accepted), left_open (the handler did not answer), too_late (the request had already expired or someone else answered; code is REQUEST_EXPIRED or REQUEST_STALE), or conflict (a different answer to the same request went first). When the session becomes paused, stopped, recovery_required, or closed, the turn does not end on its own, so waitForTurn throws TurnBlockedError.
pause and resume
Pause finishes the work in progress, saves a checkpoint (the working tree and the transcript), and then takes the worker down. Resume has a new worker restore the same working tree and the same conversation from that checkpoint. Both send the current revision as expected_revision. If the session changed in between, the response is 409 REVISION_CONFLICT.
import { waitForReceipt } from "@werft-ai/sdk";
let session = await werft.sessions.get(sessionId);
const paused = await werft.sessions.pause(sessionId, {
expected_revision: session.revision,
reason: "lunch",
});
const receipt = await waitForReceipt(werft, paused.receipt_id);
console.log(receipt.operation, receipt.status);
session = await werft.sessions.get(sessionId);
console.log(session.admission_state, session.checkpoint_revision);pause succeeded
paused 2A message sent while paused gets 409 SESSION_PAUSED. If it cannot drain within 60 seconds, attention becomes PAUSE_BLOCKED. If a request is open, answer it or interrupt.
const resumed = await werft.sessions.resume(sessionId, {
expected_revision: session.revision,
});
await waitForReceipt(werft, resumed.receipt_id);
console.log((await werft.sessions.get(sessionId)).admission_state);active