diff --git a/docs.json b/docs.json index 5fcde45e4..d21a6b2a5 100644 --- a/docs.json +++ b/docs.json @@ -2572,6 +2572,18 @@ "run-cancellation/workflow-cancel-run" ] }, + { + "group": "Run Control", + "pages": [ + "run-control/overview", + "run-control/checkpointing", + "run-control/continue-run", + "run-control/regenerate", + "run-control/fork-run", + "run-control/branch-session", + "run-control/api-reference" + ] + }, { "group": "Background Execution", "pages": [ diff --git a/run-control/api-reference.mdx b/run-control/api-reference.mdx new file mode 100644 index 000000000..956124ec9 --- /dev/null +++ b/run-control/api-reference.mdx @@ -0,0 +1,213 @@ +--- +title: API Reference +description: HTTP endpoints for continuing, forking, branching, and inspecting runs. +--- + +All run-control verbs ship as both SDK methods and HTTP endpoints. The HTTP layer is what AgentOS exposes for the FE; the SDK is what you call from your own Python code. + +## Endpoint Summary + +| Endpoint | Method | What it does | +|---|---|---| +| `/agents/{agent_id}/runs/{run_id}/continue` | POST | Resume, regenerate, fork, or follow up on a run | +| `/agents/{agent_id}/runs/{run_id}/checkpoints` | GET | List checkpoint boundaries for a run | +| `/agents/{agent_id}/runs/{run_id}/checkpoints/{message_index}` | GET | Snapshot of the run truncated at a boundary | +| `/agents/{agent_id}/sessions/{session_id}/branch` | POST | Deep-copy a session into a new one | + +Team variants exist at `/teams/{team_id}/...` with identical shapes. + +## POST `/continue` + +Form-encoded body. All fields optional. + +| Field | Type | Purpose | +|---|---|---| +| `session_id` | string | The session that owns the run (required when the run isn't already loaded server-side) | +| `requirements` | JSON string | HITL tool results to apply before resuming | +| `input` | string | New user message to append before the next turn | +| `continue_from` | string | `"end"`, `"last_user"`, or a numeric message index | +| `fork` | bool | Clone with a new `run_id` | +| `regenerate` | bool | Drop the last response and redo (always forks) | +| `replace_original` | bool | Hide the source from history when regenerating (default `true`) | +| `additional_instructions` | string | Steering text appended as a user message when regenerating | +| `stream` | bool | Stream events back as SSE (default `false`) | +| `background` | bool | Run in a detached task, return immediately (default `false`) | + +### Example: HITL resolve + +```bash +curl -X POST "$HOST/agents/{agent_id}/runs/{run_id}/continue" \ + -F "session_id=sess-xyz" \ + -F 'requirements=[{"tool_execution":{"tool_call_id":"tc1","result":"approved"}}]' +``` + +### Example: Regenerate + +```bash +curl -X POST "$HOST/agents/{agent_id}/runs/{run_id}/continue" \ + -F "session_id=sess-xyz" \ + -F "regenerate=true" \ + -F "additional_instructions=Be more concise" +``` + +### Example: Fork at boundary + +```bash +curl -X POST "$HOST/agents/{agent_id}/runs/{run_id}/continue" \ + -F "session_id=sess-xyz" \ + -F "continue_from=4" \ + -F "fork=true" \ + -F "input=Try a different approach" +``` + +### Response + +The new `RunOutput` (or the resumed one if same `run_id`). Lineage fields populated when applicable: + +| Field | When it's set | +|---|---| +| `forked_from_run_id` | Set on any forked run (regenerate, fork, branched session runs) | +| `forked_from_message_index` | The actual boundary used (may be lower than requested if snapped) | +| `forked_from_session_id` | Set on runs that came from `branch_session` | +| `regenerated_from` | Set when `regenerate=True` | +| `last_checkpoint_at_message_index` | Latest mid-run checkpoint position on `checkpoint="tool-batch"` runs | + +## GET `/checkpoints` + +List the message boundaries the FE can offer as resume points. + +```bash +curl "$HOST/agents/{agent_id}/runs/{run_id}/checkpoints?session_id=sess-xyz" +``` + +### Response + +```json +{ + "run_id": "run-abc", + "session_id": "sess-xyz", + "checkpoints": [ + { + "checkpoint_id": "1", + "run_id": "run-abc", + "session_id": "sess-xyz", + "message_index": 5, + "continue_from": 5, + "status": "RUNNING", + "reason": "checkpoint", + "created_at": 1781700829, + "message_id": "msg-uuid-1", + "message_role": "tool", + "message_preview": "15.3M", + "is_latest": false + }, + { + "checkpoint_id": "2", + "run_id": "run-abc", + "session_id": "sess-xyz", + "message_index": 6, + "continue_from": 6, + "status": "COMPLETED", + "reason": "end", + "created_at": 1781700835, + "message_id": "msg-uuid-2", + "message_role": "assistant", + "message_preview": "Lagos is largest, then Tokyo...", + "is_latest": true + } + ] +} +``` + +### Fields + +| Field | Purpose | +|---|---| +| `checkpoint_id` | 1-based display ordinal (Checkpoint 1, 2, 3, ...). Use for UI labels. | +| `message_index` | Pass back as `continue_from` to resume from this boundary. Pair-safe. | +| `message_id` | Stable UUID of the message at the boundary. Use for client-side state. | +| `message_role` | `"user"`, `"assistant"`, or `"tool"`. | +| `message_preview` | First 120 chars of the boundary message's content. | +| `reason` | `"checkpoint"` (mid-run barrier) or `"end"` (terminal). | +| `status` | Run status at the time of the checkpoint. | +| `is_latest` | `true` for the terminal end-of-transcript entry. | + +Every `message_index` returned is pair-safe. Passing it back as `continue_from` will never get snapped. + +## GET `/checkpoints/{message_index}` + +Return a truncated snapshot of the run at the chosen boundary. The stored run is never mutated. + +```bash +curl "$HOST/agents/{agent_id}/runs/{run_id}/checkpoints/5?session_id=sess-xyz" +``` + +### Response + +```json +{ + "checkpoint": { ... same shape as a timeline entry ... }, + "snapshot": { + "run_id": "run-abc", + "messages": [ ... truncated to messages[:5] ... ], + "tools": [ ... only tools referenced by surviving messages ... ], + "requirements": [ ... only requirements referencing surviving tool_call_ids ... ], + "last_checkpoint_at_message_index": 5, + ... + } +} +``` + +Use to preview "this is what the run will look like if you continue from here" before firing the actual `/continue`. + +## POST `/sessions/{session_id}/branch` + +Deep-copy every run from the source session into a new session. + +```bash +curl -X POST "$HOST/agents/{agent_id}/sessions/{session_id}/branch" \ + -F "user_id=user-123" +``` + +### Response + +```json +{ + "session_id": "", + "branched_from": "" +} +``` + +The caller's `user_id` scopes the source session read. Users cannot branch sessions they don't own. + +## Status Codes + +| Code | Meaning | +|---|---| +| 200 | Run/session successfully advanced/branched/inspected | +| 400 | Invalid body (e.g. `continue_from` not parseable, `message_index` out of range) | +| 404 | Run, session, or agent/team not found | +| 409 | Run state incompatible with the requested action (e.g. continuing a `CANCELLED` run) | +| 500 | Internal error | + +## Team Endpoints + +Identical surface at `/teams/{team_id}/...`: + +- `POST /teams/{team_id}/runs/{run_id}/continue` +- `GET /teams/{team_id}/runs/{run_id}/checkpoints` +- `GET /teams/{team_id}/runs/{run_id}/checkpoints/{message_index}` +- `POST /teams/{team_id}/sessions/{session_id}/branch` + +Same request shape, same response shape. Team runs have `team_id` instead of `agent_id` and a `member_responses` array on the run. + +## SDK Equivalents + +| HTTP | SDK | +|---|---| +| `POST /runs/{run_id}/continue` | `agent.continue_run(run_id=..., ...)` | +| `POST /sessions/{session_id}/branch` | `agent.branch_session(session_id=...)` | +| `GET /runs/{run_id}/checkpoints` | Inspect `run_output.messages` directly, or use the HTTP endpoint | +| `GET /runs/{run_id}/checkpoints/{i}` | (HTTP-only. Derive locally if needed.) | + +All async variants exist (`acontinue_run`, `abranch_session`). diff --git a/run-control/branch-session.mdx b/run-control/branch-session.mdx new file mode 100644 index 000000000..6f09d936d --- /dev/null +++ b/run-control/branch-session.mdx @@ -0,0 +1,107 @@ +--- +title: Branch a Session +description: Deep-copy every run from a session into a brand-new session. +--- + +`branch_session` clones an entire session into a new session with a fresh `session_id`. Every run, plus session state and metadata, is copied. Use it to spin off an independent conversation thread from a known starting state. + +## Basic Usage + +```python +new_session_id = agent.branch_session(session_id="sess-original") + +# Continue in the new session. Independent of the original. +result = agent.run( + input="What if we went a different direction?", + session_id=new_session_id, +) +``` + +The new session contains deep copies of every run. The original session is untouched. + +## Branch vs Fork + +| | Branch session | Fork run | +|---|---|---| +| Granularity | Whole session | One run | +| Result | New `session_id`, all runs deep-copied | New `run_id`, sibling in same session | +| Endpoint | `POST /sessions/{session_id}/branch` | `POST /runs/{run_id}/continue` with `fork=True` | +| Lineage field on new entity | `branched_from` on the new session | `forked_from_run_id` on the new run | + +Use **branch** when you want a whole new conversation thread that starts from the current state of an existing one. Use **fork** when you want to try an alternative path that lives next to the original. + +## When to Use + +| Scenario | Use | +|---|---| +| "Save this conversation, let me try a different approach in a new thread" | Branch | +| "Rewind to message 3 and explore an alternative" | Fork | +| "Try the last response again with steering" | Regenerate | +| "Resume this paused run with tool results" | Continue (HITL) | + +## What Gets Copied + +- All `RunOutput` rows in the source session +- `session_data` (including `session_state`) +- `session_name` and metadata + +What does NOT carry over: + +- Live runs in flight on the source (only persisted runs are copied) +- Cross-session memory (memory is keyed by `user_id`, shared across sessions of that user by design) + +## User Scoping + +`branch_session` reads the source session with the caller's `user_id`. A user cannot branch another user's session. Pass `user_id` explicitly if you're calling outside an authenticated request context: + +```python +new_session_id = agent.branch_session( + session_id="sess-original", + user_id="user-123", +) +``` + +## Teams + +`Team.branch_session` works the same way: + +```python +new_team_session_id = team.branch_session(session_id="team-sess-original") +``` + +All team runs and any nested member runs from the source are deep-copied. The new team session is independent. + +## HTTP + +```bash +curl -X POST "$HOST/agents/{agent_id}/sessions/{session_id}/branch" \ + -F "user_id=user-123" +``` + +Returns the new `session_id`. See [API Reference](/run-control/api-reference). + +## Lineage Tracking + +The branched session carries `branched_from = `. Each copied run carries `forked_from_session_id = ` so you can render the lineage in a UI. + +## Examples + +### Agents + +| Example | What it shows | +|---|---| +| [Branch a session](https://github.com/agno-agi/agno/blob/main/cookbook/02_agents/21_fork_session/01_fork_session.py) | Spins off a new session from an existing one, runs an independent follow-up in each, and verifies the original session is untouched. | + +### Teams + +| Example | What it shows | +|---|---| +| [Branch a team session](https://github.com/agno-agi/agno/blob/main/cookbook/03_teams/26_fork_session/01_fork_session.py) | Team variant. All team runs and nested member runs are deep-copied; the new session is fully independent. | + +## Next Steps + +| Task | Guide | +|---|---| +| Sibling run in the same session | [Fork a Run](/run-control/fork-run) | +| Redo the last response | [Regenerate](/run-control/regenerate) | +| Persist sessions across requests | [Persisting Sessions](/sessions/persisting-sessions) | diff --git a/run-control/checkpointing.mdx b/run-control/checkpointing.mdx new file mode 100644 index 000000000..826642d3f --- /dev/null +++ b/run-control/checkpointing.mdx @@ -0,0 +1,127 @@ +--- +title: Checkpointing +description: Persist mid-run state after each tool batch so a crashed process can resume. +--- + +By default, a run only persists to the database at terminal states (`COMPLETED`, `PAUSED`, `ERROR`, `CANCELLED`). A worker that crashes mid-flight loses the work. `checkpoint="tool-batch"` writes a snapshot after each tool batch so the run is recoverable. + +## Basic Usage + +```python +from agno.agent import Agent +from agno.db.sqlite import SqliteDb +from agno.models.openai import OpenAIResponses + +agent = Agent( + name="research-agent", + model=OpenAIResponses(id="gpt-5.4"), + db=SqliteDb(db_file="tmp/agent.db"), + checkpoint="tool-batch", + tools=[...], +) +``` + +After every tool batch the model executes, a snapshot of the run is written to the session with `status=RUNNING`. A parallel batch (multiple tools called in one assistant turn) counts as one snapshot, written when all results have returned. + +If the process dies between batches, the latest snapshot survives. `agent.continue_run(run_id=run_id, session_id=session_id)` resumes from that snapshot: tools that already completed are not re-invoked, and the model picks up with the persisted transcript. + +Tools that were in-flight when the crash hit have no snapshot. On resume they're re-invoked with the same arguments. + +## Policies + +| Value | When the run persists | Use case | +|---|---|---| +| `"runs"` (default) | Only at terminal states (`COMPLETED`, `PAUSED`, `ERROR`, `CANCELLED`) | Chatty interactive agents. No write amplification. | +| `"tool-batch"` | After each tool batch + terminal state | Long research runs, multi-step pipelines, anything you want to recover after a crash. | + +For a run with K tool batches and a final no-tool turn you get K + 1 DB writes (K mid-run + 1 terminal). Opt in deliberately. + +## Resuming After a Crash + +A run persisted with `checkpoint="tool-batch"` and killed mid-flight (SIGKILL, OOM, power loss) stays in the DB with `status=RUNNING`. To resume: + +```python +# Same agent config, fresh process +agent = Agent(name="research-agent", db=SqliteDb(db_file="tmp/agent.db"), checkpoint="tool-batch", tools=[...]) + +result = agent.continue_run( + run_id="", + session_id="", +) +``` + +The model picks up from the last completed tool batch. Tools that already ran are not re-invoked; their results are in the persisted transcript. + +`/continue` with no body fields is the bare resume path. It works for any non-terminal status (`RUNNING`, `ERROR`, `PAUSED` with resolved approval). + +## Teams + +Teams support the same policy on the team-level loop: + +```python +from agno.team import Team + +team = Team( + name="research-team", + members=[...], + db=SqliteDb(db_file="tmp/team.db"), + checkpoint="tool-batch", +) +``` + +A team-level "tool batch" includes `delegate_task_to_member` calls. Each batch boundary writes a team checkpoint. Member agents have their own checkpoint policy on the member's `Agent(checkpoint=...)`. + +## Inspecting Checkpoints From a UI + +Every checkpoint boundary is queryable as a resume point. Use the timeline endpoint to list valid boundaries for a UI: + +```http +GET /agents/{agent_id}/runs/{run_id}/checkpoints?session_id=... +``` + +Each entry includes: + +| Field | Purpose | +|---|---| +| `checkpoint_id` | Display ordinal (`"1"`, `"2"`, …) for FE labels | +| `message_index` | Pass back as `continue_from=K` to resume from this boundary | +| `message_id` | Stable handle for client-side state | +| `message_preview` | First 120 chars of the boundary message | +| `reason` | `"checkpoint"` (mid-run) or `"end"` (terminal) | + +See [API Reference](/run-control/api-reference) for the full schema. + +## Cost + +`checkpoint="tool-batch"` writes the run JSON to the `session.runs` column after every tool batch. For a 10-batch research run that's 10 extra DB writes. Consider: + +- The `session.runs` column is a JSON blob holding all runs in the session. Writes grow with the number of runs, not just checkpoints. +- For Postgres this is one row update per batch. +- For an interactive chat agent with a 1-batch turn structure, the overhead is one extra write per turn. Usually fine. +- For agents that run a hundred parallel tools, it's K+1 writes per run. Measure before opting in. + +## Examples + +### Agents + +| Example | What it shows | +|---|---| +| [Crash recovery](https://github.com/agno-agi/agno/blob/main/cookbook/02_agents/18_checkpointing/01_crash_recovery.py) | A worker subprocess is `SIGKILL`'d mid-run. The parent process resumes the run and finishes the work. The clearest end-to-end demonstration of the feature. | +| [Tool error persistence](https://github.com/agno-agi/agno/blob/main/cookbook/02_agents/18_checkpointing/02_tool_error_persistence.py) | Two scenarios: (a) a tool raises a Python exception (caught by the loop, run continues), (b) the model call itself fails before any batch (the in-flight conversation is flushed onto the ERROR row so it's not lost). | +| [Checkpoint endpoints](https://github.com/agno-agi/agno/blob/main/cookbook/02_agents/18_checkpointing/03_checkpoint_endpoints.py) | Calls `GET /checkpoints` and `GET /checkpoints/{message_index}` via an in-process `TestClient`, prints the timeline + a derived snapshot, then resumes from the chosen boundary. | + +### Teams + +| Example | What it shows | +|---|---| +| [Crash recovery](https://github.com/agno-agi/agno/blob/main/cookbook/03_teams/23_checkpointing/01_crash_recovery.py) | Same crash-and-resume flow at the team level. | +| [Tool error persistence](https://github.com/agno-agi/agno/blob/main/cookbook/03_teams/23_checkpointing/02_tool_error_persistence.py) | Team variant of the in-flight-message flush. | +| [Checkpoint endpoints](https://github.com/agno-agi/agno/blob/main/cookbook/03_teams/23_checkpointing/03_checkpoint_endpoints.py) | Timeline + snapshot endpoints for team runs. | + +## Next Steps + +| Task | Guide | +|---|---| +| Resume a paused run with the unified `/continue` dispatch | [Continue Run](/run-control/continue-run) | +| Inspect and time-travel via checkpoints | [Fork a Run](/run-control/fork-run) | +| API endpoints | [API Reference](/run-control/api-reference) | diff --git a/run-control/continue-run.mdx b/run-control/continue-run.mdx new file mode 100644 index 000000000..212cb2411 --- /dev/null +++ b/run-control/continue-run.mdx @@ -0,0 +1,116 @@ +--- +title: Continue Run +description: The unified /continue dispatch resumes any persisted run from its current state. +--- + +`/continue` advances a persisted run. It dispatches on the body: resolve a HITL pause, retry an error, follow up on a completed run, regenerate, fork, or time-travel. One endpoint, one mental model. + +## Basic Usage + +```python +# Bare resume of a non-terminal run (RUNNING / ERROR / PAUSED with resolved approval) +result = agent.continue_run(run_id="run-abc", session_id="sess-xyz") +``` + +```python +# Resolve a HITL pause with tool results +result = agent.continue_run(run_id="run-abc", session_id="sess-xyz", requirements=[...]) +``` + +```python +# Follow up on a completed run (auto-forks: new run_id, source preserved) +result = agent.continue_run( + run_id="run-abc", + session_id="sess-xyz", + input="Now compare with Lagos", +) +``` + +## Dispatch Table + +| Body | Source `status` | Behavior | +|---|---|---| +| `requirements=[...]` | `PAUSED` (HITL) | Apply tool results, resume | +| empty | `RUNNING` / `ERROR` / `CANCELLED` | Resume in place (same `run_id`) | +| empty + resolved admin approval in DB | `PAUSED` | Apply resolution, resume | +| `input="..."` | `COMPLETED` | Append new user message, auto-fork (new `run_id`) | +| `regenerate=True` | `COMPLETED` | Redo last response, always fork | +| `continue_from=K` | any | Truncate to first K messages, fork | +| `fork=True` | any | Clone the run with a new `run_id` | + +## The 1-run-1-loop Invariant + +If the source run's loop has already finished (`status=COMPLETED`), `/continue` produces a new `run_id`. Metrics, timestamps, and events on the source row remain a record of exactly one model loop. Only mid-flight resumes (`RUNNING` / `ERROR` / `PAUSED`) stay on the same `run_id`. + +## Parameter Reference + +| Parameter | Type | Purpose | +|---|---|---| +| `run_id` | `str` | The run to continue | +| `session_id` | `str` | The session that owns the run | +| `requirements` | `List[RunRequirement]` | HITL tool results to apply before resuming | +| `input` | `Optional[str]` | New user message to append before the next turn | +| `continue_from` | `int \| "end" \| "last_user"` | Truncate to a chosen message boundary | +| `fork` | `bool` | Clone with a new `run_id` (default: `False`, but `True` is implied for completed runs and regenerate) | +| `regenerate` | `bool` | Drop the last assistant response and redo (always forks) | +| `replace_original` | `bool` | When regenerating, hide the source from history (default: `True`) | +| `additional_instructions` | `str` | Steering text appended as a user message when regenerating | + +## `continue_from` Selector + +| Value | Effect | +|---|---| +| `"end"` (default) | Full transcript. Auto-forks completed runs. | +| `"last_user"` | Drop everything past the last user message. | +| `K: int` | Keep `messages[:K]`. Pair-safe: indices that split a tool batch are snapped down to the boundary before the batch. | + +The integer form pairs naturally with checkpoint timeline entries. Indices returned by `GET /runs/{run_id}/checkpoints` are guaranteed pair-safe. + +## HTTP Equivalent + +```bash +curl -X POST "$HOST/agents/{agent_id}/runs/{run_id}/continue" \ + -F "session_id=sess-xyz" \ + -F "regenerate=true" \ + -F "additional_instructions=Be more concise" +``` + +All SDK parameters map to form fields. See [API Reference](/run-control/api-reference) for the full schema. + +## Teams + +`Team.continue_run` accepts the same parameters with the same semantics: + +```python +result = team.continue_run( + run_id="team-run-abc", + session_id="team-sess-xyz", + regenerate=True, +) +``` + +## Examples + +### Agents + +| Example | What it shows | +|---|---| +| [Continue from a boundary](https://github.com/agno-agi/agno/blob/main/cookbook/02_agents/20_time_travel/01_continue_from.py) | All four `continue_from` forms (`"end"`, `"last_user"`, integer index, and `regenerate=True`) demonstrated end-to-end. The clearest single example of the dispatch surface. | +| [Crash recovery (bare resume)](https://github.com/agno-agi/agno/blob/main/cookbook/02_agents/18_checkpointing/01_crash_recovery.py) | Empty-body `continue_run(run_id=..., session_id=...)` resuming a `RUNNING` run after a process crash. | +| [HITL resolve](https://github.com/agno-agi/agno/blob/main/cookbook/02_agents/10_human_in_the_loop/confirmation_required.py) | `continue_run(requirements=[...])` to resolve a `PAUSED` HITL run with tool results. | + +### Teams + +| Example | What it shows | +|---|---| +| [Continue from a boundary](https://github.com/agno-agi/agno/blob/main/cookbook/03_teams/25_time_travel/01_continue_from.py) | Team variant of the four-form dispatch demo. | +| [Crash recovery](https://github.com/agno-agi/agno/blob/main/cookbook/03_teams/23_checkpointing/01_crash_recovery.py) | Bare resume after a team-level crash. | + +## Next Steps + +| Task | Guide | +|---|---| +| Redo the last response | [Regenerate](/run-control/regenerate) | +| Rewind to an earlier message | [Fork a Run](/run-control/fork-run) | +| Start a new session from this one | [Branch a Session](/run-control/branch-session) | +| Persist mid-run for crash recovery | [Checkpointing](/run-control/checkpointing) | diff --git a/run-control/fork-run.mdx b/run-control/fork-run.mdx new file mode 100644 index 000000000..1c52e98e9 --- /dev/null +++ b/run-control/fork-run.mdx @@ -0,0 +1,139 @@ +--- +title: Fork a Run +description: Rewind to a message boundary and explore an alternative path. Sibling run in the same session. +--- + +A fork is a sibling run within the same session, started from a chosen message boundary of an existing run. Use it to explore alternative paths, run evals from a known-good state, or A/B-test instructions. + +## Basic Usage + +```python +fork = agent.continue_run( + run_id="run-abc", + session_id="sess-xyz", + continue_from="last_user", + fork=True, + input="Try a different approach this time", +) +``` + +The new run has a fresh `run_id`, `forked_from_run_id` pointing at the source, and `forked_from_message_index` recording where it cut. + +## Where to Fork From + +`continue_from` chooses the message boundary. + +| Value | What it keeps | +|---|---| +| `"end"` | Full transcript. Use to continue a completed run with a new turn. | +| `"last_user"` | Drop everything past the last user message. Tools will be re-invoked. | +| `K: int` | Keep `messages[:K]`. Pair-safe: indices that split a tool batch snap down to the boundary before the batch. | + +For valid integer indices on a specific run, call the [checkpoints endpoint](/run-control/api-reference). Every index it returns is pair-safe. + +## Forking vs Regenerating + +| | Fork | Regenerate | +|---|---|---| +| What's kept | Up to `continue_from` (you choose) | Everything up to last user message + tool exchanges | +| Tools re-invoked? | Depends on cut point | No. Intermediate tool exchanges survive. | +| Source visibility | Always visible | `replace_original=True` hides; `False` keeps visible | +| Use case | "Try a different reasoning path" | "Redo the final summary" | + +If you want a cheap "try again with steering" without re-running tools, use [Regenerate](/run-control/regenerate). If you want to rewind further (drop tools, drop intermediate turns, change the prompt entirely), fork. + +## Auto-Fork on COMPLETED + +Continuing a completed run always produces a new `run_id`, even without `fork=True`: + +```python +# Source is COMPLETED. /continue produces a new run_id automatically. +new_run = agent.continue_run( + run_id="run-abc", + session_id="sess-xyz", + input="Now compare with Tokyo", +) +``` + +The 1-run-1-loop invariant: a completed model loop cannot be overwritten. The new turn always lands on a new run row. + +## Pair-Safe Truncation + +Integer `continue_from=K` snaps down to a "pair-safe" boundary when `K` lands mid-tool-batch. + +Given the transcript: + +``` +[1] user +[2] assistant tool_calls=[tc1, tc2, tc3] +[3] tool: result for tc1 +[4] tool: result for tc2 +[5] tool: result for tc3 +[6] assistant: final reply +``` + +| `continue_from=K` | Actual boundary | Why | +|---|---|---| +| 1 | 1 | Clean cut | +| 2 | 1 | Would orphan assistant tool_calls | +| 3 | 1 | Mid-batch (tc2, tc3 results not yet in prefix) | +| 4 | 1 | Mid-batch (tc3 result not yet in prefix) | +| 5 | 5 | Whole batch present | +| 6 | 6 | Full transcript | + +The fork response's `forked_from_message_index` reflects the actual boundary used. If it differs from what you sent, the dispatch snapped the index down. + +Indices returned by the [checkpoints endpoint](/run-control/api-reference) are always pair-safe by construction. + +## Teams + +`Team.continue_run(fork=True, continue_from=...)` works the same way: + +```python +fork = team.continue_run( + run_id="team-run-abc", + session_id="team-sess-xyz", + continue_from="last_user", + fork=True, + input="What if we routed differently?", +) +``` + +The forked team is a new run in the same session. Member runs from the original team stay attached to the original via `parent_run_id`. The fork starts fresh and may produce new member runs. + +## HTTP + +```bash +curl -X POST "$HOST/agents/{agent_id}/runs/{run_id}/continue" \ + -F "session_id=sess-xyz" \ + -F "continue_from=last_user" \ + -F "fork=true" \ + -F "input=Try a different approach" +``` + +`continue_from` accepts `"end"`, `"last_user"`, or a numeric string (e.g. `"4"`). + +## Examples + +### Agents + +| Example | What it shows | +|---|---| +| [Fork a run](https://github.com/agno-agi/agno/blob/main/cookbook/02_agents/20_time_travel/02_fork_run.py) | `fork=True, continue_from=...` to clone a run at a chosen boundary. The forked run lives next to the original in the same session. | +| [Continue from a boundary](https://github.com/agno-agi/agno/blob/main/cookbook/02_agents/20_time_travel/01_continue_from.py) | All four `continue_from` forms (`"end"`, `"last_user"`, integer, `regenerate=True`) including the auto-fork-on-completed case. | +| [Checkpoint endpoints](https://github.com/agno-agi/agno/blob/main/cookbook/02_agents/18_checkpointing/03_checkpoint_endpoints.py) | How to discover valid integer indices: call `GET /checkpoints`, then feed `message_index` back into `continue_from=K`. Every index returned is pair-safe. | + +### Teams + +| Example | What it shows | +|---|---| +| [Fork a team run](https://github.com/agno-agi/agno/blob/main/cookbook/03_teams/25_time_travel/02_fork_run.py) | Team variant. Member runs from the source team stay attached to the source via `parent_run_id`; the forked team may delegate fresh. | +| [Continue from a boundary](https://github.com/agno-agi/agno/blob/main/cookbook/03_teams/25_time_travel/01_continue_from.py) | Team variant of the four-form dispatch demo. | + +## Next Steps + +| Task | Guide | +|---|---| +| Get valid boundaries from the timeline endpoint | [API Reference](/run-control/api-reference) | +| Redo only the last response | [Regenerate](/run-control/regenerate) | +| Whole-session clone | [Branch a Session](/run-control/branch-session) | diff --git a/run-control/overview.mdx b/run-control/overview.mdx new file mode 100644 index 000000000..b348e00d8 --- /dev/null +++ b/run-control/overview.mdx @@ -0,0 +1,72 @@ +--- +title: Run Control +sidebarTitle: Overview +description: Persist mid-run state, resume, regenerate, fork, and branch agent and team runs. +--- + +Once a run has been persisted, you can advance it, redo it, branch off it, or pick a specific point in its history to continue from. All five operations go through one endpoint: `/continue` (HTTP) or `agent.continue_run()` / `team.continue_run()` (SDK). + +## Operations + +| Operation | What it does | Stays in same session? | Same `run_id`? | +|---|---|---|---| +| Resume | Advance a run that's mid-flight (HITL pause, error, crash) | Yes | Yes | +| Regenerate | Redo the last response, optionally with steering input | Yes | No (new sibling run) | +| Fork run | Rewind to a chosen message and continue from there | Yes | No (new sibling run) | +| Branch session | Deep-copy every run into a new session | No (new session) | n/a | +| Checkpoint | Persist run state after each tool batch for crash recovery | Yes | Yes | + +## Why one endpoint + +`/continue` dispatches on the body. The same call resumes a HITL-paused run, regenerates the last response, forks at message K, or continues a completed run with a follow-up. The body fields name the verb: + +```python +# Resume a HITL pause +agent.continue_run(run_id="...", requirements=[...]) + +# Redo the last response +agent.continue_run(run_id="...", regenerate=True) + +# Rewind to message 4 +agent.continue_run(run_id="...", continue_from=4, fork=True) + +# Follow up on a completed run +agent.continue_run(run_id="...", input="Now compare with Lagos") +``` + +One code path on the server, one mental model for the caller. + +## The "1 run = 1 model loop" invariant + +A run row in the DB represents exactly one model loop. Whenever a model loop has already finished (status `COMPLETED`), `/continue` produces a new `run_id`. There is no way to mix two loops' metrics, timestamps, or events into one row. + +Mid-flight resumes (`RUNNING` / `ERROR` / `PAUSED`, where the loop never finished) stay on the same `run_id`. Everything else gets a new one. + +## Learn How To + + + + Persist mid-run state with `checkpoint="tool-batch"` so a crashed process can resume. + + + The unified `/continue` endpoint: resume HITL pauses, mid-flight errors, or completed runs with a follow-up. + + + Redo the last assistant response, optionally with steering input. + + + Rewind to a chosen message boundary and explore an alternative path. Sibling run in the same session. + + + Deep-copy every run into a brand-new session for independent exploration. + + + HTTP endpoints, request bodies, and response shapes. + + + +## Developer Resources + +- [Run-control cookbook examples](https://github.com/agno-agi/agno/tree/main/cookbook/02_agents/18_checkpointing) +- [`Agent.continue_run` reference](/reference/agents/agent) +- [`Team.continue_run` reference](/reference/teams/team) diff --git a/run-control/regenerate.mdx b/run-control/regenerate.mdx new file mode 100644 index 000000000..74a4bba22 --- /dev/null +++ b/run-control/regenerate.mdx @@ -0,0 +1,121 @@ +--- +title: Regenerate +description: Redo the last response with a fresh run_id, optionally with steering input. +--- + +`regenerate=True` drops the trailing assistant response and re-runs the model. The new attempt gets a fresh `run_id` and fresh metrics. The source run is always retained in storage. + +## Basic Usage + +```python +new_run = agent.continue_run( + run_id="run-abc", + session_id="sess-xyz", + regenerate=True, +) +``` + +The source's last assistant response is dropped. The model is invoked again on the remaining transcript. The new run is a sibling in the same session with `forked_from_run_id` and `regenerated_from` pointing at the source. + +## Steering the New Response + +```python +new_run = agent.continue_run( + run_id="run-abc", + session_id="sess-xyz", + regenerate=True, + additional_instructions="Be more concise. Use bullet points.", +) +``` + +`additional_instructions` is appended to the transcript as a user message before the model runs. Use it to guide the new output without manually editing the prompt. + +## Keep the Original Visible + +By default, regenerating hides the source run (`status=REGENERATED`) so chat history shows only the new response. + +```python +# Keep both visible for side-by-side comparison +new_run = agent.continue_run( + run_id="run-abc", + regenerate=True, + replace_original=False, +) +``` + +| `replace_original` | Source run status after regenerate | Visible in `GET /runs`? | +|---|---|---| +| `True` (default) | `REGENERATED` | No | +| `False` | unchanged (`COMPLETED`) | Yes | + +The source row is always retained in storage either way. `replace_original` only controls history visibility. + +## What Gets Dropped + +Regenerate drops only the trailing assistant message. Intermediate tool exchanges (assistant `tool_calls` plus their results) survive. The model regenerates a fresh summary of the same tool outputs without re-invoking the tools. + +``` +Source transcript: + [1] user: "weather in Paris?" + [2] assistant tool_calls=[get_weather] + [3] tool: "14°C, cloudy" + [4] assistant: "It's cloudy in Paris at 14°C." + +After regenerate=True: + [1] user: "weather in Paris?" + [2] assistant tool_calls=[get_weather] + [3] tool: "14°C, cloudy" + → model runs on [1..3], produces new [4] +``` + +This makes regenerate cheap. To force tool re-invocation, use `continue_from="last_user"` instead. + +## Always Forks + +Regenerate always creates a new run with a new `run_id`. The "1 run = 1 model loop" invariant means a completed model loop cannot be overwritten with a new one. Metrics, timestamps, and events on the source row remain accurate. + +## Teams + +`Team.continue_run(regenerate=True)` works the same way: + +```python +new_team_run = team.continue_run( + run_id="team-run-abc", + regenerate=True, + additional_instructions="Delegate fewer tasks this time", +) +``` + +Member runs from the source team stay attached to the original team via `parent_run_id`. The regenerated team is a fresh model loop that may or may not delegate to members again. + +## HTTP + +```bash +curl -X POST "$HOST/agents/{agent_id}/runs/{run_id}/continue" \ + -F "session_id=sess-xyz" \ + -F "regenerate=true" \ + -F "additional_instructions=Be more concise" +``` + +## Examples + +### Agents + +| Example | What it shows | +|---|---| +| [Regenerate end-to-end](https://github.com/agno-agi/agno/blob/main/cookbook/02_agents/19_regenerate/01_regenerate.py) | All three flavors in one file: bare regenerate, regenerate with `additional_instructions`, and regenerate with `replace_original=False` (both runs visible side by side). | +| [Continue from a boundary](https://github.com/agno-agi/agno/blob/main/cookbook/02_agents/20_time_travel/01_continue_from.py) | Direct comparison of `regenerate=True` against the related `continue_from="last_user"` and integer-index forms. Useful for understanding when to pick which. | + +### Teams + +| Example | What it shows | +|---|---| +| [Regenerate end-to-end](https://github.com/agno-agi/agno/blob/main/cookbook/03_teams/24_regenerate/01_regenerate.py) | Team variant. Member runs from the original team stay attached to the original; the regenerated team is a fresh model loop. | + +## Next Steps + +| Task | Guide | +|---|---| +| Drop more than the last response | [Fork a Run](/run-control/fork-run) | +| Continue a completed run with a follow-up | [Continue Run](/run-control/continue-run) | +| Branch the whole session | [Branch a Session](/run-control/branch-session) |