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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Added an opt-in real-LLM integration suite for batch-read continuation,
every grep output mode, guarded edit previews and writes, and stable glob
pagination.

### Fixed

- Accepted structured providers' neutral pagination defaults (`limit = 200`
and an empty cursor) in non-paginated grep modes while continuing to reject
effective pagination controls there.

## [6.5.0] - 2026-07-28

### Added
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,15 @@ node scripts/sdk_api_alignment_check.mjs
Real-provider, browser-runtime, and S3 tests are ignored unless their external
prerequisites are configured. The normal test suite is hermetic.

Run the context-tool real-LLM suite through a local Codex login:

```bash
A3S_CONTEXT_TOOLS_USE_CODEX_LOGIN=1 scripts/context_tools_real_llm.sh
```

Alternatively, point `A3S_CONFIG_FILE` at an ACL provider configuration and
run the same script without `A3S_CONTEXT_TOOLS_USE_CODEX_LOGIN`.

## License

[MIT](LICENSE)
64 changes: 62 additions & 2 deletions core/src/tools/builtin/grep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@ impl GrepOutputMode {
}
}

fn has_non_neutral_page_controls(args: &serde_json::Value) -> bool {
let limit_is_non_neutral = match args.get("limit") {
None | Some(serde_json::Value::Null) => false,
Some(value) => value.as_u64() != Some(DEFAULT_PAGE_LIMIT as u64),
};
let cursor_is_non_neutral = match args.get("cursor") {
None | Some(serde_json::Value::Null) => false,
Some(serde_json::Value::String(value)) => !value.trim().is_empty(),
Some(_) => true,
};
limit_is_non_neutral || cursor_is_non_neutral
}

pub struct GrepTool;

#[async_trait]
Expand Down Expand Up @@ -150,9 +163,13 @@ impl Tool for GrepTool {
Err(error) => return Ok(ToolOutput::error(error)),
}
} else {
if args.get("limit").is_some() || args.get("cursor").is_some() {
// Structured-output providers can materialize omitted optional
// fields with their neutral schema defaults. Do not reject a
// content/summary call merely because it carries limit=200 and an
// empty cursor; neither value changes the non-paginated result.
if has_non_neutral_page_controls(args) {
return Ok(ToolOutput::error(
"limit and cursor are supported only with output_mode='files_with_matches' or 'count'",
"non-default limit and non-empty cursor are supported only with output_mode='files_with_matches' or 'count'",
));
}
None
Expand Down Expand Up @@ -735,6 +752,49 @@ mod tests {
assert_eq!(result.metadata.unwrap()["search"]["truncated"], false);
}

#[tokio::test]
async fn test_grep_non_paginated_mode_accepts_materialized_neutral_page_defaults() {
let temp = tempfile::tempdir().unwrap();
std::fs::write(temp.path().join("a.txt"), "needle\n").unwrap();
let ctx = ToolContext::new(temp.path().to_path_buf());

let result = GrepTool
.execute(
&serde_json::json!({
"pattern": "needle",
"output_mode": "content",
"limit": DEFAULT_PAGE_LIMIT,
"cursor": ""
}),
&ctx,
)
.await
.unwrap();

assert!(result.success, "{}", result.content);
assert!(result.content.contains("needle"));
}

#[tokio::test]
async fn test_grep_non_paginated_mode_rejects_effective_page_controls() {
let ctx = ToolContext::new(PathBuf::from("/tmp"));
for controls in [
serde_json::json!({"limit": 2}),
serde_json::json!({"cursor": "2"}),
] {
let mut args = serde_json::json!({
"pattern": "needle",
"output_mode": "summary"
});
args.as_object_mut()
.unwrap()
.extend(controls.as_object().unwrap().clone());
let result = GrepTool.execute(&args, &ctx).await.unwrap();
assert!(!result.success);
assert!(result.content.contains("supported only"));
}
}

#[cfg(unix)]
#[tokio::test]
async fn test_grep_source_anchor_preserves_newline_filename_without_injection() {
Expand Down
Loading
Loading