feat: update README and documentation for new tools and features

- Updated README.md to reflect the addition of 3 new built-in tools, bringing the total to 37.
- Revised architecture documentation to indicate the increase in tool count.
- Enhanced backend documentation with updated line counts for various modules.
- Modified data documentation to change edit log format from JSON to JSONL.
- Updated dependencies documentation to reflect version upgrades for several crates.
- Improved prompts for auto-reviewer, division implementer, planner, tester, and quality reviewer to enforce stricter coding standards regarding linter bypasses.
- Refactored code in various modules to improve clarity and performance, including updates to error handling and tool execution logic.
- Added comprehensive tests for IPC frame serialization and deserialization.
This commit is contained in:
asepharyana
2026-07-13 14:39:39 +07:00
parent 00e29139c5
commit 1d50b94eec
23 changed files with 327 additions and 176 deletions
+1 -1
View File
@@ -430,7 +430,7 @@ mod tests {
return Some(Verdict::Allow);
}
if l.starts_with("verdict: block") {
let reason = line.split_once(':').map(|x| x.1).unwrap_or("blocked").trim().to_string();
let reason = line.split_once(':').map_or("blocked", |x| x.1).trim().to_string();
return Some(Verdict::Block(reason));
}
}
+57 -44
View File
@@ -1017,8 +1017,7 @@ fn run_agent_turn(
q.push_back(TurnEvent::SystemNote {
kind: "pipeline".to_string(),
message: format!(
"CEO is planning workflow (mode={})...",
mode_label,
"CEO is planning workflow (mode={mode_label})...",
),
});
}
@@ -1054,12 +1053,11 @@ fn run_agent_turn(
);
let user_msg = ChatMessage::user(format!(
"Design a structured multi-agent workflow plan for the following task:\n\n\
\"{}\"\n\n\
You must output a JSON object representing the 'specialists' configuration for {}.\n\
\"{user_request}\"\n\n\
You must output a JSON object representing the 'specialists' configuration for {required_divisions}.\n\
Each division must have a list of custom specialists defined by a pair of [label, focus_description].\n\n\
Return ONLY a JSON object with this exact structure, with no markdown codeblocks and no explanation:\n\
{}",
user_request, required_divisions, example_json
{example_json}"
));
let planner_result = tc.client.chat_with_tools_non_streaming(&[system_msg, user_msg], None);
@@ -1070,7 +1068,7 @@ fn run_agent_turn(
let mut lines = reply_text.lines();
lines.next();
let mut content = lines.collect::<Vec<&str>>();
if content.last().map(|s| s.trim() == "```").unwrap_or(false) {
if content.last().is_some_and(|s| s.trim() == "```") {
content.pop();
}
content.join("\n")
@@ -1103,7 +1101,7 @@ fn run_agent_turn(
&tc.workspace_roots,
Some(events_q),
&pipeline_abort,
custom_specialists,
&custom_specialists,
)
} else {
crate::app::workflow::company::run_company_pipeline_quick(
@@ -1112,14 +1110,14 @@ fn run_agent_turn(
&tc.workspace_roots,
Some(events_q),
&pipeline_abort,
custom_specialists,
&custom_specialists,
)
}
}
Err(e) => Err(anyhow::anyhow!("Failed to parse LLM planning JSON: {}. Cleaned JSON was: {}", e, clean_json)),
Err(e) => Err(anyhow::anyhow!("Failed to parse LLM planning JSON: {e}. Cleaned JSON was: {clean_json}")),
}
}
Err(e) => Err(anyhow::anyhow!("Failed to query LLM for planning workflow: {}", e)),
Err(e) => Err(anyhow::anyhow!("Failed to query LLM for planning workflow: {e}")),
};
match pipeline_result {
@@ -1304,44 +1302,60 @@ fn run_agent_turn(
let tool_calls = response.tool_calls.clone().unwrap_or_default();
archive_message(tc.db.as_ref(), &tc.session_id, &response);
msgs.push(response);
for tool_call in tool_calls {
let mut results_vec = Vec::new();
std::thread::scope(|s| {
let mut handles = Vec::new();
let tc_ref = tc;
for tool_call in &tool_calls {
let handle = s.spawn(move || {
let tool_name = tool_call.function.name.clone();
let args = crate::dto::chat::tool::sanitize_tool_arguments(
&tool_call.function.arguments,
);
let ws_roots: Vec<&std::path::Path> =
tc_ref.workspace_roots.iter().map(std::path::PathBuf::as_path).collect();
let verdict = crate::app::harness::Harness::gate_tool_call(
&tool_name,
&args,
&ws_roots,
);
let is_edit_tool = tool_name == "write" || tool_name == "edit";
let (output, is_error, is_edit) = match verdict {
Verdict::Allow => match execute_one_tool(
&tc_ref.tools,
&tc_ref.ctx,
&tool_name,
&tool_call.id,
&args,
&tc_ref.edit_log_session_dir,
&tc_ref.session_id,
tc_ref.db.as_ref(),
) {
Ok(result) => (result, false, is_edit_tool),
Err(e) => (e.to_string(), true, false),
},
Verdict::Block(reason) => (format!("Blocked: {reason}"), true, false),
};
(tool_call, tool_name, args, output, is_error, is_edit)
});
handles.push(handle);
}
for h in handles {
if let Ok(res) = h.join() {
results_vec.push(res);
}
}
});
for (tool_call, tool_name, args, output, is_error, is_edit) in results_vec {
if tc.abort_flag.load(std::sync::atomic::Ordering::SeqCst) {
if let Ok(mut q) = events_q.lock() {
q.push_back(TurnEvent::Error("Turn aborted by user".to_string()));
}
return Ok(());
}
let tool_name = tool_call.function.name.clone();
let args = crate::dto::chat::tool::sanitize_tool_arguments(
&tool_call.function.arguments,
);
let ws_roots: Vec<&std::path::Path> =
tc.workspace_roots.iter().map(std::path::PathBuf::as_path).collect();
let verdict = crate::app::harness::Harness::gate_tool_call(
&tool_name,
&args,
&ws_roots,
);
let is_edit_tool = tool_name == "write" || tool_name == "edit";
let (output, is_error, is_edit) = match verdict {
Verdict::Allow => match execute_one_tool(
&tc.tools,
&tc.ctx,
&tool_name,
&tool_call.id,
&args,
&tc.edit_log_session_dir,
&tc.session_id,
tc.db.as_ref(),
) {
Ok(result) => (result, false, is_edit_tool),
Err(e) => (e.to_string(), true, false),
},
Verdict::Block(reason) => (format!("Blocked: {reason}"), true, false),
};
if is_edit {
edits_this_turn += 1;
@@ -1399,7 +1413,6 @@ fn run_agent_turn(
}
}
let tool_path = args.get("path").and_then(|v| v.as_str()).map(std::string::ToString::to_string);
{
+5 -5
View File
@@ -280,7 +280,7 @@ mod tests {
assert_eq!(events.len(), 1);
match &events[0] {
StreamEvent::Token(t) => assert_eq!(t, "hello"),
other => panic!("expected Token, got {:?}", other),
other => panic!("expected Token, got {other:?}"),
}
}
@@ -293,7 +293,7 @@ mod tests {
assert_eq!(e2.len(), 1);
match &e2[0] {
StreamEvent::Token(t) => assert_eq!(t, "partial"),
other => panic!("expected Token, got {:?}", other),
other => panic!("expected Token, got {other:?}"),
}
}
@@ -329,7 +329,7 @@ mod tests {
assert_eq!(name.as_deref(), Some("bash"));
assert_eq!(arguments_delta, "{\"cmd\"");
}
other => panic!("expected ToolCallDelta, got {:?}", other),
other => panic!("expected ToolCallDelta, got {other:?}"),
}
}
@@ -346,7 +346,7 @@ mod tests {
assert_eq!(*completion_tokens, 5);
assert_eq!(*total_tokens, 15);
}
other => panic!("expected Usage, got {:?}", other),
other => panic!("expected Usage, got {other:?}"),
}
}
@@ -368,7 +368,7 @@ mod tests {
assert_eq!(a, "a");
assert_eq!(b, "b");
}
other => panic!("expected two Tokens, got {:?}", other),
other => panic!("expected two Tokens, got {other:?}"),
}
}
}
+55 -52
View File
@@ -391,67 +391,62 @@ pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender<SubagentEvent>) ->
// Push the assistant message with tool_calls into the conversation
messages.push(response);
for tool_call in &tool_calls {
// Check abort flag before each tool execution
if ctx.abort_flag.as_ref().is_some_and(|f| f.load(std::sync::atomic::Ordering::SeqCst)) {
let _ = tx.blocking_send(SubagentEvent::StepFailed {
step,
error: "subagent aborted by parent during tool execution".to_string(),
});
anyhow::bail!("subagent aborted by parent during tool call at step {step}");
}
let mut results_vec = Vec::new();
std::thread::scope(|s| {
let mut handles = Vec::new();
let tools_ref = &tools;
let tool_ctx_ref = &tool_ctx;
for tool_call in &tool_calls {
let handle = s.spawn(move || {
// Check abort flag before each tool execution
if ctx.abort_flag.as_ref().is_some_and(|f| f.load(std::sync::atomic::Ordering::SeqCst)) {
return (tool_call, Err(anyhow::anyhow!("subagent aborted by parent during tool execution")));
}
let tool_name = &tool_call.function.name;
let args = crate::dto::chat::tool::sanitize_tool_arguments(&tool_call.function.arguments);
let explicitly_allowed = ctx.allowed_tools.contains(tool_name);
let generally_allowed = ctx.allowed_tools.is_empty() || explicitly_allowed;
// Level 1: allowlist check — is this tool even permitted?
if !generally_allowed {
return (tool_call, Ok(format!("tool '{tool_name}' not allowed for this subagent")));
}
// Level 2: risky tool check — risky tools require explicit permission
if tool_is_risky(tool_name) && !explicitly_allowed {
return (tool_call, Ok(format!("risky tool '{tool_name}' requires explicit permission; not allowed for this subagent")));
}
// Level 3: Harness-style content safety gating
if let Some(block_reason) = gate_subagent_tool_call(tool_name, &args) {
return (tool_call, Ok(format!("Blocked by subagent gate: {block_reason}")));
}
let result = match tools_ref.iter().find(|t| t.name() == tool_name.as_str()) {
Some(tool) => tool.run(tool_ctx_ref, &args),
None => Err(anyhow::anyhow!("tool '{tool_name}' not found")),
};
(tool_call, result)
});
handles.push(handle);
}
for h in handles {
if let Ok(res) = h.join() {
results_vec.push(res);
}
}
});
for (tool_call, result) in results_vec {
let tool_name = &tool_call.function.name;
let args = crate::dto::chat::tool::sanitize_tool_arguments(&tool_call.function.arguments);
let explicitly_allowed = ctx.allowed_tools.contains(tool_name);
let generally_allowed = ctx.allowed_tools.is_empty() || explicitly_allowed;
let _ = tx.blocking_send(SubagentEvent::ToolCall {
tool: tool_name.clone(),
args: args.clone(),
});
// Level 1: allowlist check — is this tool even permitted?
if !generally_allowed {
let msg = format!("tool '{tool_name}' not allowed for this subagent");
messages.push(ChatMessage::tool_result(tool_call.id.clone(), msg.clone()));
let _ = tx.blocking_send(SubagentEvent::ToolResult {
tool: tool_name.clone(),
output: msg,
});
continue;
}
// Level 2: risky tool check — risky tools require explicit permission
if tool_is_risky(tool_name) && !explicitly_allowed {
let msg = format!("risky tool '{tool_name}' requires explicit permission; not allowed for this subagent");
messages.push(ChatMessage::tool_result(tool_call.id.clone(), msg.clone()));
let _ = tx.blocking_send(SubagentEvent::ToolResult {
tool: tool_name.clone(),
output: msg,
});
continue;
}
// Level 3: Harness-style content safety gating — mirrors the main
// agent's gate_tool_call checks (path traversal, reason validation,
// stub/denial/assumption scanning, bash exfiltration, destructive
// commands, sensitive path reads).
if let Some(block_reason) = gate_subagent_tool_call(tool_name, &args) {
let msg = format!("Blocked by subagent gate: {block_reason}");
messages.push(ChatMessage::tool_result(tool_call.id.clone(), msg.clone()));
let _ = tx.blocking_send(SubagentEvent::ToolResult {
tool: tool_name.clone(),
output: msg,
});
continue;
}
let result = match tools.iter().find(|t| t.name() == tool_name.as_str()) {
Some(tool) => tool.run(&tool_ctx, &args),
None => Err(anyhow::anyhow!("tool '{tool_name}' not found")),
};
match result {
Ok(output_text) => {
messages.push(ChatMessage::tool_result(tool_call.id.clone(), output_text.clone()));
@@ -461,6 +456,14 @@ pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender<SubagentEvent>) ->
});
}
Err(e) => {
let err_str = e.to_string();
if err_str.contains("subagent aborted by parent") {
let _ = tx.blocking_send(SubagentEvent::StepFailed {
step,
error: err_str.clone(),
});
anyhow::bail!("{err_str}");
}
let msg = format!("tool '{tool_name}' failed: {e}");
messages.push(ChatMessage::tool_result(tool_call.id.clone(), msg.clone()));
let _ = tx.blocking_send(SubagentEvent::ToolResult {
+8 -7
View File
@@ -93,13 +93,14 @@ fn make_division_phase(
/// context to flow through the pipeline.
///
/// Returns a consolidated executive summary string.
#[allow(clippy::ref_option)]
pub fn run_company_pipeline(
user_request: &str,
session_dir: &std::path::Path,
workspaces: &[std::path::PathBuf],
turn_events: Option<&Arc<Mutex<std::collections::VecDeque<crate::app::state::runtime::TurnEvent>>>>,
abort_flag: &Option<Arc<AtomicBool>>,
custom_specialists: HashMap<String, Vec<(String, String)>>,
custom_specialists: &HashMap<String, Vec<(String, String)>>,
) -> anyhow::Result<String> {
let divisions = division::all_divisions();
@@ -179,20 +180,21 @@ pub fn run_company_pipeline(
.map(|f| f.clone())
.unwrap_or_default();
Ok(build_executive_summary(user_request, &results, &all_findings, &divisions, &custom_specialists))
Ok(build_executive_summary(user_request, &results, &all_findings, &divisions, custom_specialists))
}
/// Run a quick company pipeline that skips non-essential divisions
/// for simple tasks. Flow: Strategy → Engineering → Quality.
///
/// This is for smaller tasks where security audit and full docs are overkill.
#[allow(clippy::ref_option)]
pub fn run_company_pipeline_quick(
user_request: &str,
session_dir: &std::path::Path,
workspaces: &[std::path::PathBuf],
turn_events: Option<&Arc<Mutex<std::collections::VecDeque<crate::app::state::runtime::TurnEvent>>>>,
abort_flag: &Option<Arc<AtomicBool>>,
custom_specialists: HashMap<String, Vec<(String, String)>>,
custom_specialists: &HashMap<String, Vec<(String, String)>>,
) -> anyhow::Result<String> {
let divisions = division::all_divisions();
let quick_divisions = &divisions[..3];
@@ -252,7 +254,7 @@ pub fn run_company_pipeline_quick(
.map(|f| f.clone())
.unwrap_or_default();
Ok(build_executive_summary(user_request, &results, &all_findings, quick_divisions, &custom_specialists))
Ok(build_executive_summary(user_request, &results, &all_findings, quick_divisions, custom_specialists))
}
/// Build a compressed executive summary from pipeline results.
@@ -277,8 +279,7 @@ fn build_executive_summary(
let mut start_index = 0;
for div in divisions {
let count = custom_specialists.get(div.name)
.map(Vec::len)
.unwrap_or(0);
.map_or(0, Vec::len);
let mut division_verdicts = Vec::new();
for offset in 0..count {
@@ -393,7 +394,7 @@ mod tests {
("Custom Label".to_string(), "Custom Focus Description".to_string())
]
);
let specs = make_division_specialists(div, "Test Request", &custom.get("Strategy").unwrap());
let specs = make_division_specialists(div, "Test Request", custom.get("Strategy").unwrap());
assert_eq!(specs.len(), 1);
if let ScriptPrimitive::Agent(prompt) = &specs[0] {
assert!(prompt.contains("Custom Label"));
+24 -26
View File
@@ -98,7 +98,7 @@ pub type LiveStateFn = Arc<dyn Fn(String, String, AgentStatus) + Send + Sync>;
/// a stuck stage from blocking the entire pipeline forever.
///
/// Return: the agent's text output, or an error on failure.
#[allow(clippy::too_many_lines, clippy::too_many_arguments)]
#[allow(clippy::too_many_lines, clippy::too_many_arguments, clippy::ref_option)]
fn spawn_single_agent(
agent_id: &str,
agent_name: &str,
@@ -185,7 +185,7 @@ fn spawn_single_agent(
// Link the shared findings Arc so note_finding calls within this
// subagent write into the same vec visible to sibling agents.
ctx.workflow_findings = Some(findings.clone());
ctx.abort_flag = abort_flag.clone();
ctx.abort_flag.clone_from(abort_flag);
// Create an mpsc channel and drain events in a background thread.
// The drain thread also pushes intra-division progress updates to the
@@ -269,34 +269,30 @@ fn spawn_single_agent(
let deadline = Duration::from_millis(timeout);
let mut elapsed = Duration::ZERO;
loop {
match done_rx.recv_timeout(poll_interval) {
Ok(r) => break r,
Err(_) => {
elapsed += poll_interval;
if elapsed >= deadline {
break Err(anyhow::anyhow!(
"subagent '{bg_name}' timed out after {timeout}ms",
));
}
if bg_abort.as_ref().is_some_and(|f| f.load(Ordering::SeqCst)) {
break Err(anyhow::anyhow!(
"subagent '{bg_name}' aborted by user",
));
}
}
if let Ok(r) = done_rx.recv_timeout(poll_interval) {
break r;
}
elapsed += poll_interval;
if elapsed >= deadline {
break Err(anyhow::anyhow!(
"subagent '{bg_name}' timed out after {timeout}ms",
));
}
if bg_abort.as_ref().is_some_and(|f| f.load(Ordering::SeqCst)) {
break Err(anyhow::anyhow!(
"subagent '{bg_name}' aborted by user",
));
}
}
} else {
loop {
match done_rx.recv_timeout(poll_interval) {
Ok(r) => break r,
Err(_) => {
if bg_abort.as_ref().is_some_and(|f| f.load(Ordering::SeqCst)) {
break Err(anyhow::anyhow!(
"subagent '{bg_name}' aborted by user",
));
}
}
if let Ok(r) = done_rx.recv_timeout(poll_interval) {
break r;
}
if bg_abort.as_ref().is_some_and(|f| f.load(Ordering::SeqCst)) {
break Err(anyhow::anyhow!(
"subagent '{bg_name}' aborted by user",
));
}
}
};
@@ -358,6 +354,7 @@ type ParallelResult = (usize, anyhow::Result<Vec<String>>);
/// Return: a `Vec<String>` of all agent outputs (or error strings) in
/// the order they were submitted.
#[allow(clippy::too_many_arguments)]
#[allow(clippy::ref_option, clippy::too_many_lines)]
pub fn execute_primitive(
primitive: &ScriptPrimitive,
args: &HashMap<String, String>,
@@ -529,6 +526,7 @@ pub fn run_workflow(
/// `spawn_agents` invocations remain fully isolated.
///
/// Return: a human-readable summary string.
#[allow(clippy::ref_option)]
pub fn run_workflow_tracked(
script: &WorkflowScript,
args: &HashMap<String, String>,
+120
View File
@@ -73,3 +73,123 @@ pub fn serialize_frame<T: serde::Serialize>(value: &T) -> Result<Vec<u8>> {
pub fn deserialize_frame<'a, T: serde::Deserialize<'a>>(data: &'a [u8]) -> Result<T> {
Ok(serde_json::from_slice(data)?)
}
#[cfg(test)]
mod tests {
use super::*;
/// Write a value, read it back, and verify exact equality.
fn roundtrip_bytes(data: &[u8]) {
let mut buf: Vec<u8> = Vec::new();
write_frame(&mut buf, data).unwrap();
let read_back = read_frame(&mut buf.as_slice())
.unwrap()
.expect("expected Some(frame)");
assert_eq!(read_back, data);
}
#[test]
fn test_write_read_roundtrip_empty() {
roundtrip_bytes(b"");
}
#[test]
fn test_write_read_roundtrip_small_text() {
roundtrip_bytes(b"hello world");
}
#[test]
fn test_write_read_roundtrip_binary() {
roundtrip_bytes(&[0x00, 0xFF, 0xAB, 0xCD, 0x01, 0x02, 0x03]);
}
#[test]
fn test_write_read_roundtrip_large() {
let data = vec![0x42u8; 100_000];
roundtrip_bytes(&data);
}
#[test]
fn test_write_rejects_too_large_frame() {
let oversized = vec![0u8; MAX_FRAME_SIZE + 1];
let mut buf = Vec::new();
let result = write_frame(&mut buf, &oversized);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("too large") || err.contains("64 MiB"));
}
#[test]
fn test_read_rejects_too_large_header() {
// Manually craft a 4-byte length header that exceeds MAX_FRAME_SIZE
let len = (MAX_FRAME_SIZE as u32).wrapping_add(1);
let header = len.to_be_bytes();
let mut buf = Vec::from(&header[..]);
buf.extend_from_slice(b"dummy");
let result = read_frame(&mut buf.as_slice());
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("too large"));
}
#[test]
fn test_read_empty_buf_returns_none() {
let empty: &[u8] = &[];
let result = read_frame(&mut &empty[..]).unwrap();
assert!(result.is_none(), "expected None for empty reader");
}
#[test]
fn test_read_partial_header_returns_none() {
// Only 2 bytes of the 4-byte header → EOF
let partial: &[u8] = &[0x00, 0x01];
let result = read_frame(&mut &partial[..]).unwrap();
assert!(result.is_none(), "expected None for partial header");
}
#[test]
fn test_read_truncated_payload_returns_err() {
let mut buf = Vec::new();
let header = (10u32).to_be_bytes();
buf.extend_from_slice(&header);
buf.extend_from_slice(b"abc"); // only 3 of 10 bytes
let result = read_frame(&mut buf.as_slice());
assert!(result.is_err(), "truncated payload should error");
}
#[test]
fn test_serialize_deserialize_roundtrip() {
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct Msg {
id: u32,
content: String,
tags: Vec<String>,
}
let original = Msg {
id: 42,
content: "hello world".into(),
tags: vec!["foo".into(), "bar".into()],
};
let bytes = serialize_frame(&original).unwrap();
let deserialized: Msg = deserialize_frame(&bytes).unwrap();
assert_eq!(original, deserialized);
}
#[test]
fn test_serialize_rejects_oversized_value() {
let huge = vec![0u8; MAX_FRAME_SIZE + 1];
let result = serialize_frame(&huge);
assert!(result.is_err());
}
#[test]
fn test_deserialize_malformed_json_errors() {
let bad_json = b"this is not json";
let result: Result<String> = deserialize_frame(bad_json);
assert!(result.is_err());
}
}
+2 -2
View File
@@ -145,8 +145,8 @@ mod tests {
log.append(EditLogEntry {
ts: i,
tool: "edit".to_string(),
path: format!("file{}.txt", i),
reason: format!("reason {}", i),
path: format!("file{i}.txt"),
reason: format!("reason {i}"),
content_sha256: "hash".to_string(),
bytes_delta: 10 + i,
origin: "main".to_string(),
+1 -1
View File
@@ -375,7 +375,7 @@ mod tests {
};
mem.write(&dir).unwrap();
let names = Memory::list(&dir);
assert!(names.contains(&"alpha".to_string()), "list should contain 'alpha', got: {:?}", names);
assert!(names.contains(&"alpha".to_string()), "list should contain 'alpha', got: {names:?}");
let _ = std::fs::remove_dir_all(&dir);
}
+1 -1
View File
@@ -141,7 +141,7 @@ impl ToolCtxBuilder {
/// Construct one instance of every built-in tool, in the fixed order exposed to the LLM.
///
/// Return: boxed trait objects for all 28 tools (fs, search, bash, git, memory, plan,
/// Return: boxed trait objects for all 37 tools (fs, search, bash, git, memory, plan,
/// workflow, utility).
pub fn all_tools() -> Vec<Box<dyn Tool>> {
vec![
+4 -4
View File
@@ -206,7 +206,7 @@ impl Tool for CompanyPipeline {
let specs = v.as_array().map(|arr| {
arr.iter().filter_map(|item| {
let pair = item.as_array()?;
let label = pair.get(0)?.as_str()?.to_string();
let label = pair.first()?.as_str()?.to_string();
let focus = pair.get(1)?.as_str()?.to_string();
Some((label, focus))
}).collect()
@@ -225,7 +225,7 @@ impl Tool for CompanyPipeline {
&ctx.workspaces,
ctx.turn_events.as_ref(),
&no_abort,
custom_specialists,
&custom_specialists,
)
}
_ => {
@@ -235,7 +235,7 @@ impl Tool for CompanyPipeline {
&ctx.workspaces,
ctx.turn_events.as_ref(),
&no_abort,
custom_specialists,
&custom_specialists,
)
}
}
@@ -273,7 +273,7 @@ impl Tool for ReadFindings {
.map(|(i, f)| format!("{}. {}", i + 1, f))
.collect::<Vec<_>>()
.join("\n");
Ok(format!("Findings in this workflow run:\n{}", formatted))
Ok(format!("Findings in this workflow run:\n{formatted}"))
}
} else {
Ok("No findings database available (called outside a workflow run).".to_string())