承認待ちと再開
権限リクエストと質問に応答し、セッションを pause して resume する
ターンがツールを実行する前に人の許可を待つ、または人に質問すると、セッションは needs_input になる。応答するとターンが続行する。セッションをしばらく休ませたいときは pause し、後で resume する。開始で作ったセッション(SDK は sessionId、HTTP API は SID)を引き続き使う。応答するにはキーに sessions:approve が、pause・resume には sessions:control が必要である。応答するのは、セッションを作った人、エージェントの管理者、owner・admin だけである(権限)。下の例はセッションを作ったキーで応答する。これは開発用サーバーの運用者キー(scripts/local.sh key)でだけ通る。Web コンソールで発行した個人キー・エージェントキーは、自分が開始したセッションのリクエストに応答できない(403 self_approval)ので、別のキーで作ったクライアントで応答する(承認用のキーを分けて使う)。
待機中のリクエスト
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' }リクエストを自分で読まずに handler に任せることもできる。waitForTurn の onRequest はターンを待つ間に生じたリクエストを、answerPending(werft, sessionId, handlers) は現在開いているリクエストを古いものから 1 回ずつ handler に渡す(質問に答える)。
リクエストは次のどちらかである。
kind: "permission": ツール実行の許可。toolとinputが、何を実行するかを示す。kind: "question": モデルがAskUserQuestionで尋ねる質問。questions[]ごとにquestion_id、質問文のprompt、選択肢のoptions[]がある。
リクエストは 30 分以内に応答しないと、拒否として終了する。その後に応答すると 409 REQUEST_EXPIRED になる。
エンジンが同じ種類の呼び出しを繰り返し確認するツール権限は、許可の応答に remember: "session" を付けると、このセッションが終わるまで同じルールを再び確認しなくなる。リクエストがそのようなルールを提案したときだけ受け付け、拒否の応答や提案のないリクエストには使えない。サーバーの許可リスト外のツール、push 承認、質問はこの応答で広がらない。許可リストの検査は、この応答に関係なくすべての呼び出しにそのまま適用される。remember を知らない古い worker が開いたリクエストには提案が付かないため、Slack のカードにも このセッション中は許可 ボタンは表示されない。Web コンソールにはこのボタンがなく、今のところ Slack のカードとこの API フィールドからだけ使える。
拒否する
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 api許可は "action": "allow" である。reason は拒否の場合だけ使い、省略できる。モデルはその理由をツールの結果として受け取り、ターンを続ける。レスポンスの approval には、誰が(answered_by)、どこから(surface: api・web・slack)応答したかが残る。
質問に答える
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: completed選択する代わりに文章で答えるには、selected_option_ids: [] と free_text を送る。
handler は permission と question の 2 種類である。handler が undefined を返すか、その種類の handler がなければ、リクエストは開いたままにして人に任せる。onAnswer が受け取る outcome は、accepted(回答が受け付けられた)、left_open(handler が答えなかった)、too_late(すでに期限切れか、別の人が答えた。code は REQUEST_EXPIRED か REQUEST_STALE)、conflict(同じリクエストに別の回答が先に届いた)のいずれかである。セッションが paused・stopped・recovery_required・closed になるとターンが自然には終わらないので、waitForTurn は TurnBlockedError を投げる。
pause と resume
pause は、進行中の作業を終わらせて checkpoint(作業ツリーと会話履歴)を残し、worker を停止する。resume は、新しい worker がその checkpoint から同じ作業ツリーと同じ会話を復元する。どちらも現在の revision を expected_revision として送る。その間にセッションが変わっていると 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 2paused の間に送ったメッセージは 409 SESSION_PAUSED になる。60 秒以内に drain できないと、attention が PAUSE_BLOCKED になる。開いているリクエストがあれば、応答するか 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