Release conductor
Goal
You want to ship a build, but two things must happen first: a human resolves the ambiguity ("which environment? which version?"), and a human signs off on the irreversible step before it runs. This is the canonical irreversible action behind a human gate pattern, and it leans on two of primer's human-in-the-loop primitives on a session:
ask_user, the agent pauses the session to ask the operator a question, and resumes with the answer.- A required tool-approval gate, a policy that forces an operator decision before a named tool can run. Approve and the gated call executes; reject and it returns a rejection result and a durable record is written.
This recipe shows both gates firing back to back in one session: first ask_user, then
the deploy approval. Every step is shown two ways: first in the console (which page
to open, what to click, which fields to fill), then a Via the CLI block with the
exact primectl command. Pick whichever you prefer; the two paths drive the same
session.
Ingredients
- An LLM provider.
- A
localworkspace for the session to run in. - A deploy tool to gate. In production this is your real "ship it" action, an MCP
toolset like
deploy-ops__run_deploy, or whatever runs your migration. The gate is tool-agnostic: it keys on the(toolset_id, tool_name)pair, so the mechanism is the same whatever the tool does. For a self-contained dry run you can gate the built-inworkspaces__write_workspace_fileand treat the written file as the deploy marker. - A required tool-approval policy on that tool (see the tool-approval reference).
If you have not connected primectl yet, see "Connecting the CLI" in the
RAG knowledge base recipe.
Walkthrough
1. Create the release-conductor agent
Give it exactly two tools: ask_user to confirm, and the deploy tool. The system
prompt forbids deploying without confirming first.
In the console:
- Go to Compute > Agents and click New agent.
- On Basic set ID to
release-conductor, add a Description, and pick the LLM provider and Model. - On Tools check
system__ask_userand your deploy tool (hereworkspaces__write_workspace_file). - On Advanced paste the system prompt (below) and set max tool turns to
5. Click Create.
Via the CLI:
primectl create -f release-conductor.yaml
kind: agent
spec:
id: release-conductor
description: Confirms a deploy target with a human, then deploys behind an approval gate.
model: { provider_id: <llm>, model_name: <model> }
tools:
- system__ask_user
- workspaces__write_workspace_file
max_tool_turns: 5
system_prompt:
- >-
You are a Release Conductor. If the target or version is ambiguous, call
ask_user to confirm the environment and version. Then deploy by writing the
RELEASE marker file with the confirmed values. Never deploy without confirming
first.
2. Gate the deploy tool with a required policy
A required policy means: every call to that tool pauses the session for an operator
decision before it runs.
In the console:
- Go to Compute > Approvals and click New approval policy.
- Set the Toolset id to
workspaces(your deploy tool's toolset), the Tool name towrite_workspace_file, and the approval type to required. Set a timeout if you like, leave it enabled, and click Create policy.
Via the CLI:
primectl create -f deploy-policy.yaml
kind: tool_approval_policy
spec:
id: tap-run-deploy
toolset_id: workspaces
tool_name: write_workspace_file
enabled: true
approval: { type: required }
timeout_seconds: 600
The resolver caches policies in-process. After creating or changing one, invalidate the cache so a running worker picks it up immediately, in the console by reopening the Approvals page (the per-tool badge re-reads policies), and on the CLI with:
primectl call tool_approval_policy invalidate
Note the policy keys on the bare pair (toolset_id + tool_name), but the
pending-approval echo and the durable record report the call's namespaced name
(workspaces__write_workspace_file).
3. Start the session ambiguously and ride both gates
Start the session with an instruction that leaves the target open, so the agent has to
ask. The agent calls ask_user (the session parks), you answer, the agent calls the
deploy tool (the gate trips, the session parks again), and you approve or reject.
In the console:
- Click New session (top right of the dashboard or the Sessions page). Set the
Binding to
agent, pickrelease-conductor, choose your Workspace, typeDeploy the latest build.into Initial instructions, and click Create. - Open the session. When it parks on
ask_user, the ask_user panel shows the prompt; type your answer (staging, v1.4.2) and submit. The turn resumes. - When the deploy call trips the gate, the session parks on the approval. Go to
Compute > Approvals (or use the in-session approval banner): the pending row
shows the tool name (
workspaces__write_workspace_file), the arguments, and Approve / Reject buttons.- Approve, and the gated tool re-dispatches and runs for real; the agent reports
success and the session ends
completed. - Reject (enter a reason such as
change freeze window), and the gated call resolves to a rejection result instead of running; the agent aborts without the deploy side effect, the session ends, and a durable rejection record is written.
- Approve, and the gated tool re-dispatches and runs for real; the agent reports
success and the session ends
Via the CLI, session run --watch (the default) handles both parks inline: it polls
the session to terminal, surfaces each prompt, and answers it from your scripted flags.
--answer feeds a canned ask_user reply, and --yes auto-approves every approval
gate:
primectl session run <workspace-id> --agent release-conductor \
-i "Deploy the latest build." \
--answer "staging, v1.4.2" --yes
That is the approve path end to end: it answers the ask_user park with
staging, v1.4.2, auto-approves the deploy gate, and watches the session to
ended: completed.
For the reject path, start without --yes and resolve the approval
non-interactively with session respond once the session parks on the gate. Start the
run watching only the ask_user park (answer it, then let it park on the gate):
primectl session run <workspace-id> --agent release-conductor \
-i "Deploy the latest build." \
--answer "staging, v1.4.2" --no-watch
then poll primectl get session <session-id> -o json -r until parked_status is
parked on the approval, and reject it:
primectl session respond tool-approval <session-id> \
--decision rejected --reason "change freeze window"
(session respond ask-user <session-id> --response "staging, v1.4.2" is the matching
one-shot for the ask_user park if you script the poll loop yourself.)
A few things worth knowing:
- Two parks, one session.
ask_userand the approval gate are independent pauses; the session parks, resumes, and parks again.session run --watchanswers both in order;session respondanswers either one-shot. - The human makes the decision, not the model. The agent only chooses to call
ask_userand the deploy tool. Whether the answer says "staging" or the gate is approved or rejected is entirely the operator's. - Reject is safe by construction. On a rejection the gated tool never executes, so
the irreversible action has no side effect. The denial is recorded; read it from the
Approvals page (resolved records are retained). There is no first-class resource for
the records list, so on the CLI it is the
primectl rawescape hatch:primectl raw GET /v1/tool_approval/records --param status=rejected -o json, then find the row whosesession_idis your session.
Testing
A scripted end-to-end test exercises the full loop both ways
(tests/e2e/test_cookbook_release_conductor_cli.py, SMK-COOKBOOK-CLI-13). It builds
the conductor and the required policy with primectl create -f, then drives the
session HITL with primectl session run --watch: the approve run uses
--answer "staging, v1.4.2" --yes to answer the ask_user park and auto-approve the
gate, and the reject run answers the ask_user park then rejects the gate with
session respond tool-approval. It gates the built-in
workspaces__write_workspace_file as the stand-in deploy (the written RELEASE file
is the observable side effect).
Expected outcome (verified):
- Approve path: the transcript
(
primectl workspace files get <ws> .state/sessions/<sid>/messages.jsonl --content) shows both theask_usercall and the deploy call; the deploytool_resultsucceeded; the session endsended/completed; and the deploy side effect (theRELEASEmarker) is on disk. - Reject path: the deploy was offered but its
tool_resultis a rejection (carrying yourreason); there is no deploy side effect; the session ends; and a rejected approval record exists with the namespacedtool_nameand yourreason.
Point the gated tool at your real deploy action and you have a release pipeline that will not ship until a human confirms the target and signs off on the irreversible step.