roder-tools 0.1.0

Agentic software development tools and SDKs for Roder.
Documentation
use roder_api::plan_review::{
    HunkDiffLine, HunkDiffLineKind, HunkRecord, HunkRollbackState, MAX_HUNK_DIFF_LINES,
};
use roder_api::tools::{ToolCall, ToolExecutionContext};
use time::OffsetDateTime;

pub(crate) fn hunk_id(call: &ToolCall, index: usize) -> String {
    format!("{}-hunk-{}", call.id, index + 1)
}

pub(crate) fn record(
    ctx: &ToolExecutionContext,
    call: &ToolCall,
    index: usize,
    path: impl Into<String>,
    old_lines: Vec<String>,
    new_lines: Vec<String>,
) -> HunkRecord {
    let path = path.into();
    let mut diff = Vec::new();
    for (old_line, line) in (1u32..).zip(old_lines.iter()) {
        diff.push(HunkDiffLine {
            kind: HunkDiffLineKind::Removed,
            text: line.clone(),
            old_line: Some(old_line),
            new_line: None,
        });
    }
    for (new_line, line) in (1u32..).zip(new_lines.iter()) {
        diff.push(HunkDiffLine {
            kind: HunkDiffLineKind::Added,
            text: line.clone(),
            old_line: None,
            new_line: Some(new_line),
        });
    }
    if diff.len() > MAX_HUNK_DIFF_LINES {
        diff.truncate(MAX_HUNK_DIFF_LINES);
    }

    HunkRecord {
        id: hunk_id(call, index),
        thread_id: ctx.thread_id.clone(),
        turn_id: ctx.turn_id.clone(),
        path: path.clone(),
        old_start: 1,
        old_lines: old_lines.len() as u32,
        new_start: 1,
        new_lines: new_lines.len() as u32,
        diff,
        tool_call_id: call.id.clone(),
        tool_name: call.name.clone(),
        plan_review_id: None,
        plan_step_id: None,
        timeline_event_id: None,
        checkpoint_id: None,
        rollback: HunkRollbackState::Available,
        reverse_patch: Some(reverse_codex_patch(&path, &old_lines, &new_lines)),
        created_at: OffsetDateTime::UNIX_EPOCH,
    }
}

pub(crate) fn from_core(
    ctx: &ToolExecutionContext,
    call: &ToolCall,
    index: usize,
    hunk: roder_edit_core::EditHunk,
) -> HunkRecord {
    let mut diff = hunk
        .diff
        .into_iter()
        .map(|line| HunkDiffLine {
            kind: match line.kind {
                roder_edit_core::HunkDiffLineKind::Context => HunkDiffLineKind::Context,
                roder_edit_core::HunkDiffLineKind::Added => HunkDiffLineKind::Added,
                roder_edit_core::HunkDiffLineKind::Removed => HunkDiffLineKind::Removed,
            },
            text: line.text,
            old_line: line.old_line,
            new_line: line.new_line,
        })
        .collect::<Vec<_>>();
    if diff.len() > MAX_HUNK_DIFF_LINES {
        diff.truncate(MAX_HUNK_DIFF_LINES);
    }

    HunkRecord {
        id: hunk_id(call, index),
        thread_id: ctx.thread_id.clone(),
        turn_id: ctx.turn_id.clone(),
        path: hunk.path,
        old_start: hunk.old_start,
        old_lines: hunk.old_lines,
        new_start: hunk.new_start,
        new_lines: hunk.new_lines,
        diff,
        tool_call_id: call.id.clone(),
        tool_name: call.name.clone(),
        plan_review_id: None,
        plan_step_id: None,
        timeline_event_id: None,
        checkpoint_id: None,
        rollback: HunkRollbackState::Available,
        reverse_patch: hunk.reverse_patch,
        created_at: OffsetDateTime::UNIX_EPOCH,
    }
}

fn reverse_codex_patch(path: &str, old_lines: &[String], new_lines: &[String]) -> String {
    let mut patch = String::from("*** Begin Patch\n");
    patch.push_str(&format!("*** Update File: {path}\n@@\n"));
    for line in new_lines {
        patch.push('-');
        patch.push_str(line);
        patch.push('\n');
    }
    for line in old_lines {
        patch.push('+');
        patch.push_str(line);
        patch.push('\n');
    }
    patch.push_str("*** End Patch\n");
    patch
}

#[cfg(test)]
mod tests {
    use super::*;
    use roder_api::policy_mode::PolicyMode;
    use serde_json::json;

    #[test]
    fn hunk_record_preserves_tool_and_turn_ids() {
        let ctx = ToolExecutionContext::new("thread-1", "turn-1", PolicyMode::Default);
        let call = ToolCall {
            id: "tool-1".to_string(),
            name: "edit".to_string(),
            arguments: json!({}),
            raw_arguments: "{}".to_string(),
            thread_id: "thread-1".to_string(),
            turn_id: "turn-1".to_string(),
        };
        let hunk = record(
            &ctx,
            &call,
            0,
            "src/lib.rs",
            vec!["old".to_string()],
            vec!["new".to_string()],
        );

        assert_eq!(hunk.id, "tool-1-hunk-1");
        assert_eq!(hunk.thread_id, "thread-1");
        assert_eq!(hunk.tool_name, "edit");
        assert_eq!(hunk.diff.len(), 2);
    }
}