Initial scaffold: GyanDoot v2.0 in Rust
Full Cargo workspace with 11 crates: - gyandoot-core: Shared types, config, error types - gyandoot-agent: Agent loop, tool execution, context compression - gyandoot-providers: LLM provider abstraction (OpenAI, Anthropic, Ollama) - gyandoot-memory: MemPalace persistent memory system - gyandoot-platform: Platform adapters (Telegram, Discord, Webhook, CLI) - gyandoot-security: Credential vault, sanitizer, threat detector - gyandoot-kanban: Task decomposition, swarm orchestration, diagnostics - gyandoot-subagents: Sub-agent spawning and orchestration - gyandoot-skills: Skills discovery, loading, preprocessing, bundles - gyandoot-profile: Multi-agent profile manager - gyandoot-gateway: Gateway sessions, routing, access control Plus: CLI entry point with clap, cross-platform installer script, KANBAN.md, .gnpl/AGENTS.md Workspace compiles clean: cargo check passes with 0 errors.
This commit is contained in:
217
crates/gyandoot-subagents/src/lib.rs
Normal file
217
crates/gyandoot-subagents/src/lib.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
//! # Gyandoot Sub-Agents
|
||||
//!
|
||||
//! Isolated sub-agent spawning and orchestration for parallel task execution.
|
||||
//! Each sub-agent runs in its own context with filtered tools and optional workdir.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
use chrono::Utc;
|
||||
use tracing::{info, error};
|
||||
|
||||
/// Unique identifier for a sub-agent worker.
|
||||
pub type WorkerId = Uuid;
|
||||
|
||||
/// Status of a sub-agent task.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum WorkerStatus {
|
||||
Pending,
|
||||
Running,
|
||||
Completed { success: bool },
|
||||
Failed(String),
|
||||
}
|
||||
|
||||
/// Configuration for a single sub-agent worker.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WorkerConfig {
|
||||
pub id: WorkerId,
|
||||
pub goal: String,
|
||||
pub context: Option<String>,
|
||||
pub allowed_tools: Option<Vec<String>>,
|
||||
pub workdir: Option<String>,
|
||||
pub max_iterations: usize,
|
||||
}
|
||||
|
||||
/// Result produced by a completed sub-agent.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WorkerResult {
|
||||
pub worker_id: WorkerId,
|
||||
pub goal: String,
|
||||
pub success: bool,
|
||||
pub summary: String,
|
||||
pub iteration_count: usize,
|
||||
pub error: Option<String>,
|
||||
pub started_at: chrono::DateTime<Utc>,
|
||||
pub completed_at: chrono::DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// A single sub-agent worker that runs a goal in isolation.
|
||||
pub struct SubAgentWorker {
|
||||
pub config: WorkerConfig,
|
||||
status: Arc<Mutex<WorkerStatus>>,
|
||||
}
|
||||
|
||||
impl SubAgentWorker {
|
||||
pub fn new(goal: &str) -> Self {
|
||||
Self {
|
||||
config: WorkerConfig {
|
||||
id: Uuid::new_v4(),
|
||||
goal: goal.to_string(),
|
||||
context: None,
|
||||
allowed_tools: None,
|
||||
workdir: None,
|
||||
max_iterations: 25,
|
||||
},
|
||||
status: Arc::new(Mutex::new(WorkerStatus::Pending)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_context(mut self, ctx: &str) -> Self {
|
||||
self.config.context = Some(ctx.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_tools(mut self, tools: Vec<String>) -> Self {
|
||||
self.config.allowed_tools = Some(tools);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_workdir(mut self, wd: &str) -> Self {
|
||||
self.config.workdir = Some(wd.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Execute the sub-agent task (simulated — real execution delegates to agent loop).
|
||||
pub async fn execute(&self) -> WorkerResult {
|
||||
let started_at = Utc::now();
|
||||
{
|
||||
let mut s = self.status.lock().await;
|
||||
*s = WorkerStatus::Running;
|
||||
}
|
||||
info!(worker_id = %self.config.id, goal = %self.config.goal, "Sub-agent started");
|
||||
|
||||
// Placeholder: real logic will run an isolated AIAgent session
|
||||
let success = true;
|
||||
let summary = format!("Completed: {}", self.config.goal);
|
||||
let iterations = 1;
|
||||
|
||||
{
|
||||
let mut s = self.status.lock().await;
|
||||
*s = WorkerStatus::Completed { success };
|
||||
}
|
||||
|
||||
WorkerResult {
|
||||
worker_id: self.config.id,
|
||||
goal: self.config.goal.clone(),
|
||||
success,
|
||||
summary,
|
||||
iteration_count: iterations,
|
||||
error: None,
|
||||
started_at,
|
||||
completed_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn status(&self) -> WorkerStatus {
|
||||
self.status.lock().await.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Manages concurrent sub-agent workers (max parallelism bounded).
|
||||
pub struct SubAgentManager {
|
||||
workers: Arc<Mutex<HashMap<WorkerId, SubAgentWorker>>>,
|
||||
max_concurrent: usize,
|
||||
}
|
||||
|
||||
impl SubAgentManager {
|
||||
pub fn new(max_concurrent: usize) -> Self {
|
||||
Self {
|
||||
workers: Arc::new(Mutex::new(HashMap::new())),
|
||||
max_concurrent,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn spawn(&self, worker: SubAgentWorker) -> WorkerId {
|
||||
let id = worker.config.id;
|
||||
let mut w = self.workers.lock().await;
|
||||
w.insert(id, worker);
|
||||
id
|
||||
}
|
||||
|
||||
/// Execute all pending workers with bounded concurrency.
|
||||
pub async fn execute_all(&self) -> Vec<WorkerResult> {
|
||||
let ids: Vec<WorkerId>;
|
||||
{
|
||||
let w = self.workers.lock().await;
|
||||
ids = w.keys().copied().collect();
|
||||
}
|
||||
|
||||
let semaphore = Arc::new(tokio::sync::Semaphore::new(self.max_concurrent));
|
||||
let mut handles = Vec::new();
|
||||
|
||||
for id in &ids {
|
||||
let sem = semaphore.clone();
|
||||
let worker: SubAgentWorker;
|
||||
{
|
||||
let w = self.workers.lock().await;
|
||||
worker = w.get(id).unwrap().clone(); // requires Clone — see note
|
||||
}
|
||||
handles.push(tokio::spawn(async move {
|
||||
let _permit = sem.acquire().await.unwrap();
|
||||
worker.execute().await
|
||||
}));
|
||||
}
|
||||
|
||||
let mut results = Vec::new();
|
||||
for h in handles {
|
||||
results.push(h.await.unwrap_or_else(|e| WorkerResult {
|
||||
worker_id: Uuid::nil(),
|
||||
goal: "failed".into(),
|
||||
success: false,
|
||||
summary: String::new(),
|
||||
iteration_count: 0,
|
||||
error: Some(e.to_string()),
|
||||
started_at: Utc::now(),
|
||||
completed_at: Utc::now(),
|
||||
}));
|
||||
}
|
||||
results
|
||||
}
|
||||
}
|
||||
|
||||
// Manual clone impl since Arc<Mutex<>> is Clone automatically
|
||||
impl Clone for SubAgentWorker {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
config: self.config.clone(),
|
||||
status: Arc::clone(&self.status),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_worker_creation_and_execution() {
|
||||
let worker = SubAgentWorker::new("Refactor auth module")
|
||||
.with_context("Project uses JWT tokens")
|
||||
.with_workdir("/tmp/test");
|
||||
let result = worker.execute().await;
|
||||
assert!(result.success);
|
||||
assert_eq!(result.goal, "Refactor auth module");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_manager_bounded_concurrency() {
|
||||
let manager = SubAgentManager::new(2);
|
||||
for i in 0..4 {
|
||||
let w = SubAgentWorker::new(&format!("Task {}", i));
|
||||
manager.spawn(w).await;
|
||||
}
|
||||
let results = manager.execute_all().await;
|
||||
assert_eq!(results.len(), 4);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user