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
1 change: 1 addition & 0 deletions docs/adr/openab-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ ANTHROPIC_API_KEY = "${ANTHROPIC_API_KEY}"
# Optional:
OPENAB_AGENT_MAX_TOKENS = "8192"
OPENAB_AGENT_TIMEOUT_SECS = "120"
OPENAB_AGENT_MAX_TOOL_LOOPS = "50" # max tool-call iterations per prompt (default 50)
```

### Steering Files
Expand Down
1 change: 1 addition & 0 deletions docs/native-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ env = { OPENAB_AGENT_OPENAI_MODEL = "gpt-5.4-mini" }
| `OPENAB_AGENT_PROVIDER` | auto-detect | Force provider (`anthropic`, `openai`, `codex`) |
| `OPENAB_AGENT_MAX_TOKENS` | `8192` | Max output tokens |
| `OPENAB_AGENT_OAUTH_CLIENT_ID` | Pi's client | Custom OAuth client ID |
| `OPENAB_AGENT_MAX_TOOL_LOOPS` | `50` | Max tool-call iterations per prompt before the agent gives up |
| `ANTHROPIC_API_KEY` | — | Anthropic API key (alternative to OAuth) |

## Authentication
Expand Down
68 changes: 64 additions & 4 deletions openab-agent/src/agent.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use anyhow::Result;
use serde::Deserialize;
use std::path::PathBuf;
use tracing::{debug, info};
use tracing::{debug, info, warn};

use crate::llm::{ContentBlock, LlmEvent, LlmProvider, Message, ToolDef};
use crate::mcp::{self, McpRuntimeManager};
Expand All @@ -25,7 +25,33 @@ Be direct and concise. Execute tasks immediately rather than explaining what you
// from the LLM and produced the "fs is disconnected, I give up" failure
// mode observed in the F1 PoC.

const MAX_TOOL_LOOPS: usize = 50;
const DEFAULT_MAX_TOOL_LOOPS: usize = 50;

fn max_tool_loops() -> usize {
let raw = match std::env::var("OPENAB_AGENT_MAX_TOOL_LOOPS") {
Ok(val) => match val.parse::<usize>() {
Ok(n) => n,
Err(e) => {
warn!(
"OPENAB_AGENT_MAX_TOOL_LOOPS={val:?} is not valid ({e}), \
falling back to {DEFAULT_MAX_TOOL_LOOPS}"
);
DEFAULT_MAX_TOOL_LOOPS
}
},
Err(_) => DEFAULT_MAX_TOOL_LOOPS,
};
if raw == 0 {
warn!(
"OPENAB_AGENT_MAX_TOOL_LOOPS=0 would prevent the agent from running; \
using minimum value of 1"
);
1
} else {
raw
}
}
Comment on lines +30 to +53

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentionally left unbounded — prompt_hard_timeout_secs (default 3600s) already acts as the cost/latency backstop. Adding a second hard cap would create a confusing interaction between the two knobs. Operators who set a high value are opting in.


/// Maximum number of messages to keep in context. When exceeded, oldest
/// messages (excluding the first user message) are dropped.
const MAX_CONTEXT_MESSAGES: usize = 100;
Expand Down Expand Up @@ -137,8 +163,14 @@ impl Agent {
});

let mut final_text = String::new();
let max_loops = max_tool_loops();
if max_loops != DEFAULT_MAX_TOOL_LOOPS {
info!("max_tool_loops={max_loops} (overridden)");
} else {
debug!("max_tool_loops={max_loops}");
}

for iteration in 0..MAX_TOOL_LOOPS {
for iteration in 0..max_loops {
Comment on lines +166 to +173

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in f6cd0b7max_tool_loops() now clamps 0 to 1 with a warn! log.

debug!("agent loop iteration {iteration}");

// Truncate context to prevent unbounded growth / token limit
Expand Down Expand Up @@ -219,7 +251,7 @@ impl Agent {

if final_text.is_empty() {
return Err(anyhow::anyhow!(
"agent exceeded maximum tool loop iterations ({MAX_TOOL_LOOPS})"
"agent exceeded maximum tool loop iterations ({max_loops})"
));
}

Expand Down Expand Up @@ -466,4 +498,32 @@ mod tests {
let content = std::fs::read_to_string(tmp.path().join("out.txt")).unwrap();
assert_eq!(content, "hello");
}

#[test]
fn test_max_tool_loops_default() {
temp_env::with_var("OPENAB_AGENT_MAX_TOOL_LOOPS", None::<&str>, || {
assert_eq!(max_tool_loops(), DEFAULT_MAX_TOOL_LOOPS);
});
}

#[test]
fn test_max_tool_loops_custom_value() {
temp_env::with_var("OPENAB_AGENT_MAX_TOOL_LOOPS", Some("200"), || {
assert_eq!(max_tool_loops(), 200);
});
}

#[test]
fn test_max_tool_loops_invalid_falls_back() {
temp_env::with_var("OPENAB_AGENT_MAX_TOOL_LOOPS", Some("abc"), || {
assert_eq!(max_tool_loops(), DEFAULT_MAX_TOOL_LOOPS);
});
}

#[test]
fn test_max_tool_loops_zero_clamps_to_one() {
temp_env::with_var("OPENAB_AGENT_MAX_TOOL_LOOPS", Some("0"), || {
assert_eq!(max_tool_loops(), 1);
});
}
}
Loading