[文書] Playbook v4: ビジョン全38要素をカバーする完全版
v3 から追加: Sprint 3: 記憶アタッチメント (#58) + ドリーミング (#59) + Web ダッシュボード (#63) Sprint 4: シータサイクル (#60) + Obsidian (#62) Sprint 2: Bus データパス統合 (#65) + ブランチ戦略 (#64) 新規 Issue: #64: 並列Codexブランチ戦略 (worktree + PR) #65: Bus データパス統合 ビジョン達成度推移: 現在 32% → Sprint1後 38% → Sprint2後 47% → Sprint3後 72% → Sprint4後 93% Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a1febb67eb
commit
ec1d25887e
29 changed files with 471 additions and 196 deletions
|
|
@ -92,13 +92,11 @@ impl ApprovalCallback for RejectHighRisk {
|
|||
async fn request_approval(&self, request: &ApprovalRequest) -> ApprovalDecision {
|
||||
match request.risk_level {
|
||||
RiskLevel::Low | RiskLevel::Medium => ApprovalDecision::Approved,
|
||||
RiskLevel::High | RiskLevel::Critical => {
|
||||
ApprovalDecision::Rejected(Some(format!(
|
||||
"Tool {} requires manual approval (risk level: {})",
|
||||
request.name,
|
||||
request.risk_level.as_str()
|
||||
)))
|
||||
}
|
||||
RiskLevel::High | RiskLevel::Critical => ApprovalDecision::Rejected(Some(format!(
|
||||
"Tool {} requires manual approval (risk level: {})",
|
||||
request.name,
|
||||
request.risk_level.as_str()
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -187,7 +185,10 @@ mod tests {
|
|||
risk_level: RiskLevel::Low,
|
||||
description: "Read file".to_string(),
|
||||
};
|
||||
assert_eq!(approver.request_approval(&low_risk).await, ApprovalDecision::Approved);
|
||||
assert_eq!(
|
||||
approver.request_approval(&low_risk).await,
|
||||
ApprovalDecision::Approved
|
||||
);
|
||||
|
||||
// High risk should be rejected
|
||||
let high_risk = ApprovalRequest {
|
||||
|
|
|
|||
|
|
@ -153,9 +153,10 @@ impl Agent {
|
|||
/// Main agent execution loop
|
||||
pub async fn run(&self, prompt: &str) -> Result<AgentResult, AgentError> {
|
||||
// Execute SessionStart hooks
|
||||
let session_context = HookContext::new()
|
||||
.with_data("prompt", prompt);
|
||||
self.hook_manager.execute(&HookEvent::SessionStart, &session_context).await;
|
||||
let session_context = HookContext::new().with_data("prompt", prompt);
|
||||
self.hook_manager
|
||||
.execute(&HookEvent::SessionStart, &session_context)
|
||||
.await;
|
||||
|
||||
self.emit_event(AgentEvent::Started {
|
||||
prompt: prompt.to_string(),
|
||||
|
|
@ -220,7 +221,9 @@ impl Agent {
|
|||
let end_context = HookContext::new()
|
||||
.with_data("iterations", &result.iterations.to_string())
|
||||
.with_data("tool_calls", &result.tool_calls.to_string());
|
||||
self.hook_manager.execute(&HookEvent::SessionEnd, &end_context).await;
|
||||
self.hook_manager
|
||||
.execute(&HookEvent::SessionEnd, &end_context)
|
||||
.await;
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
|
@ -302,7 +305,9 @@ impl Agent {
|
|||
let pre_tool_context = HookContext::new()
|
||||
.with_tool(&tool_use.name)
|
||||
.with_data("input", &tool_use.input.to_string());
|
||||
self.hook_manager.execute(&HookEvent::PreTool, &pre_tool_context).await;
|
||||
self.hook_manager
|
||||
.execute(&HookEvent::PreTool, &pre_tool_context)
|
||||
.await;
|
||||
|
||||
// Execute tool
|
||||
self.emit_event(AgentEvent::ToolExecuting {
|
||||
|
|
@ -327,7 +332,9 @@ impl Agent {
|
|||
let post_tool_context = HookContext::new()
|
||||
.with_tool(&tool_use.name)
|
||||
.with_result(&output.content.to_string());
|
||||
self.hook_manager.execute(&HookEvent::PostTool, &post_tool_context).await;
|
||||
self.hook_manager
|
||||
.execute(&HookEvent::PostTool, &post_tool_context)
|
||||
.await;
|
||||
|
||||
// Create tool result
|
||||
let content = serde_json::to_string(&output.content)
|
||||
|
|
@ -349,7 +356,9 @@ impl Agent {
|
|||
let error_context = HookContext::new()
|
||||
.with_tool(&tool_use.name)
|
||||
.with_error(&e.to_string());
|
||||
self.hook_manager.execute(&HookEvent::OnError, &error_context).await;
|
||||
self.hook_manager
|
||||
.execute(&HookEvent::OnError, &error_context)
|
||||
.await;
|
||||
|
||||
// Add error as tool result
|
||||
results.push(ContentBlock::ToolResult {
|
||||
|
|
@ -397,7 +406,9 @@ impl Agent {
|
|||
let end_context = HookContext::new()
|
||||
.with_data("iterations", &result.iterations.to_string())
|
||||
.with_data("tool_calls", &result.tool_calls.to_string());
|
||||
self.hook_manager.execute(&HookEvent::SessionEnd, &end_context).await;
|
||||
self.hook_manager
|
||||
.execute(&HookEvent::SessionEnd, &end_context)
|
||||
.await;
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,11 +134,7 @@ pub struct McpToolExecutor {
|
|||
}
|
||||
|
||||
impl McpToolExecutor {
|
||||
pub fn new(
|
||||
server_name: String,
|
||||
tool: McpTool,
|
||||
manager: Arc<AsyncMutex<McpManager>>,
|
||||
) -> Self {
|
||||
pub fn new(server_name: String, tool: McpTool, manager: Arc<AsyncMutex<McpManager>>) -> Self {
|
||||
Self {
|
||||
server_name,
|
||||
tool_name: tool.name,
|
||||
|
|
@ -178,10 +174,13 @@ impl ToolExecutor for McpToolExecutor {
|
|||
async fn execute(&self, input: Value) -> Result<ToolOutput, ToolError> {
|
||||
let mut manager = self.manager.lock().await;
|
||||
|
||||
match manager.call_tool(&self.server_name, &self.tool_name, input).await {
|
||||
match manager
|
||||
.call_tool(&self.server_name, &self.tool_name, input)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
let content = serde_json::to_string_pretty(&result)
|
||||
.unwrap_or_else(|_| result.to_string());
|
||||
let content =
|
||||
serde_json::to_string_pretty(&result).unwrap_or_else(|_| result.to_string());
|
||||
Ok(ToolOutput::success(content))
|
||||
}
|
||||
Err(e) => Err(ToolError::ExecutionFailed(e.to_string())),
|
||||
|
|
@ -270,7 +269,10 @@ impl ExecutorRegistry {
|
|||
}
|
||||
|
||||
/// Register MCP tools from an MCP manager
|
||||
pub async fn register_mcp_tools(&mut self, manager: Arc<AsyncMutex<McpManager>>) -> Result<usize, ToolError> {
|
||||
pub async fn register_mcp_tools(
|
||||
&mut self,
|
||||
manager: Arc<AsyncMutex<McpManager>>,
|
||||
) -> Result<usize, ToolError> {
|
||||
let mut count = 0;
|
||||
|
||||
// Get all tools from all servers
|
||||
|
|
@ -283,11 +285,7 @@ impl ExecutorRegistry {
|
|||
|
||||
for (server_name, tools) in all_tools {
|
||||
for tool in tools {
|
||||
let executor = McpToolExecutor::new(
|
||||
server_name.clone(),
|
||||
tool,
|
||||
manager.clone(),
|
||||
);
|
||||
let executor = McpToolExecutor::new(server_name.clone(), tool, manager.clone());
|
||||
self.register(executor);
|
||||
count += 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@ mod approval_integration_tests {
|
|||
let registry = ExecutorRegistry::with_standard_tools();
|
||||
|
||||
// Agent should be created successfully with AutoApproveAll callback
|
||||
let _agent = Agent::new(client, registry)
|
||||
.with_approval_callback(AutoApproveAll);
|
||||
let _agent = Agent::new(client, registry).with_approval_callback(AutoApproveAll);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -23,8 +22,7 @@ mod approval_integration_tests {
|
|||
let registry = ExecutorRegistry::with_standard_tools();
|
||||
|
||||
// Agent should be created successfully with RejectHighRisk callback
|
||||
let _agent = Agent::new(client, registry)
|
||||
.with_approval_callback(RejectHighRisk);
|
||||
let _agent = Agent::new(client, registry).with_approval_callback(RejectHighRisk);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -257,7 +257,9 @@ mod tests {
|
|||
let cache = TTLCache::new(Duration::from_secs(3600)); // default 1 hour
|
||||
|
||||
// Insert with shorter custom TTL
|
||||
cache.insert_with_ttl("key1", "value1", Duration::from_millis(50)).await;
|
||||
cache
|
||||
.insert_with_ttl("key1", "value1", Duration::from_millis(50))
|
||||
.await;
|
||||
assert_eq!(cache.get(&"key1").await, Some("value1"));
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
|
@ -301,7 +303,9 @@ mod tests {
|
|||
let cache = TTLCache::new(Duration::from_millis(50));
|
||||
|
||||
cache.insert("key1", "value1").await;
|
||||
cache.insert_with_ttl("key2", "value2", Duration::from_secs(3600)).await;
|
||||
cache
|
||||
.insert_with_ttl("key2", "value2", Duration::from_secs(3600))
|
||||
.await;
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
|
|
@ -437,7 +441,9 @@ mod tests {
|
|||
assert!(cache.get(&key).await.is_none());
|
||||
|
||||
// Insert
|
||||
cache.insert(key.clone(), "Response from LLM".to_string()).await;
|
||||
cache
|
||||
.insert(key.clone(), "Response from LLM".to_string())
|
||||
.await;
|
||||
|
||||
// Hit
|
||||
let cached = cache.get(&key).await;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ pub struct Config {
|
|||
pub tools: ToolConfig,
|
||||
}
|
||||
|
||||
|
||||
/// API configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
|
|
|
|||
|
|
@ -568,8 +568,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_task_with_priority() {
|
||||
let task = Task::new("priority-task", "Task with priority")
|
||||
.with_priority(10);
|
||||
let task = Task::new("priority-task", "Task with priority").with_priority(10);
|
||||
|
||||
assert_eq!(task.priority, 10);
|
||||
assert_eq!(task.name, "priority-task");
|
||||
|
|
@ -577,8 +576,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_task_with_estimated_time() {
|
||||
let task = Task::new("timed-task", "Task with time estimate")
|
||||
.with_estimated_time(120);
|
||||
let task = Task::new("timed-task", "Task with time estimate").with_estimated_time(120);
|
||||
|
||||
assert_eq!(task.estimated_time, Some(120));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -315,7 +315,9 @@ mod tests {
|
|||
fn test_fallback_strategy_lower_temperature() {
|
||||
let strategy = FallbackStrategy::lower_temperature();
|
||||
match strategy {
|
||||
FallbackStrategy::RetryWithLowerTemperature { temperature_reduction } => {
|
||||
FallbackStrategy::RetryWithLowerTemperature {
|
||||
temperature_reduction,
|
||||
} => {
|
||||
assert_eq!(temperature_reduction, 0.2);
|
||||
}
|
||||
_ => panic!("Expected RetryWithLowerTemperature"),
|
||||
|
|
|
|||
|
|
@ -66,7 +66,10 @@ impl FeatureFlagManager {
|
|||
/// `true` if the flag exists and is enabled, `false` otherwise
|
||||
pub fn is_enabled(&self, flag_name: &str) -> bool {
|
||||
let flags = self.flags.read().unwrap();
|
||||
flags.get(flag_name).map(|flag| flag.enabled).unwrap_or(false)
|
||||
flags
|
||||
.get(flag_name)
|
||||
.map(|flag| flag.enabled)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Set a feature flag
|
||||
|
|
@ -190,7 +193,10 @@ impl FeatureFlagManager {
|
|||
/// HashMap of flag names to enabled status
|
||||
pub fn export_to_map(&self) -> HashMap<String, bool> {
|
||||
let flags = self.flags.read().unwrap();
|
||||
flags.iter().map(|(name, flag)| (name.clone(), flag.enabled)).collect()
|
||||
flags
|
||||
.iter()
|
||||
.map(|(name, flag)| (name.clone(), flag.enabled))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Clear all feature flags
|
||||
|
|
|
|||
|
|
@ -29,14 +29,12 @@ pub fn find_git_root(start_path: Option<&Path>) -> Result<PathBuf> {
|
|||
};
|
||||
|
||||
match git2::Repository::discover(&search_path) {
|
||||
Ok(repo) => repo
|
||||
.workdir()
|
||||
.map(|p| p.to_path_buf())
|
||||
.ok_or_else(|| {
|
||||
Error::Git(
|
||||
"Repository is bare (no working directory). Miyabi requires a non-bare repository.".to_string()
|
||||
)
|
||||
}),
|
||||
Ok(repo) => repo.workdir().map(|p| p.to_path_buf()).ok_or_else(|| {
|
||||
Error::Git(
|
||||
"Repository is bare (no working directory). Miyabi requires a non-bare repository."
|
||||
.to_string(),
|
||||
)
|
||||
}),
|
||||
Err(e) => Err(Error::Git(format!(
|
||||
"Not in a Git repository. Please run miyabi from within a Git repository.\n\
|
||||
Searched from: {:?}\n\
|
||||
|
|
|
|||
|
|
@ -139,8 +139,7 @@ impl GitHubClient {
|
|||
|
||||
/// Create from environment variables
|
||||
pub fn from_env() -> Result<Self> {
|
||||
let token = std::env::var("GITHUB_TOKEN")
|
||||
.map_err(|_| anyhow!("GITHUB_TOKEN not set"))?;
|
||||
let token = std::env::var("GITHUB_TOKEN").map_err(|_| anyhow!("GITHUB_TOKEN not set"))?;
|
||||
let repo = std::env::var("GITHUB_REPOSITORY")
|
||||
.or_else(|_| std::env::var("REPOSITORY"))
|
||||
.map_err(|_| anyhow!("GITHUB_REPOSITORY or REPOSITORY not set"))?;
|
||||
|
|
@ -156,7 +155,11 @@ impl GitHubClient {
|
|||
// ========== Issues ==========
|
||||
|
||||
/// List issues
|
||||
pub async fn list_issues(&self, state: Option<&str>, labels: Option<&str>) -> Result<Vec<Issue>> {
|
||||
pub async fn list_issues(
|
||||
&self,
|
||||
state: Option<&str>,
|
||||
labels: Option<&str>,
|
||||
) -> Result<Vec<Issue>> {
|
||||
let mut url = format!(
|
||||
"{}/repos/{}/{}/issues",
|
||||
self.base_url, self.owner, self.repo
|
||||
|
|
@ -230,7 +233,8 @@ impl GitHubClient {
|
|||
self.base_url, self.owner, self.repo, issue_number
|
||||
);
|
||||
|
||||
let response = self.client
|
||||
let response = self
|
||||
.client
|
||||
.post(&url)
|
||||
.json(&serde_json::json!({ "labels": labels }))
|
||||
.send()
|
||||
|
|
@ -253,7 +257,8 @@ impl GitHubClient {
|
|||
self.base_url, self.owner, self.repo, number
|
||||
);
|
||||
|
||||
let response = self.client
|
||||
let response = self
|
||||
.client
|
||||
.patch(&url)
|
||||
.json(&serde_json::json!({ "state": "closed" }))
|
||||
.send()
|
||||
|
|
@ -273,10 +278,7 @@ impl GitHubClient {
|
|||
|
||||
/// List pull requests
|
||||
pub async fn list_pull_requests(&self, state: Option<&str>) -> Result<Vec<PullRequest>> {
|
||||
let mut url = format!(
|
||||
"{}/repos/{}/{}/pulls",
|
||||
self.base_url, self.owner, self.repo
|
||||
);
|
||||
let mut url = format!("{}/repos/{}/{}/pulls", self.base_url, self.owner, self.repo);
|
||||
|
||||
if let Some(s) = state {
|
||||
url = format!("{}?state={}", url, s);
|
||||
|
|
@ -314,11 +316,11 @@ impl GitHubClient {
|
|||
}
|
||||
|
||||
/// Create a pull request
|
||||
pub async fn create_pull_request(&self, request: CreatePullRequestRequest) -> Result<PullRequest> {
|
||||
let url = format!(
|
||||
"{}/repos/{}/{}/pulls",
|
||||
self.base_url, self.owner, self.repo
|
||||
);
|
||||
pub async fn create_pull_request(
|
||||
&self,
|
||||
request: CreatePullRequestRequest,
|
||||
) -> Result<PullRequest> {
|
||||
let url = format!("{}/repos/{}/{}/pulls", self.base_url, self.owner, self.repo);
|
||||
|
||||
let response = self.client.post(&url).json(&request).send().await?;
|
||||
|
||||
|
|
@ -333,7 +335,11 @@ impl GitHubClient {
|
|||
}
|
||||
|
||||
/// Merge a pull request
|
||||
pub async fn merge_pull_request(&self, number: u64, commit_message: Option<&str>) -> Result<()> {
|
||||
pub async fn merge_pull_request(
|
||||
&self,
|
||||
number: u64,
|
||||
commit_message: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let url = format!(
|
||||
"{}/repos/{}/{}/pulls/{}/merge",
|
||||
self.base_url, self.owner, self.repo, number
|
||||
|
|
@ -403,10 +409,7 @@ impl GitHubClient {
|
|||
|
||||
/// Get repository information
|
||||
pub async fn get_repo(&self) -> Result<serde_json::Value> {
|
||||
let url = format!(
|
||||
"{}/repos/{}/{}",
|
||||
self.base_url, self.owner, self.repo
|
||||
);
|
||||
let url = format!("{}/repos/{}/{}", self.base_url, self.owner, self.repo);
|
||||
|
||||
let response = self.client.get(&url).send().await?;
|
||||
|
||||
|
|
|
|||
|
|
@ -270,7 +270,11 @@ impl HookManager {
|
|||
let expanded_title = self.expand_variables(title, context);
|
||||
let expanded_message = self.expand_variables(message, context);
|
||||
// For now, just log the notification
|
||||
tracing::info!("Hook notification: {} - {}", expanded_title, expanded_message);
|
||||
tracing::info!(
|
||||
"Hook notification: {} - {}",
|
||||
expanded_title,
|
||||
expanded_message
|
||||
);
|
||||
HookResult {
|
||||
hook_name: hook.name.clone(),
|
||||
success: true,
|
||||
|
|
|
|||
|
|
@ -136,7 +136,11 @@ impl McpServer {
|
|||
}
|
||||
|
||||
/// Send a request and get a response
|
||||
pub async fn request(&mut self, method: &str, params: Option<serde_json::Value>) -> Result<McpResponse> {
|
||||
pub async fn request(
|
||||
&mut self,
|
||||
method: &str,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<McpResponse> {
|
||||
self.request_id += 1;
|
||||
let request = McpRequest {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
|
|
@ -145,9 +149,15 @@ impl McpServer {
|
|||
params,
|
||||
};
|
||||
|
||||
let stdin = self.process.stdin.as_mut()
|
||||
let stdin = self
|
||||
.process
|
||||
.stdin
|
||||
.as_mut()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get stdin"))?;
|
||||
let stdout = self.process.stdout.as_mut()
|
||||
let stdout = self
|
||||
.process
|
||||
.stdout
|
||||
.as_mut()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get stdout"))?;
|
||||
|
||||
// Send request
|
||||
|
|
@ -183,7 +193,9 @@ impl McpServer {
|
|||
return Err(anyhow::anyhow!("MCP error: {}", error.message));
|
||||
}
|
||||
|
||||
response.result.ok_or_else(|| anyhow::anyhow!("No result in response"))
|
||||
response
|
||||
.result
|
||||
.ok_or_else(|| anyhow::anyhow!("No result in response"))
|
||||
}
|
||||
|
||||
/// List available tools
|
||||
|
|
@ -193,16 +205,25 @@ impl McpServer {
|
|||
return Err(anyhow::anyhow!("MCP error: {}", error.message));
|
||||
}
|
||||
|
||||
let result = response.result.ok_or_else(|| anyhow::anyhow!("No result"))?;
|
||||
let result = response
|
||||
.result
|
||||
.ok_or_else(|| anyhow::anyhow!("No result"))?;
|
||||
let tools: Vec<McpTool> = serde_json::from_value(
|
||||
result.get("tools").cloned().unwrap_or(serde_json::Value::Array(vec![]))
|
||||
result
|
||||
.get("tools")
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Array(vec![])),
|
||||
)?;
|
||||
|
||||
Ok(tools)
|
||||
}
|
||||
|
||||
/// Call a tool
|
||||
pub async fn call_tool(&mut self, name: &str, arguments: serde_json::Value) -> Result<serde_json::Value> {
|
||||
pub async fn call_tool(
|
||||
&mut self,
|
||||
name: &str,
|
||||
arguments: serde_json::Value,
|
||||
) -> Result<serde_json::Value> {
|
||||
let params = serde_json::json!({
|
||||
"name": name,
|
||||
"arguments": arguments
|
||||
|
|
@ -424,7 +445,10 @@ command: node
|
|||
let config = McpServerConfig {
|
||||
name: "filesystem".to_string(),
|
||||
command: "npx".to_string(),
|
||||
args: vec!["-y".to_string(), "@anthropic/mcp-server-filesystem".to_string()],
|
||||
args: vec![
|
||||
"-y".to_string(),
|
||||
"@anthropic/mcp-server-filesystem".to_string(),
|
||||
],
|
||||
env: {
|
||||
let mut env = HashMap::new();
|
||||
env.insert("HOME".to_string(), "/tmp".to_string());
|
||||
|
|
@ -616,15 +640,13 @@ command: node
|
|||
#[test]
|
||||
fn test_mcp_manager_is_running() {
|
||||
let config = McpConfig {
|
||||
servers: vec![
|
||||
McpServerConfig {
|
||||
name: "server1".to_string(),
|
||||
command: "cmd".to_string(),
|
||||
args: vec![],
|
||||
env: HashMap::new(),
|
||||
auto_start: false,
|
||||
},
|
||||
],
|
||||
servers: vec![McpServerConfig {
|
||||
name: "server1".to_string(),
|
||||
command: "cmd".to_string(),
|
||||
args: vec![],
|
||||
env: HashMap::new(),
|
||||
auto_start: false,
|
||||
}],
|
||||
};
|
||||
|
||||
let manager = McpManager::with_config(config);
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ impl OpenClawClient {
|
|||
message: message.to_string(),
|
||||
};
|
||||
|
||||
let response = self.client
|
||||
let response = self
|
||||
.client
|
||||
.post(format!("{}/api/message", self.gateway_url))
|
||||
.header("Authorization", format!("Bearer {}", self.token))
|
||||
.json(&payload)
|
||||
|
|
|
|||
|
|
@ -327,8 +327,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_orchestrator_task_with_system_prompt() {
|
||||
let task = OrchestratorTask::new("task-1", "Do something")
|
||||
.with_system_prompt("You are helpful");
|
||||
let task =
|
||||
OrchestratorTask::new("task-1", "Do something").with_system_prompt("You are helpful");
|
||||
assert_eq!(task.system_prompt, Some("You are helpful".to_string()));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -178,8 +178,9 @@ impl PluginManager {
|
|||
pub fn unregister(&self, name: &str) -> Result<()> {
|
||||
let mut plugins = self.plugins.write().unwrap();
|
||||
|
||||
let mut entry =
|
||||
plugins.remove(name).ok_or_else(|| anyhow!("Plugin '{}' not found", name))?;
|
||||
let mut entry = plugins
|
||||
.remove(name)
|
||||
.ok_or_else(|| anyhow!("Plugin '{}' not found", name))?;
|
||||
|
||||
entry.plugin.shutdown()?;
|
||||
entry.state = PluginState::Shutdown;
|
||||
|
|
@ -198,10 +199,16 @@ impl PluginManager {
|
|||
pub fn execute(&self, name: &str, context: &PluginContext) -> Result<PluginResult> {
|
||||
let plugins = self.plugins.read().unwrap();
|
||||
|
||||
let entry = plugins.get(name).ok_or_else(|| anyhow!("Plugin '{}' not found", name))?;
|
||||
let entry = plugins
|
||||
.get(name)
|
||||
.ok_or_else(|| anyhow!("Plugin '{}' not found", name))?;
|
||||
|
||||
if entry.state != PluginState::Initialized {
|
||||
return Err(anyhow!("Plugin '{}' is not initialized (state: {:?})", name, entry.state));
|
||||
return Err(anyhow!(
|
||||
"Plugin '{}' is not initialized (state: {:?})",
|
||||
name,
|
||||
entry.state
|
||||
));
|
||||
}
|
||||
|
||||
entry.plugin.execute(context)
|
||||
|
|
@ -210,7 +217,10 @@ impl PluginManager {
|
|||
/// Lists all registered plugins
|
||||
pub fn list_plugins(&self) -> Vec<PluginMetadata> {
|
||||
let plugins = self.plugins.read().unwrap();
|
||||
plugins.values().map(|entry| entry.plugin.metadata()).collect()
|
||||
plugins
|
||||
.values()
|
||||
.map(|entry| entry.plugin.metadata())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Gets plugin metadata
|
||||
|
|
|
|||
|
|
@ -305,10 +305,7 @@ mod tests {
|
|||
async fn test_retry_returns_correct_value() {
|
||||
let config = RetryConfig::new(1, 10, 100);
|
||||
|
||||
let result = retry_with_backoff(config, || async {
|
||||
Ok::<i32, Error>(42)
|
||||
})
|
||||
.await;
|
||||
let result = retry_with_backoff(config, || async { Ok::<i32, Error>(42) }).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), 42);
|
||||
|
|
|
|||
|
|
@ -649,7 +649,11 @@ settings:
|
|||
let loader = RulesLoader::new(temp_dir.path().to_path_buf());
|
||||
|
||||
// Invalid YAML
|
||||
std::fs::write(temp_dir.path().join(".miyabirules"), "invalid: yaml: content:").unwrap();
|
||||
std::fs::write(
|
||||
temp_dir.path().join(".miyabirules"),
|
||||
"invalid: yaml: content:",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let result = loader.load();
|
||||
assert!(result.is_err());
|
||||
|
|
|
|||
|
|
@ -353,9 +353,11 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_session_with_system_prompt() {
|
||||
let session = Session::new("Test")
|
||||
.system_prompt("You are a helpful assistant");
|
||||
assert_eq!(session.system_prompt, Some("You are a helpful assistant".to_string()));
|
||||
let session = Session::new("Test").system_prompt("You are a helpful assistant");
|
||||
assert_eq!(
|
||||
session.system_prompt,
|
||||
Some("You are a helpful assistant".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -500,14 +500,22 @@ impl LegacyTasksFile {
|
|||
version: self.version,
|
||||
generated_at: Utc::now(),
|
||||
generated_from_event_id: None,
|
||||
tasks: self.tasks.into_iter().map(LegacyTask::into_execution_task).collect(),
|
||||
tasks: self
|
||||
.tasks
|
||||
.into_iter()
|
||||
.map(LegacyTask::into_execution_task)
|
||||
.collect(),
|
||||
file_locks: HashMap::new(),
|
||||
};
|
||||
|
||||
for task in &snapshot.tasks {
|
||||
if let Some(lock) = &task.lock {
|
||||
let owner_parts: Vec<&str> = lock.locked_by.split('@').collect();
|
||||
let agent = owner_parts.first().copied().unwrap_or("unknown").to_string();
|
||||
let agent = owner_parts
|
||||
.first()
|
||||
.copied()
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
let node = owner_parts.get(1).copied().unwrap_or("unknown").to_string();
|
||||
let expires_at = lease_expiry(lock.last_heartbeat, lock.lease_duration_sec);
|
||||
|
||||
|
|
|
|||
|
|
@ -238,12 +238,8 @@ fn convert_agent_event(event: crate::agent::AgentEvent) -> AgentStreamEvent {
|
|||
crate::agent::AgentEvent::TokenUsage { input, output } => {
|
||||
AgentStreamEvent::TokenUsage { input, output }
|
||||
}
|
||||
crate::agent::AgentEvent::Completed { result } => {
|
||||
AgentStreamEvent::Completed { result }
|
||||
}
|
||||
crate::agent::AgentEvent::Failed { error } => {
|
||||
AgentStreamEvent::Error { error }
|
||||
}
|
||||
crate::agent::AgentEvent::Completed { result } => AgentStreamEvent::Completed { result },
|
||||
crate::agent::AgentEvent::Failed { error } => AgentStreamEvent::Error { error },
|
||||
crate::agent::AgentEvent::ToolApproved { .. } => {
|
||||
// Approval events are internal, map to Started
|
||||
AgentStreamEvent::Started
|
||||
|
|
@ -254,9 +250,7 @@ fn convert_agent_event(event: crate::agent::AgentEvent) -> AgentStreamEvent {
|
|||
crate::agent::AgentEvent::ToolProgress { name, .. } => {
|
||||
AgentStreamEvent::ToolStarted { name }
|
||||
}
|
||||
crate::agent::AgentEvent::TextDelta { text } => {
|
||||
AgentStreamEvent::TextChunk { text }
|
||||
}
|
||||
crate::agent::AgentEvent::TextDelta { text } => AgentStreamEvent::TextChunk { text },
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -353,8 +347,7 @@ mod tests {
|
|||
let client = AnthropicClient::new("test-key".to_string()).unwrap();
|
||||
let registry = ExecutorRegistry::with_standard_tools();
|
||||
|
||||
let agent = StreamingAgent::new(client, registry)
|
||||
.with_system_prompt("You are helpful");
|
||||
let agent = StreamingAgent::new(client, registry).with_system_prompt("You are helpful");
|
||||
assert_eq!(agent.system_prompt, Some("You are helpful".to_string()));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -77,7 +77,6 @@ pub enum StepCondition {
|
|||
If { expression: String },
|
||||
}
|
||||
|
||||
|
||||
/// Retry configuration for a step
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RetryConfig {
|
||||
|
|
@ -120,7 +119,6 @@ pub enum FailurePolicy {
|
|||
Cleanup,
|
||||
}
|
||||
|
||||
|
||||
/// Step execution result
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StepResult {
|
||||
|
|
@ -238,7 +236,11 @@ impl WorkflowManager {
|
|||
for entry in std::fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.extension().map(|e| e == "yml" || e == "yaml").unwrap_or(false) {
|
||||
if path
|
||||
.extension()
|
||||
.map(|e| e == "yml" || e == "yaml")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
if let Ok(name) = self.load_workflow(&path) {
|
||||
loaded.push(name);
|
||||
}
|
||||
|
|
@ -264,7 +266,11 @@ impl WorkflowManager {
|
|||
}
|
||||
|
||||
/// Execute a workflow
|
||||
pub async fn execute(&self, name: &str, initial_vars: HashMap<String, String>) -> Result<WorkflowResult> {
|
||||
pub async fn execute(
|
||||
&self,
|
||||
name: &str,
|
||||
initial_vars: HashMap<String, String>,
|
||||
) -> Result<WorkflowResult> {
|
||||
let workflow = self
|
||||
.workflows
|
||||
.get(name)
|
||||
|
|
@ -361,7 +367,10 @@ impl WorkflowManager {
|
|||
|
||||
let status = if all_succeeded {
|
||||
WorkflowStatus::Completed
|
||||
} else if step_results.iter().any(|r| r.status == StepStatus::Completed) {
|
||||
} else if step_results
|
||||
.iter()
|
||||
.any(|r| r.status == StepStatus::Completed)
|
||||
{
|
||||
WorkflowStatus::PartiallyCompleted
|
||||
} else {
|
||||
WorkflowStatus::Failed
|
||||
|
|
@ -453,24 +462,20 @@ impl WorkflowManager {
|
|||
|
||||
// Execute the task
|
||||
let result = match agent.run(&expanded_task).await {
|
||||
Ok(agent_result) => {
|
||||
StepResult {
|
||||
step_id: step.id.clone(),
|
||||
status: StepStatus::Completed,
|
||||
output: Some(agent_result.output),
|
||||
error: None,
|
||||
duration_ms: step_start.elapsed().as_millis() as u64,
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
StepResult {
|
||||
step_id: step.id.clone(),
|
||||
status: StepStatus::Failed,
|
||||
output: None,
|
||||
error: Some(e.to_string()),
|
||||
duration_ms: step_start.elapsed().as_millis() as u64,
|
||||
}
|
||||
}
|
||||
Ok(agent_result) => StepResult {
|
||||
step_id: step.id.clone(),
|
||||
status: StepStatus::Completed,
|
||||
output: Some(agent_result.output),
|
||||
error: None,
|
||||
duration_ms: step_start.elapsed().as_millis() as u64,
|
||||
},
|
||||
Err(e) => StepResult {
|
||||
step_id: step.id.clone(),
|
||||
status: StepStatus::Failed,
|
||||
output: None,
|
||||
error: Some(e.to_string()),
|
||||
duration_ms: step_start.elapsed().as_millis() as u64,
|
||||
},
|
||||
};
|
||||
|
||||
// Store output variable if specified
|
||||
|
|
@ -495,7 +500,10 @@ impl WorkflowManager {
|
|||
|
||||
let status = if all_succeeded {
|
||||
WorkflowStatus::Completed
|
||||
} else if step_results.iter().any(|r| r.status == StepStatus::Completed) {
|
||||
} else if step_results
|
||||
.iter()
|
||||
.any(|r| r.status == StepStatus::Completed)
|
||||
{
|
||||
WorkflowStatus::PartiallyCompleted
|
||||
} else {
|
||||
WorkflowStatus::Failed
|
||||
|
|
@ -517,7 +525,13 @@ impl WorkflowManager {
|
|||
|
||||
for step in &workflow.steps {
|
||||
if !visited.contains_key(&step.id) {
|
||||
self.visit_step(workflow, &step.id, &mut visited, &mut temp_visited, &mut order)?;
|
||||
self.visit_step(
|
||||
workflow,
|
||||
&step.id,
|
||||
&mut visited,
|
||||
&mut temp_visited,
|
||||
&mut order,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -534,7 +548,10 @@ impl WorkflowManager {
|
|||
order: &mut Vec<String>,
|
||||
) -> Result<()> {
|
||||
if temp_visited.get(step_id).copied().unwrap_or(false) {
|
||||
return Err(anyhow::anyhow!("Circular dependency detected at step: {}", step_id));
|
||||
return Err(anyhow::anyhow!(
|
||||
"Circular dependency detected at step: {}",
|
||||
step_id
|
||||
));
|
||||
}
|
||||
|
||||
if visited.get(step_id).copied().unwrap_or(false) {
|
||||
|
|
@ -561,7 +578,11 @@ impl WorkflowManager {
|
|||
}
|
||||
|
||||
/// Evaluate a step condition
|
||||
fn evaluate_condition(&self, condition: &Option<StepCondition>, context: &WorkflowContext) -> bool {
|
||||
fn evaluate_condition(
|
||||
&self,
|
||||
condition: &Option<StepCondition>,
|
||||
context: &WorkflowContext,
|
||||
) -> bool {
|
||||
match condition {
|
||||
None | Some(StepCondition::Always) => true,
|
||||
Some(StepCondition::OnSuccess) => context
|
||||
|
|
@ -607,19 +628,17 @@ mod tests {
|
|||
let workflow = Workflow {
|
||||
name: "test-workflow".to_string(),
|
||||
description: "Test".to_string(),
|
||||
steps: vec![
|
||||
WorkflowStep {
|
||||
id: "step1".to_string(),
|
||||
name: "First Step".to_string(),
|
||||
agent: Some("code-reviewer".to_string()),
|
||||
task: "Review code".to_string(),
|
||||
depends_on: vec![],
|
||||
condition: None,
|
||||
timeout: 300,
|
||||
retry: RetryConfig::default(),
|
||||
output: Some("review_result".to_string()),
|
||||
},
|
||||
],
|
||||
steps: vec![WorkflowStep {
|
||||
id: "step1".to_string(),
|
||||
name: "First Step".to_string(),
|
||||
agent: Some("code-reviewer".to_string()),
|
||||
task: "Review code".to_string(),
|
||||
depends_on: vec![],
|
||||
condition: None,
|
||||
timeout: 300,
|
||||
retry: RetryConfig::default(),
|
||||
output: Some("review_result".to_string()),
|
||||
}],
|
||||
variables: HashMap::new(),
|
||||
on_failure: FailurePolicy::Stop,
|
||||
};
|
||||
|
|
@ -815,7 +834,10 @@ mod tests {
|
|||
};
|
||||
|
||||
manager.register(workflow);
|
||||
let result = manager.execute("output-test", HashMap::new()).await.unwrap();
|
||||
let result = manager
|
||||
.execute("output-test", HashMap::new())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.status, WorkflowStatus::Completed);
|
||||
assert_eq!(result.steps.len(), 2);
|
||||
|
|
@ -834,7 +856,10 @@ mod tests {
|
|||
on_failure: FailurePolicy::Stop,
|
||||
};
|
||||
|
||||
assert_eq!(workflow.variables.get("project"), Some(&"miyabi".to_string()));
|
||||
assert_eq!(
|
||||
workflow.variables.get("project"),
|
||||
Some(&"miyabi".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -145,16 +145,15 @@ impl App {
|
|||
Ok(Some(rules)) => {
|
||||
let count = rules.rules.len();
|
||||
if count > 0 {
|
||||
view.notifications.info(
|
||||
"Rules Loaded",
|
||||
format!("{} project rules active", count),
|
||||
);
|
||||
view.notifications
|
||||
.info("Rules Loaded", format!("{} project rules active", count));
|
||||
}
|
||||
Some(rules)
|
||||
}
|
||||
Ok(None) => None,
|
||||
Err(e) => {
|
||||
view.notifications.error("Rules Error", format!("Failed to load: {}", e));
|
||||
view.notifications
|
||||
.error("Rules Error", format!("Failed to load: {}", e));
|
||||
None
|
||||
}
|
||||
};
|
||||
|
|
@ -280,9 +279,10 @@ impl App {
|
|||
.notifications
|
||||
.error("Clipboard Error", e.to_string());
|
||||
} else {
|
||||
self.view
|
||||
.notifications
|
||||
.info("Copied", format!("{} chars", text.len()));
|
||||
self.view.notifications.info(
|
||||
"Copied",
|
||||
format!("{} chars", text.len()),
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
|
|
@ -305,7 +305,8 @@ impl App {
|
|||
#[cfg(target_os = "macos")]
|
||||
let result = std::process::Command::new("open").arg(&path).spawn();
|
||||
#[cfg(target_os = "linux")]
|
||||
let result = std::process::Command::new("xdg-open").arg(&path).spawn();
|
||||
let result =
|
||||
std::process::Command::new("xdg-open").arg(&path).spawn();
|
||||
#[cfg(target_os = "windows")]
|
||||
let result = std::process::Command::new("cmd")
|
||||
.args(["/C", "start", "", &path])
|
||||
|
|
@ -316,9 +317,7 @@ impl App {
|
|||
self.view.notifications.info("Opened", &path);
|
||||
}
|
||||
Err(e) => {
|
||||
self.view
|
||||
.notifications
|
||||
.error("Open Error", e.to_string());
|
||||
self.view.notifications.error("Open Error", e.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,8 +40,7 @@ pub enum VimMode {
|
|||
}
|
||||
|
||||
/// Keybinding style preference
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Default)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum KeybindingStyle {
|
||||
/// Standard keybindings
|
||||
#[default]
|
||||
|
|
@ -52,7 +51,6 @@ pub enum KeybindingStyle {
|
|||
Emacs,
|
||||
}
|
||||
|
||||
|
||||
/// Edit operation for undo/redo
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum EditOperation {
|
||||
|
|
@ -1479,12 +1477,14 @@ impl ChatComposer {
|
|||
return Style::default().bg(Color::Rgb(68, 71, 90)).fg(Color::Cyan);
|
||||
}
|
||||
}
|
||||
if line_idx == self.cursor.line && col == self.cursor.col
|
||||
&& matches!(ch, '(' | ')' | '[' | ']' | '{' | '}' | '<' | '>') {
|
||||
return Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
}
|
||||
if line_idx == self.cursor.line
|
||||
&& col == self.cursor.col
|
||||
&& matches!(ch, '(' | ')' | '[' | ']' | '{' | '}' | '<' | '>')
|
||||
{
|
||||
return Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
}
|
||||
|
||||
// Basic syntax highlighting
|
||||
match ch {
|
||||
|
|
|
|||
|
|
@ -36,27 +36,25 @@ pub fn render_diff_widget_with_scroll(
|
|||
// Create a DiffRender with the single file
|
||||
let mut renderer = DiffRender::new();
|
||||
renderer.files.push(diff.clone());
|
||||
|
||||
|
||||
// Get rendered lines
|
||||
let lines = renderer.render();
|
||||
|
||||
|
||||
// Create block with optional title
|
||||
let block = if let Some(t) = title {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(t.to_string())
|
||||
Block::default().borders(Borders::ALL).title(t.to_string())
|
||||
} else {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(format!("{} → {}", diff.old_path, diff.new_path))
|
||||
};
|
||||
|
||||
|
||||
// Create paragraph with scroll
|
||||
let paragraph = Paragraph::new(lines)
|
||||
.block(block)
|
||||
.wrap(Wrap { trim: false })
|
||||
.scroll((scroll, 0));
|
||||
|
||||
|
||||
frame.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -65,10 +65,7 @@ impl HistoryList {
|
|||
}
|
||||
}
|
||||
|
||||
let list_items: Vec<ListItem> = visible_lines
|
||||
.into_iter()
|
||||
.map(ListItem::new)
|
||||
.collect();
|
||||
let list_items: Vec<ListItem> = visible_lines.into_iter().map(ListItem::new).collect();
|
||||
|
||||
let list = List::new(list_items).style(Style::default().fg(colors::FG));
|
||||
frame.render_widget(list, inner);
|
||||
|
|
|
|||
|
|
@ -392,9 +392,11 @@ impl MainView {
|
|||
}
|
||||
_ => {}
|
||||
},
|
||||
ActiveOverlay::Help => if self.help_viewer.handle_key(key) == HelpAction::Close {
|
||||
self.close_overlay();
|
||||
},
|
||||
ActiveOverlay::Help => {
|
||||
if self.help_viewer.handle_key(key) == HelpAction::Close {
|
||||
self.close_overlay();
|
||||
}
|
||||
}
|
||||
ActiveOverlay::Approval => match self.approval_overlay.handle_key(key) {
|
||||
ApprovalAction::Approve(id) | ApprovalAction::ApproveAll(id) => {
|
||||
self.close_overlay();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue