feat(rvagent-learning): add Cloud Run deployment and CLI binary

- Add daily_cycle binary for running learning cycles
  - Supports --once, --status, --dry-run, --scan-dir flags
  - Shows GOAP-based discovery with Gemini 2.5 Flash
  - Submits discoveries to π.ruv.io cloud brain
- Add Dockerfile for Cloud Run deployment
  - Multi-stage Rust build with gcloud CLI
  - Runs as non-root user
- Add cloudbuild.yaml for automated deployment
  - Deploys to us-central1 on ruv-dev project
  - Injects Gemini API key from Secret Manager
- Update Cargo.toml with binary definition and tracing-subscriber

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
Reuven 2026-03-16 20:33:13 -04:00
parent 0af0ffe8ca
commit 5404a04aba
4 changed files with 342 additions and 0 deletions

View file

@ -45,6 +45,10 @@ notify = { version = "6.1", optional = true }
# rvagent-core = { path = "../rvagent-core", optional = true }
# ruvector-sona = { path = "../../sona", optional = true }
[dependencies.tracing-subscriber]
version = "0.3"
features = ["env-filter"]
[dev-dependencies]
tokio-test = "0.4"
tempfile = "3.13"
@ -58,5 +62,9 @@ full = ["scheduler"]
[lib]
crate-type = ["rlib"]
[[bin]]
name = "daily_cycle"
path = "src/bin/daily_cycle.rs"
[lints.clippy]
manual_range_contains = "allow"

View file

@ -0,0 +1,59 @@
# rvagent-learning Cloud Run Dockerfile
# Daily Learning Loop with GOAP reasoning
FROM rust:1.77-bookworm AS builder
WORKDIR /app
# Install build dependencies
RUN apt-get update && apt-get install -y \
pkg-config \
libssl-dev \
&& rm -rf /var/lib/apt/lists/*
# Copy workspace files
COPY Cargo.toml Cargo.lock ./
COPY crates/rvAgent/rvagent-learning ./crates/rvAgent/rvagent-learning
# Create dummy workspace members to satisfy Cargo
RUN mkdir -p crates/ruvector-core/src && echo "pub fn dummy() {}" > crates/ruvector-core/src/lib.rs
RUN echo '[package]\nname = "ruvector-core"\nversion = "0.1.0"\nedition = "2021"' > crates/ruvector-core/Cargo.toml
# Build release binary
RUN cargo build --release -p rvagent-learning --bin daily_cycle
# Runtime image
FROM debian:bookworm-slim
WORKDIR /app
# Install runtime dependencies
RUN apt-get update && apt-get install -y \
ca-certificates \
libssl3 \
&& rm -rf /var/lib/apt/lists/*
# Install gcloud CLI for secret manager access
RUN apt-get update && apt-get install -y curl gnupg \
&& echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] http://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list \
&& curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key --keyring /usr/share/keyrings/cloud.google.gpg add - \
&& apt-get update && apt-get install -y google-cloud-cli \
&& rm -rf /var/lib/apt/lists/*
# Copy binary
COPY --from=builder /app/target/release/daily_cycle /app/daily_cycle
# Create non-root user
RUN useradd -r -s /bin/false appuser
USER appuser
# Set environment
ENV RUST_LOG=info
ENV PORT=8080
# Health check endpoint would be on the scheduler
EXPOSE 8080
# Run the learning cycle
ENTRYPOINT ["/app/daily_cycle"]
CMD ["--once"]

View file

@ -0,0 +1,60 @@
# Cloud Build configuration for rvagent-learning
# Deploys to Cloud Run in ruv-dev project
steps:
# Build the Docker image
- name: 'gcr.io/cloud-builders/docker'
args:
- 'build'
- '-t'
- 'gcr.io/$PROJECT_ID/rvagent-learning:$COMMIT_SHA'
- '-t'
- 'gcr.io/$PROJECT_ID/rvagent-learning:latest'
- '-f'
- 'crates/rvAgent/rvagent-learning/Dockerfile'
- '.'
# Push to Container Registry
- name: 'gcr.io/cloud-builders/docker'
args:
- 'push'
- 'gcr.io/$PROJECT_ID/rvagent-learning:$COMMIT_SHA'
- name: 'gcr.io/cloud-builders/docker'
args:
- 'push'
- 'gcr.io/$PROJECT_ID/rvagent-learning:latest'
# Deploy to Cloud Run
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
entrypoint: gcloud
args:
- 'run'
- 'deploy'
- 'rvagent-learning'
- '--image'
- 'gcr.io/$PROJECT_ID/rvagent-learning:$COMMIT_SHA'
- '--region'
- 'us-central1'
- '--platform'
- 'managed'
- '--allow-unauthenticated'
- '--memory'
- '512Mi'
- '--cpu'
- '1'
- '--timeout'
- '300'
- '--set-env-vars'
- 'RUST_LOG=info,PI_RUVIO_URL=https://pi.ruv.io'
- '--set-secrets'
- 'GOOGLE_API_KEY=gemini-api-key:latest'
images:
- 'gcr.io/$PROJECT_ID/rvagent-learning:$COMMIT_SHA'
- 'gcr.io/$PROJECT_ID/rvagent-learning:latest'
options:
logging: CLOUD_LOGGING_ONLY
timeout: '1200s'

View file

@ -0,0 +1,215 @@
//! Daily Learning Cycle CLI
//!
//! Run a single learning cycle or start the scheduler.
//!
//! Usage:
//! cargo run -p rvagent-learning --bin daily_cycle -- [OPTIONS]
//!
//! Options:
//! --once Run a single cycle and exit
//! --status Show current state and exit
//! --scan-dir Directory to scan (default: current directory)
//! --dry-run Don't submit to π.ruv.io
use rvagent_learning::{
DailyLearningLoop, SchedulerConfig,
discovery::{CodebaseScanner, PatternAnalyzer, DiscoveryLog},
goap::{GoapPlanner, LearningGoal, LearningWorldState},
integration::PiRuvIoClient,
};
use std::env;
use std::time::Instant;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Initialize tracing
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive("rvagent_learning=info".parse()?)
)
.init();
let args: Vec<String> = env::args().collect();
let once = args.iter().any(|a| a == "--once");
let status_only = args.iter().any(|a| a == "--status");
let dry_run = args.iter().any(|a| a == "--dry-run");
let scan_dir = args.iter()
.position(|a| a == "--scan-dir")
.and_then(|i| args.get(i + 1))
.map(|s| s.as_str())
.unwrap_or(".");
println!("╔══════════════════════════════════════════════════════════════╗");
println!("║ RuVector Daily Learning Loop (ADR-115) ║");
println!("╠══════════════════════════════════════════════════════════════╣");
println!("║ GOAP-based discovery with Gemini 2.5 Flash reasoning ║");
println!("║ Submits discoveries to π.ruv.io cloud brain ║");
println!("╚══════════════════════════════════════════════════════════════╝");
println!();
if status_only {
show_status().await?;
return Ok(());
}
if once {
run_single_cycle(scan_dir, dry_run).await?;
} else {
run_scheduler(scan_dir).await?;
}
Ok(())
}
async fn show_status() -> anyhow::Result<()> {
println!("📊 System Status");
println!("────────────────────────────────────────");
// Check π.ruv.io connection
let pi_client = PiRuvIoClient::default_client();
let connected = pi_client.check_connection().await;
println!("π.ruv.io connection: {}", if connected { "✅ Connected" } else { "❌ Disconnected" });
// Check Gemini API key
let gemini_available = env::var("GOOGLE_API_KEY").is_ok() || env::var("GEMINI_API_KEY").is_ok();
println!("Gemini API key: {}", if gemini_available { "✅ Available" } else { "⚠️ Not set (set GOOGLE_API_KEY)" });
// Show current state
let state = LearningWorldState::default();
println!();
println!("📈 Default State");
println!("────────────────────────────────────────");
println!("Patterns discovered: {}", state.patterns_discovered);
println!("Pending submission: {}", state.patterns_pending_submission);
println!("Memory utilization: {:.1}%", state.memory_utilization * 100.0);
println!("Consolidation due: {}", state.consolidation_due);
Ok(())
}
async fn run_single_cycle(scan_dir: &str, dry_run: bool) -> anyhow::Result<()> {
println!("🔄 Running single learning cycle...");
println!(" Scan directory: {}", scan_dir);
println!(" Dry run: {}", dry_run);
println!();
let start = Instant::now();
// Phase 1: Scan codebase
println!("📂 Phase 1: Scanning codebase...");
let scanner = CodebaseScanner::new(scan_dir);
let files = scanner.scan().await?;
println!(" Found {} files to analyze", files.len());
// Phase 2: Analyze patterns
println!("🔍 Phase 2: Analyzing patterns...");
let analyzer = PatternAnalyzer::new();
let file_contents: Vec<(String, String)> = files
.into_iter()
.map(|f| (f.path.to_string_lossy().to_string(), f.content))
.collect();
let discoveries = analyzer.analyze_files(&file_contents);
println!(" Discovered {} patterns", discoveries.len());
// Show discoveries
if !discoveries.is_empty() {
println!();
println!("📋 Discoveries");
println!("────────────────────────────────────────");
for (i, d) in discoveries.iter().take(10).enumerate() {
println!("{}. [{}] {}", i + 1, format!("{:?}", d.category), d.title);
println!(" Quality: {:.2} | Files: {:?}", d.quality.composite, d.source_files);
println!(" Method: {}", d.method_attribution());
}
if discoveries.len() > 10 {
println!(" ... and {} more", discoveries.len() - 10);
}
}
// Phase 3: GOAP Planning
println!();
println!("🧠 Phase 3: GOAP Planning...");
let planner = GoapPlanner::new();
let mut state = LearningWorldState::default();
state.patterns_discovered = discoveries.len();
let goal = LearningGoal::SubmitToCloudBrain { min_quality: 0.7 };
let plan = planner.plan(&state, &goal)?;
println!(" Plan: {} actions, cost: {:.1}", plan.actions.len(), plan.estimated_cost);
for action in &plan.actions {
println!(" - {} (cost: {:.1})", action.action, action.cost);
}
// Phase 4: Submit to π.ruv.io (if not dry run)
if !dry_run && !discoveries.is_empty() {
println!();
println!("☁️ Phase 4: Submitting to π.ruv.io...");
let pi_client = PiRuvIoClient::default_client();
if pi_client.check_connection().await {
let high_quality: Vec<&DiscoveryLog> = discoveries
.iter()
.filter(|d| d.quality.composite >= 0.5)
.collect();
println!(" {} high-quality discoveries to submit", high_quality.len());
for discovery in high_quality.iter().take(3) {
match pi_client.submit(discovery).await {
Ok(response) => {
if response.success {
println!(" ✅ Submitted: {} -> {}",
discovery.title,
response.memory_id.unwrap_or_default());
} else {
println!(" ❌ Rejected: {}", response.error.unwrap_or_default());
}
}
Err(e) => {
println!(" ⚠️ Failed: {}", e);
}
}
}
} else {
println!(" ⚠️ π.ruv.io not connected, skipping submission");
}
} else if dry_run {
println!();
println!("☁️ Phase 4: Skipped (dry run mode)");
}
let duration = start.elapsed();
println!();
println!("════════════════════════════════════════");
println!("✅ Cycle complete in {:.2}s", duration.as_secs_f64());
println!(" Patterns found: {}", discoveries.len());
println!("════════════════════════════════════════");
Ok(())
}
async fn run_scheduler(scan_dir: &str) -> anyhow::Result<()> {
println!("🕐 Starting scheduled learning loop...");
println!(" Press Ctrl+C to stop");
println!();
let mut config = SchedulerConfig::default();
config.scan.root_directory = scan_dir.to_string();
let mut learning_loop = DailyLearningLoop::new(config).await?;
// Run first cycle immediately
println!("Running initial cycle...");
let result = learning_loop.run_cycle().await?;
println!("Initial cycle: {} discoveries, {} submitted",
result.discoveries_found, result.discoveries_submitted);
// Start scheduled loop
learning_loop.start().await?;
Ok(())
}