//! The assistant workflow: an operator-paced aion-authoring chat session,
//! authored in AWL.
//!
//! PROVISION IS SERVER-RUN: `assistant_provision` declares a command body, so
//! the server executes it from the deployed contract as the server's own user,
//! with no sandbox — no worker serves it. Its environment is cleared to PATH
//! only, its cwd is inherited from the server process, and the SERVER states
//! where workspaces live: the literal `{workspace_root}` placeholder in the
//! body's command is expanded at dispatch to the server's own aion home
//! `clones/` directory (reported in the startup banner's `workspace_root`
//! field). The operator supplies nothing but `objective` and `repo_path`.
//!
//! Pure orchestration over one server-run provisioning command and one
//! recorded worker action:
//!
//! assistant_provision -> [ assistant round ; await operator ]* [operator-paced]
//!
//! Round one dispatches the `assistant` agent action with the opening prompt
//! THIS DOCUMENT composes — the full working contract (the consts below), the
//! repository-attached or scratch branch fragment, and the operator's
//! objective. Every later round is the operator's continuation message
//! VERBATIM, dispatched into the SAME norn session (the worker pins
//! `{workflow_id}-assistant` + `--resume-if-exists`, so repeated dispatches of
//! the one action type resume one conversation and the contract is never
//! re-sent).
//!
//! Between rounds the session parks on the ONE control signal,
//! `assistant_continue`. Continue and end share the signal name and
//! discriminate in the payload: `end: true` finishes cleanly, a non-blank
//! `message` starts the next round, and a blank message is a no-op nudge that
//! is absorbed by the inner wait loop without spending a round.
//!
//! WHY THIS EXISTS: the Gleam-authored `assistant` package cannot start at all
//! under queue-routed serving. A project manifest's `DeclaredActivity` has one
//! field, so every activity is committed UNSCOPED and start is refused with
//! `NO_QUEUE_DECLARATION` (docs/design/aion-authoring/
//! GLEAM-PATH-CANNOT-START-FINDING.md). AWL emits a scoped contract, so it is
//! the only surface that can author this workflow. The differences between
//! this document and the Gleam original are recorded in full in
//! docs/design/aion-authoring/ASSISTANT-AWL-PORT.md — read it before changing
//! anything here.
//!
//! THE QUEUE IS PRIVATE, AND IT IS DERIVED. The `worker` block below is named
//! `assistant` — this workflow's own logical name — and NEVER `default`. It is
//! not a preference and it is not configurable: the server verifies at load
//! that the embedded document's declared queue is exactly its workflow type
//! (`crates/aion-server/src/assistant/document.rs`, `private_task_queue`), and
//! refuses the document if it declares anything else.
//!
//! The reason is #200. `default` is where an out-of-box worker comes up when
//! nobody has told it otherwise, and contract admission holds a registering
//! worker against EVERY reachable contract on its queue at once. While this
//! document sat on `default`, the first worker a newcomer started was refused
//! for not advertising `assistant` — the built-in assistant starved the very
//! workers it was meant to welcome. `default` belongs to the out-of-box
//! workers; the assistant lives on its own name.
//!
//! DELIBERATE OMISSION — no `timeout` in the header. An agent round carries no
//! timeout by design (a wedged round is cancelled or intervened, never timed
//! out), and the session itself is operator-paced: a real run has parked on
//! this signal for 28 days and resumed healthy. A workflow timeout would kill
//! exactly the sessions this workflow exists to keep alive.
workflow assistant
// The operator's opening ask. Must not be blank — there is nothing to open
// the session with. AWL has no input-validation construct, so unlike the
// Gleam original this is documented rather than enforced (see the ledger).
input objective: String
// The aion repository the session grounds its answers in: a local path or
// clone URL. Empty is the documented "no repository, scratch workspace"
// mode — the assistant then says what it cannot verify.
input repo_path: String
// The ONE control signal the session listens on. Both fields are REQUIRED
// on the wire. Send `{"message": "...", "end": false}` to continue and
// `{"message": "", "end": true}` to finish.
//
// The reason has CHANGED and the requirement has not. AWL used to be unable
// to read the value of an optional field at all — narrowing keyed on a
// binding name, so `nudge.end` could never become a `Bool`. That is fixed
// (#97-B): a guard now proves a PATH, and `when nudge.end is present and
// nudge.end` reads it. What still forces both fields is this flow's own
// shape, not the language: `nudge.end` is read where nothing has proved it
// — inside the `until` condition AND inside the `Round(...)` construction in
// the loop body, which sits under no guard. Making the fields optional means
// restructuring those reads behind guard arms, which changes the operator's
// wire contract on a session that can be parked for weeks. That is a
// deliberate change to make on purpose, not a consequence of a checker fix.
signal assistant_continue: Continuation
// The read-only surface: what an operator gets back from
// `POST /workflows/query {"query_name": "assistant_status"}`. It reads live
// in-memory state, appends no event, and answers instantly — the one thing
// that proves a session parked for weeks is alive rather than wedged. Same
// name and same shape as the Gleam original's query.
query assistant_status: AssistantStatus
outcome ended: type Session, route success
outcome capped: type Session, route success
outcome stalled: type Session, route success
// ---------------------------------------------------------------------------
// The opening prompt, as document constants.
//
// Ported from examples/agent-dev/worker/src/handlers/opening_prompt.rs — the
// worker composed this text when provisioning also materialised skill
// resources into the workspace; the declared body materialises nothing, so
// the prompt now points at `examples/assistant/resources/` inside the clone
// (attached case) or at the public clone command (scratch case), and the
// composition lives HERE, next to the branch that selects it.
//
// Raw strings are VERBATIM: every newline and space between the `"""`
// delimiters is part of the value, which is why several constants open or
// close mid-sentence — the concatenation seams in step `session` are exact.
// ---------------------------------------------------------------------------
// The contract's opening paragraph and the head of the workspace note, up to
// the workspace path the document splices in.
const contract_head = """You are the aion AWL authoring assistant: a long-running session helping an operator understand aion and author checked `.awl` workflows. The operator talks to you through the aion ops console; each of their messages arrives as a new prompt in this same session, so you keep full memory of the conversation.
## Your workspace
Your workspace for this session is `"""
// Closes the workspace path and states the working rules as a RULE, not a
// claim about this launch — because this document has more than one serving
// path and they differ. Served by `aion worker agent` from the shipped
// `harness` section, the agent now starts in a DECLARED directory — the
// section's `cwd "{workspace_root}"`, this box's own clones root — but that is
// the root ABOVE the session workspaces, not this session's workspace, and no
// tool is confined (the old `--workspace-root`/`-C` pair is still absent; see
// the harness narration). Served by scripts/setup.sh → examples/agent-dev/
// worker, the worker passes that pair and the agent IS confined and started in
// the session workspace itself. A sentence asserting either state lies on the
// other path, so the prompt instructs conduct that is correct on both; the
// spike document and ENVIRONMENT.md carry the same sentence for the same
// reason.
const workspace_confinement = """`. Do not assume you are confined to that directory or started inside it — confinement depends on how this worker was launched. `cd` into the workspace before your first shell command and start every later shell command from it, address files by absolute paths under it, and create nothing outside it."""
// Workspace-note tail, scratch branch.
const scratch_workspace = """ It is a scratch git workspace — no aion repository was provided for this session."""
// Workspace-note tail, repository-attached branch: opens before the
// operator's `repo_path` and closes after it.
const attached_workspace_open = """ It is a clone of the aion repository at `"""
const attached_workspace_close = """`; author `.awl` workflows inside the clone (e.g. under `examples/`)."""
// Ground-truth head, shared by both branches. Opens with the blank line that
// separates it from the workspace note and deliberately ends mid-sentence:
// each branch fragment completes the sentence.
const ground_truth_head = """
## Ground truth
Your PRIMARY resources are your skill documents — the distilled operating manual for this exact job, kept in the aion repository at `examples/assistant/resources/`:
- `ENVIRONMENT.md` — AWL preflight, workspace semantics, and the local/server boundary (read FIRST)
- `AWL-AUTHORING.md` — the checked `.awl` authoring method from contract through observation
- `COMMANDS.md` — exact AWL check/format/schema/emit/deploy/run and observation commands
- `AWL-REFERENCE.md` — the checked AWL workflow language surface
- `WORKERS.md` — current worker layers, task queues, shell manifests, and the host-native shell-worker law
For anything deeper, consult the aion repository itself — never answer repo-level questions from memory"""
// Ground-truth tail, repository-attached branch.
const attached_ground_truth = """. Your workspace IS a clone of the aion repository — read the skill documents, and everything else, directly from the clone."""
// Ground-truth tail, scratch branch. There are no skill documents on disk
// until the assistant clones the repository itself.
const scratch_ground_truth = """. No repository was attached to this session, so the skill documents are not on disk yet; when you need them — or anything else in the repository — clone it into your workspace (`cd` there first): `git clone --depth 1 https://github.com/ablative-io/aion.git`. The repository is private, so the clone only succeeds where this host has git credentials — if it fails, say so, answer only what you can stand behind, and note to the operator that repo-dependent questions need either host credentials or a session started with the repository path set."""
// Everything after the branch point: the valuable-paths list, the authoring
// method, the honesty rules, and the heading the operator's objective is
// appended under.
const contract_tail = """
The most valuable repository paths:
- `examples/awl-hello/awl_hello.awl` — minimal checked workflow
- `examples/dev-brief/awl/` and `examples/remediation-packet/awl/` — production AWL flows and child workflows
- `crates/aion-awl/tests/fixtures/rev2/**/valid/*.awl` — accepted grammar and checker proofs
- `crates/aion-awl/src/` — parser, checker, printer, compiler
- `crates/aion-cli/src/awl.rs`, `one_motion.rs`, and `main.rs` — the real check/emit/deploy/run commands
Quote paths when you cite them, so the operator can follow.
## How to author an AWL workflow
AWL source (`.awl`) is the authoring truth. Do not substitute a language SDK project or generated compiler output. Follow checked examples and the current checker, not memory.
The method:
1. Read `ENVIRONMENT.md`, `AWL-AUTHORING.md`, and `AWL-REFERENCE.md`; inspect the closest checker-valid `.awl` example.
2. Author the workflow header, terminal outcomes, types, worker action requirements, children/subflows, and steps in one `.awl` document. World-touching work is a typed worker action; durable orchestration stays in AWL.
3. Use explicit outcomes for selection, explicit flow vocabulary for parallel/sequential work, and explicit bounds for every loop or backward route. Never invent syntax.
4. Check after every structural change. When this session carries the aion MCP tools, use `check_document` (pass the workspace path so schema imports resolve); otherwise run `aion awl check FILE.awl`. Treat every diagnostic as a source defect; optionally run `aion awl fmt`, inspect the diff, and check again.
5. Read `WORKERS.md` before promising an action implementation. The worker name is its task queue, a declaration does not create a worker, and shell workers are host-native.
6. Deploy the checked `.awl`. With the aion MCP tools: `save_document`, then `deploy_document` with the exact content_hash the save returned — deploy verifies the SAVED document against that hash, and both tools need the deploy grant (a `deploy_denied` refusal names what is missing; report it to the operator rather than retrying). Without them: the ops console or `aion deploy FILE.awl` / `aion run FILE.awl --input 'JSON'`. Then observe the durable run in the console. Never start or restart a server as part of authoring.
## Honesty rules
- Distinguish what you VERIFIED (a file you read, a command you ran in this session) from what you believe. Say "I have not verified this" when you have not.
- Never invent AWL syntax or worker capability. When unsure, read a checker-valid fixture or the parser/checker and check the source (`check_document` over MCP, or `aion awl check`).
- If the repository is not available to you, say so and answer only what you can stand behind.
## The operator's objective
"""
/// Payload of the `assistant_continue` signal.
type Continuation {
/// The operator's next prompt, sent to the agent verbatim. Blank is a no-op
/// nudge that does not spend a round.
message: String,
/// True finishes the session cleanly as `OperatorEnded`.
end: Bool,
}
/// How the session ended. All three are CLEAN completions — the operator
/// ended it, or one of the two explicit budgets was spent. None is an error.
type Disposition = OperatorEnded | RoundCapExhausted | NudgeCapExhausted
/// What the `assistant_status` query reports: which stage the session is in,
/// and how many agent rounds it has run.
///
/// `round` is OPTIONAL where the Gleam original's is not, and the difference is
/// a language fact, not a choice. A loop's `counting` binding is readable only
/// AFTER the loop (`crates/aion-awl/src/emitter/loops.rs` removes it from the
/// body scope, and the checker refuses a body reference as "bound on some path
/// but not guaranteed"), and AWL has no arithmetic, so a round index cannot be
/// threaded through the loop either. Reporting `0` from inside the loop would
/// tell an operator that a session in round three is in round zero, which is
/// worse than saying nothing: absent means "this document cannot prove it
/// here", and every round number it DOES report is true.
type AssistantStatus { phase: String, round: Int? }
/// The recorded outcome of the server-run provision command. A bodied action's
/// result must be a record over the command outcome members (`exit_code`,
/// `stdout`, `stderr`); `exit_code` is always 0 here, because a non-zero exit
/// fails the dispatch instead of returning a result — and with no declared
/// retry policy the action gets exactly one attempt, so that failure reaches
/// the operator rather than any automatic re-run.
///
/// `stdout` is in the result because it IS the workspace path: the script
/// prints the resolved `<workspace_root>/<run_id>/repo` as its SOLE stdout
/// (every tool's chatter is redirected to stderr), so the recorded activity
/// result — and therefore durable history — carries the RESOLVED path. The
/// `{workspace_root}` placeholder lives only in the deployed contract's
/// command string and never appears in any recorded value.
type Provisioned { exit_code: Int, stdout: String }
/// The canonical agent-outcome record, as this document declares it: what
/// every dispatch of the `assistant` agent action returns. `text` is the whole accumulated output;
/// `final_message` is the last completed answer alone (both are honestly empty when the agent said
/// nothing, and equal for this harness's one-answer case); `stop_reason` is the canonical reason
/// (`end_turn` on every completed norn round — a non-completion fails the
/// dispatch instead of returning a record). The shape is demanded by the
/// checker on any `agent`-marked action; the NAME is this document's own.
/// `stop_reason` is available to route on; this session deliberately does not
/// — its routing behaviour predates the field and stands unchanged.
type Reply { text: String, final_message: String, stop_reason: String, session_id: String }
/// The one value the round loop threads, and the ONLY thing readable after
/// the loop: the prompt the next round will send, the last reply received,
/// whether the operator ended the session, and whether the wait loop gave up.
///
/// A binding made inside the nested wait loop is not guaranteed on every path
/// out of the outer loop, so the continuation itself cannot be read after the
/// loop — everything the outcomes need has to be folded into this value while
/// it is still in scope.
///
/// `stalled` is LOAD-BEARING, not bookkeeping. An AWL loop exits identically
/// whether `until` came true or `max` was reached — the lowering returns
/// `Ok(var)` on both paths (`crates/aion-awl/src/emitter/loops.rs:86-121`),
/// with no flag and no error. So a wait loop that exhausts its nudge budget
/// falls out holding a blank message, and WITHOUT this field the outer
/// `until` would be false and the session would dispatch another agent round
/// on an empty prompt, over and over, until the round budget was spent.
/// `stalled` is the only thing that makes the two exits distinguishable.
type Round { prompt: String, reply: String, ended: Bool, stalled: Bool }
/// The session's recorded result: how it ended, how many agent rounds ran, the
/// final agent reply, and the workspace path (which persists for inspection).
type Session { disposition: Disposition, rounds: Int, last_reply: String, workspace_path: String }
// The assistant's PRIVATE queue, named after the workflow itself. See the
// narration: `default` is the out-of-box workers' queue and this document
// never claims it, and the server refuses to load an embedded assistant whose
// declared queue is anything but its own workflow type.
worker assistant
// The launch lives IN the document (#204): `aion worker agent` has no flag
// for any of these settings, so without this section the verb the QUEUE MOVE
// warn points operators at refuses the document as unlaunchable — which is
// exactly what happened the night 0.14.0 shipped (#209). Four values are the
// deleted flags' own, carried verbatim from the invocation AGENT-WORKERS.md
// documented for THIS document before the migration: the norn harness, four
// activities at once, reconnect climbing 1s toward a 30s cap with a 100-drop
// budget, and the session pin that makes one workflow one continuous norn
// conversation ({workflow_id}/{activity_type} expand at spawn).
//
// The ENVIRONMENT is not parity — it is stricter, and deliberately so.
// Pre-migration the child inherited the worker's whole environment minus
// `--norn-unset-env OPENAI_API_KEY`; `env_pass` had no predecessor flag, and
// the child's environment is now CONSTRUCTED from this list and nothing
// else: exactly PATH and HOME. That subsumes the old unset (a variable not
// named here cannot reach the child at all). A box whose norn needs another
// variable adds it to this list before deploying.
//
// `--account default` is the estate's own default alias (the example
// worker's setup defaults NORN_ACCOUNT to the same value), and it rides
// with the session pin because norn refuses `--resume-if-exists` without an
// account. It is a baked value: an operator whose norn account alias is not
// `default` changes that one token before deploying — binding a harness
// ARGUMENT to a per-box or per-run value is the open half of board task #210,
// which this landing did not close.
//
// `cwd` is DECLARED, and it is the one setting a shipped document could not
// write until now. An agent's working directory decides which tree it reads
// and which files it edits; undeclared, it was inherited from wherever the
// worker process happened to be started, recorded in no document, argv or
// log. But no absolute literal is right on more than one machine, and this
// document ships inside the server binary — so it writes the placeholder
// `{workspace_root}` and the launching worker expands it against THAT box's
// own clones root (the startup banner's `workspace_root` field), then holds
// the result to the absolute rule before anything is spawned. A box that
// cannot resolve a root refuses the launch by name rather than inheriting a
// directory nobody chose.
//
// What that gives, exactly: the agent starts in the box's workspace ROOT,
// one level above the per-run workspaces the provision body materialises at
// `<root>/<run_id>/repo`. The harness section is per-worker, so it cannot
// name a per-run path — binding a setting to the run is the open half of
// board task #210, and until it lands the opening prompt is what walks the
// agent into its own session workspace.
//
// Still deliberately ABSENT: the old `--workspace-root`/`-C` confinement
// pair. Those are `args`, expanded by the ADAPTER at spawn time, and this
// landing gave placeholder expansion to path-valued SETTINGS only. So
// tool-level confinement is not expressible here yet, and served as shipped
// the agent's tools are NOT confined to the session workspace — the opening
// prompt instructs conduct that holds either way instead of asserting
// containment. An operator serving this on a real box may still add the pair
// by hand with their own root (args "--workspace-root", "<root>", "-C",
// "<root>/{workflow_id}/repo", …) before deploying.
harness
kind norn
concurrency 4
reconnect_initial_backoff 1s
reconnect_max_backoff 30s
reconnect_max_attempts 100
env_pass "PATH", "HOME"
args "--session-id", "{workflow_id}-{activity_type}", "--resume-if-exists", "--account", "default"
cwd "{workspace_root}"
/// Materialises the session workspace at `<workspace_root>/<run_id>/repo`
/// and prints that resolved path as its SOLE stdout — the recorded activity
/// result is where the workflow reads the workspace path from.
/// SERVER-RUN, NO SANDBOX: this declared body executes on the server host as
/// the server's own user, with env cleared to PATH only and cwd inherited
/// from the server.
///
/// THE SERVER STATES THE ROOT. `{workspace_root}` below is not a parameter —
/// it is a literal placeholder the server expands at dispatch into its own
/// aion home `clones/` directory (the startup banner's `workspace_root`
/// field). If that root cannot resolve to an absolute directory that exists,
/// the dispatch is refused terminally, by name; there is no fallback. The
/// empty-`$1` guard in the script is defence-in-depth against a defective
/// expansion, not a reachable path.
///
/// The action gets exactly one attempt: no `retry` policy is declared, so
/// a non-zero exit fails the dispatch and the failure reaches the operator
/// — nothing re-runs the command automatically. The collision discipline is
/// still load-bearing, for a different collision: a RE-RUN on a colliding
/// workflow id (a workflow started again under an id whose directory
/// survives) would find the earlier workspace in the way, and a clone into
/// an existing directory fails. A stale `<workspace_root>/<run_id>` is
/// therefore renamed aside to `<run_id>.superseded-<stamp>-<pid>` — never
/// deleted — and the claim proceeds fresh. Empty `repo_path` is the scratch
/// mode: the repo directory is created and `git init`ed instead of cloned.
///
/// `run_id` must be the workflow's own id: it keys the directory AND must
/// equal the id the agent-harness `--workspace-root` template expands to, or
/// the agent works somewhere other than where it was provisioned.
///
/// BYTE-IDENTICAL with `assistant_spike.awl` by requirement, not
/// coincidence: the two documents share queue `assistant`, and retained
/// contracts declaring DIFFERENT bodies for one action name refuse every
/// dispatch of it terminally.
action assistant_provision(run_id: String, repo_path: String) -> Provisioned
run "sh -c 'set -eu; case \"$2\" in \"\"|.|..|*/*) echo provision: run_id must be a single path component >&2; exit 9;; esac; if [ -z \"$1\" ]; then echo provision: workspace_root must not be empty >&2; exit 9; fi; mkdir -p \"$1\" >&2; if [ -e \"$1/$2\" ]; then mv \"$1/$2\" \"$1/$2.superseded-$(date +%Y%m%dT%H%M%S)-$$\" >&2; fi; mkdir \"$1/$2\" >&2; if [ -z \"$3\" ]; then mkdir -p \"$1/$2/repo\" >&2; git init \"$1/$2/repo\" >&2; else git clone \"$3\" \"$1/$2/repo\" >&2; fi; printf \"%s\" \"$1/$2/repo\"' -- {workspace_root} {{run_id}} {{repo_path}}"
/// One agent round under the canonical agent-outcome contract: ONE prompt
/// string in, the `Reply` record out (`text` + `stop_reason` +
/// `session_id`) — the shape
/// every harness emits from its boundary. NO timeout, by design. The worker
/// pins one norn session per action type per run, so repeated dispatches
/// resume one conversation.
///
/// The `agent` marker is what makes that routing readable from HERE. Before
/// it, the worker binary carried the same fact as a hand-written list of
/// action names, so this document could declare an agent round and the
/// worker had no way to know it had.
action assistant(prompt: String) -> Reply
agent
step provision
// Registered BEFORE the activity dispatch: provisioning clones a repository,
// so it is the first stretch of a run long enough for an operator to ask
// about, and zero rounds have run.
answer assistant_status(phase: "provisioning", round: 0)
assistant_provision(run_id: workflow.id, repo_path: repo_path) -> provisioned
// The RECORDED activity result is the one source of the workspace path: the
// body's sole stdout is the resolved `<workspace_root>/<run_id>/repo`, so
// durable history carries the real path and never the placeholder. Nothing
// in this document composes or re-derives it.
provisioned.stdout -> workspace_path
step situate after provision
// The one branch in the contract text: whether a repository was attached.
// Same test the body's script makes on `$3` — empty means scratch.
outcome scratch: if repo_path == "", route scratch_note
outcome attached: else, route attached_note
step scratch_note
// Both branch steps bind the SAME name and converge on `session`, so the
// binding is guaranteed on every path in and the checker accepts the read.
scratch_workspace + ground_truth_head + scratch_ground_truth -> situation
route session
step attached_note
attached_workspace_open + repo_path + attached_workspace_close + ground_truth_head + attached_ground_truth -> situation
route session
step session
// The round-one prompt: the full working contract with the workspace path
// and branch fragment spliced in, then the operator's objective under the
// heading `contract_tail` ends with. The seams are exact — see the const
// block's note on verbatim raw strings.
contract_head + workspace_path + workspace_confinement + situation + contract_tail + objective -> opening_prompt
// The outer loop is one agent round plus the operator wait. There is no
// round budget: the session runs until the operator ends it or a wait
// stalls out (both `max` clauses below are language formalities set beyond
// reach). The body runs at least once, so a session always gets one round.
//
// The inner loop is the operator wait, and it is where the nudge tolerance
// lives: it re-parks on `assistant_continue` until the operator either ends
// the session or sends a non-blank message, absorbing blank nudges without
// spending an agent round. AWL demands a bound on it, so the tolerance is
// finite where the Gleam version's was not.
//
// `stalled` folds the inner loop's give-up into the threaded value, and the
// outer `until` tests it. Both halves are required: an AWL loop cannot
// report WHY it exited, so without this the exhausted wait would fall
// through and the next iteration would dispatch an agent round on an empty
// prompt. See `Round`.
loop round = Round(prompt: opening_prompt, reply: "", ended: false, stalled: false) counting rounds
// Re-registered every pass, which is what makes the answer LIVE: the value
// is computed here, from the bindings in scope here, and replay re-runs the
// statement rather than evaluating anything at a yield point.
answer assistant_status(phase: "working")
assistant(prompt: round.prompt) -> reply
// The long park. A real session has sat on this signal for 28 days; this is
// the answer that says so.
answer assistant_status(phase: "awaiting_operator")
loop nudge = Continuation(message: "", end: false)
wait assistant_continue -> nudge
until nudge.end or nudge.message != ""
// The session has no operational cap (the operator's ruling: it runs
// until ended or stalled). AWL requires a literal bound on every loop,
// so this is the language formality set beyond reach, not a budget.
max 1000000
Round(prompt: nudge.message, reply: reply.text, ended: nudge.end, stalled: not nudge.end and nudge.message == "") -> round
until round.ended or round.stalled
// Same formality as the inner bound: no round budget exists — the session
// ends when the operator ends it or a wait stalls out.
max 1000000
// Past the loop the counter IS readable, so the final answer carries the
// real round count — the one point in the run where both fields are provable.
answer assistant_status(phase: "settled", round: rounds)
// Three ways out, all read off the threaded value — see `Round` for why the
// continuation itself is not readable here. Ordered deliberately: an
// operator who ends with a blank message sets BOTH `ended` and the blank
// condition, and their intent wins.
outcome operator_ended: if round.ended,
route ended(disposition: OperatorEnded, rounds: rounds, last_reply: round.reply,
workspace_path: workspace_path)
// The wait loop spent its nudge budget without the operator ever deciding,
// so the session ends here rather than dispatching another round.
outcome nudges_spent: if round.stalled,
route stalled(disposition: NudgeCapExhausted, rounds: rounds, last_reply: round.reply,
workspace_path: workspace_path)
// Formally required exhaustion arm: AWL demands a route for a loop reaching
// its `max`, but with the bound set beyond reach this is unreachable in
// practice. Kept as an OUTCOME, never an error.
outcome rounds_spent: else,
route capped(disposition: RoundCapExhausted, rounds: rounds, last_reply: round.reply,
workspace_path: workspace_path)