WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit bd51407

Browse files
gouravjshahclaude
andcommitted
feat: Add Slack approval workflow, conversation memory, and multi-tenant routing
Major Features: - Human-in-the-loop approval workflow with Slack reactions (✅/❌) - Conversation memory system for contextual follow-up messages - AgentFlow multi-tenant routing for message-to-agent mapping - Bot self-approval prevention with auto-detected bot_user_id - Improved kubectl syntax guidance in agent instructions Technical Changes: - New flow registry and router for AgentFlow-based message routing - Reaction event handling in trigger handler - Pending approvals storage with DashMap - Platform-specific approval UI (Slack reactions) - Conversation memory with channel/thread isolation Documentation: - New approval workflow guide - New conversation memory guide - Updated documentation index - Example multi-tenant flow configurations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent c35b378 commit bd51407

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+4370
-90
lines changed

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,4 +502,5 @@ NEVER create files unless they're absolutely necessary for achieving your goal.
502502
ALWAYS prefer editing an existing file to creating a new one.
503503
NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
504504
Never save working files, text/mds and tests to the root folder.
505-
- Strictly follow kubectl style implementation for aofctl. For example use "aofctl run agent" instead of "aofctl agent run". If you find anything non compliant, correct it.
505+
- Strictly follow kubectl style implementation for aofctl. For example use "aofctl run agent" instead of "aofctl agent run". If you find anything non compliant, correct it.
506+
- for every feature added, add/update docs/ so that we are keeping track of every single feautre the product has and how it works.

crates/aof-core/src/agentflow.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,44 @@ pub struct AgentFlowSpec {
9292
#[serde(default, skip_serializing_if = "Vec::is_empty")]
9393
pub triggers: Vec<FlowTrigger>,
9494

95+
/// Execution context (environment, kubeconfig, etc.)
96+
#[serde(skip_serializing_if = "Option::is_none")]
97+
pub context: Option<FlowContext>,
98+
9599
/// Global flow configuration
96100
#[serde(skip_serializing_if = "Option::is_none")]
97101
pub config: Option<FlowConfig>,
98102
}
99103

104+
/// Flow execution context - environment and runtime configuration
105+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
106+
#[serde(rename_all = "camelCase")]
107+
pub struct FlowContext {
108+
/// Kubeconfig file path (for kubectl tools)
109+
#[serde(skip_serializing_if = "Option::is_none")]
110+
pub kubeconfig: Option<String>,
111+
112+
/// Kubernetes namespace (default context)
113+
#[serde(skip_serializing_if = "Option::is_none")]
114+
pub namespace: Option<String>,
115+
116+
/// Kubernetes cluster name
117+
#[serde(skip_serializing_if = "Option::is_none")]
118+
pub cluster: Option<String>,
119+
120+
/// Environment variables to set for agent execution
121+
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
122+
pub env: HashMap<String, String>,
123+
124+
/// Working directory for tool execution
125+
#[serde(skip_serializing_if = "Option::is_none")]
126+
pub working_dir: Option<String>,
127+
128+
/// Additional context variables available in templates
129+
#[serde(flatten, skip_serializing_if = "HashMap::is_empty")]
130+
pub extra: HashMap<String, serde_json::Value>,
131+
}
132+
100133
/// Flow trigger configuration
101134
#[derive(Debug, Clone, Serialize, Deserialize)]
102135
#[serde(rename_all = "camelCase")]
@@ -137,6 +170,18 @@ pub struct TriggerConfig {
137170
#[serde(default, skip_serializing_if = "Vec::is_empty")]
138171
pub events: Vec<String>,
139172

173+
/// Channels to listen on (Slack/Discord channel names or IDs)
174+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
175+
pub channels: Vec<String>,
176+
177+
/// Users to respond to (user IDs or patterns)
178+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
179+
pub users: Vec<String>,
180+
181+
/// Message patterns to match (regex)
182+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
183+
pub patterns: Vec<String>,
184+
140185
/// Bot token (or env var reference ${VAR_NAME})
141186
#[serde(skip_serializing_if = "Option::is_none")]
142187
pub bot_token: Option<String>,
@@ -227,6 +272,11 @@ pub struct NodeConfig {
227272
#[serde(skip_serializing_if = "Option::is_none")]
228273
pub agent: Option<String>,
229274

275+
/// Inline agent configuration (YAML string)
276+
/// Use this to embed agent config directly in the flow
277+
#[serde(skip_serializing_if = "Option::is_none")]
278+
pub agent_config: Option<String>,
279+
230280
/// Input to the agent (can contain ${variable} references)
231281
#[serde(skip_serializing_if = "Option::is_none")]
232282
pub input: Option<String>,

crates/aof-core/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ pub use fleet::{
4646
};
4747
pub use agentflow::{
4848
AgentFlow, AgentFlowMetadata, AgentFlowSpec, AgentFlowState, FlowConfig,
49-
FlowConnection, FlowError, FlowExecutionStatus, FlowNode, FlowRetryConfig,
49+
FlowConnection, FlowContext, FlowError, FlowExecutionStatus, FlowNode, FlowRetryConfig,
5050
FlowTrigger, NodeCondition, NodeConfig, NodeExecutionStatus, NodeResult, NodeType,
5151
TriggerConfig, TriggerType,
5252
};

crates/aof-runtime/src/executor/agentflow_executor.rs

Lines changed: 157 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,17 @@
88
//! - Platform-specific actions (Slack, Discord, etc.)
99
1010
use std::collections::HashMap;
11+
use std::path::PathBuf;
1112
use std::sync::Arc;
1213

1314
use chrono::Utc;
14-
use tokio::sync::mpsc;
15+
use tokio::sync::{mpsc, RwLock};
1516
use tracing::{debug, error, info, warn};
1617
use uuid::Uuid;
1718

1819
use aof_core::{
19-
AgentFlow, AgentFlowState, AofError, AofResult, FlowError, FlowExecutionStatus, FlowNode,
20-
NodeExecutionStatus, NodeResult, NodeType,
20+
AgentConfig, AgentFlow, AgentFlowState, AofError, AofResult, FlowError, FlowExecutionStatus,
21+
FlowNode, NodeExecutionStatus, NodeResult, NodeType,
2122
};
2223

2324
use super::Runtime;
@@ -63,23 +64,36 @@ pub enum AgentFlowEvent {
6364
/// AgentFlow executor
6465
pub struct AgentFlowExecutor {
6566
flow: AgentFlow,
66-
#[allow(dead_code)]
67-
runtime: Arc<Runtime>,
67+
runtime: Arc<RwLock<Runtime>>,
6868
event_tx: Option<mpsc::Sender<AgentFlowEvent>>,
69+
/// Directory to search for agent YAML files
70+
agents_dir: Option<PathBuf>,
6971
}
7072

7173
impl AgentFlowExecutor {
7274
/// Create a new AgentFlow executor
73-
pub fn new(flow: AgentFlow, runtime: Arc<Runtime>) -> Self {
75+
pub fn new(flow: AgentFlow, runtime: Arc<RwLock<Runtime>>) -> Self {
7476
Self {
7577
flow,
7678
runtime,
7779
event_tx: None,
80+
agents_dir: None,
7881
}
7982
}
8083

84+
/// Create with a non-locked runtime (convenience constructor)
85+
pub fn with_runtime(flow: AgentFlow, runtime: Runtime) -> Self {
86+
Self::new(flow, Arc::new(RwLock::new(runtime)))
87+
}
88+
89+
/// Set the agents directory for loading agent configs
90+
pub fn with_agents_dir(mut self, dir: impl Into<PathBuf>) -> Self {
91+
self.agents_dir = Some(dir.into());
92+
self
93+
}
94+
8195
/// Load AgentFlow from file
82-
pub async fn from_file(path: &str, runtime: Arc<Runtime>) -> AofResult<Self> {
96+
pub async fn from_file(path: &str, runtime: Arc<RwLock<Runtime>>) -> AofResult<Self> {
8397
let content = std::fs::read_to_string(path).map_err(|e| {
8498
AofError::Config(format!("Failed to read AgentFlow config {}: {}", path, e))
8599
})?;
@@ -388,31 +402,146 @@ impl AgentFlowExecutor {
388402
Ok(agent_result)
389403
}
390404

391-
/// Run an agent (placeholder - needs integration with AgentExecutor)
405+
/// Run an agent using the runtime
406+
///
407+
/// This method:
408+
/// 1. Checks if the agent is already loaded in the runtime
409+
/// 2. If not, tries to load it from the agents directory
410+
/// 3. Applies flow context (kubeconfig, env vars) to the execution
411+
/// 4. Executes the agent and returns the result
392412
async fn run_agent(
393413
&self,
394414
agent_name: &str,
395415
input: &str,
396-
_state: &AgentFlowState,
416+
state: &AgentFlowState,
397417
) -> AofResult<serde_json::Value> {
398-
// This is a simplified implementation
399-
// In production, this would:
400-
// 1. Load agent config from registry or file
401-
// 2. Create AgentExecutor
402-
// 3. Run the agent with input
403-
// 4. Return the response
404-
405-
// For now, try to use the runtime to check if agent exists
406-
info!("Agent {} called with input: {}", agent_name, input);
407-
408-
// Return a placeholder response
409-
// The real implementation would integrate with aof_runtime::AgentExecutor
410-
Ok(serde_json::json!({
411-
"agent": agent_name,
412-
"input": input,
413-
"output": format!("Agent {} processed: {}", agent_name, input),
414-
"requires_approval": false
415-
}))
418+
info!("Executing agent '{}' with input: {}", agent_name, input);
419+
420+
// First, try to execute with the agent already loaded
421+
{
422+
let runtime = self.runtime.read().await;
423+
if runtime.has_agent(agent_name) {
424+
// Apply flow context if specified
425+
self.apply_flow_context().await?;
426+
427+
let result = runtime.execute(agent_name, input).await?;
428+
return Ok(serde_json::json!({
429+
"agent": agent_name,
430+
"input": input,
431+
"output": result,
432+
"requires_approval": false
433+
}));
434+
}
435+
}
436+
437+
// Agent not loaded - try to load from agents directory
438+
if let Some(ref agents_dir) = self.agents_dir {
439+
// Try common naming patterns
440+
let possible_paths = vec![
441+
agents_dir.join(format!("{}.yaml", agent_name)),
442+
agents_dir.join(format!("{}.yml", agent_name)),
443+
agents_dir.join(format!("{}-agent.yaml", agent_name)),
444+
agents_dir.join(format!("{}-agent.yml", agent_name)),
445+
];
446+
447+
for path in possible_paths {
448+
if path.exists() {
449+
info!("Loading agent '{}' from {}", agent_name, path.display());
450+
451+
let mut runtime = self.runtime.write().await;
452+
runtime.load_agent_from_file(path.to_string_lossy().as_ref()).await?;
453+
454+
// Apply flow context
455+
drop(runtime); // Release write lock
456+
self.apply_flow_context().await?;
457+
458+
let runtime = self.runtime.read().await;
459+
let result = runtime.execute(agent_name, input).await?;
460+
461+
return Ok(serde_json::json!({
462+
"agent": agent_name,
463+
"input": input,
464+
"output": result,
465+
"requires_approval": false
466+
}));
467+
}
468+
}
469+
}
470+
471+
// Agent config might also be embedded in the flow
472+
// Check if there's a node config with agent config
473+
if let Some(node) = self.flow.spec.nodes.iter().find(|n| {
474+
n.config.agent.as_ref() == Some(&agent_name.to_string())
475+
}) {
476+
if let Some(ref config_yaml) = node.config.agent_config {
477+
info!("Loading agent '{}' from inline config", agent_name);
478+
479+
let agent_config: AgentConfig = serde_yaml::from_str(config_yaml).map_err(|e| {
480+
AofError::Config(format!("Failed to parse inline agent config: {}", e))
481+
})?;
482+
483+
let mut runtime = self.runtime.write().await;
484+
runtime.load_agent_from_config(agent_config).await?;
485+
486+
drop(runtime);
487+
self.apply_flow_context().await?;
488+
489+
let runtime = self.runtime.read().await;
490+
let result = runtime.execute(agent_name, input).await?;
491+
492+
return Ok(serde_json::json!({
493+
"agent": agent_name,
494+
"input": input,
495+
"output": result,
496+
"requires_approval": false
497+
}));
498+
}
499+
}
500+
501+
// Could not find or load the agent
502+
Err(AofError::Config(format!(
503+
"Agent '{}' not found. Ensure it's loaded or available in the agents directory.",
504+
agent_name
505+
)))
506+
}
507+
508+
/// Apply flow context to the environment
509+
async fn apply_flow_context(&self) -> AofResult<()> {
510+
if let Some(ref context) = self.flow.spec.context {
511+
// Set KUBECONFIG if specified
512+
if let Some(ref kubeconfig) = context.kubeconfig {
513+
std::env::set_var("KUBECONFIG", kubeconfig);
514+
info!("Set KUBECONFIG to {}", kubeconfig);
515+
}
516+
517+
// Set namespace as env var if specified
518+
if let Some(ref namespace) = context.namespace {
519+
std::env::set_var("K8S_NAMESPACE", namespace);
520+
info!("Set K8S_NAMESPACE to {}", namespace);
521+
}
522+
523+
// Set cluster name if specified
524+
if let Some(ref cluster) = context.cluster {
525+
std::env::set_var("K8S_CLUSTER", cluster);
526+
info!("Set K8S_CLUSTER to {}", cluster);
527+
}
528+
529+
// Set working directory if specified
530+
if let Some(ref working_dir) = context.working_dir {
531+
std::env::set_current_dir(working_dir).map_err(|e| {
532+
AofError::Config(format!("Failed to change working directory: {}", e))
533+
})?;
534+
info!("Changed working directory to {}", working_dir);
535+
}
536+
537+
// Set additional environment variables
538+
for (key, value) in &context.env {
539+
std::env::set_var(key, value);
540+
debug!("Set env var {}={}", key, value);
541+
}
542+
}
543+
544+
Ok(())
416545
}
417546

418547
/// Execute a Conditional node
@@ -874,7 +1003,7 @@ mod tests {
8741003

8751004
#[test]
8761005
fn test_evaluate_condition() {
877-
let runtime = Arc::new(Runtime::new());
1006+
let runtime = Arc::new(RwLock::new(Runtime::new()));
8781007
let flow: AgentFlow = serde_yaml::from_str(
8791008
r#"
8801009
apiVersion: aof.dev/v1

crates/aof-runtime/src/executor/runtime.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,11 @@ impl Runtime {
328328
self.agents.keys().cloned().collect()
329329
}
330330

331+
/// Check if an agent is loaded
332+
pub fn has_agent(&self, name: &str) -> bool {
333+
self.agents.contains_key(name)
334+
}
335+
331336
/// Get agent executor by name
332337
pub fn get_agent(&self, name: &str) -> Option<Arc<AgentExecutor>> {
333338
self.agents.get(name).cloned()

crates/aof-triggers/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ aof-core.workspace = true
1313
aof-runtime.workspace = true
1414
aof-llm.workspace = true
1515
aof-memory.workspace = true
16+
aof-tools = { workspace = true, features = ["all"] }
1617

1718
# Async runtime
1819
tokio.workspace = true
@@ -22,6 +23,7 @@ futures.workspace = true
2223
# Serialization
2324
serde.workspace = true
2425
serde_json.workspace = true
26+
serde_yaml.workspace = true
2527

2628
# Error handling
2729
thiserror.workspace = true
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//! Flow module - FlowRegistry and FlowRouter for AgentFlow management
2+
//!
3+
//! This module provides:
4+
//! - `FlowRegistry` - Loads and manages AgentFlow configurations
5+
//! - `FlowRouter` - Routes incoming trigger events to matching flows
6+
7+
pub mod registry;
8+
pub mod router;
9+
10+
pub use registry::FlowRegistry;
11+
pub use router::{FlowMatch, FlowRouter, MatchReason};

0 commit comments

Comments
 (0)