Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
41 changes: 41 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,47 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

publish-crates:
name: Publish to crates.io
needs: create-release
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Publish crates in dependency order
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
# Crates in dependency order (leaf dependencies first)
CRATES=(
"aof-core"
"aof-mcp"
"aof-llm"
"aof-memory"
"aof-tools"
"aof-runtime"
"aof-triggers"
"aofctl"
)

# Wait time between publishes to allow crates.io index to update
WAIT_SECONDS=30

for crate in "${CRATES[@]}"; do
echo "📦 Publishing $crate..."
cargo publish -p "$crate" --no-verify || true
echo "⏳ Waiting ${WAIT_SECONDS}s for crates.io index to update..."
sleep $WAIT_SECONDS
done

echo "✅ All crates published!"

deploy-install-script:
name: Deploy install.sh to web
needs: create-release
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.2-beta] - 2026-01-02

### Added
- Built-in command handler support via `agent: builtin` in trigger command bindings
- Use `agent: builtin` for `/help`, `/agent`, `/fleet` to get interactive menus
- Interactive menus include fleet/agent selection buttons (Telegram/Slack)
- Keeps built-in UI handlers separate from LLM-routed commands
- Stale message filtering for webhook handlers
- Messages older than 60 seconds are silently dropped
- Prevents processing of queued messages when daemon restarts
- Configurable via `max_message_age_secs` in handler config
- `cargo install aofctl` support via crates.io publishing
- All AOF crates now published to crates.io
- Automated publishing on tagged releases
- New documentation: Built-in Commands Guide (`docs/guides/builtin-commands.md`)

### Fixed
- `aofctl serve` now produces visible startup output
- Changed from tracing (default level: error) to println for critical startup messages
Expand All @@ -16,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Intermediate acknowledgment messages ("Thinking...", "Processing...") are skipped for Git platforms
- Only the final response is posted, keeping PR threads clean
- Slack/Telegram/Discord still show real-time progress indicators
- Improved `library://` URI path resolution for agent library

## [0.3.1-beta] - 2025-12-26

Expand Down
26 changes: 15 additions & 11 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@ members = [
]

[workspace.package]
version = "0.3.1-beta"
version = "0.3.2-beta"
edition = "2021"
rust-version = "1.75"
license = "Apache-2.0"
repository = "https://github.com/yourusername/aof"
authors = ["Your Name <[email protected]>"]
repository = "https://github.com/agenticdevops/aof"
authors = ["Gourav Shah <[email protected]>"]
keywords = ["ai", "agents", "llm", "devops", "kubernetes"]
categories = ["command-line-utilities", "development-tools"]
homepage = "https://aof.sh"
documentation = "https://docs.aof.sh"

[workspace.dependencies]
# Async runtime
Expand Down Expand Up @@ -72,14 +76,14 @@ rand = "0.8"
# Regex
regex = "1.10"

# Internal workspace dependencies
aof-core = { path = "crates/aof-core" }
aof-mcp = { path = "crates/aof-mcp" }
aof-llm = { path = "crates/aof-llm" }
aof-runtime = { path = "crates/aof-runtime" }
aof-memory = { path = "crates/aof-memory" }
aof-triggers = { path = "crates/aof-triggers" }
aof-tools = { path = "crates/aof-tools" }
# Internal workspace dependencies (path for local dev, version for crates.io)
aof-core = { path = "crates/aof-core", version = "0.3.2-beta" }
aof-mcp = { path = "crates/aof-mcp", version = "0.3.2-beta" }
aof-llm = { path = "crates/aof-llm", version = "0.3.2-beta" }
aof-runtime = { path = "crates/aof-runtime", version = "0.3.2-beta" }
aof-memory = { path = "crates/aof-memory", version = "0.3.2-beta" }
aof-triggers = { path = "crates/aof-triggers", version = "0.3.2-beta" }
aof-tools = { path = "crates/aof-tools", version = "0.3.2-beta" }

# File utilities
glob = "0.3"
Expand Down
2 changes: 1 addition & 1 deletion configs/telegram-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ metadata:

spec:
server:
port: 8080
port: 3000
host: 0.0.0.0
cors: true
timeout_secs: 60
Expand Down
4 changes: 4 additions & 0 deletions crates/aof-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ license.workspace = true
repository.workspace = true
authors.workspace = true
description = "Core types, traits, and abstractions for AOF framework"
keywords.workspace = true
categories.workspace = true
homepage.workspace = true
documentation.workspace = true

[dependencies]
serde = { workspace = true }
Expand Down
4 changes: 4 additions & 0 deletions crates/aof-llm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ license.workspace = true
repository.workspace = true
authors.workspace = true
description = "Multi-provider LLM abstraction layer"
keywords.workspace = true
categories.workspace = true
homepage.workspace = true
documentation.workspace = true

[dependencies]
aof-core = { workspace = true }
Expand Down
28 changes: 28 additions & 0 deletions crates/aof-llm/src/provider/google.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ impl GoogleModel {
// Note: Gemini uses "user" and "model" roles only. Tool responses use functionResponse parts.
let mut contents: Vec<GeminiContent> = Vec::new();

// Debug: Log all incoming messages with their structure
tracing::warn!("[GOOGLE] Building request with {} messages:", request.messages.len());
for (idx, msg) in request.messages.iter().enumerate() {
let tool_calls_info = msg.tool_calls.as_ref()
.map(|tcs| format!("{} tool calls", tcs.len()))
.unwrap_or_else(|| "no tool calls".to_string());
tracing::warn!(
"[GOOGLE] Message[{}]: role={:?}, content_len={}, {}",
idx, msg.role, msg.content.len(), tool_calls_info
);
}

for (i, m) in request.messages.iter().enumerate() {
match m.role {
MessageRole::User => {
Expand Down Expand Up @@ -163,6 +175,22 @@ impl GoogleModel {
top_k: None,
};

// Debug: Log the final converted contents structure
tracing::warn!("[GOOGLE] Final contents structure ({} items):", contents.len());
for (idx, content) in contents.iter().enumerate() {
let parts_info: Vec<String> = content.parts.iter().map(|p| {
match p {
GeminiPart::Text { text } => format!("text({})", text.len()),
GeminiPart::FunctionCall { function_call } => format!("functionCall({})", function_call.name),
GeminiPart::FunctionResponse { function_response } => format!("functionResponse({})", function_response.name),
}
}).collect();
tracing::warn!(
"[GOOGLE] Content[{}]: role={}, parts=[{}]",
idx, content.role, parts_info.join(", ")
);
}

GeminiRequest {
contents,
system_instruction,
Expand Down
4 changes: 4 additions & 0 deletions crates/aof-mcp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ license.workspace = true
repository.workspace = true
authors.workspace = true
description = "Model Context Protocol (MCP) client implementation"
keywords.workspace = true
categories.workspace = true
homepage.workspace = true
documentation.workspace = true

[dependencies]
aof-core = { workspace = true }
Expand Down
4 changes: 4 additions & 0 deletions crates/aof-memory/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ license.workspace = true
repository.workspace = true
authors.workspace = true
description = "Pluggable memory backends for agent state management"
keywords.workspace = true
categories.workspace = true
homepage.workspace = true
documentation.workspace = true

[dependencies]
aof-core = { workspace = true }
Expand Down
4 changes: 4 additions & 0 deletions crates/aof-runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ license.workspace = true
repository.workspace = true
authors.workspace = true
description = "Agent execution runtime with task orchestration"
keywords.workspace = true
categories.workspace = true
homepage.workspace = true
documentation.workspace = true

[dependencies]
aof-core = { workspace = true }
Expand Down
4 changes: 4 additions & 0 deletions crates/aof-tools/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ license.workspace = true
repository.workspace = true
authors.workspace = true
description = "Modular tool implementations for AOF agents"
keywords.workspace = true
categories.workspace = true
homepage.workspace = true
documentation.workspace = true

[features]
default = ["file", "shell", "git"]
Expand Down
5 changes: 5 additions & 0 deletions crates/aof-triggers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ rust-version.workspace = true
license.workspace = true
repository.workspace = true
authors.workspace = true
description = "Event triggers and webhook handlers for AOF agents"
keywords.workspace = true
categories.workspace = true
homepage.workspace = true
documentation.workspace = true

[dependencies]
# Workspace dependencies
Expand Down
31 changes: 31 additions & 0 deletions crates/aof-triggers/src/handler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ pub struct TriggerHandlerConfig {
/// Command bindings (slash command name -> binding)
/// Maps commands like "/diagnose" to specific agents or fleets
pub command_bindings: HashMap<String, CommandBinding>,

/// Maximum age of messages to process (in seconds)
/// Messages older than this are silently dropped to handle queued messages
/// from platforms like Telegram when the daemon was down.
/// Default: 60 seconds. Set to 0 to disable.
pub max_message_age_secs: u64,
}

impl Default for TriggerHandlerConfig {
Expand All @@ -120,6 +126,7 @@ impl Default for TriggerHandlerConfig {
command_timeout_secs: 300, // 5 minutes
default_agent: None,
command_bindings: HashMap::new(),
max_message_age_secs: 60, // Drop messages older than 1 minute
}
}
}
Expand Down Expand Up @@ -836,6 +843,24 @@ impl TriggerHandler {
platform, message.id, message.user.id
);

// Check if message is too old (stale/queued messages from when daemon was down)
if self.config.max_message_age_secs > 0 {
let message_age = chrono::Utc::now()
.signed_duration_since(message.timestamp)
.num_seconds();

if message_age > self.config.max_message_age_secs as i64 {
info!(
"Dropping stale message from {}: {} seconds old (max: {}s) - text: '{}'",
platform,
message_age,
self.config.max_message_age_secs,
message.text.chars().take(50).collect::<String>()
);
return Ok(());
}
}

// Get platform for response
let platform_impl = self
.platforms
Expand Down Expand Up @@ -881,6 +906,11 @@ impl TriggerHandler {
if let Some(cmd_name) = command_name {
// Check if we have a binding for this command
if let Some(binding) = self.config.command_bindings.get(&cmd_name) {
// Check for builtin handler - skip binding and use built-in command handler
if binding.agent.as_deref() == Some("builtin") {
info!("Command '{}' uses builtin handler, falling through to built-in command parser", cmd_name);
// Fall through to TriggerCommand::parse below which handles built-ins
} else {
info!("Command '{}' matched binding: {:?}", cmd_name, binding);

// Create modified message with context from metadata if command text is empty
Expand Down Expand Up @@ -937,6 +967,7 @@ impl TriggerHandler {
info!("Routing command '{}' to agent '{}'", cmd_name, agent_name);
return self.handle_natural_language(&routed_message, platform_impl, agent_name).await;
}
} // end else (non-builtin handler)
}

// Check for default binding (for any unbound slash command)
Expand Down
5 changes: 5 additions & 0 deletions crates/aofctl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ license.workspace = true
repository.workspace = true
authors.workspace = true
description = "CLI for AOF framework - kubectl-style agent orchestration"
keywords.workspace = true
categories.workspace = true
homepage.workspace = true
documentation.workspace = true
readme = "../../README.md"

[[bin]]
name = "aofctl"
Expand Down
1 change: 1 addition & 0 deletions crates/aofctl/src/commands/serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ pub async fn execute(
command_timeout_secs: config.spec.runtime.task_timeout_secs,
default_agent: config.spec.runtime.default_agent.clone(),
command_bindings: std::collections::HashMap::new(), // Loaded from Trigger CRDs
max_message_age_secs: 60, // Drop messages older than 1 minute (handles queued messages)
};

if let Some(ref agent) = config.spec.runtime.default_agent {
Expand Down
Loading