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
57 changes: 48 additions & 9 deletions .github/workflows/ci-firecracker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,30 @@ jobs:
- name: Create VM
id: vm
run: |
RESPONSE=$(curl -sf -X POST $FCCTL_URL/api/vms \
RESPONSE_FILE=$(mktemp)
trap 'rm -f "${RESPONSE_FILE}"' EXIT
if HTTP_STATUS=$(curl -sS -o "$RESPONSE_FILE" -w "%{http_code}" -X POST "$FCCTL_URL/api/vms" \
-H 'Content-Type: application/json' \
-d "{\"vm_type\": \"$VM_TYPE\"}")
-d "{\"vm_type\": \"$VM_TYPE\"}"); then
:
else
CURL_EXIT=$?
RESPONSE=$(cat "$RESPONSE_FILE")
echo "fcctl create transport failed: curl exit $CURL_EXIT"
if [ -n "$RESPONSE" ]; then
echo "$RESPONSE"
fi
exit 1
fi
RESPONSE=$(cat "$RESPONSE_FILE")
rm -f "$RESPONSE_FILE"
trap - EXIT
echo "fcctl create status: $HTTP_STATUS"
echo "$RESPONSE"
if [ "$HTTP_STATUS" -lt 200 ] || [ "$HTTP_STATUS" -ge 300 ]; then
echo "ERROR: fcctl-web VM create failed with HTTP $HTTP_STATUS"
exit 1
fi
VM_ID=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
if [ -z "$VM_ID" ] || [ "$VM_ID" = "null" ]; then
echo "ERROR: failed to parse vm id from response"
Expand Down Expand Up @@ -144,8 +164,8 @@ jobs:
# bigbox; see .cargo/config.toml for the Darwin `dynamic_lookup`
# linker workaround on local Mac dev.
#
# All cargo invocations are dispatched via `rch exec --` so they
# share rchd's queue + slot accounting with ADF agents (see
# Build-oriented cargo invocations are dispatched via `rch exec --` so
# they share rchd's queue + slot accounting with ADF agents (see
# .docs/adr-rch-build-queue-not-firecracker-ci.md). Fail-open: if
# rchd is down or no slot is available, rch falls through to local
# cargo with no behaviour change.
Expand All @@ -161,15 +181,34 @@ jobs:
- name: Install cargo-nextest
run: |
if ! command -v cargo-nextest >/dev/null 2>&1; then
cargo install cargo-nextest --locked
curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C "${CARGO_HOME:-$HOME/.cargo}/bin"
fi
cargo nextest --version

- name: cargo nextest run --workspace
# Only test_chat_command is skipped: it requires LLM API credentials
# not present in CI. All other failures must be fixed at the source,
# not skipped. nextest uses filter expressions instead of --skip.
run: /home/alex/.local/bin/rch exec -- cargo nextest run --workspace --profile ci -E 'not test(test_chat_command)'
# test_chat_command is skipped: it requires LLM API credentials not
# present in CI. The MCP autocomplete E2E binary is excluded here only
# because it spawns nested `cargo run`; the following step covers it
# serially to avoid build-dir lock contention. All other failures must
# be fixed at the source, not skipped. nextest uses filter expressions
# instead of --skip.
# Do not wrap these invocations in `rch exec`: rch's non-compilation
# command path loses shell quoting around filter expressions and makes
# `/bin/sh` parse parentheses in predicates like `test(...)`.
# `RUSTC_WRAPPER=sccache` above still applies to rustc invocations.
run: |
cargo nextest run --workspace --profile ci \
-E 'not test(test_chat_command) - binary_id(terraphim_mcp_server::mcp_autocomplete_e2e_test)'

- name: cargo nextest run mcp_autocomplete_e2e serially
# These tests spawn `cargo run --bin terraphim_mcp_server`. Under
# nextest each test is its own process, so `#[serial]` cannot prevent
# concurrent nested Cargo invocations. Run this binary separately with
# one test thread to avoid build-dir lock contention and CI timeouts.
run: |
cargo nextest run -p terraphim_mcp_server --test mcp_autocomplete_e2e_test \
--profile ci --test-threads 1 --no-capture \
-E 'not test(test_chat_command)'

- name: sccache stats
if: always()
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/performance-benchmarking.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
performance-benchmarks:
name: Performance Benchmarks
runs-on: ubuntu-latest
timeout-minutes: 30
timeout-minutes: 45

steps:
- name: Checkout code
Expand Down
5 changes: 4 additions & 1 deletion crates/terraphim_orchestrator/src/lib_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,11 +193,14 @@ async fn test_direct_dispatch_config_starts_socket_listener() {

#[tokio::test]
async fn test_handle_direct_dispatch_spawns_agent_without_mentions() {
let temp = TempDir::new().unwrap();
let mut config = test_config();
config.working_dir = temp.path().to_path_buf();
config.compound_review.worktree_root = temp.path().join(".worktrees");
config.agents = vec![AgentDefinition {
name: "echo-agent".to_string(),
layer: AgentLayer::Core,
cli_tool: "echo".to_string(),
cli_tool: "/bin/echo".to_string(),
task: "echo hello".to_string(),
schedule: None,
model: None,
Expand Down
5 changes: 4 additions & 1 deletion crates/terraphim_rlm/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ async fn run(cli: Cli) -> Result<CliResponse, Box<dyn std::error::Error>> {
#[cfg(feature = "llm")]
{
if let Err(e) = rlm.auto_configure_llm().await {
log::warn!("LLM auto-configuration failed: {}. rlm_query will be unavailable.", e);
log::warn!(
"LLM auto-configuration failed: {}. rlm_query will be unavailable.",
e
);
}
}

Expand Down
7 changes: 6 additions & 1 deletion crates/terraphim_rlm/src/query_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,12 @@ fn truncate(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
s.to_string()
} else {
let boundary = s.floor_char_boundary(max_len);
let boundary = s
.char_indices()
.map(|(idx, _)| idx)
.take_while(|idx| *idx <= max_len)
.last()
.unwrap_or(0);
format!("{}...", &s[..boundary])
}
}
Expand Down
20 changes: 11 additions & 9 deletions crates/terraphim_rlm/src/rlm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -910,8 +910,7 @@ impl TerraphimRlm {
"llm_provider".to_string(),
serde_json::Value::String("ollama".to_string()),
);
let ollama_model = std::env::var("RLM_MODEL")
.unwrap_or_else(|_| "gemma3:270m".to_string());
let ollama_model = std::env::var("RLM_MODEL").unwrap_or_else(|_| "gemma3:270m".to_string());
role.extra.insert(
"llm_model".to_string(),
serde_json::Value::String(ollama_model.clone()),
Expand All @@ -929,7 +928,9 @@ impl TerraphimRlm {
])
.output()
.ok()?;
String::from_utf8(output.stdout).ok().map(|s| s.trim().to_string())
String::from_utf8(output.stdout)
.ok()
.map(|s| s.trim().to_string())
});

if let Some(ref key) = or_api_key {
Expand All @@ -938,7 +939,9 @@ impl TerraphimRlm {
role.llm_api_key = Some(key.clone());
role.llm_model = Some(or_model.clone());
// Cache for child processes and build_llm_from_role
unsafe { std::env::set_var("OPENROUTER_API_KEY", key); }
unsafe {
std::env::set_var("OPENROUTER_API_KEY", key);
}
log::info!("RLM auto-configure: openrouter model={}", or_model);
}

Expand All @@ -958,11 +961,10 @@ impl TerraphimRlm {
..Default::default()
});

let client = terraphim_service::llm::build_llm_from_role(&role)
.ok_or_else(|| {
log::warn!("RLM auto-configure: no LLM provider available");
RlmError::LlmNotConfigured
})?;
let client = terraphim_service::llm::build_llm_from_role(&role).ok_or_else(|| {
log::warn!("RLM auto-configure: no LLM provider available");
RlmError::LlmNotConfigured
})?;

log::info!(
"RLM LLM bridge configured: providers={} strategy={:?}",
Expand Down
46 changes: 37 additions & 9 deletions crates/terraphim_rlm/tests/backend_demo.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
//! Demonstration: RLM running locally (LocalExecutor) and via Docker (DockerExecutor).
//! Run: cargo test -p terraphim_rlm --test backend_demo -- --nocapture

use terraphim_rlm::config::{BackendType, RlmConfig};
use terraphim_rlm::TerraphimRlm;
use terraphim_rlm::config::{BackendType, RlmConfig};

#[tokio::test]
async fn demo_local_executor() {
Expand All @@ -19,11 +19,22 @@ async fn demo_local_executor() {

// Python
let r = rlm.execute_code(&session.id, "print(2+2)").await.unwrap();
println!(" [Python] 2+2 = {} (exit {})", r.stdout.trim(), r.exit_code);
println!(
" [Python] 2+2 = {} (exit {})",
r.stdout.trim(),
r.exit_code
);

// Bash
let r = rlm.execute_command(&session.id, "echo hello-from-local").await.unwrap();
println!(" [Bash] echo = {} (exit {})", r.stdout.trim(), r.exit_code);
let r = rlm
.execute_command(&session.id, "echo hello-from-local")
.await
.unwrap();
println!(
" [Bash] echo = {} (exit {})",
r.stdout.trim(),
r.exit_code
);

// Show backend type
let status = rlm.get_session_status(&session.id, false).await.unwrap();
Expand Down Expand Up @@ -55,11 +66,22 @@ async fn demo_docker_executor() {

// Python
let r = rlm.execute_code(&session.id, "print(2+2)").await.unwrap();
println!(" [Python] 2+2 = {} (exit {})", r.stdout.trim(), r.exit_code);
println!(
" [Python] 2+2 = {} (exit {})",
r.stdout.trim(),
r.exit_code
);

// Bash
let r = rlm.execute_command(&session.id, "echo hello-from-docker").await.unwrap();
println!(" [Bash] echo = {} (exit {})", r.stdout.trim(), r.exit_code);
let r = rlm
.execute_command(&session.id, "echo hello-from-docker")
.await
.unwrap();
println!(
" [Bash] echo = {} (exit {})",
r.stdout.trim(),
r.exit_code
);

// Show backend type
let status = rlm.get_session_status(&session.id, false).await.unwrap();
Expand All @@ -73,11 +95,17 @@ async fn demo_docker_executor() {
println!(" [Container hostname] {}", r.stdout.trim());

// Show Python version inside container
let r = rlm.execute_code(&session.id, "import sys; print(sys.version)").await.unwrap();
let r = rlm
.execute_code(&session.id, "import sys; print(sys.version)")
.await
.unwrap();
println!(" [Python version] {}", r.stdout.trim());

// Show container filesystem
let r = rlm.execute_command(&session.id, "ls / | head -5").await.unwrap();
let r = rlm
.execute_command(&session.id, "ls / | head -5")
.await
.unwrap();
println!(" [Container root]\n{}", r.stdout);

rlm.destroy_session(&session.id).await.unwrap();
Expand Down
21 changes: 7 additions & 14 deletions crates/terraphim_rlm/tests/skills_demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,17 @@ use terraphim_rlm::{RlmConfig, TerraphimRlm};
#[tokio::test]
async fn demo_all_skills() {
let config = RlmConfig::minimal();
let rlm = TerraphimRlm::with_executor(
config,
terraphim_rlm::LocalExecutor::new(),
)
.unwrap();
let rlm = TerraphimRlm::with_executor(config, terraphim_rlm::LocalExecutor::new()).unwrap();

// 1. Session create
let session = rlm.create_session().await.unwrap();
println!("[session create] id={} state={:?}", session.id, session.state);
println!(
"[session create] id={} state={:?}",
session.id, session.state
);

// 2. Code execution
let result = rlm
.execute_code(&session.id, "print(2+2)")
.await
.unwrap();
let result = rlm.execute_code(&session.id, "print(2+2)").await.unwrap();
println!(
"[code: 2+2] exit={} stdout={:?}",
result.exit_code, result.stdout
Expand Down Expand Up @@ -64,10 +60,7 @@ async fn demo_all_skills() {
assert_eq!(val, None);

// 8. Status
let status = rlm
.get_session_status(&session.id, false)
.await
.unwrap();
let status = rlm.get_session_status(&session.id, false).await.unwrap();
println!(
"[status] backend={:?} snapshots={}",
status.backend_type, status.snapshot_count
Expand Down
12 changes: 2 additions & 10 deletions terraphim_server/tests/api_context_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ use terraphim_server::{
GetConversationResponse, ListConversationsResponse, Status, axum_server,
};
use terraphim_service::http_client;
use terraphim_settings::DeviceSettings;
use terraphim_types::{ContextType, Document, DocumentType, RelevanceFunction};

/// Sample configuration for testing context management
Expand Down Expand Up @@ -50,15 +49,8 @@ fn create_test_config() -> Config {

/// Start a test server with context management API
async fn start_test_server() -> SocketAddr {
let server_settings =
DeviceSettings::load_from_env_and_file(None).expect("Failed to load settings");
let server_hostname = server_settings
.server_hostname
.parse::<SocketAddr>()
.unwrap_or_else(|_| {
let port = portpicker::pick_unused_port().expect("Failed to find unused port");
SocketAddr::from(([127, 0, 0, 1], port))
});
let port = portpicker::pick_unused_port().expect("Failed to find unused port");
let server_hostname = SocketAddr::from(([127, 0, 0, 1], port));

let mut config = create_test_config();
let config_state = terraphim_config::ConfigState::new(&mut config)
Expand Down
Loading