# Goal mode: loop Tasks on one Session until an objective is done

A normal Task ends when the model stops calling tools and replies — fine for a request, wrong for an objective. Goal mode inverts the contract: the user states an objective and the system keeps driving Tasks on the same Session, re-injecting the objective every round, until the goal reaches a terminal state. Going quiet no longer ends the work; the model has to claim completion through a protocol, and everything else loops.

## One API: `session.run(input, { goal })`

Goal mode is an option of the SDK's one entry point, not a separate driver: `session.run(input, { goal: { budget } })` treats the input's text as the objective, loops rounds inside the call, and ends the stream with exactly one `goal_finished` event message carrying the outcome (also written to the Trace). Hosts consume the same message stream they already consume for a plain Task; round boundaries are the injected round inputs, recognizable via `isGoalRoundInput`.

## The protocol

The loop creates `scratchpad/<session_id>/GOAL.yaml` (sibling of the PLAN.md convention) — two fields, written by the system exactly once, at creation: `objective` (the canonical value lives in the loop's memory and is re-stated in every round's block) and `status`, the model's mailbox back to the loop (only to `complete` or `blocked`, read after every round). System-side endings exist only as the `goal_finished` outcome and in server state; the file always keeps the model's own last write — the resume point an interrupted goal wants. Reads are tolerant: a missing file, broken YAML or an out-of-protocol status normalizes to `blocked`, stopping the loop instead of spinning it.

Each round's user message is a `[goal]` protocol block (same square-bracket marker family as `[use_skills]` / `[user_steering]`) followed by a plain body: round 1 carries the caller's message verbatim — skill-invocation blocks and all — and later rounds re-inject the objective (the round-1 text with leading marker blocks stripped; the same derivation is recorded in GOAL.yaml). The block embeds the GOAL.yaml content (composed from the same values the file was created with), so the model sees exactly the file it is asked to edit, and carries the current budget numbers on its own line. Because the embedded `objective` is user data, the `[goal]` closing tag is matched line-anchored: YAML serialization keeps string content off column 0 (block scalars indent, single-line values stay mid-line), so a crafted objective containing `[/goal]` cannot terminate the block early anywhere it is parsed (frontend collapse, title stripping).

The block also carries the working rules: a completion self-check (prompt-level — the system does not verify a claimed `complete`), no shrinking the objective to an easier subset, progress recorded in PLAN.md to survive compaction, and a blocked rule — the same blocking condition must persist for three consecutive rounds before the model may claim `blocked`, so a transient obstacle doesn't end the goal. A round that ends in an abort — or that the engine cut off at the per-Task `max_turns` cap — stops the whole loop without re-firing, leaving the on-disk goal `active` as a clean resume point; a hard 100-round backstop bounds a goal whose model never writes the file at all.

## Token budget

Accounting is incremental — uncached input + output (`request.total − cache_read`) summed across every request, including `run_subagent` child sessions; a spend estimate rather than a bill (cache reads cost a small fraction of uncached input and are left out). The budget (optional; `-1` = unlimited, the default) is checked between rounds; exhaustion triggers one final wrap-up round (summarize, list remaining work, no courtesy `complete`) before the system ends the goal as `budget_limited`.

## Surfaces

- **CLI chat**: `/goal[:<budget>] <objective>` (`500k` / `2m` suffixes); Ctrl-C aborts the whole loop, per-round stats lines keep the normal chat rhythm.
- **CLI run**: `--goal [budget]` turns `-m` into the objective; only a completed goal exits 0.
- **Server**: `POST /api/sessions/:id/tasks` accepts `goal: { budget }` (a `thinkingLevel` on the same request rides every round); the whole goal runs as one `running` span (the existing abort endpoint and schedule queueing behave correctly for free), progress lands in a new `goal_state` table and streams as `goal_started` / `goal_round` / `goal_finished` SSE events, and `GET /api/sessions/:id/goal` restores the latest run.
- **Web App**: a new "+" extension menu on the composer (a general-purpose entry point for input add-ons; goal mode is its first item, also reachable via `/goal` in the slash menu) engages a goal chip with an inline budget field; skills selected in the composer prefix the round-1 message as a `[use_skills]` block exactly like a normal send; every round renders as a regular user bubble with a "Goal · round N" notice beneath it, and a live banner above the composer tracks rounds, tokens and the terminal state.

Docs: a new "Goal Mode" guide page (zh/en).
