diff --git a/docs/internal/events.md b/docs/internal/events.md
index 4af5ab491..7850a5411 100644
--- a/docs/internal/events.md
+++ b/docs/internal/events.md
@@ -1252,26 +1252,6 @@ Emitted when the agent detects a tool-use loop.
No properties.
-### `agent.turn.limit`
-
-Emitted when the agent reaches its maximum turn count.
-
-```json
-{
- "id": "...", "ts": "...", "run_id": "...",
- "event": "agent.turn.limit",
- "node_id": "code", "node_label": "code",
- "session_id": "ses_abc",
- "properties": {
- "max_turns": 25
- }
-}
-```
-
-| Property | Type | Description |
-|----------|------|-------------|
-| `max_turns` | number | Maximum turns allowed |
-
### `agent.skill.expanded`
```json
diff --git a/docs/internal/fabro-event-schema-v2-concrete-shape.md b/docs/internal/fabro-event-schema-v2-concrete-shape.md
index 77f4fe5da..78134f619 100644
--- a/docs/internal/fabro-event-schema-v2-concrete-shape.md
+++ b/docs/internal/fabro-event-schema-v2-concrete-shape.md
@@ -374,7 +374,6 @@ V2 keeps the current durable family surface broadly intact.
- `agent.error`
- `agent.warning`
- `agent.loop.detected`
-- `agent.turn.limit`
- `agent.steering.injected`
- `agent.compaction.started`
- `agent.compaction.completed`
diff --git a/docs/public/agents/subagents.mdx b/docs/public/agents/subagents.mdx
index 11cf30c38..597bd96e3 100644
--- a/docs/public/agents/subagents.mdx
+++ b/docs/public/agents/subagents.mdx
@@ -44,7 +44,6 @@ Sub-agent failures do not automatically fail the parent stage. The parent receiv
Common cases:
-- **Hits `max_turns`** -- returns normally with its last output
- **Panics or errors** -- returned as a failed `wait` result
- **`spawn_agent` fails** -- returned immediately as a tool result
@@ -97,5 +96,5 @@ Sub-agents are most useful for:
Use [child runs](/execution/child-runs) instead when the delegated work should be a separate Fabro run with its own workflow, lifecycle, sandbox, checkpoints, and outputs.
-Sub-agents run with no turn limit by default. Pass `max_turns` when you want predictable cost or time bounds. All active sub-agents are cleaned up automatically when the parent session closes.
+Sub-agents run until they complete, fail, are cancelled, or hit the session's wall-clock timeout. All active sub-agents are cleaned up automatically when the parent session closes.
diff --git a/docs/public/changelog/2026-03-23.mdx b/docs/public/changelog/2026-03-23.mdx
index e3d47ecda..359f5bbc1 100644
--- a/docs/public/changelog/2026-03-23.mdx
+++ b/docs/public/changelog/2026-03-23.mdx
@@ -5,7 +5,7 @@ date: "2026-03-23"
## Unlimited agent tool rounds
-Agent stages no longer have an artificial cap on tool rounds per input or subagent turns. Previously, the default limits could cause agents to stop mid-task on complex operations. Both `max_tool_rounds_per_input` and subagent `max_turns` now default to unlimited, so agents run until they complete their work or hit the context window.
+Agent stages no longer have an artificial cap on tool rounds per input or subagent turns, so agents run until they complete their work or are interrupted.
## More
diff --git a/docs/public/reference/sdk.mdx b/docs/public/reference/sdk.mdx
index 4332f3adc..9f88c7696 100644
--- a/docs/public/reference/sdk.mdx
+++ b/docs/public/reference/sdk.mdx
@@ -84,7 +84,7 @@ pub fn new(
| Method | Description |
|---|---|
| `initialize().await` | Discovers project docs, skills, and MCP servers. Call before `process_input`. |
-| `process_input(input).await` | Sends user input and runs the agent loop until the model stops or a limit is hit. |
+| `process_input(input).await` | Sends user input and runs the agent loop until the model stops, the session is interrupted, or an error occurs. |
| `close()` | Ends the session and emits `SessionEnded`. |
| `interrupt()` | Cancels the current `process_input` call. |
| `cancel_token()` | Returns a `CancellationToken` for external cancellation. |
@@ -110,8 +110,6 @@ All fields are public. Key settings with their defaults:
| Field | Default | Description |
|---|---|---|
-| `max_turns` | `0` (unlimited) | Maximum conversation turns before stopping. |
-| `max_tool_rounds_per_input` | `200` | Maximum tool execution rounds per `process_input` call. |
| `default_command_timeout_ms` | `10,000` | Default timeout for Bash tool commands. |
| `max_command_timeout_ms` | `600,000` | Maximum allowed timeout for Bash tool commands. |
| `enable_loop_detection` | `true` | Detect and break out of repetitive tool call patterns. |
@@ -221,7 +219,6 @@ Key `AgentEvent` variants:
| `ToolCallCompleted { tool_name, tool_call_id, output, is_error }` | A tool call finished. |
| `Error { error }` | An `AgentError` occurred. |
| `LoopDetected` | The agent is repeating itself. |
-| `TurnLimitReached { max_turns }` | Turn limit hit. |
| `CompactionStarted` / `CompactionCompleted` | Context window compaction. |
| `SubAgentSpawned` / `SubAgentCompleted` | Sub-agent lifecycle. |
| `McpServerReady` / `McpServerFailed` | MCP server connection status. |
@@ -296,7 +293,7 @@ All fallible `Session` methods return `Result`:
| `SessionClosed` | `process_input` was called on a closed session. |
| `InvalidState(String)` | The session is in an unexpected state. |
| `ToolExecution(String)` | A tool execution failed. |
-| `Interrupted(InterruptReason)` | The session was cancelled (`Cancelled`) or timed out (`WallClockTimeout`). |
+| `Interrupted(InterruptReason)` | The session was cancelled or timed out. |
---
diff --git a/lib/components/fabro-agent/README.md b/lib/components/fabro-agent/README.md
index 5cac1be82..69764459e 100644
--- a/lib/components/fabro-agent/README.md
+++ b/lib/components/fabro-agent/README.md
@@ -98,8 +98,6 @@ pub trait Sandbox: Send + Sync {
```rust
pub struct SessionConfig {
- pub max_turns: usize, // 0 = unlimited
- pub max_tool_rounds_per_input: usize, // default: 200
pub default_command_timeout_ms: u64, // default: 10s
pub max_command_timeout_ms: u64, // default: 600s
pub enable_loop_detection: bool, // default: true
@@ -134,7 +132,6 @@ let env = Arc::new(LocalSandbox::new(
// 4. Configure the session
let config = SessionConfig {
- max_tool_rounds_per_input: 50,
enable_loop_detection: true,
user_instructions: Some("Always write tests first".into()),
..SessionConfig::default()
diff --git a/lib/components/fabro-agent/src/config.rs b/lib/components/fabro-agent/src/config.rs
index c54d49f01..bc22fd75c 100644
--- a/lib/components/fabro-agent/src/config.rs
+++ b/lib/components/fabro-agent/src/config.rs
@@ -106,8 +106,6 @@ pub struct ToolSecrets {
#[derive(Clone)]
pub struct SessionOptions {
- pub max_turns: usize,
- pub max_tool_rounds_per_input: usize,
pub default_command_timeout_ms: u64,
pub max_command_timeout_ms: u64,
pub reasoning_effort: Option,
@@ -149,8 +147,6 @@ pub struct SessionOptions {
impl std::fmt::Debug for SessionOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SessionOptions")
- .field("max_turns", &self.max_turns)
- .field("max_tool_rounds_per_input", &self.max_tool_rounds_per_input)
.field(
"default_command_timeout_ms",
&self.default_command_timeout_ms,
@@ -196,8 +192,6 @@ impl std::fmt::Debug for SessionOptions {
impl Default for SessionOptions {
fn default() -> Self {
Self {
- max_turns: 0,
- max_tool_rounds_per_input: 0,
default_command_timeout_ms: 10_000,
max_command_timeout_ms: 600_000,
max_tokens: None,
@@ -280,8 +274,6 @@ mod tests {
#[test]
fn default_config_values() {
let config = SessionOptions::default();
- assert_eq!(config.max_turns, 0);
- assert_eq!(config.max_tool_rounds_per_input, 0);
assert_eq!(config.default_command_timeout_ms, 10_000);
assert_eq!(config.max_command_timeout_ms, 600_000);
assert!(config.reasoning_effort.is_none());
@@ -328,13 +320,10 @@ mod tests {
#[test]
fn config_with_custom_values() {
let config = SessionOptions {
- max_turns: 50,
reasoning_effort: Some(ReasoningEffort::High),
..Default::default()
};
- assert_eq!(config.max_turns, 50);
assert_eq!(config.reasoning_effort, Some(ReasoningEffort::High));
- assert_eq!(config.max_tool_rounds_per_input, 0);
}
#[test]
diff --git a/lib/components/fabro-agent/src/session.rs b/lib/components/fabro-agent/src/session.rs
index 34986a29d..759aa8884 100644
--- a/lib/components/fabro-agent/src/session.rs
+++ b/lib/components/fabro-agent/src/session.rs
@@ -1195,10 +1195,6 @@ impl Session {
self.config.speed = speed;
}
- pub const fn set_max_turns(&mut self, max_turns: usize) {
- self.config.max_turns = max_turns;
- }
-
#[must_use]
pub const fn history(&self) -> &History {
&self.history
@@ -1210,7 +1206,14 @@ impl Session {
}
pub async fn process_input(&mut self, input: &str) -> Result<(), Error> {
- self.process_input_with_runtime(input, AgentToolRuntime::default())
+ self.process_input_with_output(input).await.map(drop)
+ }
+
+ pub(crate) async fn process_input_with_output(
+ &mut self,
+ input: &str,
+ ) -> Result