Skip to content
Open
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
07dc8dc
droid research (vibe-kanban 054135e9)
britannio Oct 18, 2025
f216943
begin droid
britannio Oct 18, 2025
270ed2c
add plan
britannio Oct 18, 2025
5e9a38f
droid implementation (vibe-kanban 90e6c8f6)
britannio Oct 18, 2025
f4e0a71
document droid (vibe-kanban 0a7f8590)
britannio Oct 18, 2025
66cb71a
red gh action (vibe-kanban f0c8b6c4)
britannio Oct 18, 2025
8c2d5c7
droid | settings bug (vibe-kanban 7deec8df)
britannio Oct 20, 2025
2932b31
glob
britannio Oct 20, 2025
4274441
tool call parsing & display (vibe-kanban e3f65a74)
britannio Oct 20, 2025
1d24e0b
show droid model (vibe-kanban 8fdbc630)
britannio Oct 20, 2025
0f91716
reliable apply patch display (vibe-kanban 3710fb65)
britannio Oct 20, 2025
15010bc
droid failed tool call handling (vibe-kanban bd7feddb)
britannio Oct 20, 2025
facc3ca
droid default (vibe-kanban 2f8a19cc)
britannio Oct 20, 2025
2370484
droid globbing rendering (vibe-kanban 76d372ea)
britannio Oct 20, 2025
bc5bc0e
droid todo list text (vibe-kanban b1bdeffc)
britannio Oct 20, 2025
a579b1d
droid workspace path (vibe-kanban 0486b74a)
britannio Oct 20, 2025
2082dbf
mcp settings (vibe-kanban 2031d8f4)
britannio Oct 20, 2025
483309d
clean up (vibe-kanban 6b1a8e2e)
britannio Oct 20, 2025
10dc8b1
delete droid json
britannio Oct 20, 2025
2cdc256
droid agent code review (vibe-kanban 6820ffd1)
britannio Oct 20, 2025
6b77771
remove unnecessary v1 change
britannio Oct 20, 2025
08ad6ee
updated droid.json schema
britannio Oct 20, 2025
e28fd06
tweak command
britannio Oct 21, 2025
60074cf
droid model suggestions (vibe-kanban 120f87d2)
britannio Oct 21, 2025
d2fb406
remove dead code
britannio Oct 21, 2025
b85506d
droid automated testing (vibe-kanban f836b4a4)
britannio Oct 21, 2025
ea05ee7
create exec_command_with_prompt
britannio Oct 21, 2025
981ae19
Add logging to error paths in action_mapper.rs (vibe-kanban 76cc5d71)
britannio Oct 21, 2025
550ae17
droid automated testing (DroidJSON -> NormalizedEntry) (vibe-kanban c…
britannio Oct 21, 2025
df25928
preserve timestamp
britannio Oct 21, 2025
90b6765
droid reasoning effort (vibe-kanban 47dae2db)
britannio Oct 21, 2025
c050258
droid path (vibe-kanban d8370535)
britannio Oct 21, 2025
64a6658
Merge branch 'main' into britannio/droid-agent
britannio Oct 21, 2025
adfa999
fix warning
britannio Oct 21, 2025
fc31a48
fix warning
britannio Oct 21, 2025
b56cad2
whitespace update
britannio Oct 23, 2025
93cba87
DomainEvent -> LogEvent
britannio Oct 23, 2025
fa7245c
Merge branch 'main' into britannio/droid-agent
Oct 23, 2025
d833449
remove msg store stream -> line converter
britannio Oct 24, 2025
d7eaa40
normalise the diff generated when the droid ApplyPatch tool call is
britannio Oct 24, 2025
b8b3b66
refactor process_event to mutate a reference to ProcessorState
britannio Oct 24, 2025
2fffa79
remove EntryIndexProvider abstraction
britannio Oct 24, 2025
442c669
remove dead code
britannio Oct 24, 2025
95147ca
remove JSON indirection when invoking extract_path_from_patch
britannio Oct 25, 2025
80f8b7e
converting DroidJson -> LogEvent produces Option instead of Vec
britannio Oct 25, 2025
ef54342
simplify droid build_command_builder
britannio Oct 25, 2025
cebf2ad
simplify droid types tests
britannio Oct 25, 2025
3eb4374
remove droid type tests
britannio Oct 26, 2025
722aa32
rename events.rs -> log_event_converter.rs
britannio Oct 26, 2025
0c6d7d7
add error log for failed parsing of DroidJson
britannio Oct 26, 2025
264896f
update snapshots
britannio Oct 26, 2025
b23a120
Fix clippy warnings in droid executor
britannio Oct 26, 2025
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 crates/db/src/models/executor_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub struct ExecutorSession {
pub id: Uuid,
pub task_attempt_id: Uuid,
pub execution_process_id: Uuid,
pub session_id: Option<String>, // External session ID from Claude/Amp
pub session_id: Option<String>, // External session ID from Claude/Amp/Droid
pub prompt: Option<String>, // The prompt sent to the executor
pub summary: Option<String>, // Final assistant message/summary
pub created_at: DateTime<Utc>,
Expand Down
4 changes: 4 additions & 0 deletions crates/executors/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ enum_dispatch = "0.3.13"
futures-io = "0.3.31"
tokio-stream = { version = "0.1.17", features = ["io-util"] }
futures = "0.3.31"
async-stream = "0.3"
bon = "3.6"
fork_stream = "0.1.0"
os_pipe = "1.2"
Expand All @@ -48,3 +49,6 @@ codex-app-server-protocol = { git = "https://github.com/openai/codex.git", packa
codex-mcp-types = { git = "https://github.com/openai/codex.git", package = "mcp-types", rev = "488ec061bf4d36916b8f477c700ea4fde4162a7a" }
sha2 = "0.10"
derivative = "2.2.0"

[dev-dependencies]
insta = { version = "1.40", features = ["yaml"] }
7 changes: 7 additions & 0 deletions crates/executors/default_profiles.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,13 @@
"model": "claude-sonnet-4"
}
}
},
"DROID": {
"DEFAULT": {
"DROID": {
"autonomy": "skip-permissions-unsafe"
}
}
}
}
}
331 changes: 331 additions & 0 deletions crates/executors/src/executors/droid/action_mapper.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
use std::path::Path;

use serde_json::Value;
use workspace_utils::path::make_path_relative;

use super::types::DroidToolData;
use crate::logs::{ActionType, FileChange, TodoItem};

pub fn generate_concise_content(tool_name: &str, action_type: &ActionType) -> String {
match action_type {
ActionType::FileRead { path } => format!("`{path}`"),
ActionType::FileEdit { path, .. } => format!("`{path}`"),
ActionType::CommandRun { command, .. } => format!("`{command}`"),
ActionType::Search { query } => format!("`{query}`"),
ActionType::WebFetch { url } => format!("`{url}`"),
ActionType::TodoManagement { .. } => "TODO list updated".to_string(),
ActionType::Other { description } => description.clone(),
_ => tool_name.to_string(),
}
}

pub fn map_tool_to_action(tool_name: &str, params: &Value, worktree_path: &Path) -> ActionType {
let tool_json = serde_json::json!({
"toolName": tool_name,
"parameters": params
});

let tool_data: DroidToolData = match serde_json::from_value(tool_json) {
Ok(data) => data,
Err(e) => {
tracing::warn!(
tool_name = %tool_name,
error = %e,
"Failed to parse DroidToolData from tool parameters"
);
return ActionType::Other {
description: tool_name.to_string(),
};
}
};

let worktree_path_str = worktree_path.to_string_lossy();

match tool_data {
DroidToolData::Read { file_path } => ActionType::FileRead {
path: make_path_relative(&file_path, &worktree_path_str),
},
DroidToolData::LS { directory_path, .. } => ActionType::FileRead {
path: make_path_relative(&directory_path, &worktree_path_str),
},
DroidToolData::Glob { patterns, .. } => ActionType::Search {
query: patterns.join(", "),
},
DroidToolData::Grep { path, .. } => ActionType::FileRead {
path: path
.map(|p| make_path_relative(&p, &worktree_path_str))
.unwrap_or_default(),
},
DroidToolData::Execute { command, .. } => ActionType::CommandRun {
command,
result: None,
},
DroidToolData::Edit { file_path, .. } => ActionType::FileEdit {
path: make_path_relative(&file_path, &worktree_path_str),
changes: vec![],
},
DroidToolData::MultiEdit { file_path, .. } => ActionType::FileEdit {
path: make_path_relative(&file_path, &worktree_path_str),
changes: vec![],
},
DroidToolData::Create { file_path, .. } => ActionType::FileEdit {
path: make_path_relative(&file_path, &worktree_path_str),
changes: vec![],
},
DroidToolData::ApplyPatch { input } => {
let path = extract_path_from_patch(&serde_json::json!({ "input": input }));
ActionType::FileEdit {
path: make_path_relative(&path, &worktree_path_str),
changes: vec![],
}
}
DroidToolData::TodoWrite { todos } => {
let todo_items = todos
.into_iter()
.map(|item| TodoItem {
content: item.content,
status: item.status,
priority: item.priority,
})
.collect();
ActionType::TodoManagement {
todos: todo_items,
operation: "update".to_string(),
}
}
DroidToolData::WebSearch { query, .. } => ActionType::WebFetch { url: query },
DroidToolData::FetchUrl { url, .. } => ActionType::WebFetch { url },
DroidToolData::ExitSpecMode { .. } => ActionType::Other {
description: "ExitSpecMode".to_string(),
},
DroidToolData::SlackPostMessage { .. } => ActionType::Other {
description: "SlackPostMessage".to_string(),
},
DroidToolData::Unknown { .. } => ActionType::Other {
description: tool_name.to_string(),
},
}
}

fn extract_path_from_patch(params: &Value) -> String {
if let Some(input) = params.get("input").and_then(|v| v.as_str()) {
for line in input.lines() {
if line.starts_with("*** Update File:") || line.starts_with("*** Create File:") {
return line
.split(':')
.nth(1)
.map(|s| s.trim().to_string())
.unwrap_or_default();
}
}
}
String::new()
}

pub fn parse_apply_patch_result(value: &Value, worktree_path: &str) -> Option<ActionType> {
let parsed_value;
let result_obj = if value.is_object() {
value
} else if let Some(s) = value.as_str() {
match serde_json::from_str::<Value>(s) {
Ok(v) => {
parsed_value = v;
&parsed_value
}
Err(e) => {
tracing::warn!(
error = %e,
input = %s,
"Failed to parse apply_patch result string as JSON"
);
return None;
}
}
} else {
tracing::warn!(
value_type = ?value,
"apply_patch result is neither object nor string"
);
return None;
};

let file_path = result_obj
.get("file_path")
.or_else(|| result_obj.get("value").and_then(|v| v.get("file_path")))
.and_then(|v| v.as_str())
.map(|s| s.to_string());

if file_path.is_none() {
tracing::warn!(
result = ?result_obj,
"apply_patch result missing file_path field"
);
return None;
}

let file_path = file_path?;

let diff = result_obj
.get("diff")
.or_else(|| result_obj.get("value").and_then(|v| v.get("diff")))
.and_then(|v| v.as_str())
.map(|s| s.to_string());

let content = result_obj
.get("content")
.or_else(|| result_obj.get("value").and_then(|v| v.get("content")))
.and_then(|v| v.as_str())
.map(|s| s.to_string());

let changes = if let Some(diff_text) = diff {
vec![FileChange::Edit {
unified_diff: diff_text,
Copy link
Contributor

Choose a reason for hiding this comment

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

normalize the diff using diff::extract_unified_diff_hunks

has_line_numbers: true,
}]
} else if let Some(content_text) = content {
vec![FileChange::Write {
content: content_text,
}]
} else {
vec![]
};

Some(ActionType::FileEdit {
path: make_path_relative(&file_path, worktree_path),
changes,
})
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_parse_apply_patch_with_diff() {
let value = serde_json::json!({
"success": true,
"file_path": "/test/file.py",
"diff": "--- previous\t\n+++ current\t\n@@ -1,3 +1,5 @@\n def hello():\n+ print('world')\n pass"
});

let result = parse_apply_patch_result(&value, "/test");

assert!(result.is_some());
if let Some(ActionType::FileEdit { path, changes }) = result {
assert_eq!(path, "file.py");
assert_eq!(changes.len(), 1);
if let FileChange::Edit {
unified_diff,
has_line_numbers,
} = &changes[0]
{
assert!(unified_diff.contains("def hello()"));
assert!(unified_diff.contains("print('world')"));
assert!(has_line_numbers);
} else {
panic!("Expected FileChange::Edit");
}
} else {
panic!("Expected ActionType::FileEdit");
}
}

#[test]
fn test_parse_apply_patch_with_content() {
let value = serde_json::json!({
"success": true,
"file_path": "/test/new_file.txt",
"content": "Hello, world!"
});

let result = parse_apply_patch_result(&value, "/test");

assert!(result.is_some());
if let Some(ActionType::FileEdit { path, changes }) = result {
assert_eq!(path, "new_file.txt");
assert_eq!(changes.len(), 1);
if let FileChange::Write { content } = &changes[0] {
assert_eq!(content, "Hello, world!");
} else {
panic!("Expected FileChange::Write");
}
} else {
panic!("Expected ActionType::FileEdit");
}
}

#[test]
fn test_parse_apply_patch_with_nested_value() {
let value = serde_json::json!({
"value": {
"success": true,
"file_path": "/test/nested.py",
"diff": "--- a\n+++ b\n@@ -1 +1,2 @@\n line1\n+line2"
}
});

let result = parse_apply_patch_result(&value, "/test");

assert!(result.is_some());
if let Some(ActionType::FileEdit { path, changes }) = result {
assert_eq!(path, "nested.py");
assert_eq!(changes.len(), 1);
} else {
panic!("Expected ActionType::FileEdit");
}
}

#[test]
fn test_parse_apply_patch_from_json_string() {
let value = serde_json::json!(
r#"{"success":true,"file_path":"/test/file.txt","content":"test content"}"#
);

let result = parse_apply_patch_result(&value, "/test");

assert!(result.is_some());
if let Some(ActionType::FileEdit { path, changes }) = result {
assert_eq!(path, "file.txt");
assert_eq!(changes.len(), 1);
} else {
panic!("Expected ActionType::FileEdit");
}
}

#[test]
fn test_parse_apply_patch_missing_file_path() {
let value = serde_json::json!({
"success": true,
"content": "some content"
});

let result = parse_apply_patch_result(&value, "/test");

assert!(
result.is_none(),
"Should return None when file_path is missing"
);
}

#[test]
fn test_parse_apply_patch_no_diff_or_content() {
let value = serde_json::json!({
"success": true,
"file_path": "/test/empty.txt"
});

let result = parse_apply_patch_result(&value, "/test");

assert!(result.is_some());
if let Some(ActionType::FileEdit { path, changes }) = result {
assert_eq!(path, "empty.txt");
assert_eq!(
changes.len(),
0,
"Should have empty changes when neither diff nor content is present"
);
} else {
panic!("Expected ActionType::FileEdit");
}
}
}
Loading
Loading