Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions document/en/api/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,11 @@ The request body is a `ChatRequest` (`src/backend/api/schemas.py`): `chat_id` an
| `run_started` | First frame of the stream | `run_id` (for resume/cancel), `message_id`, `chat_id` |
| `thinking` | Reasoning phase | `message` (phase hint) or `delta` (incremental thinking text) |
| `content` | Answer text delta | `event: "ai_message"`, `delta`, `chat_id` |
| `tool_call` | Agent invokes a tool | `tool_name`, `tool_display_name`, `tool_args`, `tool_id`, `subagent_name?` |
| `tool_call_start` | The model starts constructing a tool call | `tool_name`, `tool_display_name`, `tool_id` |
| `tool_call_delta` | Incremental tool-argument JSON (batched by the backend) | `tool_name`, `tool_id`, `arguments_delta` |
| `tool_call` | Arguments are complete and execution is about to start | `tool_name`, `tool_display_name`, `tool_args`, `tool_id`, `subagent_name?` |
| `tool_result` | Tool returns | `tool_name`, `result` (JSON), `tool_id`, `citations` (citation items) |
| `tool_pending` | Model is buffering tool args / between call start and args | `reason` (e.g. `tool_call_start` / `llm_buffering`) |
| `tool_pending` | Waiting fallback when the provider exposes no parseable deltas | `reason` (e.g. `llm_buffering`) |
| `file_confirm` | A tool is suspended awaiting user confirmation of a "My Space" write | `confirm_id`, `op`, `logical_path`, `message`, `expired`; the stream stays open — the user confirms out-of-band via `POST /v1/chats/{chat_id}/file-confirm` and the tool resumes |
| `batch_confirm` | A batch-execution plan awaits user confirmation | `plan_id`, `total`, `preview`, `default_template`, `placeholder_keys`; confirm via `POST /v1/batch/{plan_id}/confirm` |
| `meta` | Final wrap-up frame of an answer | `route`, `sources`, `artifacts`, `citations`, `warnings`, `is_markdown`, `message_id`, `workspace_files` |
Expand All @@ -124,6 +126,12 @@ data: {"type": "run_started", "run_id": "run_9f8e7d", "message_id": "msg_001", "

data: {"type": "thinking", "message": "Analyzing your question...", "chat_id": "chat_abc123"}

data: {"type": "tool_call_start", "tool_name": "internet_search", "tool_display_name": "Web Search", "tool_id": "call_01", "chat_id": "chat_abc123"}

data: {"type": "tool_call_delta", "tool_name": "internet_search", "tool_id": "call_01", "arguments_delta": "{\"query\":\"Beijing ", "chat_id": "chat_abc123"}

data: {"type": "tool_call_delta", "tool_name": "internet_search", "tool_id": "call_01", "arguments_delta": "weather today\"}", "chat_id": "chat_abc123"}

data: {"type": "tool_call", "tool_name": "internet_search", "tool_display_name": "Web Search", "tool_args": {"query": "Beijing weather today"}, "tool_id": "call_01", "chat_id": "chat_abc123"}

data: {"type": "tool_result", "tool_name": "internet_search", "result": {"result": {"query": "Beijing weather today"}}, "tool_id": "call_01", "citations": [{"id": "internet_search-1", "title": "..."}], "chat_id": "chat_abc123"}
Expand Down
4 changes: 2 additions & 2 deletions document/en/architecture/frontend.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ SSE streams bypass the JSON channel of `api.ts`; they are consumed directly from

| Hook | Responsibility |
|---|---|
| `useStreaming` | The main SSE consumer: exposes `send` / `abort` / `regenerate` / `editAndResend` / `resumeRunIfAny`; parses `content/thinking/tool_call/tool_result/tool_progress/meta/error` events, maintains text segments and the tool timeline, supports run resumption |
| `useStreaming` | The main SSE consumer: exposes `send` / `abort` / `regenerate` / `editAndResend` / `resumeRunIfAny`; parses `content/thinking/tool_call_start/tool_call_delta/tool_call/tool_result/tool_progress/meta/error` events, maintains text segments and the tool timeline, supports run resumption |
| `useChatActions` | Session-management actions: create / delete / rename / pin & favorite / export / share / summary & classification |
| `useChatInit` | Session initialization and active-run recovery on app start |
| `usePlanMode` | Plan-mode SSE consumer (shared by first execution and resume) |
Expand Down Expand Up @@ -121,7 +121,7 @@ useStreaming.send
│ 2. fetch POST /v1/chats/stream (attachments carry only file_id/name/mime_type)
│ parses data: {json} line by line
│ ├─ content/thinking → appended into segments (utils/segments.ts)
│ ├─ tool_call/tool_result → tool timeline (components/tool/)
│ ├─ tool_call_start/tool_call_delta/tool_call/tool_result → update the tool timeline in place by tool_id (components/tool/)
│ ├─ file_confirm/batch_confirm → confirm bar / modal (hard pause)
│ └─ meta → citation sources and artifact list written onto the message
Expand Down
6 changes: 4 additions & 2 deletions document/en/architecture/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,14 +174,16 @@ Each event is `data: {json}\n\n` with a `type` discriminator; the stream termina
|---|---|---|
| `content` (`event: ai_message`) | Body text delta | `chats.py::_stream_sse_response` |
| `thinking` | Extended-thinking delta | `core/chat/tool_log.py::build_thinking_event` |
| `tool_call` | Tool invocation started (name + args) | `core/chat/tool_log.py` |
| `tool_call_start` | Tool invocation starts (stable ID + name) | `core/chat/tool_log.py` |
| `tool_call_delta` | Batched incremental tool-argument JSON | `core/chat/tool_log.py` |
| `tool_call` | Tool arguments are complete and execution is about to start | `core/chat/tool_log.py` |
| `tool_result` | Tool execution result (with citations, artifact card payloads) | `core/chat/tool_log.py` + `orchestration/tool_payloads.py` |
| `tool_progress` | Progress reports from long-running tools | `chats.py` |
| `batch_confirm` / `file_confirm` | Batch-execution confirmation; MySpace write confirmation (hard-pause gate) | `chats.py` |
| `meta` | Trailing metadata: route, citation sources, artifact list, etc. | `orchestration/workflow.py` |
| `error` | Error event (immediately followed by `[DONE]`) | `chats.py` |

The frontend's `src/frontend/src/hooks/useStreaming.ts` dispatches on `type` and renders text, the tool timeline, and citations incrementally into the message bubble.
The frontend's `src/frontend/src/hooks/useStreaming.ts` dispatches on `type` and merges the start, deltas, completed call, and result in place into one tool card keyed by `tool_id`.

## Container Topology

Expand Down
88 changes: 46 additions & 42 deletions document/en/modules/autonomous-loop.md
Original file line number Diff line number Diff line change
@@ -1,73 +1,77 @@
# Autonomous Loop

> Last updated: 2026-07-10
> Last updated: 2026-08-13

The Autonomous Loop upgrades the agent from single-turn Q&A to a **long-running task that self-advances across many calls, maintains external state, and stops autonomously on a verifiable goal**. Alongside regular chat (single-turn) and plan mode (linear multi-step), it provides a third run mode: a run-level self-driving loop.
The autonomous loop upgrades the agent from single-turn Q&A to **long-running tasks that drive themselves forward across many invocations, keep state externally, and stop on verifiable goal completion**. Alongside normal chat (one-shot) and plan mode (linear multi-step), it is a third execution form: a run-level self-driving loop. The design tracks Codex `/goal` (the Ralph Loop): the goal stays alive across turns and the loop runs until the job is done, while strictly keeping maker ≠ checker — the agent doing the work is never the one grading it.

## Core loop

```
read state (persistent sandbox files) → agent runs one iteration (fresh context, same persistent sandbox)
→ environment verification (verify_cmd) → evaluator verdict → feedback + compaction handoff → next iteration
Scout (read-only workspace survey) → Plan (requirement ledger, optional check commands) → per iteration:
worker runs one round (fresh context, same persistent sandbox, exactly one requirement)
→ machine check (driver itself runs check_cmd; exit code 0 = objectively met)
→ read-only reviewer personally verifies the real output (never trusts self-reports)
→ flip / feed back / stall bookkeeping
→ replan the remaining work when a requirement gets shelved → done when all pass
```

Each iteration gets a fresh context (avoiding long-session degradation); work artifacts and progress live in files in the persistent sandbox (`PROGRESS.md` / `state.json` / `handoffs.md`) — state lives on disk, not in the context window.
- **Recon-grounded planning**: before work starts, a read-only scout `ls`/`read`/`grep`s the project or /workspace to establish ground truth (what exists, what's missing, what the pitfalls are); the planner decomposes the goal against that survey instead of guessing from the goal text. Task-style loops with an empty workspace skip the scout automatically.
- **Requirement ledger (feature_list.json)**: owned exclusively by the driver; the worker cannot add, delete, or edit entries and is fed exactly one requirement per iteration. Simple goals may decompose into just 1–2 requirements; complex ones cap at 8.
- **Hybrid acceptance**: requirements that can be judged objectively carry a read-only `check_cmd` (e.g. `test -f`, `grep -c`, word-count reconciliation) which the **driver itself executes in the sandbox** — the worker cannot cheat it. A failing check feeds the command output straight into the next round without burning a reviewer run. Semantics and quality are always verified by the **read-only reviewer subagent** opening the real files.
- **Fewer second passes**: a requirement whose machine check passed flips on a single semantic review; purely semantic requirements — and the final requirement whose flip completes the loop — still require an independent second-pass re-check (guards against premature completion).
- **State lives on disk**: every iteration starts with a fresh context (avoiding long-session degradation); artifacts and progress live in the persistent sandbox (`feature_list.json` / `PROGRESS.md` / `handoffs.md`), with a DB mirror of the ledger as the source of truth across restarts and machine changes.
- **Mid-run replanning**: when a requirement stalls for several rounds and gets shelved, the planner re-decomposes the *remaining* work (passed items are immutable) instead of shelving requirements one by one into a "partially done" ending. Replans are capped (`LOOP_MAX_REPLANS`, default 2).

## Two ways to start
## Stopping: run until done — budgets are optional

| Form | Entry | Goal & verification |
|---|---|---|
| **Conversational** (`self_verify`) | The "Autonomous Loop" toggle in the chat composer (next to "Plan Mode") | Just describe the goal in natural language — no verification config to fill in. Iterations stream back into the current conversation as normal assistant messages (a markdown transcript). |
| **Form** (`verify`) | Lab module → Autonomous Loop | Explicitly fill in `verify_cmd` / target score / budget; suited to cases with a known verification command (e.g. EdgeBench evaluation). |

**Conversational evaluation auto-picks one of two paths** (the worker chooses each round by the nature of the goal; the evaluator routes accordingly):

- **Quantifiable goal** (cost / error / pass-rate…) → the worker **writes its own** `/workspace/verify.sh`; the driver runs it independently for **rule-based scoring** (ground truth, worker self-reports not trusted). Without an explicit threshold, completion comes from **stagnation convergence**: once a valid solution exists and several consecutive rounds show no meaningful gain, it is judged done — ensuring genuine iterate-and-improve rather than exiting on the first valid solution.
- **Qualitative goal** (copy / design / polish…) → the worker produces the deliverable + an evidence note and **writes no script**; at startup one LLM call decomposes the goal into acceptance criteria, then an **independent LLM evaluator** (invoked by the driver each round, never callable by the worker — avoiding self-evaluation bias) checks the criteria one by one to emit `done / continue`.
The primary stop condition is **all ledger requirements passing**. Budgets (iterations / wall clock / tokens) default to **unlimited** (`0` or omitted = no cap); explicit positive values still apply. The anti-runaway guards are independent of budgets and always active:

## Exit decision: environment ground truth first
1. **Stall guard**: a requirement with N consecutive rounds (orchestration profile `max_attempts_per_requirement`, default 6) without reviewer-affirmed material progress gets shelved (triggering replan / HITL). The reviewer explicitly judges `progress` each round, so healthy multi-round work (e.g. writing a long document chapter by chapter) is not penalized.
2. **Failure circuit breaker**: a worker round that raises or produces nothing is treated as an environment outage — not counted as an attempt, retried after a 30s backoff; only `LOOP_MAX_CONSECUTIVE_INFRA` (default 6) consecutive such rounds trip the loop to `failed`, resumable once the environment recovers.
3. **Hard backstop**: `LOOP_HARD_MAX_ITERS` (default 500) — far beyond any real task, purely an infinite-loop fuse.

Reliability order of stop conditions:
The final flip always passes an **independent second-pass review**, and partially-completed endings automatically run one **wrap-up delivery round** (`LOOP_WRAPUP`, default on): consolidate what passed into a usable deliverable and honestly list what remains.

1. **Environment verification (primary)**: `goal_spec.verify_cmd` runs in the persistent sandbox; success is decided by exit code + output (tests pass / metric met / target file exists). In conversational mode this script is authored by the worker and run independently by the driver. Anything the environment can verify deterministically does not go through the LLM.
2. **Evaluator (fallback)**: when the goal cannot be fully verified by command, an independent evaluator reads the environment evidence and checks acceptance criteria one by one, emitting a binary verdict (done / continue / off_track). The evaluator is separate from the worker and invoked deterministically by the loop driver each round, avoiding self-evaluation bias.
3. **Budget backstop**: max iterations / wall-clock / cumulative tokens — stops on breach (the guardrail for unattended runs).
## Terminal states

On a `done` verdict, a **second verification** (re-running verify) prevents false-positive early delivery.
| State | Meaning |
|---|---|
| `completed` | All ledger requirements passed (including the final second-pass check) |
| `budget_exhausted` | Partially done: requirements shelved with no replan left, or an explicit budget / hard backstop hit |
| `cancelled` | Cancelled by the user |
| `awaiting_human` | HITL enabled and the reviewer requested human input |
| `interrupted` | Interrupted by a service restart (reconciled at startup); resumable from the checkpoint |
| `failed` | Tripped by consecutive infrastructure failures (resumable after recovery) |

## Termination outcomes
## Long-run reliability (harness)

| Terminal state | Meaning |
|---|---|
| `completed` | Environment verification passed (with second check) |
| `budget_exhausted` | Hit iteration / wall-clock / token budget |
| `cancelled` | User cancelled |
| `awaiting_human` | With HITL enabled, evaluator requested a human (optional) |
- **No age-based kill**: autonomous-loop runs are exempt from the platform-wide hard run-age cap — a loop still making healthy progress in-process is never reaped for merely running long; orphaned runs are still cleaned up by the stream-quiet rule.
- **Checkpoint resume**: dual-source ledger (DB mirror + sandbox); at startup, orphaned `running` loops are reconciled to `interrupted` (or auto-resumed with `LOOP_AUTO_RESUME=true`).
- **Resume keeps parameters**: the model, reviewer model, worker iteration cap, and thinking level persist with the loop — resuming never silently downgrades to defaults.
- **Mid-run steering**: add an instruction from the plan bar without cancelling; the driver picks it up before the next iteration and injects it into the worker prompt at top priority.

## Capabilities
## Model configuration

- **Dynamic todos**: the agent maintains an editable todo list in `state.json` each round, checking items off.
- **Self-correction**: several rounds without meaningful score improvement automatically prompts "try a fundamentally different strategy".
- **Crash resume**: after a restart, the persistent sandbox files remain, so the loop resumes from `state.json` (`LOOP_AUTO_RESUME` enabled).
- **Scheduled advancement**: scheduled tasks support a `loop` type that advances the same persistent loop on a cron cycle (rather than creating a new stateless task each time).
- **Human checkpoints (HITL)**: default is fully automatic ("record and continue"); optionally enabled per-loop to pause at key points and `/resume` after human approval.
- **Worker model**: follows the model the user selected in the conversation (same source as normal chat).
- **Reviewer/planner model**: configured independently in the admin console under **Model Management → Role Assignment → Autonomous-loop review & planning** (`loop_reviewer`); falls back to the main agent model when unassigned. The scout, decomposition, reviews, second passes, and verdict rescue all share this role.

## API

| Method | Path | Description |
|---|---|---|
| POST | `/v1/loops` | Create a loop (`goal_spec` + `budget`) |
| POST | `/v1/loops` | Create a loop (`goal_spec`; `budget` optional = unlimited) |
| GET | `/v1/loops` / `/v1/loops/{id}` | List / detail |
| POST | `/v1/loops/{id}/start` | Start (SSE streaming) |
| POST | `/v1/loops/{id}/resume` | Resume (after HITL approval / crash) |
| POST | `/v1/loops/{id}/cancel` | Cancel |
| POST | `/v1/loops/{id}/start` | Start (SSE stream) |
| POST | `/v1/loops/{id}/resume` | Resume (after HITL approval / interruption; omitted params are restored from the last start) |
| POST | `/v1/loops/{id}/steer` | Queue a mid-run instruction (applies before the next iteration) |
| POST | `/v1/loops/{id}/cancel` | Cancel (reconciles the status directly when no run is active) |
| GET | `/v1/loops/{id}/iterations` | Iteration audit trail |

Frontend entry: the **"Autonomous Loop" toggle in the chat composer** (conversational mode, `components/chat/InputArea.tsx` + `hooks/useLoopMode.ts`) or **Lab module → Autonomous Loop** (form mode, `components/lab/LabPanel.tsx`). Access is gated by the `can_run_autonomous_loop` capability (enabled by default, can be disabled per user / team).
Frontend entry: the **"Autonomous loop" toggle in the chat input** (`components/chat/InputArea.tsx` + `hooks/useLoopMode.ts`); the plan bar (`components/loop/LoopPlanBar.tsx`) shows the live requirement checklist and review state, with "Resume" and mid-run steering built in. Gated by the `can_run_autonomous_loop` capability (on by default; can be disabled per user / team).

## Related code

- Driver `orchestration/autonomous_loop.py`, evaluator `orchestration/loop_evaluator.py`
- ChatRun integration `orchestration/chat_run_executor.py` (`start_autonomous_loop_run`)
- Service `core/services/loop_service.py`, API `api/routes/v1/loops.py`
- Driver `orchestration/autonomous_loop.py`, planner `orchestration/loop_planner.py`, reviewer `orchestration/subagents/loop_reviewer.py`
- ChatRun integration `orchestration/chat_run_executor.py` (`start_autonomous_loop_run` / startup reconciliation `resume_running_loops`)
- Service `core/services/loop_service.py` (ledger mirror / start params / steering queue), API `api/routes/v1/loops.py`
- Tables `agent_loops` / `loop_iterations`
Loading
Loading