Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d9ee904
feat(tools): add hashline_edit with snapshot-tagged reads
matthewyjiang Aug 4, 2026
7957808
fix(test): canonicalize workspace root in outside-path automation assert
matthewyjiang Aug 4, 2026
44558b1
fix(tools): plan hashline ops by anchor line and drop the double parse
matthewyjiang Aug 4, 2026
c475d16
feat(tools): replace edit_file and apply_patch with hashline edit
matthewyjiang Aug 5, 2026
6768bfd
feat(tools): add snapshot store, grep tags, and hashline recovery
matthewyjiang Aug 5, 2026
91c1065
feat(prompt): prefer grep tool for hashline edit chaining
matthewyjiang Aug 5, 2026
8c30e29
refactor(tools): drop hashline session store and recovery
matthewyjiang Aug 5, 2026
6cbec8f
refactor(tools): own hashline wire format and proposed edit cards
matthewyjiang Aug 5, 2026
a8b7491
refactor(tools): harden hashline edit presentation and format pipeline
matthewyjiang Aug 5, 2026
b7a91ca
refactor(tools): split edit plan cards and harden hashline contracts
matthewyjiang Aug 5, 2026
c0aeaf2
fix(tools): harden hashline edit cards and grep chain paths
matthewyjiang Aug 5, 2026
9a72c71
fix(tests): update hashline card and explorer fingerprints for CI
matthewyjiang Aug 5, 2026
2c59bd5
refactor(tools): rename write_file tool to write
matthewyjiang Aug 5, 2026
e04050b
refactor(tools): split hashline execute and clarify edit preview modes
matthewyjiang Aug 5, 2026
6e4e473
fix(tools): drop unused format_post_edit_preview re-export
matthewyjiang Aug 5, 2026
e14f4a4
fix(tools): harden hashline edit review findings
matthewyjiang Aug 5, 2026
8c9c861
docs: remove hashline edit dev eval notes
matthewyjiang Aug 5, 2026
cb0ef19
fix(tools): make multi-file rollback test portable on Windows
matthewyjiang Aug 5, 2026
2f89722
fix(tools): restore original perms in multi-file rollback test
matthewyjiang Aug 6, 2026
1484b85
fix(tui-pty): wait for cancel-edit fixture before Esc
matthewyjiang Aug 6, 2026
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
2 changes: 1 addition & 1 deletion .agents/skills/rho-tui-herdr-testing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ The fixture is available only in debug builds. Use these exact prompts:
| Prompt | Deterministic behavior |
| --- | --- |
| `fixture stream` | Streams two reasoning chunks and two assistant output chunks with short delays. |
| `fixture tool` | Streams and executes a `write_file` call, then reports exactly one tool result. |
| `fixture tool` | Streams and executes a `write` call, then reports exactly one tool result. |
| `fixture questionnaire` | Opens a red/blue questionnaire and reports exactly-once host input delivery. |
| `fixture progress tool` | Runs `tui_fixture_progress`, emits two progress updates, and returns a fixed result. |
| `fixture steering` | Keeps a turn open for two seconds so queued input or steering can be exercised. |
Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/rho-tui-pty-testing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Scenarios use `RHO_TUI_TEST_MODE=matrix` automatically. Exact prompts:
| Prompt | Deterministic behavior |
| --- | --- |
| `fixture stream` | Streams reasoning and assistant chunks |
| `fixture tool` | Writes `.rho-tui-fixture-output.txt` via `write_file` |
| `fixture tool` | Writes `.rho-tui-fixture-output.txt` via `write` |
| `fixture questionnaire` | Red/blue questionnaire |
| `fixture progress tool` | Progress updates then fixed result |
| `fixture delay` | Partial output, long wait for cancellation |
Expand Down
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ fn request_body_removes_top_level_schema_composition_from_tools() {
ModelRequest {
messages: &[Message::user_text("hello")],
tools: &[ToolSpec {
name: "apply_patch".into(),
name: "edit".into(),
description: "edit files".into(),
input_schema: json!({
"type": "object",
Expand Down
2 changes: 1 addition & 1 deletion crates/rho-providers/src/providers/automation_fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ impl ModelProvider for AutomationFixtureProvider {
Mode::ReadPath => completed(last_tool_result(&request)?),
Mode::WritePath if turn == 0 => completed_tool_call(
"fixture-write-path",
"write_file",
"write",
serde_json::json!({
"path": required_env(PATH_ENV)?,
"content": "written outside the working directory",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::{

fn sample_tool() -> ToolSpec {
ToolSpec {
name: "apply_patch".into(),
name: "edit".into(),
description: "test tool".into(),
input_schema: serde_json::json!({
"type": "object",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use crate::model::ToolSpec;

fn sample_tool() -> ToolSpec {
ToolSpec {
name: "apply_patch".into(),
name: "edit".into(),
description: "test tool".into(),
input_schema: json!({"type":"object","properties":{}}),
}
Expand Down
14 changes: 6 additions & 8 deletions crates/rho-providers/src/providers/tui_fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::{
time::Duration,
};

mod apply_patch;
mod edit;

use rho_sdk::{
model::{
Expand Down Expand Up @@ -242,7 +242,7 @@ async fn fixture_stream(
.send(ModelEvent::ToolCallDelta {
index: 0,
id: Some(TOOL_CALL_ID.into()),
name: Some("write_file".into()),
name: Some("write".into()),
arguments: "{\"path\":\".rho-tui-fixture-output.txt\",".into(),
})
.await?;
Expand All @@ -255,12 +255,10 @@ async fn fixture_stream(
arguments: "\"content\":\"deterministic tool output\\n\"}".into(),
})
.await?;
completed_tool_call(TOOL_CALL_ID, "write_file", arguments)
completed_tool_call(TOOL_CALL_ID, "write", arguments)
}
apply_patch::PROMPT if apply_patch::is_pending(&request) => {
apply_patch::stream(&request, &events).await
}
apply_patch::CANCEL_PROMPT => apply_patch::stream_until_cancelled(&request, &events).await,
edit::PROMPT if edit::is_pending(&request) => edit::stream(&request, &events).await,
edit::CANCEL_PROMPT => edit::stream_until_cancelled(&request, &events).await,
"fixture questionnaire" if tool_result(&request, QUESTIONNAIRE_CALL_ID).is_none() => {
completed_tool_call(
QUESTIONNAIRE_CALL_ID,
Expand Down Expand Up @@ -599,7 +597,7 @@ fn fixture_response(request: &ModelRequest<'_>) -> Result<ModelResponse, Provide
result.content.lines().next().unwrap_or_default()
));
}
if let Some(text) = apply_patch::completion_text(request) {
if let Some(text) = edit::completion_text(request) {
return completed(text);
}
if let (Some(slow), Some(fast)) = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ use rho_sdk::{

use super::{completed_tool_call, fixture_sleep, tool_result};

pub(super) const PROMPT: &str = "fixture apply patch";
pub(super) const CANCEL_PROMPT: &str = "fixture cancel apply patch";
const CALL_ID: &str = "tui-fixture-apply-patch";
const CANCEL_CALL_ID: &str = "tui-fixture-cancel-apply-patch";
pub(super) const PROMPT: &str = "fixture edit";
pub(super) const CANCEL_PROMPT: &str = "fixture cancel edit";
const CALL_ID: &str = "tui-fixture-edit";
const CANCEL_CALL_ID: &str = "tui-fixture-cancel-edit";
const ORIGINAL: &str = "original line\n";
// FNV-1a tag for ORIGINAL via rho_tools::hashline::compute_file_hash.
const ORIGINAL_TAG: &str = "8022";

pub(super) fn is_pending(request: &ModelRequest<'_>) -> bool {
tool_result(request, CALL_ID).is_none()
Expand All @@ -21,12 +24,12 @@ pub(super) async fn stream(
request: &ModelRequest<'_>,
events: &ProviderEventSender,
) -> Result<ModelResponse, ProviderError> {
stream_patch(
stream_edit(
request,
events,
CALL_ID,
".rho-tui-fixture-patch.txt",
"streamed patch line",
".rho-tui-fixture-edit.txt",
"streamed edit line",
Duration::from_millis(750),
/*complete_after_sleep*/ true,
)
Expand All @@ -37,36 +40,54 @@ pub(super) async fn stream_until_cancelled(
request: &ModelRequest<'_>,
events: &ProviderEventSender,
) -> Result<ModelResponse, ProviderError> {
stream_patch(
stream_edit(
request,
events,
CANCEL_CALL_ID,
".rho-tui-fixture-cancelled-patch.txt",
"cancelled patch line",
".rho-tui-fixture-cancelled-edit.txt",
"cancelled edit line",
Duration::from_secs(30),
/*complete_after_sleep*/ false,
)
.await
}

async fn stream_patch(
async fn stream_edit(
request: &ModelRequest<'_>,
events: &ProviderEventSender,
call_id: &str,
file_name: &str,
patch_line: &str,
edit_line: &str,
sleep: Duration,
complete_after_sleep: bool,
) -> Result<ModelResponse, ProviderError> {
let input = format!("*** Begin Patch\n*** Add File: {file_name}\n+{patch_line}\n*** End Patch");
// Seed the target so the real edit tool can apply against a known tag.
let cwd = std::env::current_dir().map_err(|error| {
ProviderError::new(
rho_sdk::ProviderErrorKind::Other,
format!("fixture setup: current_dir failed: {error}"),
rho_sdk::Retryability::Permanent,
)
})?;
let target = cwd.join(file_name);
std::fs::write(&target, ORIGINAL).map_err(|error| {
ProviderError::new(
rho_sdk::ProviderErrorKind::Other,
format!(
"fixture setup: could not write '{}': {error}",
target.display()
),
rho_sdk::Retryability::Permanent,
)
})?;
let input = format!("[{file_name}#{ORIGINAL_TAG}]\nPUT 1.=1:\n+{edit_line}\n");
let open = format!("{{\"input\":\"[{file_name}#{ORIGINAL_TAG}]\\nPUT 1.=1:\\n+{edit_line}\\n");
events
.send(ModelEvent::ToolCallDelta {
index: 0,
id: None,
name: Some("apply_patch".into()),
arguments: format!(
"{{\"input\":\"*** Begin Patch\\n*** Add File: {file_name}\\n+{patch_line}\\n"
),
name: Some("edit".into()),
arguments: open,
})
.await?;
events
Expand All @@ -84,17 +105,17 @@ async fn stream_patch(
index: 0,
id: None,
name: None,
arguments: r#"*** End Patch"}"#.into(),
arguments: r#""}"#.into(),
})
.await?;
}
completed_tool_call(call_id, "apply_patch", serde_json::json!({"input": input}))
completed_tool_call(call_id, "edit", serde_json::json!({"input": input}))
}

pub(super) fn completion_text(request: &ModelRequest<'_>) -> Option<String> {
let result = tool_result(request, CALL_ID)?;
Some(format!(
"patch lifecycle complete with one result: {}",
"edit lifecycle complete with one result: {}",
result.content.lines().next().unwrap_or_default()
))
}
4 changes: 2 additions & 2 deletions crates/rho-sdk/src/hooks/envelope_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ fn before_tool_use_wire_shape_is_stable() {
#[test]
fn after_tool_use_wire_shape_is_stable() {
let payload = HookPayload::AfterToolUse(AfterToolUsePayload {
tool: tool("edit_file", Some("call-2")),
tool: tool("edit", Some("call-2")),
status: HookToolStatus::Succeeded,
failure: None,
duration_ms: Some(42),
Expand All @@ -112,7 +112,7 @@ fn after_tool_use_wire_shape_is_stable() {
assert_eq!(
wire_shape(&envelope(payload))["payload"],
json!({
"tool": { "name": "edit_file", "call_id": "call-2" },
"tool": { "name": "edit", "call_id": "call-2" },
"status": "succeeded",
"failure": null,
"duration_ms": 42,
Expand Down
12 changes: 6 additions & 6 deletions crates/rho-sdk/src/orchestration/stream_capture_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,22 +96,22 @@ fn tool_call_arguments_complete_across_nested_and_escaped_fragments() {
capture_tool_delta(&mut capture, 2, None, None, &character.to_string());
}
capture_tool_delta(&mut capture, 2, Some("call-2"), None, "");
capture_tool_delta(&mut capture, 2, None, Some("write_file"), "");
capture_tool_delta(&mut capture, 2, None, Some("write"), "");

let aborted = capture.into_aborted_assistant().unwrap();
assert_eq!(
aborted.content,
vec![ContentBlock::ToolCall(ToolCall {
id: "call-2".into(),
name: "write_file".into(),
name: "write".into(),
arguments: serde_json::from_str(arguments).unwrap(),
})]
);
assert_eq!(
aborted.tool_calls,
vec![PartialToolCall {
id: Some("call-2".into()),
name: Some("write_file".into()),
name: Some("write".into()),
arguments: arguments.into(),
}]
);
Expand Down Expand Up @@ -160,7 +160,7 @@ fn multi_chunk_object_arguments_materialize_on_aborted_capture() {
while offset < arguments.len() {
let end = (offset + CHUNK_BYTES).min(arguments.len());
let id = first.then_some("call-large");
let name = first.then_some("write_file");
let name = first.then_some("write");
capture_tool_delta(&mut capture, 0, id, name, &arguments[offset..end]);
first = false;
offset = end;
Expand All @@ -171,15 +171,15 @@ fn multi_chunk_object_arguments_materialize_on_aborted_capture() {
aborted.content,
vec![ContentBlock::ToolCall(ToolCall {
id: "call-large".into(),
name: "write_file".into(),
name: "write".into(),
arguments: serde_json::from_str(&arguments).unwrap(),
})]
);
assert_eq!(
aborted.tool_calls,
vec![PartialToolCall {
id: Some("call-large".into()),
name: Some("write_file".into()),
name: Some("write".into()),
arguments,
}]
);
Expand Down
4 changes: 2 additions & 2 deletions crates/rho-sdk/src/workspace_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ fn capabilities_are_independent_and_network_urls_are_strict() {
CapabilityRequest::write_path(
"/workspace/file",
PathScope::PrimaryWorkspace,
source("write_file"),
source("write"),
),
process_request("cargo test"),
CapabilityRequest::instruction_discovery(
Expand Down Expand Up @@ -309,7 +309,7 @@ fn default_policy_denies_every_capability_class() {
CapabilityRequest::write_path(
"/workspace/file",
PathScope::PrimaryWorkspace,
source("write_file"),
source("write"),
),
process_request("cargo test"),
CapabilityRequest::network(NetworkTarget::ToolManaged, source("web_search")),
Expand Down
1 change: 0 additions & 1 deletion crates/rho-tools/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "2"
async-trait = "0.1"
same-file = "1"
similar = "2"
uuid = { version = "1", features = ["v4"] }
Expand Down
10 changes: 8 additions & 2 deletions crates/rho-tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@
agent and adapters for registering them with `rho-sdk`. The crate is imported as
`rho_agent_tools`.

The built-in tools cover `read_file`, `write_file`, `edit_file`, `apply_patch`, `list_dir`,
`grep`, and `glob`, with shared diff generation and output limiting.
The built-in tools cover `read_file`, `write`, `edit` (hashline),
`list_dir`, `grep`, and `glob`, with shared diff generation and output limiting.
`read_file` returns UTF-8 sources as hashline views for `edit`.
`grep` content mode also mints chainable `[path#TAG]` headers on matching files
(via hashline) plus `N | text` match previews. Copy TAG and line numbers into
`edit`; do not treat preview bodies as hashline line text.
Successful `edit` results include a post-edit `[path#TAG]` numbered preview so a
follow-up edit can chain without an immediate re-read.
`coding_tools` constructs their SDK adapters, while `shell_tool` constructs the
platform shell adapter (`bash` on Linux and macOS, PowerShell on Windows).

Expand Down
Loading
Loading