Mission loop (technical reference)

Operator-facing concepts, use cases, and design patterns: Mission Control.

This page is the implementation reference for loop missions in agency-django.

Mode

Derived from loop_prompt only — no mode field:

One shot Loop

loop_prompt is NULL loop_prompt has text

Runtime flow

dispatch → Execution + Thread
     → start_mission (loop) → bootstrap_loop → run_loop_iteration(1)
          → ThreadRunner.run(prompt)
          → [HITL? → interrupted, stop scheduling]
          → evaluate_loop → LoopDecision
          → complete | wait | blocked | failed | continue(+schedule)

Each iteration = one agency.run_loop_iteration Celery task. Never a long while holding a worker.

continue enqueues iteration N+1 with optional countdown=loop_delay_seconds.

Mission fields

loop_prompt

Supervisor template. Safe placeholders: {iteration}, {max_iterations}, {remaining_iterations}, {mission_name}, {last_summary}.

max_iterations

1–100 (default 10). Exhaustion → limit_reached, not success.

loop_delay_seconds

0–86400. Celery ETA/countdown between iterations.

is_loop (read-only API)

loop_prompt is not NULL.

Execution fields (snapshot at dispatch)

iteration, max_iterations, loop_status, loop_decision, next_run_at, metadata.loop_config.

Editing the Mission during an active Execution does not change the snapshot.

LoopDecision

Pydantic model in agency.mission.loop:

{
  "status": "continue",
  "summary": "Short state for UI and logs.",
  "reason": "Why this status.",
  "next_input": "Concrete next user message (continue only).",
  "wait": null,
  "required_input": null
}

Obtained via with_structured_output(LoopDecision) — not keyword parsing. One repair retry; then failed.

follow_up is passed to ThreadRunner.continue_turn() — it appears as a user message in the Thread. Never echo JSON field names as the message body.

Evaluation context

evaluate_loop_decision() sends the mission objective, iteration counters, formatted loop_prompt, and a conversation transcript built by agency.mission.context.build_thread_context:

  • Primary source: Message rows linked to the execution thread (checkpoint persistence).

  • Fallback: execution.result["messages"] when DB rows are not yet available.

The transcript is capped (message count and character budget) so evaluator calls stay bounded. Tool lines include a short result snippet when present.

LoopDecision rejects follow_up values that are placeholder tokens (next_input, follow_up, continue, etc.) — the supervisor must return a concrete instruction in natural language.

Celery tasks

agency.start_mission

Oneshot → Agent.execute(); loop → bootstrap_loop + iteration 1.

agency.run_loop_iteration(execution_id, expected_iteration)

Idempotent claim on (execution_id, iteration).

agency.resume_mission

After HITL; re-enters loop evaluation when metadata.loop_config.is_loop.

WebSocket events

mission_loop_started, mission_iteration_started, mission_iteration_completed, mission_loop_waiting, mission_loop_blocked, mission_loop_completed, mission_loop_limit_reached, mission_loop_failed.

Small payloads only — no full history. Emitted after transaction.on_commit.

UI

Missions explorer: tabs Prompt / Loop. Execution view: loop N/M, decision summary, waiting vs HITL interrupted.

Deferred

  • Evaluation with thread / checkpoint / log snapshot — transcript injection ✅; richer tool/log tail deferred

  • Watch — tail logs or events → resume waiting executions

  • Mission cron (Celery Beat dispatch)

  • agency.display — visual confirm cards

  • Secure cancel API

See also: Create and execute a Mission. Closed backlog note: todo/history/2026-08.md.