Goals and Continuations

Corresponding source

SubjectSource
Goal metadata parsing and model-visible statenanobot/session/goal_state.py
Continuation decisions and save boundarynanobot/session/turn_continuation.py::maybe_continue_turn, prepare_save_boundary
Goal Toolsnanobot/agent/tools/long_task.py::LongTaskTool, CompleteGoalTool
Runner Goal-continuation mechanismnanobot/agent/runner.py::AgentRunner._try_drain_injections
Loop integration pointsnanobot/agent/loop.py::AgentLoop._state_run, _state_save

Learning objectives

After this lesson, you should be able to:

  • state where a sustained Goal is stored;
  • explain the responsibilities of long_task and complete_goal;
  • describe internal continuation after an iteration budget is exhausted;
  • distinguish Goal, Cron, and Subagent.

Prerequisites

  • Understand Session metadata.
  • Understand AgentRunner maximum iterations.
  • Understand the pending queue.

1. Goal is Session metadata

Current key:

goal_state

An older Session may use the compatibility key:

thread_goal

Reads accept the old key, while new writes migrate state to goal_state.

Typical Goal fields include:

  • objective;
  • status;
  • UI summary;
  • creation and update information written by Tools.

A Goal is not a separate database or an independent Agent process.

2. Two Tools

long_task

Primary responsibilities:

  • create or update the active Goal;
  • write Session metadata;
  • clear the legacy key;
  • save the Session;
  • publish GoalStateChanged.

complete_goal

Primary responsibilities:

  • mark the active Goal complete;
  • update metadata;
  • save;
  • publish the state change.

Implementation:

  • nanobot/agent/tools/long_task.py

3. How a Goal remains in context

goal_state_runtime_lines() converts the active objective into Runtime Context lines:

Goal (active):
<objective>
Summary: ...

Even if older conversation messages are compacted, Session metadata projects the Goal into every Turn’s context again.

This is more durable than recording the objective only in one user message.

Goal metadata still needs boundaries

The objective has a length limit, and the WebSocket snapshot uses a shorter presentation limit. Runtime context and UI are not complete metadata dumps.

4. LLM wall timeout for a sustained Goal

runner_wall_llm_timeout_s() returns 0.0 for a sustained-Goal Turn, disabling Runner’s ordinary outer asyncio.wait_for wall timeout. It returns None for other Turns, preserving the environment-based default policy.

This does not mean:

  • the Provider can never time out;
  • stream idle timeout is disabled;
  • Tools have no timeouts;
  • a Goal can run forever.

It changes only Runner’s wall-clock cap around one LLM request.

5. Why Continuation exists

One AgentRunner remains bounded by max_iterations. If an active Goal is unfinished when the current budget ends, immediately returning “limit reached” would interrupt long-running work.

turn_continuation.maybe_continue_turn():

  1. verifies stop_reason == "max_iterations";
  2. verifies that a pending queue is available;
  3. verifies that the Goal remains active;
  4. verifies that the continuation-round limit is not exhausted;
  5. creates internal continuation metadata;
  6. removes the synthetic terminal assistant message;
  7. suppresses the current user-visible response;
  8. puts a new internal InboundMessage into the pending queue.
flowchart TD
    R["Runner: max_iterations"] --> A{"Goal active?"}
    A -->|No| FINAL["Normal limit handling"]
    A -->|Yes| Q{"Pending queue available<br/>and rounds remain?"}
    Q -->|No| FINAL
    Q -->|Yes| I["Create internal Continuation"]
    I --> S["Save current slice without responding"]
    S --> N["Next slice continues the same Session"]
    N --> R

6. A Continuation is not a new user message

The internal message carries:

  • _internal_continuation;
  • continuation kind;
  • original visible-run start time;
  • metadata that skips or controls user persistence;
  • session_key_override.

The persistence boundary prevents the internal continuation prompt from being repeatedly saved as ordinary user input.

7. Maximum continuation rounds

Current internal constant:

_MAX_GOAL_CONTINUATION_ROUNDS = 12

It limits how many automatic slices one sustained Goal can run after repeatedly exhausting each Runner iteration budget.

Once the limit is reached, the system must still produce an explicit user-visible outcome rather than running silently forever.

8. Goal, Cron, and Subagent

MechanismCore formBackground?Persistent?
GoalSession metadataNo; still fundamentally a TurnYes
Continuationinternal message in pending queuecontinues within the current SessionIndirectly
Cronpersistent schedule/jobYesYes
Subagentbackground AgentRunner TaskYestask state is mainly in memory; result returns to parent Session

Observation task

  1. Trigger /goal or long_task in an isolated Session.
  2. Inspect goal_state in Session metadata.
  3. Find the Goal in ContextBuilder runtime lines.
  4. Observe whether maximum iterations produce a Continuation.
  5. Call complete_goal and verify metadata and runtime-event changes.
  6. Do not weaken safety limits merely to manufacture a long task.

Source and tests

  • nanobot/session/goal_state.py
  • nanobot/session/turn_continuation.py
  • nanobot/agent/tools/long_task.py
  • nanobot/agent/loop.py::_state_run
  • tests/agent/tools/test_long_task.py
  • tests/agent/test_runner_goal_continue.py
  • tests/agent/test_loop_goal_wall_timeout.py

Self-check

  1. Why store a Goal in Session metadata instead of only in history?
  2. Which timeout does a sustained Goal disable, and which timeouts remain?
  3. Why does Continuation use the pending queue?
  4. Why should the internal continuation prompt not persist as an ordinary user message?
  5. Why are Goal and Subagent not interchangeable concepts?

Next step

Read Cron and Heartbeat.